rt-async 驱动架构完善
本文记录 2026-07-09 落地的 driver model 四项结构性改进。早先的 2026-07-09-驱动架构完善计划.md(status: plan)描述的是另一种设计(RxRing 抽取 + 3 步),实际实现与之有实质差异,该计划文件已删除,以本文为最终记录。
背景
driver model 上线后暴露两个结构性缺陷:
SerialRx硬绑定 NS16550A:futures::serial::SerialRx::poll()直接调用serial_ns16550a::rx_poll(),RX 环形缓冲区 + waker + poll 逻辑全部写在 ns16550a 驱动内部。K3 的 PXA-UART 无法复用异步串口接收。- 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)
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 路由
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 专有逻辑,但SerialRxStatus与rx_register_waker跨 crate(futures)传递——futures没有riscv64feature,若用 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:
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_TABLE、heapless::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 改为直写公开槽位:
// ns16550a probe 内
crate::driver::SERIALS.register(&INSTANCE);
// 或单例角色:
crate::driver::TIMER.set(&INSTANCE);板级 glue 经 DRIVERS.set(&[...]) 注入 driver 列表。
三类关注点分离:
| 关注点 | 原语 | 实例 |
|---|---|---|
| 注册(init 期写入) | Slot::set / DeviceRegistry::register | driver probe |
| 枚举(多实例遍历) | DeviceRegistry::iter | SERIALS / I2C_BUSES / SPI_BUSES |
| 角色(单例选定) | Slot::get + 便捷访问器 | CONSOLE / TIMER / IPI / RESET / INTC |
为什么不用
once_cell?once_cell::sync::OnceCell<T>的get/set走Option<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 多实例注册表
pub static SERIALS: DeviceRegistry<&'static dyn Serial, 4> = DeviceRegistry::new();ns16550a 的 probe 改为 SERIALS.register(&INSTANCE) 而非直接 CONSOLE.set——驱动不再自我任命 console 角色。
3b. derive_console 从 chosen { stdout-path } 派生
boot() 遍历完所有节点后调 derive_console(fdt),从 SERIALS 选出默认 console 填进 CONSOLE:
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,纯契约)
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)
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
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_refslint 默认 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 选 console | derive_console(chosen.stdout-path → CONSOLE 槽) |
Zephyr DEVICE_DT_GET | Slot::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-atomic与heapless(后者仅 bus 栈用,已是既有依赖)。 - 无 alloc:全部
static数组,const fn new。 - vtable 仅在 serial-await 路径:
console()返回&'static dyn Serial,trait object 只在SerialRx::poll经rx_register_waker动态分发;Timer/Ipi/Reset/InterruptController的 trait object 调用频率极低(定时器设 deadline、IPI send/clear、关机),不在热路径。 - IRQ 分发零 vtable:
IRQ_TABLE存unsafe fn(u32)裸指针,transmute_copy直调,无 trait object。
未做(本轮)
| 项 | 原因 |
|---|---|
linkme 自动 driver 发现 | driver < 10 个,手动 default_drivers() 列表完全可控,可读性更好 |
DT interrupts 自动解析 → register_irq | IRQ 显式注册(板级 board_init 调 register_irq)可读性更好,维持现状 |
ClockController trait | K3 单板 single-hart 无此需求 |
多串口 name 匹配 derive_console | 需 driver 记录节点名,单串口板(qemu-virt)暂不需要 |
| 多层嵌套 bus controller | 当前 bus_stack_pop_to 在 level ≤1 全清,单层场景正确;多层需逐 bus 配 level |