4、光模块驱动开发:SFP/ONU光模块接口标准、I2C总线驱动、数字诊断监控(DDM)实现、光功率与温度读取

光模块驱动,说白了就是让CPU和光模块“对话”。你想想看,ONU设备要正常工作,必须知道光口有没有插好、接收光功率够不够、激光器温度是否正常。这些信息从哪里来?就是从光模块内部的寄存器里读出来的。

我个人习惯把光模块驱动分成三层:硬件接口层(I2C读写)、协议解析层(SFF-8472规范)、应用层(DDM监控上报)。今天我们就一层层把它拆开讲清楚。

4.1 SFP/ONU光模块接口标准

先说说接口标准。EPON ONU用的光模块,主流是SFP封装,也有部分低成本方案用SFF或BOSA直焊。但不管哪种封装,电气接口和I2C协议基本是统一的

我遇到过不少新手,上来就问:“这个光模块是哪个厂家的?驱动要不要改?”其实不用。只要符合SFF-8472标准,驱动代码可以通用。厂家差异只体现在DDM校准系数上,后面我们会讲。

光模块的硬件接口,关键信号就这几个:

  • TX_FAULT:发射故障指示,高电平表示激光器异常
  • TX_DISABLE:发射关断控制,高电平关断激光器
  • MOD_DEF0/1/2:I2C总线(SCL、SDA)和模块存在检测
  • RX_LOS:接收信号丢失指示

嗯,这里要注意:MOD_DEF0是接地引脚,用来检测模块是否在位。如果这个引脚被拉高,说明模块没插好或者没供电。我在调试时经常先量这个引脚,能快速定位问题。

4.2 I2C总线驱动

I2C总线是光模块通信的“高速公路”。EPON ONU主控芯片通常有硬件I2C控制器,但我们做嵌入式软件,很多时候得用GPIO模拟I2C。为什么?因为硬件I2C的时序在某些主控上有bug,或者引脚被复用了。

我建议你先写好GPIO模拟I2C的底层驱动,再封装一层标准读写接口。这样不管换什么主控,移植起来都很快。

下面是我常用的I2C读写函数原型:

/* I2C底层操作 */
int i2c_start(void);
int i2c_stop(void);
int i2c_write_byte(uint8_t data);
uint8_t i2c_read_byte(int ack);

/* 光模块专用读写 */
int sfp_read(uint8_t dev_addr, uint8_t reg_addr, uint8_t *buf, uint16_t len);
int sfp_write(uint8_t dev_addr, uint8_t reg_addr, uint8_t *buf, uint16_t len);

这里有个坑:光模块的I2C地址是0xA0(写)和0xA1(读),但有些模块内部还有第二地址0xA2(用于DDM扩展寄存器)。我曾经在项目里忘了区分这两个地址,结果读出来的温度值全是0xFF,排查了半天才发现是地址搞错了。

注意:I2C时序要严格遵循SFF-8472规范。时钟频率不要超过400kHz,否则部分老模块会通信失败。另外,每次读写之间要加至少5ms的延时,给模块内部处理时间。

4.3 数字诊断监控(DDM)实现

DDM是光模块的“体检报告”。SFF-8472标准定义了A0h和A2h两个地址空间:

  • A0h:基础信息区,存放厂商、型号、序列号、速率等
  • A2h:诊断监控区,存放实时温度、电压、偏置电流、发射/接收光功率

我画了一张图,帮你理清DDM的数据结构:

SFF-8472 DDM 数据结构 A0h 地址空间(基础信息) • 字节 0-63: 厂商信息(SFP MSA) • 字节 64-95: 扩展信息(速率、距离等) • 字节 96-127: 厂商自定义 • 字节 128-255: 保留/校验 A2h 地址空间(DDM监控) • 字节 96-97: 温度(16位有符号数) • 字节 98-99: 供电电压(16位) • 字节 100-101: 偏置电流(16位) • 字节 102-103: 发射光功率(16位) • 字节 104-105: 接收光功率(16位) DDM 数据读取流程 1. 读取 A0h 字节 0-63,验证模块类型和速率 2. 读取 A2h 字节 96-105,获取原始监控数据 3. 根据校准类型(内部/外部)进行换算 4. 与阈值寄存器比较,判断告警状态 5. 上报给上层协议栈或网管系统

4.4 光功率与温度读取

终于到了最核心的部分——怎么把原始寄存器值换算成我们能看懂的温度和光功率。

先说温度读取。温度值存放在A2h地址的字节96-97,格式是16位有符号数,单位是1/256℃。换算公式很简单:

/* 温度换算 */
int16_t raw_temp = (buf[96] << 8) | buf[97];
float temperature = raw_temp * (1.0f / 256.0f);  // 单位:℃

举个例子:如果读到0x1A 0x00,换算后就是26.0℃。如果读到0xFF 0x80,那就是-0.5℃。嗯,这个负数表示法用的是补码,C语言里直接转int16_t就行。

再说光功率读取。这里有个关键点:光模块分内部校准和外部校准两种

  • 内部校准:模块自己算好了,直接读出来就是mW或dBm值
  • 外部校准:需要读取校准系数,自己手动换算

怎么区分?看A0h地址的字节92的bit 3。如果为1,就是内部校准;为0,就是外部校准。

我建议你统一按外部校准处理,因为内部校准的模块其实也是用同样的换算公式,只是系数由厂家预置好了。下面是我项目里用的换算代码:

/* 光功率换算(单位:mW) */
float sfp_calc_rx_power(uint8_t *buf)
{
    uint16_t raw = (buf[104] << 8) | buf[105];
    float power_mw;
    
    if (internal_calibration) {
        /* 内部校准:直接按公式换算 */
        power_mw = raw * 0.1f;  // 单位0.1µW,转成mW
    } else {
        /* 外部校准:读取A2h字节56-91的系数 */
        float rx_power_4 = read_cal_coeff(56);  // 4阶系数
        float rx_power_3 = read_cal_coeff(60);
        float rx_power_2 = read_cal_coeff(64);
        float rx_power_1 = read_cal_coeff(68);
        float rx_power_0 = read_cal_coeff(72);
        
        power_mw = rx_power_4 * pow(raw, 4) +
                   rx_power_3 * pow(raw, 3) +
                   rx_power_2 * pow(raw, 2) +
                   rx_power_1 * raw +
                   rx_power_0;
    }
    
    return power_mw;
}
经验之谈:实际项目中,我很少用外部校准的4阶多项式。因为大部分模块的线性度很好,直接用线性插值就够了。而且浮点运算在MCU上很慢,我一般会转成定点数计算,或者查表。

最后说说告警阈值。SFF-8472在A2h地址的字节0-55定义了各种阈值:温度高/低告警、电压高/低告警、光功率高/低告警等。我建议你至少实现接收光功率的低告警检测,因为这是判断光路是否正常的关键指标。

我曾经在一个项目中,光模块的接收光功率阈值设得太宽松,导致光缆断了都没告警。后来我把阈值改成了-28dBm(对应灵敏度附近),问题才解决。嗯,这个值要根据实际光模块的规格书来调。

核心要点回顾:
  • I2C驱动要稳定,注意时序和地址区分
  • DDM数据在A2h地址空间,温度是16位有符号数
  • 光功率换算要区分内部/外部校准
  • 告警阈值要结合实际光模块规格设置

好了,光模块驱动这块就讲到这里。代码写好了,记得用示波器抓一下I2C波形,确认时序没问题。我每次调试新模块,都会先读一下厂商信息(A0h字节0-63),能读出来就说明通信通了,后面的事就好办了。

专注资料整理