1. GPIO 和 Pinctrl 子系统的使用
在 LED 驱动程序中,我们使用寄存器操作的方式来配置硬件,这种方式是非常低效的,对于具有大量寄存器的芯片肯定需要将寄存器操作,可以由芯片厂的BSP工程师来封装成库,然后来让驱动开发人员进行调用,这就是今天引入的 GPIO 和 Pinctrl 子系统。
1.1 Pinctrl 子系统
参考文档:内核目录\Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt
Pinctrl 子系统涉及 2 个对象:pin controller、client device。
- pin controller 提供服务,可以用它来复用引脚、配置引脚。
- client device 使用服务,声明自己要使用哪些引脚的哪些功能,怎么配置它们。
1.1.1 pin controller
pin controller 是一个软件上的概念,用来复用引脚,还可以配置引脚(比如上下拉电阻等)。
pin controller 和后面讲到的 GPIO Controller 不是一回事,GPIO Controller 只是把引脚配置为输入、输出等简单的功能。
1.1.2 client device
Pinctrl 系统的客户,就是使用引脚的设备,它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。例如:

上图中,左边是 pin controller 节点,右边是 client device 节点.
(1) pin state
比如对于一个 UART 设备,它有多个“状态”:default、sleep 等,那对应的引脚在设备处于不同状态时也需要配置为不同的模式。例如UART默认状态两个引脚需要配置为输出+输入模式,在休眠模式下节约功耗则可以配置为均输出高电平。
(2) groups 和 function
一个设备会用到一个或多个引脚,这些引脚就可以归为一组(group);
这些引脚可以复用为某个功能:function。
当然:一个设备可以用到多组引脚,比如 A1、A2 两组引脚,A1 组复用为 F1 功能,A2 组复用为 F2 功能。
(3) Generic pin multiplexing node 和 Generic pin configuration node
在上图左边的 pin controller 节点中,一些其他属性可以描述不同的引脚信息,例如,哪组 (group) 引脚配置为哪个设置功能 (setting),比如上拉、下拉等。
pin controller 节点的格式,没有统一的标准。每家芯片都不一样。 甚至上面的 group、function 关键字也不一定有,但是概念是有的。
Pinctrl 子系统举例:

1.1.3 代码中怎么引用 pinctrl
基本上驱动程序不用管,当设备切换状态时,对应的 pinctrl 就会被自动调用来切换引脚状态。
非要调用,也有函数:
1 | devm_pinctrl_get_select_default(struct device *dev); // 使用"default"状态的引脚 |
1.2 GPIO 子系统
要操作 GPIO 引脚,先把所用引脚配置为 GPIO 功能,这通过 Pinctrl 子系统来实现。
然后就可以根据设置引脚方向(输入还是输出),获取引脚电平(读),设置引脚电平(写),这通过 GPIO 子系统来实现。
1.2.1 在设备树中指定引脚
几乎所有 ARM 芯片中,GPIO 都分为几组,每组中有若干个引脚。
所以在使用 GPIO 子系统之前,就要先确定引脚的组别和组内序号
在设备树中,“GPIO组”就是一个 GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。
下图是一些芯片的 GPIO 控制器节点,它们一般都是厂家定义好,在 xxx.dtsi 文件中:

暂时只需要关心里面的这 2 个属性:
1 | gpio-controller; |
- gpio-controller 表示这个节点是一个 GPIO Controller,它下面有很多引脚。
- #gpio-cells = <2> 表示这个控制器下每一个引脚要用 2 个 32 位的数 (cell)来描述。具体用几个数以及如何表示都可以自己决定;普遍的用法是,用第 1 个 cell 来表示哪一个引脚,用第 2 个 cell 来表示有效电平:
1 | GPIO_ACTIVE_HIGH: 高电平有效 |
片厂来定义 GPIO Controller ,我们如果需要修改设备树文件,只需要引用他们规定的引脚即可:

例如上图中,在设备树文件,我们定义的设备节点中直接引用 “gpios” 或者 “xxx-gpios” 属性即可
1.2.2 在驱动代码中调用 GPIO 子系统
GPIO 子系统有两套接口:
- 基于描述符 - descriptor-based。函数都有前缀
gpiod_,它使用 gpio_desc 结构体来表示 一个引脚; - 过时的接口 - legacy。函数都有前缀
gpio_,它使用一个整数来表示一个引脚。
要操作一个引脚,首先要 get 引脚,然后设置方向,读值、写值。
(1) 常用函数
需包含头文件:
1 |
|

