前面的学习,我们使用直接操作寄存器的方法编写驱动。这只是为了让大家掌握驱动程序的本质,在实际开发过程中我们可不这样做,太低效了!如果驱动开发都是这样去查找寄存器,那我们就变成“寄存器工程师”了,即使是做单片机的都不执着于裸写寄存器了。

Linux 下针对引脚有 2 个重要的子系统: GPIO、 Pinctrl。

1 Pinctrl子系统重要概念

1.1 引入

无论是哪种芯片,都有类似下图的结构

要想让 pinA、 B 用于GPIO,需要设置 IOMUX 让它们连接到 GPIO 模块;

要想让 pinA、 B 用于I2C,需要设置 IOMUX 让它们连接到 I2C 模块

所以 GPIO、 I2C 应该是并列的关系,它们能够使用之前,需要设置 IOMUX。有时候并不仅仅是设置 IOMUX,还要配置引脚,比如上拉、下拉、开漏等等。

现在的芯片动辄几百个引脚,在使用到 GPIO 功能时,让你一个引脚一个引脚去找对应的寄存器,这要疯掉。术业有专攻,这些累活就让芯片厂家做吧──他们是 BSP 工程师。我们在他们的基础上开发,我们是驱动工程师。开玩笑的, BSP 工程师是更懂他自家的芯片,但是如果驱动工程师看不懂他们的代码,那你的进步也有限啊。

所以,要把引脚的复用、配置抽出来,做成 Pinctrl 子系统,给 GPIO、I2C 等模块使用

等BSP工程师在 GPIO 子系统、 Pinctrl 子系统中把自家芯片的支持加进去后,我们就可以非常方便地使用这些引脚了:点灯简直太简单了。

大多数的芯片,没有单独的 IOMUX 模块,引脚的复用、配置等等,就是在GPIO 模块内部实现的。在硬件上 GPIO 和 Pinctrl 是如此密切相关,在软件上它们的关系也非常密切。所以这 2 个子系统我们一起讲解。

1.2 重要概念

这会涉及 2 个对象: pin controller、 client device。

前者提供服务:可以用它来复用引脚、配置引脚。

后者使用服务:声明自己要使用哪些引脚的哪些功能,怎么配置它们。

① pin controller:

在芯片手册里你找不到 pin controller,它是一个软件上的概念,你可以认为它对应 IOMUX──用来复用引脚,还可以配置引脚(比如上下拉电阻等)。

注意:pin controller 和 GPIO Controller 不是一回事,前者控制的引脚可用于 GPIO 功能、 I2C 功能;后者只是把引脚配置为输入、输出等简单的功能。 即先用 pin controller 把引脚配置为GPIO,再用 GPIO Controler 把引脚配置为输入或输出。

② client device

“客户设备”,谁的客户? Pinctrl 系统的客户,那就是使用 Pinctrl 系统的设备,使用引脚的设备。它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。

左边是 pin controller 节点,右边是 client device 节点

(a)pin state

对于一个“ client device”来说,比如对于一个 UART 设备,它有多个“状态”: default、 sleep 等,那对应的引脚也有这些状态。比如默认状态下, UART 设备是工作的,那么所用的引脚就要复用为 UART 功能。在休眠状态下,为了省电,可以把这些引脚复用为 GPIO 功能;或者直接把它们配置输出高电平。上图中, pinctrl-names 里定义了 2 种状态: default、 sleep。

第 0 种状态用到的引脚在 pinctrl-0 中定义,它是 state_0_node_a,位于 pincontroller 节点中。
第 1 种状态用到的引脚在 pinctrl-1 中定义,它是 state_1_node_a,位于 pincontroller 节点中。
当这个设备处于 default 状态时, pinctrl 子系统会自动根据上述信息把所用引脚复用为 uart0 功能。
当这个设备处于 sleep 状态时, pinctrl 子系统会自动根据上述信息把所用引脚配置为高电平。

