news 2026/11/3 17:55:30

STM32H743以太网RMII调试与Cache一致性排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32H743以太网RMII调试与Cache一致性排查

以太网这块,STM32H743 是我调过的 MCU 里"硬件看着最简单、软件最容易翻车"的典型代表。原理图上就十来根线,CubeMX 里点几下就能生成工程,可真跑起来,link 灯不亮、灯亮了 ping 不通、ping 通了跑一会儿就死机,各种现象轮着来。我这几年基于 STM32H743 做过几版带以太网的产品,从最开始的"照着例程抄但就是不通",到后来能把问题定位到某一根走线、某一个 MPU 属性、某一个 cache line 上,中间踩的坑基本覆盖了 RMII 调试的所有主要分支。这篇就把这些经验整理出来:以太网 RMII 接口在 H743 上到底难在哪、硬件阶段必须提前定死哪些事、软件配置里哪几处是"不配就等随机死机"、以及按现象反推的排查路径。不管你是刚拿到第一块带网口的 H743 板子,还是已经通了一部分但被偶发问题折磨,应该都能从里面找到对应的位置。

1. 先把问题拆开:H743 以太网为什么容易翻车

1.1 三个子系统叠加,任何一层出问题现象都一样

很多人调以太网的习惯是"不通就改代码",改netif参数、改 LwIP 配置、换例程版本,折腾几天没结果。问题在于,一条以太网链路从物理层到应用层,中间压着三个相对独立的子系统,而它们的故障表现高度重合——都是"ping 不通"。

第一层是PHY 与 MCU 之间的 RMII 物理链路,包括 50MHz 参考时钟、TXD/RXD 数据线、CRS_DV 载波侦听,这一层不通,MAC 根本不知道线插没插。第二层是ETH MAC 与 DMA 引擎,它负责把内存里的描述符和缓冲区搬运到总线上,这一层出问题往往表现为"link 是 up 的,但收不到包"或者"发得出去收不回来"。第三层是Cortex-M7 的存储系统,包括 MPU 属性、L1 D-Cache、AXI 总线仲裁,这是 H7 系列独有的复杂度,F4/F7 上攒的经验到 H7 上直接失效。

我自己的习惯是,上电第一件事不是跑 ping,而是把这三层按顺序点亮:先确认 MDIO 能读到 PHY 的 ID 寄存器,说明 MDC/MDIO 通了;再读 BMSR 确认 link 状态和速率,说明 RMII 时钟和自协商通了;最后才关心能不能收发包。这样一旦卡住,至少知道卡在第几层,不会在三个方向上来回乱撞。

提示:MDIO 是独立于 RMII 数据通道的慢速管理接口,它能在数据通道完全没通的情况下正常工作。所以"MDIO 能读、link 起不来"和"MDIO 都读不到"是两个完全不同的问题方向,前者往时钟和数据线走,后者往 MDC/MDIO 引脚和 PHY 供电走。

1.2 RMII 与 MII 的取舍:省下的 6 根线换来了什么约束

选 RMII 基本不是因为性能,而是因为引脚。MII 需要 4 根 TXD、4 根 RXD、TX_CLK、RX_CLK、TX_EN、RX_DV、RX_ER、CRS、COL,加起来十六七根;RMII 把它砍到 9 根:REF_CLK、TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、RX_ER(可选)、MDC、MDIO。对于 LQFP100 甚至 LQFP64 这种小封装,这 6 根线往往就是一个 SPI Flash 加一个 SDIO 的差距。

代价有三条,每一条都是实际的坑。

第一,数据位宽从 4 位降到 2 位,时钟从 25MHz 提到 50MHz。MII 的 25MHz 时钟下每个周期传 4 位,RMII 的 50MHz 时钟下每个周期传 2 位,带宽都是 100Mbps。但 50MHz 对走线和时钟质量的要求明显更高,REF_CLK 的抖动、占空比、边沿质量都会直接影响采样窗口。

第二,RMII 没有独立的 TX_CLK 和 RX_CLK,收发共用同一个 50MHz 参考时钟。这意味着时钟必须由单一源产生并同时送到 MCU 和 PHY,两边看到的必须是同一个时钟。一旦你让 MCU 用一个时钟源、PHY 用另一个时钟源,哪怕频率标称一样,ppm 级别的偏差累积起来也会让采样点漂移,表现为"偶尔通、偶尔不通"或者"温度一变化就不通"。

第三,RMII 的 TXD0/TXD1、RXD0/RXD1 是单端信号,不是差分。这一点被误解得特别多。有人在 PCB 上按 100Ω 差分去画 RXD0/RXD1,结果阻抗完全不对。RMII 的数据线是各自独立的单端 50Ω 走线,参考完整地平面,只有真正需要差分的是以太网变压器出来的那对 TX+/TX-、RX+/RX-。

