1. 光模块兼容码到底是什么东西
1.1 从一个真实场景说起
你从仓库翻出一只第三方光模块,外观、波长、传输距离都跟原厂那只一模一样,插进交换机光口,指示灯死活不亮。换回原厂模块,秒亮。这时候你大概率会骂一句“又被锁了”。这个“锁”,锁的就是兼容码。
光模块兼容码,本质上是写在模块内部一颗小容量存储芯片里的一串标识数据。这颗芯片通常是EEPROM,遵循 I2C 总线读写,地址一般是 0xA0(部分双地址模块还有 0xA2 页)。交换机、OLT、路由器这些设备在光口初始化阶段,会先通过 I2C 去读这颗 EEPROM,把里面的厂商信息、型号、序列号、支持的速率和距离等字段读出来,然后跟设备固件里预置的白名单做比对。比对通过,才允许这个口进入正常工作状态;比对不通过,口就保持 down,或者虽然 link 起来了但业务不通。
所以“兼容码”不是一个单一字段,而是一整套被设备认可的标识集合。行业里常说的“写码”“改码”“刷码”,指的就是用工具去改写这颗 EEPROM 里的内容,让它看起来像设备认可的那只模块。
1.2 兼容码里到底存了哪些字段
不同封装(SFP、SFP+、QSFP+、QSFP28、QSFP-DD 等)的 EEPROM 映射不一样,但核心字段大同小异。以最常见的 SFP/SFP+ 为例,A0 页 0~127 字节是基础信息区,大致分布如下:
| 字节地址 | 字段含义 | 说明 |
|---|---|---|
| 0 | Identifier | 标识类型,SFP 通常为 0x03 |
| 1 | Ext Identifier | 扩展标识 |
| 2 | Connector | 连接器类型,如 LC 为 0x07 |
| 3~10 | Transceiver | 速率、波长等编码 |
| 11 | Encoding | 编码方式 |
| 12 | BR Nominal | 标称速率,单位 100Mbps |
| 13 | Reserved | 保留 |
| 14~19 | Length | 支持的传输距离 |
| 20~35 | Vendor Name | 厂商名称,16 字节 ASCII |
| 36 | Reserved | 保留 |
| 37~39 | Vendor OUI | 厂商 OUI |
| 40~55 | Vendor PN | 厂商型号,16 字节 ASCII |
| 56~59 | Vendor Rev | 版本号 |
| 60~61 | Wavelength | 波长 |
| 62 | Reserved | 保留 |
| 63 | CC_BASE | 0~62 字节的校验和 |
| 64~65 | Options | 选项位 |
| 66 | BR Max | 最高速率 |
| 67 | BR Min | 最低速率 |
| 68~83 | Vendor SN | 序列号,16 字节 ASCII |
| 84~91 | Date Code | 生产日期 |
| 92 | Diagnostic Monitoring Type | 诊断监控类型 |
| 93 | Enhanced Options | 增强选项 |
| 94 | SFF-8472 Compliance | 合规版本 |
| 95 | CC_EXT | 64~94 字节的校验和 |
| 96~127 | Vendor Specific | 厂商自定义区 |
设备读的时候,重点看的是 Vendor Name、Vendor PN、Vendor OUI 这几个字段,有些设备还会校验 CC_BASE 和 CC_EXT 两个校验和。校验和不对,直接判定模块非法。这就是为什么你手动改了几个字节,模块反而彻底不认了——校验和没重算。
1.3 为什么设备厂商要搞兼容码这套机制
从设备厂商的角度,锁兼容码有几个现实理由。一是保证整网质量,光模块是链路里最容易出问题的环节,原厂模块经过匹配测试,出故障时责任清晰。二是商业利益,光模块是耗材,原厂模块毛利高,锁码能保住这块收入。三是技术支持成本,如果随便什么模块都能插,出了问题设备厂商要花大量精力去排查到底是设备问题还是模块问题。
从用户角度,原厂模块价格往往是第三方的三到十倍,大批量部署时成本差距非常可观。所以第三方模块市场一直存在,兼容码改写也就成了刚需。这里要说明的是,改写兼容码这个操作本身是模块厂商和集成商在做的常规工作,普通用户拿到手的第三方模块通常已经写好了对应设备的码,插上就能用。真正需要自己动手改码的场景,一般是库存模块要适配新设备、或者批量模块的码写错了需要返工。
2. 第三方模块插进 OLT 和交换机,到底能不能用
2.1 交换机侧的兼容逻辑
交换机对光模块的兼容策略,不同品牌、不同型号、不同固件版本差异很大。大致可以分成三档。
第一档是完全不锁。这类设备读 EEPROM 只是为了获取模块的速率、波长、距离信息,用来配置口的参数,不校验厂商字段。你插任何符合标准的模块都能用。一些白牌交换机、部分早期型号就属于这类。
第二档是软锁。设备会读厂商字段,如果不在白名单里,会打一条告警日志,但口仍然能 up,业务也能通。这种设计是给用户一个提醒,但不强制阻断。很多中端交换机是这个策略。
第三档是硬锁。厂商字段不在白名单,口直接不 up,或者 up 了但流量不通。这种就是真正意义上的“锁码”。高端交换机、OLT 设备上比较常见。
判断你的设备属于哪一档,最直接的办法就是插一只已知非原厂的模块上去,看口的状态和日志。如果口 up 了但有告警,就是软锁;如果口完全不 up,就是硬锁。
2.2 OLT 侧的特殊性
OLT 跟普通交换机不太一样。OLT 的 PON 口下面挂的是 ONU,涉及测距、授权、加密等一系列流程。OLT 对光模块的兼容码校验通常比交换机更严格,因为 PON 口的发光时序、接收灵敏度要求更精确。有些 OLT 不仅校验 EEPROM 里的厂商字段,还会校验模块的校准参数(A2 页里的 DDMI 校准数据),如果校准数据跟 OLT 预期的格式对不上,即使厂商字段写对了,口也可能起不来。
另外 OLT 的 PON 口光模块跟普通以太网光模块在波长、发光功率、接收灵敏度上都不一样。PON 口下行 1490nm、上行 1310nm(GPON),或者下行 1577nm、上行 1270nm(XGS-PON)。你拿一只普通千兆以太网光模块插 OLT 的 PON 口,即使兼容码写对了,物理层也对不上,根本不可能通。这一点经常被混淆,需要区分清楚:兼容码解决的是“设备认不认这只模块”,物理参数解决的是“这只模块能不能干这个活”。
2.3 实际兼容性速查
下面这张表是我在实际项目中总结的常见组合,供参考。注意固件版本会影响结果,同一型号不同版本可能表现不同。
| 设备类型 | 模块类型 | 兼容码匹配 | 物理参数匹配 | 实际可用性 |
|---|---|---|---|---|
| 千兆交换机 | 第三方千兆 SFP | 是 | 是 | 可用 |
| 千兆交换机 | 第三方千兆 SFP | 否 | 是 | 看锁定策略,软锁可用,硬锁不可用 |
| 万兆交换机 | 第三方万兆 SFP+ | 是 | 是 | 可用 |
| 万兆交换机 | 第三方千兆 SFP | 是 | 是 | 部分设备可用,速率协商可能有问题 |
| OLT PON 口 | 第三方 PON 模块 | 是 | 是 | 可用 |
| OLT PON 口 | 普通以太网模块 | 任意 | 否 | 不可用 |
| OLT 上行口 | 第三方万兆 SFP+ | 是 | 是 | 可用 |
3. 读写 EEPROM 的实操方法
3.1 硬件工具选型
要读写光模块的 EEPROM,你需要一套 I2C 读写工具。市面上常见的方案有几类。
第一类是专用的光模块编程器,比如一些厂商做的 USB 转 I2C 编程器,配套上位机软件,直接选模块类型、填字段、点写入。这类工具上手快,适合批量操作,但价格相对高,而且软件通常是闭源的,字段映射固定。
第二类是通用 I2C 编程器,比如基于 CH341、FT232 等芯片的 USB 转 I2C 小板,配合开源软件使用。这类方案灵活,价格低,但需要自己配置寄存器地址和字段偏移,适合有一定动手能力的人。
第三类是用单片机自己搭,比如用 STM32 或 ESP32 写一段 I2C 读写程序,通过串口跟电脑通信。这个方案最灵活,可以完全自定义,但开发成本高。
我个人的建议是,如果只是偶尔改几只模块,用通用 I2C 编程器加开源软件就够了;如果是批量返工,专用编程器效率更高。
3.2 I2C 读写的基本原理
光模块的 EEPROM 挂在 I2C 总线上,设备侧是 I2C 主机,模块是 I2C 从机。SFP 模块的 I2C 地址通常是 0xA0(7 位地址),部分模块的 A2 页在 0xA2。读写流程如下:
写操作:主机发送起始条件,发送从机地址加写位,发送要写的字节地址,发送数据字节,发送停止条件。
读操作:主机发送起始条件,发送从机地址加写位,发送要读的字节地址,发送重复起始条件,发送从机地址加读位,读取数据字节,发送停止条件。
这里有个细节:SFP 的 EEPROM 通常是 256 字节一页,字节地址是 8 位的,所以读写时地址指针只发一个字节。但有些模块的 A2 页有 128 字节以上的数据,需要用页选择或者两字节地址,具体看模块规格。
3.3 用 Verilog 实现 I2C 读写 EEPROM
如果你是用 FPGA 做光模块测试或者批量烧录,用 Verilog 写一个 I2C 主机控制器是常见做法。下面给一个简化的状态机框架,说明核心逻辑。
module i2c_master ( input clk, input rst_n, input start, input [6:0] dev_addr, input [7:0] reg_addr, input [7:0] wr_data, output reg [7:0] rd_data, output reg done, inout sda, inout scl ); // 状态定义 localparam IDLE = 4'd0; localparam START = 4'd1; localparam SEND_ADDR = 4'd2; localparam ACK1 = 4'd3; localparam SEND_REG = 4'd4; localparam ACK2 = 4'd5; localparam SEND_DATA = 4'd6; localparam ACK3 = 4'd7; localparam STOP = 4'd8; localparam RESTART = 4'd9; localparam READ_DATA = 4'd10; localparam NACK = 4'd11; reg [3:0] state; reg [7:0] shift_reg; reg [3:0] bit_cnt; reg sda_out; reg sda_oe; reg scl_out; // SDA 三态控制 assign sda = sda_oe ? sda_out : 1'bz; assign scl = scl_out; // 时钟分频,产生 I2C 时序 // 假设系统时钟 50MHz,I2C 速率 100kHz // 分频系数 = 50_000_000 / (100_000 * 2) = 250 reg [7:0] clk_div; reg scl_en; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin clk_div <= 8'd0; scl_en <= 1'b0; end else begin if (clk_div == 8'd249) begin clk_div <= 8'd0; scl_en <= 1'b1; end else begin clk_div <= clk_div + 1'b1; scl_en <= 1'b0; end end end // 主状态机 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; done <= 1'b0; sda_oe <= 1'b0; scl_out <= 1'b1; end else begin case (state) IDLE: begin done <= 1'b0; sda_oe <= 1'b1; sda_out <= 1'b1; scl_out <= 1'b1; if (start) state <= START; end START: begin // SCL 高时 SDA 拉低 sda_out <= 1'b0; state <= SEND_ADDR; bit_cnt <= 4'd7; shift_reg <= {dev_addr, 1'b0}; // 写操作 end SEND_ADDR: begin if (scl_en) begin scl_out <= 1'b0; sda_out <= shift_reg[7]; shift_reg <= {shift_reg[6:0], 1'b0}; if (bit_cnt == 4'd0) state <= ACK1; else bit_cnt <= bit_cnt - 1'b1; end else begin scl_out <= 1'b1; end end ACK1: begin sda_oe <= 1'b0; // 释放 SDA 读 ACK if (scl_en) begin scl_out <= 1'b0; state <= SEND_REG; bit_cnt <= 4'd7; shift_reg <= reg_addr; sda_oe <= 1'b1; end else begin scl_out <= 1'b1; end end // 后续状态类似,依次发送寄存器地址、数据 // 读操作需要 RESTART 后重新发送地址加读位 // 此处省略重复的移位逻辑 STOP: begin sda_out <= 1'b0; scl_out <= 1'b1; sda_out <= 1'b1; state <= IDLE; done <= 1'b1; end default: state <= IDLE; endcase end end endmodule这段代码的核心思路是:用状态机控制 SCL 和 SDA 的时序,每个状态处理一个字节的发送或接收,bit_cnt 计数 8 位,scl_en 是分频后的时钟使能。实际使用时需要根据你的系统时钟频率调整分频系数,并且补全读操作的状态。
注意:I2C 总线上必须接上拉电阻,通常是 4.7kΩ 到 10kΩ。光模块内部一般已经有上拉,但如果你用外部编程器,最好确认一下。SDA 和 SCL 的走线尽量短,避免干扰。
3.4 用 Python 通过 USB 转 I2C 读写
如果你用的是 CH341 这类 USB 转 I2C 小板,可以用 Python 配合相关库来操作。下面是一个示例框架。
import usb.core import usb.util import time # CH341 的 VID/PID 根据具体型号不同 VID = 0x1A86 PID = 0x5512 dev = usb.core.find(idVendor=VID, idProduct=PID) if dev is None: raise ValueError("设备未找到") dev.set_configuration() def i2c_write(dev_addr, reg_addr, data): """向指定从机地址的寄存器写入一个字节""" # CH341 的 I2C 写命令格式因固件而异 # 这里给出通用思路,具体命令字节需参考芯片手册 cmd = [0xA0, dev_addr << 1, reg_addr, data] dev.write(0x02, cmd) time.sleep(0.01) def i2c_read(dev_addr, reg_addr, length): """从指定从机地址的寄存器读取多个字节""" # 先写寄存器地址 cmd = [0xA0, dev_addr << 1, reg_addr] dev.write(0x02, cmd) time.sleep(0.01) # 再读数据 result = dev.read(0x82, length) return list(result) # 读取 SFP 模块的 Vendor Name(地址 20~35) vendor_name = i2c_read(0x50, 20, 16) print("Vendor Name:", bytes(vendor_name).decode('ascii', errors='ignore').strip()) # 读取 Vendor PN(地址 40~55) vendor_pn = i2c_read(0x50, 40, 16) print("Vendor PN:", bytes(vendor_pn).decode('ascii', errors='ignore').strip()) # 读取 CC_BASE 校验和(地址 63) cc_base = i2c_read(0x50, 63, 1)[0] print("CC_BASE:", hex(cc_base))这里 0x50 是 7 位地址 0xA0 对应的 8 位写地址形式,具体取决于库的实现。实际使用时需要根据你的编程器文档调整命令格式。
3.5 校验和的计算
改完字段后必须重算校验和,否则设备读的时候校验不过。SFP 的 CC_BASE 是 0~62 字节所有数据的低 8 位累加和,CC_EXT 是 64~94 字节的低 8 位累加和。
def calc_checksum(data_bytes): """计算低 8 位累加和""" return sum(data_bytes) & 0xFF # 假设 eeprom 是 128 字节的列表 eeprom = [0] * 128 # ... 填充数据 ... eeprom[63] = calc_checksum(eeprom[0:63]) eeprom[95] = calc_checksum(eeprom[64:95])提示:有些模块的校验和算法不是简单累加,而是带进位或者异或,具体看模块规格。改之前最好先读一只原厂模块,验证你的校验和算法跟它一致。
4. 常见问题与排查技巧
4.1 写完码插上不亮,怎么排查
这是最常见的问题。排查顺序如下。
第一步,确认物理连接。模块有没有插到位,光口有没有灰尘,光纤跳线是不是好的。用一只已知好的模块插同一个口,确认口本身没问题。
第二步,读 EEPROM 确认写入成功。把模块拔下来,用编程器读一遍,看字段是不是你写进去的值。有时候写操作没成功,或者写到了错误的页。
第三步,检查校验和。如果校验和不对,设备会直接判定模块非法。重新算一遍 CC_BASE 和 CC_EXT。
第四步,确认设备锁定策略。如果字段和校验和都对,但口还是不亮,可能是设备硬锁,白名单里没有你写的厂商名。这时候需要换一个白名单里的厂商名,或者找设备的兼容列表。
第五步,看设备日志。交换机和 OLT 通常会在日志里打印模块不识别的告警,日志里会写明是哪个字段不匹配。这是最直接的线索。
4.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 口不 up,无告警 | 模块物理损坏或未插到位 | 换模块测试 | 更换模块 |
| 口不 up,有告警 | 兼容码不匹配 | 看日志确认哪个字段 | 改写对应字段 |
| 口 up 但业务不通 | 速率或波长不匹配 | 确认模块规格与口要求 | 更换正确规格模块 |
| 写码后模块彻底不认 | 校验和错误 | 重算校验和 | 修正校验和 |
| 读 EEPROM 全 FF | I2C 通信失败 | 检查上拉电阻和接线 | 修复硬件连接 |
| 部分字段读出来是乱码 | 地址偏移搞错 | 对照规格书确认偏移 | 修正地址 |
| 批量写码后部分模块异常 | 模块个体差异或写坏 | 逐个读取验证 | 重写或更换 |
4.3 几个容易踩的坑
第一个坑是地址混淆。SFP 的 A0 页 7 位地址是 0x50,但有些编程器软件用的是 8 位地址 0xA0,有些用 7 位 0x50。搞错了就读不到数据。写之前先用一只已知模块验证地址。
第二个坑是页切换。有些模块的 A2 页数据超过 128 字节,需要先写页选择寄存器再读。如果你直接读,读到的可能是第一页的数据,不是你要的。
第三个坑是写保护。部分模块的 EEPROM 有写保护引脚或者写保护寄存器,不解除写保护就写不进去。写之前先确认模块是否支持写入,有些原厂模块是只读的。
第四个坑是校验和算法不一致。不同厂商的校验和算法可能有细微差别,比如有的用带进位的累加,有的用异或。最稳妥的办法是读一只原厂模块,用它的数据反推算法。
第五个坑是固件版本差异。同一型号设备,不同固件版本的白名单可能不一样。今天能用的码,升级固件后可能就不认了。批量部署前最好在目标固件版本上验证。
4.4 实操心得
我在实际改码过程中总结了几条经验。一是先读后写,拿到一只模块先完整读一遍,保存原始数据,万一写坏了还能恢复。二是小批量验证,不要一上来就批量写,先写一只插到目标设备上验证,确认没问题再批量操作。三是记录对应关系,每只模块的序列号和写入的码要记录清楚,方便后续排查。四是注意静电,光模块是静电敏感器件,操作时戴防静电手环,工作台接地。五是不要频繁插拔,光口和模块的金手指都有插拔寿命,频繁插拔会导致接触不良。
还有一点,改码只是让设备“认”这只模块,不代表模块的实际性能跟原厂一致。第三方模块的发光功率、接收灵敏度、温度特性可能跟原厂有差异,在对链路质量要求高的场景下,还是要谨慎使用。我见过改码后链路能 up 但误码率偏高的案例,最后还是要换回原厂模块。
5. 兼容码之外的选型考量
5.1 什么时候该用第三方模块
第三方模块适合对成本敏感、对链路质量要求不是极端苛刻的场景。比如园区网接入层、监控网络、临时链路。这些场景下,链路的余量比较大,第三方模块的性能差异通常不会造成实际问题。
不适合的场景包括:核心骨干链路、长距离传输、对误码率和时延要求高的业务、以及设备厂商明确不支持的场景。这些场景下省下的模块钱,可能还不够排查故障的人力成本。
5.2 选第三方模块看什么
选第三方模块,兼容码只是第一关。还要看几个指标。一是发光功率和接收灵敏度,这两个决定了链路的功率预算。二是消光比,影响信号质量。三是温度范围,商业级 0~70℃,工业级 -40~85℃,根据部署环境选。四是DDMI 支持,支持数字诊断的模块能读温度、电压、发光功率、接收功率,方便运维。五是质保和一致性,批量采购时同一批次的模块性能要一致,否则链路质量参差不齐。
5.3 兼容码的边界
最后要说清楚,兼容码解决的是“设备认不认”的问题,不解决“模块好不好”的问题。一只模块即使兼容码写得完美,如果本身发光功率不够、接收灵敏度差、温度漂移大,链路照样出问题。所以改码是手段,不是目的。目的是让合适的模块在合适的设备上稳定工作。如果一只模块本身规格就不匹配,改码也救不了它。
我在实际项目中遇到过有人拿万兆模块改码插千兆口,码写对了,口也 up 了,但速率协商一直有问题,最后发现是模块的速率字段跟口的能力不匹配。这种就是典型的只关注兼容码、忽略了物理参数匹配。所以每次改码之前,先把模块的规格跟设备口的要求对一遍,确认物理层能对上,再动兼容码。