I'm trying to synthesize the following hardware in Vivado, which contains an array of my_struct_t. To reach timing closure, it's important that this array is implemented with LUT RAM and not with FF RAM. This is just a toy example and the actual code instantiates a much larger array.
// A very simple struct typedef struct packed { logic [15:0] x; logic [15:0] y; } my_struct_t; module mydut #( parameter NUM_STRUCTS=128 ) ( input clk, input reset, input [31:0] update_addr, input [31:0] update_data, input update_strobe, input update_table, input [$clog2(NUM_STRUCTS)-1:0] update_id, input [$clog2(NUM_STRUCTS)-1:0] read_id, output my_struct_t read_struct ); (* ram_style = "distributed" *) my_struct_t pending_data [NUM_STRUCTS]; (* ram_style = "distributed" *) my_struct_t actual_data [NUM_STRUCTS]; always_ff @(posedge clk) begin automatic logic wen = (update_strobe && (update_addr[24 +: 8] == 8'ha5)); automatic int table_write_index = update_addr[0 +: 16]; if (wen && (update_addr[15 +: 8] == 0)) begin pending_data[table_write_index].x <= update_data; if (reset) actual_data[table_write_index].x <= update_data; end if (wen && (update_addr[15 +: 8] == 1)) begin pending_data[table_write_index].y <= update_data; if (reset) actual_data[table_write_index].y <= update_data; end if (update_table) begin actual_data[active_write_idx] <= pending_data[update_id]; end end // read attribute // combinationally driven; responsibility of containing module to latch result. always_comb begin if (update_table && (read_id == update_id)) begin read_struct = pending_data[read_id]; end else begin read_struct = actual_data[read_id]; end end endmodule If you have Vivado installed, you can play along at home by running
vivado -mode tcl -source synth.tcl With the following synth.tcl
read_verilog -sv top.sv auto_detect_xpm # synth synth_design -top mydut -part xc7k160tffg676-1 \ -flatten_hierarchy rebuilt \ -verbose -debug_log write_checkpoint -force post_synth.dcp For each element in my arrays, Vivado tells me
WARNING: [Synth 8-7186] Applying attribute ram_style = "distributed" is ignored, object 'pending_data[0][x]' is not inferred as ram due to incorrect usage It's not a usage issue. Even if I don't do anything with them:
module mydut #( parameter NUM_STRUCTS=128 ) ( input clk, input reset, input [31:0] update_addr, input [31:0] update_data, input update_strobe, input update_table, input [$clog2(NUM_STRUCTS)-1:0] update_id, input [$clog2(NUM_STRUCTS)-1:0] read_id, output my_struct_t read_struct ); (* ram_style = "distributed" *) my_struct_t pending_data [NUM_STRUCTS]; (* ram_style = "distributed" *) my_struct_t actual_data [NUM_STRUCTS]; endmodule Vivado makes sure to tell me Warning: [Synth 8-7186] before optimizing away the unused signals.
If I break out the struct elements into individual arrays like this:
(* ram_style = "distributed" *) logic [15:0] pending_data_x [NUM_STRUCTS]; (* ram_style = "distributed" *) logic [15:0] actual_data_x [NUM_STRUCTS]; (* ram_style = "distributed" *) logic [15:0] pending_data_y [NUM_STRUCTS]; (* ram_style = "distributed" *) logic [15:0] actual_data_y [NUM_STRUCTS]; I have no issues. The code is functionally identical but harder to write and maintain.
Does anyone know a way to get Vivado to handle this correctly, or is this really the state of FPGA tooling in 2025? This feels like a C compiler bug from 1975... The mapping from struct items to separate arrays is super obvious. I feel like there has to be a way around this.
Could the synthesis tool maybe flatten the struct out and then during a later post-synthesis optimization stage the FDRE be remapped to LUT?