Skip to content

rt-async 驱动架构完善

本文记录 2026-07-09 落地的 driver model 四项结构性改进。早先的 2026-07-09-驱动架构完善计划.md(status: plan)描述的是另一种设计(RxRing 抽取 + 3 步),实际实现与之有实质差异,该计划文件已删除,以本文为最终记录。

背景

driver model 上线后暴露两个结构性缺陷:

  1. SerialRx 硬绑定 NS16550Afutures::serial::SerialRx::poll() 直接调用 serial_ns16550a::rx_poll(),RX 环形缓冲区 + waker + poll 逻辑全部写在 ns16550a 驱动内部。K3 的 PXA-UART 无法复用异步串口接收。
  2. registry 包装层冗余 + 缺 chosen 派生 + 无多实例/总线platform 暴露了一组 set_console()/set_timer()/... 包装器供 driver 注入设备,但它们只是 Slot::set 的薄转发;console 由 ns16550a 在 probe 里自我任命(写死 CONSOLE.set),不读 chosen { stdout-path };同类型多设备(多串口、I2C/SPI 总线)没有枚举能力。

本轮在不动 executor / executor-macro / scheduler / app bin 的前提下,分 4 步修复。新增约 350 行、删除约 60 行。

Step 1:SerialRx 解耦

把中断驱动 RX 的 poll 原语从 ns16550a 驱动上提到 Serial trait,环形缓冲区与 waker 仍留在驱动内部(驱动负责自己的缓冲),futures 层只经 trait 调用一个统一的入口。

1a. Serial trait 新增 rx_register_waker(device.rs)

rust
pub trait Serial: Send + Sync {
    fn write(&self, buf: &[u8]);
    fn read(&self) -> Option<u8> { None }
    fn has_data(&self) -> bool { self.read().is_some() }
    /// 中断驱动 RX 的 poll 原语(关中断→注册 waker→重检→开中断)。
    fn rx_register_waker(&self, _cx: &mut core::task::Context<'_>) -> SerialRxStatus {
        SerialRxStatus::Unsupported
    }
}

pub enum SerialRxStatus {
    Ready(u8),        // 已就绪一个字节
    Pending,          // 已注册 waker,返回 Poll::Pending
    Unsupported,      // 不支持中断驱动 RX,调用方回退轮询
}

1b. ns16550a 实现 rx_register_waker

ns16550a 把原来 rx_poll 的 disable→register→recheck→enable 临界区模式搬进 trait 方法 override,内部仍用自己的静态环形缓冲。rx_handler/rx_push/rx_wake 不再对 futures 层可见。

1c. futures::serial::SerialRx 经 registry 路由

rust
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u8> {
    match platform::driver::console().rx_register_waker(cx) {
        SerialRxStatus::Ready(b) => Poll::Ready(b),
        SerialRxStatus::Pending | SerialRxStatus::Unsupported => Poll::Pending,
    }
}

SerialRx 不再引用 serial_ns16550a crate,只依赖 platform::Serial trait。

为什么 trait 方法不做 #[cfg] 门控? 关/开中断是 arch 专有逻辑,但 SerialRxStatusrx_register_waker 跨 crate(futures)传递——futures 没有 riscv64 feature,若用 cfg 门控 trait 方法,futures 在 host 编译时会看到不同的 trait 形状。故让 trait 方法始终存在、默认返回 Unsupported;arch 专有的关/开中断只在具体驱动(ns16550a)的 override 内部用 cfg 门控。host 桩依赖默认实现静默降级。

Step 2:Slot 公开 + DeviceRegistry

2a. Slot<T> 改为 pub(字段仍私有)。state: AtomicU8 + val: UnsafeCell<MaybeUninit<T>>set/get 对外可见。

2b. 新增 DeviceRegistry<T, N>

构建在 [Slot<T>; N] 数组之上,配一个线性探测游标 next: AtomicUsize

