相信很多从事嵌入式或驱动开发的工程师,尤其是新手,都会产生这样一个疑问:当我们的 Linux 系统(例如一块开发板)启动时,板载的 LED 驱动、按键驱动甚至是复杂的触屏驱动,为什么都能自动加载起来,而无需我们手动执行 insmod driver.ko 命令?
更有趣的是,内核是如何知道这些硬件存在的?它怎么找到对应的驱动?又是如何确保驱动加载后立刻去驱动那个硬件呢?
这篇文章将以 Linux 6.1 + Orange Pi 5 Plus(RK3588) 为具体的案例,逐步揭开这层迷雾,带大家理解 Linux 驱动自动加载的完整机制。
1. 核心概念与工作流程
1.1 三个关键角色
在驱动自动加载的“故事”里,有三个主要角色,它们共同协作完成了这场“自动化”表演:
1. 设备树(Device Tree)
- 一个
.dts 文件编译成的 .dtb 二进制文件。
- 它的核心作用,是向内核描述 Orange Pi 5 Plus 这块板子上有哪些硬件设备、它们连接在哪个总线上、以及各自使用什么资源(如 GPIO 引脚、中断号、时钟等)。
- 简单说,它负责告诉内核:“看,
GPIO3_A6 这个引脚上接了一个蓝色 LED”,“GPIO3_B1 上接了一个绿色 LED”。
2. 驱动程序(Driver)
- 一个内核模块或内置驱动。
- 它包含了操作硬件的具体代码逻辑。
- 更关键的是,它明确声明了自己能驱动什么样的硬件,这是通过
compatible 字段实现的。
- 例如,GPIO LED 驱动会声明:“我能驱动所有
compatible 属性为 gpio-leds 的硬件”。
3. 内核中的设备驱动框架(Bus、Device、Driver)
- 这是扮演“中间人”或“红娘”的角色。
- 它的职责包括扫描硬件、加载驱动、以及最终将合适的“设备”与“驱动”进行配对。
- 你可以把它想象成一个“婚配服务器”,确保正确的设备找到了正确的驱动。
1.2 自动加载的完整流程(以 Orange Pi 5 Plus 为例)
让我们勾勒一个 Orange Pi 5 Plus 启动时的真实场景:
【系统启动】
↓
【设备树被加载并解析】→ 内核扫描 rk3588-orangepi-5-plus.dtb
↓
├─ Device 1: gpio-leds, compatible = “gpio-leds”
├─ Device 2: pwm-fan, compatible = “pwm-fan”
├─ Device 3: sfc (SPI Flash), compatible = “jedec,spi-nor”
├─ Device 4: remotectl, compatible = “rockchip,remotectl-pwm”
└─ Device 5: GPIO3, compatible = “rockchip,rk3588-gpio”
↓
【内核驱动模块被加载】→ 驱动注册自己到内核
↓
├─ Driver A: compatible = “gpio-leds”
├─ Driver B: compatible = “pwm-fan”
├─ Driver C: compatible = “jedec,spi-nor”
├─ Driver D: compatible = “rockchip,remotectl-pwm”
└─ Driver E: compatible = “rockchip,rk3588-gpio”
↓
【内核进行配对】→ 匹配 compatible 字符串
↓
├─ gpio-leds device + gpio-leds driver ✓ 匹配!→ 调用 probe()
├─ pwm-fan device + pwm-fan driver ✓ 匹配!→ 调用 probe()
├─ spi-nor device + jedec-spi-nor driver ✓ 匹配!→ 调用 probe()
└─ … 其他设备
↓
【驱动 probe() 执行】→ 驱动被激活,硬件被初始化
↓
【驱动开始工作】
1.3 这个过程的关键词
整个过程的核心,其实是一场基于字符串的精确匹配!
设备树里的硬件节点说:“我的 compatible 是 gpio-leds”。
驱动程序在注册时说:“我的 of_match_table 里包含 gpio-leds”。
内核的设备驱动框架这个“红娘”一看,字符串完全一致,便拍板决定:“你俩配对成功,开始工作吧!”。
这就是 Linux 驱动框架 的核心设计理念之一:约定优于配置,通过标准的属性字符串来解耦硬件描述与驱动实现。
2. 设备树分析
2.1 设备树顶层结构
我们从 Orange Pi 5 Plus 官方的设备树文件 rk3588-orangepi-5-plus.dts 看起:
// arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dts
/dts-v1/;
#include “rk3588-orangepi-5-plus.dtsi”
#include “rk3588-linux.dtsi”
#include “rk3588-orangepi-5-plus-lcd.dtsi”
#include “rk3588-orangepi-5-plus-camera1.dtsi”
/ {
model = “RK3588 OPi 5 Plus”;
compatible = “rockchip,rk3588-orangepi-5-plus”, “rockchip,rk3588”; // ← 关键!
};
关键点解析:
- 顶级 compatible 字符串:
compatible = “rockchip,rk3588-orangepi-5-plus”, “rockchip,rk3588”;
这行信息向内核传达了两层意思(优先级从左到右递减):
- 首选:这是一块 Rockchip 公司的 rk3588-orangepi-5-plus 特定开发板。
- 备选:如果没有找到上面那个最匹配的专用驱动,那就把它当作一个通用的 RK3588 芯片来处理。
- 包含关系:
rk3588-orangepi-5-plus.dtsi – 定义了 Orange Pi 5 Plus 特有的硬件。
rk3588-linux.dtsi – 包含了 Linux 系统下 RK3588 芯片的通用配置。
rk3588-orangepi-5-plus-lcd.dtsi – 特定 LCD 显示屏的配置。
rk3588-orangepi-5-plus-camera1.dtsi – 摄像头的配置。
2.2 GPIO LED 设备定义
设备树中,LED 设备是这样定义的:
./arch/arm64/boot/dts/rockchip/rk3588-orangepi-5-plus.dts
leds: gpio-leds {
compatible = “gpio-leds”; // ← 关键:告诉内核这是 GPIO LED
pinctrl-names = “default”;
pinctrl-0 = <&leds_rgb>;
status = “okay”; // ← 启用这个设备
blue_led@1 {
gpios = <&gpio3 RK_PA6 GPIO_ACTIVE_HIGH>; // ← GPIO3 的第 6 号 pin (A 组)
label = “blue_led”;
linux,default-trigger = “heartbeat”; // ← LED 闪烁方式:心跳
linux,default-trigger-delay-ms = <0>;
};
green_led@2 {
gpios = <&gpio3 RK_PB1 GPIO_ACTIVE_HIGH>; // ← GPIO3 的第 9 号 pin (B 组)
label = “green_led”;
linux,default-trigger = “heartbeat”;
linux,default-trigger-delay-ms = <0>;
};
};

