adc: 节拍源 TIM6->TIM3 + ADC1 走 DMA 乒乓双缓冲,消除 2 ms 忙等(P1-5 单拍硬上界办结)
用户选定最彻底方案(B2):DMA + 定时器 TRGO 硬件触发 + 消费上一拍 已完成的半区。对应 P1-5 拆出的"单拍硬上界",现已办结。 硬件事实更正(查 CubeMX 器件库 + 本仓 HAL 头证实):F4 上 ADC1 的 外部触发清单里没有 TIM6/TIM7 TRGO(只接 DAC)。故 1 kHz 节拍源由 TIM6 改为 TIM3(PSC 83 / ARR 999,TRGO=UPDATE),同一个更新事件 同时 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1 一轮 9 通道 ③ 置位 ADC_WAKE_FLAG 唤醒 adcTask。TIM6 从 .ioc 整体移除。 代码: - Src/tim.c/h:新增 MX_TIM3_Init(含 MasterSlaveMode),删 htim6 - Src/adc.c:ExternalTrigConv=T3_TRGO + EDGE_RISING + DMAContinuousRequests=ENABLE;MspInit 挂 hdma_adc1(DMA2_Stream4 循环模式)。DDS 位关掉 = 一轮只发一次请求,静默丢 8 路 - Src/dma.c:DMA2_Stream4 NVIC 优先级 6(CubeMX 换流会把它重置为 0, 优先级 0 会抢占 FreeRTOS 临界区,违反"外设中断 ≥5") - Src/main.c:周期回调加 TIM3 分支;stm32f4xx_it.c 向量同步 - App/algo/adc_input.c:stm32_scan() → stm32_take(),逐通道 PollForConversion 全删;乒乓半区由完成计数奇偶推出(只读一个 32 位量 → 与 ISR 无竞态);STM32 侧数据固定滞后 1 拍 - App/task/adc_task.c:心跳日志拆出四类计数 观测性:adc1_seq(完成计数)/ adc1_gap(seq 前进 >1 = 丢帧)/ adc1_err(连续 ≥2 拍无新帧 = 停摆)/ adc1_hw(HAL 错误回调)。 单拍无新帧是消费者相位伪影(DMA 完成落在唤醒后 ≈67 µs),不计故障。 验证: - 主机单测 79 → 101 全绿;反向验证四连均咬(半区奇偶取反 11 条、 停摆拍置 valid 4 条、STALL_RUN_MAX 改 1 为 6 条、gap 阈值 1 条) - @build 0 错 0 警,Code 256872 → 257660 - @test PASS:adcp 122 µs → 8 µs、adc1_err=0、adc1_hw=0、 adc1_gap=27/20002 拍、adc1_seq 与 seq 恒定偏移 2 - stack_report 7/7 PASS,adcTask 832 B / 余 59%(不变) - cubemx-survival 新增 5 项审计(TIM3 分支 / T3_TRGO / DDS / DMA 挂载 / 流优先级),28 通过 0 失败 文档:ADC 采样节拍设计说明新增 §11;看门狗停摆分析 §4.4 与 §8 收口; 算法规划 L1-1/L1-3、通道映射、测试说明、各 README 同步。
This commit is contained in:
@@ -53,7 +53,8 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
- **自研代码禁用 C 库堆分配**(2026-09-18,P0-1):禁止 `malloc/calloc/realloc/strdup/free`——本机工具链 MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL 且**模拟器(宿主 CRT 堆)完全正常、照不出来**。需要动态内存只用 `pvPortMalloc/vPortFree`(FreeRTOS 堆 `configTOTAL_HEAP_SIZE`),或像配置引擎那样把分配收进平台后端(`cfg_store_alloc/free/heap_free`:MCU=FreeRTOS 堆、模拟器=宿主堆,共享核心零分配符号);分配失败必须有 `FAIL` 日志 + 自检上报,暂态峰值要写编译期预算
|
||||
- **FatFs 打开模式陷阱(2026-09-20 台架实测,`Lib/FatFs` 移植版)**:`FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(/log 写失败真根因);追加写必须用 `FA_WRITE|FA_OPEN_APPEND`。该移植的 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。标志位为旧式编号(`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(ff.c 内部定义)),与上游 FatFs R0.14 不同,勿照抄上游代码示例
|
||||
- **FatFs 文件名约束(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)**:本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**——主名≤8 字符、扩展≤3 字符、**不得以点开头**(`.fschk` 会被 `ff.c create_short` 把点后当扩展域解析超限 → `f_open` 恒返 `FR_INVALID_NAME`(6),纯 RAM 名字判定、不碰盘)。ff.c `create_short` 全部拒绝点已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`,非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入卷损坏触发集(整卷重格式化不可逆)
|
||||
- **ADC 取数只用 `tpafe5160_snapshot()`(原子快照)**(2026-10-10 修 E-44/E-45/E-58):旧的 `get_buf()` / `data_ready()` / `clear_ready()` 已从驱动**删除**——`get_buf` 只给缓冲区指针、任务再逐通道拷,两步之间被 BUSY EXTI 抢占并跨过一帧周期就拷回**两帧混合**数据(8 通道失去同一时刻性,且无错误指示);`s_ready` 电平标志有三个写者,`start_conv_irq` 触发前清 0 = **重触发本身就会抹掉一帧未消费数据**。现口径:帧序号只增不清(`tpafe5160_frame_seq()`)、取数=临界区内拷贝(`tpafe5160_snapshot(dst, count)` 返回 1/0/-1,**仅任务上下文**)、FRSTDATA 全程无跳变(芯片不驱动并行总线)由 `tpafe5160_frstdata_err()` 计数且 `tpafe5160_init()` 首帧自检直接返回 `-3` + `[ERR]`。**消费者侧(L1 采集层)**:`App/algo/adc_input.c` 提供 `adc_input_poll()`(= TPAFE 快照 + STM32 ADC1 9 通道顺序扫描)与 `adc_input_raw()`(**按语义** `adc_semantic_t` 取数,不写死下标);映射表自检在 `adc_input_init()`(查每个 STM32 语义通道确实在扫描 rank 里,防"映射到从不扫描的通道 → 恒读陈旧值")
|
||||
- **ADC 取数只用 `tpafe5160_snapshot()`(原子快照)**(2026-10-10 修 E-44/E-45/E-58):旧的 `get_buf()` / `data_ready()` / `clear_ready()` 已从驱动**删除**——`get_buf` 只给缓冲区指针、任务再逐通道拷,两步之间被 BUSY EXTI 抢占并跨过一帧周期就拷回**两帧混合**数据(8 通道失去同一时刻性,且无错误指示);`s_ready` 电平标志有三个写者,`start_conv_irq` 触发前清 0 = **重触发本身就会抹掉一帧未消费数据**。现口径:帧序号只增不清(`tpafe5160_frame_seq()`)、取数=临界区内拷贝(`tpafe5160_snapshot(dst, count)` 返回 1/0/-1,**仅任务上下文**)、FRSTDATA 全程无跳变(芯片不驱动并行总线)由 `tpafe5160_frstdata_err()` 计数且 `tpafe5160_init()` 首帧自检直接返回 `-3` + `[ERR]`。**消费者侧(L1 采集层)**:`App/algo/adc_input.c` 提供 `adc_input_poll()`(= TPAFE 快照 + **消费一帧 STM32 ADC1 DMA 乒乓缓冲**,v2.0 起零忙等)与 `adc_input_raw()`(**按语义** `adc_semantic_t` 取数,不写死下标);映射表自检在 `adc_input_init()`(查每个 STM32 语义通道确实在扫描 rank 里,防"映射到从不扫描的通道 → 恒读陈旧值");`adc_input_init()` 末尾 `HAL_ADC_Start_DMA()` **只挂一次**(此后每拍由硬件自走),失败返回 -2 并上报自检
|
||||
- **F4 上 ADC1 的硬件触发源与 DMA 流是硬约束(2026-10-10 查 CubeMX DB + 本仓 HAL 头证实,别再凭印象)**:① **ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO**——只有 T1_CC1/2/3、T2_CC2/3/4、T2_TRGO、T3_CC1、**T3_TRGO**、T4_CC4、T5_CC1/2/3、T8_CC1、**T8_TRGO**(+EXTI15);TIM6/TIM7 的 TRGO 在 F4 上**只接 DAC**(`T6_TRGO` 只存在于 CubeMX DB 的 `Expression="STM32F7"` 块里)。故"一个定时器同时驱动两片转换器"必须用 **TIM3**(PSC 83 / ARR 999 → 1 kHz,`TIM_MasterOutputTrigger=TIM_TRGO_UPDATE`)。② **ADC1 在 DMA2 上只挂 Stream0 或 Stream4**(Stream1=ADC3、Stream2/3=ADC2)→ 给 ADC1 选 **Stream4**(空闲)才不会把 SPI1_RX/USART1_RX 挤走;CubeMX 会自动挑 Stream0 并**连锁重排**在用外设,且**流的 NVIC 优先级按流号绑定**、换流即被重置为 **0**(违反"外设中断 ≥5",会抢占 FreeRTOS 临界区)→ 每次改 DMA 流后必须回 NVIC 页核对 `Src/dma.c` 的 `HAL_NVIC_SetPriority`。③ **`DMAContinuousRequests` 必须 ENABLE**(DDS 位;关掉 = 一轮 9 通道只发**一次** DMA 请求,静默丢 8 路,不报错)。④ DMA 目的缓冲**必须在 SRAM**(ADC1 走 DMA2,**DMA2 不可达 CCM 0x10000000**)→ 加大缓冲后跑 `.map` 复核地址。⑤ 消费侧用 **乒乓双缓冲**(DMA 长度 = 2×9,半传输/传输完成两个回调各代表一帧就绪),半区下标由**完成计数的奇偶**推出(只读一个 32 位计数 → 与 ISR 无竞态);STM32 侧数据因此**固定滞后 1 拍**
|
||||
- **STM32F4 ADC 上电必须自校准(2026-10-10 补,此前项目从未做过)**:CubeMX **不生成**这一步,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头 `stm32f407xx.h` **没有** `ADC_CR2_CAL`(bit3)→ 只能按 RM0090 §13.3.6 手写:`Src/adc.c` 的 `adc1_calibrate()`(`USER CODE BEGIN 1` 区,自建 `ADC1_CAL_BIT`)+ `MX_ADC1_Init()` 末尾 `USER CODE BEGIN ADC1_Init 2` 调用;时序 = 清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零。缺它**不报任何错**,只是读数带失调(实测校准让 ADC1 六路同向偏移 +10~+63 LSB,TPAFE 侧不变)。审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底。⚠ 顺带一条编译坑:**块注释里别写 `JEXT*/EXT*` 这类串**——其中的 `*/` 会提前闭合注释,报 `#7: unrecognized token`
|
||||
- **接 ADC / RS-485 消费者前的强制门禁(E-59,2026-10-10 固化)**:`adc_task` / `rs485_task` 从空桩转为真实消费者**之前**必须确认——① 驱动契约已修:TPAFE **E-44 / E-45 / E-58 已修 ✅**、RS485 **E-36 / E-37 已修 ✅** 但 **E-50(单缓冲无同步 → 包撕裂)未修 ❌**、**E-49(TX 超时拉 DE)未修 ❌**;② 栈预算:建任务前后各跑一次 `scripts/stack_report.py`。**`adc_task` 已过闸(B1,2026-10-10)**:消费者本体 + `adcTask`(512 words / **AboveNormal**——P1-5 方案 a 提优先级,`USER CODE` 区手工创建,同 timeSyncTask)+ 栈预算(静态 **560 B** / 余 **72%**,B1 实测;P1-5 后复测 **832 B** / 余 **59%**,见下)+ IWDG 心跳(`TASK_WD_ADC`)均已落地;**`rs485_task` 仍被挡(E-50 / E-49 未修)**。另按 N-18:V0.1 上两片转换器之间**禁止**做相位/功率/功率因数类运算
|
||||
- **`DBG_*` 每条日志约 90 B Flash + 216 B 栈**(2026-10-02 S10 实测;2026-10-10 修正栈量):`dbg_log.h` 的 `DBG_INFO/ERROR/DEBUG` 在**每个调用点内联**行缓冲 `char __con_line[DBG_LINE_BUF_MAX]`(默认 = `console_log.h` 的 `DBG_CON_LINE_MAX` **192 B**)+ 时间戳缓冲 **24 B**(已并入同一数组,原为独立 32 B)+ 完整 `snprintf` 调用序列 → 单条约 **90 B Flash**(字符串常量另计)。给新模块接入日志时按"条数 × 90 B"估算 Flash 代价(实测 `sd2506_init` 加 6 条 = +576 B;`app_main` 加 3 条 = +180 B);**深调用链模块另需核算栈**——每条日志在栈上多占一份行缓冲、会进 `stack_report.py` 的最坏链:实测 defaultTask 那条链上 9 个帧各含一份 → **约 1.5 KB 纯粹是日志行缓冲**(2026-10-10 把该链 5312 → 2776 B 的主要杠杆之一)。故**日志不是免费的**——已覆盖的步骤不要再加"进入前/完成后"重复锚点,只在真正黑洞(原先静默返回)处补
|
||||
@@ -77,11 +78,11 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
|
||||
`Src/main.c`(CubeMX 初始化)→ `app_main_init` → `osKernelStart` → 7 任务(`osKernelStart` 之前全是内核启动前代码:禁调任何 FreeRTOS/CMSIS-RTOS API,见「平台约束」)
|
||||
|
||||
- `app_main_init`(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟;RTC 故障时降级默认时间基准并返回 -1 触发"时钟芯片异常"自检;`sys_clock_get` 每 30 天惰性再基准化防 49.7 天 tick 回绕)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → **`adc_input_init`(L1 采集层)** → SD2506 诊断 → **启动 TIM6 1 kHz 采样节拍**(`HAL_TIM_Base_Start_IT(&htim6)`,2026-10-10;ISR 内触发 CONVST,见下)
|
||||
- `app_main_init`(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟;RTC 故障时降级默认时间基准并返回 -1 触发"时钟芯片异常"自检;`sys_clock_get` 每 30 天惰性再基准化防 49.7 天 tick 回绕)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → **`adc_input_init`(L1 采集层)** → SD2506 诊断 → **启动 TIM3 1 kHz 采样节拍**(`HAL_TIM_Base_Start_IT(&htim3)`,2026-10-10;ISR 内触发 CONVST + 同一事件经 TRGO 触发 ADC1,见下)
|
||||
- **就绪屏障**(`App/app/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY`(netTask 置位)/ `STORAGE_READY`(defaultTask:卷挂载 + 网络配置加载完成)/ `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待
|
||||
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本 → `net_init` + PHY 强制 100M 全双工 → 递增网络代次号)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 立即重建网络(新 IP 免重启生效,各 TCP 服务据代次号重建监听)
|
||||
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,`/config.ini` 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(`net_cfg_load`;旧档迁移已于 2026-10-10 删除,见「模块约定」网口配置条)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3)
|
||||
- adcTask(**B1,2026-10-10;L1 层 v2.3,2026-10-10;P1-5 事件驱动消费,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **STM32 ADC1 9 通道软件触发顺序扫描**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM6 更新中断硬件触发**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM6 分支内调 `tpafe5160_start_conv_irq()`,2026-10-10);**消费唤醒与该节拍同源**——同一 TIM6 分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM6 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,512 words / **AboveNormal**),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md`
|
||||
- adcTask(**B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费;v2.8 节拍源 TIM3 + STM32 侧 DMA,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **消费一帧 STM32 ADC1 DMA 双缓冲**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM3 更新事件硬件提供**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`;同一更新事件经 **TRGO** 触发 ADC1 一轮 9 通道转换 → 两片转换器同源对齐);**消费唤醒与该节拍同源**——同一分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM3 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,512 words / **AboveNormal**),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md`
|
||||
- **采集与算法层(L1/L2)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 扫描);L2 相量层待建。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md`
|
||||
- 全景 `docs/架构设计/项目架构总览.md`(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织)
|
||||
|
||||
|
||||
+16
-10
@@ -11,7 +11,7 @@
|
||||
|
||||
| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
|
||||
|---|---|---|---|---|
|
||||
| `adc_input.h` / `adc_input.c` | L1 | **统一语义取数层**:把两片转换器的原始码值按语义(`UA…IN`/温度)暴露,并驱动 STM32 ADC1 9 通道顺序扫描 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
|
||||
| `adc_input.h` / `adc_input.c` | L1 | **统一语义取数层**:把两片转换器的原始码值按语义(`UA…IN`/温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓双缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
|
||||
| `sample_ring.h` / `sample_ring.c` | L2 输入 | **采样环缓冲**:按语义通道保存最近 2 个周波(40 点)采样值,供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 滑动窗供数) |
|
||||
| `phasor.h` / `phasor.c` | L2 | **全周傅氏相量计算**:一周波 20 点单频 DFT → 基波/谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
|
||||
|
||||
@@ -19,14 +19,17 @@
|
||||
`sample_ring.c` / `phasor.c` **零平台依赖**(只用 `math.h` / `stddef.h` 与本目录头);
|
||||
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
|
||||
## 2. 数据流(一拍 = TIM6 1 kHz 节拍)
|
||||
## 2. 数据流(一拍 = TIM3 1 kHz 节拍)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
TIM6["TIM6 更新中断 1 kHz<br/>Src/main.c → tpafe5160_start_conv_irq()<br/>+ adc_tick_irq_notify()"] --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
|
||||
TIM6 -. "ADC_WAKE_FLAG<br/>osThreadFlagsWait" .-> TASK
|
||||
TIM3["TIM3 更新事件 1 kHz(PSC 83 / ARR 999)<br/>ISR: tpafe5160_start_conv_irq() + adc_tick_irq_notify()<br/>同一事件经 TRGO 触发 ADC1"] --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
|
||||
TIM3 --> ADC["STM32 ADC1 一轮 9 通道(≈67 us)<br/>DMA2_Stream4 循环模式自动搬运"]
|
||||
ADC --> PP["乒乓双缓冲 s_stm32_dma[2×9]<br/>半传输 / 传输完成回调各推进 seq"]
|
||||
TIM3 -. "ADC_WAKE_FLAG<br/>osThreadFlagsWait" .-> TASK
|
||||
TPAFE --> POLL
|
||||
TASK["adcTask(事件驱动唤醒,非定时轮询)<br/>App/task/adc_task.c"] --> POLL["adc_input_poll()<br/>TPAFE 快照 + STM32 ADC1 9 通道扫描"]
|
||||
PP --> POLL
|
||||
TASK["adcTask(事件驱动唤醒,非定时轮询)<br/>App/task/adc_task.c"] --> POLL["adc_input_poll()<br/>TPAFE 快照 + 消费刚完成的那半区(零忙等)"]
|
||||
POLL --> RAW["adc_input_raw(ch) × 9<br/>按语义取原始码值"]
|
||||
RAW --> RING["sample_ring_push_frame()<br/>整帧入环(40 点窗)"]
|
||||
RING --> RUN["phasor_run()<br/>逐实例 20 点重算 + 双区切换"]
|
||||
@@ -48,11 +51,12 @@ flowchart TD
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `adc_input_init()` | 清缓冲与统计 + **映射表自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内) | 内核启动前调用;返回 `-1` = 映射指向了从不扫描的通道(会恒读陈旧值) |
|
||||
| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ STM32 ADC1 9 通道软件触发顺序扫描 | **仅任务上下文**(快照走临界区);只搬码值,不做标定/运算 |
|
||||
| `adc_input_init()` | 清缓冲与统计 + **映射表自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内)+ `HAL_ADC_Start_DMA()` 挂载乒乓缓冲(**只挂一次**) | 内核启动前调用;`-1` = 映射指向了从不扫描的通道(会恒读陈旧值);`-2` = DMA 挂载失败 |
|
||||
| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ **消费刚完成的那半区**(`stm32_take()`,按 `seq` 奇偶选半区,零忙等) | **仅任务上下文**(快照走临界区);只搬码值,不做标定/运算;STM32 侧固定滞后 1 拍 |
|
||||
| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 0..4095) | 消费侧取数**唯一入口**;须先成功 `poll()`;`-2` = 该来源尚无有效数据 |
|
||||
| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标/通道号) | 供自检注入、UI 通道序;返回静态地址,勿改 |
|
||||
| `adc_input_stm32_rc()` / `adc_input_stm32_errs()` | STM32 侧最近扫描状态码 / 累计失败次数 | 心跳日志观测面(`adc1_rc` / `adc1_err`) |
|
||||
| `adc_input_stm32_rc()` | STM32 侧最近一拍状态码(0 OK / -2 本拍无新帧) | 心跳日志 `adc1_rc` |
|
||||
| `adc_input_stm32_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧(seq 一次前进 >1)/ HAL 错误回调 | 心跳日志 `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`——四类成因不同,**勿混读** |
|
||||
|
||||
**要点**
|
||||
|
||||
@@ -60,7 +64,9 @@ flowchart TD
|
||||
`UA/UN/IA` → TPAFE `buf[6]/[5]/[2]`;`UB/UC/IB/IC/IN/TEMP` → ADC1 `CH14/3/6/1/2/5`。
|
||||
**V0.2 改版只改这张表的内容**,本层 API 与上层语义不动。
|
||||
- STM32 侧是**顺序扫描**(Rank1..8 = CH0..CH7、Rank9 = CH14,与 `MX_ADC1_Init()` 严格一致),
|
||||
9 个通道**不是同一时刻**;单通道轮询超时 2 ms,超时判失败、绝不拖垮消费节拍。
|
||||
9 个通道**不是同一时刻**(一轮 ≈67 µs)。v2.0 起这一轮由 **TIM3 TRGO 硬件触发 + DMA 自动搬运**,
|
||||
消费侧**零忙等**(旧实现逐通道轮询、单通道超时 2 ms,已于 2026-10-10 移除)。
|
||||
代价:STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE 侧本来就没有同刻性(N-18),无新增误差。
|
||||
- 本文件是本层**唯一**允许出现 HAL 调用的地方(它必须驱动 ADC1);新增模块若不需要外设,
|
||||
请保持 `sample_ring` / `phasor` 那种零依赖形态,才能免桩单测。
|
||||
|
||||
@@ -165,5 +171,5 @@ flowchart TD
|
||||
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
|
||||
- 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md`
|
||||
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md`
|
||||
- 采样节拍(TIM6 1 kHz):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 相量层单测细则:`docs/测试说明/相量层单元测试说明.md`
|
||||
|
||||
+191
-51
@@ -1,12 +1,17 @@
|
||||
/*
|
||||
* 模块名称:ADC 输入层(L1 采集与预处理)
|
||||
* 模块功能:语义通道映射(L1-4)+ STM32 ADC1 顺序扫描(L1-3)+ TPAFE 快照消费。
|
||||
* 模块功能:语义通道映射(L1-4)+ STM32 ADC1 采样消费(L1-3,TIM3 TRGO 触发 +
|
||||
* DMA 双缓冲)+ TPAFE 快照消费。
|
||||
* 消费侧经 adc_input_raw() 按语义取数,不写死下标。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
|
||||
* v2.0 2026-10-10 王建锋 STM32 侧由"软件触发 + 逐通道轮询"改为
|
||||
* "TIM3 TRGO 硬件触发 + DMA 双缓冲消费":本层不再触发
|
||||
* 也不等待转换(消除最坏 2~3 ms 忙等);新增三个 HAL
|
||||
* 回调(ISR 内只计数)与 seq / hw_errs 观测量
|
||||
*/
|
||||
|
||||
#include "adc_input.h"
|
||||
@@ -16,11 +21,17 @@
|
||||
#include "adc.h" /* hadc1:MX_ADC1_Init() 已配 9 通道 / 144 cycles / 软件触发 */
|
||||
#include "tpafe5160.h" /* tpafe5160_snapshot():临界区原子快照(AGENTS 铁律) */
|
||||
|
||||
/* STM32 ADC1 单次转换轮询超时(ms)。单通道 ≈ 7.4 µs(144+12 cycles @ 21 MHz),
|
||||
* 2 ms 余量充裕;超时即判本次扫描失败,绝不长时间阻塞消费节拍 */
|
||||
#define ADC_INPUT_POLL_TIMEOUT_MS 2U
|
||||
/* STM32 ADC1 通道号上限(F4 上 ADC_CHANNEL_0..15 的宏值即通道号 0..15) */
|
||||
#define ADC_INPUT_STM32_CH_MAX 16U
|
||||
/* DMA 搬运缓冲长度 = 2 x 9 = 18:**双缓冲(乒乓)**。
|
||||
* ADC1 配 ExternalTrigConv=TIM3_TRGO + DMAContinuousRequests=ENABLE + DMA CIRCULAR,
|
||||
* 每拍(TIM3 TRGO,1 kHz)自动搬 9 个 → 每 2 拍绕一圈:半传输中断 = 前半 9 个就绪,
|
||||
* 传输完成中断 = 后半 9 个就绪。消费侧永远读"刚完成的那一半",而 DMA 正在写另一半
|
||||
* → 无撕裂、无需临界区。代价:STM32 侧数据固定滞后 1 拍(1 ms),9 路同一延迟,
|
||||
* 对 20 点 DFT 窗是纯传输延迟、不引入通道间偏差。 */
|
||||
#define ADC_INPUT_STM32_DMA_LEN (2U * ADC_INPUT_STM32_CH_NUM)
|
||||
/* 连续"无新帧"达到该拍数才计一次停摆故障(单拍是相位伪影,见 stm32_take 注释) */
|
||||
#define ADC_INPUT_STALL_RUN_MAX 2U
|
||||
|
||||
/* ===== 语义映射表(L1-4)=====
|
||||
* 权威口径:docs/硬件平台/ADC通道映射说明.md(2026-10-09 硬件确认的语义分配)。
|
||||
@@ -49,6 +60,17 @@ static const uint8_t k_stm32_rank_ch[ADC_INPUT_STM32_CH_NUM] = {
|
||||
|
||||
/* STM32 ADC1 最新一帧原始码值,按 ADC1 通道号索引(0..15) */
|
||||
static uint16_t s_stm32_raw[ADC_INPUT_STM32_CH_MAX];
|
||||
/* DMA 目的缓冲(乒乓两半区,按 Rank 序 0..8 / 9..17)。
|
||||
* ⚠ **必须落在 SRAM(0x20000000 段)**:ADC1 挂的是 **DMA2**,而 F4 的 DMA2 不可达
|
||||
* CCM(0x10000000)。实测 .map 里本文件 .bss 在 RW_IRAM1 → 普通 static 即可;
|
||||
* 日后加大缓冲或改链接段时**必须用 .map 复核地址**——放错不报编译错、不报运行错,
|
||||
* 只是数据永远搬不回来。 */
|
||||
static uint16_t s_stm32_dma[ADC_INPUT_STM32_DMA_LEN];
|
||||
static volatile uint32_t s_dma_seq; /* DMA 半帧/整帧完成次数(ISR 递增,只增不清) */
|
||||
static uint32_t s_dma_seq_seen; /* 消费侧上次看到的完成计数 */
|
||||
static volatile uint32_t s_stm32_hw_errs; /* HAL ADC/DMA 错误回调累计(ISR 写) */
|
||||
static uint32_t s_stm32_gaps; /* 丢帧:seq 一次前进 >1(消费者超拍漏读)*/
|
||||
static uint8_t s_stall_run; /* 连续"无新帧"拍数(相位伪影只会是 1 拍)*/
|
||||
/* TPAFE5160 最新一帧原始码值(16 位二进制补码),按驱动 buf 下标索引 */
|
||||
static int16_t s_tpafe_raw[ADC_INPUT_TPAFE_CH_NUM];
|
||||
static uint8_t s_tpafe_valid; /* TPAFE 侧是否已有有效帧 */
|
||||
@@ -57,49 +79,59 @@ static int s_stm32_rc; /* STM32 最近一次扫描状态码 */
|
||||
static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */
|
||||
|
||||
/*
|
||||
* 函数功能:STM32 ADC1 顺序扫描一次(L1-3)
|
||||
* 函数功能:消费一帧 STM32 ADC1 采样(L1-3,v2.0 DMA 双缓冲版)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功;-1 - 启动失败;-2 - 转换轮询超时
|
||||
* 限定条件:仅任务上下文调用;MX_ADC1_Init() 已完成
|
||||
* 函数说明:软件触发一次 9 通道扫描(ScanConvMode=ENABLE、ContinuousConvMode=
|
||||
* DISABLE),按 Rank 顺序逐通道轮询 EOC 并读 DR,结果按 ADC1 通道号
|
||||
* 存入 s_stm32_raw[]。EOCSelection=SINGLE_CONV 且未用 DMA → 每个通道
|
||||
* 产生一次 EOC,可逐通道轮询(见 HAL_ADC_PollForConversion 说明)。
|
||||
* 两条失败支路(启动失败 / 轮询超时)**都会清 s_stm32_valid**:本拍
|
||||
* STM32 侧判为无有效数据,绝不沿用上一拍旧值(详见支路内注释)。
|
||||
* 超时在**首个**失败通道处即 Stop+return,不轮询剩余通道 → 单拍耗时
|
||||
* 上界 = 一次超时(≈2~3 ms,HAL_GetTick 1 ms 粒度),不是 9 次。
|
||||
* 返 回 值:0 - 取到新一帧;-2 - 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错)
|
||||
* 限定条件:仅任务上下文调用;adc_input_init() 已挂好 DMA
|
||||
* 函数说明:本函数**既不触发转换也不等待转换**——转换由 TIM3 TRGO 硬件触发、由 DMA
|
||||
* 自动搬运。每拍只做"看完成计数是否前进 + 拷 9 个半字",故**零忙等**:
|
||||
* 原轮询版常态 adcp=122 µs、超时拍最坏 2~3 ms,现在两档都降到 ~1 µs 量级。
|
||||
* 读的是"上一拍刚完成的那一半"乒乓缓冲(DMA 此刻正在写另一半)→ 无撕裂。
|
||||
* ⚠ 计数未前进 = 本拍 STM32 侧**无有效数据**,必须清 s_stm32_valid,绝不
|
||||
* 沿用上一拍旧值(同 2026-10-10 修的陈旧帧缺陷口径)→ adc_input_raw()
|
||||
* 返 -2 → adc_task 走既有"整帧跳过"路径;下一拍自动恢复。
|
||||
*/
|
||||
static int stm32_scan(void) {
|
||||
uint8_t i;
|
||||
static int stm32_take(void) {
|
||||
uint32_t seq = s_dma_seq; /* 单次 32 位读:与 ISR 自增不产生撕裂 */
|
||||
const uint16_t *src;
|
||||
uint8_t half;
|
||||
uint8_t i;
|
||||
|
||||
if (HAL_ADC_Start(&hadc1) != HAL_OK) {
|
||||
/* 失败拍必须清有效标志,见下方超时分支说明(同一语义) */
|
||||
if (seq == s_dma_seq_seen) {
|
||||
s_stm32_valid = 0u;
|
||||
s_stm32_rc = ADC_INPUT_RC_START;
|
||||
s_stm32_errs++;
|
||||
return ADC_INPUT_RC_START;
|
||||
s_stm32_rc = ADC_INPUT_RC_STALL;
|
||||
/* 单拍"无新帧"**多半是消费者相位伪影,不是故障**:本层在 TIM3 触发瞬间被唤醒,
|
||||
* 而该拍的 DMA 完成落在 ~67 us 之后;若上一拍被日志/抢占拖过这 67 us,本拍就会
|
||||
* 看到"计数没前进"(那个完成已被上一拍消费掉)。这种伪影**只会连续 1 拍**——
|
||||
* DMA 若真停,第 2 拍仍不会前进。故只有连续 >= ADC_INPUT_STALL_RUN_MAX 拍
|
||||
* 无新帧才计入故障计数(实测开机日志突发期约 21~26 次伪影、稳态每 10 s 心跳 1 次,
|
||||
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §10.5) */
|
||||
if (s_stall_run < 255u) {
|
||||
s_stall_run++;
|
||||
}
|
||||
if (s_stall_run >= ADC_INPUT_STALL_RUN_MAX) {
|
||||
s_stm32_errs++;
|
||||
}
|
||||
return ADC_INPUT_RC_STALL;
|
||||
}
|
||||
|
||||
if ((uint32_t)(seq - s_dma_seq_seen) > 1u) {
|
||||
/* 计数一次前进 >1 = 中间有帧没被读走(乒乓缓冲已被覆盖)→ **真丢帧**,
|
||||
* 与"无新帧"分开计数:前者是消费者超拍(本任务单拍耗时 > 1 ms),
|
||||
* 后者多半是相位伪影 */
|
||||
s_stm32_gaps++;
|
||||
}
|
||||
s_stall_run = 0u;
|
||||
s_dma_seq_seen = seq;
|
||||
/* 半区下标由 seq 奇偶推出,**不再另读 ISR 变量**:DMA 从 NDTR=18 起、每拍搬 9 个,
|
||||
* 故第 1/3/5... 次完成的是前半区(下标 0)、第 2/4/6... 次是后半区(下标 1)。
|
||||
* 只读 seq 一个量 → 消费侧与 ISR 之间不存在"读到一半被换半区"的窗口。 */
|
||||
half = (uint8_t)(1u - (seq & 1u));
|
||||
src = &s_stm32_dma[((uint32_t)half * ADC_INPUT_STM32_CH_NUM)];
|
||||
|
||||
for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) {
|
||||
if (HAL_ADC_PollForConversion(&hadc1, ADC_INPUT_POLL_TIMEOUT_MS) != HAL_OK) {
|
||||
(void)HAL_ADC_Stop(&hadc1);
|
||||
/* ⚠ 失败拍**不得**沿用上一拍的旧值:本层只有一份 s_stm32_raw[],
|
||||
* 超时前几个 Rank 已写入**本拍新值**、后面几个还是**上一拍旧值** →
|
||||
* 若保持 s_stm32_valid=1,消费侧会拿到"半新半旧"的混合帧并照样压入
|
||||
* 采样环(adc_input_poll() 的返回值只由 TPAFE 决定,s_frames_skip
|
||||
* 也抓不到它),直接污染 20 点 DFT 相量窗。清标志后 adc_input_raw()
|
||||
* 对 STM32 语义通道返 -2 → adc_task 走既有的"整帧跳过"路径。
|
||||
* 下一拍扫描成功会重新置 1(自愈),无需额外恢复动作。 */
|
||||
s_stm32_valid = 0u;
|
||||
s_stm32_rc = ADC_INPUT_RC_TIMEOUT;
|
||||
s_stm32_errs++;
|
||||
return ADC_INPUT_RC_TIMEOUT;
|
||||
}
|
||||
s_stm32_raw[k_stm32_rank_ch[i]] = (uint16_t)HAL_ADC_GetValue(&hadc1);
|
||||
s_stm32_raw[k_stm32_rank_ch[i]] = src[i];
|
||||
}
|
||||
|
||||
(void)HAL_ADC_Stop(&hadc1);
|
||||
s_stm32_valid = 1u;
|
||||
s_stm32_rc = ADC_INPUT_RC_OK;
|
||||
return ADC_INPUT_RC_OK;
|
||||
@@ -145,10 +177,11 @@ static int map_check(void) {
|
||||
/*
|
||||
* 函数功能:ADC 输入层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败;-2 - DMA 挂载失败
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖);STM32 ADC1 外设已由 MX_ADC1_Init 配好
|
||||
* 函数说明:清空原始码值缓冲与统计,并做一次映射表自洽性自检;本层不重复配置
|
||||
* 外设,也不启动任何转换(STM32 侧为软件触发,按拍扫描)。
|
||||
* 函数说明:清空原始码值缓冲与统计 + 映射表自洽性自检 + **挂载一次 ADC1 DMA**
|
||||
* (此后转换由 TIM3 TRGO 硬件触发、DMA 自动搬运,本层不再触发转换)。
|
||||
* 须在启动采样节拍(app_main.c 步骤 6)之前调用。
|
||||
*/
|
||||
int adc_input_init(void) {
|
||||
uint8_t i;
|
||||
@@ -156,6 +189,9 @@ int adc_input_init(void) {
|
||||
for (i = 0u; i < ADC_INPUT_STM32_CH_MAX; i++) {
|
||||
s_stm32_raw[i] = 0u;
|
||||
}
|
||||
for (i = 0u; i < ADC_INPUT_STM32_DMA_LEN; i++) {
|
||||
s_stm32_dma[i] = 0u;
|
||||
}
|
||||
for (i = 0u; i < ADC_INPUT_TPAFE_CH_NUM; i++) {
|
||||
s_tpafe_raw[i] = 0;
|
||||
}
|
||||
@@ -164,17 +200,39 @@ int adc_input_init(void) {
|
||||
s_stm32_valid = 0u;
|
||||
s_stm32_rc = ADC_INPUT_RC_NOT_RUN;
|
||||
s_stm32_errs = 0u;
|
||||
return map_check();
|
||||
s_dma_seq = 0u;
|
||||
s_dma_seq_seen = 0u;
|
||||
s_stm32_hw_errs = 0u;
|
||||
s_stm32_gaps = 0u;
|
||||
s_stall_run = 0u;
|
||||
|
||||
if (map_check() != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 挂**一次** DMA,之后不再重启:ADC 由 TIM3 TRGO 每拍触发一轮 9 通道转换,
|
||||
* DMA 在 CIRCULAR 模式下自动搬运、硬件自动重载 NDTR → 每 2 拍绕一圈(乒乓)。
|
||||
* 内核启动前调用安全:只碰寄存器/HAL,不创建任何 CMSIS 对象(AGENTS 铁律)。
|
||||
* 此刻 TIM3 尚未启动(app_main.c 步骤 6)→ 不会发生任何转换或搬运。 */
|
||||
if (HAL_ADC_Start_DMA(&hadc1, (uint32_t *)s_stm32_dma,
|
||||
ADC_INPUT_STM32_DMA_LEN) != HAL_OK) {
|
||||
s_stm32_rc = ADC_INPUT_RC_START;
|
||||
s_stm32_errs++;
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + STM32 ADC1 顺序扫描)
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 采样)
|
||||
* 入口参数:无
|
||||
* 返 回 值:TPAFE 快照结果(1=有新帧 0=无新帧 -1=参数错)
|
||||
* 限定条件:仅**任务上下文**调用(TPAFE 快照走临界区)
|
||||
* 函数说明:先取 TPAFE 一帧,再做 STM32 ADC1 扫描;两者相互独立,STM32 扫描
|
||||
* 失败不影响 TPAFE 快照与返回值语义(STM32 侧状态另由
|
||||
* adc_input_stm32_rc() 暴露)。
|
||||
* 函数说明:先取 TPAFE 一帧,再消费 STM32 侧最近一帧 DMA 双缓冲;两者相互独立,
|
||||
* STM32 侧无新帧不影响 TPAFE 快照与返回值语义(其状态另由
|
||||
* adc_input_stm32_rc() / adc_input_stm32_seq() 暴露)。
|
||||
* ⚠ STM32 侧数据比 TPAFE 侧**滞后 1 拍**(乒乓双缓冲的固有延迟,
|
||||
* 9 路一致);按 N-18,V0.1 本就禁止跨转换器做矢量运算。
|
||||
*/
|
||||
int adc_input_poll(void) {
|
||||
int r = tpafe5160_snapshot(s_tpafe_raw, (uint8_t)ADC_INPUT_TPAFE_CH_NUM);
|
||||
@@ -183,7 +241,7 @@ int adc_input_poll(void) {
|
||||
s_tpafe_valid = 1u;
|
||||
}
|
||||
|
||||
(void)stm32_scan();
|
||||
(void)stm32_take();
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -240,12 +298,94 @@ int adc_input_stm32_rc(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 扫描累计失败次数
|
||||
* 函数功能:读取 STM32 ADC1 停摆故障累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计失败次数(启动失败或转换超时均计入)
|
||||
* 返 回 值:DMA 挂载失败次数 + "连续 >=2 拍无新帧"的次数
|
||||
* 限定条件:无
|
||||
* 函数说明:单调递增,供心跳日志观测。
|
||||
* 函数说明:单调递增,供心跳日志观测。单拍无新帧(相位伪影)**不计入**,
|
||||
* 丢帧由 adc_input_stm32_gaps() 单独计数。
|
||||
*/
|
||||
uint32_t adc_input_stm32_errs(void) {
|
||||
return s_stm32_errs;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的丢帧次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被乒乓缓冲覆盖)
|
||||
* 限定条件:无
|
||||
* 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如突发日志突发期);
|
||||
* 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障。
|
||||
*/
|
||||
uint32_t adc_input_stm32_gaps(void) {
|
||||
return s_stm32_gaps;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 DMA 整帧完成计数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 半区完成中断 + 传输完成中断的累计次数(每拍 +1)
|
||||
* 限定条件:无
|
||||
* 函数说明:这是 STM32 侧"节拍是否还在喂数据"的直接观测量:与 TPAFE 的
|
||||
* tpafe5160_frame_seq() 对称。它若停增而 TIM3 仍在跑 → DMA/ADC 侧停摆。
|
||||
*/
|
||||
uint32_t adc_input_stm32_seq(void) {
|
||||
return s_dma_seq;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 HAL 层错误回调累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:HAL_ADC_ErrorCallback 触发次数(DMA 传输错误等)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 adc_input_stm32_errs()(消费侧"无新帧")分开计数,便于区分
|
||||
* "节拍源停了"与"DMA 出错"两类故障。
|
||||
*/
|
||||
uint32_t adc_input_stm32_hw_errs(void) {
|
||||
return s_stm32_hw_errs;
|
||||
}
|
||||
|
||||
/* ===== HAL 回调(ISR 上下文)=====
|
||||
* ⚠ 三个回调都在中断里跑(DMA2_Stream4_IRQn,抢占优先级 6)→ **只允许**写 volatile
|
||||
* 计数/标志,不得调用任何 CMSIS-RTOS API、不得日志、不得拷贝大缓冲。
|
||||
* 搬运与消费全在任务侧(stm32_take())。 */
|
||||
|
||||
/*
|
||||
* 函数功能:DMA 传输完成回调 = 后半区(下标 1)9 个采样就绪
|
||||
* 入口参数:hadc - 触发回调的 ADC 句柄
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现(本板只有 ADC1)
|
||||
* 函数说明:见 stm32_take() 的奇偶推半区口径。
|
||||
*/
|
||||
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
|
||||
if (hadc == &hadc1) {
|
||||
s_dma_seq++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:DMA 半传输完成回调 = 前半区(下标 0)9 个采样就绪
|
||||
* 入口参数:hadc - 触发回调的 ADC 句柄
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现
|
||||
* 函数说明:DMA 长度 18 = 2 x 9,故半传输即"一帧就绪",与传输完成交替出现。
|
||||
*/
|
||||
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
|
||||
if (hadc == &hadc1) {
|
||||
s_dma_seq++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ADC/DMA 错误回调
|
||||
* 入口参数:hadc - 触发回调的 ADC 句柄
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现
|
||||
* 函数说明:只计数。错误后 DMA 可能已停 → 消费侧会看到 s_dma_seq 停增(rc=STALL、
|
||||
* 每拍 skip),并由 IWDG 心跳日志暴露,不在 ISR 里尝试恢复。
|
||||
*/
|
||||
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc) {
|
||||
if (hadc == &hadc1) {
|
||||
s_stm32_hw_errs++;
|
||||
}
|
||||
}
|
||||
|
||||
+52
-16
@@ -3,7 +3,8 @@
|
||||
* 模块功能:把两片转换器的原始码值按**语义**(UA/UB/UC/UN/IA/IB/IC/IN/温度)统一
|
||||
* 暴露,使上层(UI / 自检 / 保护算法)**按语义取数,不写死下标**:
|
||||
* ① 语义通道映射表(L1-4)——权威口径 docs/硬件平台/ADC通道映射说明.md;
|
||||
* ② STM32 ADC1 9 通道软件触发顺序扫描(L1-3)。
|
||||
* ② STM32 ADC1 9 通道采样(L1-3)——由 **TIM3 TRGO 硬件触发 + DMA 双缓冲
|
||||
* 搬运**(v2.0),本层只消费、不触发、不等待。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷 N-18):UA/UN/IA 走
|
||||
* TPAFE5160(16 位 / ±5 V 双极性),UB/UC/IB/IC/IN 走 STM32 ADC1
|
||||
* (12 位 / 0~3.3 V 单极性)。V0.2 改版(8 路电气量全部接 TPAFE)
|
||||
@@ -13,6 +14,10 @@
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
|
||||
* v2.0 2026-10-10 王建锋 STM32 侧改 TIM3 TRGO 硬件触发 + DMA 双缓冲消费:
|
||||
* 消除逐通道轮询的 2 ms 忙等;节拍源 TIM6 -> TIM3
|
||||
* (F4 上 ADC1 只认 TIM2/TIM3/TIM8 的 TRGO,TIM6 TRGO
|
||||
* 仅接 DAC);新增 adc_input_stm32_seq()/_hw_errs()
|
||||
*/
|
||||
|
||||
#ifndef ADC_INPUT_H
|
||||
@@ -56,29 +61,32 @@ typedef struct {
|
||||
/* STM32 ADC1 顺序扫描通道数(Rank 1..9) */
|
||||
#define ADC_INPUT_STM32_CH_NUM 9U
|
||||
|
||||
/* STM32 ADC1 最近一次扫描状态码 */
|
||||
#define ADC_INPUT_RC_OK (0) /* 扫描成功 */
|
||||
#define ADC_INPUT_RC_START (-1) /* HAL_ADC_Start 失败 */
|
||||
#define ADC_INPUT_RC_TIMEOUT (-2) /* 转换轮询超时 */
|
||||
#define ADC_INPUT_RC_NOT_RUN (-3) /* 尚未扫描过 */
|
||||
/* STM32 ADC1 最近一次取帧状态码(v2.0:DMA 消费,已无"轮询超时"这一档) */
|
||||
#define ADC_INPUT_RC_OK (0) /* 取到新一帧 */
|
||||
#define ADC_INPUT_RC_START (-1) /* HAL_ADC_Start_DMA 挂载失败 */
|
||||
#define ADC_INPUT_RC_STALL (-2) /* 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错) */
|
||||
#define ADC_INPUT_RC_NOT_RUN (-3) /* 尚未取过帧 */
|
||||
|
||||
/*
|
||||
* 函数功能:ADC 输入层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败(映射指向了不会被扫描的通道)
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败(映射指向了不会被扫描的通道);
|
||||
* -2 - STM32 ADC1 的 DMA 挂载失败
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖);STM32 ADC1 外设已由 MX_ADC1_Init 配好
|
||||
* 函数说明:清空原始码值缓冲与统计,并做一次映射表自洽性自检;本层不重复配置外设。
|
||||
* 函数说明:清缓冲与统计 + 映射表自洽性自检 + 挂载一次 ADC1 DMA(此后转换由
|
||||
* TIM3 TRGO 硬件触发、DMA 自动搬运)。须在启动采样节拍之前调用。
|
||||
*/
|
||||
int adc_input_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + STM32 ADC1 顺序扫描)
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 采样)
|
||||
* 入口参数:无
|
||||
* 返 回 值:TPAFE 快照结果(1=有新帧 0=无新帧 -1=参数错);STM32 侧状态另见
|
||||
* adc_input_stm32_rc()
|
||||
* adc_input_stm32_rc() / adc_input_stm32_seq()
|
||||
* 限定条件:仅**任务上下文**调用(TPAFE 快照走临界区)
|
||||
* 函数说明:本函数只搬运原始码值,不做标定/运算。STM32 侧扫描失败不阻断 TPAFE
|
||||
* 快照,也不影响返回值语义。
|
||||
* 函数说明:本函数只搬运原始码值,不做标定/运算;STM32 侧**零忙等**(不触发、
|
||||
* 不等待转换)。STM32 侧无新帧不阻断 TPAFE 快照,也不影响返回值语义。
|
||||
* ⚠ STM32 侧数据滞后 1 拍(乒乓双缓冲固有延迟,9 路一致)。
|
||||
*/
|
||||
int adc_input_poll(void);
|
||||
|
||||
@@ -103,7 +111,7 @@ const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch);
|
||||
int adc_input_raw(adc_semantic_t ch, int32_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 最近一次扫描状态码
|
||||
* 函数功能:读取 STM32 ADC1 最近一次取帧状态码
|
||||
* 入口参数:无
|
||||
* 返 回 值:ADC_INPUT_RC_xxx
|
||||
* 限定条件:无
|
||||
@@ -112,14 +120,42 @@ int adc_input_raw(adc_semantic_t ch, int32_t *out);
|
||||
int adc_input_stm32_rc(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 扫描累计失败次数
|
||||
* 函数功能:读取 STM32 ADC1 停摆故障累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计失败次数(启动失败或转换超时均计入)
|
||||
* 返 回 值:DMA 挂载失败次数 + "连续 >=2 拍无新帧"的次数
|
||||
* 限定条件:无
|
||||
* 函数说明:单调递增,供心跳日志观测。
|
||||
* 函数说明:单调递增,供心跳日志观测。单拍无新帧(消费者相位伪影)不计入。
|
||||
*/
|
||||
uint32_t adc_input_stm32_errs(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的丢帧次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被覆盖,未及消费)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 errs 分开——gap = 消费者超拍(单拍耗时 > 1 ms),
|
||||
* err = 节拍源/ DMA 停摆。两类故障处置不同,混在一起就查不了。
|
||||
*/
|
||||
uint32_t adc_input_stm32_gaps(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 DMA 整帧完成计数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 半区/整区完成中断的累计次数(正常时每拍 +1)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 tpafe5160_frame_seq() 对称的 STM32 侧"节拍是否还在喂数据"观测量。
|
||||
*/
|
||||
uint32_t adc_input_stm32_seq(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 HAL 层错误回调累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:HAL_ADC_ErrorCallback 触发次数(DMA 传输错误等)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 adc_input_stm32_errs() 分开计数,用于区分"节拍源停了"与"DMA 出错"。
|
||||
*/
|
||||
uint32_t adc_input_stm32_hw_errs(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+27
-14
@@ -17,7 +17,7 @@
|
||||
#include "main.h" /* ST_DIR4_GPIO_Port / ST_DIR4_Pin 等 CubeMX 宏 */
|
||||
#include "stm32f4xx_it.h" /* fault_handler_enable() */
|
||||
#include "usart.h" /* huart5 */
|
||||
#include "tim.h" /* htim6:1 kHz 采样节拍(TIM6 更新中断触发 CONVST) */
|
||||
#include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */
|
||||
#include "rs485.h"
|
||||
#include "sd2506.h"
|
||||
#include "sys_clock.h"
|
||||
@@ -92,7 +92,7 @@ static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
|
||||
* 3) GD5F NAND
|
||||
* 4) TPAFE5160 ADC → ADC 输入层(L1,语义映射 + STM32 ADC1)
|
||||
* 5) SD2506 诊断信息
|
||||
* 6) 启动 TIM6 1 kHz 采样节拍(ISR 内触发 CONVST)
|
||||
* 6) 启动 TIM3 1 kHz 采样节拍(ISR 内脉冲 CONVST;同一更新事件经 TRGO 硬件触发 ADC1 + DMA)
|
||||
*/
|
||||
int app_main_init(void) {
|
||||
/* 0) 使能 MemManage / BusFault / UsageFault(复位后默认只有 HardFault 使能)。
|
||||
@@ -184,15 +184,23 @@ int app_main_init(void) {
|
||||
}
|
||||
|
||||
/* 4b) ADC 输入层(L1)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检**
|
||||
* (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)。
|
||||
* STM32 ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles /
|
||||
* 软件触发,本层不重复配置、也不启动转换(软件触发按拍扫描)。
|
||||
* 须在启动采样节拍(步骤 6)之前。 */
|
||||
if (adc_input_init() != 0) {
|
||||
DBG_ERROR("ADC input layer: map self-check FAILED");
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC map fault");
|
||||
} else {
|
||||
DBG_INFO("ADC input layer init: OK");
|
||||
* (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)
|
||||
* + 挂载一次 STM32 ADC1 的 DMA(失败返回 -2)。
|
||||
* ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles /
|
||||
* **TIM3 TRGO 外部触发 + DMA 连续请求**,本层不重复配置、也不触发转换。
|
||||
* 须在启动采样节拍(步骤 6)之前——先挂 DMA 再放节拍,才不会漏第一拍。 */
|
||||
{
|
||||
int adc_rc = adc_input_init();
|
||||
|
||||
if (adc_rc == -1) {
|
||||
DBG_ERROR("ADC input layer: map self-check FAILED");
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC map fault");
|
||||
} else if (adc_rc != 0) {
|
||||
DBG_ERROR("ADC input layer: DMA arm FAILED (rc=%d)", adc_rc);
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC dma fault");
|
||||
} else {
|
||||
DBG_INFO("ADC input layer init: OK");
|
||||
}
|
||||
}
|
||||
|
||||
/* 4c) 相量层(L2)初始化:清双区结果与有效标志 + 复位采样环缓冲。
|
||||
@@ -231,10 +239,15 @@ int app_main_init(void) {
|
||||
sd_vbat / 1000, (sd_vbat % 1000) / 10);
|
||||
}
|
||||
|
||||
/* 6) 启动 1 kHz 采样节拍:TIM6 更新中断 → ISR 内触发 CONVST(Src/main.c)。
|
||||
/* 6) 启动 1 kHz 采样节拍:TIM3 更新事件 → ① ISR 内脉冲 CONVST(Src/main.c)
|
||||
* ② 同一事件经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)。
|
||||
* ⚠ 必须在 tpafe5160_init() 成功之后启动,否则 ISR 会在驱动首帧自检
|
||||
* (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。 */
|
||||
(void)HAL_TIM_Base_Start_IT(&htim6);
|
||||
* (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。
|
||||
* ⚠ 节拍源 v2.0 由 TIM6 改为 **TIM3**:F4 上 ADC1 的外部触发只认
|
||||
* TIM1/TIM2/TIM3/TIM4/TIM5/TIM8 的 CC 或 TRGO,**TIM6/TIM7 的 TRGO 只接
|
||||
* DAC**(查 CubeMX DB 与本仓库 stm32f4xx_hal_adc.h 的 EXTSEL 表证实)
|
||||
* → 要让两片转换器由同一个硬件事件驱动,节拍源必须换成 TIM3。 */
|
||||
(void)HAL_TIM_Base_Start_IT(&htim3);
|
||||
|
||||
DBG_INFO("App init OK");
|
||||
return 0;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
| `default_task.c/h` | defaultTask | Normal | FTL + FatFS 挂载、配置引擎属主、syslog 唯一写者、LED 指示 |
|
||||
| `net_task.c/h` | netTask | Normal | `net_stack_bring_up` → 置 NET_READY → 循环 `net_poll` + `net_process_messages` |
|
||||
| `ui_task.c/h` | uiTask | BelowNormal | LVGL 界面(菜单 / 内容页 / 待机)+ 推流 |
|
||||
| `adc_task.c/h` | (ADC 任务) | — | ADC 采集消费:TPAFE5160 快照 + STM32 ADC1 顺序扫描,经 [`../algo/adc_input.c`](../algo/README.md) **按语义**取数(采样节拍由 TIM6 硬件触发) |
|
||||
| `adc_task.c/h` | adcTask | AboveNormal | ADC 采集消费:TPAFE5160 快照 + 消费 STM32 ADC1 DMA 乒乓缓冲一帧,经 [`../algo/adc_input.c`](../algo/README.md) **按语义**取数(采样节拍由 TIM3 硬件触发) |
|
||||
| `rs485_task.c/h` | (RS-485 任务) | — | 半双工 RS-485 协议通信 |
|
||||
| `../sync/time_sync.c` | timeSyncTask | Normal | 墙钟时间同步(实现不在本目录,见 [`../sync/README.md`](../sync/README.md)) |
|
||||
|
||||
|
||||
+45
-27
@@ -1,17 +1,17 @@
|
||||
/*
|
||||
* 模块名称:ADC Sampling Task
|
||||
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 顺序扫描
|
||||
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 DMA 乒乓帧消费
|
||||
* (经 L1 层 App/algo/adc_input.c 按语义取数)→ 压入采样环
|
||||
* (App/algo/sample_ring.c)→ 全周傅氏相量重算(App/algo/phasor.c)
|
||||
* + 帧统计(节拍由 TIM6 硬件定时器提供)
|
||||
* + 帧统计(节拍由 TIM3 硬件定时器提供)
|
||||
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
|
||||
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
|
||||
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
|
||||
* **采样节拍(v2.2,2026-10-10)**:由 **TIM6 更新中断(1 kHz)** 硬件触发——
|
||||
* **采样节拍(v2.2,2026-10-10)**:由 **TIM3 更新中断(1 kHz)** 硬件触发——
|
||||
* ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的
|
||||
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。
|
||||
* **消费唤醒(v2.7,2026-10-10,Task #32 P1-5 方案 a)**:由 1 ms `osDelayUntil`
|
||||
* 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
|
||||
* 轮询改为 **TIM3 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
|
||||
* 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级
|
||||
* 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10。
|
||||
@@ -35,7 +35,7 @@
|
||||
* 为真后落日志。写者未注册时控制台走无锁直发,与
|
||||
* defaultTask 的存储初始化日志字节级交错(台架实测);
|
||||
* 就绪前挂起上报,就绪后补报最近一帧。
|
||||
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM6 更新中断(1 kHz)硬件触发;本任务
|
||||
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM3 更新中断(1 kHz)硬件触发;本任务
|
||||
* 删除自行触发,退化为纯消费者(见文件头"采样节拍")。
|
||||
* v2.3 2026-10-10 王建锋 L1-3/L1-4:取数改走 App/algo/adc_input.c(语义映射 +
|
||||
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
|
||||
@@ -59,12 +59,17 @@
|
||||
* (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask)
|
||||
* v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费):
|
||||
* 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG,
|
||||
* osWaitForever),标志由 TIM6 ISR 经 adc_tick_irq_notify() 置位;
|
||||
* osWaitForever),标志由 TIM3 ISR 经 adc_tick_irq_notify() 置位;
|
||||
* 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要);
|
||||
* 新增"消费者就绪"门 s_consumer_ready——TIM6 在 app_main_init()
|
||||
* 新增"消费者就绪"门 s_consumer_ready——TIM3 在 app_main_init()
|
||||
* 就启动,**远早于** MX_FREERTOS_Init()/osKernelStart(),
|
||||
* ISR 不得在此之前对内核动手(同 Src/main.c 里 TIM7 的
|
||||
* 2026-10-02 实测坑)。优先级 Normal → AboveNormal(见 freertos.c)
|
||||
* v2.8 2026-10-10 王建锋 节拍源 TIM6 → **TIM3**(F4 上 ADC1 的外部触发没有
|
||||
* TIM6 TRGO,只有 TIM2/TIM3/TIM8 系 → 换 TIM3 才能让
|
||||
* 两片转换器由同一硬件事件驱动);STM32 侧改 DMA 双缓冲
|
||||
* 消费,adc_input_poll() 不再忙等;心跳日志增
|
||||
* adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧")
|
||||
*/
|
||||
|
||||
#include "adc_task.h"
|
||||
@@ -82,7 +87,7 @@
|
||||
|
||||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
|
||||
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除——
|
||||
* 消费节拍改由 TIM6 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
|
||||
* 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
|
||||
#define ADC_TASK_REPORT_MS 10000U
|
||||
|
||||
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
|
||||
@@ -96,7 +101,7 @@ static uint32_t s_frames_skip;
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
|
||||
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
|
||||
* ⚠ 为什么必须有这道门:TIM6 在 app_main_init()(Src/main.c:140)里就
|
||||
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
|
||||
* HAL_TIM_Base_Start_IT 启动,**远早于** osKernelInitialize()/MX_FREERTOS_Init()/
|
||||
* osKernelStart()(Src/main.c:144/145/148)→ "节拍源已在跑"与"消费者已存在"之间有
|
||||
* 约 100 ms 窗口,其间 adcTaskHandle 还是 NULL、调度器也还没起。
|
||||
@@ -108,10 +113,10 @@ extern osThreadId_t adcTaskHandle;
|
||||
static volatile uint8_t s_consumer_ready;
|
||||
|
||||
/*
|
||||
* 函数功能:TIM6 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
|
||||
* 函数功能:TIM3 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文。TIM6 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
|
||||
* 限定条件:仅 ISR 上下文。TIM3 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
|
||||
* (=5) → osThreadFlagsSet 走 xTaskNotifyFromISR + portYIELD_FROM_ISR,合法且立即切换
|
||||
* 函数说明:Task #32 P1-5 方案 a——ISR **只发唤醒**,全部计算留在任务里(拒绝"ISR 内计算":
|
||||
* adc_input_poll() 内含 2 ms 忙等,绝不可入 ISR)。线程标志粘滞 → 单拍偶超 1 ms 时
|
||||
@@ -170,12 +175,12 @@ int adc_task_init(void) {
|
||||
* 入口参数:arg - FreeRTOS 传入参数(未使用)
|
||||
* 返 回 值:无
|
||||
* 限定条件:adc_task_init() 已调用;内核已启动
|
||||
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道顺序扫描)
|
||||
* 并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
|
||||
* **本任务的唤醒同样由该 TIM6 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
|
||||
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 乒乓缓冲)
|
||||
* 并统计。采样节拍由 TIM3 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
|
||||
* **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
|
||||
* 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||||
* EXTI 未在读帧或 TIM6 未启动(异常),由周期心跳日志暴露。
|
||||
* ⚠ 若 TIM6 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
|
||||
* EXTI 未在读帧或 TIM3 未启动(异常),由周期心跳日志暴露。
|
||||
* ⚠ 若 TIM3 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
|
||||
* IWDG 复位。这是刻意的 fail-safe("节拍源缺失"不被静默放过),不是缺陷。
|
||||
*/
|
||||
void adc_task_func(void *arg) {
|
||||
@@ -197,12 +202,13 @@ void adc_task_func(void *arg) {
|
||||
* (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */
|
||||
task_prof_enter(TASK_PROF_ADC_BEAT);
|
||||
|
||||
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + STM32 ADC1 顺序扫描
|
||||
* (仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
|
||||
* 采样节拍由 TIM6 更新中断提供(ISR 内触发 CONVST),本任务不触发。
|
||||
* P2 探针:本段单独计时。实测常态 adcp = 122 us(9 通道 x 7.4 us + TPAFE 快照);
|
||||
* 只有 STM32 侧超时才跳到 ~2.2 ms——HAL_ADC_PollForConversion 是 2 ms 不让出的忙等,
|
||||
* 而 stm32_scan() 在**首个**超时处即 Stop+return,故单拍上界是**一次**超时(不是 9 次)。
|
||||
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + 消费一帧 STM32 ADC1
|
||||
* DMA 双缓冲(仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
|
||||
* 采样节拍由 TIM3 更新事件提供(ISR 内触发 CONVST + TRGO 触发 ADC1),
|
||||
* 本任务既不触发也不等待转换。
|
||||
* P2 探针:本段单独计时。v2.0(DMA)起本段**零忙等**——原轮询版常态
|
||||
* adcp=122 us、超时拍最坏 2~3 ms(HAL_ADC_PollForConversion 是不让出的
|
||||
* 忙等),现在两档都降到 us 量级(只剩 TPAFE 快照临界区 + 拷 9 个半字)。
|
||||
* ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */
|
||||
task_prof_enter(TASK_PROF_ADC_POLL);
|
||||
r = adc_input_poll();
|
||||
@@ -301,15 +307,27 @@ void adc_task_func(void *arg) {
|
||||
|
||||
if ((int32_t)(now - report_at) >= 0) {
|
||||
if (dbg_console_writer_ready() != 0u) {
|
||||
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u adc1_rc=%d adc1_err=%lu ring=%lu",
|
||||
/* STM32 侧观测量(v2.0 DMA 版),四类各有独立成因:
|
||||
* adc1_rc = 最近一拍取帧状态(0 OK / -2 本拍无新帧)
|
||||
* adc1_seq = DMA 整帧完成计数(正常时 ≈ seq,恒定小偏移)
|
||||
* adc1_gap = 丢帧:seq 一次前进 >1 → 本任务超拍(单拍 > 1 ms)
|
||||
* adc1_err = 停摆:连续 >=2 拍无新帧 → 节拍源/DMA 真停
|
||||
* adc1_hw = HAL ADC/DMA 错误回调
|
||||
* 判读:adc1_seq 停增而 seq 仍涨 → STM32 侧停摆;
|
||||
* adc1_gap 增长 → 本任务耗时超拍(看 adcm/adcn) */
|
||||
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u ring=%lu"
|
||||
" adc1_rc=%d adc1_err=%lu adc1_seq=%lu adc1_gap=%lu adc1_hw=%lu",
|
||||
(unsigned long)s_frames_consumed,
|
||||
(unsigned long)s_frames_idle,
|
||||
(unsigned long)s_frames_skip,
|
||||
(unsigned long)tpafe5160_frame_seq(),
|
||||
(unsigned)tpafe5160_frstdata_err(),
|
||||
(unsigned long)sample_ring_frames(),
|
||||
adc_input_stm32_rc(),
|
||||
(unsigned long)adc_input_stm32_errs(),
|
||||
(unsigned long)sample_ring_frames());
|
||||
(unsigned long)adc_input_stm32_seq(),
|
||||
(unsigned long)adc_input_stm32_gaps(),
|
||||
(unsigned long)adc_input_stm32_hw_errs());
|
||||
|
||||
log_phasors("hb");
|
||||
}
|
||||
@@ -319,13 +337,13 @@ void adc_task_func(void *arg) {
|
||||
|
||||
/* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a)
|
||||
* 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**
|
||||
* → 与 TIM6 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
|
||||
* → 与 TIM3 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
|
||||
* 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask
|
||||
* (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM6 ISR 触发 CONVST 后置位
|
||||
* (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位
|
||||
* ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至
|
||||
* AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。
|
||||
* 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear;
|
||||
* osWaitForever 无超时 = 刻意的 fail-safe(TIM6 停摆 → 无心跳 → IWDG 复位)。 */
|
||||
* osWaitForever 无超时 = 刻意的 fail-safe(TIM3 停摆 → 无心跳 → IWDG 复位)。 */
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含等待) */
|
||||
(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);
|
||||
}
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-10-10 王建锋 Task #32 P1-5 方案 a:新增 ADC_WAKE_FLAG 与
|
||||
* adc_tick_irq_notify()——消费节拍由 osDelayUntil(1ms)
|
||||
* 轮询改为 TIM6 ISR 事件驱动唤醒
|
||||
* 轮询改为 TIM3 ISR 事件驱动唤醒
|
||||
*/
|
||||
|
||||
#ifndef ADC_TASK_H
|
||||
@@ -22,12 +22,12 @@ int adc_task_init(void);
|
||||
void adc_task_func(void *arg);
|
||||
|
||||
/* Task #32 P1-5(方案 a,2026-10-10):事件驱动消费。
|
||||
* TIM6 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。
|
||||
* TIM3 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。
|
||||
* 线程标志在此是**命令位**(等待即消费),不是状态位 → 用 osFlagsWaitAny,
|
||||
* 不加 osFlagsNoClear(AGENTS「就绪屏障位必须 osFlagsNoClear」只约束状态位) */
|
||||
#define ADC_WAKE_FLAG 0x0001U
|
||||
|
||||
/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM6 分支)调用。
|
||||
/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM3 分支)调用。
|
||||
* 内部自带"消费者就绪"门与句柄判空 → 调度器启动前调用也安全(理由见 .c 注释)。
|
||||
* 收在 adc_task.c 而非 main.c 内联:main.c 不碰任何内核句柄,与 task_wd_tick_probe() 同构 */
|
||||
void adc_tick_irq_notify(void);
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
## 关键点
|
||||
|
||||
- **周期重触发由 TIM6 更新中断(1 kHz)驱动**(2026-10-10):`app_main_init` 末尾 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍,`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM6 分支内调 `tpafe5160_start_conv_irq()`(ISR 内仅两次 GPIO 写)。B1 阶段曾由 `adcTask` 每 1 ms 软触发(已改)
|
||||
- **周期重触发由 TIM3 更新事件(1 kHz)驱动**(2026-10-10):`app_main_init` 末尾 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍,`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`(ISR 内仅两次 GPIO 写)。B1 阶段曾由 `adcTask` 每 1 ms 软触发(已改)。⚠ **节拍源必须是 TIM3 而非 TIM6**:TIM3 的同一个更新事件还经 **TRGO** 硬件触发 STM32 ADC1,两片转换器由此同源对齐;F4 上 TIM6/TIM7 的 TRGO 只接 DAC,喂不到 ADC1
|
||||
- **取数只用 `tpafe5160_snapshot(int16_t *dst, uint8_t count)`**(**仅任务上下文**,内部临界区原子拷贝;返回 1=新帧 / 0=无新帧 / -1=参数无效)。配套的 `tpafe5160_frame_seq()` 给帧序号、`tpafe5160_frstdata_err()` 给"FRSTDATA 无跳变"帧计数
|
||||
- ⚠️ **不要用缓冲区指针取数**:旧的 `get_buf()` / `data_ready()` / `clear_ready()` 已于 2026-10-10 **删除**(审计 E-45 / E-58)——`get_buf` 只给指针、任务再逐通道拷,中间被 BUSY EXTI 抢占就拷回**两帧混合**数据;`s_ready` 电平标志在 `start_conv_irq` 里被清 0,等于**用重触发去丢帧**。现由**帧序号**(只增不清)+ **临界区快照**取代
|
||||
- **`tpafe5160_init()` 做首帧有效性自检**(E-44):完整跑一帧转换+读取,若整帧读下来 FRSTDATA **没有发生任何跳变**(芯片没在驱动并行总线;板上上拉会把它钉死为高)→ 返回 `TPAFE5160_FRSTDATA_ERR`(-3) 并打 `[ERR] Init FRSTDATA no-toggle`。判据刻意不锁死"哪个 RD 边沿拉高/拉低"(手册只给"RD 下降沿 → FRSTDATA 高/低"两个跳变,本板未实测边沿次序),只要求"一帧内必须跳变" → 既抓死芯片,也不会因极性假设错误误杀好芯片
|
||||
- 采样数据的消费者 `App/task/adc_task.c` **已实现**(B1,2026-10-10:`tpafe5160_snapshot()` 原子取数 + 帧统计;`adcTask` 在 `Src/freertos.c` 的 `USER CODE` 区创建)。**v2.2(2026-10-10)**:采样节拍改由 TIM6 更新中断硬件触发,本任务退化为纯消费者
|
||||
- 采样数据的消费者 `App/task/adc_task.c` **已实现**(B1,2026-10-10:`tpafe5160_snapshot()` 原子取数 + 帧统计;`adcTask` 在 `Src/freertos.c` 的 `USER CODE` 区创建)。**v2.2(2026-10-10)**:采样节拍改由定时器更新中断硬件触发,本任务退化为纯消费者;**v2.8(2026-10-10)**:节拍源改 TIM3(同时经 TRGO 触发 ADC1 + DMA),STM32 侧改为消费 DMA 乒乓缓冲,单拍耗时 122 µs → **8 µs**
|
||||
- CONVST→BUSY→RD 三段时序对**建立/保持时间敏感**,改动需对照芯片手册
|
||||
- ⚠️ 本项目 ADC 相关功能**新增任务/缓冲前须核算 RAM**(见 `docs/内存管理/`);接消费者前的强制门禁见根 `AGENTS.md`「接 ADC / RS-485 消费者前的强制门禁」
|
||||
- ⚠️ **通道语义不是通道号**:`buf[i]` 对应哪个电气量必须查 `docs/硬件平台/ADC通道映射说明.md`(`buf[2]=IA`、`buf[5]=UN`、`buf[6]=UA`);V0.1 上 8 路电气量分属两片转换器属硬件缺陷(N-18)→ **禁止**跨转换器做相位/功率/功率因数类运算
|
||||
|
||||
+2
-1
@@ -55,6 +55,7 @@ void DebugMon_Handler(void);
|
||||
void DMA1_Stream3_IRQHandler(void);
|
||||
void DMA1_Stream4_IRQHandler(void);
|
||||
void EXTI9_5_IRQHandler(void);
|
||||
void TIM3_IRQHandler(void);
|
||||
void SPI1_IRQHandler(void);
|
||||
void SPI2_IRQHandler(void);
|
||||
void USART1_IRQHandler(void);
|
||||
@@ -62,11 +63,11 @@ void USART2_IRQHandler(void);
|
||||
void USART3_IRQHandler(void);
|
||||
void UART4_IRQHandler(void);
|
||||
void UART5_IRQHandler(void);
|
||||
void TIM6_DAC_IRQHandler(void);
|
||||
void TIM7_IRQHandler(void);
|
||||
void DMA2_Stream0_IRQHandler(void);
|
||||
void DMA2_Stream2_IRQHandler(void);
|
||||
void DMA2_Stream3_IRQHandler(void);
|
||||
void DMA2_Stream4_IRQHandler(void);
|
||||
void DMA2_Stream7_IRQHandler(void);
|
||||
/* USER CODE BEGIN EFP */
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ extern "C" {
|
||||
|
||||
/* USER CODE END Includes */
|
||||
|
||||
extern TIM_HandleTypeDef htim6;
|
||||
extern TIM_HandleTypeDef htim3;
|
||||
|
||||
/* USER CODE BEGIN Private defines */
|
||||
|
||||
/* USER CODE END Private defines */
|
||||
|
||||
void MX_TIM6_Init(void);
|
||||
void MX_TIM3_Init(void);
|
||||
|
||||
/* USER CODE BEGIN Prototypes */
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ main.c
|
||||
| testTask | 2048 words | BelowNormal | 硬件测试入口 |
|
||||
| uiTask | 4096 words | BelowNormal | LVGL UI 界面 |
|
||||
| timeSyncTask | 512 words | Normal | 墙钟时间同步 |
|
||||
| adcTask | 512 words | AboveNormal | TPAFE5160 采样(TIM6 1kHz 事件驱动 + 原子快照) |
|
||||
| adcTask | 512 words | AboveNormal | TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA) |
|
||||
<!-- TASK_TABLE:END -->
|
||||
|
||||
> 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读
|
||||
|
||||
+30
-14
@@ -9,7 +9,9 @@ ADC1.Channel-7\#ChannelRegularConversion=ADC_CHANNEL_6
|
||||
ADC1.Channel-8\#ChannelRegularConversion=ADC_CHANNEL_7
|
||||
ADC1.Channel-9\#ChannelRegularConversion=ADC_CHANNEL_14
|
||||
ADC1.ClockPrescaler=ADC_CLOCK_SYNC_PCLK_DIV4
|
||||
ADC1.IPParameters=Rank-1\#ChannelRegularConversion,master,Channel-1\#ChannelRegularConversion,SamplingTime-1\#ChannelRegularConversion,NbrOfConversionFlag,ScanConvMode,Rank-2\#ChannelRegularConversion,Channel-2\#ChannelRegularConversion,SamplingTime-2\#ChannelRegularConversion,Rank-3\#ChannelRegularConversion,Channel-3\#ChannelRegularConversion,SamplingTime-3\#ChannelRegularConversion,Rank-4\#ChannelRegularConversion,Channel-4\#ChannelRegularConversion,SamplingTime-4\#ChannelRegularConversion,Rank-5\#ChannelRegularConversion,Channel-5\#ChannelRegularConversion,SamplingTime-5\#ChannelRegularConversion,Rank-6\#ChannelRegularConversion,Channel-6\#ChannelRegularConversion,SamplingTime-6\#ChannelRegularConversion,Rank-7\#ChannelRegularConversion,Channel-7\#ChannelRegularConversion,SamplingTime-7\#ChannelRegularConversion,Rank-8\#ChannelRegularConversion,Channel-8\#ChannelRegularConversion,SamplingTime-8\#ChannelRegularConversion,Rank-9\#ChannelRegularConversion,Channel-9\#ChannelRegularConversion,SamplingTime-9\#ChannelRegularConversion,NbrOfConversion,ClockPrescaler
|
||||
ADC1.DMAContinuousRequests=ENABLE
|
||||
ADC1.ExternalTrigConv=ADC_EXTERNALTRIGCONV_T3_TRGO
|
||||
ADC1.IPParameters=Rank-1\#ChannelRegularConversion,master,Channel-1\#ChannelRegularConversion,SamplingTime-1\#ChannelRegularConversion,NbrOfConversionFlag,ScanConvMode,Rank-2\#ChannelRegularConversion,Channel-2\#ChannelRegularConversion,SamplingTime-2\#ChannelRegularConversion,Rank-3\#ChannelRegularConversion,Channel-3\#ChannelRegularConversion,SamplingTime-3\#ChannelRegularConversion,Rank-4\#ChannelRegularConversion,Channel-4\#ChannelRegularConversion,SamplingTime-4\#ChannelRegularConversion,Rank-5\#ChannelRegularConversion,Channel-5\#ChannelRegularConversion,SamplingTime-5\#ChannelRegularConversion,Rank-6\#ChannelRegularConversion,Channel-6\#ChannelRegularConversion,SamplingTime-6\#ChannelRegularConversion,Rank-7\#ChannelRegularConversion,Channel-7\#ChannelRegularConversion,SamplingTime-7\#ChannelRegularConversion,Rank-8\#ChannelRegularConversion,Channel-8\#ChannelRegularConversion,SamplingTime-8\#ChannelRegularConversion,Rank-9\#ChannelRegularConversion,Channel-9\#ChannelRegularConversion,SamplingTime-9\#ChannelRegularConversion,NbrOfConversion,ClockPrescaler,DMAContinuousRequests,ExternalTrigConv
|
||||
ADC1.NbrOfConversion=9
|
||||
ADC1.NbrOfConversionFlag=1
|
||||
ADC1.Rank-1\#ChannelRegularConversion=1
|
||||
@@ -35,13 +37,24 @@ ADC1.master=1
|
||||
CAD.formats=[]
|
||||
CAD.pinconfig=Dual
|
||||
CAD.provider=Component Search Engine
|
||||
Dma.ADC1.6.Direction=DMA_PERIPH_TO_MEMORY
|
||||
Dma.ADC1.6.FIFOMode=DMA_FIFOMODE_DISABLE
|
||||
Dma.ADC1.6.Instance=DMA2_Stream4
|
||||
Dma.ADC1.6.MemDataAlignment=DMA_MDATAALIGN_HALFWORD
|
||||
Dma.ADC1.6.MemInc=DMA_MINC_ENABLE
|
||||
Dma.ADC1.6.Mode=DMA_CIRCULAR
|
||||
Dma.ADC1.6.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD
|
||||
Dma.ADC1.6.PeriphInc=DMA_PINC_DISABLE
|
||||
Dma.ADC1.6.Priority=DMA_PRIORITY_LOW
|
||||
Dma.ADC1.6.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode
|
||||
Dma.Request0=USART1_TX
|
||||
Dma.Request1=USART1_RX
|
||||
Dma.Request2=SPI2_RX
|
||||
Dma.Request3=SPI2_TX
|
||||
Dma.Request4=SPI1_RX
|
||||
Dma.Request5=SPI1_TX
|
||||
Dma.RequestsNb=6
|
||||
Dma.Request6=ADC1
|
||||
Dma.RequestsNb=7
|
||||
Dma.SPI1_RX.4.Direction=DMA_PERIPH_TO_MEMORY
|
||||
Dma.SPI1_RX.4.FIFOMode=DMA_FIFOMODE_DISABLE
|
||||
Dma.SPI1_RX.4.Instance=DMA2_Stream0
|
||||
@@ -121,7 +134,7 @@ Mcu.CPN=STM32F407ZGT6
|
||||
Mcu.Family=STM32F4
|
||||
Mcu.IP0=ADC1
|
||||
Mcu.IP1=DMA
|
||||
Mcu.IP10=TIM6
|
||||
Mcu.IP10=TIM3
|
||||
Mcu.IP11=UART4
|
||||
Mcu.IP12=UART5
|
||||
Mcu.IP13=USART1
|
||||
@@ -204,7 +217,7 @@ Mcu.Pin65=PE1
|
||||
Mcu.Pin66=VP_FREERTOS_VS_CMSIS_V2
|
||||
Mcu.Pin67=VP_IWDG_VS_IWDG
|
||||
Mcu.Pin68=VP_SYS_VS_tim7
|
||||
Mcu.Pin69=VP_TIM6_VS_ClockSourceINT
|
||||
Mcu.Pin69=VP_TIM3_VS_ClockSourceINT
|
||||
Mcu.Pin7=PA4
|
||||
Mcu.Pin8=PA5
|
||||
Mcu.Pin9=PA6
|
||||
@@ -217,9 +230,10 @@ MxDb.Version=DB.6.0.181
|
||||
NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
|
||||
NVIC.DMA1_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA1_Stream4_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.DMA2_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream4_IRQn=true\:6\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.DMA2_Stream7_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
|
||||
NVIC.EXTI9_5_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
|
||||
@@ -236,7 +250,7 @@ NVIC.SavedPendsvIrqHandlerGenerated=true
|
||||
NVIC.SavedSvcallIrqHandlerGenerated=true
|
||||
NVIC.SavedSystickIrqHandlerGenerated=true
|
||||
NVIC.SysTick_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:true\:false
|
||||
NVIC.TIM6_DAC_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
|
||||
NVIC.TIM3_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
|
||||
NVIC.TIM7_IRQn=true\:15\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.TimeBase=TIM7_IRQn
|
||||
NVIC.TimeBaseIP=TIM7
|
||||
@@ -604,7 +618,7 @@ ProjectManager.ToolChainLocation=
|
||||
ProjectManager.UAScriptAfterPath=
|
||||
ProjectManager.UAScriptBeforePath=
|
||||
ProjectManager.UnderRoot=false
|
||||
ProjectManager.functionlistsort=1-SystemClock_Config-RCC-false-HAL-false,2-MX_GPIO_Init-GPIO-false-HAL-true,3-MX_DMA_Init-DMA-false-HAL-true,4-MX_USART1_UART_Init-USART1-false-HAL-true,5-MX_SPI2_Init-SPI2-false-HAL-true,6-MX_SPI1_Init-SPI1-false-HAL-true,7-MX_I2C1_Init-I2C1-false-HAL-true,8-MX_UART4_Init-UART4-false-HAL-true,9-MX_UART5_Init-UART5-false-HAL-true,10-MX_USART2_UART_Init-USART2-false-HAL-true,11-MX_USART3_UART_Init-USART3-false-HAL-true,12-MX_IWDG_Init-IWDG-false-HAL-true,13-MX_ADC1_Init-ADC1-false-HAL-true
|
||||
ProjectManager.functionlistsort=1-SystemClock_Config-RCC-false-HAL-false,2-MX_GPIO_Init-GPIO-false-HAL-true,3-MX_DMA_Init-DMA-false-HAL-true,4-MX_USART1_UART_Init-USART1-false-HAL-true,5-MX_SPI2_Init-SPI2-false-HAL-true,6-MX_SPI1_Init-SPI1-false-HAL-true,7-MX_I2C1_Init-I2C1-false-HAL-true,8-MX_UART4_Init-UART4-false-HAL-true,9-MX_UART5_Init-UART5-false-HAL-true,10-MX_USART2_UART_Init-USART2-false-HAL-true,11-MX_USART3_UART_Init-USART3-false-HAL-true,12-MX_IWDG_Init-IWDG-false-HAL-true,13-MX_ADC1_Init-ADC1-false-HAL-true,14-MX_TIM3_Init-TIM3-false-HAL-true
|
||||
RCC.48MHZClocksFreq_Value=84000000
|
||||
RCC.AHBFreq_Value=168000000
|
||||
RCC.APB1CLKDivider=RCC_HCLK_DIV4
|
||||
@@ -670,10 +684,12 @@ SPI2.Direction=SPI_DIRECTION_2LINES
|
||||
SPI2.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler
|
||||
SPI2.Mode=SPI_MODE_MASTER
|
||||
SPI2.VirtualType=VM_MASTER
|
||||
TIM6.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE
|
||||
TIM6.IPParameters=Prescaler,Period,AutoReloadPreload
|
||||
TIM6.Period=999
|
||||
TIM6.Prescaler=83
|
||||
TIM3.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE
|
||||
TIM3.IPParameters=Prescaler,Period,AutoReloadPreload,TIM_MasterSlaveMode,TIM_MasterOutputTrigger
|
||||
TIM3.Period=999
|
||||
TIM3.Prescaler=83
|
||||
TIM3.TIM_MasterOutputTrigger=TIM_TRGO_UPDATE
|
||||
TIM3.TIM_MasterSlaveMode=TIM_MASTERSLAVEMODE_ENABLE
|
||||
UART4.IPParameters=VirtualMode
|
||||
UART4.VirtualMode=Asynchronous
|
||||
UART5.IPParameters=VirtualMode
|
||||
@@ -690,7 +706,7 @@ VP_IWDG_VS_IWDG.Mode=IWDG_Activate
|
||||
VP_IWDG_VS_IWDG.Signal=IWDG_VS_IWDG
|
||||
VP_SYS_VS_tim7.Mode=TIM7
|
||||
VP_SYS_VS_tim7.Signal=SYS_VS_tim7
|
||||
VP_TIM6_VS_ClockSourceINT.Mode=Enable_Timer
|
||||
VP_TIM6_VS_ClockSourceINT.Signal=TIM6_VS_ClockSourceINT
|
||||
VP_TIM3_VS_ClockSourceINT.Mode=Internal
|
||||
VP_TIM3_VS_ClockSourceINT.Signal=TIM3_VS_ClockSourceINT
|
||||
board=custom
|
||||
rtos.0.ip=FREERTOS
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
/* USER CODE END 0 */
|
||||
|
||||
ADC_HandleTypeDef hadc1;
|
||||
DMA_HandleTypeDef hdma_adc1;
|
||||
|
||||
/* ADC1 init function */
|
||||
void MX_ADC1_Init(void)
|
||||
@@ -48,11 +49,11 @@ void MX_ADC1_Init(void)
|
||||
hadc1.Init.ScanConvMode = ENABLE;
|
||||
hadc1.Init.ContinuousConvMode = DISABLE;
|
||||
hadc1.Init.DiscontinuousConvMode = DISABLE;
|
||||
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
|
||||
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
|
||||
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
|
||||
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
|
||||
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
|
||||
hadc1.Init.NbrOfConversion = 9;
|
||||
hadc1.Init.DMAContinuousRequests = DISABLE;
|
||||
hadc1.Init.DMAContinuousRequests = ENABLE;
|
||||
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
|
||||
if (HAL_ADC_Init(&hadc1) != HAL_OK)
|
||||
{
|
||||
@@ -190,6 +191,25 @@ void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
HAL_GPIO_Init(ADC17_UB_GPIO_Port, &GPIO_InitStruct);
|
||||
|
||||
/* ADC1 DMA Init */
|
||||
/* ADC1 Init */
|
||||
hdma_adc1.Instance = DMA2_Stream4;
|
||||
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
|
||||
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
|
||||
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
|
||||
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
|
||||
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
|
||||
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
|
||||
hdma_adc1.Init.Mode = DMA_CIRCULAR;
|
||||
hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
|
||||
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
|
||||
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
|
||||
__HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1);
|
||||
|
||||
/* USER CODE BEGIN ADC1_MspInit 1 */
|
||||
|
||||
/* USER CODE END ADC1_MspInit 1 */
|
||||
@@ -223,6 +243,8 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
|
||||
|
||||
HAL_GPIO_DeInit(ADC17_UB_GPIO_Port, ADC17_UB_Pin);
|
||||
|
||||
/* ADC1 DMA DeInit */
|
||||
HAL_DMA_DeInit(adcHandle->DMA_Handle);
|
||||
/* USER CODE BEGIN ADC1_MspDeInit 1 */
|
||||
|
||||
/* USER CODE END ADC1_MspDeInit 1 */
|
||||
|
||||
@@ -59,6 +59,9 @@ void MX_DMA_Init(void)
|
||||
/* DMA2_Stream3_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 7, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
|
||||
/* DMA2_Stream4_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream4_IRQn);
|
||||
/* DMA2_Stream7_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 9, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);
|
||||
|
||||
+9
-5
@@ -133,7 +133,7 @@ int main(void)
|
||||
MX_USART3_UART_Init();
|
||||
MX_IWDG_Init();
|
||||
MX_ADC1_Init();
|
||||
MX_TIM6_Init();
|
||||
MX_TIM3_Init();
|
||||
/* USER CODE BEGIN 2 */
|
||||
HAL_Delay(100);
|
||||
|
||||
@@ -247,15 +247,19 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||||
task_wd_tick_probe();
|
||||
}
|
||||
|
||||
/* 1 kHz 采样节拍:TIM6 更新中断内脉冲 CONVST(仅两次 GPIO 写,亚 µs),
|
||||
/* 1 kHz 采样节拍:TIM3 更新事件内脉冲 CONVST(仅两次 GPIO 写,亚 µs),
|
||||
* 再唤醒 adcTask 消费这一拍(Task #32 P1-5 方案 a:ISR 只发唤醒,计算全留在任务)。
|
||||
* TIM6 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占,
|
||||
* 同一个更新事件还经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)
|
||||
* → 两片转换器由同一硬件事件驱动,采样时刻对齐。
|
||||
* TIM3 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占,
|
||||
* 读 RD 时序不会被下一次 CONVST 脉冲打断。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10。放 USER CODE 区(L2)。
|
||||
* ⚠ 节拍源 v2.0 由 TIM6 改为 TIM3:F4 上 ADC1 的外部触发清单里没有 TIM6 TRGO
|
||||
* (TIM6/TIM7 的 TRGO 只接 DAC),详见 App/app/app_main.c 步骤 6 注释。
|
||||
* ⚠ 唤醒必须走 adc_tick_irq_notify(),**不要**在这里直接 osThreadFlagsSet:
|
||||
* TIM6 在 app_main_init() 就启动,远早于建任务与 osKernelStart,
|
||||
* TIM3 在 app_main_init() 就启动,远早于建任务与 osKernelStart,
|
||||
* "消费者就绪"门在 adc_task.c 里(同本文件 TIM7 分支 2026-10-02 的实测坑)。 */
|
||||
if (htim->Instance == TIM6)
|
||||
if (htim->Instance == TIM3)
|
||||
{
|
||||
tpafe5160_start_conv_irq();
|
||||
adc_tick_irq_notify();
|
||||
|
||||
+30
-15
@@ -238,13 +238,14 @@ __asm void UsageFault_Handler(void)
|
||||
/* USER CODE END 0 */
|
||||
|
||||
/* External variables --------------------------------------------------------*/
|
||||
extern DMA_HandleTypeDef hdma_adc1;
|
||||
extern DMA_HandleTypeDef hdma_spi1_rx;
|
||||
extern DMA_HandleTypeDef hdma_spi1_tx;
|
||||
extern DMA_HandleTypeDef hdma_spi2_rx;
|
||||
extern DMA_HandleTypeDef hdma_spi2_tx;
|
||||
extern SPI_HandleTypeDef hspi1;
|
||||
extern SPI_HandleTypeDef hspi2;
|
||||
extern TIM_HandleTypeDef htim6;
|
||||
extern TIM_HandleTypeDef htim3;
|
||||
extern DMA_HandleTypeDef hdma_usart1_tx;
|
||||
extern DMA_HandleTypeDef hdma_usart1_rx;
|
||||
extern UART_HandleTypeDef huart4;
|
||||
@@ -398,6 +399,20 @@ void EXTI9_5_IRQHandler(void)
|
||||
/* USER CODE END EXTI9_5_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles TIM3 global interrupt.
|
||||
*/
|
||||
void TIM3_IRQHandler(void)
|
||||
{
|
||||
/* USER CODE BEGIN TIM3_IRQn 0 */
|
||||
|
||||
/* USER CODE END TIM3_IRQn 0 */
|
||||
HAL_TIM_IRQHandler(&htim3);
|
||||
/* USER CODE BEGIN TIM3_IRQn 1 */
|
||||
|
||||
/* USER CODE END TIM3_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles SPI1 global interrupt.
|
||||
*/
|
||||
@@ -496,20 +511,6 @@ void UART5_IRQHandler(void)
|
||||
/* USER CODE END UART5_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles TIM6 global interrupt, DAC1 and DAC2 underrun error interrupts.
|
||||
*/
|
||||
void TIM6_DAC_IRQHandler(void)
|
||||
{
|
||||
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
|
||||
|
||||
/* USER CODE END TIM6_DAC_IRQn 0 */
|
||||
HAL_TIM_IRQHandler(&htim6);
|
||||
/* USER CODE BEGIN TIM6_DAC_IRQn 1 */
|
||||
|
||||
/* USER CODE END TIM6_DAC_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles TIM7 global interrupt.
|
||||
*/
|
||||
@@ -566,6 +567,20 @@ void DMA2_Stream3_IRQHandler(void)
|
||||
/* USER CODE END DMA2_Stream3_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles DMA2 stream4 global interrupt.
|
||||
*/
|
||||
void DMA2_Stream4_IRQHandler(void)
|
||||
{
|
||||
/* USER CODE BEGIN DMA2_Stream4_IRQn 0 */
|
||||
|
||||
/* USER CODE END DMA2_Stream4_IRQn 0 */
|
||||
HAL_DMA_IRQHandler(&hdma_adc1);
|
||||
/* USER CODE BEGIN DMA2_Stream4_IRQn 1 */
|
||||
|
||||
/* USER CODE END DMA2_Stream4_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles DMA2 stream7 global interrupt.
|
||||
*/
|
||||
|
||||
@@ -24,78 +24,85 @@
|
||||
|
||||
/* USER CODE END 0 */
|
||||
|
||||
TIM_HandleTypeDef htim6;
|
||||
TIM_HandleTypeDef htim3;
|
||||
|
||||
/* TIM6 init function */
|
||||
void MX_TIM6_Init(void)
|
||||
/* TIM3 init function */
|
||||
void MX_TIM3_Init(void)
|
||||
{
|
||||
|
||||
/* USER CODE BEGIN TIM6_Init 0 */
|
||||
/* USER CODE BEGIN TIM3_Init 0 */
|
||||
|
||||
/* USER CODE END TIM6_Init 0 */
|
||||
/* USER CODE END TIM3_Init 0 */
|
||||
|
||||
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
|
||||
TIM_MasterConfigTypeDef sMasterConfig = {0};
|
||||
|
||||
/* USER CODE BEGIN TIM6_Init 1 */
|
||||
/* USER CODE BEGIN TIM3_Init 1 */
|
||||
|
||||
/* USER CODE END TIM6_Init 1 */
|
||||
htim6.Instance = TIM6;
|
||||
htim6.Init.Prescaler = 83;
|
||||
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
|
||||
htim6.Init.Period = 999;
|
||||
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
|
||||
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
|
||||
/* USER CODE END TIM3_Init 1 */
|
||||
htim3.Instance = TIM3;
|
||||
htim3.Init.Prescaler = 83;
|
||||
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
|
||||
htim3.Init.Period = 999;
|
||||
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
|
||||
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
|
||||
if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
|
||||
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
|
||||
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
|
||||
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
|
||||
if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
/* USER CODE BEGIN TIM6_Init 2 */
|
||||
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
|
||||
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE;
|
||||
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
/* USER CODE BEGIN TIM3_Init 2 */
|
||||
|
||||
/* USER CODE END TIM6_Init 2 */
|
||||
/* USER CODE END TIM3_Init 2 */
|
||||
|
||||
}
|
||||
|
||||
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
|
||||
{
|
||||
|
||||
if(tim_baseHandle->Instance==TIM6)
|
||||
if(tim_baseHandle->Instance==TIM3)
|
||||
{
|
||||
/* USER CODE BEGIN TIM6_MspInit 0 */
|
||||
/* USER CODE BEGIN TIM3_MspInit 0 */
|
||||
|
||||
/* USER CODE END TIM6_MspInit 0 */
|
||||
/* TIM6 clock enable */
|
||||
__HAL_RCC_TIM6_CLK_ENABLE();
|
||||
/* USER CODE END TIM3_MspInit 0 */
|
||||
/* TIM3 clock enable */
|
||||
__HAL_RCC_TIM3_CLK_ENABLE();
|
||||
|
||||
/* TIM6 interrupt Init */
|
||||
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 6, 0);
|
||||
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
|
||||
/* USER CODE BEGIN TIM6_MspInit 1 */
|
||||
/* TIM3 interrupt Init */
|
||||
HAL_NVIC_SetPriority(TIM3_IRQn, 6, 0);
|
||||
HAL_NVIC_EnableIRQ(TIM3_IRQn);
|
||||
/* USER CODE BEGIN TIM3_MspInit 1 */
|
||||
|
||||
/* USER CODE END TIM6_MspInit 1 */
|
||||
/* USER CODE END TIM3_MspInit 1 */
|
||||
}
|
||||
}
|
||||
|
||||
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
|
||||
{
|
||||
|
||||
if(tim_baseHandle->Instance==TIM6)
|
||||
if(tim_baseHandle->Instance==TIM3)
|
||||
{
|
||||
/* USER CODE BEGIN TIM6_MspDeInit 0 */
|
||||
/* USER CODE BEGIN TIM3_MspDeInit 0 */
|
||||
|
||||
/* USER CODE END TIM6_MspDeInit 0 */
|
||||
/* USER CODE END TIM3_MspDeInit 0 */
|
||||
/* Peripheral clock disable */
|
||||
__HAL_RCC_TIM6_CLK_DISABLE();
|
||||
__HAL_RCC_TIM3_CLK_DISABLE();
|
||||
|
||||
/* TIM6 interrupt Deinit */
|
||||
HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn);
|
||||
/* USER CODE BEGIN TIM6_MspDeInit 1 */
|
||||
/* TIM3 interrupt Deinit */
|
||||
HAL_NVIC_DisableIRQ(TIM3_IRQn);
|
||||
/* USER CODE BEGIN TIM3_MspDeInit 1 */
|
||||
|
||||
/* USER CODE END TIM6_MspDeInit 1 */
|
||||
/* USER CODE END TIM3_MspDeInit 1 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ docs/
|
||||
| [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) |
|
||||
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
|
||||
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5) |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:现状(任务周期软触发)→ 目标(TIM6 更新中断触发 CONVST)的方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析与验收判据;**规划中(未实现)** |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析、验收判据与实测剖面;**已落地并上板闭环** |
|
||||
| [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 |
|
||||
| [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 |
|
||||
| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 |
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 273,032 B (266.63 KB) | 1,048,576 B (1024 KB) | 26.0% |
|
||||
| RW_IRAM1 | 91,336 B (89.20 KB) | 131,072 B (128 KB) | 69.7% |
|
||||
| ER_IROM1 | 273,860 B (267.44 KB) | 1,048,576 B (1024 KB) | 26.1% |
|
||||
| RW_IRAM1 | 91,488 B (89.34 KB) | 131,072 B (128 KB) | 69.8% |
|
||||
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
|
||||
| **Flash (ROM 合计)** | **273,840 B (267.4 KB)** | 1,048,576 B (1024 KB) | **26.1%** |
|
||||
| **Flash (ROM 合计)** | **274,684 B (268.2 KB)** | 1,048,576 B (1024 KB) | **26.2%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
+104
-6
@@ -240,6 +240,7 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
| 2026-10-10 | **§10.1/§10.3/§10.4 复核修正(无功能改动)** | ① 2 ms 忙等的单拍上界由"9 通道 ×2 = 18 ms"更正为**一次超时 ≈2~3 ms**(`stm32_scan()` 在首个超时处即 `Stop`+`return`);② 6 次上板对照 `adcp` = **122 µs**(超时 0 次的 5 轮)/ **2209 µs**(超时 1 次的那轮)→ 2 ms 不是常态开销;③ `adcm` 2.1 ms 峰值**归因改判**为每 10 s 一次的心跳日志(`idle=0`/20000 反证无每拍 2 ms);④ 新增超时的**正确性**代价(超时拍 STM32 侧静默沿用上一拍值,`skip` 抓不到)与硬化排序 |
|
||||
| 2026-10-10 | **补 ADC1 上电自校准(§10.4)** | 发现 F4 必需的自校准在本项目**完全缺失**(CubeMX 不生成 + 裁剪 HAL 无 `HAL_ADCEx_Calibration_Start` + 裁剪 CMSIS 头无 `ADC_CR2_CAL`)→ 在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(RM0090 §13.3.6 时序),`MX_ADC1_Init()` 末尾调用。新增审计项 `adc1-calibration-call`/`-impl`。`@build` 0 错 0 警(Code 256728→256868,+140 B);`@test` PASS,`adcp` 仍 122 µs、`adc1_err=0`;实测 STM32 侧 6 路同向偏移 +10~+63 LSB、TPAFE 侧不变 |
|
||||
| 2026-10-10 | **修"失败拍沿用旧值"(§10.4 硬化 ①)** | `stm32_scan()` 两条失败支路都清 `s_stm32_valid` → 失败拍走既有 `skip` 整帧跳过,不再产出"半新半旧"混合帧污染相量窗;下一拍成功自愈。主机单测 70 → **79** 用例(新增 TC-AI-070..078),并做反向验证(改回旧行为 3 条立即 FAIL)。`@build` 0 错 0 警(Code 256868→256872)、`@test` PASS。硬化 ②(DMA 消忙等)仍未做,并更正原表述:**F4 的 ADC EOC 没有 EXTI 通道**,只有 `ADC1_2_IRQn` |
|
||||
| 2026-10-10 | **v2.8:节拍源 TIM6→TIM3 + STM32 侧 DMA 乒乓双缓冲(§11)** | 用户选定最彻底方案(DMA + TRGO 硬件触发 + 消费上一拍半区)。更正一处硬事实:**F4 上 ADC1 无 TIM6/TIM7 TRGO**(只接 DAC)→ 节拍源改 **TIM3**(PSC 83/ARR 999,`TIM_TRGO_UPDATE`),TIM6 从 `.ioc` 整体移除;ADC1 挂 **DMA2_Stream4**(CubeMX 默认抢 Stream0 会连锁搬走 SPI1_RX/USART1_RX,且换流把 NVIC 优先级重置为 **0**,违反"外设中断 ≥5")、`DMAContinuousRequests=ENABLE`、循环模式长度 2×9 乒乓,半区由 `seq` 奇偶推出(只读一个 32 位量 → 无竞态)。`stm32_scan()` → `stm32_take()`,逐通道 `PollForConversion` 全删;故障计数拆成 `adc1_seq`/`adc1_gap`/`adc1_err`/`adc1_hw` 四类(单拍无新帧 = 相位伪影,不计故障)。实测:`adcp` **122 µs → 8 µs**、`adc1_err=0`、`adc1_hw=0`、`adc1_gap=27`/20002 拍、`adc1_seq` 与 `seq` 恒定偏移 2;主机单测 79 → **101**(四组反向验证均咬);`@build` 0 错 0 警 Code 256872→**257660**;`@test` PASS;adcTask 静态链 832 B / 余 59% 不变 |
|
||||
|
||||
---
|
||||
|
||||
@@ -318,10 +319,13 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
**硬化排序**:① ✅ **已实施(2026-10-10)**——`stm32_scan()` 两条失败支路(启动失败 / 轮询
|
||||
超时)都清 `s_stm32_valid` → 本拍 STM32 侧判为无有效数据,走既有 `skip` 整帧跳过路径,
|
||||
下一拍成功自动恢复;主机侧新增 **TC-AI-070..078** 钉死并做反向验证(改回旧行为 3 条立即
|
||||
FAIL)。② **消忙等 = DMA**:`HAL_ADC_Start_DMA(&hadc1, buf, 9)`,scan 模式下每个通道 EOC
|
||||
由 DMA 自动搬进缓冲,任务侧不再逐通道 `PollForConversion`(⚠ **F4 的 ADC EOC 没有 EXTI
|
||||
通道**,只有 `ADC1_2_IRQn`——"EOC 改 EXTI"在 F4 上不成立,别照 F3/G4 的做法抄)。
|
||||
③ 双缓冲 / 等周波触发属 V0.2 硬件项(§10.5)。
|
||||
FAIL)。② ✅ **已实施(2026-10-10)= DMA**:见 **§11**——TIM3 更新事件经 TRGO 硬件触发
|
||||
ADC1 一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)`(乒乓双缓冲)由 DMA 自动搬运,
|
||||
任务侧 `HAL_ADC_PollForConversion` **全部删除**,实测单拍 `adcp` **122 µs → 8 µs**;
|
||||
"超时拍沿用旧值"这条支路在结构上消失(不再有超时概念,改由"本拍无新帧"走同一条 `skip`
|
||||
防线)。⚠ **F4 的 ADC EOC 没有 EXTI 通道**,只有 `ADC1_2_IRQn`——"EOC 改 EXTI"在 F4 上
|
||||
不成立,别照 F3/G4 的做法抄。
|
||||
③ 双缓冲已随 ② 同一批落地;**等周波触发**属 V0.2 硬件项(§10.5 / §8)。
|
||||
- **已做的一项(2026-10-10):补 F4 ADC 上电自校准**。CubeMX **不生成**该步骤,且本仓库裁剪版
|
||||
HAL **没有** `HAL_ADCEx_Calibration_Start` → 由 `Src/adc.c` 的 `adc1_calibrate()` 实现
|
||||
(`USER CODE` 区 = L2,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底)。
|
||||
@@ -333,8 +337,12 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
|
||||
### 10.5 下一版硬件建议:DMA + 双缓冲(方案 b,用户要求记录)
|
||||
|
||||
当前硅片(STM32F407)**STM32 ADC1 已支持 TIM6-TRGO → ADC1 + DMA**;瓶颈在 **TPAFE5160 接口**,
|
||||
须先以逻辑分析仪确认其接口类型再定型:
|
||||
> **2026-10-10 更新**:本节里 **STM32 侧那一半已经落地**(§11,DMA + 乒乓双缓冲 + 定时器
|
||||
> TRGO 同源触发)。原写"STM32 ADC1 已支持 **TIM6**-TRGO → ADC1 + DMA"**是错的**——
|
||||
> F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO(只接 DAC),实际用的是 **TIM3_TRGO**;
|
||||
> 已按 §11 更正。下面剩下的部分是 **TPAFE5160 侧**的下一版硬件建议,仍未办。
|
||||
|
||||
瓶颈在 **TPAFE5160 接口**,须先以逻辑分析仪确认其接口类型再定型:
|
||||
|
||||
- **若 TPAFE 为 SPI + DRDY**(推荐先验):
|
||||
- 单 1 kHz 主时钟:TIM6 `TRGO` 同时驱动 ADC1 扫描(`ADC_ExternalTrigConv`=TIM6_TRGO)与 TPAFE `CONVST`;
|
||||
@@ -368,3 +376,93 @@ t=1000 µs 下一个 TIM6 更新事件
|
||||
**教训(与 `docs/问题记录/UI任务看门狗停摆分析.md` §9.3.1 的 I-2/I-3 同类)**:把"定时轮询"换成
|
||||
"事件驱动"时,必须单独核对**节拍源启动时刻 vs 消费者创建时刻**,否则第一次上电就会踩到
|
||||
"ISR 对还不存在的消费者动手"。
|
||||
|
||||
---
|
||||
|
||||
## 11. 节拍源改 TIM3 + STM32 侧 DMA 乒乓双缓冲(v2.8,2026-10-10,**已落地并上板闭环**)
|
||||
|
||||
> 触发背景:§10.4 硬化排序里的 ②(消忙等)与 ③(双缓冲)。用户明确选择最彻底的方案
|
||||
> ("我认为最彻底的就是 B2"):**DMA + 定时器 TRGO 硬件触发 + 消费上一拍已完成的半区**。
|
||||
|
||||
### 11.1 为什么节拍源从 TIM6 换成 TIM3
|
||||
|
||||
原规划(§2.1 / §10.5)假设"TIM6 TRGO 可触发 ADC1"——**在 F4 上不成立**。查 CubeMX 器件库
|
||||
(`db/mcu/IP/ADC-STM32F417_Modes.xml`)与 `stm32f4xx_hal_adc.h`,ADC1 的外部触发只有:
|
||||
|
||||
```
|
||||
T1_CC1/2/3、T2_CC2/3/4、T2_TRGO、T3_CC1、T3_TRGO、T4_CC4、T5_CC1/2/3、T8_CC1、T8_TRGO、EXTI15
|
||||
```
|
||||
|
||||
TIM6/TIM7 的 TRGO 在 F4 上**只喂 DAC**(`T6_TRGO` 仅出现在 DB 里 `Expression="STM32F7"` 的块中)。
|
||||
要在**同一个硬件事件**上同时驱动 TPAFE(ISR 脉冲 CONVST)与 STM32 ADC1(TRGO),
|
||||
可选的通用定时器 TRGO 只有 TIM2 / TIM3 / TIM4 / TIM5 / TIM8;TIM3 空闲且为 APB1 通用定时器 →
|
||||
**改用 TIM3**(PSC 83 / ARR 999 = 1 kHz,`TIM_MasterOutputTrigger=TIM_TRGO_UPDATE`,
|
||||
`TIM_MasterSlaveMode=ENABLE`)。TIM6 从 `.ioc` 中整体移除(`Mcu.IP` / `Mcu.Pin` / `VP_*` /
|
||||
`TIM6_DAC_IRQn` 全部清掉),`htim6` / `MX_TIM6_Init` / `TIM6_DAC_IRQHandler` 一并消失。
|
||||
|
||||
⚠ `MSM=ENABLE` 而从模式 `SMS=0`(内部时钟)→ 软件 `CEN` 启动不受影响(已核对 RM0090 与生成代码)。
|
||||
|
||||
### 11.2 DMA 选型与三个静默陷阱
|
||||
|
||||
| 项 | 结论 | 踩坑代价 |
|
||||
|---|---|---|
|
||||
| 流 | **DMA2_Stream4**(ADC1 只挂 Stream0 或 Stream4;Stream1=ADC3、Stream2/3=ADC2) | CubeMX 默认抢 Stream0 → 连锁把 **SPI1_RX→Stream2、USART1_RX→Stream5**(NAND 与调试串口的 DMA 全被搬家) |
|
||||
| 优先级 | `DMA2_Stream4=6`、`DMA2_Stream0=7`、`DMA2_Stream2=9` | **换流会把该流的 NVIC 优先级重置为 0** → 违反"外设中断 ≥5",0 抢占 FreeRTOS 临界区(`configMAX_SYSCALL_INTERRUPT_PRIORITY=5`) |
|
||||
| `DMAContinuousRequests` | **ENABLE**(DDS 位) | 关掉 = 一轮 9 通道只发**一次** DMA 请求,静默丢 8 路,不报任何错 |
|
||||
| 缓冲地址 | 必须在 **SRAM**(实测 `s_stm32_dma` @ `0x20003448`,落在 `RW_IRAM1`) | ADC1 走 **DMA2**,而 F4 的 DMA2 **不可达 CCM(0x10000000)** → 放错不报编译错、不报运行错,数据永远搬不回来 |
|
||||
|
||||
模式:`DMA_CIRCULAR` + `DMA_PRIORITY_LOW` + halfword→halfword,长度 `2 × 9 = 18`。
|
||||
|
||||
### 11.3 乒乓半区怎么选才没有竞态
|
||||
|
||||
DMA 每完成 9 个半字触发一次 **半传输/传输完成** 回调,两个回调都代表"一整帧就绪",
|
||||
各自 `s_dma_seq++`(`HAL_ADC_ConvHalfCpltCallback` / `HAL_ADC_ConvCpltCallback`,
|
||||
均带 `if (hadc == &hadc1)` 句柄守卫)。消费侧:
|
||||
|
||||
```c
|
||||
half = (uint8_t)(1u - (seq & 1u)); /* seq 奇 = 刚完成前半区,偶 = 刚完成后半区 */
|
||||
src = &s_stm32_dma[half * ADC_INPUT_STM32_CH_NUM];
|
||||
```
|
||||
|
||||
**只读一个 32 位 `seq`**(原子读)→ 半区下标由它推导,不再引入第二个 ISR 写、任务读的变量
|
||||
(若另存一个 `s_dma_half` 就有"读到新 half、读到旧 seq"的窗口)。
|
||||
代价:STM32 侧数据**固定滞后 1 拍(1 ms)**——两片转换器本来就没有同刻性(N-18),无新增误差。
|
||||
|
||||
### 11.4 可观测性:四类故障分开计数
|
||||
|
||||
| 计数 | 含义 | 判读 |
|
||||
|---|---|---|
|
||||
| `adc1_seq` | DMA 整帧完成计数 | 应 ≈ TPAFE `seq`(实测恒定偏移 2 = TIM3 启动到 DMA 挂载之间的窗口) |
|
||||
| `adc1_gap` | `seq` 一次前进 >1 → **丢帧** | 消费者超拍(单拍 > 1 ms,如心跳日志突发) |
|
||||
| `adc1_err` | **连续 ≥2 拍**无新帧 → 停摆 | 节拍源/DMA 真停;单拍无新帧是**相位伪影**(见下),不计 |
|
||||
| `adc1_hw` | `HAL_ADC_ErrorCallback` | ADC/DMA 硬件报错 |
|
||||
|
||||
**相位伪影**(实测发现,故设 `STALL_RUN_MAX=2`):任务在 TIM3 触发瞬间被唤醒,而该拍的 DMA
|
||||
完成落在 ≈67 µs 之后;若上一拍被日志/抢占拖过这 67 µs,本拍就会看到"计数没前进"(那个完成
|
||||
已被上一拍消费掉)。这种伪影**只会连续 1 拍**——DMA 若真停,第 2 拍仍不会前进。
|
||||
|
||||
### 11.5 实测(`@test` PASS,2026-10-10)
|
||||
|
||||
| 指标 | DMA 前 | DMA 后 |
|
||||
|---|---|---|
|
||||
| `adcp`(单拍消费耗时) | **122 µs**(超时拍 2209 µs) | **8 µs** |
|
||||
| `adcm`(心跳拍峰值) | 2.1 ms | 2.0 ms(仍是每 10 s 心跳日志本身) |
|
||||
| `adc1_err` | — | **0** |
|
||||
| `adc1_hw` | — | **0** |
|
||||
| `adc1_gap` / `skip` | — | 27 / 20002 拍(0.13%,集中在开机日志突发期;稳态每 10 s 心跳 +1) |
|
||||
| `adc1_seq` vs `seq` | — | 20004 vs 20006(恒定偏移 2,每拍 +1) |
|
||||
| `idle` | 0 | 0 |
|
||||
| 主机单测 | 79 | **101**(新增乒乓半区、HAL 回调守卫、gap/stall 分开计数三组) |
|
||||
| `@build` | Code 256872 | **257660**(+788 B);ZI +4 B(36 B 乒乓缓冲 − 旧变量) |
|
||||
| adcTask 静态最坏链 | 832 B / 余 59% | **832 B / 余 59%**(未变,`HAL_ADC_PollForConversion` 出链) |
|
||||
|
||||
反向验证(改错实现看用例是否咬):半区奇偶取反 → 11 条 FAIL;停摆拍置 `valid=1` → 4 条 FAIL;
|
||||
gap 阈值改 `>2` → 1 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL。
|
||||
|
||||
### 11.6 遗留与注意
|
||||
|
||||
- **启动 fail-safe 语义不变但更硬**:TIM3 未启动 → ADC1 不再被触发 → `adcTask` 永久阻塞 →
|
||||
`TASK_WD_ADC` 缺席 → IWDG 复位(设计如此)。
|
||||
- `adc1_gap` 的根治手段是**缩短心跳拍**(每 10 s 一次的多行日志),不是提高 DMA 能力。
|
||||
- 加大缓冲(如 4 路乒乓)前**必须**重跑 `.map` 确认地址仍在 `RW_IRAM1`。
|
||||
- 每次改 DMA 流之后必须回 NVIC 页核对优先级(§11.2 第三行)。
|
||||
|
||||
+1
-1
@@ -244,7 +244,7 @@ S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在
|
||||
|
||||
## 4. 待确认(需手册/硬件/产品定义,暂不直接改码)
|
||||
|
||||
1. TPAFE5160 是否有自动扫描模式(**已不影响**:1 kHz 硬件定时器触发已于 2026-10-10 由 **TIM6 更新中断**方案落地,不依赖自动扫描,见 `docs/子系统设计/ADC采样节拍设计说明.md`)。
|
||||
1. TPAFE5160 是否有自动扫描模式(**已不影响**:1 kHz 硬件定时器触发已于 2026-10-10 由 **TIM3 更新中断**方案落地,不依赖自动扫描,见 `docs/子系统设计/ADC采样节拍设计说明.md`)。
|
||||
2. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(当前宏已如实标注为"未接线占位",此处确认的是**是否接入真实门控**;B1 取数不依赖该宏)。
|
||||
3. RS-485 PD0 DE 的半双工方向语义需硬件实测确认(本次仅静态审查)。
|
||||
|
||||
|
||||
+45
-44
@@ -2,8 +2,9 @@
|
||||
|
||||
> **模块**:ADC Input Layer(L1 采集与预处理,`App/algo/adc_input.c`)
|
||||
> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证语义通道映射、
|
||||
> STM32 ADC1 顺序扫描、TPAFE 快照消费与取数守卫,零硬件、零烧录、毫秒级、不占 MCU 的 RAM。
|
||||
> **状态**:✅ 已落地(79 用例,`/W4` 0 告警,已挂 pre-commit 钩子)
|
||||
> STM32 ADC1 的 DMA 乒乓帧消费(含半区选择)、TPAFE 快照消费与取数守卫,零硬件、零烧录、
|
||||
> 毫秒级、不占 MCU 的 RAM。
|
||||
> **状态**:✅ 已落地(**101** 用例,`/W4` 0 告警,已挂 pre-commit 钩子)
|
||||
> **日期**:2026-10-10
|
||||
> **关联文档**:
|
||||
> [`../../AGENTS.md`](../../AGENTS.md)(平台约束 · CubeMX 生成区铁律)、
|
||||
@@ -24,18 +25,21 @@
|
||||
|
||||
| 半边 | 内容 | 可测性 |
|
||||
|---|---|---|
|
||||
| **纯逻辑**(被测) | ① 语义通道映射表(L1-4):`UA/UB/UC/UN/IA/IB/IC/IN/TEMP` → `(来源, 下标)`;② STM32 ADC1 9 通道软件触发顺序扫描(L1-3):**Rank 顺序 ↔ ADC1 通道号 ↔ 语义**;③ 取数守卫与 poll 契约;④ 扫描状态码 / 失败计数 | 纯查表 + 状态机,与硬件无关 → **可在 PC 上位穷举** |
|
||||
| **硬件副作用** | `HAL_ADC_Start` / `PollForConversion` / `GetValue` / `Stop`、`tpafe5160_snapshot()`(临界区) | 均为**外部符号** → 主机侧给同名强符号桩即可 |
|
||||
| **纯逻辑**(被测) | ① 语义通道映射表(L1-4):`UA/UB/UC/UN/IA/IB/IC/IN/TEMP` → `(来源, 下标)`;② STM32 ADC1 帧消费(L1-3,v2.0 DMA 模型):**Rank 顺序 ↔ ADC1 通道号 ↔ 语义** + **乒乓半区选择**;③ 取数守卫与 poll 契约;④ 状态码与四类故障计数(`seq`/`gap`/`err`/`hw`) | 纯查表 + 状态机,与硬件无关 → **可在 PC 上位穷举** |
|
||||
| **硬件副作用** | `HAL_ADC_Start_DMA()`(挂载)与被测代码自己实现的三个 HAL 弱回调 `HAL_ADC_Conv[Half]CpltCallback` / `HAL_ADC_ErrorCallback`;`tpafe5160_snapshot()`(临界区) | 挂载是**外部符号** → 主机侧给同名强符号桩;三个回调由**被测代码**定义 → 测试直接调用它们注入"DMA 完成" |
|
||||
|
||||
板端套件(`test/board/`)**无法验证本层的核心判据**:
|
||||
|
||||
- **通道映射**:板端**无信号源**(见 `docs/硬件平台/ADC通道映射说明.md` §8),无法注入已知电压去
|
||||
验证"读到的是哪只脚"。而映射表一旦写错(本项目 V0.1 有 13/17 路被置换),读到的会是
|
||||
**另一只脚**或**恒为陈旧值**,且**不会有任何报错**——这正是本层最危险的失效模式。
|
||||
- **HAL 故障路径**:`HAL_ADC_Start` 失败、转换轮询超时(2 ms)在真机上极难稳定复现。
|
||||
- **HAL 故障路径**:`HAL_ADC_Start_DMA()` 挂载失败、DMA 完成计数不前进(节拍源停摆)、
|
||||
超拍丢帧、HAL 错误回调在真机上极难稳定复现,且**读错乒乓半区**这种缺陷在板端只会表现为
|
||||
"读数偶尔怪"、无法定位。
|
||||
|
||||
主机侧的做法:给**每个 Rank 注入互不相同的码值**(第 i 个 Rank 读到 `100+i`),把
|
||||
"Rank 顺序 ↔ ADC1 通道号 ↔ 语义"整条链路**逐通道钉死**;故障路径用可注入桩一次覆盖。
|
||||
主机侧的做法:给**每个 Rank 注入互不相同的码值**(第 i 个 Rank 写成 `100+i`),把
|
||||
"Rank 顺序 ↔ ADC1 通道号 ↔ 语义"整条链路**逐通道钉死**;"DMA 完成"由测试直接调用被测代码
|
||||
实现的半传输/传输完成回调注入(`dma_beat()`),故障路径用可注入桩一次覆盖。
|
||||
|
||||
> ⚠ 本单测**只覆盖 L1-3 / L1-4**。L1-5(通道标定)本层不做,故取到的是**未标定原始码值**;
|
||||
> L1-1 / L1-2(周期触发与 TPAFE 快照链路)由 `adc_task` + TPAFE 驱动承担,板端套件覆盖。
|
||||
@@ -59,11 +63,9 @@ test/host/adc_input/adc_input_test.c
|
||||
|
||||
| 平台接口 | MCU 端 | 主机侧桩 |
|
||||
|---|---|---|
|
||||
| `hadc1` | `MX_ADC1_Init()` 配置的 ADC1 句柄 | 不透明占位对象(被测代码只透传指针,从不读字段) |
|
||||
| `HAL_ADC_Start` | 软件触发一次 9 通道扫描 | 返回 `s_start_rc`(可注入 `HAL_ERROR`);复位轮询/取数计数器 |
|
||||
| `HAL_ADC_PollForConversion` | 逐 Rank 轮询 EOC(2 ms 超时) | 第 `s_poll_fail_at` 次(0 基)返回 `HAL_ERROR`;`-1` = 永不失败 |
|
||||
| `HAL_ADC_GetValue` | 读 DR | 返回 `s_poll_vals[s_rank_idx]`(默认 `100 + rank`) |
|
||||
| `HAL_ADC_Stop` | 停止转换 | 计次 `s_stop_calls` |
|
||||
| `hadc1` / `hadc_other` | `MX_ADC1_Init()` 配置的 ADC1 句柄 | 两个占位对象:被测代码只**比较指针**(三个 HAL 回调的 `hadc == &hadc1` 守卫),从不读字段;`hadc_other` 专门用来证伪守卫 |
|
||||
| `HAL_ADC_Start_DMA` | 挂载循环 DMA,长度 2×9(此后每拍由硬件自走) | 记录 `s_start_dma_calls` / `s_dma_buf` / `s_dma_len`,返回 `s_start_dma_rc`(可注入 `HAL_ERROR`) |
|
||||
| `HAL_ADC_ConvHalfCpltCallback` / `HAL_ADC_ConvCpltCallback` / `HAL_ADC_ErrorCallback` | **由被测代码实现**(覆盖 HAL 弱符号),ISR 上下文 | 测试用 `dma_beat(base)` 直接调用:按 `seq & 1` 决定往哪个半区填 `base+i`,再触发对应回调——与消费侧的奇偶推导**互相证伪** |
|
||||
| `tpafe5160_snapshot` | 临界区原子快照最新一帧 | 返回 `s_snapshot_rc`(1/0/-1);`rc==1` 时从 `s_fake_tpafe[]` 拷贝;记录 `count` 与 `dst` |
|
||||
|
||||
桩里 `ADC_CHANNEL_0..15` 的**宏值等于通道号**(F4 HAL 即如此),故 `(uint8_t)ADC_CHANNEL_14 == 14`
|
||||
@@ -106,7 +108,7 @@ test/host/adc_input/adc_input_test.c
|
||||
|
||||
> 命名 `TC-AI-NNN`(AI = Adc Input;主机侧单测自定前缀,与板端套件缩写表互不影响)。
|
||||
> 全部在 `adc_input_test.c` 内,主机侧运行。被测源文件被 `#include` 纳入同一编译单元,
|
||||
> 故**静态函数(`map_check` / `stm32_scan`)亦可直接调用**(白盒)。
|
||||
> 故**静态函数(`map_check` / `stm32_take`)亦可直接调用**(白盒)。
|
||||
|
||||
### 2.1 初始化与复位(`adc_input_init`)
|
||||
|
||||
@@ -143,44 +145,41 @@ test/host/adc_input/adc_input_test.c
|
||||
|
||||
> 反向路径(表不自洽 → -1)**结构上不可达**,见 §4.1。
|
||||
|
||||
### 2.4 STM32 ADC1 顺序扫描(`stm32_scan`,静态)
|
||||
### 2.4 STM32 ADC1 帧消费(`stm32_take`,静态;v2.0 DMA 模型)
|
||||
|
||||
| 用例 | 断言 | 验证的设计点 |
|
||||
|---|---|---|
|
||||
| **TC-AI-021** | 全通道成功 → 返回 0 | 正常路径 |
|
||||
| **TC-AI-021** | 有新帧 → 返回 0 | 正常路径 |
|
||||
| **TC-AI-022** | `adc_input_stm32_rc() == RC_OK` | 状态码 |
|
||||
| **TC-AI-023** | 成功不计入失败计数 | 统计语义 |
|
||||
| **TC-AI-024** | `HAL_ADC_Start` / `HAL_ADC_Stop` 各一次 | 外设生命周期成对 |
|
||||
| **TC-AI-025** | 轮询 9 次、取数 9 次(Rank1..9) | 扫描通道数 |
|
||||
| **TC-AI-024** | `HAL_ADC_Start_DMA` 只在 `init()` 挂**一次** | 外设生命周期(每拍不重启) |
|
||||
| **TC-AI-025** | DMA 完成计数 `seq` = 1 | 计数只由 HAL 回调推进 |
|
||||
| **TC-AI-026** | ⭐ **Rank → 通道号映射**:`UB` = CH14 / rank9 = 108 | **本层核心判据**(板端无法验) |
|
||||
| **TC-AI-027** | `UC` = CH3 / rank4 = 103 | 同上 |
|
||||
| **TC-AI-028** | `IB` = CH6 / rank7 = 106 | 同上 |
|
||||
| **TC-AI-029** | `IC` = CH1 / rank2 = 101 | 同上 |
|
||||
| **TC-AI-02A** | `IN` = CH2 / rank3 = 102 | 同上 |
|
||||
| **TC-AI-02B** | `TEMP` = CH5 / rank6 = 105 | 同上 |
|
||||
| **TC-AI-02C** | 扫描后 `UA`(TPAFE)仍为 -2 | 两来源互相独立 |
|
||||
| **TC-AI-030** | `HAL_ADC_Start` 返回 `HAL_ERROR` → 返回 -1 | 启动失败 |
|
||||
| **TC-AI-031** | 状态码 = `RC_START` | 故障可观测 |
|
||||
| **TC-AI-032** | 失败计数 +1 | 故障可观测 |
|
||||
| **TC-AI-033** | 启动失败**不**调用 `Stop` | 不对外设做多余操作 |
|
||||
| **TC-AI-034** | 启动失败后 STM32 侧仍无有效数据 | 不产出半帧 |
|
||||
| **TC-AI-035** | 第 5 个 Rank 轮询超时 → 返回 -2 | 超时路径 |
|
||||
| **TC-AI-036** | 状态码 = `RC_TIMEOUT` | 故障可观测 |
|
||||
| **TC-AI-037** | 超时计入失败计数 | 故障可观测 |
|
||||
| **TC-AI-038** | 超时前已 `Start` → 调 `Stop` 收尾 | 资源不泄漏 |
|
||||
| **TC-AI-039** | 超时后**立即退出**(只轮询 5 次,不继续扫完) | 不长时间阻塞 1 ms 保护节拍 |
|
||||
| **TC-AI-039b** | 超时后 STM32 侧无有效数据 | 不产出半帧 |
|
||||
| **TC-AI-03A** | 再失败一次 → 计数 = 2 | 跨次单调累计 |
|
||||
| **TC-AI-070..078** | ⭐ **失败拍不得沿用上一拍旧值**:先成功一拍让 STM32 侧进入有效态(`UB`=108),再注入超时拍 / `Start` 失败拍 → `adc_input_raw()` 必须返 **-2**;恢复拍重新可取数 | **陈旧帧**(2026-10-10 真机实测发现的缺陷,见下注) |
|
||||
| **TC-AI-02C** | 取帧后 `UA`(TPAFE)仍为 -2 | 两来源互相独立 |
|
||||
| **TC-AI-080..085** | ⭐ **乒乓双缓冲**:往"正在被 DMA 写的另一半区"填 900/800 系干扰值,`stm32_take()` 仍必须读到**刚完成那一半**(108 / 208) | **撕裂帧防线**(读错半区 = 半新半旧混合帧) |
|
||||
| **TC-AI-086..089** | 连投两帧不消费 → 取到最新帧(408)且 `gaps()` = 1、`errs()` = 0 | **丢帧与停摆分开计数**(消费者超拍 ≠ 节拍源停) |
|
||||
| **TC-AI-090..09A** | `HAL_ADC_ErrorCallback(&hadc1)` ×2 → `hw_errs()` = 2;用**别的句柄**调三个回调 → `seq` / `hw_errs` 均不动 | 三个 HAL 弱回调的 `hadc == &hadc1` **句柄守卫** |
|
||||
| **TC-AI-030..034** | `HAL_ADC_Start_DMA` 返回 `HAL_ERROR` → `init()` 返回 **-2**、状态码 `RC_START`、计数 +1、STM32 侧无有效数据 | 挂载失败(一次性,非每拍) |
|
||||
| **TC-AI-035..039** | 无任何 DMA 完成 → `RC_STALL`、**单拍不计故障**、`seq` 不前进、STM32 侧无有效数据 | 无新帧路径 + 相位伪影不误报 |
|
||||
| **TC-AI-03A..03E** | 连续两拍无新帧 → `errs()` = 1;第三拍 → 2;恢复拍成功且 `gaps()` 仍 0 | 停摆判据 = **连续 ≥2 拍**;跨次单调累计 |
|
||||
| **TC-AI-070..078** | ⭐ **停摆拍不得沿用上一拍旧值**:先成功一拍让 STM32 侧进入有效态(`UB`=108),再注入无新帧拍 → `adc_input_raw()` 必须返 **-2**;恢复拍重新可取数 | **陈旧帧**(2026-10-10 真机实测发现的缺陷,见下注) |
|
||||
|
||||
> ⚠ **为什么 TC-AI-034 / 039b 照不出这条缺陷**:两者都是"`adc_input_init()` 后**立刻**失败",
|
||||
> 此时 `s_stm32_valid` 本来就是 0,断言 -2 恒成立。真正的缺陷路径是**成功过之后再失败**:
|
||||
> 本层只有一份 `s_stm32_raw[]`,超时拍里"超时前几个 Rank 已写本拍新值、后面几个还是上一拍
|
||||
> 旧值",而有效标志此前**只在成功时置 1、失败时从不清零** → 消费侧照样取数、照样压入采样环
|
||||
> (`adc_input_poll()` 返回值只由 TPAFE 决定,`s_frames_skip` 抓不到),直接污染 20 点 DFT 窗。
|
||||
> 修复 = 两条失败支路都清 `s_stm32_valid`(`App/algo/adc_input.c`)。
|
||||
> **反向验证已做**:把超时支路改回旧行为,TC-AI-039b / 073 / 074 三条立即 FAIL(76/79)→
|
||||
> 证明这组用例真的咬得住。
|
||||
> ⚠ **为什么 TC-AI-034 照不出这条缺陷**:它是"`adc_input_init()` 后**立刻**失败",此时
|
||||
> `s_stm32_valid` 本来就是 0,断言 -2 恒成立。真正的缺陷路径是**成功过之后再失败**:
|
||||
> 本层只有一份 `s_stm32_raw[]`,而有效标志此前**只在成功时置 1、失败时从不清零** →
|
||||
> 消费侧照样取数、照样压入采样环(`adc_input_poll()` 返回值只由 TPAFE 决定,
|
||||
> `s_frames_skip` 抓不到),直接污染 20 点 DFT 窗。
|
||||
> 修复 = 失败支路清 `s_stm32_valid`(`App/algo/adc_input.c`)。
|
||||
> **这组用例在轮询版与 DMA 版都保留**:触发条件从"轮询超时"变成"本拍无新帧",
|
||||
> **同一条缺陷、同一个防线**。
|
||||
> **反向验证已做(v2.0)**:把停摆支路改回 `valid=1` → TC-AI-039 / 073 / 074 / 074B 四条立即
|
||||
> FAIL(89/93 时点);把半区奇偶取反 → 11 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL;
|
||||
> gap 阈值改 `>2` → 1 条 FAIL → 证明这些用例真的咬得住。
|
||||
|
||||
### 2.5 取数与参数守卫(`adc_input_raw`)
|
||||
|
||||
@@ -217,15 +216,16 @@ test/host/adc_input/adc_input_test.c
|
||||
|
||||
| 用例 | 断言 |
|
||||
|---|---|
|
||||
| **TC-AI-060** | init 后未扫描 → `RC_NOT_RUN(-3)` |
|
||||
| **TC-AI-061** | init 后失败计数 = 0 |
|
||||
| **TC-AI-060** | init 后未取帧 → `RC_NOT_RUN(-3)` |
|
||||
| **TC-AI-061** | init 后各计数 = 0 |
|
||||
| **TC-AI-062** | 一次成功后 `rc == OK` 且 `errs` 不变 |
|
||||
| **TC-AI-063** | 两次失败累计 `errs == 2` |
|
||||
| **TC-AI-064** | `rc` = **最近一次**(`RC_TIMEOUT`) |
|
||||
| **TC-AI-063** | 连续两拍无新帧 → `errs == 1`(单拍伪影不计) |
|
||||
| **TC-AI-064** | `rc` = **最近一次**(`RC_STALL`) |
|
||||
| **TC-AI-065** | 恢复成功后 `rc == OK` |
|
||||
| **TC-AI-066** | `errs` **单调不回退**(不因成功回落) |
|
||||
| **TC-AI-067** | `seq` 只由 DMA 完成回调推进(消费动作不改它) |
|
||||
|
||||
**当前结果**:70/70 PASSED(`failed=0`)。
|
||||
**当前结果**:**101/101 PASSED**(`failed=0`)。
|
||||
|
||||
---
|
||||
|
||||
@@ -274,3 +274,4 @@ V0.1 上 8 路电气量分属两片转换器且**无共同采样触发**(N-18
|
||||
|---|---|
|
||||
| 2026-10-10 | 创建;70 用例全绿(`/W4` 0 告警);覆盖 `adc_input.c` 全部 8 个函数(6 公开 + 2 静态);挂 pre-commit 钩子 `adc-input-unit-test` |
|
||||
| 2026-10-10 | **新增 TC-AI-070..078(70 → 79 用例)**:钉死"失败拍不得沿用上一拍旧值"。配套修复 `App/algo/adc_input.c` 两条失败支路(清 `s_stm32_valid`)。已做**反向验证**:改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79),证明用例有效 |
|
||||
| 2026-10-10 | **v2.0 DMA 模型改造(79 → 101 用例)**:被测代码 `stm32_scan()`(逐通道 `PollForConversion`)换成 `stm32_take()`(消费 DMA 乒乓半区)。桩面同步:删 `HAL_ADC_Start/PollForConversion/GetValue/Stop`,加 `HAL_ADC_Start_DMA` 与 `hadc_other`;"DMA 完成"由测试直接调用**被测代码自己实现**的三个 HAL 弱回调注入(`dma_beat()`)。新增三组:⭐ **TC-AI-080..089** 乒乓半区(往正在写的另一半填干扰值仍须读到刚完成那一半)+ 超拍丢帧;**TC-AI-090..09A** HAL 错误回调与 `hadc == &hadc1` 句柄守卫;**TC-AI-03A..03E / 067** 停摆(连续 ≥2 拍)与丢帧(`seq` 前进 >1)分开计数。TC-AI-070..078 陈旧帧组**原样保留**(触发条件由"轮询超时"变"本拍无新帧",同一防线)。反向验证四连:半区奇偶取反 → 11 条 FAIL;停摆拍置 `valid=1` → 4 条 FAIL;`STALL_RUN_MAX` 改 1 → 6 条 FAIL;gap 阈值改 `>2` → 1 条 FAIL |
|
||||
|
||||
@@ -129,11 +129,11 @@ STM32(U17)为 LQFP144 脚号。
|
||||
| 项 | 现状 | 影响 |
|
||||
|---|---|---|
|
||||
| **通道归属(硬件缺陷)** | 8 路电气量分属两片转换器:UA/UN/IA → TPAFE,UB/UC/IB/IC/IN → STM32 ADC | **V0.1 硬件缺陷**(非有意分配,理由见 §1)→ V0.1 上禁止跨转换器做相位/功率/功率因数/矢量类运算;**V0.2 改版要求 8 路电气量全部走 TPAFE**(登记簿 N-18) |
|
||||
| STM32 ADC 外设 | **已生成(2026-10-09,CubeMX 6.17 + FW_F4)**:`Src/adc.c`/`Inc/adc.h` 已入库并挂进 `MDK-ARM/*.uvprojx`;`Inc/stm32f4xx_hal_conf.h` 的 `HAL_ADC_MODULE_ENABLED` 已生效;HAL `stm32f4xx_hal_adc.c/_ex.c` + `stm32f4xx_ll_adc.h` 已补入仓库(HAL 原本是裁剪子集);`Src/main.c` 里 `MX_ADC1_Init()` 排在 `MX_IWDG_Init()` 之后。生成配置核对无误:9 通道 / Rank 1~9 / 全部 144 cycles / `ScanConvMode=ENABLE` / `ContinuousConvMode=DISABLE` / `ExternalTrigConv=ADC_SOFTWARE_START` + `EDGE_NONE` / `EOCSelection=ADC_EOC_SINGLE_CONV` / `DataAlign=RIGHT` / 12 位;`HAL_ADC_MspInit()` 已把 PA0~PA7+PC4 配成 `GPIO_MODE_ANALOG` | 硬件通路已打通(`hadc1` 可用);**消费者已落地**——`App/algo/adc_input.c` 的 `stm32_scan()`(L1-3,2026-10-10):`HAL_ADC_Start` + 逐通道 `PollForConversion`/`GetValue`,9 通道一轮 ≈ 67 µs |
|
||||
| STM32 ADC 外设 | **已生成(2026-10-09,CubeMX 6.17 + FW_F4)**:`Src/adc.c`/`Inc/adc.h` 已入库并挂进 `MDK-ARM/*.uvprojx`;`Inc/stm32f4xx_hal_conf.h` 的 `HAL_ADC_MODULE_ENABLED` 已生效;HAL `stm32f4xx_hal_adc.c/_ex.c` + `stm32f4xx_ll_adc.h` 已补入仓库(HAL 原本是裁剪子集);`Src/main.c` 里 `MX_ADC1_Init()` 排在 `MX_IWDG_Init()` 之后。生成配置核对无误:9 通道 / Rank 1~9 / 全部 144 cycles / `ScanConvMode=ENABLE` / `ContinuousConvMode=DISABLE` / `ExternalTrigConv=ADC_SOFTWARE_START` + `EDGE_NONE` / `EOCSelection=ADC_EOC_SINGLE_CONV` / `DataAlign=RIGHT` / 12 位;`HAL_ADC_MspInit()` 已把 PA0~PA7+PC4 配成 `GPIO_MODE_ANALOG` | 硬件通路已打通(`hadc1` 可用);**消费者已落地**——`App/algo/adc_input.c` 的 `stm32_take()`(L1-3,v2.0,2026-10-10):ADC1 由 **TIM3 TRGO 硬件触发**一轮 9 通道 + **DMA2_Stream4 循环搬运**进乒乓双缓冲,任务侧只消费刚完成的半区(零忙等),9 通道一轮 ≈ 67 µs |
|
||||
| ADC1 上电自校准 | **已补(2026-10-10)**:`Src/adc.c` 的 `USER CODE BEGIN ADC1_Init 2` 调用 `adc1_calibrate()`(实现同文件 `USER CODE BEGIN 1`,按 RM0090 §13.3.6:清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)。⚠ **CubeMX 不生成这一步**,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头**没有** `ADC_CR2_CAL`(bit3,代码里按 RM 自建 `ADC1_CAL_BIT`)→ 属 L2 手工定制,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底 | 实测有效:校准前后同一拍 STM32 侧 6 路**同向**变化 `UB 898→961`、`UC 978→1038`、`IB 906→922`、`IC 1061→1071`、`IN 1017→1055`、`T 790→846`(+10~+63 LSB ≈ 0.2~1.5 % FS);TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。缺这一步不报错,只是**读数带失调** |
|
||||
| ADC 时钟 | CubeMX 的 F4 ADC 面板**有** Clock Prescaler 一项(此前"面板没有该项、固定 DIV2"的判断有误),已选 **PCLK2/4 → ADCCLK = 21 MHz** ✓。面板里 **DIV2 是灰的、选不到**:DB 里 `FadcConstraint Max=36000000`,APB2 = 84 MHz → DIV2 = 42 MHz 超限,该候选值被 `Action="DISABLE"` 禁掉,超规值进不来 | **不设额外防护(2026-10-09 决定)**:约束已由 CubeMX 把关;`.ioc` 虽不保存 `ADC1.ClockPrescaler` 键,重生成也只会回落到约束允许的值。按 144+12 cycles 算 ≈ 7.4 µs/通道、9 通道一轮 ≈ 67 µs |
|
||||
| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费,2026-10-10)**:`App/task/adc_task.c` 每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ STM32 ADC1 顺序扫描,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);**消费唤醒由 TIM6 ISR 置位 `ADC_WAKE_FLAG`**(不再 1 ms 轮询);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / **AboveNormal**) | 数据路径已通(台架 `@test`:`frames` 严格 1000 Hz 递增、**`idle=0`**、`adc1_rc=0` / `adc1_err=0`) |
|
||||
| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM6 更新中断(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM6 分支还调 `adc_tick_irq_notify()` **唤醒消费者**(P1-5 方案 a,2026-10-10;内含"消费者就绪"门) | 连续波形(1 kHz);详见 `子系统设计/ADC采样节拍设计说明.md` §10 |
|
||||
| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费,2026-10-10)**:`App/task/adc_task.c` 每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ 消费一帧 STM32 ADC1 DMA 乒乓缓冲,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);**消费唤醒由 TIM3 ISR 置位 `ADC_WAKE_FLAG`**(不再 1 ms 轮询);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / **AboveNormal**) | 数据路径已通(台架 `@test`:`frames` 严格 1000 Hz 递增、**`idle=0`**、`adc1_rc=0` / `adc1_err=0`) |
|
||||
| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM3 更新事件(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM3 更新事件还经 **TRGO** 硬件触发 ADC1 一轮 9 通道(DMA 搬运),并调 `adc_tick_irq_notify()` **唤醒消费者**(P1-5 方案 a,2026-10-10;内含"消费者就绪"门)→ 两片转换器由同一硬件事件同源对齐 | 连续波形(1 kHz);详见 `子系统设计/ADC采样节拍设计说明.md` §10 / §11 |
|
||||
| 前端偏置 | 每路 1k 串联 + 1k 到 `*_GND` + 滤波 C,**无中点偏置** | STM32 侧只能测 **0~3.3 V 单极性**,交流量负半周被削顶;主板侧只做监视可接受,若要算 RMS/功率需硬件补偏置或明确算法口径 |
|
||||
| TPAFE 量程 | 默认 ±5 V(RANGE 接 GND),16 位二进制补码,LSB ≈ 152.59 µV | 用 `tpafe5160_to_voltage()` 换算 |
|
||||
| 取数契约(**2026-10-10 已修**,E-44 / E-45 / E-58 闭环) | 旧的 `get_buf` / `data_ready` / `clear_ready` **已删除**(删除前确认全仓库零调用者):`get_buf` 只给指针、任务再拷 → 被 BUSY EXTI 抢占就是**两帧混合**;`s_ready` 电平标志被 `start_conv_irq` 触发前清 0 → **重触发即丢未消费帧**;`read_all` 从不看 FRSTDATA(宏有定义、代码零引用)→ 芯片死亡时返回 OK + 常量垃圾。现口径:**`tpafe5160_snapshot(dst, count)`**(临界区原子拷贝,返回 1/0/-1,**仅任务上下文**)+ **`tpafe5160_frame_seq()`**(帧序号只增不清)+ **`tpafe5160_frstdata_err()`**(无跳变帧计数);`tpafe5160_init()` 增**首帧自检**——一帧读下来 FRSTDATA 无跳变(芯片不驱动并行总线;板上 R24 4.7k 上拉会把它钉死为高)→ 返回 `-3` + `[ERR]` | 消费者**只能**用 `snapshot()` 取数(`AGENTS.md` 已立为铁律);真机实测开机 `[ADC] Init OK` 现只在自检帧通过后才打印 → 判据在本板成立、非空转 |
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
| 操作系统 | FreeRTOS V10.3.1(CMSIS-RTOS v2) |
|
||||
| 已实现子系统 | 网络(CH395F)、存储(GD5F2GQ5UE SPI NAND + dhara FTL + FatFs)、时间(SD2506 RTC + `sys_clock`)、配置(cfg_file)、日志、UI(远端显示)、看门狗(IWDG)、FTP |
|
||||
| 采样硬件 | TPAFE5160(16 位、8 通道**同步**、±5 V 双极性、并行 `GPIOG[15:0]`)+ STM32 内部 ADC1(12 位、9 通道**顺序**扫描、0~3.3 V 单极性) |
|
||||
| 采样软件 | `Drivers/BSP/TPAFE5160/tpafe5160.h` 驱动就绪(原子快照 API);`App/task/adc_task.c` **已实现真实消费者(B1,2026-10-10)**——`tpafe5160_snapshot()` 取数 + 帧统计;**采样节拍由 TIM6 更新中断(1 kHz)硬件触发**(2026-10-10);STM32 侧 **`App/algo/adc_input.c` 已实现语义映射 + ADC1 9 通道扫描(L1-3/L1-4,2026-10-10)**,`HAL_ADC_Start` 已出现 |
|
||||
| 采样软件 | `Drivers/BSP/TPAFE5160/tpafe5160.h` 驱动就绪(原子快照 API);`App/task/adc_task.c` **已实现真实消费者(B1,2026-10-10)**——`tpafe5160_snapshot()` 取数 + 帧统计;**采样节拍由 TIM3 更新事件(1 kHz)硬件触发**(2026-10-10;同一事件经 TRGO 触发 ADC1 + DMA);STM32 侧 **`App/algo/adc_input.c` 已实现语义映射 + ADC1 DMA 乒乓帧消费(L1-3/L1-4,v2.0)**,`HAL_ADC_Start_DMA` 已出现 |
|
||||
| 保护算法层 | **完全缺失**(无傅氏、无保护元件、无出口、无定值模型) |
|
||||
| 已知硬件缺陷 | **N-18**:8 路电气量分属两片转换器 → V0.1 上禁止跨转换器做相位/功率/矢量运算 |
|
||||
|
||||
@@ -238,10 +238,10 @@
|
||||
|
||||
| # | 功能点 | 关键参数 | 完成判据 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| L1-1 | 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM6 更新中断(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动);台架 `@test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt`) |
|
||||
| L1-1 | 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM3 更新事件(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动。⚠ 节拍源**不能**是 TIM6/TIM7——F4 上 ADC1 收不到它们的 TRGO,见 `docs/子系统设计/ADC采样节拍设计说明.md` §11.1);台架 `@test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(早期证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt`,节拍源当时为 TIM6) |
|
||||
| L1-2 | TPAFE 快照消费 | 8 通道 / 帧 | `adc_task` 每拍调 `tpafe5160_snapshot()`,返回 1/0/-1 有处理 | ✅ B1 已实现并 `@test` 闭环(`adc_task.c`;台架 `idle=0`、`frst_err=0`) |
|
||||
| L1-3 | STM32 ADC1 启动与扫描 | 9 通道、144 cycles、软件触发 | 全库出现 `HAL_ADC_Start`;9 通道码值随注入变化 | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `stm32_scan()`——`HAL_ADC_Start` + 逐通道 `HAL_ADC_PollForConversion`/`HAL_ADC_GetValue`(EOC_SINGLE_CONV + 无 DMA,可逐通道轮询);台架 `@test` PASS——9 通道全部转换成功、**零超时**(30 s 内 `adc1_rc=0` / `adc1_err=0`,≈27 万次转换)。⚠ 判据"随注入变化"以"逐通道独立读数 + 零超时 + 跨次开机读数不同"替代(台架无信号源,同 §7.1 无示波器口径);**引脚级注入验证(`ADC通道映射说明.md` §8 步骤 2)仍为待做项** |
|
||||
| L1-4 | 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM6 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) |
|
||||
| L1-3 | STM32 ADC1 启动与扫描 | 9 通道、144 cycles、**TIM3 TRGO 硬件触发 + DMA** | 全库出现 `HAL_ADC_Start_DMA`;9 通道码值随注入变化 | ✅ 2026-10-10(v2.0):`App/algo/adc_input.c` 的 `stm32_take()`——ADC1 由 TIM3 更新事件经 TRGO 触发一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)` + DMA2_Stream4 循环模式把码值自动搬进**乒乓双缓冲**,任务侧只消费刚完成的半区(**零忙等**,实测单拍 `adcp` 122 µs → **8 µs**);台架 `@test` PASS——`adc1_seq` 每拍 +1、`adc1_err=0`、`adc1_hw=0`。⚠ 判据"随注入变化"以"逐通道独立读数 + 零停摆 + 跨次开机读数不同"替代(台架无信号源,同 §7.1 无示波器口径);**引脚级注入验证(`ADC通道映射说明.md` §8 步骤 2)仍为待做项** |
|
||||
| L1-4 | 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM3 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) |
|
||||
| L1-5 | 通道标定(一次系数 + 零漂) | 每通道 `k`、`b`;系数定点 `2^10` | 注入标准源,读数误差 ≤ 目标精度 | ⬜ |
|
||||
| L1-6 | 采样有效性校验 | FRSTDATA 跳变、帧序号单调、丢帧计数 | 芯片静默/丢帧被检出并置告警 | ⬜ |
|
||||
| L1-7 | 等周波采样(软件测频 + 调频闭环) | 20 点/周波;测频范围建议 **45~55 Hz**(参考 40.5~60.5) | 频率偏移 ±2 Hz 时幅值误差 ≤ 1% | ⬜ |
|
||||
@@ -600,3 +600,4 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5)
|
||||
| 2026-10-10 | **1 kHz 硬件定时器触发(TIM6)** | 采样节拍由任务周期软触发改为 **TIM6 更新中断(1 kHz)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 加 TIM6 分支触发 `tpafe5160_start_conv_irq()`(`USER CODE` 区);`app_main.c` 用 `HAL_TIM_Base_Start_IT(&htim6)` 替换单次触发(在 `tpafe5160_init()` 之后);`adc_task.c` 退化为**纯消费者**(1 ms 轮询仅作消费节奏)。§1.2 采样软件行、§3.2 L1-1 同步。详见 `docs/子系统设计/ADC采样节拍设计说明.md` |
|
||||
| 2026-10-10 | **L1-3/L1-4 落地:STM32 ADC1 扫描 + 语义映射** | 新增 `App/algo/adc_input.c/h`(L1 采集层):① `k_adc_map[]` **语义通道表**(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_desc()`/`adc_input_raw()` **按语义取数**;② `stm32_scan()` STM32 ADC1 9 通道软件触发顺序扫描(`HAL_ADC_Start` + 逐通道 `PollForConversion`/`GetValue`);③ `adc_input_poll()` 统一"TPAFE 快照 + STM32 扫描"。`adc_task.c` 取数改走本层(首帧日志补全 8 路 + 温度、心跳增 `adc1_rc`/`adc1_err`);`app_main.c` 在 `tpafe5160_init()` 后调 `adc_input_init()`(含**映射自检**:STM32 映射通道必落在扫描序列内,失败记 `SYSLOG_SELFTEST`);Keil 工程挂 `adc_input.c` 并加 `../App/algo` 包含路径。§1.2 采样软件行、§3.1 L1-3/L1-4 同步。`@build` 0 错 0 警;台架 `@test` PASS(`ADC input layer init: OK`、`adc1_rc=0`/`adc1_err=0`、`seq` 仍 1000.0 Hz、`adc` 栈峰值 528→584 B、回读一致、零 `[ERR]`/HardFault),证据 `测试说明/boot_log_data/20261010_prod_adc_l1_boot_log.txt` |
|
||||
| 2026-10-10 | **L2 相量层落地 + L2-2 实例数口径对齐** | 新增 `App/algo/sample_ring.c/h`(L2 输入:按语义通道保存 2 周波 = 40 点环缓冲,整帧写后统一推进写指针)与 `App/algo/phasor.c/h`(L2 核心:全周傅氏单频 DFT,N = 20;系数表**离线预乘 √2/N** → `fAmSQ` 直接为**有效值**;计算区/使用区**双区原子切换**;实例表 `k_inst[]` 数据驱动);主机侧单测 `test/host/phasor/` **57 用例全绿**(`phasor-unit-test` 钩子)。**§3.2 L2-2 行口径对齐**:参考项目为 **4 个实例**,本项目按 V0.1 有效通道展开为 **5 个**(`C01-IA-F1`/`C02-UA-F1`/`C03-UN-F1`/`C04-IA-F2`/`C05-IA-F5`),已加注说明 4 → 5 的来历;§4.3 相量区 RAM 行同步为 5 实例。⚠ §3.2 的 L2 各点状态**仍标 ⬜**——实现与主机单测已完成,但**台架 `@test` 闭环未做**,且 L2-3 的完成判据(标准源读数 = 设定有效值)在无信号源台架上不可达,故不置 ✅(口径见 §3.2 表下注) |
|
||||
| 2026-10-10 | **L1-1/L1-3 节拍源与搬运方式升级(v2.8)** | 节拍源 **TIM6 → TIM3**(更正硬事实:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO,只接 DAC),TIM3 更新事件同时触发 TPAFE CONVST 与(经 TRGO)ADC1 一轮 9 通道;L1-3 由"软件触发 + 逐通道轮询"改为 **DMA2_Stream4 循环搬运 + 乒乓双缓冲**,`stm32_scan()` → `stm32_take()`,单拍 `adcp` **122 µs → 8 µs**。§1.4 采样软件行、§3.2 L1-1/L1-3/L1-4 同步。完整设计与实测见 `docs/子系统设计/ADC采样节拍设计说明.md` §11 |
|
||||
|
||||
+22
-13
@@ -6,7 +6,8 @@
|
||||
> 改前基线(`git stash` 重建)0/6。
|
||||
> **状态**:**分析完成,全部处置已落地并上板验证**(2026-10-10)。P0(adc 压环条件 + 截止期
|
||||
> 重同步)、P1-4a(UI 心跳自产)、P2(常开 `[PROF]` 探针)、§9 方案 A(推流就绪门控 + 兜底)、
|
||||
> **P1-5(ADC 消费改 TIM6 ISR 事件驱动 + `adcTask` 提至 `AboveNormal`)** 全部落地;停摆累计
|
||||
> **P1-5(ADC 消费改定时器 ISR 事件驱动 + `adcTask` 提至 `AboveNormal`;同日 v2.8 进一步把
|
||||
> 节拍源定为 TIM3 并让 STM32 ADC1 走 TRGO + DMA 乒乓缓冲,§4.4 的 2 ms 忙等已消除)** 全部落地;停摆累计
|
||||
> **38 次上板启动 0 复现**(P0 后 22 次 + A 后 10 次 + P1-5 后 6 次)。§9 方案 B
|
||||
> (把推流搬出 uiTask)**已评估并决定不做**。
|
||||
> **阅读口径**:§2~§7 是**故障当时**的证据与归因,其中已被处置改掉的代码片段就地标注
|
||||
@@ -137,7 +138,7 @@ uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优
|
||||
对它无效。
|
||||
|
||||
> **P1-5 落地后的变化(2026-10-10)**:`adcTask` 已提到 **`AboveNormal`(32)** → 本表"uiTask
|
||||
> 垫底"的格局更极端。之所以可接受:① 改造后 `adcTask` 由 TIM6 **精确 1 kHz** 唤醒,单拍
|
||||
> 垫底"的格局更极端。之所以可接受:① 改造后 `adcTask` 由定时器(现 TIM3)**精确 1 kHz** 唤醒,单拍
|
||||
> µs~2.25 ms 且**不再有空转窗口**——改前的 `osDelayUntil` 一旦超拍就永久落后 → 不再阻塞 →
|
||||
> 以 Normal 全程抢占 uiTask,那才是本表真正担心的形态(P0-2 已修,见 §4.3);② 实测 6 次上板
|
||||
> `uip` / `uiw` / `uis` 与提优先级前**一字不差**(61.3 ms / 11.4 ms / 1 µs)。
|
||||
@@ -201,7 +202,7 @@ if (osDelayUntil(next) != osOK) { next = osKernelGetTickCount(); } /* 落后
|
||||
|
||||
**再现状(P1-5,2026-10-10)**:这段已**整体删除**——`ADC_TASK_BEAT_MS` 与 `next` 都不在了,
|
||||
循环尾改为 `(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)`,唤醒由
|
||||
TIM6 ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上消失**(不再有自定截止期,也就没有
|
||||
定时器(现 TIM3)ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上消失**(不再有自定截止期,也就没有
|
||||
"落后"这个概念),P0-2 的重同步钩子随之作废。实测 6 次上板 **`idle = 0`**(改前 1~649),
|
||||
见 §8 与 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10。
|
||||
|
||||
@@ -218,18 +219,20 @@ TIM6 ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上
|
||||
> 从不轮询剩余通道 → **单拍最坏 = 一次超时 ≈2~3 ms**(`HAL_GetTick()` 是 1 ms 粒度、判据是
|
||||
> `> Timeout`),不是 9 次。
|
||||
|
||||
**现状(P1-5 后:忙等未改,但跑它的任务优先级抬高了 → 敏感度上升)**:
|
||||
`ADC_INPUT_POLL_TIMEOUT_MS = 2U` 与忙等**仍未改**——P1-5 只做"事件驱动 + 提优先级",
|
||||
超时硬化列为后续(见
|
||||
[`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10.4)。
|
||||
三点须一起看:
|
||||
**现状(v2.8,2026-10-10:这条饥饿源已消除)**:`ADC_INPUT_POLL_TIMEOUT_MS` 与
|
||||
`HAL_ADC_PollForConversion` **已从代码中删除**——STM32 ADC1 改由 **TIM3 更新事件经 TRGO
|
||||
硬件触发 + DMA 循环搬运**,任务侧只消费 DMA 已写好的乒乓半区,单拍 `adcp` 实测
|
||||
**122 µs → 8 µs**;"超时"这个概念在 L1 层不复存在,因此也不存在 2~3 ms 自旋。
|
||||
完整设计与实测见 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §11。
|
||||
下面两段保留,作为该结论的推导与当时的实测依据(历史):
|
||||
|
||||
- 这条饥饿源如今**基本不被触发**,且**只在超时拍出现**:6 次上板对照——超时 1 次的那轮
|
||||
`adcp = 2209 µs`,超时 0 次的 5 轮 `adcp = 122 µs`(= 9 通道 × 7.4 µs + TPAFE 快照);
|
||||
`adc1_err` 从 288/10 s 降到 **0~1**;
|
||||
- 但 `adcTask` 现已在 **`AboveNormal`(32)**,一旦某拍真走到 2 ms 忙等,它挤的是
|
||||
- ~~但 `adcTask` 现已在 **`AboveNormal`(32)**,一旦某拍真走到 2 ms 忙等,它挤的是
|
||||
netTask / ftpTask / defaultTask / timeSyncTask(改前是同级竞争)→ **这是 P1-5 换来的新暴露,
|
||||
记在账上**;
|
||||
记在账上**~~ → **v2.8 已消**:忙等本体没了,这条新暴露随之关闭(2026-10-10 实测
|
||||
`adcp = 8 µs`、`adc1_err = 0`、`adc1_hw = 0`)。
|
||||
- ✅ **超时的第二个代价是正确性,不只是耗时(2026-10-10 已修)**:超时拍 `s_stm32_valid` 此前
|
||||
仍是上次成功留下的 1,而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、
|
||||
照样压环,STM32 侧几路(UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(且超时前几个 Rank 已写
|
||||
@@ -238,7 +241,10 @@ TIM6 ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上
|
||||
`s_stm32_valid`** → 本拍 `adc_input_raw()` 对 STM32 语义通道返 -2 → `adc_task` 走既有
|
||||
`skip` 整帧跳过路径,下一拍成功自动恢复。主机侧新增 **TC-AI-070..078** 钉死(旧用例
|
||||
TC-AI-034/039b 都是"init 后立刻失败",`valid` 本就是 0,照不出这条路径),并做**反向验证**:
|
||||
把支路改回旧行为 → 3 条立即 FAIL(76/79)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)**。
|
||||
把支路改回旧行为 → 3 条立即 FAIL(76/79)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)**
|
||||
→ ✅ **已办(同日 v2.8)**:DMA + TIM3 TRGO + 乒乓双缓冲落地,`stm32_scan()` 换成
|
||||
`stm32_take()`;"成功过之后失败 → 静默沿用旧值"这条缺陷路径在结构上消失(无超时概念),
|
||||
改由"本拍无新帧"触发同一条 `skip` 防线,主机用例 **TC-AI-070..078** 原样保留并仍反向验证。
|
||||
|
||||
---
|
||||
|
||||
@@ -315,7 +321,7 @@ TIM6 ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上
|
||||
4. **`[STACK]` 周期从 2.0 s 拉到 4.05 s**(17:36:19.902 → 23.951,标称 `STACK_PROBE_PERIOD_MS=2000`)
|
||||
值得注意,但**不能作为证据**:控制台缓冲满时会**丢行**,该报告也可能只是被丢了。
|
||||
5. **`idle` 的物理含义**:任务轮询与帧产生之间的相位漂移(`osDelayUntil` 的 1 ms 内核 tick
|
||||
vs TIM6 的 1 kHz 硬件节拍,两个时钟源不同),本身不是故障。
|
||||
vs 定时器(现 TIM3)的 1 kHz 硬件节拍,两个时钟源不同),本身不是故障。
|
||||
**现状**:P0-1 后 `idle` 只计数、不再压环,实测也从 649/20001 降到 **1/20000**——
|
||||
当年"它会让 §5 的缺陷持续发生"这条耦合已解除。
|
||||
6. **启动阶段 `MISS start snap=0x67` 是良性的(2026-10-10 新增)。** `[WD] armed` 在
|
||||
@@ -344,7 +350,9 @@ TIM6 ISR 提供。**"超拍永久落后 → 空转"这一类缺陷在结构上
|
||||
| **P1-3** | 把 uiTask 提到 `osPriorityNormal` | ❌ **刻意不做**:它会**掩盖** §4.2/§4.4 两个真实缺陷(当时的判断,现仍成立) |
|
||||
| **P1-4a** | 心跳提到 `ui_net_sink_poll()` **之前** | ✅ 已落地(`ui_task.c:159`,v1.3) |
|
||||
| **P1-4b** | 给 `net_accept()` 一条非阻塞查询路径 | ✅ 以更彻底的形式落地 = **§9-A 门控**(`net_can_accept()` / `net_can_read()`) |
|
||||
| **P1-5** | 原列"adcTask 单拍硬上界(ADC 改 DMA / 超时整拍跳过)"→ 经用户决策改走**方案 a:事件驱动消费**(TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒 + `adcTask` 提至 `AboveNormal`) | ✅ **已落地**(2026-10-10;6 次上板 **`idle=0`**、`frames` 严格 1000 Hz、UI 侧探针一字不差;完整记录见 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10)。⚠ 原列的"**单拍硬上界**"(DMA / 超时整拍跳过)**仍未办**,且因优先级抬高而更敏感(§4.4 现状) |
|
||||
| **P1-5** | 原列"adcTask 单拍硬上界(ADC 改 DMA / 超时整拍跳过)"→ 经用户决策改走**方案 a:事件驱动消费**(TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒 + `adcTask` 提至 `AboveNormal`) | ✅ **已落地**(2026-10-10;6 次上板 **`idle=0`**、`frames` 严格 1000 Hz、UI 侧探针一字不差;完整记录见 [`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §10)。⚠ 原列的"**单拍硬上界**"(DMA / 超时整拍跳过)**已办(同日 v2.8)**——STM32 ADC1 改 TIM3 TRGO 硬件触发 + DMA 乒乓双缓冲,忙等本体删除,
|
||||
`adcp` 122 µs → **8 µs**;该"新暴露"随之关闭(§4.4 现状)。完整记录见
|
||||
[`../子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §11 |
|
||||
| **P2-6** | 启用现成的 `TEST_LATENCY_PROBE`(`ui_task.c` 的 `ui_loop` 段) | ✅ 已被**常开探针 `App/util/task_prof.c/.h`** 取代(`[PROF]`/`[PROF2]`),无需再开测试开关 |
|
||||
| **P2-7** | 新增 adcTask 单拍 / 推流往返两个埋点 | ✅ 已落地(`TASK_PROF_ADC_BEAT` / `TASK_PROF_ADC_POLL` / `TASK_PROF_UI_SINK`) |
|
||||
| **P2-8** | 停摆取证带上"各任务在本窗口的调用计数" | ✅ 以更彻底的形式落地 = 心跳位图 **1→0 边沿**裸 UART 直发现场快照 |
|
||||
@@ -546,6 +554,7 @@ A(门控 + 兜底)→ @build + @test + e2e → 看 [PROF]:uis 掉到 µs
|
||||
| 2026-10-10 | **A 收口 + 方案 B 否决并删除**(用户定案"B 不做")。① **A 收口**:`@test` **10/10 PASS**(每轮 `uis` ≤1 µs、`uisn` ≡ 0、0 `task stall`、0 `[ERR]`)、e2e `check_remo_sink.py` **0/25600 位差异**、内容页文本断言 50/50。② **B 删除**:§9.4 由完整设计(新任务 256 words / BelowNormal / 不进 `required_mask`、帧 seqlock、栈与堆两条预算路线、观测面迁移、12 处改动清单)改写为**否决记录**——实测 `uis`=1 µs 而 `uip` 仍 60.9 ms(LVGL 帧周期地板)证明"搬出 uiTask"**不会让刷新变快**,收益只剩极端并发下的观感隔离,而成本含**必改 `.ioc`** 的 CubeMX 重生成;残留暴露与观测手段(`uis`/`uisn`)如实保留。③ **同步清理**:§9.5/§9.6/§9.7 去掉 B 条目;**N-19 / C7 不入登记簿**(当日结案,`docs/待处理内容.md` 保持无该编号);`子系统设计/看门狗设计说明.md` §9.2 的 N-19 行移除;`RemoDispBus协议与接收设计说明.md` 去掉"规划中改独立任务"段;`ui_net_sink.c/.h` 注释去掉"应搬出 uiTask"的指向 |
|
||||
| 2026-10-10 | **按项目现状全文校订(无代码改动)**:① 已被处置改掉的代码片段就地标注 **改前 / 现状**——§1 第 2 条、§4.2(`ui_task.c` 心跳顺序,P1-4a 已改)、§4.3(`osDelayUntil` 丢弃返回值,P0-2 已改)、§5(无新帧压环,P0-1 已改)、§6;§4.4 明确"忙等原样保留、P1-5 暂缓"。② §8 由"待确认后执行的 P0/P1/P2 分层建议"改写为**决策 + 结果表**(P1-3 刻意不做 / P1-4b 以 A 门控形式落地 / P1-5 暂缓 / P2-6 被 `task_prof` 取代 / P2-8 以边沿快照形式落地),并新增**当前健康剖面**表。③ §7 更新:0 复现累计 22 → **32 次**、`adc1_err` 288 → 1、`idle` 649/20001 → **1/20000**;新增 §7.7 说明 `netp=0` 是真实值(`net_poll()` 空闲只读一次 INT# GPIO)。④ §9.1/§9.2 的"改前"频率与 J1/J3 状态同步为 A 落地后的现状;删掉失效的行号引用与旧体积数字(256276 → **256368**) |
|
||||
| 2026-10-10 | **P1-5 已实施(方案 a:事件驱动消费)**:`Src/main.c` TIM6 分支在 `tpafe5160_start_conv_irq()` 后调 `adc_tick_irq_notify()`;`App/task/adc_task.c` 循环尾 `osDelayUntil` → `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 的 `adcTask` 优先级 `Normal` → **`AboveNormal`**;`App/task/adc_task.h` 新增 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补一条规划未预见的**就绪门** `s_consumer_ready`——TIM6 在 `app_main_init()` 就启动,比建任务与 `osKernelStart()` 早 ≈100 ms,ISR 不能对尚不存在的消费者动手(同本仓 TIM7 分支 2026-10-02 的实测坑)。验证:`@build` 0 错 0 警(Code 256368→**256728**)、**`@test` 6/6 PASS**、**`idle` 由 1~649 降到 0**、`frames` 严格 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 一字不差、0 `task stall`。本文同步:§4.1 补优先级变化与可接受性论证、§4.3 补"再现状"(P0-2 那段已删)、§4.4 改为"忙等未改但敏感度上升"、§8 的 P1-5 行转 ✅ 并拆出仍未办的"单拍硬上界"、健康剖面按 6 连跑复测值更新 |
|
||||
| 2026-10-10 | **v2.8 消掉 §4.4 的 2 ms 忙等(P1-5 拆出的"单拍硬上界"已办)**:节拍源 **TIM6 → TIM3**(更正一处硬事实:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO,只接 DAC),TIM3 更新事件同时 ① ISR 脉冲 TPAFE CONVST、② 经 **TRGO** 硬件触发 ADC1 一轮 9 通道、③ 同一事件置位 `ADC_WAKE_FLAG`;ADC1 挂 **DMA2_Stream4** 循环模式 + `DMAContinuousRequests=ENABLE`,长度 2×9 **乒乓双缓冲**,半区由完成计数奇偶推出。`App/algo/adc_input.c` 的 `stm32_scan()`(逐通道 `PollForConversion`)→ `stm32_take()`(零忙等);故障观测拆成 `adc1_seq`/`adc1_gap`/`adc1_err`/`adc1_hw` 四类(单拍无新帧 = 消费者相位伪影,不计故障)。实测:`adcp` **122 µs → 8 µs**、`adc1_err=0`、`adc1_hw=0`、`adc1_gap=27`/20002 拍(0.13%,集中在开机日志突发期)、`adc1_seq` 与 `seq` 恒定偏移 2;主机单测 79 → **101**;`@build` 0 错 0 警 Code 256872→**257660**;`@test` PASS;adcTask 静态最坏链 **832 B / 余 59%** 不变。CubeMX 侧三轮重生成的三处坑(ADC1 抢 Stream0 连锁搬走 SPI1_RX/USART1_RX、换流把 NVIC 优先级重置为 0、TIM6 残留)见 §11.2 |
|
||||
| 2026-10-10 | **§4.4 复核修正(无代码改动)**:① 原文"9 通道全超时最坏 = **18 ms** 连占"**有误**——`stm32_scan()` 在首个超时处即 `HAL_ADC_Stop()`+`return`(`adc_input.c:79-84`),单拍上界 = **一次**超时 ≈2~3 ms;② 6 次上板对照证明 **2 ms 只在超时拍出现**:超时 1 次那轮 `adcp = 2209 µs`,超时 0 次的 5 轮 `adcp = 122 µs`;③ `adcm ≈ 2.1 ms` 的归因由"ADC 采集"**改判**为每 10 s 一次的心跳日志(`idle = 0`/20000 反证不存在"每拍 2 ms");④ 新增超时的**正确性**代价——超时拍 `s_stm32_valid` 仍为 1 → STM32 侧静默沿用上一拍值、`skip` 抓不到 → 硬化第一步应是"超时即置 `s_stm32_valid = 0`"。同步修正 `App/task/adc_task.c` 两处过期注释与 `ADC采样节拍设计说明.md` §10.1/§10.3/§10.4 |
|
||||
| 2026-10-10 | **补 F4 ADC 上电自校准(§7.2 候选根因之一)**:核查发现本项目**从未**做过 ADC 自校准——CubeMX 不生成该步骤,且仓库裁剪版 HAL 无 `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头无 `ADC_CR2_CAL`。已在 `Src/adc.c` 的 `USER CODE` 区实现 `adc1_calibrate()`(清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)并在 `MX_ADC1_Init()` 末尾调用;新增 `cubemx-survival` 审计项 `adc1-calibration-call` / `adc1-calibration-impl`。验证:`@build` 0 错 0 警(Code 256728→**256868**)、`@test` **PASS**(`adcp` 仍 122 µs、`adc1_err=0`、`idle=0`);**校准前后同一拍 STM32 侧 6 路同向偏移** `UB 898→961`/`UC 978→1038`/`IB 906→922`/`IC 1061→1071`/`IN 1017→1055`/`T 790→846`(+10~+63 LSB),TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。§4.4 的 2 ms 忙等与"超时沿用旧值"**仍未处理** |
|
||||
| 2026-10-10 | **修 §4.4 的"超时拍静默沿用旧值"(正确性缺陷)**:`stm32_scan()` 两条失败支路(`HAL_ADC_Start` 失败 / 轮询超时)现都清 `s_stm32_valid` → 失败拍 `adc_input_raw()` 返 -2 → `adc_task` 走既有 `skip` 整帧跳过,不再把上一拍的值当新数据压入 20 点 DFT 窗;下一拍成功自动恢复。主机侧新增 **TC-AI-070..078**(70 → 79 用例)钉死——旧用例 TC-AI-034/039b 都是"init 后立刻失败"(`valid` 本就是 0),照不出"成功过之后再失败"这条路径。**反向验证已做**:把支路改回旧行为 → TC-AI-039b/073/074 立即 FAIL(76/79)。验证:`@build` 0 错 0 警(Code 256868→**256872**)、主机单测 **79/79**、`@test` **PASS**(本轮无超时:`adc1_err=0`、`skip=0`、`idle=0`)。**剩余未做 = 消掉 2 ms 忙等本身(DMA)** |
|
||||
|
||||
@@ -307,6 +307,55 @@ CHECKS = [
|
||||
"`USER CODE BEGIN 1` 区。本仓库 HAL 无 HAL_ADCEx_Calibration_Start、"
|
||||
"CMSIS 头无 ADC_CR2_CAL,两者都是裁剪造成的,勿以为『标准 HAL 里有』。",
|
||||
),
|
||||
# ---------------- ADC 采样节拍 + DMA(2026-10-10 v2.8) ----------------
|
||||
# 节拍源 TIM3:同一个更新事件 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1
|
||||
# ③ 置位 ADC_WAKE_FLAG。丢了 ② 不报编译错,只是两片转换器不再同源(相位差自由漂移)。
|
||||
dict(
|
||||
id="adc-tick-tim3-branch",
|
||||
kind="protected",
|
||||
path="Src/main.c",
|
||||
region="Callback 1",
|
||||
must_contain=["TIM3", "tpafe5160_start_conv_irq()", "adc_tick_irq_notify()"],
|
||||
hint="HAL_TIM_PeriodElapsedCallback 的 TIM3 分支必须同时做三件事:判 TIM3、脉冲 CONVST、"
|
||||
"通知消费者。⚠ 节拍源**不能**换回 TIM6/TIM7——F4 上 ADC1 的外部触发清单里没有 "
|
||||
"TIM6/TIM7 TRGO(只接 DAC),TIM3 的 TRGO 才是 ADC1 能收到的那个。",
|
||||
),
|
||||
dict(
|
||||
id="ioc-adc-trig-t3trgo",
|
||||
kind="ioc",
|
||||
key="ADC1.ExternalTrigConv",
|
||||
want="ADC_EXTERNALTRIGCONV_T3_TRGO",
|
||||
hint="ADC1 外部触发必须是 T3_TRGO(与节拍源 TIM3 配对)。改回软件触发 = 每拍由任务启动"
|
||||
"转换,DMA 与 TPAFE 不再同源对齐,且重新引入忙等。",
|
||||
severity="FAIL",
|
||||
),
|
||||
dict(
|
||||
id="ioc-adc-dma-continuous",
|
||||
kind="ioc",
|
||||
key="ADC1.DMAContinuousRequests",
|
||||
want="ENABLE",
|
||||
hint="DMAContinuousRequests(DDS 位)必须 ENABLE:关掉后一轮 9 通道只发**一次** DMA 请求"
|
||||
"→ 静默丢 8 路,不报任何编译/运行错误。",
|
||||
severity="FAIL",
|
||||
),
|
||||
dict(
|
||||
id="adc1-dma-link",
|
||||
kind="generated",
|
||||
path="Src/adc.c",
|
||||
must_contain=["hdma_adc1", "__HAL_LINKDMA", "DMA2_Stream4", "DMA_CIRCULAR"],
|
||||
hint="ADC1 的 DMA 挂载(Stream4 + 循环模式 + __HAL_LINKDMA)须在位。"
|
||||
"ADC1 在 DMA2 上只挂 Stream0 或 Stream4;选 Stream0 会连锁搬走 SPI1_RX/USART1_RX。",
|
||||
),
|
||||
dict(
|
||||
id="adc1-dma-irq-priority",
|
||||
kind="generated",
|
||||
path="Src/dma.c",
|
||||
must_contain=["HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0)"],
|
||||
hint="⚠ CubeMX 在**换 DMA 流**时会把该流的 NVIC 优先级静默重置为 **0**(实测两轮)。"
|
||||
"优先级 0 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(5) → 会抢占 FreeRTOS 临界区,"
|
||||
"违反本仓『外设中断 ≥5』。每次改 DMA 流后必须回 NVIC 页核对。",
|
||||
severity="FAIL",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ ROLE = {
|
||||
"testTask": "硬件测试入口",
|
||||
"uiTask": "LVGL UI 界面",
|
||||
"timeSyncTask": "墙钟时间同步",
|
||||
"adcTask": "TPAFE5160 采样(TIM6 1kHz 事件驱动 + 原子快照)",
|
||||
"adcTask": "TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA)",
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@
|
||||
| `cfg_file/stub/` | 桩头文件(`dbg_log.h`),覆盖 MCU 版以在 PC 上编译 |
|
||||
| `phasor/run_unit_test.py` | **相量层(L2)单测**:离线编译真实 `App/algo/phasor.c` + `sample_ring.c`(**无需任何桩**,纯算法),57 用例覆盖傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区原子交换 |
|
||||
| `phasor/phasor_test.c` | 测试单元本体(`#include` 两个生产源码 + 合成任意波形) |
|
||||
| `adc_input/run_unit_test.py` | **ADC 输入层(L1)单测**:离线编译真实 `App/algo/adc_input.c`,79 用例给每个 Rank 注入互异码值**逐通道钉死映射**,并覆盖 HAL 启动失败 / 转换超时 / **失败拍陈旧帧** / 参数错等真机难复现分支 |
|
||||
| `adc_input/run_unit_test.py` | **ADC 输入层(L1)单测**:离线编译真实 `App/algo/adc_input.c`,**101** 用例给每个 Rank 注入互异码值**逐通道钉死映射**,并覆盖 DMA 挂载失败 / **乒乓半区选择(撕裂帧防线)** / 超拍丢帧 / 停摆 / HAL 回调句柄守卫 / **停摆拍陈旧帧** / 参数错等真机难复现分支 |
|
||||
| `adc_input/adc_input_test.c` | 测试单元本体(`#include` 生产源码 + HAL / TPAFE 可注入桩) |
|
||||
| `adc_input/stub/` | 桩头文件(`adc.h` / `tpafe5160.h`) |
|
||||
| `vol_lock/run_unit_test.py` | **卷锁单测**:离线编译真实 `App/fs/vol_lock.c`,33 断言点覆盖创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传 |
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* 模块名称:ADC 输入层(L1)主机侧单元测试
|
||||
* 模块功能:在 PC 上对 App/algo/adc_input.c 做离线验证(6 个公开函数 + 2 个静态函数)。
|
||||
* 模块功能:在 PC 上对 App/algo/adc_input.c 做离线验证(8 个公开函数 + 2 个静态函数)。
|
||||
*
|
||||
* 被测对象:真实源文件 ../../../App/algo/adc_input.c(以 #include 原样纳入,
|
||||
* 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现:
|
||||
* - adc.h -> HAL_ADC_Start / PollForConversion / GetValue / Stop
|
||||
* (可注入 启动失败 / 第 N 次轮询超时 / 逐 Rank 返回值)
|
||||
* - adc.h -> HAL_ADC_Start_DMA(可注入挂载失败);DMA 完成由测试
|
||||
* 直接调用被测代码实现的 HAL_ADC_Conv[Half]CpltCallback
|
||||
* 注入(dma_beat()),并可往乒乓缓冲写任意码值
|
||||
* - tpafe5160.h-> tpafe5160_snapshot(可注入 新帧/无新帧/参数错 + 帧内容)
|
||||
*
|
||||
* 为什么要主机侧单测(而不是只用板端套件):
|
||||
@@ -29,19 +30,24 @@
|
||||
* TC-AI-019/01A 越界(ADC_SEM_COUNT)与负数语义 -> NULL
|
||||
* —— map_check(TC-AI-020)——
|
||||
* TC-AI-020 真实映射表自洽(反向路径结构上不可达,见文件末说明)
|
||||
* —— stm32_scan(TC-AI-021..078)——
|
||||
* TC-AI-021..025 全通道成功:返回 0 / 状态码 / 不计失败 / Start-Stop 各一次 / 轮询 9 次
|
||||
* —— stm32_take(TC-AI-021..078,v2.0 DMA 模型)——
|
||||
* TC-AI-021..025 取到新帧:返回 0 / 状态码 / 不计失败 / DMA 只挂一次 / seq=1
|
||||
* TC-AI-026..02B **Rank -> 通道号映射**:UB->CH14(rank9) / UC->CH3(rank4) /
|
||||
* IB->CH6(rank7) / IC->CH1(rank2) / IN->CH2(rank3) / TEMP->CH5(rank6)
|
||||
* TC-AI-02C stm32_scan 不置 TPAFE 有效标志(两来源独立)
|
||||
* TC-AI-030..034 HAL_ADC_Start 失败:返回 -1 / 状态码 RC_START / 计数 +1 / 不调 Stop
|
||||
* TC-AI-035..039 轮询超时:返回 -2 / 状态码 RC_TIMEOUT / 计数 +1 / 调 Stop 收尾 / 立即退出
|
||||
* TC-AI-03A 失败计数跨次单调累计
|
||||
* TC-AI-070..078 **失败拍不得沿用上一拍旧值**(2026-10-10 补,真机实测发现的
|
||||
* 正确性缺陷):先成功一拍让 STM32 侧进入有效态,再注入超时拍 /
|
||||
* Start 失败拍 -> adc_input_raw() 必须返 -2(而非上一拍的值);
|
||||
* 恢复拍重新置有效。TC-AI-034/039b 只覆盖"冷启动即失败",
|
||||
* 照不出"成功过之后失败 → 静默沿用旧值",故单独立一组。
|
||||
* TC-AI-02C stm32_take 不置 TPAFE 有效标志(两来源独立)
|
||||
* TC-AI-080..089 **乒乓双缓冲**:消费侧读"刚完成的半区",即使另一半正被 DMA
|
||||
* 改写也不得读到(撕裂帧防线);两拍交替半区、seq 推进;
|
||||
* 超拍未消费 -> 记一次丢帧(gap),与停摆(err)分开计数
|
||||
* TC-AI-090..09A HAL 错误回调计数 + 三个回调的 `hadc == &hadc1` 句柄守卫
|
||||
* (别的句柄不得污染本层 seq / hw_errs)
|
||||
* TC-AI-030..034 HAL_ADC_Start_DMA 失败:init 返回 -2 / 状态码 RC_START / 计数 +1
|
||||
* TC-AI-035..039 本拍无新帧:返回 RC_STALL / **单拍不计故障**(相位伪影)/ seq 不前进
|
||||
* TC-AI-03A..03E 连续两拍无新帧才计停摆;停摆与丢帧分开计数;恢复拍自愈
|
||||
* TC-AI-070..078 **停摆拍不得沿用上一拍旧值**(2026-10-10 补,真机实测发现的
|
||||
* 正确性缺陷;DMA 版改由"无新帧"触发同一路径):先成功一拍让
|
||||
* STM32 侧进入有效态,再注入无新帧拍 -> adc_input_raw() 必须返 -2;
|
||||
* 恢复拍重新置有效。TC-AI-034 只覆盖"挂载失败即无数据",
|
||||
* 照不出"成功过之后停摆 → 静默沿用旧值",故单独立一组。
|
||||
* —— adc_input_raw(TC-AI-040..04B)——
|
||||
* TC-AI-040..043 参数守卫:out=NULL / 越界 / 负数 -> -1,且不写 out
|
||||
* TC-AI-044/045 无有效数据 -> -2(两来源分别)
|
||||
@@ -71,58 +77,32 @@
|
||||
/* ===================== 平台桩(先纳入,覆盖真实头) ===================== */
|
||||
#include "adc.h"
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* 取 ADC_INPUT_STM32_CH_NUM 等常量(桩辅助函数在被测源之前) */
|
||||
|
||||
/* ===================== HAL ADC1 桩 =====================
|
||||
* 被测代码经 HAL 只做四件事:启动、逐 Rank 轮询、读 DR、停止。桩把它们变成可注入:
|
||||
* s_start_rc HAL_ADC_Start 返回值(注入启动失败)
|
||||
* s_poll_fail_at PollForConversion 第几次(0 基)失败;-1 = 永不失败(注入超时)
|
||||
* s_poll_vals[i] 第 i 个 Rank 读到的码值(默认 100+i,用于钉死 Rank->通道映射)
|
||||
* s_poll_calls / s_rank_idx 轮询次数 / 取数次数(观测:超时后是否立即退出)
|
||||
/* ===================== HAL ADC1 桩(v2.0:DMA 模型) =====================
|
||||
* 被测代码经 HAL 只做一件事:init 时挂一次 DMA。此后"一帧完成"由 DMA 中断的
|
||||
* HAL 回调告知——回调本体由被测代码实现,测试直接调它们即可注入节拍。
|
||||
* s_start_dma_rc HAL_ADC_Start_DMA 返回值(注入挂载失败)
|
||||
* s_start_dma_calls 挂载次数(正常应恒为 1:本层不反复重启 DMA)
|
||||
* s_dma_buf 被测代码交下来的乒乓缓冲地址(测试据此写"采样值")
|
||||
* s_dma_len 被测代码请求的搬运长度(应为 2 x 9 = 18)
|
||||
*/
|
||||
static HAL_StatusTypeDef s_start_rc = HAL_OK;
|
||||
static int s_poll_fail_at = -1;
|
||||
static uint32_t s_poll_vals[16];
|
||||
static int s_poll_calls = 0;
|
||||
static int s_rank_idx = 0;
|
||||
static int s_start_calls = 0;
|
||||
static int s_stop_calls = 0;
|
||||
static HAL_StatusTypeDef s_start_dma_rc = HAL_OK;
|
||||
static int s_start_dma_calls = 0;
|
||||
static uint32_t *s_dma_buf = NULL;
|
||||
static uint32_t s_dma_len = 0u;
|
||||
|
||||
/* 被测代码把 &hadc1 透传给 HAL 桩;桩不读其字段 */
|
||||
ADC_HandleTypeDef hadc1;
|
||||
/* 用于验证回调里的 `hadc == &hadc1` 守卫:别的句柄不得影响本层计数 */
|
||||
ADC_HandleTypeDef hadc_other;
|
||||
|
||||
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc) {
|
||||
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length) {
|
||||
(void)hadc;
|
||||
s_start_calls++;
|
||||
s_poll_calls = 0;
|
||||
s_rank_idx = 0;
|
||||
return s_start_rc;
|
||||
}
|
||||
|
||||
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc, uint32_t Timeout) {
|
||||
(void)hadc;
|
||||
(void)Timeout;
|
||||
{
|
||||
int idx = s_poll_calls;
|
||||
s_poll_calls++;
|
||||
if (s_poll_fail_at >= 0 && idx == s_poll_fail_at) {
|
||||
return HAL_ERROR;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc) {
|
||||
uint32_t v;
|
||||
(void)hadc;
|
||||
v = (s_rank_idx < 16) ? s_poll_vals[s_rank_idx] : 0u;
|
||||
s_rank_idx++;
|
||||
return v;
|
||||
}
|
||||
|
||||
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef *hadc) {
|
||||
(void)hadc;
|
||||
s_stop_calls++;
|
||||
return HAL_OK;
|
||||
s_start_dma_calls++;
|
||||
s_dma_buf = pData;
|
||||
s_dma_len = Length;
|
||||
return s_start_dma_rc;
|
||||
}
|
||||
|
||||
/* ===================== TPAFE5160 快照桩 ===================== */
|
||||
@@ -165,21 +145,16 @@ static int s_total = 0, s_passed = 0, s_failed = 0;
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:默认 Start 成功、轮询永不超时、第 i 个 Rank 读到 100+i(互不相同,
|
||||
* 以便逐通道钉死 Rank->通道号映射)、TPAFE 快照无新帧。
|
||||
* 函数说明:默认 DMA 挂载成功、TPAFE 快照无新帧。DMA 缓冲由 adc_input_init() 交下来,
|
||||
* 复位时清空指针(此时不得调用 dma_fill / dma_beat)。
|
||||
*/
|
||||
static void fakes_reset(void) {
|
||||
uint8_t i;
|
||||
|
||||
s_start_rc = HAL_OK;
|
||||
s_poll_fail_at = -1;
|
||||
s_poll_calls = 0;
|
||||
s_rank_idx = 0;
|
||||
s_start_calls = 0;
|
||||
s_stop_calls = 0;
|
||||
for (i = 0u; i < 16u; i++) {
|
||||
s_poll_vals[i] = 100u + (uint32_t)i;
|
||||
}
|
||||
s_start_dma_rc = HAL_OK;
|
||||
s_start_dma_calls = 0;
|
||||
s_dma_buf = NULL;
|
||||
s_dma_len = 0u;
|
||||
s_snapshot_rc = 0;
|
||||
s_snapshot_calls = 0;
|
||||
s_snapshot_count = 0u;
|
||||
@@ -189,6 +164,43 @@ static void fakes_reset(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:往乒乓缓冲的指定半区写入一帧"采样值"(base + Rank 序号)
|
||||
* 入口参数:half - 半区下标(0 / 1);base - 该帧首通道的码值
|
||||
* 返 回 值:无
|
||||
* 限定条件:须在 adc_input_init() 之后(缓冲地址由 Start_DMA 交下来)
|
||||
* 函数说明:值按 **Rank 序号** 递增,故第 i 个 Rank = base+i;配合
|
||||
* k_stm32_rank_ch=[CH0..CH7,CH14] 可逐通道钉死映射(如 UB=base+8)。
|
||||
*/
|
||||
static void dma_fill(uint8_t half, uint16_t base) {
|
||||
uint16_t *p = (uint16_t *)(void *)s_dma_buf;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) {
|
||||
p[((uint32_t)half * ADC_INPUT_STM32_CH_NUM) + i] = (uint16_t)(base + i);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:模拟"一拍 DMA 搬运完成"(写对应半区 + 触发对应 HAL 回调)
|
||||
* 入口参数:base - 本帧首通道码值
|
||||
* 返 回 值:无
|
||||
* 限定条件:须在 adc_input_init() 之后
|
||||
* 函数说明:DMA 长度 18、每拍搬 9 个 → 奇数次完成落在前半区(半传输回调)、
|
||||
* 偶数次落在后半区(传输完成回调)。半区下标由"已完成帧数"的奇偶推出,
|
||||
* 与被测代码 stm32_take() 的口径互为镜像(两边独立推导,能互相证伪)。
|
||||
*/
|
||||
static void dma_beat(uint16_t base) {
|
||||
uint8_t half = (uint8_t)(adc_input_stm32_seq() & 1u);
|
||||
|
||||
dma_fill(half, base);
|
||||
if (half == 0u) {
|
||||
HAL_ADC_ConvHalfCpltCallback(&hadc1);
|
||||
} else {
|
||||
HAL_ADC_ConvCpltCallback(&hadc1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== TC-AI-001..006:init ===================== */
|
||||
|
||||
static void tc_init(void) {
|
||||
@@ -198,16 +210,20 @@ static void tc_init(void) {
|
||||
/* 前置:制造"STM32 失败一次 + TPAFE 有效一帧"的状态,再验 init 是否清干净 */
|
||||
s_snapshot_rc = 1;
|
||||
s_fake_tpafe[6] = 4321;
|
||||
s_start_rc = HAL_ERROR;
|
||||
(void)adc_input_poll();
|
||||
(void)adc_input_poll(); /* 单拍无新帧 = 相位伪影,不计故障 */
|
||||
(void)adc_input_poll(); /* 连续第二拍无新帧 = 真停摆 */
|
||||
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-001 poll 失败后 STM32 失败计数 = 1");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-001 连续两拍无新帧 -> 停摆计数 = 1");
|
||||
CHECK((adc_input_raw(ADC_SEM_UA, &v) == 0) && (v == 4321),
|
||||
"TC-AI-002 poll 后 UA 可取数且值正确");
|
||||
|
||||
CHECK(adc_input_init() == 0, "TC-AI-003 adc_input_init 返回 0(映射自检通过)");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN,
|
||||
"TC-AI-004 init 后 STM32 状态码 = NOT_RUN");
|
||||
CHECK(s_start_dma_calls == 1, "TC-AI-003B init 挂载 DMA 恰好一次");
|
||||
CHECK(s_dma_len == (2u * ADC_INPUT_STM32_CH_NUM), "TC-AI-003C DMA 长度 = 2 x 9(乒乓双缓冲)");
|
||||
CHECK(s_dma_buf != NULL, "TC-AI-003D DMA 目的缓冲地址已交下来");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-AI-005B init 后 DMA 完成计数 = 0");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-AI-005 init 后 STM32 失败计数归零");
|
||||
CHECK((adc_input_raw(ADC_SEM_UA, &v) == -2) && (adc_input_raw(ADC_SEM_UB, &v) == -2),
|
||||
"TC-AI-006 init 后 TPAFE / STM32 两侧均无有效数据");
|
||||
@@ -261,20 +277,20 @@ static void tc_map_check(void) {
|
||||
|
||||
/* ===================== TC-AI-021..02C:stm32_scan 成功路径 ===================== */
|
||||
|
||||
static void tc_scan_ok(void) {
|
||||
static void tc_take_ok(void) {
|
||||
int32_t v = 0;
|
||||
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
dma_beat(100); /* 前半区一帧:Rank i -> 100+i */
|
||||
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_OK, "TC-AI-021 stm32_scan 全通道成功 -> 0");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-021 stm32_take 取到新帧 -> 0");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-022 状态码 = RC_OK");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-AI-023 成功不计入失败");
|
||||
CHECK((s_start_calls == 1) && (s_stop_calls == 1), "TC-AI-024 Start / Stop 各一次");
|
||||
CHECK((s_poll_calls == (int)ADC_INPUT_STM32_CH_NUM) && (s_rank_idx == (int)ADC_INPUT_STM32_CH_NUM),
|
||||
"TC-AI-025 轮询与取数各 9 次(Rank1..9)");
|
||||
CHECK(s_start_dma_calls == 1, "TC-AI-024 DMA 只在 init 挂一次(每拍不重启)");
|
||||
CHECK(adc_input_stm32_seq() == 1u, "TC-AI-025 DMA 完成计数 = 1");
|
||||
|
||||
/* Rank -> 通道号映射:第 i 个 Rank 读到 100+i。
|
||||
/* Rank -> 通道号映射:第 i 个 Rank = 100+i。
|
||||
* k_stm32_rank_ch = [CH0..CH7, CH14],故 CH3->103 / CH5->105 / CH6->106 / CH14->108。 */
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-026 UB 取到 CH14(rank9)=108");
|
||||
CHECK((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 103), "TC-AI-027 UC 取到 CH3(rank4)=103");
|
||||
@@ -283,78 +299,139 @@ static void tc_scan_ok(void) {
|
||||
CHECK((adc_input_raw(ADC_SEM_IN, &v) == 0) && (v == 102), "TC-AI-02A IN 取到 CH2(rank3)=102");
|
||||
CHECK((adc_input_raw(ADC_SEM_TEMP, &v) == 0) && (v == 105), "TC-AI-02B TEMP 取到 CH5(rank6)=105");
|
||||
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-02C stm32_scan 不置 TPAFE 有效标志");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-02C stm32_take 不置 TPAFE 有效标志");
|
||||
}
|
||||
|
||||
/* ===================== TC-AI-030..034:stm32_scan 启动失败 ===================== */
|
||||
/* ============ TC-AI-080..085:乒乓双缓冲(读刚完成半区,不读正在写的半区) ============
|
||||
* 这是 DMA 方案最关键的一条:消费侧必须读"上一拍刚完成的那一半",而 DMA 此刻正在
|
||||
* 写另一半。若读错半区,取到的是**正在被改写**的数据 → 半新半旧的撕裂帧。 */
|
||||
|
||||
static void tc_scan_start_fail(void) {
|
||||
static void tc_dma_pingpong(void) {
|
||||
int32_t v = 0;
|
||||
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
s_start_rc = HAL_ERROR;
|
||||
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_START, "TC-AI-030 HAL_ADC_Start 失败 -> -1");
|
||||
dma_beat(100); /* 第 1 帧 -> 前半区(半传输回调) */
|
||||
dma_fill(1u, 900u); /* 模拟 DMA 正在写后半区(值 900 系) */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-080 第一拍取帧成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108),
|
||||
"TC-AI-081 读的是刚完成的前半区(108),不是正在写的后半区");
|
||||
|
||||
dma_beat(200); /* 第 2 帧 -> 后半区(传输完成回调) */
|
||||
dma_fill(0u, 800u); /* 模拟 DMA 正在写前半区(值 800 系) */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-082 第二拍取帧成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208),
|
||||
"TC-AI-083 第二拍读到后半区(208)");
|
||||
CHECK(adc_input_stm32_seq() == 2u, "TC-AI-084 DMA 完成计数 = 2");
|
||||
|
||||
/* 半区奇偶口径:seq=1 -> 前半区,seq=2 -> 后半区(与 dma_beat 独立推导,互相证伪) */
|
||||
CHECK(((adc_input_raw(ADC_SEM_UC, &v) == 0) && (v == 203)), "TC-AI-085 UC 同帧内一致(203)");
|
||||
|
||||
/* 消费者超拍:两拍都没取 -> 中间那帧已被乒乓缓冲覆盖 -> 记一次丢帧 */
|
||||
dma_beat(300);
|
||||
dma_beat(400);
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-086 超拍后仍取到最新帧");
|
||||
CHECK(adc_input_stm32_gaps() == 1u, "TC-AI-087 seq 前进 2 -> 记一次丢帧");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 408), "TC-AI-088 取到的是最新帧(408)");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-AI-089 丢帧不计入停摆(两类故障分开)");
|
||||
}
|
||||
|
||||
/* ============ TC-AI-090..093:HAL 错误回调与句柄守卫 ============ */
|
||||
|
||||
static void tc_hw_errs(void) {
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
|
||||
HAL_ADC_ErrorCallback(&hadc1);
|
||||
HAL_ADC_ErrorCallback(&hadc1);
|
||||
CHECK(adc_input_stm32_hw_errs() == 2u, "TC-AI-090 错误回调累计 = 2");
|
||||
|
||||
HAL_ADC_ErrorCallback(&hadc_other);
|
||||
CHECK(adc_input_stm32_hw_errs() == 2u, "TC-AI-091 别的 ADC 句柄不得计入(hadc 守卫)");
|
||||
|
||||
HAL_ADC_ConvHalfCpltCallback(&hadc_other);
|
||||
HAL_ADC_ConvCpltCallback(&hadc_other);
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-AI-092 别的句柄的完成回调不得推进 seq");
|
||||
|
||||
HAL_ADC_ErrorCallback(&hadc1);
|
||||
CHECK(adc_input_stm32_hw_errs() == 3u, "TC-AI-093 hw_errs 与 errs 分开计数、互不干扰");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-AI-09A hw_errs 不污染 errs");
|
||||
}
|
||||
|
||||
/* ===================== TC-AI-030..034:DMA 挂载失败 ===================== */
|
||||
|
||||
static void tc_init_dma_fail(void) {
|
||||
int32_t v = 0;
|
||||
|
||||
fakes_reset();
|
||||
s_start_dma_rc = HAL_ERROR;
|
||||
|
||||
CHECK(adc_input_init() == -2, "TC-AI-030 HAL_ADC_Start_DMA 失败 -> init 返回 -2");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-AI-031 状态码 = RC_START");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-032 失败计数 +1");
|
||||
CHECK(s_stop_calls == 0, "TC-AI-033 启动失败不调用 Stop");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-034 启动失败后 STM32 侧仍无有效数据");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-034 挂载失败后 STM32 侧无有效数据");
|
||||
}
|
||||
|
||||
/* ===================== TC-AI-035..03A:stm32_scan 轮询超时 ===================== */
|
||||
/* ===================== TC-AI-035..03A:本拍无新帧(停摆) ===================== */
|
||||
|
||||
static void tc_scan_timeout(void) {
|
||||
static void tc_take_stall(void) {
|
||||
int32_t v = 0;
|
||||
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
s_poll_fail_at = 4; /* 第 5 个 Rank 轮询超时 */
|
||||
/* 没有任何 DMA 完成回调 = TIM3 未启动 / ADC 停摆 / DMA 出错 */
|
||||
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_TIMEOUT, "TC-AI-035 轮询超时 -> -2");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_TIMEOUT, "TC-AI-036 状态码 = RC_TIMEOUT");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-037 超时计入失败");
|
||||
CHECK(s_stop_calls == 1, "TC-AI-038 超时前已 Start -> 调 Stop 收尾");
|
||||
CHECK(s_poll_calls == 5, "TC-AI-039 超时后立即退出(只轮询 5 次)");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-039b 超时后 STM32 侧无有效数据");
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-AI-035 无新帧 -> RC_STALL(-2)");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-AI-036 状态码 = RC_STALL");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-AI-037 单拍无新帧不计故障(相位伪影)");
|
||||
CHECK(adc_input_stm32_seq() == 0u, "TC-AI-038 停摆期间 seq 不前进");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-039 停摆拍 STM32 侧无有效数据");
|
||||
|
||||
s_poll_fail_at = 0; /* 再失败一次 */
|
||||
(void)stm32_scan();
|
||||
CHECK(adc_input_stm32_errs() == 2u, "TC-AI-03A 失败计数跨次单调累计 = 2");
|
||||
(void)stm32_take(); /* 连续第二拍无新帧 = 真停摆 */
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-03A 连续两拍无新帧 -> 停摆计数 = 1");
|
||||
(void)stm32_take(); /* 第三拍仍无新帧 */
|
||||
CHECK(adc_input_stm32_errs() == 2u, "TC-AI-03B 停摆持续 -> 每拍累计 = 2");
|
||||
CHECK(adc_input_stm32_gaps() == 0u, "TC-AI-03C 停摆不计入丢帧(两类故障分开)");
|
||||
|
||||
dma_beat(100); /* 恢复 */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-03D 恢复后取帧成功");
|
||||
CHECK(adc_input_stm32_gaps() == 0u, "TC-AI-03E 恢复拍前进 1 -> 不算丢帧");
|
||||
}
|
||||
|
||||
/* ============ TC-AI-070..077:失败拍不得沿用上一拍旧值(陈旧帧) ============
|
||||
* 背景(2026-10-10 真机实测发现):本层只有一份 s_stm32_raw[],超时拍里"超时前
|
||||
* 几个 Rank 已写本拍新值、后面几个还是上一拍旧值",而 s_stm32_valid 此前只在
|
||||
* 成功时置 1、失败时**从不清零** → 消费侧照样取数、照样压入采样环,
|
||||
* s_frames_skip 抓不到。TC-AI-034/039b 都是"init 后立刻失败"(valid 本就是 0),
|
||||
* 因此照不出这条路径——必须先成功一拍。 */
|
||||
/* ============ TC-AI-070..078:停摆拍不得沿用上一拍旧值(陈旧帧) ============
|
||||
* 背景(2026-10-10 真机实测发现):本层只有一份 s_stm32_raw[],而 s_stm32_valid
|
||||
* 此前只在成功时置 1、失败时**从不清零** → 消费侧照样取数、照样压入采样环,
|
||||
* s_frames_skip 抓不到。轮询版的触发路径是"超时拍半新半旧",DMA 版(v2.0)改成
|
||||
* "本拍无新帧仍沿用上一拍"——**同一条缺陷、同一个防线**,故本组用例保留。
|
||||
* TC-AI-034 是"挂载失败后本就无数据"(valid 一直是 0),照不出这条路径——
|
||||
* 必须先成功一拍。 */
|
||||
|
||||
static void tc_scan_stale(void) {
|
||||
static void tc_take_stale(void) {
|
||||
int32_t v = 0;
|
||||
|
||||
/* --- 超时支路:成功拍 -> 超时拍 -> 恢复拍 --- */
|
||||
/* --- 停摆支路:成功拍 -> 无新帧拍 -> 恢复拍 --- */
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_OK, "TC-AI-070 前置:首拍成功");
|
||||
dma_beat(100);
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-070 前置:首拍成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-071 前置:UB 取到 108(有效态)");
|
||||
|
||||
s_poll_fail_at = 4; /* 第 5 个 Rank 超时(前 4 个已写新值) */
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_TIMEOUT, "TC-AI-072 第二拍轮询超时");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-073 超时拍不得沿用上一拍值 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-AI-074 超时拍连本拍已写入的 Rank 也不得出数");
|
||||
/* 第二拍没有任何 DMA 完成(TIM3 停 / ADC 停摆 / DMA 出错) */
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_STALL, "TC-AI-072 第二拍无新帧");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-073 停摆拍不得沿用上一拍值 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UC, &v) == -2, "TC-AI-074 停摆拍连其它 STM32 通道也不得出数");
|
||||
CHECK(adc_input_raw(ADC_SEM_TEMP, &v) == -2, "TC-AI-074B 同上(TEMP 亦 -2)");
|
||||
|
||||
s_poll_fail_at = -1;
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_OK, "TC-AI-075 第三拍恢复成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 108), "TC-AI-076 恢复拍重新可取数(自愈)");
|
||||
dma_beat(200);
|
||||
CHECK(stm32_take() == ADC_INPUT_RC_OK, "TC-AI-075 第三拍恢复成功");
|
||||
CHECK((adc_input_raw(ADC_SEM_UB, &v) == 0) && (v == 208), "TC-AI-076 恢复拍重新可取数(自愈)");
|
||||
|
||||
/* --- 启动失败支路:同样不得沿用 --- */
|
||||
/* --- 挂载失败支路:同样不得沿用(init 失败后整层无有效数据) --- */
|
||||
fakes_reset();
|
||||
s_start_dma_rc = HAL_ERROR;
|
||||
(void)adc_input_init();
|
||||
(void)stm32_scan(); /* 先成功一拍 */
|
||||
s_start_rc = HAL_ERROR;
|
||||
CHECK(stm32_scan() == ADC_INPUT_RC_START, "TC-AI-077 Start 失败拍");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-078 Start 失败拍不得沿用上一拍值 -> -2");
|
||||
CHECK(adc_input_raw(ADC_SEM_UB, &v) == -2, "TC-AI-078 挂载失败后不得取到任何值 -> -2");
|
||||
}
|
||||
|
||||
/* ===================== TC-AI-040..045:raw 参数守卫与无数据 ===================== */
|
||||
@@ -409,9 +486,10 @@ static void tc_poll(void) {
|
||||
fakes_reset();
|
||||
(void)adc_input_init();
|
||||
s_snapshot_rc = 0;
|
||||
dma_beat(100);
|
||||
CHECK(adc_input_poll() == 0, "TC-AI-050 快照无新帧 -> poll 返回 0");
|
||||
CHECK(adc_input_raw(ADC_SEM_UA, &v) == -2, "TC-AI-051 无新帧时 TPAFE 侧无效");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-052 同拍 STM32 扫描照常执行");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-052 同拍 STM32 取帧照常执行");
|
||||
|
||||
/* --- 参数错(-1):返回 -1,TPAFE 无效 --- */
|
||||
fakes_reset();
|
||||
@@ -436,11 +514,11 @@ static void tc_poll(void) {
|
||||
(void)adc_input_init();
|
||||
s_fake_tpafe[2] = 55;
|
||||
s_snapshot_rc = 1;
|
||||
s_start_rc = HAL_ERROR;
|
||||
CHECK(adc_input_poll() == 1, "TC-AI-056 STM32 失败不影响 poll 返回值(仍为 1)");
|
||||
/* 不注入任何 DMA 完成 = STM32 侧停摆 */
|
||||
CHECK(adc_input_poll() == 1, "TC-AI-056 STM32 停摆不影响 poll 返回值(仍为 1)");
|
||||
CHECK((adc_input_raw(ADC_SEM_IA, &v) == 0) && (v == 55),
|
||||
"TC-AI-057 STM32 失败不影响 TPAFE 取数");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_START, "TC-AI-058 STM32 失败另由状态码暴露");
|
||||
"TC-AI-057 STM32 停摆不影响 TPAFE 取数");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-AI-058 STM32 停摆另由状态码暴露");
|
||||
}
|
||||
|
||||
/* ===================== TC-AI-060..066:rc / errs 观测 ===================== */
|
||||
@@ -452,22 +530,21 @@ static void tc_rc_errs(void) {
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_NOT_RUN, "TC-AI-060 init 后未扫描 = NOT_RUN(-3)");
|
||||
CHECK(adc_input_stm32_errs() == 0u, "TC-AI-061 init 后失败计数 = 0");
|
||||
|
||||
(void)stm32_scan(); /* 成功 */
|
||||
dma_beat(100);
|
||||
(void)stm32_take(); /* 成功 */
|
||||
CHECK((adc_input_stm32_rc() == ADC_INPUT_RC_OK) && (adc_input_stm32_errs() == 0u),
|
||||
"TC-AI-062 成功: rc=OK 且 errs 不变");
|
||||
|
||||
s_start_rc = HAL_ERROR;
|
||||
(void)stm32_scan(); /* 失败 1 */
|
||||
s_start_rc = HAL_OK;
|
||||
s_poll_fail_at = 0;
|
||||
(void)stm32_scan(); /* 失败 2 */
|
||||
CHECK(adc_input_stm32_errs() == 2u, "TC-AI-063 两次失败累计 = 2");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_TIMEOUT, "TC-AI-064 rc = 最近一次(TIMEOUT)");
|
||||
(void)stm32_take(); /* 停摆 1(单拍 = 相位伪影,不计) */
|
||||
(void)stm32_take(); /* 停摆 2 -> 计 1 */
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-063 连续两拍停摆累计 = 1");
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_STALL, "TC-AI-064 rc = 最近一次(STALL)");
|
||||
|
||||
s_poll_fail_at = -1;
|
||||
(void)stm32_scan(); /* 恢复成功 */
|
||||
dma_beat(200);
|
||||
(void)stm32_take(); /* 恢复成功 */
|
||||
CHECK(adc_input_stm32_rc() == ADC_INPUT_RC_OK, "TC-AI-065 恢复成功后 rc=OK");
|
||||
CHECK(adc_input_stm32_errs() == 2u, "TC-AI-066 errs 单调递增(不因成功回落)");
|
||||
CHECK(adc_input_stm32_errs() == 1u, "TC-AI-066 errs 单调递增(不因成功回落)");
|
||||
CHECK(adc_input_stm32_seq() == 2u, "TC-AI-067 seq 只由 DMA 完成回调推进");
|
||||
}
|
||||
|
||||
/* ===================== main ===================== */
|
||||
@@ -481,14 +558,18 @@ int main(void) {
|
||||
tc_desc();
|
||||
printf("-- map self-check --\n");
|
||||
tc_map_check();
|
||||
printf("-- stm32 scan: success + rank/channel mapping --\n");
|
||||
tc_scan_ok();
|
||||
printf("-- stm32 scan: HAL_ADC_Start fail --\n");
|
||||
tc_scan_start_fail();
|
||||
printf("-- stm32 scan: poll timeout --\n");
|
||||
tc_scan_timeout();
|
||||
printf("-- stm32 scan: stale frame on failure beat --\n");
|
||||
tc_scan_stale();
|
||||
printf("-- stm32 take: success + rank/channel mapping --\n");
|
||||
tc_take_ok();
|
||||
printf("-- stm32 dma: ping-pong double buffer --\n");
|
||||
tc_dma_pingpong();
|
||||
printf("-- stm32 dma: HAL error callback + handle guard --\n");
|
||||
tc_hw_errs();
|
||||
printf("-- stm32 dma: Start_DMA fail --\n");
|
||||
tc_init_dma_fail();
|
||||
printf("-- stm32 take: no new frame (stall) --\n");
|
||||
tc_take_stall();
|
||||
printf("-- stm32 take: stale frame on stall beat --\n");
|
||||
tc_take_stale();
|
||||
printf("-- raw: guards / no-data --\n");
|
||||
tc_raw_guards();
|
||||
printf("-- raw: tpafe index + two's complement --\n");
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
做什么:
|
||||
用本机 MSVC 编译 test/host/adc_input/adc_input_test.c(它以 #include 纳入
|
||||
**生产源文件** App/algo/adc_input.c),在 PC 上离线跑语义映射表 / STM32 ADC1
|
||||
顺序扫描 / TPAFE 快照消费 / 取数守卫 / poll 契约 / 状态码统计,失败即非 0 退出。
|
||||
DMA 乒乓帧消费(含半区选择)/ TPAFE 快照消费 / 取数守卫 / poll 契约 / 四类故障
|
||||
计数(seq / gap / err / hw),失败即非 0 退出。
|
||||
零硬件、零烧录、毫秒级。
|
||||
|
||||
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
*
|
||||
* 保真点:F4 HAL 的 ADC_CHANNEL_x 宏值**即通道号 x**(0..15),本桩按此定义,
|
||||
* 故被测代码里 `(uint8_t)ADC_CHANNEL_14` 得到 14 —— 与真机一致。
|
||||
* 另声明 4 个 HAL_ADC_* 接口,由测试单元给强符号桩(可注入失败/超时/返回值)。
|
||||
* 另声明 1 个 HAL_ADC_Start_DMA 接口(由测试单元给强符号桩,可注入挂载失败)与
|
||||
* 3 个 HAL 回调(**由被测代码 adc_input.c 实现**,测试直接调用它们模拟 DMA 中断)。
|
||||
*
|
||||
* 创建日期:2026-10-10
|
||||
*/
|
||||
@@ -50,9 +51,14 @@ extern ADC_HandleTypeDef hadc1;
|
||||
#define ADC_CHANNEL_14 (0x0000000EU)
|
||||
#define ADC_CHANNEL_15 (0x0000000FU)
|
||||
|
||||
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);
|
||||
HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc, uint32_t Timeout);
|
||||
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef *hadc);
|
||||
HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef *hadc);
|
||||
/* v2.0:STM32 侧改 TIM3 TRGO 触发 + DMA 双缓冲,本层只调 Start_DMA 一次;
|
||||
* 逐通道 Start / PollForConversion / GetValue / Stop 已从被测代码中消失,桩同步删除 */
|
||||
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length);
|
||||
|
||||
/* 三个 HAL 弱回调:真机上由 DMA 中断上下文调用,这里由**被测代码实现**、
|
||||
* 由测试单元直接调用以模拟"一帧搬运完成" */
|
||||
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc);
|
||||
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc);
|
||||
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc);
|
||||
|
||||
#endif /* ADC_H */
|
||||
|
||||
Reference in New Issue
Block a user