この記事では、LatticeのICE40 FPGA UltraPlusブレークアウトボード内部の有限ステートマシンを制御するための主要なインターフェースとして機能する、SPI内蔵ハードコアをVerilog HDLを用いて実装する方法を示します。
このデモは、前回のソフトコアSPIモジュールを使用したこちらのデモと似ていますが、今回のデモではLatticeの ICE40 FPGA に内蔵されているハードウェアSPIモジュールを使用しています。以下のブロック図は、今回のデモの構成を示しています。
ホストコンピュータはx86 Linuxコンピュータであり、ブレークアウトボード上のFTDIチップを介してFPGAと通信します。ホスト側ではftdi.hライブラリを使用したC言語プログラムを実行し、USB経由でFTDIチップと通信します。ホスト側のC言語プログラムは、指定した色でLEDを点灯させるなどのコマンドを送信でき、LatticeのFPGAはホストからの要求に応答したり、自らデータを送信したりすることができます。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;
}
通信は数バイトのデータからなるパケットで行われます。マスターはスレーブに対して8バイトのデータを送信でき、最初の1バイトはコマンドのオペコードとして使用されます。続く7バイトはパラメータとして使用されます。たとえばLEDコマンドでは、LEDの色が最初のバイトのパラメータとして指定されます。
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
top.vファイルには、単純なステートマシンが実装されています。このステートマシンの目的は、まず内部のSPIハードコアモジュールを初期化し、その後、受信パケットからオペコードを読み取り、要求を処理することです。以下にtop.vのVerilog HDLファイルを示します。
//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
ホスト側のC言語プログラムは、USBケーブルを介してLatticeの ICE40 FPGA UltraPlusブレークアウトボードに接続されたコンピュータ上で、以下のようにコンパイルされます(FTDIドライバがホストコンピュータに適切にインストールされていると仮定しています)。
DigiKey_Coffee_Cup: # gcc spi_lib.c main.c -o host -lftdi
これにより、hostという実行ファイルがホストコンピュータ上に生成されます。このプログラムは、USB経由で FTDIチップに接続し、さらにFPGA 内部のハードコアSPIインタフェースを通して、Latticeの ICE40 FPGA UltraPlusブレークアウトボード上のFPGA 内部の有限ステートマシンと通信するために使用されます。内部の有限ステートマシンは、ボード上のRGB LEDと接続されています。
最初のステップとして、以下のようにLatticeの ICE40 FPGA UltraPlusブレークアウトボードを設定します。
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
...
次に、配置配線ツールを実行します。
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%
...
続いて、FPGA用の .binファイルを以下のように作成します。
DigiKey_Coffee_Cup: # icepack top.asc top.bin
最後に、以下のコマンドでFPGAの設定を行います。
DigiKey_Coffee_Cup: # iceprog -S top.bin
または、書き込む場合、以下のコードを使用します。
DigiKey_Coffee_Cup: # iceprog top.bin
FPGAに設定を書き込んだ後、以下のようにホストプログラムを使用してFPGAと通信します。
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
上記に示したとおり、これでLatticeの ICE40 FPGA UltraPlusブレークアウトボードに内蔵されたSPIハードコアモジュールのデモは完了です。以下のビデオでは、前述のホスト側C言語プログラムで定義された手順が実行されている様子を示しています。
Latticeの ICE40 FPGA UltraPlusブレークアウトボードは、多くのアプリケーションに対応できる広いプロトタイプエリアを備えた強力なプラットフォームです。DigiKeyで入手可能です。
どうぞ素晴らしい一日を!
Este artículo está disponible en español aquí.
この記事はスペイン語でこちらからご覧いただけます。

