一、 軟件平臺與硬件平臺
軟件平臺:
1、操作系統:Windows-8.1
2、開發套件:ISE14.7
硬件平臺:
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數據的部分只有一行代碼是不同的,所以也可以用移位的方法來做,然后把偶數狀態也可以整合到一起,這樣寫的代碼會更短更精煉。但出于理解更容易的角度,還是分開寫較好。
審核編輯:劉清
評論