☰
STM32F407以太网UDP收发实战:硬件选型、LWIP配置与调试验收
2026/10/15 16:04:14 网站建设 项目流程

简介:面向嵌入式开发者的STM32F407以太网UDP主机发送接收程序,基于ARM Cortex-M4内核与lwIP协议栈,演示了从EMAC控制器、PHY芯片(如LAN8720)到DMA传输、UDP报文收发的一整套实现路径。压缩包共506个文件、7.65MB,以148个头文件与127个C源文件为主体,包含Keil工程文件(uvproj)、编译中间产物(o/crf)以及少量readme与说明文档,目录结构完整,可直接打开工程定位初始化代码、lwIP配置和UDP回调函数。已有3679人学习下载,适合需要为工业控制、物联网设备搭建UDP服务或快速实现远程数据采集、调参通信的嵌入式工程师。通过分析源码,可掌握UDP套接字创建与绑定、sendto/recvfrom收发、MAC地址与中断配置、DMA高效搬运等关键步骤,同时理解lwIP在无操作系统环境下的内存管理和错误处理思路,是入门STM32以太网通信并扩展到TCP/TLS的实用参考。

1. 先把话说清:F407跑UDP,到底在做什么

STM32F407以太网UDP主机发送接收程序,一句话说就是让板子通过网口和PC或另一台设备用UDP互相发数据。很多人刚接触到F407的以太网外设时,最先搜的就是“UDP发送”和“UDP接收”,因为它意味着设备能快速接入局域网、实现状态上报和命令下发,而不用去碰TCP的握手、重传和粘包逻辑。这个需求最典型的落点是数据采集器、设备监控终端、工业控制板的上位机通信,以及调试阶段的日志上行。谁需要这篇文章?手里有一块F407开发板,想用网口做数据交互,但不太清楚怎么把LWIP协议栈、PHY芯片和UDP回调串起来的人;已经调通TCP但觉得连接维护太重的熟手,也能在这篇里拿到一套可落地的UDP收发框架。下面直接拆硬件选型和协议栈配置,再给最小工程和排障路径。

2. 硬件与协议栈选型:为什么LAN8720A配LWIP是默认方案

2.1 MAC、PHY和RMII接口:三者的分工决定了你的接线

一个以太网帧从STM32F407的CPU到网线,中间要过两层。F407内部集成的是以太网MAC,负责MAC帧封装、地址过滤、DMA搬运和中断控制;外部PHY芯片负责把MAC送出来的数字信号转成网线上的差分模拟信号。MAC和PHY之间的接口,F407支持MII和RMII两种模式。MII需要16根信号线,数据线是4位,还带独立的发送时钟和接收时钟;RMII把数据线砍到2位,总共只要七八根线,包括TXD[1:0]、RXD[1:0]、REF_CLK、CRS_DV、MDC、MDIO。我一般用RMII,理由很直接:GPIO占用少,PCB走线压力小,大部分现成开发板也都是这么画的。

代价是RMII模式下REF_CLK必须是一个外部提供的50MHz参考时钟,这颗时钟从哪里来,直接决定后面能不能调通。常见做法有两种:一种是由F407的MCO引脚输出50MHz给PHY,另一种是PHY自己挂一颗50MHz有源晶振。如果原理图上用的是MCO方案,那时钟树里MCO的分频配置就不能错,否则程序怎么改都不通。PHY芯片选型上,当前最主流的两个选择是LAN8720A和DP83848。它们性能都能满足10/100M以太网,但细节差别不小。

对比项LAN8720ADP83848
RMII参考时钟必须外部输入50MHz支持外部50MHz或自身晶振方案
默认PHY地址0x00(由PHYAD引脚决定)常见0x01,视硬件配置
MDIO寄存器访问稳定,兼容性好寄存器多,功能丰富
常见场景低成本核心板、评估板工业级、对稳定性要求高的场景

选PHY之后,原理图上还有两个细节直接影响后续调试。第一,PHY复位引脚不要和系统复位直接连在一起,尽量用普通GPIO单独控制,上电后拉低再拉高,低电平保持时间要足够长,否则PHY没有完成内部初始化。第二,RMII的REF_CLK必须实测确认频率。我一般会拿示波器量一下PHY的REF_CLK引脚,如果频率不是50MHz,后面所有链路上的问题都会表现得像玄学,时而通时而不通。

