OTA 升级的主流程看起来并不复杂:

  1. 获取固件信息。
  2. 分片下载固件。
  3. 写入 Flash。
  4. 校验固件完整性。
  5. 重启并切换到新固件。

但在实际调试中,很多 OTA 问题不是出在“会不会下载”,而是出在各种边界条件:最后一包长度不对、请求范围越界、Flash 写入地址不对齐、下载偏移和写入偏移不一致、断网重试后重复写入、校验值计算范围错误等。

这篇文章只讨论一个很具体的问题:嵌入式设备做 OTA 分片下载时,为什么容易在边界处理上出错,以及如何设计一个更稳妥的分片下载流程。

为什么要分片下载

嵌入式设备通常不会一次性把整个固件包下载到 RAM 里。

原因很简单:RAM 不够。

比如一个 MCU 只有几十 KB 或几百 KB RAM,而固件可能有几百 KB,甚至几 MB。如果直接把完整固件放进内存再写 Flash,不现实,也没有必要。

更常见的做法是:

  • 每次从服务器请求一小段固件数据。
  • 收到后立即写入 Flash 的下载分区。
  • 写完后继续请求下一段。
  • 全部下载完成后再统一校验。

这种方式就是分片下载。

常见分片大小可以是:

  • 512 字节
  • 1024 字节
  • 2048 字节
  • 4096 字节

分片大小不是越大越好。片太大,单次缓存占用高,网络失败后重传成本也高;片太小,请求次数太多,升级时间会变长。实际选择要结合 RAM 大小、网络稳定性、Flash 写入粒度和服务器接口能力。

最容易忽略的是最后一片

假设固件大小是 1025 字节,每片下载 512 字节。

很多人会自然地按下面方式理解:

第 1 片:0 ~ 511 第 2 片:512 ~ 1023 第 3 片:1024 ~ 1535

但这是错的。

固件总大小只有 1025 字节,合法范围是:

0 ~ 1024

所以正确分片应该是:

第 1 片:0 ~ 511 长度 512 第 2 片:512 ~ 1023 长度 512 第 3 片:1024 ~ 1024 长度 1

最后一片通常不会刚好等于固定分片大小。除非固件总大小正好是分片大小的整数倍。

如果程序仍然按固定长度去请求最后一片,就可能出现几个问题:

  • HTTP Range 请求越界。
  • 服务器返回错误状态码。
  • 服务器返回的数据长度和设备期望不一致。
  • 设备写入了多余数据。
  • 最终 CRC 或 Hash 校验失败。

所以 OTA 分片下载里,最后一片必须单独处理。

正确计算每一片的范围

分片计算的核心是:每次根据当前偏移和剩余长度动态计算本次下载长度。

示例代码:

#define OTA_CHUNK_SIZE 512 uint32_t offset = 0; while (offset < image_size) { uint32_t remain = image_size - offset; uint32_t chunk_size = remain > OTA_CHUNK_SIZE ? OTA_CHUNK_SIZE : remain; uint32_t range_start = offset; uint32_t range_end = offset + chunk_size - 1; /* * 请求 range_start ~ range_end 的数据 * 收到 chunk_size 字节后写入 Flash */ offset += chunk_size; }

这里最关键的是:

chunk_size = min(remain, OTA_CHUNK_SIZE);

只要每次下载长度都不超过剩余长度,就不会发生末包越界。

整数倍大小也要注意

还有一种情况也容易写错:固件大小刚好是分片大小的整数倍。

比如固件大小是 1024 字节,每片 512 字节。

正确分片是:

第 1 片:0 ~ 511 第 2 片:512 ~ 1023

没有第 3 片。

如果代码使用类似下面的计算方式:

total_packet = image_size / OTA_CHUNK_SIZE + 1;

那么 1024 / 512 + 1 = 3,就会多请求一片不存在的数据。

更稳妥的总片数计算方式是:

total_packet = (image_size + OTA_CHUNK_SIZE - 1) / OTA_CHUNK_SIZE;

这个公式可以向上取整:

1024 字节 / 512 = 2 片 1025 字节 / 512 = 3 片 1 字节 / 512 = 1 片

HTTP Range 请求怎么写

如果服务器支持 HTTP Range,可以通过 Range 头请求指定字节范围。

例如:

Range: bytes=0-511 Range: bytes=512-1023 Range: bytes=1024-1024

设备端需要检查服务器返回:

  • 状态码是否为 206 Partial Content。
  • Content-Length 是否等于本次期望长度。
  • Content-Range 是否和请求范围一致。
  • 实际收到的数据长度是否正确。

