1. 程式人生 > >(二)SPI通訊的初始化設定verilog實現

(二)SPI通訊的初始化設定verilog實現

emmmmm,一下子跳到了SPI通訊,跨度有點大,剛好學到這裡,OK少廢話。

相信學過ARM的同學對SPI通訊也有一定的認識,很多模組都需要用到SPI通訊。我就直接用黑金開發板AX301的SPI_Flash例程裡面的SPI_master給大家講解一下。夠良心的啦,黑金開發板的資料都沒有給出相應的SPI知識,這讓學過ARM但基礎知識不紮實的同學怎麼辦(說的好像就是我。。。。。。。)來吧來吧,哥給你普及一下知識吧。(講得不好體諒體諒,博主水平有限,歡迎給修改意見)

特地去原子哥里面找到SPI的時序圖,放兩張重要的:

兩張圖中可以看出,SPI通訊是通過CPHA和CPOL組合成四種方式進行通訊的,CPOL=1時,時鐘在空閒狀態下始終是高電平,反之則為低電平,而CPHA=0時捕捉的是第一個邊緣,否則為第二個時鐘邊緣。

MISO和MOSI又是什麼意思呢?M:master;I:input;S:slaver;O:output;意思就是主從裝置的輸出和輸入,那麼他們是通過什麼樣的原理傳輸資料的呢?很開心繼續為大家呈上一張圖:

如圖,SPI是通過主從裝置不斷移位實現資料的傳輸的。移位的概念懂不懂?10111000向右移一位就是01011100,這就不詳細解釋了,閒的時候多翻翻數電書,哈哈哈哈哈。

OK,說了那麼多,給大家直接呈上程式碼了:

module spi_master
(
    input                       sys_clk,
    input                       rst,
    output                      nCS,       //chip select (SPI mode)
    output                      DCLK,      //spi clock
    output                      MOSI,      //spi master data output
    input                       MISO,      //spi master input
    input                       CPOL,
    input                       CPHA,
    input                       nCS_ctrl,
    input[15:0]                 clk_div,
    input                       wr_req,
    output                      wr_ack,
    input[7:0]                  data_in,
    output[7:0]                 data_out
);
localparam                   IDLE            = 0;
localparam                   DCLK_EDGE       = 1;
localparam                   DCLK_IDLE       = 2;
localparam                   ACK             = 3;
localparam                   LAST_HALF_CYCLE = 4;
localparam                   ACK_WAIT        = 5;
reg                          DCLK_reg;
reg[7:0]                     MOSI_shift;
reg[7:0]                     MISO_shift;
reg[2:0]                     state;
reg[2:0]                     next_state;
reg [15:0]                   clk_cnt;
reg[4:0]                     clk_edge_cnt;
assign MOSI = MOSI_shift[7];
assign DCLK = DCLK_reg;
assign data_out = MISO_shift;
assign wr_ack = (state == ACK);
assign nCS = nCS_ctrl;

