当你的MCU“中风”时,它还能继续干活吗?——聊聊UART双机热备在工控现场的救命之道
从一条产线停机说起
上周三下午三点十七分,某食品包装厂的一条灌装线突然停摆。
没有报警,没有提示,PLC输出全无。排查两小时才发现:主控MCU因电源波动复位了。更糟的是,重启后状态丢失,整批产品的计数清零,最终导致整批次返工。
这不是孤例。在工业现场,我们常听到这样的对话:
“这设备太‘脆’了,电网闪一下就死。”
“每次维护都得停机半小时,客户不答应啊。”
“程序明明跑得好好的,怎么突然就不发指令了?”
问题的核心,从来不是代码写得够不够优雅,而是——系统能不能在故障发生时,假装什么都没发生?
这就引出了今天要聊的主角:用最朴素的UART,搭出高可用的双机热备系统。
你没看错。不是EtherCAT,不是PROFINET,也不是带冗余环网的高端PLC。就是那个每个STM32、每个51单片机都有的——UART。
为什么是UART?因为它“土”,但管用
它不快,但它稳
UART有多老?比很多工程师的工龄都长。
但它至今活跃在PLC、变频器、温控仪、HMI之间的通信链路上,靠的是三个字:简单、可靠、可控。
- 两根线就能通(TX/RX),连时钟都不用共享;
- 所有MCU原生支持,不需要外挂协议芯片;
- 中断+DMA模式下CPU占用极低,适合跑实时控制;
- 数据帧结构透明,逻辑分析仪一抓一个准。
更重要的是:它足够“轻”,轻到可以复制两套而不心疼资源。
这正是双机热备的前提——你要有两台能随时顶上的机器,还得让它们保持步调一致。而UART,恰好是实现这种“镜像同步”的最佳载体之一。
双机热备的本质:不是备份,是“影子替身”
很多人以为“热备”就是“等主挂了我再上”。错了。
真正的热备,是备机一直在运行,只是不出声。
想象一下:你操作一台设备,旁边站着一个和你穿一样衣服、戴一样手套的“你”。他不做动作,但你每动一下,他就默默记下来。一旦你突然倒下,他立刻接住你手里的工具,继续干——旁人甚至察觉不到换人了。
这就是热备的真相。
在我们的系统里:
-主机负责对外通信、执行控制逻辑;
-备机不驱动任何输出,但通过一条独立的UART链路,持续接收主机的状态快照;
- 两者之间还跑着心跳包,像脉搏一样告诉对方:“我还活着”。
一旦心跳停止超过200ms,备机立刻判断:“他不行了”,然后:
1. 关闭原主机的输出使能(防止双输出冲突);
2. 激活本地控制逻辑;
3. 接管通信端口,向上位机发送“我已接管”;
4. 继续生产流程,仿佛刚才那场故障从未发生。
整个过程,控制延迟小于100ms,电机不会停转,阀门不会误动作。
怎么让两个MCU“心有灵犀”?关键在这几个设计点
1. 同步链路:用UART传“状态快照”
我们用一路专用UART连接两台MCU,专门用来同步状态。别小看这根线,它是系统的“神经反射弧”。
typedef struct { uint32_t timestamp; // 时间戳 uint16_t output_bitmap; // 输出状态映射 float process_values[4]; // 关键工艺参数 uint8_t control_flags; // 控制标志位(如手动/自动模式) } SystemState_t;主机每50ms广播一次这个结构体。备机会在收到后立即更新自己的local_state,就像一面镜子。
但要注意:不能裸传!必须加CRC校验:
uint16_t crc = crc16((uint8_t*)&state, sizeof(SystemState_t)); SendToBackup(&state, sizeof(SystemState_t)); SendToBackup(&crc, 2);否则一个干扰比特导致状态错乱,可能引发误切换——这比宕机还危险。
2. 心跳机制:谁先开口谁是主?
系统上电时,如何确定谁当主机?我们采用“优先级+抢占”策略:
- 每台MCU内置唯一ID(比如序列号),ID小的默认为主;
- 上电后,主机尝试发送心跳;
- 备机如果在1秒内没收到心跳,且自己是高优先级,则发起抢占;
- 抢占成功后,拉低一个“主控使能”信号,激活输出驱动。
if ((GetTickCount() - last_heartbeat) > 200) { if (MyPriorityIsHigher()) { EnterPrimaryMode(); } }这里有个隐藏陷阱:脑裂(Split-Brain)。即两台都觉得自己是主机,同时驱动输出,轻则逻辑混乱,重则烧毁负载。
解决办法有两个:
-硬件互锁:使用OR-ing二极管电路或继电器互斥,确保只有一路输出有效;
-仲裁信号:引入第三方看门狗模块,或通过GPIO交叉检测对方状态。
3. 故障检测:不只是“收不到心跳”
你以为心跳超时就是故障?不一定。
可能是:
- 主机正在处理高优先级中断,延迟了发送;
- 瞬时干扰导致一包数据丢了;
- UART缓冲区溢出,但系统仍在运行。
所以我们不能“一丢包就切换”,那样会频繁抖动。
正确做法是:多维度综合判断
| 检测项 | 说明 |
|---|---|
| 连续N次未收到心跳 | 基础条件,N通常设为3~5 |
| 同步数据CRC错误率 > 阈值 | 可能线路受扰,提前预警 |
| 主机未响应查询命令 | 上位机轮询失败,辅助判断 |
| 本地看门狗触发 | 若主机WDT复位,可快速感知 |
只有多个条件同时满足,才启动切换,避免“惊弓之鸟”式误动作。
实战代码:如何用HAL库实现热备核心逻辑
下面这段代码,跑在备机上,是整个热备系统的“哨兵”。
#define HEARTBEAT_TIMEOUT 200 // ms #define SYNC_UPDATE_INTERVAL 50 // ms volatile uint32_t last_heartbeat_ms = 0; SystemState_t current_state; bool is_primary = false; // 收到心跳时调用(可通过中断触发) void OnHeartbeatReceived(void) { last_heartbeat_ms = GetTickCount(); } // UART接收完成回调(同步数据到达) void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &hSyncUart) { if (ParseAndValidateSyncPacket(rx_buffer)) { UpdateLocalState(parsed_state); OnHeartbeatReceived(); // 更新心跳时间 } // 重新启动接收 HAL_UART_Receive_IT(&hSyncUart, rx_buffer, PACKET_SIZE); } } // 主循环监控 void BackupMonitorTask(void) { uint32_t now = GetTickCount(); if (!is_primary) { // 备机模式:检查是否需要接管 if ((now - last_heartbeat_ms) > HEARTBEAT_TIMEOUT) { if (CanTakeover()) { // 检查优先级、输出安全等 PerformFailover(); } } else { // 正常同步,可做自检 SelfCheck(); } } Delay_ms(10); // 避免空转 }重点解读:
-OnHeartbeatReceived()是切换决策的起点;
-HAL_UART_RxCpltCallback中不仅要解析数据,还要验证CRC;
-PerformFailover()必须包含输出使能切换和事件日志记录;
- 所有函数都要考虑可重入性,避免中断嵌套出问题。
工程落地:这些坑,我替你踩过了
✅ 波特率匹配要“严丝合缝”
主备间同步UART的波特率必须高度一致。
建议:
- 使用同一型号晶振;
- 或启用MCU的自动波特率检测功能(部分STM32支持);
- 不要用内部RC振荡器,温漂太大。
实测案例:两台MCU分别用8MHz外部晶振和HSI,波特率偏差0.8%,结果每分钟丢包一次。
✅ 数据同步要“聪明”
别一股脑把所有变量都发。我们曾试过每帧发200字节状态,结果UART阻塞,切换延迟飙升到400ms。
优化方案:
-只发变化量:用差分编码,比如“output_bitmap从0x0A变为0x0E”;
-分级同步:高频发关键状态(如输出、模式),低频发历史数据;
-压缩结构体:用位域代替布尔数组,节省带宽。
✅ 输出隔离是生死线
最怕什么?双机同时输出!
解决方案:
-硬件层面:用光耦+继电器实现“输出使能”互斥;
-软件层面:定义全局互斥标志,切换时先发“禁用原主机”命令;
-电源设计:双机最好独立供电,避免共电源导致连锁崩溃。
✅ 日志一定要留痕
每次切换,必须记录:
- 时间戳;
- 切换原因(心跳超时/CRC错误/WDT复位);
- 切换前后的状态快照。
这些日志在事后分析中价值巨大。有一次客户投诉“设备莫名其妙重启”,我们调出日志发现其实是电网瞬断导致主MCU掉电,而备机无缝接管——反而证明了系统可靠。
它不适合所有场景,但在这些地方真香
✔ 水处理控制系统
水泵群控,不允许停机排水。采用UART热备后,即使主控板更换,系统仍持续运行。
✔ 电梯门控系统
门机控制器必须“永不死机”。双MCU+UART同步,实现毫秒级切换,乘客毫无感知。
✔ 包装机械中的伺服同步
虽然运动控制走CAN,但启停逻辑、急停信号仍由双机热备的UART模块管理,作为最后的安全屏障。
写在最后:老技术的新生命
有人说,UART都2024年了,还讲这个?
但我想说:技术没有新旧,只有是否被用对地方。
在追求极致性价比的工控边缘设备中,你未必需要一个万元级的冗余PLC。
两块STM32 + 两路UART + 一段精心设计的切换逻辑,就能构建出99.99%可用性的控制系统。
这不仅是成本的胜利,更是工程智慧的体现:在有限资源下,用最可靠的手段,解决最关键的问题。
下次当你面对“系统必须永不宕机”的需求时,不妨想想:
能不能给你的MCU,配个“影子替身”?
如果你正在做类似的项目,欢迎在评论区交流实战经验。我们可以一起探讨:如何把心跳做得更精准?如何防止单点失效?甚至,如何用这个思路扩展到三机冗余?
毕竟,工业控制的世界,容不得半点侥幸。