In this previous article, we explored how to create flexible inference templates for both Simple Dual-Port (SDP) and True Dual-Port (TDP) RAMs. As part of that discussion, we covered a lengthy, yet useful, workaround to enable the ram_style attribute in Vivado using a string parameter.
In this follow-up, we’ll take a closer look at alternative approaches and share a more concise inference template for Vivado. Additionally, we’ll demonstrate an exploration strategy for overcoming tool limitations, which is a common challenge in the FPGA world.
Finally, we present a unified approach compatible with both Vivado and Quartus Prime Pro, eliminating the need for separate, tool-specific modules. The updated modules have been integrated into my SystemVerilog tutorial, replacing the previous versions.
Recap of Previous Article
In the previous article, we introduced the following Vivado-specific SDP RAM inference template:
// Greg Stitt
// StittHub (www.stitt-hub.com)
module ram_sdp_vivado #(
parameter int DATA_WIDTH = 16,
parameter int ADDR_WIDTH = 10,
parameter bit REG_RD_DATA = 1'b0,
parameter bit WRITE_FIRST = 1'b0,
parameter string STYLE = ""
) (
input logic clk,
input logic rd_en,
input logic [ADDR_WIDTH-1:0] rd_addr,
output logic [DATA_WIDTH-1:0] rd_data,
input logic wr_en,
input logic [ADDR_WIDTH-1:0] wr_addr,
input logic [DATA_WIDTH-1:0] wr_data
);
if (STYLE == "block") begin : l_ram
(* ram_style = "block" *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else if (STYLE == "distributed") begin : l_ram
(* ram_style = "distributed" *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else if (STYLE == "registers") begin : l_ram
(* ram_style = "registers" *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else if (STYLE == "ultra") begin : l_ram
(* ram_style = "ultra" *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else if (STYLE == "mixed") begin : l_ram
(* ram_style = "mixed" *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else if (STYLE == "auto") begin : l_ram
(* ram_style = "auto" *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else if (STYLE == "") begin : l_ram
logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
end else begin : l_ram
initial begin
$fatal(1, "Invalid STYLE value %s", STYLE);
end
end
logic [DATA_WIDTH-1:0] rd_data_ram;
always_ff @(posedge clk) begin
if (wr_en) l_ram.ram[wr_addr] <= wr_data;
if (rd_en) rd_data_ram <= l_ram.ram[rd_addr];
end
if (WRITE_FIRST) begin : l_write_first
logic bypass_valid_r = 1'b0;
logic [DATA_WIDTH-1:0] bypass_data_r;
always_ff @(posedge clk) begin
if (rd_en && wr_en) bypass_data_r <= wr_data;
if (rd_en) bypass_valid_r <= wr_en && rd_addr == wr_addr;
end
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= bypass_valid_r ? bypass_data_r : rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = bypass_valid_r ? bypass_data_r : rd_data_ram;
end
end else begin : l_read_first
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = rd_data_ram;
end
end
endmoduleThis template was necessary because the simpler approach of specifying the ram_style attribute using a string parameter led Vivado to erroneously report a syntax error, which is a long-standing bug:
module ram_sdp_vivado_bad #(
parameter int DATA_WIDTH = 16,
parameter int ADDR_WIDTH = 10,
parameter bit REG_RD_DATA = 1'b0,
parameter bit WRITE_FIRST = 1'b0,
parameter string STYLE = ""
) (
input logic clk,
input logic rd_en,
input logic [ADDR_WIDTH-1:0] rd_addr,
output logic [DATA_WIDTH-1:0] rd_data,
input logic wr_en,
input logic [ADDR_WIDTH-1:0] wr_addr,
input logic [DATA_WIDTH-1:0] wr_data
);
// PROBLEM: causes synthesis errors in Vivado.
(* ram_style = STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
...
endmoduleFix Attempt 1
While the above template works, it can feel a bit awkward. Additionally, there are times when you might want to infer RAM directly within another module (e.g., a FIFO), rather than explicitly instantiating a RAM module. When inferring RAM within other modules, relying on a long if-else-if chain can be cumbersome, which is why I sought a more concise workaround.
At the suggestion of Joe Hurysz (thanks, Joe!), I tried an alternative workaround, which simply replaces the string parameter with a packed logic array:
module ram_sdp_vivado_attempt1 #(
parameter int DATA_WIDTH = 16,
parameter int ADDR_WIDTH = 10,
parameter bit REG_RD_DATA = 1'b0,
parameter bit WRITE_FIRST = 1'b0,
parameter logic [8*16-1:0] STYLE = "" // Use a packed logic array instead of a string
) (
input logic clk,
input logic rd_en,
input logic [ADDR_WIDTH-1:0] rd_addr,
output logic [DATA_WIDTH-1:0] rd_data,
input logic wr_en,
input logic [ADDR_WIDTH-1:0] wr_addr,
input logic [DATA_WIDTH-1:0] wr_data
);
// Simply use the logic array in place of the previous string.
(* ram_style = STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
logic [DATA_WIDTH-1:0] rd_data_ram;
always_ff @(posedge clk) begin
if (wr_en) ram[wr_addr] <= wr_data;
if (rd_en) rd_data_ram <= ram[rd_addr];
end
if (WRITE_FIRST) begin : l_write_first
logic bypass_valid_r = 1'b0;
logic [DATA_WIDTH-1:0] bypass_data_r;
always_ff @(posedge clk) begin
if (rd_en && wr_en) bypass_data_r <= wr_data;
if (rd_en) bypass_valid_r <= wr_en && rd_addr == wr_addr;
end
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= bypass_valid_r ? bypass_data_r : rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = bypass_valid_r ? bypass_data_r : rd_data_ram;
end
end else begin : l_read_first
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = rd_data_ram;
end
end
endmoduleInitially, I was satisfied with this solution, as it worked in both Vivado and Questa. However, I ran into a few cases where it introduced issues, requiring additional workarounds.
For example, the following RAM module instantiation worked as expected in Vivado:
ram_sdp_vivado_attempt1 #(
.DATA_WIDTH (DATA_WIDTH),
.ADDR_WIDTH (ADDR_WIDTH),
.REG_RD_DATA(REG_RD_DATA),
.WRITE_FIRST(WRITE_FIRST),
.STYLE ("block") // Works
) ram (
...
);However, when I replaced the hardcoded string literal with another string parameter, STYLE, Vivado began reporting errors:
parameter string STYLE = "block"
...
...
ram_sdp_vivado_attempt1 #(
.DATA_WIDTH (DATA_WIDTH),
.ADDR_WIDTH (ADDR_WIDTH),
.REG_RD_DATA(REG_RD_DATA),
.WRITE_FIRST(WRITE_FIRST),
.STYLE (STYLE) // Vivado error: [Synth 8-10531] cannot assign a string to an object of a packed type
) ram (
...
);Essentially, while Vivado had no problem converting a string literal to a packed type, it strangely couldn’t convert a string parameter to a packed type. There may be a valid reason for this error, but I didn’t want to spend hours going down the SystemVerilog spec rabbit hole, so I looked into other workarounds.
I found a workaround that solved the problem, but it wasn’t ideal. Essentially, I could convert every string parameter into a packed logic array. However, given the number of modules in my library that rely on string parameters, this approach was very unattractive, especially since those strings work fine in other situations.
Fix Attempt 2
Next, I tried combining the two approaches by keeping the string as the module parameter, while internally converting that string to a packed array before declaring the ram_style attribute:
module ram_sdp_vivado_attempt2 #(
parameter int DATA_WIDTH = 16,
parameter int ADDR_WIDTH = 10,
parameter bit REG_RD_DATA = 1'b0,
parameter bit WRITE_FIRST = 1'b0,
parameter string STYLE = "" // Still uses a string here
) (
input logic clk,
input logic rd_en,
input logic [ADDR_WIDTH-1:0] rd_addr,
output logic [DATA_WIDTH-1:0] rd_data,
input logic wr_en,
input logic [ADDR_WIDTH-1:0] wr_addr,
input logic [DATA_WIDTH-1:0] wr_data
);
// Internally converts from string to packed array.
// [Synth 8-10531] cannot assign a string to an object of a packed type
localparam logic [8*16-1:0] MEM_STYLE = STYLE;
(* ram_style = MEM_STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
...
endmoduleUnfortunately, this still didn’t work, which wasn’t surprising since it would be odd for it to work for a localparam but not a parameter. However, it did work when starting with a string literal, so it was worth trying.
Interestingly, while Questa had no issues converting a string parameter into a packed array in attempt 1, doing the same for the localparam in attempt 2 resulted in errors that provided some useful hints:
Error (suppressible): (vlog-7041) String assignment: Assigning a string to a packed type requires a cast.
Fix Attempt 3
Questa’s hint about requiring a cast (even though it wasn’t necessary for the parameter conversion) led to my next workaround attempt. Instead of directly assigning the string parameter to the packed array localparam, I decided to cast it first. Since you can’t perform this cast without a custom type, I added several lines to create a typedef and then cast using that defined type:
// Greg Stitt
// StittHub (www.stitt-hub.com)
module ram_sdp_vivado_attempt3 #(
parameter int DATA_WIDTH = 16,
parameter int ADDR_WIDTH = 10,
parameter bit REG_RD_DATA = 1'b0,
parameter bit WRITE_FIRST = 1'b0,
parameter string STYLE = ""
) (
input logic clk,
input logic rd_en,
input logic [ADDR_WIDTH-1:0] rd_addr,
output logic [DATA_WIDTH-1:0] rd_data,
input logic wr_en,
input logic [ADDR_WIDTH-1:0] wr_addr,
input logic [DATA_WIDTH-1:0] wr_data
);
// Attempt 3: Cast the string parameter to a packed array.
localparam int MAX_STYLE_LEN = 16;
typedef logic [MAX_STYLE_LEN*8-1:0] string_as_logic_t;
localparam logic [MAX_STYLE_LEN*8-1:0] MEM_STYLE = string_as_logic_t'(STYLE);
// Use the packed array in the attribute.
(* ram_style = MEM_STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
logic [DATA_WIDTH-1:0] rd_data_ram;
always_ff @(posedge clk) begin
if (wr_en) ram[wr_addr] <= wr_data;
if (rd_en) rd_data_ram <= ram[rd_addr];
end
if (WRITE_FIRST) begin : l_write_first
logic bypass_valid_r = 1'b0;
logic [DATA_WIDTH-1:0] bypass_data_r;
always_ff @(posedge clk) begin
if (rd_en && wr_en) bypass_data_r <= wr_data;
if (rd_en) bypass_valid_r <= wr_en && rd_addr == wr_addr;
end
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= bypass_valid_r ? bypass_data_r : rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = bypass_valid_r ? bypass_data_r : rd_data_ram;
end
end else begin : l_read_first
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = rd_data_ram;
end
end
endmoduleAnd it worked! While it wasn’t as ideal as I had hoped, a few extra lines of casting turned out to be an acceptable solution.
Unified SDP Template
Fortunately, the Vivado workaround for the STYLE parameter is also compatible with Quartus Prime Pro. This means we can now create a flexible SDP RAM template that works with both tools, eliminating the need for separate, tool-specific modules.
// Greg Stitt
// StittHub (www.stitt-hub.com)
module ram_sdp #(
parameter int DATA_WIDTH = 16,
parameter int ADDR_WIDTH = 10,
parameter bit REG_RD_DATA = 1'b0,
parameter bit WRITE_FIRST = 1'b0,
parameter string STYLE = ""
) (
input logic clk,
input logic rd_en,
input logic [ADDR_WIDTH-1:0] rd_addr,
output logic [DATA_WIDTH-1:0] rd_data,
input logic wr_en,
input logic [ADDR_WIDTH-1:0] wr_addr,
input logic [DATA_WIDTH-1:0] wr_data
);
// The Vivado workaround doesn't break anything in Quartus, so we reuse it.
localparam int MAX_STYLE_LEN = 16;
typedef logic [MAX_STYLE_LEN*8-1:0] string_as_logic_t;
localparam logic [MAX_STYLE_LEN*8-1:0] MEM_STYLE = string_as_logic_t'(STYLE);
// Specify ram_style for Vivado, and ramstyle for Quartus.
(* ram_style = MEM_STYLE, ramstyle = MEM_STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
logic [DATA_WIDTH-1:0] rd_data_ram;
always_ff @(posedge clk) begin
if (wr_en) ram[wr_addr] <= wr_data;
if (rd_en) rd_data_ram <= ram[rd_addr];
end
if (WRITE_FIRST) begin : l_write_first
logic bypass_valid_r = 1'b0;
logic [DATA_WIDTH-1:0] bypass_data_r;
always_ff @(posedge clk) begin
if (rd_en && wr_en) bypass_data_r <= wr_data;
if (rd_en) bypass_valid_r <= wr_en && rd_addr == wr_addr;
end
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= bypass_valid_r ? bypass_data_r : rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = bypass_valid_r ? bypass_data_r : rd_data_ram;
end
end else begin : l_read_first
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) if (rd_en) rd_data <= rd_data_ram;
end else begin : l_no_reg_rd_data
assign rd_data = rd_data_ram;
end
end
endmoduleUnified TDP Template
Similarly, we can now replace the previous tool-specific TDP modules with the following unified implementation:
// Greg Stitt
// StittHub (www.stitt-hub.com)
// The following shows a somewhat reusable true dual-port (TDP) RAM template.
// It works on some Xilinx/AMD and Intel/Altera FPGAs, as long as you don't need
// to specify the specific RAM resource, which I add in other examples.
//
// The tricky part of creating a reusable TDP template is that different FPGAs,
// even those from the same vendor, and even those within the same FPGA, can
// provide variations of behavior. In my attempt at a unified template, I
// explored the commonality across most FPGAs.
//
// It is very important to understand the exact TDP behaviors of your specific
// FPGA. Some FPGAs prohibit certain actions (e.g., reading from one port and
// writing to another port using the same address). If you don't comply with
// those requirements, you can get undefined behaviors. As added protection, I
// usually supplement this template with assertions that check for prohibited
// behaviors on my targeted FPGA.
//
// In addition, different RAM resources might provide unique behaviors that are
// ommitted from this template, but can be quite useful.
//
// Note that there is no WRITE_FIRST parameter here. I omitted it because of
// wide differences of read-during-write behaviors on different FPGAs.
//
// Note: This has not been tested in the non-pro versions of Quartus.
// SystemVerilog support is not great in those versions, so I have abandoned
// trying to support it.
module ram_tdp #(
parameter int DATA_WIDTH = 4,
parameter int ADDR_WIDTH = 8,
parameter bit REG_RD_DATA = 1'b1,
parameter string STYLE = ""
) (
input logic clk,
// Port A
input logic en_a,
input logic wr_en_a,
input logic [ADDR_WIDTH-1:0] addr_a,
input logic [DATA_WIDTH-1:0] wr_data_a,
output logic [DATA_WIDTH-1:0] rd_data_a,
// Port B
input logic en_b,
input logic wr_en_b,
input logic [ADDR_WIDTH-1:0] addr_b,
input logic [DATA_WIDTH-1:0] wr_data_b,
output logic [DATA_WIDTH-1:0] rd_data_b
);
// Apply the Vivado STYLE workaround from the SDP module. This isn't needed
// if you are only using Quartus.
localparam int MAX_STYLE_LEN = 16;
typedef logic [MAX_STYLE_LEN*8-1:0] string_as_logic_t;
localparam logic [MAX_STYLE_LEN*8-1:0] MEM_STYLE = string_as_logic_t'(STYLE);
// Set ram_style for Vivado and ramstyle for Quartus.
(* ram_style = MEM_STYLE, ramstyle = MEM_STYLE *) logic [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH];
logic [DATA_WIDTH-1:0] rd_data_ram_a, rd_data_ram_b;
always @(posedge clk) begin
if (en_a) begin
if (wr_en_a) ram[addr_a] <= wr_data_a;
else rd_data_ram_a <= ram[addr_a];
end
end
always @(posedge clk) begin
if (en_b) begin
if (wr_en_b) ram[addr_b] <= wr_data_b;
else rd_data_ram_b <= ram[addr_b];
end
end
if (REG_RD_DATA) begin : l_reg_rd_data
always_ff @(posedge clk) begin
if (en_a) rd_data_a <= rd_data_ram_a;
if (en_b) rd_data_b <= rd_data_ram_b;
end
end else begin : l_no_reg_rd_data
assign rd_data_a = rd_data_ram_a;
assign rd_data_b = rd_data_ram_b;
end
endmodule