把控制逻辑编译成一张数据表,执行时间就能在编译期算出来吗?

能。DCL 的编译产物是固定 24592 字节的路由表(1024 条 × 16 字节),引擎顺序扫描原语、没有分支跳转。43 条路由的执行时间恒为 4.86–4.99 μs:把周期从 100 μs 压到 4.5 μs,这个数不变。

能,前提是让「程序」变成「数据」。DCL 把控制逻辑编译成一张固定 24592 字节的路由表,MCU 顺序扫描原语而不做分支跳转,于是最坏情况执行时间可以静态累加。实测 43 条路由的 ISR 执行时间为 4.86–4.99 μs,把周期从 100 μs 压到 4.5 μs 时,这个数几乎不动。

现象

同一个控制程序,用 C 写和用「表」写,能给出的性能承诺完全不同:

写法 能承诺什么 承诺不了的
C + RTOS 任务 平均执行时间 最坏情况:分支预测失败约 12 周期惩罚、cache miss 未知
C + 裸机中断 典型执行时间 某次采样走到罕见分支时的耗时上界
路由表顺序扫描 最坏情况 = 路由数 × 单条耗时 只受解释开销限制的绝对速度

原因不复杂:只要程序里有数据相关的控制流,执行时间就是输入的函数。而工业控制需要的恰恰是「不随输入变化的时间」。

原因

三件事让执行时间可以脱离输入。

1. 没有数据相关的控制流。 路由表里每条记录的读取、分派、写入路径固定;唯一的动态分支是 35 个 opcode 的分派,而它的分支范围是常量,每种原语的耗时可实测、可列表。所以总耗时只与路由条数有关。

2. 拓扑排序把执行顺序挪到编译期。 源码里语句的书写顺序与数据依赖顺序常常不一致:

PID    heater  FROM temp_f  SP=60 KP=2.0
FILTER temp_f  FROM temp    LOWPASS a=0.1
SENSOR temp    FROM ADC1_CH0

人把 PID 写在最前(它最重要),但数据流要求 ADC → FILTER → PID。若按源码顺序扫描,PID 读到的是上一周期的 temp_f——行为错了,但不报错。

编译器用 Kahn 算法按信号依赖排序,保证「读某根线」的路由一定排在「写这根线」之后;若存在环,队列会提前耗尽,直接报编译错误,而不是留一个不可复现的运行期 bug。

3. 所有信号统一 float32。 没有 INT/REAL/BOOL 的类型分派,没有运行时类型转换。Cortex-M7 带硬件 FPU(fpv5-d16,-mfloat-abi=hard),浮点运算本身是一两个周期量级,因此「统一 float32」几乎不付性能代价,却消掉了运行时最贵的两张分支表。

怎么做

写源码:声明数据关系,不写流程。

SENSOR  temp      FROM ADC1_CH0
FILTER  temp_f    FROM temp        LOWPASS a=0.1
PID     heater    FROM temp_f      SP=60 KP=2.0 KI=0.1 KD=0.05 LIMIT 0 100
ALARM   overheat  FROM temp_f > 90
OUTPUT  heat_pwm  TO TIM1_CH1      FROM heater

编译:一条语句可能展开成多条路由。

PID heater FROM temp_f SP=60 KP=2.0 KI=0.1 KD=0.05 LIMIT 0 100
  → WIRE[1] → OP_PID   → WIRE[2]    参数[0] = {kp, ki, kd, sp}
  → WIRE[2] → OP_CLAMP → WIRE[3]    参数[1] = {lo=0, hi=100}
                                     状态[0] = 积分项 + 上次误差

LIMIT 0 100 在语言里是一个修饰词,在引擎里是一条独立的 CLAMP 原语。用户看到「一个带限幅的 PID」,引擎执行「PID + CLAMP 两条路由」。语言层 28 个功能块,固件层 35 个 opcode,差额来自这种展开。

产物:固定大小、自包含、带校验。

[Header 12B: magic/version/n_routes/n_params/n_states]
[ROUTE_TABLE  n × 16B]
[PARAM_TABLE  n × 16B]   每项 4 个 float32
[STATE_TABLE  n × 16B]
[CRC32 4B]               覆盖整表
总计 = 12 + 16384 + 8192 + 4 = 24592 B(固定)

固定大小与 CRC32 各有明确用途:部署协议不必处理变长消息,程序区布局静态;校验失败则不启动,保留上一版继续跑——现场「部署到一半断线」因此变成安全失败。

验证

43 条路由的同一份程序,在不同周期下测 ISR 执行时间:

周期设定 ISR 执行时间 CPU 占用
100 μs 5.0 μs 5.0%
10 μs 4.87–4.95 μs 49.5%
5.00 μs 4.86–4.99 μs 99.9%
4.50 μs 4.87–4.99 μs 110.8%(过载)

执行时间在不同周期下稳定在 4.86–4.99 μs,抖动 60 ns。这条数据说明「周期」与「工作量」已经解耦:改周期不影响耗时,改路由数才影响。

改前改后的对照:

维度 改前:C 顺序程序 改后:DCL 路由表
执行顺序 程序员写死 编译器拓扑排序
执行时间上界 需要实测或静态分析工具 编译期累加推导
改一个参数 重新编译 + 烧录 重新部署约 1 s
循环依赖 运行期表现为读到旧值 编译期报错

边界

  • 解释执行有固定开销,控制频率上限约 200 kHz;要跑 1 μs 周期必须把路由表 JIT 成 Thumb-2 机器码写进 ITCM,代价是失去在线替换能力(该分支仅 8 条路由验证通过)。
  • 不支持复杂控制流:分支逻辑要用 SR/RS、TIMER、COUNTER 等状态原语组合,写法比 if/else 繁琐,且循环依赖会被判为编译错误。
  • 「改逻辑不重烧」只覆盖原语组合,不覆盖能力扩展:新增一种运算必须改固件并重新烧录。
  • 语言层 28 个功能块与固件层 35 个 opcode 并非一一对应,跨层对照需查映射表,不能想当然。
  • 固定容量:1024 路由 / 512 参数 / 256 状态 / 1024 信号线,超限需改固件。

最后验证于 2026-10-28 · DCL 编译器 v2.0 · STM32H723ZG(RM0468)· Python 3.10

把控制逻辑编译成一张数据表,执行时间就能在编译期算出来吗?