
简介面向物联网与嵌入式开发者的STM32以太网通信实战代码包以STM32F103通过SPI接口驱动W5500模块实现基于UDP协议的网络数据收发。例程完整演示DHCP动态获取IP、创建UDP会话、等待远端连接及关闭连接的全流程适合需要快速搭建有线网络通信链路的网关、数据采集与小型物联网终端项目。压缩包共182个文件、5.88MB核心源码以42个c文件和44个h文件为主并包含Keil工程配置、编译中间产物、配置文件及可烧录的hex文件使用Keil开发环境即可直接编译。目前已有1442人学习程序在STM32F103C8T6上验证通过更换其他F103型号时只需调整芯片型号与Flash容量。代码结构清晰结合标准外设库与W5500驱动同时给出了调试器选择等工程细节提示便于嵌入式开发者快速移植也适合物联网项目集成UDP通信功能时参考复用。1. 为什么STM32与W5500的组合成了RJ45 UDP通讯的最短路径一块跑着采集业务的STM32板子要把数据送到上位机最省事的路径不是无线而是拉一根RJ45网线做以太网UDP通讯。但让STM32自己啃TCP/IP协议栈是给项目上强度。W5500这块自带硬协议栈的以太网控制芯片把UDP、ARP、ICMP这些协议全部做进硬件里MCU只负责通过SPI读写数据上层只需要调用socket API收发一个UDP报文用不了十行代码。反直觉的地方在于很多人觉得STM32F407这类芯片自带MAC外挂一个PHY芯片也能跑以太网。实际做下来RMII时钟相位、PHY寄存器配置、lwIP内存池调优这些加起来比多花十几块钱买一个带RJ45座的W5500模块更费工时。UDP没有TCP的连接状态机天然适合“发出去就不管”的数据上报场景W5500正好把底层全部消化掉。所以STM32W5500UDP成了物联网数据采集、设备状态上报、毕设网关最常见的组合之一。这套方案适合手里有STM32基础、想在几天内看到数据从网口出去、又不想在协议栈上耗时间的开发者。会SPI和中断就会用W5500。下面从硬件接线、驱动移植、UDP收发程序到联调避坑把整个落地过程讲透。2. 硬件接线与驱动移植先让W5500的SPI通道“能说话”2.1 最小硬件清单W5500模块、RJ45座、STM32的六个引脚市面常见的W5500模块已经把W5500芯片、网络变压器、RJ45座集成在一块小板上买回来只需要接电源和SPI信号线不用自己画网络变压器电路。这个模块的核心是W5500这颗芯片它内部集成了10/100M以太网MAC和PHY以及完整的TCP/IP协议栈硬件逻辑。RJ45座在模块上直接引出板级接线非常干净。最小接线是八根信号VCC和GND给模块供电RST接STM32一个普通IO用于复位SCS是SPI片选SCLK、MOSI、MISO走SPI总线INT是中断输出UDP轮询场景可以先悬空不接。W5500模块是3.3V电平绝对不能把这几个信号直接怼到5V单片机的IO上STM32大部分是3.3V系统这点一般不会踩但如果是5V的STM32F103旧板子要做电平转换。典型连接如下表我常用SPI2引脚好记W5500模块引脚STM32引脚说明VCC3.3V模块供电注意电流要够GNDGND共地RSTPB10复位低有效SCSPB12SPI片选低有效SCLKPB13SPI时钟MISOPB14SPI主收MOSIPB15SPI主发INT悬空或PB1中断引脚先悬空即可这里插一句如果你之前玩过LAN8720这类独立PHY芯片会发现W5500省掉的不只是外围电路。LAN8720要额外配RMII参考时钟时钟相位不对直接不工作以太网模块常见翻车点就在这个时钟上。W5500走的是SPI从机方式MCU侧只要SPI配置正确链路大概率一次通。2.2 SPI时序与读写函数地址、控制字节和数据的三段式帧W5500的SPI通信格式是一帧三段16位寄存器地址、8位控制字节、之后是数据。控制字节的低两位表示访问区域0表示Common寄存器1表示Socket寄存器2表示发送缓冲区3表示接收缓冲区控制字节的bit2是读写方向1为读0为写。驱动里所有寄存器读写最终都要拼成这种三段帧。用STM32 HAL库实现底层字节读写时我一般这么写static void w5500_cs_select(void) { HAL_GPIO_WritePin(ETH_CS_GPIO_Port, ETH_CS_Pin, GPIO_PIN_RESET); } static void w5500_cs_deselect(void) { HAL_GPIO_WritePin(ETH_CS_GPIO_Port, ETH_CS_Pin, GPIO_PIN_SET); } static uint8_t w5500_spi_read_byte(void) { uint8_t rx 0; HAL_SPI_Receive(hspi2, rx, 1, 100); return rx; } static void w5500_spi_write_byte(uint8_t byte) { HAL_SPI_Transmit(hspi2, byte, 1, 100); } static void w5500_spi_read_burst(uint8_t *buf, uint16_t len) { HAL_SPI_Receive(hspi2, buf, len, 1000); } static void w5500_spi_write_burst(uint8_t *buf, uint16_t len) { HAL_SPI_Transmit(hspi2, buf, len, 1000); }逻辑说明每次SPI通信期间SCS片选必须保持拉低一个burst过程不能中途释放片选否则W5500会把当前帧当错误帧丢弃。HAL_SPI_Receive和Transmit的timeout参数在联调阶段给大一点1000毫秒足够排查问题。注意IO初始化时SCS、SCLK、MOSI都要配置成推挽输出MISO配置成浮空输入或上拉输入。参数说明SPI模式选择上W5500手册说支持模式0和模式3我习惯用模式0即CPOL0、CPHA0。时钟分频第一次先用低速率比如APB1总线42MHz分到4.2MHz左右确认通信正常后再把分频系数调高降低SPI时序余量不足带来的偶发错位风险。这个“先慢后快”的习惯可以省掉很多玄学问题。2.3 把官方ioLibrary_Driver挂进来注册回调、初始化网络参数W5500的官方驱动库叫ioLibrary_Driver里面已经实现了寄存器位段读写、TX写指针回绕、RX读指针更新这些底层细节。自己从零写W5500驱动不是不行但缓冲区边界、socket命令时序这些地方容易翻车比如TX buffer写指针从0x3FFF回绕到0x0000时官方库会自动跨区处理自己写很容易丢一段数据。移植官方库的第一步是注册几个回调函数告诉驱动库“你用我的SPI函数去读写硬件”reg_wizchip_cs_cbfunc(w5500_cs_select, w5500_cs_deselect); reg_wizchip_spi_cbfunc(w5500_spi_read_byte, w5500_spi_write_byte); reg_wizchip_spiburst_cbfunc(w5500_spi_read_burst, w5500_spi_write_burst);然后初始化网络参数和缓冲区分配。W5500有8个socket每socket的发送和接收缓冲区大小可以独立配置单位是KBwiz_NetInfo netinfo { .mac {0x00, 0x08, 0xDC, 0x00, 0x01, 0x02}, .ip {192, 168, 1, 80}, .sn {255, 255, 255, 0}, .gw {192, 168, 1, 1}, .dns {8, 8, 8, 8}, .dhcp NETINFO_STATIC }; uint8_t tx_size[8] {2, 2, 2, 2, 2, 2, 2, 2}; uint8_t rx_size[8] {2, 2, 2, 2, 2, 2, 2, 2}; wizchip_init(tx_size, rx_size); wizchip_setnetinfo(netinfo);逻辑说明wizchip_init会把缓冲区大小写进W5500的Common寄存器wizchip_setnetinfo把MAC、IP、子网掩码、网关写进去。MAC地址必须局域网内唯一随便填一个以0x00开头、局域网内没被占用的地址即可。如果以后要跑Modbus TCP同一个库切到TCP协议就行UDP这套代码结构不用推倒重来。参数说明静态IP和电脑上位机必须在同一网段我调试时板子用192.168.1.80电脑手动配192.168.1.50用网线直连或过交换机都行。dns字段在纯UDP场景用不到但结构体里保留填8.8.8.8没有副作用。3. UDP收发程序设计sendto与recvfrom背后的命令与缓冲3.1 socket()UDP socket一打开就绑定了本地端口W5500的socket操作和BSD socket非常像但状态机比lwIP简单得多。UDP socket调用socket()后直接进入SOCK_UDP状态这个状态下立即能收能发不需要像TCP那样先connect再等握手。UDP本来就没有连接概念所谓“连接”只是上层业务自己定义的心跳关系。打开UDP socket的代码#define LOCAL_PORT 6000 int32_t sock socket(0, Sn_MR_UDP, LOCAL_PORT, 0); if (sock 0) { // 打开失败检查tx_size/rx_size是否分配了缓冲区 }逻辑说明socket()第一个参数是socket编号W5500支持8个这里用第0个。第二个参数Sn_MR_UDP指定协议为UDP。第三个参数是本地端口号这个端口就是W5500接收数据时监听的端口上位机发UDP报文时要发到这个端口。第四个参数flag不常用填0。参数说明本地端口建议避开5000以下常用端口我习惯用6000到9000之间的数值。要注意一个端口只能被一个socket占用如果后面要开第二个UDP socket本地端口必须换一个。很多“能发不能收”的案例问题就出在本地端口填了0系统绑了个随机端口上位机不知道往哪个端口发。3.2 sendto()目的IP和端口在这里填UDP发送的核心逻辑是用sendto指定对端IP和端口然后数据会从用户缓冲区走SPI进入W5500的TX bufferW5500硬件自动封好以太网帧、IP头、UDP头再交给内部PHY发出去。MCU侧完全不参与MAC时序和校验和计算。一个设备状态上报的例子uint8_t report_buf[64]; uint8_t dest_ip[4] {192, 168, 1, 50}; uint16_t dest_port 9000; report_buf[0] 0xAA; // 帧头 report_buf[1] 0x55; report_buf[2] dev_id; // 设备ID report_buf[3] status; // 状态字节 report_buf[4] seq; // 包序号用于去重 int32_t ret sendto(sock, report_buf, 5, dest_ip, dest_port); if (ret 0) { // 发送失败通常是TX buffer满或socket未打开 }逻辑说明sendto内部会把dest_ip和dest_port写入Sn_DIPR、Sn_DPORT寄存器然后把report_buf里的数据按Sn_TX_WR写指针拷入TX buffer写入长度后发出SEND命令。W5500硬件自动完成UDP校验和、IP头填充和MAC地址解析ARP缓存没有对端MAC时会先发ARP请求再发送。参数说明sendto最后一个参数是目标端口这个端口必须对应上位机UDP调试助手的本地端口两边必须一样才能收到。发送长度不要超过1400字节原因后面第5章专门讲这里先记住一条铁律以太网MTU是1500IP头20字节、UDP头8字节单包数据最多1472字节应用层再留余量所以协议层约定单包不超过1400字节最安全。3.3 recvfrom()无数据立即返回主循环别死等UDP接收比发送多一个关键步骤缓冲区释放。W5500收到一个UDP报文后数据先落在RX buffer里同时Sn_RX_RSR寄存器记录待处理数据长度。应用层调用recvfrom时驱动从RX buffer中读出数据解析出对端IP和端口然后更新Sn_RX_RD读指针发RECV命令释放空间。如果一直不调用recvfromRX buffer迟早被填满后续数据包会被硬件直接丢弃。接收的轮询写法uint8_t rx_buf[1400]; uint8_t src_ip[4]; uint16_t src_port; while (1) { int32_t rlen recvfrom(sock, rx_buf, sizeof(rx_buf), src_ip, src_port); if (rlen 0) { // 收到一条完整UDP报文rlen是数据长度 // 这里可以处理业务也可以回显给对端 sendto(sock, rx_buf, rlen, src_ip, src_port); } HAL_Delay(10); }逻辑说明recvfrom没有数据时立即返回-1不会阻塞所以主循环可以一直轮询。返回大于0时src_ip和src_port就是发送方的IP和端口回显时直接把它们传给sendto就能做到“收到谁的就回给谁”。这是联调阶段最好用的调试手法。参数说明接收buf长度至少要大于等于协议约定的最大单包长度我用1400字节正好卡住上限。如果buf太小recvfrom会截断应用层按截断后的长度处理容易出错。HAL_Delay(10)控制轮询节奏避免空转把CPU全占掉。UDP本身不保证可靠丢包时不会重传所以业务层的数据要带序号收端靠序号判断是否缺包这部分放在第6章讲。4. 联调闭环ping、UDP调试助手、Wireshark与打流验证4.1 同网段与ping先证明IP、MAC、PHY都在干活在写任何UDP业务之前第一步永远是ping。板子上电后电脑开cmd执行ping 192.168.1.80如果通了说明W5500的PHY已经协商出100M或10M链路MAC地址、IP地址、ARP应答都在正常工作。ping不通时不要急着怀疑UDP代码先按这个顺序查RJ45网线插上后模块指示灯亮不亮不亮先查电源和RJ45灯亮但ping不通查RST复位时序和SPI配置SPI没问题再查IPv4地址是否真的写进去了。ping通但有丢包表现为“ping断断续续”这种问题一半出在复位和时钟上一半出在上位机网卡节能策略上。W5500一侧的复位时序坑在第5章细讲电脑侧先把网卡的“EEE节能以太网”和“允许计算机关闭此设备以节约电源”两个选项关掉能排除很多假丢包。联调时要注意电脑的以太网适配器要手动设置IP地址不要用DHCP自动获取。win11系统如果“以太网选项”没出现在网络连接里多半是网卡驱动被禁用了或网线没识别到先去设备管理器看网卡状态而不是改代码。4.2 用UDP网络调试助手验证收发本地端口必须固定ping通之后打开任意一款udp网络调试工具市面常见的那几个都可以在工具里设置协议为UDP、本地IP为电脑的192.168.1.50、本地端口设为9000然后填写目标IP为192.168.1.80、目标端口为6000发送字符串。板子代码里已经写了回显逻辑调试助手能收到一模一样的字符串说明UDP收发全链路打通。这里最容易翻车的一个细节调试助手的“本地端口”必须固定成一个具体数字不能填0或随机。因为板子的sendto目标是那个端口如果工具在随机端口上监听板子回包会发到一个没有进程监听的端口报文被系统丢弃现象就是“板子能收到但工具收不到回包”。我曾经在这个问题上浪费了半天后来养成习惯先把工具本地端口写死再谈心跳。4.3 Wireshark抓包看ARP、UDP头和IP分片如果调试助手通了还想往深看用Wireshark抓包。过滤条件填udp.port6000或host 192.168.1.80就能看到板子和电脑之间完整的UDP报文。抓包窗口里能确认三件事第一W5500会自己发出ARP请求说明底层协议栈真的在硬件里跑第二UDP头里的源端口、目的端口、校验和是否正确如果校验和错误多半是SPI传输过程中数据有翻转第三如果你故意发一个超过1472字节的包抓包列表里会出现IP分片记录这正好印证W5500不做分片重组这个坑。用Wireshark看以太网帧还能注意到帧与帧之间固定有96bit的帧间隔这些时序由W5500内部MAC处理MCU完全不用管。这也是选硬协议栈芯片的好处底层物理时序全部是硬件自动的应用层只关心socket。4.4 打流与丢包率用固定包数和包序号量化可靠性联调通过后必须做一次量化测试否则不知道这个UDP通道到底能在多快的发包速率下可靠工作。常见做法是写一个小脚本我用python udp写过上位机连续发2000个包每包带一个递增序号板子收到后把序号累加并回显最终统计缺失的序号。打流参数分三档间隔20ms发一包测常规上报场景间隔5ms发一包测突发场景间隔1ms发一包测极限吞吐。如果间隔1ms时开始丢包不要慌先看丢包率是否随RX buffer增大而降低。iperf3用udp打流也可以测命令里要限制包长和带宽比如-l 1400 -b 1M不要用默认的8970字节大包去压W5500那测出来的不是真实业务指标而是分片丢弃率。这个测试的意义是把“看起来通了”变成“知道边界在哪”。我自己的经验W5500单socket RX buffer配2KB、对端间隔10ms发包时几乎不丢但把间隔压到1ms后丢包率明显上升原因是板子主循环处理速度跟不上而不是网络问题。此时优化业务逻辑优先级高于调缓冲区。5. W5500 UDP项目避坑复位时序、MTU分片与“掉线”幻觉5.1 复位后ping不通或丢包断断续续先查RST时序和SPI时钟现象板子上电后第一次ping不通按一下复位键后偶尔能通或者ping的时候通几秒断几秒。原因W5500的RST引脚要求低电平保持足够长时间再释放有些模块对复位脉宽敏感MCU上电瞬间复位引脚电位不稳定W5500没完成内部初始化。另一个常见原因是SPI时钟太快模块能通信但时序裕量不够偶发数据错位。解决复位时序固定写成三个步骤RST拉低至少100ms然后拉高再等100ms后再做任何SPI操作HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET); HAL_Delay(100);这段代码放在wizchip_init之前。SPI时钟方面把分频系数调到SPI时钟10MHz以下再观察如果问题消失就是时钟太快适当降频即可。W5500支持更高的SPI速率但不是所有模块的走线质量都扛得住高频稳定压倒一切。5.2 单包超过1472字节直接丢W5500不做IP分片重组现象上位机一次发3000字节给板子板子一条都收不到发1400字节以内就正常。原因以太网帧的MTU是1500字节超过这个大小IP层会分片传输。PC的协议栈支持分片和重组但W5500的硬协议栈没有实现IP分片重组逻辑超过MTU的UDP包要么被W5500直接丢弃要么接收端只能收到分片碎片而无法拼装。这也是搜索里常提到“udp划分IP数据报片”这个概念的落地场景分片是IP层行为不是UDP自己做的。解决应用层协议强制约定单包上限我统一用1400字节这是完全避开MTU分片的安全值。上位机发送前如果数据超过1400要自己在应用层分包每个分片带上包序号和总包数板子收齐后再组包。C#写上位机做分包组包时分包编号规则要和板子端C代码完全一致比如包结构约定为序号(2字节)总包数(1字节)数据体。5.3 能发不能收本地端口没绑定或RX缓冲分配为0现象上位机能收到板子发的UDP包但板子永远收不到上位机的包。调试助手里显示发送成功板子端recvfrom一直返回-1。原因最常见的是socket()第三个参数本地端口传了0W5500虽然也能发但接收报文时没有固定监听端口报文到达后找不到匹配的socket。另一个原因是wizchip_init里rx_size数组对应socket的值给成了0该socket的RX缓冲区没有分配硬件直接把接收数据丢弃。解决socket()的本地端口显式写成6000和上位机的目标端口一致。rx_size数组不要用0给最小的2即可单位是KB2KB足够承载几十条1400字节的报文排队。初始化完成后可以打印wizchip_getnetinfo确认IP和端口都写进去了。5.4 长时间不通讯后再连不上ARP缓存超时与NAT老化现象板子连续工作几天后ping不通上位机和板子之间彻底失联但重启板子或电脑后恢复。看起来像硬件挂了实际上不是。原因局域网内ARP表项有老化时间对端长时间没收到来自板子的IP帧会把板子的ARP映射删掉。跨网段场景更明显路由器NAT表项会因为长时间无流量而释放板子的映射消失外部再也找不到它。W5500不会主动维护这些表项。解决应用层加心跳。板子每5秒或10秒主动向上位机发一个带设备ID和序号的小UDP包上位机不需要回复。这样ARP表项和NAT映射一直被刷新掉线问题从根上消失。心跳包不要超过64字节频率不要高于1秒一次否则对网络和缓冲区都是负担。5.5 上位机报10054错误C# UDP收到ICMP端口不可达现象C#上位机用UdpClient调用Receive时抛出异常“read udp: unknown error (code10054)”。原因板子sendto的目标端口在上位机侧没有进程监听或者说上位机发完包后把监听端口关了Windows内核收到ICMP端口不可达报文后给UdpClient返回10054错误。UDP虽然无连接但操作系统会好心通知你“对面没人”。解决上位机先bind固定本地端口并启动接收线程保证接收端口一直有socket在监听然后再给板子发消息。板子端的心跳目标端口也必须是上位机实际监听的那个端口不要凭印象乱填。这个10054还有一个变种上位机用完之后没有关闭UdpClient重开时新socket没有绑定也会触发类似报错。记住一条经验凡是UDP联调出“怪毛病”先检查两端端口是否真的在监听。6. 把UDP通讯做成能交付的模块广播、组播与心跳设计6.1 一条sendto推全网子网广播地址的填法当需要向同网段所有设备广播状态时把sendto的目标IP填成子网广播地址即可。子网掩码是255.255.255.0时广播地址是192.168.1.255不确定子网边界时填255.255.255.255一般也能通。W5500硬件收到这个目标IP会自动把帧目的MAC写成FF:FF:FF:FF:FF:FF同网段所有设备都能收到。uint8_t broadcast_ip[4] {192, 168, 1, 255}; sendto(sock, report_buf, 5, broadcast_ip, dest_port);广播适合设备发现场景新设备上电后广播一个“我是谁”的报文上位机收到后回单播确认。注意广播不会穿过路由器跨网段要组播或单播转发。6.2 组播与IGMP要加入组才能收组播比广播省带宽W5500支持IGMP协议也就是硬件能处理组播组的加入和退出。用组播时目标IP要填组播地址段的D类地址驱动层socket配置里需要打开组播相关标志并触发加入组操作配置步骤比广播多一层。我的实际项目里大多数场景用广播就够组播只有在设备数量大、不想让所有设备都被无关广播打断时才值得上。如果硬件平台换成了别的以太网方案组播的坑更多因为有些低端PHY芯片对组播过滤支持不好。W5500的好处是IGMP状态机在硬件里MCU不用维护各组的报告报文定时重发省了一整块代码。6.3 心跳、序号与重传把UDP做成“看起来可靠”的通讯交付级的UDP通讯代码里一定要有四个东西心跳保活、包序号、应用层确认、超时重传。心跳保证ARP和NAT表项不老化包序号让接收方能发现丢包并去重业务层收到重要指令后回一个ACK包发送方3秒没收到ACK就重发。这一套组合拳做完UDP的可靠性在应用场景里已经无限接近TCP但实时性和代码复杂度都优于TCP。这套架构也能直接延伸到固件升级场景。做STM32 OTA时分包下载固件按1400字节切块每块带序号和CRC上位机或云平台逐块下发设备逐块收齐后组包校验。你之前为“单包不超1400字节”定的协议约束在这里变成OTA的天然分块规则不用再改传输层。最后说一个我自己的教训以前做一批采集器客户端反馈“用几天就连不上”现场换了两批板子都没解决。后来抓包发现不是W5500挂了是我调试工具本地端口没有固定板子的回包一直发给一个没人监听的端口系统悄悄把报文丢了。从那次以后我所有UDP联调的第一件事就是把调试助手本地端口写死端口号再谈心跳和重传。UDP写多了你会发现八成“硬件不稳定”的锅最后都是协议设计问题。希望帮到你。本文还有配套的精品资源点击获取