ARTICLE DETAIL

资讯详情

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

STM32F407+DP83848+LwIP裸机移植实战:从Ping通到TCP Server

STM32F407+DP83848+LwIP裸机移植实战:从Ping通到TCP Server STM32F407 加 DP83848 这个组合说它是嵌入式以太网入门最经典的搭配也不为过。F407 内置了 MAC 控制器PHY 层芯片需要外接而手头这块野火 F407 开发板恰好把 DP83848 集成在板子上RJ45 座一插就能直接上手调 LwIP 协议栈。这篇文章把我从零开始跑通 Ping、完成 TCP Client 和 TCP Server 两个实战的过程连同查过的寄存器、写过的回调函数、踩过的坑全部整理出来。如果你正准备做 STM32 以太网、LwIP 裸机移植、DP83848 驱动调试或者只是想搞清楚 TCP 在嵌入式里到底怎么落地这篇笔记应该能省你不少时间。注意这不是一篇照着官方例程念一遍的文章而是我在实际调试中一点点试出来的记录。涉及原理的部分我会说人话涉及代码的部分我会贴出可以直接参考的结构涉及排查的部分我会把“当时是怎么发现问题”的过程写清楚而不是只丢一个结论。1. 项目背景与方案选型1.1 为什么会是 F407 DP83848STM32F407 这颗芯片内置了以太网 MAC 控制器支持 10M/100M 以太网但它不带 PHY也就是物理层收发器。MAC 负责把 IP 包封装成帧、做 CRC 校验、管理流控而真正把差分信号送上网线的工作必须交给外部的 PHY 芯片。选择 DP83848 的原因很直白它是 TI 公司非常老牌的一颗 10/100M 以太网 PHY工业级应用很广资料多、寄存器手册写得清楚价格也便宜而且野火 F407 板子上板载的就是这颗芯片省去了自己画 PHY 电路的麻烦。市面上 STM32F4 系列的以太网开发板有好几种方案比如有的板子用 LAN8720A有的用 DP83848少数用 W5500 这类自带 TCP/IP 协议栈的芯片。用过之后我的体会是LAN8720A 引脚少、功耗低适合做小体积产品但寄存器细节和 DP83848 相比偏“黑盒”W5500 是硬件协议栈上手快但是编程模型完全不是 LwIP 那套也不便于移植到其他平台。DP83848 走的是标准 MDIO 管理接口寄存器完全开放配合 LwIP 这种软件协议栈可以让你把以太网底层机制看得很透学习价值最高。如果从项目选型的角度说F407 DP83848 很适合工业网关、数据采集器、简易协议转换器这类设备。不需要跑 Linux 那种完整系统用裸机或者 RTOS 就能实现 TCP 通信功耗可控成本也低。我这次用的是裸机方案没有上 RTOS主要是想把 LwIP 的裸机移植模型吃透后面再上 RTOS 心里才有底。1.2 以太网硬件基础速览刚开始接触以太网的人往往被一堆术语劝退MAC、PHY、RMII、MII、MDIO、描述符、DMA……其实拆开看没那么吓人。你可以把 MAC 理解成“负责打包和拆包的文员”PHY 是“负责把包裹搬上货车的搬运工”。文员只认逻辑电平搬运工才懂怎么把电平变成网线上跑的差分信号。STM32F407 的 MAC 和外部 PHY 之间有两种常见接口MII 和 RMII。MII 是标准接口需要 16 根数据/控制线数据位宽 4 bit时钟 25MHzRMII 是精简接口只需要 7 根线数据位宽 2 bit时钟 50MHz。现代 MCU 设计基本都用 RMII省引脚是最现实的好处。F407 的 RMII 接口包括以下几根信号线TX_EN发送使能告诉 PHY 当前正在发送数据TXD[1:0]2 位发送数据RX_DV接收数据有效标志RXD[1:0]2 位接收数据REF_CLK50MHz 参考时钟由外部时钟源提供MDIO / MDC管理接口用于读写 PHY 寄存器这里有个容易绕晕的点RMII 的 50MHz 参考时钟到底由谁产生有的方案用 MCU 的 MCO 引脚输出 50MHz 给 PHY有的方案是板子上放一颗 50MHz 有源晶振给 PHY然后把同源时钟再接到 MCU 的 REF_CLK 引脚。野火 F407 板子使用的是外部 50MHz 有源晶振方案好处是 MCU 侧不用额外消耗 MCO 引脚但你必须确保 PHY 和 MAC 看到的是同相位的 50MHz 时钟否则数据采样会出错。这一点后面硬件调试部分还会再提。MDIO 和 MDC 是用来读写 PHY 内部寄存器的通道类似 I2C 的时序一根数据线一根时钟线。通过 MDIO 可以读取 PHY 的连接状态、速度、双工模式还可以配置 PHY 的工作模式。DP83848 的 MDIO 地址由 PHYAD[0] 引脚决定野火板上拉到固定电平默认地址是 0x01调试时要知道这个值否则寄存器根本读不对。2. 硬件设计与 PHY 调试要点2.1 RMII 接口与 DP83848 关键信号如果是自己画板子接 DP83848有几个关键信号必须认真核对PHY 的 X1、X2 引脚接 50MHz 时钟源或晶振TX_EN、TXD[0]、TXD[1] 分别接 MCU 的 ETH_TX_EN、ETH_TXD0、ETH_TXD1RX_DV、RXD[0]、RXD[1] 接 MCU 的 ETH_RX_DV、ETH_RXD0、ETH_RXD1MDIO 和 MDC 接 MCU 对应的管理接口引脚。开发板上这些连接已经固定了但如果你是参考原理图设计自己的板子引脚一一对应是优先级最高的事情错一根线都不通。DP83848 还有一些引脚需要外部上下拉来配置工作模式比如 PHYAD[0] 决定 MDIO 地址RX_DV 引脚在 RMII 模式下也有复用功能必须严格按 datasheet 接。这点坑过我一次最开始我把 PHYAD[0] 悬空结果 MDIO 读出来的寄存器全是 0xFFFF折腾了很久才发现是地址没拉对。后来养成习惯画板前先确认 PHY 周边引脚有没有被初始化/复用功能干扰比如部分 GPIO 的 JTAG 复用会导致 RX_DV 信号收不到。在开发板上DP83848 和 MCU 之间是已经布好线的通常不会出硬件连接错误但你还是可以通过 MDIO 读取 PHY 的 ID 寄存器来验证通信链路是否正常。DP83848 的寄存器 0x02 读出来应该是 0x2000寄存器 0x03 读出来应该是 0x50 开头的值。如果读到这两个值说明 MDIO 通路没问题PHY 芯片也在正常工作。这是整个调试过程中第一个值得做的“冒烟测试”。2.2 时钟、复位与电源处理DP83848 的时钟是整个以太网的心脏。这颗 PHY 内部需要 50MHz 参考时钟如果时钟不稳定后续 Ping 包会大量丢包甚至完全不通。野火 F407 板子用有源晶振方案所以只要晶振本身没问题时钟一般不会出岔子。但如果你自己设计板子这里有几个注意事项电源方面DP83848 的 I/O 电源和模拟电源都需要 3.3VAVDD 和 DVDD 必须严格分开走线、分别加去耦电容最好在 PHY 旁边放一个磁珠隔离模拟电源。我见过有人把 AVDD 和 DVDD 直接短接结果信号眼图很差千兆先不谈百兆也是时通时断。复位电路上DP83848 的复位引脚是低有效需要至少 1ms 的低电平脉冲而且复位释放后要等待内部 PLL 锁定建议在代码里让 MCU 的复位 GPIO 保持低电平至少 2ms再拉高随后延时 100ms 再开始初始化 PHY。这些时序要求不复杂但很多问题就是差几毫秒导致的特别是上电瞬间 PHY 还没就绪就急着读写寄存器读回来的数据自然不可靠。还有一个必须提醒的点RMII 模式下 MAC 和 PHY 的时钟必须是同一源的。如果你用 MCU 的 MCO 输出给 PHY那 MCO 的引脚配置要在初始化以太网之前完成而且 MCO 的输出频率要确认是 50MHz 而不是 25MHz这在 CubeMX 里容易选错。如果 PHY 和 MAC 的参考时钟不同步收数据的时候会偶尔错位表现出来就是 Ping 通但是丢包率很高非常难查。2.3 硬件连接出错时的排查顺序当你把程序烧进去发现以太网完全没反应先不要急着怀疑 LwIP多半是硬件层面就卡住了。我总结的排查顺序如下看 PHY 的 LINK LED 是否点亮。DP83848 的 LED_0 通常指示连接状态如果网线插上后灯不亮先查网线、对端设备再查 PHY 到 RJ45 的差分走线。开发板如果灯不亮大概率是网线或者对端设备问题。读 PHY ID 寄存器。通过 MDIO 读寄存器 0x02 和 0x03确认读取值是否符合 DP83848 预期。如果读出来全是 0xFFFF检查 MDIO 上下拉和 PHY 地址如果读出来是 0检查 MDC/MDIO 引脚复用配置。读 BSR 寄存器寄存器 0x01的 bit2这是“连接状态”位。能读到连接状态但是灯不亮说明 LED 配置寄存器没配对连状态位都是 0说明 PHY 没检测到对端。最后才去抓 RMII 信号。有条件用逻辑分析仪抓 TX_EN 和 TXD没有条件就先把前面的寄存器检查做完。执行完这套流程90% 的“硬件不通”问题都能定位。很多人在 PHY 驱动还没确认正常的情况下就急着调 LwIP等于地基没打好就开始盖楼后面索引错误会非常痛苦。3. LwIP 裸机移植与初始化3.1 CubeMX 工程配置与底层生成野火官方例程有基于标准库的版本也有基于 HAL 库的版本我这次选择用 STM32CubeMX 生成工程原因是 HAL 库对 ETH 和 LwIP 的支持已经很成熟而且生成代码结构化更好适合在上面改出自己的逻辑。在 CubeMX 里配置以太网需要先打开 ETH 外设。时钟配置方面确保 ETH 的时钟源是 PLL 输出的 50MHz也就是 RMII 的 REF_CLK 信号。如果板子上 PHY 自带 50MHz 晶振CubeMX 里的 ETH 时钟还是要配置因为 MAC 侧也需要这个时钟域。然后是 GPIO 配置RMII 接口相关的引脚要手动设置为 ETH 复用功能包括 PA1、PA2、PA7、PB11、PB12、PB13、PC1、PC4、PC5。这一步容易漏漏了一个引脚对应信号就收到不到了。中间件部分选择 LwIP协议栈版本默认即可。在 LwIP 配置页面里有几个关键参数值得说一下MEM_SIZE是堆内存大小PBUF_POOL_SIZE是 pbuf 池大小TCP_MSS是 TCP 最大报文段长度TCP_WND是 TCP 接收窗口大小。裸机方案下MEM_SIZE建议设置不低于 10240PBUF_POOL_SIZE建议 8 到 16。你可以把 pbuf 池理解成“收件箱”的数量池太小突发流量一来数据包没有地方放直接丢弃表现为网速慢和丢包。生成代码后HAL 库会生成eth.c、lwip.c里面已经有最基本的初始化和 LwIP 调用框架。你拿到手只需要修改 IP 地址、MAC 地址然后在主循环里调用MX_LWIP_Process()。这个函数内部会轮询以太网输入和 LwIP 超时处理是整个裸机方案的核心。3.2 LwIP 裸机移植的核心参数裸机移植和 RTOS 移植最大的区别在于“谁来驱动协议栈运行”。在 RTOS 环境下LwIP 会创建一个tcpip_thread独立线程专门处理协议栈消息在裸机环境下没有线程切换所有协议栈处理都必须在你自己的主循环里“挤时间”完成。所以 LwIP 的NO_SYS宏必须设置为 1这样 LwIP 会运行在没有操作系统保护的简化模式下。这个模式下有几个点必须处理好数据包接收每次主循环轮询到有数据帧到达就调用ethernetif_input把数据交给 LwIP。如果轮询间隔太长DMA 接收描述符可能被新数据覆盖所以主循环里不能有长时间阻塞的操作。超时处理TCP 协议有重传、保活、TIME_WAIT 等机制全部依赖定时器。裸机模式下需要周期调用sys_check_timeouts()让 LwIP 处理所有超时事件。内存管理LwIP 内部大量使用内存池和内存堆在多线程模式下需要加锁裸机模式不用锁但你必须确保协议栈不会被中断嵌套打断。以太网 DMA 中断里只做标志位置位和数据搬运不要直接在中断里调用 LwIP 的 API。我见过有人把sys_check_timeouts()放在定时器中断里调用这在裸机方案下其实很危险。因为这个函数可能会触发 TCP 重传等耗时操作如果放在中断上下文会严重影响系统实时性甚至导致堆栈溢出。正确的做法是在主循环的“空闲”时间段调用。lwip.c里生成初始化代码时ipaddr、netmask、gw这几个全局变量直接用IP_ADDR4或者十六进制赋值。比如板子的 IP 想设成 192.168.1.10网关 192.168.1.1可以用下面的方式IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1);MAC 地址也在初始化之前设置可以用 HAL 库接口HAL_ETH_SetMACAddr写入。MAC 地址分单播和多播本地管理的 MAC 地址要把第一个字节的 bit1 置 1比如02:00:11:22:33:44这样就不会和真实网卡的 MAC 冲突。3.3 初始化顺序与状态轮询LwIP 初始化必须遵循严格的顺序先初始化 ETH 硬件再初始化 LwIP 协议栈最后启动网络接口。如果在 PHY 还没完成自协商之前就启动 LwIP网络接口会认为链路是断的后续不会发送任何数据这个问题我遇到过不止一次。标准的初始化流程大概是MX_ETH_Init(); // HAL_ETH_Init HAL_ETH_Start MX_LWIP_Init(); // lwip_init netif_add netif_set_up如果你用包含 PHY 地址检测的 HAL 驱动MX_ETH_Init会尝试读取 PHY 寄存器。这里要求 DP83848 的地址必须和驱动里定义的phyAddress一致。HAL 库默认 PHY 地址是 0而野火板上 DP83848 的 PHY 地址是 1如果不改初始化会超时或者读不到状态。初始化完成后的主循环一般长这样while (1) { MX_LWIP_Process(); // 内部处理收包和超时 my_udp_tcp_task(); // 你的应用逻辑 }MX_LWIP_Process()是 CubeMX 生成的展开看就是调用ethernetif_input(g_netif)处理接收再调用sys_check_timeouts()处理超时。我自己额外加了一个链路状态检测函数周期性读 PHY 的 BSR 寄存器判断网线是否插拔插拔时调用netif_set_link_up或netif_set_link_down让 TCP 连接感知到底层链路变化。这个做法在长连接场景下特别有用否则网线断了又插上TCP 还傻等着连接死活不恢复。4. Ping 通才算入门4.1 网络参数配置与连接方式以太网调通之后第一件事就是 Ping。最简单的测试拓扑是把开发板用网线直连电脑的网口两个设备点对点通信不需要路由器。直连时要注意板子和电脑的 IP 必须处在同一网段比如开发板 192.168.1.10电脑有线网卡设成 192.168.1.100子网掩码都是 255.255.255.0不上网关。电脑端设置 IP 后Windows 可能会把这张网卡识别为“未识别的网络”这是正常的不要被它吓住。关键是配置好之后先用命令行确认网卡状态然后从电脑 Ping 开发板ping 192.168.1.10如果 pong 通了返回时间大概在 1ms 以内说明以太网底层和 LwIP 的 IP 层已经正常工作。这里有个小建议先不要急着接路由器点对点直连的环境最简单没有 DHCP、没有网关转发出了问题的排查范围最小。4.2 Ping 不通怎么定位Ping 不通是最常见的初学门槛但千万不要慌。先把定位手段用上Wireshark 抓包。电脑上开 Wireshark选有线网卡然后 Ping 开发板。这时候能看到完整的数据交互过程先是 ARP 请求询问“谁是 192.168.1.10”然后开发板回 ARP 应答再是 ICMP Echo Request 和 Echo Reply。如果抓包只看到 ARP 请求但没有应答问题大概率出在 LwIP 初始化或板子根本没收到包。如果 ARP 有应答但 ICMP 没回通常是路由表或 netif 配置问题。如果连 ARP 请求都看不到先查电脑网卡 IP 设置再查网线物理连接。Wireshark 抓包是我个人最推荐的定位手段因为 Ethernet 帧是共用一个物理链路的不存在什么“隐藏”过程你眼睛看到什么协议栈就收到了什么非常直观。我调试的时候经常遇到“感觉板子没反应”结果 Wireshark 一抓发现 ARP 请求根本没发到电脑网卡上问题出在开发板到路由器的交换芯片之间。Windows 防火墙有时候会拦截 ICMP 包但一般只拦外部来的 Echo Request对发出的 Ping 不影响。如果死活 Ping 不通又不想排查防火墙可以临时关闭防火墙试试或者用手机热点、路由器的其他端口把环境隔离开来测试。4.3 稳定性和丢包优化Ping 通只是第一步关键还要看稳定性和丢包率。用ping -t连续 Ping 长时间观察有没有丢包、有没有超时。如果出现周期性丢包常见原因有三个第一LwIP 的内存池太小。收包时PBUF_POOL_SIZE不够用数据包被丢弃。这种情况在程序里表现为 Ping 时通时不通频率和流量大小相关。调整方法是增大lwipopts.h里的PBUF_POOL_SIZE。第二主循环阻塞时间过长。裸机方案下如果主循环里有阻塞延时比如HAL_Delay50ms 以上以太网 DMA 接收中断虽然能把数据搬进内存但ethernetif_input只有主循环跑到才会处理接收描述符可能被新数据覆盖。解决办法是精简主循环逻辑把耗时操作拆成状态机保证每轮循环耗时不超过几毫秒。第三时钟不稳。如果 50MHz 参考时钟抖动过大高速数据采样会出现误码。这种丢包非常随机没有规律而且只在网速较高时出现。用示波器看 REF_CLK 或 PHY 的时钟输出能确认如果发现波形上升沿不干净检查晶振负载电容和电源纹波。我自己的经验是在调完 Ping 之后还会刻意做一轮大包 Ping 测试ping -l 1400 -t用接近 MTU 的包长压测这样可以快速暴露缓冲区配置不足和 DMA 配置错误。5. TCP Client 实战5.1 三次握手和连接建立Ping 通说明 IP 层没问题但 TCP 才是真正的高频需求。TCP 是面向连接的可靠传输协议通信前必须先建立连接。三次握手的过程大家肯定听过客户端发 SYN服务器回 SYNACK客户端再回 ACK。为什么是三次不是两次因为要确认双方的收发能力都正常而且能同步初始序列号。嵌入式里你不用背这些步骤但你要知道 LwIP 在tcp_connect调用后会自动完成三次握手应用层通过回调函数感知连接成功或失败。TCP 的可靠性依赖每个包都有序列号和确认号接收方收到数据后会回 ACK。LwIP 把这些都封装好了你只需要注册接收回调、发送数据、释放 pbuf其他事情协议栈替你完成。但有个概念必须清楚TCP 是“流”协议不是“报文”协议。调用一次tcp_write发出去的数据对端在tcp_recv回调里拿到的数据长度不一定等于发送长度。所以应用层要做数据分包和粘包处理这是做 TCP 应用最常见的困惑点。5.2 客户端代码实现TCP Client 就是主动发起连接的设备。在 LwIP 裸机模型下代码结构大概是struct tcp_pcb *client_pcb; static err_t client_connected(void *arg, struct tcp_pcb *tpcb, err_t err) { tcp_recv(tpcb, client_recv); // 连接成功后打卡 const char *msg hello from stm32\r\n; tcp_write(tpcb, msg, strlen(msg), TCP_WRITE_FLAG_COPY); tcp_output(tpcb); return ERR_OK; } void start_tcp_client(void) { ip_addr_t server_ip; IP4_ADDR(server_ip, 192, 168, 1, 100); client_pcb tcp_new(); if (client_pcb NULL) return; tcp_recv(client_pcb, client_recv); tcp_err(client_pcb, client_err); tcp_connect(client_pcb, server_ip, 8080, client_connected); }这段代码包含了所有核心动作tcp_new创建一个 TCP 控制块tcp_connect发起连接client_connected是连接建立后的回调tcp_recv注册收数据回调。发送数据必须放在回调函数里不能连接一调用马上发送因为三次握手还没完成。这是新手最容易出错的地方。TCP_WRITE_FLAG_COPY这个标志要解释一下。tcp_write默认不拷贝数据只记录数据指针这意味着你的缓冲区必须维持到协议栈真正发送完成。裸机方案下用TCP_WRITE_FLAG_COPY告诉 LwIP 先把数据拷贝到内部缓冲区你后面把原始缓冲区释放或覆盖都没关系安全性更高代价是多一次内存拷贝。对嵌入式来说这个开销可以接受。5.3 PC 端联调测试调试 TCP Client 最好的办法是在电脑上运行“网络调试工具”这类工具网上很多搜一下就有。设置电脑端为 TCP Server 模式监听端口开 8080然后让开发板主动连接电脑的 IP 和端口。正常情况下工具界面会弹出“有客户端连接”的提示然后显示开发板发过来的hello from stm32。如果连接不上优先确认三件事电脑 IP 是否和开发板在同一网段、电脑上的 Server 端口是否被占用、Windows 防火墙是否拦截了进站连接。第三点是最常见的因为开发板主动连电脑电脑防火墙会拦截这个进站 SYN解决方法是放行端口或者临时关闭防火墙验证。如果连接建立后收不到数据检查client_connected里是否调用了tcp_recv注册接收回调。不注册接收回调数据到达后 LwIP 因为没有接收处理函数直接丢弃而且连接会静默关闭。这类问题在代码层面上最容易忽略因为编译不报错、连接也成功就是不背数据。用 Wireshark 在电脑端抓包可以看到完整的三次握手SYN、SYNACK、ACK。如果只看到 SYN 没有后续回应说明开发板的回包没到电脑问题在开发板侧如果三次握手都完成了但服务端收不到数据那就检查应用层回调。6. TCP Server 实战6.1 服务器代码实现TCP Server 是反向角色开发板被动监听端口电脑作为客户端来连接它。LwIP 里 Server 的实现套路和 Client 类似但有几个关键差异。首先是创建 PCB 后要先tcp_bind绑定端口然后tcp_listen转为监听状态最后注册tcp_accept回调这个回调在客户端连接请求到来时触发。struct tcp_pcb *server_pcb; static err_t server_accept(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, server_recv); return ERR_OK; } void start_tcp_server(void) { server_pcb tcp_new(); tcp_bind(server_pcb, IP_ADDR_ANY, 8080); server_pcb tcp_listen(server_pcb); tcp_accept(server_pcb, server_accept); }这里有个细节tcp_listen返回的 PCB 类型已经变了从控制块变成了监听控制块原来的server_pcb变量会被覆盖所以监听状态下轮询和释放都要用返回值。server_accept回调里拿到的newpcb才是真正建立连接后的 PCB每个客户端连接都有自己的 PCB互不干扰。6.2 连接接收与数据处理Server 场景下数据接收的核心在server_recv回调。这个回调在每次收到数据时被调用参数里有 pbuf里面有从网络发来的原始数据。处理完毕必须调用tcp_recved告诉 LwIP 数据已经处理完窗口可以继续向前滑动然后释放 pbuf。static err_t server_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p NULL) { // 对端关闭连接 tcp_close(tpcb); return ERR_OK; } tcp_recved(tpcb, p-tot_len); // 回显把收到的数据再发回去 tcp_write(tpcb, p-payload, p-len, TCP_WRITE_FLAG_COPY); tcp_output(tpcb); pbuf_free(p); return ERR_OK; }你可能会奇怪为什么接收数据后要tcp_recved一下。这是因为 TCP 有流量控制接收窗口的大小决定了发送方能发多少数据。如果应用层一直不告诉协议栈“数据我消费掉了”窗口会被耗尽对端就发不动了造成死锁。网上很多 LwIP 的例子都漏了这步结果跑一段时间后连接像卡死一样其实是窗口满了。回显逻辑是我调试时最常用的验证手段。电脑端往开发板发什么开发板就回什么可以直观确认双向链路都通。如果你的应用是协议转换比如把接收到的 Modbus TCP 帧转成串口发送那就在server_recv里做解包、转发的逻辑转发完成后再回响应。6.3 长连接与短连接的取舍TCP Server 实战做完你一定会面临一个问题连接保持多久短连接是收发完数据就断开长连接是一直保持不中断。这个选择没有绝对的对错而是看资源和使用场景。短连接的好处是逻辑简单、PCB 和内存都能及时释放适合低频请求-响应式通信比如查询一次设备状态然后断开。缺点是每次建立连接都有三次握手的开销在低速率设备上消耗不可忽略。长连接适合数据持续上报、频繁交互的场景比如温湿度传感器周期性上传数据实时性要求高TCP 连接顶起来不要三天两头断。缺点是嵌入式资源有限内存池里的 PCB 数量有限每个连接都占资源所以并发连接数要控制。LwIP 裸机下默认能同时支持的 TCP 连接数受MEMP_NUM_TCP_PCB这个宏限制默认值往往只有几个。如果你的产品需要支持同时接入多个设备记得把这个宏调大同时注意内存占用会随之增加。长连接还有一个头疼的问题怎么判断对端还活着网线被拔掉、对端断电本端 TCP 不一定能立刻感知。LwIP 提供了 TCP 保活机制开启后每隔一段时间发探测包如果对端不应答就关闭连接。实际使用中我会在应用层做更快的超时判断维护一个“最近活跃时间”变量接收数据就更新主循环周期性检查如果超过 N 秒没收到数据主动tcp_close连接。这种“应用层看门狗”比协议栈保活更快、更可控。7. 常见问题速查与踩坑记7.1 问题速查表表格整理一下这次调试过程中遇到的最典型问题后面再做以太网项目可以直接翻现象大概率原因排查/解决办法网口灯不亮网线/对端设备问题或 PHY 供电异常换网线读 PHY ID 寄存器确认 PHY 正常MDIO 读到的寄存器全是 0xFFFFPHY 地址不对或 MDIO 引脚配置错误检查 PHYAD[0] 引脚电平核对 CubeMX 引脚复用Ping 不通但 ARP 有应答本机 IP、网关配置不一致用ipconfig检查电脑网卡确认同一网段Ping 通但时断时续pbuf 池太小或主循环阻塞调大PBUF_POOL_SIZE精简主循环耗时逻辑TCP 连接建立但收不到数据tcp_recv回调未注册在 accept/connected 回调中显式调用tcp_recv跑一会 TCP 连接卡死收到数据后没调用tcp_recved回调处理完数据立即调用tcp_recved拔网线再插TCP 不恢复链路状态变化没有告知 LwIP轮询 PHY 状态调用netif_set_link_up/down电脑连不上开发板 ServerWindows 防火墙拦截临时关闭防火墙或放行对应端口这张表里没有包含“玄学”问题每个现象背后都有明确的机制你照着排查大概率能定位。如果遇到表格外的问题优先开 Wireshark 抓包把每一层的交互过程看清楚再说。7.2 调试工具和几条经验工欲善其事必先利其器。这次调试我主要用了四样东西Wireshark 抓包看协议交互过程、命令行 Ping 测连通性、串口调试助手打印板子日志、网络调试工具模拟 PC 端 TCP Server/Client。前三样大家都很熟最后一样是 TCP 联调必备省去自己写 PC 上位机的时间。写代码的层面有一条经验很关键不要一上来就改 LwIP 协议栈内部的代码。LwIP 的源码经过了非常细致的优化和多年大量项目的验证你觉得自己“改一下会更好”的地方大概率是你没理解它的设计。真要改优先改配置文件lwipopts.h里的宏那些才是为使用者的灵活性留的口子。如果确实要动源码用#if XXX_DEBUG这类宏包住调试代码不要破坏原有逻辑。另一条经验是关于调试日志的。以太网协议栈的调试和裸机 GPIO 调试完全是两个世界你不能依靠肉眼观察判断数据走到哪一层了。我习惯在关键点加串口日志PHY 初始化返回值、LwIP 初始化状态、链路状态变化、TCP 连接建立、TCP 数据收发事件。日志加了详细的时间戳出问题时能回放整个流程。这个习惯在追查“偶发断连”这类问题时特别有用因为这种问题十次里有九次现场抓不住。最后说一个压箱底的经验如果条件允许一定要做上电自动重连和异常自动恢复机制。嵌入式设备的以太网应用不是调通一次就完了很多时候是设备在现场跑着跑着网络就断了没有人去拔电重启。合理的做法是PHY 链路状态没了就等链路恢复TCP 连接断了就开启重连定时器定时器到期后重新tcp_connect同时把应用层状态机复位到初始状态。把这些机制做好设备才能在无人值守的场景下稳定运行这也是嵌入式软件和“能跑的 Demo”之间最本质的区别。
返回列表