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

El presente artículo demuestra como implementar el SPI build-in-core via Verilog HDL que sirve para implementar la comunicación entre la computadora y una máquina de estados finitos dentro del Lattice ICE40 FPGA UltraPlus Breakout Board.,

Este demo es similar al previo donde un soft-core SPI module fue usado, no obstante, este demo usa el módulo interno SPI en la plataforma Lattice ICE40 FPGA. El próximo diagrama de bloque ilustra el sistema,

La computadora, la cual es una x86 con el sistema operativo Linux se comunica con el FPGA usando el circuito integrado FTDI en la plataforma, corre un programa en C usando la libreria ftfi.h para comunicarse con el FTDI usando el USB. El programa anfitrión puede enviar comandos tales como prender los LEDS con un color especifico y el Lattice FPGA puede contestar al programa anfitrión para enviar la data. EL programa que implementa esto es el siguiente main.c,

#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;
}

La comunicación se realiza con paquetes de unos bytes de data, el anfitrión puede enviar 8 bytes de data hacia el esclavo, el primer byte se usa como el opcode del comando. Los próximos 7 son los parámetros, por ejemplo, el comando LED, contiene el color del LED como el primer 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

Un máquina de estados sencillo es implementada en el archivo top.v, su propósito fue inicializar el module interno SPI primeramente, entonces lee el opcode del paquete de data y procesar la solicitud apropiada. Aqui está el archivo top.v Verilog HDL que implementa esto,


//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

El programa en C que está en el anfitrión fue compilado en la computadora que está conectada a la plataforma Lattice ICE40 FPGA UltraPlus Breakout Board. via el cable USB, es como sigue (asumiendo que los drivers del FTDI fueron instalados apropiadamente en la computadora).,


DigiKey_Coffee_Cup: # gcc spi_lib.c main.c -o host -lftdi

Eso va a crear un ejecutable llamado host en la compuradora anfitriona para comunicarse con elLattice ICE40 FPGA UltraPlus Breakout Board. via el USB a el circuito integrado FTDI y luego al módulo de interfaz SPI y a la máquina de estados finitos dentro del FPGA. La máquina de estados finitos interna se conecta al RGB LED en la plataforma.

El primer paso es configurar el 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%

...

y se crea el archivo .bin para el FPGA como sigue,

DigiKey_Coffee_Cup: #  icepack top.asc top.bin

finalmente se procede a configurar el FPGA como sigue,

DigiKey_Coffee_Cup: #  iceprog -S top.bin

o se se desea hacer la configuración por flash así,

DigiKey_Coffee_Cup: #   iceprog top.bin

Luego de subir la configuración al FPGA entonces se usa el programa anfitrion host para comunicarse con el FPGA como sigue,

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

Como se muestra previamente esto completa el demo del modulo SPI hard-core dentro del Lattice ICE40 FPGA UltraPlus Breakout Board.

El siguiente video muestra este demo con los previos pasos definidos por el programa anfirtrion escrito en lenguaje C a medida que se ejecutan. La plataforma Lattice ICE40 FPGA UltraPlus Breakout Board., es una potente herramienta de desarollo para muchas aplicaciones y está disponible en DigiKey.

Que tenga un buen día.

Este artículo está disponible en inglés aquí.

This article is available in english here.

1 Like