STM32MP157驱动开发——princtrl和GPIO子系统
0.前言
之前使用了三种方式编写并挂载了LED设备驱动:旧版字符设备驱动、新版字符设备驱动(改进设备号的申请方式)、设备树下的字符设备驱动(从设备树中获取设备属性)。本质上是对GPIO口的高低电平进行操作,但与裸机下的驱动开发没有太大区别,都是对板子的寄存器进行操作。在Linux系统中,对这种简单的GPIO驱动提供了pinctrl和gpio子系统,可以更加简便的进行开发。
一、pinctrl子系统
Linux讲究驱动的分离与分层,也就是要按照面向对象编程的设计思想来设计设备驱动框架。在之前的驱动编写中,需要先获取到GPIO的寄存器地址(包括直接定义和从reg属性中获取),然后操作这些寄存器来设置GPIO的功能、上下拉、速度等。这种配置方式繁琐,且容易产生冲突,因为有一些GPIO可以复用成其他功能,如SPI引脚、定时器引脚等。
pinctrl子系统就是为了简化属性配置、减少功能冲突而设计的,开发人员只需要在设备树文件中设置好某个pin的相关属性即可,初始化工作交给pinctrl功能来完成。pinctrl子系统的源码在linux kernel源码目录的driver/pinctrl中,其主要工作内容包括三个方面:
①获取设备树中的pin信息
②根据获取到的pin信息设置pin的复用功能
③根据获取到的pin信息设置pin的电气属性,如上下拉、速度、驱动能力等
1.pinctrl节点相关内容
stm32mp151.dtsi文件中pinctrl节点内容:
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>;
interrupt-parent = <&exti>;
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0 1>;
pins-are-numbered;
......
};
#address-cells 属性值为 1 和#size-cells 属性值为 1,表示 pinctrl 下的所有子节点的 reg 第一位是起始地址,第二位为长度。
ranges 属性表示 STM32MP1 的 GPIO 相关寄存器地址, STM32MP1 系列芯片最多拥有 176 个通用 GPIO,分为 12 组,分别为:标号为ABCDEFGHIJ、KZ,前者每组有0-15一共16个引脚,KZ两组每组0-7一共8个引脚。寄存器地址PA~PK的在一起,起始为0x50002000,结束地址0x5000C3FF,由pinctrl节点描述。PZ的起始地址0x54004000,终止为0x540043FF,由pinctrl_z节点单独描述。所以ranges属性中,0x50002000表示起始地址,0xa400表示寄存器地址范围。
interrupt-parent 属性值为“&exti”,表示父中断为 exti。
stm32mp15-pinctrl.dtsi文件中的pinctrl节点内容:
&pinctrl {
......
m_can1_pins_a: m-can1-0 {
pins1 {
pinmux = <STM32_PINMUX('H', 13, AF9)>; /* CAN1_TX */
slew-rate = <1>;
drive-push-pull;
bias-disable;
};
pins2 {
pinmux = <STM32_PINMUX('I', 9, AF9)>; /* CAN1_RX */
bias-disable;
};
};
......
pwm1_pins_a: pwm1-0 {
pins {
pinmux = <STM32_PINMUX('E', 9, AF1)>, /* TIM1_CH1 */
<STM32_PINMUX('E', 11, AF1)>, /* TIM1_CH2 */
<STM32_PINMUX('E', 14, AF1)>; /* TIM1_CH4 */
bias-pull-down;
drive-push-pull;
slew-rate = <0>;
};
};
......
};
表示向pinctrl节点中追加内容。每个 pincrtl 节点必须至少包含一个子节点来存放 pincrtl 相关信息,也就是 pinctrl 集,集合里面存放当前外设所用到引脚的相关属性。例如m_can1_pins_a节点分别用pins1和pins2描述了 PH13 和 PI9 两个IO口的相关配置,因为这两个IO的功能和属性不同。而pwm1_pins_a只用了一个pins节点描述了三个属性相同的IO: PE9、PE11、PE14,这三个IO同属于PWM1。关于STM32的PIN信息存放在源码目录下的文档Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.yaml 中。
2.pins子节点的相关属性
在 pins 子节点里面存放了外设的引脚描述信息,包括:
①pinmux属性:用来存放外设所要使用的所有 IO
如上述代码中的 pinmux = <STM32_PINMUX('H', 13, AF9)>;
用 STM32_PINMUX
宏来配置引脚和引脚的复用功能,这个宏定义在include/dt-bindings/pinctrl/stm32-pinfunc.h中:
#define PIN_NO(port, line) (((port) - 'A') * 0x10 + (line))
#define STM32_PINMUX(port, line, mode) (((PIN_NO(port, line)) << 8) | (mode))
port:表示用哪一组 GPIO(例:H 表示为 GPIO 第 H 组,也就是 GPIOH)。
line:表示这组 GPIO 的第几个引脚(例:13 表示为 GPIOH_13,也就是 PH13)。
mode:表示当前引脚要做哪种复用功能(例:AF9 表示为用第 9 个复用功能)
关于可用的复用功能可以查阅《STM32MP157A&D 数据手册》,一个 IO 最大有 16种复用方法:AF0~AF15。
例如:
表格中为PH13的引脚复用功能,如果要将PH13设置为FDCAN1的TX引脚,需要复用成AF9。如果想要复用成UART_4的功能,就使用AF8。
使用AF8时就按照如下配置:
&pinctrl {
......
myuart4_pins_a: myuart4-0 {
pins{
pinmux = <STM32_PINMUX('H', 13, AF8)>;
};
};
};
建议一个 PIN 只作为一个外设使用,否则在驱动编写时要对解析出来的属性值做出筛选后调用。不能将一个引脚同时复用成两个功能。
stm32-pinfunc.h:
/* define PIN modes */
#define GPIO 0x0
#define AF0 0x1
#define AF1 0x2
#define AF2 0x3
#define AF3 0x4
#define AF4 0x5
#define AF5 0x6
#define AF6 0x7
#define AF7 0x8
#define AF8 0x9
#define AF9 0xa
#define AF10 0xb
#define AF11 0xc
#define AF12 0xd
#define AF13 0xe
#define AF14 0xf
#define AF15 0x10
#define ANALOG 0x11
#define RSVD 0x12
此文件中定义了一个 PIN 的所有配置项 AF0-AF15,除了 A0~AF15,还有 GPIO、 ANALOG 这两个,如果一个 PIN 只是作为最基本的 GPIO 功能,那么就用“GPIO”;如果要用作模拟功能,如 ADC 采集引脚,那么就设置为“ANALOG”。
②电气属性配置
电气属性在pinctrl子系统中不是必须的,可以不用配置,但是前面的pinmux属性必须设置。stm32-pinctrl.yaml文件中描述了如何配置引脚的电气属性。
bootlean 类型表示在 pinctrl 子系统只要定义这个电气属性即可,例如:要禁用内部电压,只要在 PIN 的配置集里添加“bias-disable”,这个时候 bias-pull-down和 bias-pull-up 都不能使用,因为已经禁用了内部电压,所以不能配置上下拉。 enum 类型使用方法与 C 语言的一样,比如:要设置 PIN 速度为最低就可以使用“slew-rate=<0>”。
以上述的PH13作为串口TX为例:
&pinctrl {
......
myuart4_pins_a: myuart4-0 {
pins1{
pinmux = <STM32_PINMUX('H', 13, AF8)>;
bias-pull-up;
drive-push-pull;
};
};
};
添加bias-pull-up; drive-push-pull;
表示给 myuart4-0 添加了两个电气属性,分别为内部上拉和推挽输出。
3.在设备树中添加pinctrl节点
步骤:
1.创建对应的节点
2.添加pins属性值
3.在pins节点中添加PIN配置信息
添加完后的节点模板如下:
&pinctrl {
uart4_pins: uart4-0 {
pins1{
pinmux = <STM32_PINMUX('G', 11, AF6)>; /* UART4_TX */
bias-disable;
drive-push-pull;
};
pins2{
//RX配置,此处省略
}
};
};
注:对于 STM32MP1 而言,如果一个 IO 用作 GPIO 功能的时候不需要创建对应的 pinctrl 节点。但对于其他一些芯片,应该规范写法,创建相应的节点,并设置pinmux = <STM32_PINMUX('I', 0, GPIO)>
将其设置为GPIO属性。
二、GPIO子系统
pinctrl 子系统主要是设置 PIN(有的 SOC 叫做 PAD)的复用和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO,那么就要用到 gpio 子系统了。
gpio 子系统用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO。
1.设备树中的gpio信息
以PI0引脚所在的GPIOI为例,在设备树stm32mp151.dtsi中:
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "st,stm32mp157-pinctrl";
......
gpioi: gpio@5000a000 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x800 0x400>;
clocks = <&rcc GPIOI>;
st,bank-name = "GPIOI";
status = "disabled";
};
};
在gpioi子节点中:
gpio-controlle
表示 gpioi 节点是个 GPIO 控制器,每个 GPIO 控制器节点必须包含“gpio-controller”属性;
#gpio-cells
属性和“#address-cells”类似, #gpio-cells 应该为 2,表示一共有两个 cell,第一个 cell 为 GPIO 编号,比如“&gpioi 0”就表示 PI0。第二个 cell 表示 GPIO 极性,如果为0(GPIO_ACTIVE_HIGH)表示高电平有效,如果为 1(GPIO_ACTIVE_LOW)表示低电平有效;
reg
属性设置了 GPIOI 控制器的寄存器基地址偏移为 0X800,因此 GPIOI 寄存器地址为 0X50002000+0X800=0X5000A000;
clocks
属性指定这个 GPIOI 控制器的时钟;
当某个具体的引脚作为 GPIO 使用的时候还需要进一步设置,例如:将 PG1 用作 SD 卡的检测(CD)引脚,那么在SD卡的设备节点中,需要指定该引脚设置:
&sdmmc1 {
pinctrl-names = "default", "opendrain", "sleep";
pinctrl-0 = <&sdmmc1_b4_pins_a &sdmmc1_dir_pins_a>;
pinctrl-1 = <&sdmmc1_b4_od_pins_a &sdmmc1_dir_pins_a>;
pinctrl-2 = <&sdmmc1_b4_sleep_pins_a &sdmmc1_dir_sleep_pins_a>;
cd-gpios = <&gpiog 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; /*指定控制引脚*/
disable-wp;
......
status = "okay";
};
属性“cd-gpios”描述了 SD 卡的 CD 引脚使用的哪个 IO,属性值一共有三个,“&gpiog”表示 CD 引脚所使用的 IO 属于 GPIOG 组,“1”表示 GPIOG 组的第 1 号 IO,通过这两个值 SD 卡驱动程序就知道 CD 引脚使用了 PG1 这个 GPIO。最后一个是“GPIO_ACTIVE_LOW | GPIO_PULL_UP”,这个宏包含在include/linux/gpio/machine.h文件中定义的枚举类型gpio_lookup_flags里。“GPIO_ACTIVE_LOW”表示低电平有效,“GPIO_PULL_UP” 表示上拉,所以该语句表示,PG1引脚为低电平时,SD卡有效。
注:这里也能看出,在STM32MP1中引脚被用作GPIO时不需要添加pinctrl节点。
2.gpio子系统API函数
① 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:申请成功
其他值:申请失败
② gpio_free函数
功能:如果不使用某个 GPIO 了,就需要调用 gpio_free 函数进行释放
原型:
void gpio_free(unsigned gpio)
参数:
gpio:要释放的 gpio 标号
返回值:无
③ gpio_direction_input函数
功能:用于设置某个 GPIO 为输入
原型:
int gpio_direction_input(unsigned gpio)
参数:
gpio:要设置为输入的 GPIO 标号
返回值:
0:设置成功
负值:设置失败
④ gpio_direction_output函数
功能:用于设置某个 GPIO 为输出,并设置输出默认值
原型:
int gpio_direction_output(unsigned gpio, int value)
参数:
gpio:要设置为输出的 GPIO 标号
value: GPIO 默认输出值
返回值:
0:设置成功
负值:设置失败
⑤ gpio_get_value函数
功能:用于获取某个 GPIO 的值(0 或 1),此函数是个宏
原型:
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
参数:
gpio:要获取的 GPIO 标号
返回值:
非负值:要获取的 GPIO 标号
负值:获取失败
⑥ gpio_set_value函数
功能:用于设置某个 GPIO 的值,此函数是个宏
原型:
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
参数:
gpio:要设置的 GPIO 标号
value: 要设置的值
返回值:无
3.设备树中添加gpio节点
步骤:
1.创建led设备节点
2.添加GPIO信息
添加完后的节点模板如下:
led {
compatible = "atk,led";
gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
status = "okay";
};
三、使用pinctrl和gpio子系统驱动LED
1.与gpio相关的OF函数
① of_gpio_named_count函数
功能:用于获取设备树某个属性里面定义了几个 GPIO 信息(空的GPIO信息也会统计)
原型:
int of_gpio_named_count(struct device_node *np, const char *propname)
参数:
np:设备节点
propname:要统计的 GPIO 属性。
返回值:
正值:统计到的 GPIO 数量
负值:失败
② of_gpio_count函数
功能:和 of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的 GPIO 数量
原型:
int of_gpio_count(struct device_node *np)
参数:
np:设备节点
返回值:
正值:统计到的 GPIO 数量
负值:失败
③ of_get_named_gpio函数
功能:获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似<&gpioi 0(GPIO_ACTIVE_LOW | GPIO_PULL_UP)>的属性信息转换为对应的 GPIO 编号,此函数在驱动中使用很频繁!
原型:
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
参数:
np:设备节点
propname:包含要获取 GPIO 信息的属性名
index: GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0。
返回值:
正值:获取到的 GPIO 数量
负值:失败
2.修改设备树文件
在stm32mp157d-atk.dts文件夹的根节点/
下创建LED灯节点,节点名为gpioled,内容如下:
gpioled {
compatible = "alientek,led";
status = "okay";
led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
};
由于LED引脚为普通的GPIO脚,所以不需要设置pinctrl节点。修改完后使用make dtbs
命令即可编译得到设备树文件,用此文件启动Linux开发板内核。
3.驱动程序编写
gpioled.c:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#define GPIOLED_CNT 1 /*设备号个数*/
#define GPIOLED_NAME "gpioled" /*名字*/
#define LEDOFF 0 /*关灯*/
#define LEDON 1 /*开灯*/
/*dtsled设备结构体*/
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编号*/
};
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;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0){
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; /*获取状态值*/
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 retvalue = 0;
const char *str;
/*获取设备树中的属性数据*/
/*1、获取设备节点:gpioled*/
gpioled.nd = of_find_node_by_path("/gpioled");
if(gpioled.nd == NULL){
printk("gpioled node not find!\r\n");
return -EINVAL;
} else {
printk("gpioled node find!\r\n");
}
/*2、获取status属性内容*/
retvalue = of_property_read_string(gpioled.nd, "status", &str);
if(retvalue < 0){
printk("status read failed!\n\r");
return -EINVAL;
}
if(strcmp(str, "okay")){
printk("gpioled not okay!\r\n");
return -EINVAL;
}
/*3、获取compatible属性内容并匹配*/
retvalue = of_property_read_string(gpioled.nd, "compatible", &str);
if(retvalue < 0){
printk("gpioled: Failed to get compatible property!\n\r");
return -EINVAL;
}
if(strcmp(str, "alientek,led")){
printk("gpioled: Compatible match failed!\r\n");
return -EINVAL;
}
/*4、获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号*/
gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
if(gpioled.led_gpio < 0){
printk("can't get led-gpio!\r\n");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpioled.led_gpio);
/*5、向gpio子系统申请使用GPIO*/
retvalue = gpio_request(gpioled.led_gpio, "LED-GPIO");
if(retvalue){
printk(KERN_ERR "gpioled: Failed to request led-gpio!\r\n");
return retvalue;
}
/*6、设置PI0为输出,并且输出高电平,默认关闭LED*/
retvalue = gpio_direction_output(gpioled.led_gpio, 1);
if(retvalue < 0){
printk("Can't set GPIO!\r\n");
}
/*注册字符设备驱动*/
/*创建设备号*/
if(gpioled.major){ /*已经定义过设备号*/
gpioled.devid = MKDEV(gpioled.major, 0);
retvalue = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
if(retvalue < 0){
pr_err("cannot register %s char driver, [retvalue = %d]\r\n", GPIOLED_NAME, GPIOLED_CNT);
goto free_gpio;
}
} else { /*没有定义设备号*/
retvalue = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME); /*申请设备号*/
if(retvalue < 0){
pr_err("%s couldn't alloc_chedev_region, ret=%d\r\n", GPIOLED_NAME, retvalue);
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);
/*初始化cdev*/
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev, &gpioled_fops);
/*添加一个cdev*/
retvalue = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
if(retvalue < 0){
goto del_unregister;
}
/*创建类*/
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
if(IS_ERR(gpioled.class)){
goto del_cdev;
}
/*创建设备*/
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("Amonter");
MODULE_INFO(intree, "Y");
主要修改:
1.添加GPIO相关的头文件<linux/gpio.h>
2.删除了地址映射的相关宏定义、函数声明及实现,包括io_map和unmap的相关设备操作,改用gpio的相关API,如gpio_request和gpio_free
3.在设备结构体gpio_dev中添加int类型的变量led_gpio,用于保存从设备树中读取到的引脚标号
4.整体的驱动只保留了open、read、write、release操作,精简了驱动的内容。另外在init初始化函数中,取消了通过寄存器操作来初始化的步骤,改用OF函数中与GPIO相关的API来读取GPIO引脚标号、申请使用GPIO、设置GPIO电气属性等操作。在write中,也是通过GPIO相关API来设置引脚电平。在exit释放操作中,使用gpio_free函数来进行最后的GPIO释放。
注:在init函数中的 /*3、获取compatible属性内容并匹配*/
部分,用于匹配的字符串需要与设备树中定义的属性值相同,包括空格和缩进等字符,否则会匹配失败。
4.编译及运行
驱动编写完成后即可进行编译及运行测试,同前几节。之前编写的测试程序依然可用。
运行结果: