Button-查询法
本篇是linux下按键设备驱动,采用查询法,也是属于字符设备类的驱动,一起来动手吧。下面的话,老朋友可以跳过了直接从《需求描述》章节看起,新朋友可以试着看看。
特别说明:本系列教程可以配套《韦东山视频教程二期》,是韦老师教程的有益补充。
Csdn地址如下:
https://blog.csdn.net/chichi123137/article/details/89741831
前言
在嵌入式行业,有很多从业者。我们工作的主旋律是拿开源代码,拿厂家代码,完成产品的功能,提升产品的性能,进而解决各种各样的问题。或者是维护一个模块或方向,一搞就是好几年。
时间长了,中年润发现我们对从零开始编写驱动、应用、算法、系统、协议、文件系统等缺乏经验。没有该有的广度和深度。中年润也是这样,工作了很多年,都是针对某个问题点修修补补或者某个模块的局部删删改改。很少有机会去独自从零开始编写一整套完整的代码。
当然,这种现状对于企业来说是比较正常的,可以降低风险。但是对于员工本身,如果缺乏必要的规划,很容易工作多年却还是停留在单点的层面,而丧失了提升到较高层面的机会。随着时间的增长很容易丧失竞争力。
另外,根据中年润的经验,绝大多数公司对于0-5年经验从业者的定位主要是积极的问题解决者。而对于5-10经验从业者的定位主要是积极的系统规划者和引领者。在这种行业规则下,中年润认为,每个从业者都应该问自己一句,“5年后,我是否具备系统化把控软件的能力呢?”。
当前的这种行业现状,如果我们不做出一点改变,是没有办法突破的。有些东西,仅仅知道是不够的,还需要深思熟虑的思考和必要的训练,简单来说就是要知行合一。
也许有读者会有疑惑?这不就是重复造轮子么?我们确实是在重复造轮子,因为别人会造轮子那是别人的能力,我们自己会造轮子是我们自己的能力。在行业中,有太多的定制化需求是因为轮子本身有原生性缺陷,我们无法直接使用,或者需要对其进行改进,或者需要抽取开源代码的主体思想和框架,根据公司的需要定制自己的各项功能。设想,如果我们具备这种能力,必然会促使我们在行业中脱颖而出,而不是工作很多年一直在底层搬砖。底层搬砖没什么不好,问题是当有更廉价更激情的劳动力涌进来的时候,我们这些老的搬砖民工也就失去了价值。我们不会天天重复造轮子,我们需要通过造几个轮子使得自己具备造轮子的能力,从而更好的适应这个环境,适应这个世界。
针对当前行业现状,中年润经过深思熟虑,想为大家做点实实在在的事情,希望能够帮助大家在巩固基础的同时提升系统化把控软件的能力。当然,中年润的水平也有限,有些观点也只是一家之谈,希望大家独立思考,谨慎采用,如果写的有错误或者不对的地方还请读者们批评斧正,我们一起共同进步。
在这里简单介绍下中年润,中年润现在就职于一家大型国际化公司,工作经验6年,硕士毕业。曾经担任过组内的项目主管,项目经理,也曾经组建过新团队,带领大家冲锋陷阵。在工作中,有做的不错的地方,也有失误的地方,有激情的时刻,也有失落的时刻。现在偏安一隅,专心搞技术,目前个人规划的技术方向是嵌入式和AI基础设施建设,以及嵌入式和AI的融合发展。
最后,说了这么多,中年润希望,在未来的日子里和未知的领域里,你我同行,为我们的美好生活而努力奋斗。
总体目标
本篇文章的目标是介绍如何从自顶向下从零编写linux下的按键字符设备驱动。着力从总体思路,需求端,分析端,实现端,详尽描述一个完整需求的开发流程,是中年润多年经验的提炼,希望读者能够有所收获。最后的实战目标,请读者尽量完成,这样读者才能形成自己的思路。
本示例采用arm920架构,天祥电子生产的tx2440a开发板,核心为三星的s3c2440。Linux版本为2.6.31,是已经移植好的版本。编译器为arm920t-eabi-4.1.2.tar。
总体思路
总体思路是严格遵循需求的开发流程来,不遗漏任何思考环节。读者在阅读时请先跟中年润的思路走一遍,然后再抛弃中年润的思路,按照自己的思路走一遍,如果遇到困难请先自己思考,实在不会再来参考中年润的思路和实现。
中年润在写代码的的总体思路如下:
需求描述—能够详细完整的描述一个需求。
需求分析—根据需求描述,提取可供实现的功能,需要有定量或者定性的指标。(从宏观上确定需要什么功能)。
需求分解—根据需求分析,考虑要实现需求所需要做的工作(根据宏观确定的功能,拆分成小的可单独实现的功能)。
编写思路—根据需求分解从总体上描述应该如何编写代码,(解决怎么在宏观上实现)。
详细步骤—根据编写思路,落实具体步骤,(解决怎么在微观上实现)。
编写框架—根据编写思路,实现总体框架(实现编写思路里主体框架,细节内容留在具体代码里编写)。
具体代码—根据编写框架和详细步骤,编写每一个函数里所需要实现的小功能,主要是实现驱动代码,测试代码。
Makefile—用来编译驱动代码。
目录结构—用来说明当完成编码后的结果。
测试步骤—说明如何对驱动进行测试,主要是加载驱动模块,执行测试代码。
执行结果—观察执行结果是否符合预期。
结果总结—回顾本节的知识点,api,结构体。
实战目标—说明如何根据本文档训练。
请大家尽量按照自顶向下的学习思路来学习和实战,因为我们所有工作的动力都是我们心中的需求。这些步骤仅仅是我们达到目标所要走过的路。目录起到提纲挈领的重要作用,写的时候要实时看下提纲,看有没有偏离自己的方向。
需求描述
使用linux提供的字符设备api接口,编写一个按键字符设备驱动和一个测试代码,能够在输入./button命令后,按下按键时,在串口输出按下了哪个按键。
需求分析
对于按键字符设备驱动来说,测试代码就是我们的用户,因此我们可以通过分析测试代码的逻辑来分析我们的需求。
测试代码中用户的工作流程如下:
1用户调用open函数打开/dev/mybutton设备节点,获取fd
2用户拿到fd之后,调用read函数读取fd中的值
根据用户的工作流程,我们可以梳理出驱动所要做的工作:
1用户调用open系统调用打开/dev/mybutton设备节点,获取fd
1.1驱动需要提供一个设备节点/dv/mybutton
1.2驱动需要提供一个操作设备节点的open函数
2用户拿到fd之后,调用read系统调用读取fd中的值
2.1 驱动需要提供操作设备节点的read函数,用来将读取的按键数据返回
需求分解
根据《需求分析》的结果,将宏观确定的功能拆分成小的可单独实现的功能,我们需要做以下几件工作。
针对测试代码:
1打开/dev/mybutton设备,获得fd
2读fd,并显示按下了哪个按键。
针对驱动代码:
1创建设备节点/dev/mybutton
2提供打开设备的open函数
3提供读设备的read函数
另外,我们还需要指定我们要操作的gpio管脚,因此引出4和5
4需要将按键所对应的gpio管脚的物理地址映射到某个地方,方便在驱动代码中控制
5需要在读函数中根据按键gpio引脚的高低电平,判断当前是哪个按键按下,并将数据丢给用户。
编写思路
编写思路主要用来搭建代码框架,解决在宏观上如何用代码实现驱动的功能。
确定目标:
1我们需要一个设备节点,存在于/dev/目录下(因此需要在入口函数创建设备)
2我们需要一个open函数(用来配置gpio为输入模式)
3我们需要一个read函数(用来返回哪个按键按下)
4我们需要操作按键所对应的gpio(因此需要映射gpio管脚的地址)
5我们需要判断按键所对应的gpio管脚的高低电平
确定基本思路:
0搭建基础框架
0.1编写代码框架,头文件
0.2编写空的出口函数
0.3编写空的入口函数
0.4修饰出口函数,修饰入口函数,声明LICENSE
在入口函数中所做的工作如下
1入口函数
1.1注册一个字符设备,名字为s3c_button
1.1.1定义file_operations结构体
1.1.2编写空的打开函数
1.1.2编写空的读函数
1.2创建一个类,名字为button,(决定了class下目录的名字)
1.3创建一个设备节点,名字为mybutton(决定了/dev下目录的名字)
1.4映射gpio的物理地址为虚拟地址,一个是控制寄存器地址,一个是数据寄存器地址
在出口函数中所作的工作如下
2出口函数
2.1卸载字符设备
2.2删除设备节点
2.3销毁这个类
2.4取消地址映射
详细步骤
详细步骤主要用来在代码框架里填充必要的细节代码,解决在微观上如何用代码实现驱动各个小功能。
0搭建基础框架
0.1编写代码框架,头文件
0.2编写空的出口函数
0.3编写空的入口函数
0.4修饰出口函数,修饰入口函数,声明LICENSE
在入口函数中所做的工作如下
1入口函数
1.1注册一个字符设备,名字为s3c_button
1.1.1定义file_operations结构体
1.1.2编写空的打开,读操作函数
1.2创建一个类,名字为button,(决定了class下目录的名字)
1.3创建一个设备节点,名字为mybutton(决定了/dev下目录的名字)
1.4映射gpio的物理地址为虚拟地址,一个是控制寄存器地址,一个是数据寄存器地址
在出口函数中所作的工作如下
2出口函数
2.1卸载字符设备
2.2删除设备节点
2.3销毁这个类
2.4取消地址映射
编写操作函数
3编写file_operations里的操作函数
3.1编写打开函数
3.1.1配置GPF4,5,6,7为输入引脚(用来感知按键的gpio引脚)(根据s3c2440 芯片手册配置)
3.2编写读函数
3.2.1如果所要读的字节数不对,返回错误
3.2.2读按键gpio管脚对应的gpio数据寄存器gpfdat
3.2.3根据所读的数据来判断按下了哪个按键
3.2.4将数据丢给用户
3.2.5返回读取到的数据的个数
编写框架
根据编写思路,实现总体框架(实现编写思路里主体框架,细节内容留在具体代码里编写)。
/* 本文件名字为button_drv_skel.c*/
/* 本文件是依照button-查询法驱动<编写思路>章节编写,本文件
* 的目的是编写代码框架,不做具体细节的编写
*/
/* 本头文件是linux2.6.31内核所提供的,其他版本按需调整 */
/* 0.1编写代码框架,头文件 */
#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 <asm/irq.h>
#include <mach/hardware.h>
#include <plat/regs-serial.h>
#include <mach/regs-gpio.h>
#include <asm/uaccess.h>
int major;
static struct class *buttondrv_class;
static struct device *buttondrv_class_dev;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
/* 1.1.2编写空的打开函数 */
static int button_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
/* 1.1.2编写空的读函数 */
ssize_t button_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
return 0;
}
/* 1.1.1定义file_operations结构体 */
static struct file_operations sencod_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = button_drv_open,
.read = button_drv_read,
};
/* 0.2编写空的入口函数 */
static int button_drv_init(void)
{
/* 1.1注册一个字符设备,名字为s3c_button */
major = register_chrdev(0, "button_drv", &sencod_drv_fops);
/* 1.2创建一个类,名字为button,(决定了class下目录的名字) */
buttondrv_class = class_create(THIS_MODULE, "button_drv");
/* 1.3创建一个设备节点,名字为myled(决定了/dev下目录的名字) */
buttondrv_class_dev = device_create(buttondrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
/* 1.4映射gpio的物理地址为虚拟地址,一个是控制寄存器地址,一个是数据寄存器地址 */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
return 0;
}
/* 0.3编写空的出口函数 */
static void button_drv_exit(void)
{
/* 2.1 卸载字符设备 */
unregister_chrdev(major, "button_drv");
/* 2.2 删除设备节点 */
device_unregister(buttondrv_class_dev);
/* 2.3 销毁这个类 */
class_destroy(buttondrv_class);
/* 2.4 取消地址映射 */
iounmap(gpfcon);
}
/* 0.4修饰出口函数,修饰入口函数,声明LICENSE */
module_init(button_drv_init);
module_exit(button_drv_exit);
MODULE_LICENSE("GPL");
驱动代码
根据《编写框架》《详细步骤》章节,编写每一个函数里所需要实现的小功能。
/* 本文件名字为button_drv.c*/
/* 本文件是依照button-查询法驱动<编写思路>章节编写,本文件
* 的目的是编写代码框架,不做具体细节的编写
*/
/* 本头文件是linux2.6.31内核所提供的,其他版本按需调整 */
/* 0.1编写代码框架,头文件 */
#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 <asm/irq.h>
#include <mach/hardware.h>
#include <plat/regs-serial.h>
#include <mach/regs-gpio.h>
#include <asm/uaccess.h>
int major;
static struct class *buttondrv_class;
static struct device *buttondrv_class_dev;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
/* 1.1.2编写空的打开函数 */
static int button_drv_open(struct inode *inode, struct file *file)
{
/* 3.1.1配置GPF4,5,6,7为输入引脚 */
*gpfcon &= ~((0x3<<(2*4)) | (0x3<<(2*5)) | (0x3<<(2*6)) | (0x3<<(2*7)));
return 0;
}
/* 1.1.2编写空的读函数 */
ssize_t button_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
/* 返回4个引脚的电平 */
unsigned char key_vals[4];
int regval;
unsigned long ret;
/* 3.2.1如果所要读的字节数不对,返回错误 */
if (size != sizeof(key_vals))
return -EINVAL;
/* 读GPF4,5,6,7 */
/* 3.2.2读按键gpio管脚对应的gpio数据寄存器gpfdat */
regval = *gpfdat;
/* 3.2.3根据所读的数据来判断按下了哪个按键 */
key_vals[0] = (regval & (1<<4)) ? 1 : 0;
key_vals[1] = (regval & (1<<5)) ? 1 : 0;
key_vals[2] = (regval & (1<<6)) ? 1 : 0;
key_vals[3] = (regval & (1<<7)) ? 1 : 0;
/* 3.2.4将数据丢给用户 */
ret = copy_to_user(buf, key_vals, sizeof(key_vals));
if ( ret < 0) {
printk("copy_to_user failed\n");
return -EFAULT;
}
/* 3.2.5返回读取到的数据的个数 */
return sizeof(key_vals);
}
/* 1.1.1定义file_operations结构体 */
static struct file_operations sencod_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = button_drv_open,
.read = button_drv_read,
};
/* 0.2编写空的入口函数 */
static int button_drv_init(void)
{
/* 1.1注册一个字符设备,名字为s3c_button */
major = register_chrdev(0, "s3c_button", &sencod_drv_fops);
/* 1.2创建一个类,名字为button,(决定了class下目录的名字) */
buttondrv_class = class_create(THIS_MODULE, "button");
/* 1.3创建一个设备节点,名字为mybutton(决定了/dev下目录的名字) */
buttondrv_class_dev = device_create(buttondrv_class, NULL, MKDEV(major, 0), NULL, "mybutton");
/* 1.4映射gpio的物理地址为虚拟地址,一个是控制寄存器地址,一个是数据寄存器地址 */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
return 0;
}
/* 0.3编写空的出口函数 */
static void button_drv_exit(void)
{
/* 2.1 卸载字符设备 */
unregister_chrdev(major, "button_drv");
/* 2.2 删除设备节点 */
device_unregister(buttondrv_class_dev);
/* 2.3 销毁这个类 */
class_destroy(buttondrv_class);
/* 2.4 取消地址映射 */
iounmap(gpfcon);
}
/* 0.4修饰出口函数,修饰入口函数,声明LICENSE */
module_init(button_drv_init);
module_exit(button_drv_exit);
MODULE_LICENSE("GPL");
测试代码
测试代码编写思路如下:
1打开/dev/mybutton文件
2根据读到的值不同,打印不同的值
测试代码如下:
/* 本文件是button_test.c,是根据button-查询法驱动的
* <测试代码>章节编写,主要任务是用来测试
* button 驱动
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
int fd = 0;
int cnt = 0;
int ret = 0;
unsigned char key_vals[4] = {0};
fd = open("/dev/mybutton", O_RDWR);
if (fd < 0) {
printf("can't open /dev/mybutton!\n");
return -1;
}
while (1) {
ret= read(fd, key_vals, sizeof(key_vals));
if (ret < 0) {
printf("some thing wrong happend errno is %d\n",ret);
} else {
if (!key_vals[0] || !key_vals[1] || !key_vals[2] ||
!key_vals[3])
{
printf("%04d key pressed: %d %d %d %d\n",
cnt++, key_vals[0], key_vals[1], key_vals[2],
key_vals[3]);
}
}
}
return 0;
}
Makefile
KERN_DIR = /home/linux/tools/linux-2.6.31_TX2440A
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.o
目录结构
代码编写完成的目录结构如下所示。直接执行make即可生成.ko文件。
.
├── button_drv.c(驱动代码)
├── button_drv_skel.c(驱动框架代码)
├── button_test.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.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,依次按下四个按键,观察串口的打印信息。
执行结果
执行完./button后,依次按下四个按键,串口的日志如下,按下一个按键一次,会出现很多次打印。
按下第一个按键会打印0111,按下第二个会打印1011,按下第三个会打印1101,按下第四个会打印1110。
0000 key pressed: 0 1 1 1
0001 key pressed: 0 1 1 1
0002 key pressed: 0 1 1 1
……
0155 key pressed: 1 0 1 1
0156 key pressed: 1 0 1 1
0157 key pressed: 1 0 1 1
……
0310 key pressed: 1 1 0 1
0311 key pressed: 1 1 0 1
0312 key pressed: 1 1 0 1
……
0465 key pressed: 1 1 1 0
0466 key pressed: 1 1 1 0
0467 key pressed: 1 1 1 0
结果总结
在本篇文章中,中年润跟读者分享了led字符设备驱动的编写思路和方法,其中贯穿始终的有几个函数和关键数据结构,它们分别是:
struct file_operations
struct class
struct class_device
register_chrdev
class_create
device_create
unregister_chrdev
device_destroy
class_destroy
ioremap
iounmap
请读者尽力去了解这些函数的作用,入参,返回值。
问题汇总
暂无
实战目标
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
更多原创文章请关注微信公众号。另外,中年润还代理销售韦东山老师的视频教程,欢迎读者咨询。在中年润这里购买了韦东山老师的视频教程,除了能得到韦东山官方的技术支持外,还能获得中年润细致入微的技术和非技术的支持和帮助。欢迎选购哦。
学习交流群
入群小助手