- 嵌入式
- 驱动开发
- 通信
- 物联网
【免费下载链接】tinyusb
An open source cross-platform USB stack for embedded system
本篇技术指南围绕 TinyUSB 仓库中fix-ci-hs分支的设计文档展开,讲解如何从 NXP i.MX RT106x 的硅片 Errata(ERR050101)重新归因一次顽固的设备端挂死(wedge),并据此移除qhd_start_xfer()中针对 EP0 的"prime 后校验"代码块。读者读完将掌握:ChipIdea HS 控制器的 ENDPTPRIME/ENDPTSTAT/ENDPTSETUPSTAT 寄存器语义、如何在代码中区分"真正的端点冲突 bug"与"被误判的软件保护",以及一套可复现的软件门禁 + 硬件 A/B 验证流程,用于安全地删除一段看似必要实则有害的防御代码。
问题背景:mimxrt1064_evk 上的不可中断挂死
设计文档记录的起点是一次反复出现的现场故障:mimxrt1064_evk板卡会停止应答主机传输,URB 永远无法完成,Linux 侧的testusb进程进入不可中断睡眠(D-state)而阻塞不返回,整个测试台架随之宕掉。四天内在 Linux usbtest 测试集的 queued control 与 bulk 测试中出现八次。
在真因查明之前,团队提出过一个软件层面的理论:一个恰好在 prime 操作中途到达的 SETUP 会静默取消 EP0 的 prime,从而让端点永久停在 NAK 状态。基于这个理论,qhd_start_xfer()中加入了 post-prime 校验块。该理论的支撑证据是一条抓包记录——EP0 的 status 阶段 ZLP 已被武装(armed)但未完成 prime,设备侧的控制传输状态比主机侧领先一拍。
根因:i.MX RT1064/RT1060 Errata ERR050101
随后查明的真正原因是硅片级缺陷,而非软件逻辑错误:i.MX RT1064_A / RT1060_A 的 Errata ERR050101。当本设备的一个等时(isochronous)IN 端点处于活动状态时,若共享同一主机的另一台设备上有指向相同端点号的 IN token,会静默地取消(unprime)本设备的一个 OUT 端点——无论该端点是 control、bulk、interrupt 还是 isochronous 类型。NXP 明确表示此现象无法由软件检测,且不会产生任何中断。
关键洞察是:ERR050101 的适用范围明确覆盖 control OUT 端点,而控制传输的 status 阶段本质上就是一个 OUT 端点。因此那条原本用于支撑"SETUP 中途取消 prime"理论的抓包证据,用 ERR050101 同样可以完美解释——旧理论从此失去了独立证据。
仓库中的 errata 配套实现
这条 errata 在仓库中有完整的配套落点,可作为理解本文背景的佐证:
- usb_descriptors.c:当
CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101定义时,usbtest 示例的 iso IN 端点号被强制设为0x87(默认分支是 0x83),即把端点从 3 号移到 7 号,远离其他设备常用端点号(提交42870b15b)。 - tusb_mcu.h:针对 RT1015/RT1020/RT1024/RT1050(无修复计划)以及 RT1060/RT1064 rev A(rev B 已修复)系列自动定义该 errata 宏。
- tusb_option.h:提供默认值为 0 的可覆盖开关,允许按板卡覆盖。
端点迁移的效果是决定性的:迁移后连续340 次无挂死运行,而此前板卡通常几小时内就会重新挂死。
变更内容:删除qhd_start_xfer()中的 post-prime 校验块
设计文档的核心变更非常收敛——只删一处:src/portable/chipidea/ci_hs/dcd_ci_hs.c中qhd_start_xfer()的尾部校验块。删除的内容包括:
- 有界等待的
ENDPTPRIME位清零轮询(bounded drain); - 超时后的
ENDPTFLUSH兜底冲刷; - 基于
ENDPTSTAT | ENDPTCOMPLETE与ENDPTSETUPSTAT的判定逻辑。
变更后的函数尾部(当前仓库源码 dcd_ci_hs.c 即为该形态):
// start transfer dcd_reg->ENDPTPRIME = TU_BIT(epnum + (dir ? 16 : 0)); return true;对比被删除的旧代码形态(摘自实现计划中引用的原文):
// start transfer const uint32_t prime_bit = TU_BIT(epnum + (dir ? 16 : 0)); dcd_reg->ENDPTPRIME = prime_bit; if (epnum == 0) { uint32_t guard = CI_HS_BUSY_SPIN; while (dcd_reg->ENDPTPRIME & prime_bit) { if (!guard--) { dcd_reg->ENDPTFLUSH = prime_bit; // never leave a wedged prime armed over a freed buffer return false; } } if (!((dcd_reg->ENDPTSTAT | dcd_reg->ENDPTCOMPLETE) & prime_bit) && (dcd_reg->ENDPTSETUPSTAT & TU_BIT(0))) { return false; // prime cancelled (setup mid-prime): the pending SETUP re-drives EP0 } } return true;为什么这个删除是安全的
设计文档给出了两条核心理由:
- 无独立证据:该校验服务于一个已被 errata 取代的错误理论,支撑证据(status ZLP 已武装未 prime、设备领先主机一个控制传输)被 ERR050101 同等解释。此前的通用化版本已经作为"易回归且针对厂商文档声明软件不可检测的故障"而被回滚(
565bb0d99),本次删除只是清掉残留。 - 存在真实的误报路径:评审者指出,一个已被中断处理程序正常完成的传输,其寄存器状态与一次被取消的 prime 读起来完全相同——该校验会把正常完成误判为取消(false-fail)。这是纯软件防护在实际硬件上的负收益。
性能收益与寄存器语义
从寄存器层面看,每次 EP0 传输由此省去两次寄存器自旋和四次 volatile 读取。相关寄存器定义见 ci_hs_type.h:ENDPTPRIME(端点 Prime)、ENDPTSTAT(端点状态)、ENDPTCOMPLETE(端点完成)、ENDPTSETUPSTAT(端点 SETUP 状态),均为 32 位 volatile 寄存器。ENDPTPRIME的位编码TU_BIT(epnum + (dir ? 16 : 0))中,OUT 端点占 0–15 位、IN 端点占 16–31 位,这一位布局在删除后的代码中保持不变。
调用链不受影响
qhd_start_xfer()的签名static bool qhd_start_xfer(uint8_t rhport, uint8_t epnum, uint8_t dir)及其 bool 返回类型均保持不变,两个调用方无需改动:
- dcd_edpt_xfer()(常规端点传输);
- dcd_edpt_xfer_fifo()(无 dcache 场景下的 FIFO 传输)。
特别地,dcd_set_address() 依赖dcd_edpt_xfer()的返回值做地址生效门控:若 EP0 status 阶段未能送出,则回滚DEVICEADDR(USB 2.0 9.4.6 要求地址仅在 status 阶段成功完成后生效)。删除 post-prime 校验不影响 pre-prime 锁存守卫的 false 返回路径,因此 usbd 侧的断点移除逻辑依然成立,不会产生级联改动。
刻意保留的部分
设计文档明确列出"不随本次变更删除"的相邻代码,理解这一点可以避免把本次删除误当"剥离一切防御":
| 保留项 | 依据 |
|---|---|
Pre-prime setup 锁存守卫(qhd_start_xfer()中 prime 写入之前的if (epnum == 0)块) | UM10503 25.10.8.1.1 第 4 步原文:"Before priming for status/handshake phases ensure that ENDPTSETUPSTAT is '0'";且该守卫早于挂死理论存在,与 errata 无关。当前源码见 dcd_ci_hs.c |
| SETUP 时的 EP0 flush 及其完成等待 | flush 对应 25.10.8.1.1 第 3 步说明;等待存在的原因是"未完成的 flush 可能收回一个刚 prime 好的响应",该交互与校验块相互独立。对应逻辑在 dcd_int_handler() 的ENDPTSETUPSTAT分支中 |
| BUS_RESET_START/END 拆分及其余评审驱动的加固 | 独立于本理论,保留 |
| 全部经硬件验证的改动:rf_tv 修复、lpc11u37 栈迁移、lpc55s28 板卡接入、lpc55 Make OHCI 链接修复、ERR050101 端点迁移本身 | 各自有独立的硬件证据 |
此外,提交信息专门记录了 handoff 抓包证据的重新归因,避免下一位读者基于同一条证据再次推导出已被取代的理论。
验证方案:软件门禁 + 硬件 A/B
本次变更是 A/B 验证的第二半("with it" 半边):2026-08-16 已封存 10 次 30/30 全量测试、15 次 TEST 27、15 次 tests 9/10、10 次 tests 11/12/24,全部干净。删除侧需要按下面流程完成对等的证据采集。
第 1 步:先 rebase 再构建
master 已经前移(midi2/usbtmc/video 等改动),必须先 rebase 再重建,否则验证通过的树并不是将要合并的树——验证一棵错误版本的树就是虚假通过。执行git fetch origin master && git rebase origin/master,冲突时逐 hunk 解决、保留双方意图。
第 2 步:软件门禁
pre-commit run --all-files(含 trailing-whitespace、codespell、unique-PIDs、ceedling 等钩子);- 四个板卡的全量示例构建:
mimxrt1064_evk、lpcxpresso18s37、lpcxpresso11u37、lpcxpresso55s28(CMake + Ninja,MinSizeRel); - 两条 Make 链接金丝雀:
host/cdc_msc_hid(lpcxpresso55s28)、device/cdc_msc_throughput(lpcxpresso11u37)——两者在分支生命期中曾被破坏,是 BSP 改动的回归探测器; ceedling test:all(单元测试,位于 test/unit-test)。
注意 Make 金丝雀选 lpc55s28/lpc11u37 属于 ip3511 驱动的板卡,但它们的目的是验证BSP 层改动而非本提交,与本变更的 ci_hs 范畴互补。
第 3 步:硬件前置条件(仅 mimxrt1064_evk)
只有这块板卡被测——它是台架上唯一的 ci_hs 板,另外两块跑 ip3511 驱动,本变更不触及。前置条件:
- CI 空闲:
pgrep -f "hil_test.py [--]-retry"无结果; - 全程持有板卡锁(
test/hil/hil_lock.py); - 用
loadfile烧录(其内建 Program & Verify;台架上的 JLinkExeV9.66 没有verifyfile命令); - 确认重新枚举为
cafe:4010、序列号BAE96FB95AFA6DBB8F00005002001200; - 必须用
lsusb -v确认 iso IN 端点仍为0x87——这块板曾两次"静默无效烧录",端点号是防止旧镜像冒充新镜像的最后一道校验(对应 usb_descriptors.c 的 errata 分支)。
第 4 步:运行矩阵
- 5 次全量 30-case 测试:用 test/hil/usbtest.py 驱动 Linux testusb 测试集,每次迭代后扫描一次
testusb的 D-state(ps -eo stat,...)。 - 15 次控制路径聚焦循环:
--tests 9,10,14,21——queued control、ch9 子集、两个 ctrl_out 用例。这是关键补充:全量测试每轮对每个控制用例只采样一次,而这些恰恰是被删除校验原本保护的路径,必须用聚焦循环放大暴露概率。
验收标准与回滚触发
- 验收:5/5 次全量测试均 30/30,15/15 控制循环全部通过,且不存在存活时间超过单个用例运行时间的
testusbD-state。 - 回滚触发:任何控制用例失败(用例 9/10/14/21 上报 errno 110 或 71)或出现滞留 D-state,即证明该校验原本是承重的——此时
git revert恢复代码、重跑确认失败消失,并把该结果写进提交信息。
设计文档为这条回滚路径给出了一个值得记住的方法论态度:负面结果同样是发现,不是挫折。
方法总结:一次"删除防御代码"的完整证据链
这个案例的工程方法论可以提炼为一条可复用的证据链:
- 现象(wedge、D-state、URB 不完成)→ 产生一个软件理论(SETUP 中途取消 prime)→ 写入防御代码;
- 真因(ERR050101 硅片 errata)出现后,回头审视:旧理论的支持证据是否被新归因同等解释?——是,则防御代码失去独立证据;
- 代价审计:防御代码本身有真实误报路径(完成即读同取消),且在每次 EP0 传输上付出确定性开销;
- 拆除验证:不追求"证明它有用",而是"证明没有它系统依然健康"——通过聚焦被保护路径的 A/B 运行矩阵实现,并保留明确的回滚触发器。
对于 ChipIdea HS / i.MX RT 平台的开发者,本案例还有一个更直接的技术结论:遵循参考手册(UM10503 25.10.8.1.1)要求的 pre-prime setup 锁存守卫就够了——它是手册明确要求、且与 errata 无关的防御;而针对"厂商文档声明软件不可检测"的故障追加 post-prime 软件校验,不仅徒劳,还会引入误报与每次传输的固定开销。
- 嵌入式
- 驱动开发
- 通信
- 物联网
【免费下载链接】tinyusb
An open source cross-platform USB stack for embedded system
相关推荐
TinyUSB usbtest 实战指南:用 Linux 内核 30 项 USB 电池测试验证与移植 DCD 驱动
TinyUSB usbtest 实战指南:用 Linux 内核 30 项 USB 电池测试验证与移植 DCD 驱动 examples/device/usbtes
嵌入式驱动开发通信物联网Facebook iOS SDK 广告归因测试:A/B 测试框架与数据验证方法
Facebook iOS SDK 广告归因测试:A/B 测试框架与数据验证方法 广告归因是移动营销效果评估的核心环节,但iOS平台的归因数据准确性常受设备权限、
移动开发认证鉴权社交数据分析jevgrep 发布流程全解:从版本标识、归档校验到 npm 发布与安装后验证
jevgrep 发布流程全解:从版本标识、归档校验到 npm 发布与安装后验证 导读 本文围绕 jevgrep 仓库的 发布文档 https://link.gi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考