Gateware Blinky

This tutorial walks through the process of developing a simple “blinky” example for Cynthion’s ECP5 FPGA. We’ll use the Amaranth Language & toolchain to create the design and generate a FPGA bitstream using the Yosys Open SYnthesis Suite.

Prerequisites

Before you begin, please make sure you have installed the Cynthion tools by following Getting Started with Cynthion.

Install Toolchain

To generate bitstreams for Cynthion you will need a synthesis toolchain that can convert the Verilog produced by Amaranth into a bitstream for Cynthion’s ECP5 FPGA.

For these tutorials we recommend YoWASP which provides unofficial WebAssembly-based packages for Yosys and NextPNR. It runs a little slower than the official OSS CAD Suite distribution but it’s platform-independent and much easier to get started with.

Install YoWASP using pip:

pip install yowasp-yosys yowasp-nextpnr-ecp5

Create a new Amaranth module

Create a new file called gateware-blinky.py and add the following code to it:

1from amaranth import *
2
3class Top(Elaboratable):
4    def elaborate(self, platform):
5        m = Module()
6
7        return m

Amaranth designs are built from a hierarchy of smaller modules, which are called elaboratables. The Top class expresses that this will be the top-level or entry-point of your design.

Right now the Top module does not do anything except to create an Amaranth Module() and return it from the elaborate(...) method.

The elaborate(...) method also takes an argument called platform that contains resources specific to the board or platform the module is compiled for.

In this case, the argument will be an instance of the Cynthion Board Description and contain a map of Cynthion resources such as LEDs, USB PHY’s, USER PMOD connectors and board constraints.

Obtain a platform resource

Edit gateware-blinky.py and add the highlighted line:

1from amaranth import *
2
3class Top(Elaboratable):
4    def elaborate(self, platform):
5        m = Module()
6
7        leds: Signal(6) = Cat(platform.request("led", n).o for n in range(0, 6))
8
9        return m

Amaranth platform resources can be obtained via the platform.request(name, number=0) method where name is the name of the resource and number is the index of the resource if there are more than one defined.

In this case we use a Python list comprehension to obtain all six FPGA led’s and concatenate them into a six-bit addressable Amaranth Signal using the Cat operation.

Timer State

To make the LED blink at predictable intervals we’ll use a simple timer.

To start with, let’s define the timer state by adding the highlighted lines:

 1from amaranth import *
 2
 3class Top(Elaboratable):
 4    def elaborate(self, platform):
 5        m = Module()
 6
 7        leds: Signal(6) = Cat(platform.request("led", n).o for n in range(0, 6))
 8
 9        half_freq: int    = int(60e6 // 2)
10        timer: Signal(25) = Signal(range(half_freq))
11
12        return m

First we’ll declare a variable half_freq which is exactly half of Cynthion FPGA’s default clock frequency in Hz, next we’ll declare timer to be an Amaranth Signal which is wide enough to contain a value equal to half_freq - 1.

If we increment the timer by one for each clock cycle until it reaches half_freq - 1 we get a timer with a 500ms period.

Timer Logic

Now that we have a state definition for our timer we can move forward to the implementation logic, edit your file and add the highlighted lines:

 1from amaranth import *
 2
 3class Top(Elaboratable):
 4    def elaborate(self, platform):
 5        m = Module()
 6
 7        leds: Signal(6) = Cat(platform.request("led", n).o for n in range(0, 6))
 8
 9        half_freq: int    = int(60e6 // 2)
10        timer: Signal(25) = Signal(range(half_freq))
11
12        with m.If(timer == half_freq - 1):
13            m.d.sync += leds.eq(~leds)
14            m.d.sync += timer.eq(0)
15
16        with m.Else():
17            m.d.sync += timer.eq(timer + 1)
18
19        return m

Amaranth combines normal Python expressions with Amaranth in order to describe a design. Whenever you see the prefix m. you are making a call to the Module object you created at the beginning of the elaborate(...) method. These calls are what build the logic which makes up a design.

The with m.If(...): and with m.Else(): blocks operate much like you’d expect where, every clock-cycle, the expression timer == half_freq - 1 will be evaluated and trigger the corresponding branch.

The first block represents the point at which the timer has expired and we’d like to change the state of the LEDs and then reset the timer back to zero.

In the second block the timer is still active so we simply increment timer by one.

Put It All Together

The contents of gateware-blinky.py should now look like this:

 1#!/usr/bin/env python3
 2#
 3# This file is part of Cynthion.
 4#
 5# Copyright (c) 2024 Great Scott Gadgets <info@greatscottgadgets.com>
 6# SPDX-License-Identifier: BSD-3-Clause
 7
 8from amaranth import *
 9
10class Top(Elaboratable):
11    def elaborate(self, platform):
12        m = Module()
13
14        leds: Signal(6) = Cat(platform.request("led", n).o for n in range(0, 6))
15
16        half_freq: int    = int(60e6 // 2)
17        timer: Signal(25) = Signal(range(half_freq))
18
19        with m.If(timer == half_freq - 1):
20            m.d.sync += leds.eq(~leds)
21            m.d.sync += timer.eq(0)
22
23        with m.Else():
24            m.d.sync += timer.eq(timer + 1)
25
26        return m
27
28if __name__ == "__main__":
29    from luna import top_level_cli
30    top_level_cli(Top)

Build and Upload FPGA Bitstream

Make sure your Cynthion CONTROL port is plugged into the host, open a terminal and then run:

python gateware-blinky.py

The blinky gateware will now be synthesized, placed, routed and automatically uploaded to the Cynthion’s FPGA.

Once this process has completed successfully all six of Cynthion’s FPGA LEDs should be flashing on and off.

Exercises

  1. Modify the tutorial to turn the FPGA LEDs into a binary counter that increments by one every 250ms.

  2. Connect the USER PMOD A port to the output of your counter and use a logic analyzer (e.g. GreatFET One) to view the values as they increment.

More information: