Module 4: Introduction to Verilog HDL ยท 4.2 Verilog Language Basics

Vectors and Arrays

A one-bit signal is a wire. Most of a design moves buses: a 32-bit address, an 8-bit byte, a 16-bit instruction. In Verilog a bus is a vector, and slicing, joining and indexing vectors is most of what bus-level RTL consists of.

Declaring vectors

A vector is a wire or reg with a range, most significant index first:

`timescale 1ns / 1ps
`default_nettype none

module vector_declarations (
    input  wire [31:0] data_in,
    output reg  [15:0] data_out
);

    wire [7:0]  byte_a;
    wire [15:0] halfword;
    wire [63:0] doubleword;
    reg  [3:0]  nibble;

    assign byte_a     = data_in[7:0];
    assign halfword   = data_in[15:0];
    assign doubleword = {32'h0, data_in};   // zero-extend to 64 bits

    always @(*) begin
        data_out = data_in[31:16];
    end

endmodule

The width is MSB minus LSB plus one, so [7:0] is 8 bits and [8:0] is 9. Writing [8:0] for a byte is a classic mistake; count the indices. Always write ranges descending, [N-1:0]. Ascending ranges like [0:7] are legal, put the most significant bit at index 0, and cause confusion whenever they meet a descending one.

Selecting bits

A single bit by index:

wire [31:0] word;
assign msb  = word[31];
assign lsb  = word[0];
assign bit7 = word[7];

The index can be a variable, and synthesis handles that as a mux.

A part-select with constant bounds:

assign byte3       = word[31:24];
assign byte0       = word[7:0];
assign nibble_high = word[7:4];
assign upper_half  = word[31:16];

Both bounds must be constants. When the position depends on a signal, for instance picking byte N of a word, Verilog-2001 has the indexed part-select:

wire [31:0] word;
wire [1:0]  byte_sel;
wire [7:0]  selected_byte;

assign selected_byte = word[byte_sel*8 +: 8];   // 8 bits starting at bit byte_sel*8

x[base +: width] takes width bits upward from base, that is x[base+width-1 : base]; x[base -: width] takes them downward, x[base : base-width+1]. The width is a constant, the base can be anything. This is the tool for pulling lanes out of a wide bus:

`timescale 1ns / 1ps
`default_nettype none

module byte_extractor (
    input  wire [31:0] word,
    input  wire [1:0]  byte_sel,   // 0 is the least significant byte
    output wire [7:0]  byte_out
);

    assign byte_out = word[byte_sel*8 +: 8];

endmodule

Concatenation

Braces join signals and literals into a wider vector, most significant first:

wire [3:0] high_nibble, low_nibble;
wire [7:0] full_byte;

assign full_byte = {high_nibble, low_nibble};   // [7:4] and [3:0]

The result is as wide as the sum of the parts. Literals are fine inside it, which is how you widen a value to keep a carry:

wire [7:0] a;
wire [8:0] sum_no_overflow;

assign sum_no_overflow = {1'b0, a} + {1'b0, 8'd200};   // 9-bit add

And it is the natural way to write a shift register:

always @(posedge clk) begin
    shift_reg <= {shift_reg[6:0], data_in};   // shift left, new bit at the bottom
end

Replication

{N{expr}} repeats an expression N times, inside an outer set of braces. Its main job is sign extension:

wire [7:0]  byte_val;
wire [31:0] sign_extended;

assign sign_extended = {{24{byte_val[7]}}, byte_val};
// 8'b1000_0000, which is -128, becomes 32'hFFFFFF80

wire [15:0] pattern;
assign pattern = {4{4'hA}};   // 16'hAAAA

$signed() will make the synthesiser extend the sign for you in arithmetic, but writing it out with replication is clearer to whoever reviews the code.

Arrays

An array is declared with a second range after the name:

reg [7:0]  memory   [0:255];   // 256 entries of 8 bits
reg [31:0] reg_file [0:31];    // 32 registers of 32 bits

The range before the name is the width of each element, the packed dimension. The range after it is the number of elements, the unpacked dimension. You index the unpacked dimension first:

`timescale 1ns / 1ps
`default_nettype none

module memory_model (
    input  wire       clk,
    input  wire       we,
    input  wire [7:0] addr,
    input  wire [7:0] data_in,
    output reg  [7:0] data_out
);

    reg [7:0] mem [0:255];

    always @(posedge clk) begin
        if (we)
            mem[addr] <= data_in;
        data_out <= mem[addr];    // registered read
    end

endmodule

A whole array cannot be used in an expression; you index an element first, and then you can select bits of it:

reg [7:0] arr [0:3];
assign foo = arr;         // illegal
assign foo = arr[2];      // an 8-bit element
assign bar = arr[1][3];   // bit 3 of element 1

A ROM from a file

$readmemh fills an array from a text file of hex values, one per line, at the start of simulation:

`timescale 1ns / 1ps
`default_nettype none

module rom_256x8 (
    input  wire [7:0] addr,
    output reg  [7:0] data
);

    reg [7:0] rom_mem [0:255];

    initial begin
        $readmemh("rom_contents.hex", rom_mem);
    end

    always @(*) begin
        data = rom_mem[addr];
    end

endmodule

$readmemb is the binary version. This is the one initial block that belongs in a design file: the FPGA tools recognise an array with an initial $readmemh as a block RAM with initial contents and build exactly that.

Practice Exercise

With a 32-bit bus axi_data and a 2-bit sel: extract byte 2 with a static part-select; extract the byte chosen by sel; swap the two 16-bit halves; and sign-extend byte 0 to 32 bits.

Solution
`timescale 1ns / 1ps
`default_nettype none

module axi_manipulations (
    input  wire [31:0] axi_data,
    input  wire [1:0]  sel,
    output wire [7:0]  byte2_static,
    output wire [7:0]  byte_dynamic,
    output wire [31:0] swapped,
    output wire [31:0] sign_ext
);

    assign byte2_static = axi_data[23:16];
    assign byte_dynamic = axi_data[sel*8 +: 8];
    assign swapped      = {axi_data[15:0], axi_data[31:16]};
    assign sign_ext     = {{24{axi_data[7]}}, axi_data[7:0]};

endmodule

Vectors handle any width you write down. The next article is about not writing the width down at all: parameters, which let one module serve at 8, 16 and 32 bits from a single source file.

โ† wire and reg Parameters โ†’