1.3 50MHz 参考时钟到底该由谁提供

这是 RMII 调试里第一个必须拍板的问题,而且必须在画板之前拍板,改板代价很大。常见三种方案,我列个表对比。

方案时钟来源优点缺点适用场景
APHY 输出 50MHz 给 MCUBOM 最省,只需一颗 25MHz 晶振依赖 PHY 内部 PLL,部分型号需配置自举引脚;PHY 复位期间无时钟最常见,量产首选
B独立 50MHz 有源晶振,同时供 MCU 和 PHY时钟质量最好,抖动最小,最稳多一颗器件,成本略高调试样板、对稳定性要求高的场合
CMCU 输出 50MHz 给 PHY省晶振抖动大、占空比难保证,且 H7 的 MCO 分频不一定能整除出 50MHz不推荐,除非 HSE 本身就是 50MHz 可直接旁路

方案 A 是主流做法,具体接法是:PHY 外挂 25MHz 晶振,内部 PLL 倍频到 50MHz,从nINT/REFCLKO引脚输出,接到 STM32H743 的PA1(ETH_RMII_REF_CLK)。这里有两个容易忘的点。

一是 PHY 的这个引脚是复用的,输出 50MHz 还是输出中断信号,取决于自举引脚(比如 LED2/REGOFF 这类引脚在上电时的电平)。如果这个引脚被错配成中断输出,REF_CLK 就没了,现象是 MDIO 也许能读(因为 MDC 是 MCU 自己产生的),但链路永远起不来。我见过一次就是因为这个自举电阻贴错位置,查了两天才定位。

二是PHY 复位期间 REF_CLK 是没有输出的。如果你的HAL_ETH_Init()在 PHY 复位完成之前就去初始化 MAC 并等待时钟稳定,可能会超时失败。稳妥的顺序是:先拉低 PHY 复位,延时,释放复位,等 50~100ms 让 PHY 内部稳压和 PLL 稳定,再初始化 ETH 外设。

方案 B 我在调试样板上用得最多。一块 50MHz 有源晶振,输出经过一个小的时钟缓冲器,或者直接星型走线分两路,一路到 PA1,一路到 PHY 的 REF_CLK 输入脚。这样做的好处是把变量彻底锁死:不管 PHY 怎么配、怎么复位,MCU 这边始终有干净的 50MHz。定位问题时用它做对照,能快速判断问题是不是出在时钟上。

2. 硬件阶段就该定下来的事

2.1 时钟网络:精度、走线和去耦

RMII 对参考时钟的精度要求是 ±50ppm,这个指标 25MHz 晶振基本都能满足,但实际出问题的往往不是标称精度,而是负载电容匹配。晶振的负载电容(CL)必须和 PCB 上的实际电容匹配,两颗对地电容的取值要按CL = C1*C2/(C1+C2) + Cstray反推,Cstray 一般取 3~5pF。很多人直接抄 22pF,如果晶振 CL 是 12pF,实际就偏了,频率偏移到几十 ppm,短期内看不出问题,长期跑会有偶发丢包。

走线上,50MHz 的 REF_CLK 属于"不高不低"的尴尬频段,不用按射频对待,但也不能随便拉。我的做法是:走线尽量短(控制在 3cm 以内最好),全程参考完整地平面,不要在参考平面开槽的地方穿过,两边包地并多打过孔,避免和 SDMMC、USB、LCD 这类高速信号长距离平行。如果 REF_CLK 是从 PHY 输出到 MCU,中间不要串电阻,除非实测有过冲需要 22~33Ω 的源端匹配。

去耦方面,PHY 的电源引脚每颗都要有 0.1uF 就近放置,整体再配一颗 10uF。如果 PHY 内部有 LDO 输出(很多 10/100M PHY 是单 3.3V 供电,内部再产生 1.2V 内核电压),那个输出脚必须外接规定的电容,容值不能随意改,否则内部稳压环路不稳,会出现链路随机掉线。

2.2 PCB 走线:单端 50Ω、等长和回流路径

前面说过 RMII 数据线是单端信号,这里展开讲具体怎么走。

TXD0/TXD1、TX_EN 是一组,从 MCU 到 PHY,方向单一;RXD0/RXD1、CRS_DV、RX_ER 是另一组,从 PHY 到 MCU。这两组的走线建议按 50Ω 单端阻抗控制,线宽按叠层算,一般四层板(信号-地-电源-信号)上 6mil 左右。等长方面,同一组内的偏差控制在 100mil 以内就够了,RMII 的 50MHz 对应周期 20ns,信号在 FR4 上的传播速度大约 6mil/ps,100mil 对应约 17ps,相对 20ns 完全不是问题。所以不用像 DDR 那样做到 5mil 精度,但也别一根 2cm 一根 8cm。

