Project F

Hello Arty - Part 2

Published · Updated

Welcome back to our three-part FPGA tutorial with SystemVerilog and the Digilent Arty A7. In part two, we’re going to learn about clocks and counting. Along the way, we’ll cover maintaining state with flip-flops, timing things with clock dividers, creating our first Verilog module, and controlling LEDs with pulse width modulation. You might be surprised how far counting takes you: by the end of this tutorial, you’ll be creating RGB lighting effects worthy of a cheesy gaming PC. This post is also available for the Nexys Video.

New to the series? Start with part 1. Already completed part 2? Jump to part 3.

Share your thoughts with @WillFlux on Mastodon or Twitter. If you like what I do, sponsor me. 🙏

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.

Learning to Count

Think about what happens when you count 1, 2, 3, 4, 5…. You need to remember the current number in order to decide what comes next. If you’re interrupted while counting, you lose your place and have to start over. Without memory, you can’t count.

In part 1 our designs had no memory; to put it more abstractly they didn’t maintain any state.

That’s not quite true: when you moved a slide switch, it did maintain its state. Alas, that’s not a viable approach for circuit design: the switches are huge, and the FPGA can’t move them! Instead, we’re going to make use of flip-flops.

A flip-flop maintains the state of a single bit. That’s right: 1 bit. Are you dismayed? Don’t be: the FPGA on the Arty board has 40,000 flip-flops, and we don’t use them as general-purpose ram. Think of a flip-flop as a tiny, extremely fast, box for storing something you’re working on right now: like a CPU register.

When a flip-flop receives a trigger, it updates its bit. You could use any signal to trigger an update, but we’ll always use a common clock to trigger our flip-flops simultaneously. Using a common clock ensures that all signals are updated and ready at the same time. The clock is like the conductor of an orchestra keeping everyone playing to time.

To see a flip-flop in action watch the LearnElectronics video: Digital Logic: D type Flip Flop.

Counting Project

Create a new project in Vivado called hello-arty-2 using the same settings as in part 1: an RTL Project with the Arty A7-35 board.

Add a new SystemVerilog design source to your project called top.sv with [src]:

module top (
    input wire logic clk,
    output     logic [3:0] led
    );

    logic [31:0] cnt = 0;  // 32-bit counter

    always_ff @(posedge clk) begin
        cnt <= cnt + 1;
        led[0] <= cnt[26];
    end
endmodule

In part 1, we used always_comb blocks. Flip-flips require a new type of block: always_ff.

Our always_ff block has the positive edge (posedge) of the clock (clk) on its sensitivity list @(...). This means the block is triggered on every rising (positive) edge of the clock.

We’re using the 100 MHz oscillator on the Arty board, so we get a rising clock edge every 10 nanoseconds: this is known as the clock period. Every 10 ns our counter cnt increments; when it reaches its maximum value, it rolls over to 0 and repeats.

How long does it take the 32-bit counter to roll over?

At the same time as our counter is being incremented, one bit of the counter is being copied to a flip-flop that maintains the LED state. Statements in an always_ff block happen independently and in parallel.

Assignment
You may have noticed we snuck in a new type of assignment with flip-flops:

always_comb blocks use = for assignment
always_ff blocks use <= for assignment

We won’t go into the reasons for this right now, but follow this rule and all will be well.

Constraints

