MCU做到FPGA的实力?来看看这个开源项目怎么做的

5 阅读9分钟

MCU 输出抖动实测 <10ns:我让 MCU 不「跑程序」,而是跑一张路由表

仓库:JimmyZ-zengmin/dcl-controller(MIT,刚发 v0.1.1) 一句话介绍:在通用 STM32H723(Cortex-M7)上,用一张路由表代替一段程序来驱动 GPIO/PWM——100μs 硬件定时周期,输出链路抖动实测 < 10 ns(DMA 搬运 + ITCM 零等待),输出时刻由硬件锁存、与 CPU 计算解耦。


先讲一个让每个玩 MCU 的人都睡不着的问题

你有没有在示波器上看过自己写的控制程序?

「我明明用定时器触发了中断,在中断里把 GPIO 置高置低,为什么示波器上每一帧的边沿还是歪歪扭扭、前后晃几十纳秒甚至几百纳秒?」

不是你代码写得烂。这是传统做法的结构性宿命

在「定时器中断 → 中断里算完直接写 IO」这条经典路径里,「CPU 算完的那一刻」就是「输出生效的那一刻」。而 CPU 算出结果的那一刻本身就会抖:中断什么时候真正进、总线那一刻有没有被别的设备抢走、cache 有没有 miss 一下……这些不确定性会原封不动地传导到你的输出引脚上。

我在 ESP32 上实测过:输出抖动峰峰值 462 ns,恰好等于 ISR 入口的抖动。CPU 入口抖多少,引脚就跟着抖多少。你超频、你优化、你把中断优先级拉满,都只是在减小这个数,永远消不掉——因为抖动源就在链路正中间。

想跳过铺垫直接看代码?👉 github.com/JimmyZ-zeng…


核心洞察:别让 CPU 负责输出时刻

把刚才那句话翻过来想:

如果我们不让 CPU 去决定输出时刻,而是让 CPU 只负责「把值算好放在那里」,再由一块不受 CPU 影响的硬件在固定时刻把值「锁存」到引脚上——那 CPU 那点抖动,还重要吗?

不重要了。输出时刻已经不在 CPU 手里。CPU 早算完一帧、晚算完一帧,只要它在锁存时刻之前交卷,引脚翻不翻、何时翻,完全由硬件决定。

根除抖动的本质,是「解耦计算与输出」,而不是「把计算也变确定」。

这句话把问题从「让 CPU 跑得绝对准时」(几乎不可能)降维成了「让一块定时器在固定时刻搬一个数」(硬件天生擅长)。


在 STM32H723 上怎么落地

思路清楚了,剩下的就是把「解耦」焊死在芯片上:

ISR 里(会抖,但无关紧要):
   计算 → 把结果写进 SHADOW_GPIO(DTCM 里的影子缓冲,无 timing 要求)

独立硬件链路(绝对准时):
   TIM1 在周期末尾(CC4≈97.5μs)产生匹配事件
        → DMAMUX 把它路由到 DMA2 Stream5
        → DMA 硬件把 SHADOW_GPIO 搬到 GPIOE_ODR
        → 引脚在同一刻翻转

从 SHADOW 到引脚这一步,CPU 完全不在场。它是定时器事件触发 DMA、DMA 直接写 GPIO 输出数据寄存器,全程 4 个 AHB 周期,没有任何软件参与、没有任何分支。这条 DMA 输出链路 + ITCM 零等待执行的抖动,实测 < 10 ns

PWM 输出走同一条哲学:比较值写在预装载影子寄存器里,由更新事件在固定时刻统一装载——所以 PWM 边沿同样不抖。

这个「解耦」要成立,靠三根支柱

  1. 硬件定时锚点:周期不是 RTOS 调度「尽量准」,而是 TIM1 硬件定时器强制 100μs。时间锚点不在软件里,在硅里。
  2. DTCM 零等待内存:路由表、传感器值、信号线全放 128KB DTCM(零等待),不存在 cache miss 的偶发卡顿,还天然避开了多核缓存一致性那类噩梦。
  3. 路由表无动态分支:引擎跑的不是「程序」,是一张静态路由表。每条路由 = 「源 → 原语 → 目标」,顺序扫描、没有 if、没有函数调用。执行时间在编译期就能预算,不会突然冒出一个 M7 分支预测失败(约 12 周期惩罚)把整帧节奏打乱。

所以,这个仓库到底是什么?

一句话:MCU 不「跑程序」,而是跑一张路由表

概念传统做法DCL 做法
控制逻辑C 程序 + RTOS 任务DCL 声明式源码 → 路由表二进制
调度RTOS 调度器「尽量公平」TIM1 硬件定时器强制 100μs
输出中断里算完直接写 IOCPU 写 SHADOW → DMA 硬件锁存
执行时间运行时才知道编译期可预算(WCET)

固件烧一次,之后改逻辑不用重新编译烧录:用 DCL 语言写控制程序,编译器编译成路由表二进制,通过 SWD 或 UART 下发到 DTCM 运行区,下一个周期就生效。

# 编译你的控制程序
cd ide/compiler
python dcl_compiler.py reactor_control.dcl -o reactor_control.bin

# 部署 + 启动(免重烧)
cd ide
python shell/main.py --cli
> :e reactor_control.dcl   # 加载
> :c                       # 编译
> :d                       # 部署(SWD 直写 DTCM)
> :start                   # 启动引擎
> :m                       # 监控 RTT

支持 35 个工业控制原语(PID / LPF / CMP / 定时器 / 计数器 / 查找表 / 布尔逻辑……),容量:1024 路由 / 512 参数 / 256 状态。通信有 USART2 115200 部署协议(已验证)、FDCAN CANopen 500kbps(已实现)。


数据的"时间戳":每个周期看到的,是上一个周期末尾的数据

这可能是 DCL 最容易被忽略、但控制工程师会秒懂的一条:引擎读到的每一个输入,都不是"实时随机到达"的,而是由 DMA 在周期开始前锁定好的、上一个周期末尾的完整快照。

周期 N 的时间线:
  [周期 N-1 末尾]  DMA 采样并锁定全部输入(ADC / 传感器 / 外部信号)
  [周期 N 计算]    所有路由读同一份锁定数据 —— 周期内零撕裂
  [周期 N 末尾]    TIM1_CC4 → DMA 把 SHADOW 搬到 GPIO,输出生效

这套语义换来两个确定性控制里最值钱的性质:

  1. 周期内一致性:同一周期里所有路由读到的都是同一份数据——不存在"算到一半,输入被改写"的数据撕裂。传统中断程序里那种"读取读到一半值变了"的玄学 bug,在这里从根上不存在。
  2. 数据时效精确可描述:周期 N 的输出,基于的是周期 N-1 末尾的输入快照——一拍延迟,语义和 PLC 扫描、运动控制器的"当前值 / 上一拍"完全一致。控制工程师拿到手就能建模,不用猜"这个数据到底是哪个时刻的"。

仓库文档里用"火车模型"一句话说完了这个设计:DMA 采样是起点站,ISR 计算是车内处理,DMA 输出是终到站——车上的人(CPU)不用管几点到站,到点就被甩出去。


实测数据(bench 级,只列测过的)

指标数值依据
ISR 周期100.000 μs(实测均值 99.998 μs)docs/JITTER-MEASUREMENT.md
输出链路抖动(DMA 搬运 + ITCM 零等待)< 10 ns(实测)docs/00-vision/FUTURE-ROADMAP.md L2 ✅
最小稳定周期5.00 μs(200 kHz,实测压测)docs/DCL_ISR极限压测报告.md
极限验证1 μs 跑通(8 路由零抖动,解释器模式)README §已知局限
ISR 执行时间4.86–4.99 μs(43 路由)docs/DCL_ISR极限压测报告.md
CPU480 MHz(VOS0 + PLL,H723 规格内)firmware/h723-core0/Src/main.c

诚实对标(循环周期级抖动,全部为公开实测/规格数据):

设备周期抖动
DCL(H723)100 μs< 10 ns(实测)
西门子 S7-12001 ms~25 ns
Beckhoff BX510050 μs~5 ns
FPGA (Zynq)1 μs~0.5 ns(器件规格)

实测 10 ns 已经和 Beckhoff(5 ns)同一量级、明显优于西门子 S7-1200(25 ns);比 FPGA 差一个量级,那是 MCU 时钟树的物理天花板。这个差距我们在 README 里讲得很老实,不吹。

🔬 关于「<0.5 ns」的一个澄清:架构上,DMA 锁存让输出边沿的理论下限可以到 <0.5 ns(74LVC574 锁存器同款原理)。但这个数目前是理论推断,还没有示波器实测记录,所以本文不拿它当指标。只写测过的数,是仓库的一贯风格。

⚠️ 再澄清一个测量方法坑:早期报告里的「~37 ns 周期抖动」测的是 ISR 入口时刻(且 TIM1 与 DWT 跨时钟域有量化台阶),反映的是 CPU 侧波动,不是输出抖动。它不科学,已在 README 里纠正为「入口采样参考值」;真正的输出链路抖动实测是 <10 ns。这个教训本身也值得读。


为什么值得你点开仓库

  1. 思想大于代码:仓库里有三篇「核心思想」叙事文档,把零抖动确定性低周期三个问题从直觉讲到落地——不是贴代码,是讲清楚「为什么这么设计」。第一次点进来的人,读一篇就能 get 到牛在哪。
  2. 诚实得罕见:有一份 DCL闭环自查报告.md 自曝家丑(历史致命问题清单),README 明确写「不是工业 PLC 替代品」「抖动比 FPGA 差一个量级」「以太网还没打通」,连「<0.5ns 只是理论值、没实测」都写进文档。做研究展示的,敢这么自审的不多。
  3. 工具链完整:DCL 语言 → 编译器 → CLI/GUI IDE → SWD/UART 部署 → RTT 非侵入监控,一条链全开源,QUICKSTART.md 承诺 15 分钟跑通。
  4. 研究分支不藏:1μs 极限验证版(解释器模式实测跑通)、以太网研究、伺服电流环架构都开源放在那,包括 JIT 编译卡住的 UNDEFINSTR 问题——研究透明度拉满。

快速上手(15 分钟版)

git clone git@github.com:JimmyZ-zengmin/dcl-controller.git
cd dcl-controller

# 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. 编译 DCL 程序
cd ../../ide/compiler
python dcl_compiler.py reactor_control.dcl -o reactor_control.bin

# 3. 部署 + 启动 + 监控
cd ../ && python shell/main.py --cli

需要:STM32H723ZG 开发板 + ST-Link(或 CH340 UART)+ Python 3.10+。详细指引见仓库内 docs/QUICKSTART.md


结语

这个项目不是要替代 PLC,也不是要挑战 FPGA。它想验证的是:在通用 MCU 上,「用架构消除不确定性」这条路到底能走多远。

答案暂时是:100μs 周期、输出链路抖动实测 <10 ns、编译期可预算的执行时间——一个「确定性控制引擎」的原型,全开源,MIT。

如果你也在做嵌入式控制、实时系统、或者单纯好奇「定时器 + DMA 能玩出什么花」,欢迎来仓库逛逛,提 issue、加星、或者直接 fork 去玩你的控制场景。

📦 github.com/JimmyZ-zeng…