/*************這個就是狀態機的定義**************/
[email protected]
(posedge sys_clk or posedge rst) begin if(rst) state <= IDLE; else state <= next_state; end /****************end*************************/ /****************狀態機的具體過程*************/ [email protected](*) begin case(state) IDLE: if(wr_req == 1'b1) next_state <= DCLK_IDLE; else next_state <= IDLE; DCLK_IDLE: //half a SPI clock cycle produces a clock edge if(clk_cnt == clk_div) next_state <= DCLK_EDGE; else next_state <= DCLK_IDLE; DCLK_EDGE: //a SPI byte with a total of 16 clock edges if(clk_edge_cnt == 5'd15) next_state <= LAST_HALF_CYCLE; else next_state <= DCLK_IDLE; //this is the last data edge LAST_HALF_CYCLE: if(clk_cnt == clk_div) next_state <= ACK; else next_state <= LAST_HALF_CYCLE; //send one byte complete ACK: next_state <= ACK_WAIT; //wait for one clock cycle, to ensure that the cancel request signal ACK_WAIT: next_state <= IDLE; default: next_state <= IDLE; endcase end /*************詳情見圖一**************************/ /****************時鐘翻轉************************/
[email protected]
(posedge sys_clk or posedge rst) begin if(rst) /*在空閒狀態之前,SCK一直保持CPOL的極性*/ DCLK_reg <= 1'b0; else if(state == IDLE) DCLK_reg <= CPOL; else if(state == DCLK_EDGE) /*邊緣檢測時,反轉SCK*/ DCLK_reg <= ~DCLK_reg;//SPI clock edge end /****************end*****************************/ //SPI clock wait counter /*一個計數器*/ [email protected](posedge sys_clk or posedge rst) begin if(rst) clk_cnt <= 16'd0; else if(state == DCLK_IDLE || state == LAST_HALF_CYCLE) clk_cnt <= clk_cnt + 16'd1; else clk_cnt <= 16'd0; end //SPI clock edge counter [email protected](posedge sys_clk or posedge rst) begin if(rst) clk_edge_cnt <= 5'd0; else if(state == DCLK_EDGE) clk_edge_cnt <= clk_edge_cnt + 5'd1; else if(state == IDLE) clk_edge_cnt <= 5'd0; end //SPI data output /*這裡就是SPI輸出的移位方式*/ [email protected](posedge sys_clk or posedge rst) begin if(rst) MOSI_shift <= 8'd0; else if(state == IDLE && wr_req) MOSI_shift <= data_in; else if(state == DCLK_EDGE) if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b1) /*兩種方式,取決於CPHA*/ MOSI_shift <= {MOSI_shift[6:0],MOSI_shift[7]}; /*常見的移位語句,大家要敏感*/ else if(CPHA == 1'b1 && (clk_edge_cnt != 5'd0 && clk_edge_cnt[0] == 1'b0)) MOSI_shift <= {MOSI_shift[6:0],MOSI_shift[7]}; end //SPI data input [email protected](posedge sys_clk or posedge rst) begin if(rst) MISO_shift <= 8'd0; else if(state == IDLE && wr_req) MISO_shift <= 8'h00; else if(state == DCLK_EDGE) if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b0) MISO_shift <= {MISO_shift[6:0],MISO}; /*MISO輸入,然後進行移位*/ else if(CPHA == 1'b1 && (clk_edge_cnt[0] == 1'b1)) MISO_shift <= {MISO_shift[6:0],MISO}; end endmodule

(好長,害怕,其實我不是很想碼字,哎,不過算了,大家記得給我點贊,謝謝。)

自己用visio畫的狀態機,夠良心吧,參見注釋說的圖一:

我覺得這段程式碼有很多值得學習的地方。狀態機、計數器這些常見的程式碼我就忽略不說了,我來講講MISO和MOSI 的過程。

先講input吧:

 else if(state == DCLK_EDGE)         if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b0)               MISO_shift <= {MISO_shift[6:0],MISO}; /*MISO輸入,然後進行移位*/         else if(CPHA == 1'b1 && (clk_edge_cnt[0] == 1'b1))             MISO_shift <= {MISO_shift[6:0],MISO}; 可以從我畫的狀態機圖清晰地看到clk_edge_cnt是在DCLK_EDGE狀態開始計數,當CPHA=1時,捕捉的是第一個邊緣,因為從0開始數,所以只要判斷clk_edge_cnt[0] 也就是clk_edge_cnt的末位為0時,則是第一個時鐘邊緣。(佩服這句程式碼真的很嚴謹)然後輸入的MISO作為MISO_shift的末位進行移位。那麼else if也是同樣的理解方法。

那麼output也是同樣的道理。

哇寫完部落格覺得自己理解得好透徹哈哈哈哈。雖然還不知道這段程式碼怎麼用,那就期待下一篇吧。下一篇詳細去講SPI+FLASH的例項。