
野火F407的开发板在桌角吃了半年灰最近因为一个远程数据采集的需求被重新抓了壮丁。项目要求很简单板子通过网口入网既能主动向服务器上报数据也能被PC端随时连上来读状态。翻译成人话就是——以太网Ping通是底线TCP Client和TCP Server都得能跑。于是就有了这篇STM32和DP83848之间的折腾笔记核心是LwIP协议栈在F407上的裸机实战。写这篇文的初衷很直接STM32以太网这个方向教程一眼望去全是“打开CubeMX、勾选LwIP、生成代码、然后就可以抄例程了”但真正上手之后你会发现卡住你的往往是最不起眼的硬件引脚、PHY地址、时钟来源这类底层问题。这篇文章会按我实际踩坑的顺序展开野火F407与DP83848该怎么接线、CubeMX里怎么配置、LwIP裸机移植时要改哪些参数、Ping不通时如何一层层排查以及TCP Client/Server的纯裸机API实现。适合跟我一样第一次接触STM32以太网、想让板子真正连上网的朋友参考。1. 野火F407与DP83848的硬件连接别急着写代码1.1 先确认你板子上的PHY到底是谁野火的F407开发板有好几个版本不同批次板载的以太网PHY不完全一样。我手里这块板子的网口部分用的是DP83848但网上很多野火F407的教程都默认是LAN8720A这两个芯片的寄存器定义、PHY地址、驱动方式都有差异直接套用例程基本是白费功夫。所以拿到板子的第一件事是去官网下载这块板子的原理图PDF搜一下PHY芯片的具体型号和它周围的电路。确认DP83848的VDDIO电压、复位引脚接到了MCU的哪个IO、PHYAD地址配置引脚分别接到了什么电平。这一步花十分钟后面能省十个小时。另外如果你用的是外接DP83848模块还要确认模块的排针是MII接口还是RMII接口。市面上很多DP83848模块同时引出了两种接口用杜邦线连接时容易插错排插错之后不仅Link灯不亮严重时可能把PHY芯片烧掉。我的建议是直接用RMII引脚少而且F407的RMII引脚是官方推荐的固定映射不容易出错。1.2 RMII模式下F407与DP83848的引脚映射RMII是精简的以太网接口相比MII的16根线它只需要9根信号线。F407内置的以太网MAC支持RMII模式引脚映射是固定的我整理成表格方便对照STM32F407引脚RMII信号方向说明PA1ETH_RMII_REF_CLK输入50MHz参考时钟PA2ETH_MDIO双向管理数据线PC1ETH_MDC输出管理时钟线PA7ETH_RMII_CRS_DV输入载波侦听/数据有效PC4ETH_RMII_RXD0输入接收数据位0PC5ETH_RMII_RXD1输入接收数据位1PB11ETH_RMII_TX_EN输出发送使能PB12ETH_RMII_TXD0输出发送数据位0PB13ETH_RMII_TXD1输出发送数据位1DP83848模块的对应信号直接和这个表一一对接就行。需要注意PHY的复位引脚通常叫RST/NRST有的模块直接上拉到VDDIO有的需要MCU的GPIO控制。如果是后者要在CubeMX里额外配置一个GPIO初始化时先拉低再拉高完成复位拉低时间至少保持10微秒以上。这个复位时序如果不对PHY可能处于不确定状态表现出来就是Link灯不亮或者MDIO读写超时。1.3 REF_CLK的来路问题这里最容易埋雷RMII模式要求MAC和PHY共享同一个50MHz参考时钟这个时钟怎么来是端口配置里的第一个大坑。DP83848这种PHY芯片通常在设计上自带25MHz晶振然后通过内部PLL把REF_CLK输出引脚CLK_OUT变成50MHz。这种情况下你只需要把F407的PA1ETH_RMII_REF_CLK直接连到DP83848的CLK_OUT引脚。这是我最推荐的方式因为PHY和MAC的时钟天然同步不用额外配置MCO。还有一种方式是用STM32的MCO1引脚PA8输出50MHz时钟给PHY的XI/XO输入。但这种方式有几个前提PA8的MCO1时钟源要配置成PLL的50MHz输出PHY那边不能再同时接自己的晶振否则两个时钟源互相打架。我当时一开始配的是MCO方式结果发现PHY的Link状态寄存器读出来一直在“连接/断开”之间来回跳。后来把PHY模块上的25MHz晶振拆掉、改用DP83848 CLK_OUT直接输出给PA1问题才彻底消失。这里多说一句DP83848模块本身的CLK_OUT也不是所有版本都有引出来的。买模块的时候留意一下原理图如果CLK_OUT没有引出那你只能走MCO方案或者自己用有源晶振搭一个50MHz时钟。动手前一定把这些确认清楚。1.4 上电第一步用万用表和示波器验证硬件健康硬件接好之后先别急着烧KEIL工程。花五分钟做几项检查万用表量PHY芯片的电源引脚确认VDDIO是3.3V而不是3.0V或1.8V电压不对PHY不会正常工作。复位引脚在复位释放后应该是高电平。如果一直被拉低查一下GPIO配置或者复位芯片。插上网线看RJ45座子的Link/ACT指示灯。如果Link灯不亮说明物理层没起来这时候写多少软件代码都没用。先查PHY地址配置、供电、网线、网络变压器。如果有示波器在DP83848的CLK_OUT引脚上应该能测到50MHz方波幅值接近3.3V。如果这里没有时钟输出PHY芯片大概率没正确启动。我当时就遇到了Link灯不亮的情况排查了很久最后发现是模块上PHYAD地址引脚默认设置和数据手册上的默认值不一样代码里写死PHY地址0x01但实际硬件是0x00导致MDIO根本访问不到PHY。这一点我会在第3节详细说明但在这里先说清楚调试以太网第一步永远是确认硬件链路而不是看代码。2. CubeMX配置心里要有数ETH、LwIP和时钟树的正确设置2.1 时钟树F407主频168MHz下的ETH时钟CubeMX里配置F407的时钟树时一般把主频拉到168MHz这是F407的最高工作频率。ETH外设本身挂载在AHB1总线上它的工作时钟来自AHB1分频后的时钟。这一步不需要太纠结168MHz主频下AHB1默认也是168MHz以太网MAC能正常工作。真正需要注意的是RMII的REF_CLK是不是50MHz。前面说了PA1这个引脚的50MHz时钟通常来自PHY的CLK_OUT所以CubeMX里不需要额外配置PA8的MCO功能。但如果你的硬件设计是让PA8输出50MHz给PHY那CubeMX里要把PA8复用为MCO1同时在Clock Configuration里把MCO1的时钟源选为PLLCLK并分频到50MHz。在CubeMX的Pinout视图中当你使能ETH外设时STM32会自动把PA1、PA2、PC1、PA7、PC4、PC5、PB11、PB12、PB13这些引脚配置为ETH功能。这里要特别检查一件事这些引脚有没有和其他外设冲突。比如PC4同时可能是SDIO的数据线PB12能复用为SPI2_NSS。一旦冲突CubeMX会提示此时需要优先满足以太网或想办法调整。2.2 ETH外设的参数配置PHY地址先对号入座在CubeMX的Connectivity - ETH页面里主要配置这么几项Mode选择RMIIPHY Address填写实际硬件地址。DP83848常见是0x01但也遇到过硬接线是0x00或0x04的务必以硬件原理图为准。MAC Address填写自己定义的地址不要用全0。随便编一个也没关系比如02:00:11:22:33:44只要局域网内不冲突就行。如果CubeMX版本较新下面还会出现PHY Configuration比如PHY Model选择DP83848或者让你配置PHY的时钟源、速率等。这些参数选对之后生成的代码里会自动带PHY读写函数。还有一项容易被忽略ETH中断优先级。在NVIC设置里ETH的中断优先级不要设得太高。如果优先级高于系统的UART或者其他外部中断可能在高负载时阻塞其他任务。一般设置优先级为5或者更低数值更大分组选4位抢占优先级时填5~6比较稳妥。2.3 勾选LwIP中间件裸机模式下的关键选项在Middleware and Software Packs里勾选LwIP进入配置页面Version选2.0.3或者2.1.2都可以我用的是2.1.2代码接口跟2.0.3在大部分地方兼容。不要勾选 RTOS 那一项这次我们跑裸机不需要操作系统。勾上RTOS之后CubeMX会让LwIP使用信号量、邮箱等OS抽象层裸机下用不了。IP模式建议先选Static IP静态IP。IP地址192.168.1.99子网掩码255.255.255.0网关192.168.1.1。等Ping通了再考虑用DHCP动态获取。如果CubeMX版本里可以看到TCP相关选项把TCP_WND、TCP_SND_BUF这些参数先记下默认值后面lwipopts.h里再统一调。Local Hostname之类的不用管。2.4 生成代码后第一个编译前的核查点点击GENERATE CODE生成工程后不要马上下载。打开Core/Src/main.c确认MX_LWIP_Init()被调用确认while(1)循环里有MX_LWIP_Process()。MX_LWIP_Process()在NO_SYS模式下负责周期处理LwIP的超时任务比如ARP缓存超时、TCP重传计时等。如果这个函数没有被循环调用TCP连接大概率连不成功表现的第一个症状就是DHCP一直失败或者TCP连接超时。另外要检查ethernetif.c里的low_level_init()这里会读取PHY寄存器判断链接状态。具体哪一行依赖PHY地址如果是CubeMX自动生成的代码它会用你前面配置的PHY Address。如果PHY地址和实际硬件对不上函数可能一直返回超时导致以太网接口永远处于Down状态。3. LwIP裸机移植与参数调整自动生成的代码不能直接用3.1 lwipopts.h里几个决定生死的宏CubeMX生成的LwIP配置默认能“跑通Demo”但真正用在项目里有几个参数一定要根据自己的场景调整。我直接从lwipopts.h里摘几个最关键的NO_SYS必须为1这是裸机模式的核心开关。如果设成0LwIP会要求一个底层OS提供锁、邮箱等机制裸机编译直接报错。MEM_SIZE协议栈堆大小单位是字节。默认值可能只有几千甚至一千多如果TCP收发数据量稍大很容易出现ERR_MEM。我建议至少配置到20KB以上。F407的RAM有192KB具体还要看你其他变量占了多少。PBUF_POOL_SIZEPBUF池中缓冲区个数默认是8个左右。这个值如果太小在多连接并发或者连续大流量收包时新到的数据包会因为拿不到pbuf被直接丢弃表现为TCP吞吐量上不去、Ping丢包。我一般至少调到16。TCP_WNDTCP接收窗口大小单位字节。默认值可能只有4096意思是对方一次最多给你发4KB数据。如果传输大块数据这个值太小会严重限制吞吐量。调到8192或16384前提是RAM够用。TCP_SND_BUFTCP发送缓冲区大小同样默认4096。当tcp_write写入的数据超过这个值时会返回ERR_MEM需要等待tcp_sent回调腾出空间再继续。调大这个值能提升发送吞吐。这些参数之间是联动关系。PBUF_POOL_SIZE和MEM_SIZE要用RAMTCP_WND和TCP_SND_BUF也要用RAMF407虽然RAM有192KB但你的应用代码还要做数据处理、缓存、堆栈所以不要无止境调大。我个人的做法是先把基础功能跑通参数用小值再逐步调大观察内存余量。3.2 PHY地址检查MDIO能读到0x2000才算通过DP83848的PHY ID寄存器固定存在两个位置寄存器0x02读出0x2000寄存器0x03读出0xA140。这是验证MDIO管理接口是否通讯正常的金标准。我建议在ethernetif.c的low_level_init()里加一段临时调试代码读取寄存器0x02和0x03并打印出来uint16_t phy_id1 0, phy_id2 0; HAL_ETH_ReadPHYRegister(heth, 0x02, phy_id1); HAL_ETH_ReadPHYRegister(heth, 0x03, phy_id2); printf(PHY ID: 0x%04X 0x%04X\r\n, phy_id1, phy_id2);如果打印出来不是0x2000和0xA140就不要继续往下走先查PHY地址和MDIO引脚。我当时就遇到过读出来0xFFFF的情况后来发现是DP83848模块上PHYAD0引脚被接成了高电平PHY地址变成了0x00可代码里写的是0x01地址不对MDIO自然读不到。提示MDIO读寄存器时HAL_ETH_ReadPHYRegister返回HAL_TIMEOUT非常常见。除了PHY地址错误之外还有可能是MDC时钟频率过高。在HAL_ETH_Init时MDC时钟是以HCLK分频出来的CubeMX通常默认分频到2.5MHz左右不需要改。但如果一直是超时也可以从分频系数上排查。3.3 链路检测逻辑Link状态寄存器要连读两次在ethernetif.c里LwIP会周期调用一个检查PHY状态的函数通常叫Ethernetif_link_check或者类似函数它从PHY的BMSR寄存器地址0x01读取bit2来判断Link是否Up。这里有个很坑的细节PHY的Link状态位在读取后如果链路状态已经恢复该位会自动清零。也就是说你读一次得到0可能是链路真的断开也可能是这次读操作把状态位清掉了。正确做法是连续读两次第二次读到的值才是真实状态。如果代码里只读一次就判断可能在网线插拔、自动协商切换时频繁误报导致LwIP反复执行link reset和重启接口表现就是Ping时通时不通。我当时被这个问题折磨了很久最后在PHY驱动里改成这样uint16_t reg 0; HAL_ETH_ReadPHYRegister(heth, PHY_BSR, reg); HAL_ETH_ReadPHYRegister(heth, PHY_BSR, reg); if (reg PHY_LINKED) { // 链路已连接 }其实底层原理不复杂但很多人包括我第一次接触时想不到去看寄存器手册容易在应用层瞎猜原因。3.4 收发轮询裸机下ethernetif_input不能停在带RTOS的工程里LwIP通常会有一个独立线程专门处理网卡收包。但在裸机环境这个职责落到了主动调用上。CubeMX生成的工程里MX_LWIP_Process()主要负责超时时钟而收包动作需要额外调用。一种常见的做法是在主循环里高频调用while (1) { MX_LWIP_Process(); // 处理协议栈超时任务 ethernetif_input(g_netif); // 接收并注入协议栈 }ethernetif_input这个函数会从以太网DMA描述符里取已经收到的帧构造pbuf后交给LwIP的netif-input触发TCP/UDP/ICMP等协议处理。如果这一步没有执行那么即使PC发来了Ping请求F407的网络接口也永远收不到数据。关于中断还是轮询我个人的建议是调试初期选轮询先保证功能通。轮询不会丢中断只是实时性稍微差点。等整个链路稳定了再考虑改成中断驱动方式提高响应速度。4. Ping不通的排查链路从ARP到PHY一层层过4.1 第一步确认物理层真的Link上了Ping不通时最怕病急乱投医。我的排查顺序永远是固定的第一步看物理层。插好网线观察路由器或交换机上对应的网口指示灯以及F407板载RJ45的Link灯。如果Link灯不亮直接进入硬件或PHY配置排查不要看软件。如果Link灯亮了说明PHY的自动协商完成了物理层没问题。这时候在调试串口打印一下PHY的BMSR寄存器地址0x01确认bit2的值是1。打印之前注意连读两次。如果BMSR的Link位是0但灯亮多半是PHY地址读错了读到的不是DP83848的寄存器。4.2 第二步Wireshark抓包看ARP有没有应答物理层通了PC端Ping STM32的IP时第一件事是发送ARP请求询问目标IP对应的MAC地址。如果ARP请求没人应答Ping就会报“请求超时”。这一阶段在PC上打开Wireshark选对网卡过滤条件填arp然后去Ping STM32。你会看到几种情况只看到ARP请求没有ARP应答问题在STM32的MAC发送或LwIP的ARP处理上。继续往下查。看到ARP请求也有ARP应答但Ping请求超时问题可能出在ICMP处理或收发路径不对称重点查收包路径。连ARP请求都看不到问题大概率在PC的网卡配置、网段、路由或防火墙先确认PC的IP是否和STM32在同一网段防火墙是否拦截了ICMP和ARP。我当时的情况属于第二种能看到ARP应答但Ping的ICMP Echo请求发出去之后STM32没有任何响应。这说明ARP接收和发送都正常MAC收发都至少通了一半问题出在ICMP协议处理上。后来发现是ethernetif_input没有被主循环调用ICMP包进了MAC缓冲区但没被LwIP处理。4.3 第三步检查MAC接收路径和中断优先级把收包路径完整捋一遍DMA收到帧 - 触发ETH中断或轮询查询 - 调用HAL_ETH_GetReceivedFrame_IT / HAL_ETH_GetReceivedData - 构造pbuf - 调用ethernetif_input - netif-input - 协议栈处理。如果这一步是中断方式先确认ETH中断已经使能NVIC里中断号没被静默屏蔽。另外调试时可以在中断服务函数开头翻转一个GPIO用示波器看中断是否真的产生。用轮询方式时确认主循环里每轮都有调用ethernetif_input而且没有被一个耗时的阻塞循环挡住。4.4 第四步把MAC地址、IP、网关逐项过一遍这几个基础项也常被忽略。MAC地址不能用全0Linux或Windows的设备如果看到源MAC全0会直接丢弃。IP地址不要和PC重复子网掩码要一致网关可以留空但不能填一个错误网关。电脑的网卡属性里关闭IPv6阻断这一步不必要但可以顺手把PC的防火墙临时关掉排除ICMP被拦截的可能。这里有一个很实用的技巧先用一个已知能通的设备做对照。比如拿另一台PC配上同样的IP段看看能不能互Ping通。如果PC和PC之间都Ping不通那和STM32毫无关系先解决网络环境问题。5. TCP Client实战裸机下用raw API建立连接5.1 为什么裸机下要用raw TCP API而不是SocketCubeMX勾选LwIP之后你会发现生成代码里默认把LWIP_NETCONN和LWIP_SOCKET都设为0。这是因为裸机模式下没有线程调度Netconn和Socket这两个API依赖信号量和邮箱机制没有OS跑不通。raw TCP API是直接基于协议栈内核的接口通过回调函数驱动不需要阻塞和等待所以裸机下只能用它。当然如果你在CubeMX里勾选了RTOS那就可以用Netconn API甚至Socket API编程模型会更接近PC端。但在这篇文章的场景里我全程使用raw API因为它省去了RTOS引入的复杂度对理解TCP机制也更有帮助。5.2 连接流程tcp_new、tcp_connect和回调地狱TCP Client的核心流程是这样struct tcp_pcb *client_pcb NULL; ip_addr_t server_ip; uint16_t server_port 8080; void tcp_client_init(void) { IP4_ADDR(server_ip, 192, 168, 1, 100); // PC端的IP client_pcb tcp_new(); if (client_pcb ! NULL) { tcp_arg(client_pcb, NULL); tcp_err(client_pcb, tcp_client_err_callback); err_t err tcp_connect(client_pcb, server_ip, server_port, tcp_client_connected); if (err ! ERR_OK) { tcp_close(client_pcb); } } }tcp_connect是异步操作它发起SYN包后立即返回真正的连接成功要等到回调函数tcp_client_connected被调用。这个同步/异步的转变一开始很不习惯有点像在学习JavaScript时的callback地狱你发起动作然后被动等待对应的回调触发。连接成功回调里通常要做的事是注册接收回调static err_t tcp_client_connected(void *arg, struct tcp_pcb *pcb, err_t err) { if (err ERR_OK) { tcp_recv(pcb, tcp_client_recv); tcp_sent(pcb, tcp_client_sent); // 发送第一条数据 tcp_write(pcb, hello from stm32, 16, 1); tcp_output(pcb); } return ERR_OK; }5.3 tcp_write不立即发送tcp_output才是触发者很多第一次用raw API的人都会踩一个坑调用了tcp_write数据却迟迟没发出去。原因很简单tcp_write只把数据复制到内核发送缓冲区相当于把信塞进邮箱而tcp_output才真正把缓冲区的数据封装成TCP段并交给网卡发送。所以正确的做法是写完数据之后紧接着调用tcp_output。如果对数据实时性有要求还可以用TCP_WRITE_FLAG_PUSH这个flag告诉协议栈这条数据可以立即推送。比如tcp_write(pcb, data, len, TCP_WRITE_FLAG_PUSH); tcp_output(pcb);另一个常见的坑是tcp_write返回ERR_MEM。这表示发送缓冲不足。你要么等tcp_sent回调被调用说明缓冲区腾出了一部分空间之后再写入要么把一次发送的数据量改小。如果在回调里写不进去就硬写可能造成无限重试导致系统卡死。5.4 接收回调记得调用tcp_recved释放窗口当一个TCP段到达时LwIP会在协议栈上下文调用tcp_recv回调。这个回调传递进来的p参数是一个指向数据的pbuf用完必须释放这是基本要求。但更关键的在于处理完之后要调用tcp_recved(pcb, p-tot_len)告诉内核“我已经把数据交给应用层了可以扩大接收窗口继续接收新数据了。”如果不调用tcp_recvedTCP窗口会一直不释放对端发送数据时窗口越来越小很快变成0窗口双方通信直接卡死。这个卡死不是连接断开而是双方都干等着非常难排查。我在最开始写这个回调时就漏了tcp_recved结果是连上之后能收一两包数据之后就彻底没反应了。正确的接收回调模板static err_t tcp_client_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p NULL) { // 对端关闭连接浅写一下清理逻辑 tcp_close(pcb); client_pcb NULL; return ERR_OK; } // 将数据转发到串口、内存队列或应用处理函数 process_tcp_data(p-payload, p-len); tcp_recved(pcb, p-tot_len); pbuf_free(p); return ERR_OK; }5.5 重连和心跳产品化必须考虑的事情调试阶段连上就行但实际跑起来路由器重启、服务端重启、网线拔插都会导致连接断开。裸机LwIP不会自动重连必须自己处理。我的做法是在tcp_err回调里置一个断连标志位表示当前连接已释放。主循环检测到这个标志后延时几秒重新调用tcp_client_init()发起连接。注意tcp_err回调触发时pcb已经被协议栈内部释放了不要再对原来的pcb指针做任何操作否则就是悬空指针。另外建议在应用层做心跳机制。TCP是传输层协议它本身不能保证应用层心跳。每隔固定时间发送一个自定义格式的小包服务端如果连续N个周期没收到心跳就主动关闭连接。F407这边同样如果长时间没收到服务端心跳也应主动断开重连。这类经验在Modbus TCP、MQTT这类应用层协议里也很普遍底层思路是同一个道理。6. TCP Server实战监听、accept和连接资源管理6.1 创建监听sockettcp_bind后tcp_listen返回的是新pcbTCP Server端的代码流程和Client端不太一样。先用tcp_new创建一个pcb绑定IP和端口然后调用tcp_listen把它变成监听用的pcbstruct tcp_pcb *server_pcb NULL; void tcp_server_init(void) { server_pcb tcp_new(); if (server_pcb ! NULL) { err_t err tcp_bind(server_pcb, IP_ADDR_ANY, 8080); if (err ERR_OK) { server_pcb tcp_listen(server_pcb); tcp_accept(server_pcb, tcp_server_accept); } } }这里有一个必须注意的问题tcp_listen会返回一个新的pcb指针它内部会释放掉原来的pcb。所以一定要用返回值覆盖原来的指针。很多人在这一步只写成tcp_listen(server_pcb)而不接收返回值后面accept回调永远不触发代码看起来完全没问题但server就是连不上。6.2 accept回调为每个新连接分配独立的连接控制块当PC端发起TCP连接时三次握手完成后LwIP会调用tcp_accept注册的回调函数。请开始说static err_t tcp_server_accept(void *arg, struct tcp_pcb *new_pcb, err_t err) { if (err ! ERR_OK) return err; if (连接数量已达上限) { tcp_abort(new_pcb); // 直接终止新连接 return ERR_ABRT; } // 保存new_pcb到连接管理数组注册接收、断开、轮询回调 tcp_recv(new_pcb, tcp_server_recv); tcp_err(new_pcb, tcp_server_err); tcp_poll(new_pcb, tcp_server_poll, 2); // 每2秒轮询一次 return ERR_OK; }裸机环境没有多线程所有客户端的TCP收发都在同一个回路里处理。为了管理多个连接需要自己维护一个连接数组或链表每个节点保存pcb指针、连接状态、最近活跃时间等。我一般限制最多4个客户端超过就tcp_abort新连接。这样在RAM受限的MCU上能控制资源开销。6.3 tcp_poll轮询对付异常断网的关键TCP是可靠的传输层协议但也敌不过物理断线。比如客户端突然断电或者网线被拔掉对端不会发FIN包服务端也不知道连接已死。如果不对这种半开连接做处理连接表中的pcb会一直占着时间久了资源耗尽。LwIP提供了tcp_poll回调可以在accept里注册。tcp_poll会在每隔一段周期内被调用周期长度在第三个参数里指定单位是“协议栈心跳间隔”。我设置为2意思是每2个心跳调用一次。在这个回调里检查连接最近是否有数据收发如果超时比如30秒没有任何数据就主动tcp_abort断开这个连接并释放对应的连接控制块。6.4 数据下发主动推送和请求应答的两种模式TCP Server在两个典型模式下的实现差别还是明显的。请求应答模式最简单客户端发一帧请求服务端解析后在recv回调里直接组织响应tcp_write加tcp_output发回去。这种模式在Modbus TCP、私有协议、HTTP服务器里非常常见。所有处理在一个回调函数内就能完成不需要额外的全局状态机。主动推送模式稍微复杂比如定时上传传感器数据或者状态发生变化时主动通知客户端。这需要一个定时器周期性遍历所有已连接的pcb向每个连接发送数据。此时要特别注意发送缓冲区的状态如果某个客户端接收很慢tcp_write可能返回ERR_MEM这时不能硬塞要跳过该连接等下一个周期再发。另外在遍历连接时如果这个连接刚被关闭pcb指针可能已经失效要在数据结构里做好标记。6.5 PC端联调工具和经验Windows下我用的是网络调试助手这一类通用工具可以快速测试服务端端口是否监听正常。发送自定义帧的时候要注意字节序STM32是小端模式网络字节序是大端所以我一般自己定义协议头时统一用大端避免两端对不上。Linux下更推荐用Python写临时脚本改起来方便还能直接看到十六进制内容。给一个最简单的TCP客户端脚本用来对F407做联调import socket s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((192.168.1.99, 8080)) s.send(b\x01\x03\x00\x00\x00\x0A) print(s.recv(1024)) s.close()如果这个脚本能收到STM32的响应说明TCP Server链路完全通了。之后再做压力测试比如循环发1000次数据观察有没有丢包、断连、内存泄漏。7. 稳定性问题复盘DP83848接收不稳定、Ping时通时不通、速度上不去7.1 DP83848接收数据不稳定硬件和软件分别怎么查DP83848这个PHY芯片本身是成熟物料但如果出现接收不稳定比如Ping丢包、TCP重传率高、偶发收不到数据不外乎几个原因。硬件层面先排查REF_CLK 50MHz时钟抖动。当时钟质量不好时PHY的数据采样会出现位错误轻则CRC校验失败、重则整个链路频繁掉线。用示波器看CLK_OUT引脚如果波形沿不够陡或者有过冲、振铃优先处理电源退耦和时钟走线。电源纹波。PHY芯片对供电质量要求较高3.3V的纹波超过50mV就可能引发随机错误。在PHY引脚旁边并联高频去耦电容是必须的。RMII信号线之间的串扰。特别是杜邦线连接时TXD0、TXD1、TX_EN、REF_CLK这些线如果绑在一起太长高速信号会互相干扰。尽量缩短连接线有条件的话用排线和地线一一间隔。软件层面排查顺序先把自动协商改成强制100M全双工。在PHY的BMCR寄存器地址0x00写入0x2000可以强制100M全双工。如果强制之后问题消失说明自动协商协议有兼容性问题。调大PBUF_POOL_SIZE。接收缓冲区不够时新到的帧直接丢弃表现就是Ping丢包或者大包传不了。开启LwIP的调试日志看有没有报PBUF或内存不足的错误。LWIP_DEBUG打开后信息量会非常大串口波特率建议调高点。7.2 Ping时通时不通大多数时候不是Ping的问题我遇到过这么一种现象刚上电时Ping通了十几包之后全部超时重启又恢复。查来查去最后发现MAC地址没写设备在使用一个随机生成的或者初始化的MAC地址不是最终发现是PHY的自动协商状态不稳定链路在100M和10M之间来回切换。TCP/IP协议栈在检测到链路断开时会执行netif_set_link_down并释放相关连接。如果PHY状态寄存器不稳定一会儿Link Up一会儿Link DownLwIP就反复重置网络接口Ping自然断断续续。解决方式有两个方向硬件上改善网络变压器和PHY芯片之间的匹配换一根质量更好的网线试试。软件上在PHY驱动里增加状态变化去抖比如连续三次读到相同的Link状态才认为链路真的变了。另外还有一个容易被忽视的点STM32F407的ETH DMA描述符数量默认是4个TX和4个RX。如果RX描述符全部被占满新到的包就会直接丢弃。而这个状态可能持续很久导致Ping超时。在HAL配置里把接收描述符数量增加到8个能缓解这个问题。7.3 传输速率上不去怎么定位瓶颈如果TCP吞吐量一直卡在上不了几MB/s可以从三个方向定位瓶颈第一协议栈窗口太小。TCP_WND和TCP_SND_BUF都只有几KB时吞吐量必然受限。这就好比一条水管协议栈的窗口相当于水管的直径直径只有几毫米物理链路再快也白搭。以F407的RAM余量把这两个值调到16KB左右是可行的。第二发送路径中没有批量处理。如果一帧一个数据包每包只有几十字节以太网帧的有效载荷利用率太低实际吞吐量自然上不去。尽量把数据攒到一定长度再一次性发送比如1024字节或更多。第三中断频率过高导致CPU占用过大。每收一个小包就进一次中断F407虽然在100M带宽下勉强够用但中断处理开销会影响协议栈其他工作。优化方式是关掉ETH中断改轮询不是轮询在低速率下省CPU但要控制频率。更有效的是把DMA描述符和pbuf池调大减少中断唤醒次数。7.4 要不要直接用别人的例程我的建议是参考但不复制野火官方给F407提供了很多以太网例程比如TCP、UDP、HTTP Server等。我最初的方案就是直接拿官方例程改结果因为板子批次不同、PHY型号不同调试了三天还没跑通。后来痛定思痛基于CubeMX重新生成工程把例程里几个核心文件比如ethernetif.c、lwipopts.h作为参考逐一比对反而两天就通了。原因很简单CubeMX生成的工程匹配当前的HAL库版本和芯片型号而野火例程是在特定时间和环境下编写的库函数、结构体定义和寄存器配置可能有细微差异。直接用容易遇到莫名其妙的编译错误或运行异常。参考官方例程的正确姿势是把关键逻辑摘出来看比如PHY初始化顺序、收发回调的写法然后结合自己的工程重新实现。8. 一些个人经验以太网调试的节奏比技术本身更重要最后聊几个调完整个项目后的体会。节奏上我强烈建议按照“Ping通 - TCP Client - TCP Server”的顺序推进。Ping通过意味着MAC收发和ARP、ICMP处理都没问题这是最底层的验证。跳过这一步直接做TCP Client一旦连不上你很难区分是MAC收发包问题、协议栈路由问题还是TCP握手问题。每前进一步都建立在上一步被验证的基础上排查范围会小很多。日志上调试阶段串口打印的优先级比性能高。LwIP本身提供了LWIP_DEBUG宏打开后能输出TCP状态机的详细日志包括SYN、ACK、重传等事件对理解三次握手和异常断连帮助巨大。生产环境再关掉就行。版本上CubeMX、HAL库、LwIP的版本要记录下来。不同版本组合之间偶发差异性很大。我遇到过同样一份代码在LwIP 2.0.3下稳定跑换到2.1.2之后出现了pbuf泄漏最后定位是协议栈配置宏在不同版本里默认值不一致。遇到玄学问题换个版本组合试一试是很有效的排查手段。扩展上当你把这套以太网链路跑通之后上面的应用层协议就只是堆逻辑的事了。比如最常用的Modbus TCP本质是自定义应用层协议在TCP上的封装再比如温度采集系统也就是在TCP链路上加一个简单的请求响应帧。最近不少人在聊STM32鱼缸远程控制、车载以太网、和变频器通讯做Modbus TCP包括用TCP长连接做设备远程维护其实底层都是这一套东西——先把TCP/IP链路搞稳定剩下的交给应用逻辑。踩过这些坑之后回头看STM32以太网没有想象中那么高不可攀但也不是把CubeMX勾一勾就能跑的省心事。硬件的PHY型号、时钟、地址软件的协议栈配置、回调机制、内存管理每一层都能设一个“关卡”卡住你。希望这篇笔记能帮准备入坑的朋友少走几个弯路。