不要只要请求成功就直接写 Flash。更稳妥的判断是:

if (status_code != 206) { return OTA_ERR_HTTP_STATUS; } if (content_length != chunk_size) { return OTA_ERR_LENGTH_MISMATCH; } if (recv_len != chunk_size) { return OTA_ERR_RECV_INCOMPLETE; }

如果服务器不支持 Range,也可以设计自己的接口,例如:

GET /firmware.bin?offset=1024&length=512

无论使用哪种协议,设备端都要坚持一个原则:请求多少、收到多少、写入多少,这三个数字必须一致。

下载偏移和 Flash 地址不要混在一起

OTA 中通常有两个概念:

  • 下载偏移:当前下载到固件文件的第几个字节。
  • Flash 地址:当前要写到 Flash 分区的哪个地址。

很多 bug 来自这两个概念混用。

假设下载分区起始地址是:

#define OTA_PART_ADDR 0x08040000

那么当前写入地址应该是:

flash_addr = OTA_PART_ADDR + offset;

而不是直接把 offset 当成 Flash 地址。

推荐写成比较清楚的形式:

uint32_t image_offset = 0; uint32_t flash_addr = OTA_PART_ADDR + image_offset;

这样后面排查日志时也更容易看懂。

写 Flash 前要检查分区大小

下载前必须检查固件大小是否超过 OTA 分区。

if (image_size > OTA_PART_SIZE) { return OTA_ERR_IMAGE_TOO_LARGE; }

否则固件写入可能越过当前分区,覆盖参数区、备份区甚至其他程序区域。

这是很危险的错误。OTA 失败最多只是升级失败,但 Flash 越界写入可能导致设备无法恢复。

如果固件包带头部,还要明确 image_size 指的是:

  • 包总大小?
  • APP 镜像大小?
  • 去掉头部后的有效固件大小?

这个定义必须清楚,否则校验和写入范围都会出错。

Flash 擦除和写入也有边界

大多数 MCU Flash 都有擦除粒度,例如按页或扇区擦除。

假设:

Flash 页大小:2048 字节 固件大小:5000 字节

需要擦除的页数不是:

5000 / 2048 = 2 页

而是:

ceil(5000 / 2048) = 3 页

可以这样计算:

