嵌入式Linux驱动——6 Pinctrl和GPIO子系统
目录
1.Pinctrl
1.1 pin controller
1.2 client device
1.3 补充说明
2.GPIO 子系统
2.1 利用 GPIO Controller 来指定使用哪个引脚:
2.2 怎么在设备节点引用某个引脚呢?
2.3 配置 GPIO
具体新接口和老接口的区别:
1. 新接口
2.老接口
引脚号怎么确定?
可以通过查看 sysfs 来确定 base number:
还可以基于 sysfs 操作引脚:
3.基于 GPIO 子系统的 LED 驱动程序
流程思路:
具体步骤(以led为例):
1.在设备树中添加 Pinctrl 信息
1.1 在设备树 iomuxc_snvs 节点中添加 pin controller
1.2 在 led 设备节点添加 client device 信息
2.在 led 设备节点中添加 GPIO 信息:
3.GPIO Controller 不需要改动,厂商在.dtsi文件中已经设置好了
完整的 led 设备节点(包含 Pinctrl 和 GPIO 子系统信息):
4.驱动的编写
配置一个 pin 引脚十分复杂,以配置 GPIO 为例:
- 配置引脚复用功能(IOMUX)为 GPIO
- 配置引脚参数(上拉、下拉...)
- 配置 GPIO (方向、数据...)
难道每个引脚都通过一个一个配置寄存器吗?这不可能
因此需要使用 Pinctrl 子系统(用于配置引脚复用功能)
因为一般都是把引脚配置为 GPIO,因此这里把 GPIO 子系统也讲了(跟 Pinctrl 类似,用于配置 GPIO)
1.Pinctrl
Pinctrl 跟设备树是紧密相连的,一般涉及分为两个部分:
1.1 pin controller
一个软件上的概念,可实现 IOMUX 的功能作用:选择引脚复用功能、配置引脚(比如上下拉电阻等)
例如可以把 pin 配置复用为 GPIO 功能和引脚参数,但 GPIO 的配置(方向、数据...)得使用后面讲的 GPIO 子系统
它是设备树中的一个节点,其中有多个子节点,每个子节点声明了:
- 该节点使用了哪些引脚
- 把这些引脚复用为了什么功能
- 把引脚参数配置为了多少
1.2 client device
“客户设备”,谁的客户?Pinctrl 系统的客户(理解为 pin controller 的客户更准确)
它是一个在设备树根目录下的设备节点,节点中各属性内容主要是:
- 用 pinctrl-names 声明设备的状态(可以有多个)
- 声明每个状态分别使用 pin controller 的哪个子节点来配置引脚(pinctrl-x = < &pin conctrl >,x 代表第几个状态)
图示:
在图中,
当设备是 default 状态时,Pinctrl 子系统会自动配置好 pincontroller 中 state_0_node_a 节点的引脚和功能
当设备是 sleep 状态时,Pinctrl 子系统会自动配置好 pincontroller 中 state_1_node_a 节点的引脚和功能
1.3 补充说明
pin controller 节点的格式没有统一的标准,但内容都是一样的:该子节点使用哪些引脚、复用成什么功能
示例:不同设备的 pin conctrller 和 client device
2.GPIO 子系统
先通过 Pinctrl 把 pin 配置为 GPIO 功能,接着通过 GPIO 子系统配置 GPIO:引脚方向...等
一般也分为两部分 GPIO Controller 和 device client(跟Pinctrl是同一个,配置好引脚的复用功能后紧接着就是配置GPIO)
在使用 GPIO 子系统之前,就要先确定:它是哪组的?组里的哪一个?
2.1 利用 GPIO Controller 来指定使用哪个引脚:
(注意,只是指定使用哪个引脚,配置引脚要在后面使用函数配置)
在设备树中,GPIO按组来分,一个 “GPIO 组” 就是一个 GPIO Controller,它们一般都是厂家定 义好,在 xxx.dtsi 文件中:
可以看到,在设备树中,GPIO 节点按组来分,需要重点关注 GPIO 组节点的两个属性:
gpio-controller、#gpio-cells=<2>
- gpio-controller:声明该组GPIO节点是一个 GPIO Controller ,
- #gpio-cells = <2>”:表明在设备节点 device client 中每一个引脚要用 2 个 32 位的数(cell)来描述。
(一般是一个数描述用哪个引脚,另一个数描述该引脚是高电平有效还是低电平有效)
定义 GPIO Controller 是芯片厂家的事,我们只需要在设备树中引用该 .dtsi 文件就行
2.2 怎么在设备节点引用某个引脚呢?
在自己的设备节点中使用属性 “[<name>-]gpios” ,示例如下:
可以使用 gpios 属性,也可以使用 <name>-gpios 属性
一个实现了 Pinctrl 和 GPIO 子系统的设备节点示例:
myled { compatible = "100ask,leddrv";/*pinctrl部分*/ pinctrl-names = "default"; //状态pinctrl-0 = <&myled_for_gpio_subsys>; //状态0对应的引脚复用 /*GPIO子系统部分*///指定gpio组,并用两个参数描述引脚:3:pin3;GPIO_ACTIVE_LOW:低电平有效led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; };
在这里虽然 Pinctrl 和 GPIO 子系统配置的引脚是同一个,但这是两个不同的操作,所以要引用 2 次
2.3 配置 GPIO
引用好gpio引脚后,就要开始对其进行配置了
GPIO 子系统有两套接口:
- 一套是新的,基于描述符(descriptor-based)(不用管描述符是什么),函数前缀为 “gpiod_”
- 一套是老的(legacy),函数前缀为 “gpio_”
PS:建议使用新的
在驱动程序中使用这些函数要包含头文件:
- 新接口(descriptor-based):#include <linux/gpio/consumer.h>
- 老接口(legacy):#include <linux/gpio.h>
常见函数如下图:
一般的操作流程:
- 通过 get 函数获取引脚
- 使用上示各种函数配置该引脚(在函数中传入获取的引脚)
具体新接口和老接口的区别:
1. 新接口
新接口用 gpio_desc 结构体表示一个引脚
新接口通过 get 函数获得的是一个引脚的 gpio_desc 结构体,其他的配置函数就是通过传入这个 gpio_desc 结构体实现具体引脚的配置
示例:
假设在设备树中有如下节点:
foo_device { compatible = "acme,foo"; ... led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */ <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */ <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>; };
那么可以使用下面的函数获得引脚:
struct gpio_desc *red, *green, *blue, *power; red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);
在代码中,因为 led-gpios 中引用了多个引脚,因此使用 gpiod_get_index 获取 led-gpios 中的不同引脚的 gpio_desc
当 gpios 中只引用了一个引脚时候,可以直接使用 gpiod_get 获取该引脚的 gpio_desc
后续使用配置函数,就可以通过传入不同引脚的 gpio_desc,实现对不同引脚的配置
gpiod_get 函数可以顺便设置引脚的电平(上示第3个参数),也可以通过 gpiod_set_value 设置
注意:gpiod_set_value 设置的值是“逻辑值”,不一定等于物理值(高电平有效时逻辑1=物理1,低电平有效时逻辑1=物理0)
2.老接口
老接口用编号代表一个接口
老接口 get 函数获得的是一个引脚的编号,其他的配置函数就是通过传入这个编号实现具体引脚的配置
旧的 “gpio_” 函数没办法根据设备树信息获得引脚,它需要先知道引脚号。
引脚号怎么确定?
在 GPIO 子系统中,每注册一个 GPIO Controller 时会确定它的 “base number”那么这个控制器里的第 n 号引脚的号码就是:base number + n。
可以通过查看 sysfs 来确定 base number:
- 先在开发板的/sys/class/gpio 目录下,找到各个 gpiochipXXX 目录(这里每个目录代表一组GPIO Controller ):
- 进入某个 gpiochip 目录,查看文件 label 的内容
- 根据 label 的内容对比设备树,lable中一般包含 GPIO Controller 的寄存器基地址
可以通过在设备树(dtsi 文件)中查找该基地址,就可以知道这对应哪一个 GPIO Controller,就可以把该组 GPIO Controller 与 base number 匹配起来了
如下图所示:
这样就确定了 96 是 gpio4 的 base number,base number + n(第几个引脚)就得到引脚号了
还可以基于 sysfs 操作引脚:
以 100ask_imx6ull 为例,它有一个按键,原理图如下:
那么 GPIO4_14 的号码是 96+14=110,可以如下操作读取按键值:
[root@100ask:~]# echo 110 > /sys/class/gpio/export //获取110号引脚
[root@100ask:~]# echo in > /sys/class/gpio/gpio110/direction //设置引脚方向为in
[root@100ask:~]# cat /sys/class/gpio/gpio110/value //打印出引脚值
[root@100ask:~]# echo 110 > /sys/class/gpio/unexport //释放110号引脚
注意:如果驱动程序已经使用了该引脚,那么将会 export 失败,会提示下面的错误:
3.基于 GPIO 子系统的 LED 驱动程序
流程思路:
- 先把某个引脚配置为 GPIO 功能
这要使用 Pinctrl 子系统,只需要在设备树中的我们的设备节点里指定就可以。在驱动代码上不需要做任何事情。 - 设备节点中还需要写出 gpios 属性,声明GPIO子系统引用的引脚
- 设备树节点会被内核转换为 platform_device,对应的,驱动代码中要注册一个 platform_driver,在 probe 函数中:获取在GPIO子系统引用的引脚、注册 file_operations。
- 在 file_operations 中就可以通过调用上面 GPIO 子系统的配置函数实现设置GIPO方向、读值/写值等操作
具体步骤(以led为例):
1.在设备树中添加 Pinctrl 信息
1.1 在设备树 iomuxc_snvs 节点中添加 pin controller
安装“Pins_Tool_for_i.MX_Processors_v6_x64.exe”后运行,打开 IMX6ULL 的配置文件“MCIMX6Y2xxx08.mex”,就可以在 GUI 界面中选择引脚生成 Pinctrl 的 pin controller 信息
可以看到,生成的信息提示我们要把该引脚的 pin contronller 信息放到设备树 iomuxc_snvs 节点下
&iomuxc_snvs { …… myled_for_gpio_subsys: myled_for_gpio_subsys{ fsl,pins = < //第一个宏表示了把GPIO5_3配置为GPIO,第二个数表示了了要设置的参数(上下拉电阻等)MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x000110A0 >;};
1.2 在 led 设备节点添加 client device 信息
声明:
- pinctrl-names:设备的各种状态
- pinctrl-<x>:不同状态对应引脚复用的配置
2.在 led 设备节点中添加 GPIO 信息:
根据 GPIO Controller 节点里的 “#gpio-cells” 属性值,在 led 设备节点中添加 “[name]-gpios” 属性
指定:使用的是哪一个 GPIO Controller 里的哪一个引脚,还有其他 Flag 信息(高低电平有效等)
3.GPIO Controller 不需要改动,厂商在.dtsi文件中已经设置好了
完整的 led 设备节点(包含 Pinctrl 和 GPIO 子系统信息):
myled { //用于与platform driver相匹配compatible = "100ask,leddrv"; //状态pinctrl-names = "default"; //0号状态pinctrl要复用的引脚及其功能pinctrl-0 = <&myled_for_gpio_subsys>; //GPIO子系统要引用的引脚led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; };
4.驱动的编写
流程:
(1)定义 platform driver 结构体
- 在 .of_match_table 中定义好和 led 设备节点相匹配的 compatible
- 定义好 probe 函数
在 probe 函数中:
- 通过 gpiod_get 获取 gpio 引脚
- 注册 file_operations 结构体
- 创建类和设备节点
源码示例:
static int chip_demo_gpio_probe(struct platform_device *pdev) {//int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*获取GPIO引脚*//*设备树中定义了:led-gpios=<...>,因此通过“led”在pdev->dev获取*/led_gpio = gpiod_get(&pdev->dev, "led", 0);if (IS_ERR(led_gpio)) {dev_err(&pdev->dev, "Failed to get GPIO for led\n");return PTR_ERR(led_gpio);}/*注册file_operations*/major = register_chrdev(0, "100ask_led", &led_drv); /* /dev/led *//*创建类*/led_class = class_create(THIS_MODULE, "100ask_led_class");if (IS_ERR(led_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "led");gpiod_put(led_gpio);return PTR_ERR(led_class);}/*创建设备节点*/device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_led0 */return 0;}
(2)定义 file_operations 结构体
在 file_operations 结构体中实现对GPIO的配置
源码示例:
/*.read函数,这里没作操作*/
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/*.write函数:向GPIO引脚写入status值*/
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;char status;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(&status, buf, 1);/* 根据GPIO引脚,写入status值,控制LED */gpiod_set_value(led_gpio, status);return 1;
}/*.open函数:设置引脚方向*/
static int led_drv_open (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 根据GPIO引脚,设置引脚方向,初始化LED */gpiod_direction_output(led_gpio, 0);return 0;
}/*.release函数:没做任何操作*/
static int led_drv_close (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/*定义自己的file_operations结构*/
static struct file_operations led_drv = {.owner = THIS_MODULE,.open = led_drv_open,.read = led_drv_read,.write = led_drv_write,.release = led_drv_close,
};
完整源码:
#include <linux/module.h>
#include <linux/platform_device.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>/* 1. 确定主设备号 */
static int major = 0;
static struct class *led_class;
static struct gpio_desc *led_gpio;/*.read函数,这里没作操作*/
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/*.write函数:向GPIO引脚写入status值*/
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{int err;char status;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = copy_from_user(&status, buf, 1);/* 根据GPIO引脚,写入status值,控制LED */gpiod_set_value(led_gpio, status);return 1;
}/*.open函数:设置引脚方向*/
static int led_drv_open (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/* 根据GPIO引脚,设置引脚方向,初始化LED */gpiod_direction_output(led_gpio, 0);return 0;
}/*.release函数:没做任何操作*/
static int led_drv_close (struct inode *node, struct file *file)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);return 0;
}/*定义自己的file_operations结构*/
static struct file_operations led_drv = {.owner = THIS_MODULE,.open = led_drv_open,.read = led_drv_read,.write = led_drv_write,.release = led_drv_close,
};static int chip_demo_gpio_probe(struct platform_device *pdev)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);/*获取GPIO引脚*//*设备树中定义了:led-gpios=<...>,因此通过“led”在pdev->dev获取*/led_gpio = gpiod_get(&pdev->dev, "led", 0);if (IS_ERR(led_gpio)) {dev_err(&pdev->dev, "Failed to get GPIO for led\n");return PTR_ERR(led_gpio);}/*注册file_operations*/major = register_chrdev(0, "100ask_led", &led_drv); /* /dev/led *//*创建类*/led_class = class_create(THIS_MODULE, "100ask_led_class");if (IS_ERR(led_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "led");gpiod_put(led_gpio);return PTR_ERR(led_class);}/*创建设备节点*/device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_led0 */return 0;}static int chip_demo_gpio_remove(struct platform_device *pdev)
{device_destroy(led_class, MKDEV(major, 0));class_destroy(led_class);unregister_chrdev(major, "100ask_led");gpiod_put(led_gpio);return 0;
}/*定义和设备节点匹配的compatible*/
static const struct of_device_id ask100_leds[] = {{ .compatible = "100ask,leddrv" },{ },
};/* 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {.probe = chip_demo_gpio_probe,.remove = chip_demo_gpio_remove,.driver = {.name = "100ask_led",.of_match_table = ask100_leds,},
};/* 在入口函数:注册platform_driver */
static int __init led_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = platform_driver_register(&chip_demo_gpio_driver); return err;
}/* 出口函数:卸载platform_driver */
static void __exit led_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);platform_driver_unregister(&chip_demo_gpio_driver);
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");