El presente artículo demonstrará como se puede implementar un SPI soft-core via Verilog HDL que sirve como la conneción principal para controlar la máquina de estados finitos dentro del Lattice ICE40 FPGA UltraPlus Breakout Board.,
Este demo está compuesto por un programa desde una computadora con un sistema operativo de Linux y una aplicación escrita en lenguaje C y compilada para el mismo. Esta aplicación ejerce la función de comunicación entre la computadora y la plataforma Lattice ICE40 FPGA UltraPlus Breakout Board., via el circuito integrado FTDI en la misma. La plataforma Lattice ICE40 FPGA UltraPlus Breakout Board. va a implementar un esclavo soft 4 wire SPI en Verilog HDL que sirve para establecer esa comunicación con la computadora desde el FPGA. Aquí está el diagrama de blolque del demo,
El módulo SPI soft-core esta definido en el archivo spi_slave.v Verilog HDL module el cuál se mostrará luego. La comunicación se realize con paquetes de datos de Communication de 4 Bytes. El primer byte es el opcode en la linea MOSI, en la linea MISO está el byte de status. Los otros dos bytes son usadas para transmitir la data del usuario. Los siguientes op codes son establecidos,
OPCODE | Description
0x0 | Nop, does nothing
0x1 | Init, starts the state machine on the fpga side
0x2 | Writes 16bits to be inverted on the fpga
0x3 | Reads the 16 inverted bits on the next communcation
0x4 | Writes led value to be on the breakout board. (RGB, LSB is R)
0x5 | Reads which of the RGB led is on, on the next SPI communication
0x6 | The host computer will send 4*24bits values (vector)
0x7 | Reads the 4*24bits values
El programa esta en el archivo llamado main.c en la computadora como se muestra a continuación,
#include <stdio.h>
#include "spi_lib.h"
#define SPI_NOP 0x00
#define SPI_INIT 0x01
#define SPI_SEND_BIT_INV 0x02
#define SPI_READ_REQ_BIT_INV 0x03
#define SPI_SET_LED 0x04
#define SPI_READ_REQ_LED 0x05
#define SPI_SEND_VEC 0x06
#define SPI_READ_VEC 0x07
int main()
{
spi_init();
uint8_t no_param[3] = {0x00, 0x00, 0x00};
uint8_t spi_status = 0;
uint8_t data_read[3];
uint8_t val_inv[3] = {0x38, 0xAE, 0x3B};
/*
assign LED_R = ~led[0];
assign LED_G = ~led[1];
assign LED_B = ~led[2];
*/
uint8_t val_led_red[3] = {0x00, 0x0, 0x01};
uint8_t val_led_yellow[3] = {0x00, 0x00, 0x03};
uint8_t val_led_green[3] = {0x00, 0x00, 0x02};
uint8_t val_led_blue[3] = {0x00, 0x00, 0x04};
uint8_t val_led_purple[3] = {0x00, 0x00, 0x05};
uint8_t val_led_turquoise[3] = {0x00, 0x00, 0x06};
uint8_t val_led_white[3] = {0x00, 0x00, 0x07};
spi_send(SPI_INIT, no_param, NULL); // init
spi_send(SPI_SEND_BIT_INV, val_inv, &spi_status); // send values bit inversion
printf("send inversion data, status: 0x%x\n", spi_status);
spi_send(SPI_READ_REQ_BIT_INV, no_param, NULL); //send read request
spi_read(data_read, &spi_status); // read data inversion
for (size_t i = 0; i < 3; i++) {
printf("bit inversion read idx %i: 0x%x, should be 0x%x\n", i, data_read[i], 0xFF&~val_inv[i]);
}
printf("status: 0x%x\n", spi_status);
spi_send(SPI_SET_LED, val_led_yellow, &spi_status); // led yellow
printf("send yellow led, status: 0x%x\n", spi_status);
spi_send(SPI_READ_REQ_LED, no_param, NULL); //send led read request
spi_read(data_read, &spi_status); // read led data
printf("led_data read: 0x%x, 0x%x, 0x%x, status:0x%x\n", data_read[2], data_read[1], data_read[0], spi_status);
//wait 2sec before setting led in blue
usleep(2000*1000);
spi_send(SPI_SET_LED, val_led_blue, &spi_status); // set led blue
printf("send blue led, status: 0x%x\n", spi_status);
//send 4 values the fastest possible
for (size_t i = 0; i < 4; i++) {
int send_value = (i+1)*16;
spi_send24b(SPI_SEND_VEC, send_value, &spi_status);
printf("sent vector val: 0x%x, status: 0x%x\n", send_value, spi_status);
}
usleep(1000);
//send read request, the fpga will send the 4 values
spi_send(SPI_READ_VEC, no_param, &spi_status);
printf("sent read req vector, status: 0x%x\n", spi_status);
//read values the fastest possible
for (size_t i = 0; i < 4; i++) {
spi_read(data_read, &spi_status);
printf("vector read: 0x%x, 0x%x, 0x%x, status:0x%x\n", data_read[2], data_read[1], data_read[0], spi_status);
}
int delay = 2000*100;
while(1)
{
spi_send(SPI_SET_LED, val_led_yellow, &spi_status); // led yellow
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
spi_send(SPI_SET_LED, val_led_blue, &spi_status); // led blue
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
spi_send(SPI_SET_LED, val_led_red, &spi_status); // led red
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
spi_send(SPI_SET_LED, val_led_green, &spi_status); // led green
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
spi_send(SPI_SET_LED, val_led_purple, &spi_status); // led purple
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
spi_send(SPI_SET_LED, val_led_turquoise, &spi_status); // led turquoise
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
spi_send(SPI_SET_LED, val_led_white, &spi_status); // led white
//printf("send yellow led, status: 0x%x\n", spi_status);
//wait 2sec before setting led in blue
usleep(delay);
}
return 0;
}
Luego de que este programa anfitrion se compila en la computadora con Linux, se usa la siguiente definición de la máquina de estados finitos en el archivo verilog HDL top.v. Este archivo es usado en este demo e implementa la máquina de estados finitos dentro del FPGA,
//opcodes:
//0x00 nop
//0x01 init
//0x02 write 16bits inverted
//0x03 read 16bits inverted
//0x04 write leds (16bits LSB)
//0x05 read leds (16bits LSB)
//0x06 write vector, the computer will send 4 * 24bit values
//0x07 read vector, the fpga will send 4 * 24bit 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);
reg spi_reset;
wire spi_wr_buffer_free;
reg spi_wr_en;
reg [23:0] spi_wr_data;
wire spi_rd_data_available;
reg spi_rd_data_available_buf;
reg spi_rd_ack;
wire [31:0] spi_rd_data;
parameter NOP=0, INIT=1, WR_INVERTED=2, RD_INVERTED=3, WR_LEDS=4, RD_LEDS=5, WR_VEC=6, RD_VEC=7;
spi_slave spi_slave_inst(.clk(clk), .reset(spi_reset),
.SPI_SCK(SPI_SCK), .SPI_SS(SPI_SS), .SPI_MOSI(SPI_MOSI), .SPI_MISO(SPI_MISO),
.wr_buffer_free(spi_wr_buffer_free), .wr_en(spi_wr_en), .wr_data(spi_wr_data),
.rd_data_available(spi_rd_data_available), .rd_ack(spi_rd_ack), .rd_data(spi_rd_data)
);
reg [2:0] led;
reg [31:0] spi_recv_data_reg;
reg handle_data;
reg [23:0] reg_bits_inversion;
reg [23:0] vector [0:4];
//reg [7:0] vec_ptr;
reg [2:0] vec_ptr;
reg sending_vector;
assign LED_R = ~led[0];
assign LED_G = ~led[1];
assign LED_B = ~led[2];
integer i;
initial begin
for(i = 0; i < 4; i=i+1) begin
vector[i] = 0;
end
spi_reset = 0;
spi_wr_en = 0;
spi_wr_data = 0;
spi_rd_ack = 0;
vec_ptr = 0;
sending_vector = 0;
led = 0;
spi_recv_data_reg = 0;
handle_data = 0;
end
always @(posedge clk)
begin
//defaults
spi_rd_ack <= 0;
spi_wr_en <= 0;
spi_rd_data_available_buf <= spi_rd_data_available;
if(spi_rd_data_available == 1 && spi_rd_data_available_buf == 0) begin // rising edge
spi_recv_data_reg <= spi_rd_data;
spi_rd_ack <= 1;
handle_data <= 1;
end
//sends the 4-24bit vector with spi
if(sending_vector == 1 && spi_wr_buffer_free == 1) begin
spi_wr_en <= 1;
spi_wr_data[23:0] <= vector[vec_ptr];
if(vec_ptr < 3) begin
vec_ptr <= vec_ptr+1;
end else begin
vec_ptr <= 0;
sending_vector <= 0;
end
end
if(handle_data == 1) begin
case(spi_recv_data_reg[7:0])
WR_INVERTED: begin
reg_bits_inversion[23:0] <= ~spi_recv_data_reg[31:8];
end
RD_INVERTED: begin
spi_wr_en <= 1;
spi_wr_data[23:0] <= reg_bits_inversion[23:0];
end
WR_LEDS: begin
led[2:0] <= spi_recv_data_reg[26:24];
end
RD_LEDS: begin
spi_wr_en <= 1;
spi_wr_data[23:0] <= {21'b0 ,led[2:0]};
end
WR_VEC: begin
vector[vec_ptr] <= spi_recv_data_reg[31:8];
if(vec_ptr < 3)
begin
vec_ptr <= vec_ptr+1;
end else begin
vec_ptr <= 0;
end
end
RD_VEC: begin
sending_vector <= 1;
end
endcase
handle_data <= 0;
end
end
endmodule
El correspondiente esclavo SPI soft HDL es el archivo spi_slave.v,
// receive: byte2 | byte1 | byte0 | opcode/status
//read all the data, but can write only the two bytes as opcode contains metadata
module spi_slave(input wire clk, input wire reset,
input wire SPI_SCK, input wire SPI_SS, input wire SPI_MOSI, output wire SPI_MISO,
output wire wr_buffer_free, input wire wr_en, input wire [23:0] wr_data,
output reg rd_data_available, input wire rd_ack, output reg [31:0] rd_data
);
reg [4:0] counter_read; //max 32
reg [1:0] spi_clk_reg;
reg [1:0] spi_ss_reg;
wire spi_ss_falling_edge;
wire spi_ss_rising_edge;
reg [1:0] mosi_reg;
reg miso_out_reg;
reg [7:0] state_rd;
reg wr_reg_full;
reg [23:0] wr_data_reg; //written data to send to spi/miso
reg wr_queue_full;
reg [23:0] wr_data_queue; //waiting to be written in the register, avoid a write while communcating with SPI
reg buffer_rd_ack;
reg [31:0] rd_data_local;
//states
parameter IDLE = 0, INIT=IDLE+1, RD_WAIT_DATA=INIT+1, RD_WAIT_ACK=RD_WAIT_DATA+1, WR_WAIT_DATA=RD_WAIT_ACK+1, WR_WAIT_ACK=WR_WAIT_DATA+1;
assign SPI_MISO = miso_out_reg;
wire spi_clk_rising_edge;
wire spi_clk_falling_edge;
assign spi_clk_rising_edge = (spi_clk_reg[1:0] == 2'b01);
assign spi_clk_falling_edge = (spi_clk_reg[1:0] == 2'b10);
assign spi_ss_rising_edge = (spi_ss_reg[1:0] == 2'b01);
assign spi_ss_falling_edge = (spi_ss_reg[1:0] == 2'b10);
initial begin
counter_read = 0;
spi_clk_reg = 0;
spi_ss_reg = 0;
mosi_reg = 0;
miso_out_reg = 0;
state_rd = INIT;
wr_reg_full = 0;
wr_data_reg = 24'hcafe77;
wr_queue_full = 0;
wr_data_queue = 0;
buffer_rd_ack = 0;
rd_data = 0;
rd_data_local = 0;
rd_data_available = 0;
end
assign wr_buffer_free = ((~wr_queue_full) & (~wr_reg_full) & (~wr_en));
always @(posedge clk)
begin
if(reset == 1) begin
rd_data <= 0;
rd_data_local <= 0;
rd_data_available <= 0;
state_rd <= INIT;
end else begin
spi_clk_reg <= {spi_clk_reg[0], SPI_SCK};
mosi_reg <= {mosi_reg[0], SPI_MOSI};
spi_ss_reg <= {spi_ss_reg[0], SPI_SS};
if (spi_ss_falling_edge == 1 || spi_ss_rising_edge == 1) begin
counter_read <= 0;
end
if(spi_clk_rising_edge == 1'b1) begin //default on spi clk
miso_out_reg <= 0; //default
end
case (state_rd)
INIT : begin // wait the init opcode from host (0x1) and nothing else
if(spi_clk_rising_edge == 1'b1) begin
rd_data_local[31:0] <= {mosi_reg[0], rd_data_local[31:1]};
counter_read <= counter_read + 1;
if(counter_read == 5) begin //status, write master to slave successful
miso_out_reg <= 1;
end
if(counter_read >= 31) begin //finish recv
if(rd_data_local[8:1] == 8'h1) begin //received init opcode, otherwise ignore
state_rd <= RD_WAIT_DATA;
end
counter_read <= 0;
end
end
end
RD_WAIT_DATA : begin
if(spi_clk_rising_edge == 1'b1) begin
if(counter_read == 5 && rd_data_available == 0) begin //status, write master to slave successful
miso_out_reg <= 1;
end
if (wr_reg_full == 1) begin //something ready to be written
//bits 0-7 reserved for status, starting to write wr_data_reg
//one clock before to be sent the next on miso
if(counter_read == 6) begin //status, read master to slave successful
miso_out_reg <= 1;
end else if(counter_read >= 7 && counter_read < 31) begin
miso_out_reg <= wr_data_reg[0];
wr_data_reg[23:0] <= {wr_data_reg[0], wr_data_reg[23:1]};
end
end
rd_data_local[31:0] <= {mosi_reg[0], rd_data_local[31:1]};
counter_read <= counter_read + 1;
if(counter_read >= 31) begin //finish recv
if (wr_reg_full == 1) begin //something was written, now free
wr_reg_full <= 0;
wr_data_reg <= 24'h00; //clear write buffer
end
if(rd_data_available == 0) begin
rd_data_available <= 1;
rd_data <= {mosi_reg[0], rd_data_local[31:1]};
end
state_rd <= RD_WAIT_DATA;
counter_read <= 0;
end
end
end
default : begin
end
endcase
if(rd_ack == 1 && rd_data_available == 1 && buffer_rd_ack == 0) begin
buffer_rd_ack <= 1;
end
if(buffer_rd_ack == 1 && counter_read == 0) begin
rd_data_available <= 0;
buffer_rd_ack <= 0;
end
//write to the queue
if (wr_en == 1 && (~wr_reg_full) && (~wr_queue_full) ) begin
wr_queue_full <= 1;
wr_data_queue <= wr_data;
end
//move from queue to reg only when no com (counter_read == 0)
if(wr_queue_full == 1 && counter_read == 0) begin
wr_data_reg <= wr_data_queue;
wr_queue_full <= 0;
wr_reg_full <= 1;
end
end
end
endmodule
El siguiente video ilustra este demo,
Que tenga un buen día.
Este artículo está disponible en inglés aquí.
This article is available in english here.

