SPI 플래시

FPGA 보드에는 FPGA의 구성(configuration)을 저장하는 다리 8개짜리 작은 칩이 달려 있는 경우가 많습니다. 이 칩은 수 메가바이트 용량의 플래시 메모리이며, 직렬 프로토콜로 접근합니다. FPGA 구성이 차지하는 용량은 수십 킬로바이트에 불과합니다. 따라서 우리 데이터를 저장할 공간이 넉넉하게 남으며, 심지어 코드를 저장해 두고 FemtoRV32가 그 코드를 곧바로 실행하게 할 수도 있습니다! 사용할 수 있는 RAM이 6Kb뿐인 IceStick 같은 보드에서는 아주 반가운 소식입니다. 훨씬 큰 프로그램을 실행할 수 있고 RAM 전체를 데이터에 쓸 수 있기 때문입니다. 하지만 대가가 따릅니다. RAM보다 훨씬 느립니다.

시작하기 전에 꼭 알아 두어야 할 중요한 점이 있습니다(이것 때문에 저는 벽에 머리를 찧을 정도로 고생했습니다!). 일부 장치(예: IceBreaker)에서는 기본적으로, FPGA가 시작될 때 SPI 플래시에서 구성을 읽어 들인 직후 SPI 플래시가 "Deep Power Down" 모드로 들어갑니다. 그래서 보내는 명령을 모두 무시합니다!. 깨우는 방법은 두 가지입니다.

버전은 네 가지이며, 느린 것부터 빠른 것 순서로 나열했습니다. 각 버전은 이 파일에 구현되어 있습니다.

버전(사용하는 명령) 32비트 읽기당 사이클 수 특징
SPI_FLASH_READ 64 저속(50 MHz) 표준
SPI_FLASH_FAST_READ 72 고속(100 MHz) 더미 사이클 사용
SPI_FLASH_FAST_READ_DUAL_OUTPUT 56 고속 MOSI 방향 반전
SPI_FLASH_FAST_READ_DUAL_IO 44 고속 MISO와 MOSI 방향 반전

1단계: READ 명령(h03) 사용하기

먼저 SPI 플래시 모듈의 인터페이스를 살펴봅시다:

module MappedSPIFlash(
    input wire         clk,          // system clock
    input wire         rstrb,        // read strobe     
    input wire [17:0]  word_address, // address of the word to be read

    output wire [31:0] rdata,        // data read
    output wire        rbusy,        // asserted if busy receiving data             

                     // SPI flash pins
    output wire        CLK,  // clock
    output reg         CS_N, // chip select negated (active low)        
    output wire        MOSI, // master out slave in (data to be sent to flash)
    input wire         MISO  // master in slave out (data received from flash)
);

이 모듈은 FemtoRV32의 주소 공간에 매핑되므로, femtosoc 메모리 인터페이스에 잘 맞는 인터페이스가 필요합니다. 이 모듈에는 클록 clk, 데이터를 읽으려 할 때마다 하이가 되는 읽기 스트로브 rstrb, 그리고 읽으려는 32비트 워드의 주소를 담는 word_address가 있습니다. 데이터는 rbusy가 로우가 되는 즉시 rdata에 나타납니다. 다음으로 SPI 플래시 메모리의 핀들이 있습니다. 여기에는 클록 CLK, 칩 선택 CS_N(액티브 로우), 프로세서에서 SPI 플래시로 가는 데이터를 위한 MOSI(Master Out Slave In) 핀, 그리고 SPI 플래시에서 프로세서로 가는 데이터를 위한 MISO(Master In Slave Out) 핀이 있습니다.

