Design Challenge: Traffic Light Controller — A Complete Finite-State Machine

Design a complete, synchronous finite-state machine that cycles RED to GREEN to YELLOW to RED with a clocked state register, next-state logic, output logic, and an asynchronous reset — the canonical FSM design exercise that every sequential-logic project in real digital design descends from.

The spec asks for exactly what a real traffic light controller needs: a finite-state machine cycling RED → GREEN → YELLOW → RED, built from a clock, a state register, next-state logic, output logic, and a reset. This is the textbook Moore-machine design problem — the FSM style where outputs depend only on the current state, not on any input — and this project walks it end to end: choosing a state encoding, drawing the state diagram, deriving next-state and output logic tables, minimizing them into equations or a case-statement HDL description, and — critically — deciding how long each state lasts, since 'RED → GREEN → YELLOW → RED' says nothing about timing, and a real controller must hold GREEN far longer than YELLOW. This lesson treats timing as a first-class design decision, not an afterthought.

1. Requirements and Specification

Inputs: CLOCK (a periodic timing signal — this design uses a 1 Hz tick, i.e. one clock pulse per second, produced by a frequency divider from a faster oscillator) and RESET (asynchronous, forces the FSM back to a known starting state). Output: a 3-bit one-hot LIGHT[2:0] = {RED, YELLOW, GREEN}, where exactly one bit is 1 at any time (this is a Moore machine — the output depends only on which state the FSM is currently in).

Since the spec gives the state sequence but not timing, this design pins down realistic durations expressed in clock ticks at 1 Hz: RED for 5 ticks (5 seconds), GREEN for 4 ticks, YELLOW for 2 ticks — chosen to reflect a real intersection (long stop, shorter go, short caution) while keeping the tick counts small enough to hand-verify. The FSM therefore needs more than 3 raw states — it needs a way to count 'how many ticks remain in this state,' which is the actual engineering content behind an apparently simple 3-state cycle.

2. High-Level Architecture

Three blocks, following the spec's explicit list: a State Register (flip-flops holding the current state — this design uses a combined 'color state' (2 bits: RED/GREEN/YELLOW) plus a small down-counter (3 bits, enough to count down from 5) tracking ticks remaining in the current color); Next-State Logic (combinational logic computing the next color state and next counter value from the current state and counter, on every clock tick); and Output Logic (combinational logic mapping the current color state directly to the LIGHT[2:0] one-hot output — trivial once the color state is known, since this is a Moore machine).

This 'state + duration counter' architecture is the standard way to build any FSM where states must last a specific number of clock cycles rather than exactly one cycle each — a bare 3-state FSM (RED→GREEN→YELLOW→RED, one tick per state) would cycle a full light change every 3 seconds, which is unrealistic; embedding a countdown inside each state is what lets GREEN legitimately last longer than YELLOW using the exact same state machine structure.

3. Detailed Design: State Diagram and Logic

Color state encoding: 00 = RED, 01 = GREEN, 10 = YELLOW (2 bits, one unused code 11 reserved/treated as an error state that RESET recovers from). State transition: while counter > 0, decrement counter and hold the same color; when counter reaches 0, move to the next color in sequence and reload the counter with that new color's duration (RED loads 5, GREEN loads 4, YELLOW loads 2).

Next-state logic (per tick): if RESET, next_color = RED (00), next_counter = 5. Else if counter != 0, next_color = current color (unchanged), next_counter = counter − 1. Else (counter == 0): if current color = RED, next_color = GREEN, next_counter = 4; if current color = GREEN, next_color = YELLOW, next_counter = 2; if current color = YELLOW, next_color = RED, next_counter = 5.

Output logic (Moore — depends only on color state, not counter): LIGHT = 100 (RED bit set) when color=00; LIGHT = 001 (GREEN bit set) when color=01; LIGHT = 010 (YELLOW bit set) when color=10. Because this is a Moore machine, the output is guaranteed glitch-free between clock edges — it only ever changes synchronously with the state register, never combinationally mid-cycle, which is an important safety property for a real traffic light (you never want two lights flickering on simultaneously due to a logic hazard).

4. Testing and Verification

Simulate at least two full RED→GREEN→YELLOW→RED cycles (22 clock ticks: 5+4+2+5+4+2) and confirm the LIGHT output holds each color for exactly its specified tick count, with the color-to-color transitions landing on exactly the right tick boundaries (e.g., LIGHT should switch from RED to GREEN precisely on the 6th tick after entering RED, never on the 5th or 7th — an off-by-one counter reload is the single most common bug in this design).

Test RESET at multiple different points in the cycle — mid-RED, mid-GREEN, mid-YELLOW, and exactly on a color-transition tick — and confirm in every case the FSM returns immediately to RED with a freshly-loaded 5-tick counter, not a partially-decremented one. Also explicitly test the unused counter/state combinations your encoding allows (e.g., color=11) to confirm the design recovers safely rather than locking up or producing an undefined LIGHT pattern — a real safety-relevant design should never allow a state where more than one light bit is asserted or where all light bits are 0.

State Encoding and Duration Table

The counter width (3 bits) must be able to hold the largest duration (5), so 3 bits (0-7 range) is the minimum safe width. One-hot output encoding (exactly one bit set) makes it trivially easy to verify in simulation that no two lights are ever on simultaneously — check LIGHT[2]+LIGHT[1]+LIGHT[0] == 1 at every tick.

Color state encoding (2 bits) + duration in clock ticks:
  00 = RED     -> 5 ticks -> next: GREEN  (counter reload = 5)
  01 = GREEN   -> 4 ticks -> next: YELLOW (counter reload = 4)
  10 = YELLOW  -> 2 ticks -> next: RED    (counter reload = 2)
  11 = (unused/error; RESET recovers to RED)

Output logic (Moore, depends on color state only):
  color=00 (RED)    -> LIGHT[2:0] = 100
  color=01 (GREEN)  -> LIGHT[2:0] = 001
  color=10 (YELLOW) -> LIGHT[2:0] = 010

Code Examples

