Menu

Your First Verilog Module: A Step-by-Step Walkthrough

Write your first complete Verilog module from scratch - declaration, ports, a piece of combinational logic, and a testbench that drives it. Runnable in the browser.

This page includes runnable editors - edit, run, and see output instantly.

A Module Is the Unit of Verilog

Everything in Verilog lives inside a module. A module wraps up a piece of circuit: it declares what signals come in, what signals go out, and what wires/registers/logic sit between them. Every chip you've ever seen is a tree of modules instantiating other modules, all the way down to vendor-supplied gate primitives.

The shape is always the same:

module name(port_list);
    // declarations: wire, reg, parameter
    // body: assigns, instantiations, always blocks
endmodule

We'll build up to a complete module in stages.

The Smallest Useful Module: A Two-Input AND Gate

We need something simple and self-contained. A two-input AND gate is perfect: two inputs, one output, one line of logic.

Press Run. You should see the four rows of an AND truth table. Let's go through every piece.

Reading the Module Declaration

module and_gate(
    input  wire a,
    input  wire b,
    output wire y
);
  • module and_gate declares a module named and_gate. The name is how other modules will instantiate it.
  • The parenthesized list is the port list - the signals visible from outside.
  • input wire a - a is an input port; it's a wire (driven from outside).
  • output wire y - y is an output port driven by something inside the module.

If you wanted to be terse you could write input a instead of input wire a - direction alone makes the default type wire. But being explicit is a habit worth forming. Module Ports covers the full set of port shapes.

The Body

assign y = a & b;

That's a continuous assignment. It says "whenever a or b changes, recompute y as the bitwise AND of the two." There's no clock, no timing - the relationship is always true. That's pure combinational logic.

endmodule closes the block. The module is done.

The Testbench

You can't run and_gate alone. You need a second module that drives its inputs and watches its outputs. That's the testbench, and by convention we call it test, tb, or <design>_tb.

module test;
    reg  a, b;
    wire y;

    and_gate dut(.a(a), .b(b), .y(y));
    ...
endmodule

Three things to notice:

  • No port list. A testbench is the top of the simulation - nothing outside it.
  • reg a, b and wire y. Inputs to the design under test (DUT) are reg in the testbench because we're driving them from a procedural block. The output is a wire because the DUT drives it.
  • and_gate dut(.a(a), .b(b), .y(y)). This is instantiation. We're stamping out a copy of and_gate and calling it dut (a common name - "design under test"). The .a(a) syntax says "connect the port named a on the instance to the local signal named a." Module Instantiation goes deeper.

The Stimulus

initial begin
    a = 0; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 0; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 1; b = 0; #1 $display("a=%b b=%b y=%b", a, b, y);
    a = 1; b = 1; #1 $display("a=%b b=%b y=%b", a, b, y);
    $finish;
end

The initial block runs once at simulation start. Inside it:

  • a = 0; b = 0; drives the inputs. These are blocking assignments - the order matters, and each one happens before the next.
  • #1 advances simulated time by 1 unit. We need this so y has time to settle after the input changes. Without the #1, $display would print the old value of y.
  • $display(...) prints to the simulation console. The format string works like C's printf: %b is binary, %d is decimal, %h is hex, %t is simulation time.
  • $finish ends the simulation. Without it the simulator would keep advancing time forever waiting for an event that never comes.

Try Breaking It

Modify the module to be an OR gate (change & to |) and re-run. The truth table flips. Now try a XOR:

Same skeleton. Different operator. That's the whole game for combinational logic - declare ports, write assigns, sweep inputs, observe outputs.

A Two-Output Module

Modules can have more than one output. Here's a half-adder - two inputs, sum and carry-out:

The two assign statements live next to each other but happen in parallel - there's no "first compute sum then compute carry." Both are always true. That's the concurrency we talked about in Hardware vs Software, made concrete.

What You Now Know

You've seen the entire skeleton of a Verilog file: a design module with declared ports and a body, plus a testbench module that instantiates it, drives its inputs in an initial block, and reports results. Almost every Verilog source file you'll ever read fits this template. The rest of the language is just filling in richer logic, multi-bit signals, clocked behavior, and bigger testbenches.

Up next: comments and code style, so your modules stay readable as they grow.

Frequently Asked Questions

What is the simplest Verilog module?

The smallest legal Verilog module is just module name; endmodule - no ports, no body. The smallest useful one is a one-output module: module and_gate(input wire a, input wire b, output wire y); assign y = a & b; endmodule. That's a real piece of combinational logic you can drop into any larger design.

How do I run a Verilog module?

You can't run a module on its own - it's a circuit description, not a program. You write a testbench module that instantiates your design and drives its inputs, then compile both with iverilog -o sim design.v test.v and run vvp sim. The browser editor on this page does both steps for you when you press Run.

What is a testbench in Verilog?

A testbench is a second module - usually with no ports - whose job is to exercise your design. It instantiates the design, wiggles its inputs through an initial block, observes the outputs with $display or $monitor, and calls $finish when done. Testbenches are not synthesizable; they exist only to verify behavior.

Why does my Verilog code need $finish?

Because hardware never stops. A simulator pretends time is passing, and without an explicit $finish it would keep advancing forever waiting for new events. $finish tells the simulator 'we're done, exit cleanly'. In a testbench it's the last line of the initial block - run the test, then finish.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED