pinctrl 子系统
简介
Linux 驱动讲究驱动分离和分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
我们先来回顾一下 LED 的驱动流程:
①、修改设备树,添加相应的节点,节点里面的重点是设置 reg 属性,reg 属性包含了 GPIO 相关寄存器。
②、获取 reg属性中 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并初始化。
③、在②中将 GPIO1_IO03这个 PIN 复用为了 GPIO 功能,因此需要设置 GPIO_IO03 这个 GPIO 相关的寄存器,也就是 GPIO1_DR 和 GPIO1_GDIR 两个寄存器。
总结一下,②中完成对 GPIO1_IO03 这个 pin 的初始化,设置这个 pin 的复用功能,③中完成这个 GPIO 的初始化,设置 GPIO 为输入、输出等。因此 Linux 内核针对 PIN 的配置推出了 pinctrl 子系统,对于 GPIO 的配置推出了 gpio 子系统。
大多数 SOC 的 pin 都是支持复用的,此外我们还需要配置 pin 的电器属性,比如上/下拉、速度、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐。pinctrrl 子系统就是为了更好的配置而引入的,pinctrl 子系统的主要工作内容如下
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,著需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,pinctrl 子系统源码目录为 drivers/pinctrl。
I.MX6ULL 的 pinctrl 子系统驱动
要使用 pinctrl 子系统,我们需要在设备树里面配置 PIN 的配置信息,毕竟 pinctrl 子系统要根据你提供的信息来配置 PIN 功能,一般会在设备梳理创建一个节点来描述 PIN 的配置信息。打开 imx6ul.dtsi,找到一个叫做 iomuxc 的节点,如下所示:
iomuxc: pinctrl@20e0000 {
compatible = "fsl,imx6ul-iomuxc";
reg = <0x020e0000 0x4000>;
};
这里的 0x020e0000 指的是 IOMUXC 寄存器块的物理机地址,换句话说,CPU 内部有一块专门控制“引脚复用”的寄存器区域,它在 SoC 的内存映射空间里,从 0x020E0000 开始。在 ARM SoC 里,所有外设都是“内存映射”的。CPU 访问外设 ≈ 访问一段特殊地址的内存。设备树中的
reg = <0x020e0000 0x4000>;
意思是这个硬件块从 0x020E0000 开始,占用 0x4000 字节(16KB)。打开之前修改过的 imx6ull-hcw-emmc.dtsi,找到如下内容:
1 | &iomuxc { |
这段代码就是向 iomuxc 节点追加数据,不同的外设使用的 PIN 不同,因此将某个外设所使用的所有 PIN 都组织在一个子节点里面。因此,可以得到完整的 iomuxc 节点,如下所示:
1 | iomuxc: pinctrl@20e0000 { |
代码中,compatible 属性值为“fsl,imx6ul-iomuxc”,前面讲解设备树的时候说过,Linux 内核会根据 compatbile 属性值来查找对应的驱动文件,所以我们在 Linux 内核源码中全局搜索字符串“fsl,imx6ul-iomuxc”就会找到 I.MX6ULL 这颗 SOC 的 pinctrl 驱动文件。这里的 pinctrl-names = “default”; 意味着这个控制器有一个默认状态组。外设如果引用 pinctrl-0,通常会用这个状态。
接下来我们来看这一句
MX6UL_PAD_CSI_MCLK__CSI_MCLK 0x1b088
我们来拆解他,第一部分 MX6UL_PAD_CSI_MCLK__CSI_MCLK,这个宏定义在 imx6ull-pinfunc.h,这个宏已经告诉内核,这个 PAD 的 MUX 寄存器位置,PAD 控制寄存器位置,应该写哪个 MUX 模式,输入选择值。其结构为
物理引脚名__功能名
后面的 0x1b088 为电气属性。
在设备树中添加 pinctrl 节点模板
我们已经简单了解了 pinctrl 子系统,接下来我们尝试一下在设备树中添加某个外设的 PIN 信息。这里我们虚拟一个名为 “test” 的设备,test 使用了 GPIO1_IO00 这个 PIN 的 GPIO 功能,pinctrl 节点添加过程如下:
1、创建对应的节点
同一个外设的 PIN 都放到一个节点里面,打开 imx6ull-hcw-emmc.dts,在 iomuxc 节点中的 imx6ul-evk 子节点下添加 pinctrl_test 节点,添加完成后如下所示
pinctrl_test: testgrp
{
}
2、添加“fsl,pins”属性
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为 “fsl,pins”,因为对于 I.MX 系列SOC而言,pinctrl 驱动程序是通过读取 “fsl,pins” 属性值来获取 PIN 的配置信息,完成后如下所示:
pinctrl_test: testgrp
{
fsl,pins = <
>;
}
3、在“fsl,pins”属性中添加 PIN 配置信息
最后在 “fsl,pins” 属性中添加具体的 PIN 配置信息,完成后如下所示:
pinctrl_test: testgrp
{
fsl,pins = <
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config
>;
}
至此,我们已经在 imx6ull-hcw-emmc.dtsi 文件中添加好了 test 设备所使用的 PIN 配置信息。
GPIO 子系统
GPIO 子系统简介
之前学习了 pinctrl 子系统,其特点是设置 PIN ,(有的SOC 叫做 PAD)的复用和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO 为输入输出,读取 GPIO 的值等等。我们在设备树中添加 GPIO 相关的信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO。
设备树中的 gpio 信息
I.MX 开发板上的 UART1_RTS_B 作为 SD 卡的引脚检测,首先是将 UART1_RTS_B 复用为 GPIO1_IO19,并且设置电气属性,也就是 pinctrl 节点。打开 imx6ull-hcw-emmc.dtsi,可以看到这个引脚的配置如下:
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
pinctrl 配置好以后就是设置 gpio 了,SD 卡驱动程序通过读取 GPIO_IO19 的值来判断 SD 卡有没有插入。在设备树 SD 卡节点下添加一个属性来描述 SD 卡的 CD 引脚就行了,SD 卡驱动直接读取这个属性值就可以知道 SD卡的 CD 引脚使用的是哪个 GPIO 了。SD 卡连接在 I.MX6ULL 的 usdhc1 接口上,可以在同文件中找到名为 usdhc1 的节点,这个节点就是 SD 卡设备节点,如下所示:
&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;
wakeup-source;
vmmc-supply = <®_sd1_vmmc>;
status = "okay";
};
可以看出,默认情况下,使用的是 pinctrl_usdhc1 这套引脚配置。属性 “cd-gpios” 描述了 SD 卡的 CD 引脚使用哪个 IO。属性值一共有三个,“&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,“19” 表示 GPIO1 组的第 19 号 IO。“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH” 就表示高电平有效。
gpio 子系统的 API 函数
对于我们来说,设置好设备树之后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO ,gpio 子系统向我们屏蔽了具体的读写寄存器过程,这就是驱动分离与分层,大家各司其职,做好自己的本职工作即可。gpio 子系统提供的常用 API 函数如下:
1、gpio_request
gpio_request 函数用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request 进行申请,函数原型如下
int gpio_request(unsigned gpio, const char *label)
函数参数和返回值含义如下:
gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号。
label:给 gpio 设置个名字。
返回值:0,申请成功;其他值,申请失败。
2、gpio_free
如果不适用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放。函数原型如下:
void gpio_free(unsigned gpio)
函数参数和返回值含义如下:
gpio:要释放的 gpio 标号。
返回值:无。
3、gpio_direction_input函数
此函数用于设置某个 GPIO 为输入,函数原型如下所示:
int gpio_direction_input(unsigned gpio)
函数的参数和返回值含义如下
gpio:要设置为输入的 GPIO 标号。
返回值:0,设置成功;负值,设置失败。
4、gpio_direction_output函数
同上
5、gpio_get_value函数
此函数用于获取某个 GPIO 的值(0或1),这是个宏,定义为:
#define gpio_get_value __gpio_get_value int __gpio_get_value(unsigned gpio)
函数参数和返回值含义如下:
gpio:要设置为输入的 GPIO 标号。
返回值:非负值,得到的 GPIO 值;负值,获取失败。
6、gpio_set_value函数
此函数用于设置某个 GPIO 的值,此函数是个宏,定义如下:
#define gpio_set_value __gpio_set_value void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio:要设置的 GPIO 标号。
value:要设置的值。
返回值:无
关于 gpio 子系统常用的 API 函数就讲这些,这些是我们用的最多的。
在设备树中添加 gpio 节点模板
继续完成之前的 test 设备,在前面我们已经说了如何创建 test 设备的 pinctrl 节点,接下来我们来创建 test 设备的 GPIO 节点。
1、创建 test 设备节点
在根节点 “/” 下创建 test 设备子节点,如下所示
test{
};
2、添加 pinctrl 信息
将我们之前创建的 pinctrl_test 节点加入其中。如下所示:
test{
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_test>;
};
3、添加 GPIO 属性信息
最后需要在 test 节点中添加 GPIO 属性信息,表明 test 所使用的 GPIO 是哪个引脚,添加完成以后如下所示:
test{
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_test>;
gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
};
关于 pinctrl 子系统和 gpio 子系统就坐到这里,接下来就是使用这两个子系统区驱动开发板上的 LED 灯。
与 gpio 相关的 of 函数
在上面的代码中,我们定义了一个名为 gpio 的属性,gpio 属性描述了 test 这个设备所使用的 GPIO。在驱动程序中需要读取 gpio 属性内容,Linux 内核提供了几个关于 GPIO 有关的 OF 函数,常用的几个 OF 函数如下所示:
1、of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信息,其函数的原型如下
int of_gpio_named_count(struct device_node *np, const char *propname)
函数参数和返回值含义如下:
np:设备节点。
propname:要统计的 GPIO 属性。
返回值:正值,统计到的 GPIO 数量;负值,失败。
2、of_gpio_count 函数
of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的 GPIO 信息,函数原型如下所示:
int of_gpio_count(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:正值,统计到的 GPIO 数量;负值,失败。
3、of_get_named_gpio 函数
此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的 GPIO 编号,此函数在驱动中使用很频繁!函数原型如下:
int of_get_named_gpio(struct device_node const char int *np, *propname, index)
函数参数和返回值含义如下:
np:设备节点。
propname:包含要获取 GPIO 信息的属性名。
index:GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO 的编号,如果只有一个 GPIO 信息的话此参数为 0。
返回值:正值,获取到的 GPIO 编号;负值,失败。
实验程序编写
修改设备树文件
1、添加 pinctrl 节点
I.MX6U-ALPHA 开发板上的 LED 灯使用了 GPIO1_IO03 这个 PIN,打开 imx6ull-hcw-emmc.dtsi,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:
pinctrl_led: ledgrp{
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0
>;
};
2、添加 LED 设备节点
在根节点下创建 LED 灯节点,节点名为 gpioled,节点内容如下
gpioled{
#address-cells = <1>;
#size-cells = <1>;
compatible = "hcw-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
3、检查 PIN 是否被其他外设使用
这一点非常重要!
PIN 有没有被其他外设使用包括两个方面:
①、检查 pinctrl 设置。
②、如果这个 PIN 被配置为 GPIO 的话,检查这个 GPIO 有没有背别的外设所使用。
在 imx6ull-hcw-emmc.dtsi 中找到如下内容:
pinctrl_tsc: tscgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0xb0
MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0xb0
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0xb0
MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0xb0
>;
};
很明显,这里和上面的 PIN 配置冲突了,我们将这一段屏蔽。因为我们还把这个 PIN 配置为了 GPIO,所以还需要查找一下有没有别的外设使用了 GPIO_IO03,在此文件中搜索 gpio1 3,找到如下内容:
&tsc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_tsc>;
xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
measure-delay-time = <0xffff>;
pre-charge-time = <0xfff>;
status = "okay";
};
tsc 是 TSC 的外设节点,从 726 行可以看出,tsc 外设也使用了 GPIO1_IO03,同样我们需要将这一行屏蔽掉。然后在继续搜索“gpio1 3”,看看除了本章的 LED 灯以外还有没有其他的地方也使用了 GPIO1_IO03,找到一个屏蔽一个。
设备树编写完成后使用“make dtbs”命令重新编译设备树,然后重新启动 Linux 系统。进入 /proc/device-tree 文件夹,可以看到 gpioled 节点存在。

LED 灯驱动程序编写
设备树准备好以后就可以编写驱动程序了,由此我们来回忆一下驱动程序的基本写法。(正在学习中,等待更新)
1 分配设备号
2 初始化 cdev
3 注册 cdev
4 创建设备节点
5 实现 file_operations
6 卸载驱动释放资源
新建文件,写上头文件,并且写好注册注销基本函数框架。创建如下结构体
1 | struct gpioled_dev{ |
这段代码将一个 LED 设备需要的所有信息,打包成一个设备对象。这里面每一个成员的作用如下。
1、dev_t devid 设备号,Linux 用它区分设备。
2、struct cdev cdev。字符设备对象,它负责把 file_operations 挂到设备号上。
3、struct class *class,设备类,作用是让系统自动生成 /sys/class/gpioled。
4、struct device *device,设备对象,创建以后系统会自动生成/dev/gpioled
5、major、minor 主设备号和次设备号。
6、struct device_node *nd,设备树节点,驱动需要去设备树找 LED 的 GPIO,设备树可能是:
led {
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
驱动就会
gpioled.nd = of_find_node_by_path("/led");
7、int led_gpio,这个是真正控制 LED 的 GPIO 编号,比如 GPIO1_IO03,驱动会解析设备树并得到编号gpioled.led_gpio = of_get_named_gpio(gpioled.nd,”led-gpio”,0); 然后控制 LED:gpio_set_value(gpioled.led_gpio,1);
接下来 我们写好四个基本函数的框架:
1 | static int led_open(struct inode *inode, struct file *filp) |
现在,我们来完成各个函数的具体内容。
1、init 函数
init 函数需要获取设备节点、获取设备树中的 gpio 属性,得到 LED 所使用的编号。然后时注册字符设备驱动,首先创建设备号,然后初始化 cdev(init 并 添加),然后创建类,最后创建一个设备。
1 | static int __init gpioled_init(void) |
接下来是 exit 函数
1 | static void __exit gpioled_exit(void) |
最后是 write 函数
1 | static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) |
最终代码放在文章末尾。
编译装载运行
编译生成 ko 模块并装载到文件系统中。运行测试程序,成功点亮 LED 灯。
完整驱动代码
1 |
|
1 | KERNELDIR := /home/hcw//learn/linux-6.1.161/ |