真正影响大的是回流路径。RMII 的信号回流走的是参考平面,如果某根线在走的过程中跨过了地平面的分割缝,回流路径被迫绕远,形成一个大环路,辐射和串扰都会上去。四层板如果第二层是完整地,基本不会有这个问题;但如果板上还有模拟部分、隔离部分,地平面被切开,就要特别注意别让 RMII 的线跨过去。

MDC/MDIO 是低速信号(MDC 一般不超过 2.5MHz),走线要求宽松,但MDIO 需要上拉电阻,典型 1.5k~2.2k 到 3.3V。这个上拉经常被漏掉,现象是 MDIO 读出来永远是 0xFFFF 或者 0x0000。有些 PHY 内部带上拉,手册里会写明,但外置一个更保险。

还有 PHY 到 RJ45 变压器这一段,是真正的差分:TX+/TX-、RX+/RX- 按 100Ω 差分走,尽量等长,远离其他信号。变压器到 RJ45 之间的中心抽头要按 PHY 手册接终端电阻和电容。这段走错了,现象是"能 link 但误码率高",很难查。

2.3 PHY 的复位、地址和自举引脚

PHY 的复位时序是被低估的一个点。多数 PHY 要求 nRST 拉低保持一段时间(几十毫秒级别),释放后还要等内部 PLL 锁定才能访问。我的标准做法是:上电后立刻拉低 nRST,保持 50ms,释放,再延时 100ms 才开始 MDIO 通信。这个时间比手册要求长,但换来的是彻底排除复位时序这个变量。

PHY 地址决定了 MDIO 通信时用哪个地址访问它。地址一般由几个自举引脚在上电时的电平决定,常见的是 PHYAD0 复用某个功能引脚。如果这个引脚悬空,地址是不确定的,可能这个板子读到 0,换个批次就读到 3。自举引脚一定要用电阻明确拉高或拉低,不能悬空,这是硬件设计的基本纪律,但我在实际板子上见过不止一次。

自举引脚同时还决定其他配置,比如 RMII 模式还是 MII 模式、Auto-MDIX 是否使能、内部 LDO 是否使能。有些 PHY(比如某些 10/100M 型号)需要专门的寄存器位切换到 RMII 模式,而不是靠自举。这种就要在初始化代码里先写这个寄存器,再去做其他配置。我遇到过一款 PHY,默认是 MII 模式,不切到 RMII 就永远收不到数据,但 link 灯是亮的——因为 link 状态是 PHY 内部自协商的结果,跟 MAC 侧接口模式无关。这种"灯亮但没数据"的现象特别容易误导人。

2.4 供电与地平面:那些不起眼但致命的细节

PHY 的供电质量直接影响链路稳定性。3.3V 这一路最好单独从电源树分出来,经过一颗磁珠或者 0Ω 电阻隔离,配上足够的去耦。如果 PHY 和 MCU 的数字电源完全共用且走线很长,MCU 侧的大电流跳变会在 PHY 电源上产生纹波,表现为大流量传输时误码率上升。

地平面方面,如果板子涉及 RJ45 的隔离变压器,变压器初级侧(PHY 侧)和次级侧(网口侧)的地是分开的,中间通过变压器隔离。这个隔离地的处理有讲究:初级侧地要完整,不能和次级侧在 PCB 上直接连,否则隔离就白做了。同时初级侧地要通过一个高压电容(比如 1nF/2kV)连到次级侧地,给共模噪声提供泄放路径。这个电容漏了,网口抗干扰能力会明显下降,具体表现是插拔网线或者附近有干扰源时链路断开重连。

3. 软件配置里最容易被忽略的三件事

3.1 MPU 配置:不配就是随机死机

这是 H7 相比 F4/F7 最大的差异点,也是新手最容易栽的地方。Cortex-M7 带 L1 D-Cache,默认在main()开头调用SCB_EnableDCache()。开了 cache 之后,CPU 写内存可能只是写进 cache,并没有真正落到 SRAM;DMA 读内存时读的是 SRAM 的真实内容,于是两者不一致。反过来,DMA 往 SRAM 写了新数据,CPU 读的时候读到的是 cache 里的旧数据。

标准解法是把以太网的描述符区和缓冲区放到一块Non-Cacheable的内存区域。H743 的 D2 SRAM(起始地址 0x30000000,共 288KB)就是专门给这类外设 DMA 用的,把它整体或部分配置成 non-cacheable 最省事。

static void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); /* Region 0: D2 SRAM 前 64KB,给以太网描述符和缓冲区 */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.BaseAddress = 0x30000000; MPU_InitStruct.Size = MPU_REGION_SIZE_64KB; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* Region 1: 其余 SRAM 保持可 cache,提升 CPU 性能 */ MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }

MPU_Config()必须在SCB_EnableDCache()之前调用,顺序错了配置不生效。这一点 CubeMX 生成的代码里是对的,但如果你是手工移植或者改了main()的结构,很容易搞乱。

不配 MPU 的后果不是"一定不通",而是"有时候通,跑一会儿就不通"。因为描述符区第一次被访问时可能刚好没进 cache,程序看起来正常;跑起来之后某次 cache 换出,描述符被覆盖,DMA 读到垃圾数据,现象是随机死机或者收发包错乱。这类问题最难查,因为你改任何一处代码都可能让现象消失。

3.2 描述符与缓冲区的内存布局

描述符是 DMA 和 CPU 之间约定好的数据结构,CPU 写描述符告诉 DMA 缓冲区在哪、多长,DMA 处理完后回写状态位。H7 的 ETH 使用新版 DMA,描述符结构是ETH_DMADescTypeDef,每个 16 字节。

几个硬性约束:描述符表起始地址要 4 字节对齐(实际上按 32 字节对齐更保险,避免跨 cache line),缓冲区地址也建议 32 字节对齐。缓冲区大小按最大帧长预留,标准以太网帧 1518 字节,加上 VLAN tag 和可能的对齐需求,我一般按1536 字节每个 buffer 来分配。

数量上,收和发各 8 个描述符是能用但不宽裕的配置。我用得多的是 RX 16 个、TX 8 个,或者 RX 32 个、TX 16 个。数量多了占内存(每个 1536 字节,RX 32 个就是 48KB),但能明显降低突发流量下的丢包率。

#define ETH_RX_DESC_CNT 16 #define ETH_TX_DESC_CNT 8 #define ETH_BUF_SIZE 1536 ETH_DMADescTypeDef DMARxDscrTab[ETH_RX_DESC_CNT] __attribute__((section(".eth_desc"), aligned(32))); ETH_DMADescTypeDef DMATxDscrTab[ETH_TX_DESC_CNT] __attribute__((section(".eth_desc"), aligned(32))); uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_BUF_SIZE] __attribute__((section(".eth_buf"), aligned(32))); uint8_t Tx_Buff[ETH_TX_DESC_CNT][ETH_BUF_SIZE] __attribute__((section(".eth_buf"), aligned(32)));

链接脚本里对应加两段:

.eth_desc (NOLOAD) : { . = ALIGN(32); *(.eth_desc) . = ALIGN(32); } > RAM_D2 .eth_buf (NOLOAD) : { . = ALIGN(32); *(.eth_buf) . = ALIGN(32); } > RAM_D2

aligned(32)这个属性一定要加。原因和 cache line 有关:Cortex-M7 的 cache line 是 32 字节,如果缓冲区起始地址没对齐,做 cache invalidate 操作时会把相邻的数据一起失效掉,那些数据如果恰好是别的变量,就会出现莫名其妙的错乱。我在一个项目里就是因为某个 buffer 地址是 0x30000804 而不是 32 的整数倍,导致相邻的一个计数器变量每次收包都被清零,查了很久。

3.3 Cache 维护与中断优先级

即便把描述符和缓冲区放在 non-cacheable 区域,如果你的应用层还要对这些数据做二次处理(比如把收到的包复制到另一个可 cache 的缓冲区,或者发送前在可 cache 区组装数据),Cache 维护仍然要做。

发送方向的规则是:CPU 写完数据之后、启动 DMA 之前,执行Clean操作,把 cache 里的脏数据写回 SRAM,保证 DMA 读到的是最新内容。接收方向反过来:DMA 写完之后,CPU 读之前,执行Invalidate操作,把 cache 里的对应行作废,强制 CPU 从 SRAM 重新读。

/* 发送前:把数据从 cache 刷到 SRAM */ SCB_CleanDCache_by_Addr((uint32_t *)buf, len); /* 接收后:作废 cache 中的对应区域 */ SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);

注意 Clean 和 Invalidate 都是按 cache line 粒度操作的,传进去的地址和长度会被扩展到 32 字节对齐的边界。所以如果两个变量共用同一个 cache line,Invalidate 一个会把另一个也作废——如果另一个变量恰好有未写回的数据,就丢了。这就是为什么共享同一 cache line 的变量要特别注意,也是为什么缓冲区要 32 字节对齐。

中断优先级是另一个坑,尤其配合 FreeRTOS 使用时。FreeRTOS 有一个configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY的概念,高于这个优先级(数值更小)的中断不能调用任何 FreeRTOS 的 API。以太网中断里通常要释放信号量通知 tcpip 线程,属于会调用 API 的情况,所以 ETH 中断的优先级数值必须 ≥ 这个阈值。

