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.

The Fastest, Easiest FPGA Blinker, Ever!

Everyone who wants to get started with FPGAs [1] has two thoughts:

  1. "I don't know how to program an FPGA."
  2. "FPGA programming tools are large and complicated."

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:

  1. Setting up your FPGA programming environment.
  2. Writing a MyHDL program for a logic circuit that blinks an LED.
  3. Simulating the program to check the operation of the blinker.
  4. Compiling the program into a bitstream that can be loaded into the FPGA.
  5. Downloading the bitstream into the FPGA.

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.
<class 'myhdl.StopSimulation'>: No more events

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()
  Time    clk    cnt    led
------  -----  -----  -----
     0      0      0      0
     1      1      1      0
     2      0      1      0
     3      1      2      0
     4      0      2      0
     5      1      3      0
     6      0      3      0
     7      1      4      1
     8      0      4      1
     9      1      5      1
    10      0      5      1
    11      1      6      1
    12      0      6      1
    13      1      7      1
    14      0      7      1
    15      1      0      0
    16      0      0      0
    17      1      1      0
    18      0      1      0
    19      1      2      0
    20      0      2      0
    21      1      3      0
    22      0      3      0
    23      1      4      1
    24      0      4      1
    25      1      5      1
    26      0      5      1
    27      1      6      1
    28      0      6      1
    29      1      7      1
    30      0      7      1
    31      1      0      0

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:

icestick FPGA 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.
c:\python35-32\lib\site-packages\ipykernel_launcher.py:1: UserWarning: 
    toVerilog(): Deprecated usage: See http://dev.myhdl.org/meps/mep-114.html
  """Entry point for launching an IPython kernel.
Out[10]:
[<myhdl._always_comb._AlwaysComb at 0x51f68f0>,
 <myhdl._always_seq._AlwaysSeq at 0x52a28d0>]

The toVerilog function creates a file called blinker.v containing the following Verilog code:

In [11]:
print(open('blinker.v').read())
// File: blinker.v
// Generated by MyHDL 1.0dev
// Date: Wed Aug 30 23:23:57 2017


`timescale 1ns/10ps

module blinker (
    clk_i,
    led_o
);


input clk_i;
output led_o;
wire led_o;

reg [21:0] cnt;




assign led_o = cnt[(22 - 1)];


always @(posedge clk_i) begin: BLINKER_LOC_INSTS_CHUNK_INSTS_K
    cnt <= (cnt + 1);
end

endmodule

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
init..
cdone: high
reset..
cdone: low
flash ID: 0x20 0xBA 0x16 0x10 0x00 0x00 0x23 0x12 0x67 0x21 0x13 0x00 0x49 0x00 0x34 0x04 0x11 0x11 0x20 0x31
file size: 32220
erase 64kB sector at 0x000000..
programming..
reading..
VERIFY OK
cdone: high
Bye.

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.

blinking 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())
from pygmyhdl import *

initialize()

@chunk
def blinker(clk_i, led_o, length):
    cnt = Bus(length, name='cnt')

    @seq_logic(clk_i.posedge)
    def logic_b():
        cnt.next = cnt + 1

    @comb_logic
    def logic_a():
        led_o.next = cnt[length-1]

clk = Wire(name='clk')
led = Wire(name='led')
blinker(clk_i=clk, led_o=led, length=3)

# Run a simulation of the LED blinker.
clk_sim(clk, num_cycles=16)

# Not executing in Jupyter, so don't display waveforms.
#show_waveforms()

# Show the simulation results as a table.
show_text_table()

# Output the Verilog code for the blinker.
toVerilog(blinker, clk_i=clk, led_o=led, length=22)

# Create the pin constraint file.
with open('blinker.pcf', 'w') as pcf:
    pcf.write(
'''
set_io led_o 99
set_io clk_i 21
'''
    )

Then, just execute the file with Python:

In [18]:
!python blinker.py
  Time    clk    cnt    led
------  -----  -----  -----
     0      0      0      0
     1      1      1      0
     2      0      1      0
     3      1      2      0
     4      0      2      0
     5      1      3      0
     6      0      3      0
     7      1      4      1
     8      0      4      1
     9      1      5      1
    10      0      5      1
    11      1      6      1
    12      0      6      1
    13      1      7      1
    14      0      7      1
    15      1      0      0
    16      0      0      0
    17      1      1      0
    18      0      1      0
    19      1      2      0
    20      0      2      0
    21      1      3      0
    22      0      3      0
    23      1      4      1
    24      0      4      1
    25      1      5      1
    26      0      5      1
    27      1      6      1
    28      0      6      1
    29      1      7      1
    30      0      7      1
    31      1      0      0
<class 'myhdl.StopSimulation'>: No more events
blinker.py:31: UserWarning: 
    toVerilog(): Deprecated usage: See http://dev.myhdl.org/meps/mep-114.html
  toVerilog(blinker, clk_i=clk, led_o=led, length=22)

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
init..
cdone: high
reset..
cdone: low
flash ID: 0x20 0xBA 0x16 0x10 0x00 0x00 0x23 0x12 0x67 0x21 0x13 0x00 0x49 0x00 0x34 0x04 0x11 0x11 0x20 0x31
file size: 32220
erase 64kB sector at 0x000000..
programming..
reading..
VERIFY OK
cdone: high
Bye.

And that's it! The LED on your iCEstick board should be blinking.

Summary

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.

Notes

[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.