目录
一、概念介绍
- C库实现open,read,write 调用之后会进到内核
- open,read,write的实现实际上是实现了一条swi val指令,这条指令会产生一个异常,相当于发生中断一样,然后进到内核的异常处理函数里面
- 内核系统调用接口 System Call interaface 根据发生异常的原因,调用不同处理函数sys_open、sys_read、sys_write
- sys_open、sys_read、sys_write被VFS:virtual File System 处理打开不同文件来找到不同的驱动程序
- 应用:open、read、write 对应驱动:led_open、led_read、led_write(自己命名并注册),而应用程序怎么去调用驱动里面的open、read、write依赖于驱动程序框架
二、LED驱动程序的测试
2.1 框架的搭建过程
- 写出驱动程序中的led_open、led_read、led_write等等
- 告诉内核
a.定义一个file_operations结构例如first_drv_fops
b.把这个结构告诉内核
register_chrdev(major, "first_drv", &first_drv_fops);其中major为主设备号,名字不重要自己命名,第三者参数为定义的file_operations结构
一般major写为0,使系统自动分配,其返回值就是major
c.驱动的入口函数来调用(可以自己命名例如,first_drv_init)
修饰入口函数module_init(first_drv_init);
d.有入口函数也就有出口函数(可以自己命名例如,firest_drv_exit)
修饰出口函数module_exit(firest_drv_exit);
- 对于修饰函数module_init(),定义一个结构体,结构体里面有一个函数指针,指向入口函数,当我们去加载或者安装一个驱动的时候,内核会找到这个结构体,然后调用里面的函数指针,入口函数就会把file_operations结构告诉内核
- 应用程序打开一个设备文件 /dev/xxx,通过命令ls-l,可以看到c_ _ _,_ _ _,_ _ _ major mior,其中c代表字符设备,若是d代表目录,major为主设备号,mior为次设备号
- 怎么打开,根据设备类型c和主设备号major就能够找到注册进去的file_operations结构
- register_chrdev最直接的作用:把file_operations结构填充进去
- 小总结:应用程序app利用C库打开的设备文件例如为字符设备,由VFS在字符设备中根据主设备号找到chrdev数组对应的file_operations结构,然后驱动文件实现open、read、write,定义一个file_operations结构,用入口函数把结构体放到内核的数组里面去
- DEMO:
驱动程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static int first_drv_open(struct inode *inode, struct file *file)
{
printk("first_drv_open\n");
return 0;
}
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = first_drv_open,
};
int major;
int firest_drv_init(void) //根据驱动名各有不同
{
major = register_chrdev(0, "first_drv", &first_drv_fops);
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
return 0;
}
void firest_drv_exit(void)
{
unregister_chrdev(major, "first_drv"); //卸载驱动
}
module_init(firest_drv_init);
module_exit(firest_drv_exit);
测试程序(应用程序):
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd;
fd = open("dev/xyz",O_RDWR);
if(fd < 0)
printf("cant't open!\n");
return 0;
}
Makefile: 用来编译驱动程序,编译一个驱动程序依赖内核,内核需要先编译好
KERN_DIR = /work/system/linux-2.6.22.6
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += First_drv.o
- 由于没有自动的创建设备节点,因此我们需要手动创建设备节点
mknod /dev/xyz c 111 0
- 列出内核目前支持的设备,可以看出我们注册的信息
cat /proc/devices
- 加载驱动,卸载驱动,查看驱动
insmod First_drv.ko
rmmod First_drv
lsmod
- 编译驱动和测试程序到开发板上后,加载驱动,执行测试程序./firstdrvtest,结果如下,打开成功
first_drv_open
2.2 测试改进
- 驱动设备号是我们手工建立的mknod /dev/xyz c 111 0,还有另外一种方法自动创建,udev机制实现,在S3C2440的busybox中mdev机制为udev的简化版本,根据系统信息,创建设备节点,在入口函数使用如下两个函数
class_create(THIS_MODULE, "firstdrv");
class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");
- 对于class_create函数创建了firstdrv这个类,对于class_device_create会根据这个类创建xyz这个设备,由mdev会自动创建/dev/xyz设备节点
- 在出口函数消除
class_device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class);
- 在修饰函数后面加上以下宏定义解决上面函数未识别的问题
MODULE_LICENSE("GPL");
- 卸载之前的驱动,加载改进的驱动,使用ls -l /dev/xyz可看到自动创建的设备节点
- 对于S3C2440,为什么会这样自动运行更改?因为脚本文件中/etc/init.d/rcS中echo /sbin/mdev > /proc/sys/kernel/hotplug,hotplug热拔插,当插入U盘等设备内核就会调用/proc/sys/kernel/hotplug,hotplug指向/sbin/mdev会自动去创建设备节点
2.3 操作LED
- 写一个点LED驱动,需要框架(上述已构建)和完善硬件的操作
a.看原理图 b.看芯片手册 c.写代码
- 对于单片机操作物理地址,对于linux在驱动程序操作虚拟地址
- 用ioremap()把物理地址映射为虚拟地址,入口函数执行,以防多次调用
- 在出口函数取消映射iounmap()
- DEMO:
驱动程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *firstdrv_class;
static struct class_device *firstdrv_class_dev;
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int first_drv_open(struct inode *inode, struct file *file)
{
printk("first_drv_open\n");
/* 配置 GPF456为输出 */
*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
return 0;
}
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
copy_from_user(&val, buf, count); //利用参数buf向用户提取数据,而copy_to_user()函数向用户传输数据
if(val == 1){
//点灯
*gpfdat &= ~((1 << 4) |(1 << 5) | (1 << 6));
}else{
//关灯
*gpfdat |= (1 << 4) |(1 << 5) | (1 << 6);
}
printk("first_drv_write\n");
return 0;
}
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = first_drv_open,
.write = first_drv_write,
};
int major;
int firest_drv_init(void) //根据驱动名各有不同
{
major = register_chrdev(0, "first_drv", &first_drv_fops);
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/leds */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
return 0;
}
void firest_drv_exit(void)
{
unregister_chrdev(major, "first_drv"); //卸载驱动
class_device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class);
iounmap(gpfcon);
}
module_init(firest_drv_init);
module_exit(firest_drv_exit);
MODULE_LICENSE("GPL");
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
/* firstdrvtest on
* firstdrvtest off
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("dev/xyz",O_RDWR);
if(fd < 0)
printf("cant't open!\n");
if(argc != 2){
printf("Usage:\n");
printf("%s <on|off>\n", argv[0]);
}
if(strcmp(argv[1], "on") == 0){
val = 1;
}else{
val = 0;
}
write(fd, &val, 4);
return 0;
}
- 根据自己的开发板来设置相应的寄存器,编译后加载驱动,执行测试程序./firstdrvtest on可以看到开发板上led全点亮
三、按键驱动之查询方式
- 四个按键分别为GPF0、GPF2、GPG3和GPG11
- 驱动程序中read成员使用copy_to_user函数将健值返回给应用程序
驱动程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *seconddrv_class;
static struct class_device *seconddrv_class_dev;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static int second_drv_open(struct inode *inode, struct file *file)
{
/* 配置GPF0 2 为输入引脚 */
*gpfcon &= ~((0x3 << (0*2)) | (0x3 << (2*2)));
/* 配置GPG3 11 为输入引脚 */
*gpgcon &= ~((0x3 << (3*2)) | (0x3 << (11*2)));
return 0;
}
ssize_t second_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
/* 返回4个引脚的电平 */
unsigned char key_vals[4];
int regval;
if(size != sizeof(key_vals))
return -EINVAL;
/* 读 GPF0,2 */
regval = *gpfdat;
key_vals[0] = (regval & (1<<0)) ? 1 : 0;
key_vals[1] = (regval & (1<<2)) ? 1 : 0;
/* 读 GPG3,11 */
regval = *gpgdat;
key_vals[2] = (regval & (1<<3)) ? 1 : 0;
key_vals[3] = (regval & (1<<11)) ? 1 : 0;
copy_to_user(buf, key_vals, sizeof(key_vals));
return sizeof(key_vals);
}
static struct file_operations second_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = second_drv_open,
.read = second_drv_read,
};
int major;
static int second_drv_init(void)
{
major = register_chrdev(0, "second_drv", &second_drv_fops);
seconddrv_class = class_create(THIS_MODULE, "seconddrv");
seconddrv_class_dev = class_device_create(seconddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
return 0;
}
static void second_drv_exit(void)
{
unregister_chrdev(major, "second_drv");
class_device_unregister(seconddrv_class_dev);
class_destroy(seconddrv_class);
iounmap(gpfcon);
iounmap(gpgcon);
}
module_init(second_drv_init);
module_exit(second_drv_exit);
MODULE_LICENSE("GPL");
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
/* seconddrvtest on
* seconddrvtest off
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_vals[4];
int cnt =0;
fd = open("dev/buttons", O_RDWR);
if(fd < 0)
printf("cant't open!\n");
while(1){
read(fd, key_vals, sizeof(key_vals));
if(!key_vals[0] || !key_vals[1] || !key_vals[2] || !key_vals[3]){
printf("%04d key pressed: %d %d %d %d\n", cnt++, key_vals[0], key_vals[1], key_vals[2], key_vals[3]);
}
}
return 0;
}
- 编译后加载驱动,执行测试程序,后台运行测试程序再用top命令查看,类似于windows的任务管理器,可见得测试程序占用98%CPU,因此这种while不断查询的方式耗资源
./seconddrvtest &
top
四、按键驱动之中断处理
4.1 Linux异常处理结构
异常,就是可以打断CPU正常运行流程的一些事情,比如外部中断、未定义的指令、试图修改只读的数据、执行swi指令(Software Interrupt Instruction,软件中断指令)等。当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断的程序。操作系统中经常通过异常来完成一些特定的功能
- linux内核对异常的设置
内核在start_kernel函数(源码在: init/main.c中)中调用trap_init、init_lRQ两个函数来设置异常的处理函数。
trap_init函数分析:
trap init函数(代码在 arch/arm/kernel/traps.c 中)被用来设置各种异常的处理向量,包括中断向量。所谓“向量”,就是一些被安放在固定位置的代码,当发生异常时,CPU 会自动执行这些固定位.置上的指令。ARM架构CPU的异常向量基址可以是Ox00000000,也可以是OxffffO000,Linux内核使用后者。trap_init 函数将异常向量复制到Oxff0000处,部分代码如下:
void __init trap_init(void)
{
...
721 memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
722 memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
...
}
第721行中,vectors等于OxfII0000。地址._vectors_start~-__vectors_end之间的代码就是异常向量,在 arch/arm/kernelentry-armv.S 中定义,它们被复制到地址Oxfm0000处。
异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。这些“更复杂的代码”在地址_stubs_start~_stubs_end之间,它们在 arch/arm/kernelentry-armv.S中定义。第722行将它们复制到地址Oxffffo000+0x200处。
,中断也是一种异常,对于init_IRQ函数(代码在.arch/arm/kernel/irq.c中)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数 asm do_lRQ就可以调用这些函数作进一步处理。
4.2 Linux中断处理结构
- 内核里中断处理是一种框架 ,单片机下的中断处理a.分辨是哪个中断b.调用处理函数c.清中断,对于linux,三项都是在asm_do_IRQ实现的
(1)发生中断时,CPU执行异常向量vector__irq的代码。
(2)在vector_irq里面,最终会调用中断处理的总入口函数asm _do_IRQ。( 3 ) asm_do_IRQ根据中断号调用irq_desc 数组项中的handle_irq。
( 4) handle_ irq 会使用chip成员中的函数来设置硬件,比如清除中断、禁止中断、重新使能中断等。
(5 ) handle_irq 逐个调用用户在 action链表中注册的处理函数。
可见,中断体系结构的初始化就是构造这些数据结构,比如irq_desc 数组项中的handle_irq、chip等成员;用户注册中断时就是构造action链表;用户卸载中断时就是从action链表中去除不需要的项。
- Linux中断处理体系结构图
- 注册中断程序request_irq函数在open函数中注册其内容:
1.分配irqaction结构,里面成员指向其参数
2.执行setup_irq(irq, 分配的irqaction结构)
a.在结构体rq_desc[irq]数组项里面的action链表里加入irqaction
b.desc->chip->settype() 设置引脚
c.desc->chip->startup/enable 使能中断
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *devname, void *dev_id);
- 参数irq中断号,例如按键可以从原理图看是外部中断0 在irqs.h中定义了中断号,参数二为中断处理函数,参数三为中断处理方法,在manage.c中setup.irq的set_type 在irq.h中定义,这里双边沿触发中断,参数四为ID,在卸载时需要其参数
- 卸载中断处理程序在close中卸载,作用:一是将irqaction 从链表移除,二是禁止中断
free_irq(unsigned int irq, void *dev_id);
4.3 编写代码
- 如果没有按键动作发生,则使进程休眠使用wait_event_interruptible()函数;
- 唤醒休眠的进程,wake_up_interruptible();
- 中断服务函数:用结构体的方式处理健值
驱动程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h> //需要包含的头文件
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *thirddrv_class;
static struct class_device *thirddrv_class_dev;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 中断事件标志, 中断服务程序将它置1,third_drv_read将它清0 */
static volatile int ev_press = 0;
struct pin_desc{
unsigned int pin;
unsigned int key_val;
};
/* 健值:按下时,0x01,0x02,0x03,0x04 */
/* 健值:松开时,0x81,0x82,0x83,0x84 */
static unsigned char key_val;
struct pin_desc pins_desc[4] = {
{S3C2410_GPF0 , 0x01},
{S3C2410_GPF2 , 0x02},
{S3C2410_GPG3 , 0x03},
{S3C2410_GPG11 , 0x04}
};
/*
* 确定按键值
*/
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
struct pin_desc *pindesc = (struct pin_desc *)dev_id;
unsigned int pinval;
/* 读取PIN值 */
pinval = s3c2410_gpio_getpin(pindesc->pin);
/* 确定按键值 */
if(pinval){
/* 松开 */
key_val = 0x80 | pindesc->key_val;
}else{
/* 按下 */
key_val = pindesc->key_val;
}
ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
return IRQ_HANDLED;
}
static int third_drv_open(struct inode *inode, struct file *file)
{
/* 配置GPF0 2 为输入引脚 */
/* 配置GPG3 11 为输入引脚 */
request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
return 0;
}
ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
/* 如果没有按键动作发生,休眠 */
wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作,返回健值 */
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
int third_drv_close(struct inode *inode, struct file *file)
{
free_irq(IRQ_EINT0, &pins_desc[0]);
free_irq(IRQ_EINT2, &pins_desc[1]);
free_irq(IRQ_EINT11, &pins_desc[2]);
free_irq(IRQ_EINT19, &pins_desc[3]);
return 0;
}
static struct file_operations third_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = third_drv_open,
.read = third_drv_read,
.release = third_drv_close,
};
int major;
static int third_drv_init(void)
{
major = register_chrdev(0, "third_drv", &third_drv_fops);
thirddrv_class = class_create(THIS_MODULE, "thirddrv");
thirddrv_class_dev = class_device_create(thirddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
return 0;
}
static void third_drv_exit(void)
{
unregister_chrdev(major, "third_drv");
class_device_unregister(thirddrv_class_dev);
class_destroy(thirddrv_class);
iounmap(gpfcon);
iounmap(gpgcon);
}
module_init(third_drv_init);
module_exit(third_drv_exit);
MODULE_LICENSE("GPL");
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
/*
* thirddrvtest
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_val;
fd = open("dev/buttons", O_RDWR);
if(fd < 0)
printf("cant't open!\n");
while(1){
read(fd, &key_val, 1);
printf("key_val = 0x%x\n", key_val);
}
return 0;
}
- 用exec 5</dev/buttons打开驱动程序,利用cat /proc/interrupts可以看到哪些中断被打开了,ps当前sh进程为770,ls -l /proc/770/fd ,其中5就指向了/dev/buttons,关闭:exec 5<&-
- 再次查看ls -l /proc/771/fd //5就没有了,cat /proc/interrupts也看不到中断,在close中释放中断
# insmod third_drv.ko
# exec 5</dev/buttons
# ps
PID Uid VSZ Stat Command
1 0 3092 S init
2 0 SW< [kthreadd]
3 0 SWN [ksoftirqd/0]
4 0 SW< [watchdog/0]
5 0 SW< [events/0]
6 0 SW< [khelper]
55 0 SW< [kblockd/0]
56 0 SW< [ksuspend_usbd]
59 0 SW< [khubd]
61 0 SW< [kseriod]
73 0 SW [pdflush]
74 0 SW [pdflush]
75 0 SW< [kswapd0]
76 0 SW< [aio/0]
710 0 SW< [mtdblockd]
745 0 SW< [kmmcd]
762 0 SW< [rpciod/0]
770 0 3096 S -sh
774 0 1308 R ./seconddrvtest
783 0 3096 R ps# ls -l /proc/770/fd
lrwx------ 1 0 0 64 Jan 1 00:47 0 -> /dev/console
lrwx------ 1 0 0 64 Jan 1 00:47 1 -> /dev/console
lrwx------ 1 0 0 64 Jan 1 00:47 10 -> /dev/tty
lrwx------ 1 0 0 64 Jan 1 00:47 2 -> /dev/console
lr-x------ 1 0 0 64 Jan 1 00:47 5 -> /dev/buttons
- 执行后台运行 ./thirddrvtest &,此时现象为等待按键按下直到休眠被中断打破
- 执行top命令,其CPU占比为5%,相比查询方式极大提高了效率
五、添加poll机制
详解:https://blog.csdn.net/FrankyzhangC/article/details/6692210
- 目的:根据poll机制定义的时间来查询驱动程序,当没有按键按下,每经过设定的时间申报一下
- 应用程序通过poll函数来调用驱动中的poll函数
- 上述代码中在驱动程序file_operations结构中添加poll成员,并注释掉read函数的休眠
ssize_t forth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
/* 如果没有按键动作发生,休眠 */
// wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作,返回健值 */
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
unsigned int forth_drv_poll(struct file *file, poll_table *wait)
{
unsigned mask = 0;
poll_wait(file, &button_waitq, wait);//不会立即休眠
if(ev_press){
mask |= POLLIN | POLLRDNORM;
}
return mask;
}
static struct file_operations forth_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = forth_drv_open,
.read = forth_drv_read,
.release = forth_drv_close,
.poll = forth_drv_poll
};
测试程序:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- struct pollfd *fds可查询多个驱动程序,nfds_t nfds这里查询1个,int timeout以ms为单位,根据返回值来判断是否超时
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
/*
* forthdrvtest
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_val;
int ret;
struct pollfd fds[1];
fd = open("dev/buttons", O_RDWR);
if(fd < 0)
printf("cant't open!\n");
fds[0].fd = fd;
fds[0].events = POLLIN;
while(1){
ret = poll(fds, 1, 5000);
if(ret == 0){
printf("time out\n");
}else{
read(fd, &key_val, 1);
printf("key_val = 0x%x\n", key_val);
}
}
return 0;
}
- 编译后加载驱动,执行测试函数,当没按键按下,每隔5s返回
六、异步通知
- 按键驱动程序方法中->查询方式:耗资源、中断方式:read、poll机制:指定超时时间,共同的特点:应用程序主动去Read
- 异步通知:驱动程序提醒应用程序,应用程序再读取按键
- 利用进程间发信号:signal()函数
- 目标:按下按键时,驱动程序通知应用程序
步骤:1.应用程序:注册新号处理函数 2.谁发:驱动 3.发给应用程序app->app要告诉驱动PID 4.怎么发,在驱动中kill_fasyn();
- 定义在fasync_struct结构体,file_operations结构中添加fasync成员,在中断函数中调用kill_fasyn()函数
驱动程序:
struct fasync_struct *button_async;
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
struct pin_desc *pindesc = (struct pin_desc *)dev_id;
unsigned int pinval;
/* 读取PIN值 */
pinval = s3c2410_gpio_getpin(pindesc->pin);
/* 确定按键值 */
if(pinval){
/* 松开 */
key_val = 0x80 | pindesc->key_val;
}else{
/* 按下 */
key_val = pindesc->key_val;
}
ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
kill_fasync(&button_async, SIGIO, POLL_IN);
return IRQ_HANDLED;
}
static int fifth_drv_fasync (int fd, struct file *filp, int on)
{
printk("driver: fifth_drv_fasync\n");
return fasync_helper (fd, filp, on, &button_async); //结构体被初始化 kill_fasyn()就可以用
}
static struct file_operations fifth_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = fifth_drv_open,
.read = fifth_drv_read,
.release = fifth_drv_close,
.poll = fifth_drv_poll,
.fasync = fifth_drv_fasync,
};
测试程序:
- 调用kill_fasyn()函数,需要应用程序调用fcntl函数来初始化fasync_struct结构体才可以调用
为了使设备支持异步通知机制,驱动程序中涉及以下3项工作:
1. 支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应进程ID。
不过此项工作已由内核完成,设备驱动无须处理。
2. 支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。
驱动中应该实现fasync()函数。
3. 在设备资源可获得时,调用kill_fasync()函数激发相应的信号
fcntl(fd, F_SETOWN, getpid()); // 告诉内核,发给谁 把pid进程号告诉驱动程序
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags | FASYNC); // 改变fasync标记,最终会调用到驱动的faync > fasync_helper:初始化/释放fasync_struct
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
/*
* fifthdrvtest
*/
int fd;
void my_signal_fun(int signum)
{
unsigned char key_val;
read(fd, &key_val, 1);
printf("key_val: 0x%x\n", key_val);
}
int main(int argc, char **argv)
{
int ret;
int Oflags;
signal(SIGIO, my_signal_fun);
fd = open("dev/buttons", O_RDWR);
fcntl(fd, F_SETOWN, getpid());
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags | FASYNC);
if(fd < 0)
printf("can't open!\n");
while(1){
sleep(1000);
}
return 0;
}
- 验证结构体被初始化在驱动程序fifth_drv_fasync函数中打印验证
- 编译加载驱动,执行测试程序,驱动中fasync成员被调用,结构体fasync_struct初始化成功,直到按键按下时,才将信息告诉应用程序
七、同步互斥与阻塞
7.1 变量实现同步出现的问题
- 目的:同一时刻只能有一个应用程序打开/dev/buttons
修改程序如下
static int canopen = 1;
static int sixth_drv_open(struct inode *inode, struct file *file)
{
if(--canopen != 0){
canopen++;
return -EBUSY;
} ...
int sixth_drv_close(struct inode *inode, struct file *file)
{
canopen++;
...
- 在汇编情况下,对于--canopen,分三步a.读出值 b.修改值 c.写回
- 以上这种情况:由于linux是多任务的,假设有A程序打开,在汇编情况下,先读出canopen的值,若这时被切换到B程序打开,也会先读出canopen的值,再修改,此时B可以打开驱动,到切换到A程序时,由于一开始读出也是1,因此也能打开成功,这种概率很小,但不能忽视
7.2 方式一:原子操作
常用原子操作函数举例:
atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
atomic_read(atomic_t *v); //返回原子变量的值
void atomic_inc(atomic_t *v); //原子变量增加1
void atomic_dec(atomic_t *v); //原子变量减少1
int atomic_dec_and_test(atomic_t *v); //自减操作后测试其是否为0,为0则返回true,否则返回false
- 对于原子操作atomic_dec_and_test读出修改写回一次性完成,不能被打断,将上述代码修改如下:
static atomic_t canopen = ATOMIC_INIT(1); 定义原子变量canopen并初始化为1
static int sixth_drv_open(struct inode *inode, struct file *file)
{
if(!atomic_dec_and_test(&canopen)){
atomic_inc(&canopen); //原子变量增加1
return -EBUSY;
}
...
int sixth_drv_close(struct inode *inode, struct file *file)
{
atomic_inc(&canopen);
...
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
/*
* sixthdrvtest
*/
int fd;
int main(int argc, char **argv)
{
unsigned char key_val;
int ret;
int Oflags;
if(fd < 0){
printf("can't open!\n");
return -1;
}
while(1){
ret = read(fd, &key_val, 1);
printf("key_val: 0x%x, ret = %d\n", key_val, ret);
sleep(5); // 单位s
}
return 0;
}
- 编译加载驱动,执行测试程序,第一个测试程序打开成功,第二个,第三个等打开都会打印出can't open!
7.3 方式二:信号量
信号量(semaphore)是用于保护临界区的一种常用方法,只有得到信号量的进程才能执行临界区代码。
当获取不到信号量时,进程进入休眠等待状态。定义信号量
struct semaphore sem;
初始化信号量
void sema_init (struct semaphore *sem, int val);
void init_MUTEX(struct semaphore *sem);//初始化为0static DECLARE_MUTEX(button_lock); //定义互斥锁
获得信号量
void down(struct semaphore * sem);
int down_interruptible(struct semaphore * sem); //获取不到锁,就休眠,休眠状态可被打断,别人可以结束掉改进程
int down_trylock(struct semaphore * sem);//获取不到锁,就返回,判断返回值
释放信号量
void up(struct semaphore * sem);
- DEMO:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <linux/poll.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *sixthdrv_class;
static struct class_device *sixthdrv_class_dev;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 中断事件标志, 中断服务程序将它置1,sixth_drv_read将它清0 */
static volatile int ev_press = 0;
struct fasync_struct *button_async;
struct pin_desc{
unsigned int pin;
unsigned int key_val;
};
/* 健值:按下时,0x01,0x02,0x03,0x04 */
/* 健值:松开时,0x81,0x82,0x83,0x84 */
static unsigned char key_val;
struct pin_desc pins_desc[4] = {
{S3C2410_GPF0 , 0x01},
{S3C2410_GPF2 , 0x02},
{S3C2410_GPG3 , 0x03},
{S3C2410_GPG11 , 0x04}
};
//static atomic_t canopen = ATOMIC_INIT(1); 定义原子变量canopen并初始化为1
static DECLARE_MUTEX(button_lock); //定义互斥锁
/*
* 确定按键值
*/
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
struct pin_desc *pindesc = (struct pin_desc *)dev_id;
unsigned int pinval;
/* 读取PIN值 */
pinval = s3c2410_gpio_getpin(pindesc->pin);
/* 确定按键值 */
if(pinval){
/* 松开 */
key_val = 0x80 | pindesc->key_val;
}else{
/* 按下 */
key_val = pindesc->key_val;
}
ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
kill_fasync(&button_async, SIGIO, POLL_IN);
return IRQ_HANDLED;
}
static int sixth_drv_open(struct inode *inode, struct file *file)
{
#if 0
if(!atomic_dec_and_test(&canopen)){ //自减操作后测试其是否为0,为0则返回true,否则返回false,读出修改写回一次性完成,不能被打断
atomic_inc(&canopen); //原子变量增加1
return -EBUSY;
}
#endif
down(&button_lock);
/* 配置GPF0 2 为输入引脚 */
/* 配置GPG3 11 为输入引脚 */
request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
return 0;
}
ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
/* 如果没有按键动作发生,休眠 */
wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作,返回健值 */
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
int sixth_drv_close(struct inode *inode, struct file *file)
{
// atomic_inc(&canopen);
free_irq(IRQ_EINT0, &pins_desc[0]);
free_irq(IRQ_EINT2, &pins_desc[1]);
free_irq(IRQ_EINT11, &pins_desc[2]);
free_irq(IRQ_EINT19, &pins_desc[3]);
/* 释放信号量 */
up(&button_lock);
return 0;
}
unsigned int sixth_drv_poll(struct file *file, poll_table *wait)
{
unsigned mask = 0;
poll_wait(file, &button_waitq, wait);//不会立即休眠
if(ev_press){
mask |= POLLIN | POLLRDNORM;
}
return mask;
}
static int sixth_drv_fasync (int fd, struct file *filp, int on)
{
printk("driver: sixth_drv_fasync\n");
return fasync_helper (fd, filp, on, &button_async);
}
static struct file_operations sixth_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = sixth_drv_open,
.read = sixth_drv_read,
.release = sixth_drv_close,
.poll = sixth_drv_poll,
.fasync = sixth_drv_fasync,
};
int major;
static int sixth_drv_init(void)
{
major = register_chrdev(0, "sixth_drv", &sixth_drv_fops);
sixthdrv_class = class_create(THIS_MODULE, "sixthdrv");
sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
return 0;
}
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
class_device_unregister(sixthdrv_class_dev);
class_destroy(sixthdrv_class);
iounmap(gpfcon);
iounmap(gpgcon);
}
module_init(sixth_drv_init);
module_exit(sixth_drv_exit);
MODULE_LICENSE("GPL");
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
/*
* sixthdrvtest
*/
int fd;
int main(int argc, char **argv)
{
unsigned char key_val;
int ret;
int Oflags;
fd = open("dev/buttons", O_RDWR | O_NONBLOCK);
if(fd < 0){
printf("can't open!\n");
return -1;
}
while(1){
ret = read(fd, &key_val, 1);
printf("key_val: 0x%x, ret = %d\n", key_val, ret);
sleep(5); // 单位s
}
return 0;
}
- 编译加载驱动,执行测试程序,第一个打开成功,第二测试程序由于获取不到信号量,处于不可中断的睡眠状态
7.4 处理应用程序的阻塞标志
- fd = open("...", O_RDWR | O_NONBLOCK); 处理阻塞
- 修改驱动程序:
static int sixth_drv_open(struct inode *inode, struct file *file)
{
#if 0
if(!atomic_dec_and_test(&canopen)){ //自减操作后测试其是否为0,为0则返回true,否则返回false,读出修改写回一次性完成,不能被打断
atomic_inc(&canopen); //原子变量增加1
return -EBUSY;
}
#endif
if(file->f_flags & O_NONBLOCK){
if(down_trylock(&button_lock))
return -EBUSY;
}else{
/* 获取信号量 */
down(&button_lock);
}
...
ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
if(file->f_flags & O_NONBLOCK){
if(!ev_press)
return -EBUSY;
}else{
/* 如果没有按键动作发生,休眠 */
wait_event_interruptible(button_waitq, ev_press);
}
...
- 测试程序默认是阻塞,加上非阻塞标志:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
/*
* sixthdrvtest
*/
int fd;
void my_signal_fun(int signum)
{
unsigned char key_val;
read(fd, &key_val, 1);
printf("key_val: 0x%x\n", key_val);
}
int main(int argc, char **argv)
{
unsigned char key_val;
int ret;
int Oflags;
fd = open("dev/buttons", O_RDWR | O_NONBLOCK);
if(fd < 0){
printf("can't open!\n");
return -1;
}
while(1){
ret = read(fd, &key_val, 1);
printf("key_val: 0x%x, ret = %d\n", key_val, ret);
sleep(5); // 单位s
}
return 0;
}
- 编译加载驱动,执行测试程序,第一个测试程序打开成功,第二个测试程序由于非阻塞的情况下又获取不到信号量,立即在open函数返回
八、按键驱动利用定时器防抖动
- 问题:在按键按下时会出现抖动,以致于出现多次中断
- 解决方法:在驱动程序中加上定时器,当按键按下时产生中断,重载定时器超时时间,在抖动的过程中产生中断都会重载定时器超时时间,直到定时器超时才对健值进行处理
- 入口函数初始化定时器init_timer()函数,参考内核例子,其参数为一个struct timer_lis结构体
init_timer(&buttons_timer);
//buttons_timer.data = (unsigned long)sCpnt; //处理函数传参
//buttons_timer.expires = jiffies + 100*Hz; /*10s */ 在中断服务函数再设置
buttons_timer.function = buttons_timer_function;
//buttons_timer.expires = 0;
- DEMO:
驱动程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <linux/poll.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *sixthdrv_class;
static struct class_device *sixthdrv_class_dev;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static struct timer_list buttons_timer; //需要定义的结构体
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static volatile int ev_press = 0;
struct fasync_struct *button_async;
struct pin_desc{
unsigned int pin;
unsigned int key_val;
};
static unsigned char key_val;
struct pin_desc pins_desc[4] = {
{S3C2410_GPF0 , 0x01},
{S3C2410_GPF2 , 0x02},
{S3C2410_GPG3 , 0x03},
{S3C2410_GPG11 , 0x04}
};
static struct pin_desc *irq_pd;
static DECLARE_MUTEX(button_lock); //定义互斥锁
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
/* 10ms后启动定时器 */
irq_pd = (struct pin_desc *)dev_id;
mod_timer(&buttons_timer, jiffies + HZ/100) ;
return IRQ_HANDLED;
}
static int sixth_drv_open(struct inode *inode, struct file *file)
{
if(file->f_flags & O_NONBLOCK){
if(down_trylock(&button_lock))
return -EBUSY;
}else{
down(&button_lock);
}
request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
return 0;
}
ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if(size != 1)
return -EINVAL;
if(file->f_flags & O_NONBLOCK){
if(!ev_press)
return -EBUSY;
}else{
wait_event_interruptible(button_waitq, ev_press);
}
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
int sixth_drv_close(struct inode *inode, struct file *file)
{
free_irq(IRQ_EINT0, &pins_desc[0]);
free_irq(IRQ_EINT2, &pins_desc[1]);
free_irq(IRQ_EINT11, &pins_desc[2]);
free_irq(IRQ_EINT19, &pins_desc[3]);
up(&button_lock);
return 0;
}
unsigned int sixth_drv_poll(struct file *file, poll_table *wait)
{
unsigned mask = 0;
poll_wait(file, &button_waitq, wait);
if(ev_press){
mask |= POLLIN | POLLRDNORM;
}
return mask;
}
static int sixth_drv_fasync (int fd, struct file *filp, int on)
{
printk("driver: sixth_drv_fasync\n");
return fasync_helper (fd, filp, on, &button_async);
}
static struct file_operations sixth_drv_fops = {
.owner = THIS_MODULE,
.open = sixth_drv_open,
.read = sixth_drv_read,
.release = sixth_drv_close,
.poll = sixth_drv_poll,
.fasync = sixth_drv_fasync,
};
int major;
static void buttons_timer_function(unsigned long data)//定时器处理函数
{
struct pin_desc *pindesc = irq_pd;
unsigned int pinval;
if(!pindesc)
return;
/* 读取PIN值 */
pinval = s3c2410_gpio_getpin(pindesc->pin);
/* 确定按键值 */
if(pinval){
/* 松开 */
key_val = 0x80 | pindesc->key_val;
}else{
/* 按下 */
key_val = pindesc->key_val;
}
ev_press = 1;
wake_up_interruptible(&button_waitq);
kill_fasync(&button_async, SIGIO, POLL_IN);
}
static int sixth_drv_init(void)
{
init_timer(&buttons_timer);
buttons_timer.function = buttons_timer_function;
add_timer(&buttons_timer);
major = register_chrdev(0, "sixth_drv", &sixth_drv_fops);
sixthdrv_class = class_create(THIS_MODULE, "sixthdrv");
sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
return 0;
}
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
class_device_unregister(sixthdrv_class_dev);
class_destroy(sixthdrv_class);
iounmap(gpfcon);
iounmap(gpgcon);
}
module_init(sixth_drv_init);
module_exit(sixth_drv_exit);
MODULE_LICENSE("GPL");
- 将中断函数中处理按键的程序转移到定时器处理函数,而中断函数处理重装载超时时间,转移的时候需要对中断函数中的dev_id参数进行全局处理,定时器函数处理需要其参数
- 在中断函数中的一条语句,修改定时器的超时时间 jiffies是个全局变量,系统每隔10ms产生系统时钟中断 cat /proc/interrupts,jiffies会累加,buttons_timer中有个expires超时时间变量,可以设置为当前值+某个值,1s就是HZ(100),即1s内jiffies会加100
mod_timer(&buttons_timer, jiffies + HZ/100) ;
- 入口函数中add_timer(&buttons_timer),把定时器告诉内核,当定时时间到,buttons_timer_function函数就被调用
- 入口函数中定时器初始化,一开始超时时间为0,而jiffier会大于0,因此在按键没按下会触发定时函数,解决方法在函数中加上判断if(!pindesc) return;
static void buttons_timer_function(unsigned long data)
{
struct pin_desc *pindesc = irq_pd;
unsigned int pinval;if(!pindesc)
return;
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int fd;
int main(int argc, char **argv)
{
unsigned char key_val;
int ret;
int Oflags;
fd = open("dev/buttons", O_RDWR);
if(fd < 0){
printf("can't open!\n");
return -1;
}
while(1){
ret = read(fd, &key_val, 1);
printf("key_val: 0x%x, ret = %d\n", key_val, ret);
}
return 0;
}
- 编译加载驱动,执行测试程序,执行结果,按下按键不会误多次触发