怎么用一张路由表在 STM32H723 上做到 100 μs 确定性控制?
把控制逻辑编译成 24592 字节的路由表,由 TIM1 强制 100.000 μs 周期顺序扫描。43 条路由的 ISR 执行 4.86–4.99 μs,最小稳定周期 5.00 μs(200 kHz);改逻辑秒级部署,不用重烧固件。
做法是让 MCU 不执行程序,而是按表执行原语:DCL 源码在 PC 上编译成一张 24592 字节的路由表,通过 SWD 或 UART 写进 DTCM,由 TIM1 以 100.000 μs 周期触发 ISR 顺序扫描。43 条路由的实测 ISR 执行时间为 4.86–4.99 μs,最小稳定周期 5.00 μs(200 kHz),而执行时间与周期设定无关。
现象
传统扫描式 PLC 与 RTOS 方案有一个共同点:周期是工作量的函数。
| 场景 | 观察到的事实 |
|---|---|
| 控制程序加 5 条逻辑 | 扫描周期从 1.2 ms 变成 1.6 ms |
| 某个采样值走到从未执行的分支 | 单次扫描多花几百纳秒,周期抖动变大 |
| 用 RTOS 任务做控制 | 周期取决于调度器与任务优先级,最坏情况难以静态论证 |
这两类方案都能跑,但「这一拍到底什么时候开始」无法在编译期给出确定答案。
原因
周期能变成常量,靠的是三件事同时成立。
1. 时间锚点在硬件里,不在软件里。 周期由 TIM1 的 ARR 决定,中断由硬件事件触发。软件只能提前完成,不能推迟周期。推论:CPU 快 10 倍,周期不会变短,只是余量变大。
2. 执行时间只取决于路由条数。 引擎没有 if/for、没有函数调用链、没有类型分派、没有堆分配。每条路由是「读源 → 一次 opcode 分派 → 写目标」,耗时固定。于是最坏情况执行时间(WCET)可以静态推导:
WCET = Σ(路由数 × 该原语的固定耗时)
3. 数据全部在零等待内存。 路由表、信号总线、参数、状态都放在 DTCM(128 KB,0x20000000),不存在 cache miss 导致的偶发多花几百纳秒。
三者合起来,把「这套系统运行时稳不稳」从一个统计问题变成了一个可以写进文档的算术问题。
怎么做
数据结构:一条路由 16 字节。
typedef struct __attribute__((packed, aligned(4))) {
uint8_t src_type; /* 0=SENSOR 1=WIRE 2=CONST */
uint8_t src_index;
uint8_t dst_type; /* 3=WIRE */
uint8_t dst_channel;
uint8_t op; /* 原语操作码 */
uint8_t flags; /* bit0 = enabled */
uint16_t param_idx; /* 参数表索引 */
uint16_t state_offset; /* 状态表索引(PID/LPF 等) */
uint16_t actuator_idx; /* 0=仅WIRE 1-4=PWM 32-63=GPIOE */
uint16_t wire2_idx; /* 第二输入 */
} RouteEntry_t;
执行引擎:每个周期扫一遍表。
void TIM1_UP_IRQHandler(void) {
for (uint32_t i = 0; i < n_routes; i++) {
if (!(rt[i].flags & 1U)) continue;
float src = read_source(&rt[i]); /* DTCM 单周期访存 */
float out = execute_primitive(rt[i].op, src,
rt[i].param_idx, rt[i].state_offset);
WIRE_MAP[rt[i].dst_channel] = out;
if (rt[i].actuator_idx) ACTUATOR_STATUS[rt[i].actuator_idx] = out;
}
}
内存布局:引擎区与程序区物理分离。
| 区域 | DTCM 偏移 | 内容 | 谁改 |
|---|---|---|---|
| 引擎区 | 0x0000–0x16FF | TIMING、SENSOR_MAP、ACTUATOR、WIRE_MAP、LUT | 烧录时确定 |
| 程序区 | 0x1700–0x87FF | ROUTE_TABLE 1024×16B、PARAM_TABLE 512×16B、STATE_TABLE 256×16B | 每次部署覆盖 |
| 诊断区 | 0x8800–0xDFFF | RTT 控制块、环形日志、告警缓冲 | 运行时写入 |
引擎烧一次;之后换控制逻辑只覆盖程序区,部署动作不触碰引擎代码,风险被结构性隔离。
部署:三种方式。
# 1) 只烧引擎,一次性
cd firmware/h723-core0 && build.bat
py -3 -m pyocd flash -t stm32h723xx build/core0_h723.elf
py -3 -m pyocd reset -t stm32h723xx
# 2) 编译控制程序 → 固定 24592 字节路由表二进制
cd ../../ide/compiler
python dcl_compiler.py reactor_control.dcl -o reactor_control.bin
# 3) 部署 + 启动 + 监控(CLI)
cd .. && python shell/main.py --cli
# :e reactor_control.dcl → :c → :d → :start → :m
无调试器时走 UART:USART2(PD5/PD6)115200 8N1,帧格式 [0xC0][CMD][LEN:2B LE][PAYLOAD][CRC16:2B LE],CRC-16/CCITT(poly=0x1021)。
验证
把同一个 43 路由程序逐步压缩周期,观察 ISR 执行时间:
| 周期 | ARR | ISR 执行时间 | CPU 占用 | 状态 |
|---|---|---|---|---|
| 100.0 μs | 13600 | 5.0 μs | 5.0% | 正常 |
| 20.0 μs | 2720 | 4.9 μs | 24.7% | 正常 |
| 10.0 μs | 1359 | 4.87–4.95 μs | 49.5% | 正常 |
| 5.00 μs | 679 | 4.86–4.99 μs | 99.9% | 临界 |
| 4.50 μs | 611 | 4.87–4.99 μs | 110.8% | 过载 |
注意第三列:周期从 100 μs 压到 4.5 μs,执行时间几乎不变(执行时间抖动仅 60 ns)。由此得到两个可复用的结论:
- 「最小稳定周期」不是试出来的经验值,而是执行时间上界加几十纳秒。
- WCET 可以静态累加得到,不需要跑遍所有输入组合。
改前改后的对照:
| 维度 | 改前:扫描式程序 | 改后:路由表引擎 |
|---|---|---|
| 周期 | 随代码量变化(1–10 ms) | 固定 100.000 μs |
| 执行时间 | 运行时才知道 | 编译期可累加推导 |
| 改参数 | 重新编译 + 烧录约 60 s | 部署约 1 s,不重烧 |
| 输出时刻 | 扫描末尾,随负载浮动 | 硬件事件点,与 CPU 无关 |
边界
- 控制频率上限约 200 kHz(43 路由实测 5.00 μs)。解释执行有固定开销,1 μs 周期需要把路由表 JIT 成机器码写进 ITCM,且只跑通 8 条路由(研究分支,非生产配置)。
- 单个周期内能执行的路由数受 WCET 约束:100 μs 周期下 43 条路由只用 5%,但压到 5 μs 就接近满载,没有余量可用。
- 不适配 SIL3:单核 Cortex-M7 无 Lockstep、无 ECC 保护的关键路径、无硬件 Fault 收集。
- 通信侧目前可靠的是 UART(已验证)、SWD、CANopen(FDCAN1 500 kbps);以太网与 EtherCAT 未实现。
- 路由表容量固定:1024 路由 / 512 参数 / 256 状态 / 1024 信号线,超出需改固件而非改程序。
最后验证于 2026-10-28 · STM32H723ZG(RM0468)· arm-none-eabi-gcc · pyOCD · Python 3.10