Traffic Light Controller Verilog Code: FSM + Testbench

A traffic light controller is one of the most popular finite state machine (FSM) exercises in digital design, and for good reason: it maps an everyday object onto a handful of states, a timer, and a clean set of outputs. In this tutorial you will write a working traffic light controller Verilog code that sequences a main road and a side road through green, yellow, and red, with every phase duration exposed as a parameter. Everything here is complete Verilog-2005 that compiles and runs under Icarus Verilog.

We start with a four-state Moore machine, then add an “all-red” clearance interval driven by an internal tick generator, and finish with a self-checking testbench that proves the two roads are never green at the same time. You can build and run each file with iverilog -g2005 -o sim file.v && vvp sim.

The design at a glance

A standard two-road intersection cycles through four phases. When one road is green or yellow, the other must stay red. We encode this as a Moore FSM: the outputs depend only on the current state, which makes the light behaviour easy to read and safe to reason about. A down-counter holds each state for the number of clock “ticks” set by its parameter, then the machine advances.

StateMain roadSide roadDuration parameter
MAIN_GREENGREENREDMAIN_GREEN_TICKS
MAIN_YELLOWYELLOWREDMAIN_YEL_TICKS
SIDE_GREENREDGREENSIDE_GREEN_TICKS
SIDE_YELLOWREDYELLOWSIDE_YEL_TICKS

The FSM: states, a timer, and outputs

The module below keeps three combinational blocks (dwell time, next state, and output decode) plus one clocked block that owns the state register and the countdown. Each light is a 2-bit code: 00 red, 01 green, 10 yellow. The testbench uses short durations so the simulation finishes in a few hundred time units.

// traffic_light.v  -  Verilog-2005
// Four-state traffic light controller for a main road and a side road.
// Durations are parameters, so you can retune the timing without rewiring.