2.2 LWIP协议栈:裸机raw API还是带OS线程

F407上跑UDP基本绕不开LWIP这个名字。它是轻量级TCP/IP协议栈,为RAM和Flash受限的MCU设计,支持IP、ICMP、UDP、TCP,还有DHCP客户端等可选功能。集成方式有两种:一是用芯片厂商提供的图形化初始化工具生成基础代码,二是手工把LWIP源码移植进工程。我的建议是走图形化工具生成,不是因为它生成的代码有多优雅,而是它能把ETH的DMA描述符、PHY初始化、LWIP内存池一次性配好,出错的概率最小。自己手工移植不是不行,但排查起来会多很多变量,不适合第一步就走。

在编程模型上,LWIP有两条路线。裸机方式使用raw API,也就是udp_new、udp_bind、udp_recv这一套回调接口;带RTOS的方式则常用netconn或socket API,LWIP会在自己的线程里等信号量,再分发给应用层。这里的取舍很实际:如果整个工程只有以太网一个业务,裸机raw API就够了,一个udp_recv回调加一个发送函数,逻辑非常清晰,内存占用也更好估算。如果工程里还要跑Modbus轮询、按键扫描、屏幕刷新这些任务,我一般会引入FreeRTOS,然后走netconn层,因为发送函数可以从任意线程调用,不用太担心LWIP内部临界区冲突。

内存模型是选型时最容易忽略的点。LWIP内部大量内存是静态规划的,裸机版默认不开线程,所有协议处理都在中断加主循环里完成;带OS版本会为协议栈单独建线程,需要一个较大的栈空间。无论选哪种模式,编译前都要把内存池大小想清楚,不然程序跑一段时间突然收不到包,大部分原因就是PBUF池被占满或者内存堆耗尽,而这种现象在UDP裸机模式下尤其隐蔽,因为不会有明显的报错。

2.3 UDP比TCP适合什么样的主机通信

TCP有可靠传输、顺序保证,但代价是连接状态、重传队列和三次握手。针对“主机发送接收程序”这个标题场景,UDP一般更合适,理由可以拆成三点:数据采集设备要求低延迟,UDP没有ACK和窗口管理,发送方不需要等确认;设备大多处于固定局域网,丢包率本身很低,偶尔丢一包可以在应用层解决;主机可能随时掉线重连,UDP不需要维护连接状态,对端重启后马上能恢复通信。

UDP的缺点同样明显,没有重传,没有顺序保证,所以实际做产品时我不会只发裸数据,通常会在应用层加一个帧格式:帧头、长度、帧序号、数据、校验。帧序号用来检测丢包和乱序,校验和用来确认数据完整性,对端收到后能立刻判断要不要补发。这个设计在F407和主机侧是对等的,属于“UDP不可靠,应用层来兜底”的常规做法。把选型讲清楚之后,下面直接进入配置步骤和最小UDP收发代码。

3. 把LWIP跑起来:从图形化初始化到第一个能收发的UDP工程

3.1 CubeMX侧的关键配置:PHY地址、时钟树和RMII引脚

打开图形化初始化工具,选好F407型号后,按下面这张配置清单走一遍,大部分网络不通的问题都能在源头规避。

配置项推荐值说明
SYS DebugSerial Wire留出调试口,避免引脚冲突
RCC HSECrystal/Ceramic Resonator板载晶振作为主时钟来源
ETH外设RMII选择RMII模式,自动分配引脚
ETH PHY Address0x00(LAN8720A默认)与原理图PHYAD引脚一致
MCO1输出50MHz给PHY REF_CLK若PHY侧无独立时钟,必须启用
LWIPEnabled,裸机raw API不带OS时选裸机模式

RMII模式下F407会占用一组固定引脚:PA1是RMII_REF_CLK,PA2是MDIO,PA7是CRS_DV,PC1是MDC,PC4和PC5是RXD0和RXD1,PB11是TX_EN,PB12和PB13是TXD0和TXD1。画板子的时候这组引脚不能挪作他用,即使没用到以太网功能也最好不要复用,因为信号时序要求比较严格。核对原理图时重点看PHY的地址引脚,LAN8720A的PHYAD默认下拉,地址是0x00,如果原理图上把它拉高,CubeMX里的PHY地址就要对应改成1,否则初始化阶段读不到PHY的ID。

时钟树这里值得多说一句。RMII要求REF_CLK是精确的50MHz,如果板子用MCO供给这颗时钟,就要在时钟树里根据实际晶振频率配置PLL和MCO分频。不同板子的外部晶振不一样,有8MHz也有25MHz,所以这里没有固定的一组M/N/Q可以直接抄,正确做法是配置完以后用示波器量PA8引脚的频率,确认是50MHz再往下走。我在调试时见过太多次因为MCO少配了一级分频,PHY完全不通,还把问题怀疑到芯片本身。

3.2 LWIP内存参数:MEM_SIZE、PBUF_POOL_SIZE和PBUF_BUFSIZE怎么配

CubeMX生成代码时会自动带入LWIP的配置文件,里面有几个参数直接决定UDP收发能否承受住负载。这些参数看起来只是数字,但调错后的现象非常具有迷惑性,例如能ping通但UDP收不到、回调偶发不触发、高帧率下丢包。主要关注三个:

参数名常见起点值影响
MEM_SIZE4096或更大全局内存堆大小,发送较大数据块时不足会返回ERR_MEM
PBUF_POOL_SIZE16到32接收和发送缓冲区数量,偏低时高负载丢包
PBUF_POOL_BUFSIZE1520单个PBUF容量,小于1500会导致以太网大帧被丢
MEMP_NUM_UDP_PCB4或8可同时创建的UDP控制块数量

如果只是一条UDP收发链路,用8个PBUF池和1520字节的BUFSIZE就够跑。如果需要同时向上位机发送视频流或大批量日志,我会把PBUF_POOL_SIZE拉到32左右,同时把MEM_SIZE提高到8192以上。这个配置不能盲目改大,F407的内核RAM总共128KB,LWIP占太多,业务代码就没有地方放。合理做法是先把工程跑起来,测试中观察丢包率,再按需逐步增加,直到稳定为止。

3.3 最小UDP回显代码:回调、pbuf释放和发送函数

现在给一个能在F407上直接跑通的最小UDP回显工程。目标:开发板绑定8080端口,PC发任意数据过来,板子原样回发到PC。先看接收回调部分。

/* udp_echo.h 中声明全局PCB句柄 */ struct udp_pcb *udp_echo_pcb; /* 接收缓冲区,回调里立即拷贝,否则pbuf复用会覆盖数据 */ static uint8_t rx_buffer[2048]; /* UDP接收回调函数 */ static void udp_echo_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { uint16_t len; struct pbuf *resp; if (p == NULL) { return; /* 收到空pbuf则直接返回 */ } /* 立即把payload拷贝到自己的缓冲区 */ len = p->len; memcpy(rx_buffer, p->payload, len); pbuf_free(p); /* 释放接收pbuf,这步不能漏 */ /* 用新的pbuf组装回显数据 */ resp = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); if (resp != NULL) { memcpy(resp->payload, rx_buffer, len); udp_sendto(pcb, resp, addr, port); pbuf_free(resp); /* 发送完成后释放,否则内存池会枯竭 */ } } /* 创建UDP控制块并绑定端口 */ void udp_echo_init(void) { udp_echo_pcb = udp_new(); if (udp_echo_pcb != NULL) { udp_bind(udp_echo_pcb, IP_ADDR_ANY, 8080); udp_recv(udp_echo_pcb, udp_echo_recv, NULL); } }

这段代码最关键的是pbuf的释放时机。LWIP的接收pbuf是从固定内存池分配的,回调函数返回后,这个pbuf就会被协议栈回收复用。如果你在回调里保存了payload指针,留到主循环再读,大概率读到的是被覆盖后的数据。所以回调里第一件事是memcpy,然后立刻pbuf_free,最后的发送用新pbuf完成。这个习惯比任何优化都重要。而udp_sendto内部的addr和port参数决定了回包发给谁,它们来自回调参数,天然带着对端的IP地址和端口,不需要自己再保存。

再看主动发送的接口,这个函数可以从任意地方调用,例如按键、定时器或者传感器采集完成后:

/* 向上位机发送一帧UDP数据 */ void udp_send_packet(uint8_t *data, uint16_t len, const ip_addr_t *dst_ip, u16_t dst_port) { struct pbuf *p; p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); if (p == NULL) { return; /* 内存不足时直接退出,不能阻塞业务 */ } memcpy(p->payload, data, len); if (udp_sendto(udp_echo_pcb, p, dst_ip, dst_port) != ERR_OK) { /* 返回值非ERR_OK说明发送失败,最常见的是内存不足或ARP未解析 */ } pbuf_free(p); }

第一次调用udp_sendto时,LWIP会先查ARP表,如果目标IP不在缓存里,协议栈会先发ARP请求,等拿到应答后UDP包才能正常出去。所以在实际项目中,设备上电后我会主动向主机广播一个UDP包,例如发一个状态帧到255.255.255.255:8080,让主机和交换机的ARP表都先建立记录,后续通信就不会有“第一个包丢掉了”的问题。如果上位机环境无法接收广播,那就先ping一下开发板,效果是一样的。

主循环里还有一件事不能省。裸机模式下LWIP依赖主循环来驱动超时处理和报文转发逻辑,所以主循环结构一般是无线循环里调用MX_LWIP_Process(),中间插入其他业务函数时要注意耗时,一个超过几十毫秒的阻塞型任务都会让UDP的实时性明显变差。如果确实有大压力业务,就考虑把系统切到FreeRTOS加netconn模式。

4. 从回显到能干活:主机收发程序的调试思路与验证手段

4.1 先用PC回环测试定位问题

程序烧进去之后,第一步不是直接接工业现场,而是先在桌面环境下做回环验证。把PC的网卡IP手动设置为192.168.1.100,掩码255.255.255.0,开发板IP设置为192.168.1.10,然后用网线直连。现在的PC网卡基本都支持自适应交叉,不需要特意用交叉线。PC端打开网络调试软件或者直接写个Python脚本,向192.168.1.10:8080发送一条消息。这里有一个判断技巧:如果板子的UDP回调能被触发,但回包PC收不到,那问题出在发送路径;如果回调根本没触发,连ping都不通,那问题在物理链路、PHY时钟或者ETH初始化。

先给出一个最小测试脚本,PC端用来发UDP数据并接收回显:

import socket import sys # 目标地址与端口,按开发板实际IP修改 dst_ip = "192.168.1.10" dst_port = 8080 msg = b"ping from host" # 创建UDP socket并发送 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(2.0) sock.sendto(msg, (dst_ip, dst_port)) print("sent:", msg) try: data, addr = sock.recvfrom(1024) print("received:", data, "from", addr) except socket.timeout: print("timeout, no response from board")

如果发出去后没有及时回应,第一步ping开发板IP,看能不能通。ping通了但UDP没反应,说明IP层没问题,要回到LWIP配置去检查端口号、PCB创建和回调注册;ping不通,则说明问题在更低层。我常用的排查顺序是:先量PHY的REF_CLK,再读PHY寄存器的ID,然后看ETH的DMA描述符状态。这三步走完,物理层的嫌疑基本可以排除。

4.2 抓包观察链路:看UDP请求、ARP应答和ICMP

光凭开发板调试串口打日志,很难判断“包到底有没有到网线”。所以我习惯打开抓包工具,在PC端实时抓以太网口数据包。抓包不需要装驱动,只要PC网卡能收到数据即可。常用的过滤表达式有以下几条:

udp.port == 8080 arp icmp

第一眼先看ARP。PC向开发板发UDP时,如果开发板回包了,PC会先发送ARP请求询问192.168.1.10的MAC地址。抓包列表里如果能看到PC发出的ARP请求,但看不到开发板的ARP应答,那问题几乎一定出在PHY的发送通道或者LWIP的ARP模块上。我在类似产品上遇到过一种情况:PHY能收到数据,但发送引脚虚焊,导致所有回包都出不去,现象就是UDP请求有、回包没有、ARP也没有应答,和这种抓包结果完全吻合。

再看UDP层。PC发的请求报文能抓到,但板子没有回应,那就需要区分两种情况:一是板子收到了包但应用层没有处理,二是板子根本没收到包。为了区分,我一般会在回调里加一个调试计数器,每进一次回调就把计数打印到串口。如果串口没有计数增长,说明包没有进到应用层,重点查LWIP的PBUF池和中断是否正常触发;如果计数增长了但不回包,重点查udp_sendto的返回值和发送缓冲区状态。

