SPI 簡介
SPI 全稱為 Serial Peripheral Interface,譯為串行外設接口。它是 Motorola 公司推出的一種相對高速的同步、全雙工的通信總線協(xié)議。
SPI 一般有以下幾個特點:
◆ 因為時鐘線的存在,SPI 是同步的串行通信總線。
◆ 因為 Master 與 Slave 之間存在兩根不同方向的數(shù)據(jù)線,所以 SPI 支持全雙工傳輸。
◆ SPI 通信協(xié)議簡單,數(shù)據(jù)傳輸速率較快,傳輸速率沒有特殊設定,一般在 10兆波特率以下,最高可支持 30Mbps 甚至 50Mbps。
◆ SPI 采用主從機通信模式,應用廣泛,多用于 EEPROM、Flash、實時時鐘 (RTC)、 數(shù)模轉(zhuǎn)換器 (ADC) 等模塊的通信。
◆ 缺點是沒有應答機制和校驗機制,不能確認是否接收到數(shù)據(jù)、是否傳輸有錯。
SPI 引腳
SPI 通信最少需要 4 根信號線,分別是 CSN、SCLK、MOSI 與 MISO。各個信號說明如下:
◆ CS/CSN: Chip Select,主設備產(chǎn)生的從設備片選信號。當 Slave 片選信號有效時,Slave 可被 Master 訪問并進行通信。一個 Master 可能有多個片選信號,但一個 Slave 只能有一個片選信號。CS 表示片選信號為高時開始數(shù)據(jù)傳輸,CSN 表示片選信號為低時開始數(shù)據(jù)傳輸。
◆ SCLK: Master 產(chǎn)生的時鐘信號,用于數(shù)據(jù)的同步傳輸。時鐘頻率決定了數(shù)據(jù)傳輸速率。
◆ MOSI: Mater Output Slave Input,主設備輸出、從設備輸入的數(shù)據(jù)線,用于 Master 向 Slave 進行數(shù)據(jù)傳輸。
◆ MISO: Mater Input Slave Output,主設備輸入、從設備輸輸出的數(shù)據(jù)線,用于 Slave 向 Master 進行數(shù)據(jù)傳輸。
SPI 的通信采用主從模式,通常會有一個主設備和一個或多個從設備。
某 Master 與 多個 Slave 通過 SPI 信號線的互聯(lián)示意圖如下。

SPI 協(xié)議
SPI 通信有 4 種傳輸模式,規(guī)定了數(shù)據(jù)在不同時鐘邊沿的采樣與發(fā)送規(guī)則,由時鐘極性 (CPOL,Clock Polarity) 和時鐘相位 (CPHA,Clock Phase) 兩兩組合來定義。其中,CPOL 參數(shù)決定了時鐘信號 SCLK 空閑狀態(tài)為低電平還是高電平,CPHA 參數(shù)決定了數(shù)據(jù)是在時鐘 SCKL 的上升沿采樣還是下降沿采樣。Slave 可能在出廠時就配置為某種模式不能修改的固定模式,這就要求 SPI Master 發(fā)送的傳輸模式要與 Slave 一致。
SPI 的 4 種傳輸模式示意圖如下:

SPI 的 4 種傳輸模式說明如下:
◆ CPOL=0,CPHA=0:空閑態(tài)時 SCLK 處于低電平,數(shù)據(jù)采樣發(fā)生在 SCLK 時鐘的第一個邊沿時刻。即 Slave 在 SCLK 由低電平到高電平的上升沿跳變時進行數(shù)據(jù)采樣,Master 在 SCLK 下降沿發(fā)送數(shù)據(jù)。
◆ CPOL=0,CPHA=1:空閑態(tài)時 SCLK 處于低電平,數(shù)據(jù)采樣發(fā)生在 SCLK 時鐘的第二個邊沿時刻。即 Slave 在 SCLK 下降沿接收數(shù)據(jù),Master 在 SCLK 上升沿發(fā)送數(shù)據(jù)。
◆ CPOL=1,CPHA=0:空閑態(tài)時 SCLK 處于高電平,數(shù)據(jù)采樣發(fā)生在 SCLK 時鐘的第一個邊沿時刻。即 Slave 在 SCLK 下降沿接收數(shù)據(jù),Master 在 SCLK 上升沿發(fā)送數(shù)據(jù)。
◆ CPOL=1,CPHA=1:空閑態(tài)時 SCLK 處于高電平,數(shù)據(jù)采樣發(fā)生在 SCLK 時鐘的第二個邊沿時刻。即 Slave 在 SCLK 上升沿接收數(shù)據(jù),Master 在 SCLK 下降沿發(fā)送數(shù)據(jù)。
以 CPOL=1、CPHA=1 為例,說明 SPI Master 向 Slave 傳輸 4bit 數(shù)據(jù)、并讀取 4bit 數(shù)據(jù)的過程,示意圖如下。

