尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

纯Verilog实现FPGA硬解PNG:DEFLATE与Huffman解码实战 1. 项目缘起与整体设计思路1.1 为什么要在FPGA里硬解PNG做图像处理的朋友大概率都遇到过这个场景上位机或者摄像头给过来一张PNG图片想在FPGA内部直接参与后续的缩放、叠加、滤波等流水线处理结果发现PNG是压缩格式没法像BMP那样直接按像素读。常规做法是先在PC端把PNG转成RAW或者BMP再喂给FPGA但这样一来整个系统就离不开上位机产品化的时候很别扭。PNG解码本身不是什么新课题zlib、libpng这些库在软件端已经非常成熟。但把PNG解码搬到FPGA上用纯Verilog从零实现难点集中在三块第一是DEFLATE解压涉及Huffman变长码和LZ77滑动窗口回溯天然带反馈依赖跟FPGA擅长的流水线思路相冲第二是滤波反演PNG有五种滤波类型逐像素依赖前一个像素必须串行处理第三是内存带宽解码中间数据量比原始压缩数据大几十倍怎么用有限的片上RAM配合外部DDR把数据流转起来是决定方案能不能落地的关键。这个项目的定位很明确给需要把PNG解码集成进FPGA图像处理链路的工程师一套可直接复用的纯Verilog方案不依赖任何软核、不调用厂商IP从比特流解析到最终像素输出全部自己实现。适合有一定Verilog基础、做过图像采集或显示项目、想深入理解压缩格式硬件解码的开发者。哪怕你只是想搞明白Huffman解码在硬件里到底怎么落地这套代码也值得啃一遍。1.2 整体架构怎么切分整个解码链路我按数据流方向切成四级流水每一级职责单一接口用简单的valid/ready握手方便单独仿真和替换。第一级是PNG文件解析与块提取。PNG文件由8字节固定头加若干数据块chunk组成关键块有IHDR图像头、PLTE调色板索引色才有、IDAT压缩数据可能多个、IEND结束。这一级负责扫描块结构把IHDR里的宽高、位深、颜色类型、隔行方式提取出来把多个IDAT的数据拼成一条连续的压缩码流写进外部DDR的输入缓冲区。第二级是DEFLATE解压。这是最重的一块内部再分Huffman解码、LZ77回溯、输出组装三个子模块。输入是压缩码流输出是滤波后的原始扫描行数据。这里我用的是双Huffman表literal/length表和distance表配合一个深度足够的滑动窗口缓存。第三级是滤波反演。PNG每行数据在压缩前会做滤波类型有None、Sub、Up、Average、Paeth五种。反演时必须按行顺序、按像素顺序串行做因为Sub依赖左边像素、Up依赖上一行同位置像素、Paeth还依赖左上角像素。这一级把滤波后的数据还原成真实像素值。第四级是像素格式转换与输出。根据颜色类型灰度、真彩、索引、带Alpha等和位深1/2/4/8/16把像素组装成统一的RGB888或RGBA8888格式写入输出帧缓存供后续模块读取。四级之间用DDR做数据缓冲片上只保留当前行和上一行的必要数据。这样做的原因是PNG解码的中间数据量远大于片上BRAM容量比如一张1024x768的真彩图一行原始数据就是3KB加上滤波需要的上一行再算上滑动窗口片上根本放不下必须借外部存储。1.3 方案选型背后的取舍有人会问为什么不直接用Xilinx或Intel的Huffman IP原因有两个一是这类IP通常绑定特定器件系列换平台就得重来二是PNG的DEFLATE有它自己的细节比如动态Huffman表的码长限制、距离码的额外位通用IP不一定完全匹配。纯Verilog实现虽然工作量大但可移植性拉满从低端Artix到高端Kintex甚至国产FPGA都能跑。另一个取舍是滑动窗口放片上还是放DDR。放片上用BRAM实现访问延迟低但窗口大小受BRAM容量限制PNG的LZ77窗口最大32KB用BRAM实现32KB窗口在中等规模FPGA上可行但会占用大量BRAM资源。放DDR则容量无忧但每次回溯都要读DDR延迟高、带宽压力大。我的做法是折中窗口用片上BRAM实现但只缓存最近若干KB超出部分回退到DDR通过地址映射统一管理。这样既保证了常见情况下的低延迟又不会因为窗口太大把BRAM吃光。2. 核心细节解析与实操要点2.1 PNG块结构解析的硬件实现PNG文件头是固定的8字节89 50 4E 47 0D 0A 1A 0A。硬件里用一个8字节的移位寄存器逐字节比对全部匹配才进入块解析状态。块的结构是4字节长度大端 4字节类型 数据 4字节CRC。长度和类型都要做字节序转换因为PNG是大端而FPGA内部通常按小端处理。解析状态机我设计成五个状态IDLE、READ_LEN、READ_TYPE、READ_DATA、READ_CRC。每个块解析完根据类型分派IHDR把宽高、位深、颜色类型、压缩方法、滤波方法、隔行方式锁存到配置寄存器PLTE把调色板数据写入片上RAMIDAT把数据流写入DDR输入缓冲同时维护一个写指针IEND触发解码完成标志。这里有个容易踩的坑IDAT可能有多个它们的数据在逻辑上是连续的必须按顺序拼接。我在实现时用一个连续的DDR地址空间每收到一个IDAT就接着上次的写指针继续写不重新对齐。另外CRC校验在硬件里做完整计算比较费资源如果对数据完整性要求不是极致可以只做长度和类型的合法性检查CRC跳过或者只做抽样校验。我在工程里提供了带CRC和不带CRC两个版本按需选用。注意IHDR必须是第一个块且只能出现一次。解析时如果第一个块不是IHDR直接报错退出不要继续往下跑否则后面全是垃圾数据。2.2 Huffman解码的硬件落地DEFLATE的Huffman解码是变长码每个符号的码长不固定硬件里没法像软件那样用查表循环。我的做法是构建一棵规范Huffman树然后用逐位比较的方式解码。具体来说先把码表按码长排序生成每个码长对应的起始码值和符号索引解码时从码流里逐位读入每读一位就检查当前累积的码值是否落在某个码长的有效范围内。为了提速我用了一个两级查表结构第一级用固定位数比如9位做快速查表如果码长不超过9位一次就能解出符号如果超过9位再走第二级慢速路径逐位比较。实测下来大部分符号的码长都在9位以内所以平均每个符号1到2个时钟周期就能解出吞吐率足够支撑1080p级别的图像解码。Huffman表本身在动态模式下是从码流里读出来的需要先解析码长数组再重建码表。码长数组本身也是Huffman编码的用的是固定的码长码表。这一层嵌套容易绕晕我的建议是先把固定Huffman表的解码跑通再上动态表。固定表只有两张码长固定调试起来直观得多。LZ77回溯部分长度和距离都有额外位要读。长度码29到31对应的是带额外位的长匹配距离码30到31也是。额外位的读取要在Huffman解码之后紧接着做不能漏。我见过有人在这里漏读额外位导致后续码流全部错位解出来的图像花屏排查了半天才发现是少读了几位。2.3 滤波反演的串行处理技巧滤波反演必须严格按行、按像素顺序做因为每种滤波都依赖前一个像素或上一行像素。硬件里用一个行缓冲存上一行反演后的像素当前行反演时同时读上一行对应位置和左边已反演的像素。五种滤波的计算方式不同None直接输出Sub加左边像素Up加上边像素Average加左边和上边的平均Paeth用左边、上边、左上三个像素做预测。Paeth的预测函数稍微复杂但硬件里就是几个加法和比较组合逻辑能搞定。位深小于8的时候一个字节里打包了多个像素反演前要先拆包。比如4位灰度一个字节两个像素反演要按像素粒度做做完再打包回去或者直接输出。这里容易出错的是行尾对齐PNG每行数据按字节对齐如果一行像素数乘以位深不是8的倍数末尾会补位反演时要跳过这些补位不能当成有效像素。实操心得滤波反演的行缓冲建议用双口BRAM实现一个口读上一行一个口写当前行读写可以同时进行。如果只用单口RAM读写要分时吞吐率会掉一半。3. 实操过程与核心环节实现3.1 工程目录结构与模块划分十套工程源码我按平台和功能做了分类目录结构统一如下png_decoder/ ├── rtl/ │ ├── png_parser.v # 块解析 │ ├── inflate_top.v # DEFLATE解压顶层 │ ├── huffman_dec.v # Huffman解码 │ ├── lz77_window.v # 滑动窗口 │ ├── filter_reverse.v # 滤波反演 │ ├── pixel_convert.v # 像素格式转换 │ └── ddr_ctrl.v # DDR读写控制 ├── sim/ │ ├── tb_png_parser.v │ ├── tb_inflate.v │ └── tb_top.v ├── con/ │ └── top.xdc # 引脚约束 └── doc/ └── register_map.md # 寄存器映射说明十套工程的区别主要在顶层封装和DDR控制器有的用Xilinx MIG有的用Intel UniPHY有的用国产器件的DDR控制器还有两套是纯片上BRAM版本适合小图或者低端器件。每套工程的RTL核心是一样的换平台只需要替换DDR控制器和约束文件。3.2 关键参数计算与配置DDR输入缓冲的大小要按最大压缩比估算。PNG的压缩比通常在2:1到10:1之间极端情况可能更高。假设最大支持1920x1080真彩图原始数据约6MB按10:1压缩比算压缩数据约600KB。输入缓冲分配1MB足够。输出帧缓存按原始数据算1920x1080x4字节RGBA约8MB分配16MB留余量。滑动窗口大小我配置成32KB这是DEFLATE规范允许的最大值。片上BRAM实现32KB窗口在Xilinx 7系列上大约占16个36Kb BRAM对中等规模器件可以接受。如果BRAM紧张可以降到8KB但压缩率会受影响因为距离超过8KB的匹配就找不到了。Huffman解码的查表深度我设成9位对应512个表项。两级查表的第二级用逐位比较最多支持15位码长DEFLATE规范上限。这个配置在Artix-7上综合后大约占2000个LUT频率能跑到150MHz以上。3.3 仿真验证流程仿真我分三步走先用小图比如16x16验证功能正确性再用中等图256x256验证吞吐率最后用大图1920x1080验证DDR带宽和整体时序。测试激励的生成方法用Python的PIL库把BMP转成PNG同时导出原始像素数据作为参考。仿真时把PNG文件读进testbench解码输出跟参考数据逐像素比对。Icarus Verilog和Vivado自带的仿真器我都试过Icarus启动快、适合小规模调试Vivado仿真器对时序和DDR模型支持更好。# Icarus Verilog 编译仿真示例 iverilog -o tb_top tb_top.v ../rtl/*.v vvp tb_top比对脚本我用Python写读仿真输出的像素文件和参考文件逐字节比较输出差异位置和数量。差异为0才算通过。注意仿真时DDR模型要加延迟不能设成0延迟否则综合后的时序可能对不上。我一般设读延迟20个周期、写延迟10个周期接近实际DDR控制器的表现。4. 常见问题与排查技巧实录4.1 解码花屏的几种典型原因花屏是最常见的现象原因五花八门。我整理了一个排查表按出现概率从高到低排列现象可能原因排查方法整幅图花屏Huffman表解析错误检查码长数组读取顺序图像上半部分正常下半部分花滑动窗口溢出检查窗口地址回绕逻辑图像有规律条纹滤波反演行对齐错误检查行尾补位处理颜色错乱像素格式转换错误核对颜色类型和位深配置图像偏移隔行扫描未处理检查隔行方式配置Huffman表解析错误是最隐蔽的因为码流本身没报错只是解出来的符号不对。我的排查方法是把解出的符号序列打印出来跟软件端zlib的解码结果比对看从第几个符号开始分叉分叉点往前就是问题所在。4.2 DDR带宽不够导致的丢帧大图解码时DDR带宽是瓶颈。输入压缩数据读、滑动窗口回溯读写、输出像素写三股流量叠加。我实测1920x1080真彩图解码DDR有效带宽需要约400MB/s才能实时。如果DDR控制器配置的位宽或频率不够就会出现丢帧或者解码卡顿。优化手段有几个一是滑动窗口尽量放片上减少DDR回溯二是输入压缩数据用突发读一次读一大块缓存到片上FIFO三是输出像素用突发写攒够一行再写DDR。这三招下来DDR带宽需求能降一半左右。4.3 时序不收敛的解决思路Huffman解码的组合逻辑比较深容易成为时序瓶颈。我的做法是把逐位比较拆成流水线每级只处理一位虽然延迟增加但频率能上去。另外查表RAM用分布式RAM而不是BRAM读延迟更小。滤波反演的Paeth预测函数组合逻辑也不浅我在中间插了一级寄存器把三个像素的读取和预测计算分开时序明显改善。综合时如果还差一点可以给关键路径加max_delay约束让工具重点优化。实操心得先保证功能正确再优化时序。我见过有人一上来就追求高频结果功能都没跑通调时序纯属浪费时间。功能对了时序慢慢磨总能收敛。5. 十套工程的差异化说明与选用建议5.1 按平台分类的工程清单十套工程我按平台和存储方案做了区分方便不同背景的开发者直接选用工程编号平台DDR方案适用场景01Xilinx Artix-7MIG通用入门02Xilinx Kintex-7MIG高性能03Xilinx Zynq-7000PS DDR软硬协同04Intel Cyclone IVUniPHY低成本05Intel Cyclone VUniPHY中端06国产安路自研控制器国产化07纯片上BRAM无DDR小图低端08纯片上BRAM无DDR小图低端09Xilinx Artix-7MIG带CRC校验10Xilinx Kintex-7MIG带CRC校验01到06是带DDR的完整版支持大图。07和08是纯片上版本只支持小图比如256x256以内适合BRAM资源少或者不想折腾DDR的场景。09和10在01和02基础上加了CRC校验对数据完整性要求高的选这两套。5.2 移植到新平台需要改什么移植的核心工作是替换DDR控制器和约束文件。RTL部分跟平台无关只要DDR控制器提供标准的读写接口地址、数据、使能、应答直接对接就行。我定义的DDR接口是简单的类SRAM接口读写各一套移植时写个适配层即可。约束文件要改时钟频率、引脚分配、时序例外。如果新平台的BRAM结构不同滑动窗口的RAM推断可能要调整比如改用厂商提供的RAM IP或者调整读写模式。这些在doc目录的移植指南里有详细说明。5.3 技术支持与代码获取十套工程的源码我打包在一起附带仿真脚本、测试图片和比对工具。代码注释我写得比较密关键状态机和计算逻辑都有说明。遇到问题可以先看doc目录的FAQ大部分常见问题都有记录。如果FAQ解决不了可以把仿真波形和日志发过来我帮忙定位。技术支持的范围包括编译报错、仿真不通过、时序不收敛、移植适配。不包含帮你改需求、帮你写论文、帮你做非PNG格式的解码。我在实际调试这套代码的过程中最大的体会是PNG解码的难点不在单个模块而在模块之间的数据流配合。Huffman解出的符号怎么喂给LZ77LZ77的输出怎么对齐到滤波反演的行边界滤波后的数据怎么按像素格式打包这些接口如果没定义清楚单个模块仿真都过连起来就花屏。我的建议是先把接口时序图画出清楚再动手写代码能省很多返工时间。另外小图调通再上大图功能调通再优化时序这个顺序别乱乱了就是给自己找麻烦。
返回列表