一、 軟件平台與硬件平台
軟件平台:
1、操作系統:Windows-8.1
2、開發套件:ISE14.7
3、仿真工具:ModelSim-10.4-SE
4、Matlab版本:Matlab2014b/Matlab2016a
硬件平台:
1、 FPGA型號:Xilinx公司的XC6SLX45-2CSG324
2、 Flash型號:WinBond公司的W25Q128BV Quad SPI Flash存儲器
提示:如果圖片不清晰,請把圖片在瀏覽器的新建標簽頁打開或保存到本地打開。
二、 原理介紹
上一篇博客《SPI總線的原理與FPGA實現》中已經有關於標准SPI協議的原理與時序的介紹,這里不再贅述。本節主要是討論QSPI(Quad SPI,四線SPI總線)的相關內容。我的開發板上有一片型號是W25Q128BV的Quad SPI Flash存儲器,本文將以它為例子來說明QSPI操作的一些內容。
W25Q128BV的Quad SPI Flash存儲器的Top View如下圖所示
這塊芯片一共有8個有用的管腳,其每個管腳的功能定義如下圖所示
由上圖可知2號管腳DO(IO1),3號管腳 /WP(IO2),5號管腳DI(IO0)以及7號管腳/HOLD(IO3)均為雙向IO口,所以在編寫Verilog代碼的時候要把它們定義為inout類型,inout類型的信號既可以做輸出也可以作為輸入,具體在代碼里面如何處理后文會有介紹。
QSPI Flash每個引腳的詳細描述如下:
1、Chip Select(/CS)
片選信號Chip Select(/CS)的作用是使能或者不使能設備的操作,當CS為高時,表示設備未被選中,串行數據輸出線(DO或IO0,IO1,IO2,IO3)均處於高阻態,當CS為低時,表示設備被選中,FPGA可以給QSPI Flash發送數據或從QSPI Flash接收數據。
2、串行數據輸入信號DI以及串行輸出信號DO
W25Q128BV支持標准SPI協議,雙線SPI(Dual SPI)協議與四線SPI(Quad SPI)協議。標准的SPI協議在串行時鍾信號(SCLK)的上升沿把串行輸入信號DI上的數據存入QSPI Flash中,在串行時鍾信號(SCLK)的下降沿把QSPI Flash中的數據串行化通過單向的DO引腳輸出。而在Dual SPI與Quad SPI中,DI與DO均為雙向信號(既可以作為輸入,也可以作為輸出)。
3、Write Project(/WP)
寫保護信號的作用是防止QSPI Flash的狀態寄存器被寫入錯誤的數據,WP信號低電平有效,但是當狀態寄存器2的QE位被置1時,WP信號失去寫保護功能,它變成Quad SPI的一個雙向數據傳輸信號。
4、HOLD(/HOLD)
HOLD信號的作用是暫停QSPI Flash的操作。當HOLD信號為低,並且CS也為低時,串行輸出信號DO將處於高阻態,串行輸入信號DI與串行時鍾信號SCLK將被QSPI Flash忽略。當HOLD拉高以后,QSPI Flash的讀寫操作能繼續進行。當多個SPI設備共享同一組SPI總線相同的信號的時候,可以通過HOLD來切換信號的流向。和WP信號一樣,當當狀態寄存器2的QE位被置1時,HOLD信號失去保持功能,它也變成Quad SPI的一個雙向數據傳輸信號。
5、串行時鍾線
串行時鍾線用來提供串行輸入輸出操作的時鍾。
W25Q128BV的內部結構框圖如下圖所示:
更多詳細的內容請閱讀W25Q128BV的芯片手冊。由於本文要進行4線SPI的操作,但QSPI Flash默認的操作模式是標准單線SPI模式,所以在每次進行4線SPI操作的時候一定要先把狀態寄存器2的QE位(倒數第2位)置1,然后才能進行QSPI操作。
最后介紹一下我的開發板上QSPI Flash硬件原理圖如下圖所示:
三、 目標任務
1、編寫標准SPI 協議 Verilog代碼來操作QSPI Flash,並用ChipScope抓出各個指令的時序與芯片手冊提供的時序進行對比
2、在標准SPI協議的基礎上增加Quad SPI的功能,並用ChipScope抓出Quad SPI的讀寫數據的時序
3、對比標准SPI與Quad SPI讀寫W25Q128BV的ChipScope時序,感受二者的效率差距
四、 設計思路與Verilog代碼編寫
4.1、 命令類型的定義
W25Q128BV一共有35條命令,這里不可能把所有命令的邏輯都寫出來,所以截取了一部分常用的命令作為示例來說明QSPI Flash的操作方法。由於命令數目很多,所以在這個部分先對各個命令類型做一個初步定義,下文的代碼就是按照這個定義來編寫的。
命令編號 |
命令類型(自定義) |
命令碼(芯片手冊定義) |
命令功能 |
1 |
5’b0XXXX |
8’h00 |
無 |
2 |
5’b10000 |
8’h90 |
讀設備ID |
3 |
5’b10001 |
8’h06 |
寫使能 |
4 |
5’b10010 |
8’h20 |
扇區擦除 |
5 |
5’b10011 |
8’h05/8’h35 |
讀狀態寄存器1/2 |
6 |
5’b10100 |
8’h04 |
關閉寫使能 |
7 |
5’b10101 |
8’h02 |
寫數據操作(單線模式) |
8 |
5’b10110 |
8’h01 |
寫狀態寄存器 |
9 |
5’b10111 |
8’h03 |
讀數據操作(單線模式) |
10 |
5’b11000 |
8’h32 |
寫數據操作(四線模式) |
11 |
5’b11001 |
8’h6b |
讀數據操作(四線模式) |
說明:
1、命令類型是我自己隨便定義的,可以隨便修改。命令碼是芯片手冊上定義好,不能修改,更詳細的內容請參考W25Q128芯片手冊。
2、命令類型的最高位是使能位,只有當最高位為1時,命令才有效(在代碼里面寫的就是只有當最高位為1時才能進入SPI操作的狀態機)。
3、進行四線讀寫操作之前,一定要把四線讀寫功能的使能位打開,方法是通過寫狀態寄存器命令把狀態寄存器2的QE位(倒數第二位)置1。
4.2、 如何用Matlab產生存放在ROM中的.coe文件格式的數據
上一節設計了一個把存放在ROM中的數據用SPI總線發出來的例子,ROM里面只存放了10個數據,所以可以直接把這10個數據填寫到.coe文件就可以了,由於QSPI Flash的頁編程(寫數據)指令最大支持256字節的寫操作,所以下面的例子的功能是把ROM中存放的256個字節(8-bit)數據先寫入QSPI Flash中,然后在讀出來。由於數據太多(256個),所以一個一個填寫肯定不現實,所以可以利用Matlab來直接產生.coe文件,Matlab的完整代碼如下:
width=8; %rom中數據的寬度 depth=256; %rom的深度 y=0:255; y=fliplr(y); %產生要發送的數據,255,254,253,...... ,2,1,0 fid = fopen('test_data.coe', 'w'); % 打開一個.coe文件 % 存放在ROM中的.coe文件第一行必須是這個字符串,16表示16進制,可以改成其他進制 fprintf(fid,'memory_initialization_radix=16;\n'); % 存放在ROM中的.coe文件第二行必須是這個字符串 fprintf(fid,'memory_initialization_vector=\n'); % 把前255個數據寫入.coe文件中,並用逗號隔開,為了方便知道數據的個數,每行只寫一個數據 fprintf(fid,'%x,\n',y(1:end-1)); % 把最后一個數據寫入.coe文件中,並用分號結尾 fprintf(fid,'%x;\n',y(end)); fclose(fid); % 關閉文件指針
用Matlab2014b運行上面的代碼以后會在與這個.m文件相同的目錄下產生一個.coe文件,這個.coe文件可以導入到ROM中。
4.3、 標准SPI總線操作QSPI Flash思路與代碼編寫
上一篇博客《SPI總線的原理與FPGA實現》已經介紹過用spi_module這個模塊去讀取QSPI Flash的Manufacturer/Device ID,事實上除了上篇博客提供的那種方法以外,還可以直接在時鍾信號的下降沿發送數據,時鍾信號的上升沿來接受數據來完成讀ID的操作,當FPGA在時鍾的下降沿發送數據的時候,那么時鍾的上升沿剛好在數據的正中間,QSPI Flash剛好可以在這個上升沿把數據讀進來,讀操作則正好相反。但是有很多有經驗的人告訴我在設計中如非必要最好不要使用時鍾下降沿觸發的設計方法,可能是因為大多數FPGA里面的Flip Flops資源都是上升沿觸發的,如果在Verilog代碼采用下降沿觸發的話 ,綜合的時候會在CLK輸入信號前面綜合出一個反相器,這個反相器可能會對時鍾信號的質量有影響,具體的原因等我再Google上繼續搜索一段時間在說。這個例子由於狀態機相較前幾篇博客來說相對復雜,所以接下來寫代碼我還是采用下降沿發送數據,上升沿接收數據的方式來描述這個狀態機。
接下來的任務就是抽象出一個狀態機。上一篇博客僅僅讀一個ID就用了6個狀態,所以采用上一篇博客的設計思路顯然不太現實,但對於初學者而言,上一篇博客仍然有一個基本的指引作用。通過閱讀QSPI Flash的芯片手冊,可以發現,所有的命令其實至多由以下三個部分組成:
1、發送8-bit的命令碼
2、發送24-bit的地址碼
3、發送數據或接收數據
所有命令的狀態跳變圖可由下圖描述
所以按照這個思路來思考的話抽象出來的狀態機的狀態並不多。單線模式的狀態為以下幾個:
1、空閑狀態:用來初始化各個寄存器的值
2、發送命令狀態:用來發送8-bit的命令碼
3、發送地址狀態:用來發送24-bit的地址碼
4、讀等待狀態:當讀數據操作正在進行的時候進入此狀態等待讀數據完畢
5、寫數據狀態(單線模式):在這個狀態FPGA往QSPI Flash里面寫數據
6、結束狀態:一條指令操作結束,並給出一個結束標志
完整的代碼如下:
`timescale 1ns / 1ps module qspi_driver ( output O_qspi_clk , // SPI總線串行時鍾線 output reg O_qspi_cs , // SPI總線片選信號 output reg O_qspi_mosi , // SPI總線輸出信號線,也是QSPI Flash的輸入信號線 input I_qspi_miso , // SPI總線輸入信號線,也是QSPI Flash的輸出信號線 input I_rst_n , // 復位信號 input I_clk_25M , // 25MHz時鍾信號 input [4:0] I_cmd_type , // 命令類型 input [7:0] I_cmd_code , // 命令碼 input [23:0] I_qspi_addr , // QSPI Flash地址 output reg O_done_sig , // 指令執行結束標志 output reg [7:0] O_read_data , // 從QSPI Flash讀出的數據 output reg O_read_byte_valid , // 讀一個字節完成的標志 output reg [3:0] O_qspi_state // 狀態機,用於在頂層調試用 ); parameter C_IDLE = 4'b0000 ; // 空閑狀態 parameter C_SEND_CMD = 4'b0001 ; // 發送命令碼 parameter C_SEND_ADDR = 4'b0010 ; // 發送地址碼 parameter C_READ_WAIT = 4'b0011 ; // 讀等待 parameter C_WRITE_DATA = 4'b0101 ; // 寫數據 parameter C_FINISH_DONE = 4'b0110 ; // 一條指令執行結束 reg [7:0] R_read_data_reg ; // 從Flash中讀出的數據用這個變量進行緩存,等讀完了在把這個變量的值給輸出 reg R_qspi_clk_en ; // 串行時鍾使能信號 reg R_data_come_single ; // 單線操作讀數據使能信號,當這個信號為高時 reg [7:0] R_cmd_reg ; // 命令碼寄存器 reg [23:0] R_address_reg ; // 地址碼寄存器 reg [7:0] R_write_bits_cnt ; // 寫bit計數器,寫數據之前把它初始化為7,發送一個bit就減1 reg [8:0] R_write_bytes_cnt ; // 寫字節計數器,發送一個字節數據就把它加1 reg [7:0] R_read_bits_cnt ; // 寫bit計數器,接收一個bit就加1 reg [8:0] R_read_bytes_cnt ; // 讀字節計數器,接收一個字節數據就把它加1 reg [8:0] R_read_bytes_num ; // 要接收的數據總數 reg R_read_finish ; // 讀數據結束標志位 wire [7:0] W_rom_addr ; wire [7:0] W_rom_out ; assign O_qspi_clk = R_qspi_clk_en ? I_clk_25M : 0 ; // 產生串行時鍾信號 assign W_rom_addr = R_write_bytes_cnt ; //////////////////////////////////////////////////////////////////////////////////////////// // 功能:用時鍾的下降沿發送數據 //////////////////////////////////////////////////////////////////////////////////////////// always @(negedge I_clk_25M) begin if(!I_rst_n) begin O_qspi_cs <= 1'b1 ; O_qspi_state <= C_IDLE ; R_cmd_reg <= 0 ; R_address_reg <= 0 ; R_qspi_clk_en <= 1'b0 ; //SPI clock輸出不使能 R_write_bits_cnt <= 0 ; R_write_bytes_cnt <= 0 ; R_read_bytes_num <= 0 ; R_address_reg <= 0 ; O_done_sig <= 1'b0 ; R_data_come_single <= 1'b0 ; end else begin case(O_qspi_state) C_IDLE: // 初始化各個寄存器,當檢測到命令類型有效(命令類型的最高位位1)以后,進入發送命令碼狀態 begin R_qspi_clk_en <= 1'b0 ; O_qspi_cs <= 1'b1 ; O_qspi_mosi <= 1'b0 ; R_cmd_reg <= I_cmd_code ; R_address_reg <= I_qspi_addr ; O_done_sig <= 1'b0 ; if(I_cmd_type[4] == 1'b1) begin //如果flash操作命令請求 O_qspi_state <= C_SEND_CMD ; R_write_bits_cnt <= 7 ; R_write_bytes_cnt <= 0 ; R_read_bytes_num <= 0 ; end end C_SEND_CMD: // 發送8-bit命令碼狀態 begin R_qspi_clk_en <= 1'b1 ; // 打開SPI串行時鍾SCLK的使能開關 O_qspi_cs <= 1'b0 ; // 拉低片選信號CS if(R_write_bits_cnt > 0) begin //如果R_cmd_reg還沒有發送完 O_qspi_mosi <= R_cmd_reg[R_write_bits_cnt] ; //發送bit7~bit1位 R_write_bits_cnt <= R_write_bits_cnt-1'b1 ; end else begin //發送bit0 O_qspi_mosi <= R_cmd_reg[0] ; if ((I_cmd_type[3:0] == 4'b0001) | (I_cmd_type[3:0] == 4'b0100)) begin //如果是寫使能指令(Write Enable)或者寫不使能指令(Write Disable) O_qspi_state <= C_FINISH_DONE ; end else if (I_cmd_type[3:0] == 4'b0011) begin //如果是讀狀態寄存器指令(Read Register) O_qspi_state <= C_READ_WAIT ; R_write_bits_cnt <= 7 ; R_read_bytes_num <= 1 ;//讀狀態寄存器指令需要接收一個數據 end else if( (I_cmd_type[3:0] == 4'b0010) || (I_cmd_type[3:0] == 4'b0101) || (I_cmd_type[3:0] == 4'b0111) || (I_cmd_type[3:0] == 4'b0000) ) begin // 如果是扇區擦除(Sector Erase),頁編程指令(Page Program),讀數據指令(Read Data),讀設備ID指令(Read Device ID) O_qspi_state <= C_SEND_ADDR ; R_write_bits_cnt <= 23 ; // 這幾條指令后面都需要跟一個24-bit的地址碼 end end end C_SEND_ADDR: // 發送地址狀態 begin if(R_write_bits_cnt > 0) //如果R_cmd_reg還沒有發送完 begin O_qspi_mosi <= R_address_reg[R_write_bits_cnt] ; //發送bit23~bit1位 R_write_bits_cnt <= R_write_bits_cnt - 1 ; end else begin O_qspi_mosi <= R_address_reg[0] ; //發送bit0 if(I_cmd_type[3:0] == 4'b0010) // 扇區擦除(Sector Erase)指令 begin //扇區擦除(Sector Erase)指令發完24-bit地址碼就執行結束了,所以直接跳到結束狀態 O_qspi_state <= C_FINISH_DONE ; end else if (I_cmd_type[3:0] == 4'b0101) // 頁編程(Page Program)指令 begin O_qspi_state <= C_WRITE_DATA ; R_write_bits_cnt <= 7 ; end else if (I_cmd_type[3:0] == 4'b0000) // 讀Device ID指令 begin O_qspi_state <= C_READ_WAIT ; R_read_bytes_num <= 2 ; //接收2個數據的Device ID end else if (I_cmd_type[3:0] == 4'b0111) // 讀數據(Read Data)指令 begin O_qspi_state <= C_READ_WAIT ; R_read_bytes_num <= 256 ; //接收256個數據 end end end C_READ_WAIT: // 讀等待狀態 begin if(R_read_finish) begin O_qspi_state <= C_FINISH_DONE ; R_data_come_single <= 1'b0 ; end else begin R_data_come_single <= 1'b1 ; // 單線模式下讀數據標志信號,此信號為高標志正在接收數據 end end C_WRITE_DATA: // 寫數據狀態 begin if(R_write_bytes_cnt < 256) // 往QSPI Flash中寫入 256個數據 begin if(R_write_bits_cnt > 0) //如果數據還沒有發送完 begin O_qspi_mosi <= W_rom_out[R_write_bits_cnt] ; //發送bit7~bit1位 R_write_bits_cnt <= R_write_bits_cnt - 1'b1 ; end else begin O_qspi_mosi <= W_rom_out[0] ; //發送bit0 R_write_bits_cnt <= 7 ; R_write_bytes_cnt <= R_write_bytes_cnt + 1'b1 ; end end else begin O_qspi_state <= C_FINISH_DONE ; R_qspi_clk_en <= 1'b0 ; end end C_FINISH_DONE: begin O_qspi_cs <= 1'b1 ; O_qspi_mosi <= 1'b0 ; R_qspi_clk_en <= 1'b0 ; O_done_sig <= 1'b1 ; R_data_come_single <= 1'b0 ; O_qspi_state <= C_IDLE ; end default:O_qspi_state <= C_IDLE ; endcase end end ////////////////////////////////////////////////////////////////////////////// // 功能:接收QSPI Flash發送過來的數據 ////////////////////////////////////////////////////////////////////////////// always @(posedge I_clk_25M) begin if(!I_rst_n) begin R_read_bytes_cnt <= 0 ; R_read_bits_cnt <= 0 ; R_read_finish <= 1'b0 ; O_read_byte_valid <= 1'b0 ; R_read_data_reg <= 0 ; O_read_data <= 0 ; end else if(R_data_come_single) // 此信號為高表示接收數據從QSPI Flash發過來的數據 begin if(R_read_bytes_cnt < R_read_bytes_num) begin if(R_read_bits_cnt < 7) //接收一個Byte的bit0~bit6 begin O_read_byte_valid <= 1'b0 ; R_read_data_reg <= {R_read_data_reg[6:0],I_qspi_miso} ; R_read_bits_cnt <= R_read_bits_cnt + 1'b1 ; end else begin O_read_byte_valid <= 1'b1 ; //一個byte數據有效 O_read_data <= {R_read_data_reg[6:0],I_qspi_miso} ; //接收bit7 R_read_bits_cnt <= 0 ; R_read_bytes_cnt <= R_read_bytes_cnt + 1'b1 ; end end else begin R_read_bytes_cnt <= 0 ; R_read_finish <= 1'b1 ; O_read_byte_valid <= 1'b0 ; end end else begin R_read_bytes_cnt <= 0 ; R_read_bits_cnt <= 0 ; R_read_finish <= 1'b0 ; O_read_byte_valid <= 1'b0 ; R_read_data_reg <= 0 ; end end rom_data rom_data_inst ( .clka(I_clk_25M), // input clka .addra(W_rom_addr), // input [7 : 0] addra .douta(W_rom_out) // output [7 : 0] douta ); endmodule
接下來就是寫一個測試代碼對這個單線模式SPI驅動,為了保證把上面的所有指令都測試一遍,測試代碼如下:
module qspi_top ( input I_clk , input I_rst_n , output O_qspi_clk , // SPI總線串行時鍾線 output O_qspi_cs , // SPI總線片選信號 output O_qspi_mosi , // SPI總線輸出信號線,也是QSPI Flash的輸入信號線 input I_qspi_miso // SPI總線輸入信號線,也是QSPI Flash的輸出信號線 ); reg [3:0] R_state ; reg [7:0] R_flash_cmd ; reg [23:0] R_flash_addr ; reg R_clk_25M ; reg [4:0] R_cmd_type ; wire W_done_sig ; wire [7:0] W_read_data ; wire W_read_byte_valid ; wire [2:0] R_qspi_state ; //////////////////////////////////////////////////////////////////// //功能:二分頻邏輯 //////////////////////////////////////////////////////////////////// always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_clk_25M <= 1'b0 ; else R_clk_25M <= ~R_clk_25M ; end //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// //功能:測試狀態機 //////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) begin R_state <= 4'd0 ; R_flash_addr <= 24'd0 ; R_flash_cmd <= 8'h00 ; R_cmd_type <= 5'b0_0000 ; end else begin case(R_state) 4'd0://讀Device ID指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h90 ; R_flash_addr <= 24'd0 ; R_cmd_type <= 5'b1_0000 ; end end 4'd1://寫Write disable instruction begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h04 ; R_cmd_type <= 5'b1_0100 ; end end 4'd2://寫使能(Write Enable)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h06 ; R_cmd_type <= 5'b1_0001 ; end end 4'd3:// 扇區擦除(Sector Erase)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h20 ; R_flash_addr<= 24'd0 ; R_cmd_type <= 5'b1_0010 ; end end 4'd4://讀狀態寄存器1, 當Busy位(狀態寄存器1的最低位)為0時表示擦除操作完成 begin if(W_done_sig) begin if(W_read_data[0]==1'b0) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end 4'd5://寫使能(Write Enable)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h06 ; R_cmd_type <= 5'b1_0001 ; end end 4'd6: //頁編程操作(Page Program): 把存放在ROM中的數據寫入QSPI Flash中 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h02 ; R_flash_addr<= 24'd0 ; R_cmd_type <= 5'b1_0101 ; end end 4'd7://讀狀態寄存器1, 當Busy位(狀態寄存器1的最低位)為0時表示寫操作完成 begin if(W_done_sig) begin if(W_read_data[0]==1'b0) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end 4'd8://讀256 Bytes begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h03 ; R_flash_addr<= 24'd0 ; R_cmd_type <= 5'b1_0111 ; end end 4'd9:// 空閑狀態 begin R_flash_cmd <= 8'h00 ; R_state <= 4'd9 ; R_cmd_type <= 5'b0_0000 ; end default : R_state <= 4'd0 ; endcase end end qspi_driver U_qspi_driver ( .O_qspi_clk (O_qspi_clk ), // SPI總線串行時鍾線 .O_qspi_cs (O_qspi_cs ), // SPI總線片選信號 .O_qspi_mosi (O_qspi_mosi ), // SPI總線輸出信號線,也是QSPI Flash的輸入信號線 .I_qspi_miso (I_qspi_miso ), // SPI總線輸入信號線,也是QSPI Flash的輸出信號線 .I_rst_n (I_rst_n ), // 復位信號 .I_clk_25M (R_clk_25M ), // 25MHz時鍾信號 .I_cmd_type (R_cmd_type ), // 命令類型 .I_cmd_code (R_flash_cmd ), // 命令碼 .I_qspi_addr (R_flash_addr ), // QSPI Flash地址 .O_done_sig (W_done_sig ), // 指令執行結束標志 .O_read_data (W_read_data ), // 從QSPI Flash讀出的數據 .O_read_byte_valid (W_read_byte_valid ), // 讀一個字節完成的標志 .O_qspi_state (R_qspi_state ) // 狀態機,用於在頂層調試用 ); wire [35:0] CONTROL0 ; wire [69:0] TRIG0 ; icon icon_inst ( .CONTROL0(CONTROL0) // INOUT BUS [35:0] ); ila ila_inst ( .CONTROL(CONTROL0) , // INOUT BUS [35:0] .CLK(I_clk) , // IN .TRIG0(TRIG0) // IN BUS [255:0] ); assign TRIG0[7:0] = W_read_data ; assign TRIG0[8] = W_read_byte_valid ; assign TRIG0[12:9] = R_state ; assign TRIG0[16:13] = R_qspi_state ; assign TRIG0[17] = W_done_sig ; assign TRIG0[18] = I_qspi_miso ; assign TRIG0[19] = O_qspi_mosi ; assign TRIG0[20] = O_qspi_cs ; assign TRIG0[21] = O_qspi_clk ; assign TRIG0[26:22] = R_cmd_type ; assign TRIG0[34:27] = R_flash_cmd ; assign TRIG0[58:35] = R_flash_addr ; assign TRIG0[59] = I_rst_n ; endmodule
接下來主要看看用ChipScope抓出來的時序圖和芯片手冊規定的時序圖是否完全一致。
1、讀ID指令
芯片手冊指令的讀ID指令時序圖:
ChipScope抓出來的時序圖
2、寫不使能(Write Disable)指令
芯片手冊指令的寫不使能時序圖:
ChipScope抓出來的寫不使能時序圖:
3、寫使能(Write Enable)指令
芯片手冊指令的寫使能時序圖:
ChipScope抓出來的寫使能時序圖
3、扇區擦除(Sector Erase)指令
芯片手冊的扇區擦除指令時序圖:
芯片手冊的扇區擦除指令時序圖:
4、讀狀態寄存器(Read Status Register)指令
芯片手冊的讀狀態寄存器指令時序圖:
ChipScope抓回來的讀狀態寄存器指令時序圖:
由於在擦除操作和寫操作(Page Program)操作指令發送完畢以后,芯片內部會自己執行相關的操作並把狀態寄存器1中的最低位Busy位拉高,狀態寄存器1的各位如下:
所以讀狀態寄存器指令的目的是為了檢測Busy是否為高,Busy為高的話說明擦除操作或寫操作(Page Program)操作指令並未完成,這種情況應該等待不發送其他指令,等到Busy位拉低以后說明擦除操作或寫操作(Page Program)操作指令已經完成,這時才可以執行后續的操作。值得注意的是,在上圖中讀到8個bit數據(上圖讀到的數據是0000_0011(8’h03))以后,O_qpsi_cs和O_qspi_clk信號仍然持續了2個周期的時間才變為空閑狀態,實際上,回過頭去看代碼的話很容易解釋這種情況,由於代碼里面是用時鍾的下降沿發送數據,而用時鍾的上升沿來接收數據,發送數據與接收數據的狀態切換是通過R_data_coming_signal和R_read_finish信號來轉換的,所以導致了這種情況的發生。但是讀數據相關的指令並不要求完全8-bit對齊,如果在讀數據的過程中只要O_qpsi_cs一直為低,那么會一直重復讀下去,直至O_qpsi_cs拉高為止才停止讀數據,所以上面多出來的兩個時鍾並不會影響結果的正確性。
5、寫數據(Page Program)指令(單線模式)
芯片手冊的寫數據指令時序圖:
ChipScope抓回來的時序圖
6、讀數據(Read Data)指令(單線模式)
芯片手冊的讀數據指令時序圖:
ChipScope抓回來的時序圖:
通過上面用ChipScope抓回來的時序圖與芯片手冊的時序圖進行對比可以很清晰的理解整個QSPI Flash的操作流程與時序細節。
4.4、 如何處理雙向信號(Verilog中用關鍵詞inout定義的信號都是雙向信號)
四線操作中IO0,IO1,IO2和IO3全部都可以用來發送數據以及接收數據,所以在編寫代碼的時候要把它們全部定義成雙向類型的信號,即inout類型。所以在編寫四線操作的代碼之前有必要提前熟悉一下inout信號的處理方法。
總的來說,inout信號大致有以下4個特征:
1、inout端口不能被賦值為reg型,因此,不能用於always語句中。
2、對於inout端口的邏輯判斷,要用到?:條件表達式,來控制高阻的賦值
3、inout信號需要有一個中轉的寄存器,在always語句中才可以將輸入的信號賦給輸出(用inout代替純output)
4、高阻態不要用於芯片內部,應該用邏輯引到引腳處,然后用高阻來實現。
實際上,FPGA內部的雙向信號是通過一個三態門來實現的,一個典型的三態門結構如下
描述這個三態門的Verilog代碼如下:
module Test_inout ( input I_clk, input I_rst_n, . . . inout IO_data, . . . ) reg R_data_out ; wire I_data_in ; assign IO_data = Control ? R_data_out : 1'bz ; assign I_data_in = IO_data ; always @(posedge I_clk or negedge I_rst_n) begin . . . ; end endmodule
上面的代碼表達的意思是:當Control為1是,IO_data信號作為輸出,輸出R_data_out的值,當Control為0時,IO_data信號作為輸入,輸入的值賦給I_data_in變量。
值得注意的是,inout作為輸出的時候不能直接給他賦值,而需要通過一個中間變量R_data_out來間接賦值,同時inout變量的輸入輸出屬性必須通過一個控制信號Control來控制,控制性號為高時為inout信號作為輸出,輸出R_data_out的值,為低時inout處於高阻狀態,這時才作為輸入接收外部的數據。
4.5、 四線SPI總線操作QSPI Flash思路與代碼編寫
上一小節已經完成了單線模式QSPI Flash的讀寫以及其他相關操作,這一節將在上一節的基礎上加上四線讀寫功能。通過進一步閱讀W25Q128BV的芯片手冊可知,在進行四線讀寫操作之前一定要把QE(Quad Enable)位置為1,而QE位在狀態寄存器2的倒數第二位,見下圖
除此以外,四線讀操作還需要在讀數據之前等待8個dummy clock用來加快讀數據的速度(詳細內容請閱讀芯片手冊)。所以,四線操作相對於單線操作而言除了要增加四線模式讀寫數據的狀態以外還要增加一個寫狀態寄存器的功能用來開啟QE(Quad Enable)位以及一個dummy用來等待8個clock。
綜上所述,四線模式的狀態為以下幾個:
1、空閑狀態:用來初始化各個寄存器的值
2、發送命令狀態:用來發送8-bit的命令碼
3、發送地址狀態:用來發送24-bit的地址碼
4、讀等待狀態(單線模式):當讀數據操作正在進行的時候進入此狀態等待讀數據完畢
5、寫數據狀態(單線模式):在這個狀態FPGA往QSPI Flash里面寫數據
6、寫狀態寄存器狀態:用來把狀態寄存器的QE(Quad Enable)置1
7、Dummy Clock狀態:四線讀數據之前需要等待8個dummy clock
8、寫數據狀態(四線模式):在這個狀態FPGA往QSPI Flash里面通過四線模式寫數據
9、讀等待狀態(四線模式):在這個狀態等待FPGA從QSPI Flash里面通過四線模式讀數據完成
10、結束狀態:一條指令操作結束,並給出一個結束標志
其中標紅的狀態是四線模式的代碼在單線模式代碼的基礎上增加的四個狀態。
四線模式的完整代碼如下所示:
`timescale 1ns / 1ps module qspi_driver ( output O_qspi_clk , // QSPI Flash Quad SPI(QPI)總線串行時鍾線 output reg O_qspi_cs , // QPI總線片選信號 inout IO_qspi_io0 , // QPI總線輸入/輸出信號線 inout IO_qspi_io1 , // QPI總線輸入/輸出信號線 inout IO_qspi_io2 , // QPI總線輸入/輸出信號線 inout IO_qspi_io3 , // QPI總線輸入/輸出信號線 input I_rst_n , // 復位信號 input I_clk_25M , // 25MHz時鍾信號 input [4:0] I_cmd_type , // 命令類型 input [7:0] I_cmd_code , // 命令碼 input [23:0] I_qspi_addr , // QSPI Flash地址 input [15:0] I_status_reg , // QSPI Flash狀態寄存器的值 output reg O_done_sig , // 指令執行結束標志 output reg [7:0] O_read_data , // 從QSPI Flash讀出的數據 output reg O_read_byte_valid , // 讀一個字節完成的標志 output reg [3:0] O_qspi_state // 狀態機,用於在頂層調試用 ); parameter C_IDLE = 4'b0000 ; // 空閑狀態 parameter C_SEND_CMD = 4'b0001 ; // 發送命令碼 parameter C_SEND_ADDR = 4'b0010 ; // 發送地址碼 parameter C_READ_WAIT = 4'b0011 ; // 單線模式讀等待 parameter C_WRITE_DATA = 4'b0101 ; // 單線模式寫數據到QSPI Flash parameter C_FINISH_DONE = 4'b0110 ; // 一條指令執行結束 parameter C_WRITE_STATE_REG = 4'b0111 ; // 寫狀態寄存器 parameter C_WRITE_DATA_QUAD = 4'b1000 ; // 四線模式寫數據到QSPI Flash parameter C_DUMMY = 4'b1001 ; // 四線模式讀數據需要8個時鍾周期的dummy clock,這可以加快讀數據的速度 parameter C_READ_WAIT_QUAD = 4'b1010 ; // 四線模式讀等待狀態 // QSPI Flash IO輸入輸出狀態控制寄存器 reg R_qspi_io0 ; reg R_qspi_io1 ; reg R_qspi_io2 ; reg R_qspi_io3 ; reg R_qspi_io0_out_en ; reg R_qspi_io1_out_en ; reg R_qspi_io2_out_en ; reg R_qspi_io3_out_en ; reg [7:0] R_read_data_reg ; // 從Flash中讀出的數據用這個變量進行緩存,等讀完了在把這個變量的值給輸出 reg R_qspi_clk_en ; // 串行時鍾使能信號 reg R_data_come_single ; // 單線操作讀數據使能信號,當這個信號為高時 reg R_data_come_quad ; // 單線操作讀數據使能信號,當這個信號為高時 reg [7:0] R_cmd_reg ; // 命令碼寄存器 reg [23:0] R_address_reg ; // 地址碼寄存器 reg [15:0] R_status_reg ; // 狀態寄存器 reg [7:0] R_write_bits_cnt ; // 寫bit計數器,寫數據之前把它初始化為7,發送一個bit就減1 reg [8:0] R_write_bytes_cnt ; // 寫字節計數器,發送一個字節數據就把它加1 reg [7:0] R_read_bits_cnt ; // 寫bit計數器,接收一個bit就加1 reg [8:0] R_read_bytes_cnt ; // 讀字節計數器,接收一個字節數據就把它加1 reg [8:0] R_read_bytes_num ; // 要接收的數據總數 reg R_read_finish ; // 讀數據結束標志位 wire [7:0] W_rom_addr ; wire [7:0] W_rom_out ; assign O_qspi_clk = R_qspi_clk_en ? I_clk_25M : 0 ; // 產生串行時鍾信號 assign W_rom_addr = R_write_bytes_cnt ; // QSPI IO方向控制 assign IO_qspi_io0 = R_qspi_io0_out_en ? R_qspi_io0 : 1'bz ; assign IO_qspi_io1 = R_qspi_io1_out_en ? R_qspi_io1 : 1'bz ; assign IO_qspi_io2 = R_qspi_io2_out_en ? R_qspi_io2 : 1'bz ; assign IO_qspi_io3 = R_qspi_io3_out_en ? R_qspi_io3 : 1'bz ; //////////////////////////////////////////////////////////////////////////////////////////// // 功能:用時鍾的下降沿發送數據 //////////////////////////////////////////////////////////////////////////////////////////// always @(negedge I_clk_25M) begin if(!I_rst_n) begin O_qspi_cs <= 1'b1 ; O_qspi_state <= C_IDLE ; R_cmd_reg <= 0 ; R_address_reg <= 0 ; R_qspi_clk_en <= 1'b0 ; //QSPI clock輸出不使能 R_write_bits_cnt <= 0 ; R_write_bytes_cnt <= 0 ; R_read_bytes_num <= 0 ; R_address_reg <= 0 ; O_done_sig <= 1'b0 ; R_data_come_single <= 1'b0 ; R_data_come_quad <= 1'b0 ; end else begin case(O_qspi_state) C_IDLE: // 初始化各個寄存器,當檢測到命令類型有效(命令類型的最高位位1)以后,進入發送命令碼狀態 begin R_qspi_clk_en <= 1'b0 ; O_qspi_cs <= 1'b1 ; R_qspi_io0 <= 1'b0 ; R_cmd_reg <= I_cmd_code ; R_address_reg <= I_qspi_addr ; R_status_reg <= I_status_reg ; O_done_sig <= 1'b0 ; R_qspi_io3_out_en <= 1'b0 ; // 設置IO_qspi_io3為高阻 R_qspi_io2_out_en <= 1'b0 ; // 設置IO_qspi_io2為高阻 R_qspi_io1_out_en <= 1'b0 ; // 設置IO_qspi_io1為高阻 R_qspi_io0_out_en <= 1'b0 ; // 設置IO_qspi_io0為高阻 if(I_cmd_type[4] == 1'b1) begin //如果flash操作命令請求 O_qspi_state <= C_SEND_CMD ; R_write_bits_cnt <= 7 ; R_write_bytes_cnt <= 0 ; R_read_bytes_num <= 0 ; end end C_SEND_CMD: // 發送8-bit命令碼狀態 begin R_qspi_io0_out_en <= 1'b1 ; // 設置IO_qspi_io0為輸出 R_qspi_clk_en <= 1'b1 ; // 打開SPI串行時鍾SCLK的使能開關 O_qspi_cs <= 1'b0 ; // 拉低片選信號CS if(R_write_bits_cnt > 0) begin //如果R_cmd_reg還沒有發送完 R_qspi_io0 <= R_cmd_reg[R_write_bits_cnt] ; //發送bit7~bit1位 R_write_bits_cnt <= R_write_bits_cnt-1'b1 ; end else begin //發送bit0 R_qspi_io0 <= R_cmd_reg[0] ; if ((I_cmd_type[3:0] == 4'b0001) | (I_cmd_type[3:0] == 4'b0100)) begin //如果是寫使能指令(Write Enable)或者寫不使能指令(Write Disable) O_qspi_state <= C_FINISH_DONE ; end else if (I_cmd_type[3:0] == 4'b0011) begin //如果是讀狀態寄存器指令(Read Register) O_qspi_state <= C_READ_WAIT ; R_write_bits_cnt <= 7 ; R_read_bytes_num <= 1 ;//讀狀態寄存器指令需要接收一個數據 end else if( (I_cmd_type[3:0] == 4'b0010) || // 如果是扇區擦除(Sector Erase) (I_cmd_type[3:0] == 4'b0101) || // 如果是頁編程指令(Page Program) (I_cmd_type[3:0] == 4'b0111) || // 如果是讀數據指令(Read Data) (I_cmd_type[3:0] == 4'b0000) || // 如果是讀設備ID指令(Read Device ID) (I_cmd_type[3:0] == 4'b1000) || // 如果是四線模式頁編程指令(Quad Page Program) (I_cmd_type[3:0] == 4'b1001) // 如果是四線模式讀數據指令(Quad Read Data) ) begin O_qspi_state <= C_SEND_ADDR ; R_write_bits_cnt <= 23 ; // 這幾條指令后面都需要跟一個24-bit的地址碼 end else if (I_cmd_type[3:0] == 4'b0110) begin //如果是Write Status Register O_qspi_state <= C_WRITE_STATE_REG ; R_write_bits_cnt <= 15 ; end end end C_WRITE_STATE_REG : begin R_qspi_io0_out_en <= 1'b1 ; // 設置IO0為輸出 if(R_write_bits_cnt > 0) begin //如果R_cmd_reg還沒有發送完 R_qspi_io0 <= R_status_reg[R_write_bits_cnt] ; //發送bit15~bit1位 R_write_bits_cnt <= R_write_bits_cnt - 1 ; end else begin //發送bit0 R_qspi_io0 <= R_status_reg[0] ; O_qspi_state <= C_FINISH_DONE ; end end C_SEND_ADDR: // 發送地址狀態 begin R_qspi_io0_out_en <= 1'b1 ; if(R_write_bits_cnt > 0) //如果R_cmd_reg還沒有發送完 begin R_qspi_io0 <= R_address_reg[R_write_bits_cnt] ; //發送bit23~bit1位 R_write_bits_cnt <= R_write_bits_cnt - 1 ; end else begin R_qspi_io0 <= R_address_reg[0] ; //發送bit0 if(I_cmd_type[3:0] == 4'b0010) // 扇區擦除(Sector Erase)指令 begin //扇區擦除(Sector Erase)指令發完24-bit地址碼就執行結束了,所以直接跳到結束狀態 O_qspi_state <= C_FINISH_DONE ; end else if (I_cmd_type[3:0] == 4'b0101) // 頁編程(Page Program)指令,頁編程指令和寫數據指令是一個意思 begin O_qspi_state <= C_WRITE_DATA ; R_write_bits_cnt <= 7 ; end else if (I_cmd_type[3:0] == 4'b0000) // 讀Device ID指令 begin O_qspi_state <= C_READ_WAIT ; R_read_bytes_num <= 2 ; //接收2個數據的Device ID end else if (I_cmd_type[3:0] == 4'b0111) // 讀數據(Read Data)指令 begin O_qspi_state <= C_READ_WAIT ; R_read_bytes_num <= 256 ; //接收256個數據 end else if (I_cmd_type[3:0] == 4'b1000) begin //如果是四線模式頁編程指令(Quad Page Program) O_qspi_state <= C_WRITE_DATA_QUAD ; R_write_bits_cnt <= 7 ; end else if (I_cmd_type[3:0] == 4'b1001) begin //如果是四線讀操作 O_qspi_state <= C_DUMMY ; R_read_bytes_num <= 256 ; //接收256個數據 R_write_bits_cnt <= 7 ; end end end C_DUMMY: // 四線讀操作之前需要等待8個dummy clock begin R_qspi_io3_out_en <= 1'b0 ; // 設置IO_qspi_io3為高阻 R_qspi_io2_out_en <= 1'b0 ; // 設置IO_qspi_io2為高阻 R_qspi_io1_out_en <= 1'b0 ; // 設置IO_qspi_io1為高阻 R_qspi_io0_out_en <= 1'b0 ; // 設置IO_qspi_io0為高阻 if(R_write_bits_cnt > 0) R_write_bits_cnt <= R_write_bits_cnt - 1 ; else O_qspi_state <= C_READ_WAIT_QUAD ; end C_READ_WAIT: // 單線模式讀等待狀態 begin if(R_read_finish) begin O_qspi_state <= C_FINISH_DONE ; R_data_come_single <= 1'b0 ; end else begin R_data_come_single <= 1'b1 ; // 單線模式下讀數據標志信號,此信號為高標志正在接收數據 R_qspi_io1_out_en <= 1'b0 ; end end C_READ_WAIT_QUAD: // 四線模式讀等待狀態 begin if(R_read_finish) begin O_qspi_state <= C_FINISH_DONE ; R_data_come_quad <= 1'b0 ; end else R_data_come_quad <= 1'b1 ; end C_WRITE_DATA: // 寫數據狀態 begin if(R_write_bytes_cnt < 256) // 往QSPI Flash中寫入 256個數據 begin if(R_write_bits_cnt > 0) //如果數據還沒有發送完 begin R_qspi_io0 <= W_rom_out[R_write_bits_cnt] ; //發送bit7~bit1位 R_write_bits_cnt <= R_write_bits_cnt - 1'b1 ; end else begin R_qspi_io0 <= W_rom_out[0] ; //發送bit0 R_write_bits_cnt <= 7 ; R_write_bytes_cnt <= R_write_bytes_cnt + 1'b1 ; end end else begin O_qspi_state <= C_FINISH_DONE ; R_qspi_clk_en <= 1'b0 ; end end C_WRITE_DATA_QUAD : begin R_qspi_io0_out_en <= 1'b1 ; // 設置IO0為輸出 R_qspi_io1_out_en <= 1'b1 ; // 設置IO1為輸出 R_qspi_io2_out_en <= 1'b1 ; // 設置IO2為輸出 R_qspi_io3_out_en <= 1'b1 ; // 設置IO3為輸出 if(R_write_bytes_cnt == 9'd256) begin O_qspi_state <= C_FINISH_DONE ; R_qspi_clk_en <= 1'b0 ; end else begin if(R_write_bits_cnt == 8'd3) begin R_write_bytes_cnt <= R_write_bytes_cnt + 1'b1 ; R_write_bits_cnt <= 8'd7 ; R_qspi_io3 <= W_rom_out[3] ; // 分別發送bit3 R_qspi_io2 <= W_rom_out[2] ; // 分別發送bit2 R_qspi_io1 <= W_rom_out[1] ; // 分別發送bit1 R_qspi_io0 <= W_rom_out[0] ; // 分別發送bit0 end else begin R_write_bits_cnt <= R_write_bits_cnt - 4 ; R_qspi_io3 <= W_rom_out[R_write_bits_cnt - 0] ; // 分別發送bit7 R_qspi_io2 <= W_rom_out[R_write_bits_cnt - 1] ; // 分別發送bit6 R_qspi_io1 <= W_rom_out[R_write_bits_cnt - 2] ; // 分別發送bit5 R_qspi_io0 <= W_rom_out[R_write_bits_cnt - 3] ; // 分別發送bit4 end end end C_FINISH_DONE: begin O_qspi_cs <= 1'b1 ; R_qspi_io0 <= 1'b0 ; R_qspi_clk_en <= 1'b0 ; O_done_sig <= 1'b1 ; R_qspi_io3_out_en <= 1'b0 ; // 設置IO_qspi_io3為高阻 R_qspi_io2_out_en <= 1'b0 ; // 設置IO_qspi_io2為高阻 R_qspi_io1_out_en <= 1'b0 ; // 設置IO_qspi_io1為高阻 R_qspi_io0_out_en <= 1'b0 ; // 設置IO_qspi_io0為高阻 R_data_come_single <= 1'b0 ; R_data_come_quad <= 1'b0 ; O_qspi_state <= C_IDLE ; end default:O_qspi_state <= C_IDLE ; endcase end end ////////////////////////////////////////////////////////////////////////////// // 功能:接收QSPI Flash發送過來的數據 ////////////////////////////////////////////////////////////////////////////// always @(posedge I_clk_25M) begin if(!I_rst_n) begin R_read_bytes_cnt <= 0 ; R_read_bits_cnt <= 0 ; R_read_finish <= 1'b0 ; O_read_byte_valid <= 1'b0 ; R_read_data_reg <= 0 ; O_read_data <= 0 ; end else if(R_data_come_single) // 此信號為高表示接收數據從QSPI Flash發過來的數據 begin if(R_read_bytes_cnt < R_read_bytes_num) begin if(R_read_bits_cnt < 7) //接收一個Byte的bit0~bit6 begin O_read_byte_valid <= 1'b0 ; R_read_data_reg <= {R_read_data_reg[6:0],IO_qspi_io1} ; R_read_bits_cnt <= R_read_bits_cnt + 1'b1 ; end else begin O_read_byte_valid <= 1'b1 ; //一個byte數據有效 O_read_data <= {R_read_data_reg[6:0],IO_qspi_io1} ; //接收bit7 R_read_bits_cnt <= 0 ; R_read_bytes_cnt <= R_read_bytes_cnt + 1'b1 ; end end else begin R_read_bytes_cnt <= 0 ; R_read_finish <= 1'b1 ; O_read_byte_valid <= 1'b0 ; end end else if(R_data_come_quad) begin if(R_read_bytes_cnt < R_read_bytes_num) begin //接收數據 if(R_read_bits_cnt < 8'd1) begin O_read_byte_valid <= 1'b0 ; R_read_data_reg <= {R_read_data_reg[3:0],IO_qspi_io3,IO_qspi_io2,IO_qspi_io1,IO_qspi_io0};//接收前四位 R_read_bits_cnt <= R_read_bits_cnt + 1 ; end else begin O_read_byte_valid <= 1'b1 ; O_read_data <= {R_read_data_reg[3:0],IO_qspi_io3,IO_qspi_io2,IO_qspi_io1,IO_qspi_io0}; //接收后四位 R_read_bits_cnt <= 0 ; R_read_bytes_cnt <= R_read_bytes_cnt + 1'b1 ; end end else begin R_read_bytes_cnt <= 0 ; R_read_finish <= 1'b1 ; O_read_byte_valid <= 1'b0 ; end end else begin R_read_bytes_cnt <= 0 ; R_read_bits_cnt <= 0 ; R_read_finish <= 1'b0 ; O_read_byte_valid <= 1'b0 ; R_read_data_reg <= 0 ; end end rom_data rom_data_inst ( .clka(I_clk_25M), // input clka .addra(W_rom_addr), // input [7 : 0] addra .douta(W_rom_out) // output [7 : 0] douta ); endmodule
頂層測試狀態機的完整代碼如下:
module qspi_top ( input I_clk , input I_rst_n , output O_qspi_clk , // QPI總線串行時鍾線 output O_qspi_cs , // QPI總線片選信號 inout IO_qspi_io0 , // QPI總線輸入/輸出信號線 inout IO_qspi_io1 , // QPI總線輸入/輸出信號線 inout IO_qspi_io2 , // QPI總線輸入/輸出信號線 inout IO_qspi_io3 // QPI總線輸入/輸出信號線 ); reg [3:0] R_state ; reg [7:0] R_flash_cmd ; reg [23:0] R_flash_addr ; reg R_clk_25M ; reg [4:0] R_cmd_type ; reg [15:0] R_status_reg ; wire W_done_sig ; wire [7:0] W_read_data ; wire W_read_byte_valid ; wire [2:0] R_qspi_state ; //////////////////////////////////////////////////////////////////// //功能:二分頻邏輯 //////////////////////////////////////////////////////////////////// always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_clk_25M <= 1'b0 ; else R_clk_25M <= ~R_clk_25M ; end //////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////// //功能:測試狀態機 //////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) begin R_state <= 4'd0 ; R_flash_addr <= 24'd0 ; R_flash_cmd <= 8'h00 ; R_cmd_type <= 5'b0_0000 ; end else begin case(R_state) 4'd0://讀Device ID指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h90 ; R_flash_addr <= 24'd0 ; R_cmd_type <= 5'b1_0000 ; end end 4'd1://寫不使能(Write disable)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h04 ; R_cmd_type <= 5'b1_0100 ; end end 4'd2://寫使能(Write Enable)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h06 ; R_cmd_type <= 5'b1_0001 ; end end 4'd3:// 扇區擦除(Sector Erase)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h20 ; R_flash_addr<= 24'd0 ; R_cmd_type <= 5'b1_0010 ; end end 4'd4://讀狀態寄存器1, 當Busy位(狀態寄存器1的最低位)為0時表示擦除操作完成 begin if(W_done_sig) begin if(W_read_data[0]==1'b0) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end 4'd5://寫狀態寄存器2(Write Status Register2)指令,用來把QE(Quad Enable)位置1 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h01 ; R_cmd_type <= 5'b1_0110 ; R_status_reg<= 16'h0002 ; end end 4'd6://寫使能(Write Enable)指令 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h06 ; R_cmd_type <= 5'b1_0001 ; end end 4'd7: //四線模式頁編程操作(Quad Page Program): 把存放在ROM中的數據寫入QSPI Flash中 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h32 ; R_flash_addr<= 24'd0 ; R_cmd_type <= 5'b1_1000 ; end end 4'd8://讀狀態寄存器1, 當Busy位(狀態寄存器1的最低位)為0時表示寫操作完成 begin if(W_done_sig) begin if(W_read_data[0]==1'b0) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end else begin R_flash_cmd <= 8'h05 ; R_cmd_type <= 5'b1_0011 ; end end 4'd9://四線模式讀256 Bytes數據 begin if(W_done_sig) begin R_flash_cmd <= 8'h00 ; R_state <= R_state + 1'b1 ; R_cmd_type <= 5'b0_0000 ; end else begin R_flash_cmd <= 8'h6b ; R_flash_addr<= 24'd0 ; R_cmd_type <= 5'b1_1001 ; end end 4'd10:// 結束狀態 begin R_flash_cmd <= 8'h00 ; R_state <= 4'd10 ; R_cmd_type <= 5'b0_0000 ; end default : R_state <= 4'd0 ; endcase end end qspi_driver U_qspi_driver ( .O_qspi_clk (O_qspi_clk ), // QPI總線串行時鍾線 .O_qspi_cs (O_qspi_cs ), // QPI總線片選信號 .IO_qspi_io0 (IO_qspi_io0 ), // QPI總線輸入/輸出信號線 .IO_qspi_io1 (IO_qspi_io1 ), // QPI總線輸入/輸出信號線 .IO_qspi_io2 (IO_qspi_io2 ), // QPI總線輸入/輸出信號線 .IO_qspi_io3 (IO_qspi_io3 ), // QPI總線輸入/輸出信號線 .I_rst_n (I_rst_n ), // 復位信號 .I_clk_25M (R_clk_25M ), // 25MHz時鍾信號 .I_cmd_type (R_cmd_type ), // 命令類型 .I_cmd_code (R_flash_cmd ), // 命令碼 .I_qspi_addr (R_flash_addr ), // QSPI Flash地址 .I_status_reg (R_status_reg ), // QSPI Flash狀態寄存器 .O_done_sig (W_done_sig ), // 指令執行結束標志 .O_read_data (W_read_data ), // 從QSPI Flash讀出的數據 .O_read_byte_valid (W_read_byte_valid ), // 讀一個字節完成的標志 .O_qspi_state (R_qspi_state ) // 狀態機,用於在頂層調試用 ); wire [35:0] CONTROL0 ; wire [99:0] TRIG0 ; icon icon_inst ( .CONTROL0(CONTROL0) // INOUT BUS [35:0] ); ila ila_inst ( .CONTROL(CONTROL0) , // INOUT BUS [35:0] .CLK(I_clk) , // IN .TRIG0(TRIG0) // IN BUS [255:0] ); assign TRIG0[7:0] = W_read_data ; assign TRIG0[8] = W_read_byte_valid ; assign TRIG0[12:9] = R_state ; assign TRIG0[16:13] = R_qspi_state ; assign TRIG0[17] = W_done_sig ; assign TRIG0[18] = IO_qspi_io0 ; assign TRIG0[19] = IO_qspi_io1 ; assign TRIG0[20] = IO_qspi_io2 ; assign TRIG0[21] = IO_qspi_io3 ; assign TRIG0[22] = O_qspi_cs ; assign TRIG0[23] = O_qspi_clk ; assign TRIG0[28:24] = R_cmd_type ; assign TRIG0[36:29] = R_flash_cmd ; assign TRIG0[60:37] = R_flash_addr ; assign TRIG0[61] = I_rst_n ; assign TRIG0[77:62] = R_status_reg ; endmodule
接下來就對比一下各個指令的時序圖,由於有一部分時序圖在單線模式已經對比過了,這里只對比寫狀態寄存器,四線讀操作,四線寫操作三個指令的時序圖
1、寫寫狀態寄存器(Write Status Register)指令
芯片手冊寫狀態寄存器時序圖:
ChipScope抓出來的寫狀態寄存器時序圖:
2、四線寫數據(Quad Input Page Program)指令
芯片手冊四線寫數據時序圖:
ChipScope抓回來的四線寫數據時序圖:
3、四線讀數據(Quad Read Data)指令
芯片手冊四線讀數據(Fast Read Quad Output)時序圖:
ChipScope抓回來的四線讀數據(Fast Read Quad Output)時序圖:
通過對比各個指令與芯片手冊的指令,可以發現各個指令的時序與芯片手冊完全吻合,至此四線SPI操作QSPI Flash的代碼全部測試通過。你可以把ROM里面的數據換成你自己的數據發給QSPI Flash,ROM里面數據則可以通過4.2小節的Matlab自定義產生。
4.6、 四線模式與單線模式讀寫數據時的性能對比
由於QSPI Flash四線模式中共有四根信號線,它們全部可以用做輸入輸出來傳輸數據,所以傳輸同樣大小的數據塊時候,四線模式的速度是單線模式的四倍。下面列出我在最初調試QSPI Flash的時候抓出來的兩張圖,分別是四線模式與單線模式寫數據對比圖與 四線模式與單線模式讀數據對比圖
四線模式與單線模式寫數據對比圖:
四線模式與單線模式讀數據對比圖:
從上面可以很清晰的看出讀寫同一塊數據的時候,四線模式相較於單線模式而言,速度有了4倍的提升。
五、 進一步思考
5.1、 用Verilog編寫QSPI Flash驅動的意義何在?
事實上,Xilinx的FPGA的已經有專門的引腳用來與QSPI Flash相連,當我們把.bin文件或者.mcs文件通過Jtag固化到QSPI Flash中的時候,ISE軟件中的iMPACT會自動把.bin文件或者.mcs文件以SPI協議的格式寫入到QSPI Flash中,由於QSPI Flash是一種非易失性存儲器,所以斷電后,數據並不會丟失,當FPGA斷電重啟以后,它會自動從QSPI Flash中加載事先固化到QSPI Flash里面的程序到內部的RAM中執行,而不像.bit掉電程序就會丟失。既然FPGA已經支持QSPI Flash的協議,為什么還要寫QSPI Flash的驅動呢?原因是在實際的項目中,如果要遠程對FPGA的代碼進行在線升級的話就必須有一套QSPI Flash的驅動來配合CPU進行升級操作。
比如,某廠家生產了幾千台4G基站,而且已經發貨到國外各個國家商用。而后期研發人員調試的時候發現FPGA部分有一個小Bug需要修復或者說需要增加一個新功能,代碼寫好了以后如果沒有在線升級的功能,你只能一個一個去用jtag重新固化程序,這樣會耗費大量的人力成本而且極其不方便,而如果有在線升級的功能只需要通過遠程登錄連接到CPU上發送幾條指令就可以完成升級功能,大大提高了工作效率。
5.2、 關於在代碼中同時使用時鍾的上升沿和下降沿操作時有什么風險
關於這個問題目前還沒有找到比較權威的說法,以下兩個說法是提的最多的:
1、FPGA內部的觸發器都是上升沿觸發的,所以,如果在代碼中用下降沿觸發的話會在時鍾引腳的前面綜合出一個反相器,這個反相器可能會對時鍾信號的質量有影響
2、集成電路設計的書中有對晶振的描述,外部晶振產生的時鍾信號上升沿的時間幾乎是一樣的,但是每個周期的下降沿的時間卻無法保證完全一致,這與工藝有很大關系,所以采用下降沿觸發有可能存在風險。
希望有這方面經驗的網友能提供一些權威的說法。