AlienTek的IMX6ULL开发板自带了一个按键和一个LED灯,这两个外设分别接在两个不同的GPIO端口,各自独立。我们想把按键作为灯的开关,通过按压按键来控制灯的亮灭,即灯亮时按一下则灯灭,灯灭时按一下则灯亮,这里的“按一下”是指按键从被按下到抬起后的整个过程。对开发板SOC而言,按键的物理状态可通过与其相连的GPIO端口的电平值得到反映,GPIO本身可作为中断控制器,GPIO输入电平的值或者电平值的变化都可以作为中断触发信号,中断的处理过程由Linux中断子系统作统一管理。我们可以利用Linux框架下的中断来实现前面所说的按键开关功能。
[1] Hardware层
① 电路原理
首先要明确按键和灯的硬件信息。LED灯和按键的硬件原理图主要如下。据图我们可得知LED0的阴极和GPIO1_03连接,阳极电平为3.3V,所以GPIO1_03输出低电平时灯亮,否则灯灭。按键与引脚UART1_CTS连接,这是哪个GPIO端口?查阅IMX6ULL的参考手册如下图,可以得知UART1_CTS复用GPIO1_18的功能,我们可以通过设置复用寄存器,把该引脚的功能配成GPIO,那么根据原理图可知,当按键按下时GPIO1_18输入低电平,平时输入高电平。
② 硬件的设备树表示
当前的Linux内核框架下,开发板的硬件信息都是基于设备树呈现的,所以我们要把以上的硬件外设写成设备树的形式以供内核读取。我们直接在设备树SOC的根目录下创建2个独立节点,分别表示LED和按键,如下。LED的节点名称为gpioled,其节点属性pinctrl-0指向pinctrl_led节点,它描述了GPIO1_03的复用、电气属性等信息,字段MX6UL_PAD_GPIO1_IO03__GPIO1_IO03可在imx6ul-pinfunc.h中查到,它是由5个十六进制数组成的宏定义,表示该端口配置为GPIO功能,即GPIO1_IO03。后面跟的数0x10B0表示了GPIO1_IO03的电气配置参数,它的各位含义可参照参考手册中的寄存器IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03。属性字段led-gpio描述了gpioled节点使用到的GPIO信息,“&gpio1 3”就是指GPIO1_03,GPIO_ACTIVE_LOW表示“低电平有效”,“有效”在这里是指LED灯亮,由于GPIO输出低电平时灯亮,所以我们说低有效。这样LED的设备树节点用到的信息就完成了。按键的节点名称为key,它用到的引脚GPIO信息和LED的引脚信息写法类似,无须赘述。需要关注的是,字段interrupt-parent意思是中断继承,因为我们要利用按键GPIO的中断功能,所以需要在这里添加中断相关的信息,中断继承的内容是&gpio1,GPIO1本身是中断控制器,这里的意思是按键连接的GPIO的中断触发由GPIO1中断控制器处理。后面的interrupts字段,18表示GPIO1_18在GPIO1中断控制器的中断索引,IRQ_TYPE_EDGE_RISING表示GPIO输入电平上升沿触发中断,也就是按键弹起时中断应得到响应。
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0
>;
};
pinctrl_key: keygrp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080
>;
};
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-gpioled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_RISING>;
status = "okay";
};
[2] Driver层
我们在driver层定义一个字符设备,整个驱动框架按Linux字符设备框架搭建,主要的几个任务有从设备树获取定义好的硬件信息、按键GPIO申请中断、定义中断服务函数、按键消抖、LED亮灭控制等。我们设想的开关控制灯亮灭的逻辑是:按键被按压完成后触发中断,在中断服务函数中更新灯当前的亮灭状态,driver把该状态上报到app层,app层读取LED状态并据此发送相应的控制命令到driver,driver根据命令控制LED灯GPIO的输出电平从而控制亮灭。在文件操作结构体file_operations中,我们可以实现read和ioctl这两个接口,前者用来向应用层报告“按键被按”这个消息,后者用来实现LED灯的控制方法。
对于机械按键而言,往往需要考虑按键抖动问题,在按键刚按完后立即读对应的GPIO电平,由于抖动存在常会出现随机的电平值,时高时低,难以说明按键的真正状态。一般需要在按键动作结束后间隔几毫秒至几十毫秒,再去获取GPIO电平,此时基本上能得到真实的按键状态,即消抖。但是在中断处理函数中最好不要做任何延时操作,我们可以利用内核定时器的特性,当按键动作后中断被触发,在中断处理函数中激活定时器,这样定时器将在给定的时间后调用定时器处理函数,我们在定时器处理函数中再去读取按键GPIO电平。
① 设备结构
我们首先定义设备结构体,如下,把能用到的几乎所有信息都写进去了。成员ledIndex和keyIndex表示LED和按键对应的GPIO编号,这是通过系统提供的of函数从设备树外设节点获取得到的,后面要用于灯的控制及中断申请等操作。成员keyRelease标志按键的动作是否完成,完成为true,否则为false。成员irqHandler将来要指向我们自定义的中断处理函数。
typedef struct _keyGpioledDev {
struct cdev dev; // 字符设备
dev_t devid; // 设备号
int major; // 主设备号
int minor; // 次设备号
#define DEVNAME "key_gpioled" // 设备名
#define CLASSNAME "key_gpioled" // 设备类名
struct class *devclass; // 设备类
struct device *devdevice; // 设备
struct device_node *devnode_led; // LED设备节点
int ledIndex; // LED GPIO编号
#define SETLEDON (_IO(0xEF, 0x1)) // 开灯命令
#define SETLEDOFF (_IO(0xEF, 0x2)) // 关灯命令
struct device_node *devnode_key; // 按键设备节点
int keyIndex; // 按键GPIO编号
int irqNum; // 中断号
bool keyRelease; // 按键动作完成标志
irqreturn_t (*irqHandler)(int, void *); // 按键GPIO中断服务函数指针
struct timer_list timer; // 按键消抖定时器
} keyGpioledDev;
② 驱动模块初始化
驱动初始化时主要处理3件事情,即注册字符设备(注册cdev、创建类和设备等)、从设备树获取外设节点(申请GPIO使用权、申请中断等)和初始化定时器(指定定时器触发函数)。获取外设节点的GPIO信息后,首先要调用函数gpio_request获得GPIO的使用权限。对于按键GPIO,先调用gpio_direction_input将GPIO1_18配置成输入模式,然后调用gpio_to_irq根据GPIO编号得到中断号,并指定该中断号关联的中断服务函数,再调用request_irq向系统申请按键的GPIO中断,中断触发方式要按上升沿配置,并且要给这个接口传入设备实例指针,这样在中断服务函数中就可以获取到设备信息。在中断处理中我们激活定时器,让它在一定时间后执行定时器触发函数。在定时器触发函数中,我们应该读取GPIO1_18的电平值,如果为高电平则表示按键已经弹起,按键动作完成了,否则表示按键还处于被按压的状态,按键动作未完成。
/* 驱动模块初始化 */
static int __init keyGpioled_init(void) {
/* 注册字符设备(注册cdev、创建类和设备等) */
if((setupCdev()) != 0) {
printk("setupCdev failed\n");
return -1;
}
/* 从设备树获取外设节点(申请GPIO使用权、申请中断等) */
if((getDevnode()) != 0) {
printk("getDevnode failed\n");
return -1;
}
/* 初始化定时器(指定定时器触发函数) */
timerInit();
return 0;
}
/* 中断服务函数 */
irqreturn_t key0_irqHandler(int irq, void *dev_id) {
keyGpioledDev *temp_dev = (keyGpioledDev *)dev_id;
printk("key0_irqHandler triggered\n");
temp_dev->timer.data = (volatile long)dev_id; // 让timer.data指向设备实例,可以在定时器触发函数中使用设备信息
mod_timer(&temp_dev->timer, jiffies + msecs_to_jiffies(10)); // 激活定时器,10ms后运行触发函数
return IRQ_RETVAL(IRQ_HANDLED);
}
/* 定时器触发函数 */
void timer_func(unsigned long arg) {
keyGpioledDev *temp_dev = (keyGpioledDev *)arg;
unsigned char value;
value = gpio_get_value(temp_dev->keyIndex); // 读取按键GPIO电平值
if(value == 1) {
printk("key released\n");
temp_dev->keyRelease = true; // 按键释放
} else if(value == 0) {
printk("key not released\n");
} else if(value < 0) {
printk("gpio_get_value error\n");
}
}
③ 系统调用接口实现
我们主要实现read和ioctl。在read中,我们先判断设备实例中的keyRelease标志是否为true,是则设置灯控制标志,如果当前灯亮则标志为关灯,否则标志为开灯,然后将keyRelease和灯控制标志一起通过copy_to_user报告给上层。在ioctl中,根据上层发送来的灯控制命令调用gpio_set_value设置LED GPIO的输出电平,实现开灯或关灯,这里的灯控制命令就是上面设备结构体中定义的SETLEDON和SETLEDOFF。
static ssize_t keyGpioled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
unsigned long ret;
keyGpioledDev *temp_dev = filp->private_data;
if(temp_dev->keyRelease == true) { // 按键动作完成
updateLedOnFlag(); // 设置灯控制标志
memset(flag, 0, sizeof(flag));
flag[0] = keyReleaseFlag;
flag[1] = ledOnFlag;
ret = copy_to_user(buf, flag, sizeof(flag)); // 报告给上层
if(ret != 0) {
printk("copy_to_user failed\n");
return -1;
}
temp_dev->keyRelease = false; // 清除按键动作完成标志,等待下一次按键动作完成
} else
return -EINVAL;
return 0;
}
static long keyGpioled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
keyGpioledDev *temp_dev = filp->private_data;
switch(cmd) {
case SETLEDON:
printk("ioctl cmd SETLEDON\n");
gpio_set_value(temp_dev->ledIndex, 0);
break;
case SETLEDOFF:
printk("ioctl cmd SETLEDOFF\n");
gpio_set_value(temp_dev->ledIndex, 1);
break;
default:
printk("unknown ioctl cmd\n");
break;
}
return 0;
}
④ driver完整代码
#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/fs.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/* 设备结构体 */
typedef struct _keyGpioledDev {
struct cdev dev; // 字符设备
dev_t devid; // 设备号
int major; // 主设备号
int minor; // 次设备号
#define DEVNAME "key_gpioled" // 设备名
#define CLASSNAME "key_gpioled" // 设备类名
struct class *devclass; // 设备类
struct device *devdevice; // 设备
struct device_node *devnode_led; // LED设备节点
int ledIndex; // LED GPIO编号
#define SETLEDON (_IO(0xEF, 0x1)) // 开灯命令
#define SETLEDOFF (_IO(0xEF, 0x2)) // 关灯命令
struct device_node *devnode_key; // 按键设备节点
int keyIndex; // 按键GPIO编号
int irqNum; // 中断号
bool keyRelease; // 按键动作完成标志
irqreturn_t (*irqHandler)(int, void *); // 按键GPIO中断服务函数指针
struct timer_list timer; // 按键消抖定时器
} keyGpioledDev;
keyGpioledDev keyGpioled; // 设备实例
unsigned char keyReleaseFlag = 1; // 按键完成标志
unsigned char ledOnFlag; // 开灯标志
unsigned char flag[2];
static int keyGpioled_open(struct inode *inode, struct file *filp) {
filp->private_data = &keyGpioled; // 设置文件私有数据指向设备实例
printk("keyGpioled_open\n");
return 0;
}
static int keyGpioled_release(struct inode *inode, struct file *filp)
{
printk("keyGpioled_release\n");
return 0;
}
/* 灯亮灭控制标志切换 */
void updateLedOnFlag(void) {
if(!ledOnFlag)
ledOnFlag = 1;
else
ledOnFlag = 0;
}
static ssize_t keyGpioled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
unsigned long ret;
keyGpioledDev *temp_dev = filp->private_data;
if(temp_dev->keyRelease == true) { // 按键动作完成
updateLedOnFlag(); // 设置灯控制标志
memset(flag, 0, sizeof(flag));
flag[0] = keyReleaseFlag;
flag[1] = ledOnFlag;
ret = copy_to_user(buf, flag, sizeof(flag)); // 报告给上层
if(ret != 0) {
printk("copy_to_user failed\n");
return -1;
}
temp_dev->keyRelease = false; // 清除按键动作完成标志,等待下一次按键动作完成
} else
return -EINVAL;
return 0;
}
static long keyGpioled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
keyGpioledDev *temp_dev = filp->private_data;
switch(cmd) {
case SETLEDON:
printk("ioctl cmd SETLEDON\n");
gpio_set_value(temp_dev->ledIndex, 0);
break;
case SETLEDOFF:
printk("ioctl cmd SETLEDOFF\n");
gpio_set_value(temp_dev->ledIndex, 1);
break;
default:
printk("unknown ioctl cmd\n");
break;
}
return 0;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.open = keyGpioled_open,
.read = keyGpioled_read,
.unlocked_ioctl = keyGpioled_ioctl,
.release = keyGpioled_release,
};
static int setupCdev(void) {
if((alloc_chrdev_region(&keyGpioled.devid, 0, 1, DEVNAME)) != 0) {
printk("alloc_chrdev_region failed\n");
return -1;
}
keyGpioled.major = MAJOR(keyGpioled.devid);
keyGpioled.minor = MINOR(keyGpioled.devid);
printk("major dev num = %d, minor dev num = %d\n", keyGpioled.major, keyGpioled.minor);
keyGpioled.dev.owner = THIS_MODULE;
cdev_init(&keyGpioled.dev, &fops);
if((cdev_add(&keyGpioled.dev, keyGpioled.devid, 1)) != 0) {
printk("cdev_add failed\n");
return -1;
}
keyGpioled.devclass = class_create(THIS_MODULE, CLASSNAME);
if(IS_ERR(keyGpioled.devclass)) {
printk("class_create failed\n");
cdev_del(&keyGpioled.dev);
return PTR_ERR(keyGpioled.devclass);
}
keyGpioled.devdevice = device_create(keyGpioled.devclass, NULL, keyGpioled.devid, NULL, DEVNAME);
if(IS_ERR(keyGpioled.devdevice)) {
printk("device_create failed\n");
class_destroy(keyGpioled.devclass);
cdev_del(&keyGpioled.dev);
return PTR_ERR(keyGpioled.devdevice);
}
return 0;
}
static int getLednode(void) {
keyGpioled.devnode_led = of_find_node_by_path("/gpioled"); // 获取gpioled设备树节点
if(keyGpioled.devnode_led == NULL) {
printk("of_find_node_by_path gpioled failed\n");
return -1;
}
keyGpioled.ledIndex = of_get_named_gpio(keyGpioled.devnode_led, "led-gpio", 0); // 获取LED GPIO编号
if(keyGpioled.ledIndex < 0) {
printk("of_get_named_gpio led-gpio failed\n");
return -1;
} else
printk("get ledIndex (gpio num): %d\n", keyGpioled.ledIndex);
if((gpio_request(keyGpioled.ledIndex, "led0")) != 0) { // 申请GPIO1_03的使用权限
printk("gpio_request led0 failed\n");
return -1;
}
if((gpio_direction_output(keyGpioled.ledIndex, 1)) < 0) { // 默认关闭LED灯
printk("unable to set gpio: %d\n", keyGpioled.ledIndex);
gpio_free(keyGpioled.ledIndex);
return -1;
}
ledOnFlag = 0; // 默认灯不亮
return 0;
}
/* 定时器触发函数 */
void timer_func(unsigned long arg) {
keyGpioledDev *temp_dev = (keyGpioledDev *)arg;
unsigned char value;
value = gpio_get_value(temp_dev->keyIndex); // 读取按键GPIO电平值
if(value == 1) {
printk("key released\n");
temp_dev->keyRelease = true; // 按键释放
} else if(value == 0) {
printk("key not released\n");
} else if(value < 0) {
printk("gpio_get_value error\n");
}
}
/* 中断服务函数 */
irqreturn_t key0_irqHandler(int irq, void *dev_id) {
keyGpioledDev *temp_dev = (keyGpioledDev *)dev_id;
printk("key0_irqHandler triggered\n");
temp_dev->timer.data = (volatile long)dev_id; // 让timer.data指向设备实例,可以在定时器触发函数中使用设备信息
mod_timer(&temp_dev->timer, jiffies + msecs_to_jiffies(10)); // 激活定时器,10ms后运行触发函数
return IRQ_RETVAL(IRQ_HANDLED);
}
static int getKeynode(void) {
keyGpioled.devnode_key = of_find_node_by_path("/key");
if(keyGpioled.devnode_key == NULL) {
printk("of_find_node_by_path key failed\n");
return -1;
}
keyGpioled.keyIndex = of_get_named_gpio(keyGpioled.devnode_key, "key-gpio", 0);
if(keyGpioled.keyIndex < 0) {
printk("of_get_named_gpio key-gpio failed\n");
return -1;
} else
printk("get keyIndex (gpio num): %d\n", keyGpioled.keyIndex);
if((gpio_request(keyGpioled.keyIndex, "key0")) != 0) {
printk("gpio_request failed\n");
return -1;
}
if((gpio_direction_input(keyGpioled.keyIndex)) != 0) { // 按键GPIO配置为输入
printk("gpio_direction_input failed\n");
gpio_free(keyGpioled.keyIndex);
return -1;
}
keyGpioled.irqNum = gpio_to_irq(keyGpioled.keyIndex); // 获取GPIO1_18对应的中断号
if(keyGpioled.irqNum < 0) {
printk("gpio_to_irq failed\n");
return -1;
} else
printk("get gpio irq num: %d\n", keyGpioled.irqNum);
keyGpioled.irqHandler = key0_irqHandler;
/* 申请中断 */
if((request_irq(keyGpioled.irqNum, keyGpioled.irqHandler, IRQF_TRIGGER_RISING, "key0", &keyGpioled)) != 0) {
printk("request_irq failed\n");
return -1;
}
keyGpioled.keyRelease = false; // 默认没有按键动作
return 0;
}
static int getDevnode(void) {
if((getLednode()) != 0) {
printk("getLednode failed\n");
return -1;
}
if((getKeynode()) != 0) {
printk("getKeynode failed\n");
return -1;
}
return 0;
}
void timerInit(void) {
init_timer(&keyGpioled.timer);
keyGpioled.timer.function = timer_func;
printk("timerInit\n");
}
/* 驱动模块初始化 */
static int __init keyGpioled_init(void) {
/* 注册字符设备(注册cdev、创建类和设备等) */
if((setupCdev()) != 0) {
printk("setupCdev failed\n");
return -1;
}
/* 从设备树获取外设节点(申请GPIO使用权、申请中断等) */
if((getDevnode()) != 0) {
printk("getDevnode failed\n");
return -1;
}
/* 初始化定时器(指定定时器触发函数) */
timerInit();
return 0;
}
static void __exit keyGpioled_exit(void) {
del_timer_sync(&keyGpioled.timer); // 删除定时器
free_irq(keyGpioled.irqNum, &keyGpioled); // 释放申请到的中断
gpio_free(keyGpioled.keyIndex); // 释放申请到使用权限的GPIO
gpio_free(keyGpioled.ledIndex);
device_destroy(keyGpioled.devclass, keyGpioled.devid);
class_destroy(keyGpioled.devclass);
cdev_del(&keyGpioled.dev);
unregister_chrdev_region(keyGpioled.devid, 1);
}
module_init(keyGpioled_init);
module_exit(keyGpioled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fym");
[3] App层
我们在应用层使用read轮询是否有内核上报按键完成标志和灯控制标志,一旦有则根据灯控制标志来调用ioctl的具体命令,向内核发送LED控制信号,主要的实现如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <linux/ioctl.h>
#define SETLEDON (_IO(0xEF, 0x1))
#define SETLEDOFF (_IO(0xEF, 0x2))
int main(int argc, char *argv[])
{
int fd;
int ret = 0;
char *filename;
unsigned char data[2];
if (argc != 2) {
printf("error usage!\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can not open file %s\n", filename);
return -1;
}
while (1) {
ret = read(fd, data, sizeof(data));
if(ret < 0) {
} else {
printf("keyReleaseFlag: %d, ledOnFlag: %d\n", data[0], data[1]);
if(data[0]) {
if(data[1]) {
if((ioctl(fd, SETLEDON)) != 0) {
printf("ioctl error\n");
break;
}
} else {
if((ioctl(fd, SETLEDOFF)) != 0) {
printf("ioctl error\n");
break;
}
}
} else {
printf("wrong keyReleaseFlag\n");
break;
}
}
}
close(fd);
return ret;
}
[4] 实践结果
编写驱动模块的Makefile,编译更新后的设备树、内核驱动和上层应用,得到新的内核模块、dtb及可执行文件,把它们导入到IMX6ULL开发板系统中的相应目录,重启开发板。首先加载驱动模块,可看到内核打印信息:
对照driver代码可知这些日志位置是在驱动模块加载函数中,显示该字符设备的主设备号是248,LED对应GPIO的编号是3,按键GPIO编号是18,这些都和硬件信息相符,获得的中断号是46。查看系统中的中断列表:
可见最后一列中断名称为key0的那行,正是我们申请的GPIO中断,最左侧是中断号46,目前被触发次数为0,因为我们还没按下按键。运行上层应用对应的可执行文件,然后按下开发板上的按键KEY0数次,可以看到每完成一次按键动作,应用层收到一次标志上报,同时现象上LED亮灭逐次翻转:
与此对应的,内核打印了相同次数的中断触发、按键完成及灯控制命令等信息:
根据日志信息也可验证驱动调用顺序,每次都是先进入中断服务key0_irqHandler,再进入定时器触发函数打印按键释放信息,然后打印来自上层的灯控制命令信息。这时再次查看中断信息:
发现46号中断的触发次数为13,对应上面key0_irqHandler triggered日志次数也是13,但实际上刚才按下按键的次数为10,对应应用层日志也是打印10次,这里就体现了前面所说的按键抖动问题,由于抖动一次按键动作可能引起了不止一次的GPIO电平上升过程,而定时器延时消抖的作用使得按键次数和灯状态翻转次数保持了一致。