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
| Construct | Meaning | Hardware it usually becomes |
|---|---|---|
| module ... endmodule | A block with ports, like a chip | A sub-circuit |
| wire | A connection driven continuously | A net between gates |
| reg | A variable assigned inside always or initial blocks | A flip-flop or plain logic, depending on the block |
| assign | Continuous assignment to a wire | Combinational logic |
| always @(*) | Block re-evaluated when any input changes | Combinational logic |
| always @(posedge clk) | Block executed at each rising edge | Flip-flops |
| initial | Runs once at time zero | Nothing; 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
// ---------- 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.
`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.