module traffic_light #(
    parameter integer MAIN_GREEN_TICKS = 6,
    parameter integer MAIN_YEL_TICKS   = 2,
    parameter integer SIDE_GREEN_TICKS = 4,
    parameter integer SIDE_YEL_TICKS   = 2
) (
    input  wire       clk,
    input  wire       rst_n,      // active-low synchronous reset
    output reg  [1:0] main_light, // 2'b00 RED, 2'b01 GREEN, 2'b10 YELLOW
    output reg  [1:0] side_light
);

    // Light encodings
    localparam [1:0] RED = 2'b00, GREEN = 2'b01, YELLOW = 2'b10;

    // State encodings
    localparam [1:0] S_MAIN_GREEN = 2'd0,
                     S_MAIN_YEL   = 2'd1,
                     S_SIDE_GREEN = 2'd2,
                     S_SIDE_YEL   = 2'd3;

    reg [1:0]  state, next_state;
    reg [31:0] cnt;                 // down-counter of ticks left in the state

    // How many ticks the current state should last
    reg [31:0] dwell;
    always @(*) begin
        case (state)
            S_MAIN_GREEN: dwell = MAIN_GREEN_TICKS;
            S_MAIN_YEL:   dwell = MAIN_YEL_TICKS;
            S_SIDE_GREEN: dwell = SIDE_GREEN_TICKS;
            S_SIDE_YEL:   dwell = SIDE_YEL_TICKS;
            default:      dwell = MAIN_GREEN_TICKS;
        endcase
    end

    // Next-state logic: advance only when the counter has expired
    always @(*) begin
        next_state = state;
        if (cnt == 32'd1) begin
            case (state)
                S_MAIN_GREEN: next_state = S_MAIN_YEL;
                S_MAIN_YEL:   next_state = S_SIDE_GREEN;
                S_SIDE_GREEN: next_state = S_SIDE_YEL;
                S_SIDE_YEL:   next_state = S_MAIN_GREEN;
                default:      next_state = S_MAIN_GREEN;
            endcase
        end
    end

    // Sequential block: state register and tick counter
    always @(posedge clk) begin
        if (!rst_n) begin
            state <= S_MAIN_GREEN;
            cnt   <= MAIN_GREEN_TICKS;
        end else if (cnt == 32'd1) begin
            state <= next_state;
            // Preload the counter for the state we are about to enter
            case (next_state)
                S_MAIN_GREEN: cnt <= MAIN_GREEN_TICKS;
                S_MAIN_YEL:   cnt <= MAIN_YEL_TICKS;
                S_SIDE_GREEN: cnt <= SIDE_GREEN_TICKS;
                S_SIDE_YEL:   cnt <= SIDE_YEL_TICKS;
                default:      cnt <= MAIN_GREEN_TICKS;
            endcase
        end else begin
            cnt <= cnt - 32'd1;
        end
    end

    // Output logic (Moore): outputs depend only on the current state
    always @(*) begin
        case (state)
            S_MAIN_GREEN: begin main_light = GREEN;  side_light = RED;    end
            S_MAIN_YEL:   begin main_light = YELLOW; side_light = RED;    end
            S_SIDE_GREEN: begin main_light = RED;    side_light = GREEN;  end
            S_SIDE_YEL:   begin main_light = RED;    side_light = YELLOW; end
            default:      begin main_light = RED;    side_light = RED;    end
        endcase
    end

endmodule

// ---------------------------------------------------------------------------
// Testbench
// ---------------------------------------------------------------------------
module tb_traffic_light;

    reg        clk;
    reg        rst_n;
    wire [1:0] main_light, side_light;

    // Short durations keep the simulation fast
    traffic_light #(
        .MAIN_GREEN_TICKS(6),
        .MAIN_YEL_TICKS(2),
        .SIDE_GREEN_TICKS(4),
        .SIDE_YEL_TICKS(2)
    ) dut (
        .clk(clk),
        .rst_n(rst_n),
        .main_light(main_light),
        .side_light(side_light)
    );

    // 10-time-unit clock period
    initial clk = 1'b0;
    always #5 clk = ~clk;

    // Turn a 2-bit light code into a readable word
    function [47:0] decode;
        input [1:0] light;
        case (light)
            2'b00:   decode = "RED   ";
            2'b01:   decode = "GREEN ";
            2'b10:   decode = "YELLOW";
            default: decode = "??????";
        endcase
    endfunction

    // Print one line every time either light changes
    always @(main_light or side_light)
        $display("t=%0t  MAIN=%s  SIDE=%s",
                 $time, decode(main_light), decode(side_light));

    initial begin
        rst_n = 1'b0;
        #12 rst_n = 1'b1;        // hold reset across the first clock edge
        #280;                    // run through more than one full cycle
        $display("Simulation finished.");
        $finish;
    end

endmodule
t=5  MAIN=GREEN   SIDE=RED   
t=65  MAIN=YELLOW  SIDE=RED   
t=85  MAIN=RED     SIDE=GREEN 
t=125  MAIN=RED     SIDE=YELLOW
t=145  MAIN=GREEN   SIDE=RED   
t=205  MAIN=YELLOW  SIDE=RED   
t=225  MAIN=RED     SIDE=GREEN 
t=265  MAIN=RED     SIDE=YELLOW
t=285  MAIN=GREEN   SIDE=RED   
Simulation finished.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpj9l5st44/traffic_light.v:135: $finish called at 292 (1s)

How the code works

  • Parameterized durations. Each phase length is an integer parameter in ticks. Override them at instantiation to retime the intersection without touching the logic.
  • One timer, reused. A single down-counter cnt holds the current phase. When it reaches 1 the FSM moves to the next state and preloads the counter for that state in the same clock edge, so no tick is lost.
  • Moore outputs. The output block reads only state, so the lights are glitch-free and the design is easy to verify.
  • Synchronous active-low reset. On reset the machine starts in MAIN_GREEN. Note that the testbench holds rst_n low across the first rising edge using a time delay rather than deasserting exactly on the edge.

Adding an all-red clearance interval

Real intersections insert a brief phase where both roads are red, giving cars time to clear before cross-traffic starts. This version adds two all-red states and introduces an internal tick generator: a parameterized clock divider that produces a one-cycle pulse every CLKS_PER_TICK clocks. The FSM then advances on ticks, so your durations describe seconds (or whatever a tick represents) instead of raw clock cycles.

// tlc_allred.v  -  Verilog-2005
// Traffic light controller with a one-tick "all-red" clearance interval
// between phases, and an internal tick generator (clock divider) so the
// durations below are expressed in ticks rather than raw clock cycles.

module tlc_allred #(
    parameter integer CLKS_PER_TICK = 4,  // clocks that make up one "tick"
    parameter integer MAIN_GREEN     = 4,
    parameter integer MAIN_YELLOW    = 2,
    parameter integer SIDE_GREEN     = 3,
    parameter integer SIDE_YELLOW    = 2,
    parameter integer ALL_RED        = 1
) (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [1:0] main_light,
    output reg  [1:0] side_light
);

    localparam [1:0] RED = 2'b00, GREEN = 2'b01, YELLOW = 2'b10;

    localparam [2:0] S_MG = 3'd0,  // main green
                     S_MY = 3'd1,  // main yellow
                     S_AR1 = 3'd2, // all red (main -> side)
                     S_SG = 3'd3,  // side green
                     S_SY = 3'd4,  // side yellow
                     S_AR2 = 3'd5; // all red (side -> main)

    // --- Tick generator: one-clock pulse every CLKS_PER_TICK clocks ---
    reg [31:0] div;
    wire       tick = (div == CLKS_PER_TICK - 1);
    always @(posedge clk) begin
        if (!rst_n)      div <= 32'd0;
        else if (tick)   div <= 32'd0;
        else             div <= div + 32'd1;
    end

    // --- State and per-state tick counter ---
    reg [2:0]  state;
    reg [31:0] cnt;

    // Ticks the current state should last
    reg [31:0] dwell;
    always @(*) begin
        case (state)
            S_MG:  dwell = MAIN_GREEN;
            S_MY:  dwell = MAIN_YELLOW;
            S_AR1: dwell = ALL_RED;
            S_SG:  dwell = SIDE_GREEN;
            S_SY:  dwell = SIDE_YELLOW;
            S_AR2: dwell = ALL_RED;
            default: dwell = MAIN_GREEN;
        endcase
    end

    always @(posedge clk) begin
        if (!rst_n) begin
            state <= S_MG;
            cnt   <= MAIN_GREEN;
        end else if (tick) begin
            if (cnt == 32'd1) begin
                case (state)
                    S_MG:  begin state <= S_MY;  cnt <= MAIN_YELLOW; end
                    S_MY:  begin state <= S_AR1; cnt <= ALL_RED;     end
                    S_AR1: begin state <= S_SG;  cnt <= SIDE_GREEN;  end
                    S_SG:  begin state <= S_SY;  cnt <= SIDE_YELLOW; end
                    S_SY:  begin state <= S_AR2; cnt <= ALL_RED;     end
                    S_AR2: begin state <= S_MG;  cnt <= MAIN_GREEN;  end
                    default: begin state <= S_MG; cnt <= MAIN_GREEN; end
                endcase
            end else begin
                cnt <= cnt - 32'd1;
            end
        end
    end

    // Moore outputs
    always @(*) begin
        case (state)
            S_MG:  begin main_light = GREEN;  side_light = RED;    end
            S_MY:  begin main_light = YELLOW; side_light = RED;    end
            S_SG:  begin main_light = RED;    side_light = GREEN;  end
            S_SY:  begin main_light = RED;    side_light = YELLOW; end
            default: begin main_light = RED;  side_light = RED;    end // both all-red states
        endcase
    end

endmodule

// ---------------------------------------------------------------------------
// Testbench
// ---------------------------------------------------------------------------
module tb_tlc_allred;

    reg        clk;
    reg        rst_n;
    wire [1:0] main_light, side_light;

    tlc_allred #(
        .CLKS_PER_TICK(2),
        .MAIN_GREEN(4),
        .MAIN_YELLOW(2),
        .SIDE_GREEN(3),
        .SIDE_YELLOW(2),
        .ALL_RED(1)
    ) dut (
        .clk(clk),
        .rst_n(rst_n),
        .main_light(main_light),
        .side_light(side_light)
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    function [47:0] decode;
        input [1:0] light;
        case (light)
            2'b00:   decode = "RED   ";
            2'b01:   decode = "GREEN ";
            2'b10:   decode = "YELLOW";
            default: decode = "??????";
        endcase
    endfunction

    always @(main_light or side_light)
        $display("t=%0t  MAIN=%s  SIDE=%s",
                 $time, decode(main_light), decode(side_light));

    initial begin
        rst_n = 1'b0;
        #12 rst_n = 1'b1;
        #560;                 // one full cycle = 26 ticks * 2 clks * 10 = 520
        $display("Simulation finished.");
        $finish;
    end

endmodule
t=5  MAIN=GREEN   SIDE=RED   
t=85  MAIN=YELLOW  SIDE=RED   
t=125  MAIN=RED     SIDE=RED   
t=145  MAIN=RED     SIDE=GREEN 
t=205  MAIN=RED     SIDE=YELLOW
t=245  MAIN=RED     SIDE=RED   
t=265  MAIN=GREEN   SIDE=RED   
t=345  MAIN=YELLOW  SIDE=RED   
t=385  MAIN=RED     SIDE=RED   
t=405  MAIN=RED     SIDE=GREEN 
t=465  MAIN=RED     SIDE=YELLOW
t=505  MAIN=RED     SIDE=RED   
t=525  MAIN=GREEN   SIDE=RED   
Simulation finished.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpwhsr8aag/tlc_allred.v:135: $finish called at 572 (1s)

In the log you will see both lights go RED between the main and side phases, which is exactly the clearance interval we wanted.

Proving it is safe with a self-checking testbench

The single most important property of a traffic light controller is that two conflicting roads are never green together. Instead of reading the log by eye, let the testbench assert it. The checker samples the outputs on every clock edge and counts conflicts; at the end it prints PASS or FAIL.

// tlc_checked.v  -  Verilog-2005
// A compact traffic light FSM plus a SELF-CHECKING testbench that proves the
// most important safety property: the two roads must never be green together.

module tlc_checked #(
    parameter integer MAIN_GREEN  = 3,
    parameter integer MAIN_YELLOW = 1,
    parameter integer SIDE_GREEN  = 3,
    parameter integer SIDE_YELLOW = 1
) (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [1:0] main_light,
    output reg  [1:0] side_light
);
    localparam [1:0] RED = 2'b00, GREEN = 2'b01, YELLOW = 2'b10;
    localparam [1:0] S_MG = 2'd0, S_MY = 2'd1, S_SG = 2'd2, S_SY = 2'd3;

    reg [1:0]  state;
    reg [31:0] cnt;

    always @(posedge clk) begin
        if (!rst_n) begin
            state <= S_MG; cnt <= MAIN_GREEN;
        end else if (cnt == 32'd1) begin
            case (state)
                S_MG: begin state <= S_MY; cnt <= MAIN_YELLOW; end
                S_MY: begin state <= S_SG; cnt <= SIDE_GREEN;  end
                S_SG: begin state <= S_SY; cnt <= SIDE_YELLOW; end
                S_SY: begin state <= S_MG; cnt <= MAIN_GREEN;  end
                default: begin state <= S_MG; cnt <= MAIN_GREEN; end
            endcase
        end else begin
            cnt <= cnt - 32'd1;
        end
    end

    always @(*) begin
        case (state)
            S_MG: begin main_light = GREEN;  side_light = RED;    end
            S_MY: begin main_light = YELLOW; side_light = RED;    end
            S_SG: begin main_light = RED;    side_light = GREEN;  end
            S_SY: begin main_light = RED;    side_light = YELLOW; end
            default: begin main_light = RED; side_light = RED;    end
        endcase
    end
endmodule

// ---------------------------------------------------------------------------
// Self-checking testbench
// ---------------------------------------------------------------------------
module tb_tlc_checked;
    reg        clk, rst_n;
    wire [1:0] main_light, side_light;
    integer    errors = 0;

    localparam [1:0] GREEN = 2'b01;

    tlc_checked dut (
        .clk(clk), .rst_n(rst_n),
        .main_light(main_light), .side_light(side_light)
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    // Safety check: after reset, both roads must never show green together
    always @(posedge clk) begin
        if (rst_n && main_light == GREEN && side_light == GREEN) begin
            errors = errors + 1;
            $display("t=%0t  ERROR: both roads green!", $time);
        end
    end

    initial begin
        rst_n = 1'b0;
        #12 rst_n = 1'b1;
        #400;                    // several full cycles
        if (errors == 0)
            $display("PASS: the two roads were never green at the same time.");
        else
            $display("FAIL: %0d conflicting-green events detected.", errors);
        $finish;
    end
endmodule
PASS: the two roads were never green at the same time.
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpuuiu7pf3/tlc_checked.v:83: $finish called at 412 (1s)

Common mistakes

  1. Deasserting reset exactly on a clock edge. If the testbench sets rst_n high on the same rising edge the DUT samples it, a race can leave the state register at x forever. Hold reset across at least one edge with a time delay, as shown above.
  2. Forgetting a default in every case. Without a default branch, an unreached state code can infer a latch or leave outputs undefined. Always cover the else.
  3. Mixing blocking and non-blocking. Use non-blocking <= for sequential state and counters, and blocking = only inside combinational always @(*) blocks. Mixing them causes simulation-versus-synthesis mismatches.
  4. No yellow phase. Jumping straight from green to the opposite green removes the safety margin. Keep a yellow (and ideally an all-red) between conflicting greens.
  5. Off-by-one timers. Decide whether a duration of N means N ticks or N+1. Here the counter transitions when it hits 1, so a parameter of N holds the state for exactly N ticks.

Quick check

Why is this controller modelled as a Moore machine rather than a Mealy machine?

Because the lights depend only on the current state, not on any external input. Moore outputs are stable for the whole phase and glitch-free, which is exactly what a traffic light needs.

What does the tick generator in the second example buy you?

It divides the fast system clock into slower ticks, so phase durations can be written in human units (such as seconds) instead of raw clock cycles. Change CLKS_PER_TICK and all phase timings scale together.

Why add an all-red state between the main and side phases?

It gives vehicles already in the intersection time to clear before cross-traffic gets a green, preventing collisions during the hand-off between roads.

How does the self-checking testbench decide PASS or FAIL?

On every clock edge after reset it checks whether both main_light and side_light are green. It counts any such conflict; if the count is zero at the end it prints PASS, otherwise FAIL with the number of violations.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD