[ARM Linux 驱动开发] 借助 pinctrl 和 gpio 子系统编写 LED 驱动

这章介绍如何利用 pinctrl 和 gpio 子系统进一步简化 LED 驱动的开发。

实验自己已经做过一遍,感受下来是框架设计更抽象独立了。剩下的我们负责的任务就是“填鸭式”地在设备树中指定相应内容。下面就让我们来梳理一下。

1. pinctrl 子系统

回顾我们之前所有关于 LED 驱动实验的文章,关于管脚的设置内容很比较容易抽象的:

1. 设置管脚的复用功能,比如复用为 GPIO。

2. 设置管脚的电器属性,比如上/下拉和速度等等。

因为有非常多的管脚都需要以上相同的操作,所以 pinctrl 子系统就是为此针对管脚的设置而设计的。

我们查看 arch/arm/boot/dts/imx6ull-14x14-evk.dts 设备树文件中的 iomuxc 节点。再关注其下的 imx6ul-evk 节点下的 hoggrp-1 节点,以此学习需要指定什么参数:

片段1 arch/arm/boot/dts/imx6ull-14x14-evk.dts
  • &iomuxc {
  •     pinctrl-names = "default";
  •     pinctrl-0 = <&pinctrl_hog_1>;
  •     imx6ul-evk {
  •         pinctrl_hog_1: hoggrp-1 {
  •             fsl,pins = <
  •                 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19   0x17059
  •                 MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT    0x17059
  •                 MX6UL_PAD_GPIO1_IO09__GPIO1_IO09        0x17059
  •             >;
  •         };
  •         /* …… */
  •     };
  •     /* …… */
  • };

可以看到这边主要就是 fsl,pins 这个属性。文档中对这个属性的描述为:每个项由 6 个整数组成,代表一个管脚的复用和配置设置。前 5 个整数 <mux_reg conf_reg input_reg mux_val input_val> 使用 PIN_FUNC_ID 宏指定,可以在设备树源文件夹下的 imx*-pinfunc.h 文件中找到。最后一个整数 CONFIG 是管脚的配置值,如该管脚的上拉。

见 Documentation/devicetree/bindings/pinctrl/fsl,imx-pinctrl.txt 文件。

我们再结合具体例子来了解和验证一下上述关于 fsl,pins 属性的介绍,以 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 举例。在 arch/arm/boot/dts/imx6ul-pinfunc.h 头文件中可以找到 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 的定义:

  • #define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 mux_reg : 0x0090

iomuxc 节点最外层的定义在 imx6ull.dtsi 文件中:

  1. iomuxc: iomuxc@020e0000 {
  2.     compatible = "fsl,imx6ul-iomuxc";
  3.     reg = <0x020e0000 0x4000>;
  4. };

从节点名称和 reg 属性中可以看出复用这组寄存器组的基地址为 0x020e0000。图 1 是 MX6UL_PAD_UART1_RTS_B 寄存器的信息,可以看到它的地址为“20E_0000h base + 90h offset = 20E_0090h”,即这边的 0x0090(mux_reg) 对应的就是寄存器的偏移地址。

图1 复用寄存器信息

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 conf_reg : 0x031C

和复用寄存器类似,在图 2 中可以看到 0x031C 就是相应配置寄存器的偏移地址(“20E_0000h base + 318h offset = 20E_0318h”)。

图2 配置寄存器信息

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 input_reg : 0x0000

同样 input_reg 表示输入寄存器的偏移地址,没有的话就设置为 0。像 MX6UL_PAD_UART1_RTS_B 就没有输入寄存器,因此设置为 0x0000。

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 mux_val : 0x5

mux_val 指定复用寄存器的值,0x5(101b) 对应图 1 中的内容可知,这里将 MX6UL_PAD_UART1_RTS_B 设置为 GPIO1_IO19。

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 input_val : 0x0

input_val 指定输入寄存器的值。MX6UL_PAD_UART1_RTS_B 因为没有输入寄存器,所以此值无效。

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 CONFIG : 0x17059

