☰
STM32H750+LAN8720A+LWIP以太网调试与吞吐优化
2026/11/2 13:56:16 网站建设 项目流程

1. 先把方案边界说清楚:H750 + LAN8720A + LWIP 到底能做成什么

板子焊完、烧进去、串口打出一行 IP,然后电脑上ping通了——这大概是嵌入式网络里最舒坦的三十秒。STM32H750 + CubeIDE + FreeRTOS + ETH(LAN8720A) + LWIP这套组合,是当下做工业网关、数据采集终端、协议转换盒、小型边缘节点时相当常见的一条路:480MHz 的 Cortex-M7 留足了业务算力,LAN8720A 便宜好焊、原生 RMII,LWIP 开源免费又是 FreeRTOS 的默认搭档,CubeIDE 把图形化配置、链接脚本、下载调试串成一条线,理论上点几下鼠标就能生成一个能联网的骨架。

但骨架归骨架。我见过太多人卡在“生成代码后 ping 不通”这一步,一卡就是两三天。问题几乎从来不在某一个大件上,而在它们之间的接缝:PHY 的 50MHz 参考时钟到底由谁出、PHY 地址跟 strap 引脚对不对得上、ETH 中断优先级有没有越界、DMA 缓冲落在 DTCM 里导致 DMA 根本访问不到、开了 D-Cache 之后描述符被缓存一致性搞坏。这些东西单看每个都不难,凑在一起就变成一张互相牵扯的网。

这篇内容面向三类人:刚拿到 H750 板子想跑通网络的新手,能从硬件接线一路跟到 ping 通;已经在跑但吞吐上不去、偶发死机的,可以直接跳到内存配置和缓存那几节;还有一类是把这套方案往产品上搬的,重点关注第 3 章的生成后修补和第 6 章的坑位清单。所有参数我都会给出取值理由和计算过程,你照着改就行,但更希望你明白为什么这么改。

2. 硬件层:RMII 接口与 LAN8720A 的接线细节

2.1 引脚映射表与 50MHz 参考时钟的两种走法

先把引脚钉死。H750 的 ETH 外设走 RMII 时,引脚是固定的 AF11 复用,没有第二个选择,所以画原理图阶段就必须照这张表来:

信号H750 引脚方向说明
ETH_REF_CLKPA1输入RMII 的 50MHz 参考时钟,必须由外部提供
ETH_MDIOPA2双向管理接口数据线,需要 1.5k~10k 上拉到 3.3V
ETH_MDCPC1输出管理接口时钟,最高 25MHz,实际跑 1~2.5MHz
ETH_CRS_DVPA7输入载波侦听/数据有效
ETH_RXD0PC4输入接收数据 bit0
ETH_RXD1PC5输入接收数据 bit1
ETH_TX_ENPB11输出发送使能
ETH_TXD0PB12输出发送数据 bit0
ETH_TXD1PB13输出发送数据 bit1

注意这张表只有 9 根线,这正是选 RMII 而不是 MII 的直接理由:MII 要 16 根以上,还要 25MHz 的独立收发时钟,PCB 上多出来的走线面积和等长要求都是成本。RMII 把数据位宽从 4 位砍到 2 位,时钟提到 50MHz,用频率换线数,在百兆这个速率上完全够用。

真正的分歧点在PA1 上的那个 50MHz 从哪来。两种做法,各有取舍:

方案 A,PHY 出时钟给 MCU。LAN8720A 的 nINT/REFCLKO 引脚配置成 REF_CLK Output 模式,内部把 25MHz 晶振倍频到 50MHz 输出,直接连到 PA1,同时这个网络也接回 PHY 自己的 REF_CLK 输入脚。这是绝大多数参考设计的做法,好处是 MCU 侧不需要额外配置 MCO,不占用 PLL,时钟源单一不容易出相位问题。代价是 PHY 必须焊 25MHz 晶振。

方案 B,MCU 出时钟给 PHY。用 MCO1(PA8)输出 50MHz 送到 PHY 的 XTAL1/CLKIN,PHY 侧不使能 REFCLK 输出。省一颗晶振,但要求 MCO 的 50MHz 精度够——HSE 25MHz 时 PLL 配出 50MHz 需要额外占用一路 PLL 分频,而且 MCO 输出的抖动比晶振倍频差,一旦偏离 50ppm 就容易出现链路能起来但丢包率偏高的情况。我一般不推荐新手走这条,除非 BOM 上真的差这一颗料。

提示:不管走哪个方案,PA1 上一定要能用示波器量到干净的 50MHz、幅度接近 3.3V 的方波。量不到就别往下调软件了,纯浪费时间。

2.2 PHY 地址、复位时序与 strap 引脚

LAN8720A 的 PHY 地址不是软件随便写的,是由RXER/PHYAD0这个复用引脚在复位上升沿那一刻的电平锁存决定的:下拉就是地址 0,上拉就是地址 1。很多板子为了省事把这个脚直接下拉到地,那么代码里heth.Init.PHYAddress就必须填 0,填 1 的话 MDIO 读出来永远是 0xFFFF。

复位这块我建议不要图省事用 RC 复位。用一个 GPIO 接 nRST,上电后拉低 20ms 再释放,然后至少等 100ms再去访问 MDIO。原因很实在:LAN8720A 内部有 PLL 和 1.2V 稳压器,时钟起来到内部逻辑可用有个过程,你复位一放开立刻去读寄存器,读回来的 ID 经常是错的或者直接超时,白白怀疑接线。

nINT 引脚如果不用中断方式读 PHY 状态(大部分设计都是轮询),可以直接悬空不接,但要注意某些封装里它和 REFCLKO 是同一个脚,那就更不能乱接了。设计时务必对着数据手册的引脚复用表确认一遍,这个地方看错一次,后面调试能怀疑人生。

2.3 电源、晶振与 PCB 走线的实操要求

VDDCR 那个脚千万别省电容。LAN8720A 内部 1.2V 稳压器的输出脚需要一颗 1μF 的低 ESR 陶瓷电容到地,我遇到过一块板子为了省地方只放了 100nF,结果现象是上电十分钟内正常,之后开始间歇性丢包、ping时通时断,查了两天才发现是这个电容的问题。这类模拟电源脚的储能不足,表现出来往往不是“完全不通”,而是“时好时坏”,最难查。

25MHz 晶振的负载电容按晶振规格书来,一般 18~22pF,频偏控制在 50ppm 以内。晶振走线尽量短,下方铺地,不要从芯片底下穿过。

RMII 的 TX±/RX± 是 100Ω 差分对,等长控制在 5mil 以内,参考地平面要完整,绝对不能跨分割。TXD0/TXD1/TX_EN 这三根是 50MHz 的单端信号,尽量短、尽量少打过孔,长度差控制在 100mil 内。MDIO/MDC 是低速信号,可以放宽,但 MDIO 的上拉电阻别忘。

电源上我习惯用一颗磁珠把 PHY 的 3.3V 和 MCU 的数字 3.3V 隔开,各自配 10μF + 100nF。PHY 在 100M 全双工下的瞬态电流不小,共用一个不干净的网络,最后背锅的往往是 MAC 侧的 DMA 报错。

2.4 上电自检:三行代码判断 MDIO 通路通不通

在写任何协议栈代码之前,先写一个最朴素的探针。这几行代码跑通,说明时钟、复位、MDIO 全对,后面 90% 的硬件问题就已经排除了:

#define LAN8720_ADDR 0U static void PHY_Probe(ETH_HandleTypeDef *heth) { uint32_t id1 = 0, id2 = 0, bcr = 0; if (HAL_ETH_ReadPHYRegister(heth, LAN8720_ADDR, 0x02, &id1) != HAL_OK) { printf("MDIO 读失败:查 MDC/MDIO 连线、上拉电阻、PHY 地址\r\n"); return; } HAL_ETH_ReadPHYRegister(heth, LAN8720_ADDR, 0x03, &id2); printf("PHY ID = 0x%04lX%04lX\r\n", id1, id2); /* LAN8720A 应为 0x0007C0F1 */ HAL_ETH_ReadPHYRegister(heth, LAN8720_ADDR, 0x00, &bcr); bcr |= (1U << 12) | (1U << 9); /* 使能自协商并重启 */ HAL_ETH_WritePHYRegister(heth, LAN8720_ADDR, 0x00, bcr); }

读出来是0x0007C0F1就说明 PHY 活着;是0x0000FFFF或者0x00000000,分别对应 MDIO 线断开和时钟完全没起来。这两个值能直接把问题范围砍掉一半,比盲目查代码高效得多。

3. CubeIDE 工程配置:顺序比参数更重要

3.1 时钟树:先定 480MHz,再倒推 ETH 相关分频

打开 CubeMX 的第一件事是配时钟,因为 ETH 的 MDC 分频、FreeRTOS 的 tick、串口波特率全都依赖它。以 25MHz 外部晶振、目标 480MHz 主频为例:

  • PLL1:M = 5,N = 192,P = 2,Q = 4,得到 25/5×192 = 960MHz 的 VCO,P 分频后 480MHz 给 CPU;
  • HPRE 设为 2,AHB = 240MHz;
  • APB1/APB2/APB3/APB4 按外设需求分频,串口用的 APB 不要低于 100MHz,否则高速波特率容易出误差。

ETH 的时钟源在 H7 上主要有两条路:MAC 侧的寄存器访问时钟挂在 AHB 上,而 RMII 的实际收发时序由外部 50MHz REF_CLK 驱动。所以时钟树里真正需要你操心的是MDC 的分频,它决定了 PHY 管理接口能不能正常通信。目标是把 MDC 落在 1~2.5MHz 之间,绝不能超过 25MHz 上限。CubeMX 的 ETH 参数页里直接选和你 HCLK 相符的时钟区间就行,选完如果不放心,上板后用示波器量一下 PC1,频率对得上再继续。

3.2 ETH 参数逐项确认,每一项都有理由

生成代码前把这一页参数过一遍,我按重要性排:

参数建议值为什么这么取
ModeRMII引脚少,百兆够用
PHY Address0必须与 RXER/PHYAD0 的 strap 电平一致
Auto NegotiationEnable让 PHY 自己协商到 100M 全双工
RX Descriptor Length4~8突发流量时多几个描述符能少吃丢包
TX Descriptor Length4~8同上,且影响连续发送能力
RX Buffer Size1536至少 1524(1518 + 对齐余量),取 1536 是 32 字节对齐
RX Buffer Count4~8与描述符数量匹配
Checksum Offload视场景见 5.3 节的取舍分析
MAC Address自定义别用全 0,部分交换机会直接丢弃

RX Buffer Size 这个值很多人随手用默认的 1524,其实要留出对齐余量。DMA 对缓冲地址有对齐要求,你按 1536 取整,正好是 32 的倍数,跟 M7 的 cache line 也对得上,后面处理缓存一致性时省事。这种“多花 12 字节买个安心”的地方,我从来不含糊。

3.3 FreeRTOS 与 LWIP 的联动,有一个必须提前决定的坑

在 Middleware 里勾选 FreeRTOS 时,会让你选 CMSIS-RTOS 的版本:V1 还是 V2。这里必须提前想清楚——CubeMX 生成的 LWIP 中间件(sys_arch.c)默认是基于 CMSIS-RTOS V1 的 API 写的,里面用的是osMessagePut、osSemaphoreWait这一套。如果你把 FreeRTOS 接口选成 V2,生成的工程编译能过,但链接时会缺一堆符号,或者运行起来 tcpip 线程压根起不来。

我的建议是直接选 V1,让 LWIP 和 RTOS 用同一套兼容层,省掉手工改 sys_arch.c 的麻烦。非要上 V2 的话,你得把 sys_arch.c 里的邮箱、信号量、线程创建全部重写一遍,工作量不大但容易漏,而且每次重新生成代码都会被覆盖,得自己维护一份 patch。

优先级设置也要在这里定好:tcpip_thread的优先级建议高于业务线程,至少不能低于。网络收包是有实时性要求的,如果 tcpip 线程被业务线程长期抢占,表现就是 ping 的延迟忽高忽低、TCP 吞吐抖动大。

3.4 生成代码后必须手工补的四处

CubeMX 生成的代码能跑,但离“好用”还差四刀,每次重新生成都要补:

第一刀,MPU 与 Cache。H750 默认开 I-Cache 和 D-Cache,这对 480MHz 的 M7 是必须的,但 ETH 的 DMA 描述符和收发缓冲如果在可缓存区,就会出现“我明明发了,对方收不到”或者“收到的数据是上一包的”这种鬼现象。做法是用 MPU 把缓冲所在的那块 SRAM 配成 Normal non-cacheable,或者干脆在收发前后手动做 cache 维护。我选前者,一劳永逸。

第二刀,链接脚本。把 ETH 的描述符数组和 RX 缓冲池单独划到 D2 SRAM(0x30000000 起)里,和普通变量隔开:

/* 为 ETH DMA 单独划区,绕开 D-Cache 干扰 */ .lwip_sec (NOLOAD) : { . = ABSOLUTE(0x30040000); *(.RxDecripSection) . = ABSOLUTE(0x30040060); *(.TxDecripSection) . = ABSOLUTE(0x30040200); *(.Rx_PoolSection) } >RAM_D2

地址按你自己 SRAM 的规划来,核心要求只有两点:32 字节对齐(cache line 大小),以及不与其它变量重叠。D2 SRAM 是 DMA 可达的,而DTCM(0x20000000)DMA 根本访问不到,这一点是 H7 上最容易踩的坑之一,缓冲区一旦落到 DTCM,现象是 DMA 描述符标志永远不置位。

第三刀,tcpip 线程栈。默认给的是 1024 个字(约 4KB),跑纯 ping 够用,一旦上 TCP 且窗口开大,很容易栈溢出。CubeMX 里改不了就直接改lwipopts.h里的TCPIP_THREAD_STACKSIZE,我一般给到 1536~2048。

第四刀,Flash 空间。H750 的片内 Flash 只有 128KB,这一点很多人上手时没概念。HAL + FreeRTOS + LWIP + 你的业务代码,编译出来 100KB 出头是常态,稍微加点 printf 和浮点运算就爆。要么开-Os、用 newlib-nano、裁掉不用的 HAL 模块,要么老老实实上外部 QSPI Flash 做内存映射运行。这个决策要在项目初期定,别等写完业务才发现放不下。

4. FreeRTOS 下 LWIP 的运行骨架

4.1 三种 API 怎么选,别一上来就用 socket

LWIP 提供三层接口,选错了后面全是返工:

接口是否阻塞RAM 占用适用场景
RAW API回调式最小裸机、极简设备、对资源抠到字节
netconn阻塞式中等RTOS 下的常规业务,顺序化写法
socket阻塞式最大需要移植 Linux 侧代码,看重可移植性

RAW API 是事件驱动的,收数据靠回调,你写业务时得把流程拆成一堆状态变量,代码可读性很差。但它是唯一能在裸机上跑的接口。

netconn 是 LWIP 自己的顺序式 API,配合 FreeRTOS 用起来最顺手:netconn_new→netconn_bind→netconn_listen→netconn_accept→netconn_recv,一路往下写,跟写文件差不多。它在底层还是走 tcpip_thread 的邮箱机制,所以线程安全是有保障的。

socket 层在 LWIP 里其实也是基于 netconn 封装的一层兼容层,多耗一点内存换来代码可以直接搬去 Linux。如果你的项目未来要做双平台,用 socket 值;如果就是单平台产品,netconn 更省。

4.2 内存池的定量计算,不要凭感觉填数字

这是决定吞吐和稳定性的核心。先把几个关键量的关系理清楚:

PBUF_POOL_BUFSIZE至少要能装下一个完整的以太网帧载荷:

14(以太网头)+ 20(IP 头)+ 20(TCP 头)+ 1460(MSS)= 1514 对齐到 4 字节 → 1516,取 1528 或 1536 留余量

然后是 TCP 窗口。窗口大小决定了“在等对端 ACK 之前最多能发多少数据”,它的下限由带宽时延积决定:

BDP = 带宽 × RTT = 12.5 MB/s × 0.001 s = 12.5 KB TCP_WND 至少 12.5 KB,即 8~9 个 MSS

局域网 RTT 通常小于 1ms,所以TCP_WND = 8 × 1460 = 11680是一个够用且不浪费的取值。要跨公网或者经过多级交换,可以上到 16 个 MSS,但注意不启用窗口缩放时上限是 65535 字节。

给一份我常用的配置对照:

参数默认值高吞吐建议值说明
MEM_SIZE160032768LWIP 堆,pcb 选项和动态 pbuf 都从这来
PBUF_POOL_SIZE1632收包缓冲个数,突发丢包看这里
PBUF_POOL_BUFSIZE5921528必须 ≥ 1514 + 对齐
TCP_MSS5361460以太网标准 MTU 下取满
TCP_WND2144116808 × MSS
TCP_SND_BUF214411680同上,影响发送流水线
MEMP_NUM_TCP_SEG1632必须 ≥ TCP_SND_QUEUELEN
MEMP_NUM_PBUF1632

粗略算一下内存占用:PBUF_POOL_SIZE × PBUF_POOL_BUFSIZE = 32 × 1528 ≈ 48KB,加上MEM_SIZE32KB,总共 80KB 左右。H750 的 AXI SRAM 有 512KB,完全放得下,所以别在这上面抠。

4.3 中断优先级与时基,这里错一次能查一整天

FreeRTOS 有个铁律:任何调用xxxFromISR的中断,其优先级数值必须 ≥configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(数值越大优先级越低)。CubeMX 里这个值默认是 5,也就是说 ETH 中断的抢占优先级必须配成 5~15。

ETH 中断里会通过信号量通知 tcpip 线程去取包,所以它必然要调 FromISR 系列 API。如果 ETH 中断优先级被设成 0(CubeMX 里 ETH 的默认值经常是 0),结果就是运行时触发configASSERT断言卡死,或者更糟——不报错但信号量状态错乱,表现为“有时候能收,有时候卡住”。

SysTick 的优先级设成 15(最低),这是 FreeRTOS 的要求,别动。另外注意HAL_Delay在 FreeRTOS 下的行为:CubeMX 生成的SysTick_Handler里会同时调用HAL_IncTick()和xPortSysTickHandler(),所以HAL_Delay还能用,但精度受 tick 频率影响,而且绝对不能在中断里调用HAL_Delay——如果中断优先级高于 SysTick,tick 永远不增,直接死等。

4.4 链路状态回调与热插拔处理

网线拔掉再插上,协议栈不会自己复活,你得自己处理。CubeMX 生成的ethernetif.c里

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

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

立即咨询