正点原子 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.o
make 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/)任意目录
编译入口内核顶层 makemake -C 内核树 M=$PWD modules
生效方式重启开发板、跑新 zImageinsmod 即生效,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 驱动为例,把每一步和系统里能看到的现象一一对应:

  1. insmod drv.ko:内核把模块代码映射进内核空间,执行 module_init 注册的 mydrv_init。此后:/proc/devices 里出现 myled(主设备号 10);/sys/class/misc/myled0/ 目录建立;内核发出 uevent,mdev 收到后创建 /dev/myled0;ioremap 建立 GPIO 寄存器的页表映射;LED 被初始化成熄灭状态。
  2. open("/dev/myled0"):VFS 按设备节点里的设备号找到注册的 cdev,给 file 结构体挂上我们的 file_operations,调用 my_open,返回 fd。之后这个 fd 的所有操作都直达驱动的 fops。
  3. write(fd, &sw, 4):sys_write → fops->my_write → copy_from_user 把用户态的 sw 拷进内核 → readl/writel 操作 GPIO1_DR 的 bit3 → 灯亮或灭。dmesg 能看到驱动的 pr_info 打印。
  4. cat /sys/class/misc/myled0/led_status:不经过 fops,走的是 sysfs 属性——内核调用 my_show,把 "LED_OFF\n" 填进 buf 返回给 cat。
  5. 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 子系统)、中断、并发控制,笔记会继续更新。

继续阅读

嵌入式开发面试知识手册

嵌入式开发工程师面试知识手册 适用对象:零基础起步、目标是通过嵌入式软件/驱动/BSP 方向面试的读者 编写依据:赵哲简历全部知识点 + 智能车 AGV 控制平台完整项目代码(i.MX6ULL Linux 端 + STM32F407 固件)+ Linux 驱动练习代码包 + embedded-agent 嵌入式开发智能体仓库 + 真实面试手撕代码与追问记录 使用方法:每篇按"通俗解释 → 原理细节 → 命令/API → 代码实例 → 项目结合 → 记忆口诀 → 面试问答"组织;口诀用于快速记忆,问答用于自测。第9篇是讲项目的演练场,第10篇是手撕代码实战题库。 总目录 * 第1篇 C语言与数据结构 * 1.1 编译链接全过程 * 1.2 数据类型与内存

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:misc 设备——字符设备驱动的快捷方式

上两篇分别用手动 mknod 和 class_create+device_create 注册字符设备,调用链一长串。这篇用 misc(混杂)设备 一步搞定。代码来自实际工程 code/2026/20260803/05_misc_led,已验证可用。 1. 完成什么实验? 用 misc_register 重写 LED 驱动:一步完成设备号分配、字符设备注册、/dev/myled0 节点创建,同时保留上一篇的 sysfs 属性 led_status。 2. 什么是 misc 设备? misc(混杂)设备是 Linux 给“不好归类的小字符设备”

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:自动创建 /dev 节点与 sysfs 属性

上一篇的 LED 驱动还要手动 mknod 才能用。这篇在同一驱动上加两步:用 class_create + device_create 让内核自动生成 /dev 节点,再挂一个 sysfs 属性文件,用 cat/echo 就能看灯、控灯。代码来自实际工程 code/2026/20260803/04_led_sys,已验证可用。 1. 完成什么实验? 在 LED 字符设备驱动基础上:insmod 后自动生成 /dev/myled0(不再手动 mknod);同时在 /sys/class/LED/myled0/ 下挂出 led_status 属性文件,

作者:azhezhezhe