Add the following arty.xdcconstraints file to your project [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 -period 10.00 [get_ports {clk}];

## 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]}];

See part 1 if you need a reminder on how to add constraints.

Note how the period of the clock is set to 10.00 ns. This tells Vivado what the oscillator clock period is: you can’t use it to adjust the clock frequency! We’ll learn how to generate other frequencies later in this tutorial.

Generate & Program Bitstream

We’re ready to run this design on our board. We could individually synthesise, implement, and generate bitstream as we learnt in part 1. However, you don’t have to run these steps one-by-one: you can select Generate Bitstream, and Vivado performs all the necessary steps.

Program your board, and you should see the green LED LD4 blinking. We’ve wired up bit 26 of our counter to led[0], so it changes state every 226 x 10 ns = 0.67 seconds. That’s one blink every 1.34 seconds.

Blinky, Blinky

Try replacing the always_ff block in top.sv with [src]:

always_ff @(posedge clk) begin
    cnt <= cnt + 1;
    led[0] <= cnt[26];
    led[1] <= cnt[24];
    led[2] <= cnt[22];
    led[3] <= cnt[20];
end

NB. We use code snippets to focus on what’s changed, but the full design is available via [src].

Rerun bitstream generation and program your board. How often is each LED blinking?

Flashing LEDs

LD7 on our board (led[3] in Verilog) looks like it’s on all time, but is less bright. It’s blinking fifty times per second, so fast that your eyes hardly see the flashes. We’ll make use of this later.

Division of Time

Earlier we said we would always use a single clock in our designs. What happens if we want to do something less than 100 million times a second? We could take a bit from a counter, but there’s a more elegant way using a strobe. With a strobe we’re not limited to dividing by powers of two.

For example, to divide by three:

logic stb;
logic [1:0] cnt = 0;
always_ff @(posedge clk) begin
    if (cnt != 2'd2) begin
        stb <= 0;
        cnt <= cnt + 1;
    end else begin
        stb <= 1;
        cnt <= 0;
    end
end

This example divides by three because we start counting at zero: 0, 1, 2, 0, 1, 2…

The following simulation shows how the counter and strobe vary over time:

Strobe dividing by three

To use the strobe, you add a test for it inside your always block:

always_ff @(posedge clk) begin
    if (stb) begin
        // do something every time the strobe fires
    end
end

It’s tempting to simplify this by using: always_ff @(posedge stb).

Never do this!

While this might work for simple designs, it quickly leads to unstable designs and debugging headaches. Hardware design is hard enough without messing with clock distribution and crossing clock domains unnecessarily. Always use a proper clock signal in your sensitivity list.

Every Second Counts

Let’s say you want a one-second strobe: our clock is 100 MHz, so we need to divide by 100 million! Our counter needs to be 27 bits wide because log2(100,000,000) = 26.6.

We can use our new one-second strobe to turn four green LEDs into a simple binary clock [src]:

module top (
    input wire logic clk,
    output     logic [3:0] led
    );

    localparam DIV_BY = 27'd100_000_000;  // 100 million

    logic stb;
    logic [26:0] cnt = 0;
    always_ff @(posedge clk) begin
        if (cnt != DIV_BY-1) begin
            stb <= 0;
            cnt <= cnt + 1;
        end else begin
            stb <= 1;
            cnt <= 0;
        end
    end

    always_ff @(posedge clk) begin
        if (stb) led <= led + 1;
    end
endmodule

led is a normal variable, so we can perform arithmetic on it, even though each of its bits is driving an LED. localparam allows us to define a constant. You can use underscores to make numbers more readable.

Our binary clock counts from zero to fifteen, then repeats. You can extend this design to create a clock with seconds, minutes, and hours. How many LEDs would you need for this?

Brighter, Dimmer

Earlier we used a counter to blink LEDs. At low switching speeds they blink, but at higher speeds, they look like they’re on all the time, but less bright. By altering the proportion of time the signal is high, and hence the LED is illuminated, we can control the perceived brightness. The is known as pulse width modulation or PWM.

PWN is commonly used in digital circuits to control the speed of motors as well as the brightness of lights (for example in the backlight of LCD screens).

The proportion of time the signal is high is known as the duty cycle. An 8-bit value (0-255), is commonly used for the duty cycle. The brightness of the LED is proportional to the duty: for example, if the duty is 64/255, the LED is roughly a quarter as bright.

Pulse Width Modulation

In the following example, we reduce the brightness of four LEDs using a small duty cycle of 5/255. Replace the existing module in top.sv with [src]:

module top (
    input wire logic clk,
    output     logic [3:0] led
    );

    logic [7:0] cnt = 0;
    logic [7:0] duty = 8'd5;

    always_ff @(posedge clk) begin
        cnt <= cnt + 1;
        led[3:0] <= (cnt < duty) ? 4'b1111 : 4'b0000;
    end
endmodule

Run through the usual steps to program your board. The green LEDs should all be very dim; around 2% of maximum brightness.

Try adjusting the duty value to different values between 0 and 255 to see how the brightness changes. You can use binary, decimal, or hexadecimal literals as you like.

Duty Cycle Module

If we wanted to control the brightness of each individual LED using the above approach we’d end up with a great deal of duplication. Instead, we can break the PWM design out into a separate Verilog module. You should keep each module in a separate file.

The pwm module has inputs and outputs, just like the top module: it takes a duty input between 0 and 255 and produces the expected PWM output.

Create a new design source called pwm.sv, being sure to choose SystemVerilog as the file type. The module looks like this [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 pwm_out = (cnt < duty);
endmodule

ProTip: If you only have one statement in a block you don’t need begin and end.

This design makes use of an always_comb and an always_ff block. pwm_out only depends on its current inputs, so we use an always_comb block. Whereas the counter cnt needs to maintain state, so has to use an always_ff block.

Combinational Logic
Depends on the combination of present inputs; past inputs are irrelevant. Combinational outputs change immediately. Combinational logic is created with an always_comb block.

Sequential Logic
Depends on the past sequence of inputs, as well as the present ones. Sequential outputs change on a clock edge and use flip-flops. Sequential logic is created with an always_ff block.

Don’t worry if this doesn’t make sense right away. Keep designing and come back to it.

Duty to our LEDs

To use the pwm module within our top module, we create an instance and connect up the inputs and outputs. Each module instance needs a unique name. We’re using four LEDs, so we create module instances: pwm_led_0, pwm_led_1 etc. Replace your existing top.sv with [src]:

module top (
    input wire logic clk,
    output     logic [3:0] led
    );

    pwm pwm_led_0 (.clk, .duty(4),   .pwm_out(led[0]));
    pwm pwm_led_1 (.clk, .duty(16),  .pwm_out(led[1]));
    pwm pwm_led_2 (.clk, .duty(64),  .pwm_out(led[2]));
    pwm pwm_led_3 (.clk, .duty(255), .pwm_out(led[3]));
endmodule

ProTip: You can write .clk rather than .clk(clk) because the names match.

Once you’ve programmed your board with the updated design, you should see four green LEDs lit, getting brighter from LD4 to LD7.

Red, Green, Blue

This is the 21st century: we don’t have to slum it with green LEDs: we have RGB!

An RGB LED isn’t really magic: it’s a red, green, and blue LED side-by-side. Provided they’re close enough together the three component LEDs create the illusion of many different colours. An OLED screen is composed of millions of tiny LEDs. We have four, but it’s a start.

Each RGB LED has three pins: one for each of red, green, and blue. We can control them with PWM as if they were three separate LEDs.

To use the RGB LEDs we need to replace the old LED constraints with these new ones [src]:

## RGB LEDs
set_property -dict {PACKAGE_PIN G6  IOSTANDARD LVCMOS33} [get_ports {led_r[0]}];
set_property -dict {PACKAGE_PIN F6  IOSTANDARD LVCMOS33} [get_ports {led_g[0]}];
set_property -dict {PACKAGE_PIN E1  IOSTANDARD LVCMOS33} [get_ports {led_b[0]}];
set_property -dict {PACKAGE_PIN G3  IOSTANDARD LVCMOS33} [get_ports {led_r[1]}];
set_property -dict {PACKAGE_PIN J4  IOSTANDARD LVCMOS33} [get_ports {led_g[1]}];
set_property -dict {PACKAGE_PIN G4  IOSTANDARD LVCMOS33} [get_ports {led_b[1]}];
set_property -dict {PACKAGE_PIN J3  IOSTANDARD LVCMOS33} [get_ports {led_r[2]}];
set_property -dict {PACKAGE_PIN J2  IOSTANDARD LVCMOS33} [get_ports {led_g[2]}];
set_property -dict {PACKAGE_PIN H4  IOSTANDARD LVCMOS33} [get_ports {led_b[2]}];
set_property -dict {PACKAGE_PIN K1  IOSTANDARD LVCMOS33} [get_ports {led_r[3]}];
set_property -dict {PACKAGE_PIN H6  IOSTANDARD LVCMOS33} [get_ports {led_g[3]}];
set_property -dict {PACKAGE_PIN K2  IOSTANDARD LVCMOS33} [get_ports {led_b[3]}];

Then replace the top module in top.sv with [src]:

module top (
    input  wire logic clk,
    output      logic [3:0] led_r,
    output      logic [3:0] led_g,
    output      logic [3:0] led_b
    );

    pwm pwm_led_r0 (.clk, .duty(0),  .pwm_out(led_r[0]));
    pwm pwm_led_g0 (.clk, .duty(64), .pwm_out(led_g[0]));
    pwm pwm_led_b0 (.clk, .duty(64), .pwm_out(led_b[0]));
endmodule

The LED LD0 should be a cyan colour (50/50 mix of green and blue). If you look closely, you’ll see the separate green and blue components.

If you use a duty cycle of 32 for red, 4 for green, and 48 for blue, then you’ll get pinky-purple. Try out some different combinations: some colours are easier to create than others. The RGB LEDs are intensely bright, so I recommend limiting your duty values to a maximum of 64.

Four RGB LEDs

Explore

Put your new-found LED control skills to the test. Imagine you’ve been asked to create a controller for a new RGB-backlit keyboard, but before you get the job, you need to demo two effects:

  • Breath - one colour slowly fades from off to full brightness and back again
  • Spectrum - cycle through all the colours: red, orange, yellow, green, blue, pink, red…

Use can use a slide switch to select different effects.

What’s Next?

In part 3 of this series, we cover enums, case statements, button debouncing, shift registers, and the all-important finite state machine.

If you’re looking for other great resources, try recommended FPGA sites.