记得以前在学习51单片机时,要控制一个GPIO输出高、低电平,就得根据对应IO寄存器每一bit的作用和含义进行配置,时钟、速率、方向、上下拉等等。51单片机是一款8位MCU,在配置寄存器相对容易,如果是32位甚至64位的处理器呢,会大大增加配置的复杂度。后来ST推出了STM32,并迅速收获一大批开发者和市场占有份额,至今依然是岿然不动。原因之一就是ST推出了一个“BSP标准库”(当然后来有推出了HAL、LL库等),BSP库屏蔽了底层寄存,大大释放了开发者的工作量,让开发者眼前一亮——原来MCU也可以这样开发。确实,BSP层应该由原厂实现,驱动工程师只做驱动,应用工程师专注做应用。
linux下的pintcrl和gpio子系统就类似于ST的“BSP库”,但是linux的pinctrl和gpio系统实现的功能和过程要远远比STM32的“BSP库”复杂。linux下引入pincrtl和gpio子系统,大大释放了驱动工程师的工作量,特别是引入“设备树”之后,使用一个外设时,对于pin引脚的初始化和管理,只需通过设备树描述即可,然后由pin子系统管理;对于gpio则由gpio子系统管理。
因此,与CPU引脚“关联”的设备驱动,最终都会调用pincrtl和gpio子系统。二者是设备驱动的基础,这二者也是一个设备驱动。
CPU的gpio引脚除了的方向、速度、上下拉、驱动能力等基本的电气特性外,一般会包括复用功能,即该引脚既可以作为普通gpio,还可能复位为i2c引脚、uart引脚等。如果采用直接配置寄存器的方式进行驱动开发,会非常繁琐,每更改一个功能就得重新翻阅手册配一遍寄存器,另一方面还可能存在“冲突”问题,比如该引脚已被复用为i2c在使用,但被驱动工程师忽略了,再去使用该gpio时会导致未知预期的问题。引入pintctrl子系统就可以解决诸如此类问题,结合设备树的使用,只需把pin信息在设备树描述清楚,即由pinctrl子系统介入管理。
pinctrl对于pin管理功能:
- .关联设备树,根据设备树pin信息在内核起来后进行配置pin引脚
- pin复用功能管理
- pin电气特性设置
#源自rk3399-firefly-port.dtsi
&pinctrl {
buttons {
pwrbtn: pwrbtn {
rockchip,pins = <0 5 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
leds {
led_power: led-power {
rockchip,pins = <2 27 RK_FUNC_GPIO &pcfg_pull_none>;
};
led_user: led-user {
rockchip,pins = <0 13 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
..........
#源自rk3399.dtsi
i2c3 {
i2c3_xfer: i2c3-xfer {
rockchip,pins =
<4 17 RK_FUNC_1 &pcfg_pull_none>, #复用为i2c属性
<4 16 RK_FUNC_1 &pcfg_pull_none>;
};
i2c3_gpio: i2c3_gpio {
rockchip,pins =
<4 17 RK_FUNC_GPIO &pcfg_pull_none>,#复用为普通gpio属性
<4 16 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
以上设备树摘自部分rk3399的设备树文件:
#源自rk3399.dtsi
i2c3: i2c@ff130000 {
compatible = "rockchip,rk3399-i2c";
reg = <0x0 0xff130000 0x0 0x1000>;
clocks = <&cru SCLK_I2C3>, <&cru PCLK_I2C3>;
clock-names = "i2c", "pclk";
interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH 0>;
pinctrl-names = "default"; #默认功能是i2c
pinctrl-0 = <&i2c3_xfer>; #使用i2c复用功能的pin属性
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
pin驱动一般芯片原厂已经提供,rk3399 pin驱动位于源码位于“kernel/drivers/pinctrl/pinctrl-rockship.c”中。“pinctrl-rockchip.c”支持了瑞芯微常用的CPU,如rk3288、rk3399、px30等,使用哪一款CPU,与linux其他驱动一样,可以通过pinctrl节点的设备树进行自动选择匹配。
rockchip pinctrl驱动匹配表
static const struct of_device_id rockchip_pinctrl_dt_match[] = {
........
{ .compatible = "rockchip,rk3328-pinctrl",
.data = &rk3328_pin_ctrl },
{ .compatible = "rockchip,rk3366-pinctrl",
.data = &rk3366_pin_ctrl },
{ .compatible = "rockchip,rk3368-pinctrl",
.data = &rk3368_pin_ctrl },
{ .compatible = "rockchip,rk3399-pinctrl",
.data = &rk3399_pin_ctrl },
{},
};
static struct platform_driver rockchip_pinctrl_driver = {
.probe = rockchip_pinctrl_probe,
.driver = {
.name = "rockchip-pinctrl",
.pm = &rockchip_pinctrl_dev_pm_ops,
.of_match_table = rockchip_pinctrl_dt_match,
},
};
rk3399 pinctrl 节点设备树描述
pinctrl: pinctrl {
compatible = "rockchip,rk3399-pinctrl";
rockchip,grf = <&grf>;
rockchip,pmu = <&pmugrf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
.........
pinctrl子系统主要是管理pin的电气属性和复用功能,而gpio子系统则是管理gpio的申请释放、控制输入输出、io中断等功能。gpio子系统屏蔽了gpio相关寄存器的配置过程,换而提供了常用的接口函数给驱动工程师使用,方便gpio相关的驱动开发。
常见与gpio相关的驱动:
gpio子系统功能
- 对于驱动层,屏蔽gpio寄存器配置细节,提供统一gpio操作接口
- 对于BSP层,统一框架,方便不同CPU接入,只需更换pinctrl子系统的驱动
gpio子系统对于驱动层的API位于“/kernel/include/linux/gpio.h”中。
【1】检查gpio是否可用
int gpio_is_valid(int number);
参数/ | 含义/含义 |
---|---|
number | gpio序号 |
返回 | 可用返回true,不可用返回false |
【2】申请使用一个gpio
使用一个gpio前,必须向内核申请该gpio。
int gpio_request(unsigned gpio, const char *label)
参数/ | 含义 |
---|---|
gpio | 待申请gpio序号 |
label | gpio命名 |
返回 | 成功返回0,失败返回负数 |
【3】释放已申请gpio
如果不使用该gpio,则需要释放,否则其他模块申请不到该gpio序号。
int gpio_free(unsigned gpio)
参数/ | 含义 |
---|---|
gpio | 待释放gpio序号 |
label | gpio命名 |
【4】设置gpio输入模式
int gpio_direction_input(unsigned gpio)
参数 | 含义 |
---|---|
gpio | 待设置gpio序号 |
返回 | 成功返回0,失败返回负数 |
【5】设置gpio输出模式
void gpio_set_value(unsigned gpio, int value)
参数 | 含义 |
---|---|
gpio | 待设置gpio序号 |
value | 默认输出状态 |
返回 | 成功返回0,失败返回负数 |
【6】读取 gpio状态
int gpio_get_value(unsigned int gpio)
参数 | 含义 |
---|---|
gpio | 待读取gpio序号 |
返回 | 成功返回gpio状态(1/0),失败返回负数 |
【7】设置 gpio状态
void gpio_set_value(unsigned int gpio, int value)
参数 | 含义 |
---|---|
gpio | 待设置gpio序号 |
value | 待设置值 |
【8】中断号映射
int gpio_to_irq(unsigned gpio)
参数 | 含义 |
---|---|
gpio | 待设置gpio序号 |
返回 | 成功返回中断号,失败返回负数 |
利用pinctrl和gpio子系统实现一个io翻转控制LED。firefly-rk3399板子上有两个LED,分别是power led和user led。linux自带的“gpio-leds”驱动是在linux led框架的基础上实现的,我们使用user led的gpio口,不使用led框架,直接使用gpio子系统实现io输出状态控制和读取。
【1】首先屏蔽rk3399原有的“led user”设备树,否则驱动会冲突。
leds {
compatible = "gpio-leds";
......
/*user {
* label = "firefly:yellow:user";
* linux,default-trigger = "ir-user-click";
* gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
* pinctrl-names = "default";
* pinctrl-0 = <&led_user>;
* default-state = "off";
*};
*/
};
【2】添加本次实现的驱动设备树。
在“kernel/arch/arm64/boot/dts/rockchip/rk3399-firefly.dtsi”
gpiopin{
compatible = "gpiopin"; /*驱动兼容属性*/
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;/* gpio描述,提供给pinctrl子系统使用*/
pinctrl-names = "default"; /*pin脚默认状态*/
pinctrl-0 = <&led_user>; /*直接使用rk原命名的pin脚设备树,也可以单独重新命名*/
default-state = "off"; /*默认gpio输出0*/
};
#源自rk3399-firefly-port.dtsi
&pinctrl {
leds {
led_power: led-power {
rockchip,pins = <2 27 RK_FUNC_GPIO &pcfg_pull_none>;
};
led_user: led-user {
rockchip,pins = <0 13 RK_FUNC_GPIO &pcfg_pull_none>;/*pin脚为普通gpio模式*/
};
};
修改完设备树,可以编译内核,更新板子boot区域(内核和设备树文件)。
设备驱动则是套用linux的platform驱动框架了,platform驱动框架结合设备树一起使用,通过设备节点和驱动匹配,然后注册驱动。另外,通过gpio子系统,控制一个gpio是非常简易的事情了,就类似使用STM32的标准库控制gpio。
static struct of_device_id of_gp0_ids[] = {
{.compatible = "gpiopin"}, /* 与节点设备树“compatible ”属性一致 */
{ }
};
static struct platform_driver gp0_driver = {
.driver = {
.owner = THIS_MODULE,
.name = DEV_NAME,
.of_match_table = of_match_ptr(of_gp0_ids),
},
.probe = gp0_probe,
.remove = gp0_remove,
};
module_platform_driver(gp0_driver); /* platform 驱动注册和注销 */
probe探测函数,驱动和设备树匹配时,会调用该函数,该函数完成gpio配置及字符驱动的创建过程。
static int gp0_probe(struct platform_device *pdev)
{
struct device *dev;
int ret = -1;
dev_t id = 0;
//gp0.nd = of_find_node_by_path("/gpiopin");
gp0.nd = pdev->dev.of_node;
if(gp0.nd == NULL)
{
printk("get node faileed\n");
return -1;
}
gp0.gpio = of_get_named_gpio(gp0.nd, "gpios", 0);/* 获取gpio序号,"gpios"为设备树的描述信息 */
if(gp0.gpio < 0)
{
printk("get gpio failed\n");
return -1;
}
ret = gpio_request(gp0.gpio, "gp0"); /* 申请GPIO */
if(ret < 0)
{
printk("gpio request failed\n");
return ret;
}
ret = gpio_direction_output(gp0.gpio, 0); /* output,low default */
if(ret<0)
{
printk("gpio set failed\n");
gpio_free(gp0.gpio);
return ret;
}
/*创建字符驱动过程*/
......
}
read/write函数主要调用gpio子系统函数读取或者设置gpio状态。
static ssize_t gp0_read(struct file *pfile, char __user *buf, size_t size, loff_t *offset)
{
int ret = 0;
struct gpiopin_dev *p;
char level = 0;
p = pfile->private_data;
level = gpio_get_value(p->gpio);
ret = copy_to_user(buf, &level, sizeof(level));
return ret;
}
static ssize_t gp0_write(struct file *pfile, const char __user *buf, size_t size, loff_t *offset)
{
int ret = 0;
struct gpiopin_dev *p;
char level = 0;
p = pfile->private_data;
ret = copy_from_user(&level, buf, size);
gpio_set_value(p->gpio, level);
return ret;
}
ifeq ($(KERNELRELEASE),)
KERNELDIR = /opt/rk3399/linux-sdk/linux-sdk/kernel
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *.ko .mod.o *.mod.c *.symvers
else
obj-m := gpiopin.o
endif
执行Makefile把驱动程编译为模块文件,生成gpiopin.ko文件,把文件通过NFS、U盘等传输至板子,系统起来后手动insmod到内核。驱动加载成功,会在“/dev”目录上生成设备文件。如图,“gp0”就是生成的驱动文件。
测试应用程序,执行“aarch64-linux-gnu-gcc gpiopin_app.c -o gpiopin_app”,生成可以执行文件“gpiopin_app”,把文件通过NFS、U盘等传输至板子“/home”目录,然后需更改文件属性,增加可执行属性。
int main(int argc, int **argv)
{
int fd;
uint8_t w_level = 1, r_level = 0;
fd = open("/dev/gp0", O_RDWR);
if(-1 == fd)
{
perror("open gpiopin failed\n");
return -1;
}
printf("open gpiopin driver succ\n");
for(;;)
{
write(fd, &w_level, 1);
read(fd, &r_level, 1);
printf("pin = %d\n", r_level);
w_level = !w_level;
sleep(1);
}
close(fd);
return 0;
}
https://github.com/Prry/rk3399
本文主要目的是理清pinctrl子系统和gpio子系统的使用,关于pinctrl和gpio内部的原理实现可以参考以下分析文章。pinctrl子系统和gpio子系统的驱动属于原厂“BSP”范畴,芯片原厂已经实现,如果是驱动工程师只需熟悉其接口(当然能够深入理解其实现原理和框架是最好的)即可进行驱动开发工作。
【1】pinctrl 子系统框架 https://blog.csdn.net/ZHONGkunjia/article/details/89873417
【2】pinctrl子系统分析 http://www.luyixian.cn/news_show_10718.aspx
【3】pinctrl子系统 https://blog.csdn.net/u012830148/article/details/80609337
【4】gpio子系统 http://www.wowotech.net/sort/gpio_subsystem