用户选定最彻底方案(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 同步。
13 KiB
App/algo/ — 采集与算法层(L1 / L2)
自研代码,属
docs/算法/继电保护功能复现规划.md§2.1 分层中的 L1(采集与预处理) 与 L2(相量计算)。原则:算法与 I/O、与调度分离——本层不建任务、不做保护判据、 不碰寄存器(唯一例外见 §3.1),只做"数"(原始码值 → 语义通道)与"相量" (采样序列 → 复相量),因而可被主机侧单元测试(L0.5)离线全覆盖(落实登记簿 A-4)。任务侧(
App/task/adc_task.c)只做消费与编排;任何新算法一律落在本目录,勿塞进App/task/。
1. 文件清单(一眼版)
| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
|---|---|---|---|---|
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 |
依赖关系(自下而上,无环):adc_input.h(纯枚举 + 声明)← sample_ring ← phasor。
sample_ring.c / phasor.c 零平台依赖(只用 math.h / stddef.h 与本目录头);
adc_input.c 另依赖 adc.h(hadc1)与 tpafe5160.h(快照),主机侧以桩头替身。
2. 数据流(一拍 = TIM3 1 kHz 节拍)
flowchart TD
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
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 点重算 + 双区切换"]
RUN --> GET["phasor_get(idx)"]
GET --> CONS["消费者:adcTask 日志(现状)<br/>UI / 自检 / L3 保护任务(待接)"]
初始化落点:adc_input_init() 与 phasor_init() 由 App/app/app_main.c 的
app_main_init() 在内核启动前调用(三者 *_init() 均无 RTOS 依赖、幂等);
phasor_init() 内部会连带 sample_ring_init(),等价于"刚上电"。
3. 逐文件说明
3.1 adc_input.c / adc_input.h — L1 ADC 输入层
解决的问题:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 N-18),且网名 AINn
≠ 板级 ADCn ≠ 转换器通道号(13/17 路置换)。若消费侧直接索引驱动缓冲,改版即全线错位。
本层把"哪一路"收进一张表,上层按语义取数、不写死下标。
| API | 干什么 | 注意 |
|---|---|---|
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() |
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——四类成因不同,勿混读 |
要点
- 语义映射表
k_adc_map[]的唯一权威口径是docs/硬件平台/ADC通道映射说明.md:UA/UN/IA→ TPAFEbuf[6]/[5]/[2];UB/UC/IB/IC/IN/TEMP→ ADC1CH14/3/6/1/2/5。 V0.2 改版只改这张表的内容,本层 API 与上层语义不动。 - STM32 侧是顺序扫描(Rank1..8 = CH0..CH7、Rank9 = CH14,与
MX_ADC1_Init()严格一致), 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那种零依赖形态,才能免桩单测。
3.2 sample_ring.c / sample_ring.h — L2 输入采样环
解决的问题:相量计算要"以当前时刻为终止的一周波",而 L1 每次只给最新一帧。
本层按语义通道存最近 SAMPLE_RING_CYCLES = 2 个周波(SAMPLE_RING_DEPTH = 40 点),
与参考系统 CN_BUFSAM_CYCLE = 2 一致,并为后续加长数据窗(1.5~2 周波抗衰减直流)预留空间。
| API | 干什么 | 返回 |
|---|---|---|
sample_ring_init() |
清全部通道历史与计数(幂等,内核启动前可调) | — |
sample_ring_push_frame(values, n) |
整帧压入(长度须 = ADC_SEM_COUNT) |
0 / -1 参数错 |
sample_ring_window(ch, n, out) |
取该语义通道最近 n 点,按时间序旧 → 新(out[n-1] 最新) |
0 / -1 参数错 / -2 点数不足 |
sample_ring_filled() |
已累积点数(饱和于 40)——判"够不够一周波" | 0..40 |
sample_ring_frames() |
累计入环帧数(不饱和,1 kHz 下约 49.7 天回绕) | 帧数 |
要点
- 整帧写入后统一推进写指针 → 单帧内不可能"半新半旧",消费侧取到的窗永远落在整帧边界。
- 本层只做按时序存/取,不做任何跨通道运算:同刻性由上游 L1 负责,本层既不保证也不假设。
frames()与 L1 的tpafe5160_frame_seq()对照即可判丢帧(L1-6 的判据之一); 本层只提供计数,判定不在这里。- 上层若某通道本拍取数失败,应整拍跳过入环(现
adc_task的s_frames_skip即此), 切勿补 0 点——0 会污染相量窗。
3.3 phasor.c / phasor.h — L2 相量计算
算法:全周傅氏(单频 DFT),N = PHASOR_N = 20(1 kHz @ 50 Hz)。系数表
k_fft_sin/k_fft_cos 离线生成并预乘 √2/N(口径 docs/算法/傅氏算法详解.md §2.3),
故 fAmSQ = √(fRe²+fIm²) 直接就是有效值,fAm = √2·fAmSQ 为峰值。运行期不调 sinf/cosf。
| API | 干什么 | 注意 |
|---|---|---|
phasor_init() |
双区清零 + 使用区/就绪复位 + sample_ring_init() |
幂等;内核启动前调用 |
phasor_inst_count() / phasor_inst(idx) |
遍历实例表({name, sem, harm}) |
返回静态地址,勿改;越界 NULL |
phasor_run() |
逐实例取 20 点窗 → 重算 → 写计算区 → 一次性切换使用区 | 仅任务上下文;须先 push_frame() |
phasor_get(idx, &out) |
读使用区结果 | 0 / -1 参数错 / -2 未攒满一周波 |
phasor_calc(x, n, harm, &out) |
对给定窗口算 harm 次相量 |
纯函数,可脱离采样环独立单测 |
要点
- 实例表数据驱动(
k_inst[],现 5 行):C01-IA-F1(过流/过负荷)、C02-UA-F1(过压/欠压/频率)、C03-UN-F1(零序过压)、C04-IA-F2(二次谐波制动)、C05-IA-F5(五次谐波选线)。 新增实例只在表尾加一行,遍历/日志/单测自动适配。 - 双区(计算区 / 使用区):结果先写非使用区,全部实例写完后单字节
volatilestore 切换s_use_zone(Cortex-M 上原子)→ 消费侧无锁、无撕裂。今日唯一消费者是 adcTask 自身, 该机制为后续独立 L3 保护任务预留。 - 任一实例数据不足一周波 → 本拍整体不产出(不切换使用区),避免"部分新部分旧"的混合结果。
- 每拍 20 点全部重算(暴力法,无累积误差;迭代加减首尾项会累积)。相量刷新周期 = 1 ms。
- 相位(L2-4)未常驻结构体,需要时自行
atan2f(fIm, fRe)。
4. 关键约定
- 语义映射唯一权威口径:
docs/硬件平台/ADC通道映射说明.md。取数一律经adc_input_raw(), 禁止在消费侧直接索引驱动缓冲或写死下标。 - 禁止跨转换器运算(V0.1 硬件缺陷 N-18):
UA/UN/IA在 TPAFE、UB/UC/IB/IC/IN在 STM32 ADC1, 两者无共同采样触发 → 无同一时刻性、相位不确定。故本层不做跨转换器的相位 / 功率 / 功率因数 / 负序零序类矢量运算(V0.2 把 8 路电气量全接 TPAFE 后方可解禁)。 - 标定(L1-5)尚未实现(缺标准源):现以原始码值充当工程量(系数 = 1), 因此相量读数只证明链路通,不可作幅值精度判据。
- 本层不建任务、不等事件、不写日志之外的副作用;日志由调用侧(adcTask)打。
- 内核启动前只允许调
*_init();adc_input_poll()/phasor_run()仅任务上下文。
5. 静态足迹(BSS,供栈/RAM 预算参考)
| 位置 | 量 |
|---|---|
sample_ring.c s_ring[9][40] float |
1440 B |
phasor.c s_zone[2][5] phasor_t |
160 B |
adc_input.c s_stm32_raw[16] + s_tpafe_raw[8] |
48 B |
运行期栈:phasor_run() 的 win[20] |
80 B/帧 |
改容量(SAMPLE_RING_CYCLES / 实例数 / ADC_SEM_COUNT)前后各跑一次
& '.venv\Scripts\python.exe' scripts/stack_report.py(口径见 mem-budget skill)。
6. 构建与测试
-
挂载:三个
.c已登记在MDK-ARM/STM32F407-Demo.uvprojx,../App/algo已在<IncludePath>内。 本层不进simulator/CMakeLists.txt(UI 模拟器不跑采集链路)。 -
主机侧单测(L0.5)——生产源文件以
#include原样纳入,无需改产品代码:套件 命令 pre-commit 钩子 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) & '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.pyphasor-unit-testADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) & '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.pyadc-input-unit-test改本目录任一
.c/.h,提交时对应钩子自动跑;详见docs/测试说明/相量层单元测试说明.md。 -
板端:
@build0 错误 0 警告 →@test看[ADC_TASK]首拍/心跳日志(phasor(...)行、ring/skip计数、adc1_rc/adc1_err)。
7. 新增一个算法模块的固定动作
- 在本目录建
xxx.c/h,文件头写模块注释块(规范 V1.0),尽量零平台依赖以便免桩单测。 - 用
new-source-fileskill 注册到MDK-ARM/STM32F407-Demo.uvprojx(目录已在 IncludePath, 若新建子目录则必须补 IncludePath)。 - 加
test/host/<模块>/主机单测 +.pre-commit-config.yaml钩子(照phasor-unit-test抄)。 - 消费者写在
App/task/,本层只出"数";跨转换器运算的禁令见 §4。
8. 相关文档
- 通道映射权威口径:
docs/硬件平台/ADC通道映射说明.md - 分层与路线图(L1/L2 清单与状态):
docs/算法/继电保护功能复现规划.md - 傅氏算法与有效值口径:
docs/算法/傅氏算法详解.md - 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):
docs/子系统设计/ADC采样节拍设计说明.md - 相量层单测细则:
docs/测试说明/相量层单元测试说明.md