目录
Pinctrl子系统的概念
GPIO子系统的概念
定义自己的GPIO节点
GPIO子系统的函数
引脚号的确定
基于GPIO子系统的驱动程序
驱动程序
设备树修改
之前我们进行驱动开发的时候,对于硬件的操作是依赖于ioremap对寄存器的物理地址进行映射,以此来达到对操控硬件的目的,但是在实际的开发中,如果对引脚一个个的进行物理地址的映射并不现实,我们在这里使用Pinctrl子系统来完成对引脚的配置以及复用等。pinctrl子系统由BSP工程师来完成,驱动工程师使用这个子系统
Pinctrl子系统的概念
pinctrl中引入两个概念,一个是client device 一个是 controller,client device表示客户,也就是子系统的使用者,在这里主要指定状态,也就是引脚在不同的状态下使用的不同配置,而controller则主要是对应状态的具体详细配置
假如此时引脚的状态为“default”,那么就是对应pinctrl-0这个节点,那么引脚的功能(function)设置为uart0,groups设置为“u0rxtx”,“u0rtscts”。
client device的格式一般是一样的,但是controller这里的格式,不同的开发板有不同的格式,格式虽然不同,但是概念是一样的
GPIO子系统的概念
当我们使用pinctrl子系统设置了引脚为GPIO模式的时候,我们就可以使用GPIO子系统来设置引脚方向(输入还是输出)、读值──获得电平状态,写 值──输出高低电平。
在之前,我们一般是使用寄存器操控来控制引脚,而在这里我们使用GPIO子系统,完全是使用API来控制引脚,而不是设计寄存器操作,这样,不管是使用什么板都是兼容的
我们可以在设备树中指定GPIO引脚,然后在驱动程序中,直接使用对应的函数来获得 GPIO、设置 GPIO 方向、读 取/设置 GPIO 值。
几乎所有芯片的GPIO都是分组的,我们需要在自己所需要操控的GPIO分组中,找到自己所需要的。如下就是一个GPIO Controller,也就是一个GPIO分组,为GPIO分组1.一般来说,GPIO Controller是由厂家来实现的,我们只负责使用
我们暂时关注这两个属性 。“gpio-controller”表示这个节点是一个 GPIO Controller,它下面有很多引脚。 “#gpio-cells = ”表示这个控制器下每一个引脚要用 2 个 32 位的数 (cell)来描述。至于为什么需要使用两个cell来表示,一般第一个cell(第一个32位数)表示哪个引脚,第二个表示有效电平。
GPIO_ACTIVE_HIGH : 高电平有效
GPIO_ACTIVE_LOW : 低电平有效
定义自己的GPIO节点
定义 GPIO Controller 是芯片厂家的事,那么我们该如何定义自己的节点呢。
如下图所示,我在的这个节点中,通过gpios = 多少来设置我在这个节点当中使用的是哪个GPIO节点,一般的使用格式为:GPIO组 引脚 什么电平激活 , 下图中的表示为,此节点使用的是GPIO5组当中的3引脚,当电平为低电平的时候激活
(如下的设置为设备树当中的例子,只需要看 gpios = 这个属性即可)
一般来说,如果我们要定义自己的节点,那么格式如下:
compatible属性,作用是与驱动代码中的of_match_table进行匹配,当匹配成功之后,驱动代码才会调用probe,获取到引脚硬件信息。
pinctrl-name和pinctrl-0都是pinctrl的信息,用于设置引脚在不同状态下(pinctrl-name决定)的引脚复用和状态设置(开漏、上拉等)(pinctrl-0.....对应的状态决定)
led-gpio用来决定引脚信息,这个属性的节点为name-gpio,如下代码中可知,name为led,则在驱动代码中可以通过name来获得引脚信息
mynode{
compatible = ;
pinctrl-name = ;
pinctrl-0 = ;
led-gpio = ;
};
GPIO子系统的函数
当我们通过pinctrl子系统定义完了引脚信息后,我们应该如何在驱动代码中获取设备树中对应的引脚信息呢,我们使用如下这些GPIO 子系统的接口函数。
GPIO 子系统有两套接口:基于描述符的(descriptor-based)、老的(legacy)。通常情况下,我们使用前者,也就是基于描述符的。前者使用起来更为简单,后者还要基于引脚号。前者的函数都有前缀“gpiod_”,后者的函数都有前缀“gpio_”。
使用这些函数需要包含如下头文件
#include <linux/gpio/consumer.h> // descriptor-based
#include <linux/gpio.h> // legacy
常用函数如下
descriptor-based | legacy |
获得 GPIO | |
gpiod_get | gpio_request |
gpiod_get_index | |
gpiod_get_array | gpio_request_array |
devm_gpiod_get | |
devm_gpiod_get_index | |
devm_gpiod_get_array | |
设置方向 | |
gpiod_direction_input | gpio_direction_input |
gpiod_direction_output | gpio_direction_output |
读值、写值 | |
gpiod_get_value | gpio_get_value |
gpiod_set_value | gpio_set_value |
释放 GPIO | |
gpio_free | gpio_free |
gpiod_put | gpio_free_array |
gpiod_put_array | |
devm_gpiod_put | |
devm_gpiod_put_array |
我们假设在设备树中存在如下节点
foo_device {
compatible = "acme,foo";
...
led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */
<&gpio 16 GPIO_ACTIVE_HIGH>, /* green */
<&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */
power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
};
那我们我们该如何在驱动程序中获得如下的节点信息呢,使用如下方法
我们看red,第二个参数“led”也就是name,led-gpios,格式为<name>-gpio,那么led就是name,0表示获得第一个name为led的参数信息,GPIOD_OUT_HIGH表示设置为激活状态,但是并不一定是高电平,它的意思是逻辑输出,是一个逻辑值。如果引脚低电平点亮,则为0,反之则为1.
struct gpio_desc *red, *green, *blue, *power;
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);
引脚号的确定
如果我们需要使用旧的GPIO函数才操控引脚,那么我们就需要知道引脚号为多少,才能够操控对应的引脚,那么引脚号一般是怎么计算的呢。
一般来说,假如GPIO1的基引脚号为base,那么GPIO0的第n号引脚号为base+n
进入开发板目录:/sys/class/gpio ,可以看到如下GPIO分组
它表示GPIO的分组,我们进入gpioip128,得到如下
查看label属性
通过如上的地址“20ac000”这个地址通过查阅芯片手册或者设备树即可知道引脚号为128的是哪个GPIO组的基引脚号
如下为imx6ull.dtsi中的信息,可知20ac000这个地址表示为gpio5组的基引脚号,那么gpioip128就表示gpio5组了,那么可知,如果我们需要使用GPIO5的引脚3,那么此引脚的引脚号则为 128+3 = 131
在这里我们可以做一个测试,已知按键的电路图如下
可知它使用的是GPIO4的引脚14,我们可推算GPIO4组的基引脚号为96,那么引脚14就是96+14=110,引脚号110表示这个引脚,键入如下命令
echo 110 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio110/direction
cat /sys/class/gpio/gpio110/value
echo 110 > /sys/class/gpio/unexport
echo 110 > /sys/class/gpio/export 这个命令将编号为110的GPIO引脚导出为用户空间的设备,使其可以在用户空间进行操作。
echo in > /sys/class/gpio/gpio110/direction 这个命令设置编号为110的GPIO引脚的方向为输入("in")。
cat /sys/class/gpio/gpio110/value 这个命令读取编号为110的GPIO引脚的当前值。如果引脚连接的是一个按钮或传感器,它会输出0或1,表示引脚的状态是低电平或高电平。
echo 110 > /sys/class/gpio/unexport 这个命令将编号为110的GPIO引脚取消导出,使其不再在用户空间可用。
通过如上命令可以在用户空间读取按键值,但是如果此引脚被使用,则会出现如下提醒
基于GPIO子系统的驱动程序
驱动程序
如下函数为probe函数,只有当设备树与驱动成功配对后,才会调用此函数,主要的目的是在驱动设备中获取设备树当中的引脚等信息
如上两个函数主要为驱动和设备树节点对应成功后以及卸载时所需要设置的步骤,往下实现file_operation结构体中的相关提供给应用层的函数(这里实现open和write举例)
gpiod_set_value第二个参数传入的是逻辑值,只要传入的是1,那么它一定是激活引脚的电平,这个在dts的属性中可以设置引脚是低电平有效还是高电平有效,子系统会达到控制效果。
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
/* 1. 确定主设备号 */
static int major = 0;
static struct class *led_class;
static struct gpio_desc *led_gpio;
/* 3. 实现对应的open/read/write等函数,填入file_operations结构�? */
static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
char status;
//struct inode *inode = file_inode(file);
//int minor = iminor(inode);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);
/* 根据次设备号和status控制LED */
gpiod_set_value(led_gpio, status);
return 1;
}
static int led_drv_open (struct inode *node, struct file *file)
{
//int minor = iminor(node);
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 根据次设备号初始化LED */
gpiod_direction_output(led_gpio, 0);
return 0;
}
static int led_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 定义自己的file_operations结构�? */
static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open,
.read = led_drv_read,
.write = led_drv_write,
.release = led_drv_close,
};
/* 4. 从platform_device获得GPIO
* 把file_operations结构体告诉内核:注册驱动程序
*/
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
//int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 4.1 设备树中定义�? led-gpios=<...>; */
led_gpio = gpiod_get(&pdev->dev, "led", 0);
if (IS_ERR(led_gpio)) {
dev_err(&pdev->dev, "Failed to get GPIO for led\n");
return PTR_ERR(led_gpio);
}
/* 4.2 注册file_operations */
major = register_chrdev(0, "100ask_led", &led_drv);
led_class = class_create(THIS_MODULE, "100ask_led_class");
if (IS_ERR(led_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "led");
gpiod_put(led_gpio);
return PTR_ERR(led_class);
}
device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_led0 */
return 0;
}
static int chip_demo_gpio_remove(struct platform_device *pdev)
{
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
unregister_chrdev(major, "100ask_led");
gpiod_put(led_gpio);
return 0;
}
static const struct of_device_id ask100_leds[] = {
{ .compatible = "100ask,leddrv" },
{ },
};
/* 1. 定义platform_driver */
static struct platform_driver chip_demo_gpio_driver = {
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "100ask_led",
.of_match_table = ask100_leds,
},
};
/* 2. 在入口函数注册platform_driver */
static int __init led_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&chip_demo_gpio_driver);
return err;
}
/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函�? * 卸载platform_driver
*/
static void __exit led_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&chip_demo_gpio_driver);
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
设备树修改
imx6ull芯片,有对应的设备树生成的工具,使用 “Pins_Tool_for_i.MX_Processors_v6_x64.exe”可以生成对应的设备树文件。
把自动生成的信息放在设备树 arch/arm/boot/dts/100ask_imx6ull-14x14.dts 下
然后,在此设备树文件中定义自己的新节点,添加在根节点下
由于GPIO5 3 已经被用来做CPU指示灯,需要再设备树中关闭它
由于都是使用pinctrl来控制,我们在dts中搜索 MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03
在搜索框中的节点名,既可以反推找到使用该节点的节点
设置完后,在内核目录下使用 make dtbs 编译,设备树文件拷贝到网络文件系统中,更新设备树,安装编译后的驱动程序