Introduction to Abstract Verilog: Part 2

Memories

Both asynchronous and synchronous memories, which are common in FPGAs, are supported by Abstract Verilog. These memories have one read port and one write port. (Single ported memories can be constructed from these dual-ported memories. Memories with multiple ports are beyond the scope of Abstract Verilog.)

Memories are extensions of registers and are treated similarly. Assignments (writes) to memories take place at the next clock edge just like with registers. Reads, however, can be either asynchronous or synchronous. In asynchronous (combinational) memories, the memory output value changes whenever the read address changes. In synchronous (pipelined) memories, the memory output value does not change until the next clock edge.

Memories are declared similarly to Verilog (multiple dimensions from Verilog 2001 are supported):

MEMORY [3:0] y [0:7]; // 8 locations of 4 bits each

SYNCMEM [7:0] mem [0:127]; // Synchronous memory

In both synchronous and asynchronous memories, writes are just like assignments to registered signals:

y[i] <-- foo;

mem[0] <-- bar;

That is, the value is written to the memory location on the next rising clock edge. If two writes are performed to the same location or different locations before the next clock edge, then only the last write is performed. For example, in

y[2] <-- foo;

y[4] <-- bar;

the first assignment does not execute. Only the second write is executed since the memory has only one write port. (This should be considered illegal, but the compiler cannot figure this out.)

Memory reads are done using special variables that are defined by the memory. Reads to a memory named “foo” are done into the special read variable named “foo_data”. This special read variable can be assigned only by a memory read. Reading asynchronous memory must use the combinational assignment:

y_data = y[i];

Only one read can be performed in a single clock cycle. (If you perform two reads in a clock cycle, the simulator may try to do something, but this will certainly not work in the synthesized circuit. Again, the compiler cannot detect this reliably.)

Reads of synchronous memory must use the sequential assignment operator:

y_data <-- y[k];

This assignment takes place on the next clock tick, which means the value read from y[k] is available in the variable y_data after the next clock tick. Again, the y_data register is generated automatically when y is declared as a synchronous memory. Typically, a state machine performs the read in one state and then uses the data in the next state. Of course, reads can be pipelined, so that while the read data is being used, the next value is being read from memory. (If two reads take place to the same memory before the next clock edge, only the last read is performed.)

Example Program using Asynchronous Memory

Here is a simple example of the implementation of a synchronous queue using asynchronous memory:

module queue(clk, reset, din, push, full, dout, pop, empty);

input clk, reset;

// Input port

input [7:0] din;

input push;

output full;

// Output port

output [7:0] dout;

input pop;

output empty;

parameter SIZE = 32;

MEMORY [7:0] mem[0:SIZE-1];

REGISTER [4:0] head = 0, tail = 0;

COMB [4:0] tailP1 = tail+1;

COMB [7:0] dout;

COMB full = (tailP1 == head); // Tail has almost caught up

COMB empty = (head == tail);

ALWAYS begin

mem_data = mem[head]; // Async memory

dout = mem_data; // Must use special variable

// Write to queue tail

if (push & ~full) begin

mem[tail] <-- din;

tail <-- tailP1;

end

// Pop queue head

if (pop & ~empty) begin

head <-- head + 1;

end

end

endmodule // queue

Example Program using Synchronous Memory

The program implements a variable-length shift register using a synchronous memory.

module dp11(clk, reset, shift, length, dataIn, dataOut);

parameter WIDTH = 16;

input clk, reset;

input shift; // Shift control signal

input [6:0] length; // Shift register length up to 127

// Input port

input [WIDTH-1:0] dataIn;

// Output port

output [WIDTH-1:0] dataOut;

SYNCMEM [WIDTH-1:0] SR[0:127];

// Connect output to shift register memory output

COMB [WIDTH-1:0] dataOut;

REGISTER [6:0] ptr = 0;

ALWAYS begin

SR_data <-- SR[ptr]; // Read must use SR_data

dataOut = SR_data;

SR[ptr+length-1] <-- dataIn;

if (shift) ptr <-- ptr + 1;

end

endmodule

Two-dimensional memories are also allowed as shown in the following example:

SYNCMEMORY [15:0] mem[0:255][0:255];

ALWAYS begin

mem[xin][yin] <-- datain;

mem_data <-- mem[xout][yout];

dataout = mem_data;

end

Verilog allows higher dimensions, but Abstract Verilog doesn’t yet.

Verilog Constructs

You can use many of the usual Verilog constructs when writing Abstract Verilog programs. For example, Abstract Verilog supports the Verilog 2001 signed declaration:

input signed[23:0] a, b, c;

COMB signed [23:0] temp;

Other useful Verilog features that are supported include localparam, integer.

Some of the more useful features of Verilog are tasks and functions, which allow abstraction, modularity and reuse in a program.

Functions

Verilog functions correspond to normal (inline) functions in programming languages. They take a number of inputs, and have a single output. You can think of a function as representing a combinational logic block with a single output. You may declare COMB signals in a function, but no REGISTER signals. Here is an example of the max function;

input [7:0] in1, in2, in3;

output [7:0] out;

REGISTER [7:0] out;

COMB [7:0] x, y, z;

COMB [7:0] next;

function max;

input [7:0] a, b, c;

begin

max = a;

if (b > a & b > c) max = b;

if (c > a & c > b) max = c;

end

endfunction

ALWAYS begin

out <-- max(in1, in2, in3);

next = max(x, y, z);

end

Tasks

