Verilog Basics: Modules, Always Blocks and a Self-Checking Testbench

Verilog is a hardware description language (HDL). It looks a little like C, but it describes hardware: every line becomes gates, wires and flip-flops that all operate in parallel. It is used for both FPGA and ASIC design, and it is part of most ECE syllabi. In this tutorial you will write a full adder in dataflow style, build a 4-bit adder by instantiating it four times, write a clocked counter, and verify everything with testbenches that check results automatically. All code runs on the free Icarus Verilog simulator and the waveforms open in GTKWave.

Core Concepts

ConstructMeaningHardware it usually becomes
module ... endmoduleA block with ports, like a chipA sub-circuit
wireA connection driven continuouslyA net between gates
regA variable assigned inside always or initial blocksA flip-flop or plain logic, depending on the block
assignContinuous assignment to a wireCombinational logic
always @(*)Block re-evaluated when any input changesCombinational logic
always @(posedge clk)Block executed at each rising edgeFlip-flops
initialRuns once at time zeroNothing; simulation only

Two assignment operators cause most beginner bugs. Blocking assignment (=) updates immediately, like software, and is used in combinational always blocks. Non-blocking assignment (<=) schedules all updates to happen together at the end of the time step, which models how all flip-flops capture their inputs at the same clock edge. Rule of thumb: use <= in clocked blocks and = in combinational blocks, and never assign the same variable from two different always blocks.

Numbers are written as width, apostrophe, base and value, so 4'b1010 is four binary bits and 8'hFF is eight bits of hex. Values can also be x (unknown) and z (high impedance), which is why testbenches compare with !== rather than !=.

Design: Full Adder, 4-Bit Adder and Counter

design.v - full adder (dataflow), 4-bit ripple adder (structural) and 4-bit counter (behavioural)
// ---------- 1-bit full adder, dataflow style ----------
module full_adder (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (cin & (a ^ b));
endmodule

// ---------- 4-bit ripple-carry adder, structural style ----------
module adder4 (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       cin,
    output wire [3:0] sum,
    output wire       cout
);
    wire c1, c2, c3;   // internal carries

    full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c1));
    full_adder fa1 (.a(a[1]), .b(b[1]), .cin(c1),  .sum(sum[1]), .cout(c2));
    full_adder fa2 (.a(a[2]), .b(b[2]), .cin(c2),  .sum(sum[2]), .cout(c3));
    full_adder fa3 (.a(a[3]), .b(b[3]), .cin(c3),  .sum(sum[3]), .cout(cout));
endmodule

// ---------- 4-bit up counter, behavioural style ----------
module counter4 (
    input  wire       clk,
    input  wire       rst,   // synchronous, active high
    input  wire       en,    // count enable
    output reg  [3:0] q
);
    always @(posedge clk) begin
        if (rst)
            q <= 4'd0;
        else if (en)
            q <= q + 4'd1;   // wraps from 15 to 0
    end
endmodule

How the code maps to hardware

The three modules show the three modelling styles. The full adder uses dataflow style: two assign statements that a synthesis tool turns directly into XOR, AND and OR gates. The 4-bit adder uses structural style: it creates four instances of full_adder and wires them together through c1, c2 and c3, exactly as you would on a breadboard. Named port connections such as .cin(c1) are safer than positional ones because the order of ports cannot be mixed up.

The counter uses behavioural style. Because q is assigned inside always @(posedge clk), the tool infers four D flip-flops, and the expression q + 4'd1 becomes a 4-bit incrementer feeding their D inputs. The if-else chain becomes a multiplexer in front of the flip-flops: reset has priority, then enable, and when neither is active the flip-flops simply reload their present value. Note that q is declared as output reg because it is assigned in an always block; declaring a signal as reg does not by itself create a register.

Self-Checking Testbenches

A testbench is a module with no ports. It instantiates the design under test (DUT), drives its inputs, and checks its outputs. Printing values and reading them by eye does not scale, so the adder testbench below applies all 512 combinations of a, b and cin and compares against the + operator. The counter testbench generates a clock and checks the count after a known number of edges.

tb.v - exhaustive test of adder4 and a clocked test of counter4
`timescale 1ns/1ps

module tb_adder4;
    reg  [3:0] a, b;
    reg        cin;
    wire [3:0] sum;
    wire       cout;
    reg  [4:0] expected;
    integer    i, errors;

    adder4 dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));

    initial begin
        $dumpfile("adder4.vcd");
        $dumpvars(0, tb_adder4);
        errors = 0;
        for (i = 0; i < 512; i = i + 1) begin
            {cin, a, b} = i[8:0];   // 1 + 4 + 4 = 9 bits
            #10;                    // let the ripple settle
            expected = a + b + cin;
            if ({cout, sum} !== expected) begin
                $display("FAIL a=%d b=%d cin=%b got %d expected %d",
                         a, b, cin, {cout, sum}, expected);
                errors = errors + 1;
            end
        end
        $display("adder4: %0d errors", errors);
    end
endmodule

module tb_counter4;
    reg        clk = 0;
    reg        rst = 1;
    reg        en  = 0;
    wire [3:0] q;

    counter4 dut (.clk(clk), .rst(rst), .en(en), .q(q));

    always #5 clk = ~clk;          // 10 ns period, 100 MHz

    initial begin
        $dumpfile("counter4.vcd");
        $dumpvars(0, tb_counter4);
        @(negedge clk) rst = 0; en = 1;   // change inputs away from the active edge
        repeat (20) @(posedge clk);
        #1;
        if (q !== 4'd4)            // 20 counts wrap: 20 mod 16 = 4
            $display("counter4: FAIL q=%d", q);
        else
            $display("counter4: PASS q=%d", q);
        $finish;
    end
endmodule

Running the Simulation

Install Icarus Verilog and GTKWave (on Ubuntu, sudo apt install iverilog gtkwave; Windows installers are also available). Compile each testbench with the design file, choose the top module with -s, and run the result with vvp.

$ iverilog -o sim_add -s tb_adder4 design.v tb.v
$ vvp sim_add
VCD info: dumpfile adder4.vcd opened for output.
adder4: 0 errors

$ iverilog -o sim_cnt -s tb_counter4 design.v tb.v
$ vvp sim_cnt
VCD info: dumpfile counter4.vcd opened for output.
counter4: PASS q= 4

$ gtkwave counter4.vcd

In GTKWave, expand the testbench hierarchy, add clk, rst, en and q to the wave window and set q to decimal display. You will see q increment on every rising edge after reset is released. To see a failure being caught, change ^ to | in the sum equation of the full adder and rerun: the testbench lists every wrong combination.

Common Mistakes

  • Missing a signal in a hand-written sensitivity list, so simulation and synthesis disagree. Use always @(*) for combinational logic.
  • Not assigning an output in every branch of a combinational always block, which infers an unwanted latch.
  • Using = in clocked blocks, which creates order-dependent simulation results.
  • Changing testbench inputs exactly at the active clock edge, causing races. Drive inputs on the opposite edge.
  • Expecting delays like #10 in design code to synthesise. Delays are ignored by synthesis tools.

Summary

Verilog describes parallel hardware using modules, wires, regs, continuous assignments and always blocks. Dataflow, structural and behavioural styles can be mixed freely. Use non-blocking assignment for flip-flops and blocking assignment for combinational logic. Always verify with a self-checking testbench rather than inspecting waveforms by eye; an exhaustive loop for small blocks and clocked checks for sequential ones catch errors quickly. The same code can then be synthesised for an FPGA.