一下讲解一个mini2440上运行的初级的字符设备驱动来对嵌入式驱动程序有个基本的入门。这个驱动能够支持的功能:中断方式产生按键值,poll方式和异步通信方式读取按键值,此外还加入内和定时器来进行键消抖,每次产生一个按键中断时就延时10ms再调用超时函数来读取键值。
#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>
#include <linux/poll.h>
static struct class *drv_class;
static struct class_device *drv_class_dev;
static struct timer_list buttons_timer; //定义一个定时器
//volatile unsigned long *gpfcon;
//volatile unsigned long *gpfdat;
struct fasync_struct * button_async_queue; //异步操作的文件结构体指针,其中最重要的成员是fd,用于和应用层的文件描述符fd关联起来,从而知道对应的进程
static DECLARE_WAIT_QUEUE_HEAD(button_waitq); //定义一个等待队列
/* 中断事件标志, 超时中断服务程序将它置1,drv_read将它清0 */
static volatile int ev_press = 0;
//定义按键的结构体
struct pin_desc{
unsigned int pin; //按键的物理地址
unsigned int key_val; //键值
};
/* 键值: 按下时,<span style="font-family: Arial, Helvetica, sans-serif;">四个按键的键值分别为 </span>0x01, 0x02, 0x03, 0x04 */
/* 键值: 松开时,<span style="font-family: Arial, Helvetica, sans-serif;">四个按键的键值分别为</span> 0x81, 0x82, 0x83, 0x84 */
static unsigned char key_val;//保存当前按键的键值
/*
* K1,K2,K3,K4对应GPG0,GPG3,GPG5,GPG6
*/
//定义4个按键的结构体变量
struct pin_desc pins_desc[4] = {
{S3C2410_GPG0, 0x01},
{S3C2410_GPG3, 0x02},
{S3C2410_GPG5, 0x03},
{S3C2410_GPG6, 0x04},
};
struct pin_desc *irq_pd; //保存当前按键的结构体,在超时中断处理函数中使用,通过按键中断处理函数的参数dev_id进行赋值
//按键中断处理函数,参数irq为中断号,dev_id为中断发生时传入的参数,该参数在注册中断的时候指定
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
irq_pd=(struct pin_desc*)dev_id; //通过参数dev_id保存按键到全局的按键结构体变量irq_pd中
mod_timer(&buttons_timer,jiffies+HZ/100); //修改定时器的超时时间为jiffies+1,即延时10ms发生超时
return IRQ_RETVAL(IRQ_HANDLED);
}
//每当应用程序调用fcntl(fd,F_SETFL,flags);时驱动层就会调用此函数,在异步操作文件结构体中button_async_queue更新信息为flags,因此要进行异步操作时flags应包含FASYNC参数。
static int fasync (int fd, struct file *filp, int model)
{
printk("seventh fasync\n");
return fasync_helper (fd, filp, model, &button_async_queue);
}
//应用层open函数对应的驱动层函数,参数inode表示物理层的设备,代表一个设备,file代表应用层打开的设备,可以有多个,
static int drv_open(struct inode *inode, struct file *file)
{
/* GPG0,GPG3,GPG5,GPG6为<span style="font-family: Arial, Helvetica, sans-serif;">四个按键的中断引脚: EINT8,EINT11,EINT13,EINT14 ,request_irq五个参数的含义:中断号,中断处理函数,中断触发方式(这里为双边沿触发,中断名称(可在/proc/interrupts中查看到),传递给中断处理函数的参数
request_irq(IRQ_EINT8, buttons_irq, IRQT_BOTHEDGE, "K1", &pins_desc[0]);
request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "K2", &pins_desc[1]);
request_irq(IRQ_EINT13, buttons_irq, IRQT_BOTHEDGE, "K3", &pins_desc[2]);
request_irq(IRQ_EINT14, buttons_irq, IRQT_BOTHEDGE, "K4", &pins_desc[3]);
return 0;
}
/*应用层read函数对应的驱动层函数,参数file表示fd所代表的已经打开的设备,buf代表read传来的内存起始地址,size为read指定的内存长度,ppos为文件读写的位置,在驱动层进行修改,在应用层进行读取。*/
ssize_t drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if (size != 1) //由于是读取的按键值为一个字节,那么
return -EINVAL;
/* 将当前进程挂到button_waitq等待队列中,如果ev_press为真则把当前进程放入阻塞队列进行休眠,等待唤醒,如果ev_press为假则把当前进程放入就绪队列,等待调度,而ev_press的意义表示按键是否读取到,若无按键,ev_press为0,进程休眠 */
wait_event_interruptible(button_waitq, ev_press);
/* 执行下面这一句时说明进程已经被唤醒,按键发生并且读取到按键值key_val,并传入用户空间的buf。插讲:如果从用户写入内核用函数copy_from_user(kernel_buf,user_buf,len) */
copy_to_user(buf, &key_val, 1);
ev_press = 0; //清除中断标记,用于下次调用read时的休眠
return 1;//返回读取的字节数给read
}
//应用层close对应的驱动层函数。
int drv_close(struct inode *inode, struct file *file)
{
/*当调用free_irq(int irq,void* dev_id)注销中断处理函数时(通常卸载驱动时其中断处理函数也会被注销掉),因为dev_id是唯一的,所以可以通过它来判断从共享中断线上的多个中断处理程序中删除指定的一个。如果没有这个参数,那么kernel不可能知道给定的中断线上到底要删除哪一个处理程序。*/
free_irq(IRQ_EINT8, &pins_desc[0]);
free_irq(IRQ_EINT11, &pins_desc[1]);
free_irq(IRQ_EINT13, &pins_desc[2]);
free_irq(IRQ_EINT14, &pins_desc[3]);
return 0;
}
//应用层poll(struct pollfd fds[],nfds_t nfds,int timeout)会调用sys_poll,sys_poll会调用此驱动层函数,把button_waitq放入wait等待队列,如果返回值mask不等0,那么应用层函数poll立马返回,否则button_waitq在wait队列上休眠timeout,期间不断查询mask返回值,一旦mask不等于0,或者超时时间到那么poll就返回。
static unsigned drv_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(file, &button_waitq, wait); // 不会立即休眠
if (ev_press)
mask |= POLLIN | POLLRDNORM;
return mask;
}
static struct file_operations fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = drv_open,
.read = drv_read,
.release = drv_close,
.poll = drv_poll,
.fasync = fasync,
};
此为定时器超时中断处理函数。<p>static void buttons_times_func(unsigned long data)
{
<span style="white-space:pre"> </span>struct pin_desc * pindesc = irq_pd;
<span style="white-space:pre"> </span>unsigned int pinval;
<span style="white-space:pre"> </span>if(!irq_pd)
<span style="white-space:pre"> </span>return;
<span style="white-space:pre"> </span>pinval = s3c2410_gpio_getpin(pindesc->pin);<span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">//获取按键引脚的高低电平值</span><span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);"></span><span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">//如果是高电平,说明按键松开</span>
<span style="white-space:pre"> </span>if (pinval)
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>/* 松开 */
<span style="white-space:pre"> </span>key_val = 0x80 | pindesc->key_val;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>else
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>/* 按下 */
<span style="white-space:pre"> </span>key_val = pindesc->key_val;
<span style="white-space:pre"> </span>}</p><p><span style="white-space:pre"> </span>ev_press=1; <span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">/* 表示中断发生了 */ </span></p><p><span style="white-space:pre"> </span>wake_up_interruptible(&button_waitq); <span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">/</span><span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);"> </span><span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">* 唤醒休眠的进程 */</span></p><p> <span style="white-space:pre"> </span>kill_fasync(&button_async_queue, SIGIO, POLL_IN); /<span style="font-family: monospace; white-space: pre; background-color: rgb(240, 240, 240);">/给button_async_queue发送SIGIO信号,button_async_queue中存储了驱动程序对应的进程</span>
}
int major; //保存主设备号</p><p>//安装驱动时的驱动初始化函数,加载驱动命令 "insmod 驱动名.ko"
static int seventh_drv_init(void)
{</p><p><span style="white-space:pre"> //初始化定时器</span>
<span style="white-space:pre"> </span>init_timer(&buttons_timer);</p><p><span style="white-space:pre"> //设定超时间</span>
<span style="white-space:pre"> </span>buttons_timer.expires=0;</p><p><span style="white-space:pre"> //设定定时器超时时调用的函数为buttons_times_func();</span>
<span style="white-space:pre"> </span>buttons_timer.function=buttons_times_func;</p><p> /*添加定时器buttons_timer到定时器链表。此处插讲一下内核定时器工作原理:内核有一个全局变量jiffies用来表示当前的滴答次数(一般来说一个滴答为10ms),每完成一次滴答,内核会检查定时链表中哪个定时器超时(检测的方法是比较当前的滴答值jiffies和定时器中的expires的大小,前者大就超时,反正未超时),对于已经超时的定时器则调用该定时器的超时中断处理函数。*/
<span style="white-space:pre"> </span>add_timer(&buttons_timer);
<span style="white-space:pre"> </span>irq_pd=NULL;</p><p><span style="white-space:pre"> </span>//参数0表示自动分别主设备号,并返回给major,这个函数将主设备号和fops关联起来
<span style="white-space:pre"> </span>major = register_chrdev(0, "seventh_drv", &sencod_drv_fops);
<span style="white-space:pre"> //在sys目录下创建一个名为drv的类</span>
<span style="white-space:pre"> </span>seventhdrv_class = class_create(THIS_MODULE, "seventh_drv");
/*在sys/drv目录下创建一个名为buttons的设备。如果文件系统具有udev机制,那么udev会根据sys目录的一些信息在dev目录下生成buttons设备文件,供应用程序使用。那么这插讲一下udev机制,一般在文件系统用udev的简化版mdev,也是根据内核信息来创建设备文件,动态更新/dev目录,支持热插拔。在/etc/init.d/rcS文件中加入echo /sbin/mdev > /proc/sys/kernel/hotplug,表示如果sys目录下有设备更新,那么会调用/sbin/mdev程序来进行/dev目录的更新,创建相应的设备文件。*/
<span style="white-space:pre"> </span>seventhdrv_class_dev = class_device_create(seventhdrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
<span style="white-space:pre"> </span>//进行io映射,0x56000050是gpfcon寄存器的物理地址,在内核态中必须转换为虚拟地址才能使用
//<span style="white-space:pre"> </span>gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
//<span style="white-space:pre"> </span>gpfdat = gpfcon + 1;
<span style="white-space:pre"> </span>return 0;
}
//卸载驱动时调用的函数,卸载驱动命令 "rmmod 驱动名"
static void seventh_drv_exit(void)
{
<span style="white-space:pre"> </span>unregister_chrdev(major, "seventh_drv");
<span style="white-space:pre"> </span>class_device_unregister(seventhdrv_class_dev);
<span style="white-space:pre"> </span>class_destroy(seventhdrv_class);
//<span style="white-space:pre"> </span>iounmap(gpfcon);
<span style="white-space:pre"> </span>return 0;
}
//这个宏告诉内核drv_init()函数为加载驱动时调用的函数
module_init(seventh_drv_init);
//这个宏告诉内核drv_exit()函数为卸载驱动时调用的函数
module_exit(seventh_drv_exit);
MODULE_LICENSE("GPL");</p>
应用层测试程序:
1. 异步方式读取按键:属于被动方式读取,类似于中断,效率高
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
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;
signal(SIGIO, my_signal_fun); //注册信号SIGIO的处理函数
fd = open("/dev/buttons", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
fcntl(fd, F_SETOWN, getpid());//告诉内核,驱动程序fd设备文件对应的进程
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags | FASYNC); //给fd加入FASYNC标记,支持异步方式,会调用驱动程序的file_operation对应的.fasyn函数
while (1)
{
sleep(1000); //睡眠,在实际应用中程序可以干别的事情。
}
return 0;
}
2、poll方式查询按键值,效率低,占用cpu:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
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("can't open!\n");
}
fds[0].fd = fd;
fds[0].events = POLLIN; //设置查询方式为可读
while (1)
{
ret = poll(fds, 1, 5000);//超时时间为5s
if (ret == 0)
{
printf("time out\n");
}
else
{
read(fd, &key_val, 1);
printf("key_val = 0x%x\n", key_val);
}
}
return 0;
}