FPGA Python interface.
Project description
Introduction
The
fpyga
Python package simplifies workflow with Intel FPGAs. It can automatically generate verilog, control location assignments, compile, program, read/write to memory, handle multiple FPGAs connected simultaneously, and archive projects all from within Python or Jupyter notebooks. See the docs with examples at https://noeloikeau.github.io/fpyga/.
Installation
pip install fpyga
Usage
import fpyga
from fpyga import *
Preliminaries
Before using this tutorial, you should:
- Install Quartus
- Have FPGAs connected
- Have a Quartus project open "DE10"
- Generate a "MyPLL" Phase-Lock-Loop megafunction from the Quartus GUI
- Similarly generate a "DE10.cdf" file in ..\output_files folder using the Programming GUI
- Connect PIN_V11 to FPGA_CLK1_50 as input to the PLL using the Assignments GUI
- Ensure path variables point to the Quartus scripting folder to run command-line arguments
Note: this page was previously titled quartusfpga
, and included several utilities which have since been separated into the sidis
package following a full re-write. quartusfpga
is no longer maintained and has been removed.
Project Class
We will use the Project
class to smoothe over the process of writing location assignments, compiling, programming, reading/writing to memory, and archiving in Quartus. All the methods here are derived from functions defined in the other pages, which may be used as stand-alones if the class functionality isn't required.
P=Project(projectname='DE10',
projdir=r'C:\Users\Noeloikeau Charlot\Desktop\Research\Quartus',
chip=DE10,
templatefile=r'C:\Users\Noeloikeau Charlot\Desktop\Research\qdev\Repressilator_Template.txt',
__N__=3,__T__=8,__D__=1)
The templatefile
will fill in the Verilog project automatically from a base text file where we implement a Ring Oscillator, as explained below. If you're happy with Verilog and just want to see how to interface with the FPGA, feel free to skip to the following sections.
sidis Templates
We use the sidis
module for several utilities, including the Template
class, which lets you write Python formatting functions inside text documents. This means you can easily replace and expand upon the native constructs of hardware programming language like Verilog. As an example, this notebook deals with a ring of three cyclic inverters implemented on two DE10 Nano Cyclone V Intel FPGAs.
s=sidis.Template('Repressilator_Template.txt')
print(s.temp[11:19]) #These are the relevant lines for this example
localparam N=__N__; //number of nodes
localparam T=__T__; //number of measurements
localparam D=__D__; //number of inverter pairs between nodes
wire [N-1:0] node; //ring of cyclic inverters
wire [N-1:0] init; //initial state of ring
wire node_delayed_{0}_{1} /*synthesis keep*/; ZIP __N__, lambda i:i, lambda i:(i-1)%__N__
DelayLine #(D) DL{0}_{1} (node[{0}], node_delayed_{0}_{1}); ZIP __N__, lambda i:(i-1)%__N__, lambda i:i
assign node[{0}]= enable ? ~node_delayed_{1}_{0} : init[{0}]; ZIP __N__, lambda i:i, lambda i:(i-1)%__N__
The special characters __N__
, __T__
etc. will be replaced by the string interpreter in the FillForm
function. The special flag ZIP
is a custom function defined in this Python module which formats the text according to lambda functions mapped over an iterable. This produces Verilog assignments automatically at compilation, like so:
s.fill(__N__=3,__T__=8,__D__=1)
print(s.plate)
localparam N=3; //number of nodes
localparam T=8; //number of measurements
localparam D=1; //number of inverter pairs between nodes
wire [N-1:0] node; //ring of cyclic inverters
wire [N-1:0] init; //initial state of ring
wire node_delayed_0_2 /*synthesis keep*/;
wire node_delayed_1_0 /*synthesis keep*/;
wire node_delayed_2_1 /*synthesis keep*/;
DelayLine #(D) DL2_0 (node[2], node_delayed_2_0);
DelayLine #(D) DL0_1 (node[0], node_delayed_0_1);
DelayLine #(D) DL1_2 (node[1], node_delayed_1_2);
assign node[0]= enable ? ~node_delayed_2_0 : init[0];
assign node[1]= enable ? ~node_delayed_0_1 : init[1];
assign node[2]= enable ? ~node_delayed_1_2 : init[2];
These methods however are much more general; see the sidis
page https://noeloikeau.github.io/sidis/ .
Devices and Assignments
Now we want to assign some locations using functions from the
Scripting
page to locations defined on theDevices
page.
We have 3 nodes in our ring, let's see if we can place them at LAB (10,10) on the DE10
:
print(DE10(x=10,y=10),DE10(x=10,y=10,n=0))
LAB LABCELL
Looks like we're good. We use Project.set_loc
to set location assignments to the quartus project settings (.qsf) file:
[P.set_loc(x=10,y=10,n=3*i,name=f'node[{i}]',style='a') for i in range(3)]
Which writes
set_location_assignment LABCELL_X10_Y10_N0 -to "node[0]"
set_location_assignment LABCELL_X10_Y10_N3 -to "node[1]"
set_location_assignment LABCELL_X10_Y10_N6 -to "node[2]"
to a new file qsf_source.qsf
, and adds the argument source qsf_source.qsf
to our project DE10.qsf
, thereby assigning these locations to the node
logic elements comprising our ring of inverters to the (10,10)
LAB on the FPGA at the LABCELL coordinates N=0,3,6
.
We now compile, program, and archive our project with Project.Bootup
:
P.compile()
P.program_all()
P.archive()
The compilation was successful, the DE10
's were each programmed with the same DE10.sof
file, and a new file DE10-2020-09-12.qar
was generated, which contains everything needed for someone to boot up our entire project using the Quartus GUI under Tools->Archive.
Memory Interface
Let's take a look at our FPGAs and how to interface with their memory instances, as explained in the Memory
page:
P.get_insts()
[['DE-SoC [USB-1]',
'@2: 5CSEBA6(.|ES)/5CSEMA6/.. (0x02D020DD)',
[['0', '1', '1', 'RW', 'ROM/RAM', 'BIT'],
['1', '8', '3', 'RW', 'ROM/RAM', 'READ'],
['2', '2', '3', 'RW', 'ROM/RAM', 'WRIT']]],
['DE-SoC [USB-2]',
'@2: 5CSEBA6(.|ES)/5CSEMA6/.. (0x02D020DD)',
[['0', '1', '1', 'RW', 'ROM/RAM', 'BIT'],
['1', '8', '3', 'RW', 'ROM/RAM', 'READ'],
['2', '2', '3', 'RW', 'ROM/RAM', 'WRIT']]]]
The two FPGA's are connected on ports USB-1
and USB-2
. Their names are given by the second string, 5CSEBA6...
. In fact, there are also SoC
s (system-on-a-chip) connected to these ports, which we could see using GetPorts
. But we automatically filtered the SOC
s out, as for our purposes we want to program the FPGAs directly.
The last arguments in the FPGA lists [0,1,1,RW,ROM/RAM,BIT]...
are memory instances. They are formatted like:
- Instance index, used in read/write calls
- Number of words at that index
- Number of bits per word
- Read/write functionality
- Type of memory (here ROM/RAM)
- Custom name string defined in Verilog module
Here, instance 0
is an enable
bit which we use to begin the dynamics of the 3-bit ring. When the enable bit is low, the ring is set to its initial state, specified by instance 2
, which can hold two initial configurations of the 3 node network. When the enable bit is high, the ring begins to invert itself, and the state of each node state is recorded in instance 1
at approximately 1ns
intervals a total of 8
times.
Since we haven't flipped the enable bit or set any initial conditions, the ring should be all 0s on both FPGAs. We check this using Project.ReadAllData:
P.read_all(inst=1)
array([[[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]],
[[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]]], dtype=uint8)
The first 8x3
column is the state of the 3-node network over 8ns on FPGA DE-SoC [USB-1]
, the second on DE-SoC [USB-2]
. This shows us that both of our FPGA rings are held fixed at zero. Now, let's set the initial conditions to 001
on instance 2
using Project.WriteAllData
:
P.write_all(inst=2,data=[[0,0,1],[0,0,1]]) #We have two initializations, but only the 0th is used
Now let's make sure it wrote correctly:
P.read_all(inst=2)
array([[[0, 0, 1],
[0, 0, 1]],
[[0, 0, 1],
[0, 0, 1]]], dtype=uint8)
Good. Now, we flip the enable bit to 1:
P.write_all(inst=0,data=[[1]])
and read the state of each network over time:
P.read_all(inst=1)
array([[[0, 0, 0],
[1, 1, 0],
[1, 0, 1],
[0, 0, 1],
[1, 1, 0],
[1, 0, 1],
[0, 1, 1],
[1, 0, 1]],
[[0, 0, 0],
[0, 1, 0],
[1, 0, 0],
[0, 0, 1],
[0, 1, 0],
[1, 0, 0],
[0, 0, 1],
[0, 0, 0]]], dtype=uint8)
So we managed to create and measure dynamical systems from multiple FPGAs at nanosecond intervals all from Python! This concludes the tutorial, but there are many other possibilities using this package, check out some of the other pages.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file fpyga-0.0.2.tar.gz
.
File metadata
- Download URL: fpyga-0.0.2.tar.gz
- Upload date:
- Size: 21.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.25.1 setuptools/54.2.0 requests-toolbelt/0.9.1 tqdm/4.36.1 CPython/3.7.4
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | efbc7ce84df5455047cef2f6022b4725c1623aabc50a33dcf16cf46c50c1eab0 |
|
MD5 | 8f52f0c9245e8f5f9a90bdfa676ddf3c |
|
BLAKE2b-256 | 2e76180e059f5d96e191c0942da2a518a80097cd01f7b8b29e1ff9c064f369d8 |
File details
Details for the file fpyga-0.0.2-py3-none-any.whl
.
File metadata
- Download URL: fpyga-0.0.2-py3-none-any.whl
- Upload date:
- Size: 16.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.25.1 setuptools/54.2.0 requests-toolbelt/0.9.1 tqdm/4.36.1 CPython/3.7.4
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9726213b384697da491ca8f31a462ccd9ca9ed4ffd5158ab1c349660e489a73c |
|
MD5 | 34520be003df99ea334869e2bfbc6e98 |
|
BLAKE2b-256 | 57ef9354101c1c6012e7c5eac3aacbcd9c64ce65b2db1135205a749c5c9fc6d9 |