(二)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的例項。