rust
pub struct DeviceRegistry<T, const N: usize> {
    slots: [Slot<T>; N],
    next: AtomicUsize,
}
impl<T, const N: usize> DeviceRegistry<T, N> {
    pub const fn new() -> Self;
    pub fn register(&self, dev: T) -> usize;  // 首个空槽填入,满 panic
    pub fn get(&self, idx: usize) -> Option<&T>;
    pub fn iter(&self) -> impl Iterator<Item = &T>;
}

register 从游标起线性探测首个空槽,返回索引;满则 panic(与 IRQ_TABLEheapless::Vec::push().unwrap() 同语义)。const 可构造、no_std/no-alloc。

2c. 删除 6 个 set_X 包装器

原来 platform 暴露的 set_console() / set_timer() / set_ipi() / set_reset() / set_intctl() / set_drivers() 全部删除。driver 的 probe 改为直写公开槽位:

rust
// ns16550a probe 内
crate::driver::SERIALS.register(&INSTANCE);
// 或单例角色:
crate::driver::TIMER.set(&INSTANCE);

板级 glue 经 DRIVERS.set(&[...]) 注入 driver 列表。

三类关注点分离

关注点原语实例
注册(init 期写入)Slot::set / DeviceRegistry::registerdriver probe
枚举(多实例遍历)DeviceRegistry::iterSERIALS / I2C_BUSES / SPI_BUSES
角色(单例选定)Slot::get + 便捷访问器CONSOLE / TIMER / IPI / RESET / INTC

为什么不用 once_cell once_cell::sync::OnceCell<T>get/setOption<T>,对 &'static dyn Trait 这类胖指针需要 AtomicPtr 承载,而胖指针(数据 + vtable,16 字节)放不进单个 usize。自建 Slot<T>MaybeUninit<T> 承载完整胖指针,AtomicU8 状态机配 Release/Acquire 序保证可见性。单 hart 串行 probe 场景下安全;多 hart 需保证仅一个 hart 调用 set

Step 3:chosen 派生 console

3a. 新增 SERIALS 多实例注册表

rust
pub static SERIALS: DeviceRegistry<&'static dyn Serial, 4> = DeviceRegistry::new();

ns16550a 的 probe 改为 SERIALS.register(&INSTANCE) 而非直接 CONSOLE.set——驱动不再自我任命 console 角色。

3b. derive_consolechosen { stdout-path } 派生

boot() 遍历完所有节点后调 derive_console(fdt),从 SERIALS 选出默认 console 填进 CONSOLE

rust
fn derive_console(fdt: &Fdt<'static>) {
    let has_chosen = fdt.chosen().and_then(|c| c.stdout()).is_some();
    let dev = SERIALS.iter().next().copied().unwrap_or_else(|| {
        panic!("derive_console: no Serial device probed{}", /* chosen 提示 */);
    });
    CONSOLE.set(dev);
}

fdt_parser 已处理 alias/路径/"name:baud" 后缀。当前为单串口板简化版:SERIALS 仅一项直接提升为 console,chosen.stdout 仅作「期望有 console」校验;多串口按 stdout.node.name 在 SERIALS 中匹配留待未来(需 driver 记录节点名)。

3c. std-chip 绕过

std-chip(host 桩)无 DTB、不调 boot(),直接 CONSOLE.set(&STD_SERIAL)。chosen 派生路径只对有 DTB 的板生效。

Step 4:bus 抽象

4a. I2cBus / SpiBus 功能 trait(device.rs,纯契约)

rust
pub trait I2cBus: Send + Sync {
    fn transfer(&self, addr: u8, msg: &mut [I2cMsg<'_>]) -> Result<(), I2cError>;
}
pub enum I2cMsg<'a> { Write(&'a [u8]), Read(&'a mut [u8]) }
pub struct I2cError;

pub trait SpiBus: Send + Sync {
    fn transfer(&self, write: &[u8], read: &mut [u8]) -> Result<(), SpiError>;
}
pub struct SpiError;

4b. I2C_BUSES / SPI_BUSES 注册表 + 活跃 bus 栈(bus.rs)

rust
pub static I2C_BUSES: DeviceRegistry<&'static dyn I2cBus, 4> = DeviceRegistry::new();
pub static SPI_BUSES: DeviceRegistry<&'static dyn SpiBus, 4> = DeviceRegistry::new();

DFS 遍历期间的活跃 bus 栈存 DeviceRegistry::register 返回的索引(不是 Node——Node 无取 bus 实例的方法)。controller probe register + push_i2c(idx);child probe current_i2c() 取栈顶收发。

4c. boot() 改为 depth-aware

rust
bus_stack_reset();
let mut prev_level = 1usize;
for node in fdt.all_nodes() {        // DFS 先序,node.level = 深度
    if node.level < prev_level {     // 离开 controller 子树
        bus_stack_pop_to(node.level);
    }
    for drv in *drivers { /* compatible 匹配 → probe */ }
    prev_level = node.level;
}

为什么 Driver::probe 签名不变? DFS 保证父节点(I2C 控制器)先于子节点(传感器)被访问。控制器 probe 时 push 索引,传感器 probe 时 current_i2c() 取栈顶——不需要显式传参,不需要改 trait。

UnsafeCell 而非 static mut Rust edition 2024 的 static_mut_refs lint 默认 deny,禁止对 static mut 取引用。活跃 bus 栈需要 &self 内部可变性(boot 期间 push/pop),用 UnsafeCell<BusStack> 承载,与 Slot 同安全模型(单 hart 串行 boot 下无并发)。

范围:当前实现对单层 controller 场景正确(controller 挂在总线根下、child 同层或更深,bus_stack_pop_to 在 level ≤1 时全清)。多层嵌套 controller 需给每个 bus 索引配 level,留作未来。

对标

业界rt-async 映射
Linux device_register + kset 枚举DeviceRegistry::register + iter(SERIALS / I2C_BUSES / SPI_BUSES)
Linux stdout-path 选 consolederive_console(chosen.stdout-path → CONSOLE 槽)
Zephyr DEVICE_DT_GETSlot::get + 便捷访问器(console() / timer()

rt-async 的取舍:rt-async 用 Slot(单例角色)+ DeviceRegistry(枚举)两个原语表达 Linux 的 device/kset 体系,省去 kobject 引用计数、sysfs、热插拔——裸机单 hart 串行 init 不需要。derive_console 是 stdout-path 的最小子集(单串口直接提升,多串口按 name 匹配留待未来)。

零成本验证

  • 无新运行时依赖Slot/DeviceRegistry 是 const-constructible 的自有类型,仅依赖 portable-atomicheapless(后者仅 bus 栈用,已是既有依赖)。
  • 无 alloc:全部 static 数组,const fn new
  • vtable 仅在 serial-await 路径console() 返回 &'static dyn Serial,trait object 只在 SerialRx::pollrx_register_waker 动态分发;Timer/Ipi/Reset/InterruptController 的 trait object 调用频率极低(定时器设 deadline、IPI send/clear、关机),不在热路径。
  • IRQ 分发零 vtableIRQ_TABLEunsafe fn(u32) 裸指针,transmute_copy 直调,无 trait object。

未做(本轮)

原因
linkme 自动 driver 发现driver < 10 个,手动 default_drivers() 列表完全可控,可读性更好
DT interrupts 自动解析 → register_irqIRQ 显式注册(板级 board_initregister_irq)可读性更好,维持现状
ClockController traitK3 单板 single-hart 无此需求
多串口 name 匹配 derive_console需 driver 记录节点名,单串口板(qemu-virt)暂不需要
多层嵌套 bus controller当前 bus_stack_pop_to 在 level ≤1 全清,单层场景正确;多层需逐 bus 配 level