我用的是 ETH 中断优先级设为 5(分组 4,即 4 位抢占优先级),configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY设为 5。有些工程的 ETH 中断优先级设成 0 或 1,然后中断里调用xSemaphoreGiveFromISR(),结果触发断言或者直接跑飞。这类问题在调试器里能看出来,但如果你没开断言,表现就是随机重启。

还有一个和中优先级配置相关的隐患:H7 的优先级分组默认是 4 位全部给抢占优先级,如果你按 F4 的习惯去设置子优先级,行为会不符合预期。CubeMX 里HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)要显式配置一次。

4. 按现象查:故障排查实战路径

4.1 现象一:MDIO 读不到 PHY,读回全是 0xFFFF 或 0x0000

这是最底层的问题,先解决它再往下走。0xFFFF 通常意味着 MDIO 线上没有设备应答,上拉电阻把线拉高了;0x0000 意味着线被拉低,可能是 PHY 没上电或者复位没释放。

排查顺序我一般是这样的:

第一步,量 PHY 的电源。3.3V 是否到位,内部 LDO 输出是否正常(如果有)。这一步用万用表就行,但我更推荐用示波器看纹波,有些 PHY 在电源纹波大的时候 MDIO 会时通时不通。

第二步,量 nRST 引脚。确认复位已经释放到高电平。有些板子上复位电路用的是 RC 加按键,电容选大了,上电后复位释放要几百毫秒,程序跑得太快就去访问 PHY,会读到失败。

第三步,量 MDC。初始化之后 MDC 上应该有时钟输出,频率是 HCLK 分频得到的,一般配置在 1~2.5MHz。如果 MDC 完全没有波形,说明 ETH 外设的时钟没使能,检查__HAL_RCC_ETH1MAC_CLK_ENABLE()、__HAL_RCC_ETH1TX_CLK_ENABLE()、__HAL_RCC_ETH1RX_CLK_ENABLE()这三句是否都调用了,以及 CubeMX 里 ETH 外设的 Clock 配置是否使能。

第四步,检查 MDIO 上拉电阻。没上拉的话,MDIO 在空闲态是浮空的,读出来不确定。

第五步,确认 PHY 地址。把地址从 0 到 31 全扫一遍,能读到合理 ID 的地址就是对的。这个技巧在不确定自举配置时特别有效,写个循环挨个读PHY_ID1(寄存器 2)和PHY_ID2(寄存器 3),正常的 PHY 会返回厂商 ID 和型号 ID,不会是 0xFFFF。

for (uint8_t addr = 0; addr < 32; addr++) { uint32_t id1 = 0, id2 = 0; if (HAL_ETH_ReadPHYRegister(&heth, addr, 0x02, &id1) == HAL_OK && HAL_ETH_ReadPHYRegister(&heth, addr, 0x03, &id2) == HAL_OK) { if (id1 != 0xFFFF && id1 != 0x0000) { printf("PHY found at addr %d, ID = %04lX%04lX\r\n", addr, id1, id2); } } }

4.2 现象二:MDIO 正常但 link 起不来,或者反复 up/down

MDIO 能读说明管理通道通了,问题大概率在 RMII 数据通道或自协商上。分几种情况。

link 完全没有:先量 PA1 上的 REF_CLK 有没有 50MHz。有示波器的话看幅度是不是接近 3.3V,占空比是不是接近 50%。如果没有时钟,回到 2.1 节查时钟方案;如果有时钟但幅度明显偏低(比如只有 1V 左右),可能是引脚配置错了,PA1 没有配成复用功能 AF11,而是配成了普通 GPIO 或者模拟输入。

link 灯亮但代码读到的 link 状态一直是 down:这是"灯亮"和"寄存器状态"不一致的经典情况。有些 PHY 的 link 指示灯是硬件直接驱动的,不依赖寄存器轮询;而软件读 BMSR(寄存器 1)的 bit2 需要先读一次 BMSR 再读一次 BMSR,因为 link 状态位是"读取即清零"的锁存机制。第一次读拿到变化标志,第二次读才是当前状态。这个细节在很多例程里没写,导致有人一直读到 0。

link 反复 up/down:优先怀疑时钟质量或电源纹波。如果时钟是 PHY 出的,检查 PHY 的 25MHz 晶振负载电容;如果电源纹波大,在大流量或者有其他外设工作时会更明显。另一种可能是自协商失败导致的反复重试,可以尝试先关掉自协商,强制 100M 全双工看看是否稳定,如果稳定了就说明是自协商过程有问题,再回头查时钟。

