news 2026/9/9 23:26:48

FPGA实战:BT656接口720x576格式的Verilog实现与时序仿真

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA实战:BT656接口720x576格式的Verilog实现与时序仿真

简介:一份基于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 XY(H=0)
有效数据第4拍1440Y0 Cb0 Y1 Cr0 ... Y719
EAV第1444拍4FF 00 00 XY(H=1)
水平消隐第1448拍280填充0x80或0x10

这里要特别提醒一个容易搞反的点:很多人以为行数据是从EAV开始排的,其实标准的排列方式是把SAV放在一行的开头,紧接着就是1440个有效字节,然后才是EAV和水平消隐。仿真波形里如果看到有效数据没有紧跟SAV,而是中间隔了一长串0x80,那多半就是这里理解错了。

2.2 625行怎么分成两场,V和F标志怎么翻

720x576是隔行扫描,一帧由两场组成。第一场(F=0)和第二场(F=1)各占约312.5行,其中有效的视频行每场只有288行,合起来才是576行。具体到行号分布,比较常用的简化模型是:

  • 第一场第1~22行为场消隐(V=1),第23~310行为有效视频(V=0,共288行);
  • 第二场从311行开始,311~335行是场消隐(V=1),336~623行是有效视频(V=0,共288行);
  • 最后624~625行回到场消隐状态。

用这个模型算下来,V=1的总行数是22+25+2=49行,符合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时为0,EAV时为1),低4位是保护位,用于接收端做校验。

76543210
含义固定1FVHP3P2P1P0

保护位的计算规则是P3=V xor H,P2=F xor H,P1=F xor V,P0=F xor V xor H。以第一场有效区(F=0,V=0)的SAV为例,H=0,代入得0x80;第一场有效区的EAV,H=1,代入得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_cnt==1727时变化,不要写成pixel_cnt==1728,否则时序上会差一拍。第三,复位时两个计数器都要清零,仿真时如果漏了清零,第一帧的行号就是乱的。

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] = 1'b1; xy_sav[6] = field_flag; xy_sav[5] = vblank_flag; xy_sav[4] = 1'b0; // H=0, SAV xy_sav[3] = vblank_flag ^ 1'b0; xy_sav[2] = field_flag ^ 1'b0; xy_sav[1] = field_flag ^ vblank_flag; xy_sav[0] = field_flag ^ vblank_flag ^ 1'b0; xy_eav[7] = 1'b1; xy_eav[6] = field_flag; xy_eav[5] = vblank_flag; xy_eav[4] = 1'b1; // H=1, EAV xy_eav[3] = vblank_flag ^ 1'b1; xy_eav[2] = field_flag ^ 1'b1; xy_eav[1] = field_flag ^ vblank_flag; xy_eav[0] = field_flag ^ vblank_flag ^ 1'b1; 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 <= 8'h80; end else if (pixel_cnt < 4) begin case (pixel_cnt[1:0]) 2'b00: bt656_out <= 8'hFF; 2'b01: bt656_out <= 8'h00; 2'b10: bt656_out <= 8'h00; 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]) 2'b00: bt656_out <= 8'hFF; 2'b01: bt656_out <= 8'h00; 2'b10: bt656_out <= 8'h00; default: bt656_out <= xy_eav; endcase end else begin bt656_out <= 8'h80; 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 <= 8'h10; else if (de) test_data <= test_data + 8'h10; else test_data <= 8'h10; 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(8'h00), .in_valid(1'b1), .bt656_out(bt656_out), .de(de), .vsync(vsync), .hsync(hsync) ); endmodule

27MHz时钟的周期约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在十进制里是255,00是0,80是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改成480i(720x480,NTSC),参数变化主要是:像素时钟从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上的时间便宜。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 23:26:45

Changes Made

Changes Made 【免费下载链接】oh-my-claudecode Teams-first Multi-agent orchestration for Claude Code 项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-claudecode file.ts:42-55: [what changed and why] Verification Build: [command] -> [pass/f…

作者头像 李华
网站建设 2026/9/9 23:25:00

基于YOLOv8的AI蒸汽除草机器人:从Ubuntu环境到目标检测实战

各位关注 AI 与机器人方向的朋友们&#xff0c;大家好。今天我想和大家分享一个非常有“落地感”的 AI 项目&#xff1a;AI 蒸汽除草机器人。最近看到明尼苏达州发明家打造无化学除草机器人的相关消息&#xff0c;确实让人眼前一亮。在环保要求越来越高的背景下&#xff0c;用高…

作者头像 李华
网站建设 2026/9/9 23:24:42

JAVA毕设项目:基于 Java 的小型宠物诊所管理系统的设计与实现 宠物诊所服务管理系统的设计与实现 (源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/9 23:24:30

贾子KLA司法理论(Kucius Theory of KLA Justice)纲要——以逻辑自洽原则为法理根基、逻辑审查先于证据审查为核心的司法公正理论体系

标题 贾子KLA司法理论&#xff08;Kucius Theory of KLA Justice&#xff09;纲要 ——以逻辑自洽原则为法理根基、逻辑审查先于证据审查为核心的司法公正理论体系 摘要 本文提出贾子KLA司法理论纲要——一套以逻辑自洽原则&#xff08;KLA, Logical Consistency Axiom&…

作者头像 李华