【接口時序】4、SPI總線的原理與Verilog實現

轉自:https://www.cnblogs.com/liujinggang/p/9609739.html#4186571 侵刪

一、 軟件平臺與硬件平臺

  軟件平臺:

    1、操作系統:Windows-8.1

    2、開發套件:ISE14.7

    3、仿真工具:ModelSim-10.4-SE

  硬件平臺:

    1、 FPGA型號:Xilinx公司的XC6SLX45-2CSG324

    2、 Flash型號:WinBond公司的W25Q128BV   Qual SPI Flash存儲器

二、 原理介紹

  SPI(Serial Peripheral Interface,串行外圍設備接口),是Motorola公司提出的一種同步串行接口技術,是一種高速、全雙工、同步通信總線,在芯片中只佔用四根管腳用來控制及數據傳輸,廣泛用於EEPROM、Flash、RTC(實時時鐘)、ADC(數模轉換器)、DSP(數字信號處理器)以及數字信號解碼器上。SPI通信的速度很容易達到好幾兆bps,所以可以用SPI總線傳輸一些未壓縮的音頻以及壓縮的視頻。

  下圖是隻有2個chip利用SPI總線進行通信的結構圖

可知SPI總線傳輸只需要4根線就能完成,這四根線的作用分別如下:

    SCK(Serial Clock):SCK是串行時鐘線,作用是Master向Slave傳輸時鐘信號,控制數據交換的時機和速率;

    MOSI(Master Out Slave in):在SPI Master上也被稱爲Tx-channel,作用是SPI主機給SPI從機發送數據;

    CS/SS(Chip Select/Slave Select):作用是SPI Master選擇與哪一個SPI Slave通信,低電平表示從機被選中(低電平有效);

    MISO(Master In Slave Out):在SPI Master上也被稱爲Rx-channel,作用是SPI主機接收SPI從機傳輸過來的數據;

 

  SPI總線主要有以下幾個特點:

  1、 採用主從模式(Master-Slave)的控制方式,支持單Master多Slave。SPI規定了兩個SPI設備之間通信必須由主設備Master來控制從設備Slave。也就是說,如果FPGA是主機的情況下,不管是FPGA給芯片發送數據還是從芯片中接收數據,寫Verilog邏輯的時候片選信號CS與串行時鐘信號SCK必須由FPGA來產生。同時一個Master可以設置多個片選(Chip Select)來控制多個Slave。SPI協議還規定Slave設備的clock由Master通過SCK管腳提供給Slave,Slave本身不能產生或控制clock,沒有clock則Slave不能正常工作。單Master多Slave的典型結構如下圖所示

   2、 SPI總線在傳輸數據的同時也傳輸了時鐘信號,所以SPI協議是一種同步(Synchronous)傳輸協議。Master會根據將要交換的數據產生相應的時鐘脈衝,組成時鐘信號,時鐘信號通過時鐘極性(CPOL)和時鐘相位(CPHA)控制兩個SPI設備何時交換數據以及何時對接收數據進行採樣,保證數據在兩個設備之間是同步傳輸的。

  3、 SPI總線協議是一種全雙工的串行通信協議,數據傳輸時高位在前,低位在後。SPI協議規定一個SPI設備不能在數據通信過程中僅僅充當一個發送者(Transmitter)或者接受者(Receiver)。在片選信號CS爲0的情況下,每個clock週期內,SPI設備都會發送並接收1 bit數據,相當於有1 bit數據被交換了。數據傳輸高位在前,低位在後(MSB first)。SPI主從結構內部數據傳輸示意圖如下圖所示

 

  SPI總線傳輸的模式:

  SPI總線傳輸一共有4中模式,這4種模式分別由時鐘極性(CPOL,Clock Polarity)和時鐘相位(CPHA,Clock Phase)來定義,其中CPOL參數規定了SCK時鐘信號空閒狀態的電平,CPHA規定了數據是在SCK時鐘的上升沿被採樣還是下降沿被採樣。這四種模式的時序圖如下圖所示:

 

  模式0:CPOL= 0,CPHA=0。SCK串行時鐘線空閒是爲低電平,數據在SCK時鐘的上升沿被採樣,數據在SCK時鐘的下降沿切換

  模式1:CPOL= 0,CPHA=1。SCK串行時鐘線空閒是爲低電平,數據在SCK時鐘的下降沿被採樣,數據在SCK時鐘的上升沿切換

  模式2:CPOL= 1,CPHA=0。SCK串行時鐘線空閒是爲高電平,數據在SCK時鐘的下降沿被採樣,數據在SCK時鐘的上升沿切換

  模式3:CPOL= 1,CPHA=1。SCK串行時鐘線空閒是爲高電平,數據在SCK時鐘的上升沿被採樣,數據在SCK時鐘的下降沿切換

  其中比較常用的模式是模式0和模式3。爲了更清晰的描述SPI總線的時序,下面展現了模式0下的SPI時序圖

 

  上圖清晰的表明在模式0下,在空閒狀態下,SCK串行時鐘線爲低電平,當SS被主機拉低以後,數據傳輸開始,數據線MOSI和MISO的數據切換(Toggling)發生在時鐘的下降沿(上圖的黑色虛線),而數據線MOSI和MISO的數據的採樣(Sampling)發生在數據的正中間(上圖中的灰色實線)。下圖清晰的描述了其他三種模式數據線MOSI和MISO的數據切換(Toggling)位置和數據採樣位置的關係圖

 

  下面我將以模式0爲例用Verilog編寫SPI通信的代碼。

