linux驱动篇-button-int-poll

    xiaoxiao2025-09-10  56

    本篇是linux下按键设备驱动,采用的中断法和poll机制,也是属于字符设备类的驱动,一起来动手吧。下面的话,老朋友可以跳过了直接从《需求描述》章节看起,新朋友可以试着看看。

    前言

    前言主要介绍了中年润写文章的目的,新朋友可以参考中年润其它文章来了解中年润的初衷。另外,之后的文章会大量借助流程图来表达中心思想,比较细节的步骤请大家参考中年润以前写过的文章。

    最近中美贸易战正酣,中年润也想尽自己的绵薄之力。同时希望能有更多的嵌入式工作者踏实下来,努力钻研相关技术。为中国的强大出一份力。

     

    总体目标

    本篇文章的目标是介绍如何从自顶向下从零编写linux下的按键驱动。着力从总体思路,需求端,分析端,实现端,详尽描述一个完整需求的开发流程。

    本示例采用arm920架构,天祥电子生产的tx2440a开发板,核心为三星的s3c2440。Linux版本为2.6.31,是已经移植好的版本。编译器为arm920t-eabi-4.1.2.tar。

     

    总体思路

    总体思路是严格遵循需求的开发流程来,不遗漏任何思考环节。读者在阅读时请先跟中年润的思路走一遍,然后再抛弃中年润的思路,按照自己的思路走一遍,如果遇到困难请先自己思考,实在不会再来参考中年润的思路和实现。

     

    中年润在写代码的的总体思路如下:

    需求描述—能够详细完整的描述一个需求。

    需求分析—根据需求描述,提取可供实现的功能,需要有定量或者定性的指标。

    需求分解—根据需求分析,考虑要实现需求所需要做的工作。

    编写思路—根据需求分解从总体上描述应该如何编写代码。

    详细步骤—根据编写思路,落实具体步骤。

    编写框架—根据编写思路,实现总体框架。

    具体代码—根据编写框架,编写每一个函数里所需要实现的小功能,主要是实现驱动代码,测试代码。

    Makefile—用来编译驱动代码。

    目录结构—用来说明当完成编码后的结果。

    测试步骤—说明如何对驱动进行测试,主要是加载驱动模块,执行测试代码。

    执行结果—观察执行结果是否符合预期。

    结果总结—回顾本节的思路,知识点,api,结构体。

    实战目标—说明如何根据本文档训练。

     

    需求描述

    编写按键驱动,要求使用中断法和poll机制。

    需求分析

    一张图概括用户进程和中断的交互流程。

    用户的工作流程如下:

    1 用户进程调用open系统调用打开/dev/mybutton设备节点,获取fd

    2 用户进程拿到fd之后,调用poll系统调用检查fd的状态

    2.1 如果超时则返回0

    2.2 如果出错返回-1

    2.3 如果有数据返回一个大于0的值

    2.4 有数据则调用read系统调用读取fd中的值

    3 如果按下或松开按键,在中断中保存当前的数据,并标记已有数据

    4 用户进程直接调用read调用读取数据

     

    我们需要做什么呢?针对用户的操作,驱动需要提供节点和以下几个函数。

    需求分解

    总体思路:

    中断中保存数据,read能够返回数据,poll函数能够根据当前数据状态返回是否可读或者可写。

     

    根据用户的每一个动作可以分析出驱动所应该做的动作

    1 用户进程调用open系统调用打开/dev/mybutton设备节点,获取fd

    1.1 需要提供一个设备节点/dv/mybutton,需要提供一个操作设备节点的open函数

    2 用户进程拿到fd之后,调用poll系统调用检查fd的状态

    2.1 如果超时则返回0,由poll系统调用+驱动保证

             驱动poll需要返回0,表示当前不可读

    2.2 如果出错返回-1,由poll系统调用保证

    2.3 如果有数据返回一个大于0的值,由poll系统调用+驱动保证      

             2.3.1 驱动poll需要返回当前状态可以读或者写

    2.4 有数据则调用read系统调用读取fd中的值

             2.4.1 需要提供操作设备节点的read函数,用来将读取的数据返回

    2.5 驱动poll关键点

             驱动poll需要将当前进程加入等待队列头,给系统调用poll使用

    3 如果按下或松开按键,在中断中保存当前的数据,并标记已有数据

    3.1 需要一个中断处理函数

    3.2 需要能够检测当前按下的是哪个按键

    3.3 需要在中断中保存数据,并标记当前数据状态为有

    4 用户进程直接调用read调用读取数据

    4.1 需要提供一个read函数

    4.2 read函数能够直接操作被保存的数据并返回给用户

     

    最终需求就分解为

    1注册字符设备,以及代表字符设备的节点

    2编写open函数

    3编写read函数

    4编写中断处理函数

    5编写poll函数

    用一张图表示我们需要做的工作。

    编写思路

    1 入口函数

    1.1 注册字符设备

    1.2 创建一个类

    1.3 在这个类下创建一个设备

    2 出口函数

    2.1 卸载字符设备

    2.2 卸载class下的设备

    2.3 销毁这个类

    3 构造file_operations结构体

    3.1 实现button_open函数

             根据4个引脚的中断号,注册4个外部中断(用来感知不同的引脚),并实现中断处理函数

    3.2 中断处理函数

             1 获取当前引脚的电平值

             2 根据电平值判断当前是按下还是松开

                      松开为高电平,返回0x8x

                      按下为低电平,返回0x0x

             3 标记中断已经触发

    3.2 实现button_read函数

             将在中断中保存的数据返回给用户,并将标志位清零,表示已无数据

    3.3 实现button_release函数  

             释放注册的4个中断

    3.4 实现poll函数

             如果无数据,直接返回0

             如果有数据,则返回可读的标志位

     

    详细步骤

     

    编写框架

    驱动代码

    因代码比较简单,我们直接通过编写思路来编写驱动代码。

    /* 本代码根据中年润文章<Button-int-poll>编写思路编写 */ #include <linux/module.h> #include <linux/ioport.h> #include <linux/io.h> #include <linux/platform_device.h> #include <linux/init.h> #include <linux/serial_core.h> #include <linux/serial.h> #include <linux/irq.h> #include <asm/irq.h> #include <asm/io.h> #include <asm/uaccess.h> #include <mach/hardware.h> #include <mach/regs-gpio.h> #include <mach/gpio-fns.h> #include <plat/regs-serial.h> #include <linux/poll.h> /* 需求分析 --- 需求分解 --- 编写框架 --- 编写代码 */ /* 编写代码 */ volatile unsigned long * gpfconf; volatile unsigned long * gpfdat; static struct class *my_button_cls; static struct device * my_button_dev; DECLARE_WAIT_QUEUE_HEAD(button_waitq); //引脚描述符 struct pin_desc{ int pin; int key_val; }; //引脚描述符,按引脚的不同定义不同的值,在中断中进行处理 struct pin_desc pin_desc[4] = { {S3C2410_GPF4_EINT4,0x1}, {S3C2410_GPF5_EINT5,0x2}, {S3C2410_GPF6_EINT6,0x3}, {S3C2410_GPF7_EINT7,0x4}, }; //标志是否进入中断 static int do_press_loos; /* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */ /* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */ /* 主要是为了区分是按下还是松开 */ static unsigned char key_val = 0; /* 3.2 中断处理函数 */ irqreturn_t button_irq(int irq, void * devid) { int pin_val; struct pin_desc * pin_readed; pin_readed = (struct pin_desc *)devid; /* 1 获取当前引脚的电平值 */ pin_val = s3c2410_gpio_getpin(pin_readed->pin); /* 2 根据电平值判断当前是按下还是松开 */ if (pin_val) /* 松开为高电平,返回0x8x */ { key_val = 0x80 | pin_readed->key_val; } else /* 按下为低电平,返回0x0x */ { //把当前按键的键值给一个全局静态变量,在read函数里给用户 key_val = pin_readed->key_val; } /* 3 标记中断已经触发 */ do_press_loos = 1; return IRQ_RETVAL(IRQ_HANDLED); } /* 3.1 实现button_open函数 */ static int button_open (struct inode * inode, struct file * filep) { /* 根据4个引脚的中断号,注册4个外部中断(用来感知不同的引脚),并实现中断处理函数 */ //request_irq里已经帮忙将GPF4-GPF7设置成中断引脚了,另外这里只检测下降沿 int ret; ret = request_irq(IRQ_EINT4,button_irq,IRQ_TYPE_EDGE_FALLING,"S1", (void *)&pin_desc[0]); if (ret) goto errout; ret = request_irq(IRQ_EINT5,button_irq,IRQ_TYPE_EDGE_FALLING,"S2", (void *)&pin_desc[1]); if (ret) goto errout; ret = request_irq(IRQ_EINT6,button_irq,IRQ_TYPE_EDGE_FALLING,"S3", (void *)&pin_desc[2]); if (ret) goto errout; ret = request_irq(IRQ_EINT7,button_irq,IRQ_TYPE_EDGE_FALLING,"S4", (void *)&pin_desc[3]); if (ret) goto errout; return 0; errout: return ret; } /* 3.2 实现button_read函数 */ static ssize_t button_read (struct file * filep, char __user * buff, size_t size, loff_t * pos) { /* 将在中断中保存的数据返回给用户,并将标志位清零,表示已无数据 */ int ret; if (size != 1) return -EINVAL; if (key_val == 0) return -EINVAL; //拷贝数据到用户空间 ret = copy_to_user(buff, &key_val, sizeof(key_val)); if (ret) { return -EFAULT; } //读取数据完毕后需要将标志位清零,表示暂时无数据可读 do_press_loos = 0; key_val = 0; return 1; } static ssize_t button_write (struct file * filep, const char __user * buff, size_t size, loff_t * pos) { return 0; } /* 3.3 实现button_release函数 */ static int button_release (struct inode * inode, struct file * filep) { /* 释放注册的4个中断 */ free_irq(IRQ_EINT4, &pin_desc[0]); free_irq(IRQ_EINT5, &pin_desc[1]); free_irq(IRQ_EINT6, &pin_desc[2]); free_irq(IRQ_EINT7, &pin_desc[3]); return 0; } /* 3.4 实现poll函数 */ unsigned int button_poll (struct file * filep, struct poll_table_struct * wait) { /* 如果无数据,直接返回0 */ unsigned int mask = 0; //将当前进程挂入等待队列头 poll_wait(filep,&button_waitq,wait); /* 如果有数据,则返回可读的标志位 */ if (do_press_loos) { mask |= POLLIN | POLLRDNORM; } return mask; } /* 3 构造file_operations结构体 */ static struct file_operations button_fops = { .owner = THIS_MODULE, .open = button_open, .read = button_read, .write = button_write, .release = button_release, .poll = button_poll, }; static int major; /* 1 入口函数 */ static int my_button_init(void) { int ret = 0; /* 1.1 注册字符设备 */ major = register_chrdev(0,"button-poll",&button_fops); /* 1.2 创建一个类 */ my_button_cls = class_create(THIS_MODULE,"button-poll"); /* 1.3 在这个类下创建一个设备 */ my_button_dev = device_create(my_button_cls,NULL,MKDEV(major,0),NULL,"mybutton"); return ret; } /* 2 出口函数 */ static void my_button_exit(void) { /*2.1 卸载字符设备*/ unregister_chrdev(major,"button-poll"); /* 2.2 卸载class下的设备 */ device_unregister(my_button_dev); /* 2.3 销毁这个类 */ class_destroy(my_button_cls); return; } /* 修饰 */ module_init(my_button_init); module_exit(my_button_exit); MODULE_LICENSE("GPL");

    测试代码

    #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,ret; unsigned char key_val; struct pollfd fds[1]; fd = open("/dev/mybutton", O_RDWR); if (fd < 0) { printf("can't open!\n"); } fds[0].fd = fd; fds[0].events = POLLIN; while (1) { ret = poll(fds, 1, 100); if (ret == 0) { printf("time out\n"); } else if (ret > 0) { read(fd, &key_val, 1); printf("key_val = 0x%x\n", key_val); } else { printf("somthing error,errno is %d\n",ret); } } return 0; }

    Makefile

    KERN_DIR = /home/linux/tools/linux-2.6.31 all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order obj-m += button_drv_int_poll.o

    目录结构

    linux@dell_chi:~/nfs_root/5-button-poll$ ls button (编译好的测试可执行文件) button_drv_int_poll.c (驱动代码) button_test_poll.c (测试代码) Makefile

    测试步骤

    0 在linux下的makefile +180行处配置好arch为arm,cross_compile为arm-linux-(或者arm-angstrom-linux-gnueabi-)

    1 在menuconfig中配置好内核源码的目标系统为s3c2440

    2 在pc上将驱动程序编译生成.ko,命令:make

    3 在pc上将测试程序编译生成elf可执行文件,生成的button就是我们所要使用的命令。

    编译:arm-angstrom-linux-gnueabi-gcc button_test_poll.c -o button

    4 挂载nfs,这样就可以在开发板上看到pc端的.ko文件和测试文件

    mount -t nfs -o nolock,vers=2 192.168.0.105:/home/linux/nfs_root  /mnt/nfs

    5 insmod button_drv.ko,加载按键的驱动模块

    6执行命令./button,依次按下四个按键,观察串口的打印信息。

     

    执行结果

    [root@TX2440A 5-button-poll]# insmod button_drv_int_poll.ko

    [root@TX2440A 5-button-poll]# ./button

    time out

    time out

    key_val = 0x4

    time out

    key_val = 0x4

    time out

    time out

    key_val = 0x3

    time out

    key_val = 0x3

    key_val = 0x2

    time out

    key_val = 0x2

    time out

    time out

    key_val = 0x1

    key_val = 0x1

    结果总结

    在本篇文章中,中年润跟读者分享了按键字符设备驱动的编写思路和方法,其中贯穿始终的有几个函数和关键数据结构,它们分别是:

    struct file_operations

    struct class

    struct class_device

     

    register_chrdev

    class_create

    device_create

    unregister_chrdev

    device_destroy

    class_destroy

    ioremap

    iounmap

    DECLARE_WAIT_QUEUE_HEAD

    request_irq / free_irq

    poll_wait

    请读者尽力去了解这些函数的作用,入参,返回值。

    问题总结

    Poll机制不用在中断处理函数里加wake_up_interruptible,也不用在read函数里加wait_event_interruptible。因为使用poll机制的进程不是靠我们加在中断函数里的唤醒函数里唤醒的。有想要了解详细机制的小伙伴可以参考我的另一篇文章《api分析篇-poll》。

    实战目标

    1请读者根据《需求描述》章节,独立编写需求分析和需求分解。

    2请读者根据需求分析和需求分解,独立编写编写思路和详细步骤。

    3请读者根据编写思路,独立写出编写框架。

    4请读者根据详细步骤,独立编写驱动代码和测试代码。

    5请读者根据《Makefile》章节,独立编写Makefile。

    6请读者根据《测试步骤》章节,独立进行测试。

    7请读者抛开上述练习,自顶向下从零开始再编写一遍驱动代码,测试代码,makefile

    8如果无法独立写出7,请重复练习1-6,直到能独立写出7。

     

    参考资料

    《linux设备驱动开发祥解》

    《TX2440开发手册及代码》

    《韦东山嵌入式教程》

    《鱼树驱动笔记》

    《s3c2440a》芯片手册英文版和中文版

     

    致谢

    感谢在嵌入式领域深耕多年的前辈,感谢中年润的家人,感谢读者。没有前辈们的开拓,我辈也不能站在巨人的肩膀上看世界;没有家人的鼎力支持,我们也不能集中精力完成自己的工作;没有读者的关注和支持,我们也没有充足的动力来编写和完善文章。看完中年润的文章,希望读者学到的不仅仅是如何编写代码,更进一步能够学到一种思路和一种方法。

     

    为了省去驱动开发者搜集各种资料来写驱动,中年润后续有计划按照本模板编写linux下的常见驱动,敬请读者关注。

    联系方式

    微信群:自顶向下学嵌入式(可先加微信号:runzhiqingqing, 通过后会邀请入群。)

    微信订阅号:自顶向下学嵌入式  公众号:EmbeddedAIOT

    博客:中年润  网址:https://blog.csdn.net/chichi123137

    邮箱:834759803@qq.com

    QQ群:766756075

    更多原创文章请关注微信公众号。另外,中年润还代理销售韦东山老师的视频教程,欢迎读者点击下面二维码购买。在中年润这里购买了韦东山老师的视频教程,除了能得到韦东山官方的技术支持外,还能获得中年润细致入微的技术和非技术的支持和帮助。欢迎选购哦。

    公众号二维码如下图

    入群小助手二维码如下图

    中年润代理销售的韦东山视频购买地址如下图

    如果略有所获,欢迎赞赏,您的支持对中年润无比重要

    最新回复(0)