简介:这份资料面向FPGA视频开发工程师、广播级板卡设计者及课程设计选题的学生,聚焦SD-SDI BT656格式下720×576i数字视频流的编码实现。内容系统梳理了576i行数据格式、帧场结构、消隐与显示起止行数、奇偶场行数范围,并详解EAV/SAV格式中F、V、H信号与P3~P0保护位的逻辑关系。在此基础上给出基于Verilog的完整编码设计,核心模块AV_GEN_CBB采用参数化写法,通过hcnt、vcnt计数器生成同步时序与10位BT656数据流,并已在ModelSim平台完成仿真验证,后续计划在ALTERA 5cefa5U19i7n+LMH0340平台做硬件验证。资源包为1个docx文档,约1.12MB,将参考资料与示例代码整合于一处,便于对照理解时序参数与代码实现。目前已有576人学习,适合作为视频类板卡开发与课程设计的实用参考。
1. 720×576i 的 SD-SDI 码流,为什么老工程师还在死磕 BT656
如果你手头有一块 FPGA 开发板、一个 SDI 输出接口,或者一台需要接入广播级设备的采集卡,那你大概率绕不开 SD-SDI 这个规格。SD-SDI 指的是标准清晰度数字分量串行接口,它把 ITU-R BT.656 并行视频数据加上嵌入音频和控制字,按 SMPTE 259M 的规范串行化后送到同轴电缆上。而 720×576i 这个分辨率,恰好是 PAL 制式下最经典的标清格式,帧率 25fps,隔行扫描,每帧两场,每场 288 行有效视频。
很多人第一次接触这个组合时会觉得“都什么年代了还搞标清”,但现实是大量演播室、医疗内窥、工业检测和航空显控设备至今仍在用 SD-SDI 做视频链路。原因很直接:线缆可以拉上百米、延迟确定、抗干扰强、生态成熟。你要做的核心工作,就是把 BT656 格式的并行数据流,按照 SD-SDI 的时序要求打包成串行码流。这篇文章会从 BT656 的数据结构讲起,一路落到编码器模块的代码实现、参数配置和调试手段,适合正在做视频接口的 FPGA 工程师、嵌入式开发者,以及需要理解 SDI 链路底层逻辑的测试人员。
2. BT656 的数据长什么样:从 EAV/SAV 到 YCbCr 4:2:2
2.1 一行 720 像素是怎么排布的
BT656 的核心思路是把同步信号嵌入到数据流里,不再需要单独的 HSYNC 和 VSYNC 引脚。对于 720×576i 的 PAL 制式,每一行的数据结构是:EAV(有效视频结束)→ 消隐区 → SAV(有效视频开始)→ 720 个像素的 YCbCr 数据。注意这里的像素时钟是 27MHz,但实际数据率是 27M 字节/秒,因为 YCbCr 4:2:2 格式下每个时钟周期传输两个字节:一个亮度 Y 和一个色度 Cb 或 Cr 交替。
一行完整的 BT656 数据包含 1440 个字节(720 像素 × 2 字节/像素),加上前后的同步字和消隐数据,总共 1728 个字节。这个数字怎么来的?PAL 制式的行周期是 64μs,27MHz 时钟下就是 1728 个时钟周期,每个周期传一个字节。所以一行就是 1728 字节,其中有效视频区占 1440 字节,消隐区占 288 字节。
同步字 EAV 和 SAV 都是 4 个字节的固定序列:FF 00 00 XY。前三个字节是固定的前导码,第四个字节 XY 携带了场标志、消隐标志和校验位。具体来说,XY 字节的 bit6 是 F(场标志,0 表示第一场,1 表示第二场),bit5 是 V(消隐标志,1 表示消隐区),bit4 是 H(0 表示 SAV,1 表示 EAV),低 4 位是保护位,由 F、V、H 计算得出。
2.2 隔行扫描下场序和消隐区的坑
720×576i 是隔行扫描,每帧分两场,第一场包含奇数行,第二场包含偶数行。BT656 里用 F 位来标识当前场,但很多初学者会在这里翻车:F 位的跳变时刻和 VSYNC 的对应关系不是想当然的。在 EAV 里,F 位在每一场的第一个消隐行开始翻转,而不是在有效视频的第一行。如果你在采集端用 F 位做场同步,但没对齐消隐区的起始位置,就会出现奇偶场颠倒,画面表现为垂直方向轻微抖动或者锯齿。
另一个容易忽略的是消隐区的行数。PAL 制式下,每场有 288 个有效行,但总行数是 312.5 行(半行偏移用于隔行)。第一场的有效行是第 23 行到第 310 行,第二场是第 336 行到第 623 行。中间那些行都是消隐行,V 位为 1。你在做编码的时候,必须严格按照这个行号范围来生成 V 位,否则接收端可能无法锁定。
2.3 用 Python 快速验证 BT656 行结构
在动手写 FPGA 之前,我习惯先用 Python 把一行数据模拟出来,确认同步字和数据结构没问题。下面这段代码生成一行 PAL 的 BT656 数据,并打印出 EAV、消隐区和 SAV 的字节内容。
# BT656 一行数据结构模拟(PAL 720x576i) # 行周期 1728 字节,有效视频 1440 字节,消隐区 288 字节 def calc_xy(field, v_flag, h_flag): """计算 XY 字节的保护位""" # bit7=1, bit6=F, bit5=V, bit4=H # 低4位保护位:bit3=F XOR V XOR H, bit2=F XOR V, bit1=F XOR H, bit0=V XOR H f = (field & 1) << 6 v = (v_flag & 1) << 5 h = (h_flag & 1) << 4 p3 = ((field ^ v_flag ^ h_flag) & 1) << 3 p2 = ((field ^ v_flag) & 1) << 2 p1 = ((field ^ h_flag) & 1) << 1 p0 = ((v_flag ^ h_flag) & 1) return 0x80 | f | v | h | p3 | p2 | p1 | p0 def gen_bt656_line(field, line_num): """生成一行 BT656 数据""" # 判断当前行是否在有效视频区 if field == 0: active = 23 <= line_num <= 310 else: active = 336 <= line_num <= 623 v_flag = 0 if active else 1 line_data = [] # EAV:H=1 xy_eav = calc_xy(field, v_flag, 1) line_data.extend([0xFF, 0x00, 0x00, xy_eav]) # 消隐区填充(简化:用 0x80 填充) blank_len = 280 if active else 0 line_data.extend([0x80] * blank_len) # SAV:H=0 xy_sav = calc_xy(field, v_flag, 0) line_data.extend([0xFF, 0x00, 0x00, xy_sav]) # 有效视频数据(简化:用递增测试值) if active: for i in range(720): y = (i + line_num) & 0xFF cb = (i * 2 + line_num) & 0xFF cr = (i * 2 + 1 + line_num) & 0xFF line_data.extend([cb, y, cr, y]) # 注意 4:2:2 顺序 return line_data[:1728] # 截断到一行长度 # 打印第一场第 23 行(有效视频起始行)的前 16 字节 line = gen_bt656_line(0, 23) print("EAV:", [hex(b) for b in line[0:4]]) print("SAV:", [hex(b) for b in line[280:284]]) print("First pixel YCbCr:", [hex(b) for b in line[284:292]])这段代码的关键在于calc_xy函数,它严格按照 BT656 规范计算保护位。很多人写 FPGA 时直接硬编码 XY 值,结果换一场就出错。用 Python 先跑一遍,可以把所有行号和场号的 XY 值都打印出来做成查找表,后面直接塞进 ROM 里。
参数说明:field为 0 或 1,line_num是当前行号(0 到 623)。注意有效视频区的行号范围是固定的,第一场 23 到 310,第二场 336 到 623。如果你用的不是 PAL 而是 NTSC,这些数字都要改。
3. 从 BT656 到 SD-SDI:编码器模块的 RTL 实现
3.1 并行转串行的基本架构
SD-SDI 的串行数据率是 270Mbps,对应 27MHz 时钟下的 10 倍频。所以编码器的核心任务是把 8 位并行数据做 8b/10b 编码,然后以 270MHz 的时钟串行移出。SMPTE 259M 规定了一些特殊的控制字符(比如 K28.5 用于同步),这些字符在 8b/10b 编码表里有固定映射。
一个典型的 RTL 架构包含这几个模块:BT656 数据解析(提取 EAV/SAV 和有效视频)、FIFO 缓冲(跨时钟域)、8b/10b 编码器、并串转换器(OSERDES 或 SelectIO)。我一般会把 BT656 解析和 8b/10b 编码放在同一个 27MHz 时钟域,FIFO 用来隔离输入视频源和 SDI 发送时钟之间的微小频差。
3.2 8b/10b 编码器的 Verilog 实现要点
8b/10b 编码不是简单的查表,它需要维护运行不一致性(Running Disparity)。每个编码后的 10 位字符要么是 6 个 1 和 4 个 0,要么是 4 个 1 和 6 个 0,具体取决于当前的 RD 状态。下面是一个简化版的 8b/10b 编码器核心逻辑,只展示了数据字符的编码,控制字符(K 码)需要额外的映射表。
// 8b/10b 编码器核心(简化版,仅数据字符) module encoder_8b10b ( input wire clk, input wire rst_n, input wire [7:0] data_in, input wire is_k, // 1 表示控制字符 output reg [9:0] data_out, output reg rd_out // 输出运行不一致性 ); reg [4:0] rd; // 当前运行不一致性,0 表示负,1 表示正 reg [9:0] code_5b6b; reg [9:0] code_3b4b; reg [4:0] rd_after_5b; reg [4:0] rd_after_3b; // 5b/6b 编码查找表(仅列部分) always @(*) begin case (data_in[4:0]) 5'b00000: code_5b6b = (rd == 0) ? 10'b100111_0000 : 10'b011000_0000; 5'b00001: code_5b6b = (rd == 0) ? 10'b011101_0000 : 10'b100010_0000; // ... 其他编码省略 default: code_5b6b = 10'b000000_0000; endcase end // 3b/4b 编码查找表(仅列部分) always @(*) begin case (data_in[7:5]) 3'b000: code_3b4b = (rd_after_5b == 0) ? 10'b1011_000000 : 10'b0100_000000; 3'b001: code_3b4b = (rd_after_5b == 0) ? 10'b1001_000000 : 10'b0110_000000; // ... 其他编码省略 default: code_3b4b = 10'b0000_000000; endcase end // 合并输出并更新 RD always @(posedge clk or negedge rst_n) begin if (!rst_n) begin data_out <= 10'b0; rd <= 0; end else begin data_out <= {code_3b4b[9:6], code_5b6b[9:4]}; // 根据编码结果更新 RD,这里需要根据实际 1 的个数计算 // 简化处理:假设编码后 1 的个数大于 0 的个数则 RD 翻转 rd <= (code_3b4b[9:6] + code_5b6b[9:4] > 5) ? ~rd : rd; end end assign rd_out = rd; endmodule这段代码里最关键的是 RD 的更新逻辑。实际工程中我不会用这种简化的加法判断,而是把每个 5b/6b 和 3b/4b 编码的 RD 变化直接做成查找表,用 case 语句输出。因为 8b/10b 的 RD 计算有严格的规则:如果编码后 1 的个数多于 0,RD 翻转;如果 0 多于 1,RD 保持;如果相等,RD 也翻转(对于某些特殊字符)。用查找表可以避免组合逻辑的毛刺。
参数说明:is_k信号用来区分数据字符和控制字符,K28.5 的编码是10'b0011111010(RD 负)或10'b1100000101(RD 正)。在 SD-SDI 里,K28.5 用于视频行的起始同步,通常放在 EAV 之前。
3.3 用 OSERDES 做并串转换的配置参数
Xilinx 7 系列 FPGA 里,OSERDESE2 原语可以配置成 10:1 的串行化模式。关键参数是DATA_WIDTH设为 10,TRISTATE_WIDTH设为 1,SERDES_MODE设为MASTER。时钟方面,需要两个时钟:CLKDIV是 27MHz,CLK是 270MHz,两者必须同源且相位对齐。我一般用 MMCM 生成 270MHz,然后分频得到 27MHz,这样能保证严格的 10 倍关系。
一个常见的坑是 OSERDES 的复位顺序。如果CLK和CLKDIV没有同时复位,或者复位释放的时刻不对,输出会出现错位,表现为 SDI 接收端无法锁定。我的做法是用一个同步复位信号,在 MMCM 锁定后延迟至少 100 个CLKDIV周期再释放。
4. 调试 SD-SDI 输出:示波器、眼图和协议分析仪怎么用
4.1 用示波器看眼图判断信号质量
SD-SDI 的 270Mbps 信号在示波器上应该看到一个清晰的眼图。如果你用的是普通示波器,至少需要 1GHz 带宽。眼图张开度不够,通常意味着输出驱动能力不足或者电缆均衡没做好。SMPTE 259M 规定输出幅度是 800mVpp ± 10%,上升时间在 0.4ns 到 1.5ns 之间。我一般会先测输出端的差分幅度,如果只有 400mVpp,那可能是驱动器配置成了单端模式。
眼图测试时,用伪随机序列(PRBS)作为测试图案,但 SD-SDI 实际传输的是视频数据,所以更准确的方法是用彩条测试图。如果眼图在特定图案下闭合,说明 8b/10b 编码的 RD 计算有问题,导致直流不平衡。
4.2 协议分析仪抓包看 EAV/SAV 时序
协议分析仪(比如常见的 SDI 分析模块)可以解析出每一行的 EAV、SAV 和 XY 字节。如果你发现分析仪报“场序错误”或者“消隐区长度异常”,那就回到 BT656 生成模块去查行号和场号的对应关系。我遇到过一种情况:FPGA 内部的行计数器在每场结束时没有正确复位,导致第二场的行号从 311 开始而不是 336,结果 V 位一直为 0,接收端把消隐行当成了有效视频。
用分析仪抓包时,重点看三个东西:EAV 的 XY 字节是否随场号变化、SAV 和 EAV 之间的消隐区长度是否稳定在 280 字节左右、有效视频区的第一个像素的 YCbCr 顺序是否正确(Cb 在前还是 Y 在前)。BT656 规定是 Cb-Y-Cr-Y 的顺序,但有些视频源输出的是 Y-Cb-Y-Cr,这个顺序搞反了颜色就会偏。
4.3 用已知彩条图案做端到端验证
最可靠的验证方法是让 FPGA 输出标准的 100% 彩条,然后用 SDI 监视器或者采集卡看画面。彩条的顺序是白、黄、青、绿、品、红、蓝、黑。如果颜色对不上,先查 YCbCr 的排列顺序,再查 8b/10b 编码的 RD 是否在行边界处正确复位。我习惯在每场的第一个消隐行插入一个特殊的测试字,这样用分析仪可以快速定位场起始位置。
5. 避坑指南:SD-SDI 编码调试中最容易翻车的 5 个点
5.1 现象:接收端锁定后画面每隔几秒闪一下
原因:输入视频源的时钟和 SDI 发送时钟存在频差,FIFO 偶尔溢出或读空。很多开发板的视频输入时钟是 27MHz 晶振,但 SDI 发送端的 270MHz 是从另一个 MMCM 出来的,两者不同源。
解决:用同一个 MMCM 生成 270MHz 和 27MHz,确保严格的 10 倍频关系。如果输入视频源是外部设备,必须在 FIFO 里做时钟域隔离,并且 FIFO 深度至少能缓冲两行数据(3456 字节)。我一般会设一个阈值,当 FIFO 里的数据少于半行时插入空闲字符,多于一行半时丢弃,但这样会导致画面轻微撕裂,更好的做法是用异步 FIFO 加背压。
5.2 现象:彩条颜色正确但白色区域有轻微噪点
原因:8b/10b 编码的 RD 在行边界处没有正确复位,导致直流不平衡累积。SD-SDI 要求每个 EAV 之前插入 K28.5 同步字符,并且 RD 必须复位到负状态。
解决:在每行的 EAV 之前强制插入 K28.5,并且把 RD 寄存器复位为 0。注意 K28.5 的编码在 RD 负时是0011111010,在 RD 正时是1100000101。如果你用查找表实现,确保在插入 K28.5 时同时更新 RD 状态。
5.3 现象:分析仪报“消隐区长度错误”,但画面正常
原因:消隐区的填充字节数不对。BT656 规定一行 1728 字节,有效视频 1440 字节,EAV 和 SAV 各 4 字节,所以消隐区应该是 1728 - 1440 - 8 = 280 字节。但有些实现会把 SAV 后面的第一个字节也算进消隐区,导致长度变成 279 或 281。
解决:严格按 280 字节填充。如果你用的是 720×576i,每行的字节数是固定的,不要根据像素时钟动态计算。我一般会在代码里写死BLANK_LEN = 280,然后用一个计数器从 0 数到 279。
5.4 现象:第二场画面比第一场暗一点
原因:F 位的翻转时刻不对。F 位应该在每场的第一个消隐行的 EAV 里翻转,而不是在有效视频的第一行。如果你在有效视频区翻转 F 位,接收端会把当前行的像素归到错误的场里,导致两场的亮度不一致。
解决:在行计数器到达场起始行(第一场第 1 行,第二场第 313 行)时翻转 F 位。注意 PAL 的场起始行号是 1 和 313,不是 0 和 312。这个行号定义在 BT656 规范里有明确说明,但不同厂家的接收芯片可能略有差异,最好用分析仪确认。
5.5 现象:FPGA 综合后时序不收敛,270MHz 时钟报 setup violation
原因:8b/10b 编码的组合逻辑太深,或者 OSERDES 的时钟约束没写对。270MHz 在 7 系列 FPGA 里不算高,但如果编码器里用了多层嵌套的 case 语句,组合延迟会超过 3.7ns。
解决:把 8b/10b 编码做成两级流水线,第一级查 5b/6b 表,第二级查 3b/4b 表并合并输出。另外,在 XDC 约束文件里要对CLK和CLKDIV做create_clock和set_clock_groups,确保时序分析工具知道这两个时钟是同步的。我一般会加set_false_path从CLKDIV到CLK,因为 OSERDES 内部有专门的时钟路由。
6. 进阶技巧:用参数化生成器适配不同制式
如果你已经跑通了 720×576i,下一步很可能是要支持 720×480i(NTSC)或者 720×576p(逐行)。与其改代码,不如把行号范围、消隐区长度、场数这些参数做成模块参数,用 Verilog 的parameter和generate语句动态生成。
下面是一个参数化 BT656 行计数器的核心片段,通过MODE参数选择 PAL 或 NTSC。
module bt656_line_ctrl #( parameter MODE = "PAL" // "PAL" 或 "NTSC" )( input wire clk, input wire rst_n, output reg [10:0] line_num, output reg field, output reg v_flag, output reg h_flag ); // 根据制式定义行数范围 localparam TOTAL_LINES = (MODE == "PAL") ? 625 : 525; localparam FIELD1_START = (MODE == "PAL") ? 23 : 20; localparam FIELD1_END = (MODE == "PAL") ? 310 : 263; localparam FIELD2_START = (MODE == "PAL") ? 336 : 283; localparam FIELD2_END = (MODE == "PAL") ? 623 : 525; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin line_num <= 0; field <= 0; end else begin if (line_num == TOTAL_LINES - 1) begin line_num <= 0; field <= ~field; end else begin line_num <= line_num + 1; end end end // 生成 V 标志 always @(*) begin if (field == 0) v_flag = ~((line_num >= FIELD1_START) && (line_num <= FIELD1_END)); else v_flag = ~((line_num >= FIELD2_START) && (line_num <= FIELD2_END)); end // H 标志在 EAV 和 SAV 之间切换,这里简化处理 always @(*) begin h_flag = (line_num < 4) ? 1 : 0; // 仅示意 end endmodule这个模块的用法很简单:例化时把MODE设成"PAL"或"NTSC",综合工具会自动裁剪掉不需要的逻辑。注意TOTAL_LINES在 PAL 下是 625,但实际行号是 0 到 624,因为隔行扫描的半行偏移被简化了。如果你要做严格的半行偏移,需要把行计数器做成 0.5 的精度,用两个时钟相位来实现。
参数说明:FIELD1_START和FIELD1_END定义了第一场的有效视频行范围,这个范围之外的 V 位为 1。h_flag的生成逻辑在实际工程中要复杂得多,需要根据字节计数器来判断 EAV 和 SAV 的位置,这里只是为了展示参数化结构。
我自己的习惯是,每做一个新制式,先用 Python 脚本生成一份完整的行号-XY 值对照表,然后直接塞进 Verilog 的 case 语句里。这样比在 RTL 里实时计算保护位要可靠得多,也更容易用形式验证工具做等价性检查。希望帮到你。
本文还有配套的精品资源,点击获取