最后一个整数是配置寄存器的值,即配置的电气属性。

在了解了 fsl,pins 属性的含义之后,我们大致猜测出驱动程序可能就是利用属性中指定的值,依次来初始化各个寄存器的。

2. gpio 子系统

如果 pinctrl 子系统中将一个管脚复用为 GPIO 的话,那么 gpio 子系统就能极大方便驱动开发者使用 GPIO。我们首先从定义的操作接口来了解它:

  • // 申请一个 GPIO 管脚
  • int gpio_request(unsigned gpio, const char *label);
  • // 释放申请的 GPIO 管脚
  • void gpio_free(unsigned gpio);
  • // 设置 GPIO 为输入
  • int gpio_direction_input(int gpio);
  • // 设置 GPIO 为输出
  • int gpio_direction_output(int gpio, int v);
  • // 获取 GPIO 
  • int gpio_get_value(int gpio);
  • // 设置 GPIO 
  • void gpio_set_value(int gpio, int v);

这些接口从功能上看 gpio_requestgpio_free 函数是一对,用于 GPIO 管脚的申请和释放。这应该是在驱动层面的概念(因为裸机上都是直接拿来用的资源),可用于检查 GPIO 资源的“互斥”。

剩下的接口从名字上很好理解,相当于 GPIO 功能的封装:gpio_direction_input 函数设置 GPIO 管脚为输入;gpio_direction_output 函数设置 GPIO 管脚为输出;gpio_get_value 函数用于获取 GPIO 管脚上的值;gpio_set_value 函数用于设置 GPIO 管脚的值。

这边还需要重点关注的是这些接口的第一个参数,它是一个整型,代表一个实际 GPIO 管脚。即 GPIO 管脚映射成了一个整数,这就是抽象和便于管理的地方。GPIO 整型标号可以通过 of_get_named_gpio 函数获得。

我们结合设备树文件里的具体例子来看一看。还是在代码片段 1 中,有管脚复用为 GPIO1_IO19:

  • MX6UL_PAD_UART1_RTS_B__GPIO1_IO19   0x17059

使用到 GPIO1_IO19 的地方如下:

  • &usdhc1 {
  •     pinctrl-names = "default""state_100mhz""state_200mhz";
  •     pinctrl-0 = <&pinctrl_usdhc1>;
  •     pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
  •     pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
  •     cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
  •     keep-power-in-suspend;
  •     enable-sdio-wakeup;
  •     vmmc-supply = <&reg_sd1_vmmc>;
  •     status = "okay";
  • };

我们关注到 cd-gpios 属性,它指示 GPIO 控制器信息,由三个值组成。第一个值是具体 GPIO 组节点的 phandle;第二个值是 GPIO 组内的管脚号;第三个值是标志号,可指定低电平有效或高电平有效。之前说的 of_get_named_gpio 函数的参数就是这类属性。

我们再看一下 gpio1 这个节点的定义:

arch/arm/boot/dts/imx6ull.dtsi
  • gpio1: gpio@0209c000 {
  •     compatible = "fsl,imx6ul-gpio""fsl,imx35-gpio";
  •     reg = <0x0209c000 0x4000>;
  •     interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
  •              <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
  •     gpio-controller;
  •     #gpio-cells = <2>;
  •     interrupt-controller;
  •     #interrupt-cells = <2>;
  • };

reg 属性的首地址和图 3 参考手册中指定的 GPIO1 组首地址是一致的。

图3 GPIO1 相关寄存器地址映射

3. 实验

本节我们使用 pinctrl 和 gpio 子系统来重写我们之前的 LED 驱动。

添加 pinctrl 子系统

首先我们打开之前移植的 imx6ull-my-emmc.dts 文件,定位到 /iomuxc/imx6ul-evk 节点。我们在其最后添加上自己定义的信息,将 LED 管脚复用为 GPIO:

  • &iomuxc {
  •     pinctrl-names = "default";
  •     pinctrl-0 = <&pinctrl_hog_1>;
  •     imx6ul-evk {
  •         // ……
  •         pinctrl_myled: myledgrp {
  •             fsl,pins = <
  •                 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0
  •             >;
  •         };
  •     };
  • };

