1. 项目缘起与整体方案拆解
GD32F4 系列作为国产 Cortex-M4 阵营里性价比相当能打的一颗料,主频能跑到 200MHz,自带以太网 MAC 控制器,做工业网关、数据采集器、边缘计算节点这类带网口的设备非常合适。但真正上手做项目的时候,很多人会卡在同一个地方:官方例程用的是自家的 PHY 或者常见的 LAN8720,而手头板子上焊的偏偏是 YT8512 这颗国产 PHY。这时候就面临一个很现实的问题——驱动怎么移植,RMII 模式下的时钟和引脚怎么配,FreeRTOS 下收发数据怎么不丢包。
我这次做的项目就是基于 GD32F427 加 YT8512,跑 FreeRTOS,实现一个稳定的以太网通信节点。整个方案的核心思路其实不复杂:GD32F4 内部集成了以太网 MAC(媒体访问控制层),它负责数据链路层的帧组装、CRC 校验、地址过滤这些活;而 PHY(物理层芯片)负责把 MAC 送来的数字信号变成差分信号发到网线上,同时把网线上的信号还原成数字信号送回 MAC。两者之间通过 RMII 接口连接,MAC 和 PHY 之间再通过 SMI(站管理接口,本质就是 MDIO 和 MDC 两根线)来读写 PHY 的寄存器,完成协商、状态查询、速率双工配置这些操作。
为什么选 RMII 而不是 MII?这是第一个要讲清楚的选择。MII 接口需要 16 根数据和控制线,RMII 精简到 7 根(两根时钟、两根发送数据、两根接收数据、一根 CRS_DV),引脚占用少了一大半。代价是 RMII 的参考时钟必须是 50MHz,而 MII 是 25MHz。对于 GD32F427 这种引脚资源紧张、又要跑 FreeRTOS 加一堆外设的场景,RMII 几乎是唯一合理的选择。YT8512 支持 RMII 模式,而且它有一个很实用的特性——可以输出 50MHz 参考时钟给 MAC 用,这样整个系统只需要一个晶振就能同时喂饱 PHY 和 MAC,省料省事。
FreeRTOS 在这个项目里的角色是任务调度和资源管理。以太网收发如果裸机轮询,CPU 会被大量占用,响应其他任务就慢了。用 FreeRTOS 之后,可以把网络接收放到一个独立任务里阻塞等待信号量,发送放到另一个任务,主控任务该干嘛干嘛。但这里有个坑:FreeRTOS 的中断优先级配置和以太网中断的优先级必须匹配,否则会出现中断里调用 FreeRTOS API 导致断言失败的问题。这个后面会详细说。
整个方案适合谁参考?如果你手上有 GD32F4 的板子,焊了 YT8512 或者类似的国产 PHY,想跑 FreeRTOS 做网络通信,那这篇内容基本可以照着抄。如果你用的是 STM32F407 加 LAN8720 或者 83848,接口逻辑是相通的,RMII 的配置思路和 PHY 寄存器操作也大同小异,同样有参考价值。甚至你只是想知道 RMII 接口到底怎么接线、50MHz 时钟从哪来、FreeRTOS 下网络任务怎么划分,这里都有答案。
2. 硬件连接与 RMII 接口配置细节
2.1 RMII 接口的引脚映射与时钟来源
RMII 接口的信号线不多,但每一根都不能接错。先把 GD32F427 这边以太网 MAC 的 RMII 引脚列出来,再对照 YT8512 的引脚定义,一根一根对。
GD32F4 的以太网 RMII 模式用到以下引脚(以常见封装为例):
| 信号名 | GD32F4 引脚 | 方向 | 说明 |
|---|---|---|---|
| REF_CLK | PA1 | 输入/输出 | 50MHz 参考时钟 |
| MDIO | PA2 | 双向 | 站管理数据 |
| MDC | PC1 | 输出 | 站管理时钟 |
| CRS_DV | PA7 | 输入 | 载波侦听/数据有效 |
| RXD0 | PC4 | 输入 | 接收数据位0 |
| RXD1 | PC5 | 输入 | 接收数据位1 |
| TX_EN | PB11 | 输出 | 发送使能 |
| TXD0 | PB12 | 输出 | 发送数据位0 |
| TXD1 | PB13 | 输出 | 发送数据位1 |
这里最关键的是 REF_CLK 这根线。RMII 协议规定 MAC 和 PHY 必须共用同一个 50MHz 参考时钟。有两种方案:第一种是外部放一个 50MHz 有源晶振,同时接到 MAC 和 PHY 的 REF_CLK 引脚;第二种是 PHY 外接 25MHz 晶振,内部 PLL 倍频到 50MHz,然后从 PHY 的 REF_CLK 引脚输出给 MAC。YT8512 支持第二种方案,这也是我推荐的做法,因为省一个晶振,而且 PHY 输出的时钟和 PHY 内部工作时钟同源,相位关系更稳定。
具体到 YT8512 的电路连接,几个要点必须注意。YT8512 的 XTAL1 和 XTAL2 之间接 25MHz 无源晶振,负载电容根据晶振规格选,一般 12pF 到 20pF 之间。PHY 的 REF_CLK 输出引脚接到 GD32F4 的 PA1。这里有个细节:GD32F4 的 PA1 在 RMII 模式下是复用功能,需要配置成对应的 AF 模式,而且要注意它的输入输出方向——当 PHY 提供时钟时,PA1 配置为输入模式,接收 PHY 送来的 50MHz 时钟。
MDIO 和 MDC 这两根线需要上拉。MDIO 是双向线,通常接一个 1.5k 到 10k 的上拉电阻到 3.3V,具体阻值看 PHY 手册推荐。MDC 是主机输出,一般不需要上拉,但有些设计为了信号完整性会加一个弱上拉。MDC 的频率不能超过 PHY 支持的最大值,YT8512 的 MDC 最高支持 25MHz,但实际用的时候建议分频到 2.5MHz 左右就够了,太快没必要,还容易受干扰。
2.2 YT8512 的地址配置与复位电路
YT8512 的 SMI 地址由几个配置引脚在上电复位时锁存决定。具体是哪几个引脚、怎么组合,得看 YT8512 的数据手册。一般来说,PHY 地址范围是 0 到 31,GD32F4 的 MAC 支持通过 MDIO 访问 32 个 PHY 地址。如果板子上只挂了一颗 PHY,地址通常配成 0 或者 1。配置引脚一般内部有弱上拉或弱下拉,但为了可靠,建议外部加明确的上下拉电阻,避免上电时引脚悬空导致地址锁存错误。
复位电路这块,YT8512 有一个 RST_N 引脚,低电平复位。标准做法是用一个 RC 电路:RST_N 接一个 10k 上拉电阻到 3.3V,同时接一个 100nF 电容到地。上电时电容充电,RST_N 保持低电平一段时间,保证 PHY 完成内部初始化。这个时间不能太短,YT8512 手册里一般要求复位低电平至少保持 10ms 以上。如果 MCU 有多余的 GPIO,也可以直接用 GPIO 控制复位,这样软件可以主动复位 PHY,调试的时候很方便。我这次就是用了一个 GPIO 来控制 RST_N,上电后先拉低 20ms 再拉高,确保 PHY 状态干净。
还有一点容易被忽略:YT8512 的某些配置引脚在复位释放的瞬间会被采样,用来决定 PHY 的工作模式(比如 RMII 还是 MII、全双工还是半双工、是否开启自动协商)。这些引脚在复位期间必须保持稳定,不能有毛刺。如果板子上这些引脚还接了其他外设,要确保上电过程中不会误触发。我踩过一次坑:某个配置引脚复用了 LED 驱动,上电时 LED 的驱动电路把引脚拉低了,导致 PHY 进入了错误的模式,后来把 LED 驱动改到别的引脚才解决。
2.3 GD32F4 以太网 MAC 的时钟配置
GD32F4 的以太网 MAC 需要两个时钟:一个是 RMII 参考时钟(50MHz),从 PA1 输入;另一个是 MAC 自身的时钟,通常由系统时钟经过分频得到。在 GD32F4 的时钟树里,以太网 MAC 的时钟源可以选择 HCLK 或者 PLL 输出,具体看参考手册的时钟树章节。
RMII 模式下,MAC 的 TX 和 RX 逻辑都依赖 50MHz 参考时钟。GD32F4 的以太网外设有一个 ETH_RMII_REF_CLK 的选择位,需要配置成从外部引脚输入。这个配置在以太网 MAC 的控制寄存器里,具体是哪个位、怎么设置,后面代码部分会讲。
系统时钟这边,GD32F427 跑 200MHz 的时候,HCLK 是 200MHz,以太网 MAC 时钟如果从 HCLK 分频,需要确保分频后的频率在 MAC 支持范围内。GD32F4 的以太网 MAC 时钟最高可以到 150MHz 左右,但实际用的时候一般分到 100MHz 或者 50MHz 就够了。分频系数在 RCU 模块里配置,具体寄存器是 RCU_CFG0 或者 RCU_CFG1 里的 ETH 时钟选择位。
这里有个实操经验:如果 RMII 参考时钟不稳定或者频率不对,MAC 初始化会卡在等待 PHY 协商完成的循环里,或者能初始化但收发数据全是错的。用示波器量 PA1 上的波形,确认是干净的 50MHz 方波,幅度在 3.3V 左右。如果波形幅度不够或者频率偏差大,先查 PHY 的 25MHz 晶振是否起振,再查 PHY 的 PLL 配置是否正确。
3. YT8512 驱动移植与 PHY 寄存器操作
3.1 PHY 标准寄存器与 YT8512 扩展寄存器
YT8512 的寄存器分为两块:一块是 IEEE 802.3 标准定义的通用寄存器,地址 0 到 15;另一块是厂商自定义的扩展寄存器,地址 16 以上。标准寄存器里最常用的几个:
| 寄存器地址 | 名称 | 关键位 | 说明 |
|---|---|---|---|
| 0x00 | BMCR | bit15 复位, bit12 自动协商使能, bit8 双工模式 | 基本控制 |
| 0x01 | BMSR | bit5 自动协商完成, bit2 链路状态 | 基本状态 |
| 0x02 | PHYID1 | 高16位ID | 厂商ID高字节 |
| 0x03 | PHYID2 | 低16位ID | 厂商ID低字节 |
| 0x04 | ANAR | 自动协商通告 | 本端能力 |
| 0x05 | ANLPAR | 链路伙伴能力 | 对端能力 |
YT8512 的 PHY ID 可以通过读寄存器 2 和 3 得到。YT8512 的 ID 一般是 0x0000 开头或者 0x4c00 开头,具体值查手册。读 ID 是验证 SMI 通信是否正常的第一步——如果读出来的 ID 全是 0xFFFF 或者 0x0000,说明 MDIO/MDC 时序有问题,或者 PHY 地址配错了。
YT8512 的扩展寄存器里有一些很实用的功能,比如:
- 寄存器 0x11:PHY 特定状态,可以读到当前协商的速率和双工模式
- 寄存器 0x1E:一些测试和配置位
- 寄存器 0x1F:RMII 模式配置,有些 PHY 需要在这里设置 RMII 还是 MII
具体到 YT8512,它的 RMII 模式通常是通过硬件配置引脚决定的,不需要软件写寄存器。但有些批次的芯片可能需要软件确认一下。我建议在初始化的时候读一下相关的配置寄存器,确认 RMII 模式已经生效。
3.2 SMI 读写时序与代码实现
SMI 接口的时序不复杂,但时序参数必须满足 PHY 的要求。MDC 是时钟,MDIO 是数据。写操作时,MAC 在 MDC 上升沿之前把数据放到 MDIO 上,PHY 在 MDC 上升沿采样;读操作时,PHY 在 MDC 上升沿之前把数据放到 MDIO 上,MAC 在上升沿采样。
GD32F4 的以太网 MAC 硬件自动处理 SMI 时序,软件只需要配置好 MDC 时钟分频,然后往 MAC 的 SMI 数据寄存器和地址寄存器写值,再触发读写操作。GD32F4 的 SMI 时钟分频在 MACMIIAR 寄存器里配置,分频系数从 HCLK 分频得到 MDC。假设 HCLK 是 200MHz,MDC 目标频率是 2.5MHz,分频系数就是 200/2.5/2 = 40,实际配置的时候要查手册确认分频公式。
下面是一段 SMI 读写的核心代码逻辑,基于 GD32 的标准外设库风格:
#define PHY_ADDR 0x00 uint16_t phy_read(uint16_t reg) { uint32_t timeout = 0; /* 等待 SMI 空闲 */ while (ETH_MAC->MACMIIAR & MACMIIAR_MB) { if (++timeout > 100000) return 0xFFFF; } /* 配置 PHY 地址、寄存器地址、读操作、时钟分频 */ ETH_MAC->MACMIIAR = (PHY_ADDR << 11) | (reg << 6) | MACMIIAR_CR_DIV42 | MACMIIAR_MB; timeout = 0; while (ETH_MAC->MACMIIAR & MACMIIAR_MB) { if (++timeout > 100000) return 0xFFFF; } return (uint16_t)(ETH_MAC->MACMIIDR & 0xFFFF); } void phy_write(uint16_t reg, uint16_t data) { uint32_t timeout = 0; while (ETH_MAC->MACMIIAR & MACMIIAR_MB) { if (++timeout > 100000) return; } ETH_MAC->MACMIIDR = data; ETH_MAC->MACMIIAR = (PHY_ADDR << 11) | (reg << 6) | MACMIIAR_CR_DIV42 | MACMIIAR_MW | MACMIIAR_MB; timeout = 0; while (ETH_MAC->MACMIIAR & MACMIIAR_MB) { if (++timeout > 100000) return; } }这段代码里加了超时保护,这是实际项目里必须的。如果 PHY 没焊好或者 MDIO 线断了,没有超时的话程序会死循环在这里,整个系统就卡死了。超时值设 100000 次循环,在 200MHz 主频下大概几毫秒,足够完成一次 SMI 操作。
3.3 PHY 初始化流程与自动协商
PHY 初始化的标准流程是这样的:
- 硬件复位 PHY(拉低 RST_N 至少 10ms)
- 读 PHY ID,确认 SMI 通信正常
- 软复位 PHY(写 BMCR 的 bit15)
- 等待软复位完成(读 BMCR,bit15 自动清零)
- 配置自动协商通告寄存器(ANAR),设置支持的速率和双工模式
- 启动自动协商(写 BMCR 的 bit12 和 bit9)
- 等待自动协商完成(轮询 BMSR 的 bit5)
- 读 PHY 特定状态寄存器,确认最终协商结果
- 根据协商结果配置 MAC 的速率和双工模式
YT8512 支持 10M/100M 自适应,支持全双工和半双工。在 RMII 模式下,100M 全双工是最常见的配置。自动协商通告寄存器里,bit8 和 bit7 分别对应 100M 全双工和 100M 半双工,bit6 和 bit5 对应 10M 全双工和 10M 半双工。一般把 100M 全双工和 10M 全双工都置上,让 PHY 和对端协商出最佳结果。
这里有个实操细节:自动协商完成之后,不要只看 BMSR 的 bit5,还要读一下 PHY 特定状态寄存器(YT8512 是 0x11),确认实际协商的速率和双工。有些交换机或者路由器的自动协商行为不太标准,BMSR 显示协商完成,但实际链路是半双工或者 10M,如果不查这个寄存器,MAC 配置成 100M 全双工,链路就会大量丢包。
还有一个坑:GD32F4 的 MAC 在初始化的时候,如果 PHY 还没协商完成,MAC 的发送和接收使能位不要急着打开。正确的顺序是先初始化 MAC 的基本配置(地址、过滤模式等),然后等 PHY 协商完成,再根据协商结果配置 MAC 的速率和双工,最后使能收发。顺序反了的话,MAC 可能以错误的速率工作,导致链路不通。
4. FreeRTOS 下的网络任务设计与中断配置
4.1 以太网中断优先级与 FreeRTOS 的兼容性
FreeRTOS 在 Cortex-M4 上运行时,会配置一个最低中断优先级阈值,通常是 configMAX_SYSCALL_INTERRUPT_PRIORITY。只有优先级数值大于等于这个阈值的中断,才允许在中断服务函数里调用 FreeRTOS 的 API(比如 xSemaphoreGiveFromISR)。优先级数值小于这个阈值的中断,FreeRTOS 内核不会屏蔽它们,但它们也不能调用 FreeRTOS API。
GD32F4 的中断优先级寄存器是 4 位有效,数值越小优先级越高。FreeRTOS 默认把 configMAX_SYSCALL_INTERRUPT_PRIORITY 设成 5 或者 6(具体看配置)。以太网中断的优先级必须设置成大于等于这个值,比如设成 6 或者 7,这样才能在以太网中断里安全地给信号量。
我见过有人把以太网中断优先级设成 0 或者 1,然后在中断里调用 xSemaphoreGiveFromISR,结果 FreeRTOS 直接触发断言,程序卡在 vAssertCalled 里。原因就是优先级太高,FreeRTOS 认为这个中断不受它管理,不允许调用它的 API。
具体配置的时候,在 FreeRTOSConfig.h 里确认这两个宏:
#define configPRIO_BITS 4 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configMAX_SYSCALL_INTERRUPT_PRIORITY \ (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))然后在以太网中断初始化的时候,把抢占优先级设成 6 或者 7:
nvic_irq_enable(ENET_IRQn, 6, 0);这样以太网中断就能安全地调用 FreeRTOS 的 FromISR 系列 API 了。
4.2 网络接收任务与信号量同步
FreeRTOS 下以太网接收的典型架构是这样的:以太网中断服务函数里,当收到一帧数据时,给一个二值信号量或者任务通知;接收任务阻塞等待这个信号量,一旦等到就去读取 MAC 的接收 FIFO,处理数据。
为什么用二值信号量而不是队列?因为以太网接收是“有数据就处理”的模式,不需要缓存多帧。二值信号量足够表达“有数据待处理”这个状态。如果数据量大、需要缓冲多帧,可以用计数信号量或者消息队列,但那样内存开销更大。
接收任务的伪代码:
void eth_rx_task(void *param) { while (1) { if (xSemaphoreTake(eth_rx_sem, portMAX_DELAY) == pdTRUE) { /* 读取 MAC 接收 FIFO */ uint32_t len = eth_read_frame(rx_buf, sizeof(rx_buf)); if (len > 0) { /* 处理数据帧 */ process_frame(rx_buf, len); } /* 重新使能接收中断 */ eth_rx_enable(); } } }这里有个关键点:GD32F4 的以太网 MAC 在收到一帧数据后,如果不及时读取 FIFO,后续的数据帧会被丢弃。所以中断里给完信号量之后,要尽快让接收任务运行。如果系统里还有其他高优先级任务,接收任务的优先级要设得足够高,避免数据丢失。
另外,MAC 的接收中断有两种模式:一种是每收到一帧就中断一次,另一种是接收 FIFO 达到一定阈值才中断。前者实时性好但中断频繁,后者中断少但延迟大。实际项目里,如果网络流量不大,用每帧中断就行;如果流量大,建议用 FIFO 阈值中断,减少中断次数。
4.3 发送任务与零拷贝优化
发送这边相对简单,但有一个性能优化点值得说。GD32F4 的 MAC 发送 FIFO 支持两种模式:一种是软件把数据拷到 MAC 的发送 FIFO 里,另一种是 DMA 模式,MAC 直接从内存读数据。DMA 模式就是所谓的“零拷贝”,数据不需要在内存和 FIFO 之间搬来搬去,CPU 占用低,吞吐量高。
在 FreeRTOS 下用 DMA 发送,需要注意内存对齐和缓存一致性。如果开了 D-Cache,DMA 读的内存区域必须保证 Cache 里的数据和内存里的一致,否则 DMA 可能读到旧数据。GD32F4 的 Cortex-M4 有 Cache,处理方法是发送前对数据缓冲区做 Clean 操作,接收后对缓冲区做 Invalidate 操作。
发送任务的伪代码:
void eth_tx_task(void *param) { while (1) { if (xQueueReceive(eth_tx_queue, &tx_msg, portMAX_DELAY) == pdTRUE) { /* 等待 MAC 发送 FIFO 有空闲 */ while (!eth_tx_ready()); /* 把数据写入发送 FIFO 或配置 DMA 描述符 */ eth_write_frame(tx_msg.buf, tx_msg.len); /* 启动发送 */ eth_tx_start(); } } }发送队列用 FreeRTOS 的消息队列,队列项里存数据指针和长度。这样发送任务和产生数据的任务解耦,产生数据的任务只管往队列里丢,发送任务负责实际发送。队列深度根据实际流量定,一般 4 到 8 就够了。
4.4 中断服务函数里的注意事项
以太网中断服务函数里要做的事情尽量少。标准做法是:
- 读 MAC 的中断状态寄存器,判断是接收中断还是发送中断
- 如果是接收中断,清除中断标志,给接收信号量
- 如果是发送中断,清除中断标志,给发送信号量或者直接处理
- 如果是错误中断,记录错误计数,必要时复位 MAC
不要在中断里做数据拷贝、协议解析这些耗时操作。中断里只做状态判断和信号量通知,具体处理交给任务。
还有一个细节:GD32F4 的以太网中断标志清除有顺序要求。有些标志必须先读某个寄存器再写另一个寄存器才能清除,顺序错了标志清不掉,中断会反复触发。具体顺序查 GD32F4 的用户手册以太网章节。
5. 调试过程中踩过的坑与排查方法
5.1 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| PHY ID 读出来是 0xFFFF | MDIO/MDC 接线错误、PHY 地址不对、PHY 未复位 | 示波器看 MDC 有无波形,检查 PHY 地址配置引脚 |
| PHY ID 读出来是 0x0000 | MDIO 被拉低、PHY 供电异常 | 量 MDIO 电压,检查 PHY 电源 |
| 自动协商一直不完成 | 网线没插、对端设备未上电、PHY 晶振未起振 | 换网线,量晶振波形,读 BMSR 看链路状态 |
| 协商完成但 ping 不通 | MAC 速率双工配置错误、IP 地址冲突 | 读 PHY 特定状态寄存器确认协商结果,检查 MAC 配置 |
| 能 ping 通但大量丢包 | 中断优先级配置错误、接收 FIFO 溢出 | 检查 FreeRTOS 中断优先级,降低网络流量测试 |
| 程序卡在 PHY 初始化 | SMI 读写没有超时保护、PHY 硬件故障 | 加超时,换 PHY 芯片测试 |
| 发送正常接收异常 | RMII 接收时钟相位问题、CRS_DV 接线错误 | 量 REF_CLK 波形,检查 CRS_DV 连接 |
5.2 几个印象深刻的调试案例
第一个坑是 JTAG 引脚冲突。GD32F4 的 PA13、PA14、PA15、PB3、PB4 这些引脚默认是 JTAG 功能。如果以太网 RMII 用到了其中某些引脚(比如 PB11、PB12、PB13 虽然不在 JTAG 默认引脚里,但有些封装会复用),或者板子上其他外设用了这些引脚,就需要先关闭 JTAG,保留 SWD。关闭 JTAG 的代码:
rcu_periph_clock_enable(RCU_AF); gpio_pin_remap_config(GPIO_SWJ_SWDPENABLE_REMAP, ENABLE);这行代码把 JTAG 禁用,只保留 SWD 调试接口。如果不做这一步,那些被 JTAG 占用的引脚没法用作普通 GPIO 或者复用功能,以太网引脚配置会失败。
第二个坑是 RMII 参考时钟的相位。YT8512 输出的 50MHz 时钟和 GD32F4 MAC 期望的时钟相位可能有偏差。表现是链路能建立,但收发数据 CRC 错误率高。解决方法是调整 PHY 的时钟输出相位配置,YT8512 有相关的配置寄存器可以微调输出时钟的相位。或者检查 PCB 走线,REF_CLK 的走线尽量短,避免过孔和分支。
第三个坑是 FreeRTOS 堆栈溢出。网络任务里用了比较大的局部数组来存数据帧,比如uint8_t rx_buf[1524],如果任务堆栈设小了,直接溢出,程序跑飞。排查方法是开启 FreeRTOS 的堆栈溢出检测:
#define configCHECK_FOR_STACK_OVERFLOW 2然后在 vApplicationStackOverflowHook 里打印出问题的任务名。网络任务的堆栈建议至少 1024 字(不是字节,是 StackType_t 的个数),如果协议栈复杂,给到 2048 字。
第四个坑是 PHY 复位不彻底。有些板子上 PHY 的复位电路只有 RC,没有 GPIO 控制。上电时如果电源上升慢,RC 复位时间可能不够,PHY 内部状态机没初始化好。表现是偶尔能工作,偶尔不行。解决方法是加一个 GPIO 控制复位,或者加大复位电容。我后来改成 GPIO 控制,上电后主动拉低 50ms,问题彻底解决。
5.3 实测性能数据
在 GD32F427 跑 200MHz、FreeRTOS 跑 1000Hz tick、YT8512 协商成 100M 全双工的条件下,实测 TCP 吞吐量大概在 60 到 80Mbps 之间。UDP 吞吐量稍高,能到 90Mbps 左右。这个性能对于大多数工业数据采集和网关应用足够了。
如果要做性能优化,几个方向:一是用 DMA 收发代替 FIFO 拷贝,减少 CPU 占用;二是增大 FreeRTOS 的 tick 频率到 1000Hz 以上,减少任务切换延迟;三是把网络任务的优先级设到合适的位置,既不能太低导致丢包,也不能太高影响其他关键任务。
CPU 占用方面,满速收发的时候,网络任务大概占用 30% 到 40% 的 CPU。如果加上协议栈处理(比如 LwIP),占用会更高。所以如果系统里还有其他重负载任务,网络性能需要适当妥协,或者换更高主频的芯片。
6. 移植过程中的经验总结与扩展思路
YT8512 在 GD32F4 上的移植,核心就三件事:RMII 引脚接对、SMI 时序配对、FreeRTOS 中断优先级设对。这三件事任何一件出问题,表现都是“网络不通”,但排查方向完全不同。我的经验是,先量 REF_CLK 有没有 50MHz,再读 PHY ID 确认 SMI 通不通,最后看自动协商结果和 MAC 配置是否匹配。按这个顺序排查,基本能覆盖 90% 的问题。
代码层面,PHY 驱动其实可以做成一个通用的框架。把 SMI 读写、PHY ID 识别、自动协商、链路状态查询这些操作抽象成函数指针,针对不同的 PHY 芯片只需要实现差异部分。这样以后换 PHY 芯片,比如换成 LAN8720 或者 83848,只需要改少量代码。我在这个项目里就是这么做的,YT8512 的驱动和 LAN8720 的驱动共用同一套上层逻辑,切换的时候只改 PHY 地址和几个特定寄存器的定义。
后续如果要扩展,几个方向比较实用。一是加上 LwIP 协议栈,把裸帧收发升级成 TCP/UDP 通信,这样能直接对接上层应用。LwIP 在 FreeRTOS 下的移植资料很多,关键是配置好内存池大小和邮箱数量。二是加上网络状态监控,定期读 PHY 的链路状态寄存器,链路断了自动重连,并且通过 LED 或者日志输出状态。三是做固件远程升级,通过以太网接收新固件写入 Flash,这个在工业现场非常实用。
最后分享一个调试小技巧:如果手头没有示波器,可以用 GD32F4 的定时器输入捕获功能来测 REF_CLK 的频率。把 PA1 配置成定时器输入捕获,测出来的频率如果是 50MHz 左右,说明时钟基本正常。虽然精度不如示波器,但至少能判断有没有时钟、频率对不对。这个方法在没有仪器的紧急情况下很管用。