LatticeのICE40 UltraPlusブレークアウトボード(SPI内蔵ハードコアのデモ)

この記事では、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í.

この記事はスペイン語でこちらからご覧いただけます。




オリジナル・ソース(English)