文章目录
开发驱动准备工作
1.正常运行的Linux系统的开发板
开发板中的Linux的Image必须是自己编译的,不能是别人编译的。笔者用的是IMX8QM的开发板,任何开发板都是一样的流程。
2.内核源码树
其实就是一个经过配置编译之后的内核源码。编译驱动的过程中就要用到内核源码树。编写代码的时候,需要用到系统内核的头文件,因此一定要有内核源码,
3.nfs挂载的rootfs
尽管内核是linux的核心,但文件却是**用户与操作系统交互所采用的主要工具。**这对linux来说尤其如此,这是因为在UNIX传统中,它使用文件I/O机制管理硬件设备和数据文件。根文件系统首先是一种文件系统,该文件系统不仅具有普通文件系统的存储数据文件的功能,但是相对于普通的文件系统,它的特殊之处在于,它是内核启动时所挂载(mount)的第一个文件系统,内核代码的映像文件保存在根文件系统中,系统引导启动程序会在根文件系统挂载之后从中把一些初始化脚本(如rcS,inittab)和服务加载到内存中去运行。我们要明白文件系统和内核是完全独立的两个部分。在嵌入式中移植的内核下载到开发板上,是没有办法真正的启动Linux操作系统的,会出现无法加载文件系统的错误。一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。Linux启动时,第一个必须挂载的是根文件系统;若系统不能从指定设备上挂载根文件系统,则系统会出错而退出启动。成功之后可以自动或手动挂载其他的文件系统。因此,一个系统中可以同时存在不同的文件系统。在 Linux 中将一个文件系统与一个存储设备关联起来的过程称为挂载(mount)。使用 mount 命令将一个文件系统附着到当前文件系统层次结构中(根)。在执行挂装时,要提供文件系统类型、文件系统和一个挂装点。根文件系统被挂载到根目录下“/”上后,在根目录下就有根文件系统的各个目录,文件:/bin /sbin /mnt等,再将其他分区挂接到/mnt目录上,/mnt目录下就有这个分区的各个目录和文件。
4.得心趁手的IDE
这里使用的是Vscode,配置过程如下:
在Ubuntu桌面新建文件夹
mkdir 8qm_driver
用Vscode打开新建的文件夹,可以使用在新建的文件夹内使用终端命令
code .
配置头文件目录
使用快捷键 Ctrl+Shift+P 选择编辑配置
输入以下内容
{
"configurations": [
{
"name": "Linux",
"includePath": [
"/home/marxist/8qm源码/imx_4.14.98_2.0.0_ga/include",
"${workspaceFolder}/**"
],
"defines": [
"__GNUC__",
"__KERNEL__"
],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c17",
"cppStandard": "gnu++14",
"intelliSenseMode": "linux-gcc-arm64"
}
],
"version": 4
}
注意:在includePath 块中,换成你的Linux内核源码的路径,一定要是和你的目标机的源码一致
比如我的目标机的Linux版本为 4.14.98
那么编译的时候内核源码也为4.14.98
当年初学驱动的时候,就因为版本不一致,dmesg一直报错,提示vermagic不一致,查了一天资料才发现这个问题,┭┮﹏┭┮
第一个Hello world 驱动程序
常见模块的操作命令
在Linux系统中,管理内核模块的常见命令包括lsmod
、insmod
、rmmod
、modinfo
等。这些命令用于列出、加载、卸载和查看内核模块的详细信息。
-
lsmod
- 作用:列出当前加载到内核中的所有模块。
- 用法:直接运行
lsmod
命令,无需参数。 - 输出:模块名称、使用次数及相关模块。
lsmod
-
insmod
- 作用:加载已编译的内核模块。
- 用法:
insmod <module_file.ko>
- 注意:加载时需要提供模块的完整路径。常用于开发阶段,手动加载模块测试。
sudo insmod my_driver.ko
-
modinfo
- 作用:显示模块的详细信息,包括依赖、版本信息、许可证等。
- 用法:
modinfo <module_file.ko>
- 输出示例:
depends
:显示模块依赖的其他模块。vermagic
:显示模块编译时的内核版本和相关信息。
modinfo my_driver.ko
-
rmmod
- 作用:卸载已加载的模块。
- 用法:
rmmod <module_name>
- 注意:模块名称不带文件扩展名
.ko
,卸载时只需提供模块名。
sudo rmmod my_driver
模块的初始化和清理
模块源代码通常使用module_init
宏声明初始化函数。这个宏指定的函数在模块加载时(使用insmod
命令)自动调用,例如:
static int __init chrdev_init(void)
{
printk(KERN_INFO "Initializing character device\n");
return 0;
}
module_init(chrdev_init);
module_init
的作用是将chrdev_init
函数与insmod
命令绑定。当运行insmod
时,这个函数被调用。初始化函数中的输出通常使用printk
打印,可以通过dmesg
命令查看内核消息。
模块的版本信息
- 内核模块的版本信息由
vermagic
字段表示。vermagic
包含了内核版本号、编译器版本等信息。加载模块时,vermagic
必须与当前运行的内核版本一致,否则insmod
会失败。这是为了确保模块与内核的兼容性和系统的稳定性。
模块中的各种宏
-
MODULE_LICENSE
- 作用:声明模块的许可证类型。常见的值包括"GPL"(开源)和"Proprietary"(私有)。
- 示例:
MODULE_LICENSE("GPL");
- 注意:使用GPL许可证表示模块遵循开源协议,加载时不会受到限制。
-
MODULE_AUTHOR
- 作用:声明模块的作者信息。
- 示例:
MODULE_AUTHOR("Author Name");
-
module_init 和 module_exit
-
作用:
module_init
宏用于指定模块的初始化函数,module_exit
宏用于指定模块的清理函数。 -
示例:
module_init(chrdev_init); module_exit(chrdev_exit);
-
-
__init 和 __exit
-
作用:这些宏用于标记初始化和清理函数。
__init
标记的函数在模块加载后被释放,以节省内存。__exit
标记的函数在模块卸载时调用。 -
示例:
static int __init chrdev_init(void) { printk(KERN_INFO "Initializing character device\n"); return 0; } static void __exit chrdev_exit(void) { printk(KERN_INFO "Exiting character device\n"); }
-
示例Hello World代码
新建一个kernel_test.c
#include <linux/init.h>
#include <linux/module.h>
//在模块加载到内核程序中被执行一次
int __init test_init(void)
{
printk("Hello World\n");
return 0;
}
//在模块从内核驱动中卸载时执行一次
void __exit test_exit(void)
{
printk("Goodbye World\n");
}
module_init(test_init); //注册此模块加载的回调函数
module_exit(test_exit); //注册此模块卸载的回调函数
MODULE_LICENSE("GPL"); //声明遵循协议
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命令去查看
使用MakeFile编译驱动模块
在源码同级别目录新建一个MakeFile文件,内容如下:
obj-m += kernel_test.o
all:
make -C /home/marxist/8qm源码/imx_4.14.98_2.0.0_ga M=$(PWD) modules
clean:
make -C /home/marxist/8qm源码/imx_4.14.98_2.0.0_ga M=$(PWD) clean
(1)KERN_DIR,变量的值就是我们用来编译这个模块的内核源码树的目录
(2)obj-m += kernel_test.o,这一行就表示我们要将kernel_test.c文件编译成一个模块
(3)make -C $(KERN_DIR) M=pwd modules 这个命令用来实际编译模块,工作原理就是:利用make -C进入到我们指定的内核源码树目录下,然后在源码目录树下借用内核源码中定义的模块编译规则去编译这个模块,编译完成后把生成的文件还拷贝到当前目录下,完成编译。
(4)make clean ,用来清除编译痕迹
模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的,这个Makefile本身是非常模式化的,3和4 部分是永远不用动的,只有1和2需要动。1是内核源码树的目录,必须根据自己的编译环境来调整
保存文件之后,在MakeFile的同级目录,直接执行 make
命令即可
成功之后,出现很多的文件,只有ko文件,才是最终能够被加载进内核的模块,其余的都是中间编译连接生成的文件
使用insmod加载模块
将编译好的模块传输到目标机器上 使用insmod
加载,成功之后,使用dmesg
命令可以查看打印的信息
使用rmmod
命令可以卸载模块,同时执行exit函数
使用modinfo
查看模块信息
至此,第一个Hello world 驱动就编写成功了