第5章 Linux 字符设备驱动之按键驱动
在第一部分的 S5PV210 裸机开发篇已经接触过很多次按键驱动了,只不过那是在没有操作系统下的驱动而已。如果对按键硬件还不熟悉的,赶紧回去复习一下。借此,也顺便说一下,以后的驱动大多涉及各种硬件各种总线,这些驱动的基础都是前面学习过的裸机知识,如果你对某种硬件不熟悉,那么驱动根本没法进行。所以,在此也提醒大家一定要先对硬件有个比较深刻的认识才能进行Linux 下的驱动编写。
还记得以前在裸机开发篇里关于按键的实例了吗?里面讲解了轮询和中断两种方法来实现按键“驱动”。哪种方法比较好呢?答案很明显,自然是中断方法比较好,理由就不用 Webee 多说了吧?
说到中断,不得不承认 Linux 下有一套非常成熟的中断系统。Linux 内核将所有的中断统一编号,使用一个 irq_desc 结构数组来描述这些中断,每个数组项对应一个中断,里面记录了中断的名字、中断状态、中断 flags、底层硬件访问函数,中断处理函数入口等等,通过它可以调用用户注册的中断处理函数。Linux 中断分为两个部分:前半部和后半部。前半部是实际响应中断的函数,需要用 request_irq 注册;后半部是由前半部调度,并在一个更安全的时间来执行的函数,该函数就是具体负责执行中断的内容。前半部不允许被其他中断干扰,因此它的内容短小,而执行后半部时可以响应其他中断。这种机制的好处是可以迅速的响应中断。
/* 参考 include/linux/interrupt.h */
request_irq(unsigned int irq, irq_handler_t handler,unsigned long flags,const char *name, void *dev)
第一个参数:中断号,比如:IRQ_EINT(16)
第二个参数:中断处理函数,需要我们编写。
第三个参数:中断标记,比如:共享中断,上升沿中断,下降沿中断。
第四个参数:中断名字
第五个参数:传递给 irq_handler_t handler 中断处理函数的指针。
当中断使用完了之后就注销中断,注销中断则使用 free_irq 函数。
void free_irq(unsigned int irq, void *dev_id)
第一个参数:中断号,与 request_irq 的中断号一致。
第二个参数:传递给 irq_handler_t handler 中断处理函数的指针。
5.3 中断按键驱动实例
5.3.1 按键原理图设计
Webee210 底板上有 8 个独立按键,分别是 S1~S8,其中 S1~S4 对应的GPIO 管脚是 GPH2_0 ~ GPH2_3,对应的外部中断是 EINT16 ~ EINT19,而S5~S8 对应的 GPIO 管脚是 GPH3_0~GPH3_3,对应的外部中断是 EINT24 ~EINT27。
5.3.2 中断按键驱动实例源码分析
中断按键驱动源码在 webee210_drivers\4th_irq_key\irq_key.c
5.3.2.1 模块的入口函数分析
分析一个驱动首先从入口函数开始,而 Irq_key_init 作为模块的入口函数,
主要做了以下几件事:
第一、使用 register_chrdev 函数向内核注册一个字符设备。
第二、分别使用 class_create、device_create 函数创建类和类下的设备,这样系统就可以自动创建设备节点了。
int major;
static int __init Irq_key_init(void)
{
/* 注册一个字符设备 */
major = register_chrdev(0, "key_drv", &irq_key_fops);
/* 成功创建类后,可在/sys/class/目录下找到 key_drv 类 */
irq_key_class = class_create(THIS_MODULE, "key_drv");
/* 在 key_drv 类下创建/dev/IRQ_KEY 设备,供应用程序打开设备*/
device_create(irq_key_class, NULL, MKDEV(major, 0),
NULL, DEVICE_NAME);
return 0;
}
5.3.2.2 模块的出口函数分析
Irq_key_exit 作为模块的出口函数,还记得以前说过的吗?出口函数一般都是做清理工作了,这不是学习驱动的重点。
static void Irq_key_exit(void)
{
unregister_chrdev(major, "key_drv");
device_destroy(irq_key_class, MKDEV(major, 0));
class_destroy(irq_key_class);
}
5.3.2.3 file_operations 结构体
在入口函数注册字符设备的时候,register_chrdev 函数的第三个参数就需要file_operations 的实例。
static struct file_operations irq_key_fops = {
.owner = THIS_MODULE,
.open
= irq_key_open,
.read
= irq_key_read,
.release = irq_key_close,
};
5.3.2.4 irq_key_fops 结构体成员函数
irq_key_fops 结 构 体 的 主 要 成 员 有 irq_key_open 、 irq_key_read 、irq_key_close 这三个函数正是该按键驱动的重点对象。
这三个函数,应该首先看 irq_key_open 函数,它主要完成 8 个按键对应的 8个外部中断的申请。
static int irq_key_open(struct inode *inode, struct file *file)
{
int i;
int err = 0;
/* 使用 request_irq 函数注册中断 */
for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
{
err = request_irq(button_irqs[i].irq, key_interrupt,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
button_irqs[i].name, (void *)&button_irqs[i]);
}
/* 注册中断失败处理 */
if (err)
{
i--;
for (; i >= 0; i--)
{
disable_irq(button_irqs[i].irq);
free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
}
return -EBUSY;
}
return 0;
}
当应用程序执行 read 时,最终就会调用到驱动程序里的 irq_key_read 函数。
这个函数主要做了二件事。
第一、使用 wait_event_interruptible 函数休眠,直到有数据可读才唤醒进程,这是非常合理的。如果没有都数据,进程还一直读,就会浪费 CPU 了。而本驱动里的数据,指的就是有没有按键动作。当有按键动作时,就会进入key_interrupt 中断处理函数,在那里会唤醒进程。
第二、使用 copy_to_user 函数,将按键产生的“数据”拷贝回应用程序。( 注意:应用空间和内核空间的包括指针数据的传递不能单纯用 memcpy 函数,必须使用 copy_from_user 函数或 copy_to_user 函数)。
static ssize_t irq_key_read(struct file *file, char __user *buf,size_t count, loff_t *ppos)
{
if (count != 1)
return -EINVAL;
/* 如果没有按键动作, 休眠,即不会马上执行 copy_to_user
* ev_press = 0 时,进程会休眠,当有按键动作时,
* 会进入按键中断处理函数,里面将 ev_press = 1,
* 然后唤醒进程,然后马上执行 copy_to_user,继续往下跑。
*/
wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作, 返回键值给应用程序 */
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
5.3.2.5 key_interrupt 中断处理函数
当有按键动作时,就会进入 key_interrupt 按键中断处理函数。它主要做了以下二件事。
第一、使用 gpio_get_value 函数判断按键是按下状态还是松开状态,并记录按键值,这个按键值最终通过 irq_key_read 函数返回给应用程序。
第二、使用 wake_up_interruptible 函数唤醒进程,因为之前在 irq_key_read 函数之前休眠了。
/* 中断处理函数 */
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
struct button_irq_desc *button_irqs =
(struct button_irq_desc *)dev_id;
unsigned int pinval;
pinval = gpio_get_value(button_irqs->pin);
if (pinval)
{
/* 松开 */
key_val = 0x80 | button_irqs->key_val;
}
else
{
/* 按下 */
key_val = button_irqs->key_val;
}
ev_press = 1;
/* 唤醒休眠的进程 */
wake_up_interruptible(&button_waitq);
return IRQ_RETVAL(IRQ_HANDLED);
}
5.3.3中断按键驱动实例测试程序
按键驱动测试程序源码在 webee210_drivers\4th_irq_key\key_test.c
/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */
int main(void)
{
int buttons_fd;
char key_val;
char key_num;
/* 打开设备 */
buttons_fd = open("/dev/IRQ_KEY", O_RDWR);
if (buttons_fd < 0)
{
printf("Can not open device key\n");
return -1;
}
printf(" Please press buttons on webe210 board\n");
while(1)
{
/* 读取按键驱动发出的数据,注意 key_value
* 应该和键盘驱动中定义为一致的类型
*/
read(buttons_fd, &key_val, 1);
key_num = key_val;
if(key_num & 0x80)
{
key_num = key_num - 0x80;
printf("S%d is release on!
key_val = 0x%x\n",key_num ,key_val);
}
else
{
printf("S%d is pressed down!
key_val = 0x%x\n", key_num,key_val);
}
}
close(buttons_fd);
return 0;
}
测试程序首先打开/dev/IRQ_KEY 设备文件,这个设备文件正是刚才的按键驱动程序创建的,即相当于找到按键硬件资源。然后通过 read 函数将驱动里的数据读到应用程序,这个 read 函数最终会调用到驱动里的 irq_key_read 函数,它一进来就休眠,直到有数据可读(即有按键动作产生),它才进入按键中断处理函数,在中断处理函数里将会唤醒进程。然后回到 irq_key_read 函数,将产生的数据拷贝回应用程序,应用程序进行解析,并将结果打印出来。
5.3.4 中断按键驱动实例测试结果与现象
5.4 本章小结
本章贯穿字符设备驱动,再一次的学习字符设备驱动,让读者能够更加熟悉字符设备驱动的编写流程,因为它在 Linux 内核下,实在是太常出现了。并引出Linux 操作系统下的中断系统,限于篇幅,这里并没有直接分析 Linux 内核下的中断架构。有兴趣的读者,请参考《Linux 设备驱动第三版》的第十章中断处理。即使不懂 Linux 内核是怎么处理中断的,也可以写出带中断的驱动程序来。
本章最后通过中断按键驱动实例来让读者懂的如何编写带中断的字符设备驱动。通过它,读者应该更加清楚,应用程序与驱动程序之前的数据是怎么传输的。聪明的读者有可能会联想到我们常用的键盘,按键驱动与键盘驱动是不是一样的呢?其实,不一样。这里先告诉大家,键盘驱动使用的是输入子系统架构,而这里的按键驱动是字符设备驱动。关于输入子系统,后面的教程会详细分析。
附录源码:
< driver / irq_key.c >
/*
* Name:irq_key.c
* Copyright (C) 2014 Webee.JY ([email protected])
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "IRQ_KEY"
struct button_irq_desc {
int irq; /* 中断号 */
int pin; /* GPIO引脚 */
int key_val; /* 按键初始值 */
char *name; /* 名字 */
};
static struct button_irq_desc button_irqs [] = {
{IRQ_EINT(16), S5PV210_GPH2(0), 0x01, "S1"}, /* S1 */
{IRQ_EINT(17), S5PV210_GPH2(1), 0x02, "S2"}, /* S2 */
{IRQ_EINT(18), S5PV210_GPH2(2), 0x03, "S3"}, /* S3 */
{IRQ_EINT(19), S5PV210_GPH2(3), 0x04, "S4"}, /* S4 */
{IRQ_EINT(24), S5PV210_GPH3(0), 0x05, "S5"}, /* S5 */
{IRQ_EINT(25), S5PV210_GPH3(1), 0x06, "S6"}, /* S6 */
{IRQ_EINT(26), S5PV210_GPH3(2), 0x07, "S7"}, /* S7 */
{IRQ_EINT(27), S5PV210_GPH3(3), 0x08, "S8"}, /* S8 */
};
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static struct class *irq_key_class;
/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */
static unsigned char key_val;
/* 中断事件标志, 中断服务程序将它置1,irq_key_read将它清0 */
static volatile int ev_press = 0;
/* 中断处理函数 */
static irqreturn_t key_interrupt(int irq, void *dev_id)
{
struct button_irq_desc *button_irqs = (struct button_irq_desc *)dev_id;
unsigned int pinval;
pinval = gpio_get_value(button_irqs->pin);
if (pinval)
{
/* 松开 */
key_val = 0x80 | button_irqs->key_val;
}
else
{
/* 按下 */
key_val = button_irqs->key_val;
}
ev_press = 1;
/* 唤醒休眠的进程 */
wake_up_interruptible(&button_waitq);
return IRQ_RETVAL(IRQ_HANDLED);
}
static int irq_key_open(struct inode *inode, struct file *file)
{
int i;
int err = 0;
/* 使用request_irq函数注册中断 */
for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
{
err = request_irq(button_irqs[i].irq, key_interrupt, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
button_irqs[i].name, (void *)&button_irqs[i]);
}
/* 注册中断失败处理 */
if (err)
{
i--;
for (; i >= 0; i--)
{
disable_irq(button_irqs[i].irq);
free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
}
return -EBUSY;
}
return 0;
}
static ssize_t irq_key_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
if (count != 1)
return -EINVAL;
/* 如果没有按键动作, 休眠,即不会马上执行copy_to_user
* ev_press = 0时,进程会休眠,当有按键动作时,
* 会进入按键中断处理函数,里面将ev_press = 1,
* 然后唤醒进程,然后马上执行copy_to_user,继续往下跑。
*/
wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作, 返回键值给应用程序 */
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
static int irq_key_close(struct inode *inode, struct file *file)
{
int i;
/* 注销中断 */
for (i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++)
{
free_irq(button_irqs[i].irq, (void *)&button_irqs[i]);
}
return 0;
}
static struct file_operations irq_key_fops = {
.owner = THIS_MODULE,
.open = irq_key_open,
.read = irq_key_read,
.release = irq_key_close,
};
int major;
static int __init Irq_key_init(void)
{
/* 注册一个字符设备 */
major = register_chrdev(0, "key_drv", &irq_key_fops);
/* 成功创建类后,可在/sys/class/目录下找到key_drv类 */
irq_key_class = class_create(THIS_MODULE, "key_drv");
/* 在key_drv类下创建/dev/IRQ_KEY 设备,供应用程序打开设备*/
device_create(irq_key_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
return 0;
}
static void Irq_key_exit(void)
{
unregister_chrdev(major, "key_drv");
device_destroy(irq_key_class, MKDEV(major, 0));
class_destroy(irq_key_class);
}
module_init(Irq_key_init);
module_exit(Irq_key_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("webee");
MODULE_DESCRIPTION("Character drivers for irq key");
< driver / Makefile >
ifneq ($(KERNELRELEASE),)
obj-m :=irq_key.o
else
module-objs :=irq_key.o
KERNELDIR :=/home/gec/linux_kernel/linux2.6.35.7/
PWD :=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
$(RM) *.ko *.mod.c *.mod.o *.o *.order *.symvers *.cmd
< app / key_test.c >
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04,0x05,0x06,0x07,0x08 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84,0x85,0x86,0x87,0x88 */
int main(void)
{
int buttons_fd;
char key_val;
char key_num;
/* 打开设备 */
buttons_fd = open("/dev/IRQ_KEY", O_RDWR);
if (buttons_fd < 0)
{
printf("Can not open device key\n");
return -1;
}
printf(" Please press buttons on webe210 board\n");
while(1)
{
/*读取按键驱动发出的数据,注意key_value和键盘驱动中定义为一致的类型*/
read(buttons_fd, &key_val, 1);
key_num = key_val;
if(key_num & 0x80)
{
key_num = key_num - 0x80;
printf("S%d is release on! key_val = 0x%x\n",key_num ,key_val);
}
else
{
printf("S%d is pressed down! key_val = 0x%x\n", key_num,key_val);
}
}
close(buttons_fd);
return 0;
}
< app / Makefile >
#
# General Makefile
Exec := key_test
Obj := key_test.c
CC := arm-linux-gcc
$(Exec) : $(Obj)
$(CC) -o $@ $(Obj) $(LDLIBS$(LDLIBS-$(@)))
clean:
rm -vf $(Exec) *.elf *.o