怎么用一张路由表在 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

怎么用一张路由表在 STM32H723 上做到 100 μs 确定性控制?