Lattice ICE40 UltraPlus Breakout Board (Demo of SPI Built-in Core)

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!

1 Like