/* 强制 100M 全双工,用于快速验证 */ HAL_ETH_WritePHYRegister(&heth, PHY_ADDR, 0x00, 0x0000); /* 关自协商 */ HAL_Delay(10); HAL_ETH_WritePHYRegister(&heth, PHY_ADDR, 0x00, 0x2100); /* 100M + 全双工 */ HAL_Delay(100);

注意关自协商之后,如果对端是自协商的,可能出现双工不匹配——一端全双工一端半双工,表现为能通但大包丢。所以这只是排查手段,不是最终方案,验证完要改回自协商。

4.3 现象三:link 正常但 ping 不通

link 起来了说明物理层和 MAC 层基本没问题,问题在数据通道或者协议栈。我按顺序这么查。

第一,看接收计数有没有增长。在 ETH 的 DMA 统计或者 LwIP 的netif->mib2_counters里能看到收包数。如果收包数一直是 0,说明数据根本没进到 DMA,问题在 RXD 数据线或者 MAC 的接收配置。如果收包数在涨但 ping 不通,说明包进内存了但协议栈没处理,问题在 LwIP 配置或者中断没上报。

第二,确认 MAC 地址。MAC 地址全 0 或全 FF 是常见的低级错误,有些开发板出厂就没烧 MAC,代码里默认初始化为 0。全 0 的 MAC 会被交换机或对端丢弃。我一般会在ethernetif_init里检查一下,如果是全 0 就填入一个本地管理地址。

第三,用抓包工具看。在 PC 侧抓包,看有没有收到 ARP 请求。如果收到了请求但没有回应,问题在 MCU 侧没回包;如果连请求都没有,说明 MCU 根本没发出包,问题在发送路径。这一步能直接把问题范围砍一半,比盲改代码高效得多。

第四,检查 IP 配置。IP、掩码、网关要和 PC 在同一个网段。有些人用 192.168.1.x 的板子去 ping 192.168.0.x 的 PC,中间没有路由,当然不通。

第五,确认 netif 已经 up。netif_set_link_up()和netif_set_up()两个都要调用,很多例程只调了其中一个。只调netif_set_up的话网口在协议栈层面是"已启用但链路未就绪",不会响应 ARP。

第六,中断有没有真正进来。在HAL_ETH_RxCpltCallback里打个断点或者翻转一个 GPIO,确认收包中断确实触发了。如果收包计数在涨但中断没进,可能是 ETH 中断没使能,或者 NVIC 里没开。

4.4 现象四:小包能通,大流量就掉包或卡死

这类问题的定位难度最高,因为它通常不是"完全错"而是"偶发错"。按发生概率从高到低排。

第一位是 cache 一致性问题。描述符或缓冲区没放 non-cacheable 区域,或者放对了但缓冲区没 32 字节对齐,都会导致偶发数据错乱。判断方法很简单:把 D-Cache 关掉(注释掉SCB_EnableDCache()),如果关掉之后问题消失,就是 cache 问题。定位到方向之后再按 3.1 和 3.2 节的方法修。

第二位是描述符或缓冲区不够。RX 描述符只有 4 个的时候,来一波连续的小包就会溢出。DMA 状态寄存器里会有接收缓冲区不可用(RBU)或接收溢出相关的错误标志,读出来能确认。把 RX 描述符加到 16 或 32 个,缓冲区按 1536 字节分配,基本能解决。

第三位是 LwIP 的内存配置。MEM_SIZE太小会导致 pbuf 分配失败,PBUF_POOL_SIZE太小会导致接收时没有可用 pbuf 而丢包。这些配置在lwipopts.h里,默认值往往偏保守。我的经验值是MEM_SIZE至少 16KB,PBUF_POOL_SIZE至少 16,每个PBUF_POOL_BUFSIZE按 1520 左右。具体数值要按你的实际吞吐量调,但低于这个量级基本都会出问题。

第四位是 AXI 总线仲裁。H7 有多个 master 共享 AXI 总线,ETH 的 DMA、LTDC 的图层读取、SDMMC 的 DMA 都在抢带宽。如果屏幕上跑着大分辨率刷新,同时网口跑满速,就会出现 ETH 抢不到总线导致丢包。STM32H7 的 AXI 有可编程的 QoS/带宽分配机制,可以给 ETH 提高优先级,但配置比较麻烦,一般先通过降低其他外设的突发长度或者错开时序来缓解。

第五位是中断优先级导致的线程饥饿。如果 ETH 中断优先级很高且收发非常频繁,tcpip 线程可能长时间得不到调度,表现出来就是"能通但很慢"或者"通一会儿就卡住"。这种情况把 ETH 中断改成"收到一批包再通知一次",或者改用轮询模式,能明显改善。

4.5 常见故障速查表

把上面这些整理成一张表,现场排查时可以直接对着看。

