17 May 2021

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. A version for the Nexys Video will be available soon.

New to the series? Start with part 1.

Draft post: fixes and improvements to come.

Updated 2021-06-28. Get in touch with @WillFlux or open an issue on GitHub.

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:

  1. Digilent Arty A7-35T
  2. Micro USB cable to program and power the Arty
  3. Xilinx Vivado 2019 or later: Download and Install Guide
  4. 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:

  1. The user sets the timer length
  2. The user chooses when to start the timer
  3. The timer counts downs to zero
  4. 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:

Timer FSM

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 LEDs
  • cnt_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 - control
  • BTN0 - zero digit
  • BTN1 - 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):

Shift Register

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

Traffic Lights

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.

T-Junction

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 values MAIN and SIDE
  • 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 Next?

This concludes our three-part introduction to FPGA development with SystemVerilog and the Digilent Arty A7 board. If there’s enough interest, I’ll do a post on test benches with Vivado. In the meantime, grab a VGA Pmod and get designing with FPGA Graphics.

Constructive feedback is always welcome. Get in touch with @WillFlux or open an issue on GitHub.

©2021 Will Green, Project F