BLOCK 超时与完成交接
这页讨论的不是 SPI / I2C / UART 的接口,而是 BLOCK 超时在异步完成路径里的实际含义。问题的核心始终是同一个:调用者已经不等了,底层还会不会继续完成;如果会,这次完成还能不能再去唤醒旧的 waiter。
1. BLOCK timeout 的真实含义
BLOCK timeout 限制的是调用者的同步等待窗口,并不保证底层已经接受的操作会被自动取消。也就是说,超时返回不等于硬件停下来了;只要底层已经启动,迟到完成就仍然可能发生。
2. 为什么要有 detach 语义
如果 timeout 之后直接把端口或 waiter 粗暴清零,就会遇到一个典型问题:
- 调用者已经返回
TIMEOUT - 老的底层完成稍后到来
- 这次完成又错误地唤醒新调用,或者留下 stale semaphore token
所以这里要做的不是“硬清空”,而是先把当前 waiter 分离,再让迟到完成静默收尾,等旧交接彻底排空后再重新开放端口。BLOCK_DETACHED 一类状态,就是为了表达这件事。
3. AsyncBlockWait 解决什么问题
AsyncBlockWait 做的事情很单纯,就是给驱动内部“同步等待一个异步完成”的路径提供标准 handoff。它不负责取消底层硬件,只负责说清楚这次完成还属不属于当前同步调用者:Start(sem) 把 waiter 挂起成 PENDING,TryPost(...) 只有在 PENDING -> CLAIMED 成功时才允许唤醒当前 waiter,Wait(timeout) 超时后则把 waiter 切成 DETACHED。如果迟到完成只看见 DETACHED,那就只能静默回收,不能再 post。
4. 最常见的几个 bug
4.1 硬件先启动,waiter 后挂起
这是最典型的一类。
错误顺序:
- 先 arm 硬件 / 启动 DMA / 打开中断
- 再
block_wait_.Start(...)
风险是:
- 底层完成太快
- ISR 比 waiter setup 更早到
- 唤醒信号直接丢掉
正确顺序应该是:
- 先把 waiter 状态挂好
- 再把硬件暴露给 ISR / DMA / 完成路径
4.2 把旧 semaphore token 当成本次完成
如果一个 semaphore 被重复用于多次 BLOCK 调用,而等待路径只把 sem->Wait(timeout) == OK 当成成功,上一次残留的 token 就可能被误认成当前完成。这里真正要做的是先确认 busy_ 或 waiter 状态已经进入当前操作对应的 CLAIMED。只有这样,这次 wake 才属于本次操作;否则就应该继续等待,把它当成 stale token。
4.3 timeout 后只返回,不做 detach
这种实现看起来简单,但几乎一定会留下后患。因为 timeout 返回之后,老完成路径还可能修改共享状态、继续 post semaphore,甚至覆盖新 waiter 的 ownership。所以 timeout 返回前必须明确做一件事:让完成路径知道“这个 waiter 已经不属于当前调用者了”。