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() 释放资源。这一模型让驱动天然支持多实例,也让设备树成为平台设备的天然来源。

嵌入式Linux驱动协作关系 用户空间 open/read/write/ioctl 字符设备接口 cdev + file_operations 平台驱动 probe / remove 设备树 .dts/.dtb compatible / reg / irq gpios / clocks of_match_table 匹配 内核模块 .ko insmod / modprobe module_init/exit 注册 SoC 硬件资源 GPIO 控制器 · UART · 中断控制器 GIC · 时钟 · 寄存器基址 ioremap / request_irq 描述 Buildroot 集成 内核配置 M .ko 安装到 /lib/modules

五、四者协作的完整链路与常见误区

把上述机制串起来,一次典型的板级驱动加载过程是: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 中集成具体板级驱动时,就能清楚判断问题出在“描述层”“匹配层”还是“访问层”,从而快速定位并解决。