ARTICLE DETAIL

资讯详情

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

FPGA纯Verilog硬解PNG:DEFLATE流水线设计与10套工程源码实战

FPGA纯Verilog硬解PNG:DEFLATE流水线设计与10套工程源码实战 1. 为什么要在FPGA里硬解PNG做图像处理的朋友大概率都遇到过这个场景上位机或者摄像头给过来一帧数据格式是PNG压缩过的想在FPGA里直接做后续的缩放、滤波、叠加结果发现FPGA根本不认识这玩意儿。常规做法是先在PC端或者ARM端把PNG解成RGB裸数据再喂给FPGA但这样做的代价是延迟高、链路长而且一旦系统要求独立运行、没有操作系统支撑这条路就直接堵死了。PNG解码放到FPGA里做核心价值就在于把解压这一步从软件搬到硬件流水线上。PNG用的是DEFLATE无损压缩内部包含LZ77滑动窗口匹配和Huffman熵编码两层纯逻辑实现起来不算轻松但一旦跑通整条图像链路就能做到全硬件、低延迟、可流水。这套工程用纯Verilog实现不依赖任何软核或者第三方IP从文件解析到像素输出全部自己写配套10套工程源码覆盖了从仿真验证到上板调试的完整流程。这篇文章适合两类人看一类是正在做FPGA图像处理项目、需要把压缩图像接入硬件流水线的工程师另一类是想找一个完整、有难度的Verilog实战项目来练手的学习者。PNG解码涉及状态机设计、跨时钟域处理、存储器管理、位流解析等多个硬核知识点啃下来对Verilog工程能力的提升非常明显。2. PNG解码的整体架构与方案选型2.1 PNG文件格式的层次拆解PNG文件不是一整块压缩数据它是按块Chunk组织的。每个块有固定的结构4字节长度、4字节类型标识、数据区、4字节CRC校验。解码器首先要做的就是按块遍历把关键块挑出来。关键块有这么几个IHDR存放图像宽高、位深、颜色类型等元信息PLTE是调色板只有索引色图像才有IDAT是真正存放压缩像素数据的块可能有好几个需要拼接IEND是结束标志。除此之外还有tRNS、gAMA、pHYs等辅助块解码时可以跳过但遍历时必须正确识别长度否则会读偏。这里有个容易踩的坑IDAT块可能被拆成多个而且不保证按顺序连续存放。我见过有人只读第一个IDAT就开始解压结果图像只出来一条。正确做法是把所有IDAT的数据区按顺序拼成一个完整的压缩流再送进解压模块。2.2 为什么选纯Verilog而不是HLS市面上做PNG解码的方案大致三种软核跑zlib库、HLS综合C代码、纯Verilog手写。软核方案最省事但需要处理器和内存延迟不可控HLS方案开发快但生成的电路资源利用率往往不理想而且调试时信号可观测性差出了问题很难定位到具体哪一级流水。纯Verilog的优势在于每一拍信号都在你掌控之中。DEFLATE解码里的Huffman树遍历、位流读取、滑动窗口回填这些操作的时序关系非常紧密用状态机加流水线的方式写出来资源可以压得很低时序也容易收敛。代价是开发周期长需要把算法拆到寄存器传输级。这套工程选择纯Verilog本质上是为了追求确定性的时序和最小的资源占用适合对实时性有要求的嵌入式图像场景。2.3 模块划分与数据流设计整个解码器我把它拆成四个大模块数据像流水一样从前往后走文件解析模块负责按块遍历PNG提取IHDR参数拼接IDAT数据流输出连续的压缩比特流。Huffman解码模块从比特流中逐符号解出LZ77的literal/length和distance码这是整个解码里最考验时序的部分。LZ77解压模块维护一个32KB的滑动窗口根据length和distance从窗口里回填数据同时把新数据写入窗口。像素重组模块把解压出来的原始字节按位深和颜色类型还原成RGB像素处理滤波反变换输出标准像素流。模块之间用FIFO衔接这样各级可以独立跑自己的节奏不会被上下游卡死。FIFO深度需要根据最坏情况下的吞吐差来定后面会细说。3. 核心模块的Verilog实现细节3.1 文件解析与IDAT拼接的状态机设计文件解析模块的核心是一个三段式状态机。第一段负责读块头解析出长度和类型第二段根据类型决定是解析数据还是跳过第三段处理CRC并跳到下一个块。跳过非关键块时不能简单地把数据丢掉因为长度可能不是4的倍数需要按字节精确跳过。我的做法是用一个计数器记录已跳过的字节数和块长度比较相等才进入下一块。这里要注意长度字段是大端序Verilog里读进来需要做字节序转换。IDAT拼接用一个双口RAM做缓冲。每读到一个IDAT块就把数据写进RAM的连续地址同时更新写指针。所有IDAT读完后读指针从0开始把整段压缩流送给Huffman解码模块。RAM深度按最大图像尺寸估算一般1080P的PNG压缩后也就几百KB到1MB出头用片外DDR或者大容量Block RAM都行。注意IDAT块之间可能夹杂其他辅助块拼接时不能假设它们连续。必须严格按块遍历遇到IDAT才写RAM。3.2 Huffman解码的位流读取与码表构建DEFLATE的Huffman码是变长的最短7位最长可能到15位。解码时不能一次读固定位数必须逐位比较。常规做法是维护一个位缓冲寄存器每次从比特流里补充数据然后从根节点开始遍历Huffman树读到0走左子树读到1走右子树直到叶子节点。PNG里的Huffman码表不是直接存在文件里的而是用码长序列Code Length Sequence间接编码的。需要先解出每个符号的码长再用规范Huffman算法重建码表。这一步容易出错的地方是码长序列本身也是Huffman编码的存在嵌套。我的处理方式是分两遍第一遍解出码长数组第二遍根据码长生成实际的码字查找表。查找表用组合逻辑实现把15位位缓冲的高位作为索引直接查出符号和实际码长。这样一拍就能出一个符号吞吐率比逐位遍历高得多。代价是查找表占用一些LUT但现代FPGA的LUT资源足够换来的是时序上的从容。3.3 LZ77滑动窗口的存储器管理LZ77解压的核心是一个32KB的滑动窗口。每解出一个literal就把它写入窗口每解出一对length/distance就从窗口的当前位置往前推distance个字节连续拷贝length个字节到输出同时这些拷贝出来的数据也要写回窗口。窗口用Block RAM实现读写地址由一个环形指针管理。写指针每写一个字节加一超过32KB就回绕。读指针等于写指针减去distance。拷贝操作需要length个周期如果length很大最大258会阻塞流水线。优化方法是把拷贝做成突发模式一次读多个字节减少地址计算开销。这里有个隐蔽的坑当distance小于length时拷贝会出现重叠也就是刚写进去的数据马上要被读出来。这在Verilog里必须用组合逻辑或者提前一拍预取来处理否则会读到旧数据。我的做法是在拷贝状态机里判断如果读地址追上了写地址就用刚写入的数据直接旁路到输出不走RAM。3.4 像素重组与滤波反变换解压出来的数据是滤波后的字节流每个像素的每个通道都经过五种滤波之一处理过。滤波类型有None、Sub、Up、Average、Paeth五种每行的第一个字节标识该行用的滤波类型。反变换需要保存上一行的像素数据因为Up和Average、Paeth都要用到上一行对应位置的像素。我用一个行缓冲RAM存上一行当前行计算时同时读上一行和当前行已还原的左侧像素。Paeth滤波涉及三个预测值的计算和选择组合逻辑路径较长需要插入流水寄存器来保证时序。像素输出格式根据IHDR里的颜色类型决定。灰度图每像素1字节RGB每像素3字节带Alpha的RGBA每像素4字节。位深可能是1、2、4、8、16位小于8位的需要做位扩展。这部分用参数化的移位和掩码逻辑实现保证不同配置下都能正确输出。4. 10套工程源码的组织与差异化设计4.1 工程分类与适用场景10套工程不是简单复制而是按验证层次和应用场景做了区分。大致可以分成三类工程编号类型主要用途特点01-03仿真验证功能验证与调试带完整Testbench波形可查04-07上板验证实际硬件跑通含约束文件适配常见开发板08-10应用集成系统级联调含DDR读写、视频输出接口仿真工程适合刚开始啃代码的阶段可以逐模块跑波形看每个状态机的跳转是否符合预期。上板工程加了时序约束和引脚分配直接综合就能下载。应用集成工程把PNG解码和DDR控制器、视频时序生成器连起来形成一个完整的图像显示链路。4.2 仿真工程的Testbench搭建要点Testbench的写法直接决定调试效率。我的习惯是给每个模块单独写一个Testbench用Icarus Verilog跑因为IVerilog轻量、启动快适合频繁迭代。顶层再写一个集成Testbench把PNG文件读进来跑完整解码最后把输出像素写成BMP文件用图片查看器直接看结果。读PNG文件用$readmemh或者$fread都行但要注意文件是二进制$readmemh按十六进制读会出问题。我一般用$fopen打开文件$fread按字节读进一个数组再逐字节送给解码器。输出BMP时手动写文件头把像素数据按BGR顺序写进去这样Windows图片查看器能直接打开。提示仿真时把关键状态机的状态、FIFO的读写指针、Huffman解码的符号都加到波形里出问题时一眼就能看出卡在哪一级。4.3 上板工程的时序约束与资源评估上板工程最关键的是时序约束。PNG解码器通常跑在100MHz到150MHz具体取决于器件速度等级。约束文件里要对时钟做周期约束对输入输出做虚假路径或者多周期路径设置。资源占用方面以Xilinx 7系列为例一个完整的PNG解码器大概占LUT3000到5000FF2000到4000Block RAM10到20块主要用在滑动窗口和行缓冲DSP基本不用这个规模在中低端FPGA上完全放得下。如果资源紧张可以把滑动窗口放到片外SRAM或者DDR里但会引入额外的读写延迟需要重新平衡流水线。4.4 应用集成工程的DDR读写配合应用集成工程里PNG解码后的像素数据要写进DDR再由视频输出模块读出来显示。这里涉及跨时钟域和带宽分配。解码器输出速率和DDR读写速率不匹配时需要加FIFO做缓冲。DDR控制器的读写仲裁要小心。如果视频输出是实时刷新的读优先级要高于写否则会出现画面撕裂。我的做法是给读通道更高的仲裁权重写通道用突发模式攒够一批再写减少对读的干扰。5. 实操流程与关键参数计算5.1 从零跑通第一套仿真工程的步骤拿到源码后建议按这个顺序走先跑Huffman解码模块的独立Testbench喂一段已知的压缩数据看解出的符号序列对不对。再跑LZ77解压模块用固定的length/distance序列检查窗口回填是否正确。然后跑文件解析模块用一个简单的PNG文件看IHDR参数和IDAT拼接结果。最后跑顶层集成仿真输入完整PNG输出BMP用图片查看器验证。每一步都要看波形不要只看最终结果。中间某一级出错最终结果可能只是轻微偏差但波形能直接定位到问题。5.2 FIFO深度的计算与选择模块间FIFO深度不是随便定的。以Huffman解码到LZ77这一级为例Huffman解码的吞吐率是每周期一个符号LZ77解压遇到length时可能需要多个周期。最坏情况下LZ77处理一个length258的匹配需要258个周期这期间Huffman会产出258个符号。如果FIFO深度小于258Huffman就会阻塞。实际设计中我会把FIFO深度设为512留一倍余量。深度太大浪费Block RAM太小会频繁反压降低整体吞吐。计算方法是找出下游最坏情况的处理周期数乘以上下游时钟频率比再乘一个1.5到2的安全系数。5.3 位流读取的跨字节处理PNG的压缩流是比特级的但存储器是按字节读的。位流读取模块需要维护一个位缓冲每次从存储器读一个字节补充到缓冲低位然后从高位取需要的位数。这里的关键是位缓冲的移位方向要统一。我习惯用左移新字节放到低位取数据时从高位取。这样码字在缓冲里的顺序和比特流顺序一致不容易搞反。每次取完n位缓冲左移n位同时更新有效位数。有效位数不足时触发读存储器补充。注意比特流的位序是MSB先出和常规的字节序不同。读进来的字节要先做位反转或者直接在移位逻辑里按MSB优先处理。6. 常见问题与排查技巧实录6.1 解码结果花屏或颜色错乱的排查花屏最常见的原因是滤波反变换出错。先检查滤波类型字节有没有正确读取再检查上一行缓冲的读写地址有没有错位。如果颜色整体偏了多半是颜色类型解析错误比如把RGB当成RGBA处理导致通道错位。还有一种情况是IDAT拼接时漏了某个块或者块长度解析错误导致数据错位。用波形看IDAT写RAM的地址是否连续中间有没有跳变能快速定位。6.2 时序不收敛的常见原因与解决时序违例通常出在Huffman查找表的组合逻辑上。15位索引的查找表如果直接综合成LUT路径可能太长。解决办法是插入流水寄存器把查找分成两级第一级查高8位第二级查低7位两级之间打一拍。代价是增加一个周期的延迟但时序能轻松收敛。另一个常见违例点是Paeth滤波的预测值计算三个候选值的比较和选择逻辑较深。同样用流水线切开或者用查找表预计算部分结果。6.3 仿真通过但上板失败的差异分析仿真通过上板失败九成是时序或者复位问题。先检查约束文件有没有漏掉时钟或者输入输出延迟约束。再检查复位信号是否同步释放异步复位同步释放是基本要求否则状态机可能在上电时进入非法状态。还有一种情况是仿真时用的PNG文件太小没有触发某些边界条件。上板时换一张大图IDAT块多、length值大就可能暴露出FIFO深度不够或者窗口回绕处理错误的问题。建议仿真时就用大图跑覆盖各种边界。6.4 常见问题速查表现象可能原因排查方向图像全黑像素输出使能未拉高检查输出状态机图像上半部分正常下半部分花行缓冲地址回绕错误检查行计数器颜色偏红或偏蓝通道顺序错误检查RGB/BGR映射解码中途卡死FIFO满且下游不读检查反压逻辑时序违例组合逻辑过深插入流水寄存器上板无输出复位未释放检查复位同步电路7. 工程扩展与性能优化方向7.1 支持隔行PNG与Adam7交错标准PNG支持Adam7交错模式图像被分成7个pass每个pass包含不同密度的像素。解码交错PNG需要在像素重组阶段增加pass解析逻辑按pass顺序把像素填到正确位置。这部分我目前工程里没做因为实际项目中交错PNG很少见但如果要完整支持PNG规范这是必须补的一块。实现思路是在IHDR里读交错标志如果开启像素重组模块要维护7个pass的坐标计数器每个pass结束后跳到下一个。输出像素时按最终坐标写RAM全部pass跑完再统一读出。7.2 多像素并行解码的可行性分析当前设计是单像素流水每周期出一个像素。如果要提高吞吐可以考虑多符号并行Huffman解码。DEFLATE允许一次解多个符号只要位流足够。实现上是把查找表复制多份同时查多个位置然后做冲突检测和合并。这个优化复杂度较高收益在像素率要求极高的场景才明显。一般1080P60的像素率是148.5MHz单像素流水跑150MHz刚好够。4K场景才需要考虑并行。7.3 与DDR控制器的带宽匹配优化应用集成工程里PNG解码写DDR的带宽要和DDR实际带宽匹配。如果DDR是16位400MHz理论带宽800MB/s实际有效带宽可能只有60%到70%。解码器输出如果是1080P60 RGB888带宽需求是148.5M×3≈445MB/s加上视频读取的带宽总需求接近DDR上限。优化方法是写DDR时用突发长度8或16减少命令开销。读视频时用双缓冲一块读的时候另一块写避免读写冲突。仲裁器要给视频读最高优先级保证不丢帧。这套工程我断断续续调了挺久最深的体会是PNG解码的难点不在单个模块而在模块之间的握手和反压。每一级都要能独立暂停和恢复否则一处卡住整条流水线就死了。另外仿真一定要用真实的大图跑小图跑通不代表没问题边界条件往往在大图里才暴露。源码里我把每个模块的接口都做了参数化换图像尺寸或者颜色格式时只需要改顶层参数不用动内部逻辑这一点在实际项目里省了很多事。
返回列表