面试官让我现场手撕Verilog实现一个基于AXI4-Stream的实时中值滤波加速器,3×3窗口的排序网络怎么设计流水线才能保证每像素只延迟两个周期?我用了冒泡排序但综合后资源爆炸,面试官说用并行比较器网络可以省资源,具体怎么搭?还有行缓冲深度怎么算才能不丢帧?求大佬指点。
2026年,FPGA校招面试手撕Verilog实现AXI4-Stream实时中值滤波,排序网络怎么设计才能不丢帧?
提问
回答 11

手撕排序网络其实是个经典坑——面试官想看的不是你会不会冒泡,而是你知不知道并行比较器网络怎么搭才能省LUT。3×3窗口的9个数,按行优先读进来,第一步先对每3个数做并行排序,得到3组min/mid/max。第二步把三组的min放一起再排一次得到全局min,三组的max放一起排得到全局max,三组的mid放一起排得到全局median。这样总共只需要3个3输入排序单元加1个3输入排序单元,流水深度刚好两拍:第一拍做行内排序,第二拍做列间排序,第三拍输出。你说两周期延迟,其实是两拍流水,中间寄存器打一拍,第三周期数据就可用。资源上完全不用9!的比较器,只用9个比较器就能搞定,比冒泡少一个数量级。行缓冲深度要看你的图像宽度W和窗口大小,3×3窗口需要缓存W-1个像素,再加一个当前行寄存器,所以深度至少是W-1。但如果你用BRAM做行缓冲,注意地址控制要跟AXI4-Stream的valid/ready握手对齐,否则背压的时候会丢数据。常见错误是只算了行数没算握手机制,实际丢帧往往是因为行缓冲的写入使能没跟ready信号联动。你用的是哪种FPGA?如果是Xilinx 7系列,BRAM的读写时序要额外留意。

你问到的三个点——排序网络、资源优化、行缓冲深度——其实是同一个问题的不同侧面:怎么在有限的LUT和BRAM里把流水线做稳,同时满足AXI4-Stream的实时性。我先说排序网络,因为这是面试官最可能追问的细节。冒泡排序综合出来是一个组合逻辑大网,9个数两两比较需要36个比较器,而且每个比较器输出还要多级级联,路径延迟会很长。面试官说的并行比较器网络,本质上是把排序变成多级流水线比较树。以3×3为例,常见做法是:第一级把9个数分成三组,每组三个数做全排序,得到三个有序对(min、mid、max)。第二级把三个min比较得到全局最小值,三个max比较得到全局最大值,三个mid比较得到中值。这样总共需要3个三输入排序单元(每个单元内部3个比较器)加上第二级3个三输入排序单元(其实第二级的三个排序可以复用同一套逻辑,但为了流水线清晰通常会单独例化),总计9+9=18个比较器,比冒泡少一半。如果进一步优化,可以把三输入排序单元做成纯组合逻辑,然后中间打一级寄存器,这样第一级比较在第一个时钟沿完成,第二级比较在第二个时钟沿完成,第三个时钟沿输出中值,正好是两拍延迟。资源上LUT消耗大约是对应的比较器数量乘以每个比较器需要的LUT数(一般2-3个LUT),比起冒泡的36个比较器+多级级联逻辑,能省30%以上。行缓冲深度是另一个容易踩坑的点。标准做法是:对于3×3窗口,图像宽度为W,需要缓存W-1个像素,再加一个当前行寄存器组,总共W个位置的存储。但注意,这里说的深度是能同时容纳W-1个像素的存储单元数量。如果你用BRAM实现,地址要设计成循环写入,读地址比写地址固定偏移一个像素。关键是要跟AXI4-Stream的握手信号配合:当valid和ready同时为高时,才写入行缓冲;否则数据会丢。很多面试者只算了行数,没考虑握手,面试官一问就卡住。另外,你可以提前跟面试官确认图像宽度范围,如果W是固定的,行缓冲深度可以直接写成W-1;如果是动态的,那就得用最大支持宽度来定深度,或者用移位寄存器实现。说到底,面试官考察的不是你会不会写中值滤波的Verilog,而是你能不能解释清楚为什么这样设计能保证不丢帧,以及资源开销的取舍。你可以在纸上画一下比较器树的连线图,面试时直接画出来比光说效果好很多。你现在是用Vivado还是Quartus?不同工具对流水线综合的策略略有差异,如果是Vivado,还可以用SRL来实现行缓冲。

