Hello Arty - Part 3
Welcome back to our three-part FPGA tutorial with SystemVerilog and the Digilent Arty A7. In this third instalment, we build a countdown timer and model traffic lights. There’s a lot to get through this time: enums, case statements, button debouncing, shift registers, and the all-important finite state machine.
New to the series? Start with part 1.
Requirements
For this series, we are using the Digilent Arty A7-35T, a $130 dev board based on a Xilinx Artix-7 FPGA. This board is widely available and supports Xilinx’s Vivado software, which runs on Linux and Windows.
For this Hello Arty series you need:
- Digilent Arty A7-35T
- Micro USB cable to program and power the Arty
- Xilinx Vivado 2019 or later: Download and Install Guide
- Digilent board files
The original Arty (without the A7) is the same physical board so you can use that too.
Source
The SystemVerilog designs featured in this series are available from the projf-explore git repo under the open-source MIT licence: build on them to your heart’s content. The rest of the blog content is subject to standard copyright restrictions: don’t republish it without permission.
New Project
Create a new project in Vivado called timer
using the same settings as in part 1: an RTL Project with the Arty A7-35 board.
Timer
In part 2, we created a binary clock that counted up to 15. A timer is a clock that counts down from a time chosen by the user. Our timer is more complex than our clock; not only does it require user input, but it has distinct operating modes:
- The user sets the timer length
- The user chooses when to start the timer
- The timer counts downs to zero
- The timer informs the user it’s reached zero
As with our clock, we limit ourselves to 4-bit values so we can use the four LEDs for binary display.
Finite State Machine
Our timer moves between several different modes, for example: setting the time, then counting down. To handle these different modes, we use a Finite State Machine (FSM).
Finite state machines are one of the most widely used concepts in digital design. A finite state machine has a limited number of states with well-defined transitions between them. Simple real-world FSMs include lifts (elevators), traffic lights, and vending machines.
It helps to quickly sketch out your FSM states and transitions. For our timer design, I’ve drawn this:
States
- Idle - wait for the user to tell us what to do
- Init - set initial values, such as default timer length
- Set Time - user presses buttons to set timer length
- Countdown - timer is running
- Done - flash LEDs to tell user timer is done
Transitions
- From Idle to Init - user presses control button
- From Init to Set Time - automatically happens after one clock tick
- From Set Time to Countdown - user presses control button
- From Countdown to Done - after the timer reaches zero
- From Done to Idle - after LED flashing counter reaches zero
FSM Implementation
Now we have a design; we need to translate this into Verilog.
I’m going to create the timer FSM in two parts: one handles the transitions between states, and one carries out the other logic, such as counting down. Using two separate processes is only one approach to FSMs; you’ll see another later in this tutorial.
FSM Theory
This series focuses on practical design. If you want to learn the theory behind finite machines, how they’re encoded and what distinguishes a Moore FSM from a Mealy, you can find plenty of academic resources online. A good place to start is with MIT 6.111 (PDF).
Enums
We use an enum to define our states. Enums are one of the small but useful features added in SystemVerilog. As in software programming, an enum creates a variable with a fixed list of named values.
For example, you could represent the days of the week:
enum {SUN, MON, TUE, WED, THU, FRI, SAT} week_day;
Using an enum makes your finite state machine less error-prone and easier to debug.
State Transitions
To handle state transitions, we use combinational logic with a case statement.
A case statement allows you to select between several different options, again much in the same way as software programming. In C, you have the switch statement.
We handle our state transitions using the conditional operator we learnt about in part 1:
enum {IDLE, INIT, SET_TIME, COUNTDOWN, DONE} state, state_next;
always_comb begin
case (state)
IDLE: state_next = (sig_ctrl) ? INIT : IDLE;
INIT: state_next = SET_TIME;
SET_TIME: state_next = (sig_ctrl) ? COUNTDOWN : SET_TIME;
COUNTDOWN: state_next = (led == 0) ? DONE : COUNTDOWN;
DONE: state_next = (cnt_flash == 0) ? IDLE : DONE;
default: state_next = IDLE;
endcase
end
In hardware design, you should ensure all cases are covered, or you will have problems. Alas, SystemVerilog doesn’t enforce this (unlike Rust match). We’ve got an explicit default for this case statement that catches any value that doesn’t have an explicit option.
ProTip: Unlike a C switch statement, there is no fall-through between Verilog case options.
The state changes match up with our design. For example, if we’re in the IDLE
state, we transition to the INIT
state when sig_ctrl
goes high; otherwise, we remain idle.
The signals used in transitions are:
sig_ctrl
- signals a control button press (discussed shortly)led
- timer value and drives LEDscnt_flash
- flashing counter
Combinational logic doesn’t maintain state, so we register the FSM state in an always_ff
block:
// move to next FSM state
always_ff @(posedge clk) state <= state_next;
We have a working FSM, but it doesn’t do anything useful: we need some logic to handle the behaviour of each operating modes.
Initialization
The INIT
state allows us to do initialization when our FSM starts. In this design, we use INIT
to set a default timer length and the counter for the flashing LEDs. We can use led
to hold the time remaining as we want to display this in binary on the LEDs anyway.
localparam DEF_TIME = 4'b1000; // default timer in seconds
localparam FLASH_TIME = 2; // seconds to flash when done
logic [$clog2(FLASH_TIME+1)-1:0] cnt_flash; // counter for done flashing
// finite state machine: behaviour
always_ff @(posedge clk) begin
case (state)
INIT: begin
led <= DEF_TIME; // set default time
cnt_flash <= FLASH_TIME; // initialize flash timer
end
// ...
The default timer length, 4'b1000
, shows the timer is ready to accept input but doesn’t inconvenience the user: the 1
gets shifted out on the first button press. We’ll explain how this works shortly.
User Input
Handling user input is the most complex part of the design. In this section, you’ll learn about button debouncing and shift registers.
We use three buttons on the Arty board for user input:
BTN3
- controlBTN0
- zero digitBTN1
- one digit
The user presses the control button to switch mode, while zero and one set the timer length. Handling button presses seems trivial, but this is an occasion where hardware requires more thought than software.
What defines a single button press?
In practice, a button rarely makes a single clean contact when pressed. Instead, a button will ‘bounce’, making several brief contacts before making a definite connection. Hence the need to “debounce” a button before using it in a design.
Debounce
We have three buttons to handle, so we create a module to handle debouncing. Add a new module called debounce.sv
to your project with [src]:
module debounce (
input wire logic clk, // clock
input wire logic in, // signal input
output logic out, // signal output (debounced)
output logic onup // on up (one tick)
);
// sync with clock and combat metastability
logic sync_0, sync_1;
always_ff @(posedge clk) sync_0 <= in;
always_ff @(posedge clk) sync_1 <= sync_0;
logic [19:0] cnt; // 2^20 = 10 ms counter at 100 MHz
logic idle, max;
always_comb begin
idle = (out == sync_1);
max = &cnt;
onup = ~idle & max & out;
end
always_ff @(posedge clk) begin
if (idle) begin
cnt <= 0;
end else begin
cnt <= cnt + 1;
if (max) out <= ~out;
end
end
endmodule
This module synchronizes the button press with our clock, then waits for 10 ms before considering a button to be pressed. You can easily tweak the width of cnt
if you want to adjust the delay. If the delay is too short, you risk getting phantom button presses; if the delay is too long, we could miss a press, and the interface doesn’t feel responsive.
The debounce module provides an onup
signal that is high for one clock tick after a button is released.
We have three buttons, and each needs a separate instance of the debounce module:
// debounce each button using the debounce module
logic sig_ctrl, sig_0, sig_1; // button press signals
debounce deb_ctrl (.clk, .in(btn_ctrl), .out(), .onup(sig_ctrl));
debounce deb_0 (.clk, .in(btn_0), .out(), .onup(sig_0));
debounce deb_1 (.clk, .in(btn_1), .out(), .onup(sig_1));
We don’t use the out
signal, hence the empty brackets: .out()
.
Shift Register
To record the user button presses, we treat the four-bit LED variable as a shift register.
In a shift register, the output of one register connects to the input of the next. With a left shift, all values move left one place, with the left-most bit falling off the end with a new bit added on the right. A right shift works the same way but in the opposite direction. Don’t worry if this isn’t clear; we will look at some real-world examples.
Concat
In Verilog, we don’t need anything special to create a shift register. We use the Concatenation operator {a,b}
to left (or right) shift a variable.
Concatenation combines the bits of two (or more) variables. For example, if we concat a two-bit value and a four-bit value, we get a six-bit value:
{2'b10, 4'b1001} == 6'b101001
To left-shift 1
into led
, we combine it with three existing bits from led
and discard led[3]
:
led <= {led[2], led[1], led[0], 1'b1};
We can express this more compactly:
led <= {led[2:0], 1'b1};
Shift Logic
Numerical input comes from two separate buttons, so we need to handle both:
SET_TIME: begin
if (sig_0) led <= {led[2:0], 1'b0}; // user pressed 0
else if (sig_1) led <= {led[2:0], 1'b1}; // user pressed 1
end
Shift Example
Our timer defaults to 4'b1000
(eight seconds).
If the user presses 1
then 0
, the timer becomes 4'b0010
(two seconds):
Note how the first press shifts out the 1
on the left and shifts in a 1
on the right.
Counting Down
After the complexity of user input, counting is straightforward.
led
holds the remaining time. Every time stb
is high, we decrement the time:
COUNTDOWN: if (stb) led <= led - 1;
The one-second strobe should be familiar from part 2:
// generate 1 second strobe from 100 MHz clock
localparam DIV_BY = 27'd100_000_000; // 100 million
logic stb;
logic [$clog2(DIV_BY)-1:0] cnt_stb;
always_ff @(posedge clk) begin
if (cnt_stb != DIV_BY-1) begin
stb <= 0;
cnt_stb <= cnt_stb + 1;
end else begin
stb <= 1;
cnt_stb <= 0;
end
end
Done and Dusted
When the timer reaches zero, we flash all four LEDs.
We take a bit from the counter to set the flash rate;
I’ve chosen the 23rd bit, but you can easily adjust this with FLASH_BIT
:
localparam FLASH_BIT = 23;
DONE: begin
led <= {4{cnt_stb[FLASH_BIT]}}; // flash all four LEDs
if (stb) cnt_flash <= cnt_flash - 1;
end
We’re using the concat operator again, this time to replicate a bit to drive all four LEDs.
Replication is easiest to understand with a couple of examples:
{4{1'b1}} == 4'b1111
{4{2'b10}} == 8'b10101010
Top Timer
The finished timer top.sv
looks like this [src]:
module top (
input wire logic clk,
input wire logic btn_ctrl,
input wire logic btn_0,
input wire logic btn_1,
output logic [3:0] led
);
localparam DEF_TIME = 4'b1000; // default timer in seconds
localparam FLASH_BIT = 23; // bit of counter to use for done flashing
localparam FLASH_TIME = 2; // seconds to flash when done
logic [$clog2(FLASH_TIME+1)-1:0] cnt_flash; // counter for done flashing
// debounce each button using the debounce module
logic sig_ctrl, sig_0, sig_1; // button press signals
debounce deb_ctrl (.clk, .in(btn_ctrl), .out(), .onup(sig_ctrl));
debounce deb_0 (.clk, .in(btn_0), .out(), .onup(sig_0));
debounce deb_1 (.clk, .in(btn_1), .out(), .onup(sig_1));
// finite state machine: state change
enum {IDLE, INIT, SET_TIME, COUNTDOWN, DONE} state, state_next;
always_comb begin
case (state)
IDLE: state_next = (sig_ctrl) ? INIT : IDLE;
INIT: state_next = SET_TIME;
SET_TIME: state_next = (sig_ctrl) ? COUNTDOWN : SET_TIME;
COUNTDOWN: state_next = (led == 0) ? DONE : COUNTDOWN;
DONE: state_next = (cnt_flash == 0) ? IDLE : DONE;
default: state_next = IDLE;
endcase
end
// move to next FSM state
always_ff @(posedge clk) state <= state_next;
// generate 1 second strobe from 100 MHz clock
localparam DIV_BY = 27'd100_000_000; // 100 million
logic stb;
logic [$clog2(DIV_BY)-1:0] cnt_stb;
always_ff @(posedge clk) begin
if (cnt_stb != DIV_BY-1) begin
stb <= 0;
cnt_stb <= cnt_stb + 1;
end else begin
stb <= 1;
cnt_stb <= 0;
end
end
// finite state machine: behaviour
always_ff @(posedge clk) begin
case (state)
INIT: begin
led <= DEF_TIME; // set default time
cnt_flash <= FLASH_TIME; // initialize flash timer
end
SET_TIME: begin
if (sig_0) led <= {led[2:0], 1'b0}; // user pressed 0
else if (sig_1) led <= {led[2:0], 1'b1}; // user pressed 1
end
COUNTDOWN: if (stb) led <= led - 1;
DONE: begin
led <= {4{cnt_stb[FLASH_BIT]}}; // flash all four LEDs
if (stb) cnt_flash <= cnt_flash - 1;
end
default: led <= 4'b0000;
endcase
end
endmodule
Building Your Timer
We need some constraints, including the three buttons. Save the following as arty.xdc
[src]:
## FPGA Configuration I/O Options
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]
## Board Clock: 100 MHz
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports {clk}];
create_clock -name clk_100m -period 10.00 [get_ports {clk}];
## Buttons
set_property -dict {PACKAGE_PIN B8 IOSTANDARD LVCMOS33} [get_ports {btn_ctrl}];
set_property -dict {PACKAGE_PIN D9 IOSTANDARD LVCMOS33} [get_ports {btn_0}];
set_property -dict {PACKAGE_PIN C9 IOSTANDARD LVCMOS33} [get_ports {btn_1}];
## LEDs
set_property -dict {PACKAGE_PIN H5 IOSTANDARD LVCMOS33} [get_ports {led[0]}];
set_property -dict {PACKAGE_PIN J5 IOSTANDARD LVCMOS33} [get_ports {led[1]}];
set_property -dict {PACKAGE_PIN T9 IOSTANDARD LVCMOS33} [get_ports {led[2]}];
set_property -dict {PACKAGE_PIN T10 IOSTANDARD LVCMOS33} [get_ports {led[3]}];
Make sure your project includes top.sv
, debounce.sv
, and arty.xdc
.
Select Generate Bitstream, then program your Arty board using Hardware Manager.
To use the timer, press BTN3
once to enter “set time” mode, where you can use BTN0
and BTN1
to set your timer length. Press BTN3
again to start the timer. When the timer reaches zero, it flashes all four LEDs before returning to the idle state, ready to be set again.
And There Was Light
In the second part of this tutorial, we’re going to use what we’ve already learnt to model traffic lights. This should solidify what you learnt in the first part and allows us to play with the RGB LEDs again. :)
We’re going to start a new project for our traffic light design. Close your timer design and create a fresh project in Vivado called traffic-lights
using the same settings as in part 1: an RTL Project with the Arty A7-35 board.
T-Junction
For this project, we’re modelling a T-junction with three sets of traffic lights controlling traffic flow.
I’m going to use British traffic lights for my model, but you can adjust the colours and sequence to match your local roads. The usual sequence for a British traffic light is:
- red
- red + amber
- green
- amber
- red
The specification for our traffic lights:
- If the main road is not red, the side road must be red (and vice versa)
- Both main road signals show the same lights at all times (we will use one LED for both)
Timings should be as follows:
- 1 second of red lights for both roads between changeover
- 1 second of red + amber before turning green
- 10 seconds of green on the main road
- 5 seconds of green on the side road
- 2 seconds of amber after green, before turning red
We store these times in parameters, so they’re easy to edit and debug:
// traffic light phases in seconds
localparam T_RED = 1;
localparam T_RED_AMBER = 1;
localparam T_GREEN_MAIN = 10;
localparam T_GREEN_SIDE = 4;
localparam T_AMBER = 2;
We need a register to track how long each phase is lasting:
logic [3:0] cnt_phase; // 4-bit allows up to 15 second phase
And a register to track which set of lights is active:
logic light_set; // active lights: 0 = main, 1 = side
Light FSM
For this FSM, we’re going to use a single process combining state change and behaviour:
enum {IDLE, INIT, RED, RED_AMBER, GREEN, AMBER} state;
always_ff @(posedge clk) begin
case (state)
IDLE: state <= INIT;
INIT: begin
state <= RED;
cnt_phase <= T_RED;
light_set <= 0; // main set
end
RED: begin
if (cnt_phase == 0) begin
state <= RED_AMBER;
cnt_phase <= T_RED_AMBER;
end else if (stb) cnt_phase <= cnt_phase - 1;
end
RED_AMBER: begin
if (cnt_phase == 0) begin
state <= GREEN;
cnt_phase <= (light_set == 1) ? T_GREEN_SIDE : T_GREEN_MAIN;
end else if (stb) cnt_phase <= cnt_phase - 1;
end
GREEN: begin
if (cnt_phase == 0) begin
state <= AMBER;
cnt_phase <= T_AMBER;
end else if (stb) cnt_phase <= cnt_phase - 1;
end
AMBER: begin
if (cnt_phase == 0) begin
state <= RED;
cnt_phase <= T_RED;
light_set <= ~light_set; // switch active light set
end else if (stb) cnt_phase <= cnt_phase - 1;
end
default: state <= IDLE;
endcase
end
However, we’re going to put the light colour logic in a separate block for clarity.
Light & Colour
A regular traffic light has three bulbs: red, amber, and green. We have four RGB LEDs but want to model two (or more) sets of traffic lights, so we’ll use one RGB LED per traffic light, switching it between the different colours. For red + amber, I’ll use a colour intermediate between red and amber.
We’re going to use PWM with our RGB LEDs, just like we did in part 2. Create a new module called pwm.sv
using the following [src]:
module pwm (
input wire logic clk,
input wire logic [7:0] duty,
output logic pwm_out
);
logic [7:0] cnt = 8'b0;
always_ff @(posedge clk) begin
cnt <= cnt + 1;
end
always_comb begin
pwm_out = (cnt < duty);
end
endmodule
We control the light colours like so:
// PWM duty values are 8-bit
logic [7:0] duty_main_r, duty_main_g;
logic [7:0] duty_side_r, duty_side_g;
pwm pwm_main_r (.clk, .duty(duty_main_r), .pwm_out(led_main_r));
pwm pwm_main_g (.clk, .duty(duty_main_g), .pwm_out(led_main_g));
pwm pwm_side_r (.clk, .duty(duty_side_r), .pwm_out(led_side_r));
pwm pwm_side_g (.clk, .duty(duty_side_g), .pwm_out(led_side_g));
// set light colour based on active light set and state
always_ff @(posedge clk) begin
// all lights are red by default
duty_main_r <= 8'd64;
duty_main_g <= 8'd0;
duty_side_r <= 8'd64;
duty_side_g <= 8'd0;
case (state)
RED_AMBER: begin
if (light_set == 0) begin
duty_main_r <= 8'd56;
duty_main_g <= 8'd8;
end else if (light_set == 1) begin
duty_side_r <= 8'd56;
duty_side_g <= 8'd8;
end
end
GREEN: begin
if (light_set == 0) begin
duty_main_r <= 8'd0;
duty_main_g <= 8'd64;
end else if (light_set == 1) begin
duty_side_r <= 8'd0;
duty_side_g <= 8'd64;
end
end
AMBER: begin
if (light_set == 0) begin
duty_main_r <= 8'd48;
duty_main_g <= 8'd16;
end else if (light_set == 1) begin
duty_side_r <= 8'd48;
duty_side_g <= 8'd16;
end
end
endcase
end
This design ensures all signals have a default value by explicitly setting them separately from the case
statement.
We keep the system safe by having both sets of lights be red by default and using an if ... else
block such that only one set of lights can have their colour altered from red.
Traffic Control
Our finished traffic light top.sv
looks like this [src]:
module top (
input wire logic clk,
output logic led_main_r,
output logic led_main_g,
output logic led_side_r,
output logic led_side_g
);
// traffic light phases in seconds
localparam T_RED = 1;
localparam T_RED_AMBER = 1;
localparam T_GREEN_MAIN = 10;
localparam T_GREEN_SIDE = 4;
localparam T_AMBER = 2;
logic [3:0] cnt_phase; // 4-bit allows up to 15 second phase
logic light_set; // active lights: 0 = main, 1 = side
// generate 1 second strobe from 100 MHz clock
localparam DIV_BY = 27'd100_000_000; // 100 million
logic stb;
logic [$clog2(DIV_BY)-1:0] cnt_stb;
always_ff @(posedge clk) begin
if (cnt_stb != DIV_BY-1) begin
stb <= 0;
cnt_stb <= cnt_stb + 1;
end else begin
stb <= 1;
cnt_stb <= 0;
end
end
// finite state machine: state change & behavior
enum {IDLE, INIT, RED, RED_AMBER, GREEN, AMBER} state;
always_ff @(posedge clk) begin
case (state)
IDLE: state <= INIT;
INIT: begin
state <= RED;
cnt_phase <= T_RED;
light_set <= 0; // main set
end
RED: begin
if (cnt_phase == 0) begin
state <= RED_AMBER;
cnt_phase <= T_RED_AMBER;
end else if (stb) cnt_phase <= cnt_phase - 1;
end
RED_AMBER: begin
if (cnt_phase == 0) begin
state <= GREEN;
cnt_phase <= (light_set == 1) ? T_GREEN_SIDE : T_GREEN_MAIN;
end else if (stb) cnt_phase <= cnt_phase - 1;
end
GREEN: begin
if (cnt_phase == 0) begin
state <= AMBER;
cnt_phase <= T_AMBER;
end else if (stb) cnt_phase <= cnt_phase - 1;
end
AMBER: begin
if (cnt_phase == 0) begin
state <= RED;
cnt_phase <= T_RED;
light_set <= ~light_set; // switch active light set
end else if (stb) cnt_phase <= cnt_phase - 1;
end
default: state <= IDLE;
endcase
end
// PWM duty values are 8-bit
logic [7:0] duty_main_r, duty_main_g;
logic [7:0] duty_side_r, duty_side_g;
pwm pwm_main_r (.clk, .duty(duty_main_r), .pwm_out(led_main_r));
pwm pwm_main_g (.clk, .duty(duty_main_g), .pwm_out(led_main_g));
pwm pwm_side_r (.clk, .duty(duty_side_r), .pwm_out(led_side_r));
pwm pwm_side_g (.clk, .duty(duty_side_g), .pwm_out(led_side_g));
// set light colour based on active light set and state
always_ff @(posedge clk) begin
// all lights are red by default
duty_main_r <= 8'd64;
duty_main_g <= 8'd0;
duty_side_r <= 8'd64;
duty_side_g <= 8'd0;
case (state)
RED_AMBER: begin
if (light_set == 0) begin
duty_main_r <= 8'd56;
duty_main_g <= 8'd8;
end else if (light_set == 1) begin
duty_side_r <= 8'd56;
duty_side_g <= 8'd8;
end
end
GREEN: begin
if (light_set == 0) begin
duty_main_r <= 8'd0;
duty_main_g <= 8'd64;
end else if (light_set == 1) begin
duty_side_r <= 8'd0;
duty_side_g <= 8'd64;
end
end
AMBER: begin
if (light_set == 0) begin
duty_main_r <= 8'd48;
duty_main_g <= 8'd16;
end else if (light_set == 1) begin
duty_side_r <= 8'd48;
duty_side_g <= 8'd16;
end
end
endcase
end
endmodule
Build Your Lights
We need some constraints. Save the following as arty.xdc
[src]:
## FPGA Configuration I/O Options
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]
## Board Clock: 100 MHz
set_property -dict {PACKAGE_PIN E3 IOSTANDARD LVCMOS33} [get_ports {clk}];
create_clock -name clk_100m -period 10.00 [get_ports {clk}];
## RGB LEDs
set_property -dict {PACKAGE_PIN G6 IOSTANDARD LVCMOS33} [get_ports {led_main_r}];
set_property -dict {PACKAGE_PIN F6 IOSTANDARD LVCMOS33} [get_ports {led_main_g}];
set_property -dict {PACKAGE_PIN G3 IOSTANDARD LVCMOS33} [get_ports {led_side_r}];
set_property -dict {PACKAGE_PIN J4 IOSTANDARD LVCMOS33} [get_ports {led_side_g}];
Make sure your project includes top.sv
, pwm.sv
, and arty.xdc
.
Select Generate Bitstream, then program your Arty board using Hardware Manager.
This design runs automatically without any user interaction. You should see two of the RGB LEDs cycle through the traffic light colour sequence.
Explore
That about wraps it up for this post, but there are oodles of things you can do to improve and expand on these designs. Here are a few ideas:
- Create a separate module for the one-second strobe
- Add external hardware to the timer
- Use a seven-segment display in place of the LEDs for timer display
- Use a speaker or buzzer instead of flashing LEDs
- Use an enum for
light_set
with valuesMAIN
andSIDE
- Add a pedestrian crossing phase to your traffic lights
- Pedestrians can request to cross using a push button on the Arty
- Use a third RGB light to show when it’s safe for pedestrians to cross
- How do you prevent unsafe combinations of lights?
What’s Next?
This concludes our three-part introduction to FPGA development with SystemVerilog and the Digilent Arty A7 board. In the meantime, grab a VGA Pmod and get designing with Beginning FPGA Graphics. You can also check out my FPGA & RISC-V Tutorials and recommended FPGA sites.
Get in touch on Mastodon, Bluesky, or X. If you enjoy my work, please sponsor me. 🙏