2019年5月12日日曜日

SPI通信(マスター)回路をVerilogで記述

SPI通信のプロトコルを理解するためにVerilogでSPI通信のマスターを記述した。今回はテストベンチでシミュレーションまで行った。
SCLK周波数の切り替えやCPOL・CPHAモード変更、CSイネーブル後のSCLK動作開始までのタイミング変更、送受信データのビット数変更などに対応させ、多くのデバイスで対応できるようにした。





SPI通信の概要

SCLKで同期をとってMOSIとMISOでデータのやり取りを行う通信。I2C通信と同じくらいメジャーな通信規格。おもにセンサー-マイコン間や基板上のIC間といった短い距離の通信インターフェースとして使われる。くわしくは別途参照。


SPI通信(マスター)のverilogソースコード

`timescale 1ns / 1ps

module SPI_IP#(
    parameter  DATA_WIDTH = 32,
    parameter  DATA_WIDTH_INIT = 0,
    parameter  CS_SCLK_TIME = 10, //clock count 
    parameter  SLAVE_NUM = 1,
    parameter  SLAVE_NUM_INIT = 0
    )
    (
    input   wire    CLK_in,
    input   wire    NRESET_in,
    
    output  wire    SCLK_out, 
    output  reg     [SLAVE_NUM-1:0] CS_out,
    output  wire    MOSI_out,
    input   wire    MISO_in,
    
    input   wire    [31:0] DATA_BIT_NUM_in,
    input   wire    [DATA_WIDTH -1:0] SEND_DATA_in,
    output  reg     [DATA_WIDTH -1:0] RECE_DATA_out,
    input   wire    [SLAVE_NUM -1:0] DEVICE_SELECT_in,  //1=enable(CS)
 
    input   wire    [31:0] SCLK_PERIOD_in,    // Edge width time( double is SCK period)
    input   wire    [1:0] POL_PHA_in,    // bit0=POL(0:SCLK idles low.1:SCLK idles high), bit1=PHA(0:Latches on neg edge,1:Latches on posi edge)    
    input   wire    START_in, 
 
    output  reg     [31:0] STATUS_out

    
    );
  
    //----------
    // Resister
    //----------    
    reg     SCLK_reg;
    reg [2:0] MISO_ff3_reg;
    
    reg     start_ff = 1'b0;
    reg     start_ris_reg;
    
    reg     [31:0] SCLK_PERIOD_reg;
    reg     [31:0] DATA_BIT_NUM_reg;
    
    reg     [31:0] CS_counter_reg;        
    
    reg     [31:0] SCLK_PERIOD_counter_reg;
    reg     [31:0] SCLK_edge_counter_reg;

    reg     [DATA_WIDTH -1:0] SEND_DATA_shift_reg;
    
    reg     [DATA_WIDTH -1:0] RECE_DATA_shift_reg;
    
    reg     done_reg;
    reg     busy_reg;
    
    assign SCLK_out = (POL_PHA_in[0] == 1'b0) ? SCLK_reg : ~SCLK_reg;
    
    
    //--------------------
    // Remove metastable
    //--------------------    
    always @( posedge CLK_in, negedge NRESET_in )
  begin
   if( NRESET_in == 1'b0 )
                MISO_ff3_reg <= 3'b0;
   else
    MISO_ff3_reg <= { MISO_ff3_reg[1], MISO_ff3_reg[0], MISO_in };
  end 
      
    //---------------
    // State Machine
    //--------------- 
 reg [5:0] cur, nxt;
 
    localparam  IDLE       = 5'b0_0001,
                DATA_SET        = 5'b0_0010,
                SCLK_ON_WAIT    = 5'b0_0100,
                SEND_AND_RECEIVE= 5'b0_1000,
                FIN             = 5'b1_0000;
                
    always @( posedge CLK_in, negedge NRESET_in )
  begin
   if ( NRESET_in == 1'b0 )
    cur <= IDLE;
   else
    cur <= nxt;
  end
       
 always @* 
  begin
   case ( cur )
    IDLE:
     if( start_ris_reg == 1'b1 )
      nxt = DATA_SET;
     else
      nxt = IDLE;
      
    DATA_SET:
      nxt = SCLK_ON_WAIT;
                        
    SCLK_ON_WAIT:
     if( CS_counter_reg >= CS_SCLK_TIME )
      nxt = SEND_AND_RECEIVE;
     else
      nxt = SCLK_ON_WAIT;
                        
    SEND_AND_RECEIVE:
     if( SCLK_edge_counter_reg == DATA_BIT_NUM_in && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 )
      nxt = FIN;
     else
      nxt = SEND_AND_RECEIVE;

    FIN:
     nxt = IDLE;
                    
    default:
     nxt = IDLE;
   
   endcase
  end      
        
    //--------------------------
    // Detect Start Rising Edge
    //--------------------------
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if( NRESET_in == 1'b0 )
                begin
                    start_ff <= 1'b0;
                    start_ris_reg <= 1'b0;
                end
            else
                begin
                    start_ff <=START_in;
                    start_ris_reg <= ~start_ff & START_in;
                end
        end 

    //----------------------
    // Data set on register
    //----------------------
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                begin
                    SCLK_PERIOD_reg <= 32'h0000_0000;
                    DATA_BIT_NUM_reg <= 32'h0000_0000;
                end
            else if ( nxt == DATA_SET )
                begin
                    SCLK_PERIOD_reg <= SCLK_PERIOD_in;
                    DATA_BIT_NUM_reg <= DATA_BIT_NUM_in;
                end
            else
                begin
                    SCLK_PERIOD_reg <= SCLK_PERIOD_reg;
                    DATA_BIT_NUM_reg <= DATA_BIT_NUM_reg;
                end
        end 
    
    //------------
    // CS control
    //------------
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                CS_counter_reg <= 32'h0000_0000;                    
            else if ( nxt == SCLK_ON_WAIT )
                CS_counter_reg <= CS_counter_reg + 32'h0000_0001;
            else
                CS_counter_reg <= 32'h0000_0000;
        end 
            
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                CS_out <= ~SLAVE_NUM_INIT;                    
            else if ( cur == DATA_SET && nxt == SCLK_ON_WAIT )
                CS_out <= ~DEVICE_SELECT_in;        
            else
                CS_out <= CS_out;
        end 

    //-----------
    // Make SCLK
    //-----------
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                SCLK_PERIOD_counter_reg <= 32'h0000_0000;
            else if ( SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 )
                SCLK_PERIOD_counter_reg <= 32'h0000_0000;
            else if ( nxt == SEND_AND_RECEIVE )
                SCLK_PERIOD_counter_reg <= SCLK_PERIOD_counter_reg + 32'h0000_0001;
            else
                SCLK_PERIOD_counter_reg <= 32'h0000_0000;
        end 
        
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                SCLK_reg <= 1'b0;
            else if ( cur == SCLK_ON_WAIT && nxt == SEND_AND_RECEIVE )
                SCLK_reg <= ~SCLK_reg;
            else if ( SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                SCLK_reg <= ~SCLK_reg;
            else if ( nxt == FIN )
                SCLK_reg <= 1'b0;
            else
                SCLK_reg <= SCLK_reg;
        end
            
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                SCLK_edge_counter_reg <= 32'h0000_0000;
            else if ( POL_PHA_in[1] == 1'b0 && SCLK_out == 1'b0 && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                SCLK_edge_counter_reg <= SCLK_edge_counter_reg + 32'h0000_0001;
            else if ( POL_PHA_in[1] == 1'b0 && SCLK_out == 1'b0 && cur == SCLK_ON_WAIT && nxt == SEND_AND_RECEIVE )
                SCLK_edge_counter_reg <= SCLK_edge_counter_reg + 32'h0000_0001;
            else if ( POL_PHA_in[1] == 1'b1 && SCLK_out == 1'b1 && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                SCLK_edge_counter_reg <= SCLK_edge_counter_reg + 32'h0000_0001;
            else if ( POL_PHA_in[1] == 1'b1 && SCLK_out == 1'b1 && (cur == SCLK_ON_WAIT && nxt == SEND_AND_RECEIVE) )
                SCLK_edge_counter_reg <= SCLK_edge_counter_reg + 32'h0000_0001;
            else if ( nxt != SEND_AND_RECEIVE )
                SCLK_edge_counter_reg <= 32'h0000_0000;
            else
                SCLK_edge_counter_reg <= SCLK_edge_counter_reg;
        end 

    //-----------
    // Send data
    //-----------
    assign MOSI_out = SEND_DATA_shift_reg[DATA_WIDTH -1];
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                SEND_DATA_shift_reg <= DATA_WIDTH_INIT;                    
            else if ( nxt == DATA_SET && cur == IDLE )
                SEND_DATA_shift_reg <= SEND_DATA_in << (DATA_WIDTH - DATA_BIT_NUM_in);
            else if ( POL_PHA_in[1] == 1'b0 && SCLK_out == 1'b1 && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                SEND_DATA_shift_reg <= {SEND_DATA_shift_reg[DATA_WIDTH -2:0],1'b0};
            else if ( POL_PHA_in[1] == 1'b1 && SCLK_out == 1'b0 && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                SEND_DATA_shift_reg <= {SEND_DATA_shift_reg[DATA_WIDTH -2:0],1'b0};
            else if ( nxt == IDLE )
                SEND_DATA_shift_reg <= DATA_WIDTH_INIT;
            else
                SEND_DATA_shift_reg <= SEND_DATA_shift_reg;
        end 
            
    //--------------
    // Receive data
    //--------------
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                RECE_DATA_shift_reg <= DATA_WIDTH_INIT;
            else if( nxt == IDLE )
                RECE_DATA_shift_reg <= DATA_WIDTH_INIT;
            else if ( POL_PHA_in[1] == 1'b0 && SCLK_out == 1'b0 && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                RECE_DATA_shift_reg <= {RECE_DATA_shift_reg[DATA_WIDTH -2:0], MISO_ff3_reg[2]};
            else if ( POL_PHA_in[1] == 1'b0 && SCLK_out == 1'b0 && cur == SCLK_ON_WAIT && nxt == SEND_AND_RECEIVE )
                RECE_DATA_shift_reg <= {RECE_DATA_shift_reg[DATA_WIDTH -2:0], MISO_ff3_reg[2]};
            else if ( POL_PHA_in[1] == 1'b1 && SCLK_out == 1'b1 && SCLK_PERIOD_counter_reg == SCLK_PERIOD_reg - 32'h0000_0001 && nxt == SEND_AND_RECEIVE )
                RECE_DATA_shift_reg <= {RECE_DATA_shift_reg[DATA_WIDTH -2:0], MISO_ff3_reg[2]};
            else if ( POL_PHA_in[1] == 1'b1 && SCLK_out == 1'b1 && (cur == SCLK_ON_WAIT && nxt == SEND_AND_RECEIVE) )
                RECE_DATA_shift_reg <= {RECE_DATA_shift_reg[DATA_WIDTH -2:0], MISO_ff3_reg[2]};
            else
                RECE_DATA_shift_reg <= RECE_DATA_shift_reg;
        end
        
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                RECE_DATA_out <= DATA_WIDTH_INIT;
            else if ( nxt == FIN )
                RECE_DATA_out <= RECE_DATA_shift_reg;
            else
                RECE_DATA_out <= RECE_DATA_out;
        end 

    //--------
    // Status
    //--------
    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                done_reg <= 1'b0;
            else if( nxt == FIN )
                done_reg <= 1'b1;
            else if( start_ris_reg == 1'b1 )
                done_reg <= 1'b0;
            else
                done_reg <= done_reg;
        end 

    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                busy_reg <= 1'b0;
            else if( nxt == DATA_SET )
                busy_reg <= 1'b1;
            else if( nxt == IDLE )
                busy_reg <= 1'b0;
            else
                busy_reg <= busy_reg;
        end 

    always @( posedge CLK_in, negedge NRESET_in )
        begin
            if ( NRESET_in == 1'b0 )
                STATUS_out <= 32'h0000_0000;
            else
                STATUS_out <= {24'h00_0000, done_reg, busy_reg, nxt};
        end 

endmodule

パラメータについて

DATA_WIDTH:SPI通信で送受信するデータのビット幅を指定
DATA_WIDTH_INIT:上記パラメータで指定されたビット幅を持つレジスタのリセット用。ほんとは32’h0000というように記述しなければならないが、VIVADOではなぜか動作がおかしくなるので0と記述してある。(こことか読めばbit幅のパラメータ化がなんとなくわかるはず。http://www.darwin.esys.tsukuba.ac.jp/home/ohyou/verilog/parameter)
CS_SCLK_TIME:CSが立ち下がってからSCLKが動作し始めるまでの時間を指定。クロックカウントの値で指定する。
SLAVE_NUM:本SPIマスター回路につながるスレーブデバイスの数を指定。ここで指定した本数のCS出力信号が生成される。
SLAVE_NUM_INIT:DATA_WIDTH_INITと同様の存在。


入出力について

CLK_in:クロック
NRESET_in :リセット(負論理)

SCLK_out:SPI通信のSCLK信号
CS_out:SPI信号のCS信号
MOSI_out:SPI信号のMOSI信号
MISO_in:SPI信号のMISO信号

DATA_BIT_NUM_in:SPI通信で送受信するデータのビット数を入力
SEND_DATA_in:SPI通信で送信したいデータを入力
RECE_DATA_out:SPI通信で受信したデータが出力される
DEVICE_SELECT_in:SPI通信を行うデバイスを入力。1を立てたビットのCSがLOWになる

SCLK_PERIOD_in:SCLKの周期の1/2を入力。クロックカウントの値で入力する。(例:CLKが100MHzでここの値を10にしたとき、SCLKの周期は0.2μsとなる)
POL_PHA_in:CPOL、CPHAの設定を入力。SCLKは0bit目が0ならIDLE LOW、1ならIDLE HIGH。1bit目が0なら立下りエッジでデータをラッチ、1なら立ち上がりエッジでデータをラッチする。
START_in:SPI通信をスタートするとき1を入力する。回路内ではこの信号の立ち上がりエッジで動作するため、再度通信をスタートさせる場合は一度0にした後1にしてやる必要がある。

STATUS_out:ステータスを出力する。0-5bit:現在のステートマシンの位置、6bit:busy(1なら通信中)、7bit:done(1ならSPI通信終了している)


動作について

回路が動き始めるとまずはIDLE状態になる。
START_inの立ち上がりエッジを検出すると、DATA_SET状態になりSCLK_PERIOD_in、DATA_BIT_NUM_in、SEND_DATA_inの内容が内部レジスタに取り込まれ、DEVICE_SELECT_inで1が立っているbitと同じbitのCS_outがLOWになる。
その後、SCLK_ON_WAIT状態になり、パラメータCS_SCLK_TIMEで設定したクロックカウント分だけ待機する。
待機後、SEND_AND_RECEIVE状態になり、SCLK_PERIOD_in で設定したSCLK周期でDATA_BIT_NUM_inで設定したbit数ぶんの通信を行いFIN状態になり、IDLE状態に戻る。

DATA_BIT_NUM_in、SEND_DATA_in、DEVICE_SELECT_in、SCLK_PERIOD_in、POL_PHA_inといった入力信号はSTART_inの立ち上がりエッジまでには確定しておくこと。


テストベンチのソースコード

`timescale 1ns / 1ps

