1. 字符设备驱动简介
字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。 比如常见的点灯、按键、IIC、SPI、LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux驱动基本原理:Linux中一切皆为文件,驱动加载成功后会在/dev目录下生成一个相应的文件,应用程序通过对这个名为/dev/xxx的文件进行相应的操作即可实现对硬件的操作。
(资料图)
比如LED驱动,会有/dev/led驱动文件,应用程序使用open函数来打开该文件; 若要点亮或关闭led,就使用write函数写入开关值; 若要获取led灯的状态,就用read函数从驱动文件中读取相应的状态; 使用完成后使用close函数关闭该驱动文件。
Linux软件从上到下可分为4层结构,如下图左示。 以控制LED为例,具体过程如下图右示:
每个系统调用,在驱动中都有与之对应的驱动函数,内核include/linux/fs.h文件中有个file_operations结构体,就是Linux内核驱动操作函数集合:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t*); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ...... ......};
Linux驱动运行方式有以下两种:
将驱动编译进内核中, 当Linux内核启动时就会自动运行驱动程序将驱动编译成模块, 在内核启动后使用insmod命令加载驱动模块在驱动开发阶段一般都将其编译为模块,不需要编译整个Linux代码,方便调试驱动程序。 当驱动开发完成后,根据实际需要,可以选择是否将驱动编译进Linux内核中。
2. Linux设备号
2.1 设备号的组成
Linux中每个设备都有一个设备号,由主设备号和次设备号两部分组成:
主设备号表示某一个具体的驱动次设备号表示使用这个驱动的各个设备Linux 提供了名为dev_t的数据类型表示设备号,其本质是32位的unsigned int数据类型,其中高12位为主设备号,低20位为次设备号,因此Linux中主设备号范围为0~4095
在文件include/linux/kdev_t.h中提供了几个关于设备号操作的宏定义:
#define MINORBITS 20#define MINORMASK ((1U << MINORBITS) - 1)#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MINORBITS:表示次设备号位数,一共20位MINORMASK:表示次设备号掩码MAJOR:用于从dev_t中获取主设备号,将dev_t右移20位即可MINOR:用于从dev_t中获取次设备号,取dev_t的低20位的值即可MKDEV:用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号2.2 主设备号的分配
主设备号的分配包括静态分配和动态分配。 静态分配需要手动指定设备号,并且要注意不能与已有的重复,一些常用的设备号已经被Linux内核开发者给分配掉了,可使用cat /proc/devices命令查看当前系统中所有已经使用了的设备号。
动态分配是在注册字符设备之前先申请一个设备号,系统会自动分配一个没有被使用的设备号, 这样就避免了冲突。 在卸载驱动的时候释放掉这个设备号即可。
设备号的申请函数
//设备号申请函数int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)// dev:保存申请到的设备号// baseminor:次设备号起始地址// count:要申请的设备号数量// name:设备名字
设备号的释放函数
//设备号释放函数void unregister_chrdev_region(dev_t from, unsigned count)// from:要释放的设备号// count:表示从 from 开始,要释放的设备号数量
3. 字符设备驱动开发模板
3.1 加载与卸载
在编写驱动的时候需要注册模块加载和卸载这两种函数:
module_init(xxx_init); //注册模块加载函数module_exit(xxx_exit); //注册模块卸载函数
module_init():向内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用insmod命令加载驱动时,xxx_init函数就会被调用
module_exit():向内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用rmmod命令卸载驱动时,xxx_exit函数就会被调用
字符设备驱动模块加载和卸载模板如下所示:
/* 驱动入口函数 */staticint __init xxx_init(void){/*入口函数具体内容*/ return0;}/* 驱动出口函数 */staticvoid __exit xxx_exit(void){/*出口函数具体内容*/}/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit);
驱动编译完成以后扩展名为.ko,有两种命令可以加载驱动模块:
insmod:最简单的模块加载命令,但不能解决模块的依赖关系modprobe:会分析模块的依赖关系,将所有的依赖模块都加载到内核中卸载驱动也有两种命令:
rmmod:最简单的模块卸载命令modprobe -r:除了卸载指定的驱动,还卸载其所依赖的其他模块,若依赖模块还在被其它模块使用,就不能使用该命令来卸载驱动模块3.2 注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,卸载驱动模块时也要注销掉字符设备。 字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)//major:主设备号//name:设备名字,指向一串字符串//fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量static inline void unregister_chrdev(unsigned int major, const char *name)//major:要注销的设备对应的主设备号//name:要注销的设备对应的设备名
一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行
//定义了一个file_operations结构体变量,就是设备的操作函数集合static struct file_operations test_fops;/* 驱动入口函数 */static int __init xxx_init(void){ /* 入口函数具体内容 */ int retvalue = 0; /* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符设备注册失败,自行处理 */ } return 0;}/* 驱动出口函数 */static void __exit xxx_exit(void){ /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest");}/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit);
3.3 实现设备的具体操作函数
file_operations结构体就是设备的具体操作函数。 假设对chrtest这个设备有如下两个要求:
能够实现打开和关闭操作:需要实现open和release这两个函数能够实现进行读写操作:需要实现read和write这两个函数实现file_operations中的这四个函数,完成后的内容框架如下所示:
/* 打开设备 */static int chrtest_open(struct inode *inode, struct file *filp){ /* 用户实现具体功能 */ return 0;}/* 从设备读取 */static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){ /* 用户实现具体功能 */ return 0;}/* 向设备写数据 */static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){ /* 用户实现具体功能 */ return 0;}/* 关闭/释放设备 */static int chrtest_release(struct inode *inode, struct file *filp){ /* 用户实现具体功能 */ return 0;}
然后是驱动的入口(init)和出口(exit) 函数:
//定义了一个file_operations结构体变量test_fops,就是设备的操作函数集合static struct file_operations test_fops = { .owner = THIS_MODULE, .open = chrtest_open, .read = chrtest_read, .write = chrtest_write, .release = chrtest_release,}/* 驱动入口函数 */static int __init xxx_init(void){ /* 入口函数具体内容 */ int retvalue = 0; /* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符设备注册失败,自行处理 */ } return 0;}/* 驱动出口函数 */static void __exit xxx_exit(void){ /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest");}/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(xxx_init);module_exit(xxx_exit);
3.4 添加LICENSE和作者信息
LICENSE是必须添加的,否则编译时会报错,作者信息可加可不加
MODULE_LICENSE() //添加模块 LICENSE 信息MODULE_AUTHOR() //添加模块作者信息
综上所述,字符设备驱动开发流程如下图所示:
4. 字符设备驱动开发实验
下面以正点原子的IMX6ULL开发板为平台,完整的编写一个虚拟字符设备驱动模块。 chrdevbase不是实际存在的一个设备,只是为了学习字符设备的开发的流程
4.1 驱动程序编写
宏定义及变量定义
#include #include #include #include #include #include #define CHRDEVBASE_MAJOR 200 /* 主设备号 */#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */static char readbuf[100]; /* 读缓冲区 */static char writebuf[100]; /* 写缓冲区 */static char kerneldata[] = {"kernel data!"};
打开、关闭、读取、写入函数实现
staticintchrdevbase_open(structinode*inode,structfile*filp){ printk("chrdevbase open!\\r\\n"); return0;}staticssize_tchrdevbase_read(structfile*filp,char __user *buf,size_t cnt,loff_t*offt){ int retvalue =0; /* 向用户空间发送数据 */ memcpy(readbuf, kerneldata,sizeof(kerneldata)); retvalue =copy_to_user(buf, readbuf, cnt); if(retvalue ==0){ printk("kernel senddata ok!\\r\\n"); }else{ printk("kernel senddata failed!\\r\\n"); } printk("chrdevbase read!\\r\\n"); return0;}staticssize_tchrdevbase_write(structfile*filp,constchar __user *buf,size_t cnt,loff_t*offt){ int retvalue =0; /* 接收用户空间传递给内核的数据并且打印出来 */ retvalue =copy_from_user(writebuf, buf, cnt); if(retvalue ==0){ printk("kernel recevdata:%s\\r\\n", writebuf); }else{ printk("kernel recevdata failed!\\r\\n"); } printk("chrdevbase write!\\r\\n"); return0;}staticintchrdevbase_release(structinode*inode,structfile*filp){printk("chrdevbase release!\\r\\n");return0;}
驱动加载与注销
staticstructfile_operations chrdevbase_fops ={ .owner = THIS_MODULE, .open = chrdevbase_open, .read = chrdevbase_read, .write = chrdevbase_write, .release = chrdevbase_release,};/*驱动入口函数 */staticint __init chrdevbase_init(void){ int retvalue =0; retvalue =register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,&chrdevbase_fops); if(retvalue <0){ printk("chrdevbase driver register failed\\r\\n"); } printk("chrdevbase init!\\r\\n"); return0;}/* 驱动出口函数 */staticvoid __exit chrdevbase_exit(void){unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit!\\r\\n");}/* 将上面两个函数指定为驱动的入口和出口函数 */module_init(chrdevbase_init);module_exit(chrdevbase_exit);
LICENSE与作者
MODULE_LICENSE("GPL");MODULE_AUTHOR("andyxi");
4.2 应用程序编写
应用程序运行在用户空间,其通过输入相应的指令来对chrdevbase设备执行读或者写操作。 下面将程序进行分段介绍
头文件和main函数入口,以及main函数的传参处理
#include "stdio.h"#include "unistd.h"#include "sys/types.h"#include "sys/stat.h"#include "fcntl.h"#include "stdlib.h"#include "string.h"static char usrdata[] = {"usr data!"};int main(int argc, char *argv[]){ int fd, retvalue; char *filename; char readbuf[100], writebuf[100]; if(argc != 3){ printf("Error Usage!\\r\\n"); return -1; } filename = argv[1]; /* 打开驱动文件 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("Can"t open file %s\\r\\n", filename); return -1; }
对 chrdevbase 设备的具体操作
if(atoi(argv[2])==1){/* 从驱动文件读取数据 */ retvalue =read(fd, readbuf,50); if(retvalue <0){ printf("read file %s failed!\\r\\n", filename); }else{ /* 读取成功,打印出读取成功的数据 */ printf("read data:%s\\r\\n",readbuf); } } if(atoi(argv[2])==2){ /* 向设备驱动写数据 */ memcpy(writebuf, usrdata,sizeof(usrdata)); retvalue =write(fd, writebuf,50); if(retvalue <0){ printf("write file %s failed!\\r\\n", filename); } }
关闭设备
/* 关闭设备 */ retvalue = close(fd); if(retvalue < 0){ printf("Can"t close file %s\\r\\n", filename); return -1; } return 0;}
4.3 程序编译
程序编译包括驱动程序编译和应用程序编译两个部分
驱动程序编译:将驱动程序编译为.ko模块
创建Makefile文件
# KERNELDIR:开发板所使用的Linux内核源码目录KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi# CURRENT_PATH:当前路径,通过运行“pwd”命令获取CURRENT_PATH := $(shell pwd)# obj-m:将 chrdevbase.c 这个文件编译为chrdevbase.ko模块obj-m := chrdevbase.obuild: kernel_modules# -C 表示切换工作目录到KERNERLDIR目录# M 表示模块源码目录# modules 表示编译模块kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
输入make命令即可编译,编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块
注意:若直接make编译可能会出错,是因为kernel中没有指定编译器和架构,使用了默认的x86平台编译报错。 解决办法就是在内核顶层Makefile中,直接定义ARCH和CROSS_COMPILE这两个的变量值为 arm和 arm-linux-gnueabihf- 即可
应用程序编译:无需内核参与,直接编译即可
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
使用file命令,查看生成的chrdevbaseApp文件信息,如下图示,文件是32位LSB格式,ARM版本的,因此只能在ARM芯片下运行
4.4 运行测试
为了方便测试,Linux系统选择通过TFTP从网络启动,并且使用NFS挂载网络根文件系统。 确保开发板系统移植成功,能正常启动。 具体的实现方法可参考之前介绍过的系统移植专辑系列文章
加载驱动模块
在根文件系统创建/lib/modules/4.1.15文件夹,用来存放驱动模块
/lib/modules是通用的4.1.15根据所使用的内核版本来设置,否则modprobe命令无法加载驱动模块在Ubuntu中将chrdevbase.ko和chrdevbaseAPP,复制到根文件系统的 rootfs/lib/modules/4.1.15 目录中
在开发板中使用insmod或modprobe命令来加载驱动文件
输入lsmod命令即可查看当前系统中存在的模块,输入cat /proc/devices命令,查看当前系统中有没有chrdevbase 这个设备
创建设备节点文件:驱动加载成功后,需要在/dev目录下创建一个与之对应的设备节点文件,应用程序通过操作这个设备节点文件来完成对具体设备的操作
使用mknod命令创建/dev/chrdevbase设备节点文件
mknod /dev/chrdevbase c 200 0#/dev/chrdevbase 是要创建的节点文件# c 表示这是个字符设备# 200 是设备的主设备号# 0 是设备的次设备号
创建完后可使用ls /dev/chrdevbase -l命令查看是否存在
操作设备测试:使用应用程序读写设备,对/dev/chrdevbase文件进行读写操作
# 读操作命令./chrdevbaseApp /dev/chrdevbase 1# 输出“ kernel senddata ok!”是驱动程序中chrdevbase_read函数输出的信息# “read data:kernel data!”就是chrdevbaseAPP打印出来的接收到的数据# 写操作命令./chrdevbaseApp /dev/chrdevbase 2# “kernel recevdata:usr data!”,是驱动程序中的chrdevbase_write函数输出的
卸载驱动模块:若不再使用某个设备的话可以将其驱动卸载掉。 输入rmmod命令卸载驱动后,使用lsmod命令查看chrdevbase这个模块还存不存在
至此,Linux字符设备驱动开发完成。 本文介绍了驱动开发中的字符驱动开发的基本模式,并使用一个虚拟的字符设备驱动进行测试,了解驱动程序与应用程序之间的调用关系。
标签: