07-S3C2440驱动学习(一)嵌入式linux字符设备驱动-查询+中断+引入poll机制的按键驱

2019-07-12 20:37发布

一、查询方式的按键驱动程序

查询方式的按键驱动程序,与LED驱动程序类似,我们来复习一下上节的写好的字符设备驱动程序框架,改写出查询方式的按键驱动程序。 (1)按键驱动程序如下: Open中配置引脚 Read中返回引脚状态 入口函数:地址映射 虚拟地址 #include #include #include #include #include #include #include #include #include #include //相关函数的头文件 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 sencod_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", &sencod_drv_fops); //注册驱动程序中的方法,生成以主设备号为索引,sencod_drv_fops为内容的字符设备数组 seconddrv_class = class_create(THIS_MODULE, "second_drv"); //自动生成字符设备节点 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); return 0; } module_init(second_drv_init); //标记second_drv_init为入口函数,设备注册驱动insmod的时候,调用该方法 module_exit(second_drv_exit); //标记second_drv_exit为出口函数,设备注册驱动rmmod的时候,调用该方法 MODULE_LICENSE("GPL"); //遵循"GPL"协议,不写会产生一些错误
(2)APP应用测试程序:
#include #include #include #include /* seconddrvtest */ 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("can't open! "); } 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 ", cnt++, key_vals[0], key_vals[1], key_vals[2], key_vals[3]); } } return 0; }
(3)实验结果
查询方式的按键程序,极大的耗费了资源。 (4)需要注意的地方 1.驱动中我们在read中使用了copy_to_user方法,因为read方法的参数列表适合于copy_to_user方法。 使用时参数列表的buf、size是测试程序传进来的,我们不需要定义,是形参与用户空间的参数对应。 我们定义一个key_vals数组,对其赋值,然后将其值传递给用户空间的buf中。 用户调用 read(fd, key_vals, sizeof(key_vals));驱动程序调用second_drv_read(fd,buf,4),实现内核驱动与应用之间数据传递 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;
copy_to_user(buf, key_vals, sizeof(key_vals));

return sizeof(key_vals);
}
2file_operations结构体中涉及的方法:
3.top命令类似于window下任务管理器可以查看各个进程占用资源的情况 4.查看寄存器的值regval : gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
        regval = *gpfdat;

二、中断方式的按键驱动程序

(1)说明: 使用中断方式,那么肯定有一个中断的初始化注册,就是告诉内核我按下按键的时候会触发一个中断,同时一定有一个中断处理函数来处理中断发生时应该做什么。 linux内核中 如何告诉内核我按下按键了给我触发中断并实现中断处理函数呢。 使用的函数如下: request_irq():open驱动程序时调用
free_irq():卸载驱动程序时解除按键中断
1.注册中断函数原型在Kernel/irq/manage.c中定义:
intrequest_irq(unsigned int irq, irq_handler_t handler,
                         unsigned long irqflags, const char *devname,void *dev_id)
void *:void即“无类型”,void *则为“无类型指针”,可以指向任何数据类型
参数1:中断号
参数2:向系统注册的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id参数将被传递给它 参考内核中如下函数
参数3:触发方式,在下面定义了一些宏
参数4:设置中断名称,通常是设备驱动程序的名称  在cat /proc/interrupts中可以看到此名称。
参数5:设备ID,卸载时用,在中断共享时也会用到,一般设置为这个设备的设备结构体或者NULL

request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
2.注销函数定义在Kernel/irq/manage.c中定义: 
    
void free_irq(unsigned int irq, void *dev_id)
返回值函数运行正常时返回 0 ,否则返回对应错误的负值。
(2)中断方式的按键驱动代 #include #include #include #include #include #include #include #include #include #include #include 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); /* 中断事件标志, 中断服务程序buttons_irq将它置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; 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_RETVAL(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 sencod_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", &sencod_drv_fops); thirddrv_class = class_create(THIS_MODULE, "third_drv"); 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); return 0; } module_init(third_drv_init); module_exit(third_drv_exit); MODULE_LICENSE("GPL"); (3)驱动编译的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 += third_drv.o (4)驱动测试应用程序 #include #include #include #include /* thirddrvtest */ int main(int argc, char **argv) { int fd; unsigned char key_val; fd = open("/dev/buttons", O_RDWR); if (fd < 0) { printf("can't open! "); } while (1) { read(fd, &key_val, 1); printf("key_val = 0x%x ", key_val); } return 0; }(5)运行结果
(6)需要注意的地方
1.wait_event_interruptible(button_waitq, ev_press); 这个函数会判断ev_press这个参数,如果=0,进入休眠,不返回,程序停止在此处。当被唤醒时,从此处继续执行。 button_waitq:static DECLARE_WAIT_QUEUE_HEAD(button_waitq);我的理解是这个button_waitq代表了这个应用程序的进程,被挂载到DECLARE_WAIT_QUEUE_HEAD某个队列中
如果没有这个休眠操作,程序read的时候,会不断返回一个老值,并不能降低CPU资源。 当中断发生,会执行中断处理函数,此时唤醒队列中次应用的进程,继续执行,返回结果。     ev_press = 1;                  /* 表示中断发生了 */
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */
2.不写测试程序如何打开某个设备节点: exec 5会调用open 释放应用程序:
exec 5<&-  会调用release
3查看当前进程:ps(可以查看进程状态 s:休眠)

三、加入poll机制

上一节我们实现了中断的按键驱动程序。CPU占用已经很低,且没有按键的时候会休眠,那么我们为什么还需要poll机制呢。之前的测试程序是这样: while (1)
{
read(fd, &key_val, 1);
printf("key_val = 0x%x ", key_val);
}
在read中ev_press=0;进程被休眠,程序停止在这里,当中断发生后,进程被唤醒,继续执行copy_to_user(buf, &key_val, 1);
ev_press = 0; 
进行返回,这个时候测试程序返回,然后重新进入read函数。也就是说没有poll机制,大部分时间程序都处在read中休眠的那个位置。如果我们不想让程序停在这个位置,而是希望当有按键按下时,我们再去read,因此我们编写poll函数,测试程序调用poll函数根据返回值,来决定是否执行read函数。 (1)驱动程序加入: static unsigned forth_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; } forth_drv_poll是sys_poll中的一部分,在里面被调用。ev_press默认为0,此时驱动中的poll返回默认值0,会发生休眠。 当中断发生,ev_press=1,此时返回值不是0,会调用read函数。
(2)测试代码: #include #include #include #include #include /* 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("can't open! "); } fds[0].fd = fd; fds[0].events = POLLIN; while (1) { ret = poll(fds, 1, 5000); if (ret == 0) { printf("time out "); } else { read(fd, &key_val, 1); printf("key_val = 0x%x ", key_val); } } return 0; }(3)需要注意的地方 1.如果超时时间到了也会返回,然后根据返回值决定是否执行read函数。上一节的read没有按键按下一直不会返回。 2.期待得到POLLIN这个返回值,代表有数据可读。是一个宏,根据自己可能出现的现象来定义。 3.驱动中的forth_drv_poll是sys_poll中的一部分,应用程序调用poll,并非简单的调用驱动中的poll 4.forth_drv_poll的返回值,决定了sys_poll的返回值。中断发生,返回值为POLLIN,即有数据可读,根据返回值执行read函数。