plat_button驱动学习总结
前面学习了LED驱动代码和基于platform总线的LED驱动代码,对于基本的ARM开发板驱动以及platform总线有了简单的了解,现在开始对按键的驱动进行学习总结。
按键驱动同样是基于platform总线来写的驱动,整个驱动和设备的注册过程和之前的plat_led驱动完全相同,不同的是在plat_led驱动的基础之上,增加了中断和阻塞以及轮询等相关知识。
初始化函数int __init s3c_button_init(void)
废话不多说,同样拿到驱动程序,首先映入眼帘的应该是初始化函数,在plat_button驱动程序中即int __init s3c_button_init(void)
static int __init s3c_button_init(void)
{
int ret = 0;
ret = platform_device_register(&s3c_button_device);//注册设备
if(ret)
{
printk(KERN_ERR "%s: Can't register platform device %d\n", __FUNCTION__, ret);
goto fail_reg_plat_dev;
}
dbg_print("Regist S3C %s Device successfully.\n", DEV_NAME);
ret = platform_driver_register(&s3c_button_driver);//注册驱动
if(ret)
{
printk(KERN_ERR "%s: Can't register platform driver %d\n", __FUNCTION__, ret);
goto fail_reg_plat_drv;
}
dbg_print("Regist S3C %s Driver successfully.\n", DEV_NAME);
return 0;
fail_reg_plat_drv:
platform_driver_unregister(&s3c_button_driver);
fail_reg_plat_dev:
return ret;
}
这里可以看到在初始化函数s3c_button_init中所做的事和plat_led驱动程序中的初始化函数所做的事情是相同的,即将设备和驱动注册到虚拟总线上,如果注册失败返回相应的错误信息并释放之前的注册内容,如果注册成功则打印注册成功信息并正常退出初始化函数。稍作说明的有:
* 宏 __init:编译器将标 init的所有代码存在特殊的内存段中,初始化结束后就释放这段内存
platform_driver函数
当设备和驱动都已经注册到总线之后,内核开始调用probe函数执行,这是由platform_driver结构体决定的。
static int s3c_button_probe(struct platform_device *dev)
{
int result = 0;
dev_t devno;
/*分配主次设备号 */
if (0 != dev_major) //如果已经有设备编号则静态获取
{
devno = MKDEV(dev_major, dev_minor);
result = register_chrdev_region(devno, 1, DEV_NAME);
}
else //否则动态获取设备号
{
result = alloc_chrdev_region(&devno, dev_minor, 1, DEV_NAME);
dev_major = MAJOR(devno);
}
/* 如果设备号获取失败,打印相关错误信息 */
if (result < 0)
{
printk("%s driver can't get major %d\n", DEV_NAME, dev_major);
return result;
}
/* 初始化 button_device 结构体并且注册 cdev 结构体*/
memset(&button_device, 0, sizeof(button_device));
button_device.data = dev->dev.platform_data;
cdev_init (&(button_device.cdev), &button_fops);//初始化cdev
button_device.cdev.owner = THIS_MODULE;
result = cdev_add (&(button_device.cdev), devno , 1); //添加cdev到内核
if (result) //如果添加失败打印错误信息
{
printk (KERN_NOTICE "error %d add %s device", result, DEV_NAME);
goto ERROR;
}
/*让驱动安装的时候自动创建节点*/
button_device.dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(button_device.dev_class))
{
printk("%s driver create class failture\n",DEV_NAME);
result = -ENOMEM;
goto ERROR;
}
/*2.6.24以下和以上的内核版本device_create函数参数不一样*/
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,24)
device_create(button_device.dev_class, NULL, devno, NULL, DEV_NAME);
#else
device_create (button_device.dev_class, NULL, devno, DEV_NAME);
#endif
printk("S3C %s driver version %d.%d.%d initiliazed.\n", DEV_NAME, DRV_MAJOR_VER, DRV_MINOR_VER, DRV_REVER_VER);
return 0;
ERROR:
printk("S3C %s driver version %d.%d.%d install failure.\n", DEV_NAME, DRV_MAJOR_VER, DRV_MINOR_VER, DRV_REVER_VER);
cdev_del(&(button_device.cdev));
unregister_chrdev_region(devno, 1);
return result;
}
有几点说明:
* 关于cdev结构体
内核中每一个字符设备都对应着一个cdev结构体,其定义如下:
cpp
struct cdev {
struct kobject kobj; // 每个 cdev 都是一个 kobject
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned int count; // 设备范围号大小
};
* register_chrdev_region函数
register_chrdev_region(dev_t first,unsigned int counter, char *name) 函数的作用为获取一个或多个设备编号,first为要分配设备编号起始值,count为设备号个数,name为设备名字
* struct class结构体
内核中定义了struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
* alloc_chrdev_region函数
其原型为:alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned count,char *name)
dev用来保存已分配范围的第一个编号,firstminor为第一个第一个被使用的次设备号,count为设备号个数,name为设备名字。
file_operations结构体
在驱动程序中我们保留了一些设备编号,file_operation结构体就是驱动程序的这些编号连接到内核。这个结构中的每一个成员变量都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,设置为NULL。该驱动程序中的file_operations为:
static struct file_operations button_fops = {
.owner = THIS_MODULE,
.open = button_open,
.read = button_read,
.poll = button_poll,
.release = button_release,
};
由结构体file_operations可以看到,设备和驱动注册成功之后,会依次调用open函数,read函数和poll函数。下面来看open函数实现了哪些功能。
button_open函数
static int button_open(struct inode *inode, struct file *file)
{
struct button_device *pdev ;
struct s3c_button_platform_data *pdata;
int i, result;
pdev = container_of(inode->i_cdev,struct button_device, cdev);//从inode结构体获取idev信息
pdata = pdev->data;
file->private_data = pdev;
/* Malloc for all the buttons remove dithering timer */
pdev->timers = (struct timer_list *) kmalloc(pdata->nbuttons*sizeof(struct timer_list), GFP_KERNEL);
if(NULL == pdev->timers)
{
printk("Alloc %s driver for timers failure.\n", DEV_NAME);
return -ENOMEM;
}
memset(pdev->timers, 0, pdata->nbuttons*sizeof(struct timer_list));
/* Malloc for all the buttons status buffer */
pdev->status = (unsigned char *)kmalloc(pdata->nbuttons*sizeof(unsigned char), GFP_KERNEL);
if(NULL == pdev->status)
{
printk("Alloc %s driver for status failure.\n", DEV_NAME);
result = -ENOMEM;
goto ERROR;
}
memset(pdev->status, 0, pdata->nbuttons*sizeof(unsigned char));
init_waitqueue_head(&(pdev->waitq));
for(i=0; i<pdata->nbuttons; i++)
{
/*初始化所有按键的状态为UP状态 */
pdev->status[i] = BUTTON_UP;
/* 初始化按键的中断服务程序 */
setup_timer(&(pdev->timers[i]), button_timer_handler, i);
/* 设置控制按键的GPIO管脚为下降沿触发 */
s3c2410_gpio_cfgpin(pdata->buttons[i].gpio, pdata->buttons[i].setting);//配置gpio引脚为中断模式
irq_set_irq_type(pdata->buttons[i].nIRQ, IRQ_TYPE_EDGE_FALLING);//中断模式为下降沿触发
/*一旦发生中断请求,调用s3c_button_intterupt函数*/
result = request_irq(pdata->buttons[i].nIRQ, s3c_button_intterupt, IRQF_DISABLED, DEV_NAME, (void *)i);
if( result )//如果中断申请失败
{
result = -EBUSY;
goto ERROR1;
}
}
return 0;
ERROR1:
kfree((unsigned char *)pdev->status);
while(--i)
{
disable_irq(pdata->buttons[i].nIRQ);
free_irq(pdata->buttons[i].nIRQ, (void *)i);
}
ERROR:
kfree(pdev->timers);
return result;
}
在open函数中我们初始化定时器,并设置相应GPIO引脚为中断模式,一旦中断触发,程序将进入到中断服务函数s3c_button_intterupt中执行,在s3c_button_intterupt函数中判断了相应的中断号,并打开定时器进行延时,延时到达之后程序进入定时器中断服务函数,执行响应按键产生的事件。
对于open函数应注意几点:
* 关于inode结构
inode结构体在内部表示文件,其包含了大量有关文件的信息,
struct inode {
dev_t i_rdev;
struct cdev * i_cdev;
}
其中i_rdev表示真正的设备编号,cdev表示字符设备的内核内部结构,当inode表示一个字符设备时,cdev包含了指向struct cdev的结构体指针
与file结构体不同,file表示打开的文件描述符,只有打开文件后才生效。对于同一个文件,可能有多个file结构,但它们都指向单个的inode结构体。
* 宏Container_of
Container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址。在该函数中,Container_of的目的为获取结构体button_device的地址。
* 关于timr_list
在linux内核中,定时器由timer_list结构体表示
在
中断服务程序s3c_button_intterupt
前面open函数中将按键对应的GPIO设置为中断模式,一旦中断触发,即按键按下,程序将会跳到中断服务程序即s3c_button_intterupt中执行
static irqreturn_t s3c_button_intterupt(int irq,void *de_id)//下降沿中断,中断服务函数
{
int i;
int found = 0;
struct s3c_button_platform_data *pdata = button_device.data;
for(i=0; i<pdata->nbuttons; i++)
{
if(irq == pdata->buttons[i].nIRQ)//如果检测到相应中断号
{
found = 1; //置标志位found为1
break;
}
}
/*如果found为0,说明没有中断产生,即没有按键按下,跳出函数*/
if(!found) /* An ERROR interrupt */
return IRQ_NONE;
/* 当进入中断后检测到按键状态为UP ,设置按键状态为BUTTON_UNCERTAIN,等待下一步验证*/
if(BUTTON_UP == button_device.status[i])
{
button_device.status[i] = BUTTON_UNCERTAIN;
mod_timer(&(button_device.timers[i]), jiffies+TIMER_DELAY_DOWN);//激活定时器
}
return IRQ_HANDLED;
}
在中断服务程序中,如果检测到按键对应的GPIO引脚为UP状态,先设置为BUTTON_UNCERTAIN模式,启用定时器,延时一段时间再判断GPIO的状态。称之为消抖。如果延时一定时间GPIO为低电平状态,说明按键确实按下。
定时器中断服务函数button_timer_handler()
在按键触发中断后,在中断服务函数中启用了定时器,用来消抖。当定时器的计数值溢出之后进入到定时器中断服务函数button_timer_handler()里面执行。
static void button_timer_handler(unsigned long data)//定时器中断服务函数
{
struct s3c_button_platform_data *pdata = button_device.data;
int num =(int)data;
int status = s3c2410_gpio_getpin( pdata->buttons[num].gpio );
if(LOWLEVEL == status)//如果按键状态为低电平
{
if(BUTTON_UNCERTAIN == button_device.status[num]) /* Come from interrupt */
{
button_device.status[num] = BUTTON_DOWN;
printk("%s pressed.\n", pdata->buttons[num].name);
/* Wake up the wait queue for read()/poll() */
button_device.ev_press = 1;
wake_up_interruptible(&(button_device.waitq));//唤醒等待队列
}
/* Cancel the dithering */
mod_timer(&(button_device.timers[num]), jiffies+TIMER_DELAY_UP);
}
else
{
//dbg_print("Key Released!\n");
button_device.status[num] = BUTTON_UP;
// enable_irq(pdata->buttons[num].nIRQ);
}
return ;
}
可以看到,在定时器中断服务函数中,延时一定的值之后,检测到按键对应GPIO的引脚为低电平的时候,判断为按键按下,并且在标准输出打印对应的按键按下这一消息。
在该函数中需要注意的几点:
- wake_up_interruptible()函数
其原型为void wake_up_interruptible (wait_queue_head_t *q);,起作用为 唤醒 q 指定的注册在等待队列上的进程。该函数不能直接的立即唤醒进程,而是由调度程序转换上下文,调整为可运行状态。
- mod_timer()函数
当一个定时器已经被插入到内核动态定时器链表中后,我们还可以通过mod_timer()函数修改该定时器的expires值。
button_read函数
根据结构体file_operations的成员顺序,现在来分析button_read函数
static int button_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct button_device *pdev = file->private_data;
struct s3c_button_platform_data *pdata;
int i, ret;
unsigned int status = 0;
pdata = pdev->data;
dbg_print("ev_press: %d\n", pdev->ev_press);
if(!pdev->ev_press)//如果按键没有按下,ev_press=0
{
if(file->f_flags & O_NONBLOCK)
{
dbg_print("read() without block mode.\n");
return -EAGAIN;
}
else
{
/* Read() will be blocked here */
dbg_print("read() blocked here now.\n");
/*该函数修改task的状态为TASK_INTERRUPTIBLE,意味着该进程将不会继续运行直到被唤醒,然后被添加到等待队列wq中。*/
wait_event_interruptible(pdev->waitq, pdev->ev_press);
}
}
pdev->ev_press = 0;//清除标志位
for(i=0; i<pdata->nbuttons; i++)
{
dbg_print("button[%d] status=%d\n", i, pdev->status[i]);
/*通过位操作判断哪一个按键按下,并将其状态保存到status*/
status |= (pdev->status[i]<<i);
}
/*将status保存的键值复制到用户空间buf地址*/
ret = copy_to_user(buf, (void *)&status, min(sizeof(status), count));
return ret ? -EFAULT : min(sizeof(status), count);
}
```
关于button_read函数,要注意一下几点
- **关于__user宏 **
__user表示该变量是从用户空间传过来的地址,不可直接使用
- **wait_event_interruptible**
- **copy_to_user()函数**
在linux系统下,内核空间与用户空间的内存不能直接互访,即两个空间的数据不能够直接通过地址传送,copy_to_user()函数完成用户空间到内核空间的复制,与之相对应的copy_from_user()函数完成内核空间到用户空间的复制。 在内核空间,驱动程序在button_read()函数中将按键的键值保存在status里面,但是在用户空间需要通过判断键值从而判断是哪一个按键按下,所以这里调用copy_to_user()函数将内核空间的status的值复制到用户空间的buf地址处。
- **键值的获取 **
在button_read函数中,如何通过键值判断是哪一个按键被按下呢,来举例说明
假如key3按下
status[0] = 0
status[1] = 0
status[2] = 1
status[3] = 0
status[2]<<2 | status = 0001<<2 | status = 0100 | 0000 = 0100
status = 0100 = 4
所以status = 4的时候表示按键3按下
####button_poll()函数
```cpp
static unsigned int button_poll(struct file *file, poll_table * wait)
{
struct button_device *pdev = file->private_data;
unsigned int mask = 0;
/*void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);*/
/*typedef struct poll_table_struct {
poll_queue_proc qproc;
unsigned long key;
} poll_table*/
poll_wait(file, &(pdev->waitq), wait);
if(pdev->ev_press)
{
mask |= POLLIN | POLLRDNORM; /* The data aviable */
}
return mask;
}
关于button_poll()函数有几点说明
- poll_wait函数
其原型为void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait),它的作用就是把当前进程添加到wait参数指定的等待列表(poll_table)中。需要注意的是这个函数是不会引起阻塞。
到此,plat_button驱动程序相对重要的部分分析到这里,下一篇内容将总结一下基于驱动程序的应用程序以及实例测试驱动程序。