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.

