ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F407基于TFTP的以太网IAP远程固件升级实现

STM32F407基于TFTP的以太网IAP远程固件升级实现 简介面向STM32嵌入式开发者的固件更新示例工程将意法半导体STM32F407高性能MCU与德州仪器DP83640以太网PHY芯片结合完整演示通过TFTP协议在设备运行中更新固件的实现思路。STM32F407具备浮点运算单元与丰富外设DP83640支持10/100Mbps速率和MII/RMII接口两者搭配可构成稳定的嵌入式网络节点。资源围绕MAC驱动、TFTP客户端、IAP引导等核心模块展开既解释了MAC层如何负责数据帧的物理收发与中断处理也展示了TFTP作为轻量级文件传输协议如何在无复杂认证条件下完成固件推送适合正在学习STM32网络通信、远程升级或工业设备维护的工程师参考。包体为RAR压缩格式大小约1.09MB内部具体文件类型与数量暂未披露但描述内容已明确覆盖工程源码、驱动配置及更新流程说明。目前已有179人学习下载能够帮助读者理解MAC层驱动与DP83640的协同机制掌握TFTP更新流程并借助IAP实现不依赖外部编程器的在线升级同时示例还体现了Visual C工具链在单片机开发中的配合使用对构建高效嵌入式网络应用或完善远程维护方案有直接参考价值。1. 把STM32F407的固件升级放到TFTP上是网络化设备最务实的解现场设备跑着STM32F407板载DP83640做10/100M以太网PHY需要远程更新固件。传统IAP用串口或者USB但设备已经接入网络再拉一条串口线并不现实。TFTPTrivial File Transfer Protocol没有复杂的握手和状态正好嵌进一个不带操作系统的bootloader里。实际项目里我用Visual C写了一个简单的TFTP客户端把编译好的.bin通过UDP端口69推给STM32F407MCU端跑一个轻量级TFTP服务器数据块直接写进内部Flash完成后软复位跳转到新程序。这个方案的坑在于MAC驱动、PHY配置和Flash擦写时序而不是TFTP协议本身。适合做网络化传感器、工业控制器和IoT网关的开发者参考。2. STM32F407的MAC与DP83640 PHY驱动要点2.1 为什么选DP83640而不是LAN8720DP83640是TI的工业级PHY支持IEEE 1588 PTP内置时钟同步硬件适合需要时间同步的测控设备。LAN8720便宜但没有1588支持而且RMII模式下对时钟精度要求更高。本项目的TFTP升级不依赖PTP但板子既然用了DP83640MAC驱动就要按它的寄存器映射和状态机来写。DP83640支持MII和RMII两种模式。MII需要16根数据线外加时钟RMII只要4根数据线但需要50MHz的REF_CLK。STM32F407的MAC通过MII/RMII接口与PHY连接DMA描述符负责在内存和MAC之间搬运数据包。两种模式的选择会直接影响引脚分配和时钟树配置下面这个表是项目选型时最常对着看的特性MIIRMII数据线4位×收发2位×收发时钟25MHz50MHz引脚数约16约7适用场景高性能、低EMI要求引脚紧张我一般根据原理图决定如果REF_CLK由外部晶振给PHY就选RMII否则用MII更稳妥。RMII的50MHz时钟如果由MCU提供还要注意MCO1引脚的分频配置这一项经常在CubeMX里被忽略。2.2 MAC初始化与DMA描述符STM32F407的以太网MAC使用DMA描述符发送和接收数据。每个描述符对应一个缓冲区配置好地址、长度、控制位后由DMA引擎自动处理。初始化步骤打开相关时钟、配置引脚复用、复位MAC、设置MAC地址、配置DMA总线模式、建立描述符链。ETH_DMADescTypeDef DMARxDscrTab[RX_BUF_NUM] __attribute__((aligned(4))); ETH_DMADescTypeDef DMATxDscrTab[TX_BUF_NUM] __attribute__((aligned(4))); uint8_t RxBuff[RX_BUF_NUM][ETH_MAX_PACKET_SIZE] __attribute__((aligned(4))); uint8_t TxBuff[TX_BUF_NUM][ETH_MAX_PACKET_SIZE] __attribute__((aligned(4))); void MAC_Init(uint8_t *mac_addr) { ETH_InitTypeDef eth_config; uint32_t i; RCC-AHB1ENR | RCC_AHB1ENR_ETHMACEN | RCC_AHB1ENR_ETHMACTXEN | RCC_AHB1ENR_ETHMACRXEN; // 引脚复用PA1/PA2/PA7 为 MIIPB11/PB12/PB13 等PC 口做 MDIO/MDC GPIOA-AFR[0] | (0x0B (1*4)) | (0x0B (2*4)) | (0x0B (7*4)); GPIOG-AFR[0] | (0x0B (11*4)) | (0x0B (13*4)) | (0x0B (14*4)); GPIOG-AFR[1] | (0x0B (0*4)) | (0x0B (3*4)) | (0x0B (5*4)); GPIOG-OSPEEDR | 0x3F (11*2); // 高速输出 eth_config.ETH_BusWidth ETH_BUSWIDTH_32BIT; eth_config.ETH_TxDMADesc DMATxDscrTab; eth_config.ETH_RxDMADesc DMARxDscrTab; ETH_Init(eth_config, 0); // 设置MAC地址低字节在最低位 for (i 0; i 6; i) { ETH-MACA0HR (ETH-MACA0HR ~(0xFF (i*8))) | (mac_addr[i] (i*8)); } ETH_Start(); }这段代码把手写寄存器风格和标准库混在一起只是为了说明关键点。实际工程里用STM32CubeMX生成HAL代码会更省事但描述符数量、缓冲区大小和对齐属性仍然要人工确认。发送和接收描述符各用4个缓冲区长度用ETH_MAX_PACKET_SIZE1518字节这是完整不带VLAN标签的以太网帧上限。描述符在内存中按链式结构组织最后一个描述符的环形链路要指回第一个否则DMA会在传输几个包后停止。2.3 PHY寄存器配置与Link状态检测DP83640的PHY地址通常由硬件地址引脚决定常见是0x01或0x02。上电后要等待PHY复位完成然后读取基本状态寄存器地址0x01的bit2link status。另外要配置自动协商、速度/双工模式。STM32的ETH库有ETH_ReadPHYRegister和ETH_WritePHYRegister底层走的是MDIO总线时序由硬件自动生成。#define DP83640_PHY_ADDR 1 #define DP83640_BMSR 0x01 uint32_t PHY_Init(void) { uint32_t timeout 100000; uint16_t val; // 软复位 PHY控制寄存器 bit15 ETH_WritePHYRegister(DP83640_PHY_ADDR, 0x00, 0x8000); while (timeout--) { val ETH_ReadPHYRegister(DP83640_PHY_ADDR, 0x00); if ((val 0x8000) 0) break; } // 开启自动协商 ETH_WritePHYRegister(DP83640_PHY_ADDR, 0x00, 0x1000); // 等待链路建立BMSR bit2 为 link status timeout 500000; while (timeout--) { val ETH_ReadPHYRegister(DP83640_PHY_ADDR, DP83640_BMSR); if (val 0x0004) return 0; } return 1; }这里两个timeout的作用不一样第一个是等待软复位完成第二个是等对端交换机或PC网卡协商出链路。如果对端是旧交换机自动协商可能要2到3秒所以第二个超时不能太短。实际调试时我习惯在每次读PHY寄存器前加一点延时因为MDIO读取频率太高时部分PHY会丢弃命令。另外DP83640的中断脚可以接到MCU的外部中断用来快速感知网线拔出和插入但如果bootloader里只是想升级固件轮询完全够用。3. TFTP协议与STM32F407上的IAP实现3.1 TFTP协议流程TFTP基于UDP服务器固定监听69端口。客户端发送读请求RRQ或写请求WRQ服务器返回数据块或ACK。标准块大小512字节最后一块不足512字节表示传输结束。Option negotiation允许客户端请求更大块大小blksize、超时时间timeout和文件大小tsize能显著提高升级速度。选项作用典型值blksize每块数据长度1024/1468timeout重传超时秒数1-3tsize文件总字节数固件大小windowsize窗口大小批量ACK4-16在STM32作为服务器时我一般只支持blksize协商到1024因为内部Flash页大小是16KB不同型号不同块越大擦除开销越小。但包太大在10Mbps半双工下容易出错1468字节是IP MTU限制下的最大值。TFTP服务器即使不支持某个选项也要返回一个正常的ACK只是不包含该选项如果直接忽略请求客户端会卡在选项协商阶段。3.2 Flash分区与IAP引导STM32F407内部Flash共1MB从0x08000000开始。Bootloader放在前面的64KB用户程序放在0x08010000偏移量64KB。应用程序编译时需要设置VECT_TAB_OFFSET0x10000。升级过程在Bootloader中执行MCU上电先从Bootloader启动检查是否有升级请求比如一个GPIO电平或串口命令没有就直接跳入用户程序。#define APP_ADDR 0x08010000 #define APP_STACK *(volatile uint32_t *)APP_ADDR #define APP_RESET (void (*)(void))(*(volatile uint32_t *)(APP_ADDR 0x04)) void JumpToApp(void) { if (APP_STACK 0x2FFE0000) { __disable_irq(); SysTick-CTRL 0; SCB-VTOR APP_ADDR; __set_MSP(APP_STACK); APP_RESET(); } }跳转条件是栈顶指针的值在SRAM范围内0x2FFE0000只是常见的RAM上边界掩码。很多开发者忘记关中断跳过去以后旧中断又触发导致HardFault。另一个容易忽略的是Flash写入期间不能有中断访问Flash所以擦写操作要在关中断或低优先级任务里做。如果使用了RTOS跳转前还要挂起所有任务、停止调度器否则RTOS的SysTick中断会在新程序还没初始化完时触发。3.3 在STM32上写TFTP服务器MCU端TFTP服务器是一个状态机而不是阻塞式循环。收到WRQ后解析文件名和选项然后发送ACK block 0。之后每收到一个DATA块写入Flash并发送ACK。UDP接收用DMA描述符数据包到达时置一个标志主循环轮询。typedef enum { WAIT_WRQ, WAIT_DATA, TRANSFER_DONE, ERROR } TFTP_STATE; uint16_t tftp_buffer[512]; uint32_t flash_write_addr APP_ADDR; void TFTP_Process(uint8_t *packet, uint16_t len) { uint16_t opcode (packet[0] 8) | packet[1]; switch (state) { case WAIT_WRQ: if (opcode 2) { if (TFTP_ParseOptions(packet, len)) { TFTP_SendAck(0); state WAIT_DATA; flash_write_addr APP_ADDR; FLASH_Unlock(); } } break; case WAIT_DATA: if (opcode 3) { uint16_t block (packet[2] 8) | packet[3]; uint16_t data_len len - 4; if ((flash_write_addr 0xFFF) 0) { FLASH_ErasePage(flash_write_addr); } for (uint16_t i 0; i data_len; i 2) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, flash_write_addr i, *(uint16_t *)(packet 4 i)); } flash_write_addr data_len; TFTP_SendAck(block); if (data_len tftp_blksize) { state TRANSFER_DONE; FLASH_Lock(); JumpToApp(); } } break; } }这个写法省略了错误包处理但核心逻辑清晰。第一个坑packet 4 i可能不是2字节对齐Cortex-M4支持非对齐访问但某些编译器会生成较慢的代码。第二个坑每收到一个块就写Flash而Flash编程前要擦除整个扇区。上面代码在地址页边界擦除如果固件跨越多个扇区需要在适当位置切换。第三个坑TFTP的重传机制要求MCU端能识别重复的DATA块即block编号与当前期望不符时重新发送上次ACK不能直接丢弃。否则客户端会一直重传同一块升级永远完不成。提示MCU端作为TFTP服务器时一定要在发送ACK后清空对应的DMA接收描述符。否则下一个数据包到达时没有空闲描述符硬件会自动丢弃客户端那边看起来就是超时重传。4. Visual C上位机与TFTP工具的使用4.1 用Visual C写TFTP客户端在Windows主机上我用Visual C的WinSock库写了一个极简TFTP客户端。它负责把固件文件按块发送给STM32并接收ACK。工程属性需要链入ws2_32.lib初始化WSA。#include winsock2.h #pragma comment(lib, ws2_32.lib) #define TFTP_PORT 69 #define BLKSIZE 1024 SOCKET s; sockaddr_in server; int seq 0; char data[BLKSIZE 4]; WSADATA wsa; WSAStartup(MAKEWORD(2, 2), wsa); s socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); server.sin_family AF_INET; server.sin_port htons(TFTP_PORT); inet_pton(AF_INET, 192.168.1.100, server.sin_addr); // 发送WRQ请求文件名firmware.bin协商blksize1024 char wrq[] {0x00, 0x02, f,i,r,m,w,a,r,e,.,b,i,n,0, b,l,k,s,i,z,e,0,1,0,2,4,0}; sendto(s, wrq, sizeof(wrq), 0, (sockaddr*)server, sizeof(server));这段代码只演示WRQ的构造实际还要处理ACK包的校验、超时重发、文件读取和块编号递增。注意TFTP的块编号从1开始服务器的第一个响应是ACK block 0。客户端收到这个ACK后才开始发DATA块。如果直接发DATAMCU端还在WAIT_WRQ状态会丢掉所有包。UDP socket默认没有超时所以客户端每发一个WRQ或DATA包就要setsockopt设置SO_RCVTIMEO超时后重新发送同一内容。重传次数建议控制在3到5次避免无限循环。4.2 使用标准tftp工具与脚本化Windows自带tftp.exe是客户端但只支持普通模式不能协商blksize。在批量生产中我更喜欢用SolarWinds TFTP Server或Tftpd64配合脚本自动分发。下面是一个批处理脚本它编译完固件后自动上传到三个设备echo off for /L %%i in (1,1,3) do ( echo upload firmware.bin to 192.168.1.10%%i tftp -i 192.168.1.10%%i put firmware.bin timeout /t 3 /nobreak nul )for /L循环是批处理语法tftp -i表示二进制模式。每次上传后等3秒给设备留出Flash擦写和重启时间。如果设备支持选项协商最好用Tftpd64的界面模式日志里能看到块大小和ACK时间。实际生产中发现Windows防火墙会拦截UDP 69端口的入站请求导致MCU的ACK发不出来。排查时直接看netsh firewall show state或者临时关闭防火墙验证。4.3 常见问题定位现象可能原因排查方法WRQ发出后无回应PHY link没建立读取PHY状态寄存器确认网线协商发送几个块后停止端到端块编号不同步Wireshark抓UDP包看ACK序号写入Flash后程序跑飞跳转前没关中断调试器看PC值检查VTOR设置TFTP超时UDP端口被防火墙拦截关闭Windows防火墙或加放行规则还有一个隐蔽问题STM32F407的ETH DMA读Flash时如果Flash正在被编程总线会忙。所以MCU端TFTP服务器在写Flash时必须保证DMA接收描述符已经准备就绪。我一般在进入写Flash前关闭ETH DMA接收中断写完后再打开防止数据进来时拿不到DMA描述符。调试时如果发现丢包优先看ETH-DMASR的RBU标志这个标志置位说明接收队列已满。5. 固件版本校验与更新回滚技巧5.1 在固件头部加入元信息把版本号和CRC32放在固件文件的前16字节。Bootloader在写入完成后、跳转之前读取头部并校验。CRC覆盖除去头部以外的全部数据。这样避免了设备升级后因为一个bit翻转变砖。typedef struct { uint32_t magic; // 0xDEC0DE01 uint32_t version; uint32_t crc32; uint32_t length; uint8_t reserved[16]; } firmware_header_t;校验时用HAL_CRC或者纯软件CRC32。注意STM32F407的硬件CRC单元使用标准CRC32初始值为0xFFFFFFFF和zlib的一致可以直接用。length字段可以用来覆盖整个固件长度防止Class B页之外的垃圾数据被跳过。5.2 使用双备份区实现自动回滚如果Flash容量允许可以在1MB内放两个应用槽A槽和B槽。Bootloader启动时检查当前槽的CRC如果无效则加载另一个槽。TFTP升级默认写入非活动槽升级完成后标记新槽有效并跳转。下次上电如果新槽CRC失败自动回到旧槽。这个方案只适合应用小于512KB的场景1MB Flash可以放下两个200KB的固件。uint32_t active_slot 0; // Bootloader 中 if (CheckCRC((void*)APP_ADDR)) { active_slot 0; } else if (CheckCRC((void*)BACKUP_ADDR)) { active_slot 1; } else { EnterTftpServer(); }回滚的关键是“标记有效”要在应用首启成功后做。可以在固件头部留一个boot_count字段应用启动时自增正常运行5分钟后清零。Bootloader读到boot_count大于3则判定当前槽有问题切到另一个槽。这个技巧能覆盖大部分升级过程中断电的场景。5.3 用DWT计数器做TFTP超时控制DP83640内置IEEE 1588时钟可以通过读取其寄存器拿到UTC时间。虽然TFTP没有时间同步但可以在应用层把固件写入完成时的PTP时间戳一起保存到备份区。这样运维人员能精确知道每次升级发生的时间点。实现时在升级成功回调里调用PHY_GetTimestamp()把秒和纳秒写入Flash末尾的日志区。注意PTP时钟需要外部主时钟同步如果网络里没有PTP master时间戳只有相对意义。另一个更简单的技巧用STM32F407内部的DWT时钟周期计数器作为TFTP重传超时的参考不依赖SysTick。DWT是Cortex-M4的调试组件计数值等于CPU周期精度极高。初始化CoreDebug-DEMCR | 124; DWT-CYCCNT0; DWT-CTRL | 1;之后读取DWT-CYCCNT就能计算微秒级延时。在做TFTP块间超时时用这个计数器比HAL_Delay准得多而且可以在中断里用不会阻塞主流程。本文还有配套的精品资源点击获取
返回列表