把控制逻辑编译成一张数据表,执行时间就能在编译期算出来吗?
能。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