3、FOTA关键技术选型:分片传输、断点续传、校验机制、安全加密

做FOTA升级,说白了就是给远在千里之外的设备“换脑子”。

这事儿看着简单,实际坑特别多。我这些年踩过的雷,十个手指头都数不过来。今天咱们就把几个关键技术掰开揉碎了讲清楚。

3.1 分片传输:大文件怎么拆?

LoRaWAN的MTU很小,一个包最多就两百多字节。你想想看,一个固件动辄几百KB,怎么传?

答案就是分片。把大固件切成小块,一块一块发。

分片大小怎么定?

我个人习惯用256字节一片。为什么?

  • LoRaWAN一个包的有效载荷最多242字节(DR0场景下更少)
  • 去掉协议头,256字节的片内数据刚好能塞进两个包
  • 256是2的幂,计算偏移量时位运算快

分片协议结构(我常用的)

typedef struct {
    uint16_t fragment_index;  // 片序号,从0开始
    uint16_t total_fragments; // 总片数
    uint8_t  data[256];       // 片内数据
    uint16_t crc16;           // 本片校验
} __attribute__((packed)) fota_fragment_t;

避坑指南:我曾经遇到过一个问题——分片序号用uint8_t,结果固件超过255片就溢出了。后来改成uint16_t才解决。嗯,这里要注意,别省那一个字节。

3.2 断点续传:设备断电了怎么办?

LoRaWAN升级最怕什么?传了一半,设备没电了,或者信号断了。

没有断点续传,就得从头再来。一个256KB的固件,在SF12速率下可能要传好几个小时。重传?用户早骂娘了。

我的实现方案

  1. 接收状态持久化:每收到一片,就把当前进度写到Flash里
  2. 进度记录结构
typedef struct {
    uint32_t magic;              // 魔数,0xFOTA2024
    uint16_t last_received_idx;  // 最后收到的片序号
    uint16_t total_fragments;    // 总片数
    uint32_t firmware_crc32;     // 固件整体CRC
    uint8_t  received_bitmap[32];// 位图,记录哪些片已收(支持256片)
} fota_progress_t;

小技巧:用位图记录已收片,比用数组省空间。256片只需要32字节。设备重启后,服务器问一句“你收到哪了?”,设备把位图发回去,服务器接着没传完的继续发。

断点续传的边界情况

  • 如果设备在写Flash时断电,可能导致进度记录损坏。我一般写两份备份,互为校验。
  • 如果服务器端也重启了,需要设备上报进度。所以服务器也要维护一个会话状态。

3.3 校验机制:怎么保证数据没传错?

LoRaWAN的空中干扰很常见。一个比特错了,整个片就废了。

校验机制分两层:

第一层:片内校验

每个分片自带CRC16。收到后先算一遍,不对就请求重传这一片。

第二层:整体校验

所有片收完后,把固件拼起来,算一个CRC32或者SHA256。跟服务器下发的固件哈希对比。

我推荐的双重校验流程

// 伪代码
bool verify_fragment(fota_fragment_t *frag) {
    uint16_t calc_crc = crc16(frag->data, frag->data_len);
    if (calc_crc != frag->crc16) {
        // 请求重传
        request_retransmit(frag->fragment_index);
        return false;
    }
    // 写入Flash
    write_to_flash(frag);
    return true;
}

bool verify_firmware() {
    uint32_t calc_crc = crc32(flash_base, firmware_size);
    if (calc_crc != expected_crc32) {
        // 整体校验失败,标记固件无效
        mark_firmware_invalid();
        return false;
    }
    // 校验通过,跳转执行
    jump_to_firmware();
    return true;
}

避坑指南:我曾经遇到过CRC32碰撞的问题——两个不同的固件算出来一样的CRC32。概率极低,但万一碰上了呢?后来我改用SHA256做整体校验。虽然计算慢一点,但安全多了。

3.4 安全加密:防止固件被篡改

你想想看,如果固件在传输过程中被人篡改了,设备执行了恶意代码,后果是什么?

轻则设备变砖,重则整个网络被控制。所以加密不是可选项,是必选项。

FRITZ框架简介

FRITZ是我参与设计的一个轻量级FOTA安全框架。核心思想就三点:

  1. 固件签名:用私钥对固件哈希签名,设备用公钥验签
  2. 传输加密:每个分片用AES-128加密,密钥由会话协商生成
  3. 防回滚:固件版本号递增,防止攻击者用旧版本漏洞攻击

FRITZ框架的加密流程

// 服务器端
1. 计算固件SHA256哈希
2. 用私钥对哈希签名 (ECDSA)
3. 将固件分片,每片用AES-128-CTR加密
4. 发送加密分片 + 签名

// 设备端
1. 接收加密分片,用会话密钥解密
2. 所有片收齐后,拼成完整固件
3. 计算固件SHA256哈希
4. 用公钥验证签名
5. 验证通过,写入执行区

密钥管理

  • 每个设备出厂时烧录唯一的公钥对
  • 会话密钥通过LoRaWAN的FRMPayload加密通道协商
  • 私钥保存在服务器HSM中,绝不外泄

重要提醒:千万不要把私钥硬编码在固件里!我曾经见过一个项目,私钥直接写在源码里,结果固件被反编译后,整个产品线的设备都被攻破了。密钥一定要用安全元件(SE)或者TEE来保护。

3.5 综合选型建议

说了这么多,到底怎么选?我根据项目经验给个参考:

场景 分片大小 校验方式 加密方式 断点续传
小批量(<100台) 128字节 CRC16+CRC32 AES-128 可选
中等规模 256字节 CRC16+SHA256 AES-256 必需
大规模/高安全 256字节 CRC16+ECDSA签名 AES-256+会话密钥 必需+位图记录

我个人建议,除非你的产品真的对成本极度敏感,否则直接上最高安全配置。为什么?因为FOTA升级是一次性的投入,但安全漏洞的修复成本可能是天价。

好了,关键技术选型就聊到这儿。下一章咱们讲讲怎么把这些技术落地到代码里。