FPGA笔试题:用Verilog写一个同步FIFO,并考虑深度为2的幂次方的情况。

开放18 回答 135 浏览

这道题在各大公司的FPGA笔试和面试中出现的频率极高,堪称“典中典”。题目要求通常包括:1. 编写同步FIFO的Verilog代码,包含写使能、读使能、数据输入输出、满空标志等。2. 深度参数化,且要求深度为2的幂次方(如16,32,64),以便用指针高效判断满空。3. 可能会追问如何判断“满”和“空”,以及格雷码指针的作用。有没有朋友可以分享一下标准的实现代码和设计思路?一起讨论下其中的关键点和易错点。

分享:
  • 硅农预备役001

    同步FIFO这个题确实太经典了,笔试面试几乎必问。我一般会先定义模块,把深度做成参数,而且强制约束为2的幂次方,这样指针用格雷码转换后判断满空会方便很多。

    关键点在于读写指针的管理。我习惯用两个寄存器来存写指针和读指针,都是位宽比深度多一位。多出来的那位用来区分满和空的状态。读写时钟是同一个,所以是同步的。

    判断空很简单,就是读写指针完全相等的时候。判断满则是写指针比读指针多绕了一圈,也就是最高位不同,但其余低位相同。这里用格雷码来传递指针可以避免跨时钟域问题,虽然同步FIFO是一个时钟,但很多题目会延伸问异步FIFO,所以提前用格雷码也算个好习惯。

    代码结构上,我会分开写指针更新逻辑、满空标志生成、以及RAM或者寄存器数组的数据存储部分。数据存储直接用二维寄存器实现就行,根据写指针的低位部分作为地址写入,读的时候类似。

    易错点有几个。一个是满空标志的生成时序,最好用组合逻辑直接根据指针比较产生,确保能及时反应。另一个是深度参数化时,地址位宽的计算要小心,别算错了。还有读写使能同时有效且FIFO为空或为满时的行为要定义清楚,一般同时有效且为空时,写入的数据可以直接被读出,这叫旁路模式,但题目不要求的话按正常优先级处理也行。

    下面是我常用的一个代码框架,你可以参考一下。注意这是同步的,所以没做跨时钟域处理。

    module sync_fifo
    #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH = 16 // 必须为2的幂
    )
    (
    input wire clk,
    input wire rst_n,
    input wire wr_en,
    input wire rd_en,
    input wire [DATA_WIDTH-1:0] din,
    output wire [DATA_WIDTH-1:0] dout,
    output wire full,
    output wire empty
    );

    // 计算地址位宽,比深度多一位用于区分满空
    localparam PTR_WIDTH = $clog2(DEPTH) + 1;
    localparam ADDR_WIDTH = $clog2(DEPTH);

    reg [PTR_WIDTH-1:0] wr_ptr, rd_ptr;
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    reg [DATA_WIDTH-1:0] dout_reg;

    // 指针转换为格雷码
    wire [PTR_WIDTH-1:0] wr_ptr_gray, rd_ptr_gray;
    assign wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
    assign rd_ptr_gray = rd_ptr ^ (rd_ptr >> 1);

    // 满空判断:比较格雷码指针
    assign empty = (wr_ptr_gray == rd_ptr_gray);
    assign full = (wr_ptr_gray == {~rd_ptr_gray[PTR_WIDTH-1], rd_ptr_gray[PTR_WIDTH-2:0]});

    // 数据输出
    assign dout = dout_reg;

    // 写逻辑
    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    wr_ptr <= 0;
    end else if (wr_en && !full) begin
    mem[wr_ptr[ADDR_WIDTH-1:0]] <= din;
    wr_ptr <= wr_ptr + 1;
    end
    end

    // 读逻辑
    always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
    rd_ptr <= 0;
    dout_reg <= 0;
    end else if (rd_en && !empty) begin
    dout_reg <= mem[rd_ptr[ADDR_WIDTH-1:0]];
    rd_ptr <= rd_ptr + 1;
    end
    end

    endmodule

    这个代码框架比较简洁,重点都体现了。实际面试时可能还会让解释为什么满空判断是那样写的,或者问格雷码的具体好处,可以准备一下。

  • 芯片爱好者小王

    同步FIFO啊,这个确实经典。我一般用双端口RAM做存储,读写指针用格雷码,这样跨时钟域也方便(虽然题目是同步的)。深度是2的幂次方的话,指针可以直接用二进制,满空判断用最高位和其余位比较。比如深度16,指针宽度5位,低4位是地址,最高位用来区分是否绕了一圈。当读写指针完全相等是空,当读写指针除了最高位外相等但最高位不同就是满。代码结构就是 always 块控制指针和标志位生成,再例化一个RAM。注意写满不写,读空不读就行。

  • 嵌入式学习ing

    我面试就被问过这个。关键点就两个:一是满空标志的产生逻辑,二是深度为2的幂次方时如何优化。很多人直接用计数器记录数据个数来判断满空,虽然简单但不够高效。用指针的话,二进制指针需要比较所有位,而用格雷码指针可以降低亚稳态风险(虽然同步FIFO用不到跨时钟域,但通常设计会考虑扩展为异步)。我的实现里,读写指针用格雷码寄存,然后格雷码转二进制去生成RAM地址,满空判断直接比较格雷码指针,条件是:(wptr_gray == {~rptr_gray[ADDR_WIDTH:ADDR_WIDTH-1], rptr_gray[ADDR_WIDTH-2:0]}) 表示满,相等表示空。注意ADDR_WIDTH是地址位宽,深度=2^ADDR_WIDTH。

  • 数字电路萌新007

    贴个简单思路吧。模块声明带参数DEPTH,且用localparam ADDR_WIDTH = $clog2(DEPTH) 自动计算地址宽度。读写指针用 [ADDR_WIDTH:0] 位宽,多出的一位作为折回标志。always @(posedge clk) 里,如果复位,指针清零,满空标志置位。否则,写使能且不满时,写指针加1,数据写入双端口RAM的对应地址;读使能且不空时,读指针加1,从RAM读出数据。满标志 assign full = (wptr[ADDR_WIDTH] != rptr[ADDR_WIDTH]) && (wptr[ADDR_WIDTH-1:0] == rptr[ADDR_WIDTH-1:0]);空标志 assign empty = (wptr == rptr)。注意指针加1要用 modular addition,即溢出后自动回绕。RAM可以用reg数组模拟,或者调用IP。

  • Verilog新手村

    同步FIFO这个题确实太经典了,几乎必考。我的实现思路一般是:用双端口RAM做存储,读写指针用二进制计数,但满空判断需要比较指针。深度是2的幂次方的好处是,指针可以自动回绕,用简单的位操作就能实现。比如深度是16,指针宽度5位(因为要区分满和空),低4位是地址,最高位用来标志“绕了一圈”。空就是读写指针完全相等,满就是读写指针的高位相反但低位相等。代码框架大概就是 always @(posedge clk) 控制指针和标志位生成,注意复位时指针清零、满空标志置位。格雷码指针主要是为了跨时钟域打拍同步时减少亚稳态,但同步FIFO在一个时钟域里,其实可以不用格雷码,不过面试官可能会问异步FIFO的区别,所以最好提一下。

  • FPGA探索者

    我直接贴一段我常用的参数化代码吧,深度用参数 DEPTH 表示,且要求是2的幂次方。关键点是读写指针的位宽要比地址多一位,用来判断满状态。空标志很简单,就是 rd_ptr == wr_ptr。满标志是 (wr_ptr[ADDR_WIDTH] != rd_ptr[ADDR_WIDTH]) && (wr_ptr[ADDR_WIDTH-1:0] == rd_ptr[ADDR_WIDTH-1:0])。注意写满时不能再写,读空时不能再读,这是基本的流控。另外,读写使能同时有效且空满都不成立时,可以同时操作,这是同步FIFO的优势。易错点可能是满空标志的生成时序,最好用组合逻辑或者根据指针直接生成,不要有额外延迟。

  • FPGA实验小白

    从经验分享角度,这道题除了写代码,面试官常问:为什么深度要2的幂次方?答案就是为了指针回绕方便,用二进制加法自然溢出就能实现,不需要比较器判断是否达到深度-1。还有,格雷码指针在同步FIFO里不是必须的,但如果你写了,面试官可能会觉得你考虑周全,知道异步FIFO的常见做法。另一个关键点是FIFO的“保守”设计:满空标志可以提前一点或延迟一点生成,比如“几乎满”“几乎空”,但笔试题一般要求精确。最后,测试时一定要覆盖边界情况:连续写直到满、连续读直到空、同时读写等。代码风格上,建议用 parameter 定义深度,方便修改。

  • FPGA学号2

    同步FIFO啊,笔试老熟人了。我的思路是用双端口RAM做存储,读写指针用格雷码。深度是2的幂,这样指针绕回的时候用位宽截断就行,比如深度16,指针用5位,低4位是地址,最高位用来区分是否绕了一圈。判断空就是读写指针完全相等,判断满是读写指针最高位不同但低地址位相同。代码骨架大概这样:module sync_fifo #(parameter DEPTH=16, WIDTH=8) (input clk, rst_n, wr_en, rd_en, input [WIDTH-1:0] din, output reg [WIDTH-1:0] dout, output full, empty); 然后定义读写指针reg [$clog2(DEPTH):0] wr_ptr, rd_ptr; 存RAM。always @(posedge clk) if (wr_en && !full) ram[wr_ptr[$clog2(DEPTH)-1:0]] <= din; 指针更新和标志生成是重点,记得用格雷码转换函数减少亚稳态风险。易错点:满空标志要用次态指针判断,别用现态,不然会滞后一个周期。

  • 码电路的小王

    我一般直接写个可综合的版本,深度参数化用$clog2。关键确实在指针。为什么用格雷码?因为读写指针是跨时钟域比较吗?不对,同步FIFO是同一个时钟,其实可以不用格雷码,用二进制也行。但很多题目要求格雷码可能是为了和异步FIFO统一思路,或者考察知识迁移。实际写的时候,我习惯把指针多设一位作为wrap-around位。比如深度8,指针用4位。空:wr_ptr == rd_ptr;满:wr_ptr[3] != rd_ptr[3] && wr_ptr[2:0] == rd_ptr[2:0]。注意写使能和读使能同时有效时,如果满了就不能写,空了就不能读,但非满非空时可以同时操作。代码里标志逻辑要用组合逻辑还是时序看要求,笔试通常组合生成,但实际可能会打一拍。

  • 电子爱好者小张

    分享个简洁的实现要点吧。1. 存储用二维reg或者例化RAM。2. 读写指针每次+1时要取模,因为深度是2的幂,所以直接溢出截断低地址位就行,比如 ptr_next = ptr + 1; ptr = ptr_next; 地址取 ptr[$clog2(DEPTH)-1:0]。3. 满空判断用指针的整个位宽比较,不需要单独最高位,因为深度2的幂时,指针从0加到DEPTH-1再回到0,其二进制值会自然循环。但这样判断满需要区分“真满”和“真空”时指针相等的情况,所以通常还是多一位。我推荐用计数器记录FIFO内数据个数,最简单直观,满空直接比较计数器和0、DEPTH。但面试官可能想要指针方案。易错点:复位后指针清零,满空标志要正确。测试时多覆盖边界情况,比如同时读写、写满、读空。

登录后可在本页底部提交回答

提问者

芯片小学生查看主页

描述场景与已尝试方案,更容易获得有效解答

浏览「其他」

相关问题

同分类问答

提问建议

  • 标题写清核心疑问,避免「求助」「请问」等空泛用语
  • 正文补充环境、版本、报错信息或截图
  • 先搜索本站是否已有相近问题,减少重复提问
  • 若与课程相关,请标明课时或章节便于讲师定位

技术问答

问完之后的闭环

  • 关联课程精学高频问题往往对应章节,建议回到课程补基础。
  • 产出与互助解决过程可写成笔记,帮助后续同学。

探索全站