双MCU热备架构:主从MCU心跳检测、故障切换机制、共享存储器设计
各位工程师朋友,咱们接着聊。上一章我讲了单MCU方案的那些坑,这一章咱们重点说说双MCU热备架构。说实话,在制氧机这种生命支持设备上,我个人的习惯是——能上双MCU就别省。为什么?因为一旦主控挂了,病人可能就面临缺氧风险。这个责任,咱们扛不起。
一、为什么是双MCU热备?
先说说热备的概念。热备,说白了就是两台MCU同时在线,一台干活,一台待命。主MCU正常运行时,从MCU就在旁边盯着,随时准备接班。
我在项目中遇到过一种情况:单MCU方案里,电源纹波突然增大,导致MCU复位。虽然看门狗能拉回来,但复位那几百毫秒里,氧浓度已经掉下去了。双MCU热备就能解决这个问题——主MCU挂了,从MCU无缝接管,病人几乎感觉不到切换。
这里有个关键点:热备不是冷备。冷备是从MCU断电休眠,等主MCU挂了再上电启动,那切换时间至少几百毫秒,甚至几秒。热备是从MCU一直通电运行,只是不输出控制信号,切换时间可以做到微秒级。
核心原则:热备架构的目标是让故障切换对系统功能的影响降到最低。在制氧机中,这个"最低"意味着氧浓度波动不超过±1%,流量波动不超过±0.5L/min。
二、主从MCU心跳检测机制
心跳检测,是双MCU热备的"神经"。没有心跳,从MCU就不知道主MCU是死是活。
2.1 心跳信号怎么设计?
我建议用独立的GPIO引脚来传输心跳信号,别复用通信总线。为什么?因为如果I2C或SPI总线本身出了问题,心跳信号也跟着丢了,从MCU会误判主MCU故障,造成不必要的切换。
心跳信号的形式,我一般用方波。主MCU每隔10ms翻转一次GPIO电平,从MCU用外部中断捕获。如果从MCU在50ms内没检测到电平变化,就认为主MCU挂了。
// 主MCU心跳生成代码(伪代码)
void heartbeat_task(void)
{
while(1)
{
HEARTBEAT_PIN ^= 1; // 翻转GPIO电平
delay_ms(10); // 10ms间隔
}
}
// 从MCU心跳检测代码(伪代码)
void heartbeat_isr(void)
{
last_heartbeat_time = get_sys_tick();
heartbeat_lost_count = 0;
}
void heartbeat_monitor_task(void)
{
uint32_t now = get_sys_tick();
if((now - last_heartbeat_time) > 50) // 50ms超时
{
heartbeat_lost_count++;
if(heartbeat_lost_count >= 3) // 连续3次超时
{
take_over_system(); // 接管系统
}
}
}
经验之谈:我曾经遇到过一个问题——主MCU的心跳任务被高优先级中断阻塞了,导致心跳信号短暂丢失。从MCU检测到超时,直接切换了。后来我加了"连续丢失N次才切换"的机制,误切的问题就解决了。
2.2 心跳超时时间怎么定?
这个时间不能太短,也不能太长。太短了容易误触发,太长了切换不及时。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 心跳周期 | 10ms | 主MCU翻转GPIO的间隔 |
| 单次超时 | 50ms | 从MCU检测不到心跳的容忍时间 |
| 连续丢失次数 | 3次 | 触发切换的阈值 |
| 总切换延迟 | ≤150ms | 从故障发生到切换完成 |
三、故障切换机制
切换机制是热备架构的核心。你想想看,如果切换过程中氧浓度掉下去了,那热备还有什么意义?
3.1 切换条件
不是所有异常都要切换。我一般把故障分为三级:
- 一级故障(立即切换):MCU死机、看门狗超时、心跳丢失。这种故障不切换,系统就瘫了。
- 二级故障(延迟切换):传感器异常、通信错误。先尝试恢复,恢复不了再切换。
- 三级故障(不切换):轻微数据校验错误、偶发中断。记录日志就行,不用切换。
注意:千万别把切换条件设得太敏感。我见过一个项目,因为ADC采样偶尔超限就触发切换,结果一天切换了十几次,比不切换还糟糕。
3.2 切换过程
切换过程要平滑。从MCU接管时,不能直接"抢"控制权,得先确认主MCU确实挂了。
- 确认故障:从MCU连续3次检测到心跳丢失,进入切换准备状态。
- 接管控制:从MCU将输出使能引脚拉高,同时将主MCU的输出使能引脚拉低。
- 状态同步:从MCU从共享存储器读取最后一次保存的系统状态,包括氧浓度设定值、流量设定值、运行模式等。
- 恢复输出:从MCU按照读取的状态恢复控制输出,完成切换。
// 切换逻辑伪代码
void take_over_system(void)
{
// 1. 确认主MCU故障
if(!is_primary_alive())
{
// 2. 接管控制权
PRIMARY_ENABLE_PIN = 0; // 禁用主MCU输出
BACKUP_ENABLE_PIN = 1; // 启用从MCU输出
// 3. 读取共享状态
system_state_t state = read_shared_memory();
// 4. 恢复控制
set_oxygen_concentration(state.o2_concentration);
set_flow_rate(state.flow_rate);
set_operation_mode(state.mode);
// 5. 记录切换事件
log_event("Primary failed, backup took over");
}
}
3.3 切换后的处理
切换完成后,从MCU变成新的主MCU。原来的主MCU如果恢复了,不能直接抢回控制权,得先做自检,然后以从MCU身份重新加入系统。
我习惯的做法是:切换后,原主MCU进入"冷静期",运行自检程序。自检通过后,向新主MCU发送"就绪"信号。新主MCU根据系统负载决定是否切回去,或者等到下次维护时手动切换。
四、共享存储器设计
共享存储器是双MCU通信的"黑匣子"。两个MCU通过它交换状态信息,确保切换时数据不丢失。
4.1 存储器选型
我推荐用双端口RAM或者共享SRAM。别用EEPROM或Flash,写入速度太慢,而且有擦写寿命限制。
| 存储器类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 双端口RAM | 两个MCU可同时访问,速度快 | 成本高,引脚多 | 高端制氧机 |
| 共享SRAM | 成本低,设计简单 | 需要仲裁机制 | 中低端制氧机 |
| SPI Flash | 容量大,非易失 | 写入慢,有寿命 | 仅用于日志记录 |
4.2 数据结构和访问协议
共享存储器的数据结构要简单、紧凑。我一般用结构体来定义:
// 共享存储器数据结构
typedef struct {
uint32_t magic; // 魔数,用于校验数据有效性
uint16_t sequence_number; // 序列号,用于检测数据更新
uint16_t o2_concentration; // 氧浓度设定值(0.1%精度)
uint16_t flow_rate; // 流量设定值(0.1L/min精度)
uint8_t operation_mode; // 运行模式
uint8_t fault_code; // 故障代码
uint32_t timestamp; // 时间戳
uint16_t crc; // CRC校验
} shared_memory_t;
访问协议要注意互斥。两个MCU不能同时写同一个地址,否则数据会乱。我常用的方法是:
- 主MCU写,从MCU读:主MCU定期更新共享数据,从MCU只读不写。
- 使用序列号:每次写入时递增序列号,读取时检查序列号是否变化,确保读到的是最新数据。
- CRC校验:每次写入计算CRC,读取时校验CRC,防止数据被破坏。
避坑指南:我曾经遇到过一个问题——主MCU写入共享数据时,写到一半被中断打断了,从MCU读到了不完整的数据。后来我加了"双缓冲"机制:主MCU先写缓冲区A,写完后原子地切换指针,从MCU只读缓冲区B。这样就不会读到半成品数据了。
4.3 数据同步策略
共享数据不是实时同步的,那样太浪费带宽。我一般按优先级分三个同步周期:
- 高频同步(1ms):氧浓度、流量等关键控制参数。
- 中频同步(10ms):运行模式、故障状态等状态信息。
- 低频同步(100ms):累计运行时间、维护记录等非实时数据。
这样既保证了关键数据的实时性,又不会占用太多总线带宽。
五、实际项目中的注意事项
最后,我总结几个实际项目中容易踩的坑:
- 电源隔离:两个MCU的电源要独立,别共用一个LDO。否则主MCU短路,从MCU也跟着掉电,热备就成笑话了。
- 看门狗独立:每个MCU都有自己的看门狗,别共享。主MCU挂了,从MCU的看门狗还能正常工作。
- 切换测试:生产前要做至少1000次切换测试,模拟各种故障场景。我见过一个项目,切换逻辑在仿真时没问题,实际跑起来因为时序问题偶尔失败。
- 日志记录:每次切换都要记录原因和时间,方便事后分析。我习惯用非易失存储器保存最近100次切换记录。
嗯,双MCU热备架构就讲到这里。说白了,这套架构的核心就是"冗余"二字——用两套硬件、两套软件,换来系统的高可靠性。下一章咱们聊聊故障注入测试,看看怎么用"暴力"手段验证这套架构到底靠不靠谱。