STM32 BootLoader 实战(五):基于 W5500 网口的 YMODEM 升级 APP 固件
摘要串口 YMODEM 升级适合调试和近距离维护现场设备数量多以后网口升级会更方便。W5500 自带硬件 TCP/IP 协议栈STM32 只需要通过 SPI 操作 Socket就可以做一个轻量级 TCP 升级通道。这篇把前面的 YMODEM 接收逻辑搬到 W5500 TCP 连接上重点处理下面几个问题BootLoader 如何初始化 W5500BootLoader 做 TCP Server 还是 TCP ClientTCP 是字节流YMODEM 包解析要怎么适配什么时候清除 APP 有效标志网线拔掉、TCP 断开、升级超时以后怎么处理W5500 接收的数据如何写入 APP Flash阅读前默认工程已经完成BootLoader 固定运行在0x08000000APP 已经按偏移地址链接APP 已经完成中断向量表重定位BootLoader 已经封装 Flash 擦写接口BootLoader 已经具备 APP 有效标志和参数区串口 YMODEM 接收逻辑已经能跑通目录1. 为什么网口升级还可以继续用 YMODEM2. W5500 网口升级整体流程3. 硬件连接和启动条件4. 网络参数和 Socket 规划5. W5500 初始化代码6. 建立 TCP Server7. 把 TCP 接收适配成 YMODEM 字节接口8. YMODEM over TCP 的接收流程9. 上位机发送方式10. 断线、超时和重复升级处理11. 调试日志和状态码12. 常见问题13. 总结1. 为什么网口升级还可以继续用 YMODEMW5500 走 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},.dhcpNETINFO_STATIC};PC 和设备在同一个网段时上位机连接设备 IP192.168.1.88 端口5000 协议TCPSocket 分配Socket 0BootLoader 升级 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_ttx0xFFU;uint8_trx0U;(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#includewizchip_conf.h#includesocket.hstaticint32_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 0uint8_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 ServerW5500 的 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;stategetSn_SR(BOOT_NET_SOCKET);switch(state){caseSOCK_CLOSED:retsocket(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_tstartHAL_GetTick();while((HAL_GetTick()-start)timeout_ms){int32_tstateBootNet_ServerProcess();if(state1){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;}retsend(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_tstartHAL_GetTick();uint16_treceived0U;while(receivedlength){uint8_tstategetSn_SR(BOOT_NET_SOCKET);if((stateSOCK_CLOSED)||(stateSOCK_CLOSE_WAIT)){return-1;}if(state!SOCK_ESTABLISHED){return-1;}int32_tretrecv(BOOT_NET_SOCKET,data[received],length-received);if(ret0){received(uint16_t)ret;startHAL_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_byteBootUart_RecvByte,.send_byteBootUart_SendByte};W5500 端口staticconstBootPortOps_t g_net_port{.recv_byteBootNet_RecvByte,.send_byteBootNet_SendByte};YMODEM 接收函数改成传入端口int32_tBoot_YmodemUpgrade(constBootPortOps_t*port){YmodemPacket_t packet;BootFileInfo_t file_info;uint32_twrite_addrAPP_BASE_ADDR;uint32_treceived_size0U;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_IDLE0,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((received0x3FFFU)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 很快跳到 APPTCP Server 还没来得及监听也会表现为端口连不上。12.2 TCP 已连接但上位机一直不发送检查 BootLoader 是否发送字符C。YMODEM 发送端通常要等接收端发C后才开始发送第 0 包。如果 BootLoader 没发C上位机可能一直等待。12.3 发送普通 bin 文件失败YMODEM 不是简单裸发.bin文件。它需要第 0 包文件名 文件大小 第 1 包开始固件数据 EOT传输结束 结束空包YMODEM 批量传输结束只用 TCP 工具直接发送.binBootLoader 不能按 YMODEM 解析。12.4 接收一半失败重点看TCP 是否断开W5500 Socket 是否进入SOCK_CLOSE_WAITrecv()是否长期返回 0Flash 写入是否耗时过长YMODEM 包超时时间是否太短上位机是否严格等待 ACK 后再发下一包如果上位机连续发送太快而 BootLoader 又在阻塞擦写 Flash容易出现接收节奏问题。可以先降低发送速度确认流程稳定后再优化。12.5 APP 写入成功但复位后不运行按前几篇的检查顺序来APP 链接地址是否正确APP 中断向量表是否重定位APP_BASE_ADDR 0是否为 SRAM 地址APP_BASE_ADDR 4是否为 APP Reset_HandlerYMODEM 第 0 包是否误写入 APP 区APP 有效标志是否写入APP CRC 是否匹配网口升级只是传输方式不同跳转失败的根因仍然多半在 APP 地址、向量表、Flash 写入和有效标志。12.6 多台设备同时升级怎么处理基础版本按 IP 单台升级。多台设备可以使用每台设备固定不同 IP 上位机按 IP 列表逐台连接升级 升级完成后等待设备重启 再升级下一台不要让多个上位机同时连同一台设备的升级 Socket。BootLoader 阶段只保留单连接逻辑。13. 总结W5500 网口升级的核心不是重新写一套升级协议而是把已有的 YMODEM 接收逻辑搬到 TCP 字节流上。这篇的关键点BootLoader 用 W5500 建立 TCP ServerPC 上位机作为 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 网口升级 嵌入式 单片机