最近在准备FPGA面试,看到很多公司喜欢问AXI4-Stream接口下的图像处理加速器设计。比如实现一个3×3中值滤波,如何用Verilog设计流水线以处理像素流?行缓冲如何配置才能平衡延迟和资源?还有,如果输入分辨率是1080p,如何保证实时性?希望有经验的大佬能详细讲讲设计思路和代码框架。
2026年,FPGA工程师面试被问如何用Verilog实现一个支持AXI4-Stream的实时图像滤波加速器,如何从流水线和行缓冲角度设计?
提问
回答 12

作为在校生备考,我觉得你先把行缓冲的结构想清楚:3×3滤波需要同时访问三行数据,所以至少需要两个FIFO或BRAM构成行缓冲链。典型做法是用两个行缓冲,每个存一行图像数据,第三个窗口寄存器组从当前行和两个缓冲行中并行取出9个像素。对于1080p,每行1920像素,8位灰度的话行缓冲深度就是1920,宽度8位;如果BRAM宽度够,可以拼成16位或32位来节省资源。流水线方面,把读像素、填窗口、排序求中值、写结果分成4级,每级一拍,这样每时钟周期能处理一个像素,实时性就靠这个吞吐率保证。面试时最好能画出时序图和模块划分,Verilog代码框架可以先写行缓冲控制逻辑和移位寄存器组,中值排序用排序网络或并行比较器实现。

作为一线工程师,我提个工程取舍点:行缓冲深度不要死抠1920,实际存1920个像素就够了,但要注意处理边界像素时要么补零要么复制边缘,这会增加一点控制逻辑。流水线设计上,我一般把读行缓冲、窗口移位、排序、写回分成4级,每级一拍,这样吞吐率就是1像素/时钟。如果时钟跑150MHz,1080p@60fps需要约124MHz像素时钟,所以完全够用。资源优化上,中值排序用3级比较器网络,只比9个值排序省LUT;行缓冲用BRAM或分布式RAM看剩余资源,一般BRAM更省面积。面试官常问的坑是:行缓冲的写使能和读地址怎么同步,以及如何避免跨时钟域问题——AXI4-Stream的valid/ready握手要在输入级处理好,输出也要遵循ready-last规则。

从面试官角度,这个问题核心是想看你有没有系统级思维。首先,AXI4-Stream接口需要处理tvalid/tready/tdata/tlast/tuser等信号,设计时输入级要有一个握手模块,把像素流同步到内部时钟域。行缓冲配置上,如果分辨率是1080p,建议用两个独立的行缓冲FIFO,每个深度1920,宽度等于像素位宽;这样第一行数据流入时同时写到行缓冲0和窗口第一行,第二行数据流入时从行缓冲0读旧行、向行缓冲1写新行,同时更新窗口第二行,以此类推。流水线方面,我期望看到你讲清楚每一级做什么:级1是写行缓冲和更新窗口,级2是排序网络,级3是输出。实时性验证要算一下:1080p@60fps,像素时钟约148.5MHz,如果设计能跑到200MHz以上,且流水线无气泡,那就没问题。常见误区是忽略tlast信号用于帧结束复位行缓冲指针,以及tuser信号用于帧起始同步,这些细节面试官很在意。

刚入行的时候我也被问过类似题,我觉得你先把问题拆成两层来看。第一层是AXI4-Stream的握手协议,你不能只想着valid/ready一拍搞定,实际上input stage要做跨时钟域同步或者FIFO缓冲,不然上游发来的像素流跟你内部时钟不同步就会丢数据。第二层是行缓冲的吞吐匹配,1080p@60fps算下来每像素约5.6ns,如果你的设计在150MHz下能稳定输出,那单时钟周期处理一个像素就够了。我建议你先用状态机把握手信号拆成两拍:第一拍采样tvalid和tdata,第二拍根据tready决定是否推进流水线。行缓冲我习惯用Xilinx的FIFO原语,深度设成1921而不是1920,多一个位置用来处理边界补零,这样边界像素也能正常滤波。流水线分三级:第一级写行缓冲并更新窗口寄存器组,第二级用比较器网络算中值,第三级输出滤波结果。面试时你提一嘴tuser信号用来传递帧同步,很多面试官会眼睛一亮。

作为一名面试官,我其实不太想听你背代码,更想看你怎么权衡资源与延迟。假设你选BRAM做行缓冲,那要注意BRAM有读延迟,读地址发出后要等1到2拍才能读到数据,这会打乱你的窗口对齐。所以行缓冲读数据那级要插入寄存器延迟,让窗口寄存器组在同一个时钟沿采样到三行对齐的像素。我常见到有人直接写always @(posedge clk)读行缓冲然后立刻赋给窗口,结果仿真没问题,上板后图像错位。另外,中值排序不一定非要用完整9输入排序网络,3×3窗口可以先按行排序,再按列排序,最后取对角线中值,这样只用6个比较器,面积小不少。实时性验证方面,如果输入分辨率是1080p,那像素时钟频率约148.5MHz,你设计跑到200MHz的话还要考虑写回AXI4-Stream时的反压处理,一旦tready拉低,流水线要能暂停,否则会丢像素。我一般加一个valid寄存器做握手机制,ready为低时保持当前输出有效直到握手成功。