(b)groups 和 function

一个设备会用到一个或多个引脚,这些引脚就可以归为一组(group);
这些引脚可以复用为某个功能: function。

(c)Generic pin multiplexing node 和 Generic pin configuration node

在上图左边的 pin controller 节点中,有子节点或孙节点,它们是给 client device 使用的。
可以用来描述复用信息:哪组(group)引脚复用为哪个功能(function);
可以用来描述配置信息:哪组(group)引脚配置为哪个设置功能(setting),比如上拉、下拉等。

注意:

pin controller 节点的格式, 没有统一的标准!!!!每家芯片都不一样。
甚至上面的 group、 function 关键字也不一定有,但是概念是有的

1.3 示例

1.4 代码中怎么引用 pinctrl

这是透明的,我们的驱动基本不用管 。当设备切换状态时 , 对 应 的pinctrl 就会被调用。
比如在 platform_device 和 platform_driver 的枚举过程中,流程如下

当系统休眠时,也会去设置该设备 sleep 状态对应的引脚,不需要我们自己去调用代码。非要自己调用,也有函数

devm_pinctrl_get_select_default(struct device *dev);      // 使用"default"状态的引脚
pinctrl_get_select(struct device *dev, const char *name); // 根据 name 选择某种状态的引脚
pinctrl_put(struct pinctrl *p);                           // 不再使用, 退出时调用

2 GPIO 子系统重要概念

2.1 引入

要操作 GPIO 引脚,先把所用引脚配置为 GPIO 功能,这通过 Pinctrl 子系统来实现。然后就可以根据设置引脚方向(输入还是输出)、读值──获得电平状态,写值──输出高低电平。

以前我们通过寄存器来操作 GPIO 引脚,即使 LED 驱动程序,对于不同的板子它的代码也完全不同。

当 BSP 工程师实现了 GPIO 子系统后,我们就可以

⚫在设备树里指定 GPIO 引脚
⚫在驱动代码中:使用 GPIO 子系统的标准函数获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值。

这样的驱动代码,将是单板无关的。

2.2 在设备树中指定引脚

在几乎所有ARM芯片中, GPIO 都分为几组,每组中有若干个引脚。所以在使用 GPIO 子系统之前,就要先确定:它是哪组的?组里的哪一个?

在设备树中,“GPIO 组”就是一个 GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“ gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。

有代码更直观,下图是一些芯片的 GPIO 控制器节点,它们一般都是厂家定义好,在 xxx.dtsi 文件中:

没有在/home/book/100ask_stm32mp157_pro-sdk/Linux-5.4/arch/arm/boot/dts/stm32mp157-100ask-pinctrl.dtsi

找到类似GPIO的代码

我们暂时只需要关心里面的这 2 个属性:

gpio-controller;
#gpio-cells = <2>;

⚫ “gpio-controller”表示这个节点是一个 GPIO Controller,它下面有很多引脚。
⚫ “#gpio-cells = <2>”表示这个控制器下每一个引脚要用 2 个 32 位的数(cell)来描述。

为什么要用 2 个数?其实使用多个 cell 来描述一个引脚,这是 GPIO Controller 自己决定的。比如可以用其中一个 cell 来表示那是哪一个引脚,用另一个 cell 来表示它是高电平有效还是低电平有效,甚至还可以用更多的cell 来示其他特性。

普遍的用法是,用第1个 cell 来表示哪一个引脚,用第 2 个 cell 来表示有效电平:

GPIO_ACTIVE_HIGH : 高电平有效
GPIO_ACTIVE_LOW :   低电平有效

定义 GPIO Controller 是芯片厂家的事,我们怎么引用某个引脚呢?在自己的设备节点中使用属性"[<name>-]gpios",示例如下

上图中,可以使用 gpios 属性,也可以使用 name-gpios 属性。

2.3 在驱动代码中调用 GPIO 子系统

