STM32 BootLoader 实战(五):基于 W5500 网口的 YMODEM 升级 APP 固件
摘要
串口 YMODEM 升级适合调试和近距离维护,现场设备数量多以后,网口升级会更方便。W5500 自带硬件 TCP/IP 协议栈,STM32 只需要通过 SPI 操作 Socket,就可以做一个轻量级 TCP 升级通道。
这篇把前面的 YMODEM 接收逻辑搬到 W5500 TCP 连接上,重点处理下面几个问题:
- BootLoader 如何初始化 W5500
- BootLoader 做 TCP Server 还是 TCP Client
- TCP 是字节流,YMODEM 包解析要怎么适配
- 什么时候清除 APP 有效标志
- 网线拔掉、TCP 断开、升级超时以后怎么处理
- W5500 接收的数据如何写入 APP Flash
阅读前默认工程已经完成:
- BootLoader 固定运行在
0x08000000 - APP 已经按偏移地址链接
- APP 已经完成中断向量表重定位
- BootLoader 已经封装 Flash 擦写接口
- BootLoader 已经具备 APP 有效标志和参数区
- 串口 YMODEM 接收逻辑已经能跑通
目录
- 1. 为什么网口升级还可以继续用 YMODEM
- 2. W5500 网口升级整体流程
- 3. 硬件连接和启动条件
- 4. 网络参数和 Socket 规划
- 5. W5500 初始化代码
- 6. 建立 TCP Server
- 7. 把 TCP 接收适配成 YMODEM 字节接口
- 8. YMODEM over TCP 的接收流程
- 9. 上位机发送方式
- 10. 断线、超时和重复升级处理
- 11. 调试日志和状态码
- 12. 常见问题
- 13. 总结
1. 为什么网口升级还可以继续用 YMODEM
W5500 走 TCP 后,YMODEM 不是必需项。
TCP 已经保证数据按顺序到达,也会处理重传。理论上可以自己定义一个更简单的协议:
固件头 + 固件长度 + 固件 CRC + 固件数据继续使用 YMODEM 的原因主要有三个。
第一,前面串口升级已经写好了 YMODEM 包解析、文件大小解析、CRC16 校验、EOT 结束处理。网口升级只需要替换底层收发接口。
第二,YMODEM 第 0 包自带文件名和文件大小,BootLoader 可以直接拿文件大小检查 APP 分区。
第三,YMODEM 每包带 CRC16,即使 TCP 已经可靠,也能在应用层多做一次包级检查,调试时更容易定位问题。
所以这篇采用下面的思路:
串口升级: UART 接收字节 -> YMODEM 解析 -> Flash 写 APP 网口升级: W5500 TCP 接收字节 -> YMODEM 解析 -> Flash 写 APP上层 YMODEM 状态机尽量不改,只替换底层RecvByte和SendByte。
2. W5500 网口升级整体流程
这里让 BootLoader 作为 TCP Server。
PC 上位机作为 TCP Client,连接到设备固定 IP 和固定端口,然后通过这条 TCP 连接发送 YMODEM 数据。
流程如下:
STM32 上电 | v 运行 BootLoader | v 判断是否进入升级模式 | +-- 否:检查 APP 有效,跳转 APP | +-- 是:初始化 W5500 | v 配置静态 IP | v 打开 TCP Server | v 等待 PC 连接 | v 周期发送 'C' | v 接收 YMODEM 固件 | v 擦除 APP 区域 | v 写入 APP Flash | v 校验 APP | v 写 APP 有效标志 | v 复位TCP Server 模式更适合 BootLoader:
- 设备 IP 固定,上位机主动连接
- BootLoader 不需要知道上位机 IP
- 多台设备现场维护时,按 IP 逐个升级
- 逻辑比 TCP Client 更直观
如果产品现场 IP 不固定,也可以在 BootLoader 里跑 DHCP。但 BootLoader 阶段越简单越稳,基础版本先用静态 IP。
3. 硬件连接和启动条件
W5500 和 STM32 通过 SPI 通信。常见连接如下:
W5500 SCS -> STM32 SPI_NSS 或普通 GPIO W5500 SCLK -> STM32 SPI_SCK W5500 MISO -> STM32 SPI_MISO W5500 MOSI -> STM32 SPI_MOSI W5500 RST -> STM32 GPIO W5500 INT -> STM32 GPIO,可选 W5500 3V3 -> 3.3V W5500 GND -> GNDBootLoader 中至少需要控制:
- SPI 初始化
- CS 片选
- RST 复位
- W5500 读写寄存器
- Socket 状态轮询
升级模式可以通过下面几种方式进入:
按键进入升级模式 APP 写升级标志后复位 BootLoader 检查 APP 无效后进入 上电后等待固定时间,如果有网络连接则进入升级工程里更常见的是组合方式:
APP 有效 + 没有升级请求:跳转 APP APP 无效:停留 BootLoader 检测到升级按键:停留 BootLoader 检测到 APP 写入升级标志:停留 BootLoader网口升级不应该在 APP 正常有效时随便擦除 APP。要先进入 BootLoader 升级模式,再打开 W5500 升级端口。
4. 网络参数和 Socket 规划
基础版本使用静态 IP。
示例网络参数:
#defineBOOT_NET_SOCKET0#defineBOOT_NET_PORT5000Ustaticwiz_NetInfo g_boot_net_info={.mac={0x00,0x08,0xDC,0x11,0x22,0x33},.ip={192,168,1,88},.sn={255,255,255,0},.gw={192,168,1,1},.dns={8,8,8,8},.dhcp=NETINFO_STATIC};PC 和设备在同一个网段时,上位机连接:
设备 IP:192.168.1.88 端口:5000 协议:TCPSocket 分配:
Socket 0:BootLoader 升级 TCP Server Socket 1~7:暂不使用BootLoader 里不要一开始就堆太多网络功能。DHCP、DNS、HTTP、MQTT 都可以放到 APP 里。BootLoader 阶段只保留最小升级通道。
5. W5500 初始化代码
WIZnet 官方 ioLibrary 使用一组回调适配 SPI 和片选。
底层先准备几个函数:
externSPI_HandleTypeDef hspi1;#defineW5500_CS_GPIO_PortGPIOA#defineW5500_CS_PinGPIO_PIN_4#defineW5500_RST_GPIO_PortGPIOA#defineW5500_RST_PinGPIO_PIN_3staticvoidW5500_Select(void){HAL_GPIO_WritePin(W5500_CS_GPIO_Port,W5500_CS_Pin,GPIO_PIN_RESET);}staticvoidW5500_Unselect(void){HAL_GPIO_WritePin(W5500_CS_GPIO_Port,W5500_CS_Pin,GPIO_PIN_SET);}staticuint8_tW5500_ReadByte(void){uint8_ttx=0xFFU;uint8_trx=0U;(void)HAL_SPI_TransmitReceive(&hspi1,&tx,&rx,1U,100U);returnrx;}staticvoidW5500_WriteByte(uint8_tdata){(void)HAL_SPI_Transmit(&hspi1,&data,1U,100U);}staticvoidW5500_Reset(void){HAL_GPIO_WritePin(W5500_RST_GPIO_Port,W5500_RST_Pin,GPIO_PIN_RESET);HAL_Delay(10);HAL_GPIO_WritePin(W5500_RST_GPIO_Port,W5500_RST_Pin,GPIO_PIN_SET);HAL_Delay(100);}注册回调并初始化 W5500:
#include"wizchip_conf.h"#include"socket.h"staticint32_tBootNet_Init(void){uint8_ttx_size[8]={2,2,2,2,2,2,2,2};uint8_trx_size[8]={2,2,2,2,2,2,2,2};W5500_Reset();reg_wizchip_cs_cbfunc(W5500_Select,W5500_Unselect);reg_wizchip_spi_cbfunc(W5500_ReadByte,W5500_WriteByte);if(wizchip_init(tx_size,rx_size)!=0){return-1;}ctlnetwork(CN_SET_NETINFO,(void*)&g_boot_net_info);return0;}这里每个 Socket 分配 2KB TX、2KB RX。W5500 内部总共有 32KB Buffer,基础升级只用 Socket 0,也可以给 Socket 0 分配更大缓存。
例如只使用 Socket 0:
uint8_ttx_size[8]={8,0,0,0,0,0,0,0};uint8_trx_size[8]={8,0,0,0,0,0,0,0};先用 2KB 调通,再按实际吞吐调整。
6. 建立 TCP Server
W5500 的 TCP Server 状态大致如下:
SOCK_CLOSED | v socket() | v SOCK_INIT | v listen() | v SOCK_LISTEN | v PC 连接 | v SOCK_ESTABLISHED封装一个 TCP Server 维护函数:
staticint32_tBootNet_ServerProcess(void){uint8_tstate;int32_tret;state=getSn_SR(BOOT_NET_SOCKET);switch(state){caseSOCK_CLOSED:ret=socket(BOOT_NET_SOCKET,Sn_MR_TCP,BOOT_NET_PORT,0);if(ret!=BOOT_NET_SOCKET){return-1;}break;caseSOCK_INIT:if(listen(BOOT_NET_SOCKET)!=SOCK_OK){close(BOOT_NET_SOCKET);return-1;}break;caseSOCK_LISTEN:break;caseSOCK_ESTABLISHED:return1;caseSOCK_CLOSE_WAIT:disconnect(BOOT_NET_SOCKET);close(BOOT_NET_SOCKET);break;default:close(BOOT_NET_SOCKET);break;}return0;}等待上位机连接:
staticint32_tBootNet_WaitClient(uint32_ttimeout_ms){uint32_tstart=HAL_GetTick();while((HAL_GetTick()-start)<timeout_ms){int32_tstate=BootNet_ServerProcess();if(state==1){return0;}HAL_Delay(10);}return-1;}BootLoader 可以在串口打印状态:
NET: init w5500 NET: ip 192.168.1.88 NET: listen 5000 NET: client connected7. 把 TCP 接收适配成 YMODEM 字节接口
YMODEM 接收层需要两个底层函数:
int32_tBoot_RecvByte(uint8_t*data,uint32_ttimeout_ms);voidBoot_SendByte(uint8_tdata);串口版本底层是HAL_UART_Receive()和HAL_UART_Transmit()。
W5500 版本底层换成recv()和send()。
staticint32_tBootNet_Send(constuint8_t*data,uint16_tlength){int32_tret;if(getSn_SR(BOOT_NET_SOCKET)!=SOCK_ESTABLISHED){return-1;}ret=send(BOOT_NET_SOCKET,(uint8_t*)data,length);if(ret!=length){return-1;}return0;}staticvoidBootNet_SendByte(uint8_tdata){(void)BootNet_Send(&data,1U);}接收指定长度:
staticint32_tBootNet_Recv(uint8_t*data,uint16_tlength,uint32_ttimeout_ms){uint32_tstart=HAL_GetTick();uint16_treceived=0U;while(received<length){uint8_tstate=getSn_SR(BOOT_NET_SOCKET);if((state==SOCK_CLOSED)||(state==SOCK_CLOSE_WAIT)){return-1;}if(state!=SOCK_ESTABLISHED){return-1;}int32_tret=recv(BOOT_NET_SOCKET,&data[received],length-received);if(ret>0){received+=(uint16_t)ret;start=HAL_GetTick();continue;}if((HAL_GetTick()-start)>=timeout_ms){return-2;}}return(int32_t)received;}staticint32_tBootNet_RecvByte(uint8_t*data,uint32_ttimeout_ms){if(BootNet_Recv(data,1U,timeout_ms)==1){return0;}return-1;}这里有一个关键点:TCP 是字节流,不保留发送端的包边界。
上位机一次send()1029 字节,STM32 端可能分多次recv()收到。STM32 端一次recv()到 500 字节、300 字节、229 字节都正常。
所以 YMODEM 层不能假设一次 TCP 接收就是一个完整 YMODEM 包。正确做法是像串口一样按字节读取,或者按指定长度累计读取。
8. YMODEM over TCP 的接收流程
前面串口 YMODEM 的单包接收函数可以继续使用。
把底层函数替换成:
staticint32_tBoot_UartRecvByte(uint8_t*data,uint32_ttimeout_ms){returnBootNet_RecvByte(data,timeout_ms);}staticvoidBoot_UartSendByte(uint8_tdata){BootNet_SendByte(data);}函数名也可以改成更通用的:
staticint32_tBootPort_RecvByte(uint8_t*data,uint32_ttimeout_ms);staticvoidBootPort_SendByte(uint8_tdata);这样同一份 YMODEM 代码可以支持串口和网口:
typedefstruct{int32_t(*recv_byte)(uint8_t*data,uint32_ttimeout_ms);void(*send_byte)(uint8_tdata);}BootPortOps_t;串口端口:
staticconstBootPortOps_t g_uart_port={.recv_byte=BootUart_RecvByte,.send_byte=BootUart_SendByte};W5500 端口:
staticconstBootPortOps_t g_net_port={.recv_byte=BootNet_RecvByte,.send_byte=BootNet_SendByte};YMODEM 接收函数改成传入端口:
int32_tBoot_YmodemUpgrade(constBootPortOps_t*port){YmodemPacket_t packet;BootFileInfo_t file_info;uint32_twrite_addr=APP_BASE_ADDR;uint32_treceived_size=0U;port->send_byte(YMODEM_CRC);if(Ymodem_ReceivePacket(port,&packet,1000U)!=YMODEM_PACKET_DATA){return-1;}if(Ymodem_ParseHeaderPacket(packet.data,&file_info)!=0){port->send_byte(YMODEM_CAN);return-1;}if(BootFlash_CheckImageSize(file_info.file_size)!=0){port->send_byte(YMODEM_CAN);return-1;}Boot_BeginUpgrade(APP_BASE_ADDR,file_info.file_size);if(BootFlash_EraseApp(file_info.file_size)!=0){port->send_byte(YMODEM_CAN);return-1;}returnBoot_YmodemReceiveData(port,&file_info,write_addr,&received_size);}上面只是骨架。核心规则仍然和串口一致:
第 0 包:只解析文件名和文件大小,不写 Flash 第 1 包开始:写入 APP_BASE_ADDR EOT 后:校验 APP,设置 APP 有效标志网口升级的入口:
int32_tBoot_NetYmodemUpgrade(void){if(BootNet_Init()!=0){return-1;}if(BootNet_WaitClient(30000U)!=0){return-1;}returnBoot_YmodemUpgrade(&g_net_port);}9. 上位机发送方式
普通串口工具的 YMODEM 功能默认走串口,不一定能直接对 TCP Socket 发送。
网口 YMODEM 需要下面两种上位机之一:
支持 Raw TCP 连接并支持 YMODEM 发送的终端工具 自定义 TCP YMODEM 上位机调试时可以先做一个简单上位机:
1. 连接 192.168.1.88:5000 2. 等待 BootLoader 发字符 'C' 3. 发送 YMODEM 第 0 包 4. 等待 ACK 和 'C' 5. 发送固件数据包 6. 发送 EOT 7. 发送结束空包上位机日志可以这样打印:
TCP: connect 192.168.1.88:5000 YMODEM: wait C YMODEM: send header app.bin 58240 YMODEM: send data 1024 / 58240 YMODEM: send data 2048 / 58240 YMODEM: send eot YMODEM: doneBootLoader 端日志:
NET: client connected YMODEM: wait header YMODEM: file app.bin, size 58240 FLASH: erase app YMODEM: receiving YMODEM: received 1024 / 58240 YMODEM: received 2048 / 58240 YMODEM: eot APP: verify ok APP: set valid SYS: reset如果上位机只是普通 TCP 发送文件,不走 YMODEM 流程,BootLoader 会一直等待第 0 包格式,升级不会成功。
10. 断线、超时和重复升级处理
网口比串口多一个问题:TCP 连接可能随时断开。
常见情况:
网线被拔掉 交换机断电 PC 上位机异常退出 TCP 连接超时 W5500 Socket 进入 CLOSE_WAIT处理规则分两段。
10.1 还没擦 APP 前断线
如果还没有解析到 YMODEM 第 0 包,或者文件大小还没检查通过,断线后不动 APP。
未收到合法第 0 包 | +-- 不清 APP 有效标志 +-- 不擦 APP +-- 关闭 Socket +-- 重新 listen10.2 开始擦 APP 后断线
一旦执行了:
Boot_BeginUpgrade(APP_BASE_ADDR,file_info.file_size);BootFlash_EraseApp(file_info.file_size);APP 就不能再被认为有效。
断线后处理:
保持 APP 无效状态 关闭 Socket 重新打开 TCP Server 等待重新发送完整固件基础版本不做断点续传。因为断点续传需要上位机、参数区、YMODEM 流程一起配合,复杂度会上去。
现场产品更稳的做法是:
升级失败 -> APP 无效 -> BootLoader 等待重新完整升级10.3 Socket 异常恢复
封装一个 Socket 复位函数:
staticvoidBootNet_ResetSocket(void){disconnect(BOOT_NET_SOCKET);close(BOOT_NET_SOCKET);}接收失败时调用:
staticint32_tBootNet_HandleUpgradeError(void){BootNet_ResetSocket();while(1){if(BootNet_WaitClient(0xFFFFFFFFU)==0){returnBoot_YmodemUpgrade(&g_net_port);}}}不要在升级失败后直接跳 APP。APP 有效标志已经被清除,就停留在 BootLoader。
10.4 超时时间设置
不同阶段超时时间可以分开设置:
#defineBOOT_NET_WAIT_CLIENT_TIMEOUT_MS30000U#defineBOOT_YMODEM_WAIT_HEADER_MS30000U#defineBOOT_YMODEM_PACKET_TIMEOUT_MS10000U#defineBOOT_NET_IDLE_TIMEOUT_MS60000U第 0 包可以等久一点,因为上位机连接后可能还没点发送。
数据包接收阶段不能无限等。长时间没有数据,就关闭连接,重新进入等待升级。
11. 调试日志和状态码
网口升级调试时,串口日志仍然很有用。
可以定义状态码:
typedefenum{BOOT_NET_STATE_IDLE=0,BOOT_NET_STATE_INIT,BOOT_NET_STATE_LISTEN,BOOT_NET_STATE_CONNECTED,BOOT_NET_STATE_WAIT_YMODEM,BOOT_NET_STATE_ERASE,BOOT_NET_STATE_RECEIVE,BOOT_NET_STATE_VERIFY,BOOT_NET_STATE_DONE,BOOT_NET_STATE_ERROR}BootNetState_t;日志不要每包都刷太多,低速串口打印会影响接收节奏。
可以按 4KB 或 16KB 打印一次:
staticvoidBoot_LogProgress(uint32_treceived,uint32_ttotal){if((received&0x3FFFU)==0U){printf("YMODEM: %lu / %lu\r\n",received,total);}}基础日志:
NET: init NET: link ok NET: listen 5000 NET: connected YMODEM: header ok FLASH: erase ok YMODEM: receive 16384 / 58240 YMODEM: receive 32768 / 58240 APP: verify ok APP: valid SYS: reset失败日志:
NET: disconnected YMODEM: packet timeout FLASH: write failed APP: crc failed BOOT: wait new firmware12. 常见问题
12.1 PC 能 ping 通设备,但连不上 5000 端口
检查:
- BootLoader 是否真的进入升级模式
- Socket 是否进入
SOCK_LISTEN - 端口号是否一致
- PC 防火墙是否拦截
- W5500 网关和子网掩码是否配置正确
如果 BootLoader 很快跳到 APP,TCP Server 还没来得及监听,也会表现为端口连不上。
12.2 TCP 已连接,但上位机一直不发送
检查 BootLoader 是否发送字符C。
YMODEM 发送端通常要等接收端发C后才开始发送第 0 包。
如果 BootLoader 没发C,上位机可能一直等待。
12.3 发送普通 bin 文件失败
YMODEM 不是简单裸发.bin文件。
它需要:
第 0 包:文件名 + 文件大小 第 1 包开始:固件数据 EOT:传输结束 结束空包:YMODEM 批量传输结束只用 TCP 工具直接发送.bin,BootLoader 不能按 YMODEM 解析。
12.4 接收一半失败
重点看:
- TCP 是否断开
- W5500 Socket 是否进入
SOCK_CLOSE_WAIT recv()是否长期返回 0- Flash 写入是否耗时过长
- YMODEM 包超时时间是否太短
- 上位机是否严格等待 ACK 后再发下一包
如果上位机连续发送太快,而 BootLoader 又在阻塞擦写 Flash,容易出现接收节奏问题。
可以先降低发送速度,确认流程稳定后再优化。
12.5 APP 写入成功,但复位后不运行
按前几篇的检查顺序来:
- APP 链接地址是否正确
- APP 中断向量表是否重定位
APP_BASE_ADDR + 0是否为 SRAM 地址APP_BASE_ADDR + 4是否为 APP Reset_Handler- YMODEM 第 0 包是否误写入 APP 区
- APP 有效标志是否写入
- APP CRC 是否匹配
网口升级只是传输方式不同,跳转失败的根因仍然多半在 APP 地址、向量表、Flash 写入和有效标志。
12.6 多台设备同时升级怎么处理
基础版本按 IP 单台升级。
多台设备可以使用:
每台设备固定不同 IP 上位机按 IP 列表逐台连接升级 升级完成后等待设备重启 再升级下一台不要让多个上位机同时连同一台设备的升级 Socket。BootLoader 阶段只保留单连接逻辑。
13. 总结
W5500 网口升级的核心不是重新写一套升级协议,而是把已有的 YMODEM 接收逻辑搬到 TCP 字节流上。
这篇的关键点:
- BootLoader 用 W5500 建立 TCP Server
- PC 上位机作为 TCP Client 连接设备
- TCP 是字节流,接收端要累计读取,不能假设一次
recv()就是一包 - YMODEM 第 0 包只解析文件名和文件大小
- 文件大小合法后再清 APP 有效标志、擦除 APP
- 第 1 包开始写入 APP Flash
- 写完后校验 APP,再写有效标志
- TCP 断开后关闭 Socket,重新等待完整升级
- APP 无效时不跳转 APP
串口升级和网口升级可以共用同一套 YMODEM 状态机、同一套 Flash 擦写接口、同一套 APP 校验和参数区逻辑。底层只替换收发字节接口,BootLoader 的整体结构会更清楚。
参考标签
STM32 BootLoader W5500 YMODEM IAP TCP 网口升级 嵌入式 单片机