4. 基于MDIO的寄存器访问层实现

在3.2T光模块的固件开发中,MDIO(Management Data Input/Output)是主机与模块内部管理寄存器之间最基础的通信通道。无论是读取模块温度、配置CDR参数,还是访问DSP内部状态,最终都要落到对MDIO寄存器的读写操作上。如果每个业务模块都直接操作MDIO控制器硬件,代码将迅速变得难以维护、难以移植,也无法统一处理超时和错误。因此,本节的核心任务是构建一个寄存器访问层:把底层MDIO时序封装成可靠的读写函数,向上提供统一的寄存器访问接口。

一、MDIO读写函数的分层封装

MDIO在IEEE 802.3 Clause 22/45中定义了帧格式,一次完整的读写包含前导码、起始码、操作码、PHY地址、寄存器地址、 turnaround 和16位数据。硬件控制器通常已经处理了帧的串行化,软件层需要关注的是:发起一次事务、等待完成、判断结果。我们将其封装为三个层次:

  • 硬件抽象层:直接写MDIO控制器的命令寄存器,读取状态寄存器的完成位与错误位。
  • 事务层:实现 mdio_read(phy, reg, &val) 与 mdio_write(phy, reg, val),负责发起、轮询、超时判断。
  • 寄存器库层:面向业务,提供按名称访问、多字节访问、位域操作等便捷接口。

阻塞与非阻塞的选择取决于调用上下文。在初始化阶段或单次配置时,使用阻塞方式最直观:发起事务后循环查询完成位,直到成功或超时。典型实现如下(示例参数,非实测):

int mdio_read_blocking(uint8_t phy, uint16_t reg, uint16_t *val)
{
    mdio_start_read(phy, reg);              /* 写命令寄存器,启动事务 */
    for (uint32_t i = 0; i < MDIO_POLL_MAX; i++) {
        uint32_t st = mdio_read_status();
        if (st & MDIO_ST_DONE) {
            if (st & MDIO_ST_ERR) return -EIO;   /* 无响应/校验错 */
            *val = mdio_read_data();
            return 0;
        }
    }
    return -ETIMEDOUT;                      /* 轮询耗尽仍未完成 */
}

非阻塞方式则把“发起”和“收尾”拆开:发起后立即返回,由状态机或中断在完成时回调。它适合在中断上下文或需要同时管理多条MDIO链的场景,避免长时间占用CPU。两种方式共用同一套状态位判断逻辑,区别只在于等待策略。

二、超时处理的设计要点

MDIO事务在物理层可能因模块未上电、地址错误、总线被拉低而永远不完成。超时阈值不能拍脑袋:Clause 22 一帧约64位,在2.5MHz MDIO时钟下约26μs,加上控制器开销,单次事务通常在百微秒量级。因此轮询上限可按“最坏事务时间 × 安全系数”估算。例如按200μs上限、每次轮询约1μs计,轮询次数取512次较为稳妥。超时后应复位MDIO控制器状态机再返回错误,否则残留状态会导致后续事务全部失败。同时要区分“超时”和“总线错误”:前者可能是模块忙,后者往往意味着硬件连接问题,二者的重试策略不同。

MDIO寄存器访问层结构与事务状态 寄存器库层 按名访问 · 多字节 · 位域操作 事务层 mdio_read / mdio_write · 阻塞/非阻塞 · 超时 硬件抽象层 命令寄存器 · 状态位 · 数据寄存器 单次事务状态流转 空闲 发起事务 轮询完成位 读取数据 超时/错误 DONE 轮询耗尽 复位控制器 32位寄存器 = 两个16位MDIO字 高16位 → 低地址字(先读/写) 低16位 → 高地址字(后读/写) 拼接:val = (hi << 16) | lo

三、多字节寄存器的字节序处理

3.2T模块中大量寄存器是32位甚至64位宽,例如DSP的误码统计、光功率的定点数。而MDIO单次事务只搬运16位,因此一个32位寄存器需要拆成两次访问。这里有两个容易踩坑的地方:字序和字节序。

字序指高低16位分别对应哪个寄存器地址。多数光模块遵循“低地址存高16位”的约定,即先访问基地址得到 hi,再访问基地址+1得到 lo,最终 val = (hi << 16) | lo。但不同厂商的DSP可能相反,必须在驱动中通过配置项显式声明,而不是硬编码。字节序则指16位字内部的高低字节,MDIO传输本身按网络序(大端)组织,若主机是小端,需要在拼接后做 ntohs 类转换。建议在寄存器库中统一以“主机序的32位值”作为对外接口,把字序和字节序的转换全部收敛在库内部,业务代码只面对一个干净的 uint32_t。

写操作还要注意读-改-写风险:若一个32位寄存器的高16位和低16位分属不同功能域,直接分两次写可能在中途被硬件采样到不一致的中间值。稳妥做法是先读出完整32位,修改目标位域,再按字序写回;对时序敏感的寄存器,则需查手册确认是否支持原子更新或需要先置忙标志。

四、通用寄存器读写库的接口与测试

一个可复用的库应当把“地址映射”和“访问机制”解耦。推荐用一张寄存器描述表驱动:每项包含名称、MDIO地址、位宽、字序标志、读写权限。库对外暴露 reg_read_u32(name, &val)、reg_write_u32(name, val)、reg_update_bits(name, mask, val) 等接口,内部根据描述表自动选择单字或双字事务。这样新增寄存器只需加表项,不必改代码。

测试应覆盖三类用例:正常读写(写入已知图案再读回比对,如0xA5A5_5A5A)、边界与位域(验证掩码更新不影响相邻位)、异常路径(用不存在的PHY地址触发超时,确认返回 -ETIMEDOUT 且控制器已复位、后续正常事务仍可成功)。测试时建议先读一个只读的版本寄存器作为“链路存活”探针,再执行写测试,避免在链路异常时误判为库逻辑错误。下表给出一个可直接参考的测试用例骨架:

用例操作预期结果判据
链路探针读版本寄存器返回非0且不超时ret==0
32位回环写0xA5A55A5A后读回读值等于写入值val==0xA5A55A5A
位域更新对bit[7:4]写0x3相邻位保持不变(val&0xF0)==0x30
超时路径访问PHY地址0x1F返回-ETIMEDOUTret==-ETIMEDOUT
恢复验证超时后再读版本寄存器仍能成功ret==0
把字序、字节序、超时阈值都做成库的配置项而非宏常量,同一套代码即可适配不同厂商的3.2T模块,显著降低移植成本。

完成上述封装后,上层业务不再关心MDIO帧格式和轮询细节,只需按寄存器名读写。这层抽象也为后续的告警轮询、参数校准和自动化测试提供了稳定底座——当出现读值异常时,可以快速定位是链路超时、字序配置错误还是位域掩码问题,而不是在业务代码里逐层排查。