4、HLS实现8B/10B编码器:编码表映射、运行不一致(RD)控制、HLS代码实现与仿真
8B/10B编码,说白了就是把8位数据变成10位数据。为什么要多此一举?因为串行传输需要直流平衡,需要足够的跳变沿来恢复时钟。我当年第一次接触SerDes时,觉得这玩意儿就是查表,后来才发现,真正的坑在于运行不一致(Running Disparity,简称RD)的控制。
4.1 编码表映射:从8位到10位的魔法
8B/10B编码的核心思想,是把8位数据拆成3位和5位两部分,分别映射到4位和6位的编码。嗯,这里要注意,这个拆分是有讲究的——高位3位映射成4位,低位5位映射成6位。
编码表分为两部分:
- 5B/6B编码:将低5位(D0-D4)映射为6位编码
- 3B/4B编码:将高3位(D5-D7)映射为4位编码
每个数据字节对应两个编码版本:RD+(正极性)和RD-(负极性)。为什么需要两个版本?因为要保证编码后的数据流中0和1的数量尽量平衡。
关键点:编码表不是随便设计的。它保证了最大连续相同位数不超过5位,这就保证了接收端能可靠地恢复时钟。
我在项目中遇到过一个问题:直接用查表法实现编码,结果综合出来的LUT数量惊人。后来我改用组合逻辑直接计算,面积减少了40%。所以,查表虽好,但别滥用。
4.2 运行不一致(RD)控制:编码的灵魂
RD是什么?简单说,就是当前编码数据流中1比0多多少。RD为正表示1多,RD为负表示0多。
控制规则其实很简单:
- 如果当前RD为正,就选择RD-版本的编码(让1少一点)
- 如果当前RD为负,就选择RD+版本的编码(让1多一点)
- 如果编码本身是平衡的(0和1数量相等),则RD保持不变
我曾经踩过一个坑:RD更新时机搞错了。应该在编码完成后立即更新RD,而不是等到下一个时钟周期。否则会导致连续两个编码都选择同一个极性版本,直流平衡就崩了。
避坑指南:RD的初始值通常设为负(RD-)。这是标准做法,不要问我为什么,这是前辈们用血泪总结出来的经验。
4.3 HLS代码实现:从C到RTL的优雅转换
用HLS实现8B/10B编码器,核心就是两个函数:编码映射函数和RD控制函数。我个人习惯把编码表定义成数组,然后用组合逻辑实现查表。
先看编码映射的核心代码:
// 5B/6B编码表
const ap_uint<6> enc_5b6b_rd_plus[32] = {
0x19, 0x1B, 0x12, 0x13, 0x1D, 0x15, 0x16, 0x17,
0x1A, 0x0E, 0x0D, 0x0C, 0x1E, 0x1C, 0x1F, 0x0B,
0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03,
0x02, 0x01, 0x00, 0x0F, 0x1F, 0x0F, 0x1F, 0x0F
};
// 3B/4B编码表
const ap_uint<4> enc_3b4b_rd_plus[8] = {
0x9, 0xB, 0xA, 0xD, 0xC, 0xE, 0x7, 0x1
};
RD控制逻辑:
// 计算当前编码的极性
int calc_disparity(ap_uint<10> code) {
int ones = 0;
for (int i = 0; i < 10; i++) {
ones += code[i];
}
return (ones > 5) ? 1 : (ones < 5) ? -1 : 0;
}
// 选择编码版本
ap_uint<10> encode_8b10b(ap_uint<8> data, int &rd) {
ap_uint<6> code_6b;
ap_uint<4> code_4b;
// 根据RD选择编码版本
if (rd >= 0) {
code_6b = enc_5b6b_rd_minus[data.range(4,0)];
code_4b = enc_3b4b_rd_minus[data.range(7,5)];
} else {
code_6b = enc_5b6b_rd_plus[data.range(4,0)];
code_4b = enc_3b4b_rd_plus[data.range(7,5)];
}
// 更新RD
rd += calc_disparity(code_6b);
rd += calc_disparity(code_4b);
return (code_6b, code_4b);
}
优化技巧:HLS综合时,calc_disparity函数会被综合成组合逻辑。如果觉得面积太大,可以改成查表法——预计算所有1024种10位编码的极性值。
4.4 仿真验证:别让bug溜走
仿真验证8B/10B编码器,我一般会做三件事:
- 功能验证:遍历所有256种输入数据,检查编码是否正确
- RD验证:检查RD是否在合理范围内(通常-2到+2之间)
- 直流平衡验证:长时间运行,检查0和1的比例是否接近50%
仿真代码示例:
void test_8b10b_encoder() {
int rd = -1; // 初始RD为负
int total_ones = 0, total_zeros = 0;
for (int i = 0; i < 256; i++) {
ap_uint<8> data = i;
ap_uint<10> code = encode_8b10b(data, rd);
// 检查编码是否正确
assert(decode_10b8b(code) == data);
// 统计0和1的数量
for (int j = 0; j < 10; j++) {
if (code[j]) total_ones++;
else total_zeros++;
}
}
// 检查直流平衡
float ratio = (float)total_ones / (total_ones + total_zeros);
assert(abs(ratio - 0.5) < 0.01);
}
你想想看,如果直流平衡没做好,接收端的CDR(时钟数据恢复)电路就会出问题。我曾经在一个项目中,因为RD控制逻辑写错了,导致接收端频繁失锁,查了三天才找到原因。
4.5 知识体系总览
下面这张图展示了8B/10B编码器的完整知识体系:
这张图把8B/10B编码器的核心逻辑串起来了。从8位输入开始,经过5B/6B和3B/4B两级编码映射,再由RD状态机控制版本选择,最终输出10位编码。验证环节覆盖了功能、RD范围和直流平衡三个维度。
总结:8B/10B编码器看似简单,但RD控制是真正的难点。我建议你在实现时,先把编码表准备好,再写RD控制逻辑,最后做仿真验证。顺序别搞反了,否则debug会非常痛苦。
可可精品资料库