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.
| State | Main road | Side road | Duration parameter |
|---|---|---|---|
| MAIN_GREEN | GREEN | RED | MAIN_GREEN_TICKS |
| MAIN_YELLOW | YELLOW | RED | MAIN_YEL_TICKS |
| SIDE_GREEN | RED | GREEN | SIDE_GREEN_TICKS |
| SIDE_YELLOW | RED | YELLOW | SIDE_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
integerparameter in ticks. Override them at instantiation to retime the intersection without touching the logic. - One timer, reused. A single down-counter
cntholds the current phase. When it reaches1the 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_nlow 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
- Deasserting reset exactly on a clock edge. If the testbench sets
rst_nhigh on the same rising edge the DUT samples it, a race can leave the state register atxforever. Hold reset across at least one edge with a time delay, as shown above. - Forgetting a default in every case. Without a
defaultbranch, an unreached state code can infer a latch or leave outputs undefined. Always cover the else. - Mixing blocking and non-blocking. Use non-blocking
<=for sequential state and counters, and blocking=only inside combinationalalways @(*)blocks. Mixing them causes simulation-versus-synthesis mismatches. - 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.
- 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.
Share your question in comments or talk to our mentor team for batch guidance.
Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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