Notice the exact tick where each transition occurs: the FSM decrements the counter WHILE still showing the old color, and only changes color (and reloads the NEW color's counter) on the tick where the OLD counter would have gone to -1. This 'counter reaches 0, then next tick both color and counter change together' behavior is the precise design decision that an off-by-one bug most often gets wrong — some designs mistakenly change color the same tick the counter hits 0, giving RED only 5 ticks total instead of the intended pattern, or GREEN only 3 instead of 4. Expected result: Full period = 5 + 4 + 2 = 11 ticks; two full periods = 22 ticks matches the test plan above.

6 GREEN 3 001 <- transition happened between tick 5 and 6 Two separate always blocks, by design: the first (clocked) block is the state register plus next-state logic combined; the second (combinational, always @(*)) is the pure Moore output logic, which depends only on color and never directly on counter or the clock — keeping the output glitch-free between edges.

end else if (counter != 0) begin\n counter <= counter - 1; // hold color, count down

Common mistake: A common bug is checking counter == 0 to trigger the transition in the SAME clause that also decrements counter, causing the transition to fire one tick early or the last state duration to be off by one — keeping the 'still counting down' and 'time to transition' branches as strictly separate cases (as above) avoids this.

tick  color   counter(after)  LIGHT
 1    RED     4               100
 2    RED     3               100
 3    RED     2               100
 4    RED     1               100
 5    RED     0               100   <- counter hits 0 this tick, LIGHT still RED
 6    GREEN   3               001   <- transition happened between tick 5 and 6
 7    GREEN   2               001
 8    GREEN   1               001
 9    GREEN   0               001
10    YELLOW  1               010   <- transition happened between tick 9 and 10
11    YELLOW  0               010
12    RED     4               100   <- back to RED, cycle repeats (5+4+2=11 tick period)
...
module traffic_light (
  input clk, reset,
  output reg [2:0] light   // {RED, YELLOW, GREEN}
);
  reg [1:0] color;          // 00=RED 01=GREEN 10=YELLOW
  reg [2:0] counter;

  always @(posedge clk or posedge reset) begin
    if (reset) begin
      color   <= 2'b00;
      counter <= 3'd5;
    end else if (counter != 0) begin
      counter <= counter - 1;      // hold color, count down
    end else begin
      case (color)
        2'b00: begin color <= 2'b01; counter <= 3'd4; end // RED -> GREEN
        2'b01: begin color <= 2'b10; counter <= 3'd2; end // GREEN -> YELLOW
        2'b10: begin color <= 2'b00; counter <= 3'd5; end // YELLOW -> RED
        default: begin color <= 2'b00; counter <= 3'd5; end // recover from 11
      endcase
    end
  end

  always @(*) begin
    case (color)
      2'b00: light = 3'b100; // RED
      2'b01: light = 3'b001; // GREEN
      2'b10: light = 3'b010; // YELLOW
      default: light = 3'b000;
    endcase
  end
endmodule

Traffic Light FSM State Diagram

Each state loops on itself (counting down) until its duration expires, then advances to the next state and reloads a new duration — RESET can force RED from any point in the cycle.

RESET (async, from any state)
     .-------------------------------.
     |                               |
     v                               |
  +-------+   counter=0    +-------+ |  counter=0   +--------+
  |  RED  |--------------->| GREEN |----------------| YELLOW |
  | (5tk) |                | (4tk) |                | (2tk)  |
  +---^---+                +-------+                +----+---+
      |                                                   |
      |                    counter=0                      |
      +---------------------------------------------------+

Important Rules

Output logic must depend only on the current color state (Moore machine), never on the counter value or directly on inputs, so LIGHT changes only synchronously with the state register.

Exactly one bit of LIGHT[2:0] must be asserted at all times — never two lights simultaneously, never zero.

RESET must be asynchronous and must reload both the color state AND the counter to their RED/5 starting values together, never one without the other.

The unused state encoding (11) must be handled explicitly (recovering to RED), not left as an unreachable 'don't care' that could lock up the FSM if reached by a transient glitch.

Real Intersection Controllers Use Exactly This Structure

Real traffic signal controllers are built from precisely this pattern — a state register for the current phase, a countdown timer for phase duration (often adjustable via DIP switches or software configuration for different intersections), and output logic driving the actual lamp relays — with additional real-world features layered on top such as pedestrian-crossing states, left-turn arrow phases, and vehicle-sensor inputs that can extend or shorten a phase. The Moore-machine discipline of 'outputs only change synchronously with state' is also a genuine safety requirement in real signal hardware: it is what guarantees the lamps never show two conflicting colors during a transition, which regulatory traffic-signal standards explicitly require.

Building output logic that reads the counter value directly (e.g., 'LIGHT = RED if counter > 2 else GREEN'), mixing timing logic into the output stage.

Keep output logic purely a function of the color state register (a 3-way case/mux on color alone); let the separate next-state logic own all counter/timing decisions.

Mixing counter comparisons into the output logic couples two concerns that should be independent, makes the output combinationally dependent on more signals than necessary (increasing glitch risk since the counter changes every tick), and makes it much harder to later change a state's duration without also having to re-derive the output equations.

Reloading the counter to a fixed value but forgetting to also transition the color state (or vice versa) in the same clocked always-block branch.

Always update color and counter together, in the same branch of the same clocked process, whenever a state transition occurs.

If color and counter are updated in separate, unsynchronized processes, a race condition can leave them briefly inconsistent (e.g., color has advanced to GREEN but counter still holds RED's old countdown value), corrupting the timing of every subsequent state.

Why does this FSM need a separate down-counter in addition to the 3-value color state, instead of just cycling RED->GREEN->YELLOW->RED once per clock tick? A. The counter is only needed to make the simulation waveform easier to read B. A bare 3-state FSM would spend exactly one clock tick in each color, making every light duration identical; the counter lets each state last a different, configurable number of ticks (5 for RED, 4 for GREEN, 2 for YELLOW) using the same state machine C. Verilog requires a counter in every FSM by language rule D. The counter is what generates the clock signal itself

B. A bare 3-state FSM would spend exactly one clock tick in each color, making every light duration identical; the counter lets each state last a different, configurable number of ticks (5 for RED, 4 for GREEN, 2 for YELLOW) using the same state machine — Without a counter, a synchronous FSM with N states advances one state per clock edge, giving every state identical duration (one clock period). Real traffic lights need unequal durations (long RED, medium GREEN, short YELLOW), which requires each state to 'loop on itself' for a configurable number of ticks before advancing — exactly what the per-state down-counter provides.

Why is this design specifically implemented as a Moore machine (output depends only on current state) rather than a Mealy machine (output also depends on current inputs)? A. Moore machines are always faster to simulate B. A Moore output changes only synchronously with the clocked state register, so LIGHT is guaranteed to be glitch-free and stable between clock edges — an important safety property for a real traffic light, where a momentary combinational glitch showing two lights at once would be unacceptable C. Mealy machines cannot represent cyclic behavior like RED->GREEN->YELLOW->RED D. Moore machines don't require a clock signal at all

B. A Moore output changes only synchronously with the clocked state register, so LIGHT is guaranteed to be glitch-free and stable between clock edges — an important safety property for a real traffic light, where a momentary combinational glitch showing two lights at once would be unacceptable — A Mealy machine's output can change asynchronously in response to input changes, between clock edges, which risks brief output glitches if inputs are noisy or combinational logic has hazards. A Moore machine's output is purely a function of the registered state, so it only ever updates synchronously with the clock — for a safety-relevant output like traffic lights, that glitch-free guarantee is worth the (here, negligible) extra complexity.

A design changes the color state on the SAME clock tick that the counter is decremented to 0 (rather than waiting one more tick), while also reloading the counter with the wrong new value on that same edge. What symptom would you most likely observe in simulation? A. No observable difference from a correct design B. The affected state's total duration is off by one tick (the FSM leaves that color one tick earlier or later than the specified duration), which the worked trace's tick-by-tick verification is specifically designed to catch C. The FSM output would become permanently stuck at 000 D. RESET would stop working entirely

B. The affected state's total duration is off by one tick (the FSM leaves that color one tick earlier or later than the specified duration), which the worked trace's tick-by-tick verification is specifically designed to catch — An off-by-one error in exactly when the counter-reaches-0 condition triggers the color transition (and the corresponding reload value) shows up as that state's LIGHT output lasting one tick more or fewer than specified — which is precisely why the testing section calls for tracing tick-by-tick and checking that transitions land on the exact expected tick boundary, not just checking that the right sequence of colors eventually appears.

Add a pedestrian 'WALK request' input button that, when pressed during the RED phase, extends RED by 3 extra ticks (giving pedestrians more crossing time) but has no effect if pressed during GREEN or YELLOW.

Add a walk_req flip-flop set by the button press and cleared once consumed; in the RED-state transition branch (when counter reaches 0), check walk_req: if set, reload counter with 3 (extra ticks) instead of transitioning to GREEN, and clear walk_req; if not set, transition to GREEN as before. This effectively inserts one extra 'RED-extended' sub-state without needing a new color encoding, since it's still color=RED with a re-triggered counter.

You'll need one more flip-flop to latch the WALK request (so a brief button press isn't missed if it happens between clock edges), and you'll need to modify the RED counter-reload logic to check that latched request only when a RED→GREEN transition is about to happen.