第2章:系统架构设计——OTA系统的整体架构、云端服务器设计、设备端升级代理设计

好,咱们进入正题。

上一章聊了OTA升级的基本概念,这一章咱们把骨架搭起来。说白了,就是搞清楚整个系统长什么样,云端该做什么,设备端又该做什么。

我刚开始做OTA架构时,犯过一个低级错误——把云端和设备端的职责混在一起。结果呢?云端要管设备状态,设备端要处理业务逻辑,两边都臃肿不堪。后来重构了一次,才明白什么叫「各司其职」。

2.1 OTA系统的整体架构

先看宏观。一个典型的OTA系统,分三层:

  • 云端管理层:负责固件存储、版本管理、升级策略、设备认证。
  • 通信传输层:负责数据下发、断点续传、加密传输。
  • 设备执行层:负责固件接收、校验、备份、更新、回滚。

这三层之间,通过HTTP/HTTPS或者MQTT协议通信。我个人习惯用MQTT做指令下发,用HTTPS做固件下载。为什么?MQTT轻量,适合心跳和命令;HTTPS稳定,适合大文件传输。

核心原则:云端只做策略,设备只做执行。别让设备去判断「该不该升级」,那是云端的事。

你想想看,如果每个设备都自己去检查版本、决定升级时机,那网络风暴、升级冲突分分钟教你做人。我在一个智能家居项目里就吃过这个亏——2000台设备同时去拉取固件,直接把CDN打挂了。

2.2 云端服务器设计

云端设计,我把它拆成四个模块:

2.2.1 固件管理模块

这个模块管三件事:

  • 版本管理:每个固件要有唯一版本号,建议用语义化版本(如v2.1.3)。
  • 差分管理:存储全量包和差分包。差分包能省流量,但生成算法要选好——bsdiff还是xdelta?我踩过坑,bsdiff对二进制文件效果好,但内存占用高;xdelta均衡些。
  • 灰度发布:按设备ID、地域、固件版本分批推送。别一上来就全量,那是自杀式升级。

我的习惯:灰度比例从5%开始,观察24小时无异常再扩大到30%,最后全量。每次灰度都要有自动回滚机制。

2.2.2 设备管理模块

云端得知道每个设备的「底细」:

  • 当前固件版本
  • 硬件型号(不同型号固件不通用)
  • 升级状态(空闲/下载中/升级中/完成/失败)
  • 最后在线时间

这些信息存在数据库里。我建议用Redis做缓存,MySQL做持久化。设备上报状态时先写Redis,再异步刷到MySQL,避免高并发写库。

2.2.3 升级策略模块

这是云端的「大脑」。它决定:

  • 哪些设备可以升级(白名单/黑名单)
  • 什么时间升级(静默时段、业务低峰期)
  • 升级失败怎么办(重试次数、回滚策略)

举个例子,我曾经给一个工业网关做升级策略:只允许凌晨2点到4点升级,且电池电量必须高于60%。如果升级失败,自动回滚到上一个版本,最多重试3次。

2.2.4 安全认证模块

这个不能省。云端要验证设备身份,设备也要验证固件来源。我常用的方案:

  • 设备端用唯一密钥(烧录在安全芯片里)
  • 固件包用RSA签名,设备端验签
  • 通信链路用TLS加密

注意:千万别把密钥硬编码在代码里。我见过有人把私钥写在固件里,结果固件被反编译,整个产品线被抄。密钥要放在安全存储区,或者用硬件加密芯片。

2.3 设备端升级代理设计

设备端这边,我管它叫「升级代理」(Update Agent)。它是个独立模块,不依赖业务逻辑。为什么?因为业务代码可能升级失败,但升级代理必须活着。

2.3.1 升级代理的核心职责

  1. 心跳上报:定期告诉云端「我还活着,当前版本是xxx」。
  2. 指令监听:接收云端的升级指令,包括固件URL、版本号、校验值。
  3. 固件下载:支持断点续传、校验完整性。
  4. 固件校验:验签、校验CRC/SHA256。
  5. 备份与更新:把当前固件备份到备用分区,再写入新固件。
  6. 回滚处理:如果新固件启动失败,自动切回备份分区。

嗯,这里要注意:下载和校验必须在升级代理里做,不能交给业务线程。业务线程挂了,下载就断了,那设备就成砖了。

2.3.2 双分区设计

这是设备端最核心的设计。我画个简化的分区表:

分区 用途 大小
Bootloader 引导加载,决定启动哪个固件 64KB
Firmware_A 当前运行固件 1MB
Firmware_B 备份/新固件 1MB
Config 配置参数、升级状态 16KB

升级流程是这样的:

  1. 设备当前运行在Firmware_A。
  2. 收到升级指令,下载新固件到Firmware_B。
  3. 校验通过后,修改Config分区里的启动标志,指向Firmware_B。
  4. 重启,Bootloader读取Config,启动Firmware_B。
  5. 如果Firmware_B启动失败(比如看门狗超时),Bootloader自动切回Firmware_A。

关键点:Config分区要写两次。第一次写「准备切换」,第二次写「切换成功」。如果第一次写完就断电,重启后Bootloader发现标志是「准备切换」,但Firmware_B不完整,就自动回滚。这叫「原子性操作」。

2.3.3 升级代理的代码骨架

我贴一段伪代码,帮你理解升级代理的主循环:

void update_agent_task() {
    while (1) {
        // 1. 上报心跳
        report_heartbeat();
        
        // 2. 检查是否有升级指令
        upgrade_cmd_t cmd = check_upgrade_command();
        if (cmd.valid) {
            // 3. 下载固件到备用分区
            int ret = download_firmware(cmd.url, PARTITION_B);
            if (ret != SUCCESS) {
                report_status(UPGRADE_FAIL_DOWNLOAD);
                continue;
            }
            
            // 4. 校验固件
            ret = verify_firmware(PARTITION_B, cmd.sha256);
            if (ret != SUCCESS) {
                report_status(UPGRADE_FAIL_VERIFY);
                continue;
            }
            
            // 5. 设置启动标志
            set_boot_flag(PARTITION_B);
            
            // 6. 重启
            system_reset();
        }
        
        // 7. 休眠一段时间
        sleep(HEARTBEAT_INTERVAL);
    }
}

这段代码看着简单,但实际项目里要考虑很多边界情况。比如下载到一半断网了,要能断点续传;比如备用分区空间不够,要能清理旧数据。

避坑指南:我曾经在下载过程中没做超时处理,结果设备卡在下载状态整整三天。后来加了超时机制:30分钟没下载完,自动重置状态,重新上报心跳。

2.4 通信协议设计

最后聊两句通信。云端和设备端之间,我建议用两种协议:

  • MQTT:用于指令下发和状态上报。QoS设为1(至少一次),保证消息不丢。
  • HTTPS:用于固件下载。支持断点续传(Range头),支持分块下载。

为什么不用MQTT下载固件?因为MQTT的payload有限制,而且大文件传输效率低。HTTPS有成熟的CDN加速,更适合。

嗯,这一章的内容差不多了。总结一下:

  • 整体架构分三层:云端、传输、设备。
  • 云端管策略,设备管执行。
  • 设备端一定要用双分区+升级代理,保证升级失败能回滚。
  • 通信协议各司其职,别混用。

下一章,咱们深入设备端,聊聊Bootloader怎么设计,以及如何实现「不死升级」。