ARTICLE DETAIL

资讯详情

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

纯Verilog实现FPGA硬件PNG解码:DEFLATE与Huffman解码实战

纯Verilog实现FPGA硬件PNG解码:DEFLATE与Huffman解码实战 1. 为什么要在FPGA里硬解PNGPNG这种格式做图像处理的朋友都不陌生。无损压缩、支持透明通道、浏览器和操作系统原生支持几乎成了截图和素材分发的默认选择。但如果你把PNG丢进FPGA项目里问题马上就来了FPGA没有操作系统没有libpng没有zlib甚至连个像样的堆内存管理都没有。你拿到的只是一串字节流得自己从文件头开始一个字节一个字节地啃。我最早接触这个需求是在一个工业相机项目上。上位机传过来的参考图像是PNG格式需要在FPGA端实时解码出来做比对。当时第一反应是让上位机转成BMP或者RAW再发下来但客户不干说他们的图像库全是PNG改格式等于重做整个数据链路。没办法只能硬着头皮在FPGA里实现PNG解码。PNG解码的核心难点不在解压算法本身而在于整个数据流的组织方式。PNG用的是DEFLATE压缩这玩意儿结合了LZ77和Huffman编码解压过程需要维护一个32KB的滑动窗口还要动态构建Huffman树。更麻烦的是PNG的滤波机制每个扫描行前面有一个滤波类型字节需要根据它做反向滤波才能还原原始像素。这些操作在CPU上就是几行代码的事但在FPGA里你得用状态机、BRAM和流水线把它们全部硬件化。纯verilog实现的好处在于可移植性。不管你用的是Xilinx、Altera还是国产FPGA只要综合工具支持verilog-2001这套代码就能跑。不依赖任何厂商IP不需要软核处理器资源占用可控时序收敛也相对容易。我见过太多项目因为用了厂商特定的IP核换平台时整个重写那种痛苦经历过一次就够了。这套方案适合谁呢如果你正在做FPGA图像处理、视频采集、工业检测或者任何需要从存储介质读取PNG素材的场景这套代码可以直接拿来用。如果你只是想学习DEFLATE解压算法或者Huffman解码的硬件实现这套工程也是很好的参考。当然前提是你得懂基本的verilog语法知道什么是状态机、什么是BRAM、什么是流水线。完全零基础的话建议先把verilog计数器、case语句、模块例化这些基础打牢再看。2. PNG解码的整体架构设计2.1 从文件头到像素输出的数据流拆解PNG文件的结构是分块的每个块有固定的格式4字节长度、4字节类型、数据区、4字节CRC。第一个块必须是IHDR里面包含图像的宽、高、位深、颜色类型、压缩方法、滤波方法和隔行扫描方式。接下来可能有PLTE调色板块、tRNS透明块、gAMA、pHYs等辅助块然后是IDAT数据块可能有一个或多个最后以IEND结束。解码流程可以分成几个阶段首先是文件头解析提取IHDR中的关键参数然后是IDAT数据收集把所有IDAT块的数据拼接成一个连续的压缩数据流接着是DEFLATE解压把压缩数据还原成滤波后的扫描行数据最后是反向滤波和颜色空间转换输出最终的RGB或RGBA像素。在FPGA里实现这个流程我采用的是分段流水线架构。文件头解析用一个独立的状态机IDAT收集用FIFO缓冲DEFLATE解压是核心模块反向滤波和颜色转换放在最后。每个阶段之间用FIFO或者双口BRAM做数据缓冲这样可以实现流水线并行提高吞吐率。为什么不用一个大的状态机从头做到尾因为PNG解码的各个阶段速率差异很大。文件头解析可能几十个周期就完成了但DEFLATE解压需要处理成千上万个符号。如果串行执行大部分时间都在等最慢的阶段。分段流水线可以让各个阶段同时工作整体吞吐率取决于最慢的那个阶段而不是所有阶段之和。2.2 模块划分与接口定义整个工程我划分了以下几个核心模块png_header_parser负责解析IHDR块输出图像宽度、高度、位深、颜色类型等参数。接口包括输入字节流、输出参数有效信号和参数总线。idat_collector从字节流中筛选出IDAT块的数据写入一个大的BRAM缓冲区。需要处理多个IDAT块连续拼接的情况。inflate_decoderDEFLATE解压核心包含Huffman解码、LZ77滑动窗口和输出控制三个子模块。unfilter_engine反向滤波根据每行的滤波类型字节对像素数据做还原。需要维护前一行的像素数据用于Up、Average和Paeth滤波。pixel_formatter颜色空间转换把解码后的像素数据转换成统一的RGB888或RGBA8888格式输出。模块之间的接口我统一采用valid-ready握手协议数据位宽根据阶段不同有所调整。文件头解析和IDAT收集用8位字节流DEFLATE解压内部用32位符号流反向滤波和像素输出用24位或32位像素流。这里有个设计决策值得说一下为什么IDAT数据要先收集到BRAM再解压而不是边收边解因为DEFLATE的Huffman树是动态构建的在解压第一个符号之前必须先读完整个动态Huffman头。如果边收边解状态机会变得非常复杂而且BRAM的读写冲突会增加。先收集再解压逻辑清晰调试也方便。代价是需要一块额外的BRAM对于大多数FPGA来说这点资源不算什么。2.3 资源估算与选型建议以一张1024x768的RGB888 PNG图片为例解码过程中需要存储的数据包括压缩数据缓冲区、解压后的扫描行数据、前一行的像素数据、Huffman码表、滑动窗口。压缩数据缓冲区的大小取决于PNG文件大小一般按最大文件尺寸来分配。1024x768的RGB888原始数据是2.25MBPNG压缩后通常在500KB到1MB之间。我一般分配1MB的BRAM作为压缩数据缓冲区用双口BRAM实现位宽32位深度256K。解压后的扫描行数据一行1024像素RGB888每像素3字节一行就是3KB。加上滤波类型字节一行需要3KB1字节。我通常分配两行缓冲区一行用于当前行解压一行用于反向滤波时参考前一行。Huffman码表需要存储码长和码字DEFLATE最多有286个literal/length码和30个distance码。每个码表项用16位存储码字和码长总共需要(28630)*2632字节用分布式RAM或者小BRAM实现。滑动窗口是32KB这是DEFLATE规范规定的。用一块36Kb的BRAM刚好够用配置成32位位宽、8K深度。综合下来整个解码器大约需要2-3块36Kb BRAM用于数据缓冲1-2块用于码表和滑动窗口加上一些分布式RAM和寄存器。逻辑资源方面Huffman解码和LZ77匹配是主要消耗大约需要2000-3000个LUT和1000-2000个FF。对于中低端FPGA来说这个资源占用是可以接受的。3. DEFLATE解压的硬件实现细节3.1 Huffman解码的状态机设计DEFLATE的Huffman解码是整条链路里最复杂的部分。动态Huffman模式下码表是压缩数据流里定义的需要先解析码长序列再构建码表。码长序列本身也是用Huffman编码的用的是固定的码长码表这个码表在RFC 1951里有明确定义。我实现Huffman解码用的是逐位比较法。状态机从压缩数据流里逐位读取每读一位就和码表里的所有码字比较看是否匹配。这种方法逻辑简单但速度慢最坏情况下每个符号需要读15位比较286次。为了提高速度我做了两级优化。第一级是码长分组把码字按长度分成1到15组每组内的码字按数值排序。解码时先读一位确定码长范围再在对应组内做二分查找。第二级是并行比较用组合逻辑同时比较多个码字把比较结果编码成one-hot信号再用优先编码器输出匹配的符号。实测下来优化后的Huffman解码器每个符号平均需要3-5个时钟周期对于大多数应用来说已经够用了。如果追求极致速度可以用查表法把压缩数据流的前15位作为地址直接查表输出符号和码长。但查表法需要2^1532768个表项每个表项存储符号和码长需要一块不小的BRAM。我一般不建议用查表法除非你的FPGA BRAM资源非常充裕。这里有个坑要注意DEFLATE的Huffman码是位反转的。也就是说压缩数据流里先出现的是码字的最高位但Huffman树的构建是从最低位开始的。我在第一次实现时没注意这一点解码出来的符号全是乱的排查了两天才发现是位序问题。解决办法是在构建码表时把码字做位反转或者在读取压缩数据时按位反转的顺序读。3.2 LZ77滑动窗口的BRAM实现LZ77解压的核心是滑动窗口。当解码出一个长度-距离对时需要从滑动窗口中距离当前位置offset个字节的地方复制length个字节到输出。滑动窗口的大小是32KB用一块双口BRAM实现写端口用于写入新解压出的字节读端口用于复制历史数据。滑动窗口的地址管理是个关键点。我用一个12位的写指针和12位的读指针写指针始终指向最新写入的位置读指针指向要复制的位置。当写指针到达32KB边界时回绕到0。复制操作时读指针从(写指针 - offset) mod 32KB开始连续读length个字节同时把这些字节写入输出FIFO和滑动窗口。这里有个细节当length大于offset时复制操作会覆盖到刚刚写入的数据。比如offset1length10意味着把最后一个字节重复10次。这种重叠复制在CPU上很简单但在硬件里需要特殊处理。我的做法是在复制过程中每写入一个字节就更新写指针读指针也跟着更新这样就能正确处理重叠情况。BRAM的读写冲突是另一个需要注意的地方。当复制操作正在进行时新的解压数据可能也需要写入滑动窗口。我用了简单的仲裁策略复制操作优先新数据写入等待。因为复制操作通常很快几个周期就完成了不会造成明显的性能损失。滑动窗口的初始化也有讲究。DEFLATE规范规定滑动窗口初始时全部填充0。我在复位时用一个计数器遍历整个BRAM把每个地址都写0。这个过程需要32768个周期对于大多数应用来说可以接受。如果不想等可以在第一次读取时判断地址是否被写过没写过就返回0。但这样会增加读逻辑的复杂度我一般还是选择复位时初始化。3.3 动态Huffman码表的构建流程动态Huffman码表的构建是DEFLATE解压里最繁琐的部分。压缩数据流里首先出现的是HLIT、HDIST和HCLEN三个参数分别表示literal/length码的数量、distance码的数量和码长码的数量。然后是一个HCLEN个3位元素的序列表示码长码的码长。接着用码长码解码出HLITHDIST个码长最后根据这些码长构建literal/length码表和distance码表。在FPGA里实现这个过程我用了一个三级状态机。第一级读取HLIT、HDIST、HCLEN第二级读取码长码的码长并构建码长码表第三级用码长码表解码出所有码长并构建最终的码表。码长码表的构建相对简单因为码长码最多19个码长最多7位。我用一个19项的数组存储每个码长码的码长然后按照标准Huffman构建算法生成码字。标准Huffman构建算法是先统计每个码长的数量然后计算每个码长的起始码字最后按顺序分配码字。literal/length码表和distance码表的构建类似但数量更多。literal/length码最多286个distance码最多30个。码长最大15位。构建过程需要两个数组一个存储每个符号的码长一个存储每个符号的码字。构建完成后把码字和码长写入BRAM供解码状态机使用。这里有个优化技巧码表构建只需要做一次之后整个IDAT数据流的解码都复用这个码表。所以码表构建的延迟可以忽略不计不需要特别优化。我把码表构建状态机的时钟频率设得比较低用组合逻辑慢慢算节省资源。码表存储我用的是分布式RAM因为码表不大而且需要同时读取多个码字做并行比较。分布式RAM的读延迟是0组合逻辑直接输出非常适合这种场景。如果用BRAM读延迟至少1个周期会拖慢解码速度。4. 反向滤波与像素输出的实现4.1 五种滤波类型的硬件处理PNG的滤波是为了提高压缩率对每一行像素数据做预处理。滤波类型有五种None、Sub、Up、Average、Paeth。每个扫描行的第一个字节是滤波类型后面的字节是滤波后的像素数据。解码时需要根据滤波类型做反向运算还原原始像素。None滤波最简单滤波后的数据就是原始数据直接输出即可。Sub滤波是当前像素减去左边像素反向运算就是当前像素加上左边像素。Up滤波是当前像素减去上边像素反向运算就是当前像素加上上边像素。Average滤波是当前像素减去左边和上边像素的平均值反向运算就是当前像素加上这个平均值。Paeth滤波最复杂需要计算左边、上边、左上三个像素的预测值反向运算就是当前像素加上这个预测值。在FPGA里实现这五种滤波我用了一个统一的计算单元。每个像素周期计算单元根据滤波类型选择对应的运算输出还原后的像素值。Sub和Up滤波只需要一个加法和一个寄存器Average需要两个加法和一个移位Paeth需要三个加法和几个比较器。Paeth滤波的预测函数是这样的p a b - c其中a是左边像素b是上边像素c是左上像素。然后计算pa abs(p - a)pb abs(p - b)pc abs(p - c)选择最小的那个对应的像素作为预测值。如果pa最小选apb最小选bpc最小选c。这个逻辑用组合逻辑实现大约需要十几个LUT。滤波类型字节的处理需要注意每个扫描行只有一个滤波类型字节它出现在该行像素数据的最前面。我在解压输出阶段用一个行计数器来跟踪当前处理到第几行当行计数器变化时从数据流中提取滤波类型字节并更新滤波类型寄存器。4.2 行缓冲与跨行像素引用Up、Average和Paeth滤波都需要引用上一行的像素数据。所以需要维护一个行缓冲区存储上一行的原始像素值。行缓冲区的大小等于图像宽度乘以每像素字节数。对于1024像素宽的RGB888图像行缓冲区需要3KB。我用一块双口BRAM实现行缓冲区写端口用于写入当前行还原后的像素读端口用于读取上一行的像素。读地址和写地址同步递增读出的数据就是上一行对应位置的像素值。这里有个时序问题当处理当前行的第一个像素时需要读取上一行的第一个像素。但上一行的第一个像素是在上一个行周期写入的如果行缓冲区只有一块读写会冲突。我的解决办法是用两块行缓冲区做乒乓操作一块用于当前行写入一块用于上一行读取。处理完一行后交换两块缓冲区的角色。乒乓操作的控制逻辑用一个行结束信号触发。当一行处理完成时行结束信号翻转两块缓冲区的读写角色互换。这样当前行写入缓冲区A时从缓冲区B读取上一行数据下一行写入缓冲区B时从缓冲区A读取上一行数据。行缓冲区的初始化也需要考虑。第一行没有上一行Up、Average和Paeth滤波在引用上一行像素时应该返回0。我在复位时把两块行缓冲区都清零这样第一行处理时读出的上一行像素全是0符合PNG规范的要求。4.3 颜色类型转换与输出格式统一PNG支持多种颜色类型灰度、RGB、调色板、灰度Alpha、RGBA。每种颜色类型的像素数据组织方式不同解码后需要统一转换成RGB888或RGBA8888格式输出。灰度图像每个像素1字节表示亮度值。转换成RGB888就是RGB亮度值。灰度Alpha图像每个像素2字节第一个是亮度第二个是Alpha。转换成RGBA8888就是RGB亮度AAlpha。RGB图像每个像素3字节直接就是R、G、B三个分量。RGBA图像每个像素4字节直接就是R、G、B、A四个分量。调色板图像每个像素1字节是调色板的索引。需要先从PLTE块中读取调色板数据然后用索引查表得到RGB值。PLTE块最多256项每项3字节总共768字节。我用一块小BRAM存储调色板解码时用像素值作为地址读出RGB数据。位深也是需要考虑的因素。PNG支持1、2、4、8、16位位深。1、2、4位位深的图像每个字节包含多个像素需要先做位解包。16位位深的图像每个分量2字节需要截断成8位输出。我目前实现的版本支持8位位深这是最常见的场景。如果需要支持其他位深可以在像素输出阶段增加一个位解包模块。颜色类型转换我用一个组合逻辑实现根据颜色类型选择对应的转换路径。输出统一为RGB888或RGBA8888用一个valid信号指示输出有效。下游模块只需要关心像素数据不需要知道原始的颜色类型。5. 工程源码的组织与移植方法5.1 目录结构与文件说明这套工程源码我按照功能模块划分目录每个模块一个文件夹包含verilog源文件、testbench和仿真脚本。顶层目录下有一个rtl文件夹存放所有综合相关的verilog文件一个sim文件夹存放仿真相关的文件一个doc文件夹存放设计文档和接口说明一个prj文件夹存放不同FPGA平台的工程文件。rtl文件夹下的文件组织如下png_decoder_top.v顶层模块例化所有子模块并连接接口。png_header_parser.vIHDR块解析模块。idat_collector.vIDAT数据收集模块。inflate_decoder.vDEFLATE解压顶层模块。huffman_decoder.vHuffman解码模块。lz77_window.vLZ77滑动窗口模块。unfilter_engine.v反向滤波模块。pixel_formatter.v像素格式转换模块。bram_dp.v双口BRAM封装模块根据FPGA厂商不同可以替换。fifo_sync.v同步FIFO模块用于跨时钟域数据缓冲。sim文件夹下有针对每个模块的testbench以及一个顶层testbench用于整体仿真。testbench用verilog编写可以配合Icarus Verilog或商业仿真工具使用。我提供了几个测试用的PNG文件覆盖不同的颜色类型和滤波类型组合。prj文件夹下有针对Xilinx Vivado、Intel Quartus和国产FPGA开发工具的工程文件。每个工程文件里已经配置好了综合选项、约束文件和引脚分配可以直接打开编译。5.2 跨平台移植的注意事项虽然verilog是标准语言但不同FPGA厂商的综合工具对某些语法的支持程度不同。我在编写代码时尽量使用verilog-2001标准语法避免使用厂商特定的原语和属性。BRAM的例化是一个需要特别注意的地方。不同厂商的BRAM原语名称和端口定义不同。Xilinx叫RAMB36E1Intel叫M9K或M20K国产FPGA各有各的叫法。我的做法是写一个通用的BRAM封装模块bram_dp.v内部用条件编译或者参数化选择不同的原语。移植时只需要修改这个文件其他模块不需要动。时钟管理也是移植时容易出问题的地方。不同FPGA的PLL或MMCM原语不同锁定信号的名字也不同。我把时钟管理单独放在一个模块里移植时替换这个模块即可。约束文件的格式各厂商也不同。Xilinx用XDCIntel用SDC国产FPGA用各自的格式。我提供了每种平台的约束文件模板包括时钟周期约束、输入输出延迟约束和引脚分配。移植时根据实际板卡的引脚定义修改即可。5.3 仿真验证与上板调试仿真验证是保证代码正确性的关键步骤。我用Icarus Verilog做功能仿真因为它开源免费而且仿真速度比商业工具快。testbench读取PNG文件把字节流逐字节送入解码器然后收集输出的像素数据和用软件解码的结果做比对。仿真时需要注意几个点第一PNG文件要提前转换成verilog可以读取的格式我一般用Python脚本把PNG文件转成十六进制文本文件testbench用readmemh读取。第二仿真时间可能比较长一张1024x768的图片解码需要几百万个时钟周期仿真可能需要几分钟到几十分钟。第三要覆盖不同的滤波类型和颜色类型组合我准备了十几张测试图片每张覆盖一种组合。上板调试时我一般先用一个简单的测试图片比如64x64的纯色图片验证基本功能。然后用复杂的图片比如照片验证各种滤波类型和Huffman码表的正确性。调试手段包括用ILA或SignalTap抓取内部信号用LED指示解码状态用串口输出解码进度和错误信息。这里有个调试技巧在解码器的关键节点插入计数器统计每个阶段处理的符号数、像素数和周期数。如果某个阶段的计数和预期不符就能快速定位问题。比如Huffman解码器输出的符号数应该等于压缩数据流中的符号总数如果少了说明码表构建有问题如果多了说明解码状态机有误触发。6. 常见问题与排查技巧实录6.1 解码输出花屏的几种原因花屏是最常见的解码问题表现为输出的像素数据杂乱无章或者部分区域正确部分区域错误。根据我的经验花屏的原因主要有以下几种第一种是Huffman码表构建错误。如果码表构建时码长或码字算错了解码出的符号就会错位导致后续所有数据都乱掉。排查方法是把构建好的码表和软件解码的码表做比对看是否一致。我写了一个Python脚本用zlib库解码PNG文件输出每个符号的码字和码长和verilog仿真结果对比。第二种是滑动窗口地址计算错误。如果复制操作时读地址算错了复制出的数据就是错的。排查方法是抓取滑动窗口的读写指针和复制操作的offset、length和软件解码的对应操作比对。我一般在仿真时打印这些信息和Python脚本的输出做逐行比对。第三种是反向滤波的滤波类型判断错误。如果滤波类型字节提取错了整行的反向滤波都会用错误的算法导致该行像素全错。排查方法是抓取每行的滤波类型字节和软件解码的结果比对。PNG规范允许编码器为每行选择不同的滤波类型所以滤波类型字节是变化的不能假设所有行都用同一种滤波。第四种是行缓冲区的乒乓切换时机错误。如果乒乓切换早了一个周期或晚了一个周期会导致某一行引用了错误的上一行数据。排查方法是抓取行计数器和乒乓选择信号看切换时机是否和行结束信号对齐。6.2 时序不收敛的优化策略DEFLATE解压器的逻辑深度比较大特别是在Huffman解码和LZ77复制这两个环节组合逻辑路径可能很长导致时序不收敛。我遇到过几次时序问题总结了几种优化策略第一种是插入流水线寄存器。在Huffman解码器的比较逻辑后面插入一级寄存器把比较结果寄存一拍再输出。这样会增加一个周期的延迟但能显著改善时序。LZ77复制操作也可以插入流水线把地址计算和数据读取分成两个周期。第二种是降低时钟频率。如果时序实在收敛不了可以把解码器的时钟频率降低到50MHz或更低。PNG解码对吞吐率的要求通常不高50MHz足够处理大多数应用场景。降低频率后组合逻辑的延迟余量变大时序容易收敛。第三种是优化组合逻辑。比如Huffman解码的并行比较可以把286个码字分成几组每组用一个独立的比较器然后用树形结构汇总结果。这样虽然增加了面积但减少了单个比较器的输入数量降低了逻辑深度。第四种是使用BRAM的输出寄存器。BRAM原语通常有可选的输出寄存器打开后读延迟增加一个周期但时序会好很多。我在滑动窗口和码表存储上都打开了输出寄存器牺牲一个周期的延迟换取时序余量。6.3 资源占用过高的裁剪方法如果FPGA资源紧张需要对解码器做裁剪。我总结了几种裁剪方法按影响程度从低到高排列第一种是减少压缩数据缓冲区的大小。如果PNG文件不会超过256KB可以把缓冲区从1MB减到256KB节省3/4的BRAM。代价是只能解码小于256KB的PNG文件。第二种是降低并行度。Huffman解码器可以只用一个比较器逐位比较而不是并行比较。这样LUT消耗减少一半以上但解码速度降低到原来的1/3左右。第三种是复用行缓冲区。如果图像宽度不大可以用一块行缓冲区通过时分复用来实现乒乓操作。代价是控制逻辑变复杂而且可能引入气泡。第四种是去掉不常用的功能。比如如果不支持调色板图像可以去掉PLTE解析和查表逻辑。如果不支持16位位深可以去掉位解包逻辑。这些功能去掉后能节省不少资源。第五种是降低输出位宽。如果下游模块只需要RGB565可以在像素输出阶段直接截断成16位节省输出FIFO和后续处理的资源。裁剪时要权衡资源和功能根据实际项目需求选择。我一般建议保留完整的解码功能只在资源实在不够时才做裁剪。因为PNG格式的兼容性很重要裁剪太多可能导致某些图片无法解码。6.4 常见问题速查表问题现象可能原因排查方法解决方法输出全黑文件头解析错误宽高为0抓取IHDR解析结果检查文件头解析状态机输出全白像素输出使能一直有效抓取输出valid信号检查输出控制逻辑花屏Huffman码表错误比对码表和软件解码检查码表构建状态机部分行错位行缓冲区乒乓切换错误抓取行计数器和切换信号调整切换时机颜色偏差颜色类型判断错误抓取颜色类型寄存器检查颜色类型解析时序不收敛组合逻辑路径过长查看时序报告插入流水线或降低频率资源占用高并行度过高查看资源报告降低并行度或裁剪功能仿真不通过testbench数据格式错误检查readmemh文件重新生成测试数据上板无输出时钟或复位问题用示波器测时钟检查时钟约束和复位逻辑解码速度慢时钟频率低或并行度低统计解码周期数提高频率或增加并行度7. 工程源码的使用与二次开发建议7.1 快速上手步骤拿到这套源码后最快的上手方式是先用仿真跑通。打开sim文件夹下的顶层testbench里面已经配置好了一个测试PNG文件的路径。用Icarus Verilog编译运行观察波形和输出日志。如果仿真通过说明代码功能正确可以进入下一步。仿真通过后打开prj文件夹下对应你FPGA平台的工程文件。工程里已经添加了所有rtl文件配置好了综合选项和约束。直接点击综合和实现看时序报告和资源报告。如果时序不收敛参考第6.2节的优化策略调整。如果资源占用过高参考第6.3节的裁剪方法调整。综合实现通过后生成比特流并下载到FPGA。用测试图片验证上板功能。我建议先用小尺寸的简单图片比如64x64的纯色图片确认基本功能正常。然后用大尺寸的复杂图片验证各种边界情况。7.2 接口定制与功能扩展这套解码器的输入接口是8位字节流输出接口是24位或32位像素流。如果你的系统接口不同可以修改顶层模块的接口定义。比如输入是32位AXI-Stream可以加一个位宽转换模块把32位拆成4个8位字节。输出是16位RGB565可以加一个截断模块把24位RGB888截成16位。功能扩展方面可以增加对隔行扫描PNG的支持。隔行扫描PNG的像素数据按Adam7算法交织存储解码后需要做去交织。去交织需要额外的行缓冲区和地址计算逻辑复杂度不低。如果项目不需要隔行扫描可以跳过这个功能。还可以增加对16位位深的支持。16位位深的PNG每个分量2字节解码后需要截断成8位。截断方式可以是取高8位也可以是做伽马校正后取高8位。伽马校正需要查表会增加一些资源消耗。如果需要在解码过程中做图像处理比如缩放、旋转、滤波可以在像素输出阶段插入处理模块。因为像素输出是标准的RGB888或RGBA8888流任何图像处理模块都可以直接对接。7.3 性能优化与资源权衡性能优化的核心是提高时钟频率和增加并行度。时钟频率受限于时序收敛可以通过插入流水线、优化组合逻辑、使用BRAM输出寄存器等方法来提高。并行度可以通过增加Huffman解码器的比较器数量、增加LZ77复制操作的位宽、增加像素输出的位宽来提高。资源权衡的核心是找到性能和面积的平衡点。如果项目对解码速度要求不高可以降低并行度节省资源。如果项目对资源要求不严格可以提高并行度加快解码速度。我一般建议先用中等配置综合后看时序和资源报告再根据实际情况调整。这里有个经验值可以参考对于1024x768的RGB888 PNG图片中等配置的解码器大约需要3000个LUT、2000个FF和4块36Kb BRAM时钟频率可以跑到100MHz解码一帧大约需要10毫秒。这个性能对于大多数工业检测和视频处理应用来说已经足够了。8. 实际项目中的经验体会我在多个项目中使用了这套PNG解码器踩过不少坑也积累了一些经验。最大的体会是仿真验证一定要充分不要等到上板才发现问题。PNG格式的边界情况很多比如空IDAT块、多个IDAT块、滤波类型混合、调色板索引越界等。这些情况在仿真时很容易构造上板后却很难复现。我一般会准备一套覆盖各种边界情况的测试图片每次修改代码后都跑一遍仿真。另一个体会是资源估算要留余量。综合工具的资源报告是理论值实际布局布线后资源占用可能会增加10%到20%。如果综合后资源占用超过80%布局布线可能会失败。我一般会把资源占用控制在70%以内留出足够的余量。还有一点时钟约束要准确。PNG解码器的时钟频率直接影响解码速度但时钟频率越高时序收敛越难。我一般先设一个保守的频率比如50MHz确保时序收敛。然后逐步提高频率直到时序刚好收敛。这样可以在保证功能的前提下获得最高的性能。最后分享一个小技巧在解码器的关键节点插入计数器统计每个阶段处理的符号数、像素数和周期数。这些计数器不需要综合到硬件里只在仿真时有效。通过分析这些计数可以快速定位性能瓶颈和逻辑错误。我在调试Huffman解码器时就是靠符号计数器发现码表构建错误的。
返回列表