2026年校招面试,被要求手撕Verilog实现AXI4-Stream实时JPEG-LS无损压缩,预测和残差编码流水线怎么设计才能不丢帧且满足1080P60?面试官追问了行缓冲深度和边界处理,求具体推导和代码思路。
FPGA校招面试,手撕Verilog实现AXI4-Stream实时JPEG-LS无损压缩,预测和残差编码流水线怎么设计才能不丢帧且满足1080P60?
11 个回答

行缓冲深度直接等于图像宽度,边界用镜像填充,这你肯定已经知道了。核心流水线分三级:预测、残差编码、熵编码。预测器用MED算法,注意它需要当前像素的左、上、左上三个参考像素,所以行缓冲至少要存一行,并且要等上一行第三个像素算完才能开始当前行第一个像素。时钟频率做到150MHz以上,1080P60的像素时钟约148.5MHz,刚好够。面试官追问边界处理时,你就说第一行和第一列用固定值预测,其余用镜像填充,这样BRAM只占4块左右。追问:你用的开发板具体型号是什么?不同系列的BRAM容量差别挺大。

其实面试官问流水线不丢帧,核心在两点:一是预测器的数据依赖怎么解,二是残差编码的吞吐能不能跟上像素时钟。先说预测,MED算法需要当前像素左边、上边、左上角三个值,典型做法是用行缓冲存上一行全部像素,当前行用一个寄存器堆缓存左边两个像素值。这样每个时钟周期都能算出当前像素的预测值,不需要等。但要注意第一行和第一列的边界处理——面试官追问的镜像填充,其实就是把边界像素的值对称复制过来,比如当前像素在行首,左邻像素就用当前像素自身,上邻像素用上一行同列值。残差编码用Golomb-Rice,它的编码长度可变,直接做会引入背压。常见做法是把残差先写进一个深度不大的FIFO,熵编码查表从里面取,这样即使某次编码需要多个周期,也不会阻塞前面的流水级。时钟频率要超一点,比如155MHz,留余量给时序收敛。整体BRAM占用大约4块,因为行缓冲用一块BRAM就够了,FIFO用一块,查表用两块。面试官大概率会追问:如果图像宽度不是BRAM深度对齐怎么办?你就说用双口RAM加地址偏移,或者按最大宽度分配,浪费几个地址没关系。最后提醒一句:面试时别背代码,画个流水线时序图,讲清楚每个周期数据怎么流动,比写满屏Verilog更分高。

面试官问这个,其实是想看你有没有真的调过硬件。1080P60的像素时钟148.5MHz,如果三级流水线每级都一个周期搞定,那直接流就行。但Golomb-Rice编码查表时,如果残差很大,编码长度可能超过32位,查表逻辑的路径延迟会变大。我见过有人在这里没注意,结果时序违例。补救办法是把查表拆成两级流水,或者用BRAM做双端口查表,一个周期出结果。另一个坑是行缓冲的读端口冲突:预测器同时要读当前行和上一行,如果行缓冲只用单端口BRAM,就得隔周期读,导致吞吐减半。正确做法是用真双口BRAM,一个端口读上一行,一个端口写当前行。或者用两块单口BRAM交替读写,像乒乓操作。你准备面试时,最好能把MED算法的Verilog代码自己写一遍,跑个仿真,确认边界像素的输出值对不对。追问:你目前的代码仿真过1080P的随机图像吗?还是只测了固定图案?这会影响你对流水线时序的理解深度。

面试官问流水线不丢帧,其实就是看你有没有算过像素时钟和各级延迟的账。MED预测器用三级流水,行缓冲用真双口BRAM,每周期都能拿到三个参考像素,残差编码用FIFO解耦,时钟跑到155MHz留余量,基本就稳了。边界用镜像填充,第一行第一列直接复制自身,代码量不大但面试官很爱细问。

说个实际调板子时容易忽略的点:Golomb-Rice编码的查表路径延迟。残差范围从0到4095,对应编码长度从几比特到三十多比特不等,查表逻辑的扇出和组合逻辑级数很容易超标。我见过有人仿真通过,上板发现时序违例,最后在查表前加了一级寄存器才解决。正确做法是把查表拆成两级:第一级根据残差高位选table index,第二级取实际码字,这样单级延迟减半。另外,行缓冲的深度要精确等于图像宽度,但镜像填充时边界像素的地址生成逻辑要注意——比如当前像素在第二列,左邻像素的地址是当前列减一,但上邻和左上邻的地址要跨行,得用行缓冲地址加宽度偏移。如果地址计算放在同个周期,BRAM读端口冲突就来了。建议把地址计算提前一拍,或者用双口BRAM分别读上下行。追问一句:你用的BRAM是单口还是真双口?这个选型直接影响流水线结构。

