来自朱有鹏课堂笔记
整体思路:
1、先做实验
2、分析细节、讲解理论
3、自己编写驱动,然后再写应用程序来测试驱动。
一、开启驱动开发之路
1、驱动开发的所需物品
(1)正常运行的 linux 系统的开发板。
1、开发板上面跑的内核,zImage 必须是自己编译的。
(2)内核源码树(文件夹套文件夹的形式)
1、内核源码树:就是一个经过配置编译之后,的内核源码。
(3)nfs 挂载的 rootfs
1、在主机 Ubuntu 当中必须有一个搭建好的 nfs 服务器。
2、目的:将主机 UBuntu 当中的文件,实时传输到开发板当中。
2、驱动开发的步骤
(1)驱动源码编写、Makefile编写、编译
1、Makefile 的编写已经形成了一定的固定套路。所以它不需要我们一行一行自己写
(2)insmod装载模块、测试、rmmod卸载模块
1、insmod装载模块:动态装载驱动。(就是不将驱动放到linux内核当中)
2、测试:写一个应用程序,来操作这个驱动。
3、rmmod卸载模块:动态卸载驱动。
3、实践
(1)copy原来提供的 x210kernel.tar.bz2 ,找一个干净的目录(/root/driver),解压之,并且配置编译。
在编译之前:
确保ARCHCOROSS_COMPILE的值。(架构和交叉编译工具链)
编译完成后得到了:1、内核源码树。2、编译ok的zImage
我们自己配置编译:
这个zImage和内核源码树是一伙的,所以驱动安装时版本校验不会出错。
(2)fastboot将第1步中得到的zImage烧录到开发板中去启动(或者将zImage丢到tftp的共享目录,uboot启动时tftp下载启动),将来驱动编译好后,就可以在这个内核中去测试。
(3)简单分析:驱动.c 文件编译之后的产物:

Makefile :指导编译链接
module_test.c:生成module_test.o、module_test.mod.c、module_test.mod.o
Module.symvers:
modules.order
module_test.ko:kernel object, 就是驱动的目标文件
二、最简单的模块源码分析(1)
1、常用的模块操作命令
1、模块操作命令:模块的挂载,卸载,查看,设置等等。
2、是 Linux 支持的命令。
(1)lsmod (list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表
(2)insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko .
在模块名字后面一定要加上
.ok的后缀。
(3)modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。,用法是modinfo xxx.ko

(4)rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx
注意卸载模块时只需要输入模块名即可,不能加
.ko后缀
(5)剩下的后面再说,暂时用不到(如modprobe、depmod等)
(6)我们生成一个 .ko 文件,我们一般的开发步骤:
1、modinfo: 查看以下相关信息
2、lsmod:查看一下当前系统所安装的模块
3、insmod:安装我们想要安装的模块。
2、模块的安装与卸载
模块源文件:
#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 helloworld init\n");
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
module_init(chrdev_init); //执行这个宏就相当于进到模块安装函数里面执行函数
module_exit(chrdev_exit); //执行这个宏就相当于进到模块卸载函数里面执行函数
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
模块的安装:
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
return 0;
}
module_init(chrdev_init); //执行这个宏就相当于进到模块安装函数里面执行函数
模块的卸载
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
module_exit(chrdev_exit); //执行这个宏就相当于进到模块卸载函数里面执行函数
1、
module_init(chrdev_init):是一个宏。
2、这个宏声明了一个函数chrdev_init, 作用就是:将这个函数和我们insmod命令绑定起来。
3、也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。
4、其实安装模块的函数,还需要我们自己去写,内核并不提供。
(2)照此分析,那 insmod 时就应该能看到 chrdev_init 中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。
原因是ubuntu中拦截了,要怎么才能看到呢?
在ubuntu中使用
dmesg命令就可以看到了。
(3)模块安装时insmod命令内部帮我们做了些什么?
1、帮我们调用module_init宏所声明的函数
2、lsmod 的时候能看到多了一个模块,也是 insmod 帮我们在内部做了记录。但是我们就不用管了。
3、模块的版本信息
(1)使用 modinfo 查看模块的版本信息
(2)在配置内核的时候, zImage 中也有一个确定的版本信息
(3)insmod时模块的 vermagic ,必须和内核的相同,否则不能安装,报错信息为:
1、所以说不是随便的一个模块拿过来,我们就可以直接安装的。
2、模块的版本信息是为了保证模块和内核的兼容性,是一种安全措施。
insmod: ERROR: could not insert module module_test.ko: Invalid module format
(4)如何保证模块的vermagic,和内核的vermagic一致?
编译模块的内核源码树,就是我们编译正在运行的这个内核的那个内核源码树即可。说白了就是模块和内核要同出一门。
1、使用先编译生成一个内核,
2、然后使用这个内核,去编译我们的模块。
4、模块中常用宏
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
注:MODULE_xxx这种宏作用是,用来添加模块描述信息
(1)MODULE_LICENSE,模块的许可证。一般声明为GPL许可证,而且最好不要少,否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
1、GPL许可证:
2、Dual BSD 许可证:
(2)MODULE_AUTHOR:描述模块的作者
(3)MODULE_DESCRIPTION:描述模块的介绍信息
(4)MODULE_ALIAS:描述模块的别名信息
5、函数修饰符
// 模块安装函数
static int __init chrdev_init(void)
{
printk(KERN_INFO "chrdev_init helloworld init\n");
return 0;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
}
(1)static:保证这两个函数,不会被其他 .c 程序来调用。
修饰局部变量:
将局部变量,变为本文件内都可以使用的变量。
修饰全局变量和函数:
1、在函数的返回类型前加上关键字static,函数就被定义成为静态函数。
2、普通函数的定义和声明默认情况下是extern的
3、但静态函数只是在声明他的文件当中可见,不能被其他文件所用。因此定义静态函数有以下好处:
<1> 其他文件中,可以定义相同名字的函数,不会发生冲突。
<2> 静态函数,不能被其他文件所用。
(2)__init,__exit 本质上是个宏定义,在内核源代码中就有 #define __init xxxx。

1、这个__init 的作用就是将被他修饰的函数放入 .init.text 段中去
(本来默认情况下函数是被放入 .text 段中)(所有代码默认放入文本段,数据放入data段)
2、.init.text 段 是我们自定义的一个段。
3、整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。
4、内核启动时统一会加载.init.text段中的这些模块安装函数,当我们安装模块完成之后,这些函数就没有用了。
5、加载完后就会把这个段给释放掉以节省内存。
二、最简单的模块源码分析(2)
1、printk函数详解
(1)printk 在内核源码中用来打印信息的函数,用法和printf非常相似。
(2)printk 和 printf 最大的差别:
1、printf 是C库函数,是在应用层编程中使用的,不能在linux内核源代码中使用;
2、printk 是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数
3、只能在内核源码范围内使用,不能在应用编程中使用。
(3)printk 相比 printf来说还多了个:打印级别的设置。
#define KERN_EMERG "<0>" /* 系统不可使用 */
#define KERN_ALERT "<1>" /* 需要立即采取行动 */
#define KERN_CRIT "<2>" /* 严重情况 */
#define KERN_ERR "<3>" /* 错误情况 */
#define KERN_WARNING "<4>" /* 警告情况 */
#define KERN_NOTICE "<5>" /* 正常情况, 但是值得注意 */
#define KERN_INFO "<6>" /* 信息型消息 */
#define KERN_DEBUG "<7>" /* 调试级别的信息 */
printk(KERN_INFO "chrdev_init helloworld init\n");
printk("<6>" "chrdev_init helloworld init\n");
1、printk 的打印级别是用来控制printk打印的这条信息是否在终端上显示的。
2、应用程序中的调试信息要么全部打开要么全部关闭,一般用条件编译来实现(DEBUG宏)
3、但是在内核中,因为内核非常庞大,打印信息非常多,有时候整体调试内核时打印信息要么太多找不到想要的,要么一个没有没法调试。所以才有了打印级别这个概念。
(4)操作系统的命令行中也有一个打印信息级别属性,值为 0-7 。
1、当前操作系统中执行printk的时候会去对比 printk 中的打印级别,和我的命令行中设置的打印级别
2、小于我的命令行设置级别的信息会被放行打印出来,大于的就被拦截的。
譬如我的ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4大的就不能打印出来。
(5)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置,都不能直接打印出来,必须dmesg命令去查看。
2、关于驱动模块中的头文件
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。
1、应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。
2、驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。
3、驱动编译的Makefile分析
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel
obj-m += module_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
cp:
cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
这个 makefile 大概包含四个部分:
(1)KERN_DIR,变量的值:就是我们用来编译这个模块的,内核源码树的目录
(2)这一行就表示我们要将module_test.c文件编译成一个模块。
obj-m += module_test.o
-m : 单独编译成一个模块
-y :将这个模块,编译链接到 zImage 里面去
(3)这个命令用来实际编译模块,工作原理就是:
1、利用
make -C进入到我们指定的内核源码树目录下
2、然后在源码目录树下,借用内核源码中定义的模块编译规则去编译这个模块
3、编译完成后把生成的文件还拷贝到当前目录下,完成编译。
总结:
1、我们这个 Make file 只是一个索引,引导我们找到内核源码树
2、内核源码树里面才详细定义了我们的编译规则。
3、所以从本质上来看,真正做编译工作的是:我们的内核源码树
make -C $(KERN_DIR) M=`pwd` modules
`make -C` 进入到我们指定的内核源码树目录下
`pwd` :` ` 表示将 pwd 当作一个命令去执行,相当于在这里直接打印我们的全路径
M=`pwd` :根据这个记录然后返回到当前路径下面来
make modules : 编译modules 这个目标,这个目标在我们的内核源码树当中
# 开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel
这个目录要到达:Makefile 的上面。

(4)make clean ,用来清除编译痕迹
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。
这个Makefile本身是非常模式化的,3和4部分是永远不用动的,只有1和2需要动。
1、是内核源码树的目录,你必须根据自己的编译环境
2、后面驱动的名字,必须和我们的模块名字相同。
三、用开发板来调试模块
1、设置 bootcmd 使开发板通过tftp下载自己建立的内核源码树编译得到的 zImage
1、只需要将 zImage 放到我们的 tftp 服务器的目录下面即可。
2、不需要将整个内核源码树放进去。
set bootcmd 'tftp 0x30008000 zImage;bootm 0x30008000'
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
console=ttySAC2,115200 :使用串口2,波特率为 115200
3、修改Makefile中的KERN_DIR使其指向自己建立的内核源码树
4、将自己编译好的驱动 .ko 文件放入nfs共享目录下去
可以在 Makefile 当中多写一个目标: 直接 make cp 即可
cp:
cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
5、开发板启动后使用 insmod、rmmod、lsmod等去进行模块实验
注意:在开发板当中可以验证,printk 的打印级别的操作。
1、
cat /proc/sys/kernel/printk:只看第一个数字。
2、修改我们的打印级别,然后进行对比
本文详细介绍Linux驱动开发的基础知识,包括所需的开发环境搭建、模块的基本操作命令及使用方法、Makefile编写技巧等内容。并深入分析了模块源码,帮助读者理解模块安装与卸载的过程。




4317

被折叠的 条评论
为什么被折叠?