三、 目標任務

  1、編寫SPI通信的Verilog代碼並利用ModelSim進行時序仿真

  2、閱讀Qual SPI的芯片手冊,理解操作時序,並利用任務1編寫的代碼與Qual SPI進行SPI通信,讀出Qual SPI Flash的Manufacturer/Device  ID

  3、用SPI總線把存放在ROM裏面的數據發出去,這在實際項目中用來配置SPI外設芯片很有用

四、 設計思路與Verilog代碼編寫

4.1、 SPI模塊的接口定義與整體設計

  Verilog編寫的SPI模塊除了進行SPI通信的四根線以外還要包括一些時鐘、復位、使能、並行的輸入輸出以及完成標誌位。其框圖如下所示

 

  其中:

  I_clk是系統時鐘;

  I_rst_n是系統復位;

  I_tx_en是主機給從機發送數據的使能信號,當I_tx_en爲1時主機才能給從機發送數據;

  I_rx _en是主機從從機接收數據的使能信號,當I_rx_en爲1時主機才能從從機接收數據;

  I_data_in是主機要發送的並行數據;

  O_data_out是把從機接收回來的串行數據並行化以後的並行數據;

  O_tx_done是主機給從機發送數據完成的標誌位,發送完成後會產生一個高脈衝;

  O_rx_done是主機從從機接收數據完成的標誌位,接收完成後會產生一個高脈衝;

  I_spi_miso、O_spi_cs、O_spi_sck和O_spi_mosi是標準SPI總線協議規定的四根線;

  要想實現上文模式0的時序,最簡單的辦法還是設計一個狀態機。爲了方便說明,這裏把模式0的時序再在下面貼一遍

 

  由於是要用FPGA去控制或讀寫QSPI Flash,所以FPGA是SPI主機,QSPI是SPI從機。

  發送:當FPGA通過SPI總線往QSPI Flash中發送一個字節(8-bit)的數據時,首先FPGA把CS/SS片選信號設置爲0,表示準備開始發送數據,整個發送數據過程其實可以分爲16個狀態:

    狀態0:SCK爲0,MOSI爲要發送的數據的最高位,即I_data_in[7]

    狀態1:SCK爲1,MOSI保持不變

    狀態2:SCK爲0,MOSI爲要發送的數據的次高位,即I_data_in[6]

    狀態3:SCK爲1,MOSI保持不變

    狀態4:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[5]

    狀態5:SCK爲1,MOSI保持不變

    狀態6:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[4]

    狀態7:SCK爲1,MOSI保持不變

    狀態8:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[3]

    狀態9:SCK爲1,MOSI保持不變

    狀態10:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[2]

    狀態11:SCK爲1,MOSI保持不變

    狀態12:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[1]

    狀態13:SCK爲1,MOSI保持不變

    狀態14:SCK爲0,MOSI爲要發送的數據的最低位,即I_data_in[0]

    狀態15:SCK爲1,MOSI保持不變

  一個字節數據發送完畢以後,產生一個發送完成標誌位O_tx_done並把CS/SS信號拉高完成一次發送。通過觀察上面的狀態可以發現狀態編號爲奇數的狀態要做的操作實際上是一模一樣的,所以寫代碼的時候爲了精簡代碼,可以把狀態號爲奇數的狀態全部整合到一起。

  接收:當FPGA通過SPI總線從QSPI Flash中接收一個字節(8-bit)的數據時,首先FPGA把CS/SS片選信號設置爲0,表示準備開始接收數據,整個接收數據過程其實也可以分爲16個狀態,但是與發送過程不同的是,爲了保證接收到的數據準確,必須在數據的正中間採樣,也就是說模式0時序圖中灰色實線的地方纔是代碼中鎖存數據的地方,所以接收過程的每個狀態執行的操作爲:

    狀態0:SCK爲0,不鎖存MISO上的數據

    狀態1:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[7]

    狀態2:SCK爲0,不鎖存MISO上的數據

    狀態3:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[6]

    狀態4:SCK爲0,不鎖存MISO上的數據

    狀態5:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[5]

    狀態6:SCK爲0,不鎖存MISO上的數據

    狀態7:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[4]

    狀態8:SCK爲0,不鎖存MISO上的數據

    狀態9:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[3]

    狀態10:SCK爲0,不鎖存MISO上的數據

    狀態11:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[2]

    狀態12:SCK爲0,不鎖存MISO上的數據

    狀態13:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[1]

    狀態14:SCK爲0,不鎖存MISO上的數據

    狀態15:SCK爲1,鎖存MISO上的數據,即把MISO上的數據賦值給O_data_out[0]

  一個字節數據接收完畢以後,產生一個接收完成標誌位O_rx_done並把CS/SS信號拉高完成一次數據的接收。通過觀察上面的狀態可以發現狀態編號爲偶數的狀態要做的操作實際上是一模一樣的,所以寫代碼的時候爲了精簡代碼,可以把狀態號爲偶數的狀態全部整合到一起。而這一點剛好與發送過程的狀態剛好相反。

  思路理清楚以後就可以直接編寫Verilog代碼了,spi_module模塊的代碼如下:

複製代碼

