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,排查了半天才发现是地址搞错了。
4.3 数字诊断监控(DDM)实现
DDM是光模块的“体检报告”。SFF-8472标准定义了A0h和A2h两个地址空间:
- A0h:基础信息区,存放厂商、型号、序列号、速率等
- A2h:诊断监控区,存放实时温度、电压、偏置电流、发射/接收光功率
我画了一张图,帮你理清DDM的数据结构:
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;
}
最后说说告警阈值。SFF-8472在A2h地址的字节0-55定义了各种阈值:温度高/低告警、电压高/低告警、光功率高/低告警等。我建议你至少实现接收光功率的低告警检测,因为这是判断光路是否正常的关键指标。
我曾经在一个项目中,光模块的接收光功率阈值设得太宽松,导致光缆断了都没告警。后来我把阈值改成了-28dBm(对应灵敏度附近),问题才解决。嗯,这个值要根据实际光模块的规格书来调。
- I2C驱动要稳定,注意时序和地址区分
- DDM数据在A2h地址空间,温度是16位有符号数
- 光功率换算要区分内部/外部校准
- 告警阈值要结合实际光模块规格设置
好了,光模块驱动这块就讲到这里。代码写好了,记得用示波器抓一下I2C波形,确认时序没问题。我每次调试新模块,都会先读一下厂商信息(A0h字节0-63),能读出来就说明通信通了,后面的事就好办了。