SPI 프로토콜 자체는 매우 간단합니다. 데이터를 읽으려면 CS_N을 로우로 내린 다음, MOSI를 통해 읽기 명령 h03을 한 번에 한 비트씩 보내고, 이어서 24비트 주소를 보내야 합니다. 그러면 MISO에 데이터가 나타납니다. 데이터는 최상위 비트부터 송수신됩니다. CLK의 상승 에지는 비트의 정확히 한가운데에 와야 합니다. 비트 한가운데에 맞추는 일은 조금 번거롭습니다. 보통은 CLK를 반 클록만큼 시프트해야 합니다. DDR 전용 블록을 사용하면 그렇게 할 수 있지만, 그러면 설계의 이식성이 없어집니다(예를 들어 Ice40과 ECP5에서 방식이 다릅니다). 하지만 간단한 해결책이 있습니다. clk의 하강 에지에서 비트를 시프트하면 됩니다! 그러면 설계가 매우 간단해져서, 명령과 주소를 위한 시프터 하나와 수신 데이터를 위한 또 다른 시프터 하나가 있게 됩니다. 상태 기계를 둘 수도 있지만 꼭 필요하지는 않습니다. 보낼 비트 수가 0이 아니면 'sending' 상태에 있는 것입니다. 받을 비트 수가 0이 아니면 '수신' 상태에 있는 것입니다. 그리고 마지막 비트를 보낸 직후 '송신' 상태에서 '수신' 상태로 넘어갑니다!

   reg [5:0]  snd_bitcount;
   reg [31:0] cmd_addr;
   reg [5:0]  rcv_bitcount;
   reg [31:0] rcv_data;
   wire       sending   = (snd_bitcount != 0);
   wire       receiving = (rcv_bitcount != 0);
   wire       busy = sending | receiving;
   assign     rbusy = !CS_N; 
   
   assign  MOSI  = cmd_addr[31];
   initial CS_N  = 1'b1;
   assign  CLK   = !CS_N && clk; 

   // since least significant bytes are read first, we need to swizzle...
   assign rdata = {rcv_data[7:0],rcv_data[15:8],rcv_data[23:16],rcv_data[31:24]};
   
   always @(negedge clk) begin
      if(rstrb) begin
     CS_N <= 1'b0;
     cmd_addr <= {8'h03, 4'b0000,word_address[17:0], 2'b00};
     snd_bitcount <= 6'd32;
      end else begin
     if(sending) begin
        if(snd_bitcount == 1) begin
           rcv_bitcount <= 6'd32;
        end
        snd_bitcount <= snd_bitcount - 6'd1;
        cmd_addr <= {cmd_addr[30:0],1'b0};
     end
     if(receiving) begin
        rcv_bitcount <= rcv_bitcount - 6'd1;
        rcv_data <= {rcv_data[30:0],MISO};
     end
     if(!busy) begin
        CS_N <= 1'b1;
     end
      end
   end
endmodule

하지만 큰 제약이 하나 있습니다. READ 명령을 사용하면 속도가 50MHz로 제한됩니다. 더 빠르게 하려면 FASTREAD 명령을 사용해야 합니다.

2단계: FASTREAD 명령(h0b) 사용하기

FASTREAD 명령을 사용하려면 명령과 주소 바로 뒤에 8개의 "더미 비트"를 보내야 합니다. 이 더미 비트는 SPI 플래시가 주소를 읽고 데이터를 준비할 충분한 시간을 확보하는 데 필요합니다. 다음과 같이 간단히 구현할 수 있습니다:

      if(rstrb) begin
     CS_N <= 1'b0;
     cmd_addr <= {8'h0b, 4'b0000,word_address[17:0], 2'b00};
     snd_bitcount <= 6'd40; // instead of 32 before
     ...

3단계: DUAL INPUT FASTREAD 명령(h3b) 사용하기

이제 한 번에 한 비트 대신 두 비트씩 읽어서 훨씬 더 빠르게 만들 수 있습니다. 명령, 주소, 더미 비트를 보낸 뒤 MOSI의 방향을 반전시키면 됩니다. 이제 MOSI는 양방향 핀(inout)이라는 점에 유의하십시오. 이 핀에 Z를 쓰면 출력에서 입력으로 전환됩니다. 그러면 수신 시프터는 한 번에 두 비트씩 시프트하는데, 하나는 이전처럼 MISO에서 읽고 다른 하나는 MOSI에서 읽습니다:

module MappedSPIFlash(
    input wire         clk,          // system clock
    input wire         rstrb,        // read strobe     
    input wire [17:0]  word_address, // address of the word to be read

    output wire [31:0] rdata,        // data read
    output wire        rbusy,        // asserted if busy receiving data             

                     // SPI flash pins
    output wire        CLK,  // clock
    output reg         CS_N, // chip select negated (active low)        
    inout  wire        MOSI, // master out slave in (data to be sent to flash)
    input  wire        MISO  // master in slave out (data received from flash)
);

   wire MOSI_out;
   wire MOSI_in;
   wire MOSI_oe;

   assign MOSI = MOSI_oe ? MOSI_out : 1'bZ; 
   assign MOSI_in = MOSI;                   
   
   reg [5:0]  snd_bitcount;
   reg [31:0] cmd_addr;
   reg [5:0]  rcv_bitcount;
   reg [31:0] rcv_data;
   wire       sending   = (snd_bitcount != 0);
   wire       receiving = (rcv_bitcount != 0);
   wire       busy = sending | receiving;
   assign     rbusy = !CS_N; 

   assign  MOSI_oe = !receiving;   
   assign  MOSI_out = sending && cmd_addr[39];

   initial CS_N = 1'b1;
   assign  CLK  = !CS_N && clk; 

   // since least significant bytes are read first, we need to swizzle...
   assign rdata = {rcv_data[7:0],rcv_data[15:8],rcv_data[23:16],rcv_data[31:24]};

   always @(negedge clk) begin
      if(rstrb) begin
     CS_N <= 1'b0;
     cmd_addr <= {8'h3b, 4'b0000,word_address[17:0], 2'b00};
     snd_bitcount <= 6'd40;
      end else begin
     if(sending) begin
        if(snd_bitcount == 1) begin
           rcv_bitcount <= 6'd32;
        end
        snd_bitcount <= snd_bitcount - 6'd1;
        cmd_addr <= {cmd_addr[30:0],1'b0};
     end
     if(receiving) begin
        rcv_bitcount <= rcv_bitcount - 6'd2;
        rcv_data <= {rcv_data[29:0],MISO,MOSI_in};
     end
     if(!busy) begin
        CS_N <= 1'b1;
     end
      end
   end

endmodule

4단계: DUAL IO FASTREAD 명령(hbb) 사용하기

더 빠르게 할 수도 있습니다. 핀이 두 개 있으니, 주소를 보낼 때 두 핀을 모두 사용하면 어떻겠습니까? (그런 다음 방향을 반대로 바꾸어, 앞에서처럼 두 핀을 모두 사용해 데이터를 읽습니다.) 그런데 MOSI와 MISO라는 이름은 오해를 부를 수 있으므로, 이제는 대신 2비트 양방향 신호 IO를 사용합니다. 주의할 점이 있습니다. 명령은 앞에서와 마찬가지로 MOSI를 통해서만 전송됩니다(칩은 명령을 받기 전에는 우리가 두 선을 모두 사용할 것임을 알 수 없기 때문입니다!). 이를 위해 시프터 하나만 사용하지만(명령용 시프터와 주소용 시프터를 따로 두고 싶지 않습니다), 명령의 비트는 (bbyyttee 함수를 사용하여) 복제합니다.

module MappedSPIFlash(
    input wire         clk, // system clock
    input wire         rstrb, // read strobe        
    input wire [17:0]  word_address, // address of the word to be read

              
    output wire [31:0] rdata, // data read
    output wire        rbusy, // asserted if busy receiving data 

    output wire        CLK, // clock
    output reg         CS_N, // chip select negated (active low)        
    inout wire [1:0]   IO    // two bidirectional IO pins
);

   
   reg [4:0]  snd_clock_cnt; // send clock, 2 bits per clock (dual IO)
   reg [39:0] cmd_addr;      // command + address shift register
   reg [4:0]  rcv_clock_cnt; // receive clock, 2 bits per clock (dual IO)
   reg [31:0] rcv_data;      // received data shift register
   wire       sending   = (snd_clock_cnt != 0);
   wire       receiving = (rcv_clock_cnt != 0);
   wire       busy = sending | receiving;
   assign     rbusy = !CS_N; 

   // The two data pins IO0 (=MOSI) and IO1 (=MISO) used in bidirectional mode.
   reg IO_oe = 1'b1;
   wire [1:0] IO_out = cmd_addr[39:38];
   wire [1:0] IO_in  = IO;
   assign IO = IO_oe ? IO_out : 2'bZZ;
   
   initial CS_N = 1'b1;
   assign  CLK  = !CS_N && clk; // CLK needs to be disabled when not active.

   // since least significant bytes are read first, we need to swizzle...
   assign rdata={rcv_data[7:0],rcv_data[15:8],rcv_data[23:16],rcv_data[31:24]};

   // Duplicates the bits (used because when sending command, dual IO is
   // not active yet, and I do not want to have a separate shifter for
   // the command and for the args...).
   function [15:0] bbyyttee;
      input [7:0] x;
      begin
     bbyyttee = {
         x[7],x[7],x[6],x[6],x[5],x[5],x[4],x[4],
         x[3],x[3],x[2],x[2],x[1],x[1],x[0],x[0]
     };      
      end
   endfunction;
   
   always @(negedge clk) begin
      if(rstrb) begin
     CS_N  <= 1'b0;
     IO_oe <= 1'b1;              
     cmd_addr <= {bbyyttee(8'hbb), 4'b0000, word_address[17:0], 2'b00};
     snd_clock_cnt <= 5'd28; // cmd: 8 clocks  address: 12 clocks  dummy: 8 clocks
      end else begin
     if(sending) begin
        if(snd_clock_cnt == 1) begin
           rcv_clock_cnt <= 5'd16; // 32 bits (= 16 clocks) 
           IO_oe <= 1'b0;            
        end
        snd_clock_cnt <= snd_clock_cnt - 5'd1;
        cmd_addr <= {cmd_addr[37:0],2'b00};
     end
     if(receiving) begin
        rcv_clock_cnt <= rcv_clock_cnt - 5'd1;
        rcv_data <= {rcv_data[29:0],IO_in};
     end
     if(!busy) begin
        CS_N <= 1'b1;
     end
      end
   end
endmodule

5단계: LUT 골프

이 설계는 LUT 사용량 측면에서 상당히 무겁습니다. 이 설계에는 다음이 있습니다.

송신과 수신에 시프터 하나만 사용하고, 송신 중인지 수신 중인지 추적하는 dir 플립플롭을 추가하여 카운터도 하나만 사용하면 LUT를 많이(50~70개) 절약할 수 있습니다.

결과적으로 코드는 약간 읽기 어려워지지만, LUT 하나하나가 소중한 IceStick에서는 그럴 만한 가치가 충분합니다.

module MappedSPIFlash( 
    input wire         clk,          // system clock
    input wire         rstrb,        // read strobe     
    input wire [17:0]  word_address, // address of the word to be read

              
    output wire [31:0] rdata, // data read
    output wire        rbusy, // asserted if busy receiving data 

    output wire        CLK,  // clock
    output reg         CS_N, // chip select negated (active low)        
    inout wire [1:0]   IO    // two bidirectional IO pins
);

   reg [4:0]  clock_cnt; // send/receive clock, 2 bits per clock (dual IO)
   reg [39:0] shifter;   // used for sending and receiving

   reg        dir; // 1 if sending, 0 otherwise

   wire       busy      = (clock_cnt != 0);
   wire       sending   = (dir  && busy);
   wire       receiving = (!dir && busy);
   assign     rbusy     = !CS_N; 

   // The two data pins IO0 (=MOSI) and IO1 (=MISO) used in bidirectional mode.
   reg IO_oe = 1'b1;
   wire [1:0] IO_out = shifter[39:38];
   wire [1:0] IO_in  = IO;
   assign IO = IO_oe ? IO_out : 2'bZZ;
   
   initial CS_N = 1'b1;
   assign  CLK  = !CS_N && clk; // CLK needs to be disabled when not active.

   // since least significant bytes are read first, we need to swizzle...
   assign rdata={shifter[7:0],shifter[15:8],shifter[23:16],shifter[31:24]};

   // Duplicates the bits (used because when sending command, dual IO is
   // not active yet, and I do not want to have a separate shifter for
   // the command and for the args...).
   function [15:0] bbyyttee;
      input [7:0] x;
      begin
     bbyyttee = {
         x[7],x[7],x[6],x[6],x[5],x[5],x[4],x[4],
         x[3],x[3],x[2],x[2],x[1],x[1],x[0],x[0]
     };      
      end
   endfunction;  

   always @(negedge clk) begin
      if(rstrb) begin
     CS_N  <= 1'b0;
     IO_oe <= 1'b1;
     dir   <= 1'b1;
     shifter <= {bbyyttee(8'hbb), 4'b0000, word_address[17:0], 2'b00};
     clock_cnt <= 5'd28; // cmd: 8 clocks  address: 12 clocks  dummy: 8 clocks
      end else begin
     if(busy) begin
        shifter <= {shifter[37:0], (receiving ? IO_in : 2'b11)};
        clock_cnt <= clock_cnt - 5'd1;      
        if(dir && clock_cnt == 1) begin
           clock_cnt <= 5'd16; // 32 bits, 2 bits per clock
           IO_oe <= 1'b0;
           dir   <= 1'b0;
        end 
     end else begin
        CS_N <= 1'b1;
     end
      end
   end
endmodule

6단계: 더 빠르게 하기

더 빠르게 하기 위해 할 수 있는 일이 세 가지 있습니다.

참고 자료