module spi_module
(
    input               I_clk       , // 全局時鐘50MHz
    input               I_rst_n     , // 復位信號,低電平有效
    input               I_rx_en     , // 讀使能信號
    input               I_tx_en     , // 發送使能信號
    input        [7:0]  I_data_in   , // 要發送的數據
    output  reg  [7:0]  O_data_out  , // 接收到的數據
    output  reg         O_tx_done   , // 發送一個字節完畢標誌位
    output  reg         O_rx_done   , // 接收一個字節完畢標誌位
    
    // 四線標準SPI信號定義
    input               I_spi_miso  , // SPI串行輸入,用來接收從機的數據
    output  reg         O_spi_sck   , // SPI時鐘
    output  reg         O_spi_cs    , // SPI片選信號
    output  reg         O_spi_mosi    // SPI輸出,用來給從機發送數據          
);

reg [3:0]   R_tx_state      ; 
reg [3:0]   R_rx_state      ;

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_tx_state  <=  4'd0    ;
            R_rx_state  <=  4'd0    ;
            O_spi_cs    <=  1'b1    ;
            O_spi_sck   <=  1'b0    ;
            O_spi_mosi  <=  1'b0    ;
            O_tx_done   <=  1'b0    ;
            O_rx_done   <=  1'b0    ;
            O_data_out  <=  8'd0    ;
        end 
    else if(I_tx_en) // 發送使能信號打開的情況下
        begin
            O_spi_cs    <=  1'b0    ; // 把片選CS拉低
            case(R_tx_state)
                4'd1, 4'd3 , 4'd5 , 4'd7  , 
                4'd9, 4'd11, 4'd13, 4'd15 : //整合奇數狀態
                    begin
                        O_spi_sck   <=  1'b1                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                4'd0:    // 發送第7位
                    begin
                        O_spi_mosi  <=  I_data_in[7]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                4'd2:    // 發送第6位
                    begin
                        O_spi_mosi  <=  I_data_in[6]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                4'd4:    // 發送第5位
                    begin
                        O_spi_mosi  <=  I_data_in[5]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd6:    // 發送第4位
                    begin
                        O_spi_mosi  <=  I_data_in[4]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd8:    // 發送第3位
                    begin
                        O_spi_mosi  <=  I_data_in[3]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end                            
                4'd10:    // 發送第2位
                    begin
                        O_spi_mosi  <=  I_data_in[2]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd12:    // 發送第1位
                    begin
                        O_spi_mosi  <=  I_data_in[1]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd14:    // 發送第0位
                    begin
                        O_spi_mosi  <=  I_data_in[0]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b1                ;
                    end
                default:R_tx_state  <=  4'd0                ;   
            endcase 
        end
    else if(I_rx_en) // 接收使能信號打開的情況下
        begin
            O_spi_cs    <=  1'b0        ; // 拉低片選信號CS
            case(R_rx_state)
                4'd0, 4'd2 , 4'd4 , 4'd6  , 
                4'd8, 4'd10, 4'd12, 4'd14 : //整合偶數狀態
                    begin
                        O_spi_sck      <=  1'b0                ;
                        R_rx_state     <=  R_rx_state + 1'b1   ;
                        O_rx_done      <=  1'b0                ;
                    end
                4'd1:    // 接收第7位
                    begin                       
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[7]   <=  I_spi_miso          ;   
                    end
                4'd3:    // 接收第6位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[6]   <=  I_spi_miso          ; 
                    end
                4'd5:    // 接收第5位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[5]   <=  I_spi_miso          ; 
                    end 
                4'd7:    // 接收第4位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[4]   <=  I_spi_miso          ; 
                    end 
                4'd9:    // 接收第3位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[3]   <=  I_spi_miso          ; 
                    end                            
                4'd11:    // 接收第2位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[2]   <=  I_spi_miso          ; 
                    end 
                4'd13:    // 接收第1位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[1]   <=  I_spi_miso          ; 
                    end 
                4'd15:    // 接收第0位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b1                ;
                        O_data_out[0]   <=  I_spi_miso          ; 
                    end
                default:R_rx_state  <=  4'd0                    ;   
            endcase 
        end    
    else
        begin
            R_tx_state  <=  4'd0    ;
            R_rx_state  <=  4'd0    ;
            O_tx_done   <=  1'b0    ;
            O_rx_done   <=  1'b0    ;
            O_spi_cs    <=  1'b1    ;
            O_spi_sck   <=  1'b0    ;
            O_spi_mosi  <=  1'b0    ;
            O_data_out  <=  8'd0    ;
        end      
end

endmodule

複製代碼

  

  整個代碼的流程與之前分析的流程完全一致。接下來就對這個代碼用ModelSim進行基本的仿真。由於接收部分不再硬件上不太好測,所以這裏只對發送部分進行測試,接收部分等把代碼下載到板子裏面以後用ChipScope抓接收部分時序就一清二楚了。

發射部分的測試激勵代碼如下:

複製代碼

