LUTs (Lookup Tables)

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()


import lattice ice40
import lattice mantle40

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()

curry and uncurry

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.

Compile, build and upload.


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


init..
cdone: high
reset..
cdone: low
flash ID: 0x20 0xBA 0x16 0x10 0x00 0x00 0x23 0x64 0x34 0x65 0x03 0x00 0x71 0x00 0x26 0x27 0x12 0x16 0xD3 0xE4
file size: 32220
erase 64kB sector at 0x000000..
programming..
reading..
VERIFY OK
cdone: high
Bye.

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


module main (input [3:0] J1, output [3:0] J3);
wire  inst0_O;
wire  inst1_O;
wire  inst2_O;
wire  inst3_O;
SB_LUT4 #(.LUT_INIT(16'h68EC)) inst0 (.I0(J1[0]), .I1(J1[1]), .I2(J1[2]), .I3(J1[3]), .O(inst0_O));
SB_LUT4 #(.LUT_INIT(16'h68EC)) inst1 (.I0(J1[0]), .I1(J1[1]), .I2(J1[2]), .I3(J1[3]), .O(inst1_O));
SB_LUT4 #(.LUT_INIT(16'h68EC)) inst2 (.I0(J1[0]), .I1(J1[1]), .I2(J1[2]), .I3(J1[3]), .O(inst2_O));
SB_LUT4 #(.LUT_INIT(16'h68EC)) inst3 (.I0(J1[0]), .I1(J1[1]), .I2(J1[2]), .I3(J1[3]), .O(inst3_O));
assign J3 = {inst3_O,inst2_O,inst1_O,inst0_O};
endmodule