基于设备树的 LED 驱动,但是驱动的本质还是没变,都是配置 LED 灯所使用的 GPIO 寄存器,驱动开发方式和裸机基本没啥区别。
Linux 内核提供了 pinctrl 和 gpio 子系统用于GPIO 驱动,本章我们就来学习一下如何借助 pinctrl 和 gpio 子系统来简化 GPIO 驱动开发。
pinctrl子系统
pinctrl子系统简介
Linux 驱动讲究驱动分离与分层,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架,不管什么外设驱动,GPIO 驱动基本都是必须的,而 pinctrl 和 gpio 子系统又是 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 设置为通用输出功能(GPIO),也就是设置复用功能。
- 在②里面将 GPIO0_C0 这个 PIN 设置为输出模式。
②中完成对 GPIO0_C0 这个 PIN 相关的寄存器地址获取,③和④设置这个 PIN的复用功能、上下拉等
引脚设置基本需要的两个方面:
- 设置 PIN 的复用功能。
- PIN 复用为 GPIO 功能,设置 GPIO 相关属性。输入、输出功能,驱动能力等级
因此 Linux 内核针对 PIN 推出了 pinctrl 子系统,对于 GPIO 的电气属性配置推出了 gpio 子系统。
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引入的
pinctrl 子系统主要工作内容如下:
- 获取设备树中 pin 信息。
- 根据获取到的 pin 信息来设置 pin 的复用功能
- 根据获取到的 pin 信息来设置 pin 的电气特性,如驱动能力。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成,pinctrl 子系统源码目录为 drivers/pinctrl。
pinctrl子系统驱动
- 在设备树里面设置 PIN 的配置信息,毕竟 pinctrl 子系统要根据你提供的信息来配置 PIN 功能,一般会在设备树里面创建一个节点来描述 PIN 的配置信息。rk3568.dtsi 文件
- 向 pinctrl 节点追加数据,不同的外设使用的 PIN 不同、其配置也不同,因此一个萝卜一个坑,将某个外设所使用的所有 PIN 都组织在一个子节点里面。先打开 rk3568-pinctrl.dtsi文件,此文件需要编译内核以后才能得到
每个 pincrtl 节点必须至少包含一个子节点来存放 pincrtl 相关信息,也就是 pinctrl 集,这个集合里面存放当前外设用到哪些引脚(PIN)、复用配置、上下拉、驱动能力等。一般这个存放pincrtl集的子节点名字是**“rockchip,pins”。**
在“rockchip,pins”子节点里面存放外设的引脚描述信息,格式如下:
rockchip,pins = <PIN_BANK PIN_BANK_IDX MUX &phandle>
/*****************************************
1. PIN_BANK PIN_BANK 就是 PIN 所属的组
RK3568 一共有 5 组 PIN:GPIO0~GPIO4,分别对应 0~4
2. PIN_BANK_IDX PIN_BANK_IDX 是组内的编号
以 GPIO0 组为例,一共有 A0~A7、B0~B7、C0~C7、D0~D7,这 32 个 PIN。
3. MUX MUX 就是设置 PIN 的复用功能
4. phandle phandle用来描述一些引脚的通用配置信息
pcfg_pull_up、pcfg_pull_down、pcfg_pull_none 和 pcfg_pull_none_drv_level_0 就是可使用的配置项
********************************************/
设备树中添加 pinctrl 节点模板
如何在设备树中添加某个外设的 PIN 信息
创建对应的节点
在 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>;
};
添加“rockchip,pins”属性
添加一个“rockchip,pins”属性,这个属性是真正用来描述 PIN 配置信息的
uart0 {
/omit-if-no-ref/
uart0_xfer: uart0-xfer {
rockchip,pins =
/* uart0_rx */
<0 RK_PC0 3 &pcfg_pull_up>,
/* uart0_tx */
<0 RK_PC1 3 &pcfg_pull_up>;
};
/omit-if-no-ref/
uart0_ctsn: uart0-ctsn {
rockchip,pins =
/* uart0_ctsn */
<0 RK_PC7 3 &pcfg_pull_none>;
};
/omit-if-no-ref/
uart0_rtsn: uart0-rtsn {
rockchip,pins =
/* uart0_rtsn */
<0 RK_PC4 3 &pcfg_pull_none>;
};
};
按道理来讲,当我们将一个 PIN 用作 GPIO 功能的时候也需要创建对应的 pinctrl 节点,并且将所用的 PIN 复用为 GPIO 功能,但是!对于 RK3568 而言,如果一个 PIN 用作 GPIO 功能的时候不需要创建对应的 pinctrl 节点!
gpio子系统
gpio子系统简介
pinctrl 子系统重点是设置 PIN(有的 SOC 叫做 PAD)的复用和电气属性
gpio 子系统顾名思义,就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
rk3568中gpio子系统
设备树中GPIO信息
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>;
};
/****************************************************
compatible 属性值为“rockchip,gpio-bank”
reg 属性设置了 GPIO0 控制器的寄存器基地址为 0XFDD60000
interrupts 属性描述 GPIO0 控制器对应的中断信息
clocks 属性指定这个 GPIO0 控制器的时钟
“gpio-controller”表示 gpio0 节点是个 GPIO 控制器,每个 GPIO 控制器节点必须包含“gpio-controller”属性。
但是,CSI1 摄像头驱动程序怎么知道 RESET 引脚连接的 GPIO3_B6 呢?这里肯定需要设备树来告诉驱动,在设备树中的 CSI1 摄像头节点下添加一个属性来描述摄像头的 RESET 引脚就行了,CSI1 摄像头驱动直接读取这个属性值就知道摄像头的 RESET 引脚使用的是哪个 GPIO 了。
gpio子系统中的API函数
对于驱动开发人员,设置好设备树以后就可以使用 gpio 子系统提供的 API 函数来操作指定的 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 函数
void gpio_free(unsigned gpio);
// gpio:要释放的 gpio 标号。
// 返回值:无。
gpio_direction_input 函数
int gpio_direction_input(unsigned gpio);
// gpio:要设置为输入的 GPIO 标号。
// 返回值:0,设置成功;负值,设置失败。
gpio_direction_output 函数
int gpio_direction_output(unsigned gpio, int value);
// gpio:要设置为输出的 GPIO 标号。
// value:GPIO 默认输出值。
// 返回值:0,设置成功;负值,设置失败。
gpio_get_value 函数
int gpio_get_value(unsigned int gpio);
// gpio:要获取的 GPIO 标号。
// 返回值:非负值,得到的 GPIO 值;负值,获取失败。
gpio_set_value 函数
void gpio_set_value(unsigned int gpio, int value);
// gpio:要设置的 GPIO 标号。
// value:要设置的值。
// 返回值:无
设备数添加gpio节点
以led为例,学习一下如何创建 GPIO 节点。
创建led设备子节点
添加gpio属性信息
rk3568_led: led0 {
compatible = "atkrk3568-led";
status = "okay";
reg = <0x0 0xFDC20010 0x0 0x08 /* PMU_GRF_GPIO0C_IOMUX_L */
0x0 0xFDC20090 0x0 0x08 /* PMU_GRF_GPIO0C_DS_0 */
0x0 0xFDD60004 0x0 0x08 /* GPIO0_SWPORT_DR_H */
0x0 0xFDD6000C 0x0 0x08 >; /* GPIO0_SWPORT_DDR_H */
};
gpioled{
compatible = "zxk, led";
led-gpio = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;
status = "okay";
};
};
与GPIO相关的OF函数
在驱动程序中需要读取 gpio 属性内容,Linux 内核提供了几个与 GPIO 有关的 OF 函数
of_gpio_named_count 函数
int of_gpio_named_count(struct device_node *np, const char *propname);
// np:设备节点。
// propname:要统计的 GPIO 属性。
// 返回值:正值,统计到的 GPIO 数量;负值,失败。
of_gpio_count 函数
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 编号;负值,失败。