`timescale 1ns / 1ps

module tb_spi_module;

    // Inputs
    reg I_clk;
    reg I_rst_n;
    reg I_rx_en;
    reg I_tx_en;
    reg [7:0] I_data_in;
    reg I_spi_miso;

    // Outputs
    wire [7:0] O_data_out;
    wire O_tx_done;
    wire O_rx_done;
    wire O_spi_sck;
    wire O_spi_cs;
    wire O_spi_mosi;

    // Instantiate the Unit Under Test (UUT)
    spi_module uut (
        .I_clk       (I_clk         ), 
        .I_rst_n     (I_rst_n    ), 
        .I_rx_en      (I_rx_en    ), 
        .I_tx_en      (I_tx_en    ), 
        .I_data_in     (I_data_in   ), 
        .O_data_out    (O_data_out  ), 
        .O_tx_done    (O_tx_done    ), 
        .O_rx_done    (O_rx_done    ), 
        .I_spi_miso   (I_spi_miso   ), 
        .O_spi_sck    (O_spi_sck    ), 
        .O_spi_cs    (O_spi_cs    ), 
        .O_spi_mosi   (O_spi_mosi   )
    );

    initial begin
        // Initialize Inputs
        I_clk = 0;
        I_rst_n = 0;
        I_rx_en = 0;
        I_tx_en = 1;
        I_data_in = 8'h00;
        I_spi_miso = 0;

        // Wait 100 ns for global reset to finish
        #100;
        I_rst_n = 1;  

    end
    
    always #10 I_clk = ~I_clk ;
    
    always @(posedge I_clk or negedge I_rst_n)
    begin
         if(!I_rst_n)
            I_data_in <= 8'h00;
         else if(I_data_in == 8'hff)
            begin
                I_data_in <= 8'hff;
                I_tx_en <= 0;
            end
         else if(O_tx_done)
            I_data_in <= I_data_in + 1'b1 ;            
    end
      
endmodule

複製代碼

 

  ModelSim的仿真圖如下圖所示:

 

  由圖可以看到仿真得到的時序與SPI模式0的時序完全一致。

4.2、 W25Q128BV   Qual SPI Flash存儲器時序分析

  W25Q128BV,支持SPI, Dual SPI和Quad SPI接口方式。在Fast Read模式,接口的時鐘速率最大可以達到 104Mhz。 FLASH 的容量由 65536個256-byte的Page組成。W25Q128 的擦除方法有三種,一種爲 Sector 擦除(16 個 page,共 4KB),一種爲 Block 擦除(128 個 page,共 32KB), 另一種爲 Chip 擦除(整個擦除)。爲了簡單起見,順便測試一下上面寫的代碼,這裏只使用W25Q128BV的標準SPI總線操作功能,並且只完成一個讀取ID的操作,其他更高級的操作請看下一篇文章《QSPI Flash的原理與QSPI時序的Verilog實現》(鏈接:https://www.cnblogs.com/liujinggang/p/9651170.html)。我的開發板上W25Q128BV的硬件原理圖如下圖所示

 

  由於我們的任務是利用標準四線SPI總線讀取QSPI FLASH的Manufacturer/Device  ID,所以先到W25Q128BV的芯片手冊中找到它的讀Manufacturer/Device  ID的時序。時序如下圖所示:

 

  整個讀QSPI FLASH的過程爲:FPGA先拉低CS片選信號,然後通過SPI總線發送命令碼90,命令碼發完以後,發送24-bit的地址24’h000000,接着在第32個SCK的下降沿準備接收Manufacturer ID,Manufacturer ID接收完畢以後開始接收Device ID,最後把CS片選拉高,一次讀取過程全部結束。這裏既涉及到了SPI的寫操作,也涉及到了SPI的讀操作,剛好可以測試一下上面寫的代碼。

4.3、 構思狀態機並用ChipScope抓讀寫時序

  由時序圖可以很輕鬆的分析出,用一個7個狀態的狀態機來實現讀ID的過程,其中狀態的跳變可通過發送完成標誌O_tx_done與接收完成標誌O_rx_done來切換,各個狀態的功能如下:

    狀態0:打開spi_module的發送使能開關,並初始化命令字90,等O_tx_done標誌爲高後切換到下一狀態並設置好下一次要發送的數據;

    狀態1:打開spi_module的發送使能開關,並設置低8位地址00,等O_tx_done標誌爲高後切換到下一狀態並設置好下一次要發送的數據;

    狀態2:打開spi_module的發送使能開關,並設置中8位地址00,等O_tx_done標誌爲高後切換到下一狀態並設置好下一次要發送的數據;

    狀態3:打開spi_module的發送使能開關,並設置高8位地址00,等O_tx_done標誌爲高後切換到下一狀態並設置好下一次要發送的數據;

    狀態4:關閉spi_module的發送使能開關,打開spi_module的接收使能開關,等O_rx_done標誌爲高後切換到下一狀態;

    狀態5:關閉spi_module的發送使能開關,打開spi_module的接收使能開關,等O_rx_done標誌爲高後切換到下一狀態,並關閉spi_module所有使能開關;

    狀態6:結束狀態,關閉spi_module所有使能開關;

  讀ID的完整代碼如下:

複製代碼

`timescale 1ns / 1ps

module spi_read_id_top
(
    input                   I_clk       , // 全局時鐘50MHz
    input                   I_rst_n     , // 復位信號,低電平有效
    output       [3:0]      O_led_out   ,

    // 四線標準SPI信號定義
    input                   I_spi_miso  , // SPI串行輸入,用來接收從機的數據
    output                  O_spi_sck   , // SPI時鐘
    output                  O_spi_cs    , // SPI片選信號
    output                  O_spi_mosi    // SPI輸出,用來給從機發送數據 
);

