双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确实挂了。

  1. 确认故障:从MCU连续3次检测到心跳丢失,进入切换准备状态。
  2. 接管控制:从MCU将输出使能引脚拉高,同时将主MCU的输出使能引脚拉低。
  3. 状态同步:从MCU从共享存储器读取最后一次保存的系统状态,包括氧浓度设定值、流量设定值、运行模式等。
  4. 恢复输出:从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):累计运行时间、维护记录等非实时数据。

这样既保证了关键数据的实时性,又不会占用太多总线带宽。

五、实际项目中的注意事项

最后,我总结几个实际项目中容易踩的坑:

  1. 电源隔离:两个MCU的电源要独立,别共用一个LDO。否则主MCU短路,从MCU也跟着掉电,热备就成笑话了。
  2. 看门狗独立:每个MCU都有自己的看门狗,别共享。主MCU挂了,从MCU的看门狗还能正常工作。
  3. 切换测试:生产前要做至少1000次切换测试,模拟各种故障场景。我见过一个项目,切换逻辑在仿真时没问题,实际跑起来因为时序问题偶尔失败。
  4. 日志记录:每次切换都要记录原因和时间,方便事后分析。我习惯用非易失存储器保存最近100次切换记录。

嗯,双MCU热备架构就讲到这里。说白了,这套架构的核心就是"冗余"二字——用两套硬件、两套软件,换来系统的高可靠性。下一章咱们聊聊故障注入测试,看看怎么用"暴力"手段验证这套架构到底靠不靠谱。