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_DEFINEDEVICE_DT_INST_DEFINE 宏。否则,设备树节点不会生成对应的设备实例。

3.4 完整流程回顾

咱们把整个流程串起来,看看一个驱动从代码到运行,到底经历了什么:

  1. 编译阶段: 设备树解析器读取 .dts 文件,为每个 status = "okay" 的节点生成 struct device 实例。
  2. 编译阶段: SYS_INIT 宏把初始化函数注册到 .z_init_* 段。
  3. 启动阶段: 内核按 PRE_KERNEL_1PRE_KERNEL_2POST_KERNELAPPLICATION 的顺序,依次调用所有注册的初始化函数。
  4. 初始化函数内: 你拿到 struct device *dev,配置硬件寄存器、注册中断、初始化状态机。
  5. 初始化完成后: 其他模块可以通过 device_get_binding("UART_0") 获取设备句柄,调用API。

嗯,流程其实不复杂。但有几个坑,我当年都踩过:

  • 坑1: 初始化函数里不要调用 device_get_binding 去获取其他设备。因为那个设备可能还没初始化完。要用 DEVICE_DT_GET 在编译时获取引用。
  • 坑2: 优先级设得太高(比如 PRE_KERNEL_1 的0),可能会覆盖内核自己的初始化。我建议普通驱动用 POST_KERNEL + 默认优先级。
  • 坑3: 如果你的驱动依赖某个时钟或电源域,记得在设备树里配好 clockspower-domains 属性。否则初始化时硬件可能没上电。

一句话总结: SYS_INIT 管“什么时候跑”,设备树管“跑哪个设备”,优先级管“谁先跑”。三者配合,才能让驱动在正确的时间、以正确的顺序、操作正确的硬件。

好了,这一章的内容就到这儿。下一章咱们会深入 DEVICE_DT_DEFINEDEVICE_DT_INST_DEFINE 这两个宏,看看它们是怎么把设备树节点和驱动代码绑定在一起的。到时候我会分享一个我调试I2C驱动时遇到的奇葩问题——跟设备树节点命名有关,挺有意思的。