解析:
compatible = “gpio-leds”
- 这是 Linux 内核为 GPIO 连接的 LED 设备定义的标准兼容性字符串。
- 内核源码中已经有一个现成的驱动
drivers/leds/leds-gpio.c 就是用来匹配和驱动它的。
- GPIO 引脚编号
&gpio3 – 引用了 GPIO3 这个控制器。
RK_PA6 – 表示 A 组第 6 号引脚(编号从0开始)。
RK_PB1 – 表示 B 组第 1 号引脚。
- 换算成 Linux 内核的 GPIO 编号(公式:
组号*32 + 组内基址 + 引脚号):
- GPIO3_A6 = 3×32 + 0×8 + 6 = 102
- GPIO3_B1 = 3×32 + 1×8 + 1 = 105
- pinctrl 配置
pinctrl-0 = <&leds_rgb>;
这行告诉系统的 pinctrl(引脚控制)子系统,需要把 leds_rgb 这个引脚组配置为 GPIO 输出模式。
2.3 Pinctrl 配置
在设备树的 pinctrl 部分,我们可以找到 leds_rgb 的具体定义:
&pinctrl {
leds_gpio {
leds_rgb: leds-rgb {
rockchip,pins = <3 RK_PA6 RK_FUNC_GPIO &pcfg_pull_up>,
<3 RK_PB1 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
};
解析:
rockchip,pins – 指定要配置的物理引脚。
RK_FUNC_GPIO – 将这些引脚的功能设置为 GPIO(而不是其他复用功能如 I2C、UART等)。
&pcfg_pull_up – 配置为上拉模式。
2.4 PWM 风扇驱动
设备树中还定义了一个 PWM 风扇,这同样是驱动自动加载的绝佳例子:
fan: pwm-fan {
compatible = “pwm-fan”; // ← 标准 PWM 风扇驱动
#cooling-cells = <2>;
pwms = <&pwm3 0 50000 0>; // ← 使用 PWM3
cooling-levels = <0 50 100 150 200 255>; // ← 风扇速度等级
rockchip,temp-trips = <
50000 1 // 温度 50°C,风扇速度等级 1
55000 2 // 温度 55°C,风扇速度等级 2
60000 3 // 温度 60°C,风扇速度等级 3
65000 4 // 温度 65°C,风扇速度等级 4
70000 5 // 温度 70°C,风扇速度等级 5
>;
status = “okay”;
};
这个设备同样会被自动驱动:
- 内核解析设备树时,发现一个
compatible = “pwm-fan” 的设备。
- 在内核驱动中搜索能匹配
“pwm-fan” 的驱动。
- 找到
drivers/hwmon/pwm-fan.c 这个驱动。
- 调用该驱动的
probe() 函数。
- 风扇驱动被激活,开始根据温度监控并自动控制风扇转速。
2.5 红外遥控驱动
设备树中还有一个红外遥控的配置,它展示了一个芯片厂商特定的驱动:
&pwm15 {
compatible = “rockchip,remotectl-pwm”; // ← Rockchip 特定驱动
pinctrl-names = “default”;
pinctrl-0 = <&pwm15m1_pins>;
remote_pwm_id = <3>;
handle_cpu_id = <1>;
remote_support_psci = <0>;
status = “okay”;
ir_key1 {
rockchip,usercode = <0xfb04>; // ← 遥控器用户码
rockchip,key_table = <
0xa3 KEY_ENTER,
0xe4 388,
0xf5 KEY_BACK,
0xbb KEY_UP,
// … 更多按键映射
0xb2 KEY_POWER,
// … 等等
>;
};
};
这是一个 Rockchip 特定 的远程控制驱动(并非通用的 gpio-ir-receiver 驱动)。它的 compatible 字符串 “rockchip,remotectl-pwm” 会精确匹配 Rockchip 提供的内核驱动。
3. 驱动加载实现
3.1 GPIO LED 驱动源码(drivers/leds/leds-gpio.c)
让我们看看驱动是如何声明自己并响应匹配的。以下是简化版的 GPIO LED 驱动关键代码:
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/leds.h>
// 1. 驱动能驱动什么硬件 → of_match_table(设备树匹配表)
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = “gpio-leds” }, // ← 关键:声明能驱动 “gpio-leds”
{}
};
MODULE_DEVICE_TABLE(of, of_gpio_leds_match);
// 2. Probe 函数:匹配成功后被内核调用
static int gpio_leds_probe(struct platform_device *pdev) {
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node; // ← 获取对应的设备树节点
struct device_node *child;
int count = 0;
dev_info(dev, “GPIO LED driver probe called!\n”);
// 遍历设备树中此节点下的每个子节点(如 blue_led@1, green_led@2)
for_each_child_of_node(np, child) {
const char *label;
int ret;
// 读取 label 属性:”blue_led” 或 “green_led”
label = of_get_property(child, “label”, NULL);
// 获取 GPIO 引脚描述符(Linux 6.1 推荐做法)
struct gpio_desc *gpiod = fwnode_gpiod_get_index(
of_fwnode_handle(child), “gpios”, 0, GPIOD_OUT_LOW, label);
if (IS_ERR(gpiod)) {
dev_err(dev, “failed to get GPIO for %s\n”, label);
continue;
}
dev_info(dev, “Registered LED %s\n”, label);
count++;
}
dev_info(dev, “GPIO LED driver probed with %d LEDs\n”, count);
return 0;
}
// 3. Remove 函数
static int gpio_leds_remove(struct platform_device *pdev) {
dev_info(&pdev->dev, “GPIO LED driver removed\n”);
return 0;
}
// 4. 驱动结构体:注册驱动到内核
static struct platform_driver gpio_led_driver = {
.probe = gpio_leds_probe,
.remove = gpio_leds_remove,
.driver = {
.name = “leds-gpio”,
.of_match_table = of_gpio_leds_match, // ← 设备树匹配表,这是核心!
.owner = THIS_MODULE,
}
};
// 5. 驱动注册宏
module_platform_driver(gpio_led_driver);
MODULE_AUTHOR(“Kernel LED Authors”);
MODULE_DESCRIPTION(“GPIO LED driver for Linux”);
MODULE_LICENSE(“GPL”);
3.2 系统启动时的实际情况
当 Orange Pi 5 Plus 启动 Linux 6.1 时,幕后发生了这些事情:
第 1 步:U-Boot 加载设备树
U-Boot 从 eMMC/SPI Flash 启动
↓
加载 Linux 内核映像
↓
加载 rk3588-orangepi-5-plus.dtb 设备树文件
↓
将 .dtb 文件在内存中的地址传递给 Linux 内核
第 2 步:Linux 内核解析设备树
Linux 内核初始化
↓
设备树解析器读取 .dtb 文件内容
↓
在内存中创建设备树节点对象
↓
扫描所有节点,为那些拥有 `compatible` 属性的节点创建对应的 platform_device
├─ gpio-leds 节点 → 创建 platform_device(“leds”)
├─ pwm-fan 节点 → 创建 platform_device(“fan”)
├─ pwm15 节点 → 创建 platform_device(“remotectl”)
└─ … 其他设备
第 3 步:驱动注册和配对
leds-gpio.ko 驱动模块初始化(或被编译进内核)
↓
驱动调用 platform_driver_register(&gpio_led_driver)
↓
驱动的 of_match_table 告诉内核框架:“我能驱动 compatible=’gpio-leds’ 的设备”
↓
内核框架立即搜索当前已注册的设备中是否有 compatible 匹配的
↓
找到了!platform_device(“leds”) 的 compatible = “gpio-leds”
↓
配对成功!✓
↓
内核框架调用驱动的 probe() 函数:
├─ gpio_leds_probe(&platform_device(“leds”))
├─ 遍历子节点:blue_led@1 和 green_led@2
├─ 为每个 LED 获取 GPIO 引脚,并配置为输出
├─ 将 LED 注册到内核的 LED 子系统
└─ 在 /sys/class/leds/ 下创建 blue_led 和 green_led 接口
第 4 步:系统启动完毕
所有驱动加载、配对、初始化完毕
↓
蓝色 LED 和绿色 LED 开始按照设备树中定义的 “heartbeat” 方式自动闪烁
↓
用户现在可以通过 sysfs 文件系统控制 LED:
$ echo 1 > /sys/class/leds/blue_led/brightness # 点亮
$ echo 0 > /sys/class/leds/green_led/brightness # 熄灭
3.3 内核中的配对逻辑(drivers/of/device.c)
匹配的核心逻辑在内核中,简化后的代码如下:
// 这是内核如何进行 compatible 匹配的简化代码
const struct of_device_id *of_match_device(
const struct of_device_id *matches,
struct device *dev) {
// 如果设备没有关联的设备树节点,则无法进行OF匹配
if (!dev->of_node)
return NULL;
// 获取设备树节点中的 compatible 属性字符串
const char *compatible = of_get_property(dev->of_node,
“compatible”, NULL);
if (!compatible)
return NULL;
// 逐一检查驱动提供的 of_match_table 数组
while (matches->name[0] || matches->compatible[0]) {
// 比较设备与驱动的 compatible 字符串
if (!of_compat_cmp(compatible, matches->compatible,
strlen(matches->compatible))) {
return matches; // ✓ 匹配成功!
}
matches++;
}
return NULL; // ✗ 没有驱动能驱动这个设备
}
核心就是这一行比较:
if (!of_compat_cmp(compatible, matches->compatible, …))
字符串比较!当设备树节点中的 compatible 属性字符串,与驱动 of_match_table 中声明的某个字符串完全相同时,内核就判定匹配成功,并启动后续的驱动加载流程。这整个机制深刻体现了 操作系统 对硬件资源管理和抽象的统一思想。
4. 实战验证
4.1 验证 LED 驱动是否自动加载
在你的 Orange Pi 5 Plus 上,可以通过以下命令验证整个过程:
# 1. 查看 LED 设备是否被成功识别并创建了用户空间接口
$ ls /sys/class/leds/
blue_led green_led
# 2. 查看某个 LED 的当前亮度
$ cat /sys/class/leds/blue_led/brightness
0 # 当前亮度值(范围通常是 0-255)
# 3. 查看 LED 当前的触发模式(trigger),方括号[]内是当前激活的模式
$ cat /sys/class/leds/blue_led/trigger
none [heartbeat] timer oneshot transient none
# 4. 查看已加载的内核模块,确认 leds_gpio 驱动已加载
$ lsmod | grep leds
leds_gpio 8192 2
# 5. 查看内核启动日志,寻找驱动初始化的证据
$ dmesg | grep -i “gpio.*led\|leds.*gpio”
[ 0.234567] leds-gpio: GPIO LED driver probe called!
[ 0.234890] leds-gpio: Registered LED blue_led
[ 0.235123] leds-gpio: Registered LED green_led
[ 0.235234] leds-gpio: GPIO LED driver probed with 2 LEDs
# 6. 直接从内核映射的设备树信息中查看原始数据
$ cat /proc/device-tree/leds/compatible
gpio-leds
$ cat /proc/device-tree/leds/blue_led@1/label
blue_led
# 7. 手动控制 LED(让蓝色 LED 常亮)
$ echo 1 > /sys/class/leds/blue_led/brightness
# 8. 关闭 LED
$ echo 0 > /sys/class/leds/blue_led/brightness
4.2 验证风扇驱动
同样,可以验证 PWM 风扇驱动:
# 查看系统中的硬件监控(hwmon)设备
$ ls /sys/class/hwmon/
hwmon0 hwmon1 …
# 找出哪个是 pwm-fan
$ cat /sys/class/hwmon/hwmon*/name
pwm-fan # 找到它了,假设是 hwmon0
# 查看当前的 PWM 风扇速度(通常对应 pwm1)
$ cat /sys/class/hwmon/hwmon0/pwm1
# 输出:一个 0-255 之间的值,代表风扇速度等级
# 查看当前 SoC 温度(单位:毫摄氏度)
$ cat /sys/class/hwmon/hwmon0/temp1_input
45000 # 表示 45.0°C
# 在内核日志中搜索风扇初始化信息
$ dmesg | grep -i pwm-fan
[ 0.456789] pwm-fan: PWM fan initialized
[ 0.457012] pwm-fan: Cooling device registered
5. 常见问题与调试
问题 1:LED 亮了,但闪烁方式不是我想要的
可能的原因:
- 设备树中
default-trigger 属性设置错误或不符合预期。
- 内核中对应的 trigger 驱动(如
leds-trigger-heartbeat)没有被编译或加载。
调试步骤:
# 查看该 LED 所有可用的触发模式
$ cat /sys/class/leds/blue_led/trigger
none [heartbeat] timer oneshot transient none
# 手动切换到 ‘timer’ 定时闪烁模式
$ echo timer > /sys/class/leds/blue_led/trigger
# 设置闪烁参数:亮1秒,灭1秒
$ echo 1000 > /sys/class/leds/blue_led/delay_on
$ echo 1000 > /sys/class/leds/blue_led/delay_off
问题 2:驱动加载了,sysfs 接口也看到了,但 LED 就是不亮
可能的原因:
- GPIO 引脚号在设备树中配置错误。
- pinctrl 配置错误,引脚功能未设置为 GPIO 输出。
- 电平极性搞反(比如设备树中是
GPIO_ACTIVE_HIGH,但实际电路是低电平点亮)。
- 硬件问题(LED损坏、限流电阻过大、接线错误等)。
调试步骤:
# 1. 使用 gpio-tools 查看 GPIO 状态
$ sudo apt install gpiod
$ gpioinfo | grep -A2 “gpio3\|PA6\|PB1” # 查看相关GPIO的状态
# 2. 绕过驱动,直接通过 sysfs GPIO 接口手动测试(需确认GPIO未被占用)
$ echo 102 > /sys/class/gpio/export # 导出 GPIO3_A6 (编号102)
$ echo out > /sys/class/gpio/gpio102/direction
$ echo 1 > /sys/class/gpio/gpio102/value # 设置高电平,LED 应该亮
$ echo 0 > /sys/class/gpio/gpio102/value # 设置低电平,LED 应该灭
# 3. 查看 pinctrl 子系统的调试信息,确认引脚复用配置
$ cat /sys/kernel/debug/pinctrl/pinctrl-rockchip/pingroups | grep leds
问题 3:为什么驱动加载了,但它的 probe() 函数没被调用?
最常见的原因:compatible 字符串不匹配。
# 检查设备树实际传递给内核的 compatible 属性
$ cat /proc/device-tree/leds/compatible
gpio-leds
# 查看内核日志,搜索驱动注册和设备匹配相关信息
$ dmesg | grep “compatible”
# 如果设备树中误写为 “gpio-led” (少了一个s)
# 而驱动中声明的是 “gpio-leds”
# 那么字符串比较就会失败,配对不会发生,probe() 自然不会被调用。
问题 4:如何在不修改驱动代码的情况下,自定义 LED 的行为?
直接修改设备树源文件(.dts)并重新编译即可。例如,改变 LED 的默认触发模式:
blue_led@1 {
gpios = <&gpio3 RK_PA6 GPIO_ACTIVE_HIGH>;
label = “blue_led”;
// 改为上电常亮模式
linux,default-trigger = “default-on”;
// 或者改为定时器闪烁,并设置延迟参数
// linux,default-trigger = “timer”;
// linux,default-trigger-delay-ms = <1000>; // 可配合timer使用,但非标准属性,依赖驱动支持
};
总结
Linux 驱动自动加载的机制,其核心思想清晰而优雅:
- 设备树定义硬件:通过
compatible 属性给硬件贴上“身份证”。
- 驱动声明能力:通过
of_match_table 声明自己能驱动哪类“身份证”的硬件。
- 内核框架进行配对:当“身份证” (
compatible) 与“能力声明” (of_match_table) 字符串完全匹配时,自动调用驱动的 probe() 函数完成初始化。
整个过程实现了硬件描述与驱动代码的解耦,使得同一份驱动可以用于不同板卡上的相同硬件,而板卡差异仅通过设备树来描述。这正是现代 Linux 内核 支持多样化嵌入式设备的关键机制之一。
希望这篇结合 Orange Pi 5 Plus 实例如深入代码的分析,能帮助你彻底理解 Linux 驱动自动加载的奥秘。如果你对更底层的代码实现感兴趣,可以顺着文中的代码路径,在内核源码中继续探索 platform_driver_register、of_platform_default_populate 等函数的完整调用链。

版权说明:本文内容基于 Linux 内核源码及 Orange Pi 官方设备树文件进行分析,旨在技术分享与学习。文中涉及的所有代码示例均来自开源项目。更多深入的嵌入式系统及驱动开发讨论,欢迎关注 云栈社区 的相关板块。