背景

最近在实现一个需求,在DPDK层面,获取到光模块光衰信息。方案上其实也就是和内核里面的网卡驱动获取DDM信息的手段一致,读取相关寄存器就行了。

DDM

DDM全称是Digital Diagnostic Monitoring,中文一般叫数字诊断监控。支持DDM的光模块会把自身运行状态暴露出来,主机侧可以通过I2C等管理接口读取这些诊断数据。

常见的DDM信息包括:

  • 温度:光模块当前工作温度。
  • 电压:光模块供电电压。
  • Tx Bias:激光器偏置电流。
  • Tx Power:发射光功率。
  • Rx Power:接收光功率。

其中和光衰最直接相关的是Tx Power和Rx Power。Tx Power表示本端光模块发出去的光功率,Rx Power表示本端光模块收到对端的光功率。正常情况下,链路两端都可以读取各自模块的DDM信息,因此可以分别观察本端发光、收光以及对端发光、收光是否在合理范围内。

在Linux内核里,网卡驱动通常不会自己解释所有光模块诊断字段,而是通过ethtool相关接口把EEPROM或诊断页数据暴露给用户态。用户态工具再按照SFF-8472、SFF-8636等规范解析出温度、电压、发射功率、接收功率等信息。

Linux内核读取方式

以Intel i40e驱动为例,内核侧读取光模块信息的实现位于drivers/net/ethernet/intel/i40e/i40e_ethtool.c。驱动通过ethtool_ops把模块读取能力注册给内核ethtool框架:

1
2
3
4
5
6
static const struct ethtool_ops i40e_ethtool_ops = {
...
.get_module_info = i40e_get_module_info,
.get_module_eeprom = i40e_get_module_eeprom,
...
};

用户态执行类似下面的命令时:

1
ethtool -m eth0

调用链大致是:

1
2
3
4
5
6
7
8
ethtool用户态工具
-> 内核ethtool框架
-> netdev->ethtool_ops->get_module_info
-> netdev->ethtool_ops->get_module_eeprom
-> i40e_get_module_info()
-> i40e_get_module_eeprom()
-> i40e_aq_get_phy_register()
-> 外部光模块EEPROM/DDM页

i40e_get_module_info()主要做两件事:

  • 检查固件是否支持读取模块EEPROM,例如是否具备I40E_HW_CAP_AQ_PHY_ACCESS能力。
  • 根据hw->phy.link_info.module_type[0]判断模块类型,并设置modinfo->typemodinfo->eeprom_len

对于SFP模块,驱动会读取SFF-8472相关字节,判断模块是否支持DDM。如果模块不支持SFF-8472或没有实现DDM,则按ETH_MODULE_SFF_8079处理;如果支持DDM,则设置为ETH_MODULE_SFF_8472。对于QSFP+和QSFP28,驱动会根据revision字段判断使用ETH_MODULE_SFF_8436还是ETH_MODULE_SFF_8636

i40e_get_module_eeprom()负责真正读取EEPROM内容。它会根据模块类型选择不同的访问地址和页偏移:

  • SFP模块通常访问I2C地址0xA0,当offset超过基础页长度后切到0xA2读取诊断页。
  • QSFP类模块按页组织读取,驱动根据offset计算页号和页内偏移。

最终每个字节通过i40e_aq_get_phy_register()从外部模块读出,填入ethtool框架传进来的buffer。这里驱动仍然只是把模块原始数据交给上层;温度、电压、Tx Power、Rx Power等字段的展示解析,主要由用户态ethtool按照模块规范完成。

i40e_aq_get_phy_register()这个接口名字里带aq,指的是Admin Queue。它不是CPU直接去bit-bang I2C,而是驱动构造一条网卡固件命令,通过Admin Send Queue交给网卡固件执行。源码里i40e_aq_get_phy_register()是一个简单宏,实际调用i40e_aq_get_phy_register_ext()

1
2
3
4
5
6
i40e_aq_get_phy_register()
-> i40e_aq_get_phy_register_ext()
-> i40e_fill_default_direct_cmd_desc(..., i40e_aqc_opc_get_phy_register)
-> i40e_asq_send_command()
-> Admin Queue
-> firmware访问外部PHY或光模块

i40e_aq_get_phy_register_ext()会把这些参数填进i40e_aqc_phy_register_access命令结构:

  • phy_interface:选择访问对象,例如I40E_AQ_PHY_REG_ACCESS_EXTERNAL_MODULE表示外部光模块。
  • dev_address:设备地址。SFP场景下可以理解为I2C EEPROM地址,例如0xA00xA2
  • reg_address:模块页内偏移。
  • cmd_flags:控制是否切换QSFP页等行为。

然后驱动通过i40e_asq_send_command()把descriptor放到Admin Send Queue,更新队列tail寄存器通知硬件。固件处理完成后会把结果写回descriptor,驱动再从cmd->reg_value取出读到的值。对于光模块EEPROM读取,i40e_get_module_eeprom()就是循环调用这个接口,每次读一个offset,最终拼出完整的EEPROM/DDM buffer。