wire            W_rx_en     ;
wire            W_tx_en     ;
wire   [7:0]    W_data_in   ; // 要發送的數據
wire   [7:0]    W_data_out  ; // 接收到的數據
wire            W_tx_done   ; // 發送最後一個bit標誌位,在最後一個bit產生一個時鐘的高電平
wire            W_rx_done   ; // 接收一個字節完畢(End of Receive)

reg             R_rx_en     ;
reg             R_tx_en     ;
reg   [7:0]     R_data_in   ; // 要發送的數據
reg   [2:0]     R_state     ;

reg   [7:0]     R_spi_pout  ;

assign W_rx_en       = R_rx_en           ;
assign W_tx_en       = R_tx_en           ;
assign W_data_in     = R_data_in         ;
assign O_led_out     = R_spi_pout[3:0]   ;


always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_state <= 3'd0 ; 
            R_tx_en <= 1'b0 ; 
            R_rx_en <= 1'b0 ; 
        end 
    else
        case(R_state)
            3'd0: // 發送命令字90
                begin
                    if(W_tx_done)  
                        begin
                            R_state <= R_state + 1'b1 ;
                            R_data_in   <= 8'h00      ; // 提前設定好下一次要發送的數據
                        end
                    else                              
                        begin                         
                            R_tx_en     <= 1'b1       ;
                            R_data_in   <= 8'h90      ;
                        end 
                end                                   
            3'd1,3'd2,3'd3:  // 發送24位的地址信號                         
                begin                                 
                    if(W_tx_done)                       
                        begin
                            R_state <= R_state + 1'b1 ;
                            R_data_in   <= 8'h00      ; // 提前設定好下一次要發送的數據
                        end
                    else                              
                        begin                         
                            R_tx_en     <= 1'b1       ;
                            R_data_in   <= 8'h00      ;
                        end    
                end 
            3'd4:  // 接收ID  EF
                begin
                    if(W_rx_done)
                        begin
                            R_state     <= R_state + 1'b1 ;
                            R_spi_pout  <= W_data_out     ;   
                        end
                    else
                        begin
                            R_tx_en     <= 1'b0       ;
                            R_rx_en     <= 1'b1       ;
                        end    
                end 
           3'd5:   // 接收ID  17
                begin
                    if(W_rx_done)
                        begin
                            R_state     <= R_state + 1'b1 ;
                            R_spi_pout  <= W_data_out     ;
                            R_tx_en     <= 1'b0       ;
                            R_rx_en     <= 1'b0       ; 
                        end
                    else
                        begin
                            R_tx_en     <= 1'b0       ;
                            R_rx_en     <= 1'b1       ;
                        end    
                end 
            3'd6:  //結束
                begin
                    R_state     <= R_state    ;
                    R_tx_en     <= 1'b0       ;
                    R_rx_en     <= 1'b0       ;   
                end                   
        endcase     
end

spi_module U_spi_module
(
    .I_clk       (I_clk), // 全局時鐘50MHz
    .I_rst_n     (I_rst_n), // 復位信號,低電平有效
    .I_rx_en     (W_rx_en), // 讀使能信號
    .I_tx_en     (W_tx_en), // 發送使能信號
    .I_data_in   (W_data_in), // 要發送的數據
    .O_data_out  (W_data_out), // 接收到的數據
    .O_tx_done   (W_tx_done), // 發送最後一個bit標誌位,在最後一個bit產生一個時鐘的高電平
    .O_rx_done   (W_rx_done), // 接收一個字節完畢(End of Receive)
    
    // 四線標準SPI信號定義
    .I_spi_miso  (I_spi_miso), // SPI串行輸入,用來接收從機的數據
    .O_spi_sck   (O_spi_sck), // SPI時鐘
    .O_spi_cs    (O_spi_cs), // SPI片選信號
    .O_spi_mosi  (O_spi_mosi) // SPI輸出,用來給從機發送數據          
);
////////  Debug  //////////////////////////////////////////////////////////////
wire [35:0] CONTROL0 ;
wire [39:0] TRIG0     ;
icon icon (
    .CONTROL0(CONTROL0) // INOUT BUS [35:0]
);

ila ila (
    .CONTROL(CONTROL0), // INOUT BUS [35:0]
    .CLK(I_clk), // IN
    .TRIG0(TRIG0) // IN BUS [39:0]
);

assign TRIG0[0]      = W_rx_en      ;
assign TRIG0[1]      = W_tx_en      ;
assign TRIG0[9:2]    = W_data_in    ;
assign TRIG0[17:10]  = W_data_out   ;
assign TRIG0[18]     = W_tx_done    ;
assign TRIG0[19]     = W_rx_done    ;
assign TRIG0[27:20]  = R_spi_pout   ;
assign TRIG0[30:28]  = R_state      ;
assign TRIG0[31]     = O_spi_sck    ;
assign TRIG0[32]     = O_spi_cs     ;
assign TRIG0[33]     = O_spi_mosi   ;
assign TRIG0[34]     = I_spi_miso   ;
assign TRIG0[35]     = I_rst_n      ;
///////////////////////////////////////////////////////////////////////////////

endmodule

複製代碼

 

用ChipScope抓取的時序圖如下圖所示:

  通過對比與芯片手冊的時序圖可以發現,每個節拍與芯片手冊提供的讀ID的時序完全一致。

