## 问题
一块新板子,按文档把 UART 调通、LED 点亮,接着加触摸屏驱动。代码检查了两天,寄存器配置、I2C 时序、中断号全对。但系统一启动就卡死,串口连一行日志都不输出。
驱动本身没有错。错的是它被放进了 `board_late_initialize()`。
## 分析
openvela 的启动流程不是“依次初始化所有驱动”这么简单。它从 `nx_start` 开始,在不同的任务上下文中分阶段调用板级函数,每个阶段有明确的资源可用性边界:
| 阶段 | 函数 | 执行上下文 | 关键约束 |
|---|---|---|---|
| 1 | `nx_start` | Idle task | OS 入口 |
| 2 | `board_early_initialize()` | Idle task | **禁止**任何阻塞调用(如 `sem_wait`) |
| 3 | `board_late_initialize()` | AppBringup task | OS 核心就绪,可调用含事件等待的函数 |
| 4 | `board_app_initialize()` | Nsh task | **文件系统尚未挂载** |
| 5 | `rc.sysinit` | Nsh task | 挂载文件系统 |
| 6 | `board_app_finalinitialize()` | Nsh task | 适合依赖文件系统的驱动 |
| 7 | `rcS` | Nsh task | 启动用户空间服务 |
触摸屏驱动本身没有错。错在它的初始化代码里有一句读取配置文件的调用,而 `board_late_initialize()` 执行时,文件系统还没有被 `rc.sysinit` 挂载。驱动在等一个永远不会来的返回值,系统就停在那里——串口没有输出,是因为卡死发生在控制台完全就绪之前。
这不是个例。`board_early_initialize()` 运行在 Idle task 中,内核抢占能力不完整,任何可能导致阻塞的调用都会让启动早期停滞。而 `board_app_initialize()` 最容易被误用,官方文档明确标注此阶段文件系统不可用。这两个位置的错误有一个共同特征:**现象不是“驱动初始化失败”,而是系统在别处挂死**,所以排查方向很容易跑偏。
## 解释
openvela 这样设计启动时序,不是故意增加复杂度,而是由资源依赖关系决定的。
文件系统需要先被挂载才能访问,挂载动作发生在 `rc.sysinit`。中断和调度器需要先初始化,才能支持带事件等待的驱动。这些依赖构成了一个偏序关系:某些资源必须在某些阶段之后才可用。板级函数的排列顺序,就是这个偏序关系的外在表现。
官方文档其实已经把约束写清楚了。依赖文件系统的驱动应放到 `board_app_finalinitialize()`,该函数由 `boardctl(BOARDIOC_FINALINIT)` 调用,文档注明它“用于初始化依赖文件系统访问的驱动(例如 TP、Charger、Audio PA 等),可直接操作文件,无需通过延迟工作机制推后执行”。框架已经准备好了正确的时机,问题在于开发者是否把驱动放到了那个时机里。
还有一个容易忽略的细节:`stm32_bringup()` 的调用者取决于 `CONFIG_BOARD_LATE_INITIALIZE` 配置项。打开时由 `board_late_initialize()` 调用,关闭时由 `board_app_initialize()` 调用。板级代码如果硬编码了某一条路径,换一份 `defconfig` 就可能出问题。两套路径都必须编译通过。
## 解决
拿到一块新板子,按下面的顺序推进:
**第一步,确认芯片的适配层级。** 查 NuttX 上游 `arch/` 下有没有该芯片的芯片层支持。有,只做板级层;没有但架构存在,工作量上一个数量级;架构也不存在,先准备从零 Bringup 一个 CPU。STM32F407 属于第一种,RK3576 属于第二种,龙芯 2K0300 属于第三种。
**第二步,按依赖关系给每个驱动定位置。** 只依赖寄存器的,放 `board_early_initialize()`;依赖 OS 调度或信号量的,放 `board_late_initialize()` 或 `bringup`;依赖文件系统的,放 `board_app_finalinitialize()`。判断标准只有一条:**第一个所有依赖都满足的阶段。**
**第三步,先让 `nsh>` 跑起来,再逐个加驱动。** 一个能进 shell 的最小 BSP,比十个写好了但没验证的驱动更有价值。RK3576 的移植记录也印证了这一点:先稳定启动到 `nsh>`,再逐项叠加 SMP、网络、显示和 AI 能力。
**第四步,用 `defconfig` 管理功能形态。** ESP32-S3-EYE 的 `configs/` 下按功能拆成多份配置,`gpio/`、`nsh/`、`lcd/`、`wifi/` 各自独立。功能是配置出来的,不是用 `#ifdef` 在 C 代码里堆出来的。板级代码保持通用,换配置不引入回归。
移植 openvela,写驱动本身有手册和上游代码可参照,不算最难的部分。真正拉开差距的是对启动时序的判断:**这个驱动在什么上下文执行,它等的资源什么时候就位,放错了会以什么方式失败。** 把这件事想清楚,从 `__start` 到 `nsh>` 的距离,会比反复调试短得多。