module SPI_IP_SIM_0;
  localparam PERIOD = 8;
  localparam END = 1000;
  
 
  reg   CLK_in;
  reg   NRESET_in;
  
  reg   MISO_in;
  reg   [31:0] DATA_BIT_NUM_in;
  reg   [31:0] SEND_DATA_in;
  reg   DEVICE_SELECT_in;
  reg   START_in;
  reg   [31:0] SCLK_PERIOD_in;
  reg   [1:0] POL_PHA_in;
    
  SPI_IP #() SPI_IP
     (
          .CLK_in(CLK_in),
          .NRESET_in(NRESET_in),
          .SCLK_out(),
          .CS_out(),
          .MOSI_out(),
          .MISO_in(MISO_in),
          .DATA_BIT_NUM_in(DATA_BIT_NUM_in),
          .SEND_DATA_in(SEND_DATA_in),
          .DEVICE_SELECT_in(DEVICE_SELECT_in),
          .RECE_DATA_out(),
          .STATUS_out(),
          .START_in(START_in),
          .SCLK_PERIOD_in(SCLK_PERIOD_in),
          .POL_PHA_in(POL_PHA_in)
      );
    
  always 
      begin
          CLK_in = 0; #(PERIOD/2);
          CLK_in = 1; #(PERIOD/2);
      end
      
  always 
      begin
          MISO_in = 0; #(59);
          MISO_in = 1; #(53);
      end
        
  initial 
      begin
          NRESET_in = 1;
          #(PERIOD*3) NRESET_in = 0;
          #(PERIOD*3) DATA_BIT_NUM_in = 0;
                      SEND_DATA_in = 0;
                      DEVICE_SELECT_in = 0;
                      START_in = 0;
                      SCLK_PERIOD_in = 0;
                      POL_PHA_in = 0;
          #(PERIOD*3) NRESET_in = 1;
          
          #(PERIOD*3) POL_PHA_in = 2'b10;
          #(PERIOD*3) DATA_BIT_NUM_in = 8;
                      SEND_DATA_in = 8'b10011011;
                      DEVICE_SELECT_in = 1;
                      SCLK_PERIOD_in = 10;
          #(PERIOD*3) START_in = 1;
          #(PERIOD*3) START_in = 0;
          #(2000) START_in = 1;
          #(PERIOD*3) START_in = 0;

          #(PERIOD * END);
          $stop;
      end

endmodule


シミュレーション結果

立ち上がりでデータラッチ
1001_1011という送信データをMSBから送っていることが確認できる。
また読み取りデータは1010_1001であり、読み取り後RECE_DATA_outから0xA9(1001_0101)が出力されていることがわかる。

立ち下がりでデータラッチ
1001_1011という送信データをMSBから送っていることが確認できる。
また読み取りデータは1001_0101であり、読み取り後RECE_DATA_outから0x95(1001_0101)が出力されていることがわかる。


今後の展開

まだ連続して通信を行わせたり、実機で動作させておらず適当に作ったため、本当にうまく動くかわからない。ソフトから扱いやすくするため、AXIバスからDATA_BIT_NUM_inにアクセスできるようIP化などを行う必要がある。

0 件のコメント:

コメントを投稿