我一开始也是自学Verilog,踩过不少坑。给你一个具体的代码框架思路:先写一个行缓冲模块,用两个同步FIFO串联,每个FIFO深度1920,位宽8(假设灰度图)。输入像素进来后,第一个像素同时写到FIFO0和窗口第0行,第二个像素同时写到FIFO0和窗口第0行,直到第1920个像素填满FIFO0后,下一像素开始写FIFO1,同时从FIFO0读出一个旧像素给窗口第1行。这样窗口第2行直接从输入流取,第1行从FIFO0取,第0行从FIFO1取。流水线我分成四拍:第一拍采样输入并写FIFO,第二拍读FIFO并组装窗口9个像素,第三拍做中值比较,第四拍输出结果。注意每一拍都要用valid信号打拍传递,防止数据错位。面试前最好用Vivado或者Quartus跑一下时序,看BRAM读写延迟是否满足,如果时钟频率高,可以在行缓冲输出端加一级寄存器打拍。还有个误区:有人把行缓冲深度设成1920,但忘了处理行尾的tlast信号,导致帧间数据混叠,你可以在每帧开始用tuser复位行缓冲的写指针。

从在校生备考的角度看,这类面试题其实在考察你能否把数字电路基础课里的概念串起来。建议你先把AXI4-Stream握手协议的状态机画清楚,valid和ready的互锁关系是基础,但面试官更想听你讲怎么处理tready突然拉低的情况——我的做法是在输入级加一个小FIFO深度16,用almost_full信号反压上游,这样流水线核心段不用停。行缓冲部分,别一上来就写两个独立FIFO,先画一张三行数据的时间对齐图:第0行写入时窗口第一列存1920个像素,第1行到来时同时从第0行FIFO读数据给窗口第二列,这样三行数据在第三行末尾才能对齐。流水线我习惯分五级:输入握手、写FIFO与读FIFO、窗口寄存器组更新、中值排序网络、输出握手。每级之间用valid链传递,这样时序收敛容易。面试前用Vivado跑个综合看看BRAM数量,1080p下两个1920深度8位宽的FIFO刚好占一个BRAM36K,资源很省。

作为一名一线做视频处理的工程师,我想提醒你一个容易被忽视的工程细节:行缓冲的读延迟必须和流水线对齐。很多新手用BRAM做行缓冲时,读地址发出后数据要等一个时钟周期才回来,如果你直接在同一个always块里读BRAM然后赋值给窗口寄存器,那窗口第1行和第2行的数据会错一个周期。正确的做法是在BRAM输出端加一级寄存器打拍,然后用额外的移位寄存器把当前输入像素也延迟一拍,这样三行数据才能对齐。另外,中值滤波的排序网络用9输入全比较器太浪费,我一般用奇偶交换排序法,先对三行分别排序,再对三列排序,最后取对角线中值,这样只消耗6个比较器,LUT用量减半。实时性验证方面,除了算像素时钟频率,还要考虑tready反压导致的吞吐下降——如果下游模块处理慢,你的流水线必须能暂停,我习惯在每个流水级加一个valid-ready握手对,这样暂停时数据不会丢失。

从面试官视角出发,这道题的核心不是看你能不能写出完整代码,而是看你有没有系统级权衡的思维。我会先问一个开放问题:如果输入分辨率从1080p升级到4K,你的设计哪些部分需要改?回答方向应该是行缓冲深度从1920变成3840,BRAM数量翻倍,流水线级数不变但时钟频率要更高。另一个常见考察点是边界处理——3×3窗口在图像边缘时如何补像素?我期待听到你说用tuser信号标记帧起始,然后在边界处复制第一列或最后一列像素,而不是简单补零,因为补零会导致边缘出现黑边。流水线设计上,我不喜欢听人背四拍五拍,而是希望你说清楚为什么选这个级数:级数太少时序紧张,级数太多延迟大但资源更松,一般1080p@60fps下3到4级足够。最后,Verilog代码框架里一定要有参数化设计,比如WINDOW_SIZE和PIXEL_WIDTH用parameter定义,这样面试官会觉得你考虑到了复用性。

我是在校研究生,最近也在为这类面试题做专项练习。我的建议是,不用急着写完整代码,先把AXI4-Stream握手信号的状态机画清楚,这是基础。对于1080p,行缓冲深度用1920没错,但要注意边界处理——我习惯在输入级加一个计数器,跟踪当前像素在行内的位置,当列号小于2时,把窗口内缺失的像素复制边缘值,而不是补零,这样滤波后的图像不会出现黑边。流水线我分四级:第一级采样输入并写行缓冲,第二级从行缓冲读数据并组装3×3窗口,第三级做中值排序,第四级输出握手。排序网络我用三个比较器先对每行排序,再对每列排序,最后取中间值,这样只用6个比较器。面试前我会在Vivado里跑个综合,看看BRAM数量和LUT使用,如果资源超了,可以把行缓冲从BRAM改成分布式RAM,但要注意深度不能太大,否则时序会变差。
发表回答
登录后可在本页底部提交回答
