嵌入式驱动开发中pinctrl和GPIO子系统使用频率非常高,其中pinctrl子系统主要用于复用和配置引脚,GPIO子系统用于设置GPIO的输入/输出,向引脚写入数据或者从引脚读取数据。一个引脚可以复用为多种不同的功能,因此要使用GPIO子系统首先要先把引脚配置为GPIO功能。下面将分为两部分记录如何基于pinctrl子系统和GPIO子系统编写按键驱动程序。
本实验基于imx6ull开发板
1、向设备树添加内容
(1)使用pinctrl子系统复用引脚为GPIO功能
基于pinctrl和GPIO子系统进行驱动开发本质上还是基于设备树的总线设备驱动模型,但是此时设备树中节点代码的格式要遵守对应芯片厂家提供的pinctrl和GPIO子系统的格式,这样他们的软件才能正确运行。
pinctrl子系统对应的引脚的复用、配置,因此它必然要获取引脚相关的寄存器信息和配置信息,因此要在设备树中pinctrl对应的节点下添加子节点用于描述要用到的引脚。
使用NXP提供的i.MX Pins Tool v6工具,可以方便的得到配置pinctrl子系统设备树节点的代码。
在前面的按键驱动笔记中已经给出了按键的原理图,根据引脚名去芯片参考手册可以查到引脚属于GPIO1_IO18,打开i.MX Pins Tool v6工具后,在GPIO1组中找到IO18,并点击选中(打勾),如下图所示。
在图中的右侧可以看到右侧iomux节点下有一个子节点
点击查看代码
BOARD_InitPins: BOARD_InitPinsGrp { /*!< Function assigned for the core: Cortex-A7[ca7] */
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x000010B0
>;
};
点击查看代码
pinctrl_key: keyGrp { /*!< Function assigned for the core: Cortex-A7[ca7] */
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x000010B0
>;
};
在上述代码中,fsl,pins是芯片提供的pinctrl子系统规定的属性名MX6UL_PAD_UART1_CTS_B__GPIO1_IO18表示了UART1_CTS_B引脚对应的一组寄存器,它是一个宏定义,猜测它应该包含了引脚对应的复用寄存器、配置寄存器地址。具体的分析过程参考正点原子驱动开发文档中的pinctrl和gpio子系统章节。
(2)使用GPIO子系统
向iomux添加子节点是为了使用引脚的相关寄存器信息,接下来还要在设备树的根节点下添加设备对应的子节点(因为我们刚才在iomux节点下添加的子节点并不会被转换为platform_device),并引用iomux节点下的内容。这个过程与基于设备树的总线设备模型是类似,只不过我们没有直接在设备节点下写上寄存器信息,而是引用了定义好的节点的信息。
下面是在根节点下创建设备节点,并引用前面在iomux下添加的子节点
点击查看代码
pinctrl_gpio_mykey {
#address-cells = <1>;
#size-cells = <1>;
compatible = "mykey_driver"; /*用于驱动匹配*/
pinctrl-names = "default"; /*pinctrl-前缀的属性是给pinctrl子系统使用的,-gpio后缀的属性是给GPIO子系统使用的*/
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /*这里设置了低电平有效,因此引脚物理状态和逻辑值相反*/
status = "okay";
};
在上面代码中,pinctrl-names = "default";表示它只有一个默认状态default,对应这个状态pinctrl子系统所使用的节点信息由pinctrl-0指定,而pinctrl-0又引用了pinctrl_key节点,因此最终pinctrl子系统使用pinctrl_key节点中定义的属性来进行寄存器的设置从而实现引脚的复用、配置。
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>表示使用gpio1组的IO18,并且低电平有效,其中-gpio后缀是必须的,因此GPIO子系统会根据这个后缀找到对应的属性,&gpio1表示使用GPIO1组,它是一个GPIO控制器,里面包含了这一组GPIO的寄存器地址信息,根据18就可以找到GPIO1_IO18的寄存器地址信息。
status = "okay";就是使能这个节点,让它能够被内核转换为platform_device,如果设置为disable,就是屏蔽这个节点,相当于注释掉了
添加好设备树节点信息后,在设备树文件中全局搜索GPIO1_IO18和gpio1 18,查看是否有其它节点也使用了这个引脚,如果有就注释掉,否则会导致引脚冲突。
2、基于pinctrl和GPIO子系统的驱动程序代码编写
按照规定的格式写好设备树文件后,在驱动代码中并不需要调用pinctrl子系统,因为在内核加载设备树文件时,就已经根据设备树中节点的信息转换为platform_device并初始化好引脚的复用、配置了。我们要做的其实就是调用GPIO子系统提供的函数对GPIO进行方向设置、读写GPIO即可,下面是代码
点击查看代码
/*
本次实验是基于pinctrl子系统和gpio子系统进行按键驱动程序编写
platform_device由设备树生成并由pinctrl子系统所使用的引脚进行复用为gpio
在该文件中实现:
(1)定义platform_drvier结构体变量并填充内容
(a)实现probe函数:从设备树获取gpio引脚信息、注册驱动、创建设备类class,创建设备节点
(b)实现remove函数:释放gpio引脚、删除设备节点、删除设备类class、删除节点
(2)实现file_operations结构体的内容,并注册驱动
(a)实现open、read、write、release函数
*/
#include "asm/uaccess.h"
#include "linux/export.h"
#include "linux/mod_devicetable.h"
#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>
static int major; //主设备号
static struct class *key_class;
static struct gpio_desc *key_gpio; //在使用gpdio子系统时,需要通过gpio-desc这个结构体来获取引脚
/*实现file_operations结构体
实现open read 和close函数
*/
ssize_t key_read(struct file *file, char __user *user, size_t size, loff_t *offset)
{
/*读取按键状态并传递到用户空间*/
char state;
int err;
// printk("%s %s %d\n",__FILE__, __FUNCTION__, __LINE__ );
/*
在原理图中,按键按下时是低电平,但是因为设备树中设置了低电平有效,所以引脚物理状态和gpiod_get_value的返回值相反
因此按键按下时返回的是1,不按时返回0
*/
state = gpiod_get_value(key_gpio); //读取GPIO状态
// state = ~state; //直接取反,并不会从1变成0。通过输出发现,不按下时返回0,取反后输出255,按下时返回1,取反后是254因为1的二进制是0b00000001,取反后是0b11111110,对应10进制就是254
state ^= (1); //使用异或取反
printk("state = %d\n", state);
err = copy_to_user(user, &state, 1);
return 1; //读取按键的状态,只需要一个字节的char字符表示就行
}
int key_open(struct inode *node, struct file *file)
{
/*应用程序打开按键对应的设备文件时会调用这个函数,此时相当于要开始使用设备了,那么需要执行一些初始化操作
配置所用的引脚为输入
*/
printk("%s %s %d",__FILE__, __FUNCTION__, __LINE__ );
gpiod_direction_input(key_gpio); //设置GPIO为输入
return 0;
}
int key_close(struct inode *node, struct file *file)
{
/*可以将引脚恢复为高电平输出状态*/
printk("%s %s %d",__FILE__, __FUNCTION__, __LINE__ );
gpiod_direction_output(key_gpio, 0); //设置gpio为输出,并默认输出高电平
return 0;
}
struct file_operations key_oprs = {
.owner = THIS_MODULE,
.open = key_open,
.release = key_close,
.read = key_read,
};
/*实现platform_driver结构体,主要填充两个函数
(1)probe,设备和驱动相互匹配时调用
(2)remove函数 卸载设备时调用,如果直接卸载驱动,那么每个匹配的设备都会执行这个函数一次
*/
int key_probe(struct platform_device *pdev)
{
/*设备和驱动匹配时执行,因此在这里要注册设备的操作函数,创建类,创建设备,要使用GPIO子系统获取引脚*/
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*获取按键对应的gpio*/
key_gpio = gpiod_get(&pdev->dev, "key", 0);
if (IS_ERR(key_gpio)) {
dev_err(&pdev->dev, "Failed to get GPIO for key\n");
return PTR_ERR(key_gpio);
}
/*向内核注册file_operations结构体*/
major = register_chrdev(0, "key_driver", &key_oprs);
//创建class
key_class = class_create(THIS_MODULE, "key_class");
if (IS_ERR(key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "key_driver");
gpiod_put(key_gpio);
return PTR_ERR(key_class);
}
/*创建设备节点*/
device_create(key_class, NULL, MKDEV(major, 0), NULL, "key_0");
return 0;
}
int key_remove(struct platform_device *pdev)
{
/*与probe函数的操作相反*/
device_destroy(key_class, MKDEV(major, 0));
class_destroy(key_class);
unregister_chrdev(major, "key_driver");
gpiod_put(key_gpio); //释放引脚
return 0;
}
/*创建匹配表,驱动能够与设备树中下列节点匹配*/
static const struct of_device_id keys_match_table[] = {
{
.compatible = "mykey_driver"
}
};
struct platform_driver key_drv = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "mykey", //driver中的.name字段必须加上,不然运行时报错,暂时不懂得为什么
.of_match_table = keys_match_table,
}
};
static int __init key_drv_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&key_drv);
printk("err = %d", err);
return err;
}
static void __exit key_drv_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&key_drv);
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
在代码编写过程遇到的一些细节问题都已经写在注释里了,这里再总结一下:
(1)~是按位取反,如果只是把某一位或某几位取反,应该用异或^;
(2)在设备树中定义gpio1 18 是低电平有效,因此gpiod_get_value函数读取到的GPIO逻辑值和真实的引脚物理值相反;
(3)copy_to_user(user, &state, 1)中的state在定义时不需要声明为volatile,否则会产生警告,而且传入的是地址,因此每次都是从state的内存处取值,不用担心被编译器优化导致数据与预期不符;
(4)在platform_driver的driver字段中的name字段必须赋值,否则在编译时不报错,但是驱动运行时会产生空指针错误,因为驱动和设备匹配时首先比较platform_driver中的driver中的name和platform_device中的override;
(5)在驱动中读取按键状态时,不用防抖,驱动应该只提供读取功能,防抖应该是在应用程序中实现。
应用程序代码如下,在里面调用了按键驱动和led驱动,使用按键实现灯的亮灭操作
点击查看代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char** argv)
{
int fd_button, fd_led;
char val;
char state;
/*判断参数*/
if(argc != 3)
{
printf("Usage: %s <buton_dev> <led_dev>\n", argv[0]);
}
fd_button = open(argv[1], O_RDWR);
fd_led = open(argv[2], O_RDWR);
if(fd_button == -1 || fd_led == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
/*读文件, 控制led灯*/
state = 0;
write(fd_led, &state, 1);
while(1)
{
read(fd_button, &val, 1);
if(val == 0)
{
usleep(10000);
read(fd_button, &val, 1);
if(val == 0)
{
state ^= (1);
}
write(fd_led, &state, 1);
// printf("led state: %d", state);
sleep(1);
}
}
close(fd_button);
close(fd_led);
return 0;
}
至此,基于pinctrl子系统和GPIO子系统的按键驱动程序已经完成,可以看到,使用pinctrl和GPIO子系统可以简化驱动开发工作,而不用直接操作寄存器。但是不管如何,直接操作寄存器是驱动开发本质,只有掌握了本质才能用好别人的工具,要知其然,更要知其所以然。
标签:__,驱动程序,引脚,pinctrl,key,GPIO,子系统 From: https://www.cnblogs.com/starstxg/p/18183819