In short: A traffic light controller is a Moore FSM whose outputs depend only on the current state, and whose state changes are gated by a down-counter that expires after each phase's programmed duration.
Ask ten VLSI candidates to design a traffic light controller and nine will draw four circles on a whiteboard and start writing case statements. The tenth will first ask: "What is the clock frequency, what are the phase durations, and what happens at power-up?" That candidate gets the offer. This article walks through the design the way a hiring panel expects to hear it, from specification to synthesizable RTL to verification.
Why a Moore machine fits this problem
A traffic signal has a property most FSM problems lack: the outputs are purely a function of "which phase are we in". When the north-south (NS) road shows green, the east-west (EW) road shows red, full stop. No input wiggle should ever be able to glitch a lamp. That is the textbook definition of a Moore machine, where the output logic is out = f(state).
A Mealy design would let outputs react combinationally to inputs such as a pedestrian button. That can shave one clock cycle of latency, but it also means an asynchronous input can briefly produce a wrong lamp combination. For a safety-relevant output driving real lamps, a registered or state-decoded output is the safer choice. For a broader treatment of the trade-offs, see our guide to FSM design in FPGA.
Key fact: In a Moore FSM the output changes only when the state register changes, so outputs are glitch-free relative to the clock if you register them, which is exactly what you want when driving physical lamps.
The specification
Let us define a concrete four-state intersection. The board clock is 50 MHz, and the design must hold each phase for a fixed number of seconds.
| State | NS lamp | EW lamp | Duration |
|---|---|---|---|
| S_NS_GREEN | Green | Red | 30 s |
| S_NS_YELLOW | Yellow | Red | 5 s |
| S_EW_GREEN | Red | Green | 30 s |
| S_EW_YELLOW | Red | Yellow | 5 s |
Total cycle: 70 seconds. Notice that the yellow-to-green hand-off also acts as the all-red clearance in many real controllers; if your specification demands an explicit all-red interval, add two more states with a 1 or 2 second timer. The structure below absorbs that change by just adding rows to the table.
Architecture: FSM plus a timer
The cleanest way to hold a state for N seconds is not to add a dozen "wait" states. Instead, split the design into two cooperating blocks: a one-second tick generator and a phase down-counter. The FSM only sees a single flag, timer_done.
- Prescaler: counts 50,000,000 clock cycles (a 26-bit counter, since 226 = 67,108,864) and emits a one-cycle
tick_1spulse. - Phase timer: loaded with duration minus one when the state changes, decremented on every
tick_1s, and assertingtimer_donewhen it reaches zero. - State register: advances when
timer_doneandtick_1scoincide.
Using the tick as an enable instead of generating a divided clock is a key interview point. A divided clock creates a second clock domain that the timing tools must constrain and that CTS must balance. A clock-enable keeps everything on one clock, and FPGA fabrics map it directly onto the flip-flop's CE pin at no extra logic cost.
The Verilog implementation
We use the standard three-part style: a sequential state register, a combinational next-state block, and a combinational output decode. Keeping them separate makes synthesis results predictable and lets you extract the FSM cleanly in tools such as Synopsys Design Compiler or Vivado.
module traffic_light #(
parameter CLK_HZ = 50_000_000,
parameter T_GREEN = 30,
parameter T_YELLOW = 5
)(
input wire clk, rst_n,
output reg [2:0] ns_lamp, // {R,Y,G}
output reg [2:0] ew_lamp
);
localparam [1:0] NS_G=2'd0, NS_Y=2'd1, EW_G=2'd2, EW_Y=2'd3;
reg [1:0] state, next;
reg [25:0] pre;
reg [5:0] t;
wire tick = (pre == CLK_HZ-1);
wire done = (t == 0) && tick;
always @(posedge clk or negedge rst_n)
if (!rst_n) pre <= 0;
else pre <= tick ? 0 : pre + 1;
always @(posedge clk or negedge rst_n)
if (!rst_n) state <= NS_G;
else if (done) state <= next;
always @(*) case (state)
NS_G: next = NS_Y; NS_Y: next = EW_G;
EW_G: next = EW_Y; default: next = NS_G;
endcase
always @(posedge clk or negedge rst_n)
if (!rst_n) t <= T_GREEN-1;
else if (done) t <= (next==NS_Y || next==EW_Y) ? T_YELLOW-1 : T_GREEN-1;
else if (tick) t <= t - 1;
always @(*) case (state)
NS_G: begin ns_lamp=3'b001; ew_lamp=3'b100; end
NS_Y: begin ns_lamp=3'b010; ew_lamp=3'b100; end
EW_G: begin ns_lamp=3'b100; ew_lamp=3'b001; end
default: begin ns_lamp=3'b100; ew_lamp=3'b010; end
endcase
endmodule
The pattern uses non-blocking assignments in clocked blocks and blocking in combinational ones; our explainer on blocking vs non-blocking assignments covers why that rule prevents simulation races. Note that state and t are declared reg because they are assigned inside procedural blocks, a distinction explained in reg vs wire in Verilog.
Key fact: The timer must be loaded with N-1, not N, because it asserts done at zero. Loading N produces phases one second too long, a classic off-by-one bug that simulation catches only if you check actual durations.
Watch it run
Verification and the traps to avoid
You cannot simulate 70 real seconds at 50 MHz in a sensible time: that is 3.5 billion clock cycles. The professional answer is to keep CLK_HZ, T_GREEN and T_YELLOW as parameters and override them in the testbench, for example CLK_HZ=10, T_GREEN=3, T_YELLOW=2. The logic is identical, but the whole cycle fits in under 100 clock edges. Tools such as Siemens Questa, Synopsys VCS and Cadence Xcelium all handle this style. Remember to set a sensible time unit as described in the Verilog timescale directive.
Write a self-checking testbench, not a waveform you eyeball. Assert three properties on every clock:
- Mutual exclusion: NS green and EW green are never high at once, and neither is green while the other is yellow.
- Sequence: the state order is always NS_G, NS_Y, EW_G, EW_Y, with no skipping.
- Duration: measured cycles per phase equal the programmed value.
Beyond simulation, three hardware-level issues matter. First, reset: the controller must wake in a known safe state. An asynchronous assert with synchronous de-assert is the usual recommendation so that recovery timing is met. Second, illegal states: a 2-bit encoding with four states has no unused codes, but a one-hot version does, and the default branch must steer back to a safe state. Third, lamp glitches: if the lamps drive real power drivers, register the decoded outputs so a decode hazard never flashes a wrong colour.
Synthesis tools may re-encode your FSM. Vivado defaults to one-hot for small machines, while Design Compiler honours your encoding unless told otherwise. Neither choice changes functionality, but it changes gate count and timing, so check the report.
Extending the design
Once the base controller works, interviewers love follow-ups. A pedestrian request can be added by synchronising the button through two flip-flops and using it to shorten the green timer, never to jump states directly. A sensor-actuated version can add a "minimum green" state before allowing early termination. A blinking-yellow night mode is just one extra state with a toggle bit. Each of these is a table edit plus a few lines, which is the real payoff of separating the timer from the FSM. If you want to see the same discipline applied to bit-stream problems, the structure also carries over to the structural modeling style when you build the top level from separate prescaler, timer and FSM instances. Our companion piece on a sequence detector FSM approach shows the Mealy-versus-Moore trade from the other side.
If you want hands-on practice with synthesis constraints and lab tool flows, the training team at ChipXpert runs instructor-led RTL and FPGA batches, and you can explore self-paced material on our CourseTron courses page.
Key fact: Parameterise every timing constant. A design that cannot be shortened for simulation will either never be fully verified or will be verified on a different configuration than the one you ship.
Ready to go from one FSM to full FPGA design flows? Join the CourseTron FPGA and Verilog course and build, simulate and implement projects like this one step by step.
Frequently asked questions
Why use a Moore FSM instead of Mealy for a traffic light?
Moore outputs depend only on the state, so they change only on clock edges and cannot glitch from input noise. For safety-related lamp outputs this predictability outweighs the one-cycle latency a Mealy design might save.
How do I generate a one-second delay from a 50 MHz clock?
Count 50,000,000 cycles with a 26-bit counter and emit a single-cycle enable pulse. Use that pulse as a clock enable rather than creating a divided clock, which avoids extra clock domains and timing-constraint headaches.
How do I simulate a 30-second green phase quickly?
Make the clock frequency and phase durations module parameters, then override them in the testbench with tiny values. The same RTL is exercised, but the full cycle finishes in a few hundred cycles.
Should I use binary or one-hot state encoding?
For four states either works. Binary uses fewer flip-flops; one-hot gives simpler next-state logic and often better timing on FPGAs. Let the synthesis tool choose, but always give one-hot designs a safe default branch.