4.4、 用FPGA通過SPI總線配置外設芯片

  上文的例子已經包括了連續發送4個字節數據和連續接收2個字節數據,實際上在很多應用中只需要FPGA通過SPI總線給芯片發送相應寄存器的值就可以對芯片的功能進行配置了,而並不需要接收芯片返回的數據,大家可以依着葫蘆畫瓢把硬件工程師發過來的芯片寄存器表(實際上很多芯片都有配置軟件,硬件工程師在配置軟件中設定好參數以後可以自動生成寄存器表)通過像上文那樣寫一個狀態機發出去來配置芯片的功能。

  在寄存器數目比較少的情況下,比如就30~40個以下的寄存器需要配置的情況下,完全可以按照上面的思路寫一個30~40個狀態的狀態機,每個狀態通過SPI總線發送一個數據,這樣做的好處是以後想要在其他地方移植這套代碼或者做版本的維護與升級時只需要複製上一版本的代碼就可以了,移植起來非常方便。但是如果需要配置的寄存器有好幾百甚至上千個或者需要用SPI總線往一些顯示設備(比如OLED屏,液晶顯示屏)裏面發送數據的話,如果去寫一個上千個狀態的狀態機顯然不是最好的選擇,所以對於這種需要用SPI傳輸大量數據的情況,我比較推薦的方式是先把數據存放在ROM裏面,然後通過上面的SPI代碼發出去。

  在做這件事情之前,在重複理解一下SPI發送過程的時序:

    狀態0:SCK爲0,MOSI爲要發送的數據的最高位,即I_data_in[7],拉低O_tx_done信號

    狀態1:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態2:SCK爲0,MOSI爲要發送的數據的次高位,即I_data_in[6] ,拉低O_tx_done信號

         狀態3:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態4:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[5] ,拉低O_tx_done信號

    狀態5:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態6:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[4] ,拉低O_tx_done信號

    狀態7:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態8:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[3] ,拉低O_tx_done信號

    狀態9:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態10:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[2] ,拉低O_tx_done信號

    狀態11:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態12:SCK爲0,MOSI爲要發送的數據的下一位,即I_data_in[1] ,拉低O_tx_done信號

    狀態13:SCK爲1,MOSI保持不變,拉低O_tx_done信號

    狀態14:SCK爲0,MOSI爲要發送的數據的最低位,即I_data_in[0] ,拉高O_tx_done信號

    狀態15:SCK爲1,MOSI保持不變,拉低O_tx_done信號

  可以看出,每一個bit爲實際上是佔了2個時鐘週期(這裏的時鐘週期指的是系統時鐘I_clk),發送一個字節完成標誌位O_tx_done信號是在第14個狀態拉高的,也就是在最後一個bit的前時鐘週期產生了一個高電平,我之所以這麼做的目的一是爲了更好的整合代碼,把偶數狀態全部歸類到一起,二是爲了在連續發送數據時,在檢測到O_tx_done信號爲高以後,可以提前把下一次要發送的數據準備好。大家可以在對照着下面時序圖理解一下,下面這張圖可以很清晰的看到,O_tx_done信號是在最後一個數據的前一個時鐘週期拉高的。

  現在我們的目的是想要把ROM裏面的數據通過SPI總線發出來,但是由於ROM是更新了地址以後的下一個時鐘週期才能讀出新數據,也就是說,如果我們在檢測到O_tx_done爲高時更新ROM地址的話,新的數據其實並沒有準備好,直接看代碼和時序圖。

  在此之前先把ROM配置好,我配置的ROM非常簡單,Read Width設置爲8,Read Depth設置爲10,

  ROM的初始化數據.coe文件的內容如下所示:

      MEMORY_INITIALIZATION_RADIX=16;

      MEMORY_INITIALIZATION_VECTOR=

      33,

      24,

      98,

      24,

      00,

      47,

      00,

      ff,

      a3,

      49;

  頂層代碼如下所示:

複製代碼