(1) 空閑狀態(tài),SCLK、CSN、MOSI、MISO 均為高電平;
(2) SPI Master CSN 拉低,選擇對應 SPI Slave,將開始傳輸數(shù)據(jù);
(3) SCLK 拉低,同時 MOSI 輸出單 bit 數(shù)據(jù) D1 ;
(4) SCLK 拉高,此時 SPI Slave 讀取 MOSI 對應的數(shù)據(jù) D1 ;
(5) 重復此過程,直至 SPI Slave 接收到 4bit
(6) 如果 Slave 接收到的 4bit 數(shù)據(jù)包含 SPI Master 讀控制,則在 SPI Master 仍然會繼續(xù)輸出時鐘 SCLK,但無需做額外驅(qū)動 MOSI ;
(7) SPI Slave 在 SCLK 下降沿輸出數(shù)據(jù)至 MISO;
(8) SPI Master 在 SCLK 上升沿對 MISO 進行采集;
(9) 重復步驟 (6)~(8),直至 Slave 傳輸完 4bit 數(shù)據(jù)。
需要注意的是,SPI 協(xié)議中的時鐘線 SCLK、片選線 CSN 和數(shù)據(jù)線 MOSI 都是由 SPI Master 控制,并不像 UART 或 IIC 協(xié)議有明顯的通信起始信號、結(jié)束信號和通信周期,所以 SPI 通信時 SCLK 有效個數(shù)和 CSN 有效長度要控制得當。當沒有數(shù)據(jù)交互時,CSN(CS) 信號要保持為高電平 (低電平) 狀態(tài),時鐘也需要保持高電平或持低電平狀態(tài)不變。
SPI 實現(xiàn)
假設某 SPI Slave 中存在 128 個可讀可寫的 8bit 位寬的寄存器。
規(guī)定某 SPI Master 與 該SPI Slave 每次通信時傳輸 16bit 數(shù)據(jù),其中最高位 bit[15] 為讀寫控制位,次高位 bit[14:8] 為地址位,低 8bit 數(shù)據(jù)位。

下面,對 CPOL=1、CPHA=1 工作模式 (下降沿發(fā)送數(shù)據(jù)、上升沿接收數(shù)據(jù)) 的 SPI 進行 Verilog 設計與簡單仿真。
◆ 參數(shù)設計
工作時鐘:200 Mhz
波特率:20MHz
傳輸位度:16
地址位度:7
數(shù)據(jù)位寬:8
◆ SPI Master 設計
SCLK 由 SPI 模塊工作時鐘產(chǎn)生,為避免工作時鐘與 SCLK 交互時的相位差關系,SPI Master 輸出的 SCLK 、CSN 與 MOSI 信號均在工作時鐘下產(chǎn)生, SCLK 只作為輸出時鐘驅(qū)動。
SPI Master 信號端口說明如下:

