pinctrl&gpio
pinctrl&gpio子系统
pinctrl&gpio都属于设备树的一个“树枝”节点,用来更系统便捷地操作IO口。
使内核源码更好的适应芯片。
传统设备树控制IO:reg 属 性中<各个寄存器地址…>
添加子系统后的设备树:
(1)pinctrl:设置 PIN 的复用功能、电气特性(速度、驱动能力、上下拉等)。
(2)gpio:设置 GPIO为输入/输出,电平特性。
因为设备树就是描述板级信息的,为驱动文件提供外设需要的PIN,驱动文件只需要管好自己的驱动代码即可,PIN的复用和电气特性啥的 iomuxc 驱动会自动初始化,所以这又进一步封装了,其根本就是为了提高系统的可以移植性和兼容性。
pinctrl子系统
pinctrl 子系统源码目录为 drivers/pinctrl。
1、 imx6ull.dtsi 文件中一个节点iomuxc: iomuxc@020e0000。
2、基本上也是在imx6ull-alientek-emmc.dts中创建一个节点追加,不同的外设描述自己的PIN,如hog_1设备:
pinctrl_hog_1: hoggrp-1 {
fsl,pins = <
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059
MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058
>;
};
(可以在设备树中引用 C 语言中.h 文件中的内容。)
解析:
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
对应
0x0090 0x031C 0x0000 0x5 0x0 0x17059
对应
<mux_reg conf_reg input_reg mux_mode input_val> conf_val
mux_reg 复用寄存器偏移地址
conf_reg 配置寄存器偏移地址
input_reg input寄存器偏移地址(个别PIN会用到)
mux_mode 复用寄存器值
input_val input寄存器值
conf_val 配置寄存器值
pinctrl关于 PIN 配置入口函数,用来获取相关的属性信息:
添加pinctrl节点模板
接下来实际操作一下如何添加pinctrl设备:
pinctrl 设 备 树 绑 定 信 息 可 以 参 考 文 档
Documentation/devicetree/bindings/pinctrl/fsl,imx-pinctrl.txt。
1、创建对应的节点
imx6ull-alientek-emmc.dts文件,iomuxc 节点中的“imx6ul-evk”子节点下,添加“pinctrl_test”节点,注意!节点前缀一定要为“pinctrl_”。
2、添加“fsl,pins”属性
属性名字一定要为“fsl,pins”,pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息。
3、在“fsl,pins”属性中添加 PIN 配置信息
1 pinctrl_test: testgrp {
2 fsl,pins = <
3 MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config 是具体设置值*/
4 >;
5 };
gpio子系统
gpio 子系统用于初始化 GPIO 并且提供相应的 API 函数,Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
同样是imx6ull-alientek-emmc.dts 中的节点,如:
760 &usdhc1 {
761 pinctrl-names = "default", "state_100mhz", "state_200mhz";
762 pinctrl-0 = <&pinctrl_usdhc1>;
763 pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
764 pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
765 /* pinctrl-3 = <&pinctrl_hog_1>; */
766 cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
767 keep-power-in-suspend;
768 enable-sdio-wakeup;
769 vmmc-supply = <®_sd1_vmmc>;
770 status = "okay";
771 };
这个就是具体usdhc1的gpio设备节点,其中
pinctrl-0 = <&pinctrl_usdhc1>;
就是调用了pinctrl_那边儿的标签,实现 复用、电气 和 GPIO设置 的对接。
属性“cd-gpios”描述了 SD 卡的 CD 引脚使用的哪个 IO。
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
&gpio1”表示 CD 引脚所使用的 IO 属于 GPIO1 组,
“19”表示 GPIO1 组的第 19 号 IO,通过这两个值 SD 卡驱动程序就知道 CD 引脚使用了 GPIO1_IO19这 GPIO。
“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表
示高电平有效。
gpio 子系统 API 函数
gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。 配置完设备树之后, 我们只需要乖乖使用API函数就行了, 这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。
1、 gpio_request 函数
用于申请一个 GPIO 管脚。
int gpio_request(unsigned gpio, const char *label)
gpio:要申请的 gpio 标号,使用 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息,此函数会返回这个 GPIO 的标号。
label:给 gpio 设置个名字。
返回值: 0,申请成功;其他值,申请失败。
2、 gpio_free 函数
用于释放一个 GPIO 管脚。
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 默认输出值。
返回值: 0,设置成功;负值,设置失败。
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 节点模板
创建 test 设备的 GPIO 节点。
1、创建 test 设备节点。
2、添加 pinctrl 信息。
3、添加 GPIO 属性信息。
1 test {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_test>;
4 gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
5 };
与 gpio 相关的 OF 函数
Linux 内核提供了几个与 GPIO 有关的 OF 函数,可在驱动程序中调用。
1、 of_gpio_named_count 函数
用于获取设备树某个属性里面定义了几个 GPIO 信息,要注意的
是空的 GPIO 信息也会被统计到。
int of_gpio_named_count(struct device_node *np, const char *propname)
np:设备节点。
propname:要统计的 GPIO 属性。
返回值: 正值,统计到的 GPIO 数量;负值,失败。
2、 of_gpio_count 函数
专门统计“gpios”这个属性的 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 编号;负值,失败。
实验程序编写
没有封装的年代:通过设备树向 dtsled.c 文件传递相应的寄存器物理地址,然后在驱动文件中配置寄存器。
有了封装的年代:使用 pinctrl 和 gpio 子系统来完成 LED 灯驱动。
修改设备树文件
1、添加 pinctrl_ 节点(pinctrl子系统)
imx6ull-alientekemmc.dts文件下, iomuxc 节点–> imx6ul-evk 子节点–>pinctrl_led的子节点:
GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0。
1 pinctrl_led: ledgrp {
2 fsl,pins = <
3 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
4 >;
5 };
2、添加 LED 设备节点(gpio子系统)
根节点“/”–>节点gpioled( LED 灯节点):
led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。
gpio 子系统的 API 操作函数需要 GPIO 编号。
1 gpioled {
2 #address-cells = <1>;
3 #size-cells = <1>;
4 compatible = "atkalpha-gpioled";
5 pinctrl-names = "default";
6 pinctrl-0 = <&pinctrl_led>; //使用上边儿pinctrl_描述的复用和电气属性 pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点
7 led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
8 status = "okay";
9 }
3、检查 PIN 是否被其他外设使用
这一点非常重要!!!
如果 A 引脚在设备树中配置为了 I2C 的 SDA 信号,那么 A 引脚就不能再配置为 GPIO,否则的话驱动程序在申请 GPIO 的时候就会失败。(hai~剪不断,理还乱)
包括两个方面的检查:
①、检查 pinctrl 设置。
②、如果这个 PIN 配置为 GPIO 的话,检查这个 GPIO 有没有被别的外设使用。
这不巧了吗!!使用的LED灯GPIO1_IO3正好被NXP厂商的TSC电阻触摸屏接口占用了,幸运的是,原子的板子并没有使用这种外设屏幕,pinctrl和gpio的两个子系统全部注释屏蔽掉好了。
其他的找到一个屏蔽一个有关GPIO1_IO3引脚的。
最后,重新编译设备树:
make dtbs
//进入系统后看看有没有节点
cd /proc/device-tree
LED 灯驱动程序编写
gpioled.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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : gpioled.c
作者 : 左忠凯
版本 : V1.0
描述 : 采用pinctrl和gpio子系统驱动LED灯。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/7/13 左忠凯创建
***************************************************************/
#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; /* 设备节点 */
/*GPIO编号 */
int led_gpio; /* led所使用的GPIO编号*/
};
struct gpioled_dev gpioled; /* led设备 */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &gpioled; /* 设置私有数据 指向led设备结构体变量 */
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
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;
/*声明一个自己写的设备结构体的指针 指向打开文件时的私有变量,里边儿有led设备结构体变量*/
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]; /* 获取状态值 */
//用到了linux内核函数GPIO函数 调用led设备结构体变量中的gpio编号
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;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @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,
};
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int ret = 0;
/* 设置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;
} else {
printk("gpioled node find!\r\n");
}
/* 2、 获取设备树中的gpio属性,得到LED所使用的LED编号 用到了of_函数*/
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、创建类 */
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;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
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("zuozhongkai");
驱动程序总结:
以上的驱动程序和上一章中普通设备树的驱动差不多,只不过将设备GPIO初始化的一大部分释放给了设备树去完成,改为使用 Linux 内核提供的 API 函数,IO口在以后的开发中会用到很多,因此这样能更高效。
通过读取 filp 的 private_data 私有成员变量来得到设备结构体变量,也就是 gpioled。
再来总结一下驱动程序框架:
1、一些参数准备,设备结构体定义和声明等。
2、file_opreation结构体变量声明部分
3、驱动入口函数
(1)获取设备树中属性值
(2)设备初始化
(3)设备号、cdev、class、device等注册
4、驱动出口函数
(1)设备号、cdev、class、device等注销
测试 APP
同前边儿几章。
Makefile
同前边儿几章。
运行测试
都在 设备树点灯(完备版) 有详细说明。
为完成“天梯计划”,是时候学学图像识别了。