FPGA实战:BT656接口720x576格式的Verilog实现与时序仿真 📅 发布时间:2026/9/9 23:27:05 👁 浏览次数: 简介一份基于Verilog HDL的BT656视频编码实现面向FPGA开发者和数字视频接口学习者解决RGB888像素格式到BT656标准数据流的转换并适配720x576分辨率输出。压缩包共130个文件大小约4.14MB核心包含bt656.v源文件、Quartus工程配置qsf/qpf、编译下载文件sof以及大量工程过程文件cdb/hdb等便于直接查看、重新编译或二次修改。已有5136人学习。通过该设计可深入理解行/场同步信号处理、YCbCr色彩空间变换、串行时序控制、字节打包及同步字插入等关键环节适合用于视频接口课程实验也可作为FPGA标清视频输出模块的参考模板。设计结构清晰对掌握数字视频接口与FPGA实现方法具有实用价值。 上周帮一个做视频采集的朋友调FPGA他从网上下了一份“BT656 verilog代码 720*576”直接拖进工程里跑仿真波形乱成一团行消隐的位置不对有效数据到1350个像素就切走了SAV和EAV后面的XY校验字也有问题。折腾了大半天最后我让他把网上的代码全部删掉对照ITU-R BT.656标准自己重写了一份一个下午就在Modelsim里跑通了。这个事其实很典型——网上流传的BT656代码大多是某颗芯片、某个项目调试后的产物行场参数全是写死的死值换一颗芯片或者稍微改一下分辨率就崩。所以这篇文章不打算给你一份可以直接复制的完整工程而是把BT656接口在720x576这个格式下的时序参数怎么算、Verilog代码怎么组织、仿真怎么验证讲透让你能自己写出真正可靠的代码。这篇文章适合正在学FPGA视频处理的人、做视频采集卡或显示项目开发的工程师以及那些搜到代码却看不懂原理、不敢直接拿到工程里用的朋友。看完之后你不仅能把720x576跑通换到480i、1080i这类格式也不会慌。1. BT656不是复杂协议是一段裸数据流1.1 为什么这么多工程师还要自己写这份代码BT656这个接口本质上不算是那种带握手、带应答、带封装的协议。它的典型形态是一条8bit并行数据线加一个像素时钟数据和同步信息全部混在这条数据线上。同步靠的是数据流里周期性出现的“FF 00 00 XY”这4个字节接收端只要看到FF 00 00就知道接下来是同步字再按固定位置解析后面的视频数据。对比I2C、SPI这类还要额外拉控制线的总线BT656的物理连接极简一个时钟、一组数据几乎不需要额外的控制信号。但也正因为同步信息是嵌在数据流里的写代码的人必须对时序窗口有非常精确的把握——每行多少个像素、有效数据从哪里开始、消隐区填什么值、场标志在哪一行翻转只要有半拍偏差接上真实芯片后画面就会撕裂或者偏移。很多刚接触FPGA视频处理的朋友有个误区觉得这些接口IP现成的调用一下就行。但实际项目里芯片手册要求的非标准时序、多路视频拼接、自定义数据插入都会逼你打开RTL源码去改。如果连BT656最基础的计数器逻辑都没写过那时候就是两眼一摸黑。所以我一直建议视频处理入门先自己写一遍BT656比什么都有用。1.2 720x576为什么是练手的最佳起点720x576对应PAL制式的D1分辨率是安防监控、老式采集卡、DVD视频领域最经典的一档尺寸。它的像素时钟是27MHz每行1728个时钟周期每帧625行全部是整数参数。对比1080p那种一行要数3375个像素、场消隐还带各种小数的时序720x576的参数表干净得像一张小学生数学卷子。最舒服的一点是720x576的三个核心数字——625、1728、1440——和像素时钟27MHz之间没有小数关系。做FPGA计数器时直接用周期边界比较就行不用担心跨时钟域的亚像素对齐问题。我拿它当视频接口“Hello World”练手教过不少实习生基本上半天能写出能跑的代码两天能调通上板。这个投入产出比比直接啃HDMI或者MIPI协议不知道高到哪里去了。2. 先把手算参数搞清楚1728、625、1440这些数字从哪来2.1 27MHz时钟和1728行周期的来源BT656里传输的是符合BT.601采样标准的数字视频亮度信号采样率13.5MHz色差信号采样率6.75MHz。为了让亮度和色差复用一条数据线每两个亮度像素共享一对CbCr输出顺序是“Cb0 Y0 Cr0 Y1 Cb1 Y2 Cr1 Y3……”这样数据率就是13.5MHz的两倍得到27MHz的像素时钟。720个有效像素按YUV 4:2:2排列一行有效视频数据就是720个Y加上360个Cb再加360个Cr总共1440字节。每行除了这1440个有效字节还要插入EAV行结束同步、SAV行开始同步各4个字节剩下的时间就是水平消隐填充。具体分配到一行里就是区间起始位置长度内容SAV第0拍4FF 00 00 XYH0有效数据第4拍1440Y0 Cb0 Y1 Cr0 ... Y719EAV第1444拍4FF 00 00 XYH1水平消隐第1448拍280填充0x80或0x10这里要特别提醒一个容易搞反的点很多人以为行数据是从EAV开始排的其实标准的排列方式是把SAV放在一行的开头紧接着就是1440个有效字节然后才是EAV和水平消隐。仿真波形里如果看到有效数据没有紧跟SAV而是中间隔了一长串0x80那多半就是这里理解错了。2.2 625行怎么分成两场V和F标志怎么翻720x576是隔行扫描一帧由两场组成。第一场F0和第二场F1各占约312.5行其中有效的视频行每场只有288行合起来才是576行。具体到行号分布比较常用的简化模型是第一场第1~22行为场消隐V1第23~310行为有效视频V0共288行第二场从311行开始311~335行是场消隐V1336~623行是有效视频V0共288行最后624~625行回到场消隐状态。用这个模型算下来V1的总行数是2225249行符合PAL制的场消隐行数。写代码时如果你用的行计数器是0起始0~624共625行那V标志的判断窗口就要对应挪一下0~21、310~334、623~624这几个区间内V为1其他区间V为0。F标志则简单得多行号小于312为第一场大于等于312为第二场。2.3 XY信号不是随便填的保护位计算规则每行的同步头都是“FF 00 00 XY”前三个字节固定不变变的是最后一个XY。这个字节在8bit宽度下第7位固定为1第6位是F场标志第5位是V垂直消隐标志第4位是H水平消隐标志SAV时为0EAV时为1低4位是保护位用于接收端做校验。位76543210含义固定1FVHP3P2P1P0保护位的计算规则是P3V xor HP2F xor HP1F xor VP0F xor V xor H。以第一场有效区F0V0的SAV为例H0代入得0x80第一场有效区的EAVH1代入得0x9D。第二场有效区SAV是0xC7。这几个典型值在仿真波形里反复出现记不住规则也没关系能认出0x80和0x9D基本就能判断同步是否正常。3. Verilog核心实现两个计数器加一套输出控制3.1 模块端口怎么设计写这个模块之前先明确端口。我给一个比较实用的接口定义module bt656_720x576_gen( input wire clk, // 27MHz像素时钟 input wire rst_n, input wire [7:0] video_in, // Y/CbCr复用数据输入 input wire in_valid, // 输入数据有效标志 output reg [7:0] bt656_out, // BT656串行数据输出 output wire de, // 有效数据指示调试用 output wire vsync, // 场消隐指示调试用 output wire hsync // 行同步指示调试用 );有朋友会问BT656本身不是只有数据和时钟两条线吗为什么还引出一堆de、vsync、hsync这些信号是留给仿真调试和后续扩展用的。真实接芯片时你确实只送bt656_out和clk但调试时有一根de在波形上看数据窗口会方便特别多。我个人习惯是把这些调试信号用wire引到顶层并保留上板后不用改RTL就能用逻辑分析仪观察。3.2 两个计数器pixel_cnt和line_cnt核心逻辑就是两个计数器一个数行内的像素0~1727一个数帧内的行0~624。像素计数器每个时钟加一走到1727就归零行计数器在像素计数器归零的那一拍加一走到624就归零。这个嵌套关系和日常用的“时分秒”计数器一模一样没有任何高难度。localparam PIX_TOTAL 1728; localparam LINE_TOTAL 625; reg [10:0] pixel_cnt; reg [ 9:0] line_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) pixel_cnt 0; else if (pixel_cnt PIX_TOTAL - 1) pixel_cnt 0; else pixel_cnt pixel_cnt 1; end always (posedge clk or negedge rst_n) begin if (!rst_n) line_cnt 0; else if (pixel_cnt PIX_TOTAL - 1) begin if (line_cnt LINE_TOTAL - 1) line_cnt 0; else line_cnt line_cnt 1; end end写这段代码时有几个细节要注意。第一pixel_cnt的位宽至少11位因为1728大于1024用10位不够line_cnt至少10位625大于512。第二行计数器必须在pixel_cnt等于最后一个有效值的下一拍变化也就是pixel_cnt1727时变化不要写成pixel_cnt1728否则时序上会差一拍。第三复位时两个计数器都要清零仿真时如果漏了清零第一帧的行号就是乱的。3.3 XY生成逻辑用组合逻辑算不查表XY信号每个时钟周期都可能不同因为它依赖于当前行的F标志和V标志。最可靠的写法是根据公式实时计算wire field_flag (line_cnt 312); wire vblank_flag (line_cnt 22) || (line_cnt 310 line_cnt 335) || (line_cnt 623); reg [7:0] xy_sav; reg [7:0] xy_eav; always (*) begin xy_sav[7] 1b1; xy_sav[6] field_flag; xy_sav[5] vblank_flag; xy_sav[4] 1b0; // H0, SAV xy_sav[3] vblank_flag ^ 1b0; xy_sav[2] field_flag ^ 1b0; xy_sav[1] field_flag ^ vblank_flag; xy_sav[0] field_flag ^ vblank_flag ^ 1b0; xy_eav[7] 1b1; xy_eav[6] field_flag; xy_eav[5] vblank_flag; xy_eav[4] 1b1; // H1, EAV xy_eav[3] vblank_flag ^ 1b1; xy_eav[2] field_flag ^ 1b1; xy_eav[1] field_flag ^ vblank_flag; xy_eav[0] field_flag ^ vblank_flag ^ 1b1; end为什么不用查表法因为查表法要把两场、有效/消隐四种组合下的典型值全部列出来看着简单但很容易把0x9D和0xC7这种相近的值记混。用公式实时算只要F、V、H三个标志是准的XY就永远是对的。而且这个公式是从标准里推出来的换到10bit模式或者逐行模式时改动也小。3.4 输出数据通路用区间判断切出四个段输出部分的任务是在正确的时间把正确的字节送到bt656_out上。整个行内被切成四个段SAV段、有效数据段、EAV段、水平消隐段。always (posedge clk or negedge rst_n) begin if (!rst_n) begin bt656_out 8h80; end else if (pixel_cnt 4) begin case (pixel_cnt[1:0]) 2b00: bt656_out 8hFF; 2b01: bt656_out 8h00; 2b10: bt656_out 8h00; default: bt656_out xy_sav; endcase end else if (pixel_cnt 4 pixel_cnt 1443) begin bt656_out video_in; end else if (pixel_cnt 1444 pixel_cnt 1447) begin case (pixel_cnt[1:0]) 2b00: bt656_out 8hFF; 2b01: bt656_out 8h00; 2b10: bt656_out 8h00; default: bt656_out xy_eav; endcase end else begin bt656_out 8h80; end end这块有几点经验。第一同步头前面的FF 00 00是连续三个固定字节我用pixel_cnt的低2位来切是因为SAV和EAV的起点都是4的整数倍低两位必定从00开始这样case写起来最简洁。第二有效数据段的判断条件要和前面SAV段严格分开4~1443之间是有效数据但第4拍已经是有效数据的第一个字节不要漏掉。第三水平消隐段填充0x80这是比较标准的消隐电平实际项目里有时也会填0x10代表黑电平偏移这个不影响接收端识别同步只要别填成FF、00这类会干扰同步识别的值就行。de信号可以直接复用有效数据段判断条件assign de (pixel_cnt 4 pixel_cnt 1443); assign vsync vblank_flag; assign hsync (pixel_cnt 0);3.5 加一个彩条数据源方便仿真直接看为了验证时序对不对最省事的方式是内部生成一个测试图案而不是先接真实视频源。我常用的是一个“递增斜波”发生器有效数据期间每个时钟加一个固定步长消隐期间回到初始值。reg [7:0] test_data; always (posedge clk or negedge rst_n) begin if (!rst_n) test_data 8h10; else if (de) test_data test_data 8h10; else test_data 8h10; end assign video_in test_data;这样在Modelsim里拉出波形后清晰可见有效数据区是一段阶梯状上升的波形和前后消隐电平有明显的边界一眼就能看出de窗口对不对。等这个内部源验证通过再把video_in切换到外部输入调试效率能提升一大截。4. Modelsim里怎么验证别让波形图骗了你4.1 testbench搭建时钟、复位、例化写testbench的目的不是“能跑”而是“能检查”。我的习惯是仿真至少500行覆盖两次场翻转这样才能确认F和V在两个场之间切换没有异常。module tb_bt656; reg clk; reg rst_n; wire [7:0] bt656_out; wire de, vsync, hsync; always #18.5 clk ~clk; initial begin clk 0; rst_n 0; #200; rst_n 1; #40_000_000; $stop; end bt656_720x576_gen dut( .clk(clk), .rst_n(rst_n), .video_in(8h00), .in_valid(1b1), .bt656_out(bt656_out), .de(de), .vsync(vsync), .hsync(hsync) ); endmodule27MHz时钟的周期约37.037ns我一般写#18.5翻转一次这样逼近27MHz但又不是严格的浮点周期。Modelsim的仿真精度默认到ns级写#18.5185反而可能被四舍五入产生微小抖动所以18.5就够用了。如果你对时间严格敏感可以试试在timescale里把时间精度声明到1ps但纯功能仿真没必要。4.2 波形检查的三个关键位置仿真跑起来后不要直接看一整屏波形那会让人眼花缭乱。我一般把波形缩放到“一行”的尺度也就是1728个时钟周期挨个检查三个位置第一个是SAV的位置。波形里应该出现“FF 00 00 80”或“FF 00 00 C7”这样的字节序列紧接着de拉高。如果你看到SAV和de之间隔了几个时钟说明有效数据窗口的起始位置偏了。第二个是有效数据窗口的长度。把光标放在de上升沿再放到de下降沿两个光标之间应该是1440个时钟周期。数数是个笨办法但最可靠。有些网上的代码这里只有1392或者1350就是有效窗口算少了接芯片后画面右半部分会花。第三个是行与行之间的EAV。在de下降沿之后应该看到“FF 00 00 9D”第一场有效行或对应的EAV同步字。然后是一段0x80填充再到下一行SAV。这个顺序如果错了说明状态机跳转逻辑有问题接收端会完全无法锁定。4.3 自动核对导出数据用脚本查波形看多了眼睛会花尤其要跑500行的时候。更高效的做法是把bt656_out导出成文本再用脚本自动核对。Modelism里可以用$fwrite把数据写到一个hex文件里integer fd; initial begin fd $fopen(bt656_out.hex, w); end always (posedge clk) begin if (rst_n) $fwrite(fd, %02x\n, bt656_out); end跑完仿真后拿Python或者随便什么脚本读这个文件按每1728个数据一行切分检查每行的开头是不是FF 00 00 XY第5个字节到第1444个字节是否全是数据XY里的F位和V位是否和行号对应。这套流程跑一次比盯着波形看半天靠谱得多。我自己做完这个自动检查之后才敢说这份代码“真的没问题”。4.4 仿真里的常见坑第一个坑是波形窗口里bt656_out显示为十进制。FF在十进制里是25500是080是128。如果忘了改成十六进制显示整屏波形全是些毫无规律的数字很难看出同步头。在Modelsim里选中信号右键Radix改成Hexadecimal就行。第二个坑是复位释放的时机。如果rst_n在时钟上升沿的瞬间释放计数器第一拍可能出现亚稳态仿真表现是第一行数据的起始位置不确定。稳妥做法是让复位释放时刻避开时钟上升沿比如#200后再拉高这样大概率落在下降沿附近。第三个坑是忘了复位。有些初学朋友把复位信号一直接1仿真跑出来的波形看起来也是跳动的但实际上里面一堆寄存器是X态。只要在波形里看到红色的X第一件事就是检查复位有没有正常工作而不是查逻辑。第四个坑比较隐蔽testbench里video_in如果恒为0x00有效数据区全是0仿真波形看起来就像整个输出都是0很容易误以为数据通路没通。所以我在仿真激励里通常会加一个简单的数据变化比如用一个计数器产生递增数据或者直接用内部彩条源这样一眼就能看出数据有没有跟着时钟走。5. 裸代码进工程的一段血泪经验5.1 接解码芯片时先把芯片配置和代码对齐很多网友拿着这份代码去接ADV7180这类老牌视频解码芯片结果上板后没有任何画面。多数原因不是Verilog代码的问题而是芯片输出配置和你的代码假设不一致。ADV7180默认可能是10bit输出也可能通过I2C改成了8bit输出格式可能是带嵌入同步的BT656也可能是分离同步的原始YUV。你的RTL是按8bit、嵌入同步、720x576来写的芯片却配置成了别的模式当然不出图。我踩过一次很深的坑芯片手册里写“默认BT.656”我以为不用配寄存器了结果输出的是10bit格式高8位和低2位完全错位画面花成一团。后来老老实实查寄存器手册把输出宽度和同步模式显式配置了一遍所有问题迎刃而解。经验就是接芯片前先核对三件事——输出位宽、同步模式、分辨率/帧率这三项里有一项和RTL假设不符画面就是花的。5.2 改其他分辨率怎么动参数把720x576改成480i720x480NTSC参数变化主要是像素时钟从27MHz改成13.5MHz每行总周期从1728改成858其中有效视频仍是1440字节不同的是行消隐和同步字的位置要按858重新分配总行数从625变成525有效行从576变成480。代码主体不用动只改几个localparam和V标志的窗口判断。如果你用的是PLL产生时钟分频参数也要一起改。改成逐行720p60的话时钟升到74.25MHz行总周期变成1650个像素其中有效数据1920字节总行数750行但这个就不是BT656了而是BT.1120标准。逐行模式下F标志可以恒为0场消隐窗口的计算比隔行简单不少。看清这里的规律没有只要是BT656系列的接口核心就是“像素计数器行计数器输出状态机”这个骨架变的只是参数。所以写这份代码时把参数集中放在文件头部后面改分辨率就是改常量比去代码里到处搜索边界值省心得多。5.3 一个能少走很多弯路的习惯我后来写任何视频接口模块都会先把自己手上这份参数表打印出来贴显示器边上行总像素、有效像素、行消隐宽度、每帧总行数、场消隐窗口、F翻转位置。写代码前先对着参数表算一遍写完再对着参数表核一遍。听起来很老土但真的能挡掉至少一半的时序错位问题。另外强烈建议搭一套自动比对的环境不要每次都盯着波形数格子。把输出数据落到文件里脚本自动检查同步头、有效窗口长度、XY字的F/V分布几秒钟出一份报告。这套验证环境搭好后将来做BT1120、MIPI CSI-2的发送端都能复用同样的思路。我自己的体会是花在校验上的时间永远比花在Debug上的时间便宜。本文还有配套的精品资源点击获取