3. 设备驱动初始化流程:SYS_INIT宏、设备初始化优先级、驱动注册与绑定
好,咱们接着聊驱动开发的另一个核心话题——初始化流程。
说实话,很多刚接触Zephyr的朋友,上来就被各种宏和优先级搞晕了。我当年第一次看 SYS_INIT 的时候,心里也在嘀咕:这玩意儿到底是怎么把驱动“变”出来的?
今天我就带你把它彻底捋清楚。
3.1 SYS_INIT宏:驱动初始化的入口
先看一个最典型的例子。你写了一个UART驱动,想在系统启动时自动初始化,代码大概长这样:
#include <zephyr/init.h>
static int my_uart_init(const struct device *dev)
{
// 配置寄存器、使能时钟、设置中断...
printk("My UART driver initialized!\n");
return 0;
}
SYS_INIT(my_uart_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
嗯,就这么几行。但背后发生的事情,比你想象的要复杂得多。
SYS_INIT 其实是一个宏,它会在编译阶段做两件事:
- 生成一个
init_entry结构体,里面存了你的初始化函数指针、设备指针、优先级等信息。 - 把这个结构体放到特定的内存段(
.z_init_*)里。
说白了,它就是在给内核“挂号”——告诉系统:嘿,我这儿有个驱动,你得在启动时帮我跑一下。
核心要点: SYS_INIT 不负责分配设备结构体,也不负责绑定设备。它只负责注册一个“初始化任务”。设备结构体的创建和绑定,是另一套机制(后面会讲)。
3.2 设备初始化优先级:谁先谁后?
你想想看,系统里那么多驱动,总得有个先后顺序吧?比如I2C控制器必须在I2C设备之前初始化,否则设备连总线都没有,怎么通信?
Zephyr 把初始化分成了几个阶段:
| 阶段 | 宏定义 | 典型用途 |
|---|---|---|
| 第一阶段 | PRE_KERNEL_1 |
CPU架构相关、中断控制器、定时器 |
| 第二阶段 | PRE_KERNEL_2 |
基础外设(GPIO、UART、时钟) |
| 第三阶段 | POST_KERNEL |
依赖内核服务的驱动(IPC、文件系统、网络) |
| 应用阶段 | APPLICATION |
用户自定义初始化 |
每个阶段内部,还可以通过优先级数字进一步排序。数字越小,越早执行。比如:
SYS_INIT(early_init, POST_KERNEL, 10);
SYS_INIT(late_init, POST_KERNEL, 50);
这里 early_init 会先跑,late_init 后跑。
我的经验: 优先级数字别乱用。我见过有人把驱动优先级设成0,结果跟内核内部初始化冲突,系统直接挂掉。建议用 CONFIG_xxx_INIT_PRIORITY_DEFAULT 这类宏,或者至少留出10~20的余量。
3.3 驱动注册与绑定:从代码到设备
好,现在初始化函数有了,优先级也定了。但还有一个关键问题:设备结构体(struct device)是谁创建的?
答案是:设备树(Device Tree)。
Zephyr 的驱动模型里,每个设备节点在编译时都会生成一个 struct device 实例。这个实例里包含了设备名、API函数指针、私有数据等。然后,SYS_INIT 里拿到的 const struct device *dev 参数,就是指向这个实例的指针。
举个例子,你在设备树里定义了一个UART:
&uart0 {
status = "okay";
current-speed = <115200>;
};
编译后,Zephyr 会自动生成类似这样的代码(简化版):
static const struct uart_driver_api my_uart_api = {
.poll_in = my_uart_poll_in,
.poll_out = my_uart_poll_out,
// ...
};
struct device DEVICE_NAME_GET(uart0) = {
.name = "UART_0",
.api = &my_uart_api,
.config = &uart0_config,
.data = &uart0_data,
};
然后 SYS_INIT 里调用的 my_uart_init 函数,就会收到这个 DEVICE_NAME_GET(uart0) 的指针。
注意: 驱动注册和绑定是自动完成的。你不需要手动调用任何注册函数。但前提是——你的驱动必须正确实现了 DEVICE_DT_DEFINE 或 DEVICE_DT_INST_DEFINE 宏。否则,设备树节点不会生成对应的设备实例。
3.4 完整流程回顾
咱们把整个流程串起来,看看一个驱动从代码到运行,到底经历了什么:
- 编译阶段: 设备树解析器读取
.dts文件,为每个status = "okay"的节点生成struct device实例。 - 编译阶段:
SYS_INIT宏把初始化函数注册到.z_init_*段。 - 启动阶段: 内核按
PRE_KERNEL_1→PRE_KERNEL_2→POST_KERNEL→APPLICATION的顺序,依次调用所有注册的初始化函数。 - 初始化函数内: 你拿到
struct device *dev,配置硬件寄存器、注册中断、初始化状态机。 - 初始化完成后: 其他模块可以通过
device_get_binding("UART_0")获取设备句柄,调用API。
嗯,流程其实不复杂。但有几个坑,我当年都踩过:
- 坑1: 初始化函数里不要调用
device_get_binding去获取其他设备。因为那个设备可能还没初始化完。要用DEVICE_DT_GET在编译时获取引用。 - 坑2: 优先级设得太高(比如
PRE_KERNEL_1的0),可能会覆盖内核自己的初始化。我建议普通驱动用POST_KERNEL+ 默认优先级。 - 坑3: 如果你的驱动依赖某个时钟或电源域,记得在设备树里配好
clocks和power-domains属性。否则初始化时硬件可能没上电。
一句话总结: SYS_INIT 管“什么时候跑”,设备树管“跑哪个设备”,优先级管“谁先跑”。三者配合,才能让驱动在正确的时间、以正确的顺序、操作正确的硬件。
好了,这一章的内容就到这儿。下一章咱们会深入 DEVICE_DT_DEFINE 和 DEVICE_DT_INST_DEFINE 这两个宏,看看它们是怎么把设备树节点和驱动代码绑定在一起的。到时候我会分享一个我调试I2C驱动时遇到的奇葩问题——跟设备树节点命名有关,挺有意思的。