我当年准备这个题时,先画了个三级流水线的时序图,把每个周期各个模块在干什么标清楚。预测器用MED算法,行缓冲存上一行全部像素,当前行左边两个像素用寄存器缓存,这样每周期都能算出预测值。残差编码用Golomb-Rice查表,因为编码长度不定,我加了个深度为16的FIFO缓冲,确保预测级不被阻塞。时钟频率设到160MHz,留了约10MHz余量给布线延迟。边界处理用镜像填充,第一行第一列固定预测为0,代码量不大但面试官追问过为什么不用对称填充——其实镜像填充更好实现,因为地址计算只需要加减一列或一行,不用判断边界方向。BRAM用了4块,每块18Kb,1080P的行宽1920够存。整体思路就这样,但面试官可能会深挖FIFO深度怎么算,建议你提前推一下最坏情况下的编码长度分布。

关于MED预测器的三级流水线,很多人一上来就画三个级:预测、残差编码、熵编码,觉得时钟够就行。但实际面试时面试官更想听的是你如何拆解每一级的组合逻辑深度。拿预测级来说,MED算法需要同时获取左、上、左上三个像素,但行缓冲的读端口只有一个周期延迟,所以这一级里真正耗时的不是算术运算,而是地址生成和BRAM读数据的同步。我自己的做法是把地址计算提前到上一级,也就是在熵编码完成的同时,把下一像素的参考地址算好并寄存,这样预测级进来时直接读BRAM,省掉一个周期的地址组合逻辑。另一个容易被忽略的点是Golomb-Rice编码的查表路径:残差范围0-4095,对应编码长度从几比特到三十多比特不等,如果用一个组合逻辑查表,扇出和级数很容易超标。我面试时被追问过这个问题,后来把查表拆成两级:先用高位选table index,再用低位取码字,中间加一级流水,这样时序就收敛了。至于边界处理,镜像填充比对称填充好在地址计算简单——当前像素在左边界时,左邻像素的地址就是当前列加一,而不是判断边界方向再决定复制哪一侧。你准备面试时,建议自己写个简化版的仿真,只测一行像素的流水线输出,看看第一列和最后一列的值对不对。追问一句:你目前用的BRAM是单口还是真双口?这会影响你读上一行和写当前行能不能在一个周期内完成。

说个实际面试时容易翻车的地方:1080P60的像素时钟是148.5MHz,你设计时钟跑到155MHz,留了余量,这没问题。但面试官可能会追问FIFO深度怎么算——因为Golomb-Rice编码长度可变,最坏情况下残差编码需要多个周期,而预测器每周期产生一个残差,所以FIFO必须能缓存最坏情况下的累积残差。常见做法是假设编码最大延迟为4个周期,那么FIFO深度至少为4,再加两级余量,深度取8就够。但如果你用查表法直接把残差映射到码字,查表本身只要一个周期,那FIFO深度可以更小。建议你提前推一下最坏情况下的编码长度分布,或者直接跟面试官说查表延迟固定为一个周期,这样FIFO深度只需要2-3级。追问:你仿真时有没有测过连续输入一行像素时FIFO的空满状态变化?

我自己的经验是,面试官追问行缓冲深度和边界处理,本质上是在考察你对数据流时序的掌控力,而不是背公式。1080P60的像素时钟148.5MHz,三级流水线要做到不丢帧,核心矛盾在于MED预测器的数据依赖——当前像素需要左边、上边、左上三个参考值,而左边那个值是上一周期算出来的,上边和左上则要从行缓冲读出来。行缓冲深度必须等于图像宽度1920,这是硬性要求,少一个像素最后一列就会读错。但面试官真正想听的是你如何处理第一行和第一列:第一行没有上一行数据,第一列没有左边数据,镜像填充的做法是把边界像素的值对称复制,比如第一行第一列直接用自身复制三份,这样不需要额外判断分支,组合逻辑干净。另一个容易踩的坑是残差编码的吞吐匹配,Golomb-Rice编码长度可变,如果查表路径延迟超过一个周期,预测器就会被堵住。我在实习时试过把查表拆成两级流水,第一级用残差高位选table index,第二级取码字,这样单级延迟减半,FIFO深度只要4到8就能吸收波动。整体BRAM占用四块18Kb刚好够存一行,时钟频率设到155MHz留余量。如果你仿真时发现FIFO空满状态频繁翻转,可以试着把地址计算提前到上一级,比如在熵编码完成的同时算好下一像素的参考地址并寄存,这样预测级进来直接读BRAM,省掉一个周期的地址组合逻辑。追问一句:你目前用的仿真工具是Vivado还是ModelSim?不同工具对时序报告的分析粒度会影响你判断瓶颈点。

说一个很多人面试时漏掉的细节:MED预测器需要同时拿到左、上、左上三个像素,但行缓冲的读端口只有一个周期延迟,如果你把地址计算和BRAM读取放在同一级,组合逻辑深度会超标。我当年被追问后改的方案是把地址计算提前到上一级,也就是在熵编码完成的同时把下一像素的参考地址算好并寄存,这样预测级进来直接读BRAM,省掉一个周期的地址组合逻辑。另外Golomb-Rice编码的查表扇出问题也很常见,残差范围0到4095,查表组合逻辑级数容易超标,建议拆成两级流水。你仿真时有没有测过连续输入一行像素时FIFO的空满状态变化?这个很能暴露流水线设计的漏洞。
发表回答
登录后可在本页底部提交回答