1. 从零吃透 STM32F429 以太网外设:为什么它值得单独花时间啃
STM32F429IGT6 这颗片子做以太网通信,很多人第一反应是"直接上 LwIP 不就行了",但真到调试阶段,十有八九会卡在底层——PHY 不握手、RMII 时钟不对、DMA 描述符跑飞、ping 不通还找不到原因。我自己在几个工业网关和边缘采集项目里反复用过这套组合,踩过的坑足够写满一个笔记本。这篇笔记就专门聊以太网外设本身,也就是 MAC 层和 PHY 层之间的那点事,把 LwIP 之前的地基打扎实。
先说清楚这篇内容适合谁看。如果你手上正好有一块 STM32F429IGT6 的开发板,想跑通以太网通信,但对着参考手册里那一堆 MAC 寄存器、DMA 描述符、RMII 引脚复用发懵,那这篇就是给你写的。如果你已经能 ping 通但不知道数据是怎么从网线进到内存的,也建议往下看,理解底层之后调 LwIP 会顺很多。核心关键词就三个:STM32F429IGT6、以太网外设、RMII 接口,外加一个绕不开的LwIP作为最终目标。
STM32F429 的以太网外设严格来说叫Ethernet MAC,它符合 IEEE 802.3 标准,内部集成了 MAC 控制器和 DMA 控制器,但不含 PHY。PHY 是外部芯片,常见的是 LAN8720A、DP83848 这类。MAC 和 PHY 之间通过RMII或MII接口连接。F429 支持这两种,但实际项目里几乎都用 RMII,因为引脚少一半,PCB 布线压力小很多。这个选择背后的逻辑后面会详细展开。
我见过太多人一上来就抄 LwIP 的例程,结果 PHY 地址填错、时钟没配、复位引脚没拉,折腾好几天。所以这篇笔记的定位很明确:把以太网外设从硬件连接到寄存器配置再到数据收发链路,一层一层拆开讲透。等你理解了 MAC 怎么通过 DMA 搬数据、PHY 怎么通过 SMI 配置、RMII 的 50MHz 时钟从哪来,再去接 LwIP 就是水到渠成的事。
下面我会按"整体设计思路 → 核心细节 → 实操配置 → 问题排查"这条线走,中间穿插我自己项目里真实踩过的坑和验证过的参数。所有代码和配置都基于 STM32F429IGT6 + LAN8720A 这个最常见的组合,你可以直接对照着改。
2. 以太网通信整体设计与方案选型拆解
2.1 为什么是 MAC + 外部 PHY 的架构
STM32F429 没有把 PHY 集成进去,这不是偷懒,而是成本和灵活性的权衡。PHY 涉及模拟电路,对工艺要求高,集成进 MCU 会推高成本和功耗。分离设计的好处是你可以根据项目需求选不同等级的 PHY:消费级用便宜的 LAN8720A,工业级用带宽温的 DP83848,需要光纤的还能换其他方案。MAC 部分负责数字逻辑,PHY 负责模拟信号和线路编码,两者通过标准接口通信,各司其职。
这个架构里,MAC 是大脑,PHY 是嘴巴和耳朵。MAC 按照 802.3 格式组装帧、计算 CRC、管理流控;PHY 负责把数字信号变成差分模拟信号发到网线上,同时把收到的信号还原成数字。两者之间的 RMII 接口就是"神经",传输数据和时钟。
理解这个分工很重要,因为调试时你要能判断问题出在哪一层。ping 不通可能是 MAC 配置错,也可能是 PHY 没握手,还可能是 RMII 时钟偏了。分层排查能省大量时间。
2.2 RMII 与 MII 的取舍:引脚、时钟与布线
MII 需要 16 根信号线,RMII 只要 9 根。对于 F429 这种 176 脚的片子,引脚虽然够,但 PCB 上少走 7 根线意味着更少的过孔、更短的走线、更好的信号完整性。RMII 的代价是时钟要求更严格:MII 的收发时钟是分开的(TX_CLK 25MHz、RX_CLK 25MHz),RMII 则统一用 50MHz 参考时钟,收发共用。
RMII 的 50MHz 时钟来源有两种:一是外部 PHY 提供,二是 MCU 自己产生。LAN8720A 可以从外部晶振产生 50MHz 输出给 MCU,也可以接收 MCU 提供的 50MHz。我一般推荐由 PHY 提供 50MHz 给 MCU,因为 PHY 那边通常已经有一个 25MHz 晶振,内部 PLL 倍频到 50MHz 很方便,而且这样 MCU 侧不用额外配置时钟输出,减少一个潜在故障点。
注意:RMII 的 50MHz 时钟必须非常干净,抖动要小。我遇到过因为时钟走线太长导致丢包的情况,后来把时钟线缩短并加串阻才稳定。这个后面排查章节会细说。
2.3 DMA 描述符机制:数据怎么从网线进到内存
以太网外设最核心也最容易让人懵的就是DMA 描述符。你可以把它理解成一串"快递单":每个描述符记录了一个数据缓冲区在哪、多大、属于哪个帧、有没有错误。MAC 收到数据后,DMA 自动把数据写进描述符指向的缓冲区,然后更新描述符状态,最后触发中断告诉 CPU"有活了"。
发送方向反过来:CPU 把要发的数据放进缓冲区,填好描述符,置位"拥有权归 DMA",DMA 就会自动把数据发出去。这个"拥有权"(OWN 位)是理解描述符的关键——OWN=1 表示 DMA 拥有,CPU 不能碰;OWN=0 表示 CPU 拥有,DMA 不能碰。搞错这个就会出现数据被覆盖或者发不出去的诡异问题。
F429 的以太网 DMA 支持环形描述符链,收发各一条链。每条链的描述符数量可以配置,我一般收发各用 4 到 8 个。太少容易丢包,太多浪费内存。这个数量要根据你的数据吞吐量和中断处理速度来权衡,后面实操部分会给具体计算。
3. 核心细节解析与实操要点
3.1 PHY 地址与 SMI 接口:怎么跟 PHY 对话
MAC 要配置 PHY,靠的是SMI(Station Management Interface),也就是 MDC 和 MDIO 两根线。MDC 是时钟,MDIO 是双向数据。通过 SMI,MAC 可以读写 PHY 内部的寄存器,比如控制寄存器、状态寄存器、PHY 标识寄存器。
每个 PHY 有一个地址,范围 0 到 31。LAN8720A 的地址由 PHYAD0 引脚决定:接地下拉就是地址 0,接上拉就是地址 1。很多开发板默认下拉,所以地址是 0。但如果你用的是自己画的板子,一定要确认这个引脚的状态,否则 MAC 找不到 PHY,表现为读 PHY ID 全是 0 或 0xFFFF。
读 PHY 标识寄存器(地址 2 和 3)是验证 SMI 通不通的最快方法。LAN8720A 的 ID 应该是 0x0007 和 0xC0F1(具体值看数据手册)。如果读出来不对,先查 MDC 时钟是否在 1.2 到 2.5MHz 之间,再查 MDIO 上拉电阻(通常 1.5k 到 10k),最后查 PHY 地址。
实操心得:MDC 时钟不能太快,LAN8720A 最高支持 2.5MHz。F429 的 MAC 时钟是 25MHz(RMII 模式下),通过分频得到 MDC。我一般设分频系数为 16,得到约 1.56MHz,稳定可靠。
3.2 RMII 引脚映射与 GPIO 配置
F429 的以太网引脚是固定的,不能随便映射。RMII 模式下需要配置的引脚包括:
| 信号 | 方向 | F429 引脚 | 说明 |
|---|---|---|---|
| REF_CLK | 输入 | PA1 | 50MHz 参考时钟 |
| MDIO | 双向 | PA2 | SMI 数据 |
| MDC | 输出 | PC1 | SMI 时钟 |
| CRS_DV | 输入 | PA7 | 载波侦听/数据有效 |
| RXD0 | 输入 | PC4 | 接收数据位0 |
| RXD1 | 输入 | PC5 | 接收数据位1 |
| TX_EN | 输出 | PB11 | 发送使能 |
| TXD0 | 输出 | PB12 | 发送数据位0 |
| TXD1 | 输出 | PB13 | 发送数据位1 |
这些引脚要配置成复用推挽输出或浮空输入,速度等级建议设为Very High,因为 RMII 是 50MHz 信号。GPIO 的复用功能编号要查数据手册的复用表,F429 上以太网用的是 AF11。
这里有个容易忽略的点:PA1 作为 REF_CLK 输入时,要配置成浮空输入或者复用输入,不能配成输出。我见过有人把它配成推挽输出,结果时钟冲突,PHY 直接不工作。
3.3 MAC 配置寄存器:帧过滤、双工与速率
MAC 有一堆配置寄存器,但实际项目里必须配的就那么几个。MACCR(MAC Configuration Register)是最关键的:
- RE 位:接收使能,必须置 1
- TE 位:发送使能,必须置 1
- DM 位:双工模式,1 为全双工,0 为半双工
- FES 位:速率,1 为 100Mbps,0 为 10Mbps
- BL 位:退避限制,半双工时用,全双工不用管
双工和速率最好跟 PHY 自协商结果保持一致。如果你强制 MAC 全双工 100M,但 PHY 协商成半双工 10M,就会出现大量冲突和丢包。我一般让 PHY 自协商,然后读 PHY 的状态寄存器,把结果同步到 MACCR。
MACFFR(Frame Filter Register)控制帧过滤。默认情况下,MAC 会接收所有广播帧和发给自己 MAC 地址的帧。如果你要做网络抓包或者特殊协议,可能需要打开混杂模式(PM 位),接收所有帧。但正常通信时不要开,会增加 CPU 负担。
3.4 DMA 描述符的初始化与对齐要求
描述符的结构在参考手册里有明确定义,每个描述符 16 字节,包含状态、控制、缓冲区地址、下一描述符地址等字段。初始化时要特别注意内存对齐:描述符必须 4 字节对齐,缓冲区建议 4 字节对齐(虽然不强制,但对齐后 DMA 效率更高)。
我一般这样组织内存:
// 描述符必须4字节对齐 __attribute__((aligned(4))) ETH_DMADescTypeDef DMARxDscrTab[ETH_RXBUFNB]; __attribute__((aligned(4))) ETH_DMADescTypeDef DMATxDscrTab[ETH_TXBUFNB]; // 缓冲区建议4字节对齐,大小要能容纳最大帧 __attribute__((aligned(4))) uint8_t Rx_Buff[ETH_RXBUFNB][ETH_MAX_PACKET_SIZE]; __attribute__((aligned(4))) uint8_t Tx_Buff[ETH_TXBUFNB][ETH_MAX_PACKET_SIZE];ETH_MAX_PACKET_SIZE通常定义为 1524 或 1536,要能容纳最大的以太网帧(1518 字节)加上可能的 VLAN 标签。ETH_RXBUFNB和ETH_TXBUFNB我一般设 4 到 8。
注意:描述符和缓冲区不要放在栈上,也不要用局部变量。它们必须在整个通信过程中持续有效,所以要用全局变量或静态分配。我见过有人把描述符定义在函数里,函数返回后内存被回收,DMA 直接跑飞。
3.5 中断配置与优先级分组
以太网外设有多个中断源:接收完成、发送完成、接收溢出、总线错误等。我一般只开接收中断和错误中断,发送用轮询或者发送完成中断。接收中断优先级要设得比较高,但不能高过系统关键中断(比如 SysTick)。
F429 的中断优先级分组建议用NVIC_PRIORITYGROUP_4,全部 4 位用于抢占优先级。以太网中断抢占优先级设 5 左右,既保证及时响应,又不会打断更关键的任务。
中断服务函数里要做的事很简单:检查中断标志,如果是接收中断,就通知上层有数据;如果是错误中断,记录错误类型并清除标志。不要在中断里做耗时操作,比如解析协议、拷贝大量数据,这些留给主循环或任务去做。
4. 实操过程与核心环节实现
4.1 硬件连接检查清单
在写代码之前,先确认硬件没问题。我整理了一个检查清单,每次新板子都过一遍:
- PHY 供电:LAN8720A 需要 3.3V,确认电压稳定,去耦电容到位
- 晶振:LAN8720A 需要 25MHz 晶振,确认起振(用示波器看波形)
- 复位引脚:PHY 的 nRST 引脚要由 MCU 控制或上电复位,确保 PHY 正常初始化
- RMII 连线:对照前面表格逐根检查,特别是 REF_CLK 和 CRS_DV
- 网口变压器:RJ45 和 PHY 之间要有网络变压器,不能直连
- PHY 地址引脚:确认 PHYAD0 状态,确定 PHY 地址
这一步看起来简单,但我统计过,至少三成的以太网问题出在硬件。软件调半天,最后发现是晶振没起振或者变压器虚焊,这种亏吃一次就够了。
4.2 时钟树配置:50MHz 从哪来
RMII 需要 50MHz 参考时钟。如果由 PHY 提供,MCU 侧要把 PA1 配置成浮空输入,同时使能以太网时钟。如果由 MCU 提供,需要配置 MCO 输出 50MHz,但这会占用一个引脚,而且 MCO 的时钟源要选 PLL。
我推荐 PHY 提供方案。LAN8720A 的 REF_CLK 引脚可以输出 50MHz,直接连到 PA1。这样 MCU 侧只需要在 RCC 里使能ETHMAC 时钟和ETHMACTX/RX 时钟,不用额外配置。
RCC 配置代码大概这样:
// 使能以太网时钟 RCC->AHB1ENR |= RCC_AHB1ENR_ETHMACEN; RCC->AHB1ENR |= RCC_AHB1ENR_ETHMACTXEN; RCC->AHB1ENR |= RCC_AHB1ENR_ETHMACRXEN; RCC->AHB1ENR |= RCC_AHB1ENR_ETHMACPTPEN; // 使能 GPIO 时钟 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;注意:ETHMACPTPEN 是时间戳时钟,如果不用 PTP 可以不使能,但使能了也不影响。
4.3 PHY 初始化与自协商流程
PHY 初始化分几步:软复位、等待复位完成、配置自协商、等待自协商完成、读取结果。
软复位是写BCR 寄存器(地址 0)的 bit15,然后轮询这个位直到清零。LAN8720A 复位时间大概几毫秒,不要用固定延时,要轮询。
自协商配置是写BCR 的 bit12(ANEN)和 bit9(RESTART_AN)。然后轮询BSR 寄存器(地址 1)的 bit5(ANCOMPLETE),直到置 1。自协商完成后,读BSR 的 bit2 和 bit3判断速率和双工,或者读 LAN8720A 的特殊状态寄存器(地址 31)获取更详细的信息。
// 软复位 PHY ETH_WritePHYRegister(PHY_ADDRESS, PHY_BCR, PHY_RESET); while (ETH_ReadPHYRegister(PHY_ADDRESS, PHY_BCR) & PHY_RESET); // 配置自协商 ETH_WritePHYRegister(PHY_ADDRESS, PHY_BCR, PHY_AUTONEGOTIATION); ETH_WritePHYRegister(PHY_ADDRESS, PHY_BCR, PHY_AUTONEGOTIATION | PHY_RESTART_AUTONEGOTIATION); // 等待自协商完成 while (!(ETH_ReadPHYRegister(PHY_ADDRESS, PHY_BSR) & PHY_AUTONEGO_COMPLETE)); // 读取结果 uint16_t phy_status = ETH_ReadPHYRegister(PHY_ADDRESS, PHY_BSR); uint16_t speed = (phy_status & PHY_SPEED_STATUS) ? 100 : 10; uint16_t duplex = (phy_status & PHY_DUPLEX_STATUS) ? FULL : HALF;这段代码里PHY_ADDRESS要根据实际硬件填 0 或 1。PHY_SPEED_STATUS和PHY_DUPLEX_STATUS是位掩码,具体值查 LAN8720A 数据手册。
4.4 MAC 与 DMA 初始化:寄存器级配置
MAC 初始化主要是配 MACCR、MACFFR、MACMIIAR 等寄存器。MACMIIAR 控制 SMI 时钟分频和读写操作,前面说的分频系数 16 就是配在这里。
DMA 初始化更复杂一些,要配DMABMR(总线模式寄存器)、DMATDLAR(发送描述符列表地址)、DMARDLAR(接收描述符列表地址)、DMAOMR(操作模式寄存器)等。
DMABMR 里要设AAB(地址对齐突发)、PBL(可编程突发长度)、RDP(接收描述符跳过长度)等。我一般用默认值,只改FB(固定突发)和PBL。PBL 设 32 比较通用。
DMAOMR 里要设SR(启动接收)、ST(启动发送)、OSF(操作模式存储转发)、TSF(发送存储转发)、RSF(接收存储转发)。存储转发模式能减少 FIFO 溢出,但会增加一点延迟。我一般全开。
// 配置 DMA 总线模式 ETH->DMABMR |= ETH_DMABMR_AAB | ETH_DMABMR_PBL_32Beat; // 配置发送和接收描述符列表地址 ETH->DMATDLAR = (uint32_t)DMATxDscrTab; ETH->DMARDLAR = (uint32_t)DMARxDscrTab; // 配置操作模式 ETH->DMAOMR |= ETH_DMAOMR_TSF | ETH_DMAOMR_RSF | ETH_DMAOMR_OSF; ETH->DMAOMR |= ETH_DMAOMR_ST | ETH_DMAOMR_SR;4.5 数据收发链路验证:从 ping 到实际通信
配置完成后,先别急着上 LwIP。我建议先写一个最简单的收发测试:构造一个 ARP 请求帧发出去,看能不能收到回复。如果能收到,说明 MAC、PHY、DMA 链路全通了。
构造 ARP 请求帧要注意:目标 MAC 是广播地址 FF:FF:FF:FF:FF:FF,源 MAC 是你自己设的,以太网类型是 0x0806,后面跟 ARP 报文。发送时把帧放进 Tx 缓冲区,填好描述符,置位 OWN,然后启动发送。
接收方向,轮询或者中断里检查 Rx 描述符的 OWN 位,如果变成 0 说明有数据。读出来解析,看是不是 ARP 回复。
这个测试能过,再上 LwIP 就稳了。如果过不了,按下面的排查表逐项检查。
4.6 关键参数计算:缓冲区数量与中断频率
缓冲区数量不是随便定的。假设你的应用每秒要处理 1000 个帧,每个帧平均 500 字节,那么每秒数据量是 500KB。如果每个缓冲区 1524 字节,4 个接收缓冲区就是 6096 字节。DMA 填满一个缓冲区的时间取决于网络速率:100Mbps 下,1524 字节的帧传输时间约 122 微秒。4 个缓冲区能缓冲约 488 微秒。如果你的中断处理能在 488 微秒内完成,就不会丢包。
中断频率也要算。如果每个帧都触发中断,1000 帧每秒就是 1000 次中断。F429 跑 180MHz,中断处理开销可以接受,但如果帧率更高,就要考虑中断合并或者轮询模式。
我一般用 4 个接收缓冲区,中断里只做标记,主循环处理。这样中断频率可控,处理也及时。
5. 常见问题与排查技巧实录
5.1 PHY 读不到 ID 或 ID 全 F
这是最常见的问题。现象是读 PHY 寄存器返回 0x0000 或 0xFFFF。排查顺序:
- PHY 供电:用万用表量 PHY 的 VDD 引脚,确认 3.3V
- 晶振:用示波器看 25MHz 晶振是否起振,幅度是否够
- 复位:确认 nRST 引脚在上电后已经拉高
- MDC/MDIO:确认 MDC 有波形,MDIO 有上拉
- PHY 地址:确认 PHYAD0 引脚状态,地址填对
我遇到过晶振起振但幅度只有 0.5V 的情况,PHY 工作不稳定,换晶振后正常。还有一次是 MDIO 上拉电阻忘了焊,读出来全是 0。
5.2 ping 不通但 PHY 已握手
PHY 自协商成功,但 ping 不通。可能原因:
- MAC 地址冲突:确认你的 MAC 地址在网络里唯一
- IP 配置错误:确认 IP、子网掩码、网关跟对端在同一网段
- 帧过滤太严:检查 MACFFR,确认没有误开过滤
- DMA 没启动:检查 DMAOMR 的 SR 和 ST 位
- 描述符 OWN 位错误:确认发送时置 1,接收时 DMA 会清 0
我遇到过一次是 MAC 地址设成了全 0,结果所有帧都被过滤掉了。改成合法地址后正常。
5.3 丢包严重或通信不稳定
丢包通常跟缓冲区、时钟、中断有关:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 大量丢包 | 接收缓冲区太少 | 增加到 8 个 |
| 偶发丢包 | 中断优先级太低 | 提高以太网中断优先级 |
| 高速率丢包 | DMA 突发长度太小 | PBL 设为 32 |
| 特定帧丢包 | 帧长度超过缓冲区 | 增大 ETH_MAX_PACKET_SIZE |
| 随机丢包 | 时钟抖动大 | 缩短 REF_CLK 走线,加串阻 |
我有个项目在 100Mbps 满速时丢包,后来把接收缓冲区从 4 增加到 8,并把中断优先级从 6 提到 5,问题解决。
5.4 发送卡死或描述符跑飞
发送卡死通常是描述符 OWN 位没置对,或者描述符链断了。检查:
- 发送描述符的 OWN 位是否置 1
- 描述符的下一描述符地址是否正确
- 发送缓冲区地址是否有效
- DMA 的 ST 位是否置 1
我见过有人把描述符定义在栈上,函数返回后内存被覆盖,DMA 读到垃圾数据直接跑飞。改成全局变量后正常。
5.5 常见问题速查表
| 问题 | 排查第一步 | 常见根因 |
|---|---|---|
| PHY ID 读不到 | 量 PHY 供电 | 供电/晶振/复位 |
| ping 不通 | 查 MAC 地址 | 地址冲突/过滤 |
| 丢包 | 查缓冲区数量 | 缓冲区不足 |
| 发送失败 | 查 OWN 位 | 描述符配置错 |
| 通信不稳定 | 查 REF_CLK | 时钟抖动 |
| 中断不触发 | 查 NVIC 配置 | 优先级/使能 |
独家避坑技巧:每次改完硬件或软件,先用回环测试验证 MAC 和 DMA。把 MACCR 的 LM(回环模式)位置 1,发送的帧会直接回到接收端,不经过 PHY。这样能快速判断问题在 MAC 侧还是 PHY 侧。这个技巧帮我省了无数次来回换板子的时间。
5.6 调试工具与手段
调试以太网,光靠串口打印不够。我常用的工具:
- 示波器:看 REF_CLK、MDC、MDIO 波形,判断时钟和通信是否正常
- 逻辑分析仪:抓 RMII 数据线,分析帧内容
- 网络抓包工具:在 PC 侧抓包,看帧有没有发出来、格式对不对
- PHY 寄存器读取:通过 SMI 读 PHY 内部寄存器,判断链路状态
我一般先用示波器确认时钟,再用 PHY 寄存器确认链路,最后用抓包确认数据。这三步能定位九成以上的问题。
5.7 从以太网外设到 LwIP 的衔接要点
以太网外设调通后,接 LwIP 就简单了。LwIP 需要一个网络接口结构体,里面包含发送函数和接收函数。发送函数就是把你构造好的 pbuf 数据通过以太网 DMA 发出去;接收函数就是以太网中断里把数据交给 LwIP 处理。
关键点:
- pbuf 和 DMA 缓冲区的拷贝:LwIP 的 pbuf 和 DMA 缓冲区是两块内存,接收时要拷贝,发送时也要拷贝。拷贝有开销,但安全。
- 中断与主循环的分工:中断里只标记,主循环里调用 LwIP 的
ethernetif_input处理。 - PHY 状态变化处理:如果网线拔了又插,PHY 会重新自协商,LwIP 要能感知链路状态变化。
我在实际项目里,以太网外设初始化大概占整个网络调试时间的六成,剩下四成是 LwIP 配置和协议调试。把底层吃透,上层就是搭积木。
最后分享一个小技巧:如果你用的是 LAN8720A,它的特殊模式寄存器(地址 31)能读出很多有用信息,包括链路是否建立、速率、双工、甚至线序。调试时把这个寄存器读出来打印,比只看 BSR 直观得多。这个寄存器在数据手册里叫 "Special Modes Register",值得花十分钟研究一下。