Traffic-light controller

A four-state Moore machine for a junction, where a timer and the side road's queue decide when green changes hands. Holding, bus outputs, a synchronous reset.

Level
rtl
Code
traffic-light-controller
Checked
Verilator 5.052, Icarus Verilog 13.0, Yosys 0.66
Published
Authorship
Drafted with AI assistance
Status
In progress — published so it can be reviewed, not finished.

A traffic-light controller is a ring of four states — main road green, main road amber, side road green, side road amber — that moves on when the current phase has lasted long enough, or, on the side road, when nobody is left waiting. It is a Moore machine with two 2-bit outputs, main and side, one per road, where 2 is green, 1 is amber and 0 is red.

Two inputs decide when. tmr is a timer's flag, high for one clock period each time the phase has run long enough. n is the number of cars queued on the side road, three bits wide: the main road only gives way when a car is waiting, and the side road gives the green back early when its queue is empty.

tmr && n != 0line 40tmrtmr || n == 0line 46tmrline 49rstlines 26–32MAIN_GOmain=2, side=0MAIN_SLOWmain=1, side=0SIDE_GOmain=0, side=2SIDE_SLOWmain=0, side=1

Run plays this scenario from the start; driving an input continues from here.

A state machine that runs: a state diagram and its waveform on one time axis. Inputs: rst, tmr, n (3 bits). Outputs: main (2 bits), side (2 bits). clk runs at 8 ticks a period. It has 4 states, MAIN_GO, MAIN_SLOW, SIDE_GO and SIDE_SLOW, and starts in MAIN_GO on reset; 4 transitions. The waveform shows clk, rst, tmr, n, state, next, main, side.

Figure 1. Press Run. The reset is synchronous, so the state is X until the first edge; then the timer moves the green from the main road to the side road.

How it works, edge by edge

rst is high from the start, but this reset is synchronous: the register only looks at it on a rising edge. Until the first edge, at tick 4, the state is X, and so are main and side, because the machine could be in any of its states. At edge 0 the register samples rst high and lands in MAIN_GO, and the main road has green.

The next row under state is the state the register will take at the next edge if nothing changes before it — the next signal in the code. At edge 0 tmr is high and one car is waiting, so next already reads MAIN_SLOW, and at edge 1 the machine gets there.

At edge 2 tmr is low. No arrow out of MAIN_SLOW is true, so the machine holds, and the status line says so once. There is no arrow drawn for that: holding is what the code's default assignment does, not a transition. At edge 3 tmr is high again and the green passes to the side road.

Full cycle runs the ring once, with the timer firing every other edge. Empty queue drops n to 0 while the side road has green; the condition on the arrow out of SIDE_GO is tmr || n == 0, so the side road gives up its green at the next edge without waiting for the timer.

The code

traffic_light.sv
// A traffic light where a main road meets a side road. The main road keeps
// green until the timer expires with a car waiting on the side road; the side
// road keeps green until the timer expires or its queue is empty.
//
// Reset style for this module: synchronous, active high. One style per page,
// stated up front, so nothing on the page has to be read twice.
module traffic_light (
    input  logic       clk,
    input  logic       rst,
    input  logic       tmr,
    input  logic [2:0] n,
    output logic [1:0] main,
    output logic [1:0] side
);

  typedef enum logic [1:0] {
    MAIN_GO   = 2'd0,
    MAIN_SLOW = 2'd1,
    SIDE_GO   = 2'd2,
    SIDE_SLOW = 2'd3
  } state_t;

  state_t state, next;

  // The state register: the only flip-flops in the module.
  always_ff @(posedge clk) begin
    if (rst) begin
      state <= MAIN_GO;
    end else begin
      state <= next;
    end
  end

  // Next state. The default assignment holds the state when no branch fires,
  // so no arm can leave a latch behind.
  always_comb begin
    next = state;
    unique case (state)
      MAIN_GO: begin
        if (tmr && n != 0) next = MAIN_SLOW;
      end
      MAIN_SLOW: begin
        if (tmr) next = SIDE_GO;
      end
      SIDE_GO: begin
        if (tmr || n == 0) next = SIDE_SLOW;
      end
      SIDE_SLOW: begin
        if (tmr) next = MAIN_GO;
      end
      default: next = MAIN_GO;
    endcase
  end

  // Moore outputs: a function of the state alone. On each road 2 is green,
  // 1 is amber and 0 is red.
  always_comb begin
    main = 2'd0;
    side = 2'd0;
    unique case (state)
      MAIN_GO: begin
        main = 2'd2;
        side = 2'd0;
      end
      MAIN_SLOW: begin
        main = 2'd1;
        side = 2'd0;
      end
      SIDE_GO: begin
        main = 2'd0;
        side = 2'd2;
      end
      SIDE_SLOW: begin
        main = 2'd0;
        side = 2'd1;
      end
      default: begin
        main = 2'd0;
        side = 2'd0;
      end
    endcase
  end