Do not be confused by the name: Tasks are just generalized functions that can return multiple values via output ports. (These are similar to Fortran Procedures.) You can think of a task as a simple module that is connected into your program. As in functions, you may declare COMB signals, but no REGISTERs. Moreover, Abstract Verilog currently restricts the outputs of a task to be COMB signals. These outputs can of course be then assigned to REGISTER signals. Here is an example of a task:

COMB [7:0] Yout, Crout, Cbout;

localparam windowULx = 256, windowULy = 128;

`define BLUE { 8'd240, 8'd16, 8'd240 }

`define WHITE { 8'd240, 8'd128, 8'd128 }

task colors;

input [10:0] x, y;

output [7:0] Y01, Cr, Cb;

begin

if ((x < windowULx) & (y < windowULy)) begin

{ Y01, Cr, Cb } = `WHITE;

end else begin

{ Y01, Cr, Cb } = `BLUE;

end

end

endtask // colors

ALWAYS begin

colors(col, line, Yout, Crout, Cbout);

end

Extras

Abstract Verilog also contains a few extras. For example, the clogb2 function is declared and can be used by the designer. clogb2 returns the log2 of its argument. This is useful, for example,

(* *) style comments can appear in memory declarations. This is used to force the synthesis tools to use Block RAMs for these memories as shown in the following example:

SYNCMEM signed [31:0] H[0:QUERY_LENGTH-1]
(* synthesis syn_ramstyle="select_ram" *);

Inside Abstract Verilog

You do not need to know exactly how an Abstract Verilog program is translated into Verilog, but it is sometimes useful to know, especially when you are debugging the Verilog file that the translator produces.

Both COMB signals are declared as Verilog “reg” signals, except for COMB signals that are assigned in the declaration, which are declared as wires.

Construct / Implementation
COMB [3:0] c;
COMB overflow = count > 15;
/ reg [3:0] c;
wire overflow = count > 15;

REGISTER signals are more complicated: each REGISTER signal ‘foo’ is declared as two reg signals, foo and next_foo, for the register output and input respectively. At the beginning of the ALWAYS block, next_foo is initialized to foo. That is, the next value of the register is initialized to be the current value of the register. Every assignment to foo in the ALWAYS block is converted to an assignment to next_foo. An always @(posedge clock) block is generated which assigns the value of next_foo to the register signal foo. That is, foo is only assigned on the clock edge. The reset signal is used in an if-then-else to assign the initial value to foo on reset.

Construct / Implementation
REGISTER [3:0] r = init;
ALWAYS begin
r <-- foo;
c = bar;
end / reg [3:0] r, next_r;
always @(posedge clk) begin
if (reset) begin
r <= init;
end else begin
r <= next_r;
end
end
always @(*) begin
next_r = r;
c = ‘bX;
next_r = foo;
c = bar;
end

ALWAYS blocks are converted into Verilog-2001 always @(*) blocks, and COMB signal assignments left unchanged in the output. However, all COMB signals that are assigned in an ALWAYS block are initialized to ‘X’ at the beginning of the block. (This can be overwritten using the ?? flag.)

Memories are generated by declaring a read address, write address, read data, write data and write enable signals. Any assignment to the memory, e.g. y[i] <-- a + b;
causes the write address to be set, in this case to i, and the write data to be set, in this case to a + b, and the write enable is set to 1. An always @(posedge clock) block is generated that assigns the write data to the memory. Reads must be done by assigning to the implicitly declared memory read data signal, e.g. for

y_data = y[i];


y_data is declared by the memory declaration as either COMB, in the case of an asynchronous memory, or REGISTER, in the case of a synchronous memory.

Construct / Implementation
MEMORY [7:0] mem [0:127];
COMB [7:0] bar;
ALWAYS begin
mem_data = mem[i];
mem[j] <-- bar;
end / reg [7:0] mem[0:127] ;
reg [clogb2(127+1)-1:0] mem_wraddr,
mem_rdaddr;
reg [7:0] mem_wrdata;
wire [7:0] mem_data;
reg mem_we;
always @(posedge clk) begin
if (mem_we) begin
mem[mem_wraddr] <= mem_wrdata;
end
end
assign mem_data = mem[mem_rdaddr];
reg [7:0] bar;
always @(*) begin
//initialize memory variables
mem_we = 0;
mem_wrdata = 'bX;
mem_rdaddr = 'bX;
mem_wraddr = 'bX;
begin
begin
mem_rdaddr = i;
end
begin
mem_we = 1;
mem_wraddr = j;
mem_wrdata = bar;
end
end
end
SYNCMEMORY [7:0] mem [0:127];
COMB [7:0] bar;
ALWAYS begin
mem_data <-- mem[i];
mem[j] <-- bar;
end / reg [7:0] mem[0:127] ;
reg [clogb2(127+1)-1:0] mem_wraddr,
mem_rdaddr;
reg [7:0] mem_wrdata, mem_data;
reg mem_we;
always @(posedge clk) begin
if (mem_we) begin
mem[mem_wraddr] <= mem_wrdata;
end
mem_data <= mem[mem_rdaddr];
end
reg [7:0] bar;
always @(*) begin
//initialize memory variables
mem_we = 0;
mem_wrdata = 'bX;
mem_rdaddr = 'bX;
mem_wraddr = 'bX;
begin
begin
mem_rdaddr = i;
end
begin
mem_we = 1;
mem_wraddr = j;
mem_wrdata = bar;
end
end
end