merge: task/doctrim-adc-tick-history 合入 v2-on-ui
任务 doctrim(3 个提交)经 task-worktree 回收: - chore(注释): 清掉源码里不可检索的过程编码 - docs(algo): App/algo/README 缩成目录索引,详细设计迁入 docs/算法 - docs(ADC): 采样节拍设计说明只留现状,历史决策与台架记录移出
This commit is contained in:
@@ -46,7 +46,7 @@
|
||||
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 按网络代次号重建监听(新 IP 免重启生效)
|
||||
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs`;挂载后卷健康探针,FAT 区陈旧时自愈重格式化)→ 加载网络配置 → STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(netTask/uiTask 不得直接触碰配置引擎)
|
||||
- adcTask:**纯消费者**——每拍 `adc_input_poll()`(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 双缓冲),只取数不做保护运算;**节拍与唤醒同源**(TIM3 ISR 内触发 CONVST + 同一更新事件经 TRGO 触发 ADC1,并调 `adc_tick_irq_notify()` 置位,任务 `osThreadFlagsWait` 等唤醒)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`(内含"消费者就绪"门,TIM3 比调度器早 ≈100 ms 启动);手写于 `Src/freertos.c` 的 `USER CODE` 区,纳入 IWDG `required_mask`(`TASK_WD_ADC`)
|
||||
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`freq` = L2.5 测频(B8,自适应窗),`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `App/algo/README.md`
|
||||
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`freq` = L2.5 测频(B8,自适应窗),`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `docs/算法/采集与算法层设计说明.md`(设计与判据)、`App/algo/README.md`(目录索引)
|
||||
- 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md`
|
||||
|
||||
<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ FreeRTOS/CMSIS-RTOS API**(见 `AGENTS.md`「平台约束」)。
|
||||
| `app/` | **应用入口与全局**:`app_main.c/h`(初始化入口)、`app_cfg.h`(编译开关)、`sys_events.h`(就绪事件) | [`app/README.md`](app/README.md) |
|
||||
| `cfg/` | **配置引擎**:`/config.ini` 解析 + 原子持久化(旧二进制档已废弃,2026-10-10) | [`cfg/README.md`](cfg/README.md) |
|
||||
| `sync/` | **时间同步任务**:`time_sync.c/h` | [`sync/README.md`](sync/README.md) |
|
||||
| `algo/` | **采集与算法层(L1/L2)**:`adc_input.c/h`(语义通道映射 + STM32 ADC1 扫描) | [`algo/README.md`](algo/README.md) |
|
||||
| `algo/` | **采集与算法层(L1/L2/L3)**:语义取数 + 有效性判定 + 采样环/相量/测频 + 定值模型 + 保护元件框架 | [`algo/README.md`](algo/README.md)(索引);设计 → `docs/算法/采集与算法层设计说明.md` |
|
||||
| `log/` | **日志子系统**:syslog(NAND)+ 控制台单写者日志 | [`log/README.md`](log/README.md) |
|
||||
| `task/` | **FreeRTOS 任务实现**(6 个任务) | [`task/README.md`](task/README.md) |
|
||||
| `util/` | **通用工具**:CRC / 环形缓冲 / 墙钟时间 | [`util/README.md`](util/README.md) |
|
||||
|
||||
+103
-335
@@ -1,360 +1,128 @@
|
||||
# App/algo/ — 采集与算法层(L1 / L2)
|
||||
# App/algo/ — 采集与算法层(L1 / L2 / L3)
|
||||
|
||||
> **自研代码**,属 `docs/算法/继电保护功能复现规划.md` §2.1 分层中的 **L1(采集与预处理)**
|
||||
> 与 **L2(相量计算)**。原则:**算法与 I/O、与调度分离**——本层不建任务、不做保护判据、
|
||||
> 不碰寄存器(唯一例外见 §3.1),只做"数"(原始码值 → 语义通道)与"相量"
|
||||
> (采样序列 → 复相量),因而可被主机侧单元测试(L0.5)离线全覆盖(落实登记簿 **A-4**)。
|
||||
>
|
||||
> 任务侧(`App/task/adc_task.c`)只做**消费与编排**;任何新算法一律落在本目录,勿塞进 `App/task/`。
|
||||
> 本目录的**索引**:文件清单、依赖、逐文件的"改代码必须知道"、构建与测试入口、新增模块动作。
|
||||
> **详细设计——机制、契约、判据、为什么这样设计 → `docs/算法/采集与算法层设计说明.md`**(本文不复制其内容)。
|
||||
> 分层归属与功能项状态 → `docs/算法/继电保护功能复现规划.md` §2.1;
|
||||
> 通道映射唯一权威 → `docs/硬件平台/ADC通道映射说明.md`;采样节拍 → `docs/子系统设计/ADC采样节拍设计说明.md`。
|
||||
|
||||
## 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 |
|
||||
| `adc_health.h` / `adc_health.c` | L1 | **采样有效性判定层**:吃"计数快照"(帧序号/错误计数),产出静默/丢帧/单调性/错误增量故障位——判定与告警编排分离 | `adc_health_init()` `adc_health_check()` `adc_health_flags()` | L1-6 |
|
||||
| `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 |
|
||||
| `prot_set.h` / `prot_set.c` | L3 输入 | **保护定值模型**:Q10 定点 RAM 表(通讯地址对照参考 `g_tCommSetParaTab`)+ 范围校验 + 校验和封存自检闭锁 | `prot_set_init()` `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | §5 定值模型 / L5-1 |
|
||||
| `prot_elm.h` / `prot_elm.c` | L3 | **保护元件框架**:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / **过频 / 低频**) | `prot_elm_init()` `prot_elm_eval()` `prot_elm_run()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
|
||||
| `freq.h` / `freq.c` | L2 派生量 | **频率测量**:相邻两拍基波相量共轭乘积取角(每拍相位增量)+ 自适应窗长滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_init()` `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(`docs/算法/继电保护功能复现规划.md` §4.2 测频范围) |
|
||||
| `adc_input.h` / `.c` | L1 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
|
||||
| `adc_health.h` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离) | `adc_health_check()` `adc_health_flags()` | L1-6 |
|
||||
| `sample_ring.h` / `.c` | L2 输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 供数) |
|
||||
| `phasor.h` / `.c` | L2 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
|
||||
| `freq.h` / `.c` | L2 派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) |
|
||||
| `prot_set.h` / `.c` | L3 输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | L5-1 载体 |
|
||||
| `prot_elm.h` / `.c` | L3 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
|
||||
|
||||
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`;
|
||||
`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor` + `freq`。
|
||||
`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` / `freq.c` **零平台依赖**
|
||||
(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
|
||||
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`;
|
||||
`prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。
|
||||
`adc_health` 只吃调用方喂进来的计数快照,不反向依赖 L1 实现。
|
||||
|
||||
## 2. 数据流(一拍 = TIM3 1 kHz 节拍)
|
||||
**除 `adc_input.c` 外全部零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头)
|
||||
→ 可被主机单测 `#include` 原样纳入、免桩跑。`adc_input.c` 另依赖 `adc.h`(`hadc1`)与
|
||||
`tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
|
||||
```mermaid
|
||||
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 --> FREQ["freq_run()<br/>共轭乘积相位增量 + 自适应窗均值(B8)"]
|
||||
RUN --> GET["phasor_get(idx)"]
|
||||
FREQ --> FGET["freq_get() / freq_valid()"]
|
||||
GET --> CONS["消费者:adcTask 日志(现状)<br/>UI / 自检 / L3 保护任务(待接)"]
|
||||
FGET --> PCONS["消费者:prot_elm_run() OF/UF 判据(B8)<br/>adcTask 心跳 freq 行"]
|
||||
```
|
||||
## 2. 一拍的数据流(一眼版)
|
||||
|
||||
初始化落点:`adc_input_init()` 与 `phasor_init()`、`freq_init()` 由 `App/app/app_main.c` 的
|
||||
`app_main_init()` 在**内核启动前**调用(各 `*_init()` 均无 RTOS 依赖、幂等);
|
||||
`phasor_init()` 内部会连带 `sample_ring_init()`,等价于"刚上电"。
|
||||
`adc_input_poll()` → `adc_input_raw()` ×9 → `sample_ring_push_frame()` → `phasor_run()`
|
||||
→ `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
|
||||
`prot_elm_run()`)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
|
||||
完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。
|
||||
|
||||
## 3. 逐文件说明
|
||||
## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档)
|
||||
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — L1 ADC 输入层
|
||||
> **改本目录前先记四条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做
|
||||
> 相位 / 功率 / 矢量运算(N-18);③ 标定未落地,现以原始码值充当工程量,**幅值读数不可作精度判据**;
|
||||
> ④ 本层不建任务、不等事件、不写日志,`*_init()` 才可在内核前调。
|
||||
> 全文 → `docs/算法/采集与算法层设计说明.md` §10。
|
||||
|
||||
**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn`
|
||||
≠ 板级 `ADCn` ≠ 转换器通道号(13/17 路置换)。若消费侧直接索引驱动缓冲,改版即全线错位。
|
||||
本层把"哪一路"收进一张表,上层**按语义取数、不写死下标**。
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — L1 语义取数
|
||||
- 语义映射表 `k_adc_map[]` 的**内容**唯一权威是 `docs/硬件平台/ADC通道映射说明.md`;V0.2 改版只改表内容,API 不动。
|
||||
- `adc_input_poll()` **仅任务上下文**(TPAFE 快照是临界区);STM32 侧固定滞后 1 拍。
|
||||
- 四类 STM32 计数(`adc1_seq` / `adc1_gap` / `adc1_err` / `adc1_hw`)成因不同,**勿混读**。
|
||||
- 本目录**唯一**允许出现 HAL 调用的文件。→ `docs/算法/采集与算法层设计说明.md` §3。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
### 3.2 `sample_ring.c` / `.h` — L2 输入采样环
|
||||
- 整帧写入后才推进写指针 → 取到的窗永远落在整帧边界。
|
||||
- 本拍取数失败 → **整拍跳过入环**(`adc_task.c` 的 `skip`),**绝不补 0 点**。
|
||||
- 不做跨通道运算、不做丢帧判定(只出 `frames()` 计数)。→ `docs/算法/采集与算法层设计说明.md` §5。
|
||||
|
||||
### 3.3 `phasor.c` / `.h` — L2 相量计算
|
||||
- 系数表预乘 √2/N → `fAmSQ` **直接是有效值**(口径 `docs/算法/傅氏算法详解.md` §2.3)。
|
||||
- 实例表 `k_inst[]` 数据驱动:**新增实例只在表尾加一行**。
|
||||
- 任一实例不足一周波 → **本拍整体不产出**(不切使用区);每拍 20 点**全部重算**(不做增量递推)。
|
||||
- → `docs/算法/采集与算法层设计说明.md` §6。
|
||||
|
||||
### 3.4 `adc_health.c` / `.h` — L1 有效性判定
|
||||
- 三类位**严格分开**:电平(可自愈)/ 锁存(必留痕)/ 边沿事件(仅当拍);**静默 ≠ 丢帧**。
|
||||
- **gap 不告警**(消费者超拍是已知现象);告警编排在 `adc_task.c`,不在本层。
|
||||
- 首拍 `check()` 只建基线。→ `docs/算法/采集与算法层设计说明.md` §4。
|
||||
|
||||
### 3.5 `prot_set.c` / `.h` — 保护定值模型(L5-1 载体)
|
||||
- 装载 **all-or-nothing**:任一项越界整表不装载,越界项由 `prot_set_bad_mask()` 指认。
|
||||
- 批量 `prot_set_set()` 后**必须** `prot_set_seal()`;单项原子写用 `prot_set_set_sealed()`。
|
||||
- 定值项 / 实例**只许尾部追加**(枚举序 = 数组序 = 校验和覆盖序)。
|
||||
- blob **换代(返回 2)≠ 损坏(返回负)**:前者打 INFO、不记自检。→ `docs/算法/采集与算法层设计说明.md` §8、§12.3。
|
||||
|
||||
### 3.6 `prot_elm.c` / `.h` — L3 保护元件框架
|
||||
- 数据缺失两种处理**刻意不同**:相量类任一未就绪 → 整拍跳过;频率类无有效读数 → 按方向填"必不启动"值,**不整拍跳过**。
|
||||
- `prot_set_locked()` 或投退 = 0 → 元件整体复位不判据;跨上下文复归走 `prot_elm_request_reset()`。
|
||||
- 零序过流(IN 走 STM32)**故意不在实例表**;OF / UF 无独立 104 遥信点,只经 YX15 总聚合可见。
|
||||
- `prot_elm_init()` 须在 `phasor_init()` 之后。→ `docs/算法/采集与算法层设计说明.md` §9。
|
||||
|
||||
### 3.7 `freq.c` / `.h` — 频率测量(L3-5 的测量输入)
|
||||
- `freq_run()` **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调;保护口径 =
|
||||
`freq_get() == 0 && freq_valid() == 1`。
|
||||
- 精度靠**自适应窗长**(窗长 = 泄漏纹波周期的整数倍);定长平均压不到 0.02 Hz,
|
||||
换窗长 = 换精度,二者不是独立旋钮。
|
||||
- 源切换即废相位基准重建(UA / IA 无共同相位基准,N-18)。
|
||||
- 判据 **J1~J6** 与实测口径 → `docs/算法/采集与算法层设计说明.md` §7.2(与 `test/host/freq/freq_test.c` 文件头同源)。
|
||||
|
||||
## 4. 构建与测试
|
||||
|
||||
### 4.1 挂载与模拟器边界
|
||||
- 七个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
|
||||
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):七个文件全部登记在
|
||||
`scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(N-23)。
|
||||
|
||||
### 4.2 主机侧单测(L0.5)——生产源文件以 `#include` 原样纳入
|
||||
|
||||
| 模块 | 命令 | pre-commit 钩子 |
|
||||
|---|---|---|
|
||||
| `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`——四类成因不同,**勿混读** |
|
||||
| 相量 / 采样环 | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
|
||||
| ADC 输入层 | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
|
||||
| 有效性校验 | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
|
||||
| 定值模型 | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
|
||||
| 元件框架 | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
|
||||
| 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` |
|
||||
|
||||
**要点**
|
||||
改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各
|
||||
`test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。
|
||||
|
||||
- 语义映射表 `k_adc_map[]` 的**唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`:
|
||||
`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 个通道**不是同一时刻**(一轮 ≈67 µs)。v2.0 起这一轮由 **TIM3 TRGO 硬件触发 + DMA 自动搬运**,
|
||||
消费侧**零忙等**(旧实现逐通道轮询、单通道超时 2 ms,已于 2026-10-10 移除)。
|
||||
代价:STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE 侧本来就没有同刻性(N-18),无新增误差。
|
||||
- 本文件是本层**唯一**允许出现 HAL 调用的地方(它必须驱动 ADC1);新增模块若不需要外设,
|
||||
请保持 `sample_ring` / `phasor` 那种零依赖形态,才能免桩单测。
|
||||
### 4.3 板端
|
||||
`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、
|
||||
`ring` / `skip` / `adc1_*` / `tgap`);健康判据 = **零 `[ERR]`**(健康告警全以 `[ERR]` 开头,
|
||||
误报会被闭环直接抓住)。→ `docs/算法/采集与算法层设计说明.md` §12。
|
||||
|
||||
### 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`(五次谐波选线)。
|
||||
**新增实例只在表尾加一行**,遍历/日志/单测自动适配。
|
||||
- **双区(计算区 / 使用区)**:结果先写非使用区,全部实例写完后单字节 `volatile` store 切换
|
||||
`s_use_zone`(Cortex-M 上原子)→ 消费侧无锁、无撕裂。今日唯一消费者是 adcTask 自身,
|
||||
该机制为后续独立 L3 保护任务预留。
|
||||
- **任一实例数据不足一周波 → 本拍整体不产出**(不切换使用区),避免"部分新部分旧"的混合结果。
|
||||
- 每拍 20 点**全部重算**(暴力法,无累积误差;迭代加减首尾项会累积)。相量刷新周期 = 1 ms。
|
||||
- 相位(L2-4)**未常驻**结构体,需要时自行 `atan2f(fIm, fRe)`。
|
||||
|
||||
### 3.4 `adc_health.c` / `adc_health.h` — L1-6 采样有效性判定
|
||||
|
||||
**解决的问题**:驱动侧只有"观测计数"(`tpafe5160_frame_seq/_frstdata_err`、
|
||||
`adc_input_stm32_seq/_errs/_gaps/_hw_errs`),**没有判定**——芯片静默、消费者超拍丢帧、
|
||||
计数器被改写这些故障此前只能靠人读心跳日志发现。本层把判定收进纯逻辑,
|
||||
告警编排留在消费者(`adc_task.c` 的 `adc_health_report()`)→ 判定可主机免桩单测。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `adc_health_init()` | 清全部状态(幂等) | 首拍 `check()` 只建基线、不判定 |
|
||||
| `adc_health_check(&in, &out)` | 吃计数快照做一次判定(每拍调,常数次整数运算) | 差值法 `(int32_t)(now-prev)`,tick/计数回绕安全 |
|
||||
| `adc_health_flags()` | 电平故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存) | 为 L5-4 闭锁预留 |
|
||||
| `adc_health_sticky()` | 锁存位(含 `FRSTDATA` / `STM32_ERR` / `HW_ERR` 增量痕迹) | 自愈后仍留痕 |
|
||||
| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分) | 与 STM32 侧 `adc_input_stm32_gaps()` 对称 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **三类语义严格分开**:电平位(stall,可自愈)/ 锁存位(出现过必留痕)/ 边沿事件位
|
||||
(仅当拍有效)。`ADC_HE_FRSTDATA/STM32_ERR/HW_ERR` 与同名 `ADC_HF_*` **编号对齐**,
|
||||
消费者直接用事件掩码对"已留痕位图"做每类一次 `SYSLOG_SELFTEST` 的编排。
|
||||
- **gap 不告警**:消费者超拍(心跳日志 ~2 ms)是已知现象,`adc1_gap`/`tgap` 每 10 s 心跳
|
||||
可见;若按事件告警会把真故障淹掉(台架实测开机 `adc1_gap≈24`、稳态每 10 s +2)。
|
||||
- **静默 ≠ 丢帧**:d=0 才是静默候选;d>1 只说明消费者没跟上,生产者还活着——两者
|
||||
处置完全不同,判据不许混。
|
||||
- 告警策略(在 `adc_task.c`,不在本层):严重事件/电平上升沿 `DBG_ERROR` +
|
||||
`SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 32KB selftest.log);下降沿 `DBG_INFO`。
|
||||
|
||||
### 3.5 `prot_set.c` / `prot_set.h` — 保护定值模型(P1 / L5-1 载体)
|
||||
|
||||
**解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + `tagPRS` 常量表
|
||||
校验和自检;本层做 **RAM 模型 + 持久化纯逻辑层**(blob 序列化/解析;文件胶水在
|
||||
`App/cfg/prot_set_store.c`,B7),把编号 / 通讯地址 / 范围 / 默认值 / 校验和闭环钉死。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `prot_set_init()` | 装默认值 + 封存校验和(幂等) | 内核启动前可调 |
|
||||
| `prot_set_load_raw(vals)` | 整表装载(B7 由 `prot_set_store_parse` 内部走同一路径),逐项范围校验 | **all-or-nothing**:任一项越界整表不装载,`prot_set_bad_mask()` 指认越界项;影子区 + 单字交换,读侧无中间态 |
|
||||
| `prot_set_set(id, q10)` | 单项写入(范围校验,批处理路径) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写";板端单项写一律用下行 |
|
||||
| `prot_set_set_sealed(id, q10)` | **原子写+封存(B7)**:影子区改好重封后单字交换 | selfcheck 永不见中间态(收口 B5 竞态窗口);置脏标志 |
|
||||
| `prot_set_dirty_peek/take/mark()` | 脏标志三件套(B7 落盘握手) | peek=iec104Task 通知;take=属主落盘前消费;mark=写失败回置重试 |
|
||||
| `prot_set_store_serialize/parse(buf,len)` | 持久化 blob(`PSET`+版本+值区+sum32 正/反码对) | parse **整表原子**:magic/版本/长度/校验对/范围任一失败 = 一字不动 |
|
||||
| `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 |
|
||||
| `prot_set_selfcheck()` | 重算 sum32 与封存正/反码比对,失配 → 闭锁 | L5-1;seqlock 式复核:提交撞见 = 本轮跳过不误锁;`prot_set_seal()` 是解锁的唯一合法路径 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **Q10 定点**(×1024)与参考 `2^10` 折算口径一致;元数据表(地址/范围/默认)是
|
||||
ROM 常量,校验和只覆盖 RAM 值区。
|
||||
- **通讯地址逐项对照参考** `Comm/Pub/Const_CommSetPara.c` `g_tCommSetParaTab`
|
||||
(基址 0x8820);单测 TC-PROTSET-006 保证地址不重复、`min ≤ def ≤ max`、投退项 0..1。
|
||||
- ⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订——
|
||||
见规划 §5.4 落实进度注。
|
||||
- **B8 扩 8 项(32→40)**:`PS_OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF`
|
||||
**尾追加**(编号不回收,枚举序 = 数组序 = 校验和覆盖序)。OF/UF 的逐项通讯地址
|
||||
不在本仓库(参考保护定值段 0x8820~0x8882 仅示"含低压/低频"类目)→ 全部按
|
||||
**本地项 comm_addr=0** 登记(104 C_SE 不可寻址),待对照参考表补。
|
||||
频率返回系数**独立**(`RET_OF=0.99 / RET_UF=1.01`,返回门槛 = 定值×系数)——
|
||||
频率滞回带与电气量 0.95/1.05 不同量级,勿共用。
|
||||
- **B8 换代语义**:项数变 → blob 144→176 B、`PROT_SET_BLOB_VER` 1→2。B7 旧档
|
||||
(ver=1/32 项)在新固件 parse 即被**版本/长度闸**拒绝 → 出厂默认;
|
||||
`prot_set_store_load()` 返回码分流:**2 = 换代**(`prot_set_store.c` 打 INFO
|
||||
"prot set blob version change, defaults in use",**不是** CORRUPT、不记自检),
|
||||
−1 = 真损坏(defaultTask 打 ERROR + 自检留痕)。升级路径不得报成损坏。
|
||||
- **bad_mask 随项数扩为 `uint64_t`**(bit i = 第 i 项越界;消费方仅主机单测,
|
||||
板端闭锁走 `prot_set_locked()`)。
|
||||
|
||||
### 3.6 `prot_elm.c` / `prot_elm.h` — 保护元件框架(L3,P1 子集)
|
||||
|
||||
**解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段
|
||||
公共核收敛为**一个通用核 + 数据驱动实例表**(同 `phasor` 的 `k_inst` 模式),
|
||||
新增元件 = 表加一行 + 定值加几项。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调;`src=FREQ` 行无相量绑定(`pidx=0xFF`,run 不查它) |
|
||||
| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 |
|
||||
| `prot_elm_run(now_ms)` | 适配层:按实例表 `src` 组测量 → eval(`src=0` 取 `phasor_get()` rms,`src=1` 取 `freq_get()`) | 相量类任一未就绪 → **整拍跳过**(不引入陈旧判据);频率类无有效读数 → **按方向填"必不启动"值**(过量填 0 / 欠量填 `1e30` → 滞回比较恒假 → 元件复位),**不整拍跳过**——频率源缺失不得饿死电流/电压类元件 |
|
||||
| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛——
|
||||
过量/欠量方向由实例表 `over` 位统一处理(`UV`/`UF` 为欠量实例)。
|
||||
- **定时限**:`(uint32_t)(int32_t)(now - t0) >= dly` 差值法,tick 回绕安全;
|
||||
`dly=0` → 启动同拍即动作。返回**即时**(参考带 10 ms 返回延时,P4 接出口硬件时再补)。
|
||||
- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。
|
||||
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)、
|
||||
`OF/UF`(freq 层测频,B8)。零序过流(IN 走 STM32)**故意不在表中**。
|
||||
- **OF/UF 出口可见性(B8)**:两元件**未单独映射 104 遥信点**(送检点表 = B10 口径),
|
||||
其动作目前**只经 YX15 保护总聚合可见**(`iec104_pts` 的 fault-total 掩码含 OC/3U0,
|
||||
OF/UF 动作会置总位但无独立分点)——README 与规划 §3.3 L3-5 行必须同此口径,勿写"独立遥信"。
|
||||
- **板端接线已落地(B5,2026-10-12)**:`App/task/protect_task.c`(protectTask,
|
||||
20 ms 节拍)消费 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 校验和巡检;
|
||||
跨上下文复归走 `prot_elm_request_reset()`(104 YK11 落请求、eval 头部消费)。
|
||||
判据结果经 104 遥信上送(`iec104_pts` YX15~23/31)。→ `docs/算法/继电保护功能复现规划.md` §7
|
||||
⚠ 实例数 7→9(B8):protectTask 启动锚点 `elm=` 与 `prot_elm_active_mask()` 位宽随之变,
|
||||
`.serialtest` 锚点表未含 `elm=` 数字(已核),无需改配置。
|
||||
|
||||
### 3.7 `freq.c` / `freq.h` — 频率测量层(L2 派生量,L3-5 测量输入,B8)
|
||||
|
||||
**解决的问题**:过频/低频(L3-5)需要系统频率读数,而 L2 只出相量。本层把
|
||||
"相邻两拍基波相量的相位差"变成"频率",并解决**非整周波窗长的 DFT 泄漏纹波**
|
||||
这一决定精度的关键问题。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `freq_init()` | 清增量环/计数/读数/相位基准/有效标志(幂等) | 内核启动前可调,须在 `phasor_init()` 之后无要求 |
|
||||
| `freq_run()` | 每拍步进:选源(UA→IA 过幅值门)→ 共轭乘积 `P=Z(m)·conj(Z(m-1))` 取角 → 假值钳制 → 自适应窗均值 | **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调 |
|
||||
| `freq_get(out)` | 读数(Hz) | 0 有读数(可能是保持值)/ −1 参数错 / −2 从未启动收敛(写 0) |
|
||||
| `freq_valid()` | "本拍新鲜"标志 | 1 = 幅值门 OK 且已收敛;保护消费口径 = `get()==0 && valid()==1` |
|
||||
| `freq_last()` | 最近有效读数 | 从未收敛返回 0.0 |
|
||||
|
||||
**算法要点(精度是实测驱动定稿的,两轮弯路记录在 freq.c 文件头)**
|
||||
|
||||
- **相位微分免解缠绕**:`arg(Z_now·conj(Z_prev))` 天然落在 (−π, π],纯音每拍
|
||||
角**增大** 2πf/Fs(本层相量约定 φ=atan2(fIm,fRe)=窗首初相,窗每拍前滑一点);
|
||||
45~55 Hz 段 |Δφ| ≈ 0.28~0.35 rad,距 ±π 裕度巨大。
|
||||
- **自适应窗长**(核心):20 点矩形窗在非整周波时混入**负序泄漏项**
|
||||
(45/55 Hz 处 |S₂/S₁| ≈ 3~5%),相量端轨迹成椭圆 → 逐拍相位差带周期
|
||||
δ(φ) 纹波,周期 = fs/(2f) ≈ 9.1~11.1 拍。定长窗平均不整除纹波周期 →
|
||||
扫频最坏误差 **0.317 Hz**(40 拍算术平均)/ **0.197 Hz**(60 拍复数和 Kay,
|
||||
交叉项和角不随 K² 衰减,勿再尝试)。改 `K_eff = round(8·Fs/(2·f̂))`
|
||||
(45~55 Hz 段 73~89 拍)使窗长恰为纹波周期整数倍 → 纹波相消,最坏 **0.0101 Hz**。
|
||||
- **假值钳制带 35~65 Hz**(宽于系统合理带 40.5~60.5 = 参考 `CN_HERTZ_SOFTFR_MIN/MAX`,
|
||||
规划 §10.1):单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会**非对称丢弃纹波半周**
|
||||
→ 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 仍拦得住窗滑 2 拍的 ≈2f
|
||||
(90~110 Hz)与相位跳变假值;系统合理带由 OF/UF 定值范围在消费侧把关。
|
||||
- **幅值门** `FREQ_AMP_MIN=50.0`(原始码值口径,**拟定待 L1-5 标定复核**):
|
||||
低于门 = 无有效波形(浮空/PT 断线 rms≈0,相位无意义)→ 停更新、`valid` 清零、
|
||||
读数保持 → 台架浮空不发散。信号消失过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。
|
||||
- **源优先级 UA(C02)→IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流亦可测频);
|
||||
源切换即废相位基准重建(UA/IA 无共同相位基准,N-18)。
|
||||
|
||||
**精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径,数字为实测)**
|
||||
|
||||
| 判据 | 条件 | 门槛 | 实测最坏 |
|
||||
|---|---|---|---|
|
||||
| J1 扫频 | 纯音 45.00~55.00 Hz,0.5 Hz 步进(21 点) | **≤ 0.02 Hz** | **0.0101 Hz** @55.0 Hz |
|
||||
| J2 噪声 | 幅值 1000 码、噪声 rms = 1% 幅值 | **≤ 0.10 Hz** | **0.0160 Hz** |
|
||||
| J3 启动收敛 | 首个有效相量 → 首个有效读数 | **≤ 40 ms** | 20 ms(@39 ms 拍,相量 @19 ms) |
|
||||
| J4 频率阶跃 | 50→51 Hz 进入 ±0.05 Hz 并保持 | **≤ 120 ms** | 98 ms(稳态 err 0.0053 Hz) |
|
||||
| J5 零幅/浮空 | rms < 幅值门 | valid=0、不发散、保持漂移 ≤0.3 Hz | 漂移 0.13 Hz,无 NaN |
|
||||
| J6 假值钳制 | 幅值+频率双跳变拍 | 60 ms 内回 ±0.05 Hz | 0.0000 Hz |
|
||||
|
||||
附加鲁棒性(同套件实测):直流偏置 8000 码 → err 0.0018 Hz;3/5 次谐波 20%/10%
|
||||
→ err 0.0065 Hz;UA 无幅值落 IA → err 0.0020 Hz。
|
||||
|
||||
**J4 = 120 ms 是"精度优先于响应"的显式取舍(Lead 批准 2026-10-12)**:自适应窗
|
||||
(M=8 ≈ 4 个泄漏纹波周期 ≈ 4 个信号周波)换来 0.317→0.0101 Hz,代价是频率骤变后
|
||||
的窗冲刷(实测 98 ms)。OF/UF 定时限默认 **1000 ms** → 测频响应占保护总动作时间
|
||||
**<10%**;暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 120 ms 是工程整定,
|
||||
不是判据失守——若将来要压响应,须同时重估精度(缩短窗 = 精度回退到 0.2~0.3 Hz 档)。
|
||||
|
||||
**板端可见性(DoD)**:adcTask 心跳/首拍日志追加一行
|
||||
`freq(hb): f=5000 valid=1 (x0.01)`(MicroLIB 无 %f → ×0.01 定点,分辨率 0.01 Hz)。
|
||||
正常带波形应 `valid=1` 且 `f≈5000`;浮空应 `valid=0`、`f` 为保持值。
|
||||
|
||||
## 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 |
|
||||
| `adc_health.c` 内部状态(9 × uint32 + uint8) | ≈ 40 B |
|
||||
| `prot_set.c` `s_val[2][40]` + 封存/位图(64 位)/闭锁 | 336 B(B8:31→40 项) |
|
||||
| `prot_elm.c` `s_rt[9]`(16 B/实例) | 144 B(B8:7→9 实例) |
|
||||
| `freq.c` 相位增量环 `fq_d[96]` + 占位标志 + 状态 | **≈ 520 B**(B8 新增) |
|
||||
| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
|
||||
| 运行期栈:`freq_run()` 局部 | ≈ 30 B/帧(无大局部数组) |
|
||||
|
||||
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT` / `FREQ_AVG_TAPS`)前后各跑一次
|
||||
`& '.venv\Scripts\python.exe' scripts/stack_report.py`(口径见 `mem-budget` skill)。
|
||||
**B8 实测(adcTask 静态最坏链)**:见 READY 消息与 `docs/内存管理/` 台账回填。
|
||||
|
||||
## 6. 构建与测试
|
||||
|
||||
- **挂载**:八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
|
||||
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)——纯算法且模拟器无消费路径的
|
||||
新文件(`prot_set.c` / `freq.c`)改在 `scripts/check_registration.py` 的 `SIM_EXEMPT` 登记豁免
|
||||
(`registration-check` 钩子反向校验,N-23)。
|
||||
- **主机侧单测(L0.5)**——生产源文件以 `#include` 原样纳入,无需改产品代码:
|
||||
|
||||
| 套件 | 命令 | pre-commit 钩子 |
|
||||
|---|---|---|
|
||||
| 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
|
||||
| ADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
|
||||
| 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
|
||||
| 定值模型(90 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检 / B7 blob 往返与损坏拒绝 / **B8 OF-UF 行 + 64 位 bad_mask + 换代拒绝**) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
|
||||
| 元件框架(47 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合 / **B8 OF-UF 滞回与方向隔离 + run() 测频接线**) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
|
||||
| **频率测量(46 用例:J1 扫频 45~55 Hz / J2 噪声 / J3 启动收敛 / J4 阶跃 / J5 浮空不发散 / J6 假值钳制 / 直流·谐波抑制 / UA→IA 回退 / 守卫与幂等)** | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test`(**待登记**,合并时由 Lead 加钩子) |
|
||||
|
||||
改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
|
||||
- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
|
||||
`freq(...)` 行、`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]`
|
||||
(`adc_health_report` 的告警全部以 `[ERR]` 开头 → 误报会被闭环直接抓住)。
|
||||
- **定值档升级 vs 损坏(B8,台架判读)**:`prot_set_store_load()` **rc=2 = 换代**
|
||||
(旧固件/旧项数的档被版本闸拒绝)→ 只打 INFO `prot set blob version change, defaults in use`,
|
||||
**不是** `[ERR] ... CORRUPT`、不记自检;**rc<0 = 真损坏** → `[ERR] prot set blob CORRUPT` + 自检留痕。
|
||||
升级固件后首启看到"version change"一行属**预期**,勿当故障排查。
|
||||
|
||||
## 7. 新增一个算法模块的固定动作
|
||||
## 5. 新增一个算法模块的固定动作
|
||||
|
||||
1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。
|
||||
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(目录已在 IncludePath,
|
||||
若新建子目录则必须补 IncludePath)。模拟器侧二选一:有消费路径 → 加
|
||||
`simulator/CMakeLists.txt`;**纯算法无消费路径 → 加 `scripts/check_registration.py`
|
||||
的 `SIM_EXEMPT`**(照 `prot_set.c` / `freq.c` 先例写理由),否则 `registration-check` 钩子拦。
|
||||
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(新建子目录还要补 `<IncludePath>`)。
|
||||
模拟器侧二选一:有消费路径 → 加 `simulator/CMakeLists.txt`;纯算法无消费路径 → 加
|
||||
`scripts/check_registration.py` 的 `SIM_EXEMPT`(照本目录既有七条写理由),否则 `registration-check` 拦。
|
||||
3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。
|
||||
4. 消费者写在 `App/task/`,本层只出"数";跨转换器运算的禁令见 §4。
|
||||
4. 消费者写在 `App/task/`,本层只出"数";跨转换器禁令见 §3 开头四条红线。
|
||||
5. 在本 README §1 清单加一行,并在 `docs/算法/采集与算法层设计说明.md` 里写清该模块的契约与理由。
|
||||
|
||||
## 8. 相关文档
|
||||
## 6. 相关文档
|
||||
|
||||
- **本层设计与判据**:`docs/算法/采集与算法层设计说明.md`
|
||||
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
|
||||
- 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md`
|
||||
- 分层与功能项清单:`docs/算法/继电保护功能复现规划.md`
|
||||
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 相量层单测细则:`docs/测试说明/相量层单元测试说明.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1 / DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 相量层 / ADC 输入层单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`
|
||||
- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md`
|
||||
|
||||
@@ -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) {
|
||||
@@ -105,7 +105,7 @@ static int stm32_take(void) {
|
||||
* 看到"计数没前进"(那个完成已被上一拍消费掉)。这种伪影**只会连续 1 拍**——
|
||||
* DMA 若真停,第 2 拍仍不会前进。故只有连续 >= ADC_INPUT_STALL_RUN_MAX 拍
|
||||
* 无新帧才计入故障计数(实测开机日志突发期约 21~26 次伪影、稳态每 10 s 心跳 1 次,
|
||||
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §10.5) */
|
||||
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §8) */
|
||||
if (s_stall_run < 255u) {
|
||||
s_stall_run++;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 出错) */
|
||||
|
||||
+1
-1
@@ -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 是
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
* 推导与实测见 freq.c 文件头与 test/host/freq 输出)。
|
||||
* 覆盖规划 §3.3 L3-5 的测量侧与 §4.2 测频范围(45~55 Hz 建议段)。
|
||||
*
|
||||
* 原理与判据(详见 App/algo/README.md §3.7 与 test/host/freq/):
|
||||
* 原理与判据(详见 docs/算法/采集与算法层设计说明.md §7 与 test/host/freq/):
|
||||
* - 采样率 Fs = 1000 Hz(TIM3 节拍,与 phasor 同源);45~55 Hz 段每拍
|
||||
* 相位增量 ≈ 0.28~0.35 rad,距 ±π 缠绕边界裕度巨大,无歧义;
|
||||
* - 瞬时值钳制:单拍相位差折算越出 35~65 Hz(**假值带**,宽于系统合理带
|
||||
|
||||
+3
-3
@@ -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; /* 使用区是否已产出过有效结果 */
|
||||
|
||||
+1
-1
@@ -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。 */
|
||||
|
||||
+30
-32
@@ -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 §10。
|
||||
* **L1 采集层(v2.3,2026-10-10)**:取数改走 App/algo/adc_input.c——
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §6。
|
||||
* **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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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」只约束状态位) */
|
||||
|
||||
@@ -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 → 被调度饿死(本轮迟迟跑不完)
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
+5
-5
@@ -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 永久卡在这里,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点)
|
||||
* v1.0 2026-10-10 王建锋 创建:常开任务耗时探针
|
||||
*
|
||||
* 设计依据:docs/子系统设计/看门狗设计说明.md §11.4(探针字段语义与实测基线)
|
||||
*/
|
||||
|
||||
@@ -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 复位,但"占用者"无法从既有日志判定。
|
||||
|
||||
@@ -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 次。
|
||||
|
||||
+9
-8
@@ -146,13 +146,13 @@ 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(Task #32 P1-5 方案 a,2026-10-10):消费改由 TIM6 ISR 事件驱动后
|
||||
* 提优先级,使"被唤醒 → 立即抢占其余 Normal 任务",唤醒延迟固定为 µs 级。
|
||||
* 代价:本任务单拍最坏实测 adcm≈2.1 ms 会在 AboveNormal 上挤占 net/ftp/default/timeSync,
|
||||
* 其中 ADC 的 2 ms 忙等(App/algo/adc_input.c)尤其敏感 → 硬化方向见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10.4。 */
|
||||
* ⚠ 优先级 AboveNormal:消费由 TIM3 ISR 事件驱动,提优先级使"被唤醒 → 立即抢占其余
|
||||
* Normal 任务",唤醒延迟固定为 µs 级。
|
||||
* 代价:本任务单拍最坏 adcm≈2 ms 会在 AboveNormal 上挤占 net/ftp/default/timeSync;
|
||||
* 实测峰值来自每 10 s 心跳日志而非采集(STM32 侧已由 DMA 搬运,单拍取数 µs 量级)
|
||||
* → 耗时特征与代价见 docs/子系统设计/ADC采样节拍设计说明.md §9、§10。 */
|
||||
osThreadId_t adcTaskHandle;
|
||||
const osThreadAttr_t adcTask_attributes = {
|
||||
.name = "adcTask",
|
||||
@@ -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",
|
||||
|
||||
+5
-5
@@ -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 §10。放 USER CODE 区(L2)。
|
||||
* ⚠ 节拍源 v2.0 由 TIM6 改为 TIM3:F4 上 ADC1 的外部触发清单里没有 TIM6 TRGO
|
||||
* (TIM6/TIM7 的 TRGO 只接 DAC),详见 App/app/app_main.c 步骤 6 注释。
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §3.3。放 USER CODE 区(L2)。
|
||||
* ⚠ 节拍源必须能触发 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 的实测坑)。 */
|
||||
|
||||
+2
-1
@@ -90,7 +90,7 @@ docs/
|
||||
| [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) |
|
||||
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
|
||||
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(Task #32 沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析、验收判据与实测剖面;**已落地并上板闭环** |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍(现状说明)**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;节拍源为什么是 TIM3、DMA 硬约束、事件驱动消费与就绪门、实现落点、观测计数、性能特征与设计边界 |
|
||||
| [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + L0.5 全栈单测 |
|
||||
| [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 |
|
||||
| [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 |
|
||||
@@ -104,6 +104,7 @@ docs/
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [`傅氏算法详解.md`](算法/傅氏算法详解.md) | 全周波傅氏(单频 DFT)原理与工程实现:正交性、梳状带通、滑动窗、系数表折算有效值;保护/遥测双轨有效值的算法基础 |
|
||||
| [`采集与算法层设计说明.md`](算法/采集与算法层设计说明.md) | **`App/algo/` 的现状设计说明**:L1 语义取数与有效性判定、L2 采样环/相量/测频、L3 定值模型与元件框架的契约、关键机制(双区原子切换、乒乓半区、自适应测频窗、定值封存-自检-闭锁)、精度判据 J1~J6 与资源足迹 |
|
||||
| [`继电保护功能复现规划.md`](算法/继电保护功能复现规划.md) | **保护功能复现规划**:把参考系统(CLS100)保护链路映射到本项目(STM32F4)的分层功能/性能/参数清单、平台适配(N-18 等)与分阶段路线图 |
|
||||
|
||||
---
|
||||
|
||||
@@ -406,7 +406,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
| **触发条件** | `App/algo/freq.*`、`App/algo/phasor.*`、`App/algo/sample_ring.*`、`test/host/freq/**` 或 `test/host/common/unit_test_harness.py` 变动 |
|
||||
| **处理方式** | **阻止提交**(单测失败即拦截) |
|
||||
| **被测对象** | `App/algo/freq.c/h`(L2.5 测频:共轭乘积相位增量 + 解缠绕 + 自适应窗长 K_eff 对齐负序泄漏纹波周期 + 假值钳制带 35~65 Hz + 幅值门 valid=0)——纯合成信号、零 I/O |
|
||||
| **用例** | 46 断言:J1 扫频 45~55 Hz ≤0.02 Hz(实测 0.0101)/ J2 噪声 1% ≤0.10 Hz / J3 启动收敛 ≤40 ms / J4 阶跃 50→51 ≤120 ms(精度优先于响应,见 `App/algo/README.md` §3.7)/ J5 浮空 valid=0 不发散 / J6 双跳变钳制恢复 |
|
||||
| **用例** | 46 断言:J1 扫频 45~55 Hz ≤0.02 Hz(实测 0.0101)/ J2 噪声 1% ≤0.10 Hz / J3 启动收敛 ≤40 ms / J4 阶跃 50→51 ≤120 ms(精度优先于响应,见 `docs/算法/采集与算法层设计说明.md` §7.2)/ J5 浮空 valid=0 不发散 / J6 双跳变钳制恢复 |
|
||||
| **为什么需要** | 测频精度是 OF/UF 判据的地基;板端无标准源照不出来(规划 §3.3 L3-5 注入验收归 B10);定长窗负序泄漏纹波(0.317 Hz)这类坑只有扫频合成信号才能暴露 |
|
||||
| **运行器** | `test/host/freq/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
|
||||
|
||||
|
||||
+307
-431
@@ -1,468 +1,344 @@
|
||||
# ADC 采样节拍设计说明(1 kHz 硬件定时器触发)
|
||||
# ADC 采样节拍设计说明(TIM3 1 kHz 同源硬件节拍)
|
||||
|
||||
> **状态**:**已实现并通过台架验收(2026-10-10)**——TIM6 更新中断(1 kHz)触发已落地
|
||||
> (`Src/main.c` 回调 + `app_main.c` 启动 + `adc_task.c` 退化为纯消费者),`@build` 0 错 0 警;
|
||||
> 台架 `@test` **PASS**(`frame_seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`、回读一致;
|
||||
> 实测见 §7.1)。
|
||||
> **事件驱动消费改造方案 a 已落地(Task #32 P1-5,2026-10-10)**:用户决策
|
||||
> "ISR 只发唤醒 + 计算留任务 + 提优先级"(方案 a),拒绝"ISR 内计算"。消费端已从
|
||||
> 1 ms `osDelayUntil` 轮询改为 **TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒**,`adcTask`
|
||||
> 优先级 `Normal` → **`AboveNormal`**。台架实测 **`idle = 0`**(20000 拍全零;改前 1~649)、
|
||||
> `frames` 严格 1000.0 Hz、UI 侧 `uip/uiw/uis` 无退化。详见 §10。
|
||||
> **上位需求**:`docs/算法/继电保护功能复现规划.md` §3.2 L1-1(周期 CONVST 重触发 1000 Hz,
|
||||
> 与保护节拍同源)。**驱动契约**:`Drivers/BSP/TPAFE5160/README.md`。
|
||||
> **背景**:B1(2026-10-10)已把 `adc_task` 由空桩改为真实消费者,但采样节拍是
|
||||
> **任务周期软触发**(`osDelayUntil`)——CONVST 边沿 = 任务被调度到的时刻,抖动大、
|
||||
> 且与 FreeRTOS tick 同源。本项把节拍源换成**硬件定时器**。
|
||||
> **模块名称**:ADC 采样节拍与消费链——TIM3 节拍源(`Src/tim.c`)、更新事件回调(`Src/main.c`)、
|
||||
> L1 采集层(`App/algo/adc_input.c`)、消费者 `adcTask`(`App/task/adc_task.c`)。
|
||||
> **文档定位**:**现状设计说明**——这条链当前由谁产生节拍、同一个硬件事件如何驱动两片转换器、
|
||||
> STM32 侧怎么搬运、消费者怎么取数、为什么这样配置、边界与代价在哪里。**历史演进、方案对比、
|
||||
> 门禁与台架复测记录、修订日志一律不写进本文**(需要时查 git 历史)。
|
||||
> **状态**:已落地并经真机验证;下游 = L2 相量 / 测频 → L3 保护判据(`docs/算法/采集与算法层设计说明.md`)。
|
||||
> **相关文档**:
|
||||
> `docs/硬件平台/ADC通道映射说明.md`(网名 ↔ 板级 `ADCn` ↔ 转换器通道号,唯一权威)
|
||||
> `docs/算法/继电保护功能复现规划.md` §3.1 L1-1(上位需求:1000 Hz 周期 CONVST 重触发)
|
||||
> `Drivers/BSP/TPAFE5160/README.md`(驱动契约:`BUSY` / `FRSTDATA` / 原子快照)
|
||||
> `docs/子系统设计/看门狗设计说明.md` §11.4(`[PROF]` 字段语义与耗时基线)
|
||||
>
|
||||
> ⚠ **网名 `AINn` ≠ 板级 `ADCn` ≠ 转换器通道号**(17 路里 13 路置换)→ 取数必须查通道映射文档。
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 节拍链路总览](#1-节拍链路总览)
|
||||
- [2. 实现落点](#2-实现落点)
|
||||
- [3. 节拍源:TIM3 配置与启动](#3-节拍源tim3-配置与启动)
|
||||
- [4. STM32 侧:ADC1 触发与 DMA 乒乓双缓冲](#4-stm32-侧adc1-触发与-dma-乒乓双缓冲)
|
||||
- [5. TPAFE 侧:CONVST 脉冲与 BUSY 读帧](#5-tpafe-侧convst-脉冲与-busy-读帧)
|
||||
- [6. 消费者 adcTask:事件驱动唤醒](#6-消费者-adctask事件驱动唤醒)
|
||||
- [7. 单周期时序与中断交互](#7-单周期时序与中断交互)
|
||||
- [8. 可观测性:心跳计数与判读](#8-可观测性心跳计数与判读)
|
||||
- [9. 性能与资源特征](#9-性能与资源特征)
|
||||
- [10. 设计边界与已知代价](#10-设计边界与已知代价)
|
||||
- [11. 设计铁律(改这条链前必读)](#11-设计铁律改这条链前必读)
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与现状
|
||||
## 1. 节拍链路总览
|
||||
|
||||
| 项 | 改造前(B1 软触发) | 改造后(TIM6 硬件定时器,2026-10-10 已落地) |
|
||||
一句话链:**TIM3 每 1 ms 产生一个更新事件,同一个事件同时驱动两片转换器,并唤醒唯一的消费者任务**。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
T["TIM3 更新事件(1 kHz)"]
|
||||
T --> ISR["TIM3 更新中断(抢占优先级 6)"]
|
||||
T --> TRGO["TRGO_UPDATE → ADC1 外部触发(上升沿)"]
|
||||
|
||||
ISR --> CONV["PC9 CONVST 脉冲"]
|
||||
ISR --> WAKE["adc_tick_irq_notify()<br/>就绪门 + 置 ADC_WAKE_FLAG"]
|
||||
|
||||
CONV --> BUSY["TPAFE 转换 → BUSY 拉低"]
|
||||
BUSY --> EXTI["PC7 EXTI(优先级 6)读 8 通道<br/>帧缓冲 + seq++"]
|
||||
|
||||
TRGO --> SCAN["ADC1 一轮 9 通道"]
|
||||
SCAN --> DMA["DMA2_Stream4 循环搬运<br/>乒乓双缓冲 2×9 半字"]
|
||||
|
||||
WAKE --> TASK["adcTask(AboveNormal)被唤醒即抢占"]
|
||||
EXTI --> POLL["adc_input_poll():TPAFE 原子快照"]
|
||||
DMA --> TAKE["stm32_take():取上一拍完成的半区"]
|
||||
TASK --> POLL
|
||||
TASK --> TAKE
|
||||
POLL --> L2["L2 相量 / 测频 → 采样环<br/>仅确有新帧时压环"]
|
||||
TAKE --> L2
|
||||
L2 --> OUT["下游:L3 保护判据(protectTask)"]
|
||||
L2 --> HB["task_wd_beat(TASK_WD_ADC) + 10 s 心跳"]
|
||||
```
|
||||
|
||||
**关键性质**:
|
||||
|
||||
- **一个更新事件驱动两片转换器**:TPAFE 的 CONVST 由 ISR 内 GPIO 脉冲给出,STM32 ADC1 由同一事件
|
||||
的 TRGO 硬件触发 → 两者的相对相位固定,与软件调度无关。
|
||||
- **节拍精度是硬件级**:84 MHz ÷ (PSC+1) ÷ (ARR+1),与 FreeRTOS tick(TIM7)无关;硬件自动重载,
|
||||
任务被长时抢占**不漏拍**。
|
||||
- **节拍与唤醒同源**:消费者由同一个更新事件的 ISR 唤醒 → 改采样率时消费节拍自动跟随(§10)。
|
||||
- **消费者是纯消费者**:不触发转换、不自旋等转换、不做保护运算;只做"取一拍 → 压环 → 算相量 → 心跳"。
|
||||
- **零新任务、零动态分配**;这条链新增的静态 RAM 只有 `s_stm32_dma` 36 B(§9)。
|
||||
|
||||
⚠ **同一事件驱动 ≠ 8 路电气量同刻采样**:两片转换器之间没有共同采样触发点(§10 第一条)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 实现落点
|
||||
|
||||
| 位置 | 当前职责 | 改动边界 |
|
||||
|---|---|---|
|
||||
| CONVST 边沿时刻 | 任务被调度时刻(抖动 = 调度延迟,可达数十 µs~ms) | TIM 更新事件 + ISR 入口延迟(亚 µs~数 µs) |
|
||||
| 周期精度 | 依赖 FreeRTOS tick(TIM7) | 定时器硬件分频,**晶振级** |
|
||||
| 丢拍 | 任务被长时抢占会漏拍 | 硬件自动重载,不漏拍 |
|
||||
| 等周波调频 | 需改 tick 周期(不可行) | 运行期写 ARR 即可(§8) |
|
||||
| `MDK-ARM/STM32F407-Demo.ioc` | TIM3 使能与参数(PSC / ARR / TRGO / NVIC 6)、ADC1 触发源与 DMA 绑定、DMA2_Stream4 | **改这里**,重生成才不丢 |
|
||||
| `Src/tim.c` / `Inc/tim.h` | `htim3`、`MX_TIM3_Init()`、`HAL_TIM_Base_MspInit()`(`TIM3_IRQn` 优先级 6) | CubeMX 生成,勿手改 |
|
||||
| `Src/stm32f4xx_it.c` | `TIM3_IRQHandler()` → `HAL_TIM_IRQHandler(&htim3)` | 生成 |
|
||||
| `Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback()` | TIM3 分支:CONVST 脉冲 + `adc_tick_irq_notify()` | **只写 `USER CODE` 区**(§3.5) |
|
||||
| `App/app/app_main.c`(步骤 6) | 唯一的节拍启动点 `HAL_TIM_Base_Start_IT(&htim3)` | 手写;顺序约束见 §3.4 |
|
||||
| `Src/adc.c` | ADC1 触发与 DMA 参数 + `adc1_calibrate()` | 参数在生成区;自校准在 `USER CODE` 区(§4.2) |
|
||||
| `Src/dma.c` | DMA2_Stream4 参数与 NVIC 优先级 | 生成区;**改流后必回本页核对**(§4.3) |
|
||||
| `App/algo/adc_input.c` | L1:`adc_input_poll()` / `adc_input_raw()`、半区推导、DMA 完成回调、四类故障计数 | 手写 |
|
||||
| `App/task/adc_task.c` / `adc_task.h` | 消费者主循环、`adc_tick_irq_notify()` 与就绪门、10 s 心跳统计 | 手写 |
|
||||
| `Src/freertos.c` | `adcTask` 栈 640 words、优先级 `osPriorityAboveNormal` | 手写于 `USER CODE` 区(不在 `.ioc`);改栈前后各跑一次 `stack_report.py` |
|
||||
| `App/task/protect_task.c` | 下游消费者:20 ms 判据节拍取 L2 相量快照 | → `docs/算法/采集与算法层设计说明.md` §9 |
|
||||
|
||||
**判据(L1-1)**:周期 CONVST 重触发 1000 Hz;示波器见 U3 `BUSY` 连续脉冲;
|
||||
`tpafe5160_frame_seq()` 持续递增(每秒 +1000)。
|
||||
---
|
||||
## 3. 节拍源:TIM3 配置与启动
|
||||
|
||||
### 3.1 当前配置
|
||||
|
||||
| 项 | 值 | 依据 / 后果 |
|
||||
|---|---|---|
|
||||
| 计数时钟 | 84 MHz(APB1 定时器时钟 = 42 MHz × 2) | SYSCLK 168 MHz / HCLK 168 MHz / APB1 ÷4 |
|
||||
| Prescaler (PSC) | **83** | → 1 MHz 计数,**1 µs 分辨率** |
|
||||
| Counter Period (ARR) | **999** | → **1.000 kHz**(1000 µs 周期) |
|
||||
| Counter Mode | Up | — |
|
||||
| Clock Division | No Division | 与采样无关 |
|
||||
| auto-reload preload | **Enable** | 运行期写 ARR 在下一个更新事件生效 → 改频无毛刺(§10) |
|
||||
| Trigger Event Selection | **Update**(TRGO = 更新事件) | 硬件触发 ADC1(§4.1) |
|
||||
| Master-Slave Mode | MSM = ENABLE(从模式 `SMS` = 0 内部时钟) | 软件 `CEN` 启动不受影响(已核对 RM0090 与生成代码) |
|
||||
| NVIC | `TIM3_IRQn` 抢占 **6** / 子优先级 0 | 理由见 §3.3 |
|
||||
| ADC1 触发 | `ADC_EXTERNALTRIGCONV_T3_TRGO`,边沿 RISING | §4.1 |
|
||||
|
||||
`MX_TIM3_Init()` **只配置不启动**;`HAL_TIM_Base_Start_IT(&htim3)` 是唯一启动点(§3.4)。
|
||||
|
||||
### 3.2 为什么是 TIM3(不是 TIM6 / TIM7)
|
||||
|
||||
F4 上 **ADC1 的外部触发清单里没有 TIM6 / TIM7 的 TRGO**——它们的 TRGO 只接 DAC。ADC1 的可用外部触发
|
||||
(RM0090 的 `EXTSEL` 表、本仓 `stm32f4xx_hal_adc.h`、CubeMX 器件库 `db/mcu/IP/ADC-STM32F417_Modes.xml`):
|
||||
|
||||
`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`
|
||||
|
||||
想让**同一个硬件事件**同时驱动两片转换器,TRGO 型候选只剩 TIM2 / TIM3 / TIM4 / TIM5 / TIM8;
|
||||
TIM7 已被 HAL 时基占用,TIM3 空闲且同为 APB1 通用定时器 → **用 TIM3**。PSC / ARR 与 HAL 时基 TIM7
|
||||
同构(84 MHz / 1 MHz / 1000 计数),对照排查时两边可直接互参。
|
||||
|
||||
**换节拍源前先核三条**:① 该定时器的 TRGO 在 F4 上确实能触发 ADC1;② 定时器空闲(TIM7 = HAL 时基);
|
||||
③ 是否仍在 APB1 时钟域(否则 PSC / ARR 要按新时钟重算)。
|
||||
|
||||
### 3.3 优先级为什么是 6
|
||||
|
||||
- 与 TPAFE `BUSY` 的 EXTI(`EXTI9_5_IRQn`)**同抢占优先级 → 互不抢占**:读帧的 8 次 RD 翻转不会
|
||||
被打断,避免"读到一半被新转换刷新数据总线"。
|
||||
- ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(= 5)→ ISR 内允许调内核 API(`osThreadFlagsSet()`
|
||||
实为 `xTaskNotifyFromISR()` + `portYIELD_FROM_ISR()`,会立即触发上下文切换)。
|
||||
- 5 是 USART、7 是 SPI / DMA 等 → 被更高优先级 ISR 挡住的时长以 µs 计(§7)。
|
||||
- `DMA2_Stream4_IRQn` 同为 6(`Src/dma.c`)。
|
||||
|
||||
### 3.4 启动位置与顺序
|
||||
|
||||
1. `MX_TIM3_Init()` 由 `main()` 在 `app_main_init()` 之前调用(生成代码,**只配置不计数**)。
|
||||
2. **`HAL_TIM_Base_Start_IT(&htim3)` 必须排在 `tpafe5160_init()` 成功之后**(`app_main.c` 步骤 6):
|
||||
否则 ISR 会在驱动首帧有效性自检期间乱发 CONVST,使自检结果不可信。
|
||||
3. 节拍启动仍**早于内核**:`osKernelInitialize()` / `MX_FREERTOS_Init()` / `osKernelStart()` 都在其后
|
||||
→ "节拍已在跑"与"消费者已存在"之间有约 100 ms 窗口,处置见 §6.1。
|
||||
|
||||
### 3.5 共用回调与生成区落点
|
||||
|
||||
`HAL_TIM_PeriodElapsedCallback()` 被 **TIM7 与 TIM3 共用**:TIM7 分支是 HAL 时基(`HAL_IncTick()`)与
|
||||
看门狗时基(`task_wd_tick_probe()`),TIM3 分支是采样节拍。三条分支**并列**,不得合并、不得删改
|
||||
TIM7 那两条(改坏不报编译错,只在时钟与看门狗上出事)。TIM3 分支写在 `USER CODE` 保护区;
|
||||
重生成存活方法论与审计脚本 `scripts/check_cubemx_survival.py` → `docs/子系统设计/看门狗设计说明.md` §10。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案选型
|
||||
## 4. STM32 侧:ADC1 触发与 DMA 乒乓双缓冲
|
||||
|
||||
### 2.1 方案 A(**推荐**):TIM6 更新中断 → ISR 内脉冲 CONVST
|
||||
### 4.1 ADC1 当前配置
|
||||
|
||||
- TIM6 配成 1 kHz 更新中断;ISR 内调 `tpafe5160_start_conv_irq()`(现成的 GPIO 脉冲)。
|
||||
- **零引脚改动**:PC9 保持 `GPIO_Output`,驱动与 `tpafe5160_init()` 首帧自检**完全不动**。
|
||||
- BUSY(PC7)EXTI 读帧路径**完全不动**。
|
||||
- 周期精度 = 定时器硬件(晶振级);CONVST 边沿抖动 = TIM6 ISR 入口延迟(见 §5)。
|
||||
- 运行期改 ARR 即改变采样率 → 天然支持等周波调频。
|
||||
| 项 | 值 | 后果 |
|
||||
|---|---|---|
|
||||
| `ScanConvMode` | ENABLE | 一轮 9 通道 |
|
||||
| `ContinuousConvMode` | DISABLE | 每轮由外部触发启动(§7) |
|
||||
| `ExternalTrigConv` | `T3_TRGO`,`ExternalTrigConvEdge = RISING` | 与 CONVST 同一个更新事件 |
|
||||
| `NbrOfConversion` | 9 | 通道清单与语义 → `docs/硬件平台/ADC通道映射说明.md` |
|
||||
| `DMAContinuousRequests` | **ENABLE**(DDS 位) | ⚠ 关掉 = 一轮 9 通道只发**一次** DMA 请求 → **静默丢 8 路,不报任何错** |
|
||||
|
||||
### 2.2 方案 B(备选,暂不采用):TIM3_CH4/TIM8_CH4 硬件 PWM 直驱 PC9
|
||||
### 4.2 上电自校准
|
||||
|
||||
- PC9 复用为定时器通道,硬件直接产生 CONVST 方波,**零软件抖动**。
|
||||
- **代价**:① 需把 PC9 由 `GPIO_Output` 改为 AF(`.ioc` 引脚改动);
|
||||
② 驱动的 `TPAFE5160_CONVST_*` 宏(GPIO 写)全部失效 → `tpafe5160_init()` 的
|
||||
"确保 CONVST 低"与首帧自检(E-44)都要重写;③ 上电初值/空闲电平语义变化。
|
||||
- **结论**:收益(把抖动从"数 µs"降到"0")不足以抵消改动面与回归风险。
|
||||
**仅当 §5 实测抖动超过算法容限时再考虑**。
|
||||
F4 的 ADC **上电必须自校准**:缺它不报任何错,只是读数带失调。CubeMX 不生成这一步,且本仓库裁剪版
|
||||
HAL 没有 `HAL_ADCEx_Calibration_Start()`、裁剪版 CMSIS 头没有 `ADC_CR2_CAL`(位值 3 在源内自定义)
|
||||
→ `adc1_calibrate()` 手写于 `Src/adc.c` 的 `USER CODE` 区,在 `MX_ADC1_Init()` 末尾调用。
|
||||
时序按 RM0090 §13.3.6:清 `ADON` → 等硬件清零 → 置 `ADON` 上电待稳定 → 置 `CAL` → 等硬件**自动**清
|
||||
`CAL`(上限 10 ms)。存活由审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底。
|
||||
|
||||
> 下文只展开**方案 A**。
|
||||
### 4.3 DMA 选型与四条硬约束
|
||||
|
||||
| 项 | 当前值 | 为什么 / 违反的代价 |
|
||||
|---|---|---|
|
||||
| 流 | **DMA2_Stream4** | ADC1 在 DMA2 **只挂 Stream0 或 Stream4**(Stream1 = ADC3、Stream2/3 = ADC2)。取 Stream4 是为了不碰 SPI1_RX / USART1_RX 所在的流(NAND 与调试串口的 DMA)——让 CubeMX 自由分配会优先抢 Stream0,把这两路连锁搬家 |
|
||||
| NVIC 优先级 | **6** | ⚠ **换流会把该流的 NVIC 优先级重置为 0**,0 抢占 FreeRTOS 临界区(上限 5)→ 任何改流之后必须回 `Src/dma.c` 核对优先级 |
|
||||
| `DMAContinuousRequests` | ENABLE | 见 §4.1:违反时**不报错**,只是静默丢通道 |
|
||||
| 缓冲地址 | `s_stm32_dma[18]`(`uint16_t`)落在 **`RW_IRAM1`** | ⚠ ADC1 走 **DMA2**,而 F4 的 DMA2 **不可达 CCM**(`0x10000000`)→ 放错不报编译错也不报运行错,数据永远搬不回来 |
|
||||
|
||||
模式:`DMA_CIRCULAR` + `DMA_PRIORITY_LOW` + halfword→halfword,长度 `2 × 9 = 18`(两个 9 通道半区)。
|
||||
|
||||
### 4.4 半区推导(无竞态写法)
|
||||
|
||||
半传输与整传输**都**代表"一整帧就绪",两个回调共用同一个自增计数(`HAL_ADC_ConvHalfCpltCallback()` /
|
||||
`HAL_ADC_ConvCpltCallback()`,均带 `hadc == &hadc1` 句柄守卫);消费侧 `stm32_take()` 只读一个计数:
|
||||
|
||||
```c
|
||||
uint32_t seq = s_dma_seq; /* 单次 32 位读:与 ISR 自增不产生撕裂 */
|
||||
uint8_t half = (uint8_t)(1u - (seq & 1u)); /* 奇 = 刚完成前半区,偶 = 刚完成后半区 */
|
||||
const uint16_t *src = &s_stm32_dma[half * ADC_INPUT_STM32_CH_NUM];
|
||||
```
|
||||
|
||||
- **判据只读一个 32 位量**:不另存"当前半区"变量——否则会出现"读到新 `half`、读到旧 `seq`"的
|
||||
组合窗口。
|
||||
- **代价**:STM32 侧数据固定滞后一拍(1 ms)。两片转换器本就无同刻性(§10)→ 不新增误差。
|
||||
- **失败拍不沿用旧值**:`seq` 未前进(本拍无新帧)或 `HAL_ADC_Start_DMA()` 挂载失败 → 清
|
||||
`s_stm32_valid` → 本拍 STM32 侧判为无效,消费者**整帧跳过**(`skip` 计数,§8),下一拍成功自动恢复。
|
||||
- 取帧状态码:`ADC_INPUT_RC_OK` = 0 / `ADC_INPUT_RC_START` = -1 挂载失败 /
|
||||
`ADC_INPUT_RC_STALL` = -2 本拍无新帧 / `ADC_INPUT_RC_NOT_RUN` = -3 尚未取过帧。
|
||||
|
||||
---
|
||||
## 5. TPAFE 侧:CONVST 脉冲与 BUSY 读帧
|
||||
|
||||
- **PC9 CONVST** 是 `GPIO_Output`(**不是**定时器复用引脚),脉冲由 TIM3 ISR 内两次 GPIO 写产生
|
||||
→ 引脚配置、驱动 `tpafe5160_init()` 与首帧自检都不受影响。
|
||||
- 转换约 23 µs → **PC7 `BUSY`** 拉低 → `EXTI9_5_IRQn`(优先级 6)内读 8 通道(≈0.5 µs)→ 写帧缓冲、
|
||||
`seq++`。转换与读帧时序的契约在 `Drivers/BSP/TPAFE5160/README.md`。
|
||||
- **取数只用原子快照**:`tpafe5160_snapshot()`(临界区,**仅任务上下文**)。旧的 `get_buf()` /
|
||||
`data_ready()` / `clear_ready()` 已从驱动**删除**——两步之间被抢占会拷回两帧混合数据。
|
||||
- 消费者一律经 `adc_input_poll()` / `adc_input_raw()` **按语义**取数,**不得写死缓冲区下标**
|
||||
(`buf[2]=IA` 一类映射只写在 `docs/硬件平台/ADC通道映射说明.md`)。
|
||||
- `frst_err`(`tpafe5160_frstdata_err()`)是"`FRSTDATA` 无跳变"的帧计数——它是"每个周期 `BUSY`
|
||||
EXTI 都真的在读帧"的直接观测面。
|
||||
|
||||
---
|
||||
|
||||
## 3. CubeMX 配置规格(照此配置)
|
||||
## 6. 消费者 adcTask:事件驱动唤醒
|
||||
|
||||
**前提**:SYSCLK = 168 MHz / HCLK = 168 MHz / APB1 = 42 MHz(÷4)→ **APB1 定时器时钟 = 84 MHz**;
|
||||
HAL 时基 = TIM7(已占用);**TIM1~TIM6、TIM8~TIM14 当前全部空闲**。
|
||||
### 6.1 就绪门(ISR 侧的唯一入口)
|
||||
|
||||
| 步骤 | 位置 | 值 | 说明 |
|
||||
`TIM3` 的节拍远早于调度器(§3.4):这约 100 ms 窗口里 `adcTaskHandle` 还是 `NULL`、调度器也还没起。
|
||||
|
||||
- ISR **只调** `adc_tick_irq_notify()`,**不得**在回调里直接 `osThreadFlagsSet()`。前者内含就绪门
|
||||
`s_consumer_ready`——由 `adcTask` 自己在进入等待前置 1(**单写者 = 任务,ISR 只读**)。
|
||||
- 就绪门把时序约束写成**显式代码**,不依赖 FreeRTOS 对 `NULL` 句柄的内部宽容——"句柄已赋值、
|
||||
调度器未启动"这一段仍会让 ISR 版内核 API 跑在调度器之外(内核启动前不得调用内核 API 是平台级
|
||||
约束,见 AGENTS.md「RTOS 与内存」)。
|
||||
- 置位与等待之间不漏唤醒:线程标志**粘滞**,开闸(`s_consumer_ready = 1u`)放在进入循环之前。
|
||||
- **把"定时轮询"换成"事件驱动"时,必须单独核对节拍源启动时刻与消费者创建时刻**——否则第一次上电
|
||||
就会踩到"ISR 对还不存在的消费者动手"。同类约束 → `docs/子系统设计/看门狗设计说明.md` §11.5。
|
||||
|
||||
### 6.2 等待语义与优先级
|
||||
|
||||
| 项 | 当前实现 | 说明 |
|
||||
|---|---|---|
|
||||
| 等待 | `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` | 无超时 = 刻意的 fail-safe(§6.3)。`ADC_WAKE_FLAG` 是**命令位**,等待即消费,**不加** `osFlagsNoClear`(那是就绪屏障位的规则) |
|
||||
| 优先级 | `osPriorityAboveNormal` | 唤醒后**立即抢占**其余 Normal / BelowNormal 任务 → "唤醒 → 开始处理"的延迟固定在 µs 级,无 tick 量化、无累积漂移;代价见 §9、§10 |
|
||||
| 超拍合并 | 多个 TIM3 脉冲合并成一次唤醒 | 下一轮 `adc_input_poll()` 返 `r == 0` → 记 `idle`,**不重复压环**(只有 `r == 1u` 才压环)→ 20 点 DFT 窗不会被上一拍的重复值污染 |
|
||||
| 看门狗 | 每拍 `task_wd_beat(TASK_WD_ADC)`;`required_mask` 与心跳位集合一致 | 提优先级不改掩码 |
|
||||
| 本任务不做 | 不触发转换、不 `osDelay` 轮询、不做保护运算 | 判据在 `App/algo/prot_elm.c`,消费者是 `protectTask`(20 ms 节拍) |
|
||||
|
||||
### 6.3 节拍缺失是刻意的 fail-safe
|
||||
|
||||
TIM3 未启动或停摆 → ADC1 不再被触发、`adcTask` 再无唤醒 → 任务永久阻塞 → `TASK_WD_ADC` 缺席 →
|
||||
IWDG 复位。**设计如此**:节拍源缺失不允许被静默放过,不得为了"少一次复位"给等待加超时。
|
||||
|
||||
---
|
||||
|
||||
## 7. 单周期时序与中断交互
|
||||
|
||||
```text
|
||||
t = 0 TIM3 更新事件 → ISR:PC9 CONVST 上升沿脉冲 + adc_tick_irq_notify()
|
||||
同一更新事件经 TRGO 启动 ADC1 一轮 9 通道(DMA 逐通道搬运)
|
||||
t ≈ 0+ε adcTask 被唤醒(AboveNormal → 立即抢占)→ adc_input_poll()
|
||||
t ≈ 23 µs BUSY 拉低 → EXTI 读 8 通道(≈0.5 µs)→ 帧缓冲、seq++
|
||||
t ≈ 67 µs 第 9 个通道搬完 → 半传输 / 整传输回调 s_dma_seq++
|
||||
t = 1000 µs 下一个更新事件
|
||||
```
|
||||
|
||||
| 交互 | 现状 | 判据 |
|
||||
|---|---|---|
|
||||
| CONVST ↔ 读帧 | 读窗口与下一次 CONVST 相隔约 977 µs,互不重叠 | 转换只占约 23 µs |
|
||||
| TIM3 ↔ `BUSY` EXTI | 同抢占优先级 6 → **互不抢占** | 读 RD 时序不被打断(§3.3) |
|
||||
| TIM3 ↔ 更高优先级 ISR | USART(优先级 5)可挡住节拍 ISR,时长 µs 级 | 触发抖动占 1000 µs 的 < 0.5% |
|
||||
| ISR 本体 | 两次 GPIO 写 + 一次线程标志置位 | ISR 内不做任何采集与算法计算(§11 第一条) |
|
||||
| 数据就绪时刻 | TPAFE 侧本拍 ISR 后约 23 µs;STM32 侧取**上一拍已完成**的半区 | §4.4;因此 `adc1_seq` 与 `seq` 之间是一个**恒定小偏移**(§8) |
|
||||
|
||||
---
|
||||
## 8. 可观测性:心跳计数与判读
|
||||
|
||||
每 10 s 一条 `[ADC_TASK]` 心跳(`ADC_TASK_REPORT_MS` = 10000):
|
||||
|
||||
| 字段 | 来源 | 正常态 | 异常判读 |
|
||||
|---|---|---|---|
|
||||
| 1 | Timers → **TIM6** → *Activated* | ✅ 勾选 | 基础定时器,**无输出引脚**(不占用任何 IO) |
|
||||
| 2 | Prescaler (PSC) | **83** | 84 MHz ÷ (83+1) = **1 MHz** 计数时钟(1 µs 分辨率) |
|
||||
| 3 | Counter Period (ARR) | **999** | 1 MHz ÷ (999+1) = **1.000 kHz**(= 1000 µs 周期) |
|
||||
| 4 | Counter Mode | Up | 向上计数 |
|
||||
| 5 | **auto-reload preload** | **Enable** | 运行期写 ARR 在下一个更新事件生效,不产生毛刺(§8 等周波调频要用) |
|
||||
| 6 | Clock Division | No Division | 与采样无关 |
|
||||
| 7 | Trigger Event Selection | Reset(默认) | 不用 TRGO |
|
||||
| 8 | NVIC Settings → **TIM6 global interrupt** | ✅ 勾选;**Preemption Priority = 6** | 与 BUSY EXTI(`EXTI9_5_IRQn` = 6)**同级**:两者互不抢占,避免"读 RD 时序被 CONVST 脉冲打断"(§5);且 6 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=5),将来 ISR 里发任务通知也安全 |
|
||||
| `frames` | `adc_input_poll() == 1` 的拍数 | 与 `seq` 同速 | 增速低于 `seq` → 消费者被挡或超拍 |
|
||||
| `idle` | 无新帧拍数(`r == 0`) | 稳态不增长 | 持续增长 → `BUSY` EXTI 未在读帧,或节拍源停摆 |
|
||||
| `skip` | 无有效帧整帧跳过(STM32 侧失败拍,§4.4) | 极少 | 持续涨 → 看 `adc1_err` / `adc1_hw` |
|
||||
| `seq` | `tpafe5160_frame_seq()` | 每 10 s +10000(= 1000.0 Hz) | 不涨 → TIM3 未启动或转换未触发 |
|
||||
| `frst_err` | `tpafe5160_frstdata_err()` | 0 | > 0 → 读帧与转换未对齐 |
|
||||
| `ring` | `sample_ring_frames()` | `ring == frames` | 不等 → 有 `idle` / `skip` 拍被压进环(回归恒等式) |
|
||||
| `adc1_rc` | 最近一拍取帧状态 | 0(`ADC_INPUT_RC_OK`) | -2 本拍无新帧;-1 DMA 挂载失败 |
|
||||
| `adc1_seq` | `s_dma_seq`(半传输 + 整传输共用) | ≈ `seq`,**恒定小偏移** | 偏移持续增大 → STM32 侧在丢 |
|
||||
| `adc1_gap` | `seq` 一次前进 > 1 | 每 10 s 心跳 +1 | 快速上涨 → 本任务单拍 > 1 ms(看 `adcm`) |
|
||||
| `adc1_err` | **连续 ≥ 2 拍**无新帧 | 0 | > 0 → 节拍源 / DMA 真停 |
|
||||
| `adc1_hw` | `HAL_ADC_ErrorCallback()` 次数 | 0 | > 0 → ADC / DMA 硬件报错 |
|
||||
| `tgap` | `adc_health_tpafe_gaps()`(L1-6) | 极少 | TPAFE 侧丢帧,与 `adc1_gap` 对称 |
|
||||
|
||||
**生成后**:CubeMX 会新建 `Src/tim.c` + `Inc/tim.h`(含 `htim6` 与 `MX_TIM6_Init()`)、
|
||||
在 `Src/stm32f4xx_it.c` 生成 `TIM6_IRQHandler()`、在 `Src/main.c` 的 `main()` 里插入 `MX_TIM6_Init()`。
|
||||
⚠ **`MX_TIM6_Init()` 只做配置,不启动计数**——启动由用户代码负责(§4 第 4 条)。
|
||||
单拍耗时(`adcp` = `TASK_PROF_ADC_POLL`)与整拍耗时(`adcm` = `TASK_PROF_ADC_BEAT`)不在心跳里,
|
||||
由 DWT 探针的 `[PROF]` 行给出,字段语义与基线 → `docs/子系统设计/看门狗设计说明.md` §11.4。
|
||||
|
||||
> **为何不用 TIM7 那种"1 MHz 计数 + ARR=999"以外的心跳**:本配置与 HAL 时基 TIM7
|
||||
> 的构造完全一致(同为 84 MHz / 1 MHz / 1000 计数),便于对照排查;PSC 取 83 使 ARR 与
|
||||
> "微秒"一一对应,等周波调频时可直接 `ARR = 周期µs - 1`。
|
||||
**为什么"连续 ≥ 2 拍无新帧"才计 `adc1_err`(`ADC_INPUT_STALL_RUN_MAX = 2`)**:任务在 TIM3 触发瞬间
|
||||
被唤醒,而该拍的 DMA 完成约落在 67 µs 之后;若上一拍被心跳日志或抢占拖过这 67 µs,本拍就会看到
|
||||
"计数没前进"(那个完成已被上一拍消费掉)。这种**相位伪影只会连续 1 拍**——DMA 若真停,第 2 拍仍然
|
||||
不会前进。故单拍无新帧**不计故障**,只记 `ADC_INPUT_RC_STALL`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 代码改造清单
|
||||
## 9. 性能与资源特征
|
||||
|
||||
> 约定:**生成区不改**(CubeMX 重生成会覆盖);所有手工代码落 `USER CODE` 保护区(L2)。
|
||||
|
||||
### 4.1 `Src/main.c`(CubeMX 生成;改 `USER CODE` 区)
|
||||
|
||||
- `USER CODE BEGIN Includes` 增加 `#include "tpafe5160.h"`。
|
||||
- `HAL_TIM_PeriodElapsedCallback()`(**注意:该回调被 TIM7 时基共用**)增加 TIM6 分支:
|
||||
|
||||
```c
|
||||
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||||
{
|
||||
if (htim->Instance == TIM7) { HAL_IncTick(); } /* 既有:HAL 时基 */
|
||||
/* USER CODE BEGIN Callback 0 / 1 —— 加在既有 TIM7 分支之后 */
|
||||
if (htim->Instance == TIM6)
|
||||
{
|
||||
tpafe5160_start_conv_irq(); /* 1 kHz 采样节拍:ISR 内脉冲 CONVST(仅 GPIO 写,亚 µs) */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
⚠ **不得**破坏既有 `HAL_IncTick()` / `task_wd_tick_probe()` 两条 TIM7 分支。
|
||||
|
||||
### 4.2 `Src/tim.c` / `Inc/tim.h` / `Src/stm32f4xx_it.c`
|
||||
|
||||
- **CubeMX 自动生成,勿手改**(`TIM6_IRQHandler` → `HAL_TIM_IRQHandler(&htim6)`)。
|
||||
|
||||
### 4.3 `App/app/app_main.c`
|
||||
|
||||
- 增加 `#include "tim.h"`。
|
||||
- 第 213 行的**单次** `tpafe5160_start_conv_irq();` 替换为**启动周期触发**:
|
||||
|
||||
```c
|
||||
/* 6) 启动 1 kHz 采样节拍(TIM6 更新中断 → ISR 内触发 CONVST) */
|
||||
(void)HAL_TIM_Base_Start_IT(&htim6);
|
||||
```
|
||||
|
||||
- ⚠ **必须在 `tpafe5160_init()` 成功之后**再启动 TIM6,否则 ISR 会在驱动自检
|
||||
(首帧有效性探针,E-44)期间乱发 CONVST。
|
||||
|
||||
### 4.4 `App/task/adc_task.c`
|
||||
|
||||
- **删除**主循环里的 `tpafe5160_start_conv_irq();`(触发已交给 TIM6)。
|
||||
- 本任务退化为**纯消费者**:`tpafe5160_snapshot()` + 帧统计 + 10 s 心跳 + 看门狗心跳。
|
||||
1 ms 节拍(`osDelayUntil`)保留,仅用于"消费节奏"。
|
||||
- 更新文件头与函数注释(说明节拍由 TIM6 提供,本任务不再触发)。
|
||||
- **看门狗不变**:`TASK_WD_ADC` 心跳仍在循环内每拍调用。
|
||||
|
||||
### 4.5 文档同步(2026-10-10 已执行)
|
||||
|
||||
- `docs/算法/继电保护功能复现规划.md`:§3.2 L1-1 由 🔶 改为 ✅(TIM6 硬件触发)、§1.2 采样软件行 + 变更记录同步。
|
||||
- `docs/任务规划/基线与待确认.md` §1:TPAFE 自动扫描模式待确认项标注"已不影响"(TIM6 方案不依赖它)。
|
||||
- `AGENTS.md` 架构要点 `adcTask` 条 + `app_main_init` 序列:改为"TIM6 硬件触发 + 纯消费者"。
|
||||
- `Drivers/BSP/TPAFE5160/README.md`:周期重触发改为"由 TIM6 更新中断提供"。
|
||||
|
||||
---
|
||||
|
||||
## 5. 时序与优先级分析
|
||||
|
||||
**单周期时序(1 kHz,周期 1000 µs)**:
|
||||
|
||||
```
|
||||
t=0 TIM6 更新事件 → ISR → CONVST 脉冲(LOW→HIGH 上升沿)
|
||||
t≈0+ε TPAFE5160 开始转换(BUSY 拉高)
|
||||
t≈23 µs BUSY 拉低 → PC7 EXTI(优先级 6) → 读 8 通道(≈0.5 µs)→ seq++
|
||||
t=1000 µs 下一个 TIM6 更新事件
|
||||
```
|
||||
|
||||
- **读窗口与下一次 CONVST 相隔 ~977 µs**(转换仅 ~23 µs),互不重叠。
|
||||
- **TIM6 与 BUSY EXTI 同级(6)→ 互不抢占**:读 RD 时序(8 次 RD 翻转)不会被打断,
|
||||
避免"读到一半被新转换刷新数据总线"。这是选 6 而非 5 的原因。
|
||||
- **触发抖动**:TIM6 是最高优先级之一(5 为 USART,7 为 SPI/DMA);被更高优先级 ISR
|
||||
(USART,~数 µs)挡住的概率与时长都有限 → 最坏抖动约数 µs,占 1000 µs 的 <0.5%。
|
||||
- **ISR 时长**:`tpafe5160_start_conv_irq()` 只做两次 `HAL_GPIO_WritePin`(寄存器写)
|
||||
→ 亚 µs,不拖累其它中断。
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与注意事项
|
||||
|
||||
1. **共用回调**:`HAL_TIM_PeriodElapsedCallback` 同时服务 TIM7(HAL 时基)与 TIM6 →
|
||||
分支必须并列、不得互相干扰;改动后必须确认开机日志时间戳正常(时基未坏)。
|
||||
2. **启动顺序**:TIM6 必须在 `tpafe5160_init()` 之后启动(§4.3)。
|
||||
3. **不新增任务/栈**:本项**零新任务、零新栈、零动态分配** → 不动 `stack_report.py` 预算。
|
||||
4. **CubeMX 生存**:新增生成文件 `Src/tim.c` / `Inc/tim.h`;改后跑一次
|
||||
`python scripts/check_cubemx_survival.py`,若有新增生成文件被登记,按提示重建基线。
|
||||
5. **Keil 工程**:从 CubeMX 重生成工程(含 MDK-ARM)以把 `tim.c` 纳入编译;随后
|
||||
`@build` 必须 0 错 0 警。
|
||||
6. **`mem-numbers`**:本项改 Flash/RAM 极小但非零 → 若 `check_mem_numbers.py` FAIL,
|
||||
跑 `python scripts/check_mem_numbers.py --write` 刷新快照。
|
||||
7. **消费端与生产端同频**:TIM6(84 MHz 派生)与 FreeRTOS tick TIM7(同源)都恰好 1 kHz,
|
||||
相位固定 → 消费端每拍恰好取到 1 帧。**一旦进入等周波调频(采样率≠1 kHz),
|
||||
固定周期轮询会与生产端失配 → 消费端须改为"节拍事件驱动"**(见 §8)。
|
||||
✅ **该前置已满足(2026-10-10,§10)**:消费唤醒改由 TIM6 ISR 置位,与采样率天然同步。
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收判据
|
||||
|
||||
| # | 判据 | 手段 |
|
||||
| 指标 | 现状 | 来源 / 条件 |
|
||||
|---|---|---|
|
||||
| 1 | `@build` 0 错 0 警,且镜像无源文件新于 hex | `keil-build` skill(`keil_build.py`)+ `find -newermt` |
|
||||
| 2 | PC9 `CONVST` 周期 = 1.000 kHz(±0.1%) | 示波器 / 逻辑分析仪 |
|
||||
| 3 | PC7 `BUSY` 每周期一个脉冲,与 CONVST 相位固定 | 示波器双通道 |
|
||||
| 4 | `frame_seq()` 每秒 +1000(每 10 s +10000),严格递增 | `@test` 串口 `[ADC_TASK] frames=... seq=...` |
|
||||
| 5 | `frst_err=0`、`idle` 基本为 0 | 同上 |
|
||||
| 6 | 开机日志时间戳正常(TIM7 时基未坏) | 串口日志 |
|
||||
| 7 | 无 HardFault / `[ERR]` / 看门狗复位 | `@test` Forbidden 判定 |
|
||||
| 节拍频率 | 1.000 kHz,硬件自动重载 | 84 MHz / 84 / 1000;心跳 `seq` 每 10 s +10000 |
|
||||
| 单拍取数 `adcp` | **≈ 8 µs**(TPAFE 快照临界区 + 拷 9 个半字),本段**零忙等** | `[PROF]` 稳态;搬运由 DMA 完成后任务侧不再等待转换(§4) |
|
||||
| 整拍最坏 `adcm` | ≈ 2 ms 峰值,**来自每 10 s 一次的心跳日志**(多行 `DBG_INFO` + 相量打印),不是采集 | 同批 `adcp` 仍是 µs 级、`idle = 0`;勿把 `adcm` 读成采集成本 |
|
||||
| 稳态 `idle` | 0(唤醒与生产同源,无 tick 量化) | §6.2 |
|
||||
| 稳态 `adc1_gap` | 每 10 s 心跳 +1(开机日志突发期集中数十次) | §8:心跳拍超 1 ms + 相位伪影 |
|
||||
| 静态 RAM | `s_stm32_dma` 36 B 在 `RW_IRAM1` | `scripts/ram_report.py`;**加大缓冲前必须重跑 .map 确认仍在 `RW_IRAM1`**(§4.3) |
|
||||
| 任务与栈 | 零新任务;`adcTask` 640 words,静态最坏链约 1.3 KB、余量约 48% | `scripts/stack_report.py`、`docs/内存管理/RAM使用分析与优化方案.md` §2.3 |
|
||||
| 动态分配 | 无(自研代码禁 C 库堆) | AGENTS.md「平台约束」 |
|
||||
| ISR 负担 | 1 kHz 一次,两次 GPIO 写 + 一次标志置位 | §7 |
|
||||
|
||||
### 7.1 台架实测结果(2026-10-10,`@test` PASS)
|
||||
|
||||
证据:`docs/测试说明/boot_log_data/20261010_prod_adc_tim6_boot_log.txt`(`fw build: Oct 10 2026 15:21:20`)。
|
||||
|
||||
| # | 判据 | 实测 |
|
||||
|---|---|---|
|
||||
| 1 | `@build` 0 错 0 警 + 镜像时效 | ✅ 0 错 0 警;无源文件新于 hex |
|
||||
| 2 | CONVST 1.000 kHz | ✅ 以 `frame_seq` 速率替代(无示波器):`seq` 10005→20005→30005,**每 10 s 精确 +10000 = 1000.0 Hz** |
|
||||
| 3 | BUSY 每周期一脉冲 | ✅ 以 `frst_err=0` 替代:全程无"FRSTDATA 无跳变"帧(BUSY EXTI 每周期都在读帧) |
|
||||
| 4 | `frame_seq` 严格递增 | ✅ 见 #2,严格单调 |
|
||||
| 5 | `frst_err=0`、`idle` 基本为 0 | ✅ `frst_err=0`;⚠ `idle` 每 10 s +5(≈0.05%,见下) |
|
||||
| 6 | 开机时间戳正常 | ✅ 日志带墙钟时间戳,`net init: OK` 在 +530 ms |
|
||||
| 7 | 无 HardFault / `[ERR]` / 看门狗复位 | ✅ 零 `[ERR]`/HardFault;`reset cause:` 仅 1 次(调试器软复位) |
|
||||
|
||||
**关于 `idle`(改前:轮询消费的固有抖动)**:生产端(TIM6)与消费端(`adcTask` 1 ms 轮询)
|
||||
同为 1000 Hz(TIM6 与 FreeRTOS tick 源 TIM7 参数一致,均 84 MHz/84/1000),但二者无显式同步 →
|
||||
偶发 ±1 帧的相位抖动:每 10 s 约 5 次"两次轮询落在同一生产间隔"(`idle` +5)配 5 次"一次轮询跨两个
|
||||
生产间隔"(`frames` 少 5,即 9995/10000 ≈ 99.95% 取数)。
|
||||
|
||||
✅ **现状(§10 已落地,2026-10-10)**:消费端改由 **TIM6 ISR 置位 `ADC_WAKE_FLAG`** 唤醒。
|
||||
落点与当初设想的"BUSY EXTI 读帧完成时通知"不同——选 TIM6 是为了让唤醒与**采样节拍**严格同源,
|
||||
且不必把内核 API 引入 BUSY EXTI(它与 TIM6 同抢占优先级,见 §5);效果一致:实测 **`idle = 0`**
|
||||
(6 次上板、每次 20000 拍全零)→ 判据 5 由"基本为 0"收紧为**恒 0**。
|
||||
**心跳拍的连带效应**:`adc1_gap` 的根治手段是**缩短心跳**(每 10 s 一次的多行日志),不是提高 DMA 能力。
|
||||
日志成本模型 → AGENTS.md「日志与工具链」。
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续:等周波调频(不在本项范围)
|
||||
## 10. 设计边界与已知代价
|
||||
|
||||
参考系统按 **20 点/周波**采样:电网频率漂移时需让采样率跟随(50.0 Hz → 1000 Hz;
|
||||
49.5 Hz → 990 Hz),以保持傅氏全波窗对齐一个工频周期。
|
||||
|
||||
- 本配置已为此预留:**1 µs 分辨率** + **ARR 预装载使能** → 运行期
|
||||
`__HAL_TIM_SET_AUTORELOAD(&htim6, 周期µs - 1)` 即可无毛刺改频。
|
||||
- 需要配套:① 测频源(过零/DFT 相位);② 把当前采样率同步给保护算法(相量/定值);
|
||||
③ **消费端改事件驱动**(§10)——否则固定周期轮询在非 1 kHz 采样率下会丢帧。
|
||||
✅ **已落地(2026-10-10,§10)**:消费唤醒与 TIM6 同源 → 调频时只改 ARR,消费节拍自动跟随。
|
||||
| 项 | 现状 | 说明 / 出口 |
|
||||
|---|---|---|
|
||||
| **8 路电气量分属两片转换器** | 跨转换器**没有共同采样触发点** → 无同一时刻性、相位不确定 | **V0.1 硬件缺陷**(登记簿 N-18):V0.1 上**禁止**跨转换器做相位 / 功率 / 功率因数 / 矢量运算;根治要改板(8 路全接 TPAFE) |
|
||||
| STM32 侧数据滞后 | 固定 1 拍(1 ms) | 乒乓半区的结构代价;因上一条,不新增误差(§4.4) |
|
||||
| TPAFE 侧仍是"脉冲 + 中断读" | ISR 内 GPIO 脉冲、`BUSY` EXTI 逐通道读 8 次,**未 DMA 化** | 单点成本约 0.5 µs,非当前瓶颈;DMA / FMC 化的**前提未定**:接口类型与 `DRDY`、读时序须先用逻辑分析仪确认 |
|
||||
| 等周波调频 | **硬件侧已具备**:1 µs 分辨率 + ARR 预装载 + 唤醒与生产同源 → 运行期 `__HAL_TIM_SET_AUTORELOAD(&htim3, 周期µs - 1)` 即可无毛刺改频,**消费节拍自动跟随** | 采样率随动到相量窗与定值不在本层;测频见 `App/algo/freq.c`,上位需求见 `docs/算法/继电保护功能复现规划.md` §3.1 |
|
||||
| 通道有效性 | V0.1 只有部分通道测量有效 | 唯一权威 → `docs/硬件平台/ADC通道映射说明.md` |
|
||||
| AboveNormal 的挤占面 | 单拍超过 1 ms 期间抢占 net / ftp / default / timeSync / ui | 观测面 = `adcm` 与 `adc1_gap`;诊断方法与基线 → `docs/子系统设计/看门狗设计说明.md` §11 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 变更记录
|
||||
## 11. 设计铁律(改这条链前必读)
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|---|---|---|
|
||||
| 2026-10-10 | 建立本文档 | B1(ADC 采集链路)遗留项"精确 1 kHz 硬件定时器触发"的规划:方案选型(A/B)、CubeMX 配置规格、代码改造清单、时序与优先级分析、风险与验收判据、等周波调频预留 |
|
||||
| 2026-10-10 | **实现(方案 A)** | CubeMX 加 TIM6(PSC=83 / ARR=999 / ARR 预装载 / NVIC 抢占优先级 6);`Src/main.c` 回调加 TIM6 分支触发 CONVST;`app_main.c` 用 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍;`adc_task.c` 退化为纯消费者。`@build` 0 错 0 警;台架 `@test` 验收待做 |
|
||||
| 2026-10-10 | **台架 `@test` 验收 PASS** | 真机闭环:`seq` 10005→20005→30005(每 10 s +10000 = 1000.0 Hz)、`frst_err=0`、`idle` 每 10 s +5(0.05%,轮询抖动)、回读 `No difference found`、零 `[ERR]`/HardFault、`reset cause` 仅 1 次。证据 `boot_log_data/20261010_prod_adc_tim6_boot_log.txt` |
|
||||
| 2026-10-10 | **事件驱动消费方案 a 规划入档(Task #32 P1-5)** | 记录用户决策与改造清单(TIM6 ISR 置位 `ADC_WAKE_FLAG` 唤醒 `adcTask` + `adcTask` 提至 `osPriorityAboveNormal`),并附下一版硬件 DMA 双缓冲建议(§10.5)。代码待实施,消费端仍为 1 ms 轮询 |
|
||||
| 2026-10-10 | **事件驱动消费方案 a 已实施(Task #32 P1-5)** | 四处改动落地:`Src/main.c` TIM6 分支调 `adc_tick_irq_notify()`;`adc_task.c` 循环尾改 `osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever)` 并删 `ADC_TASK_BEAT_MS`/`next`;`Src/freertos.c` 优先级 `Normal`→**`AboveNormal`**;`App/task/adc_task.h` 加 `ADC_WAKE_FLAG` 与 ISR 入口。实施时补**就绪门**(§10.6,规划未预见:TIM6 比调度器早起 ≈100 ms)。`@build` 0 错 0 警(Code 256368→256728);`@test` PASS:**`idle = 0`/19999 拍**(改前 1~649)、`frames` 1000.0 Hz、`seq−frames = 7` 正是就绪窗口、UI 侧 `uip/uiw/uis` 无退化 |
|
||||
| 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% 不变 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 事件驱动消费改造(Task #32 P1-5,2026-10-10:用户决策方案 a,**已落地**)
|
||||
|
||||
> 上位背景:Task #32 暴露"UI 任务看门狗停摆"候选占用者之一是 `adcTask` 的 1 ms 轮询——
|
||||
> 原 `osDelayUntil(1ms)` 把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**,带来 0~1 ms 随机
|
||||
> 相位抖动 + 累积漂移风险,且 `adcTask` 与 `uiTask` 同为可被长时抢占的对象。用户决策:
|
||||
> **方案 a(ISR 只发唤醒 + 计算仍在任务 + 提优先级)**,并拒绝"ISR 内计算"方案。
|
||||
> **落地状态(2026-10-10)**:四处改动全部实施(清单见 §10.2),`@build` 0 错 0 警、
|
||||
> 台架 `@test` PASS,实测见 §10.3;实施时补了一条规划未预见的**就绪门**(§10.6)。
|
||||
|
||||
### 10.1 决策:为什么选 a,不选"ISR 内计算"
|
||||
|
||||
- **拒绝"ISR 内计算"**:把相量/压环搬进 TIM6 ISR 是反模式——
|
||||
① `adc_input_poll()` 内含 `HAL_ADC_PollForConversion` 的 **2 ms 忙等**(`ADC_INPUT_POLL_TIMEOUT_MS=2U`),
|
||||
绝不可入 ISR(TIM6 抢占优先级 6,自旋期间低优先级中断与全部任务都被压住;单拍最坏 =
|
||||
**一次**超时 ≈2~3 ms——`stm32_scan()` 在首个超时处即 `HAL_ADC_Stop()`+`return`
|
||||
(`App/algo/adc_input.c:79-84`),不会累加成 9 次);
|
||||
② 相量/采样环是任务级共享数据,ISR 内改写需额外临界区,破坏 `tpafe5160_snapshot()` 的
|
||||
"仅任务上下文"铁律;③ `HAL_TIM_PeriodElapsedCallback` 是 CubeMX 生成区回调,承载 L2 计算
|
||||
违反"生成区不改"约定,重生成即丢。
|
||||
- **选定方案 a**:TIM6 ISR **只置位唤醒标志**(`osThreadFlagsSet`),所有计算留在 `adcTask`;
|
||||
并把 `adcTask` 优先级由 `osPriorityNormal` 提到 `osPriorityAboveNormal`,使唤醒后**立即抢占**
|
||||
其余 Normal 任务,固定 µs 级延迟。计算量、数据共享、生成区约定全部不变。
|
||||
|
||||
### 10.2 改造内容(已实施)
|
||||
|
||||
| 文件 | 实际改动 |
|
||||
|---|---|
|
||||
| `Src/main.c` | `HAL_TIM_PeriodElapsedCallback` 的 TIM6 分支(`USER CODE` 区,L2):`tpafe5160_start_conv_irq()` 后加 **`adc_tick_irq_notify();`**——**不是**直接 `osThreadFlagsSet`(理由见 §10.6);并在 `USER CODE BEGIN Includes` 加 `#include "adc_task.h"` |
|
||||
| `App/task/adc_task.c` | 主循环尾 `osDelayUntil(next)` → **`(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);`**;删除 `ADC_TASK_BEAT_MS` 与 `next`(P0-2 的重同步随之不再需要);新增 `adc_tick_irq_notify()` 与就绪门 `s_consumer_ready` |
|
||||
| `Src/freertos.c` | `adcTask_attributes.priority`:`osPriorityNormal` → **`osPriorityAboveNormal`**(=32 > 其余 Normal=24;`configMAX_PRIORITIES=56` 有裕量) |
|
||||
| `App/task/adc_task.h` | 新增 `#define ADC_WAKE_FLAG 0x0001U` 与 `void adc_tick_irq_notify(void);`。**未**把 `extern osThreadId_t adcTaskHandle;` 放进头文件——句柄只在 `adc_task.c` 内引用,`main.c` 不碰任何内核符号(与 `task_wd_tick_probe()` 同构;规划里的这一条按此收敛) |
|
||||
|
||||
- **ISR 安全性(设计校验)**:TIM6 抢占优先级 = 6 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY`(=5) →
|
||||
`osThreadFlagsSet` 在 ISR 内走 `xTaskNotifyFromISR` + `portYIELD_FROM_ISR`(CMSIS-RTOS V2 的
|
||||
`osThreadFlagsSet` 实现),合法且会立即触发上下文切换。`configUSE_OS2_THREAD_FLAGS=1` 已开,符号已编入。
|
||||
- **看门狗不变**:`TASK_WD_ADC` 心跳仍在循环内每拍调用,`required_mask` 与心跳位集合一致,
|
||||
优先级调整不影响掩码。
|
||||
- **验证前置**:实施后须 `@build` 0 错 0 警 + 台架 `@test` 复核 `idle` 是否降至 ≈0(§7.1)。
|
||||
|
||||
### 10.3 收益与语义(含落地实测)
|
||||
|
||||
1. **唤醒延迟固定 µs 级**:原 `osDelayUntil(1ms)` 把唤醒量化到 tick 边界 → 0~1 ms 随机相位抖动 +
|
||||
一旦超拍即"永久落后/空转"风险(即 P0-2 的成因);事件驱动走 TIM6 硬件节拍,唤醒 = 生产时刻 +
|
||||
ISR/调度延迟(亚 µs~数 µs),**无 tick 量化、无累积漂移**。
|
||||
2. **idle 归零(实测已达成)**:原轮询与生产的 ±1 帧相位抖动消失——唤醒与生产同源锁定。
|
||||
3. **粘滞标志的健壮性**:若单拍偶超 1 ms,TIM6 多发脉冲合并为一次唤醒,`adc_input_poll()` 返回
|
||||
`r==0`(无新帧)记 `idle`,**不重复处理同帧**,与 P0 修复(仅 `r==1` 压环)口径一致,无 DFT 窗污染。
|
||||
|
||||
**落地实测(2026-10-10,`@test` PASS,开机 29 s,台架无客户端)**:
|
||||
|
||||
| 指标 | 改前(1 ms 轮询 + Normal) | **改后(事件驱动 + AboveNormal)** | 判读 |
|
||||
|---|---|---|---|
|
||||
| `idle` 无新帧拍数 | 649/20001;P0-1 后仍 **1/20000** | **0 / 19999**(两次心跳全零) | 相位抖动**彻底消失**,唤醒与生产同源 |
|
||||
| `frames` 速率 | 1000.0/s | **1000.0/s**(t=21155→29227 ms 恰 +8072 拍) | 节拍未丢、未重复消费 |
|
||||
| `ring` | == `frames` | == `frames`(19999) | P0-1 口径保持 |
|
||||
| `adcm` 单拍最坏 | ≈2.1 ms | **2.12~2.25 ms** | ⚠ 归因已复核:峰值来自**每 10 s 一次的心跳日志**(`DBG_INFO` + `log_phasors`),不是采集——同批 `adcp` 仅 122 µs,且 `idle=0` 反证没有"每拍 2 ms"(否则粘滞标志会合并唤醒、`idle` 必 >0) |
|
||||
| `uip` / `uiw` / `uis` | 61.3 / 11.4 ms / 1 µs | **61.3 / 11.4 ms / 1 µs** | 提优先级**未**伤到 UI(LVGL 地板不变) |
|
||||
| `defd` 控制台排空 | 46 ms | 46 ms | 未被 AboveNormal 挤动 |
|
||||
| `seq` − `frames` | — | 20006 − 19999 = **7** | 正是"TIM6 已在跑、消费者尚未就绪"的 ≈100 ms 窗口(§10.6),与设计一致 |
|
||||
|
||||
### 10.4 残留风险与后续硬化(落地后仍开放)
|
||||
|
||||
- **Task #32 §4.4 的 2 ms 忙等仍在 L1 层,且如今跑在 AboveNormal 上**:`adc_input_poll()` 内的
|
||||
`HAL_ADC_PollForConversion`(`ADC_INPUT_POLL_TIMEOUT_MS=2U`)是任务级忙等(`HAL_GetTick()`
|
||||
1 ms 粒度、判据 `> Timeout` → 实际 **2~3 ms**)。提优先级后,STM32 ADC 一旦偶发超时,这 2~3 ms
|
||||
会在**高于 net/ftp/default/timeSync** 的优先级上压住它们——比改前更敏感。
|
||||
**6 次上板对照(2026-10-10)**:超时 1 次的那轮 `adcp = 2209 µs`;超时 0 次的 5 轮
|
||||
`adcp = 122 µs`(= 9 通道 × 7.4 µs + TPAFE 快照)→ **2 ms 只在超时拍出现,不是常态开销**。
|
||||
⚠ **超时的第二个代价是正确性,不只是耗时**:超时拍 `s_stm32_valid` 仍是上次成功留下的 1,
|
||||
而 `adc_input_poll()` 的返回值只由 TPAFE 决定 → 该拍照样 `r == 1`、照样压环,STM32 侧几路
|
||||
(UB/UC/IB/IC/IN/温度)**静默沿用上一拍的值**(`adc_input.c:209-212`),`s_frames_skip`
|
||||
只统计"从未成功过"、抓不到它。
|
||||
**硬化排序**:① ✅ **已实施(2026-10-10)**——`stm32_scan()` 两条失败支路(启动失败 / 轮询
|
||||
超时)都清 `s_stm32_valid` → 本拍 STM32 侧判为无有效数据,走既有 `skip` 整帧跳过路径,
|
||||
下一拍成功自动恢复;主机侧新增 **TC-ADCIN-070..078** 钉死并做反向验证(改回旧行为 3 条立即
|
||||
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` 兜底)。
|
||||
实测校准前后同一拍 STM32 侧 6 路**同向**偏移 +10~+63 LSB、TPAFE 侧一字不变 → 此前 ADC 确实
|
||||
带失调在跑。它**不消** 2 ms 忙等,但把"未校准"这条可疑路径从本节的候选根因里划掉。
|
||||
- **启动 fail-safe(设计如此,已按此实现)**:TIM6 未启动 / 停摆 → `adcTask` 永久阻塞于
|
||||
`osThreadFlagsWait(osWaitForever)` → `TASK_WD_ADC` 缺席 → IWDG 复位。属安全 fail-safe,
|
||||
非缺陷(确保"节拍源缺失"不被静默放过)。
|
||||
|
||||
### 10.5 下一版硬件建议:DMA + 双缓冲(方案 b,用户要求记录)
|
||||
|
||||
> **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`;
|
||||
- TPAFE 数据由 **SPI-RX-DMA** 搬运;`DRDY` 下降沿 → DMA **HT/TC 中断** 实现**乒乓双缓冲**;
|
||||
- 任务只在"半满"时做后处理(压环 + 相量),CPU 完全脱离逐点搬运。
|
||||
- **若 TPAFE 为并行接口**:用 **FMC** 把 TPAFE 数据总线映射为异步 SRAM,DMA 从 FMC 读 → 内存;
|
||||
须确认 FSMC/FMC 地址/时序兼容 TPAFE 读时序(开销大于 SPI 方案)。
|
||||
- **统一收益**:CPU 脱离轮询/搬运 → 采样率可上探(如 4 k / 8 k 点/周波)、相量窗更密、腾出
|
||||
AboveNormal 算力给保护算法;与 §8 等周波调频天然契合(ARR 改频 + DMA 自动续传)。
|
||||
- **验收前置**:先以逻辑分析仪确认 TPAFE 接口类型与 `DRDY` 时序,**再**定型下一版硬件 DMA 方案,
|
||||
避免返工。
|
||||
|
||||
### 10.6 实施时补的一条:消费者就绪门(规划未预见,必须保留)
|
||||
|
||||
规划写的是"ISR 里 `osThreadFlagsSet(adcTaskHandle, ...)`"。实施时发现**时序上不能直接这么写**:
|
||||
|
||||
- TIM6 在 `app_main_init()`(`Src/main.c:140`)里就 `HAL_TIM_Base_Start_IT(&htim6)`,而
|
||||
`osKernelInitialize()` / `MX_FREERTOS_Init()` / `osKernelStart()` 在 `:144/145/148`
|
||||
→ **"节拍源已在跑"与"消费者已存在"之间有约 100 ms 窗口**,其间 `adcTaskHandle` 还是 NULL、
|
||||
调度器也还没起。
|
||||
- `osThreadFlagsSet()` 对 NULL 句柄本身安全(`cmsis_os2.c` 的入参检查返回 `osErrorParameter`),
|
||||
但"句柄已赋值、调度器未启动"这段(建任务 → `osKernelStart`)仍会让 ISR 版内核 API 在调度器外跑。
|
||||
本仓库对这类坑有实测记录:`Src/main.c` 的 **TIM7 分支注释**(2026-10-02,port.c 优先级断言
|
||||
把系统卡死在 ISR 内)。
|
||||
- **处置**:ISR 只调 `adc_tick_irq_notify()`,其内含就绪门 `s_consumer_ready`——由 `adcTask`
|
||||
自己在进入等待前置 1(单写者 = 任务,ISR 只读)。把时序约束变成**显式代码**,而不是依赖
|
||||
FreeRTOS 的内部实现细节。
|
||||
- **实测印证**:`seq − frames = 20006 − 19999 = 7`——正是这 ≈100 ms 窗口里 TIM6 发出的 7 个脉冲
|
||||
被丢弃(当时无消费者),之后一帧不差。
|
||||
|
||||
**教训(与 `docs/子系统设计/看门狗设计说明.md` §11.5 的三条"实施时才暴露"同类)**:把"定时轮询"换成
|
||||
"事件驱动"时,必须单独核对**节拍源启动时刻 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. **ISR 只发唤醒,不做计算**。相量、压环、保护判据全部留在任务上下文:忙等与算法不可入 ISR、
|
||||
任务级共享数据不能在 ISR 里改写、`HAL_TIM_PeriodElapsedCallback()` 是生成区回调承载的业务代码。
|
||||
2. **ISR 侧唤醒只能走 `adc_tick_irq_notify()`**(就绪门,§6.1);直接 `osThreadFlagsSet()` 会在内核
|
||||
启动前把调度器外代码跑进 ISR。
|
||||
3. **一个更新事件驱动两片转换器**。另起定时器分别触发 = 重新引入跨转换器的相位不确定性(§10)。
|
||||
4. **取数只用 `adc_input_poll()` / `adc_input_raw()` / `tpafe5160_snapshot()`**,按语义不按下标;
|
||||
不得自行拼"两步取数"的等价物(§5)。
|
||||
5. **改节拍源、改 DMA 流、动 `DMAContinuousRequests` 之前先读 §3.2 与 §4.3**——这几类改动的共同点是
|
||||
**违反时不报任何错**,只在数据上出事(静默丢通道、搬不回来、优先级被重置为 0)。
|
||||
6. **节拍源停摆必须导致复位**(§6.3):不得给等待加超时来"绕过"看门狗。
|
||||
7. **动栈或加深调用链前后各跑一次 `scripts/stack_report.py`**,新增缓冲前跑 `scripts/ram_report.py`(§9)。
|
||||
|
||||
@@ -548,7 +548,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
|
||||
> **来源**:`[ERR][WD] task stall: mask=0x00000067 req=0x00000077`(缺 bit4 = `TASK_WD_UI`)
|
||||
> 的完整归因与处置。故障本身已结案——**累计 38 次上板启动 0 复现**(原 L2 基线 3/10),
|
||||
> 原始分析文档按登记簿"完成即销项"规则删除。本节只留**可复用的判读口径、诊断方法、
|
||||
> 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计说明.md`](ADC采样节拍设计说明.md) §10/§11,
|
||||
> 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计说明.md`](ADC采样节拍设计说明.md) §6 与 §9,
|
||||
> UI 推流门控在 `../UI设计/RemoDispBus协议与接收设计说明.md` §3,就绪门控 API 在
|
||||
> `../网络/BSD_Socket_API_使用指南.md` §10.1。
|
||||
|
||||
@@ -580,7 +580,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
|
||||
|---|---|---|
|
||||
| 1 | **uiTask 是 `required_mask` 里唯一的 `osPriorityBelowNormal`**(其余五个全 `Normal`)。FreeRTOS 数值越大优先级越高 → 任一 Normal 任务持续就绪,uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**公平 | **至今未变**(刻意不提优先级,理由见 §11.6)。风险由"心跳自产 + 往返门控 + 采集零忙等"三面压住 |
|
||||
| 2 | **UI 心跳曾依赖 netTask 往返**:`ui_net_sink_poll()` → `net_accept()` → `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**之后**才 `task_wd_beat(TASK_WD_UI)` → netTask 慢一拍、UI 心跳晚一拍。这正是"**只有 UI 位缺失**"的根因(别的任务没这条依赖) | ✅ 已断两处:心跳提到 poll **之前**(`App/task/ui_task.c`);无客户端时门控让往返 = **0 次**(`uis` 实测 1 µs) |
|
||||
| 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计说明.md` §10) |
|
||||
| 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计说明.md` §6) |
|
||||
|
||||
⚠ 顺带揪出的两个**独立正确性**缺陷(与停摆无关,但同源发现,别只当性能问题):
|
||||
|
||||
@@ -588,7 +588,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
|
||||
等效采样间隔被拉长。当时 `idle` 占 3.2% → 窗长从 20.0 ms 漂到 ≈20.6 ms ≈ **3% 频率失配**
|
||||
→ 幅值/相位**系统性误差**(不是随机噪声)。现仅 `r == 1` 压帧,上板印证 `ring == frames`。
|
||||
- **失败拍静默沿用上一拍值**:失败支路不清 `s_stm32_valid` → 照样压环、`skip` 抓不到。
|
||||
现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §10.4;单测 TC-ADCIN-070..078 钉死)。
|
||||
现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §4.4;单测 TC-ADCIN-070..078 钉死)。
|
||||
|
||||
### 11.4 `[PROF]` / `[PROF2]` 字段语义与当前基线
|
||||
|
||||
|
||||
+2
-1
@@ -329,7 +329,8 @@ flowchart LR
|
||||
| 存储架构(NAND/FTL/FatFs) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
|
||||
| NET 阻塞模型(挂起请求表五路语义) | `docs/网络/Net层阻塞模型架构设计.md` |
|
||||
| 网络自愈(监听重建 / 沉降隔离 / 周期对账) | `docs/网络/网络自愈架构设计.md` |
|
||||
| 采集与算法层 L1~L3 | `App/algo/README.md` |
|
||||
| 采集与算法层 L1~L3(**设计与判据**) | `docs/算法/采集与算法层设计说明.md` |
|
||||
| 采集与算法层目录索引(文件清单 / 构建测试) | `App/algo/README.md` |
|
||||
| ADC 采样节拍(TIM3 1kHz 同源驱动) | `docs/子系统设计/ADC采样节拍设计说明.md` |
|
||||
| IEC 60870-5-104 上行(:2404) | `docs/子系统设计/IEC104上行设计说明.md` |
|
||||
| 墙钟时间维护 | `docs/子系统设计/系统墙钟时间维护说明.md` |
|
||||
|
||||
@@ -133,7 +133,7 @@ STM32(U17)为 LQFP144 脚号。
|
||||
| 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 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);详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10 / §11 |
|
||||
| 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);详见 `docs/子系统设计/ADC采样节拍设计说明.md` §4(DMA 乒乓搬运)与 §6(事件驱动唤醒) |
|
||||
| 前端偏置 | 每路 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` 现只在自检帧通过后才打印 → 判据在本板成立、非空转 |
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
|
||||
| # | 功能点 | 关键参数 | 完成判据 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 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-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` §3.2);台架 `@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、**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`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) |
|
||||
@@ -257,7 +257,7 @@
|
||||
| L2-1 | 全周傅氏系数表 | `sin(2πk/N)·(√2/N)`、`cos(2πk/N)·(√2/N)`,N=20 | 系数表与 `docs/算法/傅氏算法详解.md` §2.3 一致 | ✅ 2026-10-11:`phasor.c` 离线系数表**预乘 √2/N**;主机单测逐项复核(`test/host/phasor/` 系数表用例) |
|
||||
| L2-2 | 基波相量实例 | **5 个实例**(参考项目 4 个,见下方注):`C01-IA-F1` / `C02-UA-F1` / `C03-UN-F1` / `C04-IA-F2` / `C05-IA-F5` | 每实例输出 `{fRe, fIm, fAm, fAmSQ}` | ✅ 2026-10-11:`k_inst[]` 数据驱动 5 实例;台架开机即产出 `phasor(first)` 5 行(`boot_log_data/20261011_prod_phasor_boot_log.txt`) |
|
||||
| L2-3 | 相量有效值输出 | `fAmSQ = √(fRe²+fIm²)` 直接为**有效值**(系数已折算) | 标准源读数 = 设定有效值 | ✅ 2026-10-11(链路级):`fAmSQ` 口径实现 + 单测合成正弦幅值误差 ≤1%;**"标准源读数"判据在无信号源台架不可达**(同 §7.1),待 L1-5 标定后复核 |
|
||||
| L2-4 | 相位输出 | `φ = atan2(fIm, fRe)` | 相位误差 ≤ 目标值 | ✅ 2026-10-11(链路级):`fRe/fIm` 常驻双区、每拍产出,φ 由消费侧 `atan2f` 现算(不常驻结构体,见 `App/algo/README.md` §3.3);单测覆盖相位;**目标值待产品定义 + 标准源复核** |
|
||||
| L2-4 | 相位输出 | `φ = atan2(fIm, fRe)` | 相位误差 ≤ 目标值 | ✅ 2026-10-11(链路级):`fRe/fIm` 常驻双区、每拍产出,φ 由消费侧 `atan2f` 现算(不常驻结构体,见 `docs/算法/采集与算法层设计说明.md` §6);单测覆盖相位;**目标值待产品定义 + 标准源复核** |
|
||||
| L2-5 | 线/负序/零序合成 | 线电压 `Uab=Ua−Ub` 等;负序 `3U2`/`3I2`;零序 `3U0`/`3I0` | 合成相量可复用于保护元件(**V0.1 仅同转换器内可合成**,见 §1.6) | ⬜ **V0.1 不适用**:跨转换器合成被 N-18 禁止;零序走**外部专用通道** `UN`/`IN`(§6.4),无需自产 → 随 V0.2 启用 |
|
||||
| L2-6 | 每 1 ms 滑动窗刷新 | 每来新点,窗滑动一格、20 点重算(暴力法) | 相量每拍更新 | ✅ 2026-10-11:`sample_ring` 40 点环 + `phasor_run()` 每拍整窗重算(仅 `r==1` 新帧压环,P0 修复);台架 `ring` 随 `frames` 同步递增、`idle=0` |
|
||||
| L2-7 | 双区指针原子交换 | 计算区/使用区切换 | 保护元件读使用区,无撕裂 | ✅ 2026-10-11:`s_zone[2][5]` + 单字节 `volatile` 切换(Cortex-M 原子);单测覆盖双区交换;为独立 L3 任务预留无锁读 |
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
# 采集与算法层设计说明(`App/algo/`:L1 → L2 → L3)
|
||||
|
||||
> **模块名称**:自研采集与算法层,代码在 `App/algo/`(七个 `.c`,全部登记进 Keil 工程)。
|
||||
> **文档定位**:**现状设计说明**——本层当前如何组织、每个模块的契约与关键机制、为什么这样设计、
|
||||
> 边界与代价。**目录级索引(文件清单、构建与测试命令、新增模块动作)在 `App/algo/README.md`**,
|
||||
> 本文不重复其内容;本文不写历史演进、门禁复测记录与修订日志(需要时查 git 历史)。
|
||||
> **状态**:L1 → L2 → L3 主线已闭环到板端消费者(`protectTask`,20 ms 判据节拍);
|
||||
> 标定(`docs/算法/继电保护功能复现规划.md` §3.1 L1-5)尚未实现,幅值精度不在本层能力内。
|
||||
> **相关文档**:
|
||||
> 分层与功能项(L1~L5)的唯一属主登记处:`docs/算法/继电保护功能复现规划.md` §2.1(分层)、§3(功能清单)、§4.2(精度)、§7(路线图)
|
||||
> `docs/硬件平台/ADC通道映射说明.md`(**语义映射唯一权威口径**)
|
||||
> `docs/子系统设计/ADC采样节拍设计说明.md`(TIM3 1 kHz 同源节拍、ADC1+DMA 乒乓缓冲、`adcTask` 唤醒)
|
||||
> `docs/算法/傅氏算法详解.md` §2.3(有效值口径怎么来的)
|
||||
> `docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`(主机单测细则)
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 定位与边界](#1-定位与边界)
|
||||
- [2. 一拍的数据流与初始化落点](#2-一拍的数据流与初始化落点)
|
||||
- [3. L1 语义取数 `adc_input.c`](#3-l1-语义取数-adcinputc)
|
||||
- [4. L1 有效性判定 `adc_health.c`](#4-l1-有效性判定-adchealthc)
|
||||
- [5. L2 输入 `sample_ring.c`](#5-l2-输入-sampleringc)
|
||||
- [6. L2 `phasor.c` 相量计算](#6-l2-phasorc-相量计算)
|
||||
- [7. L2 派生量:频率测量 `freq.c`](#7-l2-派生量频率测量-freqc)
|
||||
- [8. L3 输入:保护定值模型 `prot_set.c`](#8-l3-输入保护定值模型-protsetc)
|
||||
- [9. L3 保护元件框架 `prot_elm.c`](#9-l3-保护元件框架-protelmc)
|
||||
- [10. 关键约定与禁令](#10-关键约定与禁令)
|
||||
- [11. 资源足迹(改容量前先看这里)](#11-资源足迹改容量前先看这里)
|
||||
- [12. 验证与判读](#12-验证与判读)
|
||||
- [13. 相关文档](#13-相关文档)
|
||||
|
||||
---
|
||||
|
||||
## 1. 定位与边界
|
||||
|
||||
- **原则:算法与 I/O、与调度分离。** 本层只做两件事——把原始码值变成**语义通道**的"数"、把采样序列
|
||||
变成**相量/频率/判据状态**。不建任务、不等事件、不碰寄存器(唯一例外 `adc_input.c`,它必须驱动
|
||||
ADC1)、不做出口动作。
|
||||
- **这样分离换到什么**:`sample_ring` / `phasor` / `adc_health` / `prot_set` / `prot_elm` /
|
||||
`freq` **零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头),因此可被主机侧
|
||||
单测以 `#include` 原样纳入、离线全覆盖(落实登记簿 **A-4**),不需要真机就能钉死判据。
|
||||
- **上下文约束**:`*_init()` 一律无 RTOS 依赖、幂等,由 `app_main_init()` 在**内核启动前**调用;
|
||||
`adc_input_poll()` / `phasor_run()` / `freq_run()` **仅任务上下文**(快照走临界区)。
|
||||
- **消费者在 `App/task/`**:`adcTask` 采集并压环、`protectTask`(B5)跑 `prot_elm_run()`;
|
||||
新算法一律落在 `App/algo/`,**勿塞进 `App/task/`**(任务只做消费与编排)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 一拍的数据流与初始化落点
|
||||
|
||||
一拍 = TIM3 的一个 1 kHz 更新事件(节拍与消费者唤醒同源,见
|
||||
`docs/子系统设计/ADC采样节拍设计说明.md` §1)。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
TIM3["TIM3 更新事件 1 kHz<br/>ISR: 脉冲 CONVST + 置唤醒标志<br/>同一事件经 TRGO 触发 ADC1"]
|
||||
TIM3 --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
|
||||
TIM3 --> ADC["STM32 ADC1 一轮 9 通道<br/>DMA2 循环模式自动搬运"]
|
||||
ADC --> PP["乒乓双缓冲<br/>半传输 / 整传输回调推进完成计数"]
|
||||
TIM3 -. "唤醒 adcTask" .-> TASK
|
||||
TPAFE --> POLL
|
||||
PP --> POLL
|
||||
TASK["adcTask(事件驱动唤醒,非定时轮询)"] --> POLL["adc_input_poll()<br/>TPAFE 原子快照 + 消费刚完成的半区"]
|
||||
POLL --> RAW["adc_input_raw(ch) × 9<br/>按语义取原始码值"]
|
||||
RAW --> RING["sample_ring_push_frame()<br/>整帧入环"]
|
||||
RING --> RUN["phasor_run()<br/>逐实例重算 + 双区切换"]
|
||||
RUN --> FREQ["freq_run()<br/>共轭乘积相位增量 + 自适应窗均值"]
|
||||
RUN --> GET["phasor_get(idx)"]
|
||||
FREQ --> FGET["freq_get() / freq_valid()"]
|
||||
GET --> CONS["消费者:adcTask 心跳日志"]
|
||||
FGET --> PCONS["消费者:prot_elm_run() 的 OF / UF 判据(B8)"]
|
||||
```
|
||||
|
||||
| 落点 | 做什么 |
|
||||
|---|---|
|
||||
| `app_main_init()`(内核启动前) | `adc_input_init()`(映射自检 + 挂 DMA 缓冲)→ `phasor_init()`(内部连带 `sample_ring_init()`)→ `freq_init()` → `prot_set_init()` → `prot_elm_init()` |
|
||||
| `adcTask`(每拍) | `adc_input_poll()` → `adc_input_raw()` ×9 → `sample_ring_push_frame()` → `phasor_run()` → `freq_run()` → 心跳与健康判定 `adc_health_check()` |
|
||||
| `protectTask`(20 ms) | 取 `phasor_get()` rms 与 `freq_get()` → `prot_elm_run()`;L5-4 采样故障闭锁、L5-1 定值校验和巡检 |
|
||||
|
||||
`prot_elm_init()` 须在 `phasor_init()` **之后**(它按名前缀解析相量实例索引);`prot_set` 独立
|
||||
(只依赖 `stdint.h`),可在任意点初始化。
|
||||
|
||||
---
|
||||
|
||||
## 3. L1 语义取数 `adc_input.c`
|
||||
|
||||
**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn`
|
||||
≠ 板级 `ADCn` ≠ 转换器通道号(17 路里 13 路置换)。消费侧若直接索引驱动缓冲,改版即全线错位。
|
||||
本层把"哪一路"收进一张表 `k_adc_map[]`,上层**按语义取数、不写死下标**。
|
||||
|
||||
| API | 契约 | 违约后果 |
|
||||
|---|---|---|
|
||||
| `adc_input_init()` | 清缓冲与统计 + **映射自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内)+ `HAL_ADC_Start_DMA()` 挂载乒乓缓冲(**只挂一次**);`-1` = 映射指向从不扫描的通道,`-2` = DMA 挂载失败 | 映射错位是**静默**的:该路恒读上一个陈旧值,不报错 |
|
||||
| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ `stm32_take()` 消费**刚完成的那半区**(按完成计数奇偶推半区,零忙等) | **仅任务上下文**(快照是临界区);在 ISR 里调会破坏 `tpafe5160_snapshot()` 的"仅任务上下文"约定 |
|
||||
| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 `0..4095`);`-2` = 该来源尚无有效数据 | 消费侧取数**唯一入口**;须先成功 `poll()` |
|
||||
| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标 / 通道号),供自检注入与 UI 通道序 | 返回静态地址,**勿改** |
|
||||
| `adc_input_stm32_rc()` / `_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | 最近一拍状态码 / DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧(计数一次前进 >1)/ HAL 错误回调 | **四类成因不同,勿混读**:对应心跳字段 `adc1_rc` / `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`(判读见 `docs/子系统设计/ADC采样节拍设计说明.md` §8) |
|
||||
|
||||
**要点**
|
||||
|
||||
- **映射表内容的唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`:`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 个通道**不是同一时刻**(一轮 ≈67 µs);由 TIM3 TRGO 硬件触发 + DMA 自动搬运(
|
||||
`docs/子系统设计/ADC采样节拍设计说明.md` §4)。STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE
|
||||
侧本来就没有同刻性(N-18),无新增误差。
|
||||
- **开机映射自检**挡住"映射到一个从不扫描的通道"这类静默错(启动日志 `ADC input layer init: OK`)。
|
||||
- 本文件是本层**唯一**允许出现 HAL 调用的地方;新增模块若不需要外设,请保持 `sample_ring` /
|
||||
`phasor` 那种零依赖形态,才能免桩单测。
|
||||
|
||||
---
|
||||
## 4. L1 有效性判定 `adc_health.c`
|
||||
|
||||
**解决的问题**:驱动侧与 L1 只有"观测计数"(`tpafe5160_frame_seq()` /
|
||||
`tpafe5160_frstdata_err()`、`adc_input_stm32_*()`),**没有判定**——芯片静默、消费者超拍丢帧、
|
||||
序号倒退这些故障原本只能靠人读心跳日志发现。本层把判定收进纯逻辑,**告警编排留在消费者**
|
||||
(`adc_task.c` 的 `adc_health_report()`)→ 判定可主机免桩单测。
|
||||
|
||||
| API | 契约 |
|
||||
|---|---|
|
||||
| `adc_health_init()` | 清状态;**首拍 `check()` 只建基线、不判定** |
|
||||
| `adc_health_check(&in, &out)` | 吃"计数快照"做一次判定(每拍调,常数次整数运算);差值法 `(int32_t)(now - prev)` → 计数与 tick 回绕都安全 |
|
||||
| `adc_health_flags()` | **电平**故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存)——为 L5-4 闭锁供数 |
|
||||
| `adc_health_sticky()` | **锁存**位:`FRSTDATA` / `STM32_ERR` / `HW_ERR` 的增量痕迹,自愈后仍留痕 |
|
||||
| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分),与 STM32 侧 `adc_input_stm32_gaps()` 对称(心跳字段 `tgap`) |
|
||||
|
||||
**要点**
|
||||
|
||||
- **三类语义严格分开**:**电平**位(stall,可自愈)/ **锁存**位(出现过必留痕)/ **边沿事件**位
|
||||
(仅当拍有效)。`ADC_HE_*` 与同名 `ADC_HF_*` **编号对齐**,消费者直接用事件掩码对"已留痕位图"
|
||||
做"每类每次开机一次 `SYSLOG_SELFTEST`"的编排。
|
||||
- **静默 ≠ 丢帧**:前进量 d = 0 才是静默候选;d > 1 只说明消费者没跟上、生产者还活着——
|
||||
两者处置完全不同,判据不许混。
|
||||
- **gap 不告警**:消费者超拍(心跳拍 ≈2 ms)是已知现象,`adc1_gap` / `tgap` 在 10 s 心跳里可见;
|
||||
若按事件告警会把真故障淹掉。
|
||||
- 告警策略(在 `adc_task.c`,**不在本层**):严重事件与电平上升沿 `DBG_ERROR` +
|
||||
`SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 `selftest.log`);下降沿 `DBG_INFO`
|
||||
("恢复"必须可见,否则只看到一次错误无从判断是否还在)。
|
||||
|
||||
---
|
||||
|
||||
## 5. L2 输入 `sample_ring.c`
|
||||
|
||||
**解决的问题**:相量计算要"以当前时刻为终止的一周波",而 L1 每次只给最新一帧。本层按**语义通道**
|
||||
存最近 `SAMPLE_RING_CYCLES = 2` 个周波(`SAMPLE_RING_DEPTH = 40` 点),与参考系统
|
||||
`CN_BUFSAM_CYCLE = 2` 一致,并为后续加长数据窗(1.5~2 周波抗衰减直流)预留空间。
|
||||
|
||||
| API | 契约 |
|
||||
|---|---|
|
||||
| `sample_ring_push_frame(values, n)` | 整帧压入,长度必须 `= ADC_SEM_COUNT` |
|
||||
| `sample_ring_window(ch, n, out)` | 取该语义通道最近 `n` 点,**按时间序旧 → 新**(`out[n-1]` 最新);`-2` = 点数不足 |
|
||||
| `sample_ring_filled()` | 已累积点数(**饱和**于 40)——"够不够一周波"的判据 |
|
||||
| `sample_ring_frames()` | 累计入环帧数(**不饱和**,1 kHz 下约 49.7 天回绕)——与 `tpafe5160_frame_seq()` 对照即判丢帧 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **整帧写入后才统一推进写指针** → 单帧内不可能"半新半旧",消费侧取到的窗永远落在整帧边界。
|
||||
- 本层**只做按时序存 / 取,不做任何跨通道运算**:同刻性由上游 L1 负责,本层既不保证也不假设。
|
||||
判定也不在本层(本层只出计数)。
|
||||
- 某通道本拍取数失败 → **整拍跳过入环**(现 `adc_task.c` 的 `s_frames_skip` 即此),
|
||||
**切勿补 0 点**——单个 0 点会显著污染 20 点 DFT 窗。
|
||||
|
||||
---
|
||||
|
||||
## 6. L2 `phasor.c` 相量计算
|
||||
|
||||
**算法**:全周傅氏(单频 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)` | 读**使用区**结果;`-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`(五次谐波选线)。**新增实例只在表尾加一行**,遍历 / 日志 / 单测自动适配。
|
||||
- **双区(计算区 / 使用区)**:结果先写非使用区,全部实例写完后单字节 `volatile` store 切换
|
||||
`s_use_zone`(Cortex-M 上原子)→ 读者无锁、无撕裂。写者是 `adcTask`(同拍),**读者跨两个
|
||||
上下文**:`adcTask` 心跳与 `protectTask` 判据(`prot_elm_run()` → `phasor_get()`)——无锁读
|
||||
的收益正在被使用,**勿改成加锁、也勿取消双区**。
|
||||
- **任一实例数据不足一周波 → 本拍整体不产出**(不切换使用区):宁可整体不刷新,也不给出
|
||||
"部分新部分旧"的混合结果。
|
||||
- **每拍 20 点全部重算**(暴力法)。不用"加尾项去头项"的增量式:增量递推会累积误差,
|
||||
暴力法每次都是干净的一周波。代价 = 每拍 20 × 实例数次乘加,相量刷新周期 1 ms。
|
||||
- **相位(L2-4)不常驻结构体**:需要时自行 `atan2f(fIm, fRe)`(`φ` 约定 = 窗首初相,见 §7)。
|
||||
|
||||
---
|
||||
## 7. L2 派生量:频率测量 `freq.c`
|
||||
|
||||
**解决的问题**:过频 / 低频(`docs/算法/继电保护功能复现规划.md` §3.3 L3-5)需要系统频率读数,
|
||||
而 L2 只出相量。本层把"相邻两拍基波相量的相位差"变成"频率",并解决**非整周波窗长的 DFT 泄漏纹波**
|
||||
——这是决定精度的关键问题。
|
||||
|
||||
| API | 契约 |
|
||||
|---|---|
|
||||
| `freq_init()` | 清增量环 / 计数 / 读数 / 相位基准 / 有效标志;幂等,内核前可调 |
|
||||
| `freq_run()` | 每拍步进:选源(UA → IA 过幅值门)→ 共轭乘积 `P = Z(m)·conj(Z(m-1))` 取角 → 假值钳制 → 自适应窗均值。**仅 `adcTask` 上下文,须在 `phasor_run()` 之后同拍调** |
|
||||
| `freq_get(out)` | `0` 有读数(可能是保持值)/ `-1` 参数错 / `-2` 从未收敛(写 0) |
|
||||
| `freq_valid()` | "本拍新鲜":幅值门 OK 且已收敛。**保护消费口径 = `freq_get() == 0 && freq_valid() == 1`** |
|
||||
| `freq_last()` | 最近有效读数;从未收敛返回 `0.0` |
|
||||
|
||||
### 7.1 为什么是"相位微分 + 自适应窗"
|
||||
|
||||
- **免解缠绕**:`arg(Z_now · conj(Z_prev))` 天然落在 `(-π, π]`,无需维护累积相位。
|
||||
本层相量约定 `φ = atan2(fIm, fRe)` = 窗首初相,窗每拍前滑一点 → 纯音下每拍角**增大**
|
||||
`2πf/Fs`;45~55 Hz 段 |Δφ| ≈ 0.28~0.35 rad,距 ±π 裕度巨大。
|
||||
- **定长窗为什么不够(自适应窗的理由)**:20 点矩形窗在非整周波时混入**负序泄漏项**
|
||||
(45 / 55 Hz 处 |S₂/S₁| ≈ 3~5 %),相量端点轨迹成椭圆 → 逐拍相位差带上周期 `δ(φ)`
|
||||
纹波,纹波周期 = `fs/(2f)` ≈ 9.1~11.1 拍。**定长平均不整除纹波周期**,残差留在均值里:
|
||||
扫频最坏 0.317 Hz(40 拍算术平均)、0.197 Hz(60 拍复数和的 Kay 法——交叉项的和角不随
|
||||
`K²` 衰减,**勿再照这条路优化**)。改 `K_eff = round(8·Fs / (2·f̂))`(45~55 Hz 段 73~89 拍)
|
||||
使窗长恰为纹波周期的整数倍 → 纹波相消,扫频最坏降到 **0.0101 Hz**。
|
||||
- **假值钳制带取 35~65 Hz,而不是系统合理带**:单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会
|
||||
**非对称丢弃纹波半周** → 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 Hz 宽于系统合理带
|
||||
40.5~60.5 Hz(参考 `CN_HERTZ_SOFTFR_MIN/MAX`,见
|
||||
`docs/算法/继电保护功能复现规划.md` §10.1),仍能拦住建"窗滑 2 拍"造成的 ≈2f(90~110 Hz)
|
||||
与相位跳变假值;系统合理带由 OF / UF 的**定值范围**在消费侧把关,不在这里重复把关。
|
||||
- **幅值门** `FREQ_AMP_MIN = 50.0`(原始码值口径,**拟定值,待 L1-5 标定复核**):低于门视为
|
||||
无有效波形(浮空 / PT 断线时 rms ≈ 0,相位无意义)→ 停止更新、`valid` 清零、读数保持,
|
||||
台架浮空因此**不发散**;信号刚消失的过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。
|
||||
- **源优先级 UA(C02) → IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流也能测频);
|
||||
**源切换即废相位基准重建**——UA 与 IA 之间没有共同相位基准(N-18),跨源相位差无意义。
|
||||
|
||||
### 7.2 精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径)
|
||||
|
||||
| 判据 | 条件 | 门槛 | 当前实测最坏 |
|
||||
|---|---|---|---|
|
||||
| J1 扫频 | 纯音 45.00~55.00 Hz,0.5 Hz 步进(21 点) | **≤ 0.02 Hz** | **0.0101 Hz** @55.0 Hz |
|
||||
| J2 噪声 | 幅值 1000 码、噪声 rms = 1 % 幅值 | **≤ 0.10 Hz** | 0.0160 Hz |
|
||||
| J3 启动收敛 | 首个有效相量 → 首个有效读数 | **≤ 40 ms** | 20 ms |
|
||||
| J4 频率阶跃 | 50 → 51 Hz 进入 ±0.05 Hz 并保持 | **≤ 120 ms** | 98 ms(稳态误差 0.0053 Hz) |
|
||||
| J5 零幅 / 浮空 | rms < 幅值门 | `valid = 0`、不发散、保持漂移 ≤ 0.3 Hz | 漂移 0.13 Hz,无 NaN |
|
||||
| J6 假值钳制 | 幅值 + 频率双跳变拍 | 60 ms 内回到 ±0.05 Hz | 0.0000 Hz |
|
||||
|
||||
附加鲁棒性(同一套件):直流偏置 8000 码 → 0.0018 Hz;3 / 5 次谐波 20 % / 10 % → 0.0065 Hz;
|
||||
UA 无幅值落 IA → 0.0020 Hz。
|
||||
|
||||
**J4 的 120 ms 是"精度优先于响应"的显式取舍**:自适应窗(M = 8 ≈ 4 个泄漏纹波周期 ≈ 4 个信号
|
||||
周波)换来 0.0101 Hz 量级的精度,代价是频率骤变后的窗冲刷(实测 98 ms)。OF / UF 定时限默认
|
||||
**1000 ms** → 测频响应占保护总动作时间 **< 10 %**,暂态假值另有 J6 兜底。**若将来要压响应时间,
|
||||
必须同时重估精度**:缩短窗就是把精度退回 0.2~0.3 Hz 档,不是免费的。
|
||||
|
||||
---
|
||||
|
||||
## 8. L3 输入:保护定值模型 `prot_set.c`
|
||||
|
||||
**解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + 常量表做校验和自检;本层做
|
||||
**RAM 模型 + 持久化纯逻辑**(blob 序列化 / 解析,文件胶住在 `App/cfg/prot_set_store.c`),
|
||||
把**编号 / 通讯地址 / 范围 / 默认值 / 校验和**闭环钉死。
|
||||
|
||||
| API | 契约 | 为什么 |
|
||||
|---|---|---|
|
||||
| `prot_set_init()` | 装默认值 + 封存校验和;幂等 | 内核前可调 |
|
||||
| `prot_set_load_raw(vals)` | 整表装载,逐项范围校验 | **all-or-nothing**:任一项越界则**整表不装载**,`prot_set_bad_mask()` 指认越界项;影子区 + 单字交换 → 读侧永不见中间态 |
|
||||
| `prot_set_set(id, q10)` | 单项写入(范围校验,批处理路径) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写" |
|
||||
| `prot_set_set_sealed(id, q10)` | **原子写 + 封存**:影子区改好重封后单字交换 | 自检永不见中间态(收口 B5 发现的竞态窗口);置脏标志 |
|
||||
| `prot_set_dirty_peek()` / `_take()` / `_mark()` | 脏标志三件套(落盘握手) | peek = 通知 iec104Task;take = 属主落盘前消费;mark = 写失败回置重试 |
|
||||
| `prot_set_store_serialize()` / `_parse()` | 持久化 blob:`PSET` + 版本 + 值区 + sum32 正 / 反码对 | parse **整表原子**:magic / 版本 / 长度 / 校验对 / 范围任一失败 = **一字不动** |
|
||||
| `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 |
|
||||
| `prot_set_selfcheck()` | 重算 sum32 与封存正 / 反码比对,失配 → 闭锁(L5-1) | seqlock 式复核:提交时撞见改写 = 本轮跳过而**不误锁**;`prot_set_seal()` 是解锁的**唯一**合法路径 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **Q10 定点**(×1024)与参考系统 `2^10` 折算口径一致;元数据表(地址 / 范围 / 默认)是 ROM
|
||||
常量,**校验和只覆盖 RAM 值区**。
|
||||
- **通讯地址逐项对照参考** `Comm/Pub/Const_CommSetPara.c` 的 `g_tCommSetParaTab`(基址
|
||||
`0x8820`);单测 TC-PROTSET-006 钉死"地址不重复、`min ≤ def ≤ max`、投退项 0..1"。
|
||||
- ⚠ **默认值与范围是拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订——进度注见
|
||||
`docs/算法/继电保护功能复现规划.md` §5.4。
|
||||
- **OF / UF 八项尾追加**(B8,32 → 40 项):`PS_OF_EN / OF_HZ / OF_DLY / UF_EN / UF_HZ / UF_DLY
|
||||
/ RET_OF / RET_UF`——**编号不回收,枚举序 = 数组序 = 校验和覆盖序**,故只许尾部追加。
|
||||
这八项的逐项通讯地址不在本仓库(参考表只示"含低压 / 低频"类目)→ 全部按 **本地项
|
||||
`comm_addr = 0`** 登记(104 C_SE 不可寻址),待对照补。
|
||||
- **频率返回系数独立**:`RET_OF = 0.99` / `RET_UF = 1.01`(返回门槛 = 定值 × 系数)——
|
||||
频率滞回带与电气量的 0.95 / 1.05 不同量级,**勿共用一个返回系数**。
|
||||
- **换代语义**(B7 → B8):项数变 → blob 144 → 176 B、`PROT_SET_BLOB_VER` 1 → 2。旧档(ver = 1 /
|
||||
32 项)在新固件 parse 时被**版本 / 长度闸**拒绝 → 回落出厂默认,`prot_set_store_load()`
|
||||
**返回 2 = 换代**(打 INFO,**不是** CORRUPT、不记自检),`-1` 才是真损坏。
|
||||
升级路径**不得**报成损坏(判读细节见 §12.3)。
|
||||
- **bad_mask 随项数扩为 `uint64_t`**(bit i = 第 i 项越界);消费方目前只有主机单测,
|
||||
板端闭锁走 `prot_set_locked()`。
|
||||
|
||||
---
|
||||
|
||||
## 9. L3 保护元件框架 `prot_elm.c`
|
||||
|
||||
**解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段公共核收敛成
|
||||
**一个通用核 + 数据驱动实例表**(与 `phasor` 的 `k_inst` 同一模式):新增元件 = 表加一行 +
|
||||
定值加几项。
|
||||
|
||||
| API | 契约 |
|
||||
|---|---|
|
||||
| `prot_elm_init()` | 复位状态 + 按名前缀(`C01/C02/C03`)解析相量实例索引;须在 `phasor_init()` 之后;`src = FREQ` 行无相量绑定(`pidx = 0xFF`) |
|
||||
| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机;主机单测入口;`NULL` 测量按 0 |
|
||||
| `prot_elm_run(now_ms)` | 适配层:按实例表 `src` 组测量(`0` 取 `phasor_get()` rms,`1` 取 `freq_get()`)再 eval |
|
||||
| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图,供 L4 出口与心跳日志 |
|
||||
| `prot_elm_request_reset()` | 跨上下文复归入口(104 YK11 落请求,`eval` 头部消费) |
|
||||
|
||||
**要点**
|
||||
|
||||
- **滞回**:已启动用**返回**门槛(定值 × 返回系数),未启动用**动作**门槛;过量 / 欠量方向由实例表
|
||||
`over` 位统一处理(`UV` / `UF` 是欠量实例)。
|
||||
- **定时限**用差值法 `(uint32_t)(int32_t)(now - t0) >= dly` → tick 回绕安全;`dly = 0` =
|
||||
启动同拍即动作。**返回是即时的**(参考系统带 10 ms 返回延时,接出口硬件时再补)。
|
||||
- **闭锁联动**:`prot_set_locked()`(L5-1)或该元件投退 = 0 → 状态整体复位、不再判据。
|
||||
- **数据缺失时的两种处理刻意不同**(这是本层最容易改错的一处):
|
||||
- 相量类**任一未就绪 → 整拍跳过**(不给判据喂陈旧值);
|
||||
- 频率类**无有效读数 → 按方向填"必不启动"值**(过量填 `0`、欠量填 `1e30` → 滞回比较恒假 →
|
||||
元件复位),**不整拍跳过**——频率源缺失不得饿死电流 / 电压类元件。
|
||||
- **V0.1 实例表**(受 N-18 约束):`OC1 / OC2 / OC3 / OL`(IA)、`OV / UV`(UA)、`3U0`
|
||||
(UN 外部通道)、`OF / UF`(`freq` 测频,B8)。**零序过流(IN 走 STM32)故意不在表中**
|
||||
——跨转换器不构成同刻矢量。
|
||||
- **OF / UF 的出口可见性**:两元件**未单独映射 104 遥信点**(送检点表口径见
|
||||
`docs/算法/继电保护功能复现规划.md` §7),动作目前**只经 YX15 保护总聚合可见**
|
||||
(`iec104_pts` 的 fault-total 掩码含 OC / 3U0;OF / UF 置总位但无独立分点)。
|
||||
**文档与代码都必须按此口径,勿写"独立遥信"。**
|
||||
- **板端接线**:`App/task/protect_task.c`(protectTask,20 ms)消费 `prot_elm_run()` +
|
||||
L5-4 采样故障闭锁 + L5-1 校验和巡检;判据结果经 104 遥信上送(`iec104_pts` YX15~23 / 31)
|
||||
→ `docs/算法/继电保护功能复现规划.md` §7。实例数变化会同时改变启动锚点 `elm=` 数字与
|
||||
`prot_elm_active_mask()` 位宽(B8:7 → 9 实例),串口锚点表若含该数字须同步核对。
|
||||
|
||||
---
|
||||
## 10. 关键约定与禁令
|
||||
|
||||
1. **语义映射只有一处权威**:`docs/硬件平台/ADC通道映射说明.md`。取数一律经 `adc_input_raw()`,
|
||||
**禁止**在消费侧直接索引驱动缓冲或写死下标。
|
||||
2. **禁止跨转换器运算**(V0.1 硬件缺陷 **N-18**):`UA/UN/IA` 在 TPAFE、`UB/UC/IB/IC/IN` 在
|
||||
STM32 ADC1,两片转换器没有共同采样触发 → 无同一时刻性、相位不确定。故本层**不做**跨转换器的
|
||||
相位 / 功率 / 功率因数 / 负序零序类矢量运算;**8 路全接 TPAFE(V0.2 硬件)后方可解禁**。
|
||||
3. **标定未实现**(缺标准源,见 `docs/算法/继电保护功能复现规划.md` §3.1 L1-5):现以**原始码值**
|
||||
充当工程量(系数 = 1)→ 相量读数只证明**链路通**,**不可作幅值精度判据**。
|
||||
4. **本层无副作用外溢**:不建任务、不等事件、不写日志(日志由调用侧打)、不碰寄存器
|
||||
(例外只有 `adc_input.c` 驱动 ADC1)。
|
||||
5. **上下文纪律**:内核启动前只允许调 `*_init()`;`adc_input_poll()` / `phasor_run()` /
|
||||
`freq_run()` / `prot_elm_run()` 只在各自任务里调。
|
||||
6. **表只许尾部追加**:`k_inst[]`(相量实例)、`prot_elm` 实例表、定值元数据表——
|
||||
枚举序 = 数组序 = 校验和覆盖序,插入或重排等于改校验和与 104 点表。
|
||||
|
||||
---
|
||||
|
||||
## 11. 资源足迹(改容量前先看这里)
|
||||
|
||||
| 位置 | 量 | 说明 |
|
||||
|---|---|---|
|
||||
| `sample_ring.c` `s_ring[9][40]`(`float`) | **1440 B** | 周波数 / 通道数一改就成倍变化 |
|
||||
| `phasor.c` `s_zone[2][5]`(`phasor_t`) | 160 B | 随实例数线性 |
|
||||
| `freq.c` 相位增量环 `fq_d[96]` + 状态 | **≈ 520 B** | 随 `FREQ_AVG_TAPS` 变化 |
|
||||
| `prot_set.c` `s_val[2][40]` + 封存 / `uint64_t` 位图 / 闭锁 | 336 B | 项数 32 → 40(B8)后的量 |
|
||||
| `prot_elm.c` `s_rt[9]`(16 B / 实例) | 144 B | 实例 7 → 9(B8)后的量 |
|
||||
| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B | — |
|
||||
| `adc_input.c` `s_stm32_dma[2×9]`(`uint16_t`) | 36 B | 必须在 `RW_IRAM1`(DMA2 不可达 CCM)→ `docs/子系统设计/ADC采样节拍设计说明.md` §4.3 |
|
||||
| `adc_health.c` 内部状态 | ≈ 40 B | — |
|
||||
| 运行期栈 `phasor_run()` 的 `win[20]` | 80 B / 帧 | 抬高 `PHASOR_N` 直接抬栈 |
|
||||
| 运行期栈 `freq_run()` 局部 | ≈ 30 B / 帧 | 无大局部数组 |
|
||||
|
||||
**改 `SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT` / `FREQ_AVG_TAPS` / 栈之前后各跑一次**
|
||||
`scripts/stack_report.py`,新增缓冲跑 `scripts/ram_report.py`(口径见 `mem-budget` skill);
|
||||
adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分析与优化方案.md` §2.3,本表不代记台账。
|
||||
|
||||
---
|
||||
|
||||
## 12. 验证与判读
|
||||
|
||||
### 12.1 主机侧单测(生产源文件以 `#include` 原样纳入,不改产品代码)
|
||||
|
||||
| 模块 | 套件钉死的东西 | 细则 |
|
||||
|---|---|---|
|
||||
| `phasor` / `sample_ring` | 系数表逐项复核、幅值与相位口径、谐波抑制与提取、频偏、采样环时序、双区交换 | `docs/测试说明/相量层单元测试说明.md` |
|
||||
| `adc_input` | 逐 Rank 注入互异码值钉死映射、HAL 启动失败与"本拍无新帧"分支、乒乓半区推导、gap / stall 分开计数 | `docs/测试说明/ADC输入层单元测试说明.md` |
|
||||
| `adc_health` | 100 ms 静默门限两侧、丢帧边沿、序号倒退锁存、错误增量、tick 回绕、`NULL` 守卫、`init` 幂等 | `test/host/adc_health/` |
|
||||
| `prot_set` | 范围校验、all-or-nothing 装载、封存-自检-闭锁、Q10 往返、元数据表自检、blob 往返与损坏拒绝、换代拒绝、`uint64_t` bad_mask | `test/host/prot_set/` |
|
||||
| `prot_elm` | 滞回与定时限、欠量方向、投退、闭锁联动、时基回绕、位图聚合、测频接线与"必不启动值" | `test/host/prot_elm/` |
|
||||
| `freq` | J1~J6(§7.2)+ 直流与谐波抑制 + UA→IA 源回退 + 守卫与幂等 | `test/host/freq/` |
|
||||
|
||||
命令与对应 pre-commit 钩子名见 `App/algo/README.md` §4(钩子清单的属主是
|
||||
`.pre-commit-config.yaml`,用例清单的属主是各 `test/host/<模块>/` 与上表细则文档——本文不复制数字,
|
||||
免得三处打架)。
|
||||
|
||||
### 12.2 板端闭环
|
||||
|
||||
`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍与心跳:`phasor(...)` 行、`freq(...)` 行
|
||||
(`f=5000 valid=1`,`×0.01` 定点,MicroLIB 无 `%f`)、`ring` / `skip` / `adc1_*` / `tgap`
|
||||
计数;健康判据 = **零 `[ERR]`**(`adc_health_report()` 的告警全部以 `[ERR]` 开头 → 误报会被闭环
|
||||
直接抓住)。浮空工况应为 `valid = 0` 且读数为保持值。
|
||||
|
||||
### 12.3 定值档"换代"与"损坏"是两件事(易误判)
|
||||
|
||||
- `prot_set_store_load()` **返回 2 = 换代**(旧固件 / 旧项数的档被版本闸拒绝)→ 只打 INFO
|
||||
`prot set blob version change, defaults in use`,**不记自检**;
|
||||
- **返回负值 = 真损坏** → `[ERR] prot set blob CORRUPT` + 自检留痕。
|
||||
|
||||
升级固件后首启看到 "version change" 一行属**预期**,不要当故障排查;反过来,把真损坏读成
|
||||
换代等于放过 NAND 数据事故——分流依据是返回码,不是日志文本。
|
||||
|
||||
---
|
||||
|
||||
## 13. 相关文档
|
||||
|
||||
- 目录索引与构建 / 测试入口:`App/algo/README.md`
|
||||
- 分层与功能项清单(L1~L5 的属主登记处):`docs/算法/继电保护功能复现规划.md`
|
||||
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
|
||||
- 采样节拍与 DMA 搬运:`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md` §2.3
|
||||
- 主机单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`
|
||||
- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md` §2.3
|
||||
@@ -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(
|
||||
|
||||
@@ -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 返回值(注入挂载失败)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* ../../../App/algo/phasor.c
|
||||
* ../../../App/algo/freq.c
|
||||
*
|
||||
* 精度判据(与 App/algo/README.md §3.7 同口径,规划 §4.2 测频范围的落实):
|
||||
* 精度判据(与 docs/算法/采集与算法层设计说明.md §7.2 同口径,测频范围见规划 §4.2):
|
||||
* J1 纯音扫频 45.00~55.00 Hz(0.5 Hz 步进):|f_meas - f_true| <= 0.02 Hz
|
||||
* (实测最坏 0.0101 Hz @55.0 Hz,判据留 2 倍裕度)
|
||||
* J2 噪声工况(幅值 1000 码、噪声 rms = 1% 幅值):|f_meas - f_true| <= 0.10 Hz
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
做什么:用本机 MSVC 离线编译 test/host/freq/freq_test.c(#include 纳入生产
|
||||
源文件 App/algo/freq.c + phasor.c + sample_ring.c),验证相位解缠绕
|
||||
测频的精度判据 J1~J6(45~55 Hz 扫频 / 噪声 / 收敛 / 阶跃 / 浮空 /
|
||||
假值钳制 / 源回退),判据数字见 freq_test.c 文件头与 App/algo/README.md。
|
||||
假值钳制 / 源回退),判据数字见 freq_test.c 文件头与 docs/算法/采集与算法层设计说明.md §7.2。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/freq/run_unit_test.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))
|
||||
|
||||
Reference in New Issue
Block a user