-
Notifications
You must be signed in to change notification settings - Fork 0
4.Amaranth Blinky Example
In this section, we’ll walk through a simple Blinky example, highlighting the similarities between traditional RTL design and Amaranth. This example illustrates a basic Amaranth workflow. To fully leverage Amaranth, the project is structured following common Python best practices, with Amaranth HDL integrating smoothly into the process. Later, we will showcase and explain some notable projects built using Amaranth.
The entire Blinky example is contained in a single Python script for simplicity. While it’s possible to break it up into multiple scripts for better modularity, it’s more convenient to keep everything for this specific module in one place.
First, as with any Python program, we need to import the necessary libraries:
from amaranth import *
from amaranth.sim import *
from amaranth.back import verilog
These imports are needed for general use of the Amaranth HDL language, the Amaranth Simulator, and the Verilog generation feature in Amaranth.
The following code is equivalent to a Verilog/SystemVerilog module. The __init__
function acts like a module declaration, where parameters, inputs, and outputs are defined. In this example, the number of LEDs and the clock divider are parameters, while the LEDs are the outputs. The clock signal, in most cases, doesn’t need to be explicitly declared in Amaranth; it’s automatically included in any module that uses synchronous operations. However, you can declare it manually or include multiple clock domains if needed.
Next is the elaborate
method, which calls the Module()
function and assigns it to the variable m. This method is required in every Amaranth project. Variables in Amaranth are declared using Signal()
, which is similar to the SystemVerilog logic
type. Inside the parentheses, you specify the number of bits, and you can also define the sign and initial value of the variable.
The platform section of the code is specific to the FPGA board being used. In Amaranth, most FPGA boards have LEDs labeled as "led" in their constraint files. The only difference is the number of LEDs available on each board. The platform code generates a constraint file for the FPGA in use, so you don’t need to worry about pin numbers for the LEDs or clock signal.
Amaranth operates with two domains: combinatorial and sequential. The combinatorial domain (m.d.comb
) handles assignments that aren’t clocked, while the sequential domain (m.d.sync
) deals with clocked processes.
In the code, the count
variable is incremented, and an If statement checks for overflow. If an overflow occurs, the LED values are toggled, and the counter is reset to zero.
class Blinky(Elaboratable):
def __init__(self, num_leds=4, clock_divider=21):
self.num_leds = num_leds
self.clock_divider = clock_divider
self.leds = Signal(num_leds)
def elaborate(self, platform):
# Create a new Amaranth module
m = Module()
# This is a local signal, which will not be accessible from outside.
count = Signal(self.clock_divider)
# If the platform is not defined then it is simulation
if platform is not None:
leds = [platform.request("led", i) for i in range(self.num_leds)]
m.d.comb += [led.o.eq(self.leds[i]) for i, led in enumerate(leds)]
# In the sync domain all logic is clocked at the positive edge of
# the implicit clk signal.
m.d.sync += count.eq(count + 1)
with m.If(count == (2**self.clock_divider - 1)):
m.d.sync += [
self.leds.eq(~self.leds),
count.eq(0)
]
return m
One of the advantages of using Python is the argument parsers, which allow you to change the design and call different functions directly from the terminal.
$ python3 Blinky.py -h
usage: Blinky.py [-h] [-s] [-b] [-v] [-p PLATFORM] [-n NUM_LEDS]
[-cd CLOCK_DIVIDER] [-cf CLOCK_FREQUENCY] [-rt RUNTIME] [-c]
[-dp] [-gw]
options:
-h, --help show this help message and exit
-s, --simulate Simulate Blinky Example
-b, --build Build the Blinky Example
-v, --verilog Generate Verilog for Blinky Example
-p PLATFORM, --platform PLATFORM
Platform module (e.g.,
amaranth_boards.ulx3s.ULX3S_85F_Platform)
-n NUM_LEDS, --num-leds NUM_LEDS
Number of LEDs
-cd CLOCK_DIVIDER, --clock-divider CLOCK_DIVIDER
Clock divider (bit width of the counter)
-cf CLOCK_FREQUENCY, --clock-frequency CLOCK_FREQUENCY
Clock frequency in MHz
-rt RUNTIME, --runtime RUNTIME
Testbench runtime in clock cycles
-c, --clean Clean generated files and build directory
-dp, --do-program Program the device after building
-gw, --gtkwave Open GTKWave after simulation
Now it is easy to build and program this Blinky example for any board through the terminal. You can also run simulations with different parameters and runtime, and open a waveform viewer like GTKWave or Surfer.