简介:这份资料面向FPGA视频开发方向的工程师与课程设计学生,聚焦SD-SDI标准下720×576i(PAL制)数字视频流的BT656编码实现。内容系统梳理了576i行数据格式、各字段组成、帧与场结构,明确消隐区与有效显示的起止行数、奇偶场行数范围,并详解EAV/SAV格式中F、V、H信号与P3、P2、P1、P0保护位的逻辑关系。在此基础上给出基于Verilog的完整编码设计,核心模块AV_GEN_CBB以27MHz时钟驱动,通过hcnt、vcnt计数器跟踪行列进度,输出10位BT656数据流,并已在ModelSim完成仿真验证,后续计划在ALTERA 5cefa5U19i7n+LMH0340平台做硬件验证。资源包为1个docx文档,约1.12MB,将参考资料与示例代码整合于一处,便于对照理解协议细节与代码实现。目前已有576人学习,适合视频类板卡开发者参考及课程设计选题使用。
1. 720×576i 的 SD-SDI 与 BT656:一条被低估的视频链路
如果你手上有 FPGA 或专用视频芯片,要输出一路标清 SDI 信号,绕不开两个词:SD-SDI 和 BT656。SD-SDI 是标清数字分量串行接口,把 4:2:2 的 YCbCr 数据加上同步信息,通过 270Mbps 的串行码流送出去;BT656 则是这两者之间的“中间语言”,它把行同步、场同步、消隐期全部编码进 8 位或 10 位并行数据流里,省掉了单独的 H/V 同步线。720×576i 是 PAL 制式的标清分辨率,50 场每秒,隔行扫描,正好对应 SD-SDI 的 270Mbps 码率。
很多人第一次做这个链路,会卡在“数据明明对了,示波器上眼图却张不开”或者“接收端画面偶尔撕裂”这类玄学问题上。这篇笔记就围绕 SD-SDI BT656 格式 720×576i 数字视频流编码,把参考资料里最核心的时序、编码规则和示例代码逻辑拆开讲,适合正在调试 FPGA 视频输出、或者需要把 BT656 并行流转换成 SDI 串行流的工程师。读完你能自己搭出一条从像素到 SDI 的完整通路,也知道哪些参数一动就会翻车。
2. BT656 的 720×576i 时序到底怎么算:从 EAV/SAV 到消隐区
2.1 为什么 720 像素的行实际有 864 个时钟
BT656 最容易被误解的一点是:有效像素只有 720 个,但一行传输的时钟数远不止 720。对于 720×576i 50Hz 系统,标准定义的一行总时钟数是 864 个。这 864 个时钟里,前 720 个是有效视频数据,剩下的 144 个是水平消隐区。消隐区不是“空着”,里面塞了 EAV(有效视频结束)和 SAV(有效视频开始)两个定时基准码,每个码占 4 个时钟。
EAV 和 SAV 都是 4 字节序列:前三个字节固定为 FF、00、00,第四个字节携带场标识、消隐区标识和校验位。第四个字节的 bit7 是固定 1,bit6 是场指示(F),bit5 是消隐区指示(V),bit4 是 H 指示,低 4 位是保护位,由 F、V、H 异或生成。接收端就是靠检测 FF 00 00 这个前导码来定位一行从哪里开始。
对于 720×576i,一帧分两场,每场 288 行有效视频,加上垂直消隐区的行,总行数是 625 行每帧。垂直消隐区里同样有 EAV/SAV,但 V 标志位会变化,用来告诉接收端“现在进入场消隐了”。如果你只按 720 个时钟去设计行计数器,接收端会直接丢同步。
2.2 用 Python 算一遍 720×576i 的 BT656 行结构
下面这段代码把一行 864 时钟拆成有效区和消隐区,并打印出 EAV/SAV 的 XY 状态字。你可以直接改参数看不同制式的变化。
# BT656 720x576i50 行结构计算 # 一行总时钟 864,有效像素 720,消隐 144 TOTAL_CLK_PER_LINE = 864 ACTIVE_PIXELS = 720 BLANKING_CLK = TOTAL_CLK_PER_LINE - ACTIVE_PIXELS # 144 # EAV/SAV 各占 4 个时钟,位于消隐区 EAV_SAV_LEN = 4 # 场标识 F:0=第一场,1=第二场 # 消隐标识 V:0=有效视频区,1=消隐区 # H:0=SAV之后,1=EAV之前 def make_xy(field, v_flag, h_flag): # 保护位 = F xor V xor H protect = (field ^ v_flag ^ h_flag) & 0x0F # bit7固定1,bit6=F,bit5=V,bit4=H,低4位保护 xy = 0x80 | (field << 6) | (v_flag << 5) | (h_flag << 4) | protect return xy # 有效视频区的 SAV:F=0, V=0, H=0 sav_active = make_xy(0, 0, 0) # 有效视频区的 EAV:F=0, V=0, H=1 eav_active = make_xy(0, 0, 1) # 消隐区的 SAV:F=0, V=1, H=0 sav_blank = make_xy(0, 1, 0) # 消隐区的 EAV:F=0, V=1, H=1 eav_blank = make_xy(0, 1, 1) print(f"一行总时钟: {TOTAL_CLK_PER_LINE}") print(f"有效像素: {ACTIVE_PIXELS}") print(f"消隐时钟: {BLANKING_CLK}") print(f"SAV(有效区) XY = 0x{sav_active:02X}") print(f"EAV(有效区) XY = 0x{eav_active:02X}") print(f"SAV(消隐区) XY = 0x{sav_blank:02X}") print(f"EAV(消隐区) XY = 0x{eav_blank:02X}")这段代码的关键在make_xy函数:保护位不是随便填的,接收端会校验 F、V、H 和低 4 位的异或关系,错了就认为定时基准码无效。实际 FPGA 里你不需要用 Python 跑,但用脚本先算清楚 XY 值,再写进 Verilog 的查找表,能省掉大量抓波形的时间。参数说明:field在隔行系统里每场翻转一次;v_flag在垂直消隐区要置 1;h_flag在 SAV 时为 0,EAV 时为 1。
2.3 垂直消隐区里那些“看不见的行”在干什么
720×576i 每帧 625 行,其中有效视频行是 576 行(分两场,每场 288 行),剩下的 49 行是垂直消隐。这 49 行不是浪费,它们承载了场同步信息、行号标识,有些系统还会在里面对齐音频或辅助数据。BT656 规定垂直消隐区的行同样有 EAV/SAV,但 V 标志位为 1,F 标志位按场切换。
如果你做的是 SD-SDI 输出,垂直消隐区的处理直接关系到接收端能不能稳定锁场。常见做法是:在垂直消隐区把 V 置 1,同时保持行计数器继续跑,不要停。有些开发者为了省事,在消隐区直接把输出拉低,结果接收端每隔几秒就闪一次,这就是典型的“省事翻车”。
3. 从 BT656 并行流到 SD-SDI 串行流:编码核心与示例代码逻辑
3.1 SD-SDI 的 270Mbps 是怎么从 27MHz 时钟来的
SD-SDI 的串行码率是 270Mbps,这个数字来自 BT656 的并行时钟 27MHz 乘以 10。BT656 并行接口每个时钟传 1 个字节(8 位),但 SD-SDI 内部用的是 10 位数据,所以先要把 8 位扩展成 10 位,再串行化。10 位数据里,低 8 位是视频数据,高 2 位是复用位,用来区分视频数据和定时基准码。
具体来说,BT656 的 FF 00 00 XY 序列在 SD-SDI 里会被映射成特定的 10 位码字。接收端检测到这些码字就知道行边界在哪里。270Mbps 的串行流经过电缆驱动后,眼图要满足 SMPTE 259M 的模板,这就要求 PCB 走线阻抗控制、驱动器预加重参数都得调对。
3.2 一个可综合的 BT656 到 SD-SDI 映射模块框架
下面用 Verilog 写一个简化的映射模块,把 BT656 的 8 位并行数据转成 10 位 SDI 数据流。注意这不是完整代码,而是核心逻辑框架,实际综合时还要加时钟域 crossing 和串行化器。
// bt656_to_sdi_10bit.v // 将 BT656 8位并行数据映射为 SD-SDI 10位数据 // 假设输入时钟 27MHz,输出 10位数据在 27MHz 下有效 module bt656_to_sdi_10bit ( input wire clk_27m, input wire rst_n, input wire [7:0] bt656_data, // BT656 8位数据 input wire bt656_de, // 数据有效指示 output reg [9:0] sdi_data, // SDI 10位数据 output reg sdi_de // SDI 数据有效 ); // BT656 定时基准码前导:FF 00 00 localparam [7:0] PREAMBLE_FF = 8'hFF; localparam [7:0] PREAMBLE_00 = 8'h00; // 状态机:检测前导码,决定是否插入 TRS 码字 reg [1:0] state; reg [7:0] data_d1, data_d2; always @(posedge clk_27m or negedge rst_n) begin if (!rst_n) begin state <= 2'd0; sdi_data <= 10'd0; sdi_de <= 1'b0; data_d1 <= 8'd0; data_d2 <= 8'd0; end else begin // 打拍保存前两个字节,用于检测 FF 00 00 data_d1 <= bt656_data; data_d2 <= data_d1; case (state) 2'd0: begin // 等待 FF 00 00 前导 if (data_d2 == PREAMBLE_FF && data_d1 == PREAMBLE_00 && bt656_data == PREAMBLE_00) begin state <= 2'd1; end // 普通视频数据:低8位直接映射,高2位补0 sdi_data <= {2'b00, bt656_data}; sdi_de <= bt656_de; end 2'd1: begin // 检测到前导,下一个字节是 XY 状态字 // SDI 里 TRS 码字有专用 10 位映射,这里简化为带标志位 sdi_data <= {2'b10, bt656_data}; // 高2位=10 表示 TRS sdi_de <= 1'b1; state <= 2'd0; end default: state <= 2'd0; endcase end end endmodule这段代码的核心思路是:用移位寄存器检测 FF 00 00 前导码,检测到之后把紧随其后的 XY 字节标记为 TRS 码字(高 2 位设为 10),普通视频数据则高 2 位补 00。实际 SD-SDI 标准里 TRS 码字的 10 位映射有固定表,这里为了展示逻辑做了简化。参数说明:bt656_de在有效像素区为高,消隐区为低;sdi_de用来指示输出数据是否有效,后级串行化器靠它决定是否发送。
3.3 串行化与电缆驱动的三个必调参数
从 10 位并行到 270Mbps 串行,通常用 FPGA 内部的 SERDES 或者专用串行化芯片。这里有三个参数必须调:
| 参数 | 典型值 | 作用 | 调错后果 |
|---|---|---|---|
| 预加重 | 0~6dB | 补偿电缆高频衰减 | 眼图闭合,接收端误码 |
| 输出摆幅 | 800mVpp | 匹配 SMPTE 259M | 幅度不够,传输距离缩短 |
| 上升/下降时间 | 0.4~1.5ns | 控制频谱 | 过冲或边沿太缓 |
预加重是最玄学的:电缆越长,高频衰减越厉害,预加重就要越大。但预加重太大,短电缆上反而会过冲。我一般会先用 3dB 起步,用示波器看眼图,再微调。输出摆幅必须用高带宽探头测,普通探头测出来会偏低。
4. 避坑与排查:SD-SDI BT656 调试中最容易翻车的 5 个点
4.1 现象:接收端画面每隔几秒闪一次黑屏
原因:垂直消隐区的 V 标志位没有正确置 1,或者场标识 F 没有按场翻转。接收端无法区分有效视频和消隐区,锁场失败。
解决:用逻辑分析仪抓一行垂直消隐区的 EAV/SAV,确认 XY 状态字的 bit5(V)为 1,bit6(F)在每场开始时翻转。如果 F 一直不变,检查场计数器是不是只计了半场。
4.2 现象:画面颜色整体偏绿或偏紫
原因:BT656 数据里 YCbCr 的字节顺序搞反了。BT656 默认是 Cb Y Cr Y 交替,如果你按 Y Cb Y Cr 发,接收端解出来的色度就错了。
解决:确认数据排列顺序。720×576i 的 BT656 里,每个时钟传一个字节,两个时钟组成一个像素的 Cb 和 Y,再两个时钟组成 Cr 和 Y。用示波器抓数据总线,对照 EAV 之后的第一个有效字节应该是 Cb。
4.3 现象:眼图张不开,误码率随电缆长度急剧上升
原因:预加重和输出摆幅不匹配,或者 PCB 走线没有做 75 欧姆阻抗控制。
解决:先确认走线阻抗,再用示波器测眼图。预加重从 0dB 开始,每加 1dB 看一次眼图,找到张开最大的点。如果怎么调都张不开,检查串行化器输出是不是被其他信号串扰了。
4.4 现象:行同步偶尔丢失,画面顶部撕裂
原因:EAV/SAV 的保护位算错了。接收端校验 F、V、H 异或关系,保护位不对就丢弃这个定时基准码。
解决:用 2.2 节的 Python 脚本重新算一遍 XY 值,确认保护位是 F、V、H 的异或。特别注意 H 标志:SAV 时 H=0,EAV 时 H=1,别写反。
4.5 现象:FPGA 综合后时序不收敛,270Mbps 输出抖动大
原因:27MHz 时钟域到 270MHz 串行域的跨时钟处理没做好,或者 SERDES 的参考时钟抖动太大。
解决:确保跨时钟用异步 FIFO,不要直接打拍。参考时钟选低抖动的晶振,相位噪声要小于 -100dBc/Hz@10kHz。如果 SERDES 支持,打开内部 PLL 的带宽调节,通常默认值就够,但抖动大时要手动收窄。
5. 进阶技巧:用伪随机序列验证整条链路
5.1 为什么静态彩条不够用
很多人调试 SD-SDI 就用彩条测试图,彩条能过不代表链路没问题。彩条是低频重复图案,对码间干扰和时钟恢复的压力很小。真正能暴露问题的是伪随机二进制序列(PRBS),它覆盖了从低频到高频的丰富频谱,接收端误码率一测就知道链路余量。
5.2 在 BT656 消隐区插入 PRBS 的方法
BT656 的垂直消隐区有几十行是“空闲”的,你可以把 PRBS 填进去。接收端在消隐区解 PRBS,和本地生成的序列对比,统计误码。下面是一个简单的 PRBS7 生成器,用 Python 模拟插入过程。
# PRBS7 生成器,用于填充 BT656 垂直消隐区 # 多项式: x^7 + x^6 + 1 def prbs7(seed=0x7F, length=100): state = seed & 0x7F seq = [] for _ in range(length): # 输出当前最高位 out = (state >> 6) & 1 seq.append(out) # 反馈: bit6 xor bit5 fb = ((state >> 6) ^ (state >> 5)) & 1 state = ((state << 1) | fb) & 0x7F return seq # 模拟填充一行消隐区(144 个时钟,去掉 EAV/SAV 的 8 个) prbs_data = prbs7(seed=0x55, length=136) print("PRBS7 前 16 位:", prbs_data[:16]) print("PRBS7 长度:", len(prbs_data))这段代码生成 136 位 PRBS7,正好填满一行消隐区去掉 EAV/SAV 后的空间。实际 FPGA 里用 LFSR 实现,每个时钟输出一位,接收端用同样的多项式解。参数说明:种子不能全零,否则序列锁死;多项式选 PRBS7 是因为 127 周期足够短,方便接收端快速同步。
5.3 用误码率判断链路余量的经验阈值
我一般会连续测 10 分钟,统计误码数。如果误码率低于 1e-12,链路余量充足;1e-9 到 1e-12 之间,勉强能用但电缆再长一点就危险;高于 1e-9,必须回去调预加重或换电缆。注意测的时候要用实际长度的电缆,不要用短跳线糊弄自己。
5.4 一个我踩过的坑:PRBS 种子设成全零
有一次为了图省事,PRBS 种子设成 0,结果接收端一直同步不上,抓了半天波形才发现序列全是 0。LFSR 的种子必须非零,这是血泪经验。后来我养成习惯,种子固定用 0x55 或 0x7F,并且在代码里加一句断言,种子为零直接报错。
5.5 把验证做成自动化脚本
最后一步,把 PRBS 发送、接收、误码统计串成一个脚本,每次改完参数跑一遍。这样你调预加重的时候,不用盯着示波器看半天,脚本会告诉你哪个参数下误码最低。我现在的习惯是:任何视频链路改动,先跑 10 分钟 PRBS,过了再上彩条,最后才接真实信号源。希望帮到你。
本文还有配套的精品资源,点击获取