把 2026-10-27 排查出的两个 ADC 采样陷阱沉淀成正式台账,按 docs/编号规范.md 的 TRAP-ADC-<nn> 编号,属主文档 docs/问题记录/ADC_Trap_Records.md。 TRAP-ADC-01:ADC 使能后闲置数秒才挂 DMA,导致 DMA 乒乓缓冲整体 +2 格相位旋转(槽 i 装的是 rank ((i+1) mod 9)+1)。全程无任何 编译或运行错误,只是数据整体错位两格,表象与「映射表写错」完全 一致,历史上据此误判过三轮。 条目重点记了那条判读陷阱:adcr[CH_i] == adcb[i] 成立不能证明通道 映射正确——它只证明拷贝循环是恒等映射,缓冲整体旋转时拷贝照样 恒等。判映射只能靠按通道号索引的独立真值(无 DMA 单通道读)。 附 9 行同开机对照证据表、5 种固件形态的 A/B 对照、修复方式、 回归判据与未证部分(微架构为何恰好丢 2 拍仍未证)。 TRAP-ADC-02:RTOS 运行期校准被 TIM3 TRGO 打断,adc1_calibrate() 返回 0xFFFF 超时哨兵。记了指纹判读要点(truth=65535 且 min 大于 max 是哨兵,不是满量程)与修法(校准期间屏蔽 EXTEN)。 关联登记三处: - docs/README.md 7.3 问题记录表新增一行 - 设计文档 4.5 节加反向链接到 TRAP-ADC-01(代码 -> 设计 -> 台账) - scripts/id_registry.json 的 TRAP 族 cursor_note 补 ADC 域 01~02 纯文档改动,不涉构建。
8.1 KiB
ADC 采样陷阱记录(TRAP-ADC 系列)
ADC1 采样链路(TIM3 1 kHz 节拍 → ADC1 九通道扫描 → DMA2_Stream4 乒乓搬运 → adc_input
语义映射)的实证陷阱:现象、判读陷阱、定案根因、修复与回归判据。动 ADC 采样、通道映射或
诊断代码之前先读本文。
编号口径:按
docs/编号规范.md采用TRAP-ADC-<nn>,本域自 01 起连续。编号不复用、不重排。 状态图例:✅ 已修复定案 · ⏳ 待修。 设计侧口径(采样节拍、寄存器配置、铁律)不在本文,见docs/子系统设计/ADC采样节拍设计说明.md;本文只记「踩过的坑」与「怎么判断踩到了」。
索引
| 编号 | 标题 | 状态 | 一句话结论 |
|---|---|---|---|
| TRAP-ADC-01 | ADC 使能后闲置数秒才挂 DMA | ✅ | 乒乓缓冲整体 +2 格旋转;挂 DMA 那一瞬 ADON 必须为 0 |
| TRAP-ADC-02 | RTOS 运行期校准被 TIM3 TRGO 打断 | ✅ | adc1_calibrate() 返回 0xFFFF 超时哨兵,诊断读数全 65535 |
TRAP-ADC-01 · ADC 使能后闲置数秒才挂 DMA → 乒乓缓冲 +2 格相位旋转
- 状态:✅ 已修复定案(2026-10-27,提交
1166ab1c) - 发现时间:2026-10-27(台架,同开机对照实验)
现象:把 AIN2(UB / PC4 / CH14)的端子接地,UB 却读出 784 码(浮空量级),
而本该浮空的 IB 读出 0。九路读数整体"错开两位":接哪一路,0 就出现在往前数两路
的位置上。
表象与"映射表写错"或"注入点标签不对"完全一致,历史上据此误判过三轮——两次接地实验
(认为 AIN3→IC=0、AIN10→UB=0)都被读成"映射错了两个槽位",实际夹子位置也在动
(U5.B7 与 U5.A8 在连接器上斜向相邻、间距不到 3 mm),两个因素叠加,谁也说不清。
判读陷阱(这条比根因更贵):
adcr[CH_i] == adcb[i]成立不能证明通道映射正确。
它只证明 stm32_take() 的拷贝循环是恒等映射(s_stm32_raw[k_stm32_rank_ch[i]] = src[i])。
DMA 从序列中途开始搬运时,整个缓冲一起旋转,拷贝照样"恒等"——旋转后的缓冲被原样复制,
看起来完美自洽。我当时正是拿这个恒等性(11:18 那次 6/6 精确相等)宣布"DMA 从来没有错过",
结论完全错误。
判通道映射只有两条路:
- 按通道号索引的独立真值——关掉 DMA 与扫描,逐路软件触发、直接读
DR; - 先按本节修法消除旋转,再做任何比对。
从 adc: 的语义标签反推永远不算证据:标签一错,整条推断链跟着跑偏。
根因(定案):触发条件是「ADC 被使能之后闲置了数秒才挂 DMA」。
MX_ADC1_Init() 在 main() 里就调用(使能 ADC),而挂 DMA 在 app_main_init() 步骤 4b,
两者之间隔着 GD5F 全片 BBT 扫描(数秒)。ADC 在这段时间里处于 ADON=1 的空转态;
等 DMA 挂上去时,搬运起点已经不在序列的 rank 1 上。
台架进一步收窄为:挂 DMA 的那一瞬间 CR2.ADON 必须为 0——不需要整个闲置期都为 0,
只要使能与搬运之间不留可观测间隔,旋转就不出现。
证据(同一次开机内对照,9/9 完美置换):先用无 DMA 单通道方式逐路取真值,再与 DMA 路径 的同一批输入按通道号对照:
| 槽(rank) | 通道 | DMA 值 | 实际装的是 | 真值 | 差 |
|---|---|---|---|---|---|
| 1 | CH0 | 850 | CH2 | 850 | 0 |
| 2 | CH1 | 932 | CH3 | 931 | 1 |
| 3 | CH2 | 853 | CH4 | 852 | 1 |
| 4 | CH3 | 819 | CH5 | 817 | 2 |
| 5 | CH4 | 946 | CH6 | 945 | 1 |
| 6 | CH5 | 954 | CH7 | 953 | 1 |
| 7 | CH6 | 0 | CH14 | 0 | 0 |
| 8 | CH7 | 690 | CH0 | 691 | 1 |
| 9 | CH14 | 790 | CH1 | 785 | 5 |
即槽 i 装的是 rank ((i+1) mod 9)+1,整个缓冲从 rank 3 开始搬,固定错开 2 格。
差值全在 5 码以内(两种读法的噪声),不是随机抖动。
A/B 对照(硬件完全不动,只换固件):
| 固件形态 | 开机次数 | UB 读数 |
结论 |
|---|---|---|---|
| 干净固件 | 7/7 | 783~786 | 旋转存在 |
| 挂 DMA 前重初始化 ADC | 5/5 | 0 | 旋转消失 |
| 同一次开机内重新挂 DMA | 1/1 | 0 | 旋转消失 |
修复后(MX_ADC1_Init() 末尾关闭) |
4/4 | 0 | 旋转消失 |
修复后(步骤 4b-pre 调 adc1_disable()) |
3/3 | 0 | 旋转消失 |
⚠ 跨开机对照无效:输入浮空时读数会随环境漂移,7 次和 5 次看着都"稳定",但两组之间 不可比。必须同一次开机内、硬件不动,才能下结论。
修复:app_main_init() 步骤 4b-pre 调 adc1_disable()(实现见 Src/adc.c 的
USER CODE BEGIN 1),把 ADC 关掉;使能动作交给 HAL_ADC_Start_DMA() 内部完成——它自己
__HAL_ADC_ENABLE + 等 Tstab(约 20 µs),然后紧接着开 CR2.DMA 并启流。
⚠ 顺序不可调换:adc1_disable() 必须严格早于 adc_input_init()。若 ADC 在挂 DMA 时
仍处于使能态,旋转会重新出现。该顺序已写进
docs/子系统设计/ADC采样节拍设计说明.md 的设计铁律第 8 条,并由 CubeMX 存活审计项
adc1-calibration-impl(整段守护 USER CODE BEGIN 1)的基线漂移兜底。
回归判据:
- 把
AIN2(UB)端子接地后开机,adc:行UB必须为 0(不是 700~800); - 九路按 rank 序无错位——接地哪一路,
0就出现在该路上,不往前偏两格; - 同开机对照(DMA 值 vs 无 DMA 单通道真值)逐路差值 ≤5 码。
未证部分:微架构层面为什么恰好丢 2 拍仍未证出(ADON=1 空转态下内部状态机如何
预推进两格)。工程结论已足够:使能与搬运不该有可观测间隔。不要为了"解释清楚"去猜,
更不要据此改动 ADC 配置。
教训:
- 恒等性 ≠ 正确性:拷贝循环恒等只能证明"没抄错",证明不了"源头没偏"。
- 同开机对照是唯一有效方法论:跨开机比浮空读数会把漂移当成差异。
- 判映射只认按通道号索引的真值,不认语义标签反推。
- "不报错的错"最贵:这一条全程没有任何编译/运行错误,只是数据整体错位两格。
TRAP-ADC-02 · RTOS 运行期校准被 TIM3 TRGO 打断 → 返回 0xFFFF
- 状态:✅ 已定性(探针代码已删,本条作为以后再写校准/诊断代码的证词保留)
- 发现时间:2026-10-27(TRAP-ADC-01 的同开机对照探针)
现象:诊断探针在 RTOS 上下文里逐路取真值,从第二路起每次都是
truth=65535 min=65535 max=0——看起来像"这一路输入是满量程",实际是校准失败。
根因:adc1_calibrate() 置 CR2.CAL 后轮询等它由硬件清零,超时上限 10 ms。但 TIM3
的 1 kHz 节拍(TRGO 触发 ADC1 常规转换)此时已经在跑,一次 TRGO 就启动常规转换、
打断 CAL 序列,CAL 位永远等不到清零 → 函数返回 0xFFFF 超时哨兵,调用方把它当成了
转换结果。固定发生在第二个被探通道,是因为第一路探完时 TIM3 还没启动。
修复(若再写探针):校准前后屏蔽外部触发——
CLEAR_BIT(ADC1->CR2, ADC_CR2_EXTEN) → 校准 → 恢复 EXTEN。
判读要点:truth 恰好等于 0xFFFF(65535)且 min=65535 / max=0(最小值大于最大值)
是超时哨兵的指纹,不是满量程读数。看到这个组合先怀疑校准失败,不要怀疑输入。
教训:任何在 RTOS 运行期调用的 ADC 校准,都必须先断开外部触发;校准是独占操作, 不能与常规采样节拍并发。
相关文档
- 设计侧口径、寄存器配置、9 行证据表的原始记录与复现方法:
docs/子系统设计/ADC采样节拍设计说明.md§4.5(设计铁律第 8 条)。 - 通道映射与引脚对应:
docs/硬件平台/ADC通道映射说明.md。 - 前端拓扑(③ 段变比 = 1:1,非分压):
docs/硬件平台/V0.0_网表详细分析报告.md§4.7。 - 零点/线性/映射的现场验证步骤与判据:
docs/测试说明/ADC前端注入验证流程.md。