# 抓包工具里查看目标端口流量 udp.port == 8080 # 查看所有ARP交互,用于判断链路层是否正常 arp # 查看ICMP回显,用于判断IP层是否通 icmp

抓包还有一个隐藏的好处,是能直接看到PC发过来的UDP载荷长度和端口。有些上位机发送端会默认把数据封装成JSON或者附加换行符,如果板子的协议解析没有考虑这一点,就会出现“收到但解析失败”的假故障。抓包能让你第一时间确认线上跑的数据格式,避免在两个设备之间互相猜。

4.3 压力测试:持续高负载下怎样才算通过

回环通了一次有两种情况:一是稳定传输,二是碰巧成功。为了不把运气当能力,我会跑一个简单的压力测试。PC端高频向开发板发送数据包,统计开发板回包的丢失率。这里给一个参考脚本,发送1000个UDP包并计算回显接收数量。

import socket import time sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(0.2) dst_ip = "192.168.1.10" dst_port = 8080 total = 1000 received = 0 for i in range(total): payload = b"stress-%04d" % i + b"x" * 1024 sock.sendto(payload, (dst_ip, dst_port)) try: data, _ = sock.recvfrom(2048) received += 1 except socket.timeout: pass # 回包超时,记录为丢失 print("total: %d, received: %d, loss: %.2f%%" % (total, received, (total - received) * 100.0 / total))

对这个测试,我的验收标准是:局域网直连环境、1000包各1KB负载、丢包率小于0.1%才算基本合格。如果出现超过1%的丢包,先看PBUF_POOL_SIZE是不是太小;如果这个数值已经调到32还丢包,再看主循环里有没有其他任务长时间占用了CPU,或者ETH中断优先级是否被别的高频中断抢占。还有一个经常被忽略的点是MAC过滤和多播帧干扰,如果开发板接入的交换机上有大量广播帧,LWIP的过滤机制也会消耗一定CPU,必要时可以用MAC过滤寄存器做针对性接收。

5. 避坑:UDP收发调试中的5个高频翻车现场

5.1 现象:网线插上但PHY的link灯不亮,初始化卡死

这是最常见的第一板故障。现象很直接,程序启动后HAL_ETH_Init返回错误,PHY的寄存器读不到正确ID,或者link状态一直起不来。原因大多数不是芯片坏了,而是给PHY的50MHz REF_CLK没有实际生成。用示波器量PA8或者PHY的REF_CLK引脚,如果频率不是50MHz,就要回到时钟树检查MCO的分频链路。另一个原因是PHY的复位引脚时序不对,复位拉低时间太短,PHY还没准备好就被访问了。解决方法是先测时钟,再接示波器看复位引脚电平变化。复位脚最好用GPIO手动控制,上电流程初始化GPIO后先拉低20ms再拉高,延时足够再访问PHY寄存器。时序和时钟都正确后,很少还有初始化失败的情况。

5.2 现象:能收到UDP包,但发回PC的回包PC收不到,主机还一直发ARP

这个场景出现在板子初步跑通后。PC端网络调试软件能收到板子发来的第一条广播,但之后发送的UDP请求都得不到回应,抓包可以看到PC在反复发ARP请求,而板子没有应答。问题根源通常是发送路径不通,而发送路径里最先出问题的就是ARP应答。ARP应答由LWIP在内部自动处理,如果它没有出现,大概率是ETH的TX描述符没有正确回收,或者PHY的TXD数据线有焊锡短路。我会先换一块确认完好的板子做交叉测试,排除硬件问题后,再看LWIP的pbuf池是否被占满。如果代码里漏掉pbuf_free,发送几次后内存池就满了,所有的ARP和ICMP都发不出去,表现恰好就是“能收不能发”。

5.3 现象:发送超过1472字节的UDP包,udp_sendto返回非ERR_OK

