尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+W5500网络Bootloader实现:远程固件升级方案与踩坑总结

STM32+W5500网络Bootloader实现:远程固件升级方案与踩坑总结 1. 项目背景为什么偏偏要做带网络的Bootloader先交代一下背景。我手上有个设备用的STM32F103C8T6主控通过SPI挂了一颗W5500做以太网通信。之前的固件升级方式是现场开盖、飞线接串口、用ISP工具烧写——这套流程在实验室里没问题可一旦设备部署到客户现场麻烦就来了。客户那里没有调试工具也不会操作烧录软件每次升级都要派工程师出差成本高不说来回折腾还容易把排线搞坏。后来被逼着做了版远程升级方案就是这篇博文要说的STM32 W5500 Bootloader。做完之后实测升级一次固件不到30秒完全不需要开盖客户在网页上点一下按钮就行。这个方案的核心思路不复杂Bootloader跑在芯片里通过W5500从网络服务器拉取固件写入内部Flash然后跳转到App执行。听完是不是觉得也就那么回事但真正落地的时候坑比想象中多得多。这篇东西适合谁看手里有STM32 W5500类似组合、想实现网络远程升级的朋友或者正在琢磨Bootloader原理、想自己写一版Bootloader的开发者。我会把从方案选型、分区规划、协议设计到代码实现的完整过程都讲一遍尤其是那些“不踩一次根本不知道”的细节。2. 整体方案设计从“能用”到“好用”的关键决策2.1 为什么选W5500而不是直接上以太网协议栈做远程升级第一个要回答的问题是用什么样的网络接入方式方案其实有好几条路STM32 以太网MAC PHY芯片比如LAN8720跑LWIP、用ESP8266这类WiFi模组、或者直接上W5500这种带硬件TCP/IP协议栈的以太网控制器芯片。我最终选了W5500核心原因是它把TCP/IP协议栈做进了硬件里。STM32F103C8T6这颗芯片只有64KB Flash、20KB RAM跑LWIP虽然也能跑但占用的资源不少还要处理一堆协议细节。W5500通过SPI接口跟MCU通信TCP、UDP、ICMP这些协议都在芯片内部处理完了MCU只需要读写Socket寄存器、收发缓冲区就够了。这对资源紧张的F103来说是巨大解放。另外一个现实因素是设备本身就要用W5500做业务通信把Bootloader建立在同一套硬件抽象上代码复用程度高不用额外增加硬件成本。这里有个经验Bootloader尽量跟业务代码共用一套底层驱动但底层驱动本身要写得足够精简、稳定因为Bootloader空间有限容不下臃肿的代码库。2.2 Flash分区规划一次规划长期受益Bootloader最基础也最容易出错的地方就是Flash分区。规划不合理后面升级功能扩展就会很被动。我的F103C8T6有64KB Flash起始地址0x08000000页大小1KB。分区如下分区地址范围大小用途Bootloader0x08000000 - 0x08003FFF16KB引导程序 网络升级功能App0x08004000 - 0x0800BFFF32KB业务应用程序Flag/参数区0x0800C000 - 0x0800FFFF16KB升级标志、版本信息、备份区域Bootloader放16KB是够用的W5500驱动加上HTTP客户端、Flash擦写、校验逻辑编译出来大概11KB左右留了点余量。App占32KB对于我这个业务逻辑不算太复杂的设备来说够用。后面16KB是参数区和预留的备份区。这里有个重要决策Bootloader的链接脚本必须把Flash起始地址设为0x08000000而App的链接脚本要设为0x08004000同时中断向量表也要重映射。App编译出来的固件是带地址信息的但下载到Flash时位置固定所以固件文件本身不需要做额外处理。2.3 主动升级与被动升级到底选择哪种模式远程升级按触发方式大致分两类主动升级设备主动去服务器拉取固件和被动升级服务器下发指令设备被动接收固件。我这边需求是“客户在后台点一下设备就能升级”所以我做了主被动结合设备上电时先跑Bootloader检查有没有升级请求标志没有就直接进App。平时App收到服务器的升级指令后通过写入特定标志位并复位触发Bootloader进入升级流程。这个设计的好处是即使App因为意外崩溃跑不起来了设备上电仍然会先进入Bootloader只要Bootloader区没被破坏就能继续通过网络恢复系统。这就是业界常说的“A/B分区”思路的简化版——我没有做双备份但通过Bootloader提供了最基本的安全网。3. Bootloader核心机制详解3.1 升级标志位的设计最简单的状态机标志位是整个升级流程的“指挥棒”。我把标志位放在Flash的Flag区用一个独立的16位变量来表示当前状态。为什么要用16位而不是8位因为8位变量在意外写入时容易产生歧义而16位可以用0xAA55 / 0x55AA这种双字节模式来降低误判概率。定义几种状态0x0000无升级请求直接跳转App0xAA55有升级请求进入网络升级流程0x55AAApp已通过完整性校验可以拉起App这里有一个关键操作写Flash前必须先擦除。F103的Flash按页擦除每页1KB擦除后全页为0xFF。所以写入标志位前要把整个Flag页擦掉再写。而且要注意写Flash时如果恰好在执行中断服务程序可能导致总线错误——所以写入操作前要关闭中断写完再恢复。这是很多新手容易忽略的地方。#define FLAG_APP_OK 0x0000 #define FLAG_UPDATE_REQ 0xAA55 #define FLAG_UPDATE_DONE 0x55AA void set_update_flag(uint16_t flag) { __disable_irq(); FLASH_Unlock(); FLASH_ErasePage(FLAG_PAGE_ADDR); FLASH_ProgramHalfWord(FLAG_PAGE_ADDR, flag); FLASH_Lock(); __enable_irq(); }3.2 跳转App的关键中断向量表重映射Bootloader跳转App时核心动作是修改栈顶指针和复位向量然后跳转过去。但光跳转还不够如果中断向量表还指向Bootloader区域App里的任何中断调用都会跳到Bootloader的向量表里执行结果就是程序跑飞或者进入HardFault。STM32F103提供了两种中断向量表重映射方式一种是通过SYSCFG_MEMRM寄存器把向量表SRAM映射另一种是直接操作VTOR寄存器Cortex-M3内核支持。F103属于Cortex-M3内核可以直接用VTOR。typedef void (*pFunction)(void); void jump_to_app(void) { uint32_t app_address APP_START_ADDR; pFunction app_entry; if (((*(volatile uint32_t *)app_address) 0x2FFE0000) 0x20000000) { app_entry (pFunction)(*(volatile uint32_t *)(app_address 4)); __disable_irq(); SCB-VTOR app_address; __set_MSP(*(volatile uint32_t *)app_address); app_entry(); } }注意那个((*(volatile uint32_t *)app_address) 0x2FFE0000) 0x20000000的判断这是在检查App起始地址处的栈顶指针是否合法。STM32的RAM地址范围是0x20000000开始所以栈顶值必须在RAM范围内否则说明Flash里没有有效的App代码不能跳转。这个检查是保险丝不要省。另外进入App前要把外设恢复到复位状态。比如我在Bootloader里初始化了W5500、串口、定时器等如果不做DeInitApp启动后这些外设的状态可能还是Bootloader残留的配置导致初始化冲突。3.3 网络升级协议简单可靠才是硬道理网络升级协议我设计得很朴素核心就三步查询版本、下载固件、校验重启。查询版本设备向服务器发一个GET请求携带当前App版本号服务器返回“有新版/无新版”的响应。这一步不是必须的但能避免重复下载相同版本。下载固件设备向服务器发HTTP Range请求从指定偏移位置拉取固件数据。这里有个经验不要一次性把整个固件都拉进RAM再写Flash因为F103的RAM只有20KB而固件可能有30KB放不下。正确做法是边下载边写入把Flash按页擦除每收到一块数据就写入对应的Flash页。因为W5500的Socket接收缓冲区和F103的RAM都有限我每次拉取1KB数据擦除1页、写入1页然后请求下一个1KB。这样做虽然请求次数多了点但逻辑简单不容易出缓冲区溢出的问题。校验固件下载完成后对整个App区做CRC32校验跟服务器发送的CRC值比对。不一致就标记升级失败清掉标志位尝试回滚。因为我这边没做完整的A/B分区备份回滚的策略是如果App校验失败就继续保持Bootloader状态等待重新下载而不是跳转到一个可能半残的App。提示网络协议层一定不要图省事省略超时重传机制。实测中路由器偶发丢包、网线接触不良都会导致TCP连接中断。我的实现里每个下载块如果5秒内没有收到响应就重试3次3次都失败则中止升级把标志位复位防止设备卡死在升级状态。4. 实操过程从零搭建完整升级链路4.1 硬件准备与接线验证硬件材料清单STM32F103C8T6最小系统板或者你自己的板子W5500以太网模块网络服务器一台PC装HTTP服务器软件就行开发阶段可以这样搞USB转TTL串口工具调试用看日志输出ST-Link V2烧录Bootloader用接线方面W5500通过SPI连接。我的用法是SPI1用作W5500通信PA4作为片选PB0作为W5500的中断引脚。接线表如下W5500模块STM32F103SCLKPA5 (SPI1_SCK)MOSIPA7 (SPI1_MOSI)MISOPA6 (SPI1_MISO)SCSPA4 (片选软件控制)RSTPB1 (复位引脚软件控制)INTPB0 (中断引脚可不用)有一个坑先提醒W5500的供电一定要做好这芯片对电源纹波比较敏感。我最初用面包板飞线的时候经常出现芯片一会儿能初始化一会儿不能后来查到是供电问题。建议用独立的3.3V LDO供电并且MCU和W5500的电源分别加100nF去耦电容。WiFi/以太网芯片这种高频器件供电不稳会出现各种诡异问题。验证硬件是否OK的简易方法给W5500上电后读它的版本寄存器地址0x0039正常会返回0x04。如果读出来是0xFF或0x00多半是SPI通信没通或者芯片供电有问题。uint8_t w5500_read_version(void) { uint8_t ver 0; w5500_read_register(0x0039, ver, 1); return ver; } // 正常返回值0x044.2 工程搭建Bootloader与App的工程配置工程我是基于标准外设库SPL搭的因为个人用习惯了HAL库也行区别不大但要注意几个关键配置点。Bootloader工程Flash起始地址0x08000000编译优化-O1兼顾性能和体积需要启用串口调试日志、SPI通信、定时器超时管理App工程Flash起始地址0x08004000编译优化-O2业务代码优先性能中断向量表偏移在SystemInit里或main开头设置SCB-VTOR 0x08004000App工程里必须在main函数最开始设置VTOR否则中断会跑飞。我见过有人把VTOR设置放在外设初始化之后结果UART中断一开启就死机——CPU收到中断去向量表找入口向量表还是Bootloader的于是跳到错误的地方。这个教训被我记了整整一个晚上。还有一点App编译生成的.hex文件包含了从0x08004000开始的地址信息烧录Bootloader时不会覆盖App区域两者互不干扰。升级时服务器托管这个.hex文件即可不需要做额外转换。但如果用的.bin文件要注意下载到Flash的地址偏移。4.3 W5500初始化与Socket配置W5500的初始化相对固定但有几个细节直接决定了网络通信的稳定性。void w5500_init_network(void) { uint8_t mac[6] {0x00, 0x08, 0xDC, 0x01, 0x02, 0x03}; uint8_t ip[4] {192, 168, 1, 100}; uint8_t gw[4] {192, 168, 1, 1}; uint8_t mask[4] {255, 255, 255, 0}; w5500_soft_reset(); w5500_set_mac(mac); w5500_set_ip(ip); w5500_set_gateway(gw); w5500_set_subnet_mask(mask); }三个要点第一软复位后要等待芯片稳定。W5500的软复位是写MR寄存器的RST位复位后建议延时10ms以上再进行寄存器配置否则可能写不进去。第二Socket的发送/接收缓冲区大小要按需配置。W5500每个Socket的收发缓冲区通过Sn_RXBUF_SIZE和Sn_TXBUF_SIZE寄存器配置单位是KB。默认是2KB我把它改成了发送2KB、接收4KB。为什么接收要大一点因为HTTP响应可能包含头部和正文如果缓冲区太小一次收不下完整数据就得处理分包逻辑会复杂很多。缓冲区大一点一次读到的数据多协议处理简单。第三Socket超时时间很关键。W5500的Socket有一个超时寄存器Sn_RTIMR/Sn_RETR控制TCP重传的超时和重试次数。默认值偏保守如果网络环境良好可以把重试次数调小一点这样握手失败能快速反馈不会傻等半分钟。我实测在局域网环境把重试次数从8次调到3次连接失败从肉眼可见的卡顿变成了秒级反馈用户体验提升明显。4.4 HTTP客户端实现比想象中简单HTTP客户端这块不用上复杂的库Bootloader里手动拼HTTP报文就行。因为功能就两个发GET请求下载固件、发GET请求查版本。// 拼HTTP GET请求 char http_request[] GET /firmware/app.bin HTTP/1.1\r\n Host: 192.168.1.50\r\n Connection: close\r\n \r\n;把这段字符串通过W5500的Socket发送出去然后等待接收响应。收到响应后先解析HTTP头找到Content-Length字段确定固件总长度。然后跳过HTTP头以\r\n\r\n为界剩下的就是固件二进制数据。这里分享一个解析HTTP头的小技巧将收到的数据存在一个局部缓冲区里用strstr函数查找\r\n\r\n找到后记录这个位置的偏移固件数据从这个偏移之后开始。注意HTTP头可能跨多个TCP段所以要先完整接收头部再开始处理固件数据不要边收边找容易乱。固件下载我用的Range请求方式char http_range_request[] GET /firmware/app.bin HTTP/1.1\r\n Host: 192.168.1.50\r\n Range: bytes0-1023\r\n Connection: close\r\n \r\n;每次只请求1KB服务器返回206 Partial Content附带这个范围的固件数据。收到后写入Flash然后请求下一个Range。这么做的优点是完全不怕缓冲区不够缺点是需要连续发很多次TCP请求耗时稍长。实测32KB固件大概发32次请求局域网内总耗时2-3秒完全可以接受。4.5 Flash写入的时序问题Flash写入是升级流程里最容易出事的地方。STM32F103的Flash写入有几点要特别注意写入前必须擦除擦除以页为单位1KBFlash编程时间有限制一般几十微秒到几毫秒期间CPU要等待写Flash时不能处理中断否则可能导致总线错误或者写入失败Flash擦写次数有限典型寿命是1万次Bootloader里不要频繁擦写Flag区我上面的实现里每下载1KB数据到内部缓冲区就执行一次“擦除1页 写入1页”的操作。写入完成后再发送下一个Range请求。这样串行处理不容易出错。void write_firmware_block(uint32_t address, uint8_t *data, uint16_t len) { uint16_t i; FLASH_Unlock(); FLASH_ErasePage(address); for (i 0; i len; i 2) { FLASH_ProgramHalfWord(address i, *(uint16_t *)(data i)); } FLASH_Lock(); }有个细节FLASH_ProgramHalfWord要求数据按半字16位对齐而W5500收到的数据是字节流可能不是偶数长度所以处理时要考虑边界。我的做法是缓冲区固定1024字节最后一块数据如果不满足1024字节用0xFF补齐后再写入因为0xFF正好是Flash擦除后的状态不影响实际固件内容。5. 实际调试中踩过的坑与排查思路5.1 跳转App后死机怎么回事第一次实现跳转App打印了启动日志后直接HardFault。排查了一圈发现两个问题一是App工程里VTOR设置得太晚在main函数里设置VTOR之前系统时钟初始化SystemInit已经执行完但此时如果有中断发生比如SysTick就会走错向量表。二是Bootloader里没有把用到的外设DeInitApp初始化UART时UART的状态还是Bootloader残留的导致配置冲突。解决方式App的main函数第一行就设置VTOR然后立即DeInit所有外设。Bootloader跳转前先做外设DeInit把时钟和引脚恢复到默认状态再跳转。这样两边都干净了。5.2 W5500连接服务器超时开发阶段经常遇到W5500发出连接请求后迟迟得不到服务器响应最后返回超时。排查思路从硬件到软件一步步来先检查网络线路W5500的Link灯是否亮起网线通不通。再检查IP配置服务器和设备必须在同一网段网关配置是否正确子网掩码是否匹配。然后检查服务器防火墙HTTP服务端口默认80是否被防火墙拦截局域网内其他设备能不能正常访问。最后看代码Socket的配置是否正确Sn_CR寄存器命令是否正常触发。我遇到的一个典型案例服务器用的Windows防火墙默认拦截了来自外部的HTTP请求我能ping通设备但设备连不上服务器。关掉防火墙后一切正常。这个问题排查了快两个小时说多了都是泪。5.3 下载到一半失败Flash擦写占用了太多时间升级过程偶尔会在中途失败表现为某个Range请求超时。后来定位到原因Flash擦写时CPU忙于等待Flash操作完成导致W5500的Socket接收缓冲区溢出丢掉了服务器下发的数据。这个问题的本质是写Flash时W5500还在接收网络数据MCU处理不过来。解决办法两个一是调大W5500的Socket接收缓冲区给网络数据更多的缓存空间二是在写Flash之前先暂停Socket接收等Flash写完再恢复。我采用了后者——写Flash前调用w5500_socket_close(socket)关闭Socket写完后重新连接、重新发起Range请求。虽然每次写Flash都要断开重连增加了连接次数但整体可靠性提升了非常多实测从偶尔失败变成从未失败。5.4 调试工具与日志没有日志寸步难行Bootloader这种“裸机”程序调试手段比App少很多。我的经验是串口日志一定要有这是Bootloader开发的生命线。在Bootloader里通过UART输出调试信息比如当前状态、收到的数据长度、校验结果、错误码能把开发调试效率提升好几倍。硬件上用USB转TTL模块接UART1即可波特率1152008N1。Bootloader启动时先打印一段信息然后打印当前状态比如[BOOT] Bootloader v1.0 [BOOT] Check flag: 0x0000 [BOOT] Jump to App 0x08004000有这行日志跳转是否成功一目了然。升级过程中打印[BOOT] Start update, total size: 32768 [BOOT] Download block 0, offset 0 [BOOT] Download block 1, offset 1024 ... [BOOT] CRC check OK, jump to App每条日志对应一个关键节点哪里卡住了一眼就能看出来。还有个工具推荐STM32 ST-LINK Utility。它不仅可以烧录程序还能读取Flash内容。调试时如果怀疑App区写入不正确可以用它读出来跟原始固件比对定位是哪个字节写错了。另外升级失败后设备无法启动也可以用它在Boot模式下擦除整个Flash重新烧录Bootloader恢复正常状态。6. 常见问题速查表问题现象可能原因排查/解决方法跳转App后死机VTOR未设置或设置太晚App的main第一行设置SCB-VTOR 0x08004000跳转App后死机Bootloader外设未DeInit跳转前HAL/SPL的DeInit所有外设W5500初始化失败SPI接线错误检查SPI引脚连接读版本寄存器确认通信W5500初始化失败供电不足/纹波过大加LDO供电 去耦电容TCP连接超时防火墙拦截检查服务器防火墙放行HTTP端口TCP连接超时网段/网关配置错误检查IP、网关、子网掩码配置下载中途失败Flash写期间Socket缓冲区溢出写Flash前关闭Socket写完重连下载完成后校验失败App固件不完整检查服务器固件文件是否正常确认Range请求偏移正确升级后App无法运行App分区被破坏用ST-LINK Utility重新烧录BootloaderAppFlag区误写中断导致Flash写入不安全写Flash前关中断写完恢复7. 优化空间与扩展建议把基础版本跑通之后如果想进一步把方案做扎实有几个方向是可以考虑的。第一个是A/B双分区。我用的是单App区 标志位方案升级过程中如果下载失败设备会停在Bootloader等待重新下载。A/B分区则是同时存两份App升级时写入非活动分区写完校验通过后切换启动分区失败就继续用原来的。这个方案的好处是升级失败几乎不影响设备运行代价是Flash占用翻倍对F103这种只有64KB Flash的芯片来说很捉襟见肘更适合F103RC256KB或者F4系列。第二个是增加TFTP或FTP支持。HTTP协议做升级够用但有的场景下服务器端更愿意用TFTP因为TFTP实现更简单、不用维护HTTP状态。W5500本身支持UDPTFTP就是基于UDP的底层实现反而更容易。缺点是TFTP没有加密和认证安全性差只能在可信内网使用。第三个是固件加密。现在固件文件明文放在HTTP服务器上任何人都能下载分析。如果产品对安全性有要求可以在固件里加签名Bootloader在下载完成后先验签再决定是否写入。具体做法是服务器在固件末尾附加一段RSA签名Bootloader使用内置公钥验证签名。代码空间和计算时间都会增加但安全性提升明显。第四个是看门狗。Bootloader运行过程中一旦发生死循环或者异常卡住设备就彻底变砖了只能开盖重新烧录。在Bootloader里启用独立看门狗IWDG主循环定期喂狗一旦卡在某个步骤超过设定时间芯片自动复位重来这是一种低成本的安全兜底。我个人体会是Bootloader这种模块属于“做的时候觉得没什么出问题了才意识到重要”的东西。它不产生业务价值但直接决定设备能不能安全、稳定地升级。技术含量不在于“知道怎么写跳转代码”或者“知道W5500怎么联网”而在于把边界情况想全网络断了怎么办、Flash写坏了怎么办、App校验失败怎么办——把这些场景逐一处置好这个Bootloader才算真正能用。后面如果时间允许我打算把A/B分区和固件签名加上再把上位机管理平台完善一下做成一套能自动检测新版本、批量推送升级的完整系统。到时有新的踩坑经验再来跟大家分享。
返回列表