第46章:MDIO接口读写测试实验
从这一章开始,我们进入以太网实战系列。第一个实验不着急收发网络数据,而是先搞定一个更基础的问题:FPGA 怎么配置和"询问"以太网 PHY 芯片? 答案就是本章的主角——MDIO 接口。
本章在讲什么?(先看这个)
本章要做的事情很单纯:
- 弄懂 MDIO 接口是什么、读写时序长什么样;
- 认识开发板上的千兆 PHY 芯片 YT8531,知道它的 PHY 地址和几个关键寄存器;
- 用 Verilog 写一个 MDIO 驱动模块 + 控制模块,实现:触摸按键触发软复位、LED 灯实时指示网口协商速率。
最终效果:开发板连上网线后,LED0 亮 = 10Mbps,LED1 亮 = 100Mbps,两个都亮 = 1000Mbps,两个都灭 = 自协商失败。
核心概念拆解
46.1 以太网与 PHY 芯片:先建立大局观
以太网(Ethernet)是局域网里应用最广的通信协议标准,最早由 Xerox 创建,后来被 IEEE 采纳为 802.3 标准。按速率分为标准以太网(10M)、快速以太网(100M)、千兆以太网(1000M),如今万兆也已普及。
我们开发板用的是最常见的 RJ45 接口(俗称"水晶头")。有个细节值得注意:
- 10M/100M 模式:只用到 1、2、3、6 四根线(1/2 发送,3/6 接收);
- 1000M 模式:8 根线全部用上,且都是双向的。

从硬件角度看,以太网接口电路由两大块构成:
- MAC(媒体访问控制)控制器:本实验中由 FPGA 实现;
- PHY(物理层)芯片:板载的 YT8531/YT8511。
PHY 的作用可以打个比方:MAC 交给他的是"平行数据"(不 管 帧 格 式),PHY 负责把并行数据转成串行流,再按物理层编码规则变成模拟信号从网线发出去;接收时反着来。另外,网线一插上,PHY 会和对端设备互相发脉冲"聊天",协商出双方都支持的最高速率和双工模式——这就是自协商(Auto Negotiation)。
46.2 MDIO 接口:MAC 和 PHY 之间的"对讲机"
PHY 芯片内部有一堆寄存器,用来配置工作模式、查询连接状态。FPGA 读写这些寄存器的通道,就是 MDIO 接口(也叫 SMI 接口,串行管理接口)。

它只有两根线:
| 信号 | 作用 | 关键约束 |
|---|---|---|
| ETH_MDC | 管理时钟,由 MAC 提供 | 最高 12.5MHz |
| ETH_MDIO | 双向数据线 | 空闲时靠外部上拉电阻保持高电平 |
46.3 MDIO 通信协议:一帧数据的"解剖"
MDIO 每次读写的帧结构如下:

| 字段 | 位数 | 说明 |
|---|---|---|
| Preamble | 32 | 前导码,32 位逻辑"1",用于同步 PHY |
| ST | 2 | 帧开始信号,固定 01 |
| OP | 2 | 操作码:读=10,写=01 |
| PHYAD | 5 | PHY 芯片地址(一个 MAC 可挂多个 PHY) |
| REGAD | 5 | 寄存器地址(最多 32 个寄存器) |
| TA | 2 | 转向位,读操作时数据线"换主人" |
| DATA | 16 | 读出或写入的数据 |
最需要理解的字段是 TA(Turnaround):
- 写操作:数据线方向不变,MAC 固定输出
2'b10,随后继续写数据; - 读操作:第一个 TA 位 MDIO 置为高阻,第二个 TA 位由 PHY 拉低表示"我听到了,准备发数据"。如果第二位是高电平,说明 PHY 没应答——大概率是 PHY 地址错了或者时序有问题。
还有一条贯穿全程的采样规则:PHY 在 MDC 上升沿采样,所以 MAC 要在 MDC 下降沿更新 MDIO 数据;读操作时反过来,MAC 在上升沿采样 PHY 输出的数据。


46.4 YT8531 芯片:PHY 地址与关键寄存器
PHY 地址由 PHYAD[2:0] 三个引脚的上下拉决定。开发板上只有 PHYAD2 接上拉,PHYAD1/PHYAD0 接下拉,所以地址是 5'h04。

