FPGA

Verilog Traffic Light Controller

By CourseTron Team ·  4 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    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.

    StateNS lampEW lampDuration
    S_NS_GREENGreenRed30 s
    S_NS_YELLOWYellowRed5 s
    S_EW_GREENRedGreen30 s
    S_EW_YELLOWRedYellow5 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.

    Moore state diagram: outputs live inside the states S_NS_GREEN NS=G EW=R (30 s) S_NS_YELLOW NS=Y EW=R (5 s) S_EW_GREEN NS=R EW=G (30 s) S_EW_YELLOW NS=R EW=Y (5 s) timer_done timer_done timer_done timer_done CourseTron · coursetron.com
    Each state carries its own lamp outputs; the only transition condition is the timer expiring.

    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_1s pulse.
    • Phase timer: loaded with duration minus one when the state changes, decremented on every tick_1s, and asserting timer_done when it reaches zero.
    • State register: advances when timer_done and tick_1s coincide.

    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.

    Datapath: tick, timer, state, decoder Prescaler /50,000,000 Phase timer down-counter State reg 2-bit Output decode 6 lamp bits tick done state state change reloads timer CourseTron · coursetron.com
    One clock, one enable pulse. The FSM stays tiny because the timer hides all the counting.

    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

    Animated cycle (compressed time) NS signal EW signal State: NS_G → NS_Y → EW_G → EW_Y timer bar refills at every state change CourseTron · coursetron.com
    Each second here stands for one state; in hardware the real waits are 30 s and 5 s.

    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.

    Advertisement