添加 LED 设备节点

接着我们在根节点下添加自己定义的 LED 节点,重点注意 led-gpio 属性的定义:

  • hanhanled {
  •     #address-cells = <1>;
  •     #size-cells = <1>;
  •     compatible = "hanhan-led";
  •     pinctrl-names = "default";
  •     pinctrl-0 = <&pinctrl_myled>;
  •     led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
  •     status = "okay";
  • };

在定义了以上内容之后,一定要检查一下管脚定义信息是否原先已经定义:

1. 检查 pinctrl 设置。

2. 检查 gpio 设置。

我们可以搜索 "IO1_IO03" 关键字来检查 IO1_IO03 是否被复用为其他功能,比如两处复用的功能不一样,具体顺序逻辑我们也不清楚,势必有影响 LED 驱动正常工作的可能性。所以这里,除了我们自定义的节点,其他使用到 GPIO1_IO03 的地方都给删掉。

同样可能使用到 GPIO1_IO3 的地方也要删除,否则 gpio_request() 会因占用而申请失败。我们可以搜索 "gpio1 3" 来找到使用到 GPIO1_IO3 的地方。

务必检查设置可能冲突的地方。

设备树内容定义好后,就可以开始写代码了:

  1. #include <linux/module.h>
  2. #include <linux/cdev.h>
  3. #include <linux/device.h>
  4. #include <linux/fs.h>
  5. #include <asm/io.h>
  6. #include <asm/uaccess.h>
  7. #include <linux/of.h>
  8. #include <linux/slab.h>
  9. #include <linux/of_address.h>
  10. #include <linux/of_gpio.h>
  11.  
  12. #define LED_CNT 1
  13. #define LED_NAME "gpioled"
  14.  
  15. struct LedDev
  16. {
  17.     /* 设备号相关 */
  18.     dev_t devId;
  19.     int major;
  20.     int minor;
  21.     /* cdev */
  22.     struct cdev cdev;
  23.     /* mdev */
  24.     struct class* class;
  25.     struct device* device;
  26.     /* GPIO 编号 */
  27.     int gpioLed;
  28. };
  29.  
  30. struct LedDev ledDev;
  31.  
  32. static int led_open(struct inode* inode, struct file* filp)
  33. {
  34.     printk("led_open\n");
  35.     return 0;
  36. }
  37.  
  38. static int led_release(struct inode* inode, struct file* filp)
  39. {
  40.     printk("led_release\n");
  41.     return 0;
  42. }
  43.  
  44. static ssize_t led_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
  45. {
  46.     int res;
  47.     char status;
  48.  
  49.     res = copy_from_user(&status, buf, sizeof(status));
  50.     if (res < 0)
  51.     {
  52.         printk("copy_from_user error\n");
  53.         return res;
  54.     }
  55.  
  56.     gpio_set_value(ledDev.gpioLed, status != 1);
  57.     return 0;
  58. }
  59.  
  60. const struct file_operations led_fops =
  61. {
  62.     .owner = THIS_MODULE,
  63.     .open = led_open,
  64.     .release = led_release,
  65.     .write = led_write,
  66. };
  67.  
  68. static int led_init(void)
  69. {
  70.     int res = 0;
  71.  
  72.     struct device_node* nd = of_find_node_by_path("/hanhanled");
  73.     if (nd == NULL)
  74.     {
  75.         printk("of_find_node_by_path failed\n");
  76.         return -EINVAL;
  77.     }
  78.  
  79.     ledDev.gpioLed = of_get_named_gpio(nd, "led-gpio"0);
  80.     if (ledDev.gpioLed < 0)
  81.     {
  82.         printk("of_get_named_gpio failed\n");
  83.         return -EINVAL;      
  84.     }
  85.     printk("led gpio number = %d\n", ledDev.gpioLed);
  86.  
  87.     res = gpio_request(ledDev.gpioLed, "gpio-led");
  88.     if (res != 0)
  89.     {
  90.         printk("gpio_request failed\n");
  91.         return -EINVAL;          
  92.     }
  93.  
  94.     res = gpio_direction_output(ledDev.gpioLed, 1);
  95.     if (res < 0)
  96.     {
  97.         printk("gpio_direction_output failed\n");
  98.         goto fail_label;
  99.     }
  100.  
  101.     /////////////////////////////////////////////////////////////
  102.     /* 注册设备号 */
  103.     if (ledDev.major)
  104.     {
  105.         ledDev.devId = MKDEV(ledDev.major, ledDev.minor);
  106.         register_chrdev_region(ledDev.devId, LED_CNT, LED_NAME);
  107.     }
  108.     else
  109.     {
  110.         alloc_chrdev_region(&ledDev.devId, 0, LED_CNT, LED_NAME);
  111.         ledDev.major = MAJOR(ledDev.devId);
  112.         ledDev.minor = MINOR(ledDev.devId);
  113.     }
  114.     printk("major = %d, minor = %d\n", ledDev.major, ledDev.minor);
  115.  
  116.     /* 设置 cdev */
  117.     ledDev.cdev.owner = THIS_MODULE;
  118.     cdev_init(&ledDev.cdev, &led_fops);
  119.     cdev_add(&ledDev.cdev, ledDev.devId, LED_CNT);
  120.  
  121.     /* 设置 mdev */
  122.     ledDev.class = class_create(THIS_MODULE, LED_NAME);
  123.     if (IS_ERR(ledDev.class))
  124.     {
  125.         return PTR_ERR(ledDev.class);
  126.     }
  127.  
  128.     ledDev.device = device_create(ledDev.class, NULL, ledDev.devId, NULL, LED_NAME);
  129.     if (IS_ERR(ledDev.device))
  130.     {
  131.         return PTR_ERR(ledDev.device);
  132.     }
  133.  
  134.     return 0;
  135.  
  136. fail_label:
  137.     gpio_free(ledDev.gpioLed);
  138.     return res;
  139. }
  140.  
  141. static void led_exit(void)
  142. {
  143.     device_destroy(ledDev.class, ledDev.devId);
  144.     class_destroy(ledDev.class);
  145.  
  146.     cdev_del(&ledDev.cdev);
  147.     unregister_chrdev_region(ledDev.devId, LED_CNT);
  148.  
  149.     /* 释放 GPIO */
  150.     gpio_free(ledDev.gpioLed);
  151. }
  152.  
  153. module_init(led_init);
  154. module_exit(led_exit);
  155. MODULE_LICENSE("GPL");
  156. MODULE_AUTHOR("hanhan");