endmodule

The reset style is stated at the top: synchronous, active high, which is why the register block is always_ff @(posedge clk) alone and tests rst inside it. Each arm of the next-state case has one if and no else: the one arrow out of each state. The outputs have a block of their own that reads only the state, because no transition drives them. The FSM viewer draws a state machine of your own from its code in this form.

Synthesis result

Yosys's generic synthesis turns traffic_light.sv into 12 cells, 2 of them flip-flops. The cells are Yosys's own gate library, not an FPGA's or a standard-cell library's: the count measures the logic, not the area of any chip.

The code gives state a 2-bit encoding, and these numbers keep it. Left to its fsm pass, Yosys re-encodes it one-hot: 4 flip-flops, 28 cells in all.

Cells after synth -nofsm, by type
CellWhat it isCount
$_SDFFE_PP0P_D flip-flop on the rising edge, active-high synchronous reset to 0, active-high enable2
$_AND_2-input AND1
$_ANDNOT_AND with its second input inverted, A & ~B2
$_MUX_2-to-1 multiplexer1
$_NAND_2-input NAND1
$_NOR_2-input NOR3
$_NOT_inverter1
$_OR_2-input OR1
Total12

Lint by Verilator 5.052, the testbench by Icarus Verilog 13.0, synthesis by Yosys 0.66.

Icarus replayed this page's three scenarios on this code, and all 1,056 values it compared match. It compares from the first edge, where the synchronous reset gives the register a known state; before that, an RTL simulation reads the unknown register more optimistically than this engine.

The netlist before synthesis

The netlist of traffic_light.sv, drawn by netlistsvg 1.0.2, is 923 × 904 px, too large to read inline: open it on its own, where the browser can zoom.

Pitfalls

A hold that was meant to be an arrow. A state whose conditions are all false keeps its state, and nothing in the code or the diagram says that was the intent. The status line's sentence is there so a figure can say it at the moment it happens; in a design, a comment on the arm does the same job.

A synchronous reset needs a clock. Until the first edge after rst goes high, the lights are whatever the register powered up as. A reset that must act without a running clock is an asynchronous one, with the recovery and removal rules that come with it.

The queue length is someone else's. n comes from a counter outside this module, on the same clock. If that counter ran on another clock, n would need synchronizing before this machine read it, and a two-flop synchronizer on each of its three bits is not enough: the bits can settle at different edges, and the machine would read a count the counter never held. A count that only ever moves by one can cross in Gray code, where each step changes one bit; any other needs a handshake.

Check yourself

No state in the diagram has an arrow back to itself. What happens at an edge where no arrow's condition is true?
The machine stays where it is. The next-state block opens with next = state, so a state whose conditions are all false keeps its state; the diagram does not draw those holds as arrows, and the status line says once that the state is holding.
Why is the state X for the first four ticks, even though rst is high from the start?
Because the reset is synchronous. The register only looks at rst on a rising edge, and the first rising edge is at tick 4; until then nothing has put it in a known state.
In the Empty queue scenario the side road loses its green one period after n drops to 0, with the timer still low. Which condition does that?
tmr || n == 0 on the arrow from SIDE_GO. With the queue empty the second half is true on its own, so the machine moves to SIDE_SLOW at the next edge without waiting for the timer.

Tags rtl state-machine moore synchronous-reset

Found an error? Send a correction