现象首要怀疑快速验证方法常见根因
MDIO 读回 0xFFFFMDIO 上拉、PHY 供电、地址错扫 0~31 地址;量 MDIO 空闲电平上拉电阻漏贴;自举引脚悬空
MDIO 读回 0x0000PHY 未上电或复位未释放量 nRST 电平和 PHY 电源复位电容过大;LDO 未起振
link 完全没有REF_CLK 缺失或引脚配置错示波器量 PA1 是否有 50MHz时钟源未配置;AF 复用配错
link 反复 up/down时钟质量、电源纹波、自协商强制 100M 全双工对比晶振负载电容错;去耦不足
link 正常收包计数为 0RXD 数据线、MAC 接收配置检查 DMA 接收错误标志引脚复用错;RX 描述符未挂载
收包正常但 ping 不通MAC 地址、IP 配置、netif 状态PC 侧抓包看 ARPMAC 全 0;只调了 set_up
小包通大包丢Cache 一致性、缓冲区不足关闭 D-Cache 对比MPU 未配;缓冲区未对齐
跑一段时间死机描述符被覆盖、内存越界关 D-Cache 对比;看 HardFault 现场描述符放可 cache 区;栈溢出
传输速率远低于 100MLwIP 内存配置、中断开销抓包看有效吞吐MEM_SIZE 过小;每包一次中断

提示:这张表里"关闭 D-Cache 对比"出现了两次,因为它是区分"软件逻辑问题"和"存储系统问题"最快的手段。花五分钟改一行代码,能省掉几小时的猜谜。

5. 性能调优与实测经验

5.1 中断与轮询的混合策略

H7 的 ETH HAL 驱动支持中断模式(HAL_ETH_Start_IT)和轮询模式(HAL_ETH_Start)。中断模式省 CPU,但每个包一次中断在满速时开销很大;轮询模式简单可控,但会占满一个线程。

我实际用的最多的是混合:接收用中断,发送用轮询加中断确认。接收中断触发后,在回调里只做一件事——释放一个信号量通知处理线程,然后立刻返回,把实际的描述符处理和协议栈交互放到线程里做。这样中断服务时间很短,不会阻塞其他中断。

void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) { BaseType_t woken = pdFALSE; xSemaphoreGiveFromISR(eth_rx_sem, &woken); portYIELD_FROM_ISR(woken); }

接收线程里用HAL_ETH_ReadData()取包,处理完再挂回去。注意HAL_ETH_ReadData()一次只取一个包,如果一次中断对应多个包到达(DMA 会连续处理多个描述符),需要循环取直到返回HAL_ERROR。

发送方向,我一般是在low_level_output里直接调用HAL_ETH_TransmitFrame(),如果返回忙就等一个短的超时。相比中断驱动,这样代码简单,也不会出现"发送完成中断来了但不知道对应哪个包"的问题。

5.2 描述符数量与缓冲区大小的取舍

描述符数量本质上是一个"用内存换抗突发能力"的权衡。我做过一组对比测试,条件是一个 100Mbps 满速的 UDP 流,包长 512 字节,持续 60 秒,统计丢包率。

RX 描述符数RX 缓冲内存占用丢包率(相对)说明
4约 6KB高,约 8%只能应付极低速率,不推荐
8约 12KB中等,约 1.5%基本可用,突发时会丢
16约 24KB低,约 0.1%推荐起点
32约 48KB极低,接近 0高吞吐场合推荐
64约 96KB无明显进一步改善内存浪费,除非有特殊需求

缓冲区大小方面,1536 字节是一个"够用且对齐友好"的选择。1536 = 32 × 48,天然 32 字节对齐,同时能容纳带 VLAN tag 的最大帧(1522 字节)并有富余。有人用 1524(HAL 里ETH_MAX_PACKET_SIZE的默认值),不带 VLAN 时够用,但一旦要处理带 VLAN 的帧就会截断,而且 1524 不是 32 的整数倍,容易在 cache 操作上出问题。

TX 描述符我一般配得比 RX 少,因为发送是主动控制的,不会像接收那样被动溢出。TX 8 个基本够,除非你要做批量发送。

5.3 实测数据与几点体会

在 H743 主频 480MHz、HCLK 240MHz、D-Cache 开启、LwIP 用NO_SYS=0加 FreeRTOS 的配置下,我用 iperf 类的工具做过几轮测试,TCP 单向吞吐大概在 60~80Mbps,UDP 能到 90Mbps 以上。这个数字不是极限,因为 LwIP 本身有内存拷贝和协议栈开销,要做更高吞吐得上零拷贝加专用网卡驱动。

几个对性能影响明显的点,按影响从大到小:

D-Cache 开不开。开启之后 CPU 侧的处理速度快很多,但前提是描述符和缓冲区放在 non-cacheable 区。如果为了省事把 D-Cache 全程关掉,吞吐大概掉 20%~30%,而且 CPU 占用明显升高。

