分类: LINUX
2008-09-11 17:37:10
第12章 Linux字符设备驱动综合实例
本章将分析5个典型的字符设备驱动,在这些驱动中,将灵活地运用到前面各章所讲解的内容。
12.1节讲解按键的设备驱动,加深读者对字符设备驱动架构、阻塞与非阻塞、中断、定时器等相关知识的理解。
12.2节讲解触摸屏的设备驱动,触摸屏的设备驱动比按键的设备驱动稍微复杂一些,但是很类似。
12.3节讲解TI的DSP提供给通用CPU的HPI(主机并行接口)的设备驱动,硬件结构为ARM+DSP,ARM的总线连接DSP的HPI接口。
12.4节讲解通用NVRAM的设备驱动,并会引入一个新的概念,即miscdevice(混杂设备)。
12.5节讲解看门狗的设备驱动,它也被归入miscdevice,这一节还会引入两个新的概念,即platform_device(平台设备)和platform_driver(平台驱动)。
NVRAM和看门狗的设备驱动与普通字符设备驱动有细微的差别。
12.1按键的设备驱动
12.1.1 按键的硬件原理
在嵌入式系统中,按键的硬件原理比较简单,通过一个上拉电阻将处理器的外部中断(或GPIO)引脚拉高,电阻的另一端连接按钮并接地即可实现。如图12.1所示,当按钮被按下时,EINT10、EIN13、EINT14、EINT15上将产生低电平,这个低电平将中断CPU(图中的CPU为S3C2410),CPU可以依据中断判断按键被按下。
但是,仅仅依据中断被产生就认定有一次按键行为是很不准确的,所有按键、触摸屏等机械设备都存在一个固有的问题,那就是“抖动”,按键从最初接通到稳定接通要经过数毫秒,其间可能发生多次“接通―断开”的过程。如果不消除“抖动”的影响,一次按键可能被理解为多次按键。
消除按键抖动影响的方法是:在判断有键按下后,进行软件延时(如20ms,在延时过程中要屏蔽对应中断),再判断键盘状态,如果仍处于按键按下状态,则可以断定该按键被按下,流程如图12.2(a)所示。如果按键对应的引脚本身不具备中断输入功能,则可以改为完全查询方式,流程如图12.2(b)所示。
图12.1 按键的硬件原理 |
图12.2 确认按键的流程 |
12.1.2 按键驱动中的数据结构
设备驱动中主要要设计的数据结构是设备结构体,按键的设备结构体中应包含一个缓冲区,因为多次按键可能无法被及时处理,可以用该缓冲区缓存按键。此外,在按键设备结构体中,还包含按键状态标志和一个实现过程中要借助的等待队列、cdev结构体。为了实现软件延时,定时器也是必要的,但可以不包含在设备结构体中。代码清单12.1给出了按键设备结构体及定时器。
代码清单12.1 按键驱动的设备结构体、定时器
1 #define MAX_KEY_BUF 16 //按键缓冲区大小 2 typedef unsigned char KEY_RET; 3 //设备结构体: 4 typedef struct 5 { 6 unsigned int keyStatus[KEY_NUM]; //4个按键的按键状态 7 KEY_RET buf[MAX_KEY_BUF]; //按键缓冲区 8 unsigned int head, tail; //按键缓冲区头和尾 9 wait_queue_head_t wq; //等待队列 10 struct cdev cdev; //cdev结构体 10 } KEY_DEV; 11 static struct timer_list key_timer[KEY_NUM];//4个按键去抖定时器 |
在按键设备驱动中,可用一个结构体记录每个按键所对应的中断/GPIO引脚及键值,如代码清单12.2所示。
代码清单12.2 按键硬件资源、键值信息结构体
1 static struct key_info 2 { 3 int irq_no; //中断号 4 unsigned int gpio_port; //GPIO端口 5 int key_no; //键值 6 } key_info_tab[4] = 7 { 8 /*按键所使用的CPU资源*/ 9 { 10 IRQ_EINT10, GPIO_G2, 1 11 } 12 , 13 { 14 IRQ_EINT13, GPIO_G5, 2 15 } 16 , 17 { 18 IRQ_EINT14, GPIO_G6, 3 19 } 20 , 21 { 22 IRQ_EINT15, GPIO_G7, 4 23 } 24 , 25 }; |
按键设备驱动的文件操作结构体如代码清单12.3所示,主要实现了打开、释放和读函数,因为按键只是一个输入设备,所以不存在写函数。
代码清单12.3 按键设备驱动文件操作结构体
1 static struct file_operations s3c2410_key_fops = 2 { 3 owner: THIS_MODULE, 4 open: s3c2410_key_open, //启动设备 5 release: s3c2410_key_release, //关闭设备 6 read: s3c2410_key_read, //读取按键的键值 7 }; |
按键设备作为一种字符设备,在其模块加载和卸载函数中分别包含了设备号申请和释放、cdev的添加和删除行为,在模块加载函数中,还需申请中断、初始化定时器和等待队列等,模块卸载函数完成相反的行为,代码清单12.4和12.5分别给出了按键设备驱动的模块加载和卸载函数,代码清单12.6和12.7分别给出了模块加载和卸载所调用的申请和释放4个中断的函数。
代码清单12.4 按键设备驱动的模块加载函数
1 static int __init s3c2410_key_init(void) 2 { 3 ...//申请设备号,添加cdev 4 5 request_irqs(); //注册中断函数 6 keydev.head = keydev.tail = 0; //初始化结构体 7 for (i = 0; i < KEY_NUM; i++) 8 keydev.keyStatus[i] = KEYSTATUS_UP; 9 init_waitqueue_head(&(keydev.wq)); //等待队列 10 11 //初始化定时器,实现软件的去抖动 12 for (i = 0; i < KEY_NUM; i++) 13 setup_timer(&key_timer[i], key_timer_handler, i); 14 //把按键的序号作为传入定时器处理函数的参数 15 } |
代码清单12.5 按键设备驱动的模块卸载函数
1 static void __exit s3c2410_key_exit(void) 2 { 3 free_irqs(); //注销中断 4 ...//释放设备号,删除cdev 5 } |
代码清单12.6 按键设备驱动的中断申请函数
1 /*申请系统中断,中断方式为下降沿触发*/ 2 static int request_irqs(void) 3 { 4 struct key_info *k; 5 int i; 6 for (i = 0; i < sizeof(key_info_tab) / sizeof(key_info_tab[1]); i++) 7 { 8 k = key_info_tab + i; 9 set_external_irq(k->irq_no, EXT_LOWLEVEL, GPIO_PULLUP_DIS); 10 //设置低电平触发 11 if (request_irq(k->irq_no, &buttons_irq, SA_INTERRUPT, DEVICE_NAME, 12 i)) //申请中断,将按键序号作为参数传入中断服务程序 13 { 14 return - 1; 15 } 16 } 17 return 0; 18 } |
代码清单12.7 按键设备驱动的中断释放函数
1 /*释放中断*/ 2 static void free_irqs(void) 3 { 4 struct key_info *k; 5 int i; 6 for (i = 0; i < sizeof(key_info_tab) / sizeof(key_info_tab[1]); i++) 7 { 8 k = key_info_tab + i; 9 free_irq(k->irq_no, buttons_irq); //释放中断 10 } 11 } |
在键被按下后,将发生中断,在中断处理程序中,应该关闭中断进入查询模式,延迟20ms以实现去抖动,如代码清单12.8所示,这个中断处理过程只包含顶半部,无底半部。
代码清单12.8 按键设备驱动的中断处理程序
1 static void s3c2410_eint_key(int irq, void *dev_id, struct pt_regs *reg) 2 { 3 int key = dev_id; 4 disable_irq(key_info_tab[key].irq_no); //关中断,转入查询模式 5 6 keydev.keyStatus[key] = KEYSTATUS_DOWNX;//状态为按下 7 key_timer[key].expires == jiffies + KEY_TIMER_DELAY1;//延迟 8 add_timer(&key_timer[key]); //启动定时器 9 } |
在定时器处理程序中,查询按键是否仍然被按下,如果是被按下的状态,则将该按键记录入缓冲区。同时启动新的定时器延迟,延迟一个相对于去抖更长的时间(如100ms),每次定时器到期后,查询按键是否仍然处于按下状态,如果是,则重新启用新的100ms延迟;若查询到已经没有按下,则认定键已抬起,这个时候应该开启对应按键的中断,等待新的按键。每次记录新的键值时,应唤醒等待队列。定时器处理流程如图12.3所示,代码清单如12.9所示。
代码清单12.9 按键设备驱动的定时器处理函数
1 static void key_timer_handler(unsigned long data) 2 { 3 int key = data; 4 if (ISKEY_DOWN(key)) 5 { 6 if (keydev.keyStatus[key] == KEYSTATUS_DOWNX) 7 //从中断进入 8 { 9 keydev.keyStatus[key] = KEYSTATUS_DOWN; 10 key_timer[key].expires == jiffies + KEY_TIMER_DELAY; //延迟 11 keyEvent(); //记录键值,唤醒等待队列 12 add_timer(&key_timer[key]); 13 } 14 else 15 { 16 key_timer[key].expires == jiffies + KEY_TIMER_DELAY; //延迟 17 add_timer(&key_timer[key]); 18 } 19 } 20 else //键已抬起 21 { 22 keydev.keyStatus[key] = KEYSTATUS_UP; 23 enable_irq(key_info_tab[key].irq_no); 24 } 25 } |
图12.3 定时器处理函数流程 |
按键设备驱动的打开和释放函数比较简单,主要是设置keydev.head、keydev.tail和按键事件函数指针keyEvent的值,如代码清单12.10所示。
代码清单12.10 按键设备驱动的打开、释放函数
1 static int s3c2410_key_open(struct inode *inode, struct file *filp) 2 { 3 keydev.head = keydev.tail = 0; //清空按键动作缓冲区 4 keyEvent = keyEvent_raw; //函数指针指向按键处理函数keyEvent_raw 5 return 0; 6 } 7 8 static int s3c2410_key_release(struct inode *inode, struct file *filp) 9 { 10 keyEvent = keyEvent_dummy; //函数指针指向空函数 11 return 0; 12 } |
代码清单12.11给出了按键设备驱动的读函数,按键设备驱动的读函数主要提供对按键设备结构体中缓冲区的读并复制到用户空间。当keydev.head ! = keydev.tail时,意味着缓冲区有数据,使用copy_to_user()拷贝到用户空间,否则,根据用户空间是阻塞读还是非阻塞读,分为如下两种情况。
l 若采用非阻塞读,则因为没有按键缓存,直接返回- EAGAIN;
l 若采用阻塞读,则在keydev.wq等待队列上睡眠,直到有按键被记录入缓冲区后被唤醒。
代码清单12.11 按键设备驱动的读函数
1 static ssize_t s3c2410_key_read(struct file *filp, char *buf, ssize_t count, 2 loff_t*ppos) 3 { 4 retry: if (keydev.head != keydev.tail) 5 //当前循环队列中有数据 6 { 7 key_ret = keyRead(); //读取按键 8 copy_to_user(..); //把数据从内核空间传送到用户空间 9 } 10 else 11 { 12 if (filp->f_flags &O_NONBLOCK) 13 //若用户采用非阻塞方式读取 14 { 15 return - EAGAIN; 16 } 17 interruptible_sleep_on(&(keydev.wq)); 18 //用户采用阻塞方式读取,调用该函数使进程睡眠 19 goto retry; 20 } 21 return 0; 22 } |
最后,解释一下代码清单12.9第11行的keyEvent()函数和代码清单12.11的keyRead()函数。在设备驱动的打开函数中,keyEvent被赋值为keyEvent_raw,这个函数完成记录键值,并使用wait_up_interrupt(&(keydev.wq))语句唤醒s3c2410_key_read()第17行所期待的等待队列。而keyRead()函数则直接从按键缓冲区中读取键值。