SPI Master 代碼描述如下:
// spi master:
// at negedge send data
// at posedge recevie data
module spi_master
  (
   input                rstn,
   input                clk,
   //data sended and received
   input  [15:0]        tx_data,
   input                tx_data_en,
   output [7:0]         rdata,
   output               rdata_valid,
   output               ready,
   //spi intf
   output               sclk,
   output               csn,
   output               mosi,
   input                miso
   );
   //100MHz clk, 10MHz spi clk
   parameter            BAUD_NUM = 100/10 ;
   //==========================================================
   //baud clk generating by baud counter
   reg [4:0]    baud_cnt_r ;
   //generating negedge sclk
   wire         baud_cnt_end = baud_cnt_r == BAUD_NUM-1;
   //generating posedge sclk
   wire         baud_cnt_half = baud_cnt_r == BAUD_NUM/2-1;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         baud_cnt_r <= 'b0 ;
      end
      else if (csn) begin
         baud_cnt_r <= 'b0 ;
      end
      else if (baud_cnt_end) begin
         baud_cnt_r <= 'b0 ;
      end
      else begin
         baud_cnt_r <= baud_cnt_r + 1'b1 ;
      end
   end
   //==========================================================
   //bit counter
   reg [7:0]            bit_cnt_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         bit_cnt_r <= 'b0 ;
      end
      else if (csn) begin
         bit_cnt_r <= 'b0 ;
      end
      //add: at posedge sclk
      else if (baud_cnt_half && bit_cnt_r != 16) begin
         bit_cnt_r <= bit_cnt_r + 1'b1 ;
      end
   end
   //(1) generate spi clk
   reg          sclk_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         sclk_r      <= 1'b1 ;
      end
      else if (csn) begin
         sclk_r      <= 1'b1 ;
      end
      else if (baud_cnt_half && bit_cnt_r != 16) begin
         sclk_r      <= 1'b0 ;
      end
      else if (baud_cnt_end) begin
         sclk_r      <= 1'b1 ;
      end
   end
   assign sclk = sclk_r ;
   //==========================================================
   //(2) generate csn
   reg          csn_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         csn_r <= 1'b1 ;
      end
      else if (tx_data_en) begin
         csn_r <= 1'b0;
      end
      //16 data finished, delay half cycle
      else if (!csn_r && bit_cnt_r == 16 && baud_cnt_half) begin
         csn_r <= 1'b1 ;
      end
   end
   assign csn =        csn_r ;
   //==========================================================
   //tx_data buffer
   reg [15:0]           tx_data_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         tx_data_r      <= 'b0 ;
      end
      else if (tx_data_en && ready) begin
         tx_data_r      <= tx_data ;
      end
   end
   //(3) generate mosi
   reg          mosi_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         mosi_r <= 1'b1 ;
      end
      else if (csn) begin
         mosi_r <= 1'b1 ;
      end
      //output tx_data
      else if (baud_cnt_half && bit_cnt_r != 16 ) begin
         mosi_r <= tx_data_r[15-bit_cnt_r] ;
      end
   end
   assign mosi = mosi_r ;
   //(4) receive data by miso
   reg [7:0]    rdata_r;
   reg          rdata_valid_r;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         rdata_r        <= 8'b0 ;
         rdata_valid_r  <= 1'b0 ;
      end
      else if (rdata_valid_r) begin
         rdata_valid_r  <= 1'b0 ;
      end
      else if (!tx_data_r[15] && bit_cnt_r ==16 && baud_cnt_end) begin
         rdata_r        <= {rdata_r[6:0], miso} ;
         rdata_valid_r  <= 1'b1 ;
      end
      else if (!tx_data_r[15] && bit_cnt_r >=9 && baud_cnt_end) begin
         rdata_r        <= {rdata_r[6:0], miso} ;
      end
   end
   reg          ready_r ;
   always @(posedge clk or negedge rstn) begin
      if (!rstn) begin
         ready_r        <= 1'b1 ;
      end
      else if (tx_data_en) begin
         ready_r        <= 1'b0 ;
      end
      else if (csn) begin
         ready_r        <= 1'b1 ;
      end
   end // always @ (negedge clk or negedge rstn)
   assign ready = ready_r ;
   assign rdata         = rdata_r ;
   assign rdata_valid   = rdata_valid_r ;
endmodule
◆ SPI Slave 設計
SPI Master 模塊只保留 4 個信號即可,片選信號可以當做復位信號使用,說明如下。

SPI Slave 也是在 SCLK 下降沿開始接收數(shù)據(jù),在 SCLK 上升沿輸出數(shù)據(jù),代碼描述如下:
// spi slave:
// at negedge send data
// at posedge recevie data
module spi_slave
  (
   input                sclk,
   input                csn,
   input                mosi,
   output               miso);
   //===============================================
   //bit counter
   reg [3:0]            bit_cnt_r ;
   always @(posedge sclk or posedge csn) begin
      if (csn) begin
         bit_cnt_r <= 'b0 ;
      end
      else begin
         bit_cnt_r <= bit_cnt_r + 1'b1 ;
      end
   end
   //===============================================
   //(1) receive rw cmd
   reg          rw_r ;
   always @(posedge sclk or posedge csn) begin
      if (csn) begin
         rw_r <= 1'b0 ;
      end
      else if (bit_cnt_r == 0) begin
         rw_r <= mosi ;
      end
   end
   //(2) receive address
   reg [6:0]    addr_r;
   always @(posedge sclk or posedge csn) begin
      if (csn) begin
         addr_r <= 6'b0 ;
      end
      else if (bit_cnt_r >= 1 && bit_cnt_r <= 7) begin
         addr_r <= {addr_r[5:0], mosi} ;
      end
   end
   //(3) receive data
   reg [7:0]    data_r;
   always @(posedge sclk or posedge csn) begin
      if (csn) begin
         data_r <= 8'b0 ;
      end
      else if (rw_r && bit_cnt_r >=8 && bit_cnt_r <= 15) begin
         data_r <= {data_r[6:0], mosi} ;
      end
   end
   //===============================================
   //write regs
   reg [7:0]    reg_group_r [127:0];
   always @(posedge sclk) begin
      if (rw_r && bit_cnt_r == 15) begin
         reg_group_r[addr_r]    <= {data_r[6:0], mosi} ;
      end
   end
   //===============================================
   //rd regs and send out
   reg          miso_r ;
   always @(negedge sclk or posedge csn) begin
      if (csn) begin
         miso_r         <= 'b0;
      end
      else if (!rw_r && bit_cnt_r >= 8 && bit_cnt_r <= 15) begin
         miso_r         <= reg_group_r[addr_r][15-bit_cnt_r] ;
      end
   end
   assign miso = miso_r ;
