ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA视频叠加字幕踩坑实录:Video Mixer与AXI VIP仿真调试

FPGA视频叠加字幕踩坑实录:Video Mixer与AXI VIP仿真调试 1. 为什么视频叠加字幕这件事值得单独写一篇踩坑记录做FPGA视频处理的人迟早会碰到一个需求把一路视频流上叠加一层静态字幕或者OSD信息。听起来简单得不行——不就是把两路像素数据按某个规则混一下吗我一开始也是这么想的结果从设计到仿真跑通前后折腾了将近一周中间踩的坑一个比一个隐蔽。这篇文章面向的是已经有一定FPGA基础、正在做或者准备做视频叠加类项目的朋友。我会把整个链路拆开讲从Video Mixer的架构选型到AXI VIP仿真环境的搭建再到Vivado里那些让人抓狂的时序和仿真问题。关键词里的FPGA、Video Mixer、AXI VIP、Vivado、Zynq这些都会覆盖到但我不打算写成手册式的教程而是按照我实际踩坑的顺序来还原整个过程。先说清楚这个项目的核心目标输入是一路视频流假设是RGB888或者YUV422格式需要在上面叠加一层静态字幕比如Camera 01这样的文字输出仍然是同样格式的视频流。字幕本身是静态的不需要动态刷新但位置和透明度要可配置。这个需求在安防监控、工业相机、医疗内窥镜等场景里非常常见。为什么说它值得单独写一篇因为叠加这个动作背后涉及的问题远比想象中多像素对齐、时序匹配、跨时钟域处理、AXI Stream握手协议的细节、仿真环境的搭建、Vivado综合时的资源优化……每一个环节都可能让你卡住半天。而且这些问题在仿真阶段和上板阶段的表现还不一样仿真过了不代表上板没问题上板跑通了也不代表仿真环境搭对了。我这次用的是Zynq平台PL端做视频处理PS端负责配置寄存器。开发工具是Vivado 2020.2仿真用的是Vivado自带的Simulator配合AXI VIP。选这个组合的原因很简单Zynq的PS-PL架构天然适合这种配置处理的场景AXI VIP则是Xilinx官方提供的验证IP能大幅简化AXI Stream接口的仿真工作。2. Video Mixer的架构选择为什么我最终没用Alpha Blending2.1 两种叠加方案的对比视频叠加字幕最直觉的做法是Alpha Blending——把字幕层和视频层按照透明度系数混合。公式很简单Output Video × (1 - α) Subtitle × α。但实际做下来我发现这个方案在FPGA上有一个很尴尬的问题乘法器资源消耗。假设视频是RGB888每个像素3个分量每个分量8bit。Alpha Blending需要3个乘法器每个分量一个如果像素时钟是148.5MHz1080p60那这三个乘法器必须在一个时钟周期内完成计算。在Zynq-7020这种中低端器件上3个8×8乘法器跑148.5MHz虽然不算太难但如果你还要做其他处理比如缩放、去噪资源就会很紧张。我最终选择的方案是查表选择的方式。具体来说字幕层不是半透明的而是二值的——要么显示字幕像素要么显示视频像素。字幕的透明度通过一个预先生成的Alpha Mask来实现Mask的每个bit对应一个字幕像素位置1表示显示字幕0表示显示视频。这样叠加逻辑就变成了一个简单的多路选择器always (posedge pixel_clk) begin if (alpha_mask[addr] 1b1) video_out subtitle_pixel; else video_out video_pixel; end这个方案的好处是资源消耗极低一个LUT就能搞定。坏处是字幕边缘会有锯齿因为二值Mask没有抗锯齿能力。但对于静态字幕来说这个问题可以通过在生成Mask时做预抗锯齿来解决——把边缘像素的Alpha值预先算好存到Mask里运行时直接查表。2.2 字幕数据的存储方式字幕数据存哪里这是个容易被忽略但很关键的问题。我一开始想的是用Block RAM存字幕位图但算了一下一个1920×1080的字幕层如果每个像素1bit那就是2Mbit需要占用大量的BRAM资源。而且大部分区域是空白的浪费严重。后来改成了字符ROM位置索引的方式。字幕内容固定为几个字符比如Camera 01每个字符用8×16的点阵表示总共10个字符就是10×8×161280bit。这些数据存在一个很小的ROM里运行时根据字符位置和行号查表输出。这样BRAM消耗从2Mbit降到了不到2Kbit几乎可以忽略不计。位置索引则通过寄存器配置PS端可以随时修改字幕的起始坐标。这个设计的好处是灵活——你可以随时改字幕内容只要重新生成ROM也可以随时改位置通过寄存器。2.3 跨时钟域问题的处理视频像素时钟和AXI Stream的时钟通常不是同一个。在我的设计里视频输入是148.5MHz像素时钟AXI Stream输出是150MHzPL端AXI时钟。这两个时钟域之间的数据传递必须做同步处理。我用的方案是异步FIFO。视频数据写入FIFO的写端口148.5MHzAXI Stream从读端口读出150MHz。FIFO的深度设为512足够缓冲一行视频数据。这里有一个坑FIFO的读写使能信号必须严格遵循AXI Stream的握手协议否则会出现数据丢失或重复。具体来说AXI Stream的TREADY信号来自下游TVALID来自上游。FIFO的读使能应该是TREADY TVALID而不是简单的TREADY。我一开始就是在这里搞错了导致仿真时数据偶尔会少一个像素查了好久才发现是握手信号的问题。3. AXI VIP仿真环境的搭建那些文档里不会告诉你的细节3.1 为什么选择AXI VIP而不是自己写Testbench自己写AXI Stream的Testbench不是不行但工作量很大。你需要手动实现TVALID/TREADY的握手逻辑、TLAST的生成、TKEEP的处理等等。而且一旦协议细节写错了仿真结果就不可信。AXI VIP是Xilinx官方提供的验证IP它把协议细节都封装好了你只需要配置几个参数就能用。但AXI VIP的文档写得比较简略很多细节需要自己摸索。我在这里踩的坑最多下面逐个说。3.2 VIP实例化时的参数配置陷阱AXI VIP的实例化需要配置大量参数其中最容易出错的是这几个参数名常见错误值正确值说明C_AXI_DATA_WIDTH3224或32必须与视频像素位宽匹配C_AXI_TDATA_WIDTH824或32容易和DATA_WIDTH混淆C_AXI_HAS_TLAST01视频流必须有TLAST标识帧结束C_AXI_HAS_TKEEP01用于标识有效字节我一开始把C_AXI_TDATA_WIDTH设成了8结果仿真时每个像素被拆成了3个beatTLAST的位置全乱了。后来改成24RGB888才正常。另一个坑是C_AXI_HAS_TKEEP。如果你的视频数据是24bit但AXI Stream的位宽是32bit那TKEEP就必须启用用来标识哪3个字节是有效的。我一开始没启用TKEEP结果VIP把无效字节也当成了有效数据导致字幕位置偏移。3.3 VIP的时钟和复位配置AXI VIP需要独立的时钟和复位信号。时钟频率必须与你的设计匹配否则仿真会报时序错误。复位信号必须是同步复位且至少保持一个时钟周期。这里有一个隐蔽的坑VIP的复位信号极性。默认情况下VIP的复位是高电平有效但很多FPGA设计的复位是低电平有效。如果你直接连过去VIP会一直处于复位状态仿真根本跑不起来。我在这里卡了半天后来查了VIP的文档才发现需要配置C_AXI_ARESETN_POLARITY参数。3.4 仿真脚本的编写技巧Vivado的Simulator支持TCL脚本控制仿真流程。我建议把仿真脚本写成独立的TCL文件而不是在GUI里手动操作。这样每次修改设计后只需要重新跑脚本不用重复点击。脚本的核心逻辑是创建VIP实例→配置参数→加载激励文件→运行仿真→检查结果。激励文件可以用CSV或者十六进制格式我习惯用CSV因为可读性好方便调试。# 创建VIP实例 create_bd_cell -type ip -vlnv xilinx.com:ip:axi_vip axi_vip_0 # 配置参数 set_property -dict [list \ CONFIG.C_AXI_DATA_WIDTH {24} \ CONFIG.C_AXI_TDATA_WIDTH {24} \ CONFIG.C_AXI_HAS_TLAST {1} \ CONFIG.C_AXI_HAS_TKEEP {1} \ ] [get_bd_cells axi_vip_0] # 运行仿真 launch_simulation run all4. Vivado综合与实现阶段的资源优化实战4.1 视频叠加逻辑的资源消耗分析综合完成后我第一件事就是看资源报告。Video Mixer的核心逻辑消耗了多少LUT、FF和BRAM结果让我有点意外LUT消耗比预期多了30%。排查后发现问题出在字幕位置的计算上。我原本用了一个乘法器来计算字幕的起始地址行号×行宽列号但Vivado把这个乘法器综合成了多个LUT的组合逻辑。后来改成移位加法行号×1920 行号10 行号8 行号7LUT消耗立刻降了下来。这个经验告诉我在FPGA里乘法器不是不能用但要用在刀刃上。对于常数乘法移位加法几乎总是更优的选择。4.2 时序收敛的常见问题视频处理设计的时序收敛通常比较困难因为像素时钟频率高逻辑层级深。我遇到的主要问题是建立时间违例Setup Violation出现在字幕地址计算和像素选择之间的路径上。解决方法有两个一是插入流水线寄存器把组合逻辑切成两级二是优化地址计算逻辑减少逻辑层级。我两个都用了最终时序余量从-0.5ns变成了0.3ns。这里有一个经验Vivado的时序报告里WNSWorst Negative Slack是最关键的指标。如果WNS是负数说明有时序违例必须解决。TNSTotal Negative Slack则反映了违例的严重程度。我的建议是WNS至少要有0.2ns的余量否则上板后可能会因为温度或电压变化而出现偶发错误。4.3 BRAM的配置优化字幕ROM用的是BRAM但Vivado默认会把ROM综合成分布式RAMLUTRAM消耗大量LUT。我手动把它改成了Block RAMLUT消耗立刻降了200多个。改的方法是在Verilog里用(* ram_style block *)属性(* ram_style block *) reg [7:0] subtitle_rom [0:1279];这个属性告诉Vivado用BRAM来实现这个存储器而不是LUTRAM。对于容量大于64bit的存储器BRAM通常是更好的选择。5. 上板调试仿真过了不代表万事大吉5.1 ILA的配置和使用技巧上板调试离不开ILAIntegrated Logic Analyzer。我在视频数据路径上插了三个ILA一个抓输入视频的TVALID/TREADY/TDATA一个抓字幕叠加后的输出一个抓AXI Stream的握手信号。ILA的采样深度设成了4096足够抓一帧视频的关键片段。触发条件设成TLAST的上升沿这样每次触发都能抓到一帧的结尾方便检查帧边界是否正确。这里有一个坑ILA的采样时钟必须与被抓信号的时钟域一致。我一开始把ILA的时钟设成了系统时钟100MHz但视频像素时钟是148.5MHz结果抓到的数据全是乱的。后来把ILA时钟改成像素时钟才正常。5.2 字幕位置偏移问题的排查上板后第一个问题是字幕位置偏了往右偏移了大约20个像素。仿真时明明是对的为什么上板就偏了排查过程是这样的先用ILA抓字幕起始地址的寄存器值发现PS端写入的值是对的。再抓字幕ROM的输出发现数据也是对的。最后抓像素选择器的输出发现字幕像素确实出现在了错误的位置。问题出在地址计算上。仿真时用的是理想时钟上板后像素时钟有抖动导致地址计算逻辑在某个时钟周期内出现了竞争冒险。解决方法是在地址计算路径上插入一级寄存器把组合逻辑和时序逻辑分开。5.3 PS端寄存器配置的注意事项Zynq的PS端通过AXI-Lite配置PL端的寄存器。这里有一个容易忽略的问题AXI-Lite的写操作是异步的PS端写完寄存器后PL端不一定立即生效。如果PL端在PS端写入的同时正在使用这个寄存器就会出现亚稳态。解决方法是在PL端对寄存器做两级同步。具体来说每个配置寄存器都经过两个触发器同步后再使用always (posedge pixel_clk) begin reg_sync1 reg_from_ps; reg_sync2 reg_sync1; end这样虽然会增加一个时钟周期的延迟但能保证寄存器的值稳定可靠。6. 几个让我印象深刻的坑和最终解决方案6.1 AXI VIP的BVALID信号问题AXI VIP的BVALID信号是写响应通道的有效信号。在仿真时我发现BVALID偶尔会莫名其妙地拉高导致仿真报错。查了很久才发现这是因为VIP的写响应通道没有正确初始化。解决方法是在仿真开始前手动给VIP的BVALID信号一个初始值低电平并在复位期间保持。具体来说在Testbench里加一段初始化代码initial begin axi_vip_0.inst.bvalid 1b0; axi_vip_0.inst.bready 1b0; #100; // 释放复位 end这个问题的根源是VIP的内部状态机在复位后没有正确初始化导致BVALID信号处于不确定状态。虽然文档里没有明确说明但这是实际使用中必须处理的问题。6.2 Vivado生成比特流失败的排查Vivado生成比特流时失败报错信息是Error when launching Vivado。这个错误很笼统可能的原因有很多。我遇到的是DRCDesign Rule Check错误具体是某个IO引脚没有分配。排查方法是打开Vivado的TCL Console输入report_drc -checks all会列出所有的DRC违例。根据违例信息逐个解决即可。常见的DRC问题包括IO标准不匹配、时钟约束缺失、BRAM初始化冲突等。6.3 仿真与上板结果不一致的通用排查思路仿真过了但上板不对这是FPGA开发中最让人头疼的问题。我的排查思路是先确认时钟和复位是否正确。用ILA抓时钟信号确认频率和占空比符合预期。再确认数据路径是否一致。用ILA抓关键节点的数据与仿真波形对比。最后确认配置寄存器是否正确。用PS端读回寄存器值确认写入生效。大部分问题都能通过这三步定位。如果还找不到原因那就检查时序约束是否完整特别是跨时钟域的约束。7. 一些可以复用的经验总结7.1 视频叠加设计的通用架构经过这次项目我总结了一个通用的视频叠加架构适用于大多数静态字幕/OSD场景输入视频经过异步FIFO进入像素处理时钟域字幕ROM根据位置索引输出字幕像素Alpha Mask控制像素选择输出经过异步FIFO回到AXI Stream时钟域PS端通过AXI-Lite配置字幕位置和内容这个架构的优点是资源消耗低、时序容易收敛、可扩展性好。如果需要动态字幕只需要把字幕ROM换成双端口RAMPS端动态更新内容即可。7.2 仿真环境的搭建清单如果你也要用AXI VIP做仿真以下是我建议的检查清单VIP的DATA_WIDTH和TDATA_WIDTH是否与设计匹配TLAST和TKEEP是否启用复位极性是否正确时钟频率是否与设计一致BVALID信号是否初始化激励文件格式是否正确7.3 上板调试的必备工具ILA至少两个一个抓输入一个抓输出VIO用于动态修改寄存器值方便调试串口打印PS端打印关键日志辅助定位问题我在实际使用中发现VIO特别有用。它可以在不重新综合的情况下修改寄存器值大大加快了调试速度。比如字幕位置偏了用VIO改一下坐标就能立刻看到效果不用每次都重新跑综合。7.4 关于Vivado版本的选择我这次用的是Vivado 2020.2整体比较稳定。有朋友问我要不要升级到2025.1我的建议是如果你的设计已经稳定运行不要轻易升级。新版本虽然功能更多但可能会引入新的bug而且IP核的兼容性也需要重新验证。除非你需要新版本特有的功能否则保持现有版本就好。另外Vivado的安装路径不要有中文和空格否则会出现各种奇怪的问题。我见过有人因为路径里有空格导致综合失败的案例排查了半天才发现是路径问题。7.5 工程清理的重要性Vivado工程用久了会变得很大因为每次综合都会生成大量中间文件。定期清理工程可以节省磁盘空间也能加快打开速度。清理的方法是删除工程目录下的.Xil文件夹和*.jou、*.log文件但不要删除.srcs和.xpr文件。如果工程已经无法正常打开可以尝试删除.Xil文件夹后重新打开Vivado会自动重建索引。8. 写在最后的个人体会这个项目从设计到仿真跑通再到上板调试前后花了大约一周时间。其中大部分时间不是在写代码而是在排查各种意想不到的问题。AXI VIP的BVALID信号、字幕位置偏移、时序违例……每一个问题都让我对FPGA视频处理有了更深的理解。如果让我给正在做类似项目的朋友一个建议那就是仿真环境一定要搭好不要跳过仿真直接上板。虽然搭仿真环境很麻烦但它能帮你提前发现80%的问题。剩下的20%上板问题大部分也能通过ILA和VIO快速定位。另外不要迷信文档。AXI VIP的文档里没有提到BVALID初始化的问题Vivado的文档里也没有说清楚BRAM的ram_style属性怎么用。这些细节只能通过实际踩坑来积累。我写这篇文章的目的就是希望后来者能少走一些弯路。最后分享一个小技巧在Vivado里用TCL脚本自动化常用操作比如生成比特流、导出硬件、启动SDK等。这样每次修改设计后只需要跑一个脚本不用重复点击菜单。脚本可以保存成.tcl文件通过source命令执行。这个习惯能帮你节省大量时间特别是在频繁迭代的阶段。
返回列表