UDP单包载荷的最大理论值是65535,但受限于以太网MTU。标准局域网MTU是1500字节,IP头部占用20字节,UDP头占用8字节,所以UDP载荷最多1472字节。LWIP默认没有开启IP分片功能,应用层尝试发送超过1472字节的数据包时,udp_sendto会返回错误,代码如果没检查返回值就会表现为“数据偶尔发不出去”。解决办法是应用层自己分包,每包控制在1472字节以内,并加上帧序号方便对端重组。这个规则在F407和PC主机两侧都要同步遵守,不然就会出现“PC能发大数据,板子却回不了”的怪现象。

5.4 现象:回调里能看到数据正确,主循环里再读就变得残缺

新手很容易这样写:udp_recv回调里把p->payload的指针保存到全局变量,主循环再去解析。结果会发现数据时而对时而乱,尤其在连续收发时几乎必定出错。原因就是之前强调的pbuf复用机制,回调返回后,pbuf回到协议栈内存池,下一个数据包到来时会直接覆盖这块内存。解决方法是回调里立即memcpy到自己的缓冲区,或者直接在自己的静态缓冲区内完成协议解析。不要试图延长pbuf的生命周期,除非你有充分理由掌握pbuf的引用计数管理,否则代价远大于收益。我踩过一次这个坑之后,现在所有UDP工程都会在回调入口第一行写拷贝动作,这个习惯能省掉一整天排查时间。

5.5 现象:FreeRTOS加LWIP,高频调用udp_sendto偶尔导致任务卡死

带操作系统的工程里,UDP收发正常且稳定,但运行一段时间后某个调用udp_sendto的任务会卡住,其他任务还在跑。原因通常是LWIP协议栈线程与调用方线程同时访问了同一个PCB结构,单个接口没有加锁机制,或者LWIP协议栈线程优先级低于业务线程,产生优先级反转。解决方法是把LWIP相关线程调成较高优先级,并在业务代码里避免连续高频调用udp_sendto,能合并的数据就合并以后再发。如果工程对实时性要求高,也可以用裸机raw API配合简单互斥,彻底绕开多线程竞争问题。裸机模式虽然要自己规划调用时机,但在F407这种单核MCU上反而更容易控制。

6. 进阶:让UDP上限更高——零拷贝收发、多播过滤与断网自动恢复技巧

6.1 零拷贝发送:先分配PBUF再往payload里填数据

常规发送流程是先准备好业务数据到静态数组,再调pbuf_alloc和memcpy填进pbuf。多了一次拷贝,对大数据块影响不小。可以反过来做:先pbuf_alloc拿到payload指针,直接把传感器数据或协议帧填进去,再调udp_sendto发送。这样省掉一次memcpy,在批量传输场景下能明显降低CPU占用。注意事项是PBUF_RAM类型的pbuf分配后payload区域是连续的,但不要超长写入,否则会踩坏内存池的管理区。

6.2 多播和广播:一对多通信的边界在哪里

如果主机要同时向多台F407设备下发命令,可以每台单独UDP单播,也可以让设备加入一个多播组,主机向多播地址发一包数据,所有加入该组的设备都能收到。LWIP里加入多播组需要调用igmp_joingroup函数,并且配置LWIP_IGMP为1。广播相对简单,目标地址直接用255.255.255.255,但要注意路由器不会转发广播,只有同一广播域内的设备才能收到。多播也有类似限制,交换机的IGMP Snooping功能会决定多播帧是只发给订阅端口还是泛洪到所有端口,板卡端需要确认组地址没有被三层设备过滤掉。

6.3 断网自动恢复:不重启设备把链路拉回来

很多现场设备没有人工看守,网线被误拔后重新插上,必须能够自恢复。这里的关键是不要复位整个MCU,只复位PHY和重新初始化ETH链路。我习惯写一个周期性任务,通过netif_is_link_up检查netif链路状态,如果链路断开,先拉低PHY复位引脚并重新初始化PHY,再调用ETH的重新初始化函数,最后恢复LWIP的netif状态。这个流程在大部分LAN8720A方案上都有效。完善的版本还可以同时检测ARP表,如果主机IP变了,要通知应用层更新通信目标。这种“软恢复”机制配合启动时的主动广播,能显著降低设备在无人值守场景下的失联率。

这类网络调试验证工作我踩过不少次坑,最值钱的教训就是顺序:先确认PHY时钟和地址,再做物理链路测试,最后才碰协议栈代码。这个顺序一旦颠倒,每个问题都会像黑匣子一样反复折磨人。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询