正点原子 I.MX6ULL MINI 开发板学习笔记:Linux 驱动架构详解——编译、树内树外与运行时效果

这是 Linux 驱动小系列的总结篇。前三篇把 LED 驱动从内核模块写到 misc 设备,属于“会用”;这篇把背后的架构讲透:驱动到底怎么编译、内核树内和树外两种方式的区别、以及驱动里每个操作在系统跑起来之后到底产生了什么效果。内容整理自我的学习笔记(note.pdf)和实际工程 code/2026/20260803。
1. 驱动代码到底怎么编译的?
不管哪种方式,最终都靠内核的 kbuild 系统。核心就三个文件的关系:
- Kconfig:定义 menuconfig 里的选项(如 CONFIG_MYMODULE),三态 y/m/n——y 编进内核,m 编成模块,n 不编;
- .config:menuconfig 的选择结果,保存在源码顶层;
- Makefile:按
obj-$(CONFIG_XXX) += xxx.o组织编译——CONFIG_XXX=y 展开成 obj-y(编进 zImage),=m 展开成 obj-m(编成 .ko)。
所以“编译驱动”本质上就是:让 kbuild 在处理你的 Makefile 时,把你的 .o 挂到 obj-y 或 obj-m 后面。树内树外只是达成这件事的两条路。
2. 方式一:内核树内写驱动(编进 zImage)
把驱动源码直接放进内核源码树,以 drivers/char/ 下新建 myhello.c 为例:
#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");然后改同目录的两个文件,让 kbuild 认识它:
# drivers/char/Kconfig 里加:
config MYMODULE
tristate "mymodule inner kernel test" # 三态:Y 编进内核 / M 编成模块 / N 不编
default y
help
driver test kconfig makefile
# drivers/char/Makefile 里加:
obj-$(CONFIG_MYMODULE) += myhello.omake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig # 找到该选项选 <*>
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16 # zImage 变大,包含驱动选 <*> 时驱动直接编进 zImage,开发板必须重启、跑新内核后才生效;选 <M> 则编成 myhello.ko。树内方式适合最终产品,开发调试期太慢。
3. 方式二:内核树外写驱动(.ko,开发期推荐)
代码放在内核源码之外的任意目录,借内核树的 kbuild 来编译。前提是内核源码已经完整编译过一遍(否则缺中间文件)。drv/Makefile:
KER_DIR := /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CUR_DIR := $(shell pwd)
TARGET ?= drv
obj-m = $(TARGET).o # obj-m:编成 $(TARGET).ko
$(TARGET)-y := drv.o # 模块名和源文件名不同时,指定模块由哪些 .o 组成
all:
$(MAKE) -C $(KER_DIR) M=$(CUR_DIR) modules
cp $(TARGET).ko ~/nfs/rootfs
make -C $(KER_DIR):先进入内核源码目录,用它的顶层 Makefile 启动 kbuild;M=$(CUR_DIR):告诉 kbuild 回到当前目录读这里的 Makefile,把 obj-m 列出的目标编成模块;- 加载卸载:
insmod drv.ko/rmmod drv,lsmod或ls /sys/module查看已加载模块; - KER_DIR 的内核版本必须与开发板运行的内核一致,否则 insmod 报 vermagic 不匹配(Invalid module format)。
4. 两种方式对比
| 项目 | 树内(zImage) | 树外(.ko) |
|---|---|---|
| 代码位置 | 内核源码树内(如 drivers/char/) | 任意目录 |
| 编译入口 | 内核顶层 make | make -C 内核树 M=$PWD modules |
| 生效方式 | 重启开发板、跑新 zImage | insmod 即生效,rmmod 可卸载 |
| init 执行时机 | 内核启动时(见下节) | insmod 那一刻 |
| 适用场景 | 产品固化、基础驱动 | 开发调试、可选功能 |
5. module_init 在两种编译下有什么不同?
同一个 module_init,两种编法执行时机完全不同:编成模块时,它把 init 函数登记为模块入口,insmod 时调用;编进 zImage 时,module_init 展开成 __initcall,函数指针被放进内核镜像的 initcall 段。内核启动流程(系统移植篇讲过)start_kernel → rest_init → kernel_init → do_basic_setup 里会执行 do_initcalls,按级别依次调用所有 initcall——树内驱动就是在这一刻完成初始化的,不需要任何人工加载动作。
6. 驱动每个操作,系统跑起来后的实际效果

以 misc 版 LED 驱动为例,把每一步和系统里能看到的现象一一对应:
- insmod drv.ko:内核把模块代码映射进内核空间,执行 module_init 注册的 mydrv_init。此后:/proc/devices 里出现 myled(主设备号 10);/sys/class/misc/myled0/ 目录建立;内核发出 uevent,mdev 收到后创建 /dev/myled0;ioremap 建立 GPIO 寄存器的页表映射;LED 被初始化成熄灭状态。
- open("/dev/myled0"):VFS 按设备节点里的设备号找到注册的 cdev,给 file 结构体挂上我们的 file_operations,调用 my_open,返回 fd。之后这个 fd 的所有操作都直达驱动的 fops。
- write(fd, &sw, 4):sys_write → fops->my_write → copy_from_user 把用户态的 sw 拷进内核 → readl/writel 操作 GPIO1_DR 的 bit3 → 灯亮或灭。dmesg 能看到驱动的 pr_info 打印。
- cat /sys/class/misc/myled0/led_status:不经过 fops,走的是 sysfs 属性——内核调用 my_show,把 "LED_OFF\n" 填进 buf 返回给 cat。
- rmmod drv:执行 mydrv_exit,按反序释放:删 sysfs 属性 → iounmap → misc_deregister。/dev/myled0 和 /sys/class/misc/myled0/ 同时消失,/proc/devices 条目移除;此后再 open /dev/myled0 直接失败。
理解这条链之后,“写驱动”就不再神秘:驱动就是往内核的各张表里登记自己的函数,系统运行中由内核在合适的时机回调。
总结
| 问题 | 答案 |
|---|---|
| 驱动怎么编译? | kbuild:Kconfig 定选项、.config 存结果、Makefile 按 obj-y/obj-m 组织 |
| 树内写驱动步骤? | 放 .c → 改 Kconfig/Makefile → menuconfig 选 <*> → 编进 zImage,重启生效 |
| 树外写驱动步骤? | obj-m + make -C 内核树 M=$PWD modules → .ko → insmod 生效 |
| module_init 两种编法区别? | 模块:insmod 时执行;树内:展开为 __initcall,内核启动时 do_initcalls 执行 |
| insmod 后系统里多了什么? | /proc/devices 条目、/sys/class 目录、/dev 节点、寄存器映射 |
| rmmod 后呢? | 全部反序消失,再 open 设备节点会失败 |
到这里,无设备树阶段的驱动开发就闭环了。接下来的方向是设备树(pinctrl/gpio 子系统)、中断、并发控制,笔记会继续更新。