本篇是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
CSDN博客:中年润 网址:https://blog.csdn.net/chichi123137
邮箱:834759803@qq.com
QQ群:766756075
更多原创文章请关注微信公众号。另外,中年润还代理销售韦东山老师的视频教程,欢迎读者点击下面二维码购买。在中年润这里购买了韦东山老师的视频教程,除了能得到韦东山官方的技术支持外,还能获得中年润细致入微的技术和非技术的支持和帮助。欢迎选购哦。
公众号二维码如下图
入群小助手二维码如下图
中年润代理销售的韦东山视频购买地址如下图
如果略有所获,欢迎赞赏,您的支持对中年润无比重要