简介:这份源码资源面向使用 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 或 0x5E(7 位地址左移一位后的写地址),具体看芯片 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 一下就知道哪些位被改了、改得对不对。这个习惯帮我抓到过好几次「以为写进去了其实写到影子寄存器」的问题。从那以后我每次碰新的交换芯片,都强制先把日志层和快照工具搭好再动配置代码,磨刀不误砍柴工。希望帮到你。
本文还有配套的精品资源,点击获取