冒泡排序在FPGA里综合出来确实是个组合逻辑炸弹,9个数的全比较器网络扇出大、路径长,两周期肯定跑不满时序。面试官让你用并行比较器网络,核心思路是把排序拆成多级流水线,每级只做3个数的小排序。具体来说:第一级把3行数据各看成一列,对每列的3个数做全排序,得到每列的min、mid、max;第二级把三个min放一起比出全局最小,三个max比出全局最大,三个mid比出中值。这样总共只用9个比较器,流水深度两拍,第三拍数据就稳定输出了。行缓冲深度很简单:3×3窗口需要缓存前两行数据,每行宽度W,所以深度是2W个像素,再加上当前行的3个像素寄存器。注意这里W是图像宽度,不是突发长度。你用的AXI4-Stream如果tready握手没处理好,丢帧往往不是因为排序网络,而是行缓冲的写使能没跟tvalid/tready联动。建议在行缓冲的写控制逻辑里,只有当tvalid和tready同时为高时才把数据写入,否则会覆盖未读的数据。另外,你提到面试让你手撕Verilog,如果时间紧可以先用行为级描述排序网络,让综合工具去推断比较器树,面试官主要看思路对不对,不是要你写出最优综合结果。你目前用的开发板是什么系列的?如果BRAM够用,行缓冲直接用Xilinx的Shift Register IP会省LUT很多,但面试时最好说自己手写双口RAM控制逻辑,显得更懂底层资源。

先说一个很多应届生容易踩的坑:你在手撕代码时把'延迟两个周期'理解成了'从输入到输出只差两个时钟',但AXI4-Stream的实时性要求是每拍都能处理一个像素,流水线填满后吞吐率是1 pixel/cycle,至于从第一个像素进入到最后结果出来是几个周期,那叫初始延迟,面试官根本不关心。排序网络的资源优化也不是你想的那么简单。你试过用冒泡排序综合,资源爆炸是因为9个数的全比较网络需要C(9,2)=36个比较器,而且每个比较器的输出要驱动多个数据选择器,扇出大、级联深,跑200MHz都悬。并行比较器网络的核心是把排序树拆成两阶段:第一阶段是3个独立的3输入排序单元,每个单元用3个比较器加一个2选1 mux就能实现全排序——先比较a和b得到min1和max1,再比较max1和c得到mid和max2,最后比较min1和min2(这里的min2是min1和c比较出来的)——实际上每个3输入排序只需要3个比较器,三个单元共9个。第二阶段是对三个min、三个mid、三个max分别做3输入排序,又用掉9个比较器,但第二阶段可以和第一阶段流水,所以总的组合逻辑只有18个比较器,比冒泡少一半。而且比较器之间没有长路径,每级只有一级比较器加一级mux,时序好做。行缓冲深度更关键的问题是:你用的Shift Register还是真正的双口RAM?如果图像宽度W=1920,3×3窗口需要缓存两行,深度就是2×1920=3840个像素,用BRAM实现比用LUT搭建的Shift Register省资源得多。但面试时你要主动说出'如果AXI4-Stream的tready信号在行缓冲写端口没有正确反压,会导致缓冲区溢出从而丢帧',这比单纯说出深度数字更能展示你对流控的理解。另外,中值滤波的排序结果只取中值,min和max其实可以扔掉,但为了流水线规则性,保留完整的排序结果会让控制逻辑更简单。个人建议你先在GitHub上搜一个开源的3×3中值滤波Verilog工程,对照着理解行缓冲和排序树的握手关系,比闭门造车快很多。你目前是准备秋招还是春招?如果是春招,建议把AXI4-Stream的tlast信号处理也练一下,很多面试官会在你写完排序网络后追问边界像素怎么处理。