endmodule
◆ Testbench
測試時,通過向 SPI 輸入包含讀寫控制位、讀寫地址、和讀寫數(shù)據(jù)的并行數(shù)據(jù),來判斷 SPI Master 向 SPI Slave 寫入和讀出的數(shù)據(jù)是否一致,以達到仿真 SPI 功能的目的。
`timescale 1ns/1ps
module test ;
   reg          clk_200mhz ;
   reg          rstn ;
   reg [15:0]   tx_data ;
   reg          tx_data_en ;
   wire         sclk, csn, mosi, miso ;
   wire [7:0]   rdata ;
   wire         rdata_valid ;
   wire         ready ;
   //==========================================
   // clk and reset
   initial begin
      clk_200mhz = 0 ;
      rstn = 0 ;
      #11.3 rstn = 1 ;
   end
   always #(2.5)   clk_200mhz  = ~clk_200mhz ;
   //==========================================
   //driver task
   task spi_cmd ;
      input [15:0]      data_send ;
      begin
         wait(ready) ;
         @(posedge clk_200mhz) ;
         # 0.7 ;
         tx_data        = data_send ;
         tx_data_en     = 1'b1 ;
         @(posedge clk_200mhz) ;
         # 0.7 ;
         tx_data_en     = 1'b0 ;
         tx_data        = 'b0 ;
         wait(ready) ;
      end
   endtask // spi_rw
   //==========================================
   //driver
   initial begin
      tx_data    = 16'b0 ;
      tx_data_en = 1'b0 ;
      //(1) wr address: 100-102
      #133.7 ;  spi_cmd({1'b1, 7'd100, 8'hAA}) ;
      #501.3 ;  spi_cmd({1'b1, 7'd101, 8'h55}) ;
      #501.3 ;  spi_cmd({1'b1, 7'd102, 8'hA5}) ;
      //(2) rd address: 102-100
      #2001.3 ; spi_cmd({1'b0, 7'd102, 8'h0}) ;
      #501.3 ;  spi_cmd({1'b0, 7'd101, 8'h0}) ;
      #501.3 ;  spi_cmd({1'b0, 7'd100, 8'h0}) ;
   end
   //finish
   reg          err_flag ;
   initial begin
      err_flag = 0 ;
      #100;
      //1st read
      @(posedge rdata_valid) ;
      @(negedge clk_200mhz) ;
      if (rdata != 8'ha5)  err_flag |= 1;
      //2nd read
      @(posedge rdata_valid) ;
      @(negedge clk_200mhz) ;
      if (rdata != 8'h55)  err_flag |= 1;
      //3rd 3read
      @(posedge rdata_valid) ;
      @(negedge clk_200mhz) ;
      if (rdata != 8'haa)  err_flag |= 1;
      #13.7 ;
      $display("-------------------------");
      if (err_flag !== 0) begin
         $display("Simulation Failed!");
      end
      else begin
         $display("Simulation Succeed!");
      end
      $display();
      $display();
      #1000 ;
      $finish ;
   end
   spi_master u_spi_master  (
       .rstn            (rstn),
       .clk             (clk_200mhz),
       //parallel
       .tx_data         (tx_data),
       .tx_data_en      (tx_data_en),
       .ready           (ready),
       //spi intf
       .sclk            (sclk),
       .csn             (csn),
       .mosi            (mosi),
       .miso            (miso),
       .rdata           (rdata),
       .rdata_valid     (rdata_valid)
   );
   spi_slave u_spi_slave  (
       .sclk            (sclk),
       .csn             (csn),
       .mosi            (mosi),
       .miso            (miso));
endmodule
◆ SPI Slave 設計
仿真中的自檢驗成功截圖如下所示。

Master 向第 100 個寄存器寫數(shù)據(jù) 0xAA 的仿真波形圖如下所示。

Master 發(fā)送讀取第 100 個寄存器的命令及 Slave 返回對應寄存器數(shù)據(jù)的波形圖如下所示。

                        電子發(fā)燒友App
                    
                
                
          
        
        




























           
            
            
                
            
評論