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 栈 / 目录组织)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user