In [1]:
from IPython.core.display import HTML
HTML(open('../css/custom.css', 'r').read())
Out[1]:
Note: If you're reading this as a static HTML page, you can also get it as an executable Jupyter notebook here.
Everyone who wants to get started with FPGAs [1] has two thoughts:
FPGAs are typically programmed using hardware description languages (HDLs) like VHDL and Verilog. Someday you might need to learn these languages, but not today! This tutorial uses MyHDL - a simple HDL based on the Python programming language. Not only is MyHDL easier to learn, you can also design, simulate and program an FPGA without leaving the Jupyter notebook in your browser.
Programming tools from the big FPGA vendors are multi-gigabyte packages with convoluted interfaces between subprograms that perform mysterious functions. Someday you might need to learn these tools, but not today! This tutorial uses APIO and the icestorm programming tools for the iCE40 FPGA family from Lattice Semiconductor. These FOSS tools take up less than 200 MB and consist of command-line programs with simple options, most of which you'll never have to use.
With these tools and this notebook, you will go from knowing nothing [2] to creating an LED blinker with an FPGA in less than twenty minutes. The rest of this notebook will step you through:
In order to use the FPGA tools, you'll need some type of computer with Python installed. It can be any kind of computer - linux box, Raspberry Pi (RPi), Windows PC - it doesn't really matter. For linux computers or the RPi, Python should already be installed. For Windows, not so much so you can follow these instructions to load Python.
Now you should have a utility called pip
that helps you install other Python modules.
APIO is one such module, so install it with the command:
pip install apio
APIO manages installing and running the FPGA tools you'll need. The easiest way to install the tools is with this command:
apio install -a
This command loads all the FPGA tools even though you really only need icestorm for this tutorial.
The next module you need is PygMyHDL:
pip install pygmyhdl
This installs MyHDL and a wrapper that makes it slightly easier to use. It also loads some utilities for monitoring and displaying logic signals as waveforms or tables during simulations.
Finally, you can install the Jupyter package that lets you do all your FPGA development in a browser tab:
pip install jupyter
(If you don't want to use Jupyter, I'll show you later how to do everything from the command line.)
If you already had Python installed, then setting up the rest of these tools should have taken less than five minutes. You can't even download the multi-gigabyte installer for a vendor-supplied FPGA tool in that amount of time.
The LED blinker is the hardware equivalent of the "Hello, World" program: it takes a clock signal as input and outputs a signal that turns an LED on and off. I'll show the design of a blinker using MyHDL in this Jupyter notebook, but you could also place this code into a file and execute it with Python.
The first thing you have to do is import the PygMyHDL module so you can use the features of MyHDL and the wrapper functions:
In [2]:
from pygmyhdl import *
After importing, set up the module to get it ready for what comes next:
In [3]:
initialize()
Next, the logic that blinks the LED is defined (don't worry, I'll step you through it!):
In [4]:
# The following function will define a chunk of logic, hence the @chunk decorator precedes it.
# The blinker logic takes three inputs:
# clk_i: This is a clock signal input.
# led_o: This is an output signal that drives an LED on and off.
# length: This is the number of bits in the counter that generates the led_o output.
@chunk
def blinker(clk_i, led_o, length):
# Define a multi-bit signal (or bus) with length bits.
# Assign it a display name of 'cnt' for use during simulation.
cnt = Bus(length, name='cnt')
# Define a piece of sequential logic. Every time there is a positive
# edge on the clock input (i.e., it goes from 0 -> 1), the value of
# cnt is increased by 1. So over a sequence of clock pulses, the
# cnt value will progress 0, 1, 2, 3, ...
@seq_logic(clk_i.posedge)
def counter_logic():
cnt.next = cnt + 1
# This is a piece of simple combinational logic. It just connects the
# most significant bit (MSB) of the counter to the LED output.
@comb_logic
def output_logic():
led_o.next = cnt[length-1]
The blinker logic is encapsulated in a Python function that's preceded with the @chunk
decorator.
This decorator takes care of PygMyHDL's housekeeping chores and should be used on any function
that defines digital hardware.
The blinker
function accepts two types of parameters: 1) normal Python objects like length
,
and 2) signals such as clk_i
and led_o
that convey single or multi-bit digital logic values in or out of the function.
Another signal, cnt
, is declared as the first statement in blinker
.
This is a multi-bit signal that holds the value of a binary counter.
The number of bits in the counter is determined by the value of the length
parameter.
For example, if length
is three, then the counter will have three bits with
bit indices of 0, 1 and 2 (which is the most significant bit or MSB).
The next portion of blinker
defines the actual function of the counter.
The @seq_logic
decorator denotes that the following function
describes some sequential logic.
The argument to the decorator, clk_i.posedge
, indicates that the function
will only be executed when the clk_i
signal makes a transition from a logic 0
to a logic 1 level.
The function after the decorator, counter_logic
, defines the operation of the counter:
the next value in the cnt
signal becomes the current value plus 1.
So as a sequence of rising edges on clk_i
is received, the value of cnt
will
increase.
Once cnt
reaches its maximum value (which would be 111
=7 for a three-bit counter),
it rolls over and goes back to zero whereupon it continues counting upward.
Following the counter logic is some combinational logic
denoted by the @comb_logic
decorator.
The output_logic
function just drives the next value of the led_o
output with the binary value found
in the MSB of the counter (as indicated by using the bit index length-1
).
The net result is that the led_o
output will transition from logic 0 to 1
like the input clock, but at a much slower rate.
(You'll see this when you simulate the operation of the blinker.)
After the blinker logic function is defined, a couple of signals are needed to connect it to a clock signal and an LED:
In [5]:
clk = Wire(name='clk') # This is a single-bit signal that carries the clock input.
led = Wire(name='led') # This is another single-bit signal that receives the LED output.
Finally, the blinker logic is instantiated by calling the blinker
function
with the clk
and led
signals passed as arguments for its I/O ports:
In [6]:
blinker(clk_i=clk, led_o=led, length=3); # Attach the clock and LED signals to a 3-bit blinker circuit.
While it's not done here, it's possible to call the blinker
function several times using
different output signals to create multiple, independent LED blinkers.
Now that the LED blinker has been designed, it's time to see if it works.
As a test, you can run a simple simulation that just toggles the clock input
a number of times using the clk_sim
utility:
In [7]:
clk_sim(clk, num_cycles=16) # Pulse the clock input 16 times.
After the simulation completes, a graphical view of the waveforms for each of the named signals is generated
using the show_waveforms
utility of the MyHDLPeek package:
In [8]:
show_waveforms()
You can also view the signal values in a tabular format:
In [9]:
show_text_table()
Looking at the waveforms, you can see the led
output pulses low-high one time for every eight pulses
of the clk
input.
In general, for an $N$-bit counter, the led
signal will pulse at ${1}\textrm{ / }{2^N}$ the frequency of the clk
signal.
Since the simulation shows the blinker is working, it's time to compile [3] the MyHDL code into a bitstream for an FPGA. The FPGA used in this tutorial is the Lattice Semiconductor iCE40HX1K that is housed on the iCEstick evaluation board:
In addition to the FPGA, the iCEstick also includes a 12 MHz clock oscillator and five LEDs so it's perfect for the LED blinker. Well, almost. Since the clock is 12 MHz, dividing it by eight will cause the LED to turn on and off at a 1.5 MHz rate. Unless you're Superman, your eyes won't notice much of anything over 30 Hz. The counter in the blinker is going to need more bits to get the LED blink rate down to something less than 5 Hz. If you make the counter length 22 bits, then the blink rate is reduced to $12,000,000 \textrm{ Hz / } 2^{22} = 2.9 \textrm{ Hz}$, or about three times per second. Even I can see that.
After adjusting the counter size, the blinker code has to be synthesized into an intermediate form which is then placed-and-routed into a particular FPGA. This is done using the Yosys synthesizer and Arachne-pnr, respectively, that are included in the icestorm tools. But Yosys works with the Verilog HDL and you're using MyHDL! Therefore, you'll have to use one of MyHDL's conversion functions to generate a Verilog version of the blinker code:
In [10]:
toVerilog(blinker, clk_i=clk, led_o=led, length=22) # Give it the function name, signal connections, and counter size.
Out[10]:
The toVerilog
function creates a file called blinker.v
containing the following Verilog code:
In [11]:
print(open('blinker.v').read())
So now you're ready to compile the Verilog code and program your first FPGA, right? Well, almost, but there's one more detail to handle. The clock oscillator and LEDs on the iCEstick are hooked to specific pins of the physical FPGA chip as follows:
iCEstick Function | FPGA Pin |
---|---|
12 MHz clock osc. | 21 |
LED D1 | 99 |
LED D2 | 98 |
LED D3 | 97 |
LED D4 | 96 |
LED D5 | 95 |
You need to tell the icestorm tools which pins the signals for the blinker are attached to.
This is done with a pin constraints file (PCF) that associates each signal name
in the parameter list of the blinker
function with a pin of the FPGA.
You can create the PCF using a text editor, or use Python like this:
In [12]:
with open('blinker.pcf', 'w') as pcf:
pcf.write(
'''
set_io led_o 99
set_io clk_i 21
'''
)
Now you really are ready to program the FPGA. The first step [4] is to synthesize the Verilog code into an intermediate form using Yosys:
In [13]:
!yosys -q -p "synth_ice40 -blif blinker.blif" blinker.v
The options modify the operation of Yosys as follows:
Option | Effect |
---|---|
-q |
Execute in quiet mode so you're not bombarded with so many progress messages. |
-p "synth_ice40 -blif blinker.blif" |
Synthesize for the iCE40 family of FPGAs and output the intermediate code to the file blinker.blif . |
The blinker.blif
file with the synthesized intermediate code is then placed-and-routed
with the Arachne-pnr utility:
In [14]:
!arachne-pnr -q -d 1k -p blinker.pcf blinker.blif -o blinker.asc
The options specify the following place-and-route operations:
Option | Effect |
---|---|
-q |
Execute in quiet mode. |
-d 1k" |
Specifically target the iCE40HX1K FPGA. |
-p blinker.pcf |
Look for pin assignments in blinker.pcf . |
-o blinker.asc |
Output the bitstream to blinker.asc . |
The output of Arachne-pnr is a bitstream of 1's and 0's that specifies how the logic cells in the FPGA are connected through internal switches to implement the blinker logic. The bitstream uses ASCII characters to represent the bits, so another utility is used to convert it into a binary file:
In [15]:
!icepack blinker.asc blinker.bin
At long last, you are ready to program the actual FPGA chip!
First, plug the iCEstick into a USB port on your computer [5].
Then issue the following command that will pass the blinker.bin
binary bitstream
through the USB port and into the FPGA on the iCEstick:
In [16]:
!iceprog blinker.bin
After the download completes, the blinker logic starts to run inside the FPGA, converting the 12 MHz clock into a 3 Hz blinking of the LED.
Looks like that's about 3 Hz to me!
OK, maybe you're one of those guys, so here's how to do it all from the command line.
First, create a file called blinker.py
using your favorite text editor (which is probably vi or Emacs, I guess).
The file just contains the same MyHDL code I showed above.
Here's my version of it:
In [17]:
print(open('blinker.py').read())
Then, just execute the file with Python:
In [18]:
!python blinker.py
At this point, you will have the blinker.v
Verilog file and the blinker.pcf
file with the pin assignments.
Then it's just a matter of running the same icestorm commands as I showed above:
In [19]:
!yosys -q -p "synth_ice40 -blif blinker.blif" blinker.v
!arachne-pnr -q -d 1k -p blinker.pcf blinker.blif -o blinker.asc
!icepack blinker.asc blinker.bin
!iceprog blinker.bin
And that's it! The LED on your iCEstick board should be blinking.
If you've made it here, congratulations! You've completed your first FPGA design. You've coded a design in MyHDL, simulated it, compiled it, and downloaded it to an FPGA and watched it run. The design was simple, but it encompassed a complete slice of the FPGA design process. There is much more to learn (like hierarchical design, state machines, etc.), but that entails more of a broadening of the slice rather than adding more steps.
[1] Maybe you don't even know what FPGAs are. Chapter 1 of this online book is a good introduction.
[2] You might want to know a bit about binary numbers and arithmetic. This chapter gives a good explanation.
[3] A high-level description of what happens when HDL code is compiled for an FPGA is given in the first few pages of Chapter 2 in this online book.
[4] The APIO package can compile the bitstream using a single command. Unfortunately the current version has a dependency that only lets it run under Python 2. Since I also wanted to support Python 3, I'm showing the explicit steps for creating a bitstream using the icestorm utilities.
[5] You'll need to manually install some drivers to use an iCEstick with the icestorm tools on Windows. Here's the steps to do that.