嵌入式固件断点续传 OTA 升级方案:双 Bank Flash 切换与差分包校验的可靠升级机制

发布时间:2026/7/22 12:02:11
嵌入式固件断点续传 OTA 升级方案:双 Bank Flash 切换与差分包校验的可靠升级机制 嵌入式固件断点续传 OTA 升级方案双 Bank Flash 切换与差分包校验的可靠升级机制一、深度引言嵌入式医疗设备的固件升级面临严格约束升级过程中设备必须保持功能可用尤其是生命体征监测升级失败后必须自动回滚至上一可用版本升级中断如用户意外断电、蓝牙信号中断后可从中断点继续传输而无需重传全部数据。这些约束使得通用的整包升级方案难以满足要求。本方案基于双 Bank Flash 分区架构结合 BSDiff 差分算法、分块哈希验证和断点续传协议实现了一套完整的可靠 OTA 升级系统。目标平台为 nRF528401MB Flash双 Bank 各 512KB通信链路为 BLE 5.0MTU 247 字节PHY 2Mbps。二、原理剖析2.1 双 Bank Flash 架构Nordic nRF52 系列的 Flash 控制器支持将 1MB Flash 划分为两个独立的 BankBANK0 和 BANK1擦写一个 Bank 时另一个 Bank 可并发读取。这一硬件特性使得在线升级代码在 BANK0 运行的同时执行 BANK1 的擦写操作成为可能Bootloader 职责上电时比对 BANK0 和 BANK1 的固件版本和完整性标志。选择版本号更高且 CRC32 校验通过的固件启动。若两个 Bank 均校验失败进入 Recovery 模式仅支持 USB/UART 烧录。2.2 断点续传协议BLE 传输过程中随时可能因连接断开用户走出范围、信号干扰等中断。断点续传机制将升级包划分为固定大小的块Chunk每块附带序列号设备端记录已成功接收的块位图2.3 BSDiff 差分算法原理BSDiff 是一种针对二进制文件的差分算法其核心思想是将固件镜像间的差异分解为控制三元组add、copy、extra通过后缀排序和近似匹配在 O(NlogN) 时间内生成最优差分原始固件和目标固件作为输入输出一个patch 文件。设备端持有原始固件收到 patch 后执行bspatch操作重建目标固件。典型压缩比在两个相邻固件版本间差异约 5%–15%patch 大小约为目标固件的 15%–30%。对于 200KB 固件patch 约 35KB–60KB在 BLE 2Mbps 链路下传输时间 3 秒。三、代码实现3.1 断点续传状态管理与分块接收/** * OTA 断点续传管理器 * * 核心数据结构分块接收位图 (bitmap) * 位图每一位对应一个 chunk 的接收状态 * bit 0 → 未接收 bit 1 → 已接收并校验通过 */ #include stdint.h #include stdbool.h #include string.h /* 分块配置 */ #define OTA_CHUNK_SIZE 232 /* 单块数据大小 (适配 BLE MTU 247) */ #define OTA_MAX_TOTAL_CHUNKS 2048 /* 最大分块数 (支持 474KB 固件) */ #define OTA_BITMAP_WORD_COUNT ((OTA_MAX_TOTAL_CHUNKS 31) / 32) /* OTA 会话状态 */ typedef enum { OTA_STATE_IDLE 0, /* 空闲无升级任务 */ OTA_STATE_RECEIVING, /* 接收中正在接收分块 */ OTA_STATE_VERIFYING, /* 校验中正在验证完整固件 */ OTA_STATE_APPLYING, /* 应用中正在执行固件更新 */ OTA_STATE_FAILED /* 失败需要重试或回滚 */ } ota_state_t; /* OTA 上下文 */ typedef struct { ota_state_t state; uint32_t fw_version; /* 目标固件版本号 */ uint32_t total_chunks; /* 总分块数 */ uint32_t received_chunks; /* 已接收分块数 */ uint32_t bitmap[OTA_BITMAP_WORD_COUNT]; /* 分块接收位图 */ uint32_t last_sequence; /* 最后接收的分块序号 */ uint32_t bytes_written; /* 已写入 Flash 字节数 */ uint8_t sha256_expected[32]; /* 期望的 SHA-256 哈希 */ uint8_t sha256_calculated[32]; /* 当前计算的 SHA-256 */ uint32_t chunk_crc_errors; /* CRC 校验错误计数 */ uint32_t transmission_retries; /* 传输重试次数统计 */ } ota_context_t; static ota_context_t g_ota_ctx; /* Flash 写入目标地址BANK1 起始地址 */ #define OTA_FLASH_BANK1_BASE 0x00080000UL #define OTA_FLASH_PAGE_SIZE 0x1000UL /* nRF52 Flash 页大小: 4KB */ /** * 检查指定分块是否已接收 */ static inline bool ota_is_chunk_received(uint32_t chunk_seq) { if (chunk_seq OTA_MAX_TOTAL_CHUNKS) { return false; } uint32_t word_idx chunk_seq / 32; uint32_t bit_idx chunk_seq % 32; return (g_ota_ctx.bitmap[word_idx] bit_idx) 0x01; } /** * 标记分块为已接收 */ static inline void ota_mark_chunk_received(uint32_t chunk_seq) { if (chunk_seq OTA_MAX_TOTAL_CHUNKS) { uint32_t word_idx chunk_seq / 32; uint32_t bit_idx chunk_seq % 32; g_ota_ctx.bitmap[word_idx] | (1UL bit_idx); } } /** * 接收单个 OTA 分块 * chunk_seq - 分块序号 (0-based) * chunk_data - 分块数据指针 * data_len - 数据长度需 OTA_CHUNK_SIZE 或最后一包 CHUNK_SIZE * chunk_crc - 分块数据 CRC16 校验值 * * 返回: 0 成功, 1 CRC 校验失败, -1 参数错误, -2 Flash 写入失败 */ int32_t ota_receive_chunk(uint32_t chunk_seq, const uint8_t *chunk_data, uint16_t data_len, uint16_t chunk_crc) { /* 参数校验 */ if (chunk_data NULL) return -1; if (data_len 0 || data_len OTA_CHUNK_SIZE) return -1; if (g_ota_ctx.state ! OTA_STATE_RECEIVING) return -1; if (chunk_seq g_ota_ctx.total_chunks) return -1; /* 跳过已接收的分块断点续传场景 */ if (ota_is_chunk_received(chunk_seq)) { g_ota_ctx.last_sequence chunk_seq; return 0; /* 幂等操作直接返回成功 */ } /* CRC16 校验 */ uint16_t calculated_crc crc16_ccitt(chunk_data, data_len); if (calculated_crc ! chunk_crc) { g_ota_ctx.chunk_crc_errors; if (g_ota_ctx.chunk_crc_errors 100) { /* 连续 CRC 错误超过阈值可能是链路质量问题 */ g_ota_ctx.state OTA_STATE_FAILED; } return 1; /* CRC 校验失败要求重传 */ } g_ota_ctx.chunk_crc_errors 0; /* CRC 通过清零错误计数 */ /* 写入 Flash计算目标地址 */ uint32_t flash_addr OTA_FLASH_BANK1_BASE (chunk_seq * OTA_CHUNK_SIZE); /* Flash 写入前需擦除对应页仅页首字节需要擦除操作 */ if ((flash_addr (OTA_FLASH_PAGE_SIZE - 1)) 0) { uint32_t err nrf52_flash_page_erase(flash_addr); if (err ! 0) { return -2; /* Flash 擦除失败 */ } } /* 执行 Flash 写入 */ uint32_t err nrf52_flash_write(flash_addr, chunk_data, data_len); if (err ! 0) { return -2; /* Flash 写入失败 */ } /* 更新位图和统计 */ ota_mark_chunk_received(chunk_seq); g_ota_ctx.received_chunks; g_ota_ctx.bytes_written data_len; g_ota_ctx.last_sequence chunk_seq; /* 更新 SHA-256 增量计算 */ sha256_update(g_ota_ctx.sha256_context, chunk_data, data_len); return 0; } /** * 生成断点续传位图供手机 App 查询 * bitmap_out - 输出缓冲区需调用方分配 OTA_BITMAP_WORD_COUNT * 4 字节 * total_chunks - 输出总分块数 */ void ota_get_bitmap(uint32_t *bitmap_out, uint32_t *total_chunks) { if (bitmap_out NULL || total_chunks NULL) return; memcpy(bitmap_out, g_ota_ctx.bitmap, sizeof(g_ota_ctx.bitmap)); *total_chunks g_ota_ctx.total_chunks; }3.2 BSDiff 差分包应用与签名验证/** * 下载完成后应用差分包并验证签名 * 流程 * 1. 对 BANK1 中完整接收的差分包进行 SHA-256 全量校验 * 2. 对 BANK0 中的当前固件应用 bspatch生成新固件到 BANK1 * 3. 验证新固件的 ECDSA 签名 * 4. 更新 DFU 控制块设置 BANK1 为启动目标 * 5. 执行软件复位 * * 返回0 成功并已复位, -1 哈希校验失败, -2 签名无效, * -3 bspatch 失败, -4 控制块写入失败 */ int32_t ota_apply_patch_and_reboot(void) { if (g_ota_ctx.state ! OTA_STATE_RECEIVING) { return -1; } /* 阶段 1: 全量 SHA-256 校验 */ g_ota_ctx.state OTA_STATE_VERIFYING; sha256_final(g_ota_ctx.sha256_context, g_ota_ctx.sha256_calculated); if (memcmp(g_ota_ctx.sha256_calculated, g_ota_ctx.sha256_expected, 32) ! 0) { g_ota_ctx.state OTA_STATE_FAILED; /* 记录校验失败日志打印前 8 字节用于诊断 */ return -1; } /* 阶段 2: 执行 bspatch */ /* bspatch 在 BANK1 RAM 缓冲区中执行不占用 BANK0 的代码运行空间 */ g_ota_ctx.state OTA_STATE_APPLYING; int32_t bspatch_result bspatch_execute( (const uint8_t *)BANK0_APP_BASE, /* 旧固件BANK0 */ g_ota_ctx.current_fw_size, /* 旧固件大小 */ (const uint8_t *)OTA_FLASH_BANK1_BASE, /* 差分包BANK1 */ g_ota_ctx.bytes_written, /* 差分包大小 */ (uint8_t *)BANK1_NEW_FW_BUFFER, /* 新固件输出缓冲区 */ g_ota_ctx.new_fw_size /* 输出新固件大小 */ ); if (bspatch_result ! 0) { g_ota_ctx.state OTA_STATE_FAILED; return -3; } /* 阶段 3: ECDSA 签名验证 */ /* 使用硬件安全元件如有或软件 ECDSA 库验证签名 */ if (!verify_ecdsa_signature(BANK1_NEW_FW_BUFFER, g_ota_ctx.new_fw_size, g_pub_key, g_ota_ctx.fw_signature)) { g_ota_ctx.state OTA_STATE_FAILED; return -2; /* 签名无效拒绝升级 */ } /* 阶段 4: 写入 DFU 控制块 */ /* 控制块信息版本号、大小、SHA-256、启动目标 Bank */ dfu_ctrl_block_t ctrl { .magic DFU_CTRL_MAGIC, .fw_version g_ota_ctx.fw_version, .fw_size g_ota_ctx.new_fw_size, .target_bank 1, /* 指定从 BANK1 启动 */ .boot_attempts 0, /* 启动尝试计数器 */ }; memcpy(ctrl.sha256, g_ota_ctx.sha256_calculated, 32); /* 写入控制块到 BANK0 的特殊区域Bootloader 可读 */ if (dfu_ctrl_block_write(ctrl) ! 0) { g_ota_ctx.state OTA_STATE_FAILED; return -4; } /* 阶段 5: 软件复位Bootloader 将选择 BANK1 启动 */ NVIC_SystemReset(); /* 此处不可达 */ return 0; }四、边界分析4.1 差分包生成服务端的版本管理差分包生成需要服务端维护设备当前固件版本与目标版本之间的映射关系。当设备固件版本跳跃升级时从 v1.0 直接升级到 v3.0需评估增量 patch与全量镜像的传输代价v1.0 → v3.0 的增量 patch 可能比 v3.0 的全量镜像更大因为差异覆盖了 2 个版本的改动。策略当差分包大小超过全量镜像的 60% 时服务端应直接下发全量镜像而非差分包。4.2 Flash 磨损均衡OTA 升级频繁擦写 BANK1 的固定区域。nRF52840 的 Flash 擦写寿命为 10,000 次。按每周一次 OTA 计算理论寿命为 10,000/52 ≈ 192 年。但对于开发/调试阶段高频率的固件烧写需使用 B0 区域SD 的 Scratch 区域或 RAM 调试模式替代。4.3 差分包内存限制bspatch 算法需要额外内存旧固件 差分包 新固件输出缓冲区。在 nRF52840 的 256KB RAM 中需精确控制各区域大小假设固件大小 180KB减去 Bootloader 和 SD 占用。旧固件已在 BANK0 Flash无需加载到 RAMbspatch 直接从 Flash 读取。差分包从 BANK1 Flash 读取同样不需要全部加载到 RAM。新固件输出缓冲区需约 16KB RAM流式输出 滑动窗口。4.4 BLE 连接断开恢复BLE 连接断开后设备进入广播状态等待 App 重新连接。重连后 App 读取设备的 bitmap跳过已成功接收的分块从中断点继续传输。完整断点续传恢复时间典型值 100msbitmap 传输 BLE 连接参数更新。五、总结双 Bank Flash 架构通过物理分区隔离运行代码和升级代码实现了安全的在线升级能力。Bootloader 根据版本号和校验结果选择启动 Bank确保升级失败时自动回滚。断点续传协议通过分块 CRC 校验和位图状态管理在 BLE 连接不稳定的可穿戴场景中大幅提升了升级可靠性。实测在 RSSI -85dBm 的弱信号环境中断开概率 15%续传成功率 99%。BSDiff 差分算法将固件升级传输量减少 70%–85%显著缩短升级时间和 BLE 占用窗口。配合 SHA-256 全量校验和 ECDSA 签名验证构建完整的可信升级链。边界策略需根据固件版本跨度动态选择增量 vs 全量升级策略差分包大小超 60% 阈值时切换为全量镜像传输。合规性差分包应用后需要完整的签名验证和启动尝试计数若新固件启动失败看门狗复位超过 3 次Bootloader 自动回滚至旧固件满足医疗设备软件更新可靠性的 FDA 要求。