1. Linux内核Pinctrl子系统与GPIO子系统

在前面章节中,我们一开始是直接在驱动代码中通过寄存器映射来控制外设,从驱动开发者的角度可以说是灾难, 因为每当芯片的寄存器发生了改动,又要重新修改驱动源码,又要重新编译驱动。 后来,我们在这个问题上更进了一步,学会了使用设备树来描述外设的各种信息,而不是将寄存器的这些内容放在驱动代码里。 这样即使设备信息修改了,我们还可以通过设备树的接口函数,去灵活的获取设备的信息。

在驱动中有没有更通用的方法,可以不涉及到具体的寄存器操作的内容呢? 在嵌入式Linux驱动开发体系中,通过Pinctrl子系统与GPIO子系统实现引脚资源的分层标准化管理:Pinctrl子系统专注于引脚功能复用、电气属性配置等底层资源分配; GPIO子系统基于Pinctrl的配置结果,实现通用输入输出引脚的电平控制与方向管理,二者协同配合,完成从硬件引脚配置到上层业务控制的全流程闭环, 彻底屏蔽底层SoC硬件差异,提升驱动的跨平台兼容性与可维护性。

1.1. Pinctrl子系统

1.1.1. Pinctrl子系统概述

Pinctrl(Pin Control,引脚控制)子系统是Linux内核专为SoC引脚资源管理设计的核心基础子系统, 承担引脚功能复用管理与引脚电气属性配置两大核心职责,解决了传统裸机开发中引脚配置分散、重复编码、跨平台适配困难的问题。

现代嵌入式SoC的单颗物理引脚普遍支持多重复用功能,例如同一引脚可配置为UART收发、I2C时钟、PWM输出或通用GPIO, 同时需适配上拉下拉、驱动强度、输出模式等电气参数。Pinctrl子系统通过设备树进行引脚资源的集中描述,由内核统一管理引脚的分配与状态切换, 避免多驱动共享引脚导致的资源冲突,实现引脚资源的规范化、轻量化管理。

核心特性:

  • 分层管理:分离引脚配置与上层业务逻辑,底层由厂商实现Pinctrl控制器驱动,上层驱动仅需调用标准接口即可完成配置;

  • 状态化切换:支持引脚多状态配置(默认工作态、休眠低功耗态、闲置态),可根据系统场景动态切换,适配低功耗需求;

  • 设备树驱动:所有引脚配置通过设备树描述,无需硬编码到驱动代码,适配不同SoC平台仅需修改设备树;

  • 电气属性全覆盖:支持上拉/下拉/禁用电阻、驱动强度、开漏/推挽、信号翻转速率、施密特触发等电气配置;

  • 资源互斥:内核统一管理引脚占用状态,避免多外设同时占用同一引脚引发硬件异常。

pinctrl子系统结构描述:

../_images/gpio00.png

如上图所示,pinctrl核心层是内核抽象出来,向下为SoC pin controler drvier提供底层通信接口的能力, 向上为其他驱动提供了控制pin的能力,比如pin复用、配置引脚的电气特性,同时也为GPIO子系统提供pin操作,而pin控制器驱动层,主要提供了操作pin的方法。

1.1.2. Pinctrl子系统主要数据结构和接口

1.1.2.1. 核心抽象描述符

从面向对象的思想来看,内核将pinctrl驱动抽象为pinctrl_desc对象,具体到soc厂商的pinctrl驱动便是该对象一个实例, 在驱动所有的pin信息以及对于pin的控制接口实例化成pinctrl_desc,并将pinctrl_desc注册到内核中,pinctrl_desc结构体如下:

pinctrl_desc
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct pinctrl_desc {
   const char *name;                      //引脚控制器名称
   const struct pinctrl_pin_desc *pins;   //描述一个pin控制器的引脚,
   unsigned int npins;                    //描述该控制器有多少个引脚
   const struct pinctrl_ops *pctlops;     //引脚操作函数,有描述引脚,获取引脚等,全局控制函数
   const struct pinmux_ops *pmxops;       //引脚复用相关的操作函数
   const struct pinconf_ops *confops;     //引脚配置相关
   struct module *owner;                  //所属内核模块,防止驱动被意外卸载
#ifdef CONFIG_GENERIC_PINCONF
   unsigned int num_custom_params;        //自定义引脚配置参数数量
   const struct pinconf_generic_params *custom_params; //自定义参数集合
   const struct pin_config_item *custom_conf_items;    //配置项映射
#endif
};

1.1.2.2. 核心注册接口

一般控制器驱动匹配设备,调用probe,最后会调用pinctrl_register函数,向内核注册pinctrl,产生pinctrl_dev,该函数如下:

pinctrl_register
1
2
struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
                                 struct device *dev, void *driver_data);

1.1.2.3. 物理引脚最小单元

描述一个引脚的结构体 struct pinctrl_pin_desc:

pinctrl_pin_desc
1
2
3
4
5
struct pinctrl_pin_desc {
   unsigned number;  //引脚唯一硬件编号
   const char *name; //引脚名称
   void *drv_data;   //厂商私有数据,寄存器地址、上下拉配置、复用映射表等
};

1.1.2.4. 引脚分组描述符

很多pin组合在一起,将功能相关的引脚打包管理,使用struct group_desc:

group_desc
1
2
3
4
5
6
struct group_desc {
   const char *name; //引脚组名称,如sys_led_pin
   int *pins;        //指向引脚编号数组,该组包含的所有物理Pin
   int num_pins;     //本组内的引脚数量
   void *data;       //私有数据,本组默认的复用配置、电气配置参数
};

1.1.3. Pinctrl子系统术语定义

以rk3568-lubancat2.dts设备树的leds节点举例:

rk3568-lubancat2.dts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
leds: leds {
   status = "okay";
   compatible = "gpio-leds";

   sys_led: sys-led {
      label = "sys_led";
      linux,default-trigger = "heartbeat";
      default-state = "on";
      gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_LOW>;
      pinctrl-names = "default";
      pinctrl-0 = <&sys_led_pin>;
   };
};

&pinctrl {
   leds {
      sys_led_pin: sys-status-led-pin {
         rockchip,pins = <0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>;
      };
   };
};

其中:

  • Pin(引脚):SoC物理引脚,是子系统管理的最小单元,具备唯一编号与名称,对应以上 gpio0 RK_PC7 ,表示引脚为GPIO0_C7 ;

  • Pin Mux(引脚复用):通过配置内部复用寄存器,将引脚切换为指定外设功能(GPIO、UART、SPI等)对应以上 RK_FUNC_GPIO ,表示复用为GPIO功能 ;

  • Pin Config(引脚配置):配置引脚的电气特性参数,决定信号传输稳定性与硬件适配性,对应以上 pcfg_pull_none ,表示不使用上下拉电阻;

  • Pin Group(引脚组):将功能关联的多个引脚打包为一组,统一配置复用与属性,简化外设引脚管理,对应以上 sys_led_pin 节点,该引脚组只有一个引脚;

  • Pin State(引脚状态):一组预定义的引脚配置集合,对应不同系统场景,如default(默认工作)、sleep(休眠)、idle(闲置),对应以上 pinctrl-names = "default" ,默认工作状态。

