Synchronous FIFO Redux

So, almost ten years ago to the day, I posted an article on implementing a synchronous FIFO. Well, take the read portion of that FIFO implementation with a grain of salt.

Asynchronous Read

To summarize, here is the portion of the FIFO which implements the memory.

 reg [width-1:0] mem [depth-1:0];
  always @(posedge clk)
    begin
      if (writing)
	mem[wr_ptr] <= wr_data;
      rd_ptr <= next_rd_ptr;
    end

  assign rd_data = mem[rd_ptr];

Here is a timing diagram of writing one piece of data to the FIFO and then immediately reading it out.

Looks great, doesn’t it?

The Problem

This code, however, does not work on Xilinx FPGAs from 6 series and above. Both XST and Vivado will happily implement the equivalent of this for the read logic. And by happily, I mean without error or warning.

always @(posedge clk)
  rd_data <= mem[rd_ptr];

As you and I can tell, that is not the same thing. And it causes the design in my old post to behave in subtly incorrect ways. You can see this when the FIFO is leaving the empty state.

The Solution

To solve this problem we need to implement a bypass register to hold the write data along with an output mux to select the bypass register when we are reading immediately following a write.

Here is the code which does the synchronous read from the RAM.

reg [width-1:0] rd_mem;
  always @(posedge clk)
    rd_mem <= mem[next_rd_ptr];

Here is the bypass register, and the code to determine when the bypass register should be used instead of the RAM output.

reg [width-1:0] bypass_reg;
always @(posedge clk)
  bypass_reg <= wr_data;

reg use_bypass;
always @(posedge clk)
  use_bypass <= writing && wr_ptr == next_rd_ptr;

And here is the output MUX.

assign rd_data = use_bypass ? bypass_reg : rd_mem;

An Alternate Approach

In looking at the Vivado Synthesis Guide (ug901), you can see in the section called RW_ADDR_COLLISION on page 63, a description of an attribute which allows the write data to take priority over the read data. If you synthesize the sync_fifo design with the synchronous_read parameter set to zero or one you will see that the same muxing logic is created either way. With synchronous_read set to one, the muxing is explicit. With it set to zero, then the mux logic is implicit and Vivado will add it for you. It looks like it is safe to use either way. Unfortunately, the attribute is not supported in XST, so there you will need to use the explicit bypass logic with the synchronous read style.

Here is an example of how to set the attribute when you declare the RAM in Verilog.

(* rw_addr_collision = "yes" *)
reg [width-1:0] mem [depth-1:0];

Conclusion

I actually find this pretty shocking that both XST and Vivado generate incorrect code without any error or warning. Clearly, this is a bug and makes me wonder what other constructs it is implementing incorrectly. You can download the complete synchronous FIFO design here.

3 thoughts on “Synchronous FIFO Redux

  1. Pingback: RAM address conflicts and a Vivado synthesis bug | Beyond Circuits

  2. Thank you for the article!
    From what I understand, the address conflict described is not for the same cycle. It is for a WR followed by RD to same location (let’s say 5, so during RD cycle use_bypass flag will be set by previous WR).
    What happens for the following scenarios?
    1) fifo is empty , and in the first clock both rd and wr enables are asserted. How to make sure that synchronous read returns the same data written at location zero? Does it require a new rd_wr_addr collision flag/bypass mux structure?
    2) fifo is not empty, and the same scenario of rd and wr enables occur for same addr location?

  3. I think case 1 is not valid. The protocol is that if you assert read and the FIFO is not empty, then a read occurs. In the case when it is empty and we are writing, on that exact cycle it is still empty. So the read is not valid. I am not sure about your case 2. The only way to have the read and write address be the same on the same cycle is to write when the FIFO is going full and read at the same time. I think this case works fine. The read value is what was stored in the RAM and not what is being written. You are correct that the issue is actually for a read from an address which was just written the previous cycle.

    My assertion in this article is that I think Xilinx actually has a bug in their synthesis tools. The tools should generate a gate-level design which implements the behavior of the RTL. Xilinx of course says this is not a bug because it is what they intended. At any rate, there is no way to change it now. There are likely existing designs which rely on the behavior. I would like to see this turn into an error rather than a warning. That way you can add the pragma to your code if you want the asynchronous read style behavior.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.