Verilog. RAM wrappers and why is it needed
I found a job thanks to my “knowledge” in the field of FPGA. Immediately problems got out that I had no idea about: existing projects occupied 90% of the logical elements, and my job was to place digital filters in the projects. It is clear that "on the forehead" to solve this problem was impossible. Rewrite existing code? It helped, but not much. And only then I realized how you can play with RAM to solve the tasks.
Attention! LEDs will not blink here. Instead, a wrapper shift implementation for M4K will be considered. This publication assumes minimal familiarity with FPGA and Verilog.
To begin with, having made the necessary filter “in the forehead” in a separate project, it became clear where it has a thin spot. It turned out to be a shift register of length 444 * 16 bits (444 is the filter order, 16 bits is the word dimension). There was something to be done with this. The thought came to the fact that the shift is a trivial operation. Get the current value of the register in step N and write the value of the register taken in step N-1 there. After reading the code examples, I found how to do this with RAM:
module pip
#(parameter DATA_WIDTH=16, parameter ADDR_WIDTH=9)
(
input [(DATA_WIDTH-1):0] data,
input [(ADDR_WIDTH-1):0] read_addr, write_addr,
input we, clk,
output reg [(DATA_WIDTH-1):0] q
);
(*ramstyle = "M4K"*)reg [DATA_WIDTH-1:0] ram[2**ADDR_WIDTH-1:0];
always @ (posedge clk)
begin
q <= ram[read_addr];
if (we)
ram[write_addr] <= data;
end
endmodule
This is taken directly from the examples, only the parameters have changed. Now it remains to make this RAM work, as described above. For this, what I called the RAM wrapper was written. Simple state machine:
module upr
#(parameter DATA_WIDTH = 16, parameter ADDR_WIDTH = 9)
(
input wire clk,
input wire en,
input wire [ (DATA_WIDTH-1) : 0 ] ram_upr,
input wire [ (DATA_WIDTH-1) : 0 ] data_in,
output wire [ (DATA_WIDTH-1) : 0 ] upr_ram,
output wire we_ram,
output wire [ (ADDR_WIDTH-1) : 0 ] adr_out
);
assign upr_ram = ram;
assign we_ram = r_we;
assign adr_out = r_adr;
reg [ 2 : 0 ] r_state = state0;
localparam state0 = 3'b001,
state1 = 3'b010,
state2 = 3'b100;
reg [ (ADDR_WIDTH-1) : 0 ] r_adr = {ADDR_WIDTH{1'b0}};
reg [ (DATA_WIDTH-1) : 0 ] ram = {DATA_WIDTH{1'b0}};
reg r_we = 1'b0;
always @(posedge clk)
if(en)
begin
case(r_state)
state0:
r_state <= state1;
state1:
r_state <= state2;
state2:
r_state <= state1;
endcase
end
always @(posedge clk)
case(r_state)
state0:
begin
r_we <= 1'b0;
r_adr <= {ADDR_WIDTH{1'b0}};
ram <= data_in;
end
state1:
begin
r_we <= 1'b1;
if(r_adr == {ADDR_WIDTH{1'b0}})
ram <= data_in;
else
ram <= ram_upr;
end
state2:
begin
r_adr <= r_adr + 1'b1;
r_we <= 1'b0;
end
endcase
endmodule
It works in two stages + one state that can be used during a reset. The state machine is extremely simple - no transition conditions, only the front of the clock signal. The first step (State1) is to capture data, or the previous output value from RAM. It turned out such a feedback. By the second step, the write signal is set to one and the RAM will capture what is needed.
This approach has a significant minus - the shift takes 2 cycles, but this problem can also be easily solved. Another significant minus is that it is impossible to “pull out” at least two values from the shift register in one clock cycle. This means that the convolutional encoder on this will not do. The advantage of this approach is the saving of logic elements (LE or Slice) due to memory.
How to solve the problem of 2 measures and, more interestingly, how to make a fully parameterized digital FIR filter on the basis of this, I will tell you if it would be interesting to anyone.