1. 项目背景与核心价值
在嵌入式网络设备开发中,我们经常需要与以太网PHY芯片进行通信配置。传统方案是依赖SoC内置的MDIO控制器,但遇到以下场景时就会捉襟见肘:
- 使用无内置MDIO控制器的MCU(如STM32F103)
- 需要调试非标准PHY芯片
- 硬件设计阶段MDIO线路存在故障
这时用GPIO模拟MDIO协议就成了救命稻草。我曾在一个工业网关项目中使用STM32的4个普通IO口成功驱动了KSZ8081 PHY芯片,实测通信速率稳定在2.5MHz,配置过程仅需12ms。这种方案最大的优势在于其硬件兼容性——只要有两个空闲GPIO(甚至一个双向IO),就能实现PHY的基础配置。
2. MDIO协议深度解析
2.1 电气特性与帧结构
MDIO协议本质是同步串行通信,包含:
- MDC(时钟线):由主设备产生,标准速率2.5MHz
- MDIO(数据线):双向开漏总线,需外接1.5kΩ上拉电阻
典型帧结构如下表所示:
| 字段 | 位数 | 说明 |
|---|---|---|
| Preamble | 32 | 前导码(32个连续1) |
| ST | 2 | 帧开始(01) |
| OP | 2 | 操作码(10=写,01=读) |
| PHYAD | 5 | PHY芯片地址 |
| REGAD | 5 | 寄存器地址 |
| TA | 2 | 状态切换(读操作时Z0,写操作时10) |
| Data | 16 | 读写数据 |
2.2 关键时序参数
- 建立时间(Tsetup):MDIO信号在MDC上升沿前至少需稳定10ns
- 保持时间(Thold):MDC下降沿后MDIO信号需保持10ns
- 输出延迟(Tvalid):从MDC下降沿到MDIO有效输出的最大延迟为300ns
提示:使用GPIO模拟时,建议将时钟周期控制在400ns以上(2.5MHz),给软件留足处理余量。
3. 硬件设计与软件实现
3.1 硬件连接方案
以STM32F407为例的典型连接方式:
c复制// GPIO引脚定义
#define MDC_PIN GPIO_PIN_0 // PB0
#define MDIO_PIN GPIO_PIN_1 // PB1
#define MDIO_PORT GPIOB
// 初始化配置
GPIO_InitTypeDef GPIO_InitStruct = {
.Pin = MDC_PIN | MDIO_PIN,
.Mode = GPIO_MODE_OUTPUT_OD,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_HIGH
};
HAL_GPIO_Init(MDIO_PORT, &GPIO_InitStruct);
3.2 核心驱动实现
3.2.1 基本信号生成
c复制// 生成时钟脉冲
static void mdio_clock_pulse(void) {
HAL_GPIO_WritePin(MDIO_PORT, MDC_PIN, GPIO_PIN_SET);
delay_ns(200); // 半周期高电平
HAL_GPIO_WritePin(MDIO_PORT, MDC_PIN, GPIO_PIN_RESET);
delay_ns(200); // 半周期低电平
}
// 写1位数据
static void mdio_write_bit(bool bit) {
HAL_GPIO_WritePin(MDIO_PORT, MDIO_PIN, bit ? GPIO_PIN_SET : GPIO_PIN_RESET);
mdio_clock_pulse();
}
3.2.2 完整帧发送
c复制void mdio_write_reg(uint8_t phy_addr, uint8_t reg_addr, uint16_t data) {
// 前导码
for(int i=0; i<32; i++) mdio_write_bit(1);
// 帧开始+操作码
mdio_write_bit(0); mdio_write_bit(1); // ST
mdio_write_bit(1); mdio_write_bit(0); // OP(写)
// PHY地址(MSB first)
for(int i=4; i>=0; i--)
mdio_write_bit((phy_addr >> i) & 0x1);
// 寄存器地址
for(int i=4; i>=0; i--)
mdio_write_bit((reg_addr >> i) & 0x1);
// 状态切换
mdio_write_bit(1); mdio_write_bit(0);
// 数据(MSB first)
for(int i=15; i>=0; i--)
mdio_write_bit((data >> i) & 0x1);
}
4. 实战调试技巧
4.1 示波器诊断要点
- 检查时钟占空比是否接近50%
- 测量MDIO信号在时钟上升沿是否稳定
- 确认前导码持续时间不少于12.8μs(32个1@2.5MHz)
4.2 常见PHY配置流程
以配置100M全双工为例:
c复制// 软复位PHY
mdio_write_reg(1, 0, 0x8000);
delay_ms(10);
// 设置100M全双工
mdio_write_reg(1, 4, 0x2100);
// 启用自动协商
mdio_write_reg(1, 0, 0x1200);
4.3 性能优化技巧
- 使用GPIO位带操作替代HAL库(速度提升5倍):
c复制#define MDIO_BITBAND *(volatile uint32_t*)(0x42000000 + (GPIOB_BASE + 0x0C)*32 + 1*4)
#define MDC_BITBAND *(volatile uint32_t*)(0x42000000 + (GPIOB_BASE + 0x0C)*32 + 0*4)
// 改写时钟脉冲函数
static inline void mdio_clock_pulse_fast(void) {
MDC_BITBAND = 1;
__NOP(); __NOP(); // 约20ns@168MHz
MDC_BITBAND = 0;
__NOP(); __NOP();
}
- 预计算帧数据并用DMA传输(适合批量配置)
5. 特殊场景处理
5.1 单GPIO实现方案
当GPIO紧张时,可用一个双向IO实现:
c复制// 切换输入输出模式
void mdio_set_dir(bool output) {
GPIO_InitTypeDef GPIO_InitStruct = {
.Pin = MDIO_PIN,
.Mode = output ? GPIO_MODE_OUTPUT_OD : GPIO_MODE_INPUT,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_HIGH
};
HAL_GPIO_Init(MDIO_PORT, &GPIO_InitStruct);
}
// 读1位数据
static bool mdio_read_bit(void) {
mdio_set_dir(false);
__NOP(); __NOP(); // 等待总线稳定
bool val = HAL_GPIO_ReadPin(MDIO_PORT, MDIO_PIN);
mdio_clock_pulse();
return val;
}
5.2 异常处理机制
- 超时检测:每个bit操作增加超时判断
- CRC校验:部分PHY支持CRC校验位(如88E1111)
- 重试机制:连续3次失败后触发硬件复位
我在实际项目中发现,温度超过85℃时GPIO翻转速度会下降约15%,这时需要适当降低时钟频率。建议在高温环境下将MDC周期调整为500ns(2MHz),同时增加20%的时序余量。