在设备树中指定了 GPIO 引脚,在驱动代码中如何使用?

也就是 GPIO 子系统的接口函数是什么?

GPIO 子系统有两套接口:基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“ gpiod_”,它使用 gpio_desc 结构体来表示一个引脚;后者的函数都有前缀“ gpio_”,它使用一个整数来表示一个引脚。

要操作一个引脚,首先要 get 引脚,然后设置方向,读值、写值。驱动程序中要包含头文件

#include <linux/gpio/consumer.h> // descriptor-based
#include <linux/gpio.h>          // legacy

下表列出常用的函数

descriptor-based legacy
获得 GPIO
gpiod_get gpio_request
gpiod_get_index
gpiod_get_array gpio_request_array
devm_gpiod_get
devm_gpiod_get_index
devm_gpiod_get_array
设置方向
gpiod_direction_input gpio_direction_input
gpiod_direction_output gpio_direction_output
读值、写值
gpiod_get_value gpio_get_value
gpiod_set_value gpio_set_value
释放 GPIO
gpio_free gpio_free
gpiod_put gpio_free_array
gpiod_put_array
devm_gpiod_put
devm_gpiod_put_array

有前缀 “devm_”的含义是“设备资源管理 ” (Managed Device Resource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。

比如在 Linux 开发过程中,先申请了 GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放 GPIO 资源。如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的GPIO 资源。

建议使用“ devm_”版本的相关函数。

假设备在设备树中有如下节点:

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;

// 取出名为led的第0个引脚,并设置为输出高电平
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);

gpiod_set_value 设置的值是“逻辑值”,不一定等于物理值。什么意思?

旧的“ gpio_”函数没办法根据设备树信息获得引脚,它需要先知道引脚号。

引脚号怎么确定?

在 GPIO 子系统中,每注册一个 GPIO Controller 时会确定它的“ base number”,那么这个控制器里的第 n 号引脚的号码就是: base number + n。

但是如果硬件有变化、设备树有变化,这个 base number 并不能保证是固定的,应该查看 sysfs 来确定 base number。

2.4 sysfs 中的访问方法

在 sysfs 中访问 GPIO,实际上用的就是引脚号,老的方法。

⚫ 先确定某个 GPIO Controller 的基准引脚号(base number),再计算出某个引脚的号码。

方法如下:

① 先在开发板的/sys/class/gpio 目录下,找到各个 gpiochipXXX 目录
② 然后进入某个 gpiochip 目录,查看文件 label 的内容
③ 根据 label 的内容对比设备树
label 内容来自设备树,比如它的寄存器基地址。用来跟设备树(dtsi 文件)比较,就可以知道这对应哪一个 GPIO Controller。

下图是在 stm32mp157 上运行的结果,通过对比设备树可知 gpiochip112对应 gpioH:

有多个gpiochipXXXX文件,其中gpiochip112的基址是112,ngpio是个数。GPIOH是第H组GPIO,有时候是一串别的字符,根据字符在设备树中查找对应的GPIO组。

所以 gpioH 这组引脚的基准引脚号就是 112,这也可以“ cat base”来再次确认。

⚫ 基于 sysfs 操作引脚:

以 STM32MP157 为例,它有一个按键,原理图如下:

那么 GPIOG_02 的号码是 96+2=98,可以如下操作读取按键值

echo 98 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio98/direction
cat /sys/class/gpio/gpio98/value

执行第三条命令的时候,同时按下按键,输出0

不用了执行

echo 98 > /sys/class/gpio/unexport

对于输出引脚,假设引脚号为 N,可以用下面的方法设置它的值为 1:

echo N > /sys/class/gpio/export
echo out > /sys/class/gpio/gpioN/direction
echo 1 > /sys/class/gpio/gpioN/value
echo N > /sys/class/gpio/unexport

3 基于 GPIO 子系统的 LED 驱动程序

3.1 编写思路