MPU 区域划分。只把描述符区配 non-cacheable、把缓冲区配 cacheable 然后靠手动 Clean/Invalidate,理论性能更好,但代码复杂度高,容易漏维护。我吃过亏,后来统一用"描述符和缓冲区都 non-cacheable"的简单方案,性能损失在 5% 以内,换来的是代码可预测性。

LwIP 的MEM_ALIGNMENT。这个宏要设成 4(32 位),如果误设成 1,会有大量非对齐访问,性能明显下降。H7 支持非对齐访问,所以功能上不会错,但会慢。

中断里的工作量。前面说过,中断里只释放信号量。我试过在中断里直接调用ethernetif_input,吞吐反而下降,因为中断上下文里不能做阻塞操作,处理路径被拉长,其他中断被延迟。

最后分享一个我调试时常用的小技巧:在 ETH 的中断回调里翻转一个空闲的 GPIO,然后用逻辑分析仪长时间抓这个引脚。正常工作时应该看到密集的脉冲;如果出现一段明显长的空白,说明那段时间没有中断进来,问题在物理层或者 DMA 卡住;如果脉冲一直很密但应用层没数据,问题在协议栈或者信号量传递。一根 GPIO 加一个逻辑分析仪,能把这几个层次清楚地区分开,比在代码里到处打印日志高效得多。

还有一个更省事的做法:把 DMA 的错误标志位(发送缓冲不可用、接收缓冲不可用、总线错误、看门狗超时这几类)在每次中断里读出来累加到一个全局变量,跑一段时间后打印。哪类错误在涨,问题就在哪一层。这个方法我用得最多,因为不用额外的硬件,也不用改协议栈,几行代码就能建立一条长期的健康监控线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 17:55:08

计算机期刊投稿避坑指南:从desk rejection到录用的实战策略

1. 这不是投稿指南&#xff0c;而是一份“被拒稿27次后”整理的生存手记 《计算机科学》期刊投稿心得——看到这个标题&#xff0c;你大概率会下意识点开&#xff0c;然后在三秒内划走。因为市面上90%的所谓“投稿心得”&#xff0c;要么是刚中一篇综述就急着立人设的应届生复盘…

作者头像 李华
网站建设 2026/11/3 17:54:33

Unity核心API解析:GameObject、Time与Input实战指南

1. Unity常用API核心概念解析作为一名Unity开发者&#xff0c;掌握常用API是构建游戏逻辑的基础能力。Unity引擎提供了丰富的API接口&#xff0c;覆盖了游戏对象操作、时间控制、输入检测、屏幕管理和摄像机控制等核心功能模块。这些API构成了Unity开发的基础框架&#xff0c;理…

作者头像 李华
网站建设 2026/11/3 17:53:36

Higress:AI原生API网关与Nginx Ingress迁移方案

1. Higress 项目概述 Higress 是一款基于 Envoy 和 Istio 构建的 AI 原生、高性能 API 网关。它将流量网关、微服务网关与 AI 网关统一于单一控制面&#xff0c;降低云原生与 AI 工作负载的运维复杂度。Higress 诞生于阿里巴巴内部大规模生产实践&#xff0c;为 CNCF 生态带来企…

作者头像 李华
网站建设 2026/11/3 17:51:52

移动端适配:元视口标签与响应式设计实战

1. 移动端适配的基石&#xff1a;元视口标签深度解析 在当今移动优先的互联网环境中&#xff0c;作为前端开发者&#xff0c;我们每天都要面对各种屏幕尺寸的设备。记得我第一次开发移动端页面时&#xff0c;发现明明在电脑上调试好的页面&#xff0c;在手机上却变成了"缩…

作者头像 李华
网站建设 2026/11/3 17:50:36

Excel数据高效导入Oracle的实践指南

1. 为什么需要Excel数据快速导入Oracle&#xff1f; 在日常数据处理工作中&#xff0c;我们经常遇到需要将Excel表格数据迁移到Oracle数据库的场景。传统的手动建表、逐条插入的方式不仅效率低下&#xff0c;而且容易出错。特别是当面对以下情况时&#xff1a; 数据量超过10万…

作者头像 李华
网站建设 2026/11/3 17:50:35

AI编码代理的上下文工程:从ChatMemory到Context-mode MCP

1. 项目概述&#xff1a;为什么“上下文工程”正在成为AI编码代理的生死线你有没有遇到过这样的情况&#xff1a;写一个Python脚本调用API&#xff0c;第一次问大模型“怎么用requests发带token的POST请求”&#xff0c;它给的代码能跑&#xff1b;但当你紧接着追加一句“再把返…

作者头像 李华