有前缀 devm_ 的含义是“设备资源管理”(Managed Device Resource), 这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。
比如在 Linux 开发过程中,先申请了 GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放 GPIO 资源。如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的 GPIO 资源。建议使用devm_版本的相关函数。
(2) 使用 descriptor-based 方式获得引脚
假设备在设备树中有如下节点:
1 | foo_device |
那么可以使用下面的函数获得引脚:
1 | struct gpio_desc *red, *green, *blue, *power; |
gpiod_set_value 设置的值是“逻辑值”,不一定等于物理值。之所以使用逻辑值,是为了保持功能代码不变。以LED不同的驱动电路为例,见下图:

(3) 使用老的方式获得引脚
旧的 gpio_ 函数没办法根据设备树信息获得引脚,它需要先知道引脚号。
在 GPIO 子系统中,每注册一个 GPIO Controller 时会确定它的“base number”,那么这个控制器里的第 n 号引脚的号码就是:base number + n。 但是如果硬件有变化、设备树有变化,这个 base number 并不能保证是固定的,应该查看 sysfs 来确定 base number。
先在开发板的/sys/class/gpio 目录下,找到各个 gpiochipxxx 目录:
然后进入某个 gpiochip 目录,查看文件 label 的内容
根据 label 的内容对比设备树。label 内容来自设备树,比如它的寄存器基地址。用来跟设备树(dtsi 文件) 比较,就可以知道这对应哪一个 GPIO Controller。
、从图中可以知道 GPIO Controller (gpio4) 对应的基准引脚号是 96,则对于引脚 “GPIO4_IO14”的引脚号为 96 + 14 = 110,可以按照如下操作读取引脚的值:
1
2
3
4echo 110 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio110/direction
cat /sys/class/gpio/gpio110/value
echo 110 > /sys/class/gpio/unexport按照如下操作设置引脚的值:
1
2
3
4echo 110 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio110/direction
echo 1 > /sys/class/gpio/gpio110/value
echo 110 > /sys/class/gpio/unexport
如果驱动程序已经使用了该引脚,那么将会 export 失败,会提示:write error: Device orr resource busy
2. 基于 Pinctrl 和 GPIO 子系统编写LED驱动
通过使用 Pinctrl 和 GPIO 子系统封装了硬件操作中大量的寄存器设备,使得驱动代码和芯片种类解绑,大大简化了驱动代码的编写。
2.1 基于 Pinctrl 子系统修改设备树
- 有些芯片提供了设备树生成工具,在 GUI 界面中选择引脚功能和配置信息, 就可以自动生成 Pinctrl 子结点。把它复制到你的设备树文件中,再在 client device 结点中引用就可以。
- 有些芯片只提供文档,那就去阅读文档,一般在内核源码目录 Documentation\devicetree\bindings\pinctrl 下面,保存有该厂家的文档。
- 如果连文档都没有,那只能参考内核源码中的设备树文件,在内核源码目录 arch/arm/boot/dts 目录下。
IMX6ULL提供了GUI工具,可以在GUI工具中配置引脚的复用和焊盘的相关参数:

将生成的 Pinctrl 子节点添加到 dts 文件中:
1 | &iomuxc_snvs { |
PS:我尝试将 pinctrl_krocz_led 节点直接放置在 &iomuxc_snvs 节点下,但是重启开发板之后发现网络连接不上;通过串口连上开发板之后,装载内核驱动模块时报错找不着引脚,不过感觉语法上应该没有问题。之后,将 pinctrl_krocz_led 放置在原有的 imx6ul-evk 或者像GUI生成的那样放在 imx6ull-board 下是可以的,没有问题。
在 dts 中新加入一个设备节点:
1 | krocz_led { |
通过上述代码,我们基于 Pinctrl 子系统设置了 LED 引脚,以及引脚复用功能为 GPIO,并且配置了引脚的相关属性,如上拉电阻阻值,驱动能力,引脚速率等。
基于 GPIO 子系统设置了 LED 引脚,以及该引脚逻辑电平和物理电平的关系,并可以通过 GPIO 子系统来设置该引脚的方向,读写引脚值等。
2.2 通用LED驱动代码
代码的主要部分:
- 注册 file_operations
- 内核模块的 init 和 exit
- platform_driver 的 probe 和 remove
2.2.1 编写 file_operations
1 | static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) |
基于 Pinctrl 和 GPIO 子系统将芯片寄存器操作封装,使得我们的驱动代码和芯片类型无关,从而使得仅这一个LED驱动代码就可以在任意芯片上通用。在不同的芯片上仅需要修改对应的设备树文件,不需要修改驱动代码文件。
2.2.2 内核模块注册
1 | static int __init led_init(void) |
主要是注册 platform_driver
2.2.3 设备驱动的注册
1 | static int chip_demo_gpio_probe(struct platform_device *pdev) |
主要获取 GPIO 引脚,注册 file_operations 结构体,创建设备文件等。