首先回顾一下如何初始化 LED 所使用的 GPIO:
①、修改设备树,添加相应的节点,节点里面重点是设置 reg 属性, reg 属性包括了 GPIO相关寄存器。
②、获取 reg 属性中 GPIOI_MODER、 GPIOI_OTYPER、 GPIOI_OSPEEDR、 GPIOI_PUPDR和 GPIOI_BSRR 这些寄存器的地址,并且初始化它们,这些寄存器用于设置 PI0 这个 PIN 的复用功能、上下拉、速度等。
③、在②里面将 PI0 这个 PIN 设置为通用输出功能,因此需要设置 PI0 这个 GPIO 相关的寄存器,也就是设置 GPIOI_MODER 寄存器。
④、在②里面将 PI0 这个 PIN 设置为高速、上拉和推挽模式,就要需要设置 PI0 的GPIOI_OTYPER、 GPIOI_OSPEEDR 和 GPIOI_PUPDR 这些寄存器。
其实这些设置跟 STM32 单片机的 GPIO 设置极为相似,但传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引入的, pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,总而言之, pinctrl 子系统将一个 PIN 复用为 GPIO 。
要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息一般会在设备树里面创建一个节点来描述 PIN 的配置信息。打开 stm32mp157.dtsi 文件,找到一个叫做 pinctrl 的节点:
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>; // pinctrl的所有子节点的reg第一位是起始地址,第二位是长度
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>; // ranges属性表示 STM32MP1 的 GPIO 相关寄存器起始地址
interrupt-parent = <&exti>; // 父中断为 exti
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0 1>;
pins-are-numbered;
......
};
/*
STM32MP1最多拥有176个GPIO,分别有 PA0~PA15 .... PZ0~PZ15,PA~PK,起始地址为 0X50002000,终止地址为 0X5000C3FF
PZ 组寄存器起始地址为 0X54004000,终止地址为 0X540043FF,所以 stm32mp151.dtsi 文件里面还有个名为“pinctrl_z”
pinctrl 节点用来描述 PA~PK 这 11 组 IO,因此 ranges 属性中的 0x50002000 表示起始地址, 0xa400 表示寄存器地址范围
*/
在 pins 子节点里面存放外设的引脚描述信息:
此属性用来存放外设所要使用的所有 IO ,比如:
pinmux = ; // 使用 STM32_PINMUX 这宏来配置引脚和引脚的复用功能
/*
* @description : 配置引脚和引脚的复用功能
* @param - port : 表示用那一组 GPIO(例: H 表示为 GPIO 第 H 组,也就是 GPIOH)
* @param - line : 表示这组 GPIO 的第几个引脚(例: 13 表示为 GPIOH_13,也就是 PH13)
* @param - mode : 表示当前引脚要做那种复用功能(例: AF9 表示为用第 9 个复用功能)这个需要查阅 STM32MP1 数据手册来确定使用哪个复用功能
* @return : 找到的父节点
建议一个 PIN 最好只能被一个外设使用
*/
STM32_PINMUX(port, line, mode);
如果一个 PIN 只是作为最基本的 GPIO 功能,那么就是用“GPIO”;如果这个引脚要用作模拟功能,比如 ADC 采集引脚,那么就设置为“ANALOG”。
电气特性在 pinctrl 子系统里不是必须的,可以不配置,但是 pinmux 属性是必须要设置的。
电气特性属性 | 类型 | 作用 |
bias-disable | bootlean | 禁止使用内部偏置电压 |
bias-pull-down | bootlean | 内部下拉 |
bias-pull-up | bootlean | 内部上拉 |
drive-push-pull | bootlean | 推挽输出 |
drive-open-drain | bootlean | 开漏输出 |
output-low | bootlean | 输出低电平 |
output-high | bootlean | 输出高电平 |
slew-rate | enum | 引脚的速度,可设置: 0~3, 0 最慢, 3 最高。 |
bootlean 类型表示了在 pinctrl 子系统只要定义这个电气属性就行了,例如:我要禁用内部电压,只要在 PIN 的配置集里添加“bias-disable”即可,这个时候 bias-pull-down和 bias-pull-up 都不能使用了,因为已经禁用了内部电压,所以不能配置上下拉。 enum 类型使用方法比如要设置 PIN 速度为最低就可以使用“slew-rate=<0>”。
比如我们需要将 PG11 这个 PIN 复用为 UART4_TX 引脚, pinctrl 节点添加过程如下:
在 pinctrl 节点下添加一个“uart4_pins”节点:
&pinctrl {
uart4_pins: uart4-0 {
/* 具体的 PIN 信息 */
};
};
添加一个“pins”子节点,这个子节点是真正用来描述 PIN 配置信,要注意,同一个 pins子节点下的所有 PIN 电气属性要一样。如果某个外设所用的 PIN 可能有不同的配置,那么就需要多个 pins 子节点,例如 UART4 的 TX 和 RX 引脚配置不同,因此就有 pins1 和 pins2 两个子节点。
&pinctrl {
uart4_pins: uart4-0 {
pins1 {
/* UART4 TX 引脚的 PIN 配置信息 */
};
};
};
最后在“pins”节点中添加具体的 PIN 配置信息:
&pinctrl {
uart4_pins: uart4-0 {
pins1 {
pinmux = ; /* UART4_TX */
bias-disable;
drive-push-pull;
};
};
};
对于 STM32MP1 而言,如果一个 IO 用作 GPIO 功能的时候不需要创建对应的 pinctrl 节点!
gpio 子系统用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO 为输入输出,读取 GPIO 的值等。目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO 。
以 PI0 这个引脚所在的 GPIOI 为例,打开stm32mp151.dtsi :
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
...
gpioi: gpio@5000a000 {
gpio-controller; // 表示 gpioi 节点是个 GPIO 控制器,每个 GPIO 控制器节点必须包含“gpio-controller”属性
#gpio-cells = <2>; // “#gpio-cells”属性和“#address-cells”类似,第一个cell是GPIO编号,第二个cell是GPIO极性,0为高电平有效,1为低电平有效
interrupt-controller; // 声明此节点为中断控制器
#interrupt-cells = <2>;
reg = <0x8000 0x400>; // reg 属性设置了 GPIOI 控制器的寄存器基地址偏移为 0X8000,因此 GPIOI 寄存器地址为 0X50002000+0X8000=0X5000A000
clocks = <&rcc GPIOI>; // 指定这个 GPIOI 控制器的时钟
st,bank-name = "GPIOI";
status = "disabled"; // 设备状态为禁用
};
};
gpioi:gpio@5000a000 以下就是 GPIOI 的控制信息,属于 pincrtl 的子节点。因为两个子系统的驱动文件一样,所以注册 pinctrl 驱动的时候就会把 gpio 驱动一起注册。
在 ST 光放的 EVK 开发板是将 PG1 用作 SD 卡检测 (CD) 引脚:
cd-gpios = <&gpiog 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
/*
&gpiog : 表示 CD 引脚所使用的 IO 属于 GPIOG 组
1 : 表示 GPIOG 组的第 1 号 IO。通过 &gpiog 1 知道,CD引脚使用了PG1
GPIO_ACTIVE_LOW | GPIO_PULL_UP : 低电平有效(当 PG1 被拉低的时候表示 SD 卡插入) | 上拉电阻
*/
PG1 用作 GPIO 的时候不需要添加其对应的 pincrl 节点
STM32MP1 的 pinctrl驱动和 gpio 驱动是同一个驱动文件,都为 pinctrl-stm32mp157.c , 入口函数都是 stm32_pctl_probe 这个函数:
int stm32_pctl_probe(struct platform_device *pdev)
{
...
for_each_available_child_of_node(np, child) {
if (of_property_read_bool(child, "gpio-controller")) { // 判断设备树节点,是否有 gpio-controller,如果存在,那么这个节点就是一个GPIO 控制器节点
ret = stm32_gpiolib_register_bank(pctl, child); // 注册 GPIO 驱动,包括生成回调函数
if (ret) {
of_node_put(child);
return ret;
}
}
}
...
}
/*
* @description : 用于申请一个 GPIO 管脚,在使用一个 GPIO 之前一定要使用 gpio_request进行申请
* @param - gpio : 要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号
* @param - label : 给 gpio 设置个名字
* @return : 0,申请成功;其他值,申请失败
*/
int gpio_request(unsigned gpio, const char *label);
/*
* @description : 如果不使用某个 GPIO 了,那么就可以调用 gpio_free 函数进行释放
* @param - gpio : 要释放的 gpio 标号
* @return : 无
*/
void gpio_free(unsigned gpio);
/*
* @description : 用于设置某个 GPIO 为输入
* @param - gpio : 要设置为输入的 GPIO 标号
* @return : 0,设置成功;负值,设置失败
*/
int gpio_direction_input(unsigned gpio);
/*
* @description : 设置某个 GPIO 为输出,并且设置默认输出值
* @param - gpio : 要设置为输出的 GPIO 标号
* @param - value : GPIO 默认输出值
* @return : 0,设置成功;负值,设置失败
*/
int gpio_direction_output(unsigned gpio, int value);
/*
* @description : 用于获取某个 GPIO 的值(0 或 1),此函数是个宏
* @param - gpio : 要获取的 GPIO 标号
* @return : 非负值,得到的 GPIO 值;负值,获取失败
*/
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio);
/*
* @description : 设置某个 GPIO 的值,此函数是个宏
* @param - gpio : 要设置的 GPIO 标号
* @param - value : 要设置的值
* @return : 无
*/
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value);
led {
/* 节点内容 */
};
led {
compatible = "atk,led";
gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
status = "okay";
};
/*
* @description : 用于获取设备树某个属性里面定义了几个 GPIO 信息,要注意的是空的 GPIO 信息也会被统计到
* @param - np : 设备节点
* @param - propname : 要统计的 GPIO 属性
* @return : 正值,统计到的 GPIO 数量;负值,失败
*/
int of_gpio_named_count(struct device_node *np, const char *propname);
/*
* @description : 函数统计的是“gpios”这个属性的 GPIO 数量,而 of_gpio_named_count 函数可以统计任意属性的 GPIO 信息
* @param - np : 设备节点
* @return : 正值,统计到的 GPIO 数量;负值,失败
*/
int of_gpio_count(struct device_node *np);
/*
* @description : 获取 GPIO 编号,编号很重要,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号
* @param - np : 设备节点
* @param - propname : 包含要获取 GPIO 信息的属性名
* @param - index : GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0
* @return : 正值,获取到的 GPIO 编号;负值,失败
*/
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index);
总结:我感觉这里比较绕,主要是区分 pinctrl 子系统和 gpio 子系统的区别:
pinctrl 子系统:引脚复用(可以把某个引脚作为普通的GPIO,也可也作为UART的TX),引脚配置(电气配置);
gpio 子系统:输入时(支持读引脚),输出时(支持输出高低电平)。
gpio 子系统主要用于控制和读取通用的输入/输出引脚,而 pinctrl 子系统主要用于管理和配置硬件引脚的功能和复用。
首先在 ~/linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 路径下找到 stm32mp157d-atk.dts 文件,在根节点 "/" 下创建 LED 节点:
gpioled {
compatible = "alientek,led";
status = "okay";
led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>; // 这里使用GPIOI 0 ,低电平有效
};
之后在 /linux/atk-mpl/linux/my_linux/linux-5.4.31 路径下输入以下命令来编译 stm32mp157d-atk.dts:
make dtbs
cd /arch/arm/boot/dts
sudo cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/ -f
进入 /proc/device-tree 查看 gpioled 节点是否存在:
跟上一节一样,创建 5_gpioled 文件,在里面创建 Vscode 工作区,并新建 gpioled.c 文件。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define GPIOLED_CNT 1 /* 设备号个数 */
#define GPIOLED_NAME "gpioled" /* 名字 */
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* gpioled设备结构体 */
struct gpioled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* led所使用的GPIO编号 */ // 此成员变量保存 LED 等所使用的 GPIO 编号
};
struct gpioled_dev gpioled; /* led设备 */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /* 设置私有数据 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
struct gpioled_dev *dev = filp->private_data; // 通过读取 filp 的 private_data 成员变量来得到设备结构体变量
retvalue = copy_from_user(databuf, buf, cnt); /* 接收APP发送过来的数据 */
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /* 获取状态值 */
/* 调用 gpio_set_value 函数来向 GPIO 写入数据,实现开/关 LED 的效果。不需要直接操作相应的寄存器 */
if(ledstat == LEDON) {
gpio_set_value(dev->led_gpio, 0); /* 打开LED灯 */
} else if(ledstat == LEDOFF) {
gpio_set_value(dev->led_gpio, 1); /* 关闭LED灯 */
}
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int ret = 0;
const char *str;
/* 设置LED所使用的GPIO */
/* 1、获取设备节点:gpioled */
gpioled.nd = of_find_node_by_path("/gpioled");
if(gpioled.nd == NULL) {
printk("gpioled node not find!\r\n");
return -EINVAL;
}
/* 2.读取status属性 */
ret = of_property_read_string(gpioled.nd, "status", &str); // 获取状态是否是"okay"
if(ret < 0)
return -EINVAL;
if (strcmp(str, "okay"))
return -EINVAL;
/* 3、获取compatible属性值并进行匹配 */
ret = of_property_read_string(gpioled.nd, "compatible", &str);
if(ret < 0) {
printk("gpioled: Failed to get compatible property\n");
return -EINVAL;
}
if (strcmp(str, "alientek,led")) {
printk("gpioled: Compatible match failed\n");
return -EINVAL;
}
/* 4、 获取设备树中的gpio属性,得到LED所使用的LED编号 */
gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0); // 获取 LED 所使用的 LED 编号。相当于将 gpioled 节点中的“led-gpio”属性值转换为对应的 LED 编号
if(gpioled.led_gpio < 0) {
printk("can't get led-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpioled.led_gpio);
/* 5.向gpio子系统申请使用GPIO */
ret = gpio_request(gpioled.led_gpio, "LED-GPIO"); // 这里设备树已经改成了led-gpio=<&gpioi 0 GPIO_ACTIVE_LOW>
if (ret) {
printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
return ret;
}
/* 6、设置PI0为输出,并且输出高电平,默认关闭LED灯 */
ret = gpio_direction_output(gpioled.led_gpio, 1);
if(ret < 0) {
printk("can't set gpio!\r\n");
}
/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (gpioled.major) { /* 定义了设备号 */
gpioled.devid = MKDEV(gpioled.major, 0);
ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
if(ret < 0) {
pr_err("cannot register %s char driver [ret=%d]\n", GPIOLED_NAME, GPIOLED_CNT);
goto free_gpio;
}
} else { /* 没有定义设备号 */
ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME); /* 申请设备号 */
if(ret < 0) {
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", GPIOLED_NAME, ret);
goto free_gpio;
}
gpioled.major = MAJOR(gpioled.devid); /* 获取分配号的主设备号 */
gpioled.minor = MINOR(gpioled.devid); /* 获取分配号的次设备号 */
}
printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);
/* 2、初始化cdev */
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev, &gpioled_fops);
/* 3、添加一个cdev */
cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
if(ret < 0)
goto del_unregister;
/* 4、创建类 */
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
if (IS_ERR(gpioled.class)) {
goto del_cdev;
}
/* 5、创建设备 */
gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
if (IS_ERR(gpioled.device)) {
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(gpioled.class);
del_cdev:
cdev_del(&gpioled.cdev);
del_unregister:
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
free_gpio:
gpio_free(gpioled.led_gpio);
return -EIO;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&gpioled.cdev);/* 删除cdev */
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); /* 注销设备号 */
device_destroy(gpioled.class, gpioled.devid);/* 注销设备 */
class_destroy(gpioled.class);/* 注销类 */
gpio_free(gpioled.led_gpio); /* 释放GPIO */
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
一样有一个流程图:
主要是简化了寄存器的配置,使用 Linux 提供的 API 函数,并且很重要的一点就是,使用 gpio 子系统的时候,需要获得设备编号,向子系统申请GPIO。
跟上一节类似,只需要更改gpioled.o的地方,并且编译方式也一样。
KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31 # Linux内核源码路径
CURRENT_PATH := $(shell pwd) # 获取当前所处路径
obj-m := gpioled.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
之后继续编译 ledApp.c 文件:
arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp
将编译后的 ledApp 和 gpioled.ko 文件复制到:
sudo cp gpioled.ko ledApp /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31
跟上一节一样:
depmod // 第一次加载驱动的时候需要运行此命令
modprobe gpioled // 加载驱动
测试打开 LED 灯和关闭:
./ledApp /dev/gpioled 1 // 打开 LED 灯
./ledApp /dev/gpioled 0 // 关闭 LED 灯
# 最后卸载驱动
rmmod gpioled.ko
总结:有了 pinctrl 子系统和 gpio 子系统后,驱动编程变得更为简单,取消了配置寄存器的过程,改为使用 Linux 内核提供的 API 函数。