复位有两种方式:
- 硬件复位:PHY_RST_N 引脚拉低持续 10ms;
- 软件复位:向寄存器 0x00 的 Bit[15] 写 1,完成后自动清零。
本实验用到三个寄存器:
| 寄存器 | 地址 | 关键位 |
|---|---|---|
| BCR(基本控制) | 0x00 | Bit[15] 软复位;Bit[12] 自协商使能 |
| BSR(基本状态) | 0x01 | Bit[5] 自协商完成;Bit[2] 连接状态 |
| PHYSR(特定状态) | 0x11 | Bit[15:14] 连接速度:10=1000M,01=100M,00=10M |
注意:原书说 YT8531"共有 22 位寄存器",结合上下文应理解为寄存器地址位宽等细节,实际以芯片手册为准——这也是提醒大家,芯片寄存器细节永远要查数据手册。
46.4 程序设计:状态机搞定 MDIO 读写
总体架构
系统分为两个核心模块:

- mdio_dri(MDIO 驱动模块):按协议时序直接驱动 ETH_MDC/ETH_MDIO 两根线;
- mdio_ctrl(MDIO 控制模块):定时轮询 PHY 状态寄存器,把速率显示到 LED;检测到触摸按键时发起软复位。
mdio_dri:六状态三段式状态机
状态跳转关系如下:
st_idle --op_exec=1--> st_pre(32位前导码) --> st_start(ST+OP)
--> st_addr(PHY地址+寄存器地址) --写--> st_wr_data(TA+写数据)
--读--> st_rd_data(TA+读数据)
--> 回到 st_idle
要点 1:双向 IO 的控制。eth_mdio 是 inout 类型,用方向信号切换:
assign eth_mdio = mdio_dir ? mdio_out : 1'bz; //控制双向io方向
assign mdio_in = eth_mdio; //MDIO数据输入
mdio_dir=1 时 FPGA 输出数据;为 0 时输出高阻,把总线让给 PHY(或让上拉电阻接管)。
要点 2:时钟分频。50MHz 系统时钟先分频出 dri_clk(12.5MHz),再二分频得到 eth_mdc(6.25MHz),远低于 12.5MHz 上限:
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dri_clk <= 1'b0;
clk_cnt <= 6'b0;
end
else if(clk_cnt == clk_divide[5:1] - 6'd1) begin
clk_cnt <= 6'b0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 6'b1;
end
另一个小技巧:eth_mdc 只在 cnt[0]==0 时为高,即只在读写操作期间产生时钟,空闲时保持高电平:
always @(posedge dri_clk or negedge rst_n) begin
if(!rst_n)
eth_mdc <= 1'b1;
else if(cnt[0] == 1'b0)
eth_mdc <= 1'b1;
else
eth_mdc <= 1'b0;
end
要点 3:读写数据的核心状态。写操作在 MDC 下降沿逐位送出 TA 和 16 位数据;读操作则在 TA 第一位把 mdio_dir 切成输入,第二位采样应答:
st_rd_data : begin
case(cnt)
7'd1 : begin
mdio_dir <= 1'b0; //MDIO引脚切换至输入状态
mdio_out <= 1'b1;
end
7'd2 : ; //TA[1]位,高阻,不操作
7'd4 : op_rd_ack <= mdio_in; //TA[0]位,0(应答) 1(未应答)
7'd6 : rd_data_t[15] <= mdio_in; //之后每两位采一位数据
...
7'd40: begin
op_done <= 1'b1; //读操作完成
op_rd_data <= rd_data_t;
end
...
endcase
end
观察 case(cnt) 里的计数是 1、3、5、7……这是因为每两个 dri_clk 周期对应一个 MDC 周期,奇数计数点正好对应 MDC 的固定相位,从而实现"下降沿更新数据、上升沿采样"的协议要求。
mdio_ctrl:定时轮询 + 软复位
控制模块内部用一个小流程计数器(flow_cnt)串起整个工作流:
flow_cnt=0: 有软复位请求 → 写寄存器0x00,数据16'h9140(Bit[15]=1软复位+Bit[12]=1自协商使能+Bit[6]=1选1000M)
定时到了 → 读寄存器0x01(BSR)
flow_cnt=2: 检查自协商完成(Bit[5])且连接成功(Bit[2]),都OK进入下一步,否则置link_error
flow_cnt=3: 读寄存器0x11(PHYSR)
flow_cnt=5: 根据Bit[15:14]译码出速率,赋给speed_status → LED
按键信号做了经典的三拍打圈处理来提取上升沿:
assign pos_rst_trig = ~rst_trig_d2 & rst_trig_d1;
定时器 TIMER_MAX=1,000,000,dri_clk 为 12.5MHz,所以每 80ms 轮询一次网口状态。
顶层与仿真
顶层 mdio_rw_test 就是把两个模块连起来,注意 PHY_ADDR 参数传 5'h04。仿真时有三个细节值得学习:
- 双向信号上拉:TB 里写
pullup(eth_mdio);,模拟板上的上拉电阻; - 官方从机模型:直接例化
mdio_slave_interface模块充当 PHY,通过类似 Wishbone 的接口给它预写入寄存器值; - defparam 缩短仿真:
defparam mdio_rw_test_inst.u_mdio_ctrl.TIMER_MAX = 1000;把 80ms 的定时缩短,避免仿真等到天荒地老。
pullup(eth_mdio); //MDIO信号上拉
defparam mdio_rw_test_inst.u_mdio_ctrl.TIMER_MAX = 1000;
46.5 下载验证
管脚分配(PL 端网口):
| 信号 | 管脚 | 说明 |
|---|---|---|
| sys_clk | U18 | 50MHz 系统时钟 |
| eth_mdc | F20 | 管理时钟 |
| eth_mdio | F19 | 双向数据 |
| eth_rst_n | G15 | PHY 复位 |
| touch_key | F16 | 触摸按键 |
| led[0]/led[1] | H15/L15 | 速率指示灯 |
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN F20 IOSTANDARD LVCMOS33} [get_ports eth_mdc]
set_property -dict {PACKAGE_PIN F19 IOSTANDARD LVCMOS33} [get_ports eth_mdio]
set_property -dict {PACKAGE_PIN G15 IOSTANDARD LVCMOS33} [get_ports eth_rst_n]
set_property -dict {PACKAGE_PIN F16 IOSTANDARD LVCMOS33} [get_ports touch_key]
上板时把网线接到 GE_PL 网口,另一端连电脑。下载后稍等几秒,两个 LED 全亮表示自协商成了 1000Mbps。想进一步确认,可以在电脑上右击"以太网 → 状态",看到速度为 1.0Gbps 即验证成功。
初学者容易踩的坑
- MDC 时钟超频:MDIO 规范要求 MDC 不超过 12.5MHz。分频系数别乱改,改小了 PHY 可能不响应。
- PHY 地址想当然:YT8531 的地址由硬件上下拉决定,本板是 5'h04 不是 0!地址错了读到的全是"未应答"。
- TA 位处理不当:读操作第 7'd1 拍就要把 mdio_dir 切输入,切晚了会把 PHY 的应答位顶掉,导致 op_rd_ack 一直为"未应答"。
- 仿真忘上拉:TB 里不加
pullup(eth_mdio)的话,空闲时 MDIO 是悬空的 'z',从机模型行为会异常。 - 软复位值写错:16'h9140 = Bit[15]软复位 + Bit[12]自协商使能 + Bit[6]速率选择,每一位都有含义,不要只写 0x8000。
小结 & 下一步
本章我们完成了以太网实战的"地基工程":
- MDIO 两线接口的协议格式(Preamble/ST/OP/PHYAD/REGAD/TA/DATA)与读写时序;
- 用六状态状态机实现了 MDIO 驱动,掌握 inout 双向总线的控制方法;
- 通过轮询 BSR/PHYSR 寄存器获取网口速率并用 LED 指示。
不过要说明的是:PHY 出厂默认配置就能工作,日常做以太网通信其实可以不碰 MDIO。下一章开始玩真格的——以太网 ARP 测试实验,让开发板和电脑真正"对话"起来。