嵌入式Linux驱动开发:i.MX6ULL按键中断驱动(非阻塞IO)
嵌入式Linux驱动开发:i.MX6ULL按键中断驱动(非阻塞IO)
概述
本文档详细介绍了在i.MX6ULL开发板上实现按键中断驱动的完整过程。该驱动程序实现了非阻塞IO操作,允许用户空间应用程序通过poll
系统调用高效地监控按键状态变化,而无需进行忙等待。本文档结合了提供的源代码和设备树文件,详细解释了驱动程序的各个组成部分及其工作原理。
源码仓库
- 仓库地址: https://gitee.com/dream-cometrue/linux_driver_imx6ull
理论基础
1. 非阻塞IO (Non-blocking I/O)
在传统的阻塞IO模型中,当应用程序调用read
等系统调用时,如果数据不可用,进程会进入睡眠状态,直到数据就绪。这在某些场景下是高效的,但在需要同时监控多个文件描述符或进行其他工作的场景下,会导致资源浪费。
非阻塞IO通过在open
系统调用时指定O_NONBLOCK
标志来实现。在这种模式下,如果read
调用时没有数据可读,系统调用会立即返回一个-EAGAIN
错误,而不是让进程睡眠。这允许应用程序立即处理其他任务,或者使用poll
或select
系统调用来监控多个文件描述符的状态。
2. poll
系统调用
poll
系统调用是实现非阻塞IO的核心机制。它允许应用程序在一个系统调用中监控多个文件描述符的读、写和异常事件。poll
会阻塞直到任何一个被监控的文件描述符就绪,或者超时。
在驱动程序中,poll
操作通过file_operations
结构体中的.poll
成员函数实现。驱动程序需要调用poll_wait
函数将当前进程添加到一个等待队列中,并返回一个描述当前文件描述符状态的掩码。
3. 等待队列 (Wait Queue)
等待队列是Linux内核中用于进程同步的机制。它允许一个或多个进程在某个条件满足之前进入睡眠状态。当条件满足时,另一个进程或中断处理程序可以唤醒等待队列中的所有进程。
在本驱动程序中,我们使用等待队列来实现poll
功能。当应用程序调用poll
时,驱动程序会将当前进程添加到等待队列中。当按键状态发生变化时,中断处理程序会通过定时器唤醒等待队列中的进程。
4. 定时器 (Timer)
定时器用于在指定的时间后执行一段代码。在本驱动程序中,定时器用于实现按键消抖。当按键中断发生时,我们启动一个20ms的定时器。在定时器超时后,我们读取按键的实际状态,以避免由于机械按键的抖动导致的误触发。
5. 原子变量 (Atomic Variables)
原子变量是内核中用于在多处理器系统中实现无锁同步的机制。对原子变量的操作是不可分割的,保证了在并发访问时的数据一致性。在本驱动程序中,我们使用原子变量keyvalue
和release
来在中断上下文和进程上下文之间安全地传递按键值和释放状态。
设备树 (Device Tree)
设备树文件imx6ull-alientek-emmc.dts
定义了开发板上的硬件配置。与本驱动程序相关的部分是/key
节点:
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
compatible = "alientek,key"
: 指定了该节点的兼容性字符串,驱动程序会根据这个字符串来匹配设备。pinctrl-0 = <&pinctrl_key>
: 引用了iomuxc
节点中的pinctrl_key
子节点,用于配置GPIO1_IO18引脚的复用功能和电气特性。key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>
: 指定了按键连接到gpio1
控制器的第18号引脚,且为高电平有效。interrupt-parent = <&gpio1>
: 指定了中断控制器为gpio1
。interrupts = <18 IRQ_TYPE_EDGE_BOTH>
: 指定了中断号为18,触发类型为上升沿和下降沿(双边沿触发)。
在驱动程序中,我们通过of_find_node_by_path("/key")
找到这个节点,并通过of_get_named_gpio
函数获取按键的GPIO号。
驱动程序分析
1. 数据结构
struct key_desc
该结构体用于描述一个按键设备:
struct key_desc
{char name[10]; // 按键名称int gpio; // GPIO号int irqnum; // 中断号unsigned char value; // 按键值irqreturn_t (*handler)(int, void *); // 中断处理函数
};
struct imx6uirq_dev
该结构体是驱动程序的核心,包含了所有必要的状态信息:
struct imx6uirq_dev
{dev_t devid; // 设备号int major; // 主设备号int minor; // 次设备号struct cdev cdev; // 字符设备struct class *class; // 设备类struct device *device; // 设备struct device_node *key_nd; // 设备树节点struct key_desc key[KEY_NUM]; // 按键描述数组struct timer_list timer; // 定时器atomic_t keyvalue; // 按键值(原子变量)atomic_t release; // 释放状态(原子变量)wait_queue_head_t r_wait; // 读等待队列
};
2. 字符设备操作
imx6uirq_open
该函数在应用程序打开设备文件时被调用。它将private_data
指针指向imx6uirq
全局变量,以便后续操作可以访问驱动程序的状态。
static int imx6uirq_open(struct inode *inode, struct file *filp)
{filp->private_data = &imx6uirq;return 0;
}
imx6uirq_release
该函数在应用程序关闭设备文件时被调用。在本驱动程序中,它不执行任何特定操作。
static int imx6uirq_release(struct inode *inode, struct file *filp)
{return 0;
}
imx6uirq_read
该函数实现了read
系统调用。它根据O_NONBLOCK
标志决定是阻塞还是非阻塞读取。
- 如果指定了
O_NONBLOCK
标志,且没有按键释放事件,则立即返回-EAGAIN
。 - 否则,调用
wait_event_interruptible
将进程添加到等待队列中,直到有按键释放事件发生。 - 一旦有事件发生,将按键值复制到用户空间缓冲区,并重置释放状态。
ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{struct imx6uirq_dev *dev = filp->private_data;u8 keyvalue, release;int ret = 0;if (filp->f_flags & O_NONBLOCK){if (atomic_read(&dev->release) == 0){return -EAGAIN;}}else{wait_event_interruptible(dev->r_wait, atomic_read(&dev->release));}keyvalue = atomic_read(&dev->keyvalue);release = atomic_read(&dev->release);if (release){ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));if (ret){ret = -EFAULT;goto fail_copy_user;}atomic_set(&dev->release, 0);}else{ret = -EAGAIN;goto fail_key_unrelease;}return sizeof(keyvalue);
}
imx6uirq_poll
该函数实现了poll
系统调用。它调用poll_wait
将当前进程添加到r_wait
等待队列中,并检查release
原子变量。如果release
为真,则返回POLLIN | POLLRDNORM
,表示文件描述符可读。
static unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{int mask = 0;struct imx6uirq_dev *dev = filp->private_data;poll_wait(filp, &dev->r_wait, wait);if (atomic_read(&dev->release)){mask = POLLIN | POLLRDNORM;}return mask;
}
3. 中断处理
key0_handler
该函数是按键中断的处理程序。它在按键状态发生变化时被调用。由于中断处理程序执行在中断上下文中,不能进行睡眠操作,因此我们不能在这里直接读取GPIO状态。相反,我们启动一个定时器,在定时器的回调函数中读取GPIO状态。
static irqreturn_t key0_handler(int irq, void *filp)
{struct imx6uirq_dev *dev = filp;dev->timer.data = (volatile long)filp;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}
timer_func
该函数是定时器的回调函数。它在定时器超时后被调用。在本函数中,我们读取按键的GPIO状态:
- 如果按键被按下(GPIO为低电平),则设置
keyvalue
为KEY0VALUE
。 - 如果按键被释放(GPIO为高电平),则设置
release
为1,并唤醒等待队列中的所有进程。
static void timer_func(unsigned long arg)
{struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){atomic_set(&dev->keyvalue, dev->key[0].value);}else{atomic_set(&dev->release, 1);}if (atomic_read(&dev->release)){wake_up_interruptible(&dev->r_wait);}
}
4. 驱动程序初始化和退出
imx6uirq_init
该函数在模块加载时被调用。它完成了以下初始化工作:
- 分配设备号(动态或静态)。
- 初始化字符设备,并将其添加到系统中。
- 创建设备类和设备文件。
- 调用
key_init1
函数初始化按键。 - 初始化定时器。
- 初始化原子变量和等待队列。
static int __init imx6uirq_init(void)
{// ... (设备号分配、字符设备注册、设备类和设备创建)ret = key_init1(&imx6uirq);if (ret < 0){goto fail_key_init;}timer1_init(&imx6uirq);atomic_set(&imx6uirq.keyvalue, KEYINVA);atomic_set(&imx6uirq.release, 0);init_waitqueue_head(&imx6uirq.r_wait);return 0;
}
key_init1
该函数负责初始化按键硬件。它通过设备树API获取按键的GPIO号,并请求GPIO、配置为输入模式,然后获取中断号并注册中断处理程序。
int key_init1(struct imx6uirq_dev *dev)
{u8 ret = 0, i = 0;dev->key_nd = of_find_node_by_path("/key");if (dev->key_nd == NULL){ret = -EFAULT;goto fail_find_nd;}dev->key[i].handler = key0_handler;dev->key[i].value = KEY0VALUE;for (i = 0; i < KEY_NUM; i++){dev->key[i].gpio = of_get_named_gpio(dev->key_nd, "key-gpios", i);if (dev->key[i].gpio < 0){ret = -EFAULT;goto fail_get_gpio;}memset(dev->key[i].name, 0, sizeof(dev->key[i].name));sprintf(dev->key[i].name, "KEY%d", i);ret = gpio_request(dev->key[i].gpio, dev->key[i].name);if (ret){ret = -EFAULT;goto fail_gpio_req;}ret = gpio_direction_input(dev->key[i].gpio);if (ret){ret = -EFAULT;goto fail_gpio_dir;}dev->key[i].irqnum = gpio_to_irq(dev->key[i].gpio);ret = request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &imx6uirq);if (ret){ret = -EFAULT;goto fail_req_irq;}}return 0;
}
timer1_init
该函数初始化定时器,设置其回调函数为timer_func
。
void timer1_init(struct imx6uirq_dev *dev)
{init_timer(&dev->timer);dev->timer.function = timer_func;
}
imx6uirq_exit
该函数在模块卸载时被调用。它负责释放所有分配的资源,包括中断、GPIO、设备文件、字符设备、设备类和设备号。
static void __exit imx6uirq_exit(void)
{u8 i = 0;for (i = 0; i < KEY_NUM; i++){free_irq(imx6uirq.key[i].irqnum, &imx6uirq);gpio_free(imx6uirq.key[i].gpio);}del_timer(&imx6uirq.timer);device_destroy(imx6uirq.class, imx6uirq.devid);class_destroy(imx6uirq.class);cdev_del(&imx6uirq.cdev);unregister_chrdev(imx6uirq.major, IMX6UIRQ_NAME);
}
用户空间应用程序
用户空间应用程序imx6uirqAPP.c
演示了如何使用poll
系统调用来监控按键状态。
1. poll
的使用
应用程序创建一个pollfd
结构体,指定要监控的文件描述符、感兴趣的事件(POLLIN
)和返回的事件。然后调用poll
函数,指定超时时间为500毫秒。
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN;
ret = poll(&fds, 1, 500);
2. 事件处理
poll
返回后,应用程序检查revents
字段以确定发生了什么事件。如果POLLIN
事件发生,则调用read
函数读取按键值。
if (ret > 0)
{if (fds.revents | POLLIN){unsigned char ch = 0;int ret = read(fd, &ch, sizeof(ch));if (ret >= 0){if (ch == KEY0VALUE){printf("User: key is pressing, ret is: %d\r\n", ret);}}}
}