My post yesterday got me digging into some issues regarding Vivado and BRAM usage. Vivado has a synthesis bug where it generates incorrect logic when you use the so-called “asynchronous” style of reading from a block RAM. You can read my previous post about placing bypass logic to emulate the asynchronous read style in a block RAM.
A quick note though. Xilinx calls this style of RAM read asynchronous. But it is only asynchronous if the read address is combinational. If you register the read address then the access is really synchronous.
Also, note that Xilinx will surely say that this is not a bug. The Vivado synthesis user’s guide says that in this case simulation won’t match synthesized results. I would argue that that is precisely the definition of a synthesis bug. Vivado should raise an error or at least a warning in this case. In fact, if you try to do a truly asynchronous read on a RAM, it will indicate that it cannot use a block RAM and it will instead use a distributed RAM.
Here is a rather contrived example of a module with two RAMs. One using the Xilinx “synchronous” read and one using the “asynchronous” read.
`timescale 1ns/1ns module read_first_example ( input clk, input reset, input [31:0] sync_wr_data, input [31:0] async_wr_data, input [8:0] sync_wr_addr, input [8:0] sync_rd_addr, input [8:0] async_wr_addr, input [8:0] async_rd_addr, input [31:0] sync_compare_value, input [31:0] async_compare_value, output sync_compare, output async_compare ); reg [31:0] ram_sync_out; reg [8:0] sync_rd_addr_q, async_rd_addr_q; always @(posedge clk) sync_rd_addr_q <= sync_rd_addr; always @(posedge clk) async_rd_addr_q <= async_rd_addr; (* ram_style = "block" *) reg [31:0] ram_sync [511:0]; always @(posedge clk) ram_sync[sync_wr_addr] <= sync_wr_data; always @(posedge clk) ram_sync_out <= ram_sync[sync_rd_addr_q]; assign sync_compare = ram_sync_out == sync_compare_value; (* ram_style = "block" *) reg [31:0] ram_async [511:0]; always @(posedge clk) ram_async[async_wr_addr] <= async_wr_data; wire [31:0] ram_async_out; assign ram_async_out = ram_async[async_rd_addr_q]; assign async_compare = ram_async_out == async_compare_value; endmodule
Example Test Driver
And here is some test code that uses the module.
`timescale 1ns/1ns module read_first_example_test; reg clk = 1; always #5 clk = ~clk; reg reset = 1; initial begin repeat(10) @(posedge clk); reset <= 0; end wire sync_compare, async_compare; reg [31:0] counter, counter1, counter2; always @(posedge clk) begin counter1 <= counter; counter2 <= counter1; if (reset) counter <= 0; else counter <= counter+1; end read_first_example read_first_example ( .clk(clk), .reset(reset), .sync_wr_data(counter), .async_wr_data(counter), .sync_wr_addr(counter[8:0]), .sync_rd_addr(counter[8:0]), .async_wr_addr(counter[8:0]), .async_rd_addr(counter[8:0]), .sync_compare_value(counter2), .async_compare_value(counter1), .sync_compare(sync_compare), .async_compare(async_compare) ); initial begin wait(!reset); repeat (1000) @(posedge clk); $finish; end endmodule
I use a counter here to generate the RAM write and read addresses, and I also use it for RAM write data and output comparisons. You can see that both RAMs write and read from the same address at the same time. You can also see that the compare value for the synchronous read is one cycle delayed from the read value from the asynchronous read.
If you simulate the design you will see that the two comparison values are true throughout the simulation. However, if you perform a gate-level simulation you will discover that the asynchronous compare output will be false. Looking at the synthesis log file you will discover that there is no error or warning. You do however get a message
INFO: [Synth 8-6430] The Block RAM "read_first_example/ram_async_reg" may get memory collision error if read and write address collide. Use attribute (* rw_addr_collision= "yes" *) to avoid collision. But to my mind, we don't have that situation because the read address is the write address delayed by one clock cycle.
Now, here's the strange part. You can add the
rw_addr_collision attribute to the ram register declaration like this.
(* rw_addr_collision = "yes" *) (* ram_style = "block" *) reg [31:0] ram_async [511:0];
Synthesize again and the design will work. Vivado will now add bypass logic to the design much like in the synchronous FIFO design in my previous post. Even more curious is that you can add the attribute to the synchronous RAM declaration and it will still behave correctly. So it appears that Vivado is capable of generating correct logic, you just need to use the magic
I certainly think this is a Vivado synthesis bug. It should never generate logic that does not match the RTL without some kind of complaint. And further, it should generate bypass logic on the asynchronous style of RAM read unless you tell it not to.