The present article will demonstrate how to implement a SPI build-in-core via Verilog HDL that will serve as the main conduit for controlling the internal finite state machine inside the Lattice ICE40 FPGA UltraPlus Breakout Board.,
This demo is similar to the previous one here where a soft-core SPI module was used instead, nevertheless, this demo uses the internal hardware SPI module on the Lattice ICE40 FPGA. The following block diagram illustrates this demo,
The host computer, which is a x86 linux computer communicates with the FPGA using the FTDI chip on the breakout board, it runs a C program using the ftdi.h library to be able to communicate with the FTDI chip using USB. A host C computer program can send commands such as lighting up the LEDS with a given color and the Lattice FPGA can answer to request from the host and send data itself. The main.c is as follows,
#include <stdio.h>
#include "spi_lib.h"
#define SPI_NOP 0x00
#define SPI_INIT 0x01
#define SPI_SEND_BIT_INV 0x02
#define SPI_SET_LED 0x04
#define SPI_SEND_VEC 0x06
#define SPI_READ_VEC 0x07
int main()
{
spi_init();
uint8_t no_param[7] = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
//last value is most important (sync of send/receive)
uint8_t init_param[7] = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x11};
uint8_t spi_status = 0;
uint8_t data_read[5];
uint8_t val_inv[7] = {0x38, 0xAE, 0x3B, 0x48, 0x0, 0x0, 0x0};
uint8_t val_led_yellow[7] = {0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
uint8_t val_led_blue[7] = {0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
if (spi_command_send(SPI_INIT, init_param) != 0){ // init
printf("trouble to get answer\n");
}
spi_command_send_recv(SPI_SEND_BIT_INV, val_inv, data_read); // send values bit inversion
for (size_t i = 0; i < 4; i++) {
printf("bit inversion read idx %lu: 0x%x, should be 0x%x\n", i, data_read[i+1], 0xFF&~val_inv[i]);
}
printf("LED command answer from fpga: the first byte should be the command (0x4), the second byte should by the LED colour\n");
spi_command_send_recv(SPI_SET_LED, val_led_yellow, data_read); // led yellow
printf("sent yellow led\n");
for (size_t i = 0; i < 5; i++) {
printf("received: [%lu]:%x\n", i, data_read[i]);
}
//wait 2sec before setting led in blue
usleep(2000*1000);
spi_command_send_recv(SPI_SET_LED, val_led_blue, data_read); // set led blue
printf("sent blue led\n");
for (size_t i = 0; i < 5; i++) {
printf("received: [%lu]:%x\n", i, data_read[i]);
}
//send 4 values the fastest possible
for (size_t i = 0; i < 4; i++) {
int send_value = (i+1)*0x01020304;
spi_command_send_32(SPI_SEND_VEC, send_value);
printf("sent vector val: 0x%x\n", send_value);
}
usleep(5000);
//send read request, the fpga will send the 4 values back, in order
for (size_t i = 0; i < 4; i++) {
spi_command_send_recv(SPI_READ_VEC, no_param, data_read);
printf("vector read: 0x%x, 0x%x, 0x%x, 0x%x\n", data_read[4], data_read[3], data_read[2], data_read[1]); //data_read[1] only the cmd
}
return 0;
}
Communication is done with packets of a few bytes of data, the master can send 8 bytes of data to the slave, the first byte is used as the opcode of the command. The seven next bytes are used as parameters, for example, the LED command, will have the colour of the LED as parameters on the first byte.
OPCODE | Description
0x0 | Nop, does nothing
0x1 | Init, starts the state machine on the fpga side, needs 0x11 as last parameter.
0x2 | Writes 32bits to be inverted by the fpga.
0x4 | Writes led value to be on the breakout board. (RGB, LSB is R)
0x6 | The host computer will send 4*32bits values (vector)
0x7 | Reads the 4*32bits values
A simple state machine is implemented in the top.v file, its goal is to initialize the internal SPI hard core module first, and then read the opcode from the incoming packet and process the request. Here is the top.v Verilog HDL file,
//opcodes:
//0x00 nop
//0x01 init
//0x02 write 32bits inverted
//0x04 write leds (8bits LSB)
//0x06 write vector, the computer will send 4 * 32bit values
//0x07 read vector, the fpga will send 4 * 32bit values
module top(input [3:0] SW, input clk, output LED_R, output LED_G, output LED_B, input SPI_SCK, input SPI_SS, input SPI_MOSI, output SPI_MISO, input [3:0] SW);
parameter NOP=0, INIT=1, WR_INVERTED=2, WR_LEDS=4, WR_VEC=6, RD_VEC=7;
//state machine parameters
parameter INIT_SPICR0=0, INIT_SPICR1=INIT_SPICR0+1, INIT_SPICR2=INIT_SPICR1+1, INIT_SPIBR=INIT_SPICR2+1, INIT_SPICSR=INIT_SPIBR+1,
SPI_WAIT_RECEPTION=INIT_SPICSR+1, SPI_READ_OPCODE=SPI_WAIT_RECEPTION+1, SPI_READ_LED_VALUE=SPI_READ_OPCODE+1,
SPI_READ_INIT=SPI_READ_LED_VALUE+1, SPI_SEND_DATA=SPI_READ_INIT+1, SPI_WAIT_TRANSMIT_READY=SPI_SEND_DATA+1,
SPI_TRANSMIT=SPI_WAIT_TRANSMIT_READY+1;
parameter SPI_ADDR_SPICR0 = 8'b00001000, SPI_ADDR_SPICR1 = 8'b00001001, SPI_ADDR_SPICR2 = 8'b00001010, SPI_ADDR_SPIBR = 8'b00001011,
SPI_ADDR_SPITXDR = 8'b00001101, SPI_ADDR_SPIRXDR = 8'b00001110, SPI_ADDR_SPICSR = 8'b00001111, SPI_ADDR_SPISR = 8'b00001100;
parameter COMMAND_SIZE=4;
reg [7:0] state_spi;
//hw spi signals
reg spi_stb; //strobe must be set to high when read or write
reg spi_rw; //selects read or write (high = write)
reg [7:0] spi_adr; // address
reg [7:0] spi_dati; // data input
wire [7:0] spi_dato; // data output
wire spi_ack; //ack that the transfer is done (read valid or write ack)
//the miso/mosi signals are not used, because this module is set as a slave
wire spi_miso;
wire spi_mosi;
wire spi_sck;
wire spi_csn;
SB_SPI SB_SPI_inst(.SBCLKI(clk), .SBSTBI(spi_stb), .SBRWI(spi_rw),
.SBADRI0(spi_adr[0]), .SBADRI1(spi_adr[1]), .SBADRI2(spi_adr[2]), .SBADRI3(spi_adr[3]), .SBADRI4(spi_adr[4]), .SBADRI5(spi_adr[5]), .SBADRI6(spi_adr[6]), .SBADRI7(spi_adr[7]),
.SBDATI0(spi_dati[0]), .SBDATI1(spi_dati[1]), .SBDATI2(spi_dati[2]), .SBDATI3(spi_dati[3]), .SBDATI4(spi_dati[4]), .SBDATI5(spi_dati[5]), .SBDATI6(spi_dati[6]), .SBDATI7(spi_dati[7]),
.SBDATO0(spi_dato[0]), .SBDATO1(spi_dato[1]), .SBDATO2(spi_dato[2]), .SBDATO3(spi_dato[3]), .SBDATO4(spi_dato[4]), .SBDATO5(spi_dato[5]), .SBDATO6(spi_dato[6]), .SBDATO7(spi_dato[7]),
.SBACKO(spi_ack),
.MI(spi_miso), .SO(SPI_MISO),
.MO(spi_mosi), .SI(SPI_MOSI),
.SCKI(SPI_SCK), .SCSNI(SPI_SS)
);
reg [2:0] led;
reg is_spi_init; //waits the INIT command from the master
reg [7:0] counter_read; //count the bytes to read to form a command
reg [7:0] command_data[7:0]; //the command, saved as array of bytes
reg [7:0] counter_send; //counts the bytes to send
reg [7:0] data_to_send; //buffer for data to be written in send register
//regs for the "vector" commands
reg [7:0] data_vector[15:0]; //4*32bits = 16*8bits
reg [3:0] counter_vector;
//leds output
assign LED_R = ~led[0];
assign LED_G = ~led[1];
assign LED_B = ~led[2];
initial begin
spi_reset = 0;
spi_wr_en = 0;
spi_wr_data = 0;
spi_rd_ack = 0;
led = 0;
spi_stb = 0;
spi_rw = 0;
spi_adr = 0;
spi_dati = 0;
is_spi_init = 0;
counter_send = 0;
counter_vector = 0;
state_spi = INIT_SPICR0;
end
always @(posedge clk)
begin
//default
spi_stb <= 0;
case (state_spi)
INIT_SPICR0 : begin //spi control register 0, nothing interesting for this case (delay counts)
spi_adr <= SPI_ADDR_SPICR0;
spi_dati <= 8'b00000000;
spi_stb <= 1;
spi_rw <= 1;
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= INIT_SPICR1;
end
end
INIT_SPICR1 : begin //spi control register 1
spi_adr <= SPI_ADDR_SPICR1;
spi_dati <= 8'b10000000; //bit7: enable SPI
spi_stb <= 1;
spi_rw <= 1;
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= INIT_SPICR2;
end
end
INIT_SPICR2 : begin //spi control register 2
spi_adr <= SPI_ADDR_SPICR2;
spi_dati <= 8'b00000001; //bit0: lsb first
spi_stb <= 1;
spi_rw <= 1;
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= INIT_SPIBR;
end
end
INIT_SPIBR : begin //spi clock prescale
spi_adr <= SPI_ADDR_SPIBR;
spi_dati <= 8'b00000000; //clock divider => 1
spi_stb <= 1;
spi_rw <= 1;
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= INIT_SPICSR;
end
end
INIT_SPICSR : begin //SPI master chip select register, absolutely no use as SPI module set as slave
spi_adr <= SPI_ADDR_SPICSR;
spi_dati <= 8'b00000000;
spi_stb <= 1;
spi_rw <= 1;
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= SPI_WAIT_RECEPTION;
counter_read <= 0;
end
end
SPI_WAIT_RECEPTION : begin
spi_adr <= SPI_ADDR_SPISR; //status register
spi_stb <= 1;
spi_rw <= 0; //read
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= SPI_WAIT_RECEPTION;
//wait for bit3, tells that data is available
if (is_spi_init == 0 && spi_dato[3] == 1) begin
state_spi <= SPI_READ_INIT;
end
if (is_spi_init == 1 && spi_dato[3] == 1) begin
if(counter_send < 6) begin //can only send 6 bytes back
state_spi <= SPI_WAIT_TRANSMIT_READY;
end else begin
state_spi <= SPI_READ_OPCODE;
end
end
end
end
SPI_WAIT_TRANSMIT_READY: begin
spi_adr <= SPI_ADDR_SPISR; //status registers
spi_stb <= 1;
spi_rw <= 0; //read
if(spi_ack == 1) begin
spi_stb <= 0;
//bit 4 = TRDY, transmit ready
if (spi_dato[4] == 1) begin
state_spi <= SPI_TRANSMIT;
end
end
end
SPI_TRANSMIT: begin
spi_adr <= SPI_ADDR_SPITXDR;
if(counter_send == 0) begin
spi_dati <= 8'b01000000;
end else begin
spi_dati <= data_to_send;
end
spi_stb <= 1;
spi_rw <= 1;
if(spi_ack == 1) begin
spi_stb <= 0;
counter_send <= counter_send + 1;
if (is_spi_init == 0) begin
state_spi <= SPI_READ_INIT;
end else begin
state_spi <= SPI_READ_OPCODE;
end
end
end
SPI_READ_INIT: begin
spi_adr <= SPI_ADDR_SPIRXDR; //read data register
spi_stb <= 1;
spi_rw <= 0; //read
if(spi_ack == 1) begin
spi_stb <= 0;
state_spi <= SPI_WAIT_RECEPTION;
command_data[counter_read] <= spi_dato;
if(spi_dato == 8'h11)begin
counter_read <= 0;
is_spi_init <= 1;
counter_send <= 0;
end
end
end
SPI_READ_OPCODE: begin
spi_adr <= SPI_ADDR_SPIRXDR; //read data register
spi_stb <= 1;
spi_rw <= 0; //read
if(spi_ack == 1) begin
spi_stb <= 0;
counter_read <= counter_read + 1;
state_spi <= SPI_WAIT_RECEPTION;
command_data[counter_read] <= spi_dato;
if( counter_read == 0 ) begin
data_to_send <= spi_dato;
end else if( command_data[0] == WR_INVERTED )begin
data_to_send <= ~spi_dato;
end else if( command_data[0] == WR_LEDS )begin
data_to_send <= spi_dato; //sends back what was written
end else if( command_data[0] == WR_VEC )begin //send vec from host
if(counter_read < 5)begin //only 4 bytes after the opcode are useful
data_vector[counter_vector] <= spi_dato;
counter_vector <= counter_vector + 1;
end
end else if( command_data[0] == RD_VEC )begin //send vec to host
if(counter_read < 5)begin //only 4 bytes after the opcode are useful
data_to_send <= data_vector[counter_vector];
counter_vector <= counter_vector + 1;
end
end
if(counter_read == 7) begin
counter_read <= 0;
counter_send <= 0;
if( command_data[0] == WR_LEDS )begin
led <= command_data[1][2:0]; //read the led value
end
end
end
end
endcase
end
endmodule
The host C program is compiled in the computer that is connected to the Lattice ICE40 FPGA UltraPlus Breakout Board. via the USB cable, as follows (assuming the FTDI drivers were installed properly in the host computer),
DigiKey_Coffee_Cup: # gcc spi_lib.c main.c -o host -lftdi
This will create an executable called host in the host computer that will used to communicate with the Lattice ICE40 FPGA UltraPlus Breakout Board. via the USB to the FTDI chip and then to the hard core SPI interface inside the FPGA and finally to the internal finite state machine of the FPGA. The internal finite state machine interfaces to the RGB LED on the board.
The first step is to configure the Lattice ICE40 FPGA UltraPlus Breakout Board. as follows,
DigiKey_Coffee_Cup: # yosys -p "synth_ice40 -top top -json top.json" top.v
....
=== top ===
Number of wires: 196
Number of wire bits: 757
Number of public wires: 196
Number of public wire bits: 757
Number of memories: 0
Number of memory bits: 0
Number of processes: 0
Number of cells: 473
SB_CARRY 27
SB_DFFE 157
SB_DFFESR 29
SB_DFFESS 2
SB_DFFSR 1
SB_LUT4 256
SB_SPI 1
...
Then proceed to do use the place and route tool,
DigiKey_Coffee_Cup: # nextpnr-ice40 --up5k --json top.json --pcf ../common/io.pcf --asc top.asc
...
Info: Device utilisation:
Info: ICESTORM_LC: 412/ 5280 7%
Info: ICESTORM_RAM: 0/ 30 0%
Info: SB_IO: 12/ 96 12%
Info: SB_GB: 1/ 8 12%
Info: ICESTORM_PLL: 0/ 1 0%
Info: SB_WARMBOOT: 0/ 1 0%
Info: ICESTORM_DSP: 0/ 8 0%
Info: ICESTORM_HFOSC: 0/ 1 0%
Info: ICESTORM_LFOSC: 0/ 1 0%
Info: SB_I2C: 0/ 2 0%
Info: SB_SPI: 1/ 2 50%
Info: IO_I3C: 0/ 2 0%
Info: SB_LEDDA_IP: 0/ 1 0%
Info: SB_RGBA_DRV: 0/ 1 0%
Info: ICESTORM_SPRAM: 0/ 4 0%
...
and create the .bin file for the FPGA as follows,
DigiKey_Coffee_Cup: # icepack top.asc top.bin
finally proceed with the configuration of the FPGA as,
DigiKey_Coffee_Cup: # iceprog -S top.bin
of if the flash is used then use this,
DigiKey_Coffee_Cup: # iceprog top.bin
After uploading the configuration to the FPGA then use the host program to communicate with the FPGA as follows,
DigiKey_Coffee_Cup: # ./host
init..
bit inversion read idx 0: 0xc7, should be 0xc7
bit inversion read idx 1: 0x51, should be 0x51
bit inversion read idx 2: 0xc4, should be 0xc4
bit inversion read idx 3: 0xb7, should be 0xb7
LED command answer from fpga: the first byte should be the command (0x4), the second byte should by the LED colour
sent yellow led
received: [0]:4
received: [1]:3
received: [2]:0
received: [3]:0
received: [4]:0
sent blue led
received: [0]:4
received: [1]:4
received: [2]:0
received: [3]:0
received: [4]:0
sent vector val: 0x1020304
sent vector val: 0x2040608
sent vector val: 0x306090c
sent vector val: 0x4080c10
vector read: 0x1, 0x2, 0x3, 0x4
vector read: 0x2, 0x4, 0x6, 0x8
vector read: 0x3, 0x6, 0x9, 0xc
vector read: 0x4, 0x8, 0xc, 0x10
As shown above this completes the demo of the hard-core SPI module inside the Lattice ICE40 FPGA UltraPlus Breakout Board. The following video shows this demo as the previous steps defined by the host C program shown above were executing,
The Lattice ICE40 FPGA UltraPlus Breakout Board., is a powerful platform with a large breadboard area for many applications and is available at DigiKey.
Have a great day!

