一、内核编程与应用编程的差异
开发驱动程序,需要解决 3 个核心问题:
如何在已经运行的操作系统内核中,执行我们自己编写的驱动代码
掌握驱动代码的标准编写框架
驱动最终是对硬件寄存器进行操作,原理和裸机编程一致,但内核不能直接访问物理地址,需要完成虚拟地址和物理地址的映射
Linux 驱动分为两种加载模式:静态加载编译进 zImage 内核镜像、动态加载生成 ko 模块。
1.1 静态加载,驱动编译进 zImage
把驱动直接编译进内核 zImage 镜像:
编写驱动.c源文件
- 准备编译相关文件:.config、Kconfig、Makefile
- 执行
make menuconfig,图形配置界面勾选驱动选项<*>, 代表编译进内核 - 重新编译生成 zImage 镜像,镜像体积会增大,驱动成为内核的一部分
将新zImage烧录开发板,开发板重启后新增驱动代码方可生效
缺点:每次修改驱动都需要重新编译内核、烧写镜像,必须重启开发板,调试效率低。
1.2 动态加载,生成 ko 模块
将驱动编译为独立的.ko模块文件,独立于zImage,系统启动后可以手动加载卸载,调试灵活。
编写驱动.c源文件
修改 Makefile:obj‑m += led.o,‑m表示编译为外部 ko 模块
执行make modules编译模块,生成.ko文件
开发板 Linux 系统运行后,执行insmod xxx.ko手动加载驱动
执行rmmod xxx卸载模块,无需重启开发板
ko 不属于 zImage 的一部分,可以按需加载卸载。
常用模块操作命令
bash
insmod myhell.ko # 加载内核模块
lsmod # 查看已经加载的内核模块
rmmod myhell # 卸载内核模块
ls /sys/module # 查看sysfs下模块相关信息
二、字符设备重要内核宏与函数编写最简驱动模块,需要掌握如下内核接口:
二、 字符设备重要内核宏与函数
| 函数 / 宏 | 功能说明 |
|---|---|
| module_init() | 驱动模块加载入口宏,执行 insmod 时调用该函数 |
| module_exit() | 驱动模块卸载入口宏,执行 rmmod 时调用该函数 |
| MODULE_LICENSE("GPL") | 模块许可证声明,必须添加,否则内核产生污染警告 |
| printk() | 内核打印函数,类似用户态 printf,不支持浮点数输出,输出等级受内核配置控制 |
| pr_info(fmt,...) | 带输出等级打印,输出普通日志信息 |
| pr_err(fmt,...) | 带输出等级打印,输出错误日志信息 |
前置开发环境准备:配置好 tags、cscope 源码索引,完成 vim/vscode 自动补全环境。
三、方式一:在内核源码目录内编写驱动
在drivers/char目录新增驱动文件,借助内核Kconfig/Makefile编译体系完成编译。
3.1 创建驱动源码
在drivers/char目录新建mydevtest.c
c
#include "linux/printk.h"
#include <linux/init.h>
#include <linux/module.h>
static int __init mymodule_init(void)
{
pr_info("i'm coming...,mymodule_init ok\n");
return 0;
}
static void __exit mymodele_exit(void)
{
pr_info("mymodule_init byebye\n");
return;
}
module_init(mymodule_init);
module_exit(mymodele_exit);
MODULE_LICENSE("GPL");
__init:标记初始化函数,内核启动完成后该段内存可以被释放
__exit:标记卸载函数,如果驱动静态编译进内核,该函数会被丢弃
3.2 修改同目录 Kconfig
在 drivers/char/Kconfig 中添加驱动配置项
Kconfig
bash
config MYMODULE
tristate "mymodule inner kernel test"
default y
help
driver test kconfig makefile
tristate 为三态选项:Y 编译进内核、M 编译成 ko 模块、N 不编译;config MYMODULE 定义配置变量,供 Makefile 使用。
3.3 修改同目录 Makefile
在 drivers/char/Makefile 增加下面一行
makefile
bash
obj-$(CONFIG_MYMODULE) += myhello.o
CONFIG_MYMODULE 由 menuconfig 的选择决定:
选择 Y:展开为obj-y += myhello.o,编译进 zImage
选择 M:展开为obj-m += myhello.o,编译生成 ko 模块
选择 N:该行直接忽略,不编译该文件
3.4 编译模块,开发板加载测试
执行make menuconfig,找到mymodule inner kernel test配置项,选择 M,编译模块
执行make modules编译模块,生成myhello.ko
将 ko 文件拷贝至开发板 nfs 根文件系统
开发板终端执行命令
bash
bash
insmod myhell.ko
lsmod
rmmod myhello
控制台输出对应打印信息,代表模块加载卸载正常。
四、方式二:内核源码目录外搭建独立驱动工程
实际项目普遍采用该方案,驱动代码完全独立于内核源码树,不需要修改内核内部 Kconfig、Makefile。
前置条件:内核源码已经完整编译完成。
4.1 工程目录结构
plaintext
project/
├── app/ # 用户态测试应用程序
│ ├── app.c
│ └── Makefile
├── drv/ # 内核驱动代码
│ ├── drv.c
│ ├── Makefile
│ └── .clangd # vscode自动补全配置文件
└── Makefile # 顶层总Makefile,一键编译app和驱动
drv 目录 Makefile(驱动模块编译)
bash
#makefile
MODULE_NAME:=drv
修改为你本机已编译完成内核源码的绝对路径
bash
KER_DIR := /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
obj-m := $(MODULE_NAME).o
all:
make -C $(KER_DIR) M=$(CUR_DIR) modules
clean:
make -C $(KER_DIR) M=$(CUR_DIR) clean
distclean:
rm -f *.ko *.o *.mod *.mod.c *.symvers *.order
make -C $(KER_DIR) M=$(CUR_DIR) distclean
注意:MODULE_NAME:=drv 赋值等号后面不要加多余空格,极易引发编译问题。
bash
#makefile
SRC:=app.c
OBJ:=app
修改为你的交叉编译器
bash
CC:=arm-linux-gnueabihf-gcc
顶层总 Makefile,一键编译全部工程
bash
all:
$(CC) $(SRC) -o $(OBJ)
clean:
rm $(OBJ)
```bash
makefile
all:
make -C ./app all
make -C ./drv all
clean:
make -C ./app clean
make -C ./drv clean
4.2 编译与开发板使用
在工程顶层目录执行make,同时编译用户应用和驱动模块,得到drv.ko与app可执行程序
将 drv.ko、app 拷贝到开发板。
开发板终端执行:
bash
bash
insmod drv.ko
./app
rmmod drv
五、两种驱动开发方案对比总结
内核源码树内写驱动
需要修改内核源码内部的 Kconfig、Makefile;适合需要合入主线内核的驱动;调试繁琐,每次修改需要关注 menuconfig 配置。
内核源码树外独立工程
驱动代码完全独立,不污染内核源码;只需要一份编译完成的内核源码;修改驱动只编译自身工程,推荐日常驱动调试使用.