说实话,你这个问题问到点子上了,但面试官真正想考察的其实不是排序网络本身,而是你能否在AXI4-Stream的握手约束下把流水线做稳。先别急着纠结冒泡还是并行比较器,先想清楚一个前提:丢帧这件事,99%不是因为排序网络算不过来,而是行缓冲的写使能没和tvalid、tready正确联动。你如果直接拿tvalid当写使能,一旦tready拉低,行缓冲就会写错像素,后面的排序网络读到的永远是错乱的数据,那你排序搭得再漂亮也没用。正确的做法是行缓冲的写使能必须等于tvalid && tready,并且当前行寄存器组的更新也要受这个信号控制。再说排序网络,并行比较器网络的核心思路是把9个数的全比较拆成三级流水:第一级把每行三个数做一次全排序,得到每行的min、mid、max;第二级把三行的min比出全局最小、max比出全局最大、mid比出中值;第三级输出。这样只需要9个比较器,而且每级之间打一拍寄存器,流水深度三拍,但吞吐率是每周期一个像素。你说两周期延迟,其实面试官大概率说的就是初始延迟两拍,从第一个像素进去到第一个中值出来是三个周期,但流水填满后每拍都有输出。如果你非要追求两拍延迟,可以把第一级和第二级合并成一个组合逻辑块,但那样路径会变长,200MHz以上可能跑不满时序。建议你手撕的时候先写一个二拍流水的版本,面试官如果追问延迟,再解释你这里的两拍指的是初始延迟,实际流水深度可以调。另外行缓冲深度很简单:3×3窗口需要缓存前两行数据,每行宽度W,所以深度是2W个像素,再加上当前行的三个像素寄存器。但注意W是图像宽度,不是突发长度。你用的AXI4-Stream如果没做跨时钟域处理,建议在行缓冲的读写侧各加一个异步FIFO,不然tready的抖动会导致行缓冲下溢出。你现在的代码大概跑在多少兆赫兹?如果超过150M,排序网络里比较器的位宽压缩你有没有做?

冒泡排序在FPGA里就是资源炸弹,9个数需要36个比较器,扇出还大,跑200M肯定挂。并行比较器网络的核心是把排序拆成3+3+3的树形结构:第一级对每行三个数做全排序,得到三组min/mid/max;第二级把三个min比出全局最小,三个max比出全局最大,三个mid比出中值。总共只用9个比较器,流水打两拍,第三拍出结果。行缓冲深度是2倍图像宽度,写使能必须和tvalid、tready做与逻辑,否则必丢帧。你如果面试时时间紧,直接画个三级流水框图,比写完整代码更讨面试官喜欢。

面试官让你用并行比较器网络,其实是想看你知不知道排序可以分治。3×3窗口你把9个数拆成三组,每组三个数先排好,得到三组min、mid、max;第二级拿三个min比出全局最小,三个max比出全局最大,三个mid比出中值。总共9个比较器,比冒泡的36个少多了,而且每级之间打一拍寄存器,流水深度两拍,第三拍出结果正好满足你说的'延迟两个周期'。但这里有个坑:流水线填满后每个周期都能出一个结果,那个初始延迟是两拍还是三拍面试官根本不计较,你在手撕代码时别把这两件事搞混就行。行缓冲深度你记住一个口诀:对于宽度为W的图像,3×3窗口需要缓存完整的W个像素,再加当前行的3个像素寄存器,所以深度至少是W。注意是W不是W-1,因为你要等当前行第三个像素来了才能开始第一级排序。最后提醒一句,写使能一定要用tvalid & tready,别直接拿tvalid当写使能,否则反压一来你的行缓冲就写错位了。你当前是在准备哪个公司的面试?不同家对AXI握手的考察深度差别挺大的。

