diff --git a/.cubemx_survival_baseline.json b/.cubemx_survival_baseline.json index a77596aa..8626cd24 100644 --- a/.cubemx_survival_baseline.json +++ b/.cubemx_survival_baseline.json @@ -23,11 +23,11 @@ "ioc-task-stacks": "58ee94dd2a093177", "adc1-hal-enabled": "1a8ed6cff34f7ba6", "adc1-calibration-call": "8d5f4b40fb218913", - "adc1-calibration-impl": "d33bc4323e243248", + "adc1-calibration-impl": "25723dd0449ac701", "adc-tick-tim3-branch": "39c3548c37f6c4ae", "ioc-adc-trig-t3trgo": "a9939de260ee46f8", "ioc-adc-dma-continuous": "189126670d85d80a", - "adc1-dma-link": "397fce92dca3b728", + "adc1-dma-link": "8a57c70611db1a02", "adc1-dma-irq-priority": "beff08e91333229b" }, "_note": "CubeMX 存活审计基线;改动用 --write 重建" diff --git a/App/app/app_main.c b/App/app/app_main.c index 592b1752..99942d0a 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -17,6 +17,7 @@ #include "stm32f4xx_it.h" /* fault_handler_enable() */ #include "usart.h" /* huart5 */ #include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */ +#include "adc.h" /* adc1_disable():挂 DMA 前把 ADC 关掉,让 Start_DMA 自己使能 */ #include "rs485.h" #include "sd2506.h" #include "boot_stats.h" /* 开机账本:上电次数(RTC 用户 SRAM)+ 本次复位原因快照(道G task-12) */ @@ -220,6 +221,24 @@ int app_main_init(void) { syslog_post(SYSLOG_SELFTEST, "ADC fault"); } + /* 4b-pre) 挂 DMA 之前**先把 ADC 关掉**,让使能动作由 HAL_ADC_Start_DMA 内部完成 + * (它自己 __HAL_ADC_ENABLE + 等 Tstab ~20 µs,然后紧接着开 CR2.DMA 并启流)。 + * + * 为什么必须这样做:MX_ADC1_Init() 在 main() 里就调用了,而挂 DMA 在这里, + * 中间隔着 GD5F 全片 BBT 扫描(数秒)。台架实测(同一次开机内用无 DMA 单通道 + * 探针取真值对照,9/9 完美置换):**ADC 使能后闲置数秒才挂 DMA,DMA 乒乓缓冲 + * 会整体 +2 格旋转** —— 槽 i 装的是 rank ((i+1) mod 9)+1,9 路通道按 rank 序 + * 整体错开 2 位。表象是"接地 AIN2、UB 却读到别的通道的值",极易被误判成 + * 映射表写错或夹子位置不对。 + * 把使能与挂 DMA 压到同一时刻后旋转消失(台架 4/4 次,同开机对照差值 ≤4 码)。 + * 证据与复现方法见 docs/子系统设计/ADC采样节拍设计说明.md §4.5。 + * + * ⚠ 顺序不可调换:本调用必须严格早于下面的 adc_input_init()。 + * 若 ADC 在本步之后仍处于使能态,旋转会重新出现。 */ + if (adc1_disable() != 0) { + DBG_ERROR("ADC1 disable before DMA arm: TIMEOUT"); + } + /* 4b) ADC 输入层(L1)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检** * (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错) * + 挂载一次 STM32 ADC1 的 DMA(失败返回 -2)。 diff --git a/Inc/adc.h b/Inc/adc.h index c0a0a036..6d2f1588 100644 --- a/Inc/adc.h +++ b/Inc/adc.h @@ -46,6 +46,12 @@ void MX_ADC1_Init(void); * 实现见 Src/adc.c 的 USER CODE BEGIN 1)。返回 0 = 完成,-1 = 等待超时 */ int adc1_calibrate(void); +/* ADC1 关闭(清 ADON 并等其生效)。实现见 Src/adc.c 的 USER CODE BEGIN 1。 + * **必须在挂 DMA 之前调用**:把"使能 ADC"推迟到 HAL_ADC_Start_DMA 内部完成, + * 使能与搬运之间不留可观测间隔。返回 0 = 已关闭,-1 = 等待超时。 + * 动机与台架证据见 docs/子系统设计/ADC采样节拍设计说明.md §4.5(DMA 缓冲 +2 格旋转)。 */ +int adc1_disable(void); + /* USER CODE END Prototypes */ #ifdef __cplusplus diff --git a/Src/adc.c b/Src/adc.c index 97621a1a..0321d22b 100644 --- a/Src/adc.c +++ b/Src/adc.c @@ -261,6 +261,8 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle) #define ADC1_CAL_TIMEOUT_MS (10U) /* ADON 0→1 后的稳定等待(µs),同 HAL 的 ADC_STAB_DELAY_US 口径 */ #define ADC1_STAB_DELAY_US (20U) +/* 关闭等待上限(ms)。ADON 由硬件清零,实测在数十 ns 内完成;10 ms 是十万倍余量 */ +#define ADC1_OFF_TIMEOUT_MS (10U) /* * 函数功能:ADC1 上电自校准(等价于 HAL_ADCEx_Calibration_Start,本仓库 HAL 无此函数) @@ -311,5 +313,38 @@ int adc1_calibrate(void) } return 0; } + +/* + * 函数功能:关闭 ADC1(清 CR2.ADON 并等硬件清零生效) + * 入口参数:无 + * 返 回 值:0 - 已关闭;-1 - 等待超时(ADON 未清零) + * 限定条件:任何上下文均可(只碰 ADC1 寄存器与 HAL_GetTick);须在挂 DMA 之前调用 + * 函数说明:**本函数的存在只为把"使能 ADC"推迟到挂 DMA 的同一刻。** + * MX_ADC1_Init() 在 main() 里调用,而挂 DMA 在 app_main_init 步骤 4b, + * 中间隔着 GD5F 全片 BBT 扫描(数秒)。台架实测(同一次开机内用无 DMA + * 单通道探针取真值对照,9/9 完美置换):ADC 使能后闲置数秒才挂 DMA, + * DMA 乒乓缓冲会整体 +2 格旋转 —— 槽 i 装的是 rank ((i+1) mod 9)+1, + * 即 9 路通道按 rank 序整体错开 2 位,表现为"接地 AIN2、UB 却读到别的 + * 通道的值"这种看似映射表写错的现象。 + * 调用本函数后 ADC 处于关闭态,HAL_ADC_Start_DMA 内部会自己 + * __HAL_ADC_ENABLE + 等 Tstab(~20 µs),然后紧接着开 CR2.DMA 并启流, + * 使能与搬运之间不再有可观测的间隔 → 旋转消失(台架 4/4 次,差值 ≤4 码)。 + * 证据、复现方法与机制分析见 docs/子系统设计/ADC采样节拍设计说明.md §4.5。 + */ +int adc1_disable(void) +{ + uint32_t tickstart; + + CLEAR_BIT(ADC1->CR2, ADC_CR2_ADON); + tickstart = HAL_GetTick(); + while ((ADC1->CR2 & ADC_CR2_ADON) != 0U) + { + if ((HAL_GetTick() - tickstart) > ADC1_OFF_TIMEOUT_MS) + { + return -1; + } + } + return 0; +} /* USER CODE END 1 */ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index b3ca4f55..b347fbad 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 261,764 B (255.63 KB) | 1,048,576 B (1024 KB) | 25.0% | +| ER_IROM1 | 261,928 B (255.79 KB) | 1,048,576 B (1024 KB) | 25.0% | | RW_IRAM1 | 81,072 B (79.17 KB) | 131,072 B (128 KB) | 61.9% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **263,336 B (257.2 KB)** | 1,048,576 B (1024 KB) | **25.1%** | +| **Flash (ROM 合计)** | **263,500 B (257.3 KB)** | 1,048,576 B (1024 KB) | **25.1%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 9dc34a9b..95ee2d61 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 261,764 B | 1,048,576 B | 25.0% | 参考 | +| ER_IROM1 (Flash) | 261,928 B | 1,048,576 B | 25.0% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 81,072 B | 131,072 B | 61.9% | PASS | diff --git a/docs/子系统设计/ADC采样节拍设计说明.md b/docs/子系统设计/ADC采样节拍设计说明.md index 6a107dee..c1f12abc 100644 --- a/docs/子系统设计/ADC采样节拍设计说明.md +++ b/docs/子系统设计/ADC采样节拍设计说明.md @@ -200,6 +200,50 @@ const uint16_t *src = &s_stm32_dma[half * ADC_INPUT_STM32_CH_NUM]; - 取帧状态码:`ADC_INPUT_RC_OK` = 0 / `ADC_INPUT_RC_START` = -1 挂载失败 / `ADC_INPUT_RC_STALL` = -2 本拍无新帧 / `ADC_INPUT_RC_NOT_RUN` = -3 尚未取过帧。 +### 4.5 使能时机与 DMA 缓冲相位(+2 格旋转,2026-10-27 实测) + +**这条是本节最反直觉的一条:DMA 缓冲会整体错开 2 格,而寄存器配置全对、拷贝循环也全对。** + +**现象。** 台架把某个端子接地,读数却出现在**另一个通道**上——例如接地 `AIN2`(`UB` / PC4 / CH14), +`UB` 读出 784 码(浮空值),而 `IB` 读出 0。表象与"映射表写错"完全一致,但 +`SQR1/SQR2/SQR3` 实测为 CH0..CH7 + CH14、`L=8`,`k_stm32_rank_ch[]` 与拷贝循环也逐条核过,全对。 + +**证据。** 同一次开机内,先用无 DMA 单通道方式(逐路关扫描、软件触发、直接读 `DR`)取真值, +再与 DMA 路径的同一批输入逐路对照,得到 9/9 完美置换: + +| 槽(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 码以内(两种读法的噪声),不是随机抖动。 + +**判据上的陷阱。** `s_stm32_raw[k_stm32_rank_ch[i]] = src[i]` 的恒等性**不能**用来证明映射正确: +DMA 从序列中途开始搬时,缓冲整体旋转,拷贝照样是恒等的。判映射只能靠 +**按通道号索引的独立真值**(无 DMA 单通道读),或按 §4.5 的修法消除旋转后再比对。 + +**根因。** 触发条件是「ADC 被使能之后**闲置了数秒**才挂 DMA」。本工程 `MX_ADC1_Init()` 在 `main()` +里调用,而挂 DMA 在 `app_main_init()` 步骤 4b,中间隔着 GD5F 全片 BBT 扫描。台架对照: +"使能后闲置数秒才挂" → 7/7 次都是 +2 旋转;"使能与挂 DMA 在同一时刻" → 6/6 次旋转为 0 +(探针固件 5/5 + 同一次开机内重新挂 DMA 1/1)。 +**微架构层面的确切原因(为何恰好丢 2 拍)尚未证出**,但工程结论是确定的:使能与搬运不该有可观测间隔。 + +**修法。** `app_main_init()` 步骤 4b-pre 调 `adc1_disable()`(实现见 `Src/adc.c` 的 `USER CODE` 区) +先把 ADC 关掉,使能动作交给 `HAL_ADC_Start_DMA()` 内部完成——它自己 `__HAL_ADC_ENABLE` + +等 Tstab(约 20 µs),然后紧接着开 `CR2.DMA` 并启流。台架验证:在 `MX_ADC1_Init()` 末尾直接关闭 +4/4 次、在步骤 4b-pre 调本 API 3/3 次,均 `UB=0`,同开机对照差值 ≤4 码。 +⚠ **顺序不可调换**:`adc1_disable()` 必须严格早于 `adc_input_init()`;若 ADC 在挂 DMA 时仍处于使能态, +旋转会重新出现。改动被删时会由审计项 `adc1-calibration-impl` 的基线漂移拦下 +(它整段守护 `Src/adc.c` 的 `USER CODE BEGIN 1`)。 + --- ## 5. TPAFE 侧:CONVST 脉冲与 BUSY 读帧 @@ -342,3 +386,6 @@ t = 1000 µs 下一个更新事件 **违反时不报任何错**,只在数据上出事(静默丢通道、搬不回来、优先级被重置为 0)。 6. **节拍源停摆必须导致复位**(§6.3):不得给等待加超时来"绕过"看门狗。 7. **动栈或加深调用链前后各跑一次 `scripts/stack_report.py`**,新增缓冲前跑 `scripts/ram_report.py`(§9)。 +8. **挂 DMA 之前必须让 ADC 处于关闭态**(§4.5):`app_main_init()` 步骤 4b-pre 的 `adc1_disable()` + 不可删除、不可后移。违反时**不报任何错**,只是 9 路通道按 rank 序整体错开 2 格—— + 表象与"映射表写错"或"注入点标签不对"完全一样,历史上曾据此误判过三轮。