尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32+EC800 OTA升级实战:Flash分区、CRC32校验与回滚机制

发布时间:2026/9/28 16:15:08

资讯中心
01
ARTICLE

STM32+EC800 OTA升级实战:Flash分区、CRC32校验与回滚机制

STM32+EC800 OTA升级实战:Flash分区、CRC32校验与回滚机制
1. 为什么EC800的OTA升级值得单独拿出来讲移远EC800这颗4G Cat.1模块这两年出货量非常大价格便宜、功耗控制得不错、AT指令集也相对规整很多做远程抄表、共享设备、环境监测的团队都在用。但真正把OTA升级跑通、跑稳的人并不多。我见过太多项目前期功能都调通了一到远程升级环节就翻车——要么升级包传了一半断了要么CRC校验对不上导致设备变砖要么升级完新固件起不来又没有回滚机制。STM32这边的OTA升级本质上要解决三个问题固件从哪里来、怎么安全地写进去、写坏了怎么办。EC800负责第一个问题它通过4G网络从服务器拉取固件包STM32负责后两个问题它要把固件写到内部Flash或外部存储校验完整性然后跳转执行。CRC校验是保证写进去的东西和服务器上的一模一样的关键手段错误处理则是保证万一不一样设备还能活着的兜底逻辑。这篇文章面向的是已经能用STM32驱动EC800做基本通信、现在想把OTA升级做扎实的开发者。我会从Flash分区规划开始讲到EC800的HTTP/HTTPS取包流程、CRC32校验的实现、双Bank切换与回滚机制最后给出几个我在实际项目中踩过的坑和对应的处理方案。代码基于STM32 HAL库EC800走AT指令不依赖特定云平台你可以直接移植到自己的项目里。注意OTA升级涉及Flash擦写和程序跳转调试阶段务必保留SWD接口可用否则一旦跳转失败就只能拆机接串口了。2. Flash分区规划升级方案的地基2.1 为什么不能把新固件直接覆盖到运行区很多人第一反应是收到固件包直接擦掉原来的APP区写进去重启。这个做法在实验室里能跑通但在现场设备上极其危险。原因很简单——擦写过程中一旦断电、断网或模块复位设备就彻底变砖了因为原来的程序已经被擦掉新的还没写完。正确的做法是在Flash里划出至少两个区域一个放当前运行的固件Active区一个放待升级的新固件Download区或Backup区。新固件先完整写入Download区并校验通过再通过一个升级标志告诉Bootloader下次启动时把Download区的内容搬到Active区或者直接把执行入口切到Download区。这样即使升级过程中出问题原来的Active区始终是完整的。2.2 以STM32F103C8T6为例的分区表STM32F103C8T6有64KB Flash分区要精打细算。下面是我在一个实际项目中用的方案区域名称起始地址大小用途Bootloader0x0800000012KB启动判断、固件搬运、跳转参数区0x080030002KB升级标志、固件版本、CRC值Active APP0x0800380024KB当前运行固件Download APP0x0800980024KB待升级固件暂存保留0x0800F8002KB预留扩展Bootloader放在最前面是因为STM32上电后固定从0x08000000取栈顶指针和复位向量这个位置改不了。参数区单独划出来是为了避免每次升级都擦写Bootloader所在的页STM32F1的Flash页大小是1KB擦写次数有限分开管理更安全。Active区和Download区大小必须一致且要能装下你的APP。24KB对于功能不太复杂的应用够用如果你的固件超过这个尺寸要么换更大Flash的型号比如C8T6换成RCT6256KB要么外挂SPI Flash做存储。外挂Flash的好处是容量大、成本低坏处是读取速度慢跳转执行前需要搬运到内部RAM或内部Flash多了一步。2.3 参数区的数据结构设计参数区虽然只有2KB但要存的东西不少。我通常定义一个结构体放在固定地址typedef struct { uint32_t magic; // 固定值0x5A5A5A5A用于判断参数区是否已初始化 uint32_t upgrade_flag; // 0无升级 1待升级 2升级成功待确认 uint32_t firmware_size; // 新固件字节数 uint32_t firmware_crc; // 新固件CRC32值 uint32_t firmware_version;// 版本号如0x00010002表示V1.2 uint8_t reserved[236]; // 预留 } OTA_Param_t;magic字段很关键。第一次烧录时参数区是0xFF读出来magic不对Bootloader就知道要初始化参数区。upgrade_flag用三个状态而不是简单的0/1是为了支持升级成功待确认机制——新固件启动后要主动把flag改成0表示我跑起来了否则Bootloader下次启动会认为升级失败并回滚。这个机制后面会详细讲。3. EC800取包从AT指令到固件落盘3.1 EC800的HTTP AT指令流程EC800支持HTTP和HTTPS走AT指令操作。整个取包流程分几步配置HTTP上下文、设置URL、发起GET、读取响应头、分块读取响应体、关闭连接。下面是我实际用的指令序列ATQHTTPCFGcontextid,1 # 使用PDP上下文1 ATQHTTPCFGresponseheader,1 # 开启响应头输出 ATQHTTPURL64,10 # 设置URL长度64超时10秒 # 模块返回CONNECT后发送URL字符串 ATQHTTPGET60 # 发起GET超时60秒 # 等待QHTTPGET: 0,200,body_len ATQHTTPREAD60 # 读取响应体超时60秒 # 模块返回CONNECT开始输出数据这里有几个细节值得说。ATQHTTPURL设置完长度后模块会返回CONNECT此时要在规定时间内把URL字符串发过去不能带回车换行。ATQHTTPGET的响应里会带上HTTP状态码和响应体长度这个长度就是固件包的字节数要存到参数区里用于后续校验。ATQHTTPREAD读出来的数据是原始二进制不是文本。如果你用串口助手调试会看到一堆乱码这是正常的。关键是要在STM32端做好接收缓冲EC800输出数据的速度取决于串口波特率我一般用115200读24KB大概需要2-3秒。3.2 分块接收与Flash写入的配合EC800的ATQHTTPREAD是一次性把整个响应体吐出来的但STM32的串口接收缓冲不可能开24KB那么大RAM不够。我的做法是开一个2KB的环形缓冲串口中断往里填主循环从环形缓冲取数据攒够1KB就写一次Flash。写Flash要注意STM32F1的Flash编程单位是半字16位也就是每次必须写2字节。所以攒的数据长度最好是偶数。另外Flash写入前必须先擦除而擦除的最小单位是页1KB。这意味着你不能边收边擦边写——擦除会把整页清掉如果这一页已经写了数据再擦就丢了。正确的顺序是先擦除整个Download区再从头开始顺序写入。擦除24KB需要擦24页每页擦除时间大概20-40ms总共不到1秒。擦完之后再开始接收数据收到1KB就写1KB写完一页再写下一页不能跳着写。// Flash写入函数示例HAL库 HAL_StatusTypeDef flash_write(uint32_t addr, uint8_t *data, uint32_t len) { HAL_FLASH_Unlock(); for (uint32_t i 0; i len; i 2) { uint16_t halfword data[i] | (data[i1] 8); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr i, halfword) ! HAL_OK) { HAL_FLASH_Lock(); return HAL_ERROR; } } HAL_FLASH_Lock(); return HAL_OK; }3.3 断点续传的取舍理论上可以做断点续传记录已经下载了多少字节下次从断点继续。但实际做下来我觉得对于24KB这个量级的固件断点续传的复杂度不值得。原因有三一是EC800的HTTP GET不支持Range头至少我用的固件版本不支持没法从中间开始取二是断点信息本身要存Flash增加了擦写次数三是24KB在4G网络下也就几秒钟的事重头再来成本很低。所以我的策略是要么一次下完要么全部重来。下载过程中如果超时或CRC不对直接清空Download区重新发起GET。这样逻辑简单可靠性反而更高。如果你的固件到了几百KB甚至MB级别那断点续传就有必要了可以考虑用EC800的FTP模式或者自己实现分片协议。4. CRC32校验不只是算个值那么简单4.1 为什么选CRC32而不是简单求和校验固件完整性最朴素的做法是把所有字节加起来取低16位。但这个做法有个致命问题它检测不出字节顺序错误。比如固件里有两个字节0x12和0x34不管它们是0x12 0x34还是0x34 0x12求和结果都是0x46。如果传输过程中字节顺序乱了简单求和发现不了。CRC32循环冗余校验则对字节顺序敏感而且能检测出绝大多数随机错误和突发错误。它的原理是把数据看成一个巨大的二进制多项式除以一个固定的生成多项式余数就是CRC值。STM32有硬件CRC外设但只支持CRC32的特定多项式0x04C11DB7和标准CRC32一致可以直接用。不过要注意STM32硬件CRC外设的输入是32位字不是字节。如果你按字节喂数据需要做移位处理。我一般直接用软件查表法速度快、移植方便不依赖特定硬件。4.2 软件CRC32查表实现static const uint32_t crc32_table[256] { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, /* ... 省略中间项 ... */ }; uint32_t crc32_calc(uint8_t *data, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc32_table[(crc ^ data[i]) 0xFF]; } return crc ^ 0xFFFFFFFF; }这个表有256个32位值占1KB Flash。如果你Flash紧张可以用半字节查表16项速度慢一点但省空间。实测在STM32F103上72MHz主频算24KB数据的CRC32大概需要3-5ms完全可以接受。4.3 校验时机与失败处理CRC校验要在两个地方做下载完成后校验一次确认Download区的内容和服务器一致Bootloader搬运前再校验一次确认存放期间没有出问题虽然概率极低但Flash位翻转不是不可能。校验失败的处理逻辑要分场景。如果是下载后校验失败说明传输过程出了问题直接清空Download区、清除升级标志、重新下载。如果是Bootloader搬运前校验失败说明Download区的数据不可信此时绝对不能搬运应该清除升级标志继续运行原来的Active区同时记录一个错误码等下次联网时上报给服务器。提示CRC值本身也要存到参数区而且参数区的写入要在CRC校验通过之后。顺序是下载完成 → 算CRC → 比对 → 通过则写参数区 → 置升级标志。5. Bootloader的搬运逻辑与回滚机制5.1 Bootloader的启动判断流程Bootloader上电后要做的事按优先级排先检查参数区magic是否有效无效则初始化然后检查upgrade_flag如果是待升级校验Download区CRC通过则搬运不通过则清除标志如果是升级成功待确认说明上次升级后新固件没起来需要回滚最后跳转到Active区执行。这个流程里升级成功待确认状态是最容易被忽略的。它的逻辑是Bootloader搬运完新固件后把flag置为待确认然后跳转到新固件。新固件启动后在初始化完成的某个时间点比如连上服务器、或者跑完自检主动把flag改为0。如果新固件有bug起不来flag就一直是待确认下次重启时Bootloader就知道上次升级失败了于是把Active区恢复成备份的旧固件。5.2 搬运过程中的断电保护搬运24KB数据按半字编程大概需要24KB/212288次编程操作每次几十微秒总共不到1秒。但这1秒内如果断电Active区就被写了一半原来的固件也毁了。所以搬运前必须先把旧固件备份到另一个区域。我的做法是Download区其实承担了双重角色——升级时它是新固件的暂存区搬运时它是旧固件的备份区。具体流程是先把Active区的旧固件复制到Download区此时Download区的新固件已经被校验过可以丢弃复制完成后再把新固件从Download区搬到Active区。这样任何时刻至少有一个完整的固件存在。等等这里有个矛盾Download区只有一个既存新固件又存旧固件放不下。所以实际方案要么是三个区Active、New、Backup要么是搬运时先把新固件从Download区读到RAM再擦Active区再从RAM写Active区。24KB的RAM对STM32F103C8T620KB RAM来说放不下所以只能用三区方案或者外挂Flash。对于Flash只有64KB的型号三区方案确实紧张。我的建议是如果固件超过16KB直接换256KB Flash的型号比如STM32F103RCT6分区就宽裕多了。省下来的调试时间和变砖风险远比芯片差价值钱。5.3 跳转到APP的代码细节跳转前要做的准备关闭所有中断、关闭外设时钟、设置主栈指针为APP区的第一个字、设置PC为APP区的第二个字。代码如下typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t stack_top *(volatile uint32_t*)app_addr; uint32_t reset_handler *(volatile uint32_t*)(app_addr 4); __disable_irq(); HAL_DeInit(); SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; SCB-VTOR app_addr; // 重定向中断向量表 __set_MSP(stack_top); pFunction app_entry (pFunction)reset_handler; app_entry(); }SCB-VTOR这行很关键。APP区的固件编译时链接地址是0x08003800它的中断向量表也在那个位置。如果不重定向VTOR中断发生时CPU还是去0x08000000找向量表就会跳到Bootloader的中断处理函数导致APP的中断全部失效。这个问题我在早期项目中遇到过现象是APP能跑但串口接收中断不触发查了半天才发现是VTOR没设。6. 错误处理那些让你半夜爬起来的情况6.1 EC800取包失败的分类处理EC800取包可能失败在多个环节每种失败的处理方式不同失败环节典型现象处理策略PDP激活失败ATQIACT返回ERROR检查SIM卡、天线、APN配置重试3次后上报HTTP连接失败ATQHTTPGET返回错误码检查URL、服务器状态退避重试响应码非200QHTTPGET: 0,404,0固件不存在或URL错误清除升级标志读取超时ATQHTTPREAD无响应复位HTTP上下文重新发起数据长度不符实际接收字节数≠响应头声明清空Download区重新下载退避重试的策略很重要。不要失败后立刻重试那样如果服务器挂了设备会疯狂发请求。我的做法是第一次失败等30秒第二次等2分钟第三次等10分钟三次都失败就放弃本次升级等下一个升级周期比如24小时后再试。6.2 Flash写入失败的兜底Flash写入失败在正常情况下很少见但一旦发生就很麻烦。可能的原因擦除不彻底、地址越界、供电不稳导致编程失败。HAL库的HAL_FLASH_Program返回错误时我的处理是立即停止写入、清除升级标志、记录错误码、重启设备。不要试图再写一次因为Flash编程失败往往意味着硬件状态异常继续写可能把参数区也搞坏。还有一个隐蔽的坑Flash擦写期间如果EC800正在接收数据串口中断会打断Flash操作。STM32F1的Flash编程期间CPU从Flash取指会被暂停如果中断服务函数也在Flash里就会导致中断响应延迟甚至丢失。我的做法是写Flash前先关闭串口接收中断写完再开。或者把中断服务函数放到RAM里执行但这需要修改链接脚本比较麻烦。6.3 升级后外设不工作的排查思路新固件跑起来了但串口不通、屏幕不亮、传感器读不到——这种情况我遇到过好几次。根本原因通常是新固件的初始化代码和Bootloader有冲突。比如Bootloader开了某个外设时钟没关APP又初始化一次导致外设状态异常。排查方法在APP的初始化代码最前面先执行一次HAL_DeInit()或者手动复位所有外设再重新初始化。另外APP里要重新配置SysTick因为Bootloader跳转前把SysTick关了。还有NVIC的中断优先级分组也要重设Bootloader和APP如果分组不同中断行为会不一致。注意APP的链接地址必须和分区表里的Active区起始地址一致否则跳转后取指会跑飞。Keil里在Target选项的IROM1里设置IAR里在Linker的Vector Table里设置。7. 实测数据与几个反直觉的结论7.1 升级耗时拆解我在一个实际项目里测过完整升级流程的耗时EC800走4G信号良好STM32F103C8T672MHz阶段耗时占比PDP激活HTTP连接3-5秒约20%固件下载24KB8-12秒约50%CRC校验3-5毫秒忽略不计Flash擦除写入1-2秒约10%Bootloader搬运跳转1-2秒约10%新固件启动自检1-3秒约10%总耗时大概15-25秒。其中下载占了大部分时间这是4G网络带宽和EC800处理速度共同决定的。想缩短时间要么压缩固件体积开-Os优化、去掉不用的库要么提高串口波特率EC800支持到921600但STM32F1的串口在高波特率下容易出错我一般用115200或230400。7.2 CRC32的碰撞概率到底有多低有人担心CRC32会碰撞——两个不同的固件算出相同的CRC值。理论上确实可能CRC32有2^32种取值对于24KB的固件随机碰撞概率大约是2^-32也就是约23亿分之一。这个概率比你设备被雷劈中还低。如果实在不放心可以在CRC之外再加一个MD5或SHA1但那样计算量大很多对STM32F1来说不划算。我的观点是CRC32用于固件完整性校验足够了。它防的是传输错误和存储错误不是防恶意篡改。如果要做安全升级那应该用数字签名那是另一个层面的问题。7.3 为什么我不推荐在APP里做升级逻辑有些方案把升级逻辑放在APP里APP收到升级指令自己下载固件自己写Flash自己跳转。这个做法的问题是APP在运行时要擦写自己所在的Flash区域风险极高。虽然技术上可以通过把擦写代码放到RAM里执行来规避但复杂度陡增而且一旦跳转失败连个兜底的Bootloader都没有。我的建议是APP只负责通信和触发真正的下载、校验、搬运全部由Bootloader完成。APP收到升级指令后置一个标志、重启剩下的交给Bootloader。这样职责清晰APP的代码量也小Flash占用少分区压力小。8. 写在最后几个让我印象深刻的现场问题第一个是EC800在弱信号下的HTTP超时。现场设备在地下室信号只有2格ATQHTTPGET经常超时。后来我把超时从60秒加到120秒并且在GET之前先发ATCSQ检查信号质量低于10就跳过本次升级。这个改动之后升级成功率从70%提到了95%以上。第二个是Flash擦除时的看门狗复位。Bootloader里开了独立看门狗擦除24页Flash需要1秒左右如果看门狗超时设得太短比如500ms擦到一半就被复位了。解决办法是在擦除循环里定期喂狗或者把看门狗超时设到3秒以上。第三个是参数区的写保护。有一次调试时手滑用ST-Link Utility把整个Flash都擦了参数区也没了。虽然Bootloader能重新初始化参数区但升级标志丢了设备就停在旧固件上不动了。后来我在参数区前后各加了一个magic值只有两个magic都对才认为参数有效这样即使部分被擦也能检测出来。OTA升级这件事说难不难说简单也不简单。核心就是把下载-校验-搬运-回滚这条链路做扎实每个环节都有失败预案。代码写完之后一定要做断电测试——在下载中、擦除中、搬运中分别拔电看设备能不能恢复。这个测试过了现场基本就不会出大问题了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。