目录
一、pinctrl子系统
1.pinctrl子系统简介
二、gpio 子系统
1.gpio 子系统简介
2.gpio 子系统API 函数
3.与gpio 相关的OF 函数
三、硬件原理图分析
四、实验程序编写
1. 修改设备树文件
2.LED 灯驱动程序编写
3.编写测试APP
五、运行测试
1. 编译驱动程序和测试APP
(1)编译驱动程序
(2)编译测试APP
2. 运行测试
上一章我们编写了基于设备树的LED 驱动,但是驱动的本质还是没变,都是配置LED 灯所使用的GPIO 寄存器,驱动开发方式和裸机基本没啥区别。Linux 是一个庞大而完善的系统,尤其是驱动框架,像GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班。Linux 内核提供了pinctrl 和gpio 子系统用于GPIO 驱动,本节我们就来学习一下如何借助pinctrl 和gpio 子系统来简化GPIO 驱动开发。
Linux 驱动讲究驱动分离与分层,pinctrl 和gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架,关于驱动的分离与分层我们后面会讲。本来pinctrl 和gpio 子系统应该放到驱动分离与分层章节后面讲解,但是不管什么外设驱动,GPIO 驱动基本都是必须的,而pinctrl 和gpio 子系统又是GPIO 驱动必须使用的,所以就将pintrcl 和gpio 子系统这一章节提前了。
我们先来回顾一下上一章是怎么初始化LED 灯所使用的GPIO,步骤如下:
①、修改设备树,添加相应的节点,节点里面重点是设置reg 属性,reg 属性包括了GPIO相关寄存器。
② 、获取reg 属性中IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并且初始化这两个寄存器,这两个寄存器用于设置GPIO1_IO03 这个PIN 的复用功能、上下拉、速度等。
③、在②里面将GPIO1_IO03 这个PIN 复用为了GPIO 功能,因此需要设置GPIO1_IO03这个GPIO 相关的寄存器,也就是GPIO1_DR 和GPIO1_GDIR 这两个寄存器。
总结一下,②中完成对GPIO1_IO03 这个PIN 的初始化,设置这个PIN 的复用功能、上下拉等,比如将GPIO_IO03 这个PIN 设置为GPIO 功能。③中完成对GPIO 的初始化,设置GPIO为输入/输出等。如果使用过STM32 的话应该都记得,STM32 也是要先设置某个PIN 的复用功能、速度、上下拉等,然后再设置PIN 所对应的GPIO。其实对于大多数的32 位SOC 而言,引脚的设置基本都是这两方面,因此Linux 内核针对PIN 的配置推出了pinctrl 子系统,对于GPIO的配置推出了gpio 子系统。
大多数SOC 的pin 都是支持复用的,比如I.MX6ULL 的GPIO1_IO03 既可以作为普通的GPIO 使用,也可以作为I2C1 的SDA 等等。此外我们还需要配置pin 的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如pin 功能冲突)。pinctrl 子系统就是为了解决这个问题而引入的,pinctrl 子系统主要工作内容如下:
①、获取设备树中pin 信息。
②、根据获取到的pin 信息来设置pin 的复用功能
③、根据获取到的pin 信息来设置pin 的电气特性,比如上/下拉、速度、驱动能力等。
pinctrl 子系统重点是设置PIN(有的SOC 叫做PAD)的复用和电气属性,如果pinctrl 子系统将一个PIN 复用为GPIO 的话,那么接下来就要用到gpio 子系统了。gpio 子系统顾名思义,就是用于初始化GPIO 并且提供相应的API 函数,比如设置GPIO为输入输出,读取GPIO 的值等。gpio 子系统的主要目的就是方便驱动开发者使用gpio,驱动开发者在设备树中添加gpio 相关信息,然后就可以在驱动程序中使用gpio 子系统提供的API函数来操作GPIO,Linux 内核向驱动开发者屏蔽掉了GPIO 的设置过程,极大的方便了驱动开发者使用GPIO。
对于驱动开发人员,设置好设备树以后就可以使用gpio 子系统提供的API 函数来操作指定的GPIO,gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。gpio 子系统提供的常用的API 函数有下面几个:
(1) 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,申请成功;其他值,申请失败。
(2)gpio_free 函数
如果不使用某个GPIO 了,那么就可以调用gpio_free 函数进行释放。函数原型如下:
void gpio_free(unsigned gpio)
函数参数和返回值含义如下:
gpio:要释放的gpio 标号。
返回值:无。
(3)gpio_direction_input 函数
此函数用于设置某个GPIO 为输入,函数原型如下所示:
int gpio_direction_input(unsigned gpio)
函数参数和返回值含义如下:
gpio:要设置为输入的GPIO 标号。
返回值:0,设置成功;负值,设置失败。
(4)gpio_direction_output 函数
此函数用于设置某个GPIO 为输出,并且设置默认输出值,函数原型如下:
int gpio_direction_output(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio:要设置为输出的GPIO 标号。
value:GPIO 默认输出值。
(5) gpio_get_value 函数
此函数用于获取某个GPIO 的值(0 或1),此函数是个宏,定义所示:
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
函数参数和返回值含义如下:
gpio:要获取的GPIO 标号。
返回值:非负值,得到的GPIO 值;负值,获取失败。
(6)gpio_set_value 函数
此函数用于设置某个GPIO 的值,此函数是个宏,定义如下
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio:要设置的GPIO 标号。
value:要设置的值。
返回值:无
我们定义了一个名为“gpio”的属性,gpio 属性描述了test 这个设备所使用的GPIO。在驱动程序中需要读取gpio 属性内容,Linux 内核提供了几个与GPIO 有关的OF 函数,常用的几个OF 函数如下所示:
(1)of_gpio_named_count 函数
of_gpio_named_count 函数用于获取设备树某个属性里面定义了几个GPIO 信息,要注意的是空的GPIO 信息也会被统计到,比如:
gpios = <0
&gpio1 1 2
0
&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 数量;负值,失败。
(2)of_gpio_count 函数
和of_gpio_named_count 函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的GPIO 数量,而of_gpio_named_count 函数可以统计任意属性的GPIO 信息,函数原型如下所示:
int of_gpio_count(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:正值,统计到的GPIO 数量;负值,失败。
(3)of_get_named_gpio 函数
此函数获取GPIO 编号,因为Linux 内核中关于GPIO 的API 函数都要使用GPIO 编号,此函数会将设备树中类似<&gpio5 7 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 编号;负值,失败。
见前文。
(1) 添加pinctrl 节点
I.MX6U-ALPHA 开发板上的LED 灯使用了GPIO1_IO03 这个PIN,打开imx6ull-alientekemmc.dts,在&iomuxc 节点的imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:
pinctrl_led: ledgrp{
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 //LED0
>;
};
将GPIO1_IO03 这个PIN 复用为GPIO1_IO03,电气属性值为0X10B0。
(2)添加LED 设备节点
在根节点“/”下创建LED 灯节点,节点名为“gpioled”,节点内容如下:
gpioled{
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>; //设置LED灯所使用的PIN对应的pinctrl节点
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
(3)检查PIN 是否被其他外设使用
半导体厂商提供的设备树是根据自己官方开发板编写的,很多PIN 的配置和我们所使用的开发板不一样。比如A 这个引脚在官方开发板接的是I2C 的SDA,而我们所使用的硬件可能将A这个引脚接到了其他的外设,比如LED 灯上,接不同的外设,A这个引脚的配置就不同。一个引脚一次只能实现一个功能,如果A引脚在设备树中配置为了I2C 的SDA 信号,那么A 引脚就不能再配置为GPIO,否则的话驱动程序在申请GPIO 的时候就会失败。检查PIN 有没有被其他外设使用包括两个方面:
①、检查pinctrl 设置。
②、如果这个PIN 配置为GPIO 的话,检查这个GPIO 有没有被别的外设使用。
在本章实验中LED 灯使用的PIN 为GPIO1_IO03,因此先检查GPIO_IO03 这个PIN 有没有被其他的pinctrl 节点使用,在imx6ull-alientek-emmc.dts 中找到如下内容:
pinctrl_tsc: tscgrp {
fsl,pins = <
/*MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0xb0
MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0xb0
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0xb0
MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0xb0*/
>;
};
因为本章实验我们将GPIO1_IO03 这个PIN 配置为了GPIO,所以还需要查找一下有没有其他的外设使用了GPIO1_IO03,在imx6ull-alientek-emmc.dts 中搜索“gpio1 3”,找到如下内容:
&tsc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_tsc>;
//xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
measure-delay-time = <0xffff>;
pre-charge-time = <0xfff>;
status = "okay";
};
设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb 文件启动Linux 系统。启动成功以后进入“/proc/device-tree”目录中查看“gpioled”节点是否存在。
新建名为“6_gpioled”文件夹,然后在6_gpioled 文件夹里面创建vscode 工程,工作区命名为“gpioled”。工程创建好以后新建gpioled.c 文件,在gpioled.c 里面输入如下内容:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define GPIOLED_CNT 1 //设备号个数
#define GPIOLED_NAME "gpioled" //名字
#define LEDOFF 0 //关灯
#define LEDON 1 //开灯
//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编号
};
struct gpioled_dev gpioled; //led设备
//打开设备
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled;//设置私有数据
return 0;
}
//从设备读取数据
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
//向设备写数据
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 error!\n");
return -EFAULT;
}
ledstat = databuf[0]; // 获取状态值
if(ledstat == LEDON)
{
gpio_set_value(dev->led_gpio, 0);//打开LED
}
else if(ledstat == LEDOFF)
{
gpio_set_value(dev->led_gpio, 1);//关闭LED
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
//驱动入口函数
static int __init led_init(void)
{
int ret = 0;
//获取设备树中的属性数据
//1.获取设备节点:gpioled
gpioled.nd = of_find_node_by_path("/gpioled");
if (gpioled.nd == NULL)
{
printk("gpioled node can not found!\r\n");
return -EINVAL;
}
else
{
printk("gpioled node has been found!\r\n");
}
//2.获取设备树中的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);
//3.设置GPIO1_IO03为输出,并且输出高点平,默认关闭LED灯
ret = gpio_direction_output(gpioled.led_gpio, 1);
if (ret < 0)
{
printk("can't set gpio!\r\n");
}
//注册字符设备驱动
//1.创建设备号
if (gpioled.major) //定义了设备号
{
gpioled.devid = MKDEV(gpioled.major, 0);
register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
}
else //没有定义设备号
{
alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME); //申请设备号
gpioled.major = MAJOR(gpioled.devid); // 获取主设备号
gpioled.minor = MINOR(gpioled.devid); // 获取次设备号
}
printk("gpioled major = %d, minor = %d\r\n", gpioled.major, gpioled.minor);
//2.初始化cdev
gpioled.cdev.owner = THIS_MODULE;
cdev_init(&gpioled.cdev, &gpioled_fops);
//3.添加一个cdev
cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
//4.初始化class
gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
if (IS_ERR(gpioled.class))
{
return PTR_ERR(gpioled.class); //返回错误码
}
//5.创建设备号
gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
if (IS_ERR(gpioled.device))
{
return PTR_ERR(gpioled.device); //返回错误码
}
return 0;
}
//驱动出口函数
static void __exit led_exit(void)
{
//注销字符设备
cdev_del(&gpioled.cdev); // 删除cdev
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);//注销设备号
device_destroy(gpioled.class, gpioled.devid);
class_destroy(gpioled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ssz");
同上一节。
编写Makefile 文件,本章实验的Makefile 文件和第四十章实验基本一样,只是将obj-m 变量的值改为gpioled.o,Makefile 内容如下所示:
KERNELDIR := /home/ssz/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := gpioled.o
build : kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
输入make命令编译。
输入如下命令编译测试ledApp.c 这个测试程序:
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp 这个应用程序。
将上一小节编译出来的gpioled.ko 和ledApp 这两个文件拷贝到rootfs/lib/modules/4.1.15 目录中,重启开发板,进入到目录lib/modules/4.1.15 中,输入如下命令加载gpioled.ko 驱动模块:
从上图可以看出,gpioled 这个节点找到了,并且GPIO1_IO03 这个GPIO 的编号为3。驱动加载成功以后就可以使用ledApp 软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/gpioled 1 //打开LED 灯
./ledApp /dev/gpioled 0 //关闭LED 灯
rmmod gpioled.ko