GPIO 的地位跟其他模块,比如 I2C、 UART 的地方是一样的,要使用某个引脚,需要先把引脚配置为 GPIO 功能,这要使用 Pinctrl 子系统,只需要在设备树里指定就可以。在驱动代码上不需要我们做任何事情。

GPIO 本身需要确定引脚,这也需要在设备树里指定。设备树节点会被内核转换为platform_device。对应的,驱动代码中要注册一个 platform_driver,在 probe 函数中:获得引脚、注册 file_operations。

在 file_operations 中:设置方向、读值/写值。 下图就是一个设备树的例子:

3.2 在设备树中添加 Pinctrl 信息

有些芯片提供了设备树生成工具,在 GUI 界面中选择引脚功能和配置信息,就可以自动生成 Pinctrl 子结点。把它复制到你的设备树文件中,再在client device 结点中引用就可以。

有 些 芯 片 只 提 供 文 档 , 那 就 去 阅 读 文 档 , 一 般 在 内 核 源 码 目 录
Documentation\devicetree\bindings\pinctrl 下面,保存有该厂家的文档。如果连文档都没有,那只能参考内核源码中的设备树文件,在内核源码目录 arch/arm/boot/dts 目录下。

最后一步,网络搜索。

Pinctrl 子节点的样式如下:

3.3 在设备树中添加 GPIO 信息

先查看电路原理图确定所用引脚,再在设备树中指定:添加”[name]-gpios”属性,指定使用的是哪一个 GPIO Controller 里的哪一个引脚,还有其他 Flag 信息,比如 GPIO_ACTIVE_LOW 等。具体需要多少个 cell 来描述一个引脚,需要查看设备树中这个 GPIO Controller 节点里的“ #gpio-cells”属性值,也可以查看内核文档。示例如下:

3.4 编程示例

在实际操作过程中也许会碰到意外的问题,现场演示如何解决。
第1步 定义、注册一个 platform_driver
第2步 在它的 probe 函数里:
        a) 根据 platform_device 的设备树信息确定 GPIO: gpiod_get
        b) 定义、注册一个 file_operations 结构体
        c) 在 file_operarions 中使用 GPIO 子系统的函数操作 GPIO:

好处: 这些代码对所有的板子都是完全一样的

⚫ 注册 platform_driver

// 资源
static const struct of_device_id winter_leds[] = {
	{ .compatible = "winter, led_drv"},
	{},
};

// 定义platform_driver
static struct platform_driver chip_demo_gpio_driver = {
	.probe = chip_demo_gpio_probe,
	.remove = chip_demo_gpio_remove,
	.driver = {
		.name = "winter_led",
		.of_match_table = winter_leds,
	},
};

// 入口函数中注册platform_driver
static int __init led_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 注册platform_driver结构体
	err = platform_driver_register(&chip_demo_gpio_driver);
	return err;
}

// 出口函数
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");

⚫ 在 probe 函数中获得 GPIO
核心代码是如下,它从该设备(对应设备树中的设备节点)获取名为“ led”的引脚。在设备树中,必定有一属性名为“ led-gpios”或“ ledgpio”。

/* 	  从platform_device获得GPIO
 *    把file_operations结构体告诉内核:注册驱动程序
 */
int chip_demo_gpio_probe (struct platform_device* pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 设备树中定义 led-gpios=<...>
    // 核心代码
	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 -1;
	}

	// 注册file_operations结构体
	major = register_chrdev(0, "winter_led", &led_drv);					// /proc/devices/winter_led
	// 注册结点
	led_class = class_create(THIS_MODULE, "winter_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, "winter_led%d", 0); /* /dev/100ask_led0 */
	return 0;
}

⚫ 在 open 函数中调用 GPIO 函数设置引脚方向-输出

// 实现对应的open/read等函数
int led_drv_open (struct inode* node, struct file* file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 根据次设备号初始化LED
	gpiod_direction_output(led_gpio, 0);
	return 0;
}

