Project F

Hello Nexys - Part 2

Published · Updated

Welcome back to our two-part FPGA tutorial with SystemVerilog and the Digilent Nexys Video. 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. This post is also available for the Arty.

New to the series? Start with part 1.

Requirements

For this series, we are be using the Digilent Nexys Video, a $480 dev board based on a Xilinx Artix-7 FPGA. This board is widely available and supports Xilinx’s latest Vivado software, which runs on Linux and Windows.

For this Hello Nexys series you need:

  1. Digilent Nexys Video
  2. Micro USB cable to program the Nexys Video board
  3. 12V power adaptor (EU/US plug is provided by Digilent - UK users will need an adaptor)
  4. Xilinx Vivado 2019 or later: Download and Install Guide
  5. Digilent board files

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 Nexys Video board has 270,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-nexys-2 using the same settings as in part 1: an RTL Project with the Nexys Video 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 Nexys Video 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 nexys.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 R4 IOSTANDARD LVCMOS33} [get_ports {clk}];
create_clock -name clk_100m -period 10.00 [get_ports {clk}];

## LEDs
set_property -dict {PACKAGE_PIN T14 IOSTANDARD LVCMOS25} [get_ports {led[0]}];
set_property -dict {PACKAGE_PIN T15 IOSTANDARD LVCMOS25} [get_ports {led[1]}];
set_property -dict {PACKAGE_PIN T16 IOSTANDARD LVCMOS25} [get_ports {led[2]}];
set_property -dict {PACKAGE_PIN U16 IOSTANDARD LVCMOS25} [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 LD0 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?

LD3 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 four 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 LD0 to LD3.

Explore

In the Arty version of this post we now turn our attention to RGB LEDs. The Nexys Video doesn’t have any RGB LEDs; instead try extending the designs to use all eight green LEDs on your board.

  • Can you reduce the intensity of an LED to 50% and make it flash once a second?
  • Create a binary clock that counts the seconds from 0 to 59.

What’s Next?

I’ve not yet written a third instalment for Nexys Video. If you’d like to continue learning with your Nexys, you can put your HDMI port to use in Beginning FPGA Graphics, or check out my other FPGA & RISC-V Tutorials.

Get in touch on Mastodon, Bluesky, or X. If you enjoy my work, please sponsor me. 🙏