整体框架和之前都是一样,包括模块加载/卸载、设备号自动分配、设备创建、设备文件节点创建等等。变化的地方就是 led_init 函数的开头(第 72 到 99 行),可以看到关于寄存器信息的硬编码信息已经消失了,因为都移到设备树那边了。我们具体介绍这一部分的编写逻辑。

首先 72 到 77 行使用 of_find_node_by_path 函数定位到我们在设备树里定义的和 LED 相关的节点("/hanhanled")。

第 79 到 85 行使用 of_get_named_gpio 函数获取到 LED GPIO 所对应的标号,信息我们定义在 "led-gpio" 属性。这个标号我们需要记录下来,因为后续在此 GPIO 上的操作都需要使用这个标号。

第 87 到 92 行使用 gpio_request 函数申请 GPIO,如果申请失败(可能是因为别的驱动在占用),我们就返回错误状态。

第 94 到 99 行使用 gpio_direction_output 函数将 GPIO 设置为输出,并一开始输出高电平(LED 灯灭)。

再者 led_write 写操作函数中,我们也可以发现写 GPIO 方便了不少:第 56 行使用 gpio_set_value 输出想要的高低电平。

4. 总结

本篇文章介绍了 pinctrl 和 gpio 子系统。只是了解了大致的使用方法,关于具体的原理后续有机会再深入研究。