diff --git a/App/algo/adc_input.c b/App/algo/adc_input.c index 13011d62..9771dbd8 100644 --- a/App/algo/adc_input.c +++ b/App/algo/adc_input.c @@ -79,16 +79,16 @@ static int s_stm32_rc; /* STM32 最近一次扫描状态码 */ static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */ /* - * 函数功能:消费一帧 STM32 ADC1 采样(L1-3,v2.0 DMA 双缓冲版) + * 函数功能:消费一帧 STM32 ADC1 采样(L1-3,DMA 双缓冲) * 入口参数:无 * 返 回 值:0 - 取到新一帧;-2 - 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错) * 限定条件:仅任务上下文调用;adc_input_init() 已挂好 DMA * 函数说明:本函数**既不触发转换也不等待转换**——转换由 TIM3 TRGO 硬件触发、由 DMA * 自动搬运。每拍只做"看完成计数是否前进 + 拷 9 个半字",故**零忙等**: - * 原轮询版常态 adcp=122 µs、超时拍最坏 2~3 ms,现在两档都降到 ~1 µs 量级。 + * 单拍只剩"读一个计数 + 拷 9 个半字"→ µs 量级。 * 读的是"上一拍刚完成的那一半"乒乓缓冲(DMA 此刻正在写另一半)→ 无撕裂。 * ⚠ 计数未前进 = 本拍 STM32 侧**无有效数据**,必须清 s_stm32_valid,绝不 - * 沿用上一拍旧值(同 2026-10-10 修的陈旧帧缺陷口径)→ adc_input_raw() + * 沿用上一拍旧值(半新半旧的混合帧会污染相量窗)→ adc_input_raw() * 返 -2 → adc_task 走既有"整帧跳过"路径;下一拍自动恢复。 */ static int stm32_take(void) { @@ -145,8 +145,8 @@ static int stm32_take(void) { * 函数说明:逐条校验映射表与扫描配置是否一致——① TPAFE 源下标须落在 * buf[0..7];② STM32 源的通道号必须出现在 k_stm32_rank_ch[] 中, * 否则该通道永远不会被扫描(读到的恒为陈旧值)。这是"通道宏写错 → - * 读到另一只脚/读不到"这一 §5 警告的代码级兜底(无信号源时无法做 - * 引脚级注入验证,见 docs/硬件平台/ADC通道映射说明.md §8)。 + * 读到另一只脚/读不到"这一警告的代码级兜底(警告口径与逐通道验证方法: + * docs/硬件平台/ADC通道映射说明.md §5、§8;台架无信号源时注入验证做不了)。 */ static int map_check(void) { uint8_t s; diff --git a/App/algo/adc_input.h b/App/algo/adc_input.h index b8484d31..93b0e1fe 100644 --- a/App/algo/adc_input.h +++ b/App/algo/adc_input.h @@ -4,7 +4,7 @@ * 暴露,使上层(UI / 自检 / 保护算法)**按语义取数,不写死下标**: * ① 语义通道映射表(L1-4)——权威口径 docs/硬件平台/ADC通道映射说明.md; * ② 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) @@ -16,8 +16,9 @@ * 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() + * (换源判据:F4 上 ADC1 收不到 TIM6/TIM7 的 TRGO,见 + * docs/子系统设计/ADC采样节拍设计说明.md §3.2);新增 + * adc_input_stm32_seq() / _hw_errs() */ #ifndef ADC_INPUT_H @@ -61,7 +62,7 @@ typedef struct { /* STM32 ADC1 顺序扫描通道数(Rank 1..9) */ #define ADC_INPUT_STM32_CH_NUM 9U -/* STM32 ADC1 最近一次取帧状态码(v2.0:DMA 消费,已无"轮询超时"这一档) */ +/* STM32 ADC1 最近一次取帧状态码(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 出错) */ diff --git a/App/algo/freq.c b/App/algo/freq.c index 0e634a71..d4fe518e 100644 --- a/App/algo/freq.c +++ b/App/algo/freq.c @@ -15,7 +15,7 @@ * ≈4 个信号周波),使窗长恰为纹波周期的整数倍 → δ(m+K)≈δ(m),残差进入 * 0.01 Hz 量级。f_hat 用上一拍读数(收敛后误差 <0.1 Hz → K_eff 偏差 <1 拍, * 自洽)。 - * **精度优先于响应的显式取舍(Lead 批准 2026-10-12)**:4 周波窗换来 + * **精度优先于响应的显式取舍**:4 周波窗换来 * 0.317→0.0101 Hz,代价是频率骤变后的窗冲刷(J4 阶跃整定 ≤120 ms,实测 * 98 ms)。OF/UF 定时限默认 1000 ms → 测频响应占保护总动作时间 <10%; * 暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 J4 的 120 ms 是 diff --git a/App/algo/phasor.c b/App/algo/phasor.c index 696f9e0a..66e83ad2 100644 --- a/App/algo/phasor.c +++ b/App/algo/phasor.c @@ -44,7 +44,7 @@ static const float k_fft_cos[PHASOR_N] = { /* ===== L2-2 相量实例表 ===== * 参考项目为 **4 个实例**(C01 基波 I/U、C02 零序基波、C03 二次谐波、C04 五次谐波, * `APP/CLS100_Dsp/InstCalCell.c`)。本项目按 V0.1 **有效通道**(`UA`/`UN`/`IA`, - * 见规划 §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开——C01 的"I/U" + * 见 docs/算法/继电保护功能复现规划.md §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开——C01 的"I/U" * 拆为两行(IA 电流 / UA 电压),并补出 C04 的五次谐波轨(L1-9)。 * 新增实例只需在表尾加一行,其余代码自动适配。 */ static const phasor_inst_t k_inst[] = { @@ -62,8 +62,8 @@ static const phasor_inst_t k_inst[] = { * (单字节 store,Cortex-M 上原子),消费侧读"使用区" → 无锁、无撕裂。 * s_use_zone 声明 volatile:阻止编译器把结果写入挪到切换之后 * (本平台单核顺序执行,无需额外内存屏障)。 - * 今日唯一消费者是 adcTask 自身(同拍读写、实际无竞争),本机制为后续 - * 独立保护任务(L3)预留。 */ + * 写者 = adcTask(同拍);读者跨两个上下文:adcTask 心跳与 protectTask 判据 + * (prot_elm_run → phasor_get)→ 无锁读正是为后者服务,勿改成加锁或去掉双区。 */ static phasor_t s_zone[2][PHASOR_INST_N]; static volatile uint8_t s_use_zone; static uint8_t s_ready; /* 使用区是否已产出过有效结果 */ diff --git a/App/app/app_main.c b/App/app/app_main.c index 3c992002..b92e99d5 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -290,7 +290,7 @@ int app_main_init(void) { * ② 同一事件经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)。 * ⚠ 必须在 tpafe5160_init() 成功之后启动,否则 ISR 会在驱动首帧自检 * (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。 - * ⚠ 节拍源 v2.0 由 TIM6 改为 **TIM3**:F4 上 ADC1 的外部触发只认 + * ⚠ 节拍源必须能触发 ADC1,故用 **TIM3**:F4 上 ADC1 的外部触发只认 * TIM1/TIM2/TIM3/TIM4/TIM5/TIM8 的 CC 或 TRGO,**TIM6/TIM7 的 TRGO 只接 * DAC**(查 CubeMX DB 与本仓库 stm32f4xx_hal_adc.h 的 EXTSEL 表证实) * → 要让两片转换器由同一个硬件事件驱动,节拍源必须换成 TIM3。 */ diff --git a/App/task/adc_task.c b/App/task/adc_task.c index 9d35caf8..c462ca6f 100644 --- a/App/task/adc_task.c +++ b/App/task/adc_task.c @@ -7,15 +7,15 @@ * 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者: * 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数, * 并纳入看门狗 required_mask(TASK_WD_ADC)。 - * **采样节拍(v2.2,2026-10-10)**:由 **TIM3 更新中断(1 kHz)** 硬件触发—— + * **采样节拍**:由 **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` + * **消费唤醒**:由 1 ms `osDelayUntil` * 轮询改为 **TIM3 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定, * 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级 * 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见 * docs/子系统设计/ADC采样节拍设计说明.md §6。 - * **L1 采集层(v2.3,2026-10-10)**:取数改走 App/algo/adc_input.c—— + * **L1 采集层**:取数改走 App/algo/adc_input.c—— * 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描), * 经 adc_input_raw() **按语义**读通道(L1-3/L1-4),不再直接触碰驱动缓冲。 * V0.1 上只有 TPAFE 三路(UA/UN/IA)测量有效;本任务只取数,不做保护运算。 @@ -49,25 +49,25 @@ * ⚠ L1-5 通道标定未实现(缺标准源)→ 暂以**原始码值** * 充作工程量(系数 = 1),故相量读数只证明**链路通**, * 不可作幅值精度判据(同 L1-3 的替代口径)。 - * v2.5 2026-10-10 王建锋 Task #32 的 P2 测量埋点:单拍加常开耗时探针 + * v2.5 2026-10-10 王建锋 常开耗时测量埋点:单拍加探针 * (task_prof ADC_BEAT),用于判定本任务是否超拍 * (名义节拍 1 ms = 168000 周期)而挤占低优先级任务 - * v2.6 2026-10-10 王建锋 Task #32 的 P0 修复(两处): - * §5 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算 + * v2.6 2026-10-10 王建锋 两处正确性修复: + * 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算 * (消除重复采样污染 20 点 DFT 窗的系统性误差); - * §4.3 osDelayUntil 返回值被丢弃 → 落后时一次性重同步 + * osDelayUntil 返回值被丢弃 → 落后时一次性重同步 * (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask) - * v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费): + * v2.7 2026-10-10 王建锋 事件驱动消费改造: * 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG, * osWaitForever),标志由 TIM3 ISR 经 adc_tick_irq_notify() 置位; - * 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要); + * 删除 ADC_TASK_BEAT_MS 与 next(上一轮加的重同步钩子因此不再需要); * 新增"消费者就绪"门 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 双缓冲 + * v2.8 2026-10-10 王建锋 节拍源换 TIM3(F4 上 ADC1 收不到 TIM6/TIM7 的 + * TRGO,换源判据见 docs/子系统设计/ADC采样节拍设计说明.md + * §3.2);STM32 侧改 DMA 双缓冲 * 消费,adc_input_poll() 不再忙等;心跳日志增 * adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧") * v2.9 2026-10-11 王建锋 L1-6 采样有效性校验:每拍调 adc_health_check() @@ -90,13 +90,13 @@ #include "task_watchdog.h" #include "console_log.h" #include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */ -#include "task_prof.h" /* P2 常开探针:单拍耗时(Task #32) */ +#include "task_prof.h" /* 常开耗时探针:单拍耗时 */ #define DBG_TAG "[ADC_TASK]" #include "dbg_log.h" /* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。 - * (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除—— + * (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随事件驱动改造删除—— * 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */ #define ADC_TASK_REPORT_MS 10000U @@ -135,8 +135,8 @@ static volatile uint8_t s_consumer_ready; * 返 回 值:无 * 限定条件:仅 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 时 + * 函数说明:ISR **只发唤醒**,全部计算留在任务里——"ISR 内计算"是反模式:取数与相量 + * 计算都读写任务级共享数据,且本回调属 CubeMX 生成区,重生成即丢。线程标志粘滞 → 单拍偶超 1 ms 时 * 多个脉冲合并成一次唤醒,下一轮 adc_input_poll() 返回 r==0 记 idle,不会重复处理同一帧 */ void adc_tick_irq_notify(void) { @@ -248,7 +248,7 @@ int adc_task_init(void) { * 限定条件:adc_task_init() 已调用;内核已启动 * 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 乒乓缓冲) * 并统计。采样节拍由 TIM3 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发; - * **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动, + * **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(事件驱动, * 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 = * EXTI 未在读帧或 TIM3 未启动(异常),由周期心跳日志暴露。 * ⚠ 若 TIM3 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 → @@ -269,8 +269,8 @@ void adc_task_func(void *arg) { int r; uint32_t now = osKernelGetTickCount(); /* 本拍时基:健康判定 + 心跳共用一份 */ - /* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过, - * 本任务(v2.7 起为 osPriorityAboveNormal)会持续抢占 uiTask + /* 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过, + * 本任务(osPriorityAboveNormal)会持续抢占 uiTask * (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */ task_prof_enter(TASK_PROF_ADC_BEAT); @@ -278,9 +278,8 @@ void adc_task_func(void *arg) { * 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 个半字)。 + * 探针:本段单独计时。本段**零忙等**(转换由 TIM3 TRGO 触发、DMA 搬运, + * 不再有逐通道轮询超时),只剩 TPAFE 快照临界区 + 拷 9 个半字 → us 量级。 * ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */ task_prof_enter(TASK_PROF_ADC_POLL); r = adc_input_poll(); @@ -296,13 +295,12 @@ void adc_task_func(void *arg) { /* 2) L2 输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部 * 相量实例(L2-6 滑动窗:每拍以"当前时刻为终止的前一周波"整窗重算)。 * - * ⚠ P0 修复(2026-10-10,Task #32 §5):**仅在本拍确有新帧(r == 1)时**才压环。 + * ⚠ **仅在本拍确有新帧(r == 1)时**才压环。 * 原实现在 r == 0(TPAFE 无新帧)时**仍然**压环 + phasor_run(),只有 r == -1 * 才跳过 —— 于是把**上一拍的重复采样值**塞进 20 点 DFT 窗,等效采样间隔被拉长。 - * 证据是日志里的恒等式 `ring = frames + idle`(09:36:21: 9957+44=10001 ✓; - * 09:36:31: 19352+649=20001 ✓)。本例 idle 占 3.2% → 窗长从 20.0 ms 漂到约 - * 20.6 ms → 对 50 Hz 信号相当于约 3% 的频率失配 → 幅值/相位**系统性**误差 - * (不是随机噪声)。修好后 `ring` 只在真实新帧上增长(ring == frames)。 + * 量级参考(台架曾测得 idle 占 3.2%):窗长从 20.0 ms 漂到约 20.6 ms → 对 50 Hz + * 相当于约 3% 频率失配 → 幅值/相位**系统性**误差(不是随机噪声)。 + * 回归判据 = 恒等式 `ring == frames`(只有真新帧才入环)。 * * 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点 * (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */ @@ -398,7 +396,7 @@ void adc_task_func(void *arg) { { if ((int32_t)(now - report_at) >= 0) { if (dbg_console_writer_ready() != 0u) { - /* STM32 侧观测量(v2.0 DMA 版),四类各有独立成因: + /* STM32 侧观测量(DMA 版),四类各有独立成因: * adc1_rc = 最近一拍取帧状态(0 OK / -2 本拍无新帧) * adc1_seq = DMA 整帧完成计数(正常时 ≈ seq,恒定小偏移) * adc1_gap = 丢帧:seq 一次前进 >1 → 本任务超拍(单拍 > 1 ms) @@ -429,16 +427,16 @@ void adc_task_func(void *arg) { } } - /* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a) + /* 7) 等下一次节拍唤醒(事件驱动) * 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界** * → 与 TIM3 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的), * 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask - * (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位 + * (空转抢占低优先级任务正是这么发生的)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位 * ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至 * AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。 * 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear; * osWaitForever 无超时 = 刻意的 fail-safe(TIM3 停摆 → 无心跳 → IWDG 复位)。 */ - task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含等待) */ + task_prof_exit(TASK_PROF_ADC_BEAT); /* 探针:单拍耗时结算(不含等待) */ (void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever); } } diff --git a/App/task/adc_task.h b/App/task/adc_task.h index d697cddd..eb86a9b6 100644 --- a/App/task/adc_task.h +++ b/App/task/adc_task.h @@ -6,7 +6,7 @@ * 创建日期:2026-07-19 * 修改记录: * v1.0 2026-07-19 王建锋 创建初始版本 - * v1.1 2026-10-10 王建锋 Task #32 P1-5 方案 a:新增 ADC_WAKE_FLAG 与 + * v1.1 2026-10-10 王建锋 新增 ADC_WAKE_FLAG 与 * adc_tick_irq_notify()——消费节拍由 osDelayUntil(1ms) * 轮询改为 TIM3 ISR 事件驱动唤醒 */ @@ -21,7 +21,7 @@ extern "C" { int adc_task_init(void); void adc_task_func(void *arg); -/* Task #32 P1-5(方案 a,2026-10-10):事件驱动消费。 +/* 事件驱动消费的唤醒标志。 * TIM3 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。 * 线程标志在此是**命令位**(等待即消费),不是状态位 → 用 osFlagsWaitAny, * 不加 osFlagsNoClear(AGENTS「就绪屏障位必须 osFlagsNoClear」只约束状态位) */ diff --git a/App/task/default_task.c b/App/task/default_task.c index 68dd7a6a..30666218 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -19,7 +19,7 @@ * FTP 文件操作与 syslog/配置的卷访问自此串行化 * (2026-10-02 台架:并发 FTL 写 → journal 竞争 → * 本任务主循环喂狗点饥饿 → IWDG 误复位) - * v1.2 2026-10-10 王建锋 Task #32 的 P2 测量接线:主循环周期上报各任务最坏 + * v1.2 2026-10-10 王建锋 任务耗时测量接线:主循环周期上报各任务最坏 * 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿** * 的瞬间留一份现场快照(task_prof_report_raw,裸 UART); * 另给控制台排空与 syslog 落盘各加一个耗时探针 @@ -44,7 +44,7 @@ #define DBG_LINE_BUF_MAX 128U #include "dbg_log.h" #include "task_watchdog.h" -#include "task_prof.h" /* P2 测量:各任务最坏耗时上报(Task #32) */ +#include "task_prof.h" /* 各任务最坏耗时上报 */ #include "vol_lock.h" /* 任务句柄与属性块(CubeMX 生成全局,定义于 Src/freertos.c): @@ -199,7 +199,7 @@ void default_task_func(void *argument) { task_wd_seg(TASK_WD_SEG_POLL); /* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */ task_wd_poll(); - /* P2 测量(Task #32):某位由"到过"变为"缺席"的瞬间留一份现场快照。 + /* 耗时测量:某位由"到过"变为"缺席"的瞬间留一份现场快照。 * 走裸 UART 直发——缺席窗口内环形缓冲可能积压,这两行必须出去。 * * ⚠ 判据用**位图边沿**,不能用 miss 累计数:`task_wd_miss_count()` 返回的是 @@ -299,7 +299,7 @@ void default_task_func(void *argument) { /* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */ task_wd_seg(TASK_WD_SEG_STACK); stack_probe_poll(); - /* P2 测量(Task #32):各任务最坏耗时 / 次数 / "仍在段内"快照。 + /* 各任务最坏耗时 / 次数 / "仍在段内"快照上报。 * 内部节流(首次 5s,此后每 2s);判据: * uisn 持续增长 → uiTask 卡在 ui_net_sink_poll 的往返里(阻塞) * uipn 持续增长而 uisn 为 0 → 被调度饿死(本轮迟迟跑不完) diff --git a/App/task/net_task.c b/App/task/net_task.c index 8dcfb85b..154e33dc 100644 --- a/App/task/net_task.c +++ b/App/task/net_task.c @@ -18,7 +18,7 @@ * freertos.c USER CODE 区重复创建已删除 * v1.7 2026-10-02 王建锋 P1-8:初始组网失败/重建失败 → 自检记录 + 30s 周期重试 * (原实现:初始失败 = netTask 无限驻留;重建失败 = 静默丢配置) - * v1.8 2026-10-10 王建锋 Task #32 的 P2 测量埋点:net_poll() 单独计时—— + * v1.8 2026-10-10 王建锋 任务耗时测量埋点:net_poll() 单独计时—— * 本任务每 ~1ms 分派一次消息队列,若驱动轮询被拉长, * 其它任务(尤其 UI 的 net_accept 往返)的等待随之拉长 */ @@ -34,7 +34,7 @@ #include "sys_log.h" #include "test_config.h" /* 测试模式宏(ENABLE_HW_LAYER_TESTS);产品构建全未定义,无作用 */ #include "task_watchdog.h" -#include "task_prof.h" /* P2 探针:net_poll 耗时(Task #32) */ +#include "task_prof.h" /* 探针:net_poll 耗时 */ #define DBG_TAG "[NET_TASK]" #include "dbg_log.h" diff --git a/App/task/ui_task.c b/App/task/ui_task.c index b6e030af..c3350528 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -6,11 +6,11 @@ * 创建日期:2026-08-30 * 修改记录: * v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入) - * v1.2 2026-10-10 王建锋 Task #32 的 P2 测量埋点:主循环加常开探针 + * v1.2 2026-10-10 王建锋 任务耗时测量埋点:主循环加常开探针 * (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返), * 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长 * 三种互斥机理(判读见 docs/子系统设计/看门狗设计说明.md §11.4) - * v1.3 2026-10-10 王建锋 Task #32 的 P1-4a(解耦心跳,看门狗设计说明 §11.3):把 task_wd_beat + * v1.3 2026-10-10 王建锋 解耦心跳(docs/子系统设计/看门狗设计说明.md §11.3):把 task_wd_beat * (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳 * 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms, * 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是 @@ -128,7 +128,7 @@ void ui_task_func(void *argument) { uint8_t tm_loop_valid = 0U; #endif - /* P2 常开探针(Task #32):TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开, + /* 常开探针:TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开, * 且样本消费者是 testTask(产品构建里不跑)→ 产品构建拿不到任何 UI 周期数据。 * 故此处独立埋点,恒定生效、只写本模块的静态计数,由 defaultTask 周期上报。 */ @@ -151,12 +151,12 @@ void ui_task_func(void *argument) { tm_loop_valid = 1U; #endif ui_port_step(UI_TASK_TICK_MS); - /* 心跳**先于**推流往返发出(P1-4a,看门狗设计说明 §11.3):UI 心跳测的是 + /* 心跳**先于**推流往返发出(判读口径 docs/子系统设计/看门狗设计说明.md §11.3):UI 心跳测的是 * "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后, * 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后, * 即使往返偶发变慢,心跳也不被其拖垮。 * ⚠ 若往返**永久**阻塞(xTaskNotifyWait portMAX_DELAY 不返回),本改只能发 - * 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头 v1.3 说明。 */ + * 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头修改记录 v1.3。 */ task_wd_beat(TASK_WD_UI); /* 推流轮询段单独 enter/exit:它内部经 net_accept → net_wait_result 走到 * xTaskNotifyWait(portMAX_DELAY) 的**无限期阻塞**。若 uiTask 永久卡在这里, diff --git a/App/util/task_prof.c b/App/util/task_prof.c index ef91e6cb..e9dd1f5e 100644 --- a/App/util/task_prof.c +++ b/App/util/task_prof.c @@ -7,7 +7,7 @@ * 作者:王建锋 * 创建日期:2026-10-10 * 修改记录: - * v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点) + * v1.0 2026-10-10 王建锋 创建:常开任务耗时探针 * * 设计依据:docs/子系统设计/看门狗设计说明.md §11.4(探针字段语义与实测基线) */ diff --git a/App/util/task_prof.h b/App/util/task_prof.h index 3104cfde..0b3c0041 100644 --- a/App/util/task_prof.h +++ b/App/util/task_prof.h @@ -9,7 +9,7 @@ * 作者:王建锋 * 创建日期:2026-10-10 * 修改记录: - * v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点) + * v1.0 2026-10-10 王建锋 创建:常开任务耗时探针 * * 设计背景(docs/子系统设计/看门狗设计说明.md §11.4): * uiTask 偶发心跳缺席导致 IWDG 复位,但"占用者"无法从既有日志判定。 diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index 3eecaec3..8d75cbf3 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -15,7 +15,7 @@ * 2026-10-02 王建锋 E-04:remo_send 校验 net_send 返回值(原丢弃)——短写/失败 * 告警,连续 3 次失败阻断推送,新连接/网络重建解除阻断 * (MCU 单连接模型不得 net_close 数据 fd,会连监听一起毁掉) - * 2026-10-10 王建锋 I-1 门控 + I-2 兜底(Task #32 §9.3,销项 N-10):accept/recv + * 2026-10-10 王建锋 I-1 门控 + I-2 兜底(销项 N-10):accept/recv * 前先用 net_can_accept()/net_can_read()(纯 RAM 读控制块)判 * "有没有活",无活直接返回——台架常态(无客户端)从每 10 ms * 一次 RPC(100 msg/s,与 FTP 抢深度 8 的队列)降到 0 次。 diff --git a/Src/freertos.c b/Src/freertos.c index cc82fce5..79db3e66 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -146,7 +146,7 @@ const osThreadAttr_t timeSyncTask_attributes = { .stack_size = 640 * 4, .priority = (osPriority_t) osPriorityNormal, }; -/* B1(2026-10-10):ADC 采集消费者(TIM6 1 kHz 采样节拍 + 原子快照 + 帧统计)。 +/* B1:ADC 采集消费者(TIM3 1 kHz 采样节拍 + 原子快照 + 帧统计)。 * 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。 * ⚠ 优先级 AboveNormal:消费由 TIM3 ISR 事件驱动,提优先级使"被唤醒 → 立即抢占其余 * Normal 任务",唤醒延迟固定为 µs 级。 @@ -173,8 +173,9 @@ const osThreadAttr_t iec104Task_attributes = { /* B5(2026-10-12):保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁)。 * 手写于此而非 .ioc(同上)。512 words:stack_report 实测静态最坏链 872 B * (float 判据 + phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 保持。 - * 优先级 Normal:V0.1 无出口硬件(规划 R7),判据是 20ms 软实时,不需要 - * adcTask 的 AboveNormal;规划 §4 的"1ms/高"随 P2 瞬时值元件再评。 */ + * 优先级 Normal:V0.1 无出口硬件(风险项见 docs/算法/继电保护功能复现规划.md §9), + * 判据是 20 ms 软实时,不需要 adcTask 的 AboveNormal;该文档 §4 设想的"1 ms / 高 + * 优先级"要等瞬时值元件落地后再评(当前无出口硬件,快一拍不改变任何出口时序)。 */ osThreadId_t protectTaskHandle; const osThreadAttr_t protectTask_attributes = { .name = "protectTask", diff --git a/Src/main.c b/Src/main.c index 9076f6d4..abbf7072 100644 --- a/Src/main.c +++ b/Src/main.c @@ -42,7 +42,7 @@ #include "app_main.h" #include "sys_clock.h" #include "task_watchdog.h" /* task_wd_tick_probe():round 8b 时间源存活性取证 */ -#include "adc_task.h" /* adc_tick_irq_notify():TIM6 节拍 → 唤醒 adcTask(P1-5 方案 a) */ +#include "adc_task.h" /* adc_tick_irq_notify():TIM3 采样节拍 → 唤醒 adcTask */ /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -253,14 +253,14 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) } /* 1 kHz 采样节拍:TIM3 更新事件内脉冲 CONVST(仅两次 GPIO 写,亚 µs), - * 再唤醒 adcTask 消费这一拍(Task #32 P1-5 方案 a:ISR 只发唤醒,计算全留在任务)。 + * 再唤醒 adcTask 消费这一拍(ISR 只发唤醒,计算全留在任务)。 * 同一个更新事件还经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运) * → 两片转换器由同一硬件事件驱动,采样时刻对齐。 * TIM3 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占, * 读 RD 时序不会被下一次 CONVST 脉冲打断。详见 * docs/子系统设计/ADC采样节拍设计说明.md §3.3。放 USER CODE 区(L2)。 - * ⚠ 节拍源 v2.0 由 TIM6 改为 TIM3:F4 上 ADC1 的外部触发清单里没有 TIM6 TRGO - * (TIM6/TIM7 的 TRGO 只接 DAC),详见 App/app/app_main.c 步骤 6 注释。 + * ⚠ 节拍源必须能触发 ADC1:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 的 TRGO + * (只接 DAC),判据见 docs/子系统设计/ADC采样节拍设计说明.md §3.2。 * ⚠ 唤醒必须走 adc_tick_irq_notify(),**不要**在这里直接 osThreadFlagsSet: * TIM3 在 app_main_init() 就启动,远早于建任务与 osKernelStart, * "消费者就绪"门在 adc_task.c 里(同本文件 TIM7 分支 2026-10-02 的实测坑)。 */ diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md index 03c02522..bbe4cc50 100644 --- a/docs/算法/采集与算法层设计说明.md +++ b/docs/算法/采集与算法层设计说明.md @@ -184,8 +184,9 @@ flowchart TD (过压 / 欠压 / 频率)、`C03-UN-F1`(零序过压)、`C04-IA-F2`(二次谐波制动)、 `C05-IA-F5`(五次谐波选线)。**新增实例只在表尾加一行**,遍历 / 日志 / 单测自动适配。 - **双区(计算区 / 使用区)**:结果先写非使用区,全部实例写完后单字节 `volatile` store 切换 - `s_use_zone`(Cortex-M 上原子)→ 消费侧无锁、无撕裂。当前唯一消费者是 `adcTask` 自身, - 该机制为 `protectTask` 之外的第二个并发读者预留。 + `s_use_zone`(Cortex-M 上原子)→ 读者无锁、无撕裂。写者是 `adcTask`(同拍),**读者跨两个 + 上下文**:`adcTask` 心跳与 `protectTask` 判据(`prot_elm_run()` → `phasor_get()`)——无锁读 + 的收益正在被使用,**勿改成加锁、也勿取消双区**。 - **任一实例数据不足一周波 → 本拍整体不产出**(不切换使用区):宁可整体不刷新,也不给出 "部分新部分旧"的混合结果。 - **每拍 20 点全部重算**(暴力法)。不用"加尾项去头项"的增量式:增量递推会累积误差, diff --git a/scripts/check_cubemx_survival.py b/scripts/check_cubemx_survival.py index ff39e25c..762e4850 100644 --- a/scripts/check_cubemx_survival.py +++ b/scripts/check_cubemx_survival.py @@ -323,7 +323,7 @@ CHECKS = [ "`USER CODE BEGIN 1` 区。本仓库 HAL 无 HAL_ADCEx_Calibration_Start、" "CMSIS 头无 ADC_CR2_CAL,两者都是裁剪造成的,勿以为『标准 HAL 里有』。", ), - # ---------------- ADC 采样节拍 + DMA(2026-10-10 v2.8) ---------------- + # ---------------- ADC 采样节拍 + DMA(TIM3 + DMA2_Stream4) ---------------- # 节拍源 TIM3:同一个更新事件 ① ISR 脉冲 TPAFE CONVST ② 经 TRGO 触发 ADC1 # ③ 置位 ADC_WAKE_FLAG。丢了 ② 不报编译错,只是两片转换器不再同源(相位差自由漂移)。 dict( diff --git a/test/host/adc_input/adc_input_test.c b/test/host/adc_input/adc_input_test.c index 85bf5ab8..57af9018 100644 --- a/test/host/adc_input/adc_input_test.c +++ b/test/host/adc_input/adc_input_test.c @@ -79,7 +79,7 @@ #include "tpafe5160.h" #include "adc_input.h" /* 取 ADC_INPUT_STM32_CH_NUM 等常量(桩辅助函数在被测源之前) */ -/* ===================== HAL ADC1 桩(v2.0:DMA 模型) ===================== +/* ===================== HAL ADC1 桩(DMA 模型) ===================== * 被测代码经 HAL 只做一件事:init 时挂一次 DMA。此后"一帧完成"由 DMA 中断的 * HAL 回调告知——回调本体由被测代码实现,测试直接调它们即可注入节拍。 * s_start_dma_rc HAL_ADC_Start_DMA 返回值(注入挂载失败) diff --git a/test/host/adc_input/stub/adc.h b/test/host/adc_input/stub/adc.h index 7600efe7..5c0a638f 100644 --- a/test/host/adc_input/stub/adc.h +++ b/test/host/adc_input/stub/adc.h @@ -51,7 +51,7 @@ extern ADC_HandleTypeDef hadc1; #define ADC_CHANNEL_14 (0x0000000EU) #define ADC_CHANNEL_15 (0x0000000FU) -/* v2.0:STM32 侧改 TIM3 TRGO 触发 + DMA 双缓冲,本层只调 Start_DMA 一次; +/* 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); diff --git a/test/run_all_suites.py b/test/run_all_suites.py index 15227338..ee45037b 100644 --- a/test/run_all_suites.py +++ b/test/run_all_suites.py @@ -34,7 +34,7 @@ & '.venv\\Scripts\\python.exe' test/run_all_suites.py --check-build # 只编译不烧录(S6,无需 MCU) & '.venv\\Scripts\\python.exe' test/run_all_suites.py --keep-config # 不还原(调试用,慎) ---check-build(S6 / 上游 P1-10 方案 A): +--check-build(S6 / 上游 P1-10): 只做"改宏 + @build",不烧录不采集,故**不需要 MCU / 串口 / ST-LINK**。 用途:① 改动测试固件或 test_config.h 后自检 4 套件仍可编译; ② 防止"某套件长期编不过而无人知"(2026-08~09 CH395F 套件实例)。 @@ -193,7 +193,7 @@ def main(argv=None): results[suite] = "BUILD_FAIL" continue - # --check-build:编译通过即达标,不烧录不采集(S6 方案 A) + # --check-build:编译通过即达标,不烧录不采集(S6) if args.check_build: results[suite] = "PASS" print("[{}] BUILD_OK -> PASS(--check-build,未烧录)".format(suite))