At the heart of an FPGA are lookup tables (LUTs). The lattice ice40 series has 4-bit lookup tables. Each table is configured with 16 single bit values, and the output is selected with an input which is a 4-bit address.
This notebook demonstrates different ways to set the initial values in a LUT in python. It is easy to create behavioral specifications of LUT functions.
In [1]:
import magma as m
m.set_mantle_target("ice40")
Initialize the icestick. We will create 4 LUTs with 4-bits inputs and 4-bits outputs so we turn on 4 inputs and 4 outputs.
In [2]:
from loam.boards.icestick import IceStick
from mantle import LUT4
icestick = IceStick()
for i in range(4):
icestick.J1[i].input().on()
icestick.J3[i].output().on()
main = icestick.DefineMain()
One way to set the entry in a LUT is to pass in a python sequence of 0s and 1s.
In [3]:
lut1 = LUT4([0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0])
m.wire( lut1(main.J1[0], main.J1[1], main.J1[2], main.J1[3]), main.J3[0] )
A second way to set the entries is to use a function with 4 inputs. This function is called with all possible input values, and the corresponding LUT entry is set to the value returned by the function.
In [4]:
# Simple 2-bit ALU
# I0 and I1 are the inputs to the ALU
# I2 and I3 select the ALU function
def f(I0, I1, I2, I3):
if I3:
if I2: return I0^I1
else: return I0&I1
else:
if I2: return I0|I1
else: return I1
lut2 = LUT4(f)
m.wire( lut2(main.J1[0], main.J1[1], main.J1[2], main.J1[3]), main.J3[1] )
A third way is to create a LUT table is to give a 16-bit number. The i'th entry in the LUT is set equal to the i'th bit in the number.
In [5]:
lut3 = LUT4(0x68EC)
m.wire( lut3(main.J1[0], main.J1[1], main.J1[2], main.J1[3]), main.J3[2] )
There is a clever trick that can be used to create these 16-bit numbers.
First, define special valuesI0 through I3.
I0 has the value such that the bit in position i % 4 == 0 are set
and the others are clear.
I1 is such that if i % 4 == 1, the bits are set and otherwise clear.
Similarly, for I2 and I3.
The values define the identify function; that I0 = f(i0, i1, i2, i3) = i0, I1 = f(i0, i1, i2, i3) = i1, and so on.
In [6]:
ZERO = 0b0000000000000000
ONE = 0b1111111111111111
I0 = 0b1010101010101010
I1 = 0b1100110011001100
I2 = 0b1111000011110000
I3 = 0b1111111100000000
LUT4(ZERO)(i0,i1,i2,i3)==0
LUT4(ONE)(i0,i1,i2,i3)==1
LUT4(I0)(i0,i1,i2,i3)==i0
LUT4(I1)(i0,i1,i2,i3)==i1
LUT4(I2)(i0,i1,i2,i3)==i2
LUT4(I3)(i0,i1,i2,i3)==i3
Forming a python logical expression of I0, I1, I2, and I3
forms new functions
that compute the logical expression of their inputs.
Any python function that performs bitwise operations between its inputs can be used.
The LUT will then compute that function.
LUT4(I0^I1^I2^I3)(i0,i1,i2,i3)==i0^i1^i2^i3
LUT4((~I2&I0)|(I2&I1)(i0,i1,i2,i3)==i2?i1:i0
In [7]:
A = I0
B = I1
S0 = I2
S1 = I3
eqn = ((S0&S1)&(A^B))|((~S0&S1)&(A&B))|((S0&~S1)&(A|B))|((~S0&~S1)&(B))
assert eqn == 0x68EC
In [8]:
lut4 = LUT4(eqn)
m.wire( lut4(main.J1[0], main.J1[1], main.J1[2], main.J1[3]), main.J3[3] )
In [9]:
m.EndDefine()
By default, LUTs have 4 separate inputs.
Often, it is convenient to combine the
inputs into a single Bits(4). This can be done with the uncurry operator.
In [10]:
def ROM4(data):
return uncurry(LUT4(data))
The inverse of uncurry is curry.
In [11]:
def LUT4(data):
return curry(ROM4(data))
Another exampe is Mux2. Can curry(Mux2) to get an array of inputs.
In [12]:
m.compile("build/lut", main)
In [13]:
%%bash
cd build
yosys -q -p 'synth_ice40 -top main -blif lut.blif' lut.v
arachne-pnr -q -d 1k -o lut.txt -p lut.pcf lut.blif
icepack lut.txt lut.bin
iceprog lut.bin
If we inspect the compiled verilog, we see that our mantle LUT uses the SB_LUT4 primitive.
Note that each of these methods for initializing
a LUT resets in a LUT with the same entries.
In [14]:
%cat build/lut.v