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.
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
// 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
endmoduleThe 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.
| Cell | What it is | Count |
|---|---|---|
$_SDFFE_PP0P_ | D flip-flop on the rising edge, active-high synchronous reset to 0, active-high enable | 2 |
$_AND_ | 2-input AND | 1 |
$_ANDNOT_ | AND with its second input inverted, A & ~B | 2 |
$_MUX_ | 2-to-1 multiplexer | 1 |
$_NAND_ | 2-input NAND | 1 |
$_NOR_ | 2-input NOR | 3 |
$_NOT_ | inverter | 1 |
$_OR_ | 2-input OR | 1 |
| Total | 12 | |
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.