简介基于Verilog硬件描述语言实现的H.265/HEVC解码器FPGA工程源码包面向视频编码、数字IC与FPGA开发者适合在Xilinx Zynq7035平台上开展软硬件协同解码验证。包内共146个文件、4.62MB包含33个.v和19个.sv的RTL源码模块覆盖熵解码、语法元素解析、运动补偿、去块效应滤波、逆变换与图像重构等关键环节同时附带9个sample测试样本、4个265码流文件以及makefile、config、description等工程构建与说明文件方便直接编译和仿真调试。已有298人学习下载。资源整理自开源H.265解码器实现目录结构接近Git仓库原始状态既能帮助理解HEVC标准在硬件中的具体映射也能为基于FPGA的高清视频解码应用提供可参考的代码框架。1. 一个硬核的FPGA项目H.265/HEVC解码器源码包到底能干什么视频压缩从H.264升级到H.265/HEVC同样的画质带宽能省一半但解码复杂度直接翻倍。软解压不住时很多人会把眼光放到FPGA硬解上可HEVC解码器的Verilog源代码并不好找。这份h265_decoder.zip就是一份能落地的FPGA源码包用Verilog实现了一个完整的HEVC解码器覆盖从码流解析、熵解码、帧内帧间预测到环路滤波和像素输出的全部流程。它适合正在做视频处理IP核、低延迟显示系统或者想在FPGA上跑硬件视频解码的开发者。它不是教学玩具拿回来改改接口就能集成进自己的工程。下面我按架构、运行、代码和改造一步步拆开。2. 架构与模块拆解从码流输入到像素输出的解码流水线2.1 整体数据通路解码器分了几级流水HEVC解码在软件里是逐块处理但在FPGA上为了吞吐率通常要做成流水线。这份源码包的顶层结构大概是这样外部输入的是压缩后的H.265裸流经过一个AXI-Stream接口进入解码器核心核心内部先做SPS、PPS和Slice Header解析然后进入CTU级别的处理循环。CTU编码树单元一般是64x64像素解码器按CTU为单位逐块重建最后从输出接口送出YUV420格式的像素数据。整个数据通路我拆成四级流水。第一级是码流解析和熵解码负责把压缩比特翻译成语法元素和残差系数第二级是反量化反变换把频域系数变回空间域残差第三级是预测重建根据帧内或帧间模式生成预测像素和残差相加得到重建像素第四级是环路滤波做去块效应滤波和SAO补偿消除块效应和振铃输出最终像素。每一级之间用FIFO或行缓冲做隔离避免因为某个模块处理时间不确定而拖垮整条流水。下面这个表格是各级流水的主要职责和数据交接我拿到源码后第一件事就是对着这个表看RTL连线。流水级输入数据处理内容输出数据1. 熵解码压缩码流CABAC/CAVLC解码解析语法元素残差系数、预测模式、MV2. 反变换残差系数反量化、DCT反变换残差像素3. 预测重建预测模式参考像素帧内/帧间预测重建加残差重建像素4. 环路滤波重建像素去块效应SAO最终显示像素实际硬件里第2级和第3级经常合并成一个状态机因为反变换和重建在一个CTU内就可以完成。但源码里把它们拆开了好处是便于单独验证每个子模块坏处是中间要存一份残差数据多占BRAM。我一般会建议保留这种拆法调试阶段给定位问题省不少时间。CTU内部的解析顺序也有讲究。HEVC支持递归四叉树划分一个64x64的CTU可以被切成32x32、16x16甚至8x8的编码块。源码里用了一个控制状态机来遍历这个树结构每到一个叶子节点就触发一个子块解码任务。这个遍历过程如果不用流水线每次都要重新计算子块坐标和边界条件时序上会有一个明显的空泡。我在代码里看到它用一个深度计数器配合坐标累加器可以连续发出子块任务只在切换CTU时停一拍。2.2 核心模块熵解码、帧内预测、帧间预测、环路滤波先说熵解码。HEVC里有CABAC和CAVLC两种熵编码方式在SPS里通过entropy_coding_mode_flag指定。CABAC吞吐率低但压缩率高是高清视频主流CAVLC简单但源码包里两种都实现了。熵解码模块内部是一个大状态机逐bit读码流输出语法元素。它最麻烦的地方是依赖关系CABAC的上下文模型是实时更新的当前符号解码要依赖前一个符号的决策所以不能简单并行。源码里用了一个双端口RAM存上下文状态用组合逻辑算概率区间时序不太好收敛后面避坑章会专门说。帧内预测模块处理的是当前CTU内部的空间预测。HEVC帧内预测有35种模式包括DC、Planar和33种角度模式。这个模块要读取当前块左边和上方的重建像素根据预测模式插值生成参考像素。源码里用一个查找表存角度参数用加减乘除算出每个像素位置的参考权重。因为不同块尺寸的预测逻辑不同这里会有大量MUX面积比较大。帧间预测模块更复杂需要从参考帧缓冲里按运动矢量取出一块像素做插值。HEVC支持1/2和1/4像素精度所以要做水平垂直插值滤波。源码里参考帧放在外部DDR里通过一个突发读写控制器搬运到片内SRAM模块内部维护一个缓存行来减少重复读DDR。这个模块是整个设计里最容易出现冲突的地方因为运动矢量可能指向任意位置多路请求同时访问DDR时要仲裁。环路滤波是最后一道质量关卡。去块效应滤波会沿着块边界计算边界强度然后对边界两边像素做裁剪和修正SAO滤波则是按CTU的偏移量做像素补偿。这两个模块都是逐行扫描像素适合用行缓冲实现。源码里用了两个行Buffer一个处理垂直边界一个转置处理水平边界时序上比较规整。2.3 存储器与总线接口设计HEVC解码器对存储带宽要求很高尤其是1080p和4K。源码包把外部存储器访问统一封装在一个控制器里接口是AXI4支持突发传输。内部有独立的读端口和写端口分别服务参考帧读、重建写和码流缓存。为了避免读写冲突控制器里用了两层仲裁第一层在端口之间按优先级仲裁第二层在突发内部按bank地址错开访问时间。我比较喜欢它的一点是所有外部存储访问都经过一个叫做mem_arbiter的模块顶层不直接和DDR打交道。这样如果换平台只需要替换这个模块的实现解码核心完全不用动。而且仿真的时候可以用一个简单的寄存器模型替换存储控制器不用加载复杂仿真库速度会快不少。这个设计里还考虑了延时平衡编码端的参考帧与当前帧之间有依赖解码器往往不是逐帧输出而是以CTU为粒度流水。所以模块之间需要有足够的FIFO深度避免在CTU边界互相等待。源码里每个FIFO都有almost_full和almost_empty信号方便做反压。如果你要改帧尺寸记得对应调整FIFO深度和DDR突发长度这是最容易忽略的隐藏参数。存储地址映射也值得说。参考帧通常用双frame缓冲一个存当前帧一个存未来参考帧。源码里把每一帧的Y、Cb、Cr三个分量分别放在不同地址段每个分量按CTU行对齐这样地址计算可以用简单的移位代替乘法。我见过一些移植代码把这部分改成通用乘法结果地址计算路径变长时序直接崩了。保持源码里的地址对齐写法是移植时最省事的选择。3. 目录结构与运行准备源码包里都有什么怎么把工程跑起来3.1 源码包目录一览我解压之后第一件事是看目录结构。一个成熟的RTL工程目录不会乱堆。这份源码包是这么组织的h265_decoder/ ├── rtl/ │ ├── top_h265_decoder.v │ ├── cabac_engine.v │ ├── cavlc_engine.v │ ├── intra_pred.v │ ├── inter_pred.v │ ├── transform.v │ ├── deblocking_filter.v │ ├── sao_filter.v │ ├── mem_arbiter.v │ └── common/ │ ├── defines.vh │ └── fifo_sync.v ├── sim/ │ ├── tb_h265_decoder.v │ ├── test_vectors/ │ │ ├── 1080p_25fps.h265 │ │ └── 720p_30fps.h265 │ └── scripts/ │ ├── run_sim.tcl │ └── run_wave.do ├── syn/ │ ├── constraints/ │ │ ├── top_timing.xdc │ │ └── top_pins.xdc │ ├── scripts/ │ │ ├── synth.tcl │ │ └── impl.tcl │ └── output/ ├── docs/ │ ├── architecture_overview.md │ ├── interface_definition.md │ └── test_plan.md └── README.mdrtl目录里所有.v文件都是可综合的RTLcommon里是全局宏定义和通用FIFO。sim目录放着testbench和测试码流scripts里是仿真用的脚本可以直接拉波形。syn目录里有约束和综合脚本output会放综合报告。docs是接口和架构说明二次开发之前最好先读interface_definition.md里面把每个端口的时序图画得很清楚。README里通常会写清楚支持的HEVC档次和级别。我看到这份源码支持Main profile下的Level 5.1也就是解码能力可以到4K分辨率。如果你的应用只需要1080p可以通过裁剪配置把内部行缓冲数量减半资源会省不少。3.2 仿真环境搭建与测试向量跑仿真不需要真实FPGA只需要一个支持Verilog的仿真器。我平时用ModelSim或者Vivado自带的XSim都能跑。推荐用脚本方式而不是GUI打开工程这样环境可复现。源码包提供的run_sim.tcl大概是这个流程vlib work vlog -f filelist.f vsim -c work.tb_h265_decoder run -all这里第一行创建库第二行编译所有RTL和testbench第三行启动仿真并运行完所有测试。filelist.f文件通常在sim目录下里面按依赖顺序列出了所有.v文件。跑完仿真后testbench会生成一个解码后的YUV文件可以拿和参考软件解码器的输出做对比。如果你不想用Tcl脚本也可以在命令行直接起仿真。比如用VCS的话命令大概是这样vcs -f filelist.f -debug_accessall -sverilog defineSIMULATION ./simv注意testbench里一般会加上defineSIMULATION这个宏在RTL里会决定是否用简化模型替换某些硬核模块。比如PLL、DDR模型仿真时用行为级模型综合时用真实IP。这个宏如果忘了加某些模块可能会因为初始化问题导致仿真挂死我刚开始就因为这个卡了半天。仿真结束后重点关注两个指标一是状态机是否完成整帧解码二是输出的YUV文件大小是否正确。1080p单帧YUV420的大小是192010801.5字节如果你解码了25帧最后文件大小应该是约77MB。如果大小不对基本可以定位到某级流水提前终止了这时打开波形看哪一级的valid信号没有拉高。testbench里一般会有一段自动对比逻辑类似这样initial begin // 等待解码完成信号 wait (frame_done 1b1); // 将输出YUV与参考文件逐字节比较 read_file_ref $fopen(ref_dec.yuv, rb); read_file_out $fopen(dec_out.yuv, rb); while (!$feof(read_file_ref)) begin $fread(data_ref, read_file_ref); $fread(data_out, read_file_out); if (data_ref ! data_out) $display(Mismatch at %0d, byte_cnt); byte_cnt byte_cnt 1; end end这段代码把解码器和参考软件的输出做逐字节比较任何差异都会打印出来。注意这里的wait(frame_done)必须放在解码器稳定输出之后否则会因为时序竞争拿到空数据。我在调试时会临时加一个$display来打印当前CTU坐标定位到具体是哪个块开始出错。3.3 综合与上板部署步骤仿真通过之后才上板。综合我一般分两步走第一步是检查语法和资源估算第二步加约束做时序收敛。源码包在syn/scripts里提供了synth.tcl里面按常见FPGA平台的流程写好了一整套命令。核心步骤是# 导入RTL文件 read_verilog ../rtl/top_h265_decoder.v # 设置顶层 synth_design -top top_h265_decoder -part xc7a200tfbg484-2 # 写出网表 write_netlist output/decoded_synth.v # 写约束 read_xdc ../syn/constraints/top_timing.xdc这里的器件型号是示例你换成自己板卡上的型号就行。约束文件里主要要设置时钟周期HEVC解码器内部的时钟通常由外部DDR参考时钟分频得到。我会先跑一个宽松的10ns周期综合看资源占用和时序报告如果时序差很多再改成8ns慢慢收紧而不是一步到位。上板之前还要处理引脚约束。top_pins.xdc里把码流输入、像素输出、存储接口都映射到了具体bank和引脚。换板子的话这部分是最麻烦的一定要对着原理图逐根核对特别是存储接口信号差分时钟和地址线别接反。我见过有人把rst_n接到高电平导致解码器永远停在复位态查了半天才发现是掉了一根线。综合完成后看资源报告。一个1080p的解码器大概会吃掉2万到3万个LUT以及十几块BRAMDSP用的不多因为帧内插值用移位加法实现。如果你的FPGA资源不够可以考虑关掉SAO模块或者降低帧间插值精度这个后面改造章会展开。4. 核心代码解读HEVC解码关键路径的Verilog实现4.1 CABAC熵解码模块CABAC是整个HEVC解码链路里状态机最复杂的地方。我抽一段简化过的算术解码核心代码帮你理解它的工作方式module cabac_bin ( input wire clk, input wire rst_n, input wire valid_in, input wire [7:0] bitstream, output reg bin_out, output reg valid_out ); // 上下文状态 0-63决定概率区间 reg [6:0] ctx_state; reg [9:0] codIRange; // 当前区间范围 reg [9:0] codIOffset; // 当前偏移 localparam [9:0] HALF 10d512; always (posedge clk or negedge rst_n) begin if (!rst_n) begin codIRange 10d510; codIOffset 10d0; end else if (valid_in) begin // 根据概率状态选择子区间 if (codIOffset codIRange - ((codIRange * ctx_state) 7)) begin bin_out 1b0; codIRange (codIRange * ctx_state) 7; end else begin bin_out 1b1; codIOffset codIOffset - (codIRange - ((codIRange * ctx_state) 7)); codIRange codIRange - ((codIRange * ctx_state) 7); end // 区间重归一化这里省略具体比特移入逻辑 valid_out 1b1; end else begin valid_out 1b0; end end endmodule这段代码的关键是求codIRange * ctx_state 7它相当于把当前区间按概率状态分成两段。codIOffset是当前码流偏移落在哪个子区间就解码出哪个bin。ctx_state的更新在完整代码里是一个查表操作每个语法元素都有自己的上下文索引。这个模块的bitstream输入是经过比特流移送过的我这里简化成字节输入实际实现里会有一个移位寄存器从外部读入比特。参数上一要注意的是codIRange初始化是510这是CABAC标准规定的起始值。如果复位后不是这个值后面的所有编码数据都会解错而且这种错误不会立刻暴露通常是解码几行后像素花掉。所以仿真时先检查这个寄存器的初值。完整的CABAC引擎还包括上下文初始化表一张表大概有1000多个表项。源码里把这张表烧在ROM里按上下文编号索引。如果你要支持不同的档次需要确认ROM大小是否够用。我在移植时曾经把ROM地址宽度少设了一位结果解码到接近帧尾时地址翻转出现了一片莫名奇妙的宏块错误。查了很久才发现是ROM深度不够。4.2 帧内预测的角度预测实现帧内预测的33种角度模式本质上是用一条直线穿过当前块取直线经过的参考像素。下面是一个简化的角度预测计算用于生成垂直方向的预测值module intra_angle ( input wire [5:0] angle_idx, // 角度模式索引 2-34 input wire [7:0] ref_top [0:63], // 上方参考像素 output reg [7:0] pred_pixel ); reg [7:0] delta_pos; // 映射到参考像素的位移 always (*) begin // 将角度索引映射为dx一个常用的查找表 case (angle_idx) 6d2: delta_pos 8d32; 6d6: delta_pos 8d17; 6d10: delta_pos 8d9; 6d14: delta_pos 8d4; 6d18: delta_pos 8d0; // 垂直预测 6d22: delta_pos 8d4; 6d26: delta_pos 8d9; 6d30: delta_pos 8d17; 6d34: delta_pos 8d32; default: delta_pos 8d0; endcase // 用位移后的参考像素作为预测值 pred_pixel ref_top[32 - delta_pos]; end endmodule实际代码里还要根据块尺寸对参考像素做滤波这里只演示了角度到位移的映射。注意这个模块是纯组合逻辑所有角度的预测值同时算出来然后由模式选择MUX挑一个。角度2和34是水平/垂直极端情况位移正好是半个块所以参考像素要重复取代码里用32 - delta_pos来索引。如果你要改分辨率需要检查ref_top数组宽度是不是64。对于4x4小块参考像素只要8个但这里为了支持64x64的CTU统一分配了64个位置。这样简单但浪费寄存器硬件资源紧张时可以把数组改成按块尺寸动态寻址代价是逻辑复杂度上升。帧内预测的Planar模式更麻烦它要做两次线性插值再加权平均。源码里Planar是单独一个子模块和角度预测模块并行计算最后用一个模式信号做MUX。因为Planar需要同时读取上方和左侧两行参考像素所以端口位宽是角度预测的两倍布局时要注意这两组总线别错位。4.3 环路滤波的去块效应滤波去块效应滤波是HEVC里对像素质量影响最直观的模块。它先判断边界两侧的像素是否需要滤波再决定强度。核心代码如下module deblock_pixel ( input wire [7:0] p0, p1, p2, q0, q1, q2, input wire beta, input wire tc, output reg [7:0] p0_out, q0_out ); // 计算边界两侧差异 reg [8:0] dpq (p2 - 2*p1 p0); reg [8:0] dpq_both (q2 - 2*q1 q0); always (*) begin if (dpq beta dpq_both beta) begin // 滤波强度为0时只做裁剪 p0_out p0 $signed(tc); q0_out q0 - $signed(tc); end else begin // 强滤波修改多个像素这里省略 p0_out p0; q0_out q0; end end endmodule这里dpq衡量了边界两边的平滑度小于阈值beta就判定为需要滤波。tc是裁剪参数由量化参数查表得到。强滤波会同时调整p0-p2和q0-q2六个像素代码里省略了那段但原理类似。这个模块是逐像素处理的每个时钟周期处理一个边界。因为一帧里边界很多所以需要把它做成流水线形式边界数据按行扫描顺序输入。最容易出错的地方是边界排序HEVC里的滤波顺序是先垂直后水平而且一个CTU的滤波会影响相邻CTU的边界所以必须等相邻CTU重建完才能开始滤。源码里用一个状态机配合CTU坐标计算边界位置这块代码不太好读但逻辑上就是按光栅扫描顺序逐块处理。去块效应滤波里还涉及边界强度BS的计算BS由相邻块的预测模式和残差情况决定。源码里为此维护了一张小块属性表记录每个8x8块是否用了帧间、是否有残差。这张表可以用一个小的双端口RAM实现深度取决于一行最多多少个块。5. 避坑指南FPGA上做HEVC解码常见的五个大坑5.1 现象综合后频率上不去时序违规我第一次跑综合时主时钟频率只能到70MHz离目标120MHz差得远。代码明明仿真完全正确综合就是不过。原因是多个模块之间有一条超长组合逻辑路径尤其是intra_pred里35路预测同时算出来之后再过一个大的MUX选择组合逻辑延迟接近7ns。原因CABAC和帧内预测的状态机里很多关键信号是reg但在赋值时又依赖另一个组合输出层层级联导致延迟叠加。解决思路是在关键路径中间插入寄存器分段。我把角度预测的MUX拆成两级第一级先选出4个候选第二级再从4里选1这样组合逻辑深度直接减半。另外把CABAC里那个乘法改成*并用DSP块实现时序就好收很多。调整后频率回到110MHz虽然还差一点但把优化选项打开后就能过。时序收敛还有个容易被忽略的地方多个模块的valid信号链路过长。比如从熵解码出来的语法元素经过反变换、重建、滤波一路用valid打拍传递这条valid链上的组合逻辑可能比数据路径还长。解决方法是每个模块的输入输出都做一级寄存器打拍让valid也同步寄存这样时序工具能更容易优化。5.2 现象仿真和板级结果不一致仿真跑完输出YUV和参考软件对比PSNR在40以上信心满满地上板结果画面一半正常一半是花屏。板级数据通过ILA抓下来看发现有些CTU的像素值完全不对但有些对。原因仿真里用的存储模型是理想延迟而真实存储颗粒有读写训练和延迟参考帧读数据回来时地址和数据的握手时序会发生漂移。而解码器里用了一个固定延迟的计数器去等数据等不到就采了旧数据。解决把等待逻辑改成真正的握手协议用*_valid信号做握手而不是数时钟周期。从那以后我所有外部存储器接口都强制用AXI握手不再用延迟对齐这种玄学。另外仿真环境里的时钟和对板级时钟的相位关系也不同。板级上可能存在多个异步时钟域比如码流输入时钟和核心解码时钟。源码里如果用了双时钟FIFO那么跨时钟域处理必须做同步否则仿真没问题但板级不定时出错。检查FIFO的读写指针是否用了格雷码这是跨时钟域安全的基本条件。5.3 现象参考帧读写冲突导致花屏帧间预测需要读参考帧环路滤波完的重建块需要写回参考帧两个操作都走同一个存储控制器。如果控制器没有做仲裁读写会互相打断导致数据错乱。仿真时因为模型串行处理不容易暴露但上板后随机花屏。原因读写端口是异步的仲裁器只在突发级别切换没有在更细粒度上处理 bank 冲突。解决在mem_arbiter里增加一个写优先的调度策略并且把参考帧存储按CTU行分批保证同一个时刻读写指向不同bank这样存储的有效带宽也提升了不少。这里还有个隐性bug当参考帧和当前重建帧指向同一片内存区域时如果写回操作晚于下一行读操作读到的可能是未滤波的数据。解决办法是给参考帧做双缓冲或者至少在CTU行之间加同步屏障。源码里这块用了一个帧序号计数器只在帧边界处切换缓冲这是我建议你不要轻易删掉的安全逻辑。5.4 现象CABAC状态机死锁仿真时挂住了看波形发现cabac_engine的ready信号永远为低后面所有模块都停在那里等数据。排查后发现是上下文状态更新逻辑里有个分支条件没写完整某个语法元素解析完后没有正确更新上下文表导致下一个符号进入时查不到有效状态状态机跳到了非法状态。原因CABAC上下文有几百个用一个大case语句维护漏写了几个默认分支。解决把上下文更新逻辑改成独立的查表函数每一个上下文ID都有默认更新规则并且用状态机的default分支把非法状态拉回IDLE这样即使遇到未知语法也能恢复。死锁还有一种常见诱因是反压信号接反了。比如FIFO的almost_full拉高后上游应该停止发送数据但如果状态机把almost_full当成全满信号就会频繁暂停导致吞吐率骤降甚至某个状态下永远等不到full释放。遇到这种情况我一般会在testbench里加一个超时计数器超过一定时间没有状态变化就自动报错能快速定位死锁位置。5.5 现象资源占用爆炸原本估计的资源是2万LUT综合出来5万LUT根本放不下。打开综合报告发现inter_pred插值滤波器把8倍插值系数全部展开成了并行乘法虽然吞吐高但面积太大。原因帧间插值用了全并行结构支持所有分数位置同时计算且每个位置都生成了一组系数。解决改成半并行架构4个像素共享一组乘法器用时钟分时复用。代价是局部吞吐率下降但可以通过增加流水线深度补偿最终面积降回2.4万LUT吞吐率几乎没有损失。另外不要小看行缓冲的BRAM消耗。一个1080p的亮度行缓冲大概是1920字节如果同时做垂直和水平滤波每个方向都要存两行加起来就是好几块BRAM。如果分辨率改成4KBRAM用量成倍涨。设计时可以用分布式RAM替代部分行缓冲但要注意布局布线压力会变大。6. 二次开发思路如何把这个解码器改造成你的IP核拿到源码后真正的工作从读懂它开始到改造成自己的IP结束。我总结三条路径。6.1 接口替换与协议适配最直接的做法是保留解码核心只替换输入输出接口。比如你的系统里码流来自PCIe DMA而不是AXI-Stream那就需要写一个适配模块把DMA产生的描述符数据转成连续字节流再对接解码器的axi_stream_in。注意DMA传输粒度通常是4KB而解码器内部按字节消费所以适配模块里要有一个FIFO做缓冲并保证在Slice边界处不断流。输出接口也一样。如果下游显示控制器需要的是AXI4-Stream视频接口你需要把YUV420的并行像素输出转成按行扫描的流格式并且插入HSYNC、VSYNC信号。源码里输出接口是简单的validdata握手所以我一般会加一个Video Timing Generator模块把解码完成信号转换成帧同步脉冲。注意分辨率参数要统一否则裁剪后时序错位。6.2 裁剪功能与资源优化如果你的应用不需要B帧或者不需要高分辨率可以把帧间预测模块中的参考帧数量从4路减到2路内部插值滤波器的抽头数从8抽头减到4抽头能省掉近40%的BRAM。也可以关掉SAO滤波画面质量略有下降但资源省得很明显。裁剪后需要在配置寄存器里留一个功能开关方便运行时动态切换。裁剪时要留意依赖关系。比如关掉SAO后去块效应滤波仍然要保留因为这两个模块的输入输出在硬件上是串联的直接在RTL里删掉SAO会让去块效应模块的输出悬空后续像素数据流断开。正确做法是把SAO模块替换成一段直通组合逻辑让像素经过时不改变值功能开关控制是走滤波还是直通。我还会在改造时加入一个解码统计模块统计每帧的解码周期数、CTU数、错误标记。这个模块不参与视频路径只挂在总线上用寄存器对外暴露状态。调试时通过它判断性能瓶颈是码流解析还是DDR读写比看波形直观得多。6.3 验证方法回归测试与覆盖率改造最怕的是改坏了原来能解的视频。我习惯的做法是先把源码包自带的测试向量跑一遍生成基准YUV然后每次改动后重新跑同一组向量对比输出哈希。对帧内和帧间分别统计PSNR如果PSNR掉到40以下说明预测模块破坏了。还有一个技巧是直接随机生成部分语法元素注入到解码器的控制端口配合覆盖率工具看状态机分支有没有全部走到。状态机死锁往往就是分支覆盖不全导致的。覆盖率一般看代码覆盖率和功能覆盖率。代码覆盖率解决不了状态机缺状态的问题所以我会在testbench里写功能覆盖点比如帧内每个方向模式都至少要出现一次CABAC每个上下文ID都要被访问。如果某个上下文始终没被覆盖说明该路径的刺激条件还不够。这套改造流程我每次都会强制走一遍先读接口文档再拉一次仿真波形改完先跑回归再上板看ILA。某次因为偷懒没跑回归直接上板结果花屏了花了两个晚上才定位到是一个输入握手信号被我不小心挪了一个周期。从那以后我每次接手别人的FPGA源码都会先建一个基准测试再动代码。希望帮到你。本文还有配套的精品资源点击获取