STM32模拟SMI总线控制RTL8306:从零开始手把手实现MDIO通信(附完整代码)

STM32模拟SMI总线控制RTL8306:从零开始手把手实现MDIO通信(附完整代码) STM32模拟SMI总线控制RTL8306从零开始手把手实现MDIO通信附完整代码在物联网设备开发中STM32与网络交换芯片的组合非常常见。当我们需要在资源受限的单片机上实现网络功能时通过GPIO模拟SMI总线协议来控制RTL8306这类交换机芯片就成为了一个经济高效的解决方案。本文将带你从零开始一步步实现STM32上的MDIO通信。1. SMI总线与MDIO协议基础SMISerial Management Interface是用于管理网络PHY设备的串行接口由MDC管理数据时钟和MDIO管理数据输入输出两条信号线组成。理解这个协议是成功实现通信的第一步。MDIO协议的主要特点包括双向通信MDIO线在读写操作中方向会发生变化非周期性时钟MDC时钟频率可以在DC到2.5MHz之间变化简单拓扑一个主设备MAC最多可连接32个从设备PHY典型的MDIO帧结构包含以下几个关键部分字段位数描述前导码32连续的1信号用于同步起始位201表示帧开始操作码210表示读01表示写PHY地址5目标PHY设备的地址寄存器地址5要读写的寄存器地址转向周期2读写方向切换的过渡周期数据16要写入或读取的数据空闲-总线恢复高阻状态2. 硬件连接与初始化在开始编码前我们需要正确连接STM32与RTL8306芯片。典型的连接方式如下// GPIO引脚定义 #define MDC_GPIO_PORT GPIOB #define MDC_PIN GPIO_PIN_6 #define MDIO_GPIO_PORT GPIOB #define MDIO_PIN GPIO_PIN_7硬件初始化包括以下几个关键步骤GPIO配置MDC引脚配置为推挽输出MDIO引脚初始配置为推挽输出后续会根据操作切换方向时钟配置确保GPIO端口时钟已使能根据实际需求配置系统时钟延时函数实现需要精确的微秒级延时来控制MDC时钟void MDIO_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; // 使能GPIO时钟 __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置MDC引脚 GPIO_InitStruct.Pin MDC_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(MDC_GPIO_PORT, GPIO_InitStruct); // 初始配置MDIO为输出 GPIO_InitStruct.Pin MDIO_PIN; HAL_GPIO_Init(MDIO_GPIO_PORT, GPIO_InitStruct); // 初始状态MDC低MDIO高 HAL_GPIO_WritePin(MDC_GPIO_PORT, MDC_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(MDIO_GPIO_PORT, MDIO_PIN, GPIO_PIN_SET); }3. MDIO时序的软件实现实现MDIO通信的核心在于精确控制时序。我们需要分别实现写时序和读时序。3.1 基本时序控制函数首先实现几个基础函数来控制MDC时钟和MDIO数据线// 设置MDC时钟线状态 static void MDC_Set(uint8_t state) { HAL_GPIO_WritePin(MDC_GPIO_PORT, MDC_PIN, state ? GPIO_PIN_SET : GPIO_PIN_RESET); } // 设置MDIO数据线状态 static void MDIO_Set(uint8_t state) { HAL_GPIO_WritePin(MDIO_GPIO_PORT, MDIO_PIN, state ? GPIO_PIN_SET : GPIO_PIN_RESET); } // 读取MDIO数据线状态 static uint8_t MDIO_Get(void) { return HAL_GPIO_ReadPin(MDIO_GPIO_PORT, MDIO_PIN); } // 生成一个MDC时钟脉冲 static void MDC_Pulse(void) { MDC_Set(0); Delay_us(1); // 确保满足最小低电平时间 MDC_Set(1); Delay_us(1); // 确保满足最小高电平时间 MDC_Set(0); }3.2 写寄存器实现写寄存器的完整流程包括发送32位前导码发送起始位01发送操作码01写发送PHY地址发送寄存器地址处理转向周期发送16位数据释放总线void MDIO_Write(uint8_t phyAddr, uint8_t regAddr, uint16_t data) { // 1. 发送32位前导码全1 for(int i0; i32; i) { MDIO_Set(1); MDC_Pulse(); } // 2. 发送起始位01 MDIO_Set(0); MDC_Pulse(); MDIO_Set(1); MDC_Pulse(); // 3. 发送操作码01写 MDIO_Set(0); MDC_Pulse(); MDIO_Set(1); MDC_Pulse(); // 4. 发送PHY地址5位 for(int i4; i0; i--) { MDIO_Set((phyAddr i) 0x01); MDC_Pulse(); } // 5. 发送寄存器地址5位 for(int i4; i0; i--) { MDIO_Set((regAddr i) 0x01); MDC_Pulse(); } // 6. 转向周期2位写操作时MDIO保持输出 MDIO_Set(1); MDC_Pulse(); MDIO_Set(0); MDC_Pulse(); // 7. 发送数据16位 for(int i15; i0; i--) { MDIO_Set((data i) 0x01); MDC_Pulse(); } // 8. 释放MDIO线上拉电阻会将其拉高 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin MDIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(MDIO_GPIO_PORT, GPIO_InitStruct); }3.3 读寄存器实现读寄存器的流程与写操作类似但在转向周期后需要改变MDIO的方向uint16_t MDIO_Read(uint8_t phyAddr, uint8_t regAddr) { uint16_t data 0; // 1. 发送32位前导码全1 for(int i0; i32; i) { MDIO_Set(1); MDC_Pulse(); } // 2. 发送起始位01 MDIO_Set(0); MDC_Pulse(); MDIO_Set(1); MDC_Pulse(); // 3. 发送操作码10读 MDIO_Set(1); MDC_Pulse(); MDIO_Set(0); MDC_Pulse(); // 4. 发送PHY地址5位 for(int i4; i0; i--) { MDIO_Set((phyAddr i) 0x01); MDC_Pulse(); } // 5. 发送寄存器地址5位 for(int i4; i0; i--) { MDIO_Set((regAddr i) 0x01); MDC_Pulse(); } // 6. 转向周期读操作时MDIO改为输入 // 第一个时钟周期MAC释放MDIO GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin MDIO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(MDIO_GPIO_PORT, GPIO_InitStruct); MDC_Pulse(); // 第一个TA周期 // 第二个时钟周期PHY驱动MDIO MDC_Pulse(); // 第二个TA周期 // 7. 读取数据16位 for(int i15; i0; i--) { MDC_Pulse(); if(MDIO_Get()) { data | (1 i); } } // 8. 恢复MDIO为输出 GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(MDIO_GPIO_PORT, GPIO_InitStruct); return data; }4. RTL8306特定功能实现RTL8306是一款常用的5端口10/100M交换机芯片通过MDIO接口可以配置其各种功能。下面我们实现几个常用的功能函数。4.1 芯片初始化void RTL8306_Init(void) { // 1. 初始化MDIO接口 MDIO_GPIO_Init(); // 2. 读取芯片ID验证通信是否正常 uint16_t id MDIO_Read(0, 2); // PHY地址0寄存器2是PHY标识1 if((id 0xFFF0) ! 0x6300) { // RTL8306的PHY标识 // 处理错误 } // 3. 配置基本参数 MDIO_Write(0, 0, 0x1140); // 配置控制寄存器 MDIO_Write(0, 4, 0x01E1); // 配置自动协商广告寄存器 // 4. 重启自动协商 uint16_t ctrl MDIO_Read(0, 0); ctrl | 0x0200; // 设置重启自动协商位 MDIO_Write(0, 0, ctrl); }4.2 端口状态检测typedef struct { uint8_t linkStatus; uint8_t speed; // 0:10M, 1:100M uint8_t duplex; // 0:半双工, 1:全双工 } PortStatus; PortStatus RTL8306_GetPortStatus(uint8_t port) { PortStatus status {0}; if(port 4) return status; // RTL8306只有5个端口(0-4) // 读取端口状态寄存器 uint16_t reg MDIO_Read(port, 1); // 寄存器1是状态寄存器 status.linkStatus (reg 0x0004) ? 1 : 0; if(status.linkStatus) { status.speed (reg 0x0002) ? 1 : 0; status.duplex (reg 0x0004) ? 1 : 0; } return status; }4.3 VLAN配置RTL8306支持简单的VLAN功能下面是一个配置示例void RTL8306_SetVLAN(uint8_t vlanId, uint8_t portMask) { // 配置VLAN索引寄存器 MDIO_Write(0, 0x10, vlanId); // VLAN ID // 配置端口成员 MDIO_Write(0, 0x11, portMask); // 启用VLAN功能 uint16_t ctrl MDIO_Read(0, 0x12); ctrl | 0x0001; // 设置VLAN使能位 MDIO_Write(0, 0x12, ctrl); }5. 调试技巧与常见问题在实际项目中MDIO通信可能会遇到各种问题。下面分享一些调试经验和常见问题的解决方法。5.1 调试技巧逻辑分析仪是必备工具捕获MDC和MDIO信号验证时序是否符合规范检查数据是否正确分段测试先测试写操作再测试读操作从简单寄存器开始如PHY标识寄存器增加调试输出void MDIO_DebugPrintFrame(uint8_t phyAddr, uint8_t regAddr, uint16_t data, uint8_t isWrite) { printf(%s PHY:%d REG:%02X DATA:%04X\n, isWrite ? WRITE : READ, phyAddr, regAddr, data); }5.2 常见问题及解决方案问题现象可能原因解决方案读取始终返回0xFFFFMDIO方向切换不正确检查转向周期后的MDIO方向设置通信不稳定时序不符合要求使用逻辑分析仪检查时序调整延时只能写不能读上拉电阻缺失确保MDIO线有适当的上拉电阻4.7kΩ部分位错误时钟边沿不稳定确保MDC信号干净增加适当的延时5.3 性能优化建议延时优化根据实际测试调整延时参数在满足时序要求的前提下尽量提高速度批量操作对多个寄存器的操作可以合并为连续操作减少不必要的总线释放和重新获取缓存配置对频繁访问的寄存器值进行缓存避免重复读取不变的配置// 示例批量写入多个寄存器 void RTL8306_BulkWrite(uint8_t phyAddr, uint8_t startReg, uint16_t *data, uint8_t count) { // 发送前导码和起始位 // ...省略前导码和起始位代码 for(uint8_t i0; icount; i) { uint8_t regAddr startReg i; // 发送操作码、PHY地址、寄存器地址 // ...省略地址发送代码 // 发送数据 for(int j15; j0; j--) { MDIO_Set((data[i] j) 0x01); MDC_Pulse(); } // 不释放总线直接开始下一个写操作 } // 最后释放总线 // ...省略总线释放代码 }