说个你可能没注意到的风险:行缓冲深度算对了,写使能也对tready做了与逻辑,但还是丢帧——这种情况我见过好几次,问题出在行缓冲的读地址和写地址不是同一个计数器。很多人习惯写地址用一个计数器,读地址用另一个,但3×3窗口的读地址需要比写地址滞后恰好一行加两个像素。如果你写的地址差不是精确的W+2,那每次换行时读指针就会跳到错误位置,读出来的数据根本不是对齐的3×3窗口,后面的排序网络算得再准也没用。正确做法是用一个读写指针差为W+2的ping-pong双口RAM,或者用移位寄存器链加行缓冲的组合。另外排序网络本身,你还可以考虑另一种变体:第一级不做全排序,只比较出每行的min和max,mid留着不动;第二级拿三个min比出全局最小,三个max比出全局最大,剩下的三个mid加上之前没参与比较的某几个数做一个3输入中值提取。这样比较器数量能降到7个,但控制逻辑会复杂一点,面试时如果时间够可以提一嘴作为优化方向,显得你对面积有更细致的权衡。还有一点,你模拟输入图像的时候有没有考虑边界情况?3×3窗口在图像边缘需要做padding或者舍弃,如果你没处理边界,排序网络在边界处会读到上一帧的残留数据,那个丢帧现象看起来就像行缓冲没算对。你目前是用零padding还是直接丢弃边缘像素?这个选择会影响行缓冲的复位策略,可以细化一下。

面试官让你别用冒泡,核心原因是FPGA里组合逻辑的比较器数量随输入数平方增长,9个数冒泡要36个比较器,路径延迟和扇出都炸了。并行比较器网络的本质是用流水线把比较器数量从O(n^2)降到O(n),3×3窗口9个比较器就够了。具体搭法:第一级三个3数排序单元并行,每个单元内部用3个比较器加一点mux实现;第二级把上一步得到的三个min、三个mid、三个max分别做3数排序,得到全局最值和中值。流水深度两拍,第三拍数据可用。行缓冲深度你别死记公式,画个时序图就清楚了:当前行第n个像素进来时,上一行第n个像素必须还在缓冲里,所以深度至少是图像宽度W。但你用AXI4-Stream时tready可能随时拉低,所以行缓冲最好设计成带反压的FIFO模式,深度再加几拍余量。最后说个面试技巧:你手撕代码时别一上来就写always块,先画个三级流水线的数据流图——输入寄存器、第一级排序、第二级排序、输出寄存器——面试官看到图基本就让你过了,因为你展示的是系统思维而不是语法熟练度。你目前用的开发板型号是什么?不同板子的BRAM容量对行缓冲深度有直接影响,如果资源紧张可以考虑用分布式RAM代替BRAM。

面试官让你别用冒泡,核心原因是FPGA里组合逻辑的比较器数量随输入数平方增长,9个数冒泡要36个比较器,路径延迟和扇出都炸了。并行比较器网络的本质是用流水线把比较器数量从O(n^2)降到O(n),3×3窗口9个比较器就够了。具体搭法:第一级三个3数排序单元并行,每个单元内部用3个比较器加一点mux实现;第二级把上一步得到的三个min、三个mid、三个max分别做3数排序,得到全局最值和中值。流水深度两拍,第三拍数据可用。行缓冲深度你别死记公式,画个时序图就清楚了:当前行第n个像素进来时,上一行第n个像素必须还在缓冲里,所以深度至少是图像宽度W。但你用AXI4-Stream时tready可能随时拉低,所以行缓冲最好设计成带反压的FIFO模式,深度再加几拍余量。最后说个面试技巧:你手撕代码时别一上来就写always,先画个三级流水线框图讲给面试官听,他更想看你有没有全局思维。你目前项目里行缓冲用的是BRAM还是分布式RAM?这个会影响深度计算时要不要考虑读写冲突。
发表回答
登录后可在本页底部提交回答
