4. PCS层编码技术:64B/66B编码原理、加扰与解扰、同步头机制
各位同学,今天我们来聊聊PCS层里一个绕不开的话题——64B/66B编码。说实话,我刚接触10G光通信那会儿,觉得这玩意儿不就是把64位数据变成66位嘛,有啥好学的?后来在项目里被同步头问题折腾了整整两周,才明白这里面的门道有多深。
4.1 为什么是64B/66B?
先说说背景。早期的8B/10B编码,每8位数据要变成10位,开销是25%。到了10G时代,这个开销就有点吃不消了。你想想看,10G的线速率,光编码就浪费掉2.5G,太奢侈了。
64B/66B的开销只有3.125%。说白了,就是每64位数据只加2位同步头。效率高了很多。但代价是什么?同步和时钟恢复的难度变大了。8B/10B有足够的跳变来恢复时钟,64B/66B就不行了,所以需要加扰来保证数据有足够的跳变。
核心要点:64B/66B编码不是简单的数据扩展,而是一个包含同步、加扰、编码的完整机制。它的目标是:高带宽效率 + 可靠的时钟恢复 + 合理的误码检测能力。
4.2 同步头(Sync Header)机制
同步头是64B/66B编码里最基础的部分。每66位数据块的前2位就是同步头。它的取值只有两种:
| 同步头值 | 含义 | 说明 |
|---|---|---|
| 01 | 数据块 | 64位全部是数据 |
| 10 | 控制块 | 64位中包含控制信息 |
注意,00和11是非法值。一旦收到这两个值,接收端就知道出错了。我在项目中遇到过一个问题:某款光模块在链路建立阶段频繁报同步头错误,查了半天发现是PCB走线串扰导致同步头被翻转。嗯,从那以后我设计PCB时都会给同步头信号做单独包地处理。
同步头的作用有两个:
- 块同步:接收端通过搜索连续的合法同步头来锁定数据块的边界
- 类型识别:区分当前块是纯数据还是包含控制信息
为什么会用01和10而不是00和11?我个人的理解是:保证每个数据块至少有一次跳变。01有跳变,10也有跳变。这样即使数据部分全是0或全是1,接收端的CDR电路也能从同步头里提取时钟信息。
4.3 加扰(Scrambler)原理
加扰是64B/66B编码里最容易被忽视但又最关键的部分。它的目的是让数据看起来更随机,避免出现长串的0或长串的1。
10G光通信标准里用的是自同步加扰器,多项式是:
G(x) = 1 + x^39 + x^58
这个多项式是什么意思?说白了,就是当前输出等于输入和过去第39位、第58位输出的异或。用C语言实现的话:
// 64B/66B 加扰器实现(简化版)
uint64_t scrambler(uint64_t data, uint64_t *lfsr) {
uint64_t output = 0;
for (int i = 0; i < 64; i++) {
// 取当前数据位
uint8_t bit = (data >> i) & 1;
// 取LFSR的第39位和第58位
uint8_t s39 = (*lfsr >> 38) & 1; // 注意:位索引从0开始
uint8_t s58 = (*lfsr >> 57) & 1;
// 输出 = 数据位 XOR s39 XOR s58
uint8_t out_bit = bit ^ s39 ^ s58;
output |= ((uint64_t)out_bit << i);
// 更新LFSR
*lfsr = (*lfsr << 1) | out_bit;
}
return output;
}
这里有个坑要注意:加扰是在同步头之后进行的。也就是说,同步头不参与加扰,只有后面的64位数据被加扰。我刚开始做的时候没注意这个顺序,结果接收端怎么都解不出来,查了两天才发现是加扰范围搞错了。
个人经验:加扰器的初始状态在标准里没有强制规定,但实际项目中通常用全1初始化。如果你用全0初始化,加扰器输出就等于输入,等于没加扰。我曾经见过一个方案用全0初始化,结果长串0的问题根本没解决。
4.4 解扰(Descrambler)原理
解扰是加扰的逆过程。因为用的是自同步加扰器,解扰器不需要知道发送端的初始状态,只需要自己同步起来就行。
解扰多项式是:
H(x) = 1 + x^39 + x^58
你没看错,和加扰多项式一模一样。自同步加扰器的特点就是加扰和解扰用同一个多项式。实现代码:
// 64B/66B 解扰器实现(简化版)
uint64_t descrambler(uint64_t data, uint64_t *lfsr) {
uint64_t output = 0;
for (int i = 0; i < 64; i++) {
uint8_t bit = (data >> i) & 1;
uint8_t s39 = (*lfsr >> 38) & 1;
uint8_t s58 = (*lfsr >> 57) & 1;
// 解扰输出 = 接收数据 XOR s39 XOR s58
uint8_t out_bit = bit ^ s39 ^ s58;
output |= ((uint64_t)out_bit << i);
// 更新LFSR用的是接收数据,不是解扰后的数据
*lfsr = (*lfsr << 1) | bit;
}
return output;
}
注意看,LFSR更新用的是接收到的数据(bit),而不是解扰后的数据(out_bit)。这是自同步加扰器的关键特性——接收端不需要知道发送端的初始状态,只要收到58位数据,LFSR就能自动同步上。
避坑指南:我曾经在FPGA实现里犯过一个错误——把解扰器的LFSR更新用成了解扰后的数据。结果就是:前58位数据解出来全是错的,后面才慢慢正确。这个问题在仿真时没发现,因为仿真数据太短了。上板测试才发现链路建立时间比预期长了10倍。
4.5 64B/66B编码的整体流程
把上面这些串起来,一个完整的64B/66B编码流程是这样的:
- 数据分组:把MAC层送来的数据按64位一组切分
- 同步头插入:根据数据类型(数据块或控制块)插入01或10
- 加扰:对64位数据进行加扰,同步头保持不变
- 并串转换:把66位数据串行化发送出去
接收端反过来:
- 串并转换:接收串行数据,恢复出66位并行数据
- 同步头检测:搜索合法同步头,锁定块边界
- 解扰:对64位数据进行解扰
- 类型判断:根据同步头判断是数据块还是控制块,做相应处理
下面我画了一张流程图,把整个编码和解码的流程串起来:
4.6 实际项目中的注意事项
最后,我总结几个实际项目中容易踩的坑:
- 同步头检测的容错:链路刚建立时,同步头可能会有误码。我一般会设计一个状态机,连续收到4个合法同步头才确认同步,连续收到4个非法同步头才宣告失步。这个阈值可以根据链路质量调整。
- 加扰器的复位时机:链路复位时,加扰器要重新初始化。但注意,不要在每个数据块之间复位加扰器,否则加扰就失去了意义。
- 控制块的识别:同步头为10时,后面的64位数据里包含块类型字段(8位)和实际数据。不同的块类型对应不同的控制信息,比如空闲、本地错误、序列等。这块内容比较多,我们后面专门讲。
一个小技巧:调试64B/66B链路时,我习惯先看同步头的错误率。如果同步头错误率高于10⁻⁶,基本可以断定是物理层的问题(信号质量、时钟抖动等)。如果同步头错误率很低但数据解出来是乱的,那大概率是加扰/解扰的同步问题。
好了,64B/66B编码的核心内容就这些。说实话,这个编码方案看起来简单,但真正实现起来细节很多。尤其是加扰器和同步头检测的状态机,稍不注意就会出问题。建议各位在实现时多做仿真,特别是边界条件和异常情况的测试。
公众号:蓝海资料掘金营,微信deep3321