RK3568 Linux驱动学习——pinctrl和gpio子系统
前言
上一章编写了基于设备树的 LED 驱动,但是驱动的本质还是没变,都是配置 LED 灯所使用的 GPIO 寄存器,驱动开发方式和裸机基本没啥区别。Linux 是一个庞大而完善的系统,尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式。Linux 内核提供了 pinctrl 和 gpio 子系统用于 GPIO 驱动,本章就来学习一下如何借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。
pinctrl 子系统
pinctrl 子系统简介
Linux 驱动讲究驱动分离与分层,pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架,关于驱动的分离与分层后面会讲。
先来回顾一下上一章是怎么初始化 LED 灯所使用的 GPIO,步骤如下:
- 修改设备树,添加相应的节点,节点里面重点是设置 reg 属性,reg 属性包括了 GPIO 相关寄存器。
- 获取 reg 属性中 PMU_GRF_GPIO0C_IOMUX_L、PMU_GRF_GPIO0C_DS_0、GPIO0_SWPORT_DR_H 和 GPIO0_SWPORT_DDR_H 这些寄存器的地址,并且初始化它们,这些寄存器用于设置 GPIO0_C0 这个 PIN 的复用功能、上下拉、驱动能力等。
- 在 2 里面将 GPIO0_C0 这个 PIN 设置为通用输出功能(GPIO),也就是设置复用功能。
- 在2里面将 GPIO0_C0 这个 PIN 设置为输出模式。
总结一下,第 2 步中完成对 GPIO0_C0 这个 PIN 相关的寄存器地址获取,第 3 和第 4 步设置这个 PIN的复用功能、上下拉等,比如将 GPIO0_C0 这个 PIN 设置为通用输出功能。如果使用过 STM32 单片机的话应该都记得,STM32 单片机也是要先设置某个 PIN 的复用功能、速度、上下拉等,然后再设置 PIN 所对应的 GPIO。RK3568 和 STM32 单片机是类似的,其实对于大多数的 32 位 SOC 而言,引脚的设置基本都是这两方面:
- 设置 PIN 的复用功能。
- 如果 PIN 复用为 GPIO 功能,设置 GPIO 相关属性。
因此 Linux 内核针对 PIN 推出了 pinctrl 子系统,对于 GPIO 的电气属性配置推出了 gpio 子系统。本小节来学习 pinctrl 子系统,下一小节再学习 gpio 子系统。
大多数 SOC 的 pin 都是支持复用的,比如 RK3568 的 GPIO3_D4 既可以作为普通的 GPIO 使用,也可以作为 PWM1_M0 引脚、GPU_AVS 引脚、UART0_RX 引脚。此外还需要配置 pin 的电气特性,比如上/下拉、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引入的,pinctrl 子系统主要工作内容如下:
- 获取设备树中 pin 信息。
- 根据获取到的 pin 信息来设置 pin 的复用功能
- 根据获取到的 pin 信息来设置 pin 的电气特性,如驱动能力。
对于使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,pinctrl 子系统源码目录为 drivers/pinctrl。
rk3568 的 pinctrl 子系统驱动
- PIN 配置信息详解
要使用 pinctrl 子系统,需要在设备树里面设置 PIN 的配置信息,毕竟 pinctrl 子系统要根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。打开 rk3568.dtsi 文件,找到一个叫做 pinctrl 的节点,如下所示:
pinctrl: pinctrl {compatible = "rockchip,rk3568-pinctrl";rockchip,grf = <&grf>;rockchip,pmu = <&pmugrf>;#address-cells = <2>;#size-cells = <2>;ranges;gpio0: gpio@fdd60000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfdd60000 0x0 0x100>;interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 0 32>;interrupt-controller;#interrupt-cells = <2>;};gpio1: gpio@fe740000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfe740000 0x0 0x100>;interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru PCLK_GPIO1>, <&cru DBCLK_GPIO1>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 32 32>;interrupt-controller;#interrupt-cells = <2>;};gpio2: gpio@fe750000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfe750000 0x0 0x100>;interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru PCLK_GPIO2>, <&cru DBCLK_GPIO2>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 64 32>;interrupt-controller;#interrupt-cells = <2>;};gpio3: gpio@fe760000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfe760000 0x0 0x100>;interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru PCLK_GPIO3>, <&cru DBCLK_GPIO3>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 96 32>;interrupt-controller;#interrupt-cells = <2>;};gpio4: gpio@fe770000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfe770000 0x0 0x100>;interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru PCLK_GPIO4>, <&cru DBCLK_GPIO4>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 128 32>;interrupt-controller;#interrupt-cells = <2>;};};
第 3536-3537 行,#address-cells 属性值为 2 和#size-cells 属性值为 2,也就是说 pinctrl 下的所有子节点的 reg 第一位加第二位是起始地址,第三位加第四位为长度。
第 3540-3604 行,rk3568 有五组 GPIO:GPIO0~GPIO4,每组 GPIO 对应的寄存器地址不同。第 3540-3551 行是 GPIO0 这个 GPIO 组对应的子节点,其中第 2619 行的 reg 属性描述了 GPIO0 对应的寄存器地址,基地址就是 0XFDD60000,驱动会得到 GPIO0 的基地址 0XFDD60000,然后加上偏移就得到了 GPIO0 的其他寄存器地址。
以上代码看起来,根本没有 PIN 相关的具体配置,别急!先打开 rk3568-pinctrl.dtsi
文件,此文件需要编译内核以后才能得到,能找到如下内容:
&pinctrl {acodec {/omit-if-no-ref/acodec_pins: acodec-pins {rockchip,pins =/* acodec_adc_sync */<1 RK_PB1 5 &pcfg_pull_none>,/* acodec_adcclk */<1 RK_PA1 5 &pcfg_pull_none>,/* acodec_adcdata */<1 RK_PA0 5 &pcfg_pull_none>,/* acodec_dac_datal */<1 RK_PA7 5 &pcfg_pull_none>,/* acodec_dac_datar */<1 RK_PB0 5 &pcfg_pull_none>,/* acodec_dacclk */<1 RK_PA3 5 &pcfg_pull_none>,/* acodec_dacsync */<1 RK_PA5 5 &pcfg_pull_none>;};};audiopwm {/omit-if-no-ref/audiopwm_lout: audiopwm-lout {rockchip,pins =/* audiopwm_lout */<1 RK_PA0 4 &pcfg_pull_none>;};/omit-if-no-ref/audiopwm_loutn: audiopwm-loutn {rockchip,pins =/* audiopwm_loutn */<1 RK_PA1 6 &pcfg_pull_none>;};/omit-if-no-ref/audiopwm_loutp: audiopwm-loutp {rockchip,pins =/* audiopwm_loutp */<1 RK_PA0 6 &pcfg_pull_none>;};/omit-if-no-ref/audiopwm_rout: audiopwm-rout {rockchip,pins =/* audiopwm_rout */<1 RK_PA1 4 &pcfg_pull_none>;};/omit-if-no-ref/audiopwm_routn: audiopwm-routn {rockchip,pins =/* audiopwm_routn */<1 RK_PA7 4 &pcfg_pull_none>;};/omit-if-no-ref/audiopwm_routp: audiopwm-routp {rockchip,pins =/* audiopwm_routp */<1 RK_PA6 4 &pcfg_pull_none>;};};bt656 {/omit-if-no-ref/bt656m0_pins: bt656m0-pins {rockchip,pins =/* bt656_clkm0 */<3 RK_PA0 2 &pcfg_pull_none>,/* bt656_d0m0 */<2 RK_PD0 2 &pcfg_pull_none>,/* bt656_d1m0 */<2 RK_PD1 2 &pcfg_pull_none>,/* bt656_d2m0 */<2 RK_PD2 2 &pcfg_pull_none>,/* bt656_d3m0 */<2 RK_PD3 2 &pcfg_pull_none>,/* bt656_d4m0 */<2 RK_PD4 2 &pcfg_pull_none>,/* bt656_d5m0 */<2 RK_PD5 2 &pcfg_pull_none>,/* bt656_d6m0 */<2 RK_PD6 2 &pcfg_pull_none>,/* bt656_d7m0 */<2 RK_PD7 2 &pcfg_pull_none>;};/omit-if-no-ref/bt656m1_pins: bt656m1-pins {rockchip,pins =/* bt656_clkm1 */<4 RK_PB4 5 &pcfg_pull_none>,/* bt656_d0m1 */<3 RK_PC6 5 &pcfg_pull_none>,/* bt656_d1m1 */<3 RK_PC7 5 &pcfg_pull_none>,/* bt656_d2m1 */<3 RK_PD0 5 &pcfg_pull_none>,/* bt656_d3m1 */<3 RK_PD1 5 &pcfg_pull_none>,/* bt656_d4m1 */<3 RK_PD2 5 &pcfg_pull_none>,/* bt656_d5m1 */<3 RK_PD3 5 &pcfg_pull_none>,/* bt656_d6m1 */<3 RK_PD4 5 &pcfg_pull_none>,/* bt656_d7m1 */<3 RK_PD5 5 &pcfg_pull_none>;};};bt1120 {/omit-if-no-ref/bt1120_pins: bt1120-pins {rockchip,pins =/* bt1120_clk */<3 RK_PA6 2 &pcfg_pull_none>,/* bt1120_d0 */<3 RK_PA1 2 &pcfg_pull_none>,/* bt1120_d1 */<3 RK_PA2 2 &pcfg_pull_none>,/* bt1120_d2 */<3 RK_PA3 2 &pcfg_pull_none>,/* bt1120_d3 */<3 RK_PA4 2 &pcfg_pull_none>,/* bt1120_d4 */<3 RK_PA5 2 &pcfg_pull_none>,/* bt1120_d5 */<3 RK_PA7 2 &pcfg_pull_none>,/* bt1120_d6 */<3 RK_PB0 2 &pcfg_pull_none>,/* bt1120_d7 */<3 RK_PB1 2 &pcfg_pull_none>,/* bt1120_d8 */<3 RK_PB2 2 &pcfg_pull_none>,/* bt1120_d9 */<3 RK_PB3 2 &pcfg_pull_none>,/* bt1120_d10 */<3 RK_PB4 2 &pcfg_pull_none>,/* bt1120_d11 */<3 RK_PB5 2 &pcfg_pull_none>,/* bt1120_d12 */<3 RK_PB6 2 &pcfg_pull_none>,/* bt1120_d13 */<3 RK_PC1 2 &pcfg_pull_none>,/* bt1120_d14 */<3 RK_PC2 2 &pcfg_pull_none>,/* bt1120_d15 */<3 RK_PC3 2 &pcfg_pull_none>;};};cam {/omit-if-no-ref/cam_clkout0: cam-clkout0 {rockchip,pins =/* cam_clkout0 */<4 RK_PA7 1 &pcfg_pull_none>;};/omit-if-no-ref/cam_clkout1: cam-clkout1 {rockchip,pins =/* cam_clkout1 */<4 RK_PB0 1 &pcfg_pull_none>;};};can0 {/omit-if-no-ref/can0m0_pins: can0m0-pins {rockchip,pins =/* can0_rxm0 */<0 RK_PB4 2 &pcfg_pull_none>,/* can0_txm0 */<0 RK_PB3 2 &pcfg_pull_none>;};/omit-if-no-ref/can0m1_pins: can0m1-pins {rockchip,pins =/* can0_rxm1 */<2 RK_PA2 4 &pcfg_pull_none>,/* can0_txm1 */<2 RK_PA1 4 &pcfg_pull_none>;};};can1 {/omit-if-no-ref/can1m0_pins: can1m0-pins {rockchip,pins =/* can1_rxm0 */<1 RK_PA0 3 &pcfg_pull_none>,/* can1_txm0 */<1 RK_PA1 3 &pcfg_pull_none>;};/omit-if-no-ref/can1m1_pins: can1m1-pins {rockchip,pins =/* can1_rxm1 */<4 RK_PC2 3 &pcfg_pull_none>,/* can1_txm1 */<4 RK_PC3 3 &pcfg_pull_none>;};};can2 {/omit-if-no-ref/can2m0_pins: can2m0-pins {rockchip,pins =/* can2_rxm0 */<4 RK_PB4 3 &pcfg_pull_none>,/* can2_txm0 */<4 RK_PB5 3 &pcfg_pull_none>;};/omit-if-no-ref/can2m1_pins: can2m1-pins {rockchip,pins =/* can2_rxm1 */<2 RK_PB1 4 &pcfg_pull_none>,/* can2_txm1 */<2 RK_PB2 4 &pcfg_pull_none>;};};cif {/omit-if-no-ref/cif_clk: cif-clk {rockchip,pins =/* cif_clkout */<4 RK_PC0 1 &pcfg_pull_none>;};/omit-if-no-ref/cif_dvp_clk: cif-dvp-clk {rockchip,pins =/* cif_clkin */<4 RK_PC1 1 &pcfg_pull_none>,/* cif_href */<4 RK_PB6 1 &pcfg_pull_none>,/* cif_vsync */<4 RK_PB7 1 &pcfg_pull_none>;};/omit-if-no-ref/cif_dvp_bus16: cif-dvp-bus16 {rockchip,pins =/* cif_d8 */<3 RK_PD6 1 &pcfg_pull_none>,/* cif_d9 */<3 RK_PD7 1 &pcfg_pull_none>,/* cif_d10 */<4 RK_PA0 1 &pcfg_pull_none>,/* cif_d11 */<4 RK_PA1 1 &pcfg_pull_none>,/* cif_d12 */<4 RK_PA2 1 &pcfg_pull_none>,/* cif_d13 */<4 RK_PA3 1 &pcfg_pull_none>,/* cif_d14 */<4 RK_PA4 1 &pcfg_pull_none>,/* cif_d15 */<4 RK_PA5 1 &pcfg_pull_none>;};/omit-if-no-ref/cif_dvp_bus8: cif-dvp-bus8 {rockchip,pins =/* cif_d0 */<3 RK_PC6 1 &pcfg_pull_none>,/* cif_d1 */<3 RK_PC7 1 &pcfg_pull_none>,/* cif_d2 */<3 RK_PD0 1 &pcfg_pull_none>,/* cif_d3 */<3 RK_PD1 1 &pcfg_pull_none>,/* cif_d4 */<3 RK_PD2 1 &pcfg_pull_none>,/* cif_d5 */<3 RK_PD3 1 &pcfg_pull_none>,/* cif_d6 */<3 RK_PD4 1 &pcfg_pull_none>,/* cif_d7 */<3 RK_PD5 1 &pcfg_pull_none>;};};
上述代码就是向 pinctrl 节点追加数据,不同的外设使用的 PIN 不同、其配置也不
同,因此一个萝卜一个坑,将某个外设所使用的所有 PIN 都组织在一个子节点里面。上述代码中第 184~242 行的 can 子节点就是 rk3568 的三个 CAN 接口所使用的引脚配置,canm0_pins、canm1_pins 和 canm2_pins 分别对应 CAN0、CAN1 和 CAN2 这三个接口。同理第 244 行的子节点 cif_clk 就是 CIF0 接口对应时钟的引脚 。 绑定文档
Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.txt 描述了如何在设备树中设置 rk3568 的 PIN 信息。
每个 pincrtl 节点必须至少包含一个子节点来存放 pincrtl 相关信息,也就是 pinctrl 集,这个集合里面存放当前外设用到哪些引脚(PIN)、复用配置、上下拉、驱动能力等。一般这个存放 pincrtl 集的子节点名字是“rockchip,pins”。
上面讲了,在 “rockchip,pins” 子节点里面存放外设的引脚描述信息,根据 rockchip,pinctrl.txt 文档里面的介绍,引脚复用设置的格式如下:
rockchip,pins = <PIN_BANK PIN_BANK_IDX MUX &phandle>
一共分为四部分:
- PIN_BANK
PIN_BANK 就是 PIN 所属的组,RK3568 一共有 5 组 PIN:GPIO0-GPIO4,分别对应 0~4,所以如果要设置 GPIO0_C0 这个 PIN,那么 PIN_BANK 就是 0。
- PIN_BANK_IDX
PIN_BANK_IDX 是组内的编号,以 GPIO0 组为例,一共有 A0-A7、B0-B7、C0-C7、D0-D7,这 32 个 PIN。瑞芯微已经给这些 PIN 编了号,打开 include/dt-bindings/pinctrl/rockchip.h 文件,有如下定义:
#define RK_PA0 0
#define RK_PA1 1
#define RK_PA2 2
#define RK_PA3 3
#define RK_PA4 4
#define RK_PA5 5
#define RK_PA6 6
#define RK_PA7 7
#define RK_PB0 8
#define RK_PB1 9
#define RK_PB2 10
#define RK_PB3 11
#define RK_PB4 12
#define RK_PB5 13
#define RK_PB6 14
#define RK_PB7 15
#define RK_PC0 16
#define RK_PC1 17
#define RK_PC2 18
#define RK_PC3 19
#define RK_PC4 20
#define RK_PC5 21
#define RK_PC6 22
#define RK_PC7 23
#define RK_PD0 24
#define RK_PD1 25
#define RK_PD2 26
#define RK_PD3 27
#define RK_PD4 28
#define RK_PD5 29
#define RK_PD6 30
#define RK_PD7 31
如果要设置 GPIO0_C0,那么 PIN_BANK_IDX 就要设置为 RK_PC0。
- MUX
MUX 就是设置 PIN 的复用功能,一个 PIN 最多有 16 个复用功能,include/dt-bindings/pinctrl/rockchip.h 文件中有如下内容:
#define RK_FUNC_GPIO 0
#define RK_FUNC_0 0
#define RK_FUNC_1 1
#define RK_FUNC_2 2
#define RK_FUNC_3 3
#define RK_FUNC_4 4
#define RK_FUNC_5 5
#define RK_FUNC_6 6
#define RK_FUNC_7 7
#define RK_FUNC_8 8
#define RK_FUNC_9 9
#define RK_FUNC_10 10
#define RK_FUNC_11 11
#define RK_FUNC_12 12
#define RK_FUNC_13 13
#define RK_FUNC_14 14
#define RK_FUNC_15 15
上面就是 16 个复用功能编号,以 GPIO0_C0 为例,查看 RK3568 数据手册,可以得到 GPIO0_C0 复用情况如下图所示:
从上图可以看出 GPIO3_D4 有 4 个复用功能:
0:GPIO0_C0
1:PWM1_M0
2:GPU_AVS
3:UART0_RX
如果要将 GPIO0_C0 设置为 GPIO 功能,那么 MUX 就设置 0,或者 RK_FUNC_GPIO。如果要设置为 PWM1_M0,那么 MUX 就设置为 1。
- phandle
最后一个就是 phandle,用来描述一些引脚的通用配置信息,打开 scripts/dtc/include-prefixes/arm/rockchip-pinconf.dtsi 文件,此文件就是 phandle 可选的配置项,如下所示:
&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 {bias-disable;drive-strength = <0>;};
上面的 pcfg_pull_up、pcfg_pull_down、pcfg_pull_none 和 pcfg_pull_none_drv_level_0 就是可使用的配置项。比如 GPIO0_C0 这个 PIN 用作普通的 GPIO,不需要配置驱动能力,那么就可以使用 pcfg_pull_none,也就是不设置上下拉。rockchip-pinconf.dtsi 里面还有很多其他的配置项,自行查阅。
最终,如果要将 GPIO0_C0 设置为 GPIO 功能,那么配置就是:
rockchip,pins =<0 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>;
设备树中添加 pinctrl 节点模板
已经对 pinctrl 有了比较深入的了解,接下来学习一下如何在设备树中添加某个外
设的 PIN 信息。比如需要将 GPIO0_D1 这个 PIN 复用为 UART2_TX 引脚,pinctrl 节点添加过程如下:
- 创建对应的节点
在 pinctrl 节点下添加一个 “uart2” 子节点,然后在 uart2 节点里面在创建一个“uart2m0_xfer: uart2m0-xfer” 子节点:
1 &pinctrl {
2 uart2 {
3 /omit-if-no-ref/
4 uart2m0_xfer: uart2m0-xfer {
5 /* 具体的 PIN 信息 */
6 };
7 };
8 };
第 3 行的“/omit-if-no-ref/”笔者没有找出说明,但是瑞芯微官方在每个外设的 pinctrl 引脚设置前都加这一行,所以这里也加上去。
第 4 行,uart2m0_xfer 子节点内就是放置具体的 PIN 配置信息。
- 添加“rockchip,pins”属性
添加一个“rockchip,pins”属性,这个属性是真正用来描述 PIN 配置信息的,这里只添加 UART2 的 TX 引脚,所以添加完以后如下所示:
1 &pinctrl {
2 uart2 {
3 /omit-if-no-ref/
4 uart2m0_xfer: uart2m0-xfer {
5 rockchip,pins =
6 /* uart2_tx_m1 */
7 <0 RK_PD1 1 &pcfg_pull_up>;
8 };
9 };
10 };
按道理来讲,当将一个 PIN 用作 GPIO 功能的时候也需要创建对应的 pinctrl 节点,并且将所用的 PIN 复用为 GPIO 功能,但是!对于 RK3568 而言,如果一个 PIN 用作 GPIO 功能的时候不需要创建对应的 pinctrl 节点!
gpio 子系统
gpio 子系统简介
上一小节讲解了 pinctrl 子系统,pinctrl 子系统重点是设置 PIN(有的 SOC 叫做 PAD)的复用和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO 为输入输出,读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
rk3568 的 gpio 子系统驱动
- 设备树中的 gpio 信息
首先肯定是 GPIO 控制器的节点信息,以 GPIO0_C0 这个引脚所在的 GPIO3 为例,打开 rk3568.dtsi,在里面找到如下所示内容:
gpio0: gpio@fdd60000 {compatible = "rockchip,gpio-bank";reg = <0x0 0xfdd60000 0x0 0x100>;interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;gpio-controller;#gpio-cells = <2>;gpio-ranges = <&pinctrl 0 0 32>;interrupt-controller;#interrupt-cells = <2>;};
上述代码就是 GPIO0 的控制器信息,属于 pincrtl 的子节点,绑定文档
Documentation/devicetree/bindings/gpio/gpio.txt 详细描述了 gpio 控制器节点各个属性信息。
compatible 属性值为“rockchip,gpio-bank”,所以在 linux 内核中搜索这个字符串就可以找到对应的 GPIO 驱动源文件,为 drivers/pinctrl/pinctrl-rockchip.c。
reg 属性设置了 GPIO0 控制器的寄存器基地址为 0XFDD60000。
interrupts 属性描述 GPIO0 控制器对应的中断信息。
clocks 属性指定这个 GPIO0 控制器的时钟。
“gpio-controller”表示 gpio0 节点是个 GPIO 控制器,每个 GPIO 控制器节点必须包含“gpio-controller”属性。
“#gpio-cells”属性和“#address-cells”类似,#gpio-cells 应该为 2,表示一共有两个 cell,第一个 cell 为 GPIO 编号,比如“&gpio0 RK_PC0”就表示 GPIO0_C0。第二个 cell 表示 GPIO 极性,如果为 0(GPIO_ACTIVE_HIGH)的话表示高电平有效,如果为1(GPIO_ACTIVE_LOW)的话表示低电平有效。
上述代码中的是 GPIO0 控制器节点,当某个具体的引脚作为 GPIO 使用的时候还需要进一步设置。正点原子 ATK-DLRK3568 开发板将 GPIO3_B6 用作 CSI1 摄像头的 RESET 引脚,GPIO3_B6 复用为 GPIO 功能,通过控制这个 GPIO 的高低电平就可以复位 CSI1 摄像头。但是,CSI1 摄像头驱动程序怎么知道 RESET 引脚连接的 GPIO3_B6 呢?这里肯定需要设备树来告诉驱动,在设备树中的 CSI1 摄像头节点下添加一个属性来描述摄像头的 RESET 引脚就行了,CSI1 摄像头驱动直接读取这个属性值就知道摄像头的 RESET 引脚使用的是哪个 GPIO 了。正点原子 ATK-DLRK3568 开发板的 CSI1 摄像头的 I2C 配置接口连接到 RK3568 的 I2C4 上。在 rk3568-atk-evb1-ddr4-v10.dtsi 中找到名为“i2c4”的节点,这个节点包含了所有连接到 I2C5 接口上的设备,如下所示:
imx415: imx415@1a {status = "okay";compatible = "sony,imx415";reg = <0x1a>;clocks = <&cru CLK_CIF_OUT>;clock-names = "xvclk";power-domains = <&power RK3568_PD_VI>;pinctrl-names = "rockchip,camera_default";pinctrl-0 = <&cif_clk>;reset-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;power-gpios = <&gpio4 RK_PB4 GPIO_ACTIVE_HIGH>;rockchip,camera-module-index = <0>;rockchip,camera-module-facing = "back";rockchip,camera-module-name = "CMK-OT1522-FG3";rockchip,camera-module-lens-name = "CS-P1150-IRC-8M-FAU";port {imx415_out: endpoint {remote-endpoint = <&mipi_in_ucam1>;data-lanes = <1 2 3 4>;};};};
属性“reset-gpios”描述了 IMX415 这个摄像头的 RESET 引脚使用的哪个 IO。
属性值一共有三个,来看一下这三个属性值的含义,“&gpio3”表示 RESET 引脚所使用的 IO 属于 GPIO3 组,“RK_PB6”表示 GPIO3 组的 PB6,通过这两个值 IMX415 摄像头驱动程序就知道 RESET 引脚使用了 GPIO3_B6 这个引脚。最后一个是“GPIO_ACTIVE_LOW ”,Linux 内核在 include/linux/gpio/machine.h 文件中定义了枚举类型 gpio_lookup_flags,内容如下:
enum gpio_lookup_flags {GPIO_ACTIVE_HIGH = (0 << 0),GPIO_ACTIVE_LOW = (1 << 0),GPIO_OPEN_DRAIN = (1 << 1),GPIO_OPEN_SOURCE = (1 << 2),GPIO_PERSISTENT = (0 << 3),GPIO_TRANSITORY = (1 << 3),
};
gpio 子系统 API 函数
对于驱动开发人员,设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 GPIO,gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。gpio 子系统提供的常用的 API 函数有下面几个:
- gpio_request 函数
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),函数原型如下:
int gpio_get_value(unsigned int gpio);
函数参数和返回值含义如下:
gpio:要获取的 GPIO 标号。
返回值:非负值,得到的 GPIO 值;负值,获取失败。
- gpio_set_value 函数
此函数用于设置某个 GPIO 的值,函数原型如下:
void gpio_set_value(unsigned int gpio, int value);
函数参数和返回值含义如下:
gpio:要设置的 GPIO 标号。
value:要设置的值。
返回值:无
关于 gpio 子系统常用的 API 函数就讲这些,这些是用的最多的。
设备树中添加 gpio 节点模板
本节以正点原子 ATK-DLRK3568 开发板上的 LED 为例,学习一下如何创建 GPIO 节点。LED 连接到了 GPIO0_C0 引脚上,首先创建一个“led”设备节点。
- 创建 led 设备节点
在根节点“/”下创建 led 设备子节点,如下所示:
1 gpioled {
2 /* 节点内容 */
3 };
- 添加 GPIO 属性信息
在 gpioled 节点中添加 GPIO 属性信息,表明所使用的 GPIO 是哪个引脚,添加完成以后如下所示:
1 gpioled {
2 compatible = "alientek,led";
3 led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;
4 status = "okay";
5 };
第 3 行,LED0 设备所使用的 gpio。
关于 pinctrl 子系统和 gpio 子系统就讲解到这里,接下来就使用 pinctrl 和 gpio 子系统来驱动 ATK-DLRK3568 开发板上的 LED 灯。
与 gpio 相关的 OF 函数
之前定义了一个名为“gpio”的属性,gpio 属性描述了 led 这个设备所使用的 GPIO。在驱动程序中需要读取 gpio 属性内容,Linux 内核提供了几个与 GPIO 有关的 OF 函数,常用的几个 OF 函数如下所示:
- of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个 GPIO 信息,要注意的是空的 GPIO 信息也会被统计到,比如:
gpios = <0&gpio1 1 20&gpio2 3 4>;
上述代码的“gpios”节点一共定义了 4 个 GPIO,但是有 2 个是空的,没有实际的含义。通过 of_gpio_named_count 函数统计出来的 GPIO 数量就是 4 个,此函数原型如下:
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 数量,而 of_gpio_named_count 函数可以统计任意属性的 GPIO 信息,函数原型如下所示:
int of_gpio_count(struct device_node *np);
函数参数和返回值含义如下:
np:设备节点。
返回值:正值,统计到的 GPIO 数量;负值,失败。
- of_get_named_gpio 函数
此函数获取 GPIO 编号,因为 Linux 内核中关于 GPIO 的 API 函数都要使用 GPIO 编号,此函数会将设备树中类似 <&gpio4 RK_PA1 GPIO_ACTIVE_LOW> 的属性信息转换为对应的 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 编号;负值,失败。
硬件原理图分析
就是 LED 原理图。
实验程序编写
章实验我们继续研究 LED 灯,在之前实验中通过设备树向 dtsled.c 文件传递相应
的寄存器物理地址,然后在驱动文件中配置寄存器。本章实验使用 gpio 子系统来完成 LED灯驱动。
检查 IO 是否已被使用
首先要检查一下 GPIO0_C0 对应的 GPIO 有没有被其他外设使用,如果这个 GPIO 已经被分配给了其他外设,那么驱动在申请这个 GPIO 就会失败,如下图所示:
上图就是 GPIO 申请失败提示,因为当前开发板系统将 GPIO0_C0 这个 IO 分配给了内核自带的 LED 驱动做心跳灯了。所以需要先关闭 GPIO0_C0 作为心跳灯这个功能,也就是将 GPIO0_C0 对应的 GPIO 释放出来。打开 rk3568-evb.dtsi 文件,找到如下图所示代码:
上图中可以看出,正点原子出厂系统默认将 GPIO0_C0 用作心跳灯了,所以系统在
启动的时候就先将 GPIO0_C0 给了心跳灯,后面再申请肯定就会失败!
解决方法就是关闭心跳灯功能,也就是在上图中第 167 行添加 status 改为 disabled,也就是禁止 work 这个节点,那么禁止心跳灯功能。这样系统启动的时候就不会将 GPIO0_C0 分配给心跳灯,后面也就可以申请了,修改后如下图所示:
上图中将 status 改为 disabled 就禁止了 led,也就是心跳灯功能,后面需要禁止哪个功能,只需要将其 status 属性改为 disabled 就可以了。
修改设备树文件
在 rk3568-atk-evb1-ddr4-v10.dtsi 文件的根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:
gpioled {compatible = "alientek,led";led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;status = "okay";};
led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO0 的 PC0,高电平
有效。稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号。
设备树编写完成以后使用 “./build.sh kernel” 命令重新编译并打包内核,然后将新编译出来的 boot.img 烧写到开发板中。启动成功以后进入“/proc/device-tree”目录中查看 “gpioled” 节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图所示:
LED 灯驱动程序编写
新建名为“05_gpioled”文件夹,然后在 05_gpioled 文件夹里面创建 vscode 工
程,工作区命名为“gpioled”。工程创建好以后新建 gpioled.c 文件,在 gpioled.c 里面输入如下内容:
/* 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编号 */
};
在设备结构体 gpioled_dev 中加入 led_gpio 这个成员变量,此成员变量保存 LED
等所使用的 GPIO 编号。
static int led_open(struct inode *inode, struct file *filp)
{filp->private_data = &gpioled; /* 设置私有数据 */return 0;
}
将设备结构体变量 gpioled 设置为 filp 的私有数据 private_data。
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, 1); /* 打开LED灯 */} else if(ledstat == LEDOFF) {gpio_set_value(dev->led_gpio, 0); /* 关闭LED灯 */}return 0;
}
通过读取 filp 的 private_data 成员变量来得到设备结构体变量,也就是 gpioled。这种将设备结构体设置为 filp 私有数据的方法在 Linux 内核驱动里面非常常见。
直接调用 gpio_set_value 函数来向 GPIO 写入数据,实现开/关 LED 的效果。
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);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);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");if (ret) {printk(KERN_ERR "gpioled: Failed to request led-gpio\n");return ret;}/* 6、设置GPIO为输出,并且输出低电平,默认关闭LED灯 */ret = gpio_direction_output(gpioled.led_gpio, 0);if(ret < 0) {printk("can't set gpio!\r\n");}
获取节点“/gpioled”。
获取“status”属性的值,判断属性是否“okay”。
获取 compatible 属性值并进行匹配。
通过函数 of_get_named_gpio 函数获取 LED 所使用的 LED 编号。相当于将 gpioled 节点中的“led-gpio”属性值转换为对应的 LED 编号。
通过函数 gpio_request 向 GPIO 子系统申请使用 GPIO0_C0。
调用函数 gpio_direction_output 设置 GPIO0_C0 这个 GPIO 为输出,并且默认低电平,这样默认就会关闭 LED 灯。
编写测试 APP
直接延用之前的就可以了。
运行测试
编译驱动程序和测试 APP
- 编译驱动程序
编写 Makefile 文件,本章实验的 Makefile 文件和之前基本一样,只是将 obj-m 变量的值改为 gpioled.o,Makefile 内容如下所示:
KERNELDIR := /home/xhj/rk3568_linux_sdk/kernel
CURRENT_PATH := $(shell pwd)
obj-m := gpioled.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
设置 obj-m 变量的值为 gpioled.o。
输入如下命令编译出驱动模块文件:
make ARCH=arm64 //ARCH=arm64 必须指定,否则编译会失败
编译成功以后就会生成一个名为“gpioled.ko”的驱动模块文件。
- 编译测试 APP
输入如下命令编译测试 ledApp.c 这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp
编译成功以后就会生成 ledApp 这个应用程序。
运行测试
由于在本实验中,直接通过修改设备树的方式永久的关闭了 LED0 心跳灯功能,所以就不需要再输入命令关闭了。
在 Ubuntu 中将上一小节编译出来的 gpioled.ko 和 ledApp 这两个文件通过 adb 命令发送到开发板的/lib/modules/4.19.232 目录下,命令如下:
adb push gpioled.ko ledApp /lib/modules/4.19.232
发送成功以后进入到开发板目录 lib/modules/4.19.232 中,输入如下命令加载 dtsled.ko 驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe gpioled //加载驱动
驱动加载成功以后会在终端中输出一些信息,如下图所示:
上图可以看出,gpioled 这个节点找到了,并且 GPIO0_C0 这个 GPIO 的编号为 236。
驱动加载成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打开 LED 灯:
./ledApp /dev/gpioled 1 //打开 LED 灯
输入上述命令以后观察开发板上的红色 LED 灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭 LED 灯:
./ledApp /dev/gpioled 0 //关闭 LED 灯
输入上述命令以后观察开发板上的红色 LED 灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
rmmod gpioled.ko