1.1.4. 瑞芯微平台设备树标准配置规范

以瑞芯微RK3568平台进行说明,核心源码与设备树文件对应如下:

  • 瑞芯微专属配置宏定义:内核源码/include/dt-bindings/pinctrl/rockchip.h

  • 瑞芯微通用电气属性配置模板:内核源码/arch/arm64/boot/dts/rockchip/rockchip-pinconf.dtsi

  • RK3568引脚控制器与复用定义(其他芯片仅名字前缀不一样):内核源码/arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi

1.1.4.1. 瑞芯微专属配置宏

GPIO控制器组

rockchip.h(位于内核源码/include/dt-bindings/pinctrl/rockchip.h)
1
2
3
4
5
6
#define RK_GPIO0     0
#define RK_GPIO1     1
#define RK_GPIO2     2
#define RK_GPIO3     3
#define RK_GPIO4     4
#define RK_GPIO6     6

瑞芯微芯片把GPIO分成多个独立组:GPIO0、GPIO1、GPIO2…

组内引脚编号

rockchip.h(位于内核源码/include/dt-bindings/pinctrl/rockchip.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define RK_PA0               0
#define RK_PA1               1
#define RK_PA2               2
#define RK_PA3               3
...
#define RK_PB0               8
#define RK_PB1               9
#define RK_PB2               10
#define RK_PB3               11
...
#define RK_PC0               16
#define RK_PC1               17
#define RK_PC2               18
#define RK_PC3               19
#define RK_PC4               20
...
#define RK_PD0               24
#define RK_PD1               25
#define RK_PD2               26
#define RK_PD3               27
...
#define RK_PD7               31

一个GPIO组(比如GPIO0)内部分4个Bank:PA/PB/PC/PD,每个Bank固定8个引脚,所以:

  • PA0~PA7 = 0~7

  • PB0~PB7 = 8~15

  • PC0~PC7 = 16~23

  • PD0~PD7 = 24~31

引脚复用功能

rockchip.h(位于内核源码/include/dt-bindings/pinctrl/rockchip.h)
1
2
3
4
5
6
#define RK_FUNC_GPIO 0
#define RK_FUNC_0    0
#define RK_FUNC_1    1
...
#define RK_FUNC_14   14
#define RK_FUNC_15   15
  • RK_FUNC_GPIO:配置为普通GPIO

  • FUNC1~15:引脚复用为外设功能(如I2C、SPI、UART、PWM等)

1.1.4.2. 瑞芯微通用电气属性配置

rockchip-pinconf.dtsi是瑞芯微官方提供的引脚电气属性通用配置库,专门用来统一封装所有引脚的硬件电气参数, 让开发者写设备树时直接复用,不用重复编写复杂的电气配置,以下截取部分典型的配置进行说明。

rockchip-pinconf.dtsi(位于内核源码/arch/arm64/boot/dts/rockchip/rockchip-pinconf.dtsi)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
&pinctrl { // 绑定芯片的引脚控制器,所有引脚配置都必须挂在这个节点下

   /omit-if-no-ref/                // 表示没被引用就不编译进内核,节省空间
   pcfg_pull_up: pcfg-pull-up {    // 配置模板标签,上拉
      bias-pull-up;                // 开启上拉电阻
   };

   /omit-if-no-ref/
   pcfg_pull_down: pcfg-pull-down { //下拉
      bias-pull-down;               // 开启下拉电阻
   };

   /omit-if-no-ref/
   pcfg_pull_none: pcfg-pull-none { // 无上下拉
      bias-disable;                 // 禁用上下拉电阻
   };

   /omit-if-no-ref/
   pcfg_pull_none_drv_level_0: pcfg-pull-none-drv-level-0 { // 无上下拉,驱动强度等级为0
      bias-disable;                 // 禁用上下拉电阻
      drive-strength = <0>;         // 驱动强度等级0
   };

   /omit-if-no-ref/
   pcfg_pull_none_drv_level_1: pcfg-pull-none-drv-level-1 { // 无上下拉,驱动强度等级为1
      bias-disable;                 // 禁用上下拉电阻
      drive-strength = <1>;         // 驱动强度等级1
   };

   ...

   /omit-if-no-ref/
   pcfg_pull_up_drv_level_0: pcfg-pull-up-drv-level-0 { // 上拉,驱动强度等级为0
      bias-pull-up;                 // 开启上拉电阻
      drive-strength = <0>;         // 驱动强度等级0
   };

   ...

   /omit-if-no-ref/
   pcfg_pull_up_smt: pcfg-pull-up-smt {   //上拉,开启施密特触发
      bias-pull-up;
      input-schmitt-enable;         // 开启施密特触发
   };

   /omit-if-no-ref/
   pcfg_pull_down_smt: pcfg-pull-down-smt {  //下拉,开启施密特触发
      bias-pull-down;
      input-schmitt-enable;
   };

   /omit-if-no-ref/
   pcfg_pull_none_smt: pcfg-pull-none-smt {  //无上下拉,开启施密特触发
      bias-disable;
      input-schmitt-enable;
   };

   ...

   /omit-if-no-ref/
   pcfg_output_high: pcfg-output-high {   // 输出高电平
      output-high;
   };

   /omit-if-no-ref/
   pcfg_output_high_pull_up: pcfg-output-high-pull-up { //上拉输出高电平
      output-high;
      bias-pull-up;
   };

   ...

};

其中电气属性说明如下表:

属性

通俗含义

bias-pull-up

开启上拉电阻(引脚默认高电平)

bias-pull-down

开启下拉电阻(引脚默认低电平)

bias-disable

无上下拉

drive-strength = <0/1/2…>

驱动能力

input-schmitt-enable

开启施密特触发

output-high

默认输出高电平

其中驱动强度等级并不是越大越强,查找 Rockchip_Developer_Guide_Linux_Pinctrl_CN.pdf,瑞芯微官方描述如下:

../_images/subsystem_pinctrl_gpio_0.jpg

提示

ohm是欧姆,根据欧姆定律,同一电压,电阻越小,电流越大,驱动强度也越强。

其中关于施密特触发器瑞芯微官方描述如下:

../_images/subsystem_pinctrl_gpio_1.jpg

1.1.4.3. 瑞芯微引脚控制器与复用定义

rk3568-pinctrl.dtsi是瑞芯微RK3566/RK3568芯片专属引脚控制器核心设备树头文件,是实现引脚管理、复用配置、硬件适配的底层基础, 也是所有外设引脚配置的唯一硬件依据。其他芯片同理,对应其芯片前缀的dtsi,如RK3588s/RK3588对应rk3588s-pinctrl.dtsi。

rk3568-pinctrl.dtsi(位于内核源码/arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
&pinctrl {
   ...

   pmu {
      pmu_pins: pmu-pins {  //PMU电源管理调试引脚
         rockchip,pins =
            /* pmu_debug0 */
            <0 RK_PA5 4 &pcfg_pull_none>,
            /* pmu_debug1 */
            <0 RK_PA6 3 &pcfg_pull_none>,
            /* pmu_debug2 */
            <0 RK_PC4 4 &pcfg_pull_none>,
            /* pmu_debug3 */
            <0 RK_PC5 4 &pcfg_pull_none>,
            /* pmu_debug4 */
            <0 RK_PC6 4 &pcfg_pull_none>,
            /* pmu_debug5 */
            <0 RK_PC7 4 &pcfg_pull_none>;
      };
   };

   pwm0 {
      pwm0m0_pins: pwm0m0-pins { //PWM0脉冲宽度调制引脚
         rockchip,pins =
            /* pwm0_m0 */
            <0 RK_PB7 1 &pcfg_pull_none>;
      };

      pwm0m1_pins: pwm0m1-pins {
         rockchip,pins =
            /* pwm0_m1 */
            <0 RK_PC7 2 &pcfg_pull_none>;
      };
   };

   ...

   uart0 {
      uart0_xfer: uart0-xfer { //串口0引脚
         rockchip,pins =
            /* uart0_rx */
            <0 RK_PC0 3 &pcfg_pull_up>,
            /* uart0_tx */
            <0 RK_PC1 3 &pcfg_pull_up>;
      };

      uart0_ctsn: uart0-ctsn {
         rockchip,pins =
            /* uart0_ctsn */
            <0 RK_PC7 3 &pcfg_pull_none>;
      };

      uart0_rtsn: uart0-rtsn {
         rockchip,pins =
            /* uart0_rtsn */
            <0 RK_PC4 3 &pcfg_pull_none>;
      };
   };

   hdmitx {
      hdmitxm0_cec: hdmitxm0-cec {
         rockchip,pins =
            /* hdmitxm0_cec */
            <4 RK_PD1 1 &pcfg_pull_none>;
      };

      hdmitxm1_cec: hdmitxm1-cec {
         rockchip,pins =
            /* hdmitxm1_cec */
            <0 RK_PC7 1 &pcfg_pull_none>;
      };

   ...
};

rockchip,pins各参数说明如下:

  • rockchip,pins = <GPIO组, 引脚号, 复用功能, 电气配置>

同一时间,一个引脚只能用一种功能

从rk3568-pinctrl.dtsi可以观察到物理引脚GPIO0_PC7有5种功能:

复用编号

用途

来源

0

普通GPIO

leds节点

1

HDMI CEC

hdmitxm1_cec

2

PWM0 输出

pwm0m1_pins

3

UART0 流控

uart0_ctsn

4

PMU 调试

pmu_pins

因此,如果LED用了复用功能0 (普通GPIO功能),就不能同时用PWM/串口/PMU/HDMI-cec功能。

同一时间,同一功能只能用一组引脚

从rk3568-pinctrl.dtsi可以观察到pwm0有两组引脚,分别为pwm0m0_pins、pwm0m1_pins,如果pwm0m0_pins组引脚复用为pwm0功能,那么pwm0m1_pins组引脚就不能复用为pwm0功能。

1
2
3
4
5
6
&pwm0 {
   pinctrl-names = "active";
   pinctrl-0 = <&pwm0m0_pins>;  //只能二选一
   //pinctrl-0 = <&pwm0m1_pins>;
   status = "okay";
};

1.2. GPIO子系统

1.2.1. GPIO子系统概述

GPIO(General Purpose Input/Output,通用输入输出)子系统是基于Pinctrl子系统的上层应用子系统,专注于完成GPIO引脚方向配置、电平读写、中断申请三大核心功能,是嵌入式驱动中使用最频繁的子系统之一。

在pinctrl子系统中把pin脚初始化成了普通GPIO后,就可以使用GPIO子系统的接口去操作IO口的电平、中断等。 驱动开发者在设备树中添加gpio相关信息,然后就可以在驱动程序中使用gpio子系统提供的API函数来操作GPIO,极大的方便了驱动开发者使用GPIO。

GPIO子系统结构描述如下图:

总线结构1

GPIO的核心是gpiolib框架,向上提供一些gpio接口给其他驱动调用,向下提供用于gpio资源注册函数。 上层的其他驱动,比如LED的驱动,可以通过函数向gpiolib申请gpio,然后设置和使用gpio。 下层的控制器驱动(一般是SOC厂商编写),启动时会注册gpio资源到gpiolib,比如引脚数量,操作函数等等。

1.2.2. GPIO子系统主要数据结构和接口

GPIO子系统主要的数据结构有gpio_device、gpio_chip、gpio_desc等,主要是为了描述gpio控制器,有引脚信息,中断信息,以及相关操作函数等。

1.2.2.1. 核心控制器

gpio_device是内核为每个GPIO控制器创建的全局唯一实例,承上启下连接通用层与厂商驱动层。

gpio_device(内核源码/drivers/gpio/gpiolib.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct gpio_device {
   int         id;            // GPIO控制器唯一编号,如gpio0、gpio1
   struct device     dev;     // 内核标准设备对象
   struct cdev    chrdev;     // 字符设备驱动
   struct device     *mockdev;
   struct module     *owner;  // 所属内核模块
   struct gpio_chip  *chip;   // 绑定厂商实现的gpio_chip
   struct gpio_desc  *descs;  // 指向GPIO引脚描述数组
   int         base;          // gpio在内核中的编号,申请gpio口时就是根据这个编号来查找
   u16         ngpio;         // 该控制器管理的GPIO引脚总数
   const char     *label;     // 标签
   void        *data;         // 厂商私有数据
   struct list_head   list;   // 内核链表,挂载所有gpio_device

#ifdef CONFIG_PINCTRL   /* 与Pinctrl子系统联动 */
   /*
   * If CONFIG_PINCTRL is enabled, then gpio controllers can optionally
   * describe the actual pin range which they serve in an SoC. This
   * information would be used by pinctrl subsystem to configure
   * corresponding pins for gpio usage.
   */
   struct list_head pin_ranges;  // 描述GPIO控制器对应的物理引脚范围,自动关联Pinctrl配置
#endif
};

1.2.2.2. 硬件操作抽象核心

gpio_chip是GPIO子系统的抽象接口模板,对应Pinctrl的pinctrl_desc。

gpio_chip(内核源码/include/linux/gpio/driver.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct gpio_chip {
   const char                *label;    //GPIO端口的名字,标签
   struct device             *dev;
   struct module             *owner;

   /* GPIO硬件操作函数集(厂商必须实现) */
   int         (*request)(struct gpio_chip *chip,  // 申请GPIO
                  unsigned offset);
   void        (*free)(struct gpio_chip *chip,     // 释放GPIO
                  unsigned offset);
   int         (*direction_input)(struct gpio_chip *chip,   // 配置为输入方向
                  unsigned offset);
   int         (*get)(struct gpio_chip *chip,      // 读取引脚电平
                  unsigned offset);
   int         (*direction_output)(struct gpio_chip *chip,  // 配置为输出方向 + 设置初始电平
                  unsigned offset, int value);
   int         (*set_debounce)(struct gpio_chip *chip,      // 配置消抖
                  unsigned offset, unsigned debounce);
   void        (*set)(struct gpio_chip *chip,      // 设置引脚电平
                  unsigned offset, int value);
   int         (*to_irq)(struct gpio_chip *chip,   // GPIO转中断号
                  unsigned offset);
   /*.....*/
   int         base;            // gpio在内核中的编号,申请gpio口时就是根据这个编号来查找
   u16         ngpio;           // 该控制器的GPIO数目
   const char  *const *names;   // 引脚名称数组
   unsigned    can_sleep;       // =1:操作GPIO会休眠,禁止在原子上下文使用,如I2C扩展GPIO;=0:原子操作,可在中断/软中断上下文使用,如SoC内置GPIO
   /*......*/
};

1.2.2.3. 单个引脚最小单元

gpio_desc是内核管理单个GPIO引脚的状态描述符,是GPIO子系统最小管理单元。

gpio_desc(内核源码/drivers/gpio/gpiolib.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct gpio_desc {
   struct gpio_device        *gdev;      // 归属的GPIO控制器
   unsigned long             flags;         // 引脚状态标志位
   /* 标志位宏定义 */
   #define FLAG_REQUESTED      0   // 位0:GPIO是否已被申请占用
   #define FLAG_IS_OUT         1   // 位1:GPIO方向(1=输出,0=输入)
   #define FLAG_EXPORT         2   // 位2:GPIO是否被sysfs导出
   #define FLAG_SYSFS          3   // 位3:通过/sys/class/gpio控制
   #define FLAG_ACTIVE_LOW     6   // 位6:低电平有效
   #define FLAG_OPEN_DRAIN     7   // 位7:开漏输出模式,I2C专用
   #define FLAG_OPEN_SOURCE    8   // 位8:开源输出模式,极少用
   #define FLAG_USED_AS_IRQ    9   // 位9:该GPIO已配置为中断引脚
   #define FLAG_IS_HOGGED      11  // 位11:GPIO被内核预占用
   #define FLAG_TRANSITORY     12  // 位12:休眠/复位后电平会丢失,仅部分GPIO

   const char     *label;   // 使用者名称
   const char     *name;    // 引脚名称
};

一个gpio_device用来表示一个GPIO控制器,GPIO Controller中每一个引脚用gpio_desc表示,引脚的相关操作函数和中断相关在gpio_chip中。

1.2.3. GPIO子系统常用API函数

GPIO子系统有两套接口,一套是描述符(descriptor-based)的,相关API函数都是以 gpiod_ 为前缀,可能前面还有 devm_ ,表示设备资源管理,一种自动释放资源的机制。 另外一套是旧的以 gpio_ 为前缀。

以下对两套API函数分别进行讲解。

1.2.3.1. 新API函数

内核推荐使用devres托管式GPIO描述符API,所有接口自动管理资源生命周期,杜绝资源泄漏,以下为驱动开发常用新API函数。

1.2.3.1.1. devm_gpiod_get函数

devm_gpiod_get函数用于从设备树中获取指定名称的GPIO描述符,基础获取接口。

函数原型:

1
struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags);

参数说明:

  • dev:设备结构体指针;

  • con_id:设备树中gpios属性的后缀名称,单GPIO设为NULL;

  • flags:GPIO初始化配置,如GPIOD_OUT_HIGH(输出高)、GPIOD_IN(输入)、GPIOD_OUT_LOW(输出低)。

返回值:成功获取返回合法的struct gpio_desc类型GPIO描述符指针;获取失败返回ERR_PTR类型错误指针。

1.2.3.1.2. devm_gpiod_get_index函数

devm_gpiod_get_index函数用于获取设备树中多GPIO配置的指定索引GPIO,适配一个外设多个GPIO的场景。

函数原型:

1
struct gpio_desc *devm_gpiod_get_index(struct device *dev, const char *con_id, unsigned int idx, enum gpiod_flags flags);

参数说明:

  • dev:设备结构体指针;

  • con_id:设备树中gpios属性的连接后缀名称,单组多GPIO场景无自定义后缀时填NULL,和devm_gpiod_get参数逻辑保持一致;

  • idx:GPIO索引编号,从0开始递增,对应设备树gpios属性中多个GPIO的排列顺序,用于精准定位目标GPIO;

  • flags:GPIO初始化配置,如GPIOD_OUT_HIGH(输出高)、GPIOD_IN(输入)、GPIOD_OUT_LOW(输出低)。

返回值:成功获取返回合法的struct gpio_desc类型GPIO描述符指针;获取失败返回ERR_PTR类型错误指针。

1.2.3.1.3. gpiod_direction_input函数

gpiod_direction_input函数用于将GPIO配置为输入方向,用于读取外部电平状态。

函数原型:

1
int gpiod_direction_input(struct gpio_desc *desc);

参数说明:

  • desc为GPIO描述符指针。

返回值:成功返回0;失败返回负错误码。

1.2.3.1.4. gpiod_direction_output函数

gpiod_direction_output函数用于将GPIO配置为输出方向,并设置初始电平。

函数原型:

1
int gpiod_direction_output(struct gpio_desc *desc, int value);

参数说明:

  • desc:GPIO描述符指针。

  • value:初始输出电平值,1表示输出有效电平,0表示输出无效电平,内核自动适配设备树定义的有效电平逻辑,无需关注硬件实际高低电平。

返回值:成功返回0;失败返回负错误码。

1.2.3.1.5. gpiod_get_value函数

gpiod_get_value函数用于读取GPIO引脚当前的有效电平状态,仅适用于已配置为输入方向的GPIO。

函数原型:

1
int gpiod_get_value(const struct gpio_desc *desc);

参数说明:

  • desc为已配置为输入模式的合法GPIO描述符指针,不允许传入输出模式的GPIO描述符。

返回值:返回1表示引脚处于有效电平状态,返回0表示处于无效电平状态,无失败错误码。

1.2.3.1.6. gpiod_set_value函数

gpiod_set_value函数用于动态设置输出型GPIO的电平状态,仅适用于已配置为输出方向的GPIO,内核自动适配有效电平逻辑,是控制GPIO输出的核心接口。

函数原型:

1
void gpiod_set_value(struct gpio_desc *desc, int value);

参数说明:

  • desc:已配置为输出模式的合法GPIO描述符指针;

  • value:1表示设置为有效电平,0表示设置为无效电平。

1.2.3.2. 旧API函数

新驱动已经不使用gpio_request、gpio_set_value等老旧全局编号API,这些老旧的API可能在某些驱动中仍会看到,以下同样进行介绍。

1.2.3.2.1. of_get_named_gpio函数

GPIO子系统大多数API函数会用到GPIO编号,GPIO编号可以通过of_get_named_gpio函数从设备树中获取。

函数原型:

1
 static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index)

参数说明:

  • np:指定设备节点。

  • propname:GPIO属性名,与设备树中定义的属性名对应。

  • index:引脚索引值,在设备树中一条引脚属性可以包含多个引脚,该参数用于指定获取那个引脚。

返回值:成功获取的GPIO编号,失败返回负数。

1.2.3.2.2. gpio_request函数
1
static inline int gpio_request(unsigned gpio, const char *label);

参数说明:

  • gpio:要申请的GPIO编号,该值是函数of_get_named_gpio的返回值。

  • label:引脚名字,相当于为申请得到的引脚取了个别名。

返回值:成功返回0,失败返回负数。

1.2.3.2.3. gpio_free函数

gpio_free函数与gpio_request是一对相反的函数,一个申请,一个释放。一个GPIO只能被申请一次, 当不再使用某一个引脚时记得将其释放掉。

函数原型:

1
static inline void gpio_free(unsigned gpio);

参数说明:

  • gpio:要释放的GPIO编号。

1.2.3.2.4. gpio_direction_output函数

gpio_direction_output函数用于将引脚设置为输出模式。

函数原型:

1
static inline int gpio_direction_output(unsigned gpio , int value);

参数说明:

  • gpio:要设置的GPIO的编号。

  • value:输出值,1表示有效电平。0表示无效电平。

返回值:成功返回0,失败返回负数。

1.2.3.2.5. gpio_direction_input函数

gpio_direction_input函数用于将引脚设置为输入模式。

函数原型:

1
static inline int gpio_direction_input(unsigned gpio)

参数说明:

  • gpio:要设置的GPIO的编号。

返回值:成功返回0,失败返回负数。

1.2.3.2.6. gpio_get_value函数

gpio_get_value函数用于获取引脚的当前状态,无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。

1
static inline int gpio_get_value(unsigned gpio);

参数说明:

  • gpio:要获取的GPIO的编号。

返回值:成功获取得到的引脚状态,失败返回负数。

1.2.3.2.7. gpio_set_value函数

gpio_set_value函数只用于那些设置为输出模式的GPIO。

1
static inline int gpio_direction_output(unsigned gpio, int value);

参数说明:

  • gpio:设置的GPIO的编号。

  • value:设置的输出值,为1输出有效电平,为0输出无效电平。

返回值:成功返回0,失败返回负数。

提示

一些详细使用说明,请参考内核源码/Documentation/driver-api/gpio/board.rst 、内核源码/Documentation/driver-api/gpio/consumer.rst等文档。

1.3. GPIO子系统和Pinctrl子系统之间的耦合关系

Pinctrl子系统负责引脚底层资源分配,解决“引脚用来做什么”的问题;GPIO子系统负责通用引脚业务控制,解决“GPIO怎么用”的问题。 二者分工明确、协同工作,是嵌入式Linux底层引脚管理的标准方案。 驱动开发需遵循先Pinctrl配置,后GPIO操作的流程,采用设备树描述+标准API的模式,保证驱动的规范化、跨平台性与稳定性。

1.4. Pinctrl子系统和GPIO子系统控制引脚电平实验

本实验在内核定时器实验基础上进行修改,基于Pinctrl子系统完成硬件引脚的复用配置与电气属性设置,依托GPIO子系统实现LED引脚的电平控制, 采用设备树描述硬件+标准API驱动的规范化开发模式,彻底摒弃底层寄存器直接操作。

本实验的示例代码目录为: linux_driver/16_gpio_subsystem

1.4.1. 设备树插件详解

设备树插件完整代码如下:

设备树插件(位于linux_driver/16_gpio_subsystem/lubancat-led-overlay.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>

/ {
   fragment@0 {
      target-path = "/";

      __overlay__ {
            led_test: led_test{
               compatible = "fire,led_test";
               led-gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>;
               pinctrl-names = "default";
               pinctrl-0 = <&led_test_pin>;
            };
      };
   };

   fragment@1 {
      target = <&pinctrl>;

      __overlay__ {
            led_test {
               led_test_pin: led_test_pin {
                  rockchip,pins = <0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>;
               };
            };
      };
   };

   fragment@2 {
      target = <&leds>;

      __overlay__ {
            status = "disabled";
      };
   };
};

设备树插件说明如下:

  1. fragment@0:LED设备节点定义

  • compatible = “fire,led_test”:驱动匹配属性,与驱动代码中of_device_id表对应,实现设备树与驱动的匹配;

  • led-gpios:GPIO属性定义,指定LED对应引脚为GPIO0_C7,高电平有效,GPIO子系统通过该属性获取引脚信息;

  • pinctrl-names & pinctrl-0:绑定Pinctrl配置,指定默认工作状态下使用led_test_pin引脚配置。

  1. fragment@1:Pinctrl引脚配置

  • 目标指向pinctrl控制器,配置引脚组led_test_pin;

  • rockchip,pins:RK平台专属Pinctrl配置,依次指定GPIO组0、引脚PC7、复用为GPIO功能、无上下拉电阻,完成引脚底层初始化。

  1. fragment@2:关闭系统默认LED驱动

  • 禁用内核默认的leds节点,避免系统自带心跳灯驱动占用目标GPIO引脚,确保本实验驱动独占硬件资源。

  1. 如果需要修改为其他引脚,如GPIO4_B5,修改以上高亮部分:

1
2
3
4
5
#修改为GPIO4_B5

led-gpios = <&gpio4 RK_PB5 GPIO_ACTIVE_HIGH>;

rockchip,pins = <4 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;

1.4.2. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/16_gpio_subsystem/gpio_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* 定义 LED 字符设备结构体 */
struct led_chrdev {
   /* 字符设备结构体 */
   struct cdev dev;
   /* 自旋锁 */
   spinlock_t spinlock;
   /* 定时器 */
   struct timer_list timer;
   /* LED 状态 */
   int led_state;
   /* 定时器间隔时间 */
   unsigned long timer_interval;
   /* LED 的 GPIO 描述符 */
   struct gpio_desc *led_gpio;
};

/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;

自定义LED字符设备结构体,整合字符设备、自旋锁、定时器、GPIO描述符、状态参数, 实现硬件资源与业务数据的统一管理,其中struct gpio_desc *led_gpio是GPIO子系统核心描述符,替代传统寄存器操作。

驱动初始化

驱动初始化(位于linux_driver/16_gpio_subsystem/gpio_subsystem.c)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
static int pdrv_led_probe(struct platform_device *pdev)
{
   /* 定义返回值变量 */
   int ret = 0;
   /* 定义主设备号变量 */
   int major;
   /* 定义次设备号变量 */
   int minor;

   /* 打印平台驱动探测信息 */
   printk("led platform driver probe\n");

   /* 第一步:提取平台设备提供的资源 */
   /* 使用 devm_kzalloc 函数为 led_chrdev 结构体分配内存并清零 */
   led_cdev = devm_kzalloc(&pdev->dev, sizeof(struct led_chrdev), GFP_KERNEL);
   if (!led_cdev)
      return -ENOMEM;

   if (pdev->dev.of_node) {
      /* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
      led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
      if (IS_ERR(led_cdev->led_gpio)) {
            ret = PTR_ERR(led_cdev->led_gpio);
            printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
            return ret;
      }
   } else {
      printk("Platform device matching is not supported in this driver\n");
      return -ENOMEM;
   }

   /* 第二步:初始化注册字符设备 */
   /* 分配设备号 */
   ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
   if (ret < 0) {
      /* 打印设备号分配失败信息 */
      printk("fail to alloc devno\n");
      /* 跳转到错误处理标签 */
      goto ioremap_err;
   }
   /* 获取主设备号 */
   major = MAJOR(devno);
   /* 获取次设备号 */
   minor = MINOR(devno);
   /* 打印主设备号和次设备号 */
   printk("major=%d, minor=%d\n", major, minor);

   /* 初始化字符设备 */
   cdev_init(&led_cdev->dev, &pdrv_led_fops);
   led_cdev->dev.owner = THIS_MODULE;

   /* 添加字符设备 */
   ret = cdev_add(&led_cdev->dev, devno, DEV_CNT);
   if (ret < 0) {
      /* 打印字符设备添加失败信息 */
      printk("fail to add cdev\n");
      /* 跳转到错误处理标签 */
      goto add_err;
   }

   /* 创建设备类 */
   class = class_create(THIS_MODULE, DEV_NAME);
   if (IS_ERR(class)) {
      /* 打印设备类创建失败信息 */
      printk("fail to create class\n");
      ret = PTR_ERR(class);
      /* 跳转到错误处理标签 */
      goto class_err;
   }

   /* 创建设备节点 */
   device = device_create(class, NULL, devno, NULL, DEV_NAME);
   if (IS_ERR(device)) {
      /* 打印设备节点创建失败信息 */
      printk("fail to create device\n");
      ret = PTR_ERR(device);
      /* 跳转到错误处理标签 */
      goto device_err;
   }

   /* 保存驱动数据 */
   platform_set_drvdata(pdev, led_cdev);

   /* 初始化自旋锁 */
   spin_lock_init(&led_cdev->spinlock);

   /* 初始化定时器 */
   timer_setup(&led_cdev->timer, led_timer_callback, 0);
   /* timer_interval单位为jiffies
   * 当HZ=100时,此处定时器实际间隔时间为 HZ/2 个 jiffies,即 HZ/2 * (1/HZ)= 0.5秒
   * 当HZ=300时,此处定时器实际间隔时间为 HZ/2 个 jiffies,即 HZ/2 * (1/HZ)= 1秒
   */
   led_cdev->timer_interval = HZ / 2;
   /* 默认关闭 LED */
   led_cdev->led_state = 0;

   return 0;

device_err:
   /* 销毁设备类 */
   class_destroy(class);

class_err:
   /* 删除字符设备 */
   cdev_del(&led_cdev->dev);

add_err:
   /* 释放设备号 */
   unregister_chrdev_region(devno, DEV_CNT);

ioremap_err:
   return ret;
}

第21行,仅需调用devm_gpiod_get即可完成GPIO描述符获取并设置引脚为输出模式+输出高电平,开发者无需编写任何寄存器操作。

需注意,devm_gpiod_get函数第二个参数传入“led”,而非设备树中写的“led-gpios”, 是Linux GPIO子系统内置的标准属性匹配规则,也是内核驱动开发的通用规范, 具体原理如下:

Linux内核GPIO子系统规定,设备树中描述GPIO资源的标准属性后缀为-gpios(复数形式,兼容单路/多路GPIO场景), gpiod系列获取GPIO的API会自动拼接“-gpios”后缀完成属性匹配,因此调用时只需传入属性前缀即可,无需写完整属性名。

对应到本实验,设备树中属性为led-gpios,前缀是led,因此函数第二个参数填led,内核会自动拼接为led-gpios去匹配设备树节点; 如果强行传入led-gpios,内核会拼接为led-gpios-gpios,导致设备树属性匹配失败,GPIO资源获取报错。

定时器回调函数

定时器回调函数(位于linux_driver/16_gpio_subsystem/gpio_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void led_timer_callback(struct timer_list *t)
{
   struct led_chrdev *led_cdev = from_timer(led_cdev, t, timer);

   /* 用于保存中断状态信息 */
   unsigned long flags;

   /* 获取自旋锁并保存中断状态 */
   spin_lock_irqsave(&led_cdev->spinlock, flags);

   /* 判断 LED 状态 */
   if (led_cdev->led_state) {
      /* 翻转 LED 电平 */
      gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
   }

   /* 重新启动定时器 */
   mod_timer(&led_cdev->timer, jiffies + led_cdev->timer_interval);

   /* 释放自旋锁并恢复中断状态 */
   spin_unlock_irqrestore(&led_cdev->spinlock, flags);
}

第14行:!gpiod_get_value(led_cdev->led_gpio)对读取到的当前电平值做取反操作,再使用gpiod_set_value设置引脚电平实现电平状态翻转。

用户态控制接口

用户态控制接口(位于linux_driver/16_gpio_subsystem/gpio_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
static ssize_t pdrv_led_write(struct file *filp, const char __user *buf,
                              size_t count, loff_t *ppos)
{
   /* 用于存储用户输入 */
   char input[32] = {0};
   /* 用于存储用户输入的时间参数 */
   unsigned long interval = 0;
   unsigned long val = 0;
   /* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
   struct led_chrdev *led_cdev = filp->private_data;
   /* 用于保存中断状态信息 */
   unsigned long flags;

   /* 打印设备写操作信息 */
   printk("pdrv_led write \r\n");

   /* 从用户空间读取输入 */
   if (copy_from_user(input, buf, min(count, sizeof(input) - 1))) {
      return -EFAULT;
   }

   /* 解析用户输入 */
   if (sscanf(input, "%lu", &val) >= 1) {

   printk("val = %ld \n", val);

   /* 获取自旋锁并保存中断状态 */
   spin_lock_irqsave(&led_cdev->spinlock, flags);

   switch (val) {
      /* 关闭 LED */
      case 0:
            led_cdev->led_state = 0;
            /* 停止定时器 */
            del_timer_sync(&led_cdev->timer);
            /* 设置 LED 为高电平 */
            gpiod_set_value(led_cdev->led_gpio, 1);
            break;
      /* 开启 LED 闪烁 */
      case 1:
            led_cdev->led_state = 1;
            /* 启动定时器 */
            mod_timer(&led_cdev->timer, jiffies + led_cdev->timer_interval);
            break;
      /* 修改定时器时间 */
      case 2:
            if (sscanf(input, "%lu %lu", &val, &interval) == 2 && interval > 0) {
               /* 转换为 jiffies */
               led_cdev->timer_interval = msecs_to_jiffies(interval);
               printk("Timer interval set to %lu ms\n", interval);
            } else {
               printk(KERN_ERR "Invalid interval value\n");
            }
            break;

      default:
            /* 释放自旋锁并恢复中断状态 */
            spin_unlock_irqrestore(&led_cdev->spinlock, flags);
            return -EINVAL;
      }
   }

   /* 释放自旋锁并恢复中断状态 */
   spin_unlock_irqrestore(&led_cdev->spinlock, flags);

   return count;
}

第37行:当case 0时,调用gpiod_set_value将引脚电平设置为高电平,关闭LED灯。

1.4.3. 编译设备树和驱动

此部分和定时器实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块gpio_subsystem.ko。

1.4.4. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

1.4.4.1. 实验操作

设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。

使用以下命令加载驱动:

1
2
3
4
5
6
7
#加载驱动
sudo insmod gpio_subsystem.ko

#信息输出如下
[ 3961.825580] led platform driver init
[ 3961.826266] led platform driver probe
[ 3961.826407] major=236, minor=0

通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#停止定时器,关闭LED
sudo sh -c "echo 0 > /dev/gpio_subsystem"

#开启定时器,LED闪烁
sudo sh -c "echo 1 > /dev/gpio_subsystem"

#定时器间隔为200ms
sudo sh -c "echo 2 200 > /dev/gpio_subsystem"

#定时器间隔为500ms
sudo sh -c "echo 2 500 > /dev/gpio_subsystem"

#修改定时器间隔为500ms信息打印如下
[ 4076.422378] pdrv_led open
[ 4076.422546] pdrv_led write
[ 4076.422689] val = 2
[ 4076.422720] Timer interval set to 500 ms
[ 4076.422751] pdrv_led release

向/dev/gpio_subsystem写入1后,led按照设定频率闪烁,内核打印对应调试信息,功能正常则说明Pinctrl与GPIO子系统配置生效。

1.4.5. 实验注意事项

  1. Pinctrl配置优先级:设备树Pinctrl配置必须优先于GPIO操作,确保引脚已复用为GPIO功能,否则GPIO API调用失败;

  2. GPIO描述符规范:使用devm_gpiod_get托管式API,禁止混用旧版gpio_request接口,避免资源泄漏;

  3. 上下文约束:定时器回调与GPIO操作运行在原子上下文,禁止调用睡眠函数,共享资源必须用自旋锁保护;

  4. 硬件资源互斥:必须通过设备树禁用系统默认LED驱动,避免多驱动抢占同一GPIO引脚。

1.5. Pinctrl子系统和GPIO子系统读取引脚电平实验

本实验在Pinctrl子系统和GPIO子系统控制引脚电平实验基础上进行修改,基于Pinctrl子系统完成LED输出引脚、按键输入引脚的复用配置与电气属性设置, 依托GPIO子系统实现按键引脚电平轮询读取与LED电平控制,结合内核定时器完成固定频率按键状态扫描, 通过判断按键电平变化实现「按键按下翻转LED状态」的联动功能,采用设备树描述硬件+标准GPIO描述符API开发模式,彻底摒弃底层寄存器直接操作。

本实验的示例代码目录为: linux_driver/17_gpio_subsystem_button

1.5.1. 设备树插件详解

设备树插件完整代码如下:

设备树插件(位于linux_driver/17_gpio_subsystem_button/lubancat-led-overlay.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>

/ {
   fragment@0 {
      target-path = "/";

      __overlay__ {
            led_test: led_test{
               compatible = "fire,led_test";
               led-gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>;
               button-gpios= <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
               pinctrl-names = "default";
               pinctrl-0 = <&led_button_pin>;
            };
      };
   };

   fragment@1 {
      target = <&pinctrl>;

      __overlay__ {
            led_button_test {
               led_button_pin: led_button_pin {
                  rockchip,pins =
                        <0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>,
                        <1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
               };
            };
      };
   };

   fragment@2 {
      target = <&leds>;

      __overlay__ {
            status = "disabled";
      };
   };
};

1. 按键引脚选定 :在Pinctrl子系统和GPIO子系统控制引脚电平实验基础上添加了按键引脚GPIO1_B2,该引脚可以查看板卡对应的 《快速使用手册》 的 “40pin引脚对照图”章节自行选择空闲的引脚进行修改。

  1. 按键引脚上拉 :其中按键引脚使用pcfg_pull_up配置内置上拉电阻,让按键引脚电平状态默认处于高电平,避免引脚悬空引起电平紊乱。

1.5.2. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/17_gpio_subsystem_button/gpio_subsystem_button.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义 LED 字符设备结构体 */
struct led_chrdev {
   /* 字符设备结构体 */
   struct cdev dev;
   /* 自旋锁 */
   spinlock_t spinlock;
   /* 定时器 */
   struct timer_list timer;
   /* LED 状态 */
   int led_state;
   /* 定时器间隔时间 */
   unsigned long timer_interval;
   /* LED 的 GPIO 描述符 */
   struct gpio_desc *led_gpio;
   /* 按钮的 GPIO 描述符 */
   struct gpio_desc *button_gpio;
   /* 按键上一次的状态 */
   int last_button_state;
};

/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;

自定义LED+按键复合字符设备结构体,整合字符设备、自旋锁、定时器、双路GPIO描述符、按键状态记忆参数,实现硬件资源与业务数据的统一管理, 新增按键GPIO描述符与状态记忆变量,解决按键电平变化判断需求。

驱动初始化

驱动初始化(位于linux_driver/17_gpio_subsystem_button/gpio_subsystem_button.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static int pdrv_led_probe(struct platform_device *pdev)
{
   // 内存分配(省略重复代码)

   /* 第一步:提取平台设备提供的资源 */
   if (pdev->dev.of_node) {
      /* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
      led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
      if (IS_ERR(led_cdev->led_gpio)) {
            ret = PTR_ERR(led_cdev->led_gpio);
            printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
            return ret;
      }

      /* 获取按钮的 GPIO 描述符,GPIOD_IN 表示设置为输入模式 */
      led_cdev->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
      if (IS_ERR(led_cdev->button_gpio)) {
            ret = PTR_ERR(led_cdev->button_gpio);
            printk(KERN_ERR "Failed to get button GPIO: %d\n", ret);
            return ret;
      }

   } else {
      printk("Platform device matching is not supported in this driver\n");
      return -ENOMEM;
   }

   //字符设备注册(省略重复代码)

   /* 初始化自旋锁 */
   spin_lock_init(&led_cdev->spinlock);

   /* 初始化定时器 */
   timer_setup(&led_cdev->timer, led_timer_callback, 0);
   /* timer_interval单位为jiffies
   * 当HZ=100时,此处定时器实际间隔时间为 HZ/2 个 jiffies,即 HZ/2 * (1/HZ)= 0.5秒
   * 当HZ=300时,此处定时器实际间隔时间为 HZ/2 个 jiffies,即 HZ/2 * (1/HZ)= 1秒
   */
   led_cdev->timer_interval = HZ / 2;
   /* 默认关闭 LED */
   led_cdev->led_state = 0;

   /* 记录按键初始状态 */
   led_cdev->last_button_state = gpiod_get_value(led_cdev->button_gpio);

   /* 打印按键初始状态 */
   printk("last_button_state = %d\n", led_cdev->last_button_state);

   // 错误处理(省略重复代码)
}
  • 第16行:申请按键GPIO时,第三个参数使用GPIOD_IN,代表将该引脚配置为输入模式,禁止输出电平,专门用于读取外部硬件电平,区别于LED的GPIOD_OUT_HIGH(输出模式+默认高电平)。

定时器回调函数

驱动初始化(位于linux_driver/17_gpio_subsystem_button/gpio_subsystem_button.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static void led_timer_callback(struct timer_list *t)
{
   struct led_chrdev *led_cdev = from_timer(led_cdev, t, timer);

   /* 用于保存中断状态信息 */
   unsigned long flags;

   // 标记是否发生按键状态变化
   int btn_changed = 0;

   // 记录按键动作 0=按下 1=松开
   int btn_action = 0;

   /* 获取自旋锁并保存中断状态 */
   spin_lock_irqsave(&led_cdev->spinlock, flags);

   /* 读取当前按键的电平状态 */
   int current_button_state = gpiod_get_value(led_cdev->button_gpio);

   /* 检查当前按键状态是否与上一次记录的状态不同 */
   if (current_button_state != led_cdev->last_button_state) {
      /* 标记状态改变 */
      btn_changed = 1;
      /* 记录当前状态 */
      btn_action = current_button_state;

      /* 当 current_button_state 为 0 时,表示检测到按键被按下 */
      if (current_button_state == 0) {
            /* 翻转 LED 电平 */
            gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
      }

      /* 更新按键上一次的状态,以便在下一次定时器回调时作为上一次的状态进行比较 */
      led_cdev->last_button_state = current_button_state;
   }

   /* 重新启动定时器 */
   mod_timer(&led_cdev->timer, jiffies + led_cdev->timer_interval);

   /* 释放自旋锁并恢复中断状态 */
   spin_unlock_irqrestore(&led_cdev->spinlock, flags);

   /* 打印按键是否按下或松开 */
   if (btn_changed) {
      if (btn_action == 0) {
            printk(KERN_INFO "按键已按下\n");
      } else {
            printk(KERN_INFO "按键已松开\n");
      }
   }

}

固定频率轮询按键输入电平,核心逻辑: 读取当前按键电平->对比上一次状态->判断是否按下/松开->按下则翻转LED电平->更新状态记忆->重启定时器实现持续轮询。

1.5.3. 编译设备树和驱动

此部分和定时器实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb和驱动模块gpio_subsystem_button.ko。

1.5.4. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

1.5.4.1. 实验操作

设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。

使用以下命令加载驱动:

1
2
3
4
5
6
7
8
#加载驱动
sudo insmod gpio_subsystem_button.ko

#信息输出如下
[   20.650983] led platform driver init
[   20.651665] led platform driver probe
[   20.651803] major=236, minor=0
[   20.652473] last_button_state = 1

通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:

1
2
3
4
5
6
7
8
#停止定时器,关闭LED
sudo sh -c "echo 0 > /dev/gpio_subsystem_button"

#定时器间隔为200ms
sudo sh -c "echo 2 200 > /dev/gpio_subsystem_button"

#开启定时器,循环读取按键引脚电平
sudo sh -c "echo 1 > /dev/gpio_subsystem_button"

开启定时器后,使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:

1
2
3
4
5
6
[  209.795942] 按键已按下
[  209.999300] 按键已松开
[  210.199296] 按键已按下
[  210.609310] 按键已松开
[  212.042821] 按键已按下
[  212.446097] 按键已松开

除了内核打印信息,也可以看到每一次连接GND引脚模拟按键按下都可以看到LED从亮转灭或从灭转亮, 说明按键控制LED的联动功能正常,Pinctrl配置与GPIO电平读取、控制正常。

1.5.5. 实验注意事项

  1. Pinctrl上下拉配置关键:按键输入引脚必须配置上拉/下拉电阻,禁止悬空,否则电平不稳定,易出现误触发;

  2. GPIO模式严格区分:输出引脚用GPIOD_OUT_XXX,输入引脚必须用GPIOD_IN,禁止混用,否则会烧毁硬件或驱动报错;

  3. 按键状态记忆必备:必须记录上一次按键状态,仅在状态变化时执行逻辑,避免电平抖动导致频繁翻转LED;

  4. 定时器轮询间隔合理:间隔过短占用CPU资源,过长响应迟钝,推荐20ms-500ms;

  5. GPIO前缀匹配规则:设备树button-gpios,驱动中传button,内核自动拼接-gpios,不可传完整名称;

  6. 硬件资源互斥:必须禁用系统默认LED节点,避免多路驱动抢占同一GPIO引脚。