老查的ARM学习笔记:chapter-1(按键驱动程序设计)
前面的博客中,有一篇通过按键玩中断的文章,不过那里的程序是裸机,也就是没有加系统下设计的程序,也就和在单片机中设计的程序一样比较简单。现在我们来看看按键的驱动程序在linux系统下是如何设计的。
1 混杂设备驱动模型**
1 混杂设备驱动描述
首先我们先来了解一下什么是混杂设备驱动模型。混杂设备其实是字符设备中的一种,主设备号是10,次设备号不同的设备称为混杂设备,在linux中,用struct miscdevice来描述一个混杂设备,从内核源码中复制过来结构原型为
struct miscdevice {
int minor; /*次设备号*/
const char *name; /*设备名*/
const struct file_operations *fops; /*文件操作*/
struct list_head list;
struct device *parent;
struct device *this_device;
const char *nodename;
mode_t mode;
};
2 注册混杂设备驱动
linux中使用misc_register函数来注册一个混杂设备驱动
int misc_register(struct miscdevice *misc)
3 注销混杂设备驱动
misc_deregister(struct miscdevice *misc)
通过上面的概述,可以通过下面的导图来搭建一个简单的按键模型驱动key.c。
#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
MODULE_LICENSE("GPL");
int key_open(struct inode *node,struct file *filp)
{
return 0;
}
struct file_operations key_fops=
{
.open = key_open,
};
struct miscdevice key_miscdev = {
.minor = 200,
.name = "key",
.fops = &key_fops,
};
static int key_init()
{
misc_register(&key_miscdev);
return 0;
}
static void key_exit()
{
misc_deregister(&key_miscdev); //注销混杂设备
}
module_init(key_init);
module_exit(key_exit);
2 中断处理流程分析
按键驱动程序中,一般采用中断的方式去处理,那我们就先来看看中断处理的步骤是什么样的,在裸机程序部分当中,步骤可分为以下三步。
1 中断存在统一入口,这个入口在start.S汇编文件中,每当中断发生的时候,都会把irq发送给pc。
2 注册中断处理程序。
3 根据中断源的编号来调用中断处理程序。
在linux中,统一的入口也是存在的,在entry-arm.S文件当中的irq_svc。这里我就来简单说一下这个流程是怎么工作的
1 首先,程序通过irq_svc找到中断入口。
2 其次拿到产生中断源的编号,也就是中断号。在裸机中,直接调取中断源和中断号就直接去操作了,但是在linux系统中,引入了irq_desc数据结构,其中含有已经有注册好的处理函数。
3 最后根据取出来事先注册好的中断处理函数来运行。
上面分析的就是为了说明在驱动中如果要用中断,驱动程序该干嘛。驱动程序有两个作用,第一是实现中断处理程序,第二个是注册中断到linux系统中。
中断处理程序设计步骤为:
1 注册中断
这步是在按键中断初始化中进行的,request_irq函数用于注册中断。
int request_irq(unsigned int irq,void (*handler)(int, void*, struct pt_regs *),
unsigned long flags,
const char *devname,
void *dev_id)
参数说明:
unsigned int irq :中断号
void(handler)(int,void ):中断处理函数
unsigned long flags:与中断管理有关的各种选项
const char *devname:设备名
void *dev_id:共享中断时使用
在flags参数中, 可以选择一些与中断管理有关的选项,如:
. IRQF_DISABLED(SA_INTERRUPT) 快速中断
如果设置该位,表示是一个“快速”中断处理程序;如果没有设置该位,那么就是一个“慢速”中断处理程序。
. IRQF_SHARED(SA_SHIRQ) 共享中断该位表明该中断号是多个设备共享的。对于共享中断,dev_id不同对应不同的设备中断,这就是dev_id 的作用。
快/慢速中断的主要区别在于:快速中断保证中断处理的原子性(不被打断),而慢速中断则不保证。换句话说,也就是“开启中断”标志位(处理器IF)在运行快速中断处理程序时是关闭的,因此在服务该中断时,不会被其他类型的中断打断;而调用慢速中断处理时,其他类型的中断仍可以得到服务。
2 中断处理
中断处理函数的原型是 irqreturn_t(int ib_irq, void *dev_id,)
这步是独立定义的,处理程序还包括
1检查设备是否产生了中断
2 清除中断产生的标志
3 相应的硬件操作
3 注销处理
当设备不再需要使用中断时(通常在驱动卸载时), 应当把它们注销, 使用函数:
void free_irq(unsigned int irq, void *dev_id)
这步是在按键模块卸载时进行的
3 按键驱动程序设计
在Linux中,硬件的初始化通常在两个地方进行。一个是在open函数实现,另外一个是在模块的初始化中实现。习惯性的在模块的初始化中实现硬件的初始化void key_hw_init()。按键的初始化要参考之前裸机程序,硬件原理图以及相关GPIO设置这里先贴上OK6410开发板上的按键硬件原理图部分:
从图中可以看出ok6410的按键中断是通过GPNCON控制寄存器来确定其功能的。
首先定义GPNCON的宏#define GPNCON 0x7f008830,S2按键,也就是KEYINT对应的位为GPN0,设置为10为外部中断。由于在linux中,不能直接使用物理地址,需要先将其转化为虚拟地址,使用的函数是gpio_config=ioremap(GPNCON,4),参数4意为分配给地址的虚拟地址4个字节,从原有寄存器中读取值使用readw(gpio_config)。
在request_irq()注册中,因为当按键按下去的时候触发中断,所以这里把第三个参数设置为下降沿触发中断,IRQF_TRIGGER_FALLING,那么第一个参数中断号irqnumber是怎么获得的呢?
在内核源码irqs.h中可以找到与芯片手册中一一对应的中断号,这个中断号就是注册函数中第一个参数。
从图中可以看出,外部中断0—4使用的中断源为INT_EINT0。
在OK6410的内核源码irqs.h可以看出,中断号需要加上一个偏移,这个偏移是32,当然在2410上也需要加上一个偏移,只不过2410上的偏移是16。这样的方式称为软中断。硬件产生的中断号只是一个序号,和芯片手册上是对应的序号,这个序号需要加上32才成为linux当中的中断号。不过内核代码已经帮我们加上了这个偏移,所以我们在填写参数的时候只需要把相应的中断名放进去就行了,由于这里采用的是S2按键对应的是EINT0中断,所以第一个参数就是IRQ_EINT0。
根据以上分析,可以在原有代码上增加中断处理部分,以下为增加按键中断功能后的代码。
#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>
#define GPNCON 0x7f008830
MODULE_LICENSE("GPL");
irqreturn_t key_int(int irq,void *dev_id)
{
//1 检测中断是否产生
//2 清除已经发生的按键中断
//3. 硬件相关操作
printk(KERN_WARNING"key down!\n"); // 执行打印的时候,必须加上KERN_WARNING前缀
return 0;
}
void key_hw_init(void)
{
unsigned int *gpio_config;
unsigned short data;
gpio_config=ioremap(GPNCON,4);
data=readw(gpio_config);
data&=~0b00;//先清零
data |= 0b10;//后两位设置成0b10
writew(data,gpio_config);
printk(KERN_WARNING"init ...!\n");
}
int key_open(struct inode *node,struct file *filp)
{
return 0;
}
struct file_operations key_fops=
{
.open = key_open,
};
struct miscdevice key_miscdev = {
.minor = 200,
.name = "key",
.fops = &key_fops,
};
static int key_init(void)
{
misc_register(&key_miscdev);
//按键初始化 硬件初始化部分一般可一放在模块初始化部分或者open函数中 这里放在模块初始化部分
key_hw_init();
//注册中断处理程序
request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);
return 0;
}
static void key_exit(void)
{
misc_deregister(&key_miscdev); //注销混杂设备
//注销中断处理程序
free_irq(IRQ_EINT(0),0);
printk(KERN_WARNING"key up!");
}
module_init(key_init);
module_exit(key_exit);
通过将上述代码下载到开发板中去,然后加载insmod key.ko,按下按键S2会在开发板上打印key down!
4 中断分层技术
首先来介绍一下中断分层,中断分层含有三种,分别是中断嵌套,中断分层方式,使用工作队列实现分层。为什么要引入中断分层,这里还得再提两点,慢速中断和快速中断。
1 慢速中断概述 当A执行过程中,有B中断产生,则会执行B中断,执行完之后执行A中断,但是当A执行过程中有另外一个同类型的A1中断产生,则A1会被忽略,一直在执行A。
2 快速中断概述 当A执行过程中,有B中断产生,B则会被忽略,同样A1会被忽略
对比1和2,会发现不管是快/慢速中断,是否为同类型的中断,都会产生中断丢失现象的可能,这不是我们想看到的,所以引入中断分层技术。
中断处理程序会做关于硬件和无关硬件的两个工作,中断处理程序随之可以分为两部分,一部分处理和硬件相关的,另外一部分处理其他无关硬件的,这就是中断分层,来减少中断处理程序运行的时间减少丢失的可能性。
中断分层方式分为
软中断
tasklet
工作队列
其中工作队列使用的较多
我们可以利用导图来设计工作队列
工作队列是怎么描述的呢?
工作队列使用struct workqueue_struct来描述,
struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name; /*workqueue name*/
int singlethread;
int freezeable; /* Freeze threads during suspend */
int rt;
};
Linux内核使用struct work_struct来描述一个工作项:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
其中,func成员是最重要的成员。
使用工作队列
要使用工作队列,同样也是要当做一个模块来进行程序设计的,创建一个模块命名为queue.c
同样要搭建字符设备模型
1 创建工作队列,使用create_workqueue,create_workqueue函数只有一个参数,参数为工作队列的名字,返回的为创建好的一个工作队列指针。
2 创建工作 使用的是INIT_WORK(*work func),它有两个参数,第一个是要初始化的工作work指针,第二个是执行的函数。首先要定义一个work_struct函数描述一个工作项,其次是给指针分配空间,使用kmalloc,最后是定义所执行的函数。
3 挂载工作队列 使用的是queue_work(*workqueue_struct,*work_struct)函数,有两个参数,第一个是工作队列的指针,第二个是要挂载的工作,就是创建好工作的指针。
下面根据上面的分析这里贴出一个示例小程序:
#include<linux/module.h>
#include<linux/init.h>
#include <linux/slab.h> /* for kmalloc */
struct workqueue_struct *my_wq; //定义一个工作队列指针
struct work_struct *work1; //定义一项工作
struct work_struct *work2; //定义一项工作
MODULE_LICENSE("GPL");
void work1_func(struct work_struct *work)
{
printk(KERN_WARNING"this is work1>\n");
}
void work2_func(struct work_struct *work)
{
printk(KERN_WARNING"this is work2>\n");
}
int init_que(void)
{
//1. 创建工作队列
my_wq = create_workqueue("my_queue");
//2. 创建工作
//work1 = kmalloc(sizeof(struct work_struct), GFP_KERNEL);
work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
INIT_WORK(work1 , work1_func );
//3. 挂载(提交)提交工作
queue_work(my_wq, work1);
//2. 创建工作
work2 = kmalloc(sizeof(struct work_struct), GFP_KERNEL);
INIT_WORK(work2 , work2_func );
//3. 挂载(提交)提交工作
queue_work(my_wq, work2);
return 0;
}
void clean_que(void)
{
}
module_init(init_que);
module_exit(clean_que);
将queue.c在linux中make后,把queue.ko文件复制到根文件系统中去,在开发板上安装queue.ko模块,实验结果如下。
程序在执行的时候,linux内核会主动的去创建内核线程,所以代码中并没有创建线程。在linux系统中,需要自己去创建工作队列的情况不多,linux内核为我们创建好了工作队列,我们只要创建工作,并把工作挂载到工作队列即可。
linux内核为我们创建好的队列是keventd_wq,我们自己做两件事就可以了。
1 创建工作
2 提交工作到默认队列
schedule_work(struct work_sruct *work),它只有一个参数,要提交工作的指针
现在在key.c中定义一项工作,把中断改为分层处理的方式。
按键中断处理程序 硬件处理部分比较简单,中断上半部 硬件中断处理基本可以不做。在原来代码中把创建工作的部分放到模块的初始化中,把定义的工作队列和工作项workqueue_struct, work_struct以及func都复制到key.c中,再把原来中断处理程序的第三步执行硬件操作改为提交下半部使用schedule_work(work1);这样就把和硬件无关的工作提取出来了,为系统节省更多的时间出来,避免应为中断程序处理部分耗时过长造成中断丢失!
#include <linux/module.h>
#include <linux/init.h>
#include <linux/miscdevice.h> /* for struct miscdevice*/
#include <linux/interrupt.h>
#include <linux/fs.h> /* for iormap */
#include <linux/io.h>
#include <linux/slab.h> /* for kmalloc */
#define GPNCON 0x7F008830
struct work_struct *work1;//定义一项工作
void work1_func(struct work_struct *work)
{
printk(KERN_WARNING"key down!\n");
}
irqreturn_t key_int(int irq, void *dev_id)
{
//1. 检测是否发生了按键中断 这里可以暂时不做,因为这里没有使用共享中断
//2. 清除已经发生的按键中断 这个是指硬件内部处理,按键CPU内部不需要做处理
//3. 提交下半部
schedule_work(work1);
return 0;
}
void key_hw_init(void) //按键硬件初始化部分
{
unsigned int *gpio_config;
unsigned short data;
gpio_config = ioremap(GPNCON, 4);//将物理地址转化为虚拟地址
data = readw(gpio_config);
data &= ~0b11; //先清零
data |= 0b10; //后两位设置成0b10
writew(data, gpio_config);
printk(KERN_WARNING"init ...!\n");
}
int key_open(struct inode *node, struct file *filp)
{
printk(KERN_WARNING"open ...!\n");
return 0;
}
struct file_operations key_fops =
{
.open = key_open,
};
struct miscdevice key_miscdev = //定义一个misdevice结构
{
.minor = 200,
.name = "key",
.fops = &key_fops,//这里key_fops是一个struct file_operations结构
};
static int key_init(void)
{
misc_register(&key_miscdev);
//按键硬件初始化
key_hw_init();
//注册中断处理程序
request_irq(IRQ_EINT(0)),key_int,IRQF_TRIGGER_FALLING,"key",0);
//创建工作
work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
INIT_WORK(work1,work1_func);
return 0;
}
static void key_exit(void)
{
misc_deregister(&key_miscdev); //注销混杂设备
//注销中断处理程序
free_irq(IRQ_EINT(0),0);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("key driver");
同样把key.ko文件放到6410板子上,可以发现当按键按下的时候,会打印key down!
5 按键定时器去抖
学过单片机的同学都知道,在按键部分的学习中,必学按键消抖,最简单的方法是延时判断,就是利用for循环等待消抖,但是操作系统不采用这种方法,因为它太占资源,操作系统采用定时器来进行消抖。
下面我们根据导图来设计代码。
linux内核采用struct timer_list来描述一个定时器
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_base *base;
};
其中,主要的参数是expire和function。
expires 说明定时器定时多长时间
function 是一个指针,若定时器超时的时候,去执行什么工作,需要自己去设置
1 初始化定时器init_timer只有一个参数,init_timer是timer_list类型的,参数为timer_list的地址。
2 设置并且定义一个超时函数
key_timer.function=key_timer_func;
void key_timer_func(unsigned long data)
{
}
3 注册定时器
add_timer(&key_timer)
启动定时器
应在中断处理程序当中启动定时器,使用的函数为
mod_timer(&key_timer,jiffer+HZ/10)
定时器在100ms后超时,HZ代表的是1s。jiffer代表的是当前时间,HZ/10代表是100ms.
定时器超时的时候需要定义一个超时函数
void key_timer_func()
具体代码如下
#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>/* for iormap */
#include <linux/slab.h> /* for kmalloc */
#define GPNCON 0x7f008830
#define GPNDAT 0x7f008834
unsigned int *gpio_data;
struct work_struct *work1;
struct timer_list key_timer;
void key_timer_func(unsigned long data)
{
unsigned int key_val;
key_val = readw(gpio_data)&0x1;//因为是S2按键,只读取GPNCON的第0位
if (key_val == 0)
printk(KERN_WARNING"key down!\n");
}
void work1_func(struct work_struct *work)
{
mod_timer(&key_timer, jiffies + (HZ /10));
}
irqreturn_t key_int(int irq,void *dev_id)
{
//1 检测中断是否产生
//2 清除已经发生的按键中断
//3 提交下半部
schedule_work(work1);
return 0;
}
void key_hw_init(void)
{
unsigned int *gpio_config;
unsigned short data;
gpio_config=ioremap(GPNCON,4);
data=readw(gpio_config);
data&=~0b00;
data |= 0b10;
writew(data,gpio_config);
gpio_data=ioremap(GPNDAT,4);
printk(KERN_WARNING"init ...!\n");
}
int key_open(struct inode *node,struct file *filp)
{
return 0;
}
struct file_operations key_fops=
{
.open = key_open,
};
struct miscdevice key_miscdev = {
.minor = 200,
.name = "key",
.fops = &key_fops,
};
static int key_init(void)
{
misc_register(&key_miscdev);//注册一个混杂设备驱动设备
//按键硬件初始化
key_hw_init();
//注册中断处理程序
request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);
//创建工作
work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
INIT_WORK(work1,work1_func);
//初始化定时器
init_timer(&key_timer);
key_timer.function = key_timer_func;
//注册定时器
add_timer(&key_timer);
return 0;
}
static void key_exit(void)
{
misc_deregister(&key_miscdev); //注销混杂设备
//注销中断处理程序
free_irq(IRQ_EINT(0),0);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("key driver");
同样,将上面的代码make后,把.ko文件放到开发板上去,按下按键的同时可以打印key down的效果。
6 驱动支持多按键优化
由于上面只针对一个按键进行实验,现在要多增加按键。这里除了S2按键,再增加一个按键S3,根据上面的讲解,增加按键部分就比较简单了,仅仅需要修改下判断部分和注册中断处理部分以及注销部分就可以了。下面提供上代码
#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include<linux/slab.h>
#define GPNCON 0x7f008830
#define GPNDAT 0x7F008834
unsigned int *gpio_data;
unsigned int key_num = 0;
struct work_struct *work1;
struct timer_list key_timer;
void key_timer_func(unsigned long data)
{
unsigned int key_val;
key_val = readw(gpio_data)&0x1;//只读取最后一位
if (key_val == 0)
printk(KERN_WARNING"OK6410 S2 key down!\n");
key_val = readw(gpio_data)&0x2;//只读取倒数第二位
if (key_val == 0)
printk(KERN_WARNING"OK6410 S3 key down!\n");
}
void work1_func(struct work_struct *work)
{
mod_timer(&key_timer, jiffies + (HZ /10));
}
irqreturn_t key_int(int irq,void *dev_id)
{
//1 检测中断是否产生
//2 清除已经发生的按键中断
//3. 提交下半部
schedule_work(work1);
return 0;
}
void key_hw_init()
{
unsigned int *gpio_config;
unsigned short data;
gpio_config=ioremap(GPNCON,4);
data=readw(gpio_config);
data&=~0b00;
data |= 0b10;
writew(data,gpio_config);
gpio_data=ioremap(GPNDAT,4);
}
int key_open(struct inode *node,struct file *filp)
{
return 0;
}
struct file_operations key_fops=
{
.open = key_open,
};
struct miscdevice key_miscdev = {
.minor = 200,
.name = "key",
.fops = &key_fops,
};
static int key_init()
{
misc_register(&key_miscdev);
//按键硬件初始化
key_hw_init();
//注册中断处理程序
request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);
request_irq(IRQ_EINT(1),key_int,IRQF_TRIGGER_FALLING,"key",1);
//创建工作
work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
INIT_WORK(work1,work1_func);
//初始化定时器
init_timer(&key_timer);
key_timer.function = key_timer_func;
//注册定时器
add_timer(&key_timer);
return 0;
}
static void key_exit()
{
misc_deregister(&key_miscdev); //注销混杂设备
//注销中断处理程序
free_irq(IRQ_EINT(0),0);
free_irq(IRQ_EINT(1),1);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("key driver");
执行的效果如图,当按下按键时,相应的按键就会被打印出来。
7 阻塞型驱动设计
1 阻塞必要性
在正规驱动代码编写过程中,对于设备方法的读和写两个方法,阻塞是必要的。
例如:A进程想从设备B读数据,B中此时还没有数据,B的驱动程序先让A进程睡眠,等到B有数据的时候,B驱动程序再把A进程唤醒,让A继续读取数据。简单的说就是驱动程序阻塞进程,使它进入等待状态。
2 内核等待队列
驱动程序把进程睡眠的时候是通过把进程放到等待队列中
定义等待队列
wait_queue_head_t my_queue
初始化等待队列
init_waitqueue_head(&my_queue)
定义+初始化等待队列
DECLARE_WAIT_QUEUE_HEAD(my_queue)
进入等待队列,睡眠
wait_event(queue,condition)
当condition(布尔表达式)为真时,立即返回;否则让进程进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。
wait_event_interruptible(queue,condition)
当condition(布尔表达式)为真时,立即返回;否则让进程进入TASK_INTERRUPTIBLE的睡眠,并挂在queue参数所指定的等待队列上。
两者的区别在于进入睡眠状态不一样,前者进入不可中断,后者进入可中断的状态。
int wait_event_killable(queue, condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_KILLABLE的睡眠,并挂在queue参数所指定的等待队列上。
从等待队列中唤醒进程
wake_up(wait_queue_t *q)
从等待队列q中唤醒状态为TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE,TASK_KILLABLE 的所有进程。
下面通过导图来进行阻塞型前驱动的设计。
下面是驱动代码key.c
#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include<linux/slab.h>
#include <linux/sched.h>
#define GPNCON 0x7f008830
#define GPNDAT 0x7F008834
unsigned int *gpio_data;
unsigned int key_num = 0;
struct work_struct *work1;
wait_queue_head_t key_q;//定义等待队列
struct timer_list key_timer;
void key_timer_func(unsigned long data)
{
unsigned int key_val;
key_val = readw(gpio_data)&0x1;
if (key_val == 0)
//printk(KERN_WARNING"OK6410 S2 key down!\n");
key_num=2;
key_val = readw(gpio_data)&0x2;
if (key_val == 0)
//printk(KERN_WARNING"OK6410 S3 key down!\n");
key_num=3;
wake_up(&key_q);//唤醒等待队列
}
void work1_func(struct work_struct *work)
{
mod_timer(&key_timer, jiffies + (HZ /10)); //设置100ms超时 1HZ=1S
}
irqreturn_t key_int(int irq,void *dev_id)
{
//1 检测中断是否产生
//2 清除已经发生的按键中断
//3. 提交下半部
schedule_work(work1);
//return 0;
return IRQ_HANDLED;
}
void key_hw_init()
{
unsigned int *gpio_config;
unsigned short data;
gpio_config=ioremap(GPNCON,4);
data=readw(gpio_config);
data&=~0b00;
data |= 0b10;
writew(data,gpio_config);
gpio_data=ioremap(GPNDAT,4);
}
int key_open(struct inode *node,struct file *filp)
{
return 0;
}
ssize_t key_read(struct file *filp, char __user *buf, size_t size, loff_t *pos)//应用程序读取按键的时候,要实现read的设备方法被应用程序调用
{
wait_event(key_q,key_num); //若key_num为0,就是没有数据的时候,进入睡眠,并挂在key_q这个等待队列上
printk(KERN_WARNING"in kernel :key num is %d\n",key_num);
copy_to_user(buf, &key_num, 4);//提供4个字节给应用程序
key_num=0;//此时清零用来下次读取
return 4;
}
struct file_operations key_fops=
{
.open = key_open,
.read = key_read,
};
struct miscdevice key_miscdev = {
.minor = 200, //次设备号
.name = "key",
.fops = &key_fops,
};
static int keys_init()
{
misc_register(&key_miscdev);
//按键硬件初始化
key_hw_init();
//注册中断处理程序
request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);
request_irq(IRQ_EINT(1),key_int,IRQF_TRIGGER_FALLING,"key",1);
//创建工作
work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
INIT_WORK(work1,work1_func);
//初始化定时器
init_timer(&key_timer);
key_timer.function = key_timer_func;
//注册定时器
add_timer(&key_timer);
//初始化等待队列
init_waitqueue_head(&key_q);
return 0;
}
static void key_exit()
{
misc_deregister(&key_miscdev); //注销混杂设备
//注销中断处理程序
free_irq(IRQ_EINT(0),0);
free_irq(IRQ_EINT(1),1);
}
module_init(keys_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("key driver");
下面是应用程序代码key_app
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd;
int key_num;
int ret;
fd=open("/dev/6410key",0); //在开发板上要创建的设备名字
if (fd<0)
printf(" open device fail!\n");
ret= read(fd,&key_num,4);
if(ret==-1)
{
printf("read fail\n",key_num);
}
printf("key is %d\n", key_num);
close(fd);
return 0;
}
在linux中,先将应用程序部分编译好了放入开发板上去
接着把key.ko复制到开发板上去,创建一个设备mknode /dev/6410key c 10 200
其中,/dev/6410key 是应用程序中设置好的,10是主设备号 200是驱动程序中设置的次设备号,执行key_app,的时候,程序会等待响应,即进入睡眠状态,当按下一个按键的时候,就会打印相应的按键值。
在以后的代码中,阻塞型的驱动程序才是正规的代码,所以这节就显得尤为重要。
下面附上本文驱动程序设计的总体导图
按键驱动到此就讲解完了,驱动的学习道路非常艰辛,本文花费一整天的时间来整理,难免有不足之处,希望各路大神可以指明本文的不足缺陷。