嵌入式 OTA 分片下载为什么容易出问题:从末包边界到 Flash 写入校验
嵌入式设备OTA升级中,分片下载的边界处理是关键难点。文章指出常见问题包括:最后一片长度计算错误、HTTP Range越界、Flash写入不对齐、CRC校验范围错误等。核心解决方案是:动态计算每片下载长度(取剩余长度和分片大小的较小值),严格检查服务器返回数据长度,确保Flash写入地址正确对齐,并区分下载偏移和Flash地址。特别强调整数倍固件大小和最后一片的特殊处理,建议通过详细日志记录off
OTA 升级的主流程看起来并不复杂:
- 获取固件信息。
- 分片下载固件。
- 写入 Flash。
- 校验固件完整性。
- 重启并切换到新固件。
但在实际调试中,很多 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 时也包含补齐内容。
每片写完后要不要立刻校验
常见有两种校验方式:
- 每片下载后做片校验。
- 全部下载完成后做整包校验。
如果服务器能提供每片 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,导致整数倍固件多请求一片。
- 最后一片仍按固定大小请求,导致 Range 越界。
- Content-Length 没检查,服务器返回多少就写多少。
- 请求成功后直接更新 offset,失败重试时跳过数据。
- Flash 写入地址把 offset 当成绝对地址。
- 固件大小超过分区但没有提前检查。
- 擦除大小没有按页对齐。
- 最后一片写入不满足 Flash 对齐要求。
- CRC 计算用了固定分片大小,而不是真实数据长度。
- OTA 标志写得太早,固件还没校验就重启切换。
这些问题单独看都不复杂,但组合在一起就会让 OTA 很不稳定。
总结
OTA 分片下载的难点不在主流程,而在边界条件。
一个稳定的 OTA 分片下载实现,至少要做到:
- 每片长度根据剩余大小动态计算。
- 最后一片不能越界。
- 固件大小不能超过 Flash 分区。
- Flash 擦除和写入要考虑对齐。
- 下载长度、接收长度、写入长度必须一致。
- 只有写入成功后才能更新 offset。
- 校验通过后才能设置升级标志。
这些细节处理好之后,OTA 的稳定性会提升很多。尤其是在网络不稳定的设备上,边界处理和失败恢复往往比“正常流程能跑通”更重要。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)