`timescale 1ns / 1ps

module spi_reg_cfg
(
    input                   I_clk       , // 全局時鐘50MHz
    input                   I_rst_n     , // 復位信號,低電平有效

    // 四線標準SPI信號定義
    input                   I_spi_miso  , // SPI串行輸入,用來接收從機的數據
    output                  O_spi_sck   , // SPI時鐘
    output                  O_spi_cs    , // SPI片選信號
    output                  O_spi_mosi    // SPI輸出,用來給從機發送數據 
);

wire            W_rx_en     ;
wire            W_tx_en     ;
wire   [7:0]       W_data_out  ; // 接收到的數據
wire             W_tx_done   ; // 發送最後一個bit標誌位,在最後一個bit產生一個時鐘的高電平
wire             W_rx_done   ; // 接收一個字節完畢

reg                R_rx_en     ;
reg                R_tx_en     ;
reg   [2:0]     R_state     ;

assign W_rx_en         = R_rx_en             ;
assign W_tx_en         = R_tx_en             ;

parameter       C_REG_NUM   =   10      ; // 要配置的寄存器個數,也是ROM的深度

parameter       C_IDLE      =   3'd0    ,
                C_SEND_DATA =   3'd1    ,
                C_DONE      =   3'd2    ;

reg   [3:0]     R_rom_addr      ;
wire  [7:0]     W_rom_out       ;

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_state <= 3'd0 ; 
            R_tx_en <= 1'b0 ; 
            R_rx_en <= 1'b0 ; 
            R_rom_addr <= 4'd0 ; 
        end 
    else
        case(R_state)
            C_IDLE: // 空閒狀態
                begin
                    R_state <= C_SEND_DATA;
                    R_tx_en <= 1'b0 ; 
                    R_rx_en <= 1'b0 ; 
                end                                   
            C_SEND_DATA:  // 發送數據狀態                       
                begin  
                    R_tx_en     <= 1'b1                 ;
                    if(R_rom_addr == C_REG_NUM)
                        begin
                            R_state <= C_DONE;
                            R_tx_en <= 1'b0   ; 
                            R_rx_en <= 1'b0   ; 
                        end
                    else if(W_tx_done) 
                        R_rom_addr     <= R_rom_addr + 1'b1  ;
                    else
                        R_rom_addr     <= R_rom_addr   ;                                                
                end 
            C_DONE:
                begin
                    R_state <= C_DONE ; 
                    R_tx_en <= 1'b0   ; 
                    R_rx_en <= 1'b0   ; 
                end                   
        endcase     
end

rom_cfg rom_cfg_inst (
  .clka         (I_clk      ), // input clka
  .addra        (R_rom_addr    ), // input [3 : 0] addra
  .douta        (W_rom_out  )  // output [7 : 0] douta
);

spi_module U_spi_module
(
    .I_clk       (I_clk), // 全局時鐘50MHz
    .I_rst_n     (I_rst_n), // 復位信號,低電平有效
    .I_rx_en     (W_rx_en), // 讀使能信號
    .I_tx_en     (W_tx_en), // 發送使能信號
    .I_data_in   (W_rom_out), // 要發送的數據
    .O_data_out  (W_data_out), // 接收到的數據
    .O_tx_done   (W_tx_done), // 發送最後一個bit標誌位,在最後一個bit產生一個時鐘的高電平
    .O_rx_done   (W_rx_done), // 接收一個字節完畢(End of Receive)
    
    // 四線標準SPI信號定義
    .I_spi_miso  (I_spi_miso), // SPI串行輸入,用來接收從機的數據
    .O_spi_sck   (O_spi_sck), // SPI時鐘
    .O_spi_cs    (O_spi_cs), // SPI片選信號
    .O_spi_mosi  (O_spi_mosi) // SPI輸出,用來給從機發送數據          
);

////////  Debug  //////////////////////////////////////////////////////////////
wire [35:0] CONTROL0 ;
wire [39:0] TRIG0     ;
icon icon (
    .CONTROL0(CONTROL0) // INOUT BUS [35:0]
);

ila ila (
    .CONTROL(CONTROL0), // INOUT BUS [35:0]
    .CLK(I_clk), // IN
    .TRIG0(TRIG0) // IN BUS [39:0]
);

assign TRIG0[0]      = W_rx_en      ;
assign TRIG0[1]      = W_tx_en      ;
assign TRIG0[9:2]       = W_rom_out    ;
assign TRIG0[17:10]  = W_data_out   ;
assign TRIG0[18]     = W_tx_done      ;
assign TRIG0[19]     = W_rx_done        ;
assign TRIG0[30:28]  = R_state      ;
assign TRIG0[31]     = O_spi_sck    ;
assign TRIG0[32]     = O_spi_cs     ;
assign TRIG0[33]     = O_spi_mosi    ;
assign TRIG0[34]     = I_spi_miso     ;
assign TRIG0[35]     = I_rst_n     ;
assign TRIG0[39:36]  = R_rom_addr     ;
///////////////////////////////////////////////////////////////////////////////

endmodule

複製代碼

  時序圖如下所示:

  從上面的時序圖可以很清楚的看出,當ROM的地址加1以後,ROM的數據是滯後了一個時鐘才輸出的,而ROM數據輸出的時刻(這個時候ROM的輸出數據並沒有穩定)剛好是spi_module模塊發送下個數據最高位的時刻,那麼這就有可能導致數據發送錯誤,從以上時序圖就可以看出8’h33和8’h24兩個數據正確發送了,但是8’h98這個數據就發送錯誤了。

  爲了解決這個問題,其實只需要把spi_module模塊的發送狀態機在加一個冗餘狀態就行了,spi_module模塊的發送狀態機一共有0~15總共16個狀態,那麼我在加一個冗餘狀態,這個狀態執行的操作和最後那個狀態執行的操作完全相同,這樣就預留了一個時鐘的時間用來預先設置好要發送的數據,這樣的效果是發送數據的最後一個bit實際上佔用了3個時鐘週期,其中第一個時鐘週期把O_tx_done拉高,後兩個時鐘週期把O_tx_done拉低。修改後的spi_module模塊的代碼如下:

複製代碼

module spi_module
(
    input               I_clk       , // 全局時鐘50MHz
    input               I_rst_n     , // 復位信號,低電平有效
    input               I_rx_en     , // 讀使能信號
    input               I_tx_en     , // 發送使能信號
    input        [7:0]  I_data_in   , // 要發送的數據
    output  reg  [7:0]  O_data_out  , // 接收到的數據
    output  reg         O_tx_done   , // 發送一個字節完畢標誌位
    output  reg         O_rx_done   , // 接收一個字節完畢標誌位
    
    // 四線標準SPI信號定義
    input               I_spi_miso  , // SPI串行輸入,用來接收從機的數據
    output  reg         O_spi_sck   , // SPI時鐘
    output  reg         O_spi_cs    , // SPI片選信號
    output  reg         O_spi_mosi    // SPI輸出,用來給從機發送數據          
);

reg [4:0]   R_tx_state      ; 
reg [3:0]   R_rx_state      ;

always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_tx_state  <=  5'd0    ;
            R_rx_state  <=  4'd0    ;
            O_spi_cs    <=  1'b1    ;
            O_spi_sck   <=  1'b0    ;
            O_spi_mosi  <=  1'b0    ;
            O_tx_done   <=  1'b0    ;
            O_rx_done   <=  1'b0    ;
            O_data_out  <=  8'd0    ;
        end 
    else if(I_tx_en) // 發送使能信號打開的情況下
        begin
            O_spi_cs    <=  1'b0    ; // 把片選CS拉低
            case(R_tx_state)
                5'd1, 5'd3 , 5'd5 , 5'd7  , 
                5'd9, 5'd11, 5'd13, 5'd15 : //整合奇數狀態
                    begin
                        O_spi_sck   <=  1'b1                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                5'd0:    // 發送第7位
                    begin
                        O_spi_mosi  <=  I_data_in[7]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                5'd2:    // 發送第6位
                    begin
                        O_spi_mosi  <=  I_data_in[6]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                5'd4:    // 發送第5位
                    begin
                        O_spi_mosi  <=  I_data_in[5]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                5'd6:    // 發送第4位
                    begin
                        O_spi_mosi  <=  I_data_in[4]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                5'd8:    // 發送第3位
                    begin
                        O_spi_mosi  <=  I_data_in[3]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end                            
                5'd10:    // 發送第2位
                    begin
                        O_spi_mosi  <=  I_data_in[2]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                5'd12:    // 發送第1位
                    begin
                        O_spi_mosi  <=  I_data_in[1]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                5'd14:    // 發送第0位
                    begin
                        O_spi_mosi  <=  I_data_in[0]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b1                ;
                    end
                5'd16:    // 增加一個冗餘狀態
                    begin
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  5'd0                   ;
                        O_tx_done   <=  1'b0                ;
                    end
                default:R_tx_state  <=  5'd0                ;   
            endcase 
        end
    else if(I_rx_en) // 接收使能信號打開的情況下
        begin
            O_spi_cs    <=  1'b0        ; // 拉低片選信號CS
            case(R_rx_state)
                4'd0, 4'd2 , 4'd4 , 4'd6  , 
                4'd8, 4'd10, 4'd12, 4'd14 : //整合偶數狀態
                    begin
                        O_spi_sck   <=  1'b0                ;
                        R_rx_state  <=  R_rx_state + 1'b1   ;
                        O_rx_done   <=  1'b0                ;
                    end
                4'd1:    // 接收第7位
                    begin                       
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[7]   <=  I_spi_miso          ;   
                    end
                4'd3:    // 接收第6位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[6]   <=  I_spi_miso          ; 
                    end
                4'd5:    // 接收第5位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[5]   <=  I_spi_miso          ; 
                    end 
                4'd7:    // 接收第4位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[4]   <=  I_spi_miso          ; 
                    end 
                4'd9:    // 接收第3位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[3]   <=  I_spi_miso          ; 
                    end                            
                4'd11:    // 接收第2位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[2]   <=  I_spi_miso          ; 
                    end 
                4'd13:    // 接收第1位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[1]   <=  I_spi_miso          ; 
                    end 
                4'd15:    // 接收第0位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b1                ;
                        O_data_out[0]   <=  I_spi_miso          ; 
                    end
                default:R_rx_state  <=  4'd0                    ;   
            endcase 
        end    
    else
        begin
            R_tx_state  <=  4'd0    ;
            R_rx_state  <=  4'd0    ;
            O_tx_done   <=  1'b0    ;
            O_rx_done   <=  1'b0    ;
            O_spi_cs    <=  1'b1    ;
            O_spi_sck   <=  1'b0    ;
            O_spi_mosi  <=  1'b0    ;
            O_data_out  <=  8'd0    ;
        end      
end

endmodule

複製代碼

  時序圖如下所示:

  觀察上面的時序圖可以發現,增加冗餘狀態以後,ROM裏面的10個數據全部發送正確了。最後把代碼綜合生成bit文件,下載到開發板裏面用ChipScope抓出時序圖如下所示

  可以看出,時序和用ModelSim得到的一模一樣。至此,整個用SPI總線傳輸ROM裏面數據的實驗全部結束。

五、 進一步思考

5.1、 如果外設芯片的數據位寬是16-bit或者32-bit怎麼辦?

  上文已經完成了8-bit數據從ROM裏面通過SPI發送出去的例子,16-bit和32-bit可以照着葫蘆畫瓢,無非就是多增加幾個狀態而已。

5.2、 發送數據的狀態機和接收數據的狀態機可以用移位的方式來做

  事實上那個狀態機的發送8-bit數據和接收8-bit數據的部分只有一行代碼是不同的,所以也可以用移位的方法來做,然後把偶數狀態也可以整合到一起,這樣寫的代碼會更短更精煉。但出於理解更容易的角度,還是分開寫較好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章