i40e中QSFP Page的含义

40G QSFP/QSFP+模块通常按照SFF-8436或SFF-8636组织EEPROM数据。和SFP的0xA00xA2两个I2C地址不同,QSFP更多是一个低128字节区域加多个上半页,也就是lower page和upper page的结构。

在i40e驱动里,QSFP类模块的最大读取长度定义为:

1
#define I40E_MODULE_QSFP_MAX_LEN 640

这640字节可以按下面方式理解:

1
2
3
4
5
offset 0   - 127 : lower page 00h
offset 128 - 255 : upper page 00h
offset 256 - 383 : upper page 01h
offset 384 - 511 : upper page 02h
offset 512 - 639 : upper page 03h

i40e_get_module_eeprom()里对QSFP的offset处理也体现了这个映射:

1
2
3
4
while (offset >= ETH_MODULE_SFF_8436_LEN) {
offset -= ETH_MODULE_SFF_8436_LEN / 2;
addr++;
}

这里ETH_MODULE_SFF_8436_LEN是256,所以超过第一个256字节后,每跨过128字节,addr就递增一次。对于QSFP场景,这里的addr可以理解为要访问的上半页编号:

1
2
3
4
原始offset 0   - 255 -> addr = 0, offset = 0   - 255
原始offset 256 - 383 -> addr = 1, offset = 128 - 255
原始offset 384 - 511 -> addr = 2, offset = 128 - 255
原始offset 512 - 639 -> addr = 3, offset = 128 - 255

也就是说,i40e最终读到的QSFP buffer不是简单的连续物理地址,而是驱动把不同page拼接成一个线性buffer后交给上层。

各page的常见内容大致如下:

  • lower page 00h:模块状态、告警标志、实时监控值,例如温度、电压、各lane的Tx Bias、Tx Power、Rx Power等。
  • upper page 00h:模块基础信息,例如Identifier、Connector、速率能力、Vendor Name、Vendor PN、Vendor SN、Revision等。
  • upper page 01h:扩展能力或应用相关信息,例如不同速率/编码模式的能力描述。具体内容和模块规范版本有关。
  • upper page 02h:用户可写EEPROM或厂商自定义区域,很多模块里这页内容不一定有统一语义。
  • upper page 03h:阈值、控制和mask类信息,常见包括温度、电压、光功率、电流等告警/告警恢复阈值,以及部分通道控制字段。

这也解释了排查时看到的现象:如果page1page2page3读出来完全一样,通常说明页切换或页访问没有真正生效。对于DDM来说,尤其要关注lower page 00h里的实时值,以及upper page 03h里的阈值。如果page 03h读错,解析出来的告警上限、下限就可能非常离谱。

DPDK调用方式

在DPDK里,应用层一般不直接调用i40e PMD内部的i40e_get_module_eeprom()。DPDK上层已经通过ethdev做了一层统一封装,应用侧调用rte_eth_*接口,具体由支持该能力的PMD去实现。

  • rte_eth_dev_get_module_info():获取光模块EEPROM类型和长度。
  • rte_eth_dev_get_module_eeprom():读取光模块EEPROM数据。

DPDK的examples/ethtool里又基于ethdev封了一层更接近Linux ethtool语义的接口:

  • rte_ethtool_get_module_info()
  • rte_ethtool_get_module_eeprom()

调用链大致是:

1
2
3
4
5
APP
-> rte_eth_dev_get_module_info()
-> rte_eth_dev_get_module_eeprom()
-> dev_ops->get_module_info / dev_ops->get_module_eeprom
-> i40e / ice / igb等PMD内部实现

对于i40e网卡,这两个API最终会走到drivers/net/i40e/i40e_ethdev.c里注册的dev_ops回调。ice、igb等驱动如果实现了同样的dev_ops,上层调用方式也是一致的;如果某个PMD或硬件不支持,则会返回-ENOTSUP

这和Linux内核里的ethtool路径是对应关系:内核是netdev->ethtool_ops->get_module_info/get_module_eeprom,DPDK是rte_eth_dev_get_module_info/get_module_eeprom -> dev_ops。两者上层框架不同,但驱动内部要解决的问题类似,都是判断模块类型、读取对应EEPROM/DDM页,再把原始数据交给上层解析。

大致调用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdint.h>
#include <string.h>

#include <rte_ethdev.h>

static int
read_module_eeprom(uint16_t port_id)
{
struct rte_eth_dev_module_info modinfo;
struct rte_dev_eeprom_info einfo;
uint8_t buf[512];
int ret;

ret = rte_eth_dev_get_module_info(port_id, &modinfo);
if (ret < 0)
return ret;

memset(&einfo, 0, sizeof(einfo));
einfo.offset = 0;
einfo.length = modinfo.eeprom_len > sizeof(buf) ?
sizeof(buf) : modinfo.eeprom_len;
einfo.data = buf;

ret = rte_eth_dev_get_module_eeprom(port_id, &einfo);
if (ret < 0)
return ret;

/*
* buf里是光模块EEPROM原始数据。
* 后续需要根据modinfo.type判断模块规范,再按SFF-8472、
* SFF-8636等格式解析温度、电压、Tx Power、Rx Power。
*/
return 0;
}

这里需要注意,ethdev这层API统一解决的是“向支持的PMD读取光模块信息”的问题。rte_eth_dev_get_module_eeprom()拿到的是模块EEPROM/DDM原始数据;如果上层工具或业务代码已经集成了解析逻辑,就可以直接展示温度、电压、Tx Power、Rx Power等DDM字段。光衰仍然不是驱动直接返回的单个字段,需要在拿到Tx Power和Rx Power之后再计算或对比,并且最好结合链路两端的数据一起判断。

DDM数据解析

DDM数据解析一般不在PMD驱动内部完成。PMD负责把模块EEPROM或诊断页读出来,上层再根据模块类型选择对应规范解析字段。

整体分层可以理解为:

1
2
3
PMD驱动:读取原始字节
ethdev:提供统一API
上层工具/业务代码:按规范解析DDM字段

解析的第一步是判断模块类型。rte_eth_dev_get_module_info()返回的typeeeprom_len可以用来区分后续该按哪类规范处理:

  • SFP/SFP+模块通常按SFF-8472解析。
  • QSFP/QSFP+模块通常按SFF-8636解析。
  • QSFP-DD、OSFP等新模块还可能涉及CMIS。

真正解析时,本质上就是从EEPROM buffer的固定偏移位置取出原始值,再按照规范定义的单位和比例因子换算成可读值。常见字段包括:

  • 温度:通常是有符号定点数,需要换算成摄氏度。
  • 电压:通常以固定步进表示,需要换算成V。
  • Tx Bias:激光器偏置电流,通常换算成mA。
  • Tx Power:发射光功率,通常先得到uW,再换算成dBm。
  • Rx Power:接收光功率,通常先得到uW,再换算成dBm。

光功率字段常见的换算逻辑是先得到微瓦值,再转成dBm:

1
dBm = 10 * log10(uW / 1000)

例如解析出来的接收光功率是500 uW,那么对应功率大约是:

1
10 * log10(500 / 1000) = -3.01 dBm

所以代码实现上通常会拆成两部分:

1
2
3
4
5
6
7
8
9
read_module_eeprom()
-> 调用rte_eth_dev_get_module_info()
-> 调用rte_eth_dev_get_module_eeprom()
-> 拿到原始EEPROM/DDM buffer

parse_module_ddm()
-> 根据module_info.type选择解析格式
-> 从固定offset读取原始字段
-> 按规范换算成温度、电压、Tx Power、Rx Power

这样做的好处是驱动适配和数据解释解耦。i40e、ice、igb等PMD只要实现ethdev的模块读取回调,上层就可以复用同一套DDM解析逻辑。

光衰

光衰本质上是光信号在传输路径上的功率损耗。对于一条光纤链路来说,可以粗略理解为:

1
光衰 = 发射光功率 - 接收光功率

实际排查时不能只看单端的Rx Power,因为接收光功率低可能有多种原因:

  • 对端光模块本身发光弱。
  • 光纤距离过长或链路损耗过大。
  • 光纤端面脏污、弯折、接头接触不良。
  • 本端光模块接收能力异常。
  • 模块类型、速率、波长或单多模光纤不匹配。

所以判断光衰时,一般需要结合两端DDM一起看。本端的Rx Power偏低,只能说明本端收到的光弱;还需要继续确认对端Tx Power是否正常,以及中间链路是否存在额外损耗。

问题

完成开发,进行自测的时候,发现其他模块都正常,但是40G的模块,读取出来的DDM信息存在问题

  1. DDM数据差异很大,告警上限阈值比下限阈值还低,并且数据很离谱
  2. 同一张网卡,同样的固件,存在能正常读取的模块,但是也存在读取的模块
  3. 不能正常读取的模块占多数,读取出来的都是异常的数据

排查

  1. 使用内核反接管网卡,用ethool输出,和dpdk上输出的内容一致。
  2. 内核使用的内部定制的内核,为了确认排除是不是我们自己的定制内核有问题,安装了ubuntu26(7.x的内核)还是一样的输出
  3. 将有问题的模块,插入交换机,用交换机的DDM查询接口,查询的信息有正常
  4. ethtool按字节输出,发现page0之后的1,2,3三个下页输出的信息完全一样

结论

没有结论

  1. 光模块本身模块存在问题,没有page03的信息,查找了规范,确认不实现page03的数据,也是能符合规范的。同时也不能确实保证测试用的模块就是正规的光模块。
  2. 实现的协议有问题,对于特定厂商的模块,厂商不符合规范,对于DDM有自己的一套解析协议。

现在最大问题是,如果模块真没写数据,那么手上的大部分模块都不符合规范。如果符合规范,我们按照dpdk以及Linux内核那套去解析,似乎还是有问题。具体还要继续排查吧。