STM32F407+LwIP+DP83848以太网移植与ping排障实战
简介面向嵌入式网络开发初学者与中级工程师的STM32F407LWIPDP83848移植例程聚焦在无操作系统环境下搭建TCP/IP服务器并完成数据收发。工程不依赖外部存储器硬件只需一块STM32F407开发板配合DP83848以太网PHY即可复现降低了移植与验证门槛适合学习LWIP协议栈裁剪、网卡驱动适配及裸机网络通信流程。压缩包共3458个文件约22.92MB以C源文件与头文件为主体辅以汇编启动代码、Keil工程配置、链接映射及说明文档便于按工程结构查阅和二次开发。已有4769人学习下载说明该方案在同类移植实践中具有一定参考价值。读者可从中获得较完整的裸机TCP/IP服务器实现框架、DP83848驱动适配思路、工程编译配置与问题排查线索并借助随包说明文件快速理解移植要点对初始化流程、收发缓冲区管理和协议栈对接形成直观参照。1. 一块能下载、能串口打印、就是 ping 不通的板子手头这块 STM32F407 的板子PHY 焊的是 DP83848程序能下载、串口能打印、灯也亮可网线插上去之后对面交换机的灯一闪一闪ping 永远是“请求超时”串口里连个 ARP 包的影子都看不到。这份 STM32F407 LwIP DP83848 移植例程解决的正是这一段最容易卡住人的链路上从 MAC 的 DMA 描述符、RMII 的九根线、50MHz 参考时钟一直到 LwIP 的 netif 注册、pbuf 收发和 socket 接口。它适合两类人一类是刚拿到带网口的 F407 板子、想先把 ping 打通的新手另一类是手上有产品要加网口、需要一个干净起点的熟手。例程本身不大但网络这一块有个特点——任何一处配置错现象都是“ping 不通”所以真正值钱的是知道每一层该看什么。2. 硬件与工程骨架RMII 九根线、50MHz 时钟和目录职责拿到这类工程我的习惯是先不打开 main.c而是先确认硬件这一层对不对。因为 LwIP 再怎么配PHY 没有链路、参考时钟没进来代码写得再漂亮也白搭。这一步花十分钟后面能省掉几个小时。2.1 DP83848 的 RMII 接线与 PHY 地址STM32F407 内部只有以太网 MAC没有 PHYDP83848 就是挂在 MAC 外面的那颗收发器。它支持 MII 和 RMII 两种接法例程基本都用 RMII——线少只要 9 根信号线PCB 好走。MII 要 16 根左右还得单独给 TX_CLK、RX_CLK板子上很少这么干。信号STM32F407 引脚复用方向说明RMII_REF_CLKPA1AF11输入50MHzMAC 和 PHY 共用RMII_MDIOPA2AF11双向管理接口数据需要 1.5k~2.2k 上拉RMII_CRS_DVPA7AF11输入载波侦听/数据有效RMII_MDCPC1AF11输出管理接口时钟通常 1~2.5MHzRMII_RXD0PC4AF11输入接收数据位 0RMII_RXD1PC5AF11输入接收数据位 1RMII_TX_ENPB11AF11输出发送使能RMII_TXD0PB12AF11输出发送数据位 0RMII_TXD1PB13AF11输出发送数据位 1这张表建议直接抄进自己的笔记。我踩过一次坑把 PB11 配成了普通推挽输出结果 TX_EN 一直是高电平PHY 那边收到的是一串垃圾链路灯亮着但一个包都发不出去查了半天以为是 LwIP 配置问题。引脚初始化用 HAL 写出来是这样关键是Alternate必须是GPIO_AF11_ETH速度和上下拉按表来/* RMII 引脚复用全部 AF11注意 PB11/PC4/PC5 默认是别的复用功能 */ static void ETH_GPIO_Config(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate GPIO_AF11_ETH; /* ETH 专用复用号 */ /* PA1REF_CLK PA2MDIO PA7CRS_DV */ gpio.Pin GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7; HAL_GPIO_Init(GPIOA, gpio); /* PC1MDC PC4RXD0 PC5RXD1 */ gpio.Pin GPIO_PIN_1 | GPIO_PIN_4 | GPIO_PIN_5; HAL_GPIO_Init(GPIOC, gpio); /* PB11TX_EN PB12TXD0 PB13TXD1 */ gpio.Pin GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13; HAL_GPIO_Init(GPIOB, gpio); /* MDIO 需要外部上拉若板上没焊可以在这里打开内部上拉兜底 */ /* HAL_GPIO_WritePin / 单独配置 PA2 的 Pull 为 GPIO_PULLUP */ }参数上只有两处值得动手改Speed建议VERY_HIGHRMII 是 50MHz 同步信号速度等级低会导致时序余量不足Pull一般是NOPULL因为外部已经有上下拉但如果你是自己画的板子、MDIO 忘了加上拉读 PHY 寄存器会一直返回全 1这一条后面避坑章还会提。PHY 地址这一项例程里通常写0x01。DP83848 的 PHYAD[4:0] 由芯片上电时几个 strap 引脚的电平决定具体是哪几根脚、默认值多少得对着数据手册的 strap 表看板子的实际焊接。地址填错的表现很典型MDIO 能通读出来全是 0xFFFF因为总线上根本没有器件应答。2.2 时钟树与 50MHz REF_CLK 的两种来源RMII 必须有一颗 50MHz 的参考时钟同时送给 MAC 和 PHY这是整块板子最容易翻车的地方。常见做法有两种。第一种板上放一颗 50MHz 有源晶振输出一分为二一路进 DP83848 的 X1一路进 MCU 的 PA1。这种方式最省心CubeMX 里把 PA1 配成 AF11 就完事不用管时钟树。市面上大部分带网口的 F407 开发板都是这么设计的。第二种板上只有 PHY 的 25MHz 晶振或者干脆没有独立晶振得让 MCU 用 MCO1PA8输出 50MHz 去喂 PHYPHY 再把时钟回送给 PA1或者反过来。这条路的麻烦在于F407 主频跑到 168MHz 时MCO1 的分频只能取 1~5168/4 42MHz凑不出 50MHz。要做就得单独算一组 PLL把 VCO 定到 200MHz、PLLP 分出 100MHz 给 MCO1、再二分频得到 50MHz代价是系统主频掉到 100MHz。很多自制板在这里反复改代码最后发现是时钟根本没出对。我的建议是先拿示波器或者逻辑分析仪量 PA1看到干净的 50MHz 方波再往下走。量不到就先解决时钟别去动 LwIP。这一步没做后面所有调试都是在黑匣子里猜。2.3 工程目录与文件职责例程的目录结构大同小异剥掉 CMSIS 和 HAL 驱动之后真正和网络相关的就那几个文件Project/ ├── Drivers/ │ ├── CMSIS/ # 内核头文件、启动文件 │ └── STM32F4xx_HAL_Driver/ # HAL含 stm32f4xx_hal_eth.c ├── Middlewares/ │ └── Third_Party/LwIP/ │ ├── src/ # 协议栈本体core / ipv4 / api │ └── system/ # 与 OS 或裸机对接的 sys_arch ├── BSP/ │ ├── stm32f4xx_eth_bsp.c # GPIO、时钟、PHY 复位 │ └── stm32f4xx_eth_bsp.h ├── LwIP/ │ ├── lwipopts.h # 全部裁剪开关都在这 │ ├── ethernetif.c # 网卡驱动low_level_init/output/input │ └── ethernetif.h └── Src/ ├── main.c # netif 注册 主循环 └── app/ # tcp_echo / udp_echo 之类的应用ethernetif.c是整个移植的核心LwIP 不管你底层是 DM9000 还是片内 MAC它只认netif结构里挂的那几个函数指针而ethernetif_init负责把这些指针填上。lwipopts.h决定协议栈编多大、开哪些功能。main.c负责把网卡挂到 netif 链表上、设 IP、把网卡使能起来。剩下的应用层写成什么样都行。有一件事要提前确认工程里用的是 HAL 库还是老的标准外设库。这两套的 ETH 驱动 API 完全不一样HAL 是HAL_ETH_Init/HAL_ETH_TransmitFrame/HAL_ETH_GetReceivedFrame老库是ETH_Init/ETH_TransmitFrame/ETH_GetReceivedFrame网上抄代码时如果混着抄编译能过但行为不对。看到不认识的函数名先去Drivers目录里搜一下它属于哪一套。3. LwIP 裁剪与 ethernetif 驱动一个 ping 包从网线到协议栈的路径协议栈这一层我一般按“进来”和“出去”两条路径读。进来是ETH DMA 收到帧 → 中断 → ethernetif_input → low_level_input → pbuf → netif-input → ip4_input → icmp_input出去反过来走。把这两条链在脑子里跑通一遍出问题的时候就知道该在哪一段下断点。3.1 lwipopts.h 里真正影响能不能通的几个开关lwipopts.h动辄一两百行但绝大多数是性能和功能开关真正会让“ping 不通”的只有那么几个。下面这段是我一般会先确认的最小集合/* ---------- 运行模式 ---------- */ #define NO_SYS 1 /* 裸机跑主循环里轮询上 FreeRTOS 就改 0 */ #define SYS_LIGHTWEIGHT_PROT 0 #define LWIP_NETCONN 0 /* 裸机下不开 netconn只用 raw API */ #define LWIP_SOCKET 0 #define MEM_ALIGNMENT 4 /* F407 是 32 位对齐到 4 */ /* ---------- 内存 ---------- */ #define MEM_SIZE (16 * 1024) #define MEMP_NUM_PBUF 16 #define PBUF_POOL_SIZE 16 /* 收包池太小会丢包 */ #define PBUF_POOL_BUFSIZE 1524 /* 必须 一个最大以太帧 */ /* ---------- 协议开关 ---------- */ #define LWIP_ARP 1 #define LWIP_ICMP 1 /* 关了就 ping 不通别手贱 */ #define LWIP_DHCP 0 /* 先用静态 IP 打通跑通再换 DHCP */ #define LWIP_UDP 1 #define LWIP_TCP 1 /* ---------- TCP 参数 ---------- */ #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS) /* ---------- 与 STM32 ETH 相关的两个关键项 ---------- */ #define CHECKSUM_BY_HARDWARE 1 /* 用 MAC 的校验和卸载 */ #define ETH_PAD_SIZE 0 /* 与底层拷贝起始偏移必须一致 */PBUF_POOL_BUFSIZE是最容易设错的一项。有人为了省 RAM 写成 512结果 MTU 1500 的包进来时一个 pbuf 装不下LwIP 会拆成链而low_level_input里如果按单个 pbuf 处理就会出现“小包能 ping大包丢”的怪现象。老老实实写 15241500 14 以太头 4 对齐余量最省事。CHECKSUM_BY_HARDWARE这一项和ethernetif.c是绑定的开了它MAC 会在发送时自动填 IP/TCP/UDP 的校验和接收时也会校验并打标记但如果你在low_level_output里没把描述符的校验和插入位打开或者收发缓冲区没按 DMA 要求对齐包发出去对方会直接丢弃。校验和错的表现是“抓包能看到包但对方不回应”。调试阶段我一般先把它关掉让软件算校验和确认整条链路通了再开硬件卸载提性能——这个顺序能少走很多弯路。3.2 low_level_init 与 DMA 描述符初始化ethernetif_init里做的事很少填netif字段、调low_level_init。真正干活的是low_level_initstatic void low_level_init(struct netif *netif) { /* 1. 网卡硬件属性LwIP 靠这几个字段决定怎么组包 */ netif-hwaddr_len ETHARP_HWADDR_LEN; /* 6 字节 MAC */ netif-mtu 1500; netif-flags NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; /* 2. MAC 地址从芯片 UID 拼一个避免所有板子出厂一个地址 */ uint32_t uid0 HAL_GetUIDw0(); uint32_t uid1 HAL_GetUIDw1(); netif-hwaddr[0] 0x02; /* 本地管理地址第一字节 bit1 置 1 */ netif-hwaddr[1] (uid0 16) 0xFF; netif-hwaddr[2] (uid0 8) 0xFF; netif-hwaddr[3] (uid0 ) 0xFF; netif-hwaddr[4] (uid1 24) 0xFF; netif-hwaddr[5] (uid1 16) 0xFF; /* 3. PHY 复位拉低至少 100us给它足够时间起来 */ HAL_GPIO_WritePin(ETH_RST_GPIO_Port, ETH_RST_Pin, GPIO_PIN_RESET); HAL_Delay(2); HAL_GPIO_WritePin(ETH_RST_GPIO_Port, ETH_RST_Pin, GPIO_PIN_SET); HAL_Delay(50); /* 4. MAC DMA 初始化描述符和缓冲区在下面这段之后才有效 */ heth.Instance ETH; heth.Init.AutoNegotiation ETH_AUTONEGOTIATION_ENABLE; heth.Init.Speed ETH_SPEED_100M; heth.Init.DuplexMode ETH_MODE_FULLDUPLEX; heth.Init.PhyAddress DP83848_PHY_ADDRESS; /* 与硬件 strap 一致 */ heth.Init.MACAddr netif-hwaddr; heth.Init.RxMode ETH_RXMODE_DMA; /* 收包走 DMA别用轮询 MAC 寄存器 */ heth.Init.ChecksumMode ETH_CHECKSUM_BY_HARDWARE; /* 和 lwipopts 里保持一致 */ if (HAL_ETH_Init(heth) ! HAL_OK) { Error_Handler(); /* 这里失败基本就是 PHY 没应答 */ } /* 5. 网卡使能MAC 开始收包 */ HAL_ETH_Start(heth); }几个参数值得单独说。PhyAddress必须和 2.1 节里硬件 strap 定的地址一致AutoNegotiation开自动协商但Speed和DuplexMode也要填因为当对端不支持协商或者协商失败时HAL 会退回这两个默认值——如果它们和实际链路不匹配就会出现“灯亮着但丢包严重”的现象经验上模式错配的丢包率会到百分之几十很难查。RxMode填ETH_RXMODE_DMA而不是轮询是因为 F407 的 MAC 收包速率很高CPU 轮询会丢包。描述符和缓冲区是分开定义的全局数组HAL 在HAL_ETH_Init里会把地址写进寄存器/* DMA 描述符与收发缓冲区必须放在 SRAM不能放 CCM */ ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB] __attribute__((aligned(4))); ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB] __attribute__((aligned(4))); uint8_t Rx_Buff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __attribute__((aligned(4))); uint8_t Tx_Buff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __attribute__((aligned(4)));提示F407 有 64KB 的 CCM RAM地址在 0x10000000。这块内存 ETH 的 DMA 访问不到描述符和收发缓冲区绝对不能放进去。默认链接脚本不会往那放但如果你自己改过*.ld或者用了__attribute__((section(.ccmram)))一定要绕开。3.3 收包 low_level_input 与 pbuf 的零拷贝边界low_level_input做的事情就三件从 DMA 描述符里拿到刚收到的帧、申请一个 pbuf、把数据拷进去。例程基本都是“拷贝式”不是零拷贝因为 F407 没有 D-Cache不需要做 cache 维护拷贝一次的开销在百兆网下完全可以接受实测拷贝一个 1500 字节包大概几微秒。static struct pbuf *low_level_input(struct netif *netif) { struct pbuf *p NULL; ETH_BufferTypeDef rxbuf; FrameTypeDef frame; /* 1. 从 DMA 取一帧没有就返回 NULL调用者继续下一次轮询 */ if (HAL_ETH_GetReceivedFrame_IT(heth) ! HAL_OK) { return NULL; } uint16_t len heth.RxFrameInfos.length; uint8_t *buf (uint8_t *)heth.RxFrameInfos.buffer; if (len 0 || len ETH_RX_BUF_SIZE) { return NULL; } /* 2. 从内存池申请 pbufPBUF_POOL 走的是固定大小池比 PBUF_RAM 快 */ p pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p NULL) { return NULL; /* 池子空了这一帧只能丢调大 PBUF_POOL_SIZE */ } /* 3. 单 pbuf 直接拷链式 pbuf 要顺着 next 一段段拷 */ if (p-next NULL) { MEMCPY(p-payload, buf, len); } else { struct pbuf *q p; uint16_t copied 0; while (q ! NULL) { MEMCPY(q-payload, buf copied, q-len); copied q-len; q q-next; } } /* 4. 告诉 DMA 这块描述符可以重新用了否则收满一圈就停 */ HAL_ETH_ReleaseRxFrame(heth); return p; }HAL_ETH_GetReceivedFrame_IT和HAL_ETH_ReleaseRxFrame这对函数是配对使用的后者不调用DMA 的描述符计数就不会往前推收满ETH_RXBUFNB帧之后网卡就哑了。这个 bug 的表现特别有迷惑性刚上电 ping 通跑几十秒或者发个大文件之后突然不通复位又好了。我在避坑章里还会再说一次因为它太好踩。配套的ethernetif_input是个死循环一次把 DMA 里积压的帧全部抽干err_t ethernetif_input(struct netif *netif) { struct pbuf *p; err_t err ERR_OK; do { p low_level_input(netif); if (p ! NULL) { /* 交给上层返回值决定谁负责释放 */ if (netif-input(p, netif) ! ERR_OK) { pbuf_free(p); /* 上层没收自己释放 */ err ERR_IF; } /* 注意上层返回 ERR_OK 时所有权已经转移这里不能再 free */ } } while (p ! NULL); return err; }所有权这一行注释是 LwIP 新手最容易写错的地方。netif-input返回ERR_OK就代表 pbuf 被上层接管了比如 IP 层收下继续处理或者已经被释放再 free 一次就是双重释放内存池的链表会被写坏跑着跑着就硬故障。反过来返回错误时你不 free就是内存泄漏同样跑一段时间就卡死。两种症状都很像所以这段代码我一般会加个计数器统计 free 次数和申请次数跑一小时看看是不是相等。裸机模式下主循环长这样ethernetif_input和sys_check_timeouts都不能少while (1) { ethernetif_input(gnetif); /* 收包 */ sys_check_timeouts(); /* 驱动 LwIP 的定时器ARP 老化、TCP 重传都要它 */ app_poll(); /* 应用自己的轮询逻辑 */ }sys_check_timeouts忘了调用现象是 TCP 连接建立得起来但过一会儿就僵住因为重传定时器根本没跑。sys_now()的实现也要记得挂到HAL_GetTick()上不然 LwIP 的时间基准是错的。4. 从 ping 到 socketTCP 回环与 UDP 收发的实测闭环底层通了之后往上就是三步注册 netif、ping 通、跑通 TCP/UDP。这三步每一步都有明确的验收标准别跳步。4.1 静态 IP 与 ARP/ICMP 打通的最小闭环先用静态 IP别急着上 DHCP。DHCP 牵扯到广播、超时重试、服务器地址多一层就多一类故障。main.c里的注册序列大概是这样struct netif gnetif; ip4_addr_t ipaddr, netmask, gw; /* 1. 协议栈初始化内部会建内存池、开定时器 */ lwip_init(); /* 2. 静态地址和 PC 配到同一网段网关先随便填或不填 */ IP4_ADDR(ipaddr, 192, 168, 1, 50); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); /* 3. 挂网卡ethernetif_init 是驱动入口ethernet_input 是以太网帧解析入口 */ if (netif_add(gnetif, ipaddr, netmask, gw, NULL, ethernetif_init, ethernet_input) NULL) { Error_Handler(); } /* 4. 设默认网卡并使能这两步漏一个都 ping 不通 */ netif_set_default(gnetif); netif_set_up(gnetif);验收标准很明确PC 和板子网线直连或者接同一个交换机PC 配 192.168.1.100/24ping 192.168.1.50能通并且延迟在 1ms 以内。如果 ping 不通但 ARP 表里能看到板子的 MAC说明以太网收发通了、IP 层有问题如果 PC 的 ARP 表里连条目都没有说明板子根本没回 ARP问题还在底层。用arp -a和 Wireshark 抓包这两招比在代码里瞎加打印快得多。4.2 一个 TCP echo server 的骨架裸机下用 raw API不用 netconn 也行但 netconn 写起来直观得多我这里给 netconn 的版本需要把LWIP_NETCONN打开裸机想省 RAM 就用tcp_new/tcp_accept那套回调static void tcp_echo_thread(void *arg) { struct netconn *conn, *newconn; err_t err; struct netbuf *buf; void *data; u16_t len; /* 1. 建 PCB 并绑定 5000 端口 */ conn netconn_new(NETCONN_TCP); netconn_bind(conn, IP_ADDR_ANY, 5000); netconn_listen(conn); for (;;) { /* 2. 阻塞等连接裸机下要把这个循环放到主循环里非阻塞地调 */ err netconn_accept(conn, newconn); if (err ! ERR_OK) { continue; } /* 3. 收多少回多少注意 netbuf 可能是链式的 */ while (netconn_recv(newconn, buf) ERR_OK) { do { netbuf_data(buf, data, len); netconn_write(newconn, data, len, NETCONN_COPY); } while (netbuf_next(buf) 0); netbuf_delete(buf); } netconn_close(newconn); netconn_delete(newconn); } }NETCONN_COPY这个标志值得说一句它表示netconn_write内部把数据拷一份再返回调用返回后你的 buffer 可以复用如果传 0NETCONN_NOCOPY数据不会被拷贝但你必须保证在数据真正发出去之前 buffer 一直有效——在裸机轮询模型下这几乎做不到所以老老实实用 COPY。测试用nc 192.168.1.50 5000或者一个 Python 脚本发几十 KB 数据能原样回来就算通了。4.3 吞吐与延迟调优的几个旋钮ping 通、echo 通之后下一步通常是想知道能跑多快。百兆全双工理论 12.5MB/s实际用 F407 跑到 8~10MB/s 是正常水平。影响最大的几个参数PBUF_POOL_SIZE和ETH_RXBUFNB前者是 LwIP 的收包池后者是 DMA 描述符个数。两个都调大一倍丢包率会明显下降代价是 RAM。F407 有 192KB SRAM一般给 LwIP 留 40~60KB 是合理的。TCP_SND_BUF和TCP_WND窗口越大单次往返能发越多数据。局域网内 RTT 很小窗口开到 4~8 个 MSS 就够了再大会吃 RAM 且收益递减。CHECKSUM_BY_HARDWARE打开后 CPU 不用算校验和大包吞吐能提 10%~20%。LWIP_NETIF_TX_SINGLE_PBUF设成 1 时LwIP 会保证交给驱动的 pbuf 是连续的省掉一次链式拷贝。调优的正确姿势是先测基线再改一个参数改完再测。一次改五个参数出了问题根本不知道是哪个引起的这是我早年最常见的翻车方式。测的时候在 PC 端用大文件传输同时看板子串口打出来的丢包计数比看 CPU 占用有用。5. 避坑与排查五个让网口起不来的高频故障这一章是我自己踩过、也帮别人看过的故障里出现频率最高的五个。每条都按“看到什么现象 → 为什么 → 怎么办”写遇到问题可以直接对号入座。5.1 PHY 链路灯不亮MDIO 也读不到现象插上网线板子侧和交换机侧的灯都不亮或者只亮电源灯串口里打印 PHY ID 是 0xFFFF 或 0x0000HAL_ETH_Init直接返回错误。原因常见有三种。一是 50MHz 参考时钟没到 PA1PHY 内部没有工作时钟二是 DP83848 的复位脚一直拉低或者复位时序不够芯片停在复位态三是 MDIO 没有上拉电阻总线处于浮空读回来全是 1。解决先用示波器量 PA1 和 PHY 的 X1确认 50MHz 存在且幅度够再量复位脚上电后应该是高电平低电平时间至少 100us最后量 MDIO 空闲电平应该是高。三样都对还是 0xFFFF就检查 PHY 地址——把PhyAddress从 0x01 改成 0x00 试一遍很多时候 strap 电阻焊的位置和原理图不一致。5.2 灯亮着、链路也协商上了但 ping 不通现象PHY 灯正常闪烁HAL_ETH_Init返回成功但 PC 上ping一直超时arp -a里也看不到板子的条目。原因这一层的坑最多。按概率排IP 和掩码配错网段最常见、MAC 地址是全 0 或者和别的设备冲突、netif_set_up忘了调、PC 端防火墙拦了 ICMP、LWIP_ICMP被裁掉了。解决先在板子上加打印把netif_is_up(gnetif)、netif-ip_addr、netif-hwaddr都打出来确认再在 PC 上用 Wireshark 抓包看板子有没有发出 ARP 请求。如果板子一个包都不发问题在发送路径low_level_output如果发了 ARP 但 PC 不回检查 PC 的 IP 是否同网段、防火墙是否放行。MAC 地址这块建议按 3.2 节用芯片 UID 拼别所有板子烧同一个。提示有些例程里netif_set_up放在了ethernetif_init内部有些放在main里改代码时容易两个地方都删掉或者都留着。全局搜一下这个函数名确认它至少被调用一次。5.3 刚上电正常跑几十秒到几分钟后彻底不通现象上电后 ping 通、TCP 也能连跑一段时间几十秒到几分钟不等突然所有通信中断复位后恢复如此循环。原因九成是 pbuf 泄漏或者 DMA 描述符没释放。ethernetif_input里netif-input返回ERR_OK之后又pbuf_free了一次内存池链表被写坏或者low_level_input里忘了调HAL_ETH_ReleaseRxFrameDMA 收满一圈就停住。第三种可能是sys_check_timeouts没在主循环里调LwIP 的定时器不跑。解决在pbuf_alloc和pbuf_free两边各加一个计数器跑十分钟看两者差值是否稳定在池子大小以内。差值是单调增长的就是泄漏差值为 0 但通信还是断就在low_level_input开头打印一次看是不是根本收不到包了——收不到就是描述符卡住检查ReleaseRxFrame是否成对调用。5.4 小包正常一发大包就 HardFault 或者重启现象ping -l 32没问题ping -l 1472直接丢包或者用 TCP 传个几十 KB 的文件板子直接进 HardFault。原因三个方向。PBUF_POOL_BUFSIZE小于 1524大包被拆成链式 pbuf而low_level_input只拷了第一个ETH_RX_BUF_SIZE定义得比 1524 小DMA 写溢出栈溢出——大包处理路径上局部变量多默认栈 1KB 不够。解决先把PBUF_POOL_BUFSIZE改成 1524、ETH_RX_BUF_SIZE改成 1524 以上并且在low_level_input里用链式循环拷贝3.3 节的代码已经处理了。如果还崩把主栈从 0x400 加到 0x1000 再试。另外确认描述符和缓冲区没被链接脚本扔进 CCMDMA 访问不到那块内存表现也是大包一写就异常。5.5 硬件校验和打开后抓包能看到请求但对方不回应现象Wireshark 抓包能看到板子发出的 TCP SYN 或者 ICMP Echo但 PC 不回应把CHECKSUM_BY_HARDWARE关掉就正常了。原因MAC 的校验和卸载需要在发送描述符里置位并且要告诉 MAC 这个包的校验和起始偏移。如果low_level_output里只把数据拷进缓冲区、没设置描述符的TDES0校验和插入位包头的校验和字段就是垃圾值对端直接丢弃。解决要么按 HAL 的规范在HAL_ETH_TransmitFrame之前把heth.TxFrameInfos的校验和参数填对要么调试阶段干脆关掉硬件卸载让 LwIP 软件算。我的习惯是永远先用软件校验和跑通全链路把它当成已知正确的基线再开硬件卸载做 A/B 对比出问题能立刻定位到是不是卸载配置的锅。6. 二次开发改造把例程的网络层拆成可复用的三件套例程拿来跑通是一回事塞进自己的产品里是另一回事。我做过几次这种迁移最后都会把网络部分拆成三层初始化层、链路管理层、应用层中间用两个明确的接口隔开。第一步是把初始化从main.c里抽出来做成net_init(void)和net_poll(void)一对函数。main里只留这两行的调用所有 netif 相关的全局变量收到一个结构体里。这样换板子、换 PHY 的时候只需要改net_init内部的实现应用代码一行不用动。抽的时候注意lwip_init()必须在netif_add之前调用这个顺序错了会直接硬故障而且报错位置和真正的原因隔得很远。第二步是加链路状态检测。DP83848 的 BMSR 寄存器 bit2 是 Link Status掉线重插的时候这个位会变。做法是在net_poll里每 500ms 读一次检测到从有到无就netif_set_link_down从无到有就netif_set_link_up。如果是插拔网线频繁的场景还要在 link down 之后把 PHY 重新软复位一次BMCR 的 bit15因为有些批次的 PHY 在异常拔插后会卡在错误状态不复位就再也协商不上。验证方法很简单反复插拔二十次看每次恢复通信要多久正常情况下应该在一秒内。第三步是应用层的超时与重连。TCP 用 netconn 的时候netconn_recv默认是无限阻塞的产品里绝对不能这么写要配合netconn_set_recvtimeout设一个几秒的超时超时后关连接重连。UDP 那边则是要注意udp_recv的回调里不能做耗时操作回一个包就赶紧返回。改造点改哪里验证方式初始化抽离main.c→net_init/net_poll换一块板子只改一个文件链路检测net_poll里读 BMSR反复插拔 20 次恢复时间 1s收发超时netconn_set_recvtimeout拔掉网线后不卡死能自动重连MAC 唯一性low_level_init里用 UID 拼多块板子同网段不冲突还有一个经常被忽略的点这套例程的ethernetif.c里low_level_output通常只处理单帧发送如果应用层是高并发写需要检查发送描述符用完时的处理——HAL 在描述符满时会返回HAL_BUSY此时要等一个发送完成中断再重试直接丢弃会导致 TCP 大量重传。我一般会在发送路径上加一个简单的等待计数连续HAL_BUSY超过十次就记一笔日志跑压力测试的时候看这个计数是不是在涨。从那以后我每次拿到一份新的网络例程不管作者写得多好都强制走一遍“先量 PA1 的 50MHz、再抓 ARP 包、最后看 pbuf 计数差”这三步确认基线干净了才往里加业务代码。这三步花不了半小时但能挡掉后面八成莫名其妙的故障。希望帮到你。本文还有配套的精品资源点击获取