二.字符设备驱动基础(1)
5.2.1.开启驱动开发之路
5.2.1.1、驱动开发的准备工作
(1)正常运行linux系统的开发板。要求开发板中的linux的zImage必须是自己编译的,不能是别人编译的。
(2)内核源码树,其实就是一个经过了配置编译之后的内核源码。
(3)nfs挂载的rootfs,主机ubuntu中必须搭建一个nfs服务器。
5.2.1.2、驱动开发的步骤
(1)驱动源码编写、Makefile编写、编译
(2)insmod装载模块、测试、rmmod卸载模块
5.2.1.3、实践
(1)copy原来提供的x210kernel.tar.bz2,找一个干净的目录(/root/driver),解压之,并且配置编译。编译完成后得到了:1、内核源码树。2、编译ok的zImage
(2)fastboot将第1步中得到的zImage烧录到开发板中去启动(或者将zImage丢到tftp的共享目录,uboot启动时tftp下载启动),将来驱动编译好后,就可以在这个内核中去测试。因为这个zImage和内核源码树是一伙的,所以驱动安装时版本校验不会出错。
5.2.2.最简单的模块源码分析1
5.2.2.1、常用的模块操作命令
(1)lsmod(list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表
(2)insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko
(3)modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。,用法是modinfo xxx.ko
(4)rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx(注意卸载模块时只需要输入模块名即可,不能加.ko后缀)
(5)剩下的后面再说,暂时用不到(如modprobe、depmod等)
5.2.2.2、模块的安装
(1)先lsmod再insmod看安装前后系统内模块记录。实践测试标明内核会将最新安装的模块放在lsmod显示的最前面。
(2)insmod与module_init宏。模块源代码中用module_init宏声明了一个函数(在我们这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。
照此分析,那insmod时就应该能看到chrdev_init中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。原因是ubuntu中拦截了,要怎么才能看到呢?在ubuntu中使用dmesg命令就可以看到了。
(3)模块安装时insmod内部除了帮我们调用module_init宏所声明的函数外,实际还做了一些别的事(譬如lsmod能看到多了一个模块也是insmod帮我们在内部做了记录),但是我们就不用管了。
5.2.2.3、模块的版本信息
(1)使用modinfo查看模块的版本信息
(2)内核zImage中也有一个确定的版本信息
(3)insmod时模块的vermagic必须和内核的相同,否则不能安装,报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format
(4)模块的版本信息是为了保证模块和内核的兼容性,是一种安全措施
(5)如何保证模块的vermagic和内核的vermagic一致?编译模块的内核源码树就是我们编译正在运行的这个内核的那个内核源码树即可。说白了就是模块和内核要同出一门。
5.2.3.最简单的模块源码分析2
5.2.3.1、模块卸载
(1)module_exit和rmmod的对应关系
(2)lsmod查看rmmod前后系统的模块记录变化
5.2.3.2、模块中常用宏
(1)MODULE_LICENSE,模块的许可证。一般声明为GPL许可证,而且最好不要少,否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
(2)MODULE_AUTHOR
(3)MODULE_DESCRIPTION
(4)MODULE_ALIAS
5.2.3.3、函数修饰符
(1)__init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
(2)__exit
5.2.3.4、static
5.2.4.最简单的模块源码分析3
5.2.4.1、printk函数详解
(1)printk在内核源码中用来打印信息的函数,用法和printf非常相似。
(2)printk和printf最大的差别:printf是C库函数,是在应用层编程中使用的,不能在linux内核源代码中使用;printk是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数,只能在内核源码范围内使用,不能在应用编程中使用。
(3)printk相比printf来说还多了个:打印级别的设置。printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的。应用程序中的调试信息要么全部打开要么全部关闭,一般用条件编译来实现(DEBUG宏),但是在内核中,因为内核非常庞大,打印信息非常多,有时候整体调试内核时打印信息要么太多找不到想要的要么一个没有没法调试。所以才有了打印级别这个概念。
(4)操作系统的命令行中也有一个打印信息级别属性,值为0-7。当前操作系统中执行printk的时候会去对比printk中的打印级别和我的命令行中设置的打印级别,小于我的命令行设置级别的信息会被放行打印出来,大于的就被拦截的。譬如我的ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4大的就不能打印出来。
(5)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。
5.2.4.2、关于驱动模块中的头文件
(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
5.2.4.3、驱动编译的Makefile分析
(1)KERN_DIR,变量的值就是我们用来编译这个模块的内核源码树的目录
(2)obj-m += module_test.o,这一行就表示我们要将module_test.c文件编译成一个模块
(3)make -C $(KERN_DIR) M=`pwd` modules 这个命令用来实际编译模块,工作原理就是:利用make -C进入到我们指定的内核源码树目录下,然后在源码目录树下借用内核源码中定义的模块编译规则去编译这个模块,编译完成后把生成的文件还拷贝到当前目录下,完成编译。
(4)make clean ,用来清除编译痕迹
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。这个Makefile本身是非常模式化的,3和4部分是永远不用动的,只有1和2需要动。1是内核源码树的目录,你必须根据自己的编译环境
5.2.5.用开发板来调试模块
5.2.5.1、设置bootcmd使开发板通过tftp下载自己建立的内核源码树编译得到的zImage
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
5.2.5.2、设置bootargs使开发板从nfs去挂载rootfs(内核配置记得打开使能nfs形式的rootfs)
setenv bootargs root=/dev/nfs nfsroot=192.168.1.141:/root/porting_x210/rootfs/rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200
5.2.5.3、修改Makefile中的KERN_DIR使其指向自己建立的内核源码树
5.2.5.4、将自己编译好的驱动.ko文件放入nfs共享目录下去
5.2.5.5、开发板启动后使用insmod、rmmod、lsmod等去进行模块实验
//test_1 #include <linux/module.h>// module_init module_exit #include <linux/init.h> // __init __exit // 模块安装函数 static int __init chrdev_init(void) { printk(KERN_INFO "chrdev_init init...\n"); //printk("<7>" "chrdev_init helloworld init\n"); //printk("<7> chrdev_init helloworld init\n"); return 0; } // 模块卸载函数 static void __exit chrdev_exit(void) { printk(KERN_INFO "chrdev_exit exit...\n"); } module_init(chrdev_init); module_exit(chrdev_exit); // MODULE_xxx这种宏作用是用来添加模块描述信息 MODULE_LICENSE("GPL"); // 描述模块的许可证 MODULE_AUTHOR("crmn"); // 描述模块的作者 MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息 MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
5.2.6.字符设备驱动工作原理1
5.2.6.1、系统整体工作原理
(1)应用层->API->设备驱动->硬件
(2)API:open、read、write、close等
(3)驱动源码中提供真正的open、read、write、close等函数实体
5.2.6.2、file_operations结构体
(1)元素主要是函数指针,用来挂接实体函数地址
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量
5.2.6.3、注册字符设备驱动
(1)为何要注册驱动
(2)谁去负责注册
(3)向谁注册
(4)注册函数从哪里来
(5)注册前怎样?注册后怎样?注册产生什么结果?
5.2.7.字符设备驱动工作原理2
5.2.7.1、register_chrdev详解(#include <linux/fs.h>)
(1)作用,驱动向内核注册自己的file_operations
(2)参数
(3)inline和static
5.2.7.2、内核如何管理字符设备驱动
(1)内核中有一个数组用来存储注册的字符设备驱动
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)
//共计0~255个号可选择作为主设备号用;应用层比如open()函数根据分配的主设备号索引来找到相对应file_operations结构体。
(4)好好理解主设备号(major)的概念
5.2.7.3、回顾和展望
(1)回顾:inline、static等关键字
(2)回顾:/proc文件系统的作用
//虚拟文件文件系统,该文件内容并不是存在于硬盘上的,全部是内核用数据结构虚拟出来的文件。
通过cat查看其实是在查看内部数据结构的值。内核里用来管理设备驱动的数据,
查看里面数组哪个格子是否为空,然后打印相关信息。
5.2.8.字符设备驱动代码实践1
5.2.8.1、思路和框架
(1)目的:给空模块添加驱动壳子
(2)核心工作量:file_operations及其元素填充、注册驱动
5.2.8.2、如何动手写驱动代码
(1)脑海里先有框架,知道自己要干嘛
(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改
(3)写下的所有代码必须心里清楚明白,不能似懂非懂
5.2.8.3、开始动手
(1)先定义file_operations结构体变量
(2)open和close函数原型确定、内容填充
5.2.9.字符设备驱动代码实践2
5.2.9.1、注册驱动
(1)主设备号的选择
(2)返回值的检测
5.2.9.2、驱动测试
(1)编译等 make && make cp
(2)insmod并且查看设备注册的现象
(3)rmmod并且查看设备注销的现象
5.2.9.3、让内核自动分配主设备号
(1)为什么要让内核自动分配
(2)如何实现?
(3)测试
//test_2 静态分配主设备号 /* head file */ #include <linux/init.h> // __init __exit #include <linux/module.h> // module_init module_exit #include <linux/fs.h> //file_operations() #define DEVNAME "chardev" static unsigned int DEVMAJOR = 250; static int test_open (struct inode *inodp, struct file *filp) { //真正应该放置的是打开这个设备的硬件操作代码,这里先用一个printk代替吧。 printk("open...\n"); return 0; } static int test_release (struct inode *inodp, struct file *filp) { printk("release...\n"); return 0; } //自定义一个file_operations结构体变量并且去填充 static struct file_operations test_fops = { .owner = THIS_MODULE,//惯例,直接写即可,其他全是函数指针 .open = test_open, //将来应用open打开这个设备时实际调用的 .release = test_release,// 就是这个.open对应的函数 }; static int __init demo_init(void) //模块安装函数 { printk("demo_init...\n"); int ret = register_chrdev(DEVMAJOR, DEVNAME, &test_fops);//注册字符设备驱动 //说明:major>0时,静态分配主设备号,函数成功返回0,失败返回负数; if (ret) { printk("register_chrdev fail...\n"); return -EINVAL; } // printk("register_chrdev seccess...\n"); return 0; } module_init(demo_init); static void __exit demo_exit(void)//模块卸载函数 { printk("demo_exit...\n"); unregister_chrdev(DEVMAJOR, DEVNAME);//注销字符设备驱动 } module_exit(demo_exit); /* driver module description */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("crmn"); MODULE_VERSION("crmn1.0"); MODULE_DESCRIPTION("example for driver module arch");
[root@FriendlyARM /]# insmod demo.ko
[ 123.665000] demo_init...
[root@FriendlyARM /]# cat /proc/devices
Character devices:
250 chardev
//test_3 让内核自动分配主设备号(为了防止发生设备号冲突!) /* head file */ #include <linux/init.h> // __init __exit #include <linux/module.h> // module_init module_exit #include <linux/fs.h> //file_operations() #define DEVNAME "chardev" static unsigned int DEVMAJOR = 250; int devmajor;//0 static int test_open (struct inode *inodp, struct file *filp) { //真正应该放置的是打开这个设备的硬件操作代码,这里先用一个printk代替吧。 printk("open...\n"); return 0; } static int test_release (struct inode *inodp, struct file *filp) { printk("release...\n"); return 0; } //自定义一个file_operations结构体变量并且去填充 static struct file_operations test_fops = { .owner = THIS_MODULE,//惯例,直接写即可,其他全是函数指针 .open = test_open, //将来应用open打开这个设备时实际调用的 .release = test_release,// 就是这个.open对应的函数 }; static int __init demo_init(void) //模块安装函数 { printk("demo_init...\n"); int devmajor = register_chrdev(0, DEVNAME, &test_fops);//注册字符设备驱动 //说明:major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的注射备号 //major=0时,动态分配主设备号,函数返回一个可用的主设备号;失败返回负数。 if (devmajor < 0) { printk("register_chrdev fail...\n"); return -EINVAL; } printk("register_chrdev seccess...[devmajor = %d]\n",devmajor); return 0; } module_init(demo_init); static void __exit demo_exit(void)//模块卸载函数 { printk("demo_exit...\n"); unregister_chrdev(devmajor, DEVNAME);//注销字符设备驱动 } module_exit(demo_exit); /* driver module description */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("crmn"); MODULE_VERSION("crmn1.0"); MODULE_DESCRIPTION("example for driver module arch");
[root@FriendlyARM /]# insmod demo.ko
[ 1098.855000] demo_init...
[ 1098.855000] register_chrdev seccess...[devmajor = 250]
[root@FriendlyARM /]# cat /proc/devices
Character devices:
250 chardev
//注意:
//major>0时,静态分配主设备号,函数成功返回0,失败返回负数;
//major=0时,动态分配主设备号,函数返回一个可用的主设备号。
static inline int register_chrdev(unsigned int major, const char *name,
2161 const struct file_operations *fops) //
2162 {
2163 return __register_chrdev(major, 0, 256, name, fops);
2164 }
/**
244 * __register_chrdev() - create and register a cdev occupying a range of minors
245 * @major: major device number or 0 for dynamic allocation
246 * @baseminor: first of the requested range of minor numbers
247 * @count: the number of minor numbers required
248 * @name: name of this range of devices
249 * @fops: file operations associated with this devices
250 *
251 * If @major == 0 this functions will dynamically allocate a major and return
252 * its number.
253 *
254 * If @major > 0 this function will attempt to reserve a device with the given
255 * major number and will return zero on success.
256 *
257 * Returns a -ve errno on failure.
258 *
259 * The name of this device has nothing to do with the name of the device in
260 * /dev. It only helps to keep track of the different owners of devices. If
261 * your module name has only one type of devices it's ok to use e.g. the name
262 * of the module here.
263 */
5.2.10.应用程序如何调用驱动
5.2.10.1、驱动设备文件的创建
(1)何为设备文件
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
5.2.10.2、写应用来测试驱动
(1)还是原来的应用
(2)open、write、read、close等
(3)实验现象预测和验证
5.2.10.3、总结
(1)整体流程梳理、注意分层
(2)后续工作:添加读写接口
//test_3
[root@FriendlyARM /]# mknod /dev/test0 c 250 0 //使用mknod创建设备文件test0
[root@FriendlyARM /]# ls /dev/test0 -l
crw-r--r-- 1 root root 250, 0 Jul 27 06:06 /dev/test0
//test_3 app_2.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FILE "/dev/test0" int main(void) { int fd = 0; fd = open(FILE,O_RDWR); if (fd < 0) { printf("open %s error...\n",FILE); return -1; } printf("open %s seccess...\n",FILE); //读写文件
//关闭文件 close(fd); return 0; }
//test_3 app_3.c #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FILE "/dev/test0" int main(void) { int fd = 0; char buf[100]; fd = open(FILE,O_RDWR); if (fd < 0) { printf("open %s error...\n",FILE); return -1; } printf("open %s seccess...\n",FILE); //读写文件 write(fd, "helloworld", 10); read(fd, buf, 100); printf("result::%s.\n", buf); //关闭文件 close(fd); return 0; }
[root@localhost app_2]# make app
arm-linux-gcc app.c -o app
[root@localhost app_2]# cat Makefile
CC=arm-linux-gcc
[root@localhost app_2]# cp app /rootfs
[root@FriendlyARM /]# ./app
[ 2728.025000] open...
open /dev/test seccess...[ 2728.025000] release...
result:��:I�ݶ. //app_3.c
5.2.11.添加读写接口
5.2.11.1、在驱动中添加
5.2.11.2、在应用中添加
5.2.11.3、测试
5.2.11.4、应用和驱动之间的数据交换
(1)copy_from_user //用来将数据从用户空间复制到内核空间(内存的复制,不是以指针的方式将内存传递过来);效率有限。
(2)copy_to_user
注意:复制是和mmap的映射相对应去区分的 //mmap()是通过映射的方式,同一块内存地址,物理内存直接映射的。
//test_4 /* head file */ #include <linux/init.h> // __init __exit #include <linux/module.h> // module_init module_exit #include <linux/fs.h> //file_operations() #include <asm/uaccess.h> #define DEVNAME "chardev" static unsigned int DEVMAJOR = 250; int devmajor;//0 char kbuf[100]; //内核空间的buf static int test_open (struct inode *inodp, struct file *filp) { //真正应该放置的是打开这个设备的硬件操作代码,这里先用一个printk代替吧。 printk("open...\n"); return 0; } static ssize_t test_read (struct file *filp, char __user *ubuf, size_t cnt, loff_t *test_fpos) { printk("read...\n"); int ret = copy_to_user(ubuf,kbuf,cnt); if (ret) { printk("copy_to_user fail...\n"); return -EINVAL;// } printk("copy_to_user seccess...\n"); return 0; } //写的本质是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作 static ssize_t test_write (struct file *filp, const char __user *ubuf, size_t cnt, loff_t *test_fpos) { printk("write...\n"); //memcpy(kbuf,ubuf);//不行,因为两者不在一个地址空间中,各自不同的虚拟空间中 //一般传递的数据不大,大的话用mmap() int ret = copy_from_user(kbuf,ubuf,cnt); if (ret) { printk("copy_from_user fail...\n"); return -EINVAL;// } printk("copy_from_user seccess...\n"); //真正的驱动中数据从应用层复制到驱动中,我们要跟据这个数据去写硬件完成硬件的操作 return 0; } static int test_release (struct inode *inodp, struct file *filp) { printk("release...\n"); return 0; } //自定义一个file_operations结构体变量并且去填充 static struct file_operations test_fops = { .owner = THIS_MODULE,//其他全是函数指针 .open = test_open, .read = test_read, .write = test_write, .release = test_release, }; /* driver module entry */ static int __init demo_init(void) //模块安装函数 { printk("demo_init...\n"); //major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的注射备号 //分配成功返回分配好的主设备号;失败返回负数。 int devmajor = register_chrdev(0, DEVNAME, &test_fops);//注册字符设备驱动 if (devmajor < 0) { printk("register_chrdev fail...\n"); return -EINVAL; } printk("register_chrdev seccess...[devmajor = %d]\n",devmajor); return 0; } module_init(demo_init); /* driver module exit */ static void __exit demo_exit(void)//模块卸载函数 { printk("demo_exit...\n"); unregister_chrdev(devmajor, DEVNAME);//注销字符设备驱动 } module_exit(demo_exit); /* driver module description */ MODULE_LICENSE("GPL"); MODULE_AUTHOR("crmn"); MODULE_VERSION("crmn1.0"); MODULE_DESCRIPTION("example for driver module arch");
//app.c
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FILE "/dev/test" int main(void) { int fd = 0; char buff[10] = {0}; fd = open(FILE,O_RDWR); if (fd < 0) { printf("open %s error...\n",FILE); return -1; } printf("open %s seccess...\n",FILE); //读写文件 write(fd,"write app!",10);//检查返回值 read(fd,buff,10); printf("read:%s\n",buff); //关闭文件 close(fd); return 0; }
[root@FriendlyARM /]# insmod demo.ko
[ 20.355000] demo_init...
[ 20.355000] register_chrdev seccess...[devmajor = 250]
[root@FriendlyARM /]# cat /proc/devices
Character devices:
250 chardev
[root@FriendlyARM /]# mknod /dev/test0 c 250 0
[root@FriendlyARM /]# ls /dev/test0 -l
crw-r--r-- 1 root root 250, 0 Jan 1 14:10 /dev/test0
[root@FriendlyARM /]# ./app
[ 201.825000] open...
open /dev/test0 seccess...[ 201.825000] write...
[ 201.825000] copy_from_user seccess...
[ 201.825000] read...
[ 201.825000] copy_to_user seccess...
[ 201.825000] release...
read:write app!

5.2.12.读写接口实践
5.2.12.1、完成write和read函数
(1)copy_from_user函数的返回值定义,和常规有点不同。
返回值如果成功复制则返回0,如果 不成功复制则返回尚未成功复制剩下的字节数。
5.2.12.2、读写回环测试
5.2.12.3、总结
(1)目前为止应用已经能够读写驱动(中的内存)
(2)后续工作:添加硬件操作代码
浙公网安备 33010602011771号