erase_size = (image_size + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE * FLASH_PAGE_SIZE;

擦除时向上按页对齐,写入时按实际固件长度写入。不要把擦除补齐出来的空白区域也算进固件 CRC,除非你的协议明确这样定义。

写入对齐问题

有些芯片要求 Flash 写入地址和长度满足 2 字节、4 字节或 8 字节对齐。

如果 OTA 分片大小是 512,这通常没问题。但最后一片可能只有 1 字节、3 字节或 5 字节,这时直接写入可能失败。

解决方式通常有两种。

第一种:最后一片补齐到写入对齐长度,补齐字节用 0xFF。

uint32_t aligned_len = align_up(chunk_size, FLASH_WRITE_ALIGN); memset(write_buf, 0xFF, aligned_len); memcpy(write_buf, recv_buf, chunk_size); flash_write(flash_addr, write_buf, aligned_len);

第二种:缓存未对齐的尾部数据,等凑够对齐长度再写。

对于简单 OTA,第一种方式更容易实现。但要注意:校验时只校验真实固件长度,不要把补齐的 0xFF 算进去,除非服务器端计算 CRC 时也包含补齐内容。

每片写完后要不要立刻校验

常见有两种校验方式:

  1. 每片下载后做片校验。
  2. 全部下载完成后做整包校验。

如果服务器能提供每片 CRC,那么每片校验可以更早发现网络传输错误。

但很多简单 OTA 只提供整包 CRC,这时可以边下载边累计 CRC:

crc = crc32_update(crc, recv_buf, chunk_size);

全部下载完成后比较:

if (crc != expected_crc) { return OTA_ERR_CRC; }

这里要注意,CRC 更新长度应该是 chunk_size,也就是真实收到的固件数据长度,不是固定的 OTA_CHUNK_SIZE。

否则最后一片会把无效数据也算进去,导致校验失败。

断网重试时如何避免重复写错

OTA 过程中网络失败很常见,尤其是 4G、Wi-Fi 信号不稳定时。

如果某一片下载失败,建议只重试当前片,不要直接从头开始。

基本逻辑:

for (retry = 0; retry < MAX_RETRY; retry++) { ret = download_chunk(offset, chunk_size, recv_buf); if (ret == 0) { break; } } if (ret != 0) { return OTA_ERR_DOWNLOAD_FAILED; }

注意:只有在这一片完整下载、长度检查通过、写入 Flash 成功后,才能更新 offset。

不要在请求发出后就更新偏移。

错误写法:

download_chunk(offset, chunk_size, recv_buf); offset += chunk_size; // 请求失败也前进了

正确写法:

ret = download_chunk(offset, chunk_size, recv_buf); if (ret == 0) { ret = flash_write(OTA_PART_ADDR + offset, recv_buf, chunk_size); } if (ret == 0) { offset += chunk_size; }

断点续传需要保存哪些信息

如果希望设备断电或重启后继续下载,就需要保存 OTA 进度。

至少需要保存:

  • 固件版本。
  • 固件总大小。
  • 当前已写入偏移。
  • 目标分区。
  • 已累计 CRC 或分片校验状态。
  • OTA 状态标志。

这些信息一般保存在参数区,而不是 RAM。

但断点续传也会带来新的问题:如何确认 Flash 中已经写入的数据仍然有效?

简单做法是重启后从头下载。实现简单,可靠性高,但耗时。

复杂做法是保存每片校验状态,重启后验证已写入部分,再从断点继续。这样效率高,但实现复杂。

对于资源有限或项目周期紧的设备,先实现“失败后重新下载”通常更稳妥。

一个推荐的 OTA 分片下载流程

可以按下面流程组织代码:

1. 获取固件信息 - version - image_size - crc32 - download_url 2. 检查固件合法性 - 版本是否允许升级 - image_size 是否为 0 - image_size 是否超过分区 3. 擦除 OTA 分区 - 按 Flash 页大小向上对齐擦除 4. 循环下载 - remain = image_size - offset - chunk_size = min(remain, OTA_CHUNK_SIZE) - 请求 offset ~ offset + chunk_size - 1 - 检查 HTTP 状态码 - 检查 Content-Length - 检查实际接收长度 - 写入 Flash - 更新 CRC - offset += chunk_size 5. 下载完成 - offset == image_size - 比较 CRC - 写入升级标志 6. 重启 - BootLoader 校验新固件 - 跳转到新 APP

重点是:每一步都要有错误返回,不要默认一定成功。

调试时建议打印哪些日志

OTA 调试日志非常重要。建议至少打印:

image_size=1025, chunk_size=512 offset=0, range=0-511, recv=512, write=0x08040000 offset=512, range=512-1023, recv=512, write=0x08040200 offset=1024, range=1024-1024, recv=1, write=0x08040400 crc_calc=0x12345678, crc_expect=0x12345678

如果出现问题,通过这些日志很容易判断:

  • 是不是多下载了一片?
  • 最后一片长度对不对?
  • offset 有没有跳变?
  • Flash 写入地址是否正确?
  • CRC 是下载过程中错了,还是写入后读回错了?

没有日志时,OTA 问题很容易变成“偶现失败”,很难定位。

常见错误总结

下面这些问题非常常见:

  1. 总片数计算多加了 1,导致整数倍固件多请求一片。
  2. 最后一片仍按固定大小请求,导致 Range 越界。
  3. Content-Length 没检查,服务器返回多少就写多少。
  4. 请求成功后直接更新 offset,失败重试时跳过数据。
  5. Flash 写入地址把 offset 当成绝对地址。
  6. 固件大小超过分区但没有提前检查。
  7. 擦除大小没有按页对齐。
  8. 最后一片写入不满足 Flash 对齐要求。
  9. CRC 计算用了固定分片大小,而不是真实数据长度。
  1. OTA 标志写得太早,固件还没校验就重启切换。

这些问题单独看都不复杂,但组合在一起就会让 OTA 很不稳定。

总结

OTA 分片下载的难点不在主流程,而在边界条件。

一个稳定的 OTA 分片下载实现,至少要做到:

  • 每片长度根据剩余大小动态计算。
  • 最后一片不能越界。
  • 固件大小不能超过 Flash 分区。
  • Flash 擦除和写入要考虑对齐。
  • 下载长度、接收长度、写入长度必须一致。
  • 只有写入成功后才能更新 offset。
  • 校验通过后才能设置升级标志。

这些细节处理好之后,OTA 的稳定性会提升很多。尤其是在网络不稳定的设备上,边界处理和失败恢复往往比“正常流程能跑通”更重要。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