
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 字节是基础信息区大致分布如下字节地址字段含义说明0Identifier标识类型SFP 通常为 0x031Ext Identifier扩展标识2Connector连接器类型如 LC 为 0x073~10Transceiver速率、波长等编码11Encoding编码方式12BR Nominal标称速率单位 100Mbps13Reserved保留14~19Length支持的传输距离20~35Vendor Name厂商名称16 字节 ASCII36Reserved保留37~39Vendor OUI厂商 OUI40~55Vendor PN厂商型号16 字节 ASCII56~59Vendor Rev版本号60~61Wavelength波长62Reserved保留63CC_BASE0~62 字节的校验和64~65Options选项位66BR Max最高速率67BR Min最低速率68~83Vendor SN序列号16 字节 ASCII84~91Date Code生产日期92Diagnostic Monitoring Type诊断监控类型93Enhanced Options增强选项94SFF-8472 Compliance合规版本95CC_EXT64~94 字节的校验和96~127Vendor 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、上行 1310nmGPON或者下行 1577nm、上行 1270nmXGS-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 地址通常是 0xA07 位地址部分模块的 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 4d0; localparam START 4d1; localparam SEND_ADDR 4d2; localparam ACK1 4d3; localparam SEND_REG 4d4; localparam ACK2 4d5; localparam SEND_DATA 4d6; localparam ACK3 4d7; localparam STOP 4d8; localparam RESTART 4d9; localparam READ_DATA 4d10; localparam NACK 4d11; 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 : 1bz; assign scl scl_out; // 时钟分频产生 I2C 时序 // 假设系统时钟 50MHzI2C 速率 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 8d0; scl_en 1b0; end else begin if (clk_div 8d249) begin clk_div 8d0; scl_en 1b1; end else begin clk_div clk_div 1b1; scl_en 1b0; end end end // 主状态机 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; done 1b0; sda_oe 1b0; scl_out 1b1; end else begin case (state) IDLE: begin done 1b0; sda_oe 1b1; sda_out 1b1; scl_out 1b1; if (start) state START; end START: begin // SCL 高时 SDA 拉低 sda_out 1b0; state SEND_ADDR; bit_cnt 4d7; shift_reg {dev_addr, 1b0}; // 写操作 end SEND_ADDR: begin if (scl_en) begin scl_out 1b0; sda_out shift_reg[7]; shift_reg {shift_reg[6:0], 1b0}; if (bit_cnt 4d0) state ACK1; else bit_cnt bit_cnt - 1b1; end else begin scl_out 1b1; end end ACK1: begin sda_oe 1b0; // 释放 SDA 读 ACK if (scl_en) begin scl_out 1b0; state SEND_REG; bit_cnt 4d7; shift_reg reg_addr; sda_oe 1b1; end else begin scl_out 1b1; end end // 后续状态类似依次发送寄存器地址、数据 // 读操作需要 RESTART 后重新发送地址加读位 // 此处省略重复的移位逻辑 STOP: begin sda_out 1b0; scl_out 1b1; sda_out 1b1; state IDLE; done 1b1; 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(idVendorVID, idProductPID) 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, errorsignore).strip()) # 读取 Vendor PN地址 40~55 vendor_pn i2c_read(0x50, 40, 16) print(Vendor PN:, bytes(vendor_pn).decode(ascii, errorsignore).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 全 FFI2C 通信失败检查上拉电阻和接线修复硬件连接部分字段读出来是乱码地址偏移搞错对照规格书确认偏移修正地址批量写码后部分模块异常模块个体差异或写坏逐个读取验证重写或更换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 了但速率协商一直有问题最后发现是模块的速率字段跟口的能力不匹配。这种就是典型的只关注兼容码、忽略了物理参数匹配。所以每次改码之前先把模块的规格跟设备口的要求对一遍确认物理层能对上再动兼容码。