[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 节点,以此学习需要指定什么参数:
- &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 文件中:
- iomuxc: iomuxc@020e0000 {
- compatible = "fsl,imx6ul-iomuxc";
- reg = <0x020e0000 0x4000>;
- };
从节点名称和 reg 属性中可以看出复用这组寄存器组的基地址为 0x020e0000。图 1 是 MX6UL_PAD_UART1_RTS_B 寄存器的信息,可以看到它的地址为“20E_0000h base + 90h offset = 20E_0090h”,即这边的 0x0090(mux_reg) 对应的就是寄存器的偏移地址。

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

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_request 和 gpio_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 = <®_sd1_vmmc>;
- status = "okay";
- };
我们关注到 cd-gpios 属性,它指示 GPIO 控制器信息,由三个值组成。第一个值是具体 GPIO 组节点的 phandle;第二个值是 GPIO 组内的管脚号;第三个值是标志号,可指定低电平有效或高电平有效。之前说的 of_get_named_gpio 函数的参数就是这类属性。
我们再看一下 gpio1 这个节点的定义:
- 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. 实验
本节我们使用 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 的地方。
务必检查设置可能冲突的地方。
设备树内容定义好后,就可以开始写代码了:
- #include <linux/module.h>
- #include <linux/cdev.h>
- #include <linux/device.h>
- #include <linux/fs.h>
- #include <asm/io.h>
- #include <asm/uaccess.h>
- #include <linux/of.h>
- #include <linux/slab.h>
- #include <linux/of_address.h>
- #include <linux/of_gpio.h>
- #define LED_CNT 1
- #define LED_NAME "gpioled"
- struct LedDev
- {
- /* 设备号相关 */
- dev_t devId;
- int major;
- int minor;
- /* cdev */
- struct cdev cdev;
- /* mdev */
- struct class* class;
- struct device* device;
- /* GPIO 编号 */
- int gpioLed;
- };
- struct LedDev ledDev;
- static int led_open(struct inode* inode, struct file* filp)
- {
- printk("led_open\n");
- return 0;
- }
- static int led_release(struct inode* inode, struct file* filp)
- {
- printk("led_release\n");
- return 0;
- }
- static ssize_t led_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int res;
- char status;
- res = copy_from_user(&status, buf, sizeof(status));
- if (res < 0)
- {
- printk("copy_from_user error\n");
- return res;
- }
- gpio_set_value(ledDev.gpioLed, status != 1);
- return 0;
- }
- const struct file_operations led_fops =
- {
- .owner = THIS_MODULE,
- .open = led_open,
- .release = led_release,
- .write = led_write,
- };
- static int led_init(void)
- {
- int res = 0;
- struct device_node* nd = of_find_node_by_path("/hanhanled");
- if (nd == NULL)
- {
- printk("of_find_node_by_path failed\n");
- return -EINVAL;
- }
- ledDev.gpioLed = of_get_named_gpio(nd, "led-gpio", 0);
- if (ledDev.gpioLed < 0)
- {
- printk("of_get_named_gpio failed\n");
- return -EINVAL;
- }
- printk("led gpio number = %d\n", ledDev.gpioLed);
- res = gpio_request(ledDev.gpioLed, "gpio-led");
- if (res != 0)
- {
- printk("gpio_request failed\n");
- return -EINVAL;
- }
- res = gpio_direction_output(ledDev.gpioLed, 1);
- if (res < 0)
- {
- printk("gpio_direction_output failed\n");
- goto fail_label;
- }
- /////////////////////////////////////////////////////////////
- /* 注册设备号 */
- if (ledDev.major)
- {
- ledDev.devId = MKDEV(ledDev.major, ledDev.minor);
- register_chrdev_region(ledDev.devId, LED_CNT, LED_NAME);
- }
- else
- {
- alloc_chrdev_region(&ledDev.devId, 0, LED_CNT, LED_NAME);
- ledDev.major = MAJOR(ledDev.devId);
- ledDev.minor = MINOR(ledDev.devId);
- }
- printk("major = %d, minor = %d\n", ledDev.major, ledDev.minor);
- /* 设置 cdev */
- ledDev.cdev.owner = THIS_MODULE;
- cdev_init(&ledDev.cdev, &led_fops);
- cdev_add(&ledDev.cdev, ledDev.devId, LED_CNT);
- /* 设置 mdev */
- ledDev.class = class_create(THIS_MODULE, LED_NAME);
- if (IS_ERR(ledDev.class))
- {
- return PTR_ERR(ledDev.class);
- }
- ledDev.device = device_create(ledDev.class, NULL, ledDev.devId, NULL, LED_NAME);
- if (IS_ERR(ledDev.device))
- {
- return PTR_ERR(ledDev.device);
- }
- return 0;
- fail_label:
- gpio_free(ledDev.gpioLed);
- return res;
- }
- static void led_exit(void)
- {
- device_destroy(ledDev.class, ledDev.devId);
- class_destroy(ledDev.class);
- cdev_del(&ledDev.cdev);
- unregister_chrdev_region(ledDev.devId, LED_CNT);
- /* 释放 GPIO */
- gpio_free(ledDev.gpioLed);
- }
- module_init(led_init);
- module_exit(led_exit);
- MODULE_LICENSE("GPL");
- 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 子系统。只是了解了大致的使用方法,关于具体的原理后续有机会再深入研究。