← All projects

A traffic light controller that waits for cars

A six-state machine in Verilog for a highway crossing a farm road. It only gives the farm road a green when a car is waiting, and it runs on a real FPGA. Below it: my ARMv8 and datapath work from Computer Architecture.

When
December 2025
Hardware
Digilent Zybo Z7-10, 50 MHz clock
Tools
Verilog, Vivado, testbenches
My role
Individual lab work

Try it

Press play, then flip the farm-road sensor and watch how the controller responds. Time runs 6× faster than on the board.

S0all red S1highway green S2highway yellow S3all red S4farm green S5farm yellow ≥30 s and car car gone ≥15 s or 30 s max

The simulator shows the behavior I designed for. The yellow and all-red times are shortened for the demo. See what I'd change for one place where my RTL didn't fully match this behavior.

How it's built

Digilent Zybo Z7-10 FPGA board
The Zybo Z7-10 board the controller runs on. Pushbuttons serve as reset and the farm-road sensor, and LEDs show the lights.

The design has two levels. tlc_controller is the top level: it owns the 31-bit timer counter, synchronizes the reset button, and exposes debug signals. Inside it, tlc_fsm decides the state and the light outputs from the timer value and the farm-road sensor.

  • Two-block FSM style. Next-state and output logic live in a combinational always @(*) block with blocking assignments. The state register updates on the clock edge with non-blocking assignments. Keeping them apart avoids simulation races and makes the hardware intent obvious.
  • Timer reset handshake. When the FSM changes state, it raises RstCount for one cycle, and the controller zeroes the counter. Each state measures its own duration from zero, as a multiple of the 50 MHz clock (50,000,000 cycles is one second).
  • Reset synchronizer. The reset is a physical push button, asynchronous to the clock, so it passes through a two-flip-flop synchronizer before it reaches the state register.
  • Debug pins. The current state and the counter-reset pulse are routed to Pmod header JB, so I could watch the FSM from outside the chip while testing on the board.
Hand-drawn state transition diagram for states S0 to S5 with transition conditions
My working state diagram, drawn before writing any RTL. Each arc is labeled with its transition condition and the counter-reset action.

The RTL

The two states that make it sensor-aware:

S1 : begin                       // highway green
    highwaySignal = Grn; farmSignal = Red;
    if (Count == D1 && farmSensor == 1) begin
        RstCount = 1; nextState = S2;  // minimum green served, car waiting
    end
    else begin RstCount = 0; nextState = S1; end
end

S4 : begin                       // farm green
    highwaySignal = Red; farmSignal = Grn;
    if (Count == D4 && farmSensor == 0 || Count == D5) begin
        RstCount = 1; nextState = S5;  // road empty, or max time reached
    end
    else begin RstCount = 0; nextState = S4; end
end

The light outputs are assigned at the top of every state branch, so every path through the combinational block drives every output. That's what prevents Vivado from inferring unwanted latches.

What I'd change now

Looking back at this design with fresh eyes, there are three things I'd fix:

  1. Compare with ≥, not ==. The transitions check Count == D1. If a car arrives a moment after the counter passes 30 s, that exact value never comes around again until the 31-bit counter wraps, about 43 s later. Using Count >= D1 would serve that car right away, which is the behavior the simulator above shows.
  2. Synchronize the sensor too. I synchronized the reset button but fed farmSensor straight into the FSM logic. It's just as asynchronous, so it should get the same two-flop treatment.
  3. Name the delays by time. The delay constants are raw cycle counts. Defining them as seconds × clock frequency would make the timing obvious at a glance and harder to get wrong.
What I learned: code that passes a testbench can still mishandle a case the testbench never exercises. Next time I'd write the tests from the requirements ("a car arriving at any moment gets served") rather than from the happy path.

Datapath and ARMv8 (Computer Architecture)

In ECEN 350 I worked from both ends of the hardware/software boundary.

ARMv8 assembly

  • Loads, stores and conditional branches
  • Function calls with correct stack frames and link-register save/restore
  • Recursive routines, including a recursive power function
  • Debugging control flow and stack corruption

Verilog datapath parts

  • 64-bit ALU
  • Register file
  • Sign extender for I-, D-, CB- and B-type immediates
  • 8-bit 2:1 mux, each part with its own testbench, checked in GTKWave

Writing the sign extender taught me why the ARM instruction formats look the way they do: each one packs its immediate into different bit positions, and the hardware has to know which format it's looking at before it can extend the value.