3. 嵌入式Linux驱动基础
在 Buildroot 构建出的嵌入式 Linux 系统中,驱动并不是“写进内核源码就完事”的孤立代码,而是由内核模块机制、设备树描述、字符设备接口以及平台设备与驱动模型共同协作完成的一次“软硬件握手”。理解这四者的分工与衔接,是后续把板级驱动正确集成进 Buildroot 的前提。
一、内核模块:驱动的可插拔载体
Linux 内核支持将部分功能编译为可动态加载的模块(.ko),运行时通过 insmod、modprobe 加载,通过 rmmod 卸载。对于板级驱动,模块化能显著缩短调试周期:修改驱动后无需重编整颗内核,只需替换 .ko 并重新加载。
# 查看已加载模块
lsmod
# 加载模块并传递参数
insmod my_led.ko gpio_pin=23
# 查看模块信息与参数
modinfo my_led.ko
# 卸载模块
rmmod my_led
模块的入口与出口由 module_init() 和 module_exit() 指定,许可证、作者、参数分别用 MODULE_LICENSE、MODULE_AUTHOR、module_param 声明。Buildroot 中通常通过内核配置把驱动设为 M,再由 package/ 或 rootfs overlay 把 .ko 安装到 /lib/modules/<版本>/。
二、设备树:把“板级差异”从代码中剥离
设备树(Device Tree)用 .dts 文本描述硬件拓扑,编译为 .dtb 后由 bootloader 传给内核。它解决的核心问题是:同一份驱动代码要适配不同板卡时,不再靠大量 #ifdef,而是靠不同的设备树节点。
/* 典型节点:LED 挂在 GPIO1_A2 */
leds {
compatible = "myboard,led";
status = "okay";
led-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>;
};
/* 平台设备节点 */
my_uart: serial@ff1c0000 {
compatible = "myboard,uart";
reg = <0xff1c0000 0x1000>;
interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_uart>;
status = "okay";
};
驱动通过 of_match_table 中的 compatible 字符串与节点匹配。若设备树写错,最常见现象是驱动加载成功但 probe 从未执行,此时应检查 /proc/device-tree/ 下节点是否存在、status 是否为 okay。
三、字符设备:用户空间访问驱动的标准入口
字符设备以字节流方式访问,典型流程是:申请设备号 → 初始化 cdev → 实现 file_operations → 注册到内核。用户空间通过 /dev/ 下的设备节点,用 open/read/write/ioctl 与驱动交互。
static int my_open(struct inode *inode, struct file *filp) { return 0; }
static ssize_t my_read(struct file *f, char __user *buf,
size_t len, loff_t *off) { return 0; }
static long my_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case LED_ON: gpio_set_value(pin, 1); break;
case LED_OFF: gpio_set_value(pin, 0); break;
default: return -EINVAL;
}
return 0;
}
static const struct file_operations my_fops = {
.owner = THIS_MODULE, .open = my_open,
.read = my_read, .unlocked_ioctl = my_ioctl,
};
设备号由主设备号与次设备号组成,可静态指定,也可用 alloc_chrdev_region() 动态申请。Buildroot 中常用 mdev 或 udev 依据 /sys/class/ 自动创建设备节点,因此驱动中应调用 class_create() 与 device_create()。
四、平台设备与驱动模型:总线—设备—驱动的统一抽象
平台设备(platform device)用于描述那些不挂在可枚举总线(如 PCI、USB)上的片上外设。Linux 设备模型把它们抽象为“总线—设备—驱动”三元组:设备提供资源(寄存器地址、中断号、时钟),驱动提供操作方法,总线负责匹配与绑定。
匹配成功后内核调用驱动的 probe(),在其中完成资源获取、字符设备注册、中断申请等;移除时调用 remove() 释放资源。这一模型让驱动天然支持多实例,也让设备树成为平台设备的天然来源。
五、四者协作的完整链路与常见误区
把上述机制串起来,一次典型的板级驱动加载过程是:Buildroot 编译内核与模块 → bootloader 加载 .dtb → 内核解析设备树生成平台设备 → 平台驱动通过 compatible 匹配并执行 probe → probe 中注册字符设备 → 用户空间通过 /dev 节点访问硬件。
| 机制 | 核心作用 | 关键接口/文件 | 典型排障点 |
|---|---|---|---|
| 内核模块 | 驱动可动态加载卸载 | module_init / insmod | 版本不匹配、依赖缺失 |
| 设备树 | 描述板级硬件差异 | .dts / compatible | status 非 okay、节点缺失 |
| 字符设备 | 提供用户空间访问入口 | cdev / file_operations | 设备号冲突、节点未创建 |
| 平台设备与驱动模型 | 统一匹配与资源管理 | platform_driver / probe | probe 未执行、资源获取失败 |
常见误区:很多初学者把驱动“加载成功”等同于“驱动工作正常”。实际上 insmod 成功只说明模块入口执行完毕,若设备树 compatible 写错,probe 根本不会运行,硬件也不会被初始化。排查时应优先查看 dmesg 中是否有 probe 相关日志,再确认 /sys/bus/platform/devices/ 与 /sys/bus/platform/drivers/ 下是否完成绑定。
实践建议:在 Buildroot 中集成板级驱动时,建议把驱动源码放在 package/ 下做成独立包,通过 KERNEL_MODULES 或自定义 .mk 安装 .ko;设备树修改放在 board/<厂商>/<板卡>/ 下,由 BR2_LINUX_KERNEL_CUSTOM_DTS_PATH 指定。这样驱动、设备树、根文件系统三者版本可控,便于复现与回滚。
掌握内核模块、设备树、字符设备与平台设备驱动模型这四块基石后,后续在 Buildroot 中集成具体板级驱动时,就能清楚判断问题出在“描述层”“匹配层”还是“访问层”,从而快速定位并解决。