目录
- 方法一:外部模块编译
- 方法二:内置模块编译
- 方法三:通过 menuconfig 图形界面
- 方法四:直接修改 .config 文件
- 方法五:通过 defconfig 预置配置
- 方法六:通过 Kbuild 条件编译
- 方法七:通过 modprobe 自动加载
- 方法八:通过内核引导参数加载
- 总结对比
- 开发阶段推荐流程
方法一:外部模块编译(最灵活)
驱动代码放在内核源码外部,单独 make 编译,生成 .ko 文件。
目录结构
/home/eh/linux/xuexi1/my_driver/01char_driver/驱动环境测试/
├── moudle.c
└── Makefile
Makefile
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
obj-m +=moudle.o
KDIR := /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译和加载
make # 编译 .ko
insmod moudle.ko # 加载
rmmod moudle # 卸载
适用场景
驱动开发调试阶段,改代码后快速重新编译测试。
方法二:内置模块编译(编译进内核镜像)
驱动代码放在内核源码内部,编译时链接进 zImage。
核心概念:Linux Kbuild 构建系统
Linux 内核使用 Kbuild 系统进行配置和编译,由三个关键组件构成:
- Kconfig – 配置选项定义(用户在 menuconfig 中看到的菜单项)
- Makefile – 编译规则(告诉构建系统如何编译、何时编译)
.config– 最终生成的配置文件(记录哪些功能被启用)
步骤 1:创建内核源码中的驱动目录
mkdir -p /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver
创建物理目录结构,还未与内核构建系统连接。
步骤 2:复制驱动源码
cp /home/eh/linux/xuexi1/my_driver/01char_driver/驱动环境测试/moudle.c \
/home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver/
此时目录结构:
drivers/char/my_driver/
├── moudle.c # 驱动源码
├── Kconfig # (待创建) 配置选项定义
└── Makefile # (待创建) 编译规则
步骤 3:创建 Kconfig(配置选项定义)
vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver/Kconfig
config MY_HELLO
tristate "Hello driver for IMX6ULL"
default y
help
A simple hello world character device driver.
配置项解析:
| 字段 | 含义 |
|---|---|
config MY_HELLO | 定义配置符号 MY_HELLO,对应变量 CONFIG_MY_HELLO |
tristate | 三态选项:y 编译进内核 / m 编译为模块 / n 不编译 |
default y | 默认编译进内核 |
help | menuconfig 中显示的帮助文本 |
作用:
- 运行
make menuconfig时,这个配置项会出现在菜单中供用户选择 - 用户的选择会写入
.config文件,如CONFIG_MY_HELLO=y
步骤 4:创建子目录 Makefile(定义编译规则)
vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver/Makefile
obj-$(CONFIG_MY_HELLO) += hello.o
Makefile 变量展开机制:
| 用户选择 | .config 内容 | Makefile 展开后 | 效果 |
|---|---|---|---|
[*] (编译进内核) | CONFIG_MY_HELLO=y | obj-y += hello.o | 编译 moudle.c → hello.o → 链接进内核 |
<M> (编译为模块) | CONFIG_MY_HELLO=m | obj-m += hello.o | 编译 moudle.c → hello.ko 模块 |
< > (不编译) | # CONFIG_MY_HELLO is not set | obj- += hello.o | 不编译(空规则) |
编译过程:
moudle.c → [gcc -c] → hello.o → [链接] → drivers/char/my_driver/built-in.o
↓
合并到 drivers/char/built-in.o
↓
合并到 drivers/built-in.o
↓
链接进 vmlinux → zImage
步骤 5:在父级 Makefile 中引用(递归编译入口)
vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/Makefile
在文件末尾添加:
obj-$(CONFIG_MY_HELLO) += my_driver/
这一行的核心作用:
**告诉 Kbuild 系统”进入 my_driver/ 子目录,寻找并执行该目录下的 Makefile”**。
本质上是递归编译的入口指令,类似于:
# 如果 CONFIG_MY_HELLO=y,则执行
cd my_driver && make
如果没有这一行会怎样?
- Kbuild 系统 不会自动扫描子目录
- 即使
my_driver/Makefile存在、CONFIG_MY_HELLO=y,也 不会编译 - 这是 显式递归 设计,而非自动遍历
递归编译流程:
drivers/char/Makefile (父级)
↓ 读取到:obj-y += my_driver/
↓ Kbuild 执行:进入 my_driver/ 子目录
↓
drivers/char/my_driver/Makefile (子级)
↓ 读取到:obj-y += hello.o
↓ Kbuild 执行:gcc -c moudle.c -o hello.o
↓
生成 hello.o 并链接到 built-in.o
- 顶层 Makefile 读取 .config
↓ - 解析 drivers/Makefile
发现:obj-y += char/
↓ - 进入 drivers/char/Makefile
发现:obj-y += my_driver/ (因为 CONFIG_MY_HELLO=y)
↓ - 进入 drivers/char/my_driver/Makefile
发现:obj-y += hello.o
↓ - 编译 moudle.c
arm-linux-gnueabihf-gcc -c moudle.c -o hello.o
↓ - 链接到内核
将 hello.o 合并到 drivers/char/built-in.o
↓ - 最终链接
drivers/char/built-in.o → drivers/built-in.o → vmlinux → zImage
步骤 6:在父级 Kconfig 中引用(配置菜单入口)
vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/Kconfig
找到合适的位置添加:
source "drivers/char/my_driver/Kconfig"
source 指令的作用:
将子目录的 Kconfig 文件 包含 到父级 Kconfig 中,类似 C 语言的 #include。
效果:
运行 make menuconfig 时:
Device Drivers --->
Character devices --->
[*] Hello driver for IMX6ULL ← 这个选项来自 my_driver/Kconfig
如果没有 source 会怎样?
CONFIG_MY_HELLO配置项 不会出现在 menuconfig 菜单中- 用户无法通过图形界面选择该驱动
- 即使手动在
.config中添加CONFIG_MY_HELLO=y,也会因为符号未定义而被make oldconfig忽略
Kconfig 树状包含关系:
arch/arm/Kconfig (顶层)
↓ source "drivers/Kconfig"
drivers/Kconfig
↓ source "drivers/char/Kconfig"
drivers/char/Kconfig
↓ source "drivers/char/my_driver/Kconfig"
drivers/char/my_driver/Kconfig
↓ 定义 config MY_HELLO
两个关键配置的对比
| 文件位置 | 作用 | 系统 | 缺少后果 |
|---|---|---|---|
source "drivers/char/my_driver/Kconfig" | 将子目录配置项注入 menuconfig 菜单 | 配置系统 | 用户看不到选项,无法启用驱动 |
obj-$(CONFIG_MY_HELLO) += my_driver/ | 告诉构建系统进入子目录编译 | 编译系统 | 即使配置启用,也不会编译驱动 |
两者独立但配合工作:
source让用户 能选择obj-y +=让编译器 能找到
步骤 7:配置并编译
cd /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
# 方式A:通过 menuconfig 图形界面
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 导航到:Device Drivers -> Character devices -> [*] Hello driver for IMX6ULL
# 按 Y 键选择,保存退出
# 方式B:直接修改 .config
echo "CONFIG_MY_HELLO=y" >> .config
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- oldconfig # 更新依赖
# 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16
完整编译流程详解
阶段 1:配置阶段(生成 .config)
make menuconfig
用户操作:
Device Drivers --->
Character devices --->
[*] Hello driver for IMX6ULL ← 选中
生成 .config 文件:
CONFIG_MY_HELLO=y
阶段 2:编译阶段(Kbuild 递归构建)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16
Kbuild 系统工作流程:
1. 顶层 Makefile 读取 .config
发现:CONFIG_MY_HELLO=y
↓
2. 解析 drivers/Makefile
发现:obj-y += char/
↓ 进入 drivers/char/
↓
3. 解析 drivers/char/Makefile
读取:obj-$(CONFIG_MY_HELLO) += my_driver/
展开为:obj-y += my_driver/ (因为 CONFIG_MY_HELLO=y)
↓ 进入 drivers/char/my_driver/
↓
4. 解析 drivers/char/my_driver/Makefile
读取:obj-$(CONFIG_MY_HELLO) += hello.o
展开为:obj-y += hello.o
↓
5. 编译驱动源码
执行:arm-linux-gnueabihf-gcc -c moudle.c -o hello.o
↓
6. 链接到 built-in.o
执行:ld -r hello.o -o my_driver/built-in.o
合并:my_driver/built-in.o → drivers/char/built-in.o
合并:drivers/char/built-in.o → drivers/built-in.o
↓
7. 最终链接内核
ld drivers/built-in.o fs/built-in.o ... -o vmlinux
↓
8. 压缩生成镜像
objcopy + gzip → arch/arm/boot/zImage
关键编译命令示例:
# 编译驱动源码
arm-linux-gnueabihf-gcc -Wp,-MD,drivers/char/my_driver/.hello.o.d \
-nostdinc -isystem /usr/lib/gcc-cross/arm-linux-gnueabihf/7/include \
-I./arch/arm/include -Iarch/arm/include/generated -Iinclude \
-D__KERNEL__ -mlittle-endian -Wall -Wundef -Wstrict-prototypes \
-fno-strict-aliasing -fno-common -O2 -fno-dwarf2-cfi-asm \
-c -o drivers/char/my_driver/hello.o drivers/char/my_driver/moudle.c
# 链接到 built-in.o
arm-linux-gnueabihf-ld -r -o drivers/char/my_driver/built-in.o \
drivers/char/my_driver/hello.o
# 合并到上层 built-in.o
arm-linux-gnueabihf-ld -r -o drivers/char/built-in.o \
drivers/char/my_driver/built-in.o drivers/char/其他.o ...
验证编译结果
检查编译产物
# 检查驱动目录的编译产物
ls drivers/char/my_driver/
# 应该看到:hello.o built-in.o moudle.c Makefile Kconfig
# 检查符号是否在内核中
arm-linux-gnueabihf-nm vmlinux | grep hello_init
# 输出:c0a12340 t hello_init
# 检查内核镜像大小(应该略微增大)
ls -lh arch/arm/boot/zImage
运行时验证
# 内核启动后,驱动自动初始化(不需要 insmod)
dmesg | grep -i hello
# 输出:[ 1.234567] Hello driver initialized
配置变量的三种状态对比
# 用户在 menuconfig 中的不同选择,产生完全不同的编译行为:
# 情况 1:编译进内核 (=y)
CONFIG_MY_HELLO=y
↓
obj-y += my_driver/ # 进入子目录
obj-y += hello.o # 编译并链接进 vmlinux
↓
drivers/char/my_driver/built-in.o → 合并到内核镜像
内核启动时自动加载,无需 insmod
# 情况 2:编译为模块 (=m)
CONFIG_MY_HELLO=m
↓
obj-m += my_driver/ # 进入子目录
obj-m += hello.o # 编译为独立模块
↓
生成 drivers/char/my_driver/hello.ko
需要手动 insmod hello.ko 加载
# 情况 3:不编译
# CONFIG_MY_HELLO is not set
↓
obj- += my_driver/ # 空规则,不进入子目录
↓
moudle.c 完全不参与编译
适用场景
| 选择 | 适用场景 |
|---|---|
=y (编译进内核) | 核心驱动、启动必需的驱动(如存储、串口) |
=m (编译为模块) | 可选驱动、调试阶段、需要动态加载卸载的驱动 |
| 不编译 | 不需要的驱动、目标硬件不支持的驱动 |
补充:编译但不链接进内核的配置方式
在内核开发中,有时需要编译某些代码生成 .o 文件,但不将其链接进最终的内核镜像。
方式 1:extra-y(编译但不链接)
extra-y += hello.o
效果:
- 会编译
moudle.c→hello.o hello.o不会链接进built-in.ohello.o不会进入最终的内核镜像
用途:
- 生成中间文件供其他目标使用(如链接脚本)
- 工具类代码(如内核构建工具)
- 测试编译但暂不启用的代码
实际示例:
# arch/arm/kernel/Makefile
extra-y += vmlinux.lds # 生成链接脚本但不链接进内核
方式 2:lib-y(归档到库但按需链接)
lib-y += hello.o
效果:
- 编译
moudle.c→hello.o - 链接到
lib.a静态库中 - 只有被其他代码引用时才会最终链接进内核
- 如果没有符号被引用,则不会进入最终镜像
用途:
- 通用库函数(如
lib/string.o) - 按需链接,减小内核体积
实际示例:
# lib/Makefile
lib-y += crc32.o # 只有当代码调用 crc32() 时才链接进内核
方式 3:自定义目标(编译为独立产物)
# 自定义编译规则
my-tool: my-tool.o
$(CC) -o my-tool my-tool.o
my-tool.o: my-tool.c
$(CC) -c my-tool.c -o my-tool.o
# 不加入 obj-y,所以不会链接进内核
效果:
- 编译生成
my-tool.o和可执行文件my-tool - 但不参与内核链接
- 常用于构建内核辅助工具
编译配置对比总结
| 配置方式 | 是否编译 | 是否链接进内核 | 用途场景 |
|---|---|---|---|
obj-y | ✅ | ✅ | 普通内核代码,编译并链接 |
obj-m | ✅ | ❌(生成 .ko) | 可加载模块 |
obj-n | ❌ | ❌ | 完全不编译 |
extra-y | ✅ | ❌ | 编译但不链接,生成中间文件 |
lib-y | ✅ | ⚠️(按需) | 编译到库,只有被引用才链接 |
| 自定义目标 | ✅ | ❌ | 编译为独立工具或测试程序 |
**最常见的”编译但不链接”场景是使用 extra-y**,用于生成构建过程需要但不应该链接进最终内核的中间产物。
方法三:通过 menuconfig 图形界面配置
启动 menuconfig
cd /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
make menuconfig
操作步骤
- 按
/键,输入MY_HELLO,搜索配置项位置 - 记住路径:
Location: -> Device Drivers -> Character devices -> My Driver - 用方向键导航到该位置
- 按
Y键 — 编译进内核([*]) - 或按
M键 — 编译为模块(<M>) - 保存退出:
Esc Esc→Yes
验证配置
grep "CONFIG_MY_HELLO" .config
# 输出应为:CONFIG_MY_HELLO=y 或 CONFIG_MY_HELLO=m
方法四:直接修改 .config 文件
添加配置项
# 编辑 .config
vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/.config
在文件末尾添加:
CONFIG_MY_HELLO=y
用 sed 命令行修改
# 编译进内核 (=y)
sed -i '/# CONFIG_MY_HELLO is not set/s/^# //' .config
sed -i 's/CONFIG_MY_HELLO=m/CONFIG_MY_HELLO=y/' .config
# 编译为模块 (=m)
sed -i 's/CONFIG_MY_HELLO=y/CONFIG_MY_HELLO=m/' .config
# 禁用 (=n)
sed -i '/CONFIG_MY_HELLO/d' .config
追加新配置
# 如果 .config 中没有 CONFIG_MY_HELLO
echo "CONFIG_MY_HELLO=y" >> .config
# 更新依赖
make oldconfig
方法五:通过 defconfig 预置配置
从现有 defconfig 修改
cd /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
# 基于 IMX6ULL 默认配置
make imx_v7_defconfig
# 添加自定义驱动配置
echo "CONFIG_MY_HELLO=y" >> .config
# 更新配置(解析依赖)
make oldconfig
# 编译
make -j4
导出当前配置为 defconfig
# 把当前 .config 导出为 defconfig(精简格式,只保留非默认选项)
make savedefconfig
# 生成的文件
mv defconfig arch/arm/configs/imx_v7_mydefconfig
方法六:通过 Kbuild 条件编译(不修改 Kconfig)
有时候只需要根据现有配置决定是否编译,不需要新增配置项。
方式 A:依赖已有配置项
# 假设 IMX6ULL 已经有 CONFIG_SOC_IMX6ULL
obj-$(CONFIG_SOC_IMX6ULL) += hello.o
方式 B:根据架构编译
obj-$(CONFIG_ARCH_MXC) += hello.o
方式 C:使用 ifdef 条件编译
ifdef CONFIG_MY_HELLO
obj-y += hello.o
endif
方式 D:根据模块参数
obj-$(CONFIG_HELLO_ENABLE) += hello.o
方法七:通过 modprobe 自动加载
如果驱动是 .ko 模块,可以让内核自动加载。
在 /etc/modules 中注册
# 开发板上执行
echo "moudle" >> /etc/modules
创建模块依赖配置
# 更新模块依赖
depmod -a
# 查看依赖关系
cat /lib/modules/4.1.15/modules.dep | grep moudle
systemd 自动加载
# 创建 systemd 服务
vi /etc/systemd/system/hello.service
[Unit]
Description=Hello Driver
After=basic.target
[Service]
Type=oneshot
ExecStart=/sbin/insmod /path/to/moudle.ko
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
systemctl enable hello
systemctl start hello
方法八:通过内核引导参数加载
在 U-Boot 或内核启动参数中指定模块路径:
# U-Boot 中设置
setenv bootargs "root=/dev/nfs nfsroot=... modules=module_name"
或在开发板启动后:
# 自动从指定目录加载所有模块
modprobe -a moudle
总结对比
| 方法 | 配置方式 | 编译位置 | 是否修改内核源码 | 适用场景 |
|---|---|---|---|---|
| 外部模块 | 外部 Makefile | 外部目录 | 不修改 | 开发调试 |
| 内置模块 | Kconfig + Makefile | 内核源码内 | 修改 | 正式集成 |
| menuconfig | 图形界面 | – | 不修改源码 | 快速配置 |
| 直接改 .config | 文本编辑器 | – | 不修改源码 | 快速测试 |
| defconfig | 预置配置 | – | 不修改源码 | 批量部署 |
| 条件编译 | 已有配置项 | 内核 | 不修改源码 | 复用配置 |
| modprobe 自动 | /etc/modules | 外部目录 | 不修改 | 自动加载 |
开发阶段推荐流程
开发阶段:外部模块(方法一)
↓ 代码稳定后
集成阶段:内置模块(方法二)+ menuconfig(方法三)
↓ 配置确定后
发布阶段:defconfig(方法五)固化配置
补充:驱动 Makefile 中的 CONFIG 判断
在 Makefile 中,CONFIG_XXX 会被替换为实际的值(y/m/n):
# 基础写法
obj-$(CONFIG_MY_HELLO) += hello.o
# 多条件判断
obj-$(CONFIG_MY_HELLO) += hello.o
obj-$(CONFIG_MY_HELLO_DEBUG) += hello_debug.o
# 子目录递归
obj-$(CONFIG_MY_HELLO) += subdir/
# 子目录的 Makefile
obj-$(CONFIG_MY_HELLO) += hello_core.o
obj-$(CONFIG_MY_HELLO) += hello_io.o
展开说明:
CONFIG_MY_HELLO=y→obj-y += hello.o→ 编译进内核CONFIG_MY_HELLO=m→obj-m += hello.o→ 编译为模块CONFIG_MY_HELLO=n→ 不生成任何编译指令 → 不编译
补充:驱动代码中的条件编译
#include <linux/module.h>
#include <linux/kernel.h>
#ifdef CONFIG_MY_HELLO_VERBOSE
#define DPRINTK(fmt, args...) printk(KERN_DEBUG "[HELLO] " fmt, ##args)
#else
#define DPRINTK(fmt, args...) do {} while(0)
#endif
#ifdef CONFIG_MY_HELLO
static int __init hello_init(void)
{
printk(KERN_INFO "Hello driver initialized\n");
DPRINTK("Verbose: module init\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello driver removed\n");
}
module_init(hello_init);
module_exit(hello_exit);
#endif
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ErHu");
MODULE_DESCRIPTION("Hello driver for IMX6ULL");