STM32通过IIC访问RTL8367RB交换芯片MDIO驱动源码解析
简介这份源码资源面向使用 STM32F103VGT6 进行以太网交换芯片调试的嵌入式开发者聚焦 MDIO 总线与 IIC 两种方式访问 RTL8367RB 的实现并配套串口指令交互可用于交换机寄存器读写、链路状态查询等调试场景。压缩包共 498 个文件约 3.14MB以 327 个 c 源文件与 92 个 h 头文件为主体涵盖 HAL 库外设驱动、DSP 运算库及 MDIO/IIC 业务代码另有 43 个 s 启动文件、28 个 icf 链接脚本与 uvprojx、ioc 等工程配置可直接导入 Keil 与 STM32CubeMX 复现。资源已有 3395 人学习下载说明其在同类调试需求中具备一定参考价值。读者可从中获得 MDIO 时序与寄存器访问的完整实现、IIC 读写 RTL8367RB 的代码范例以及串口命令解析与调试输出的组织方式便于快速移植到自有板卡并排查通信异常。1. 从一根 MDIO 线说起这份 STM32 源码到底能省你几天调过交换芯片的兄弟都懂PHY 寄存器读不出来的时候示波器抓波形抓到怀疑人生最后发现是 MDIO 的时序多打了半个时钟。这份「MDIO 之 STM32 源码」就是冲着这类场景来的它把 STM32 侧通过 IIC 总线去访问 RTL8367RB 交换芯片管理接口的整套底层驱动拆开给你看包含寄存器读写、时序控制、状态轮询这些最容易翻车的部分。适合正在做嵌入式网络设备、需要自己写 PHY/Switch 管理代码的从业者也适合刚接触 MDIO 协议、想找一个能跑通的参考实现的新手。它解决的不是「协议是什么」的问题而是「代码怎么写、参数怎么配、为什么读回来全是 0xFFFF」的问题。2. 先搞懂 MDIO 与 IIC 的桥接关系为什么不是直接怼 GPIO很多人第一反应是 MDIO 不就是两根线MDC MDIO吗直接 GPIO 模拟时序不就行了。这话对一半。RTL8367RB 这类交换芯片的管理接口确实遵循 MDIO 帧格式但在这份源码的硬件设计里STM32 并不是用 GPIO 去模拟 MDC/MDIO而是通过 IIC 总线去访问芯片内部的管理寄存器映射。也就是说IIC 是「传输层」MDIO 帧格式是「应用层语义」两者叠在一起用。理解这个分层后面看代码才不会懵。2.1 MDIO 帧结构与寄存器寻址的基本约定标准 MDIO 帧是 32 位前导码 2 位起始 2 位操作码 5 位 PHY 地址 5 位寄存器地址 2 位 turnaround 16 位数据。RTL8367RB 在兼容这套格式的同时把 PHY 地址段扩展成了芯片内部 port/register 的寻址空间。源码里你会看到类似phy_addr和reg_addr两个参数一路往下传最终被拼成一个 IIC 写地址加数据字节的组合。这里的关键认知是PHY 地址不是物理 PHY 编号而是芯片内部寄存器页的选择器。搞错这一层读出来的值永远是错的而且不会报错属于最阴的坑。常见做法是把 port 号左移若干位再或上寄存器偏移拼成 16 位地址。源码里对应的宏定义一般长这样/* RTL8367RB 内部寄存器地址拼接高字节为 port/page低字节为寄存器偏移 */ #define RTL8367_REG_ADDR(port, reg) (((uint16_t)(port) 8) | ((uint16_t)(reg) 0xFF))逻辑说明port占高 8 位reg占低 8 位这样一次 IIC 事务就能表达「哪个端口、哪个寄存器」。参数说明port取值范围取决于芯片端口数RTL8367RB 常见是 0~6 的物理口加 CPU 口reg是芯片手册里定义的寄存器偏移别自己拍脑袋填。这个宏看着简单但如果你把 port 和 reg 的顺序写反读回来的就是另一个寄存器的值现象是「能读、但值不对」比读不出来更难查。2.2 为什么用 IIC 而不是 bit-bang MDIO从工程角度看用 IIC 访问管理接口有几个现实好处STM32 的硬件 IIC 外设能保证时钟稳定性不用你手动数 NOP 来凑 MDC 频率多设备挂在同一总线时寻址清晰代码复用性高换一颗支持 IIC 管理接口的芯片改改地址宏就能用。代价是每次读写多了一层 IIC 事务开销寄存器批量读写时延会比纯 GPIO 模拟高。源码里对连续读做了优化后面第 4 章会讲。选型上我的建议是如果你的板子已经把管理接口接到 IIC 上了别折腾 GPIO 模拟直接用这份源码的 IIC 路径如果是纯 MDC/MDIO 引脚那这份代码的帧组装部分可以参考但传输层要换成 GPIO 时序。别硬套硬件拓扑决定代码结构。3. 把源码跑起来IIC 初始化到第一次读到芯片 ID拿到源码别急着编译先确认三件事IIC 外设编号、从机地址、上拉电阻。这三样任何一样不对代码再对也读不出东西。下面按实际落地顺序拆。3.1 IIC 外设与从机地址配置源码里 IIC 初始化一般封装成一个mdio_iic_init()之类的函数核心是配置时钟频率和从机地址。RTL8367RB 的 IIC 从机地址通常是 0x5C 或 0x5E7 位地址左移一位后的写地址具体看芯片 SMI 地址引脚怎么接。代码骨架/* IIC 初始化400kHz 快速模式7 位地址 */ void mdio_iic_init(void) { IIC_Init_TypeDef iic; iic.clock 400000; /* 400kHz别超过芯片手册上限 */ iic.addr_mode IIC_ADDR_7BIT; iic.slave_addr 0x5C; /* 按实际 SMI 地址引脚确定 */ iic_enable(iic); }逻辑说明clock设 400kHz 是快速模式上限RTL8367RB 管理接口一般能吃到这个频率但如果你板子走线长、上拉偏弱降到 100kHz 更稳。slave_addr必须和硬件一致不确定就拿逻辑分析仪扫一遍总线看实际出现的地址。参数说明addr_mode选 7 位还是 10 位取决于芯片RTL8367RB 用 7 位。提示IIC 上拉电阻别省4.7k 是常见值走线超过 10cm 建议降到 2.2k。上拉不够的典型现象是偶尔能读、偶尔 NACK属于玄学级故障。3.2 读芯片 ID 验证链路链路通不通最快的验证是读芯片 ID 寄存器。RTL8367RB 有固定的 Chip ID 寄存器读回来应该是一个已知值具体值查手册不同批次可能不同但不会是 0x0000 或 0xFFFF。验证代码/* 读芯片 ID验证 IIC 链路与地址拼接是否正确 */ uint16_t mdio_read_chip_id(void) { uint16_t id 0; uint16_t addr RTL8367_REG_ADDR(0, RTL8367_REG_CHIP_ID); if (mdio_iic_read(addr, id) ! 0) { return 0xFFFF; /* 读失败返回哨兵值方便上层判断 */ } return id; }逻辑说明先拼地址再走 IIC 读失败返回 0xFFFF 而不是 0这样上层能区分「读失败」和「读到 0」。参数说明RTL8367_REG_CHIP_ID是手册里的寄存器偏移别用我这里的占位名换成你手上手册的实际值。这一步过了说明 IIC 时序、从机地址、地址拼接三件事至少对了两件半剩下的就是逐个寄存器验证。如果读回来是 0xFFFF先别改代码按这个顺序查从机地址对不对 → 上拉电阻够不够 → 地址拼接的 port/reg 顺序有没有反 → 芯片是否已经复位完成。这四步能解决八成问题剩下两成交给逻辑分析仪。4. 寄存器读写封装批量操作与时序边界单次读写跑通只是开始真正落地时要面对连续读多个寄存器、写配置序列这些场景。源码里对这块做了封装但封装得好不好直接决定你调 PHY 时的效率。4.1 单寄存器读写与返回值约定源码的读写函数一般返回 int 表示成功失败数据通过指针带出。这种约定比直接返回数据值更靠谱因为数据值本身可能是任意 16 位没法用返回值表达错误。典型实现/* 写寄存器先发地址再发 16 位数据高字节在前 */ int mdio_iic_write(uint16_t addr, uint16_t data) { uint8_t buf[4]; buf[0] (uint8_t)(addr 8); buf[1] (uint8_t)(addr 0xFF); buf[2] (uint8_t)(data 8); buf[3] (uint8_t)(data 0xFF); return iic_write_bytes(0x5C, buf, 4); /* 返回 0 成功非 0 失败 */ }逻辑说明地址和数据都按高字节在前拆成字节流一次性发出去。这里假设芯片的管理接口接受「地址数据」连续写的事务格式如果你的芯片要求地址和数据分两次事务就得拆成两个iic_write_bytes调用。参数说明buf长度 4 是地址 2 字节加数据 2 字节别多别少iic_write_bytes的返回值直接透传方便上层定位是 NACK 还是总线错误。4.2 连续读的地址自增与边界处理批量读寄存器时很多芯片支持地址自增发一次起始地址就能连续读多个。但 RTL8367RB 的自增行为有边界跨 port 时不一定连续跨页时可能回绕。源码里对连续读做了分段处理避免一次事务跨过不连续区域。常见写法/* 连续读按 port 分段每段内地址自增 */ int mdio_iic_read_burst(uint8_t port, uint8_t start_reg, uint16_t *out, uint8_t count) { uint8_t i; for (i 0; i count; i) { uint16_t addr RTL8367_REG_ADDR(port, start_reg i); if (mdio_iic_read(addr, out[i]) ! 0) { return -1; /* 任一失败即中止避免脏数据 */ } } return 0; }逻辑说明这里没有真的用「一次事务读多个」而是逐寄存器读牺牲一点效率换正确性。原因是跨寄存器边界的自增行为在不同芯片上不一致逐读最稳。参数说明count别超过该 port 内连续寄存器的实际数量超了会读到保留地址返回值不可预期。如果你的场景对时延敏感可以改成「同 port 内一次事务读多个跨 port 重新发起」但要做边界判断。注意连续读失败时直接返回 -1 而不是继续读是为了避免上层拿到一半新一半旧的数据做判断。这种「脏数据比没数据更可怕」的原则在寄存器配置场景里尤其重要。5. 避坑与排查那些读不出、读不对、读着读着挂掉的事这一章全是血泪经验每条都按「现象 → 原因 → 解决」写照着对号入座能省不少时间。5.1 读回来全是 0xFFFF现象所有寄存器读出来都是 0xFFFF换寄存器地址也一样。原因IIC 从机根本没应答主机读到的全是总线拉高电平。解决先用逻辑分析仪看总线上有没有从机 ACK没有就查从机地址和上拉电阻有 ACK 但数据全 F查地址拼接是不是把 port 和 reg 写反了导致访问到不存在的地址空间。5.2 能读能写但配置不生效现象写寄存器返回成功读回来也是写进去的值但芯片行为没变。原因写到了影子寄存器或者配置寄存器需要额外的 commit 操作。RTL8367RB 部分配置是先写暂存再触发生效。解决查手册确认该寄存器是否需要「写使能 触发」两步源码里如果有mdio_commit()之类的函数别忘了调。5.3 连续读写跑一段时间后总线挂死现象单次读写正常连续跑几百次后 IIC 总线卡住后续所有事务超时。原因某次事务中途出错比如 NACK后没有正确发 STOP 条件从机一直等时钟总线被占住。解决在 IIC 读写函数里加超时和总线恢复逻辑出错后手动发 9 个时钟脉冲再发 STOP把从机状态机踢回空闲。这个恢复函数一定要有不然现场跑久了必挂。5.4 换一块板子就读不出来现象同一份代码A 板正常B 板读不出。原因B 板的 SMI 地址引脚接法不同从机地址变了或者 B 板上拉电阻用了 10k边沿太缓。解决从机地址做成可配置宏别写死上拉电阻按走线长度选值长走线用 2.2k。这两条是硬件差异代码层面只能靠配置化兜住。5.5 中断里调用读写导致死锁现象在主循环里读写正常放到中断里偶尔卡死。原因IIC 读写是阻塞式实现内部有忙等待中断上下文里等不到外设就绪就死循环。解决IIC 事务别放中断里中断里只置标志主循环处理。如果非要放改成非阻塞状态机实现但这属于另一个工程量级的事了。6. 进阶把寄存器访问做成可观测的调试层源码能跑通只是及格线真正让我少熬夜的是在它上面加一层可观测的调试封装。具体做法是给每次 MDIO 读写加一个环形缓冲日志记录时间戳、地址、数据、返回值出问题时把日志 dump 出来比拿示波器一帧帧抓快得多。实现上不复杂/* 环形日志记录最近 64 次 MDIO 事务供故障回溯 */ typedef struct { uint32_t tick; uint16_t addr; uint16_t data; int8_t ret; } mdio_log_t; static mdio_log_t s_log[64]; static uint8_t s_log_idx; static void mdio_log(uint16_t addr, uint16_t data, int ret) { s_log[s_log_idx].tick get_tick(); s_log[s_log_idx].addr addr; s_log[s_log_idx].data data; s_log[s_log_idx].ret (int8_t)ret; s_log_idx (s_log_idx 1) 0x3F; /* 64 取模环形覆盖 */ }逻辑说明每次读写前后调一次mdio_log把关键字段存下来索引按 64 取模实现环形覆盖不占额外内存管理。参数说明tick用毫秒级系统 tick 就够精度再高对定位问题帮助不大ret用 int8 存返回值省空间。这个日志层平时不输出只在检测到连续失败或者上层主动触发时 dump对实时性影响可以忽略。配合这个日志我一般还会加一个「寄存器快照对比」的小工具调 PHY 前 dump 一遍关键寄存器调完再 dump 一遍diff 一下就知道哪些位被改了、改得对不对。这个习惯帮我抓到过好几次「以为写进去了其实写到影子寄存器」的问题。从那以后我每次碰新的交换芯片都强制先把日志层和快照工具搭好再动配置代码磨刀不误砍柴工。希望帮到你。本文还有配套的精品资源点击获取