⚫ 在 write 函数中调用 GPIO 函数设置引脚值:

// 用户向内核中写数据
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__);
	copy_from_user(&status, buf, 1);
	// 根据次设备号和status控制LED
	gpiod_set_value(led_gpio, status);

	return 1;
}

⚫ 释放 GPIO:

gpiod_put(led_gpio);

完整代码

led_drv.c

#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;


// 实现对应的open/read等函数
int led_drv_open (struct inode* node, struct file* file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 根据次设备号初始化LED
	gpiod_direction_output(led_gpio, 0);
	return 0;
}
// 从内核中读数据到用户
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;
}
// 用户向内核中写数据
ssize_t led_drv_write (struct file* file, const char __user* buf, size_t size, loff_t* offset)
{
	char status;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	copy_from_user(&status, buf, 1);
	// 根据次设备号和status控制LED
	gpiod_set_value(led_gpio, status);

	return 1;
}

int led_drv_close (struct inode* noed, 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,
};


/* 	  从platform_device获得GPIO
 *    把file_operations结构体告诉内核:注册驱动程序
 */
int chip_demo_gpio_probe (struct platform_device* pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 设备树中定义 led-gpios=<...>
	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 -1;
	}

	// 注册file_operations结构体
	major register_chrdev(0, "winter_led", &led_drv);					// /proc/devices/winter_led
	// 注册结点
	led_class = class_create(THIS_MODULE, "winter_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, "winter_led%d", 0); /* /dev/100ask_led0 */
	return 0;
}



int chip_demo_gpio_remove (struct platform_device* pdev)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(led_class, MKDEV(major, 0));
	class_destroy(led_class);
	unregister_chrdev(major, "winter_led");
	gpiod_put(led_gpio);
	return 0;
}


// 资源
static const struct of_device_id winter_leds[] = {
	{ .compatible = "winter,led_drv"},
	{},
};

// 定义platform_driver
static struct platform_driver chip_demo_gpio_driver = {
	.probe = chip_demo_gpio_probe,
	.remove = chip_demo_gpio_remove,
	.driver = {
		.name = "winter_led",
		.of_match_table = winter_leds,
	},
};

// 入口函数中注册platform_driver
static int __init led_init(void)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 注册platform_driver结构体
	err = platform_driver_register(&chip_demo_gpio_driver);
	return err;
}

// 出口函数
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");

4 在 100ASK_STM32MP157_Pro 上机实验

PA10,第A组的第10个引脚这个引脚输出低电平的时候,led点亮。先将其配置成GPIO功能。

有一个设置代码的工具,很方便。

4.1 确定引脚并生成设备树节点

ST 公司对于 STM32MP157 系列芯片, GPIO 为默认模式 不需要再进行配置Pinctrl 信息。
a.Pinctrl 信息:
b. 设备节点信息 ( 放在 arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts 根节点下):

myled {
    compatible = "winter,led_drv";
    pinctrl-names = "default";
    led-gpios = <&gpioa 10 GPIO_ACTIVE_LOW>;
};

gpioa的第10个属性,低电平有效。可以看到之前的设置。

如果有其他结点也用到这个引脚,需要禁止掉,先搜索

disabled

设置工具链,编译设备树

make dtbs

这个拷贝有误,需要拷贝带lcd的设备树文件

编译之前写的驱动

4.2 测试

先确定boot分区挂载在哪里

cat /proc/mounts

这里没问题,如果不是/boot分区,重新挂载

mount /dev/mmcblk2p2 /boot

把编译出来的设备树文件拷贝到/boot目录下

重启reboot

安装驱动

insmod led_drv.ko

执行测试程序

./ledtest /dev/winter_led0 on
./ledtest /dev/winter_led0 off

现象:板子的灯在on的时候亮,在off的时候灭。

优点:避免操作寄存器,提高效率。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