关键词:pinctrl gpio子系统 RK3399 firefly-rk3399 LED
前言
前面的嵌入式Linux驱动文章中,都还是配置gpio的寄存器,本质上与裸机开发没有区别。这种方式比较繁琐,且容易出现问题,针对这种情况,Linux提供了pinctrl和gpio子系统用于引脚的驱动
pinctrl子系统针对PIN的配置,gpio子系统针对GPIO配置,如果pinctrl子系统将某个引脚复用为GPIO,那么接下来就可以使用gpio子系统配置该引脚的功能
pinctrl子系统
pinctrl子系统的主要工作内容:
- 获取设备树中的PIN信息
- 根据获取到的PIN信息配置PIN的复用功能
- 根据获取到的PIN信息配置PIN的电气特性,比如上/下拉,速度,驱动能力等
也就是说只需要在设备树中配置好PIN的相关属性,其初始化工作pinctrl子系统来完成,用户不需要关注具体的实现过程,pinctrl子系统的源码在driver/pinctrl
路径下
pinctrl子系统配置引脚
实际还是去修改设备树,在设备树中配置好引脚属性,pinctrl子系统会自动对相应引脚进行配置,用实例说明配置过程,本文这里使用firefly-rk3399这个开发板,firefly官方给出了他们更改过的dts文件,以此作为参考,与前面文章相同,这里配置GPIO0的B5引脚
- 创建对应的pinctrl节点
在Linux源代码的/arch/arm64/boot/dts/rockchip/
路径下找到rk3399-firefly.dts文件,打开后找到&pinctrl
节点(实际上pinctrl节点是定义在rk3399.dtsi内的),在该节点下新建子节点,如下:
C
&pinctrl{
pinctrlled: pinctrlled{
rockchip,pins = <0 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
- 添加rockchip,pins属性
pinctrl子系统是通过属性来获取PIN的配置信息的,对于rk3399而言属性是rockchip,pins,如上代码所示 - 在rockchip,pins属性中添加配置信息,如上代码所示,其中
0代表引脚所在的组
RK_PB5
定义在/include/dt-bindings/pinctrl/rockchip.h
内,代表B5引脚
RK_FUNC_GPIO
是将引脚复用为GPIO
&pcfg_pull_none
代表配置引脚为浮空状态
注:不同SOC的配置方式不同,SOC官方会给出参考
gpio子系统
如果pinctrl子系统将某个引脚复用为GPIO,那么接下来就可以使用gpio子系统,利用gpio子系统提供的API函数来操作、使用引脚
gpio子系统配置引脚
设备树内配置好pinctrl后,可以继续配置gpio子系统要用的引脚属性,
- 创建设备节点
同样在firefly-rk3399.dts
内,创建引脚的设备节点,在根节点下创建,如下
C
gpioled{
};
- 给设备节点添加pinctrl信息
在节点内添加pinctrl子系统关于所用到的引脚的信息,如下
C
gpioled{
pinctrl-names = "default";
pinctrl-0 = <&pinctrlled>;
};
其中
pinctrl-names
属性默认为default
pinctrl-0
属性为设备引脚的pinctrl节点
- 添加设备其他属性信息
包括compatible等信息,完成如下
C
gpioled{
#address-cells = <1>;
#size-cells = <1>;
compatible = "pinctrl-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrlled>;
led-gpio = <&gpio0 RK_PB5 GPIO_ACTIVE_HIGH>;
status = "okay";
};
API函数
设置好设备树以后,就可以使用gpio子系统提供的API函数来操作指定的引脚。gpio子系统实际还是去读写SOC的寄存器,只是这个过程由系统去完成,不需要像之前一样在驱动代码里面使用readl或writel去完成
gpio子系统提供的常用的API函数有:
gpio_request()
函数
用于申请一个引脚,在使用某个引脚前,必须先申请一个,函数原型
C
int gpio_request(unsigned gpio,const char *label)
其中
label-给申请的gpio设置一个名字
gpio-要申请的gpio标号,使用of_get_gpio()
函数从设备树获取指定GPIO属性信息,此函数会返回gpio标号
返回值-0表示成功,其他值表示申请失败
- gpio_free()函数
释放gpio,如果某个引脚不使用了,可以使用此函数释放,函数原型
C
void gpio_free(unsigned gpio)
其中
gpio-要释放的gpio标号
gpio_direction_input()
函数
用于设置引脚为输入
C
int gpio_direction_input(unsigned gpio)
其中
gpio-要设置为输入的gpio标号
返回值-0 设置成功,负值,设置失败
gpio_direction_output()
函数
用于设置引脚为输出
C
int gpio_direction_output(unsigned gpio, int value)
其中
gpio-要设置为输出的gpio标号
value-输出值
返回值-0 设置成功,负值 设置失败
gpio_get_value()
函数
用于获取某个引脚的值(0或1),此函数是一个宏,定义:
C
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
其中
gpio-要获取值的gpio标号
返回值-非负值,得到的引脚值,负值,获取失败
gpio_set_value()
函数
用于设置某个引脚的值(0或1),此函数是一个宏,定义:
C
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio,int value)
其中
gpio-要设置值的gpio标号
value-要设置的值
与gpio相关的OF函数
前面设备树相关的文章中,提到过Linux内核提供了一系列函数来获取设备树文件中的设备节点或节点属性信息,这些函数名称都有统一的前缀of_
,因此这一系列的函数也被称为OF函数,常用与gpio相关的OF函数有如下几个:
of_gpio_named_count()
函数
用于获取设备树某个属性里面定义了几个GPIO信息,即使是空的GPIO信息也会统计到,函数原型
C
int of_gpio_named_count(struct device_node *np, const char *propname)
其中
np-设备节点
propname-要统计的GPIO属性
返回值-数量,负值 统计失败
of_gpio_count()
函数
用于统计gpios这个属性的GPIO数量,函数原型
C
int of_gpio_count(struct device_node *np)
其中
np-设备节点
返回值-数量,负值 统计失败
of_get_named_gpio()
函数
Linux内核中关于GPIO的API函数要使用GPIO编号,此函数会将设备树中类似<&gpio0 RK_PA6 GPIO_ACTIVE_LOW>的属性信息转为对应的GPIO编号,函数原型
C
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index)
其中
np-设备节点
propname-要统计的GPIO属性名
index-GPIO索引
返回值-正值 GPIO编号,负值 获取失败
pinctrl和gpio子系统驱动引脚控制LED实例
pinctrl和gpio子系统避免了在驱动程序中直接配置SOC寄存器的步骤,再加上SOC官方提供了dts文件等,使得在dts文件内配置pinctrl和gpio子系统不再繁琐。使用pinctrl和gpio子系统驱动引脚控制LED实例中,采用platform结构,platform_driver中使用gpio子系统提供的API和of函数,具体参考源码
修改设备树
按照上面pinctrl和gpio子系统配置引脚的描述修改设备树,在dts文件内
&pinctrl
节点下添加子节点
C
&pinctrl{
pinctrlled:pinctrlled{
rockchip,pins = <0 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
在根节点下添加设备节点
C
gpioled{
#address-cells = <1>;
#size-cells = <1>;
compatible = "pinctrl-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrlled>;
led-gpio = <&gpio0 RK_PB5 GPIO_ACTIVE_HIGH>; //默认高电平
status = "okay";
};
将更改过的dts文件编译为dtb文件,与内核文件打包到一起,参考RK3399移植u-boot&linux内核&根文件系统
内核启动后可以在/proc/device-tree/
路径下看到设备节点,如图
修改platform_driver程序
使用gpio子系统提供的API和of函数,如下
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 <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
/*************************************
*FileName:pinctrlgpio_leddriver.c
*Fuc: RK3399,GPIO0B_5 driver
*
*Author: PineLiu
************************************/
#define CHAR_CNT 1 //设备数量
#define LED_NAME "gump_pinctrlgpio_led" //设备名称
#define LEDON 1 //点亮
#define LEDOFF 0 //熄灭
//声明设备结构体(该设备包含的属性)
struct platform_leddev{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //设备节点类 主动创建设备节点文件用
struct device *device; //设备
int major; //主设备号
struct device_node *nd; //设备节点
int myled; //led等引脚标号
};
//声明一个外部设备,本文这里是字符设备
struct platform_leddev platform_pinctrlgpio_led;
//LED打开/关闭
void led_switch(u8 sta)
{
if(sta == LEDON){
gpio_set_value(platform_pinctrlgpio_led.myled,1);
}else if(sta == LEDOFF){
gpio_set_value(platform_pinctrlgpio_led.myled,0);
}
}
//===========================================================================
//以下实现设备的具体操作函数:open函数、read函数、write函数和release函数
//===========================================================================
//打开设备
static int led_open(struct inode *inode, struct file *filp)
{
filp -> private_data = &platform_pinctrlgpio_led; //设置私有数据
return 0;
}
//从设备读取数据
static ssize_t led_read(struct file *filp,char __user *buf,
size_t cnt,loff_t *offt)
{
return 0;
}
//向设备写数据
//filp:设备文件,表示打开的文件描述
//buf :保存着要向设备写入的数据
//cnt :要写入的数据长度
//offt:相对于文件首地址的偏移
//
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;
retvalue = copy_from_user(databuf,buf,cnt);
if(retvalue < 0){
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0]; //获取状态值
if(ledstat == LEDON){
led_switch(LEDON);
}else if(ledstat == LEDOFF){
led_switch(LEDOFF);
}
return 0;
}
//关闭,释放设备
//filp: 要关闭的设备文件描述
//
static int led_release(struct inode *inode,struct file *filp)
{
return 0;
}
//==========================================================================
//以下实现的设备具体函数与内核的对应函数的映射
//==========================================================================
//映射设备操作函数
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
//==========================================================================
//内核模块相关
//==========================================================================
//probe函数
static int led_probe(struct platform_device *dev)
{
printk("led driver and device was matched!\r\n");
//===============初始化连接LED的引脚
//===========注册设备
//创建设备号
if(platform_pinctrlgpio_led.major){ //定义了设备号
platform_pinctrlgpio_led.devid = MKDEV(platform_pinctrlgpio_led.major,0);
register_chrdev_region(platform_pinctrlgpio_led.devid,CHAR_CNT,LED_NAME);
}else{ //没有定义设备号,要向系统申请设备号
alloc_chrdev_region(&(platform_pinctrlgpio_led.devid),0,CHAR_CNT,LED_NAME);
platform_pinctrlgpio_led.major = MAJOR(platform_pinctrlgpio_led.devid);
}
printk("platform_pinctrlgpio_led major = %d\r\n",platform_pinctrlgpio_led.major);
//初始化cdev
platform_pinctrlgpio_led.cdev.owner = THIS_MODULE;
cdev_init(&platform_pinctrlgpio_led.cdev,&led_fops);
//向系统注册设备
cdev_add(&platform_pinctrlgpio_led.cdev,platform_pinctrlgpio_led.devid,CHAR_CNT);
//创建类
platform_pinctrlgpio_led.class = class_create(THIS_MODULE,LED_NAME);
if(IS_ERR(platform_pinctrlgpio_led.class)){
return PTR_ERR(platform_pinctrlgpio_led.class);
}
//创建设备
platform_pinctrlgpio_led.device = device_create(platform_pinctrlgpio_led.class,NULL,platform_pinctrlgpio_led.devid,NULL,LED_NAME);
if(IS_ERR(platform_pinctrlgpio_led.device)){
return PTR_ERR(platform_pinctrlgpio_led.device);
}
//初始化引脚
//获取节点
platform_pinctrlgpio_led.nd = of_find_node_by_path("/gpioled"); //与设备树文件内设备名称相同
if(platform_pinctrlgpio_led.nd == NULL){
printk("gpioleddts node can not found!\r\n");
return -EINVAL;
}else{
printk("gpioleddts node has been found!\r\n");
}
//引脚名称
platform_pinctrlgpio_led.myled = of_get_named_gpio(platform_pinctrlgpio_led.nd,"led-gpio",0); //这里的led-gpio,在设备树该节点下定义,指向所要设置的引脚
if(platform_pinctrlgpio_led.myled < 0){
printk("cannot get gpio\r\n");
return -EINVAL;
}
//申请引脚
gpio_request(platform_pinctrlgpio_led.myled,"leddrive");
//设置引脚方向
gpio_direction_output(platform_pinctrlgpio_led.myled,1);
return 0;
}
//remove函数
static int led_remove(struct platform_device *dev)
{
gpio_set_value(platform_pinctrlgpio_led.myled,1);
gpio_free(platform_pinctrlgpio_led.myled);
//注销设备
cdev_del(&platform_pinctrlgpio_led.cdev);
unregister_chrdev_region(platform_pinctrlgpio_led.devid,CHAR_CNT);
device_destroy(platform_pinctrlgpio_led.class,platform_pinctrlgpio_led.devid);
class_destroy(platform_pinctrlgpio_led.class);
return 0;
}
//匹配表
static const struct of_device_id led_of_match[] = {
{.compatible = "pinctrl-gpioled"}, //与设备树内该设备节点的compatibel相同
{/*Sentinel*/}
};
//platform_driver
static struct platform_driver led_driver = {
.driver = {
.name = "gpioled", //设备树中节点名称
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
//驱动模块加载函数
static int __init led_init(void)
{
return platform_driver_register(&led_driver);
}
//驱动模块卸载函数
static void __exit led_exit(void)
{
platform_driver_unregister(&led_driver);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gump");
将上面代码放到ubuntu18.04内编译为ko文件,Makefile文件内容为:
bash
KERNELDIR:=/home/gump/rk3399/kernel-develop-4.4
CURRENT_PATH:=$(shell pwd)
obj-m:=pinctrlgpio_leddriver.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译命令:(可根据自己的交叉编译工具更改)
bash
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-`
得到ko文件放到嵌入式Linux系统内,并安装内核模块:
bash
sudo insmod -f pinctrlgpio_leddriver.ko
安装后在/dev
目录下会出现该设备文件夹,如图
由于默认引脚是高电平,安装内核模块后,LED处于点亮状态
测试结果
测试代码如下
C
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/*************************************************
*FileName:platformTest.c
*Func: GPIO driver module test
*
*Author: pineliu
* **********************************************/
#define LEDOFF 0
#define LEDON 1
int main(int argc,char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if (fd < 0){
printf("file %s open failed!\r\n",argv[1]);
return -1;
}
databuf[0] = atol(argv[2]);
retvalue = write(fd,databuf,sizeof(databuf));
if(retvalue < 0){
printf("led Control Error!\r\n");
close(fd);
return -1;
}
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\r\n",argv[1]);
return -1;
}
return 0;
}
将测试程序进行编译:
bash
aarch64-linux-gnu-gcc pinctrlgpioTest.c -o pinctrlgpioTest
放到嵌入式Linux系统内,并赋予可执行权限
bash
chmod +x pinctrlgpioTest
测试,熄灭led:
bash
sudo ./pinctrlgpioTest /dev/gump_pinctrlgpio_led 0
实验证明可行
总结
使用SOC的引脚,就要对引脚进行配置,通常是通过配置引脚的寄存器来实现,向前面的文章一样通过直接写寄存器的方式固然可以实现对引脚的配置,但是比较繁琐,容易出错。Linux系统使用pinctrl和gpio子系统来解决这个问题,其实本质还是配置寄存器,只不过过程由SOC厂商按照系统要求去实现了,用户就可以借助pinctrl和gpio子系统方便的配置引脚。
对于SOC的引脚而言,pinctrl子系统负责配置其复用功能和电气特性,gpio子系统负责实现其他功能,实现过程用户不需要关心,只需要按要求更改设备树就可以了