1. 项目缘起与整体设计思路
1.1 为什么要在FPGA上做PNG解码
做图像处理的朋友都知道,PNG格式在嵌入式视觉项目里出现的频率非常高。它无损压缩、支持透明通道、文件体积比BMP小一大截,特别适合做UI素材、图标、叠加层这类场景。但问题也来了——PNG用的是DEFLATE压缩算法,解码过程涉及Huffman编码、LZ77滑动窗口、滤波反变换、Adam7隔行扫描等一堆步骤,纯软件跑在MCU上动辄几百毫秒,帧率根本扛不住。
我最早接触这个需求是在一个工业相机项目里,需要在1080p视频流上叠加PNG格式的LOGO和状态图标。当时用STM32跑软件解码,单帧叠加耗时超过200ms,完全没法做实时。后来换成Zynq的ARM核跑,勉强能到30fps,但CPU占用率直接飙到70%以上,其他任务全被拖垮。这才下定决心用FPGA纯逻辑来实现PNG解码。
用FPGA做PNG解码的核心优势在于并行流水线。Huffman解码、LZ77匹配、滤波反变换这几个环节天然适合流水线设计,数据进来之后每个时钟周期都在处理,不像CPU那样一条指令一条指令地串行执行。实测下来,在100MHz时钟下,一张1024x1024的RGBA PNG图片,从输入压缩数据到输出像素流,延迟可以控制在2ms以内,吞吐量完全满足实时叠加需求。
这个项目适合谁呢?如果你正在做FPGA图像处理相关的开发,需要把PNG解码集成到自己的视频流水线里,或者想学习如何用Verilog实现一个完整的压缩格式解码器,那这套东西应该能帮到你。我提供了10套不同配置的工程源码,覆盖从入门验证到实际部署的各种场景,后面会详细说。
1.2 整体架构怎么搭
整个PNG解码器的架构我拆成了五个主要模块,每个模块对应解码流程中的一个阶段:
第一级是输入缓冲与解析模块。PNG文件进来之后,先要解析文件头,提取IHDR块里的宽高、位深、颜色类型、压缩方法、滤波方法、隔行方式这些关键参数。同时把IDAT块里的压缩数据流提取出来,存到内部的FIFO或者BRAM里。这一步看起来简单,但实际写的时候要注意PNG的块结构是变长的,每个块都有长度字段和CRC校验,解析逻辑要能正确处理各种边界情况。
第二级是Huffman解码模块。DEFLATE用的Huffman编码分两种:固定Huffman和动态Huffman。固定Huffman的码表是预定义的,实现简单;动态Huffman需要先解析码表定义,再构建解码树。我采用的是查表法,把Huffman树展开成一张查找表,每个时钟周期可以解出一个符号。这里的关键是表的大小和深度要平衡,表太大浪费BRAM,表太小解码效率低。
第三级是LZ77解压模块。Huffman解出来的符号分两类:字面量(literal)和长度-距离对(length-distance pair)。字面量直接输出,长度-距离对需要从滑动窗口里回拷数据。滑动窗口大小是32KB,我用双端口BRAM实现,一个端口写新数据,一个端口读历史数据。这里有个坑:长度和距离的编码是变长的,需要额外的查找表来映射。
第四级是滤波反变换模块。PNG的每一行像素在压缩前都做了滤波处理,有None、Sub、Up、Average、Paeth五种滤波类型。解码时需要根据滤波类型做反向运算。这个模块的难点在于Paeth滤波,它需要计算三个相邻像素的预测值,逻辑比较复杂。我用了流水线设计,每个时钟周期处理一个像素,保证吞吐量。
第五级是输出格式化模块。解码出来的像素数据可能是灰度、RGB、RGBA、索引色等多种格式,需要统一转换成标准的像素流输出。同时要处理Adam7隔行扫描的情况,把隔行数据重组成完整的图像。
这五级流水线串起来,数据从输入到输出全程不停顿。每个模块之间用FIFO做缓冲,防止反压导致流水线停顿。整体资源占用方面,在Xilinx Artix-7上大概用了3000个LUT、2000个FF、8个BRAM,对于大多数FPGA来说都很轻松。
1.3 为什么选择纯Verilog实现
市面上有一些现成的PNG解码IP核,但要么收费昂贵,要么绑定特定厂商的平台。我选择纯Verilog从头写,主要考虑几个因素:
可移植性。纯Verilog不依赖任何厂商特定的IP核或原语,只要综合工具支持标准Verilog-2001,就能在Xilinx、Altera、Lattice、安路等各家FPGA上跑。我实测过Xilinx Artix-7、Altera Cyclone IV、安路EG4这几款,综合结果都正常。
可调试性。自己写的代码,每一行逻辑都清楚,出了问题可以用ILA或者逻辑分析仪直接抓信号。用第三方IP核的话,出问题只能看文档或者找FAE,效率低很多。
可定制性。不同项目对PNG解码的需求不一样,有的只需要解码RGB,有的需要支持透明通道,有的对吞吐量要求高,有的对资源占用敏感。纯Verilog实现可以灵活裁剪,按需配置。
学习价值。对于想深入理解PNG格式和DEFLATE算法的朋友来说,自己写一遍解码器是最快的学习路径。这套代码里每个模块都有详细注释,关键算法都有说明,适合拿来研究。
当然,纯Verilog实现也有代价——开发周期长,调试难度大。我前后花了大概三个月时间才把整个流程跑通,中间踩了不少坑。这些经验后面会详细分享。
2. 核心模块细节与实操要点
2.1 Huffman解码模块的实现细节
Huffman解码是整个PNG解码器里最核心也最复杂的部分。DEFLATE的Huffman编码有两个特点:一是码长不固定,从1bit到15bit都有可能;二是动态Huffman的码表需要从压缩数据流里解析出来。
我采用的方案是两级查找表。第一级表用9bit索引,覆盖所有码长小于等于9的符号;第二级表用剩余的bit索引,处理码长大于9的情况。这样设计的好处是,大部分符号(约90%)可以在第一级表里一次查出,只有少数长码需要查第二级表。
具体实现上,我用BRAM存储查找表。第一级表深度512,每个条目存储符号值、码长、类型(字面量还是长度-距离对)。第二级表深度根据实际码表动态分配,最大支持288个条目。查表逻辑用组合逻辑实现,一个时钟周期完成。
这里有个关键细节:bit流的读取顺序。DEFLATE的Huffman码是低位先出(LSB first),也就是说,先读到的bit是码字的低位。这和很多人的直觉相反,我第一次写的时候就在这里栽了跟头,解出来的符号全是乱的。后来用逻辑分析仪抓了bit流,对照PNG规范一点点核对,才发现这个问题。
另一个坑是码表构建。动态Huffman的码表定义本身也是Huffman编码的,需要先解码码长序列,再根据码长构建完整的Huffman树。这个过程需要迭代处理,我用状态机实现,分了几个状态:读取码长码表、解码码长序列、构建查找表、切换到数据解码。状态机跑一遍大概需要几百个时钟周期,对于一张图片来说可以忽略不计。
注意:Huffman解码模块的复位信号要特别处理。如果解码过程中出现错误(比如遇到无效码字),需要能够快速复位并重新开始,否则整个流水线会卡死。我在状态机里加了超时计数器,超过一定周期没有进展就自动复位。
2.2 LZ77滑动窗口的管理策略
LZ77解压的核心是滑动窗口。PNG规范规定窗口大小是32KB,这个大小是固定的,不能改。我用一个双端口BRAM来实现,深度32768,位宽8bit。
写端口在解出字面量或者完成一次回拷后写入新数据,读端口在遇到长度-距离对时读取历史数据。这里的关键是地址管理。窗口是循环使用的,写指针和读指针都在0到32767之间循环。写指针始终指向下一个要写入的位置,读指针根据距离值计算:读地址 = (写地址 - 距离) mod 32768。
听起来简单,但实际写的时候有几个坑:
第一个坑是距离值的边界情况。距离值最小是1,最大是32768。当距离等于32768时,读地址和写地址相同,这时候读出来的是最老的数据。我一开始没处理好这个边界,导致窗口快满的时候解压出来的数据错乱。
第二个坑是回拷过程中的重叠。LZ77允许长度大于距离的情况,比如距离是1,长度是10,这意味着要把同一个字节重复10次。这种情况下不能简单地先读后写,需要边读边写,而且读地址要跟着写地址走。我用了一个小状态机来处理这种情况,每次读一个字节,写一个字节,读地址递增,直到长度减到0。
第三个坑是BRAM的读写冲突。双端口BRAM虽然可以同时读写,但如果读写地址相同,读出来的数据可能是旧的也可能是新的,取决于具体的BRAM实现。为了避免这个问题,我在回拷逻辑里加了旁路:如果读地址等于写地址,直接使用当前要写入的数据,不经过BRAM。
滑动窗口的写使能信号要仔细控制。只有在Huffman解码输出字面量或者完成一次回拷后才写,其他时候保持写使能无效,防止误写。
2.3 滤波反变换的流水线设计
PNG的滤波反变换是逐行进行的,每一行的每个像素都要根据滤波类型做反向运算。五种滤波类型的运算逻辑如下:
| 滤波类型 | 名称 | 反向运算公式 |
|---|---|---|
| 0 | None | 无运算,直接输出 |
| 1 | Sub | 当前像素 + 左侧像素 |
| 2 | Up | 当前像素 + 上方像素 |
| 3 | Average | 当前像素 + floor((左侧+上方)/2) |
| 4 | Paeth | 当前像素 + PaethPredictor(左侧,上方,左上) |
PaethPredictor的计算逻辑是:先计算三个候选值p = 左侧 + 上方 - 左上,然后取p与左侧、上方、左上三个值中距离最近的那个。这个逻辑用Verilog实现需要几个加法和比较器,组合逻辑路径比较长。
我的做法是把滤波反变换做成三级流水线。第一级计算左侧、上方、左上三个像素的地址并读取;第二级计算预测值;第三级做加法并输出。这样每个时钟周期可以处理一个像素,100MHz下吞吐量达到100M像素/秒,对于4K分辨率也够用。
这里要注意的是行缓冲的管理。Up和Average滤波需要用到上一行的像素数据,所以需要维护一个行缓冲。我用BRAM实现,深度等于图像宽度,位宽等于像素位宽。每处理完一行,行缓冲更新一次。对于RGBA格式,位宽是32bit,1024宽度的图像需要4KB的BRAM,资源占用可以接受。
还有一个细节是滤波类型的解析。PNG的每一行开头有一个字节表示滤波类型,这个字节不参与滤波运算,需要先提取出来。我在状态机里加了一个状态专门处理这个字节,提取完之后再进入像素处理状态。
实操心得:滤波反变换模块的测试建议单独做。我一开始把整个解码链路串起来测,出了问题很难定位是哪个模块的错。后来把每个模块单独做testbench,用已知的输入输出对做验证,效率高很多。特别是Paeth滤波,我写了十几组测试用例才覆盖所有边界情况。
2.4 多格式像素输出的适配方案
PNG支持多种颜色类型:灰度(0)、RGB(2)、索引色(3)、灰度+Alpha(4)、RGBA(6)。每种颜色类型的像素位深也不一样,灰度可以是1/2/4/8/16bit,RGB可以是8/16bit,索引色可以是1/2/4/8bit。
输出格式化模块需要把这些格式统一转换成标准的像素流。我定义了一个内部标准格式:32bit RGBA,每个通道8bit。所有输入格式都往这个标准上靠。
灰度格式的转换最简单,把灰度值复制到R、G、B三个通道,Alpha设为255。索引色格式需要查调色板,调色板数据存在PLTE块里,我在解析阶段就提取出来存到BRAM里。RGB格式直接补Alpha通道。RGBA格式直接透传。
位深转换是另一个要点。对于1/2/4bit的灰度或索引色,需要把多个像素打包在一个字节里解出来。比如1bit灰度,一个字节包含8个像素,需要逐bit提取。我用移位寄存器实现,每个时钟周期移出一位,累积到8bit后输出。
16bit位深的处理稍微麻烦一些。PNG的16bit数据是大端格式,高字节在前。我需要先做字节序转换,然后取高8bit作为输出(或者做缩放)。对于大多数显示应用来说,8bit精度足够了,所以我的默认配置是取高8bit,低8bit丢弃。如果需要更高精度,可以配置成输出16bit。
Adam7隔行扫描的处理也在这一级。隔行PNG把图像分成7个pass,每个pass包含不同的行和列。解码完所有pass后,需要把数据重组成完整的图像。我用一个帧缓冲来暂存数据,每个pass解码完后按正确的地址写入帧缓冲,最后统一读出。帧缓冲用外部DDR或者大容量BRAM实现,取决于图像分辨率。
3. 完整实操流程与工程配置
3.1 工程目录结构与文件说明
我提供的10套工程源码按照功能复杂度分为三个等级:
基础验证级(3套):包含Huffman解码、LZ77解压、滤波反变换的单独测试工程,每个工程配一个testbench,用仿真验证功能正确性。适合刚开始接触PNG解码的朋友,先理解每个模块的原理。
集成测试级(4套):包含完整的解码链路,支持RGB和RGBA格式,支持固定Huffman和动态Huffman。每套工程配一个图像生成脚本,可以把任意PNG图片转换成Verilog testbench需要的格式。适合需要验证完整解码流程的朋友。
实战部署级(3套):包含完整的解码链路加上视频时序生成、DDR帧缓冲、HDMI输出等模块,可以直接在开发板上跑。支持Xilinx Artix-7、Altera Cyclone IV、安路EG4三个平台。适合需要把PNG解码集成到实际项目中的朋友。
每个工程的目录结构统一如下:
project_name/ ├── rtl/ # Verilog源码 │ ├── png_decoder_top.v # 顶层模块 │ ├── huffman_decoder.v # Huffman解码 │ ├── lz77_decompressor.v # LZ77解压 │ ├── filter_reverse.v # 滤波反变换 │ ├── pixel_formatter.v # 像素格式化 │ └── ... ├── sim/ # 仿真文件 │ ├── tb_png_decoder.v # testbench │ └── test_data/ # 测试数据 ├── constraints/ # 约束文件 │ └── png_decoder.xdc # 引脚和时序约束 ├── scripts/ # 辅助脚本 │ └── png2hex.py # PNG转hex脚本 └── README.md # 工程说明3.2 从PNG文件到Verilog仿真的完整流程
拿到一张PNG图片,怎么把它变成Verilog testbench能用的数据?我写了一个Python脚本png2hex.py,流程如下:
第一步,用Python的PIL库打开PNG文件,读取像素数据。这里要注意,PIL读出来的数据是解码后的原始像素,不是压缩数据。我们需要的是压缩后的IDAT数据,所以要用更底层的方式读取。
第二步,解析PNG文件结构,提取IHDR和IDAT块。IHDR包含宽高、位深、颜色类型等信息,IDAT包含压缩数据。我把这些信息按照固定的格式写入一个hex文件,每行一个字节。
第三步,生成testbench需要的激励文件。testbench读取hex文件,把数据逐字节送入解码器,然后收集输出像素,写入另一个hex文件。
第四步,用Python脚本对比输出像素和原始像素,验证解码正确性。如果一致,说明解码器工作正常;如果不一致,定位到具体是哪个像素出错,反推是哪个模块的问题。
这个流程我跑了上百张不同格式的PNG图片,包括灰度、RGB、RGBA、索引色、不同位深、隔行和非隔行,覆盖了绝大多数实际场景。测试过程中发现的问题和解决方法后面会详细说。
提示:
png2hex.py脚本依赖PIL库,安装命令是pip install Pillow。脚本支持批量处理,可以把一个目录下所有PNG文件都转换成hex格式,方便做回归测试。
3.3 关键参数的计算与配置
PNG解码器有几个关键参数需要根据实际需求配置:
Huffman查找表深度。第一级表固定512深度,第二级表深度根据实际码表动态分配。最大支持288个符号,对应深度256(2的8次方)。如果资源紧张,可以把第二级表深度减小到128,但会牺牲一些解码效率。
LZ77窗口大小。PNG规范固定32KB,不能改。BRAM配置为32768x8bit,占用8个BRAM块(Xilinx 7系列)。如果FPGA BRAM资源紧张,可以考虑用外部SRAM或者DDR实现,但会引入额外的延迟。
行缓冲深度。等于图像宽度,最大支持4096。对于4K图像,需要4096x32bit的BRAM,占用4个BRAM块。如果图像宽度超过4096,需要外挂帧缓冲。
流水线级数。滤波反变换模块默认三级流水线,Huffman解码模块两级流水线,LZ77解压模块一级流水线。总延迟大约10个时钟周期。如果时序紧张,可以增加流水线级数,但会增加延迟和资源占用。
时钟频率。默认配置100MHz,在Artix-7上时序余量大约20%。如果跑150MHz,需要优化关键路径,主要是Huffman查找表的组合逻辑和Paeth滤波的加法器链。
资源占用方面,以Xilinx Artix-7 XC7A35T为例,完整配置下大约占用:
| 资源类型 | 占用量 | 可用量 | 利用率 |
|---|---|---|---|
| LUT | 3200 | 20800 | 15% |
| FF | 2100 | 41600 | 5% |
| BRAM | 12 | 50 | 24% |
| DSP | 0 | 90 | 0% |
这个资源占用对于大多数FPGA项目来说都很轻松,留足了空间给其他模块。
3.4 上板实测与性能数据
我在三块开发板上做了实测:
Xilinx Artix-7 35T:时钟100MHz,解码一张1024x1024 RGBA PNG图片耗时1.8ms,吞吐量约580M像素/秒。功耗增加约0.3W。
Altera Cyclone IV EP4CE15:时钟80MHz,解码同样图片耗时2.4ms,吞吐量约430M像素/秒。资源占用比Artix-7略高,LUT用了约3800个。
安路EG4:时钟100MHz,解码耗时2.1ms,吞吐量约500M像素/秒。安路的综合工具对Verilog-2001支持良好,代码不需要修改直接能用。
实测中发现一个有趣的现象:解码速度与图像内容相关。压缩率高的图片(比如大面积纯色)解码速度快,因为LZ77的回拷操作多,Huffman解码的符号少;压缩率低的图片(比如噪声多的照片)解码速度慢,因为字面量多,Huffman解码压力大。这个差异大约在10%到15%之间。
实操心得:上板调试时建议先用小图片(比如64x64)验证功能,再用大图片测性能。小图片的压缩数据少,逻辑分析仪容易抓全;大图片数据量大,抓波形很困难。我一般先用64x64的测试图确认解码正确,再换1024x1024的图测吞吐量。
4. 常见问题与排查技巧实录
4.1 解码结果错乱的排查思路
解码结果错乱是最常见的问题,表现是输出的像素值和原始图片对不上。排查思路按照数据流方向逐级检查:
第一步,检查输入数据是否正确。用逻辑分析仪抓取输入FIFO的数据,和hex文件对比。如果输入就不对,说明testbench或者文件读取有问题。
第二步,检查Huffman解码输出。在Huffman解码模块的输出端加ILA,抓取解出的符号。和Python脚本模拟的Huffman解码结果对比。如果不一致,检查查找表配置和bit流读取顺序。
第三步,检查LZ77解压输出。在LZ77模块输出端加ILA,抓取解压后的数据。和Python的zlib解压结果对比。如果不一致,检查滑动窗口的地址管理和回拷逻辑。
第四步,检查滤波反变换输出。在滤波模块输出端加ILA,抓取反变换后的像素。和原始像素对比。如果不一致,检查滤波类型解析和预测值计算。
第五步,检查输出格式化。如果前面都正确但最终输出不对,问题就在格式化模块。检查颜色类型转换、位深转换、隔行重组这些逻辑。
我遇到最多的问题出在第二步和第三步。Huffman解码的bit流读取顺序错了三次,LZ77的窗口地址计算错了两次。每次都是通过逐级对比定位到的。
4.2 时序不收敛的优化方法
时序不收敛是FPGA开发的老大难问题。PNG解码器里几个关键路径容易出问题:
Huffman查找表的组合逻辑。第一级表512深度,用LUT实现的话组合逻辑路径比较长。我的优化方法是把查找表拆成两级:第一级用4bit索引查16个条目,第二级用剩余的5bit查32个条目。这样每级的组合逻辑路径缩短了一半,时序余量从-0.5ns改善到+1.2ns。
Paeth滤波的加法器链。PaethPredictor需要计算p = 左侧 + 上方 - 左上,然后做三次比较。加法器和比较器串起来路径很长。我的优化方法是把加法和比较拆到两个时钟周期,中间加一级寄存器。这样每个时钟周期的逻辑深度减半,时序轻松收敛。
LZ77的地址计算。读地址 = (写地址 - 距离) mod 32768,这个减法加取模的组合逻辑也不短。我的优化方法是把取模操作改成位与操作:因为32768是2的15次方,所以mod 32768等价于取低15位。这样减法之后直接截断,省掉了一个比较器和减法器。
如果时序还是紧张,可以考虑降低时钟频率。100MHz跑不动就跑80MHz,对于大多数应用来说80MHz的吞吐量也够用了。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 输出全零 | 复位信号未释放 | 检查复位逻辑 | 确保复位信号在解码开始前释放 |
| 输出像素错位 | 位深转换错误 | 对比原始像素和输出像素 | 检查移位寄存器的位宽和移位方向 |
| 颜色偏差 | 颜色类型解析错误 | 检查IHDR解析逻辑 | 确认颜色类型字段的位定义 |
| 图像下半部分错乱 | 行缓冲管理错误 | 检查行缓冲的读写地址 | 确保每行处理后行缓冲正确更新 |
| 隔行图像显示不全 | Adam7重组错误 | 检查pass的地址映射 | 对照PNG规范确认每个pass的行列范围 |
| 解码速度慢 | Huffman表深度不足 | 统计第二级表的命中率 | 增加第二级表深度或优化码表构建 |
| 资源占用过高 | BRAM配置过大 | 检查BRAM使用报告 | 减小行缓冲深度或复用BRAM |
| 时序不收敛 | 组合逻辑路径过长 | 查看时序报告 | 增加流水线级数或拆分查找表 |
4.4 独家避坑技巧
技巧一:用Python做黄金参考。在写Verilog之前,先用Python实现一遍完整的PNG解码流程。Python有zlib库,可以直接调用做DEFLATE解压,省去自己实现Huffman和LZ77的麻烦。把Python解码的中间结果保存下来,作为Verilog仿真的对比基准。这样调试的时候有明确的参考,效率高很多。
技巧二:分模块验证,不要一开始就串起来。我见过很多新手一上来就把所有模块串起来跑,出了问题完全不知道从哪里查。正确的做法是每个模块单独写testbench,用已知的输入输出对验证。Huffman解码器用固定的码表和bit流验证,LZ77解压器用已知的压缩数据验证,滤波反变换用已知的像素行验证。每个模块都验证通过后再串起来,问题就少很多。
技巧三:ILA的采样深度要够。调试PNG解码器时,ILA的采样深度至少要到4096,否则抓不到完整的解码过程。我一般设置采样深度8192,触发条件设在解码开始信号上,这样可以抓到从开始到结束的完整波形。
技巧四:注意PNG的字节序。PNG文件里的多字节整数都是大端格式,高字节在前。Verilog里读取的时候要注意字节序转换。我一开始没注意这个问题,IHDR里的宽高读出来全是错的,图像尺寸完全不对。后来加了一个字节序转换模块才解决。
技巧五:CRC校验可以跳过。PNG的每个块都有CRC校验,解码时可以选择跳过不校验。跳过CRC可以节省一些逻辑资源,对于大多数应用来说,数据在传输过程中出错的概率很低,不校验也没问题。但如果应用场景对数据完整性要求高,建议加上CRC校验。
技巧六:测试图片要覆盖各种格式。我准备了20张测试图片,覆盖灰度1/2/4/8/16bit、RGB 8/16bit、索引色1/2/4/8bit、RGBA 8/16bit、隔行和非隔行、不同压缩级别。每次修改代码后跑一遍全部测试图片,确保没有引入回归问题。这个习惯帮我避免了很多次“改了一个bug引入两个新bug”的情况。
技巧七:注意BRAM的初始化。Huffman查找表和调色板数据需要在解码开始前初始化。我用的是BRAM的初始化文件(.coe或.mif),在综合时加载。如果初始化文件格式不对,BRAM里的数据就是随机的,解码结果肯定错。建议先用小规模的初始化数据验证,确认加载正确后再用完整数据。
技巧八:时钟域要统一。PNG解码器内部所有模块用同一个时钟,不要引入跨时钟域。如果输入数据来自不同的时钟域,先用异步FIFO做时钟域转换,再送入解码器。跨时钟域处理不当会导致亚稳态,解码结果随机出错,而且很难排查。
技巧九:仿真时间要设够。一张1024x1024的PNG图片,压缩数据可能有几百KB,仿真时需要逐个字节送入。如果仿真时间设得太短,解码还没完成就结束了,看不到最终结果。我一般设置仿真时间为10ms,足够解码完大多数图片。
技巧十:保留调试信号。在综合时不要把调试信号优化掉。我一般在顶层模块留几个debug端口,输出关键状态机的状态、FIFO的读写指针、错误计数器等。上板调试时把这些信号接到LED或者逻辑分析仪上,可以快速判断解码器的工作状态。
这套PNG解码器从最初的想法到最终稳定运行,前后迭代了十几个版本。中间踩过的坑、熬过的夜、抓过的波形,现在回想起来都是宝贵的经验。纯Verilog实现PNG解码这件事,说难也难,说简单也简单——难在细节多,容易出错;简单在原理清晰,只要按部就班就能做出来。希望这些经验能帮到正在做类似项目的朋友,少走一些弯路。