Hello Arty - Part 2
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.
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.
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 assignmentalways_ff
blocks use<=
for assignmentWe won’t go into the reasons for this right now, but follow this rule and all will be well.
Constraints
Add the following arty.xdc
constraints 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?
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:
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.
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 analways_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 analways_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.
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 Hello Arty, we cover enums, case statements, button debouncing, shift registers, and the all-important finite state machine. 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. 🙏