一. SPI總線協議
SPI(Serial Peripheral Interface)接口,中文為串行外設接口。它只需要3根線或4根線即可完成通信工作(這里討論4根線的情況)。
這4根通信線分別為NCS/NSS(片選信號)、SCK/SCLK(串行同步時鍾)、MOSI/SDO(主機輸出從機輸入,Master Output Slave Input)、MISO/SDI(主機輸入從機輸出)。
SPI通信有四種方式,由CPOL(時鍾極性)、CPHA(時鍾相位)的4種組合決定的。CPOL決定總線空閑時,SCK是高電平還是低電平(CPOL=,0,無數據傳輸時,SCK=0;CPOL=1,無數據傳輸時,SCK=1)。CPHA決定在數據開始傳輸時,SCK第幾個跳變沿采集數據(CPHA=0,開始傳輸時,在第一個跳變沿采集數據,第二個跳變沿改變發送數據(即改變MISO或者MOSI線上電平);CPHA=1,開始傳輸是,在第一個跳變沿改變發送的數據,在第二個跳變沿采集數據)(見圖1)。
圖1
確立可靠通信前,必須保證主從機處於同一種的傳輸方式,這里為方便起見,專門以CPOL=0,CPHA=0的傳輸方式進行討論。
需要注意的是:在CPOL=0,CPHA=0的情況下,主從機都在SCK上跳沿對數據進行采集,SCK下跳沿改變總線電平(見圖2)。

圖2
這里在使用FPGA實現SPI模塊時,做一個規定:
1. 使用CPOL=0,、CPHA=0的傳輸方式;
2. 傳輸時,以最高位先輸出,最后輸出最低位;
3. FPGA實現的SPI模塊作從機,SCK由外部主機提供;
4. 通信數據長度為8位。
二. FPGA的SPI從機實現
實現SPI從機,可以分為兩個模塊:一個是SPI接收模塊,另一個則是SPI發送模塊。
1. 首先確定模塊的輸出輸入管腳
由標題一可以知道,SPI通信腳有4根線,我們還是用到時鍾總線和模塊復位腳,因此模塊管腳可以定義為
module myspi(nrst, clk, ncs, mosi, miso, sck);
input clk, nrst; input ncs, mosi, sck; output miso;
2. SCK跳變沿檢測
原理十分簡單:使用寄存器記錄SCK狀態,由狀態判斷SCK是否出現跳變沿。
reg[2:0] sck_edge;
always @ (posedge clk or negedge nrst)
begin
if(~nrst)
begin
sck_edge <= 3'b000;
end
else
begin
sck_edge <= {sck_edge[1:0], sck};
end
end
wire sck_riseedge, sck_falledge; assign sck_riseedge = (sck_edge[2:1] == 2'b01); //檢測到SCK由0變成1,則認為發現上跳沿 assign sck_falledge = (sck_edge[2:1] == 2'b10); //檢測到SCK由1變成0,則認為發現下跳沿
3. SPI接收部分
SPI接收部分使用有限狀態機:
狀態1:等待SCK上跳沿,並將MOSI的數據移入
移位寄存器byte_received,
接收位數寄存器bit_received_cnt記錄接收到的數據位數,接收到8位數據后轉入狀態2;
狀態2:保存移位寄存器byte_received數據到
接收緩存器rec_data,
接收標志位/接收緩存器非空標志位rec_flag置高4個clk時鍾周期后轉入狀態3;
狀態3:清除rec_flag並轉入狀態1。
reg[7:0] byte_received;
reg[3:0] bit_received_cnt;
reg rec_flag;
reg[1:0] rec_status; //SPI接收部分狀態機
reg[7:0] rec_data;
reg[2:0] rec_flag_width; //SPI接收完成標志位脈沖寬度寄存器
always @ (posedge clk or negedge nrst) //每次sck都會接收數據,spi的頂端模塊狀態機決定是否取用
begin
if(~nrst)
begin
byte_received <= 8'h00;
bit_received_cnt <= 4'h0;
rec_flag <= 1'b0;
rec_status <= 2'b00;
rec_flag_width <= 3'b000;
end
else
begin
if(~ncs)
begin
case (rec_status)
2'b00: begin
if(sck_riseedge)
begin
byte_received <= {byte_received[6:0], mosi};
if(bit_received_cnt == 4'h7)
begin
bit_received_cnt <= 4'b0000;
rec_status <= 2'b01;
end
else
begin
bit_received_cnt <= bit_received_cnt+1;
end
end
end
2'b01: begin
rec_data <= byte_received;
rec_flag <= 1'b1;
if(rec_flag_width==3'b100) begin
rec_flag_width <= 3'b000;
rec_status <= 2'b11;
end
else begin
rec_flag_width <= rec_flag_width+1;
end
end
2'b11: begin
rec_flag <= 1'b0;
rec_status <= 2'b00;
end
endcase
end
end
end
這里,使用rec_flag的原因是通知另一個模塊處理接收數據(后面將會提到),rec_data若在下一次數據傳輸完成前不做處理則會丟失。
4. SPI發送部分
SPI從機一般在解析主機發送的命令后,主動發出主機所需數據,所以,SPI發送部分,需要其他模塊的觸發,並將數據送往MISO管腳。
SPI發送部分也離不開狀態機:
狀態1:等待
發送觸發標志位send_flag置高,一旦標志位send_flag置高,
發送移位寄存器
byte_sended存儲外部觸發模塊的數據send_data,miso管腳輸出發送數據最高位send_data[7],置位
正在發送標志位sending_flag,轉入狀態2;
狀態2:等待SCK上跳沿,即等待主機接收數據最高位后進入狀態3;(其實這個狀態可有可無的狀態)
狀態3:在SCK下跳沿,將發送移位寄存器byte_sended最高位移入miso管腳,當發送移位寄存器被移空,清除正在發送標志位sending_flag,進入狀態4;
狀態4:置低miso管腳,轉入狀態1。
reg miso;
reg sending_flag; //正在發送標志位
reg[7:0] byte_sended; //發送移位寄存器
reg[3:0] bit_sended_cnt; //SPI發送位計數器
reg[1:0] send_status; //SPI發送部分狀態機
always @ (posedge clk or negedge nrst)
begin
if(~nrst)
begin
byte_sended <= 8'h00;
bit_sended_cnt <= 4'b0000;
send_status <= 2'b00;
sending_flag <= 1'b0;
end
else
begin
if(~ncs)
begin
case (send_status)
2'b00: begin
if(send_flag)
begin //鎖存發送數據
send_status <= 2'b01; //2'b01;
byte_sended <= send_data;
sending_flag <= 1'b1;
miso <= send_data[7];
end
end
2'b01: begin //發送數據移入移位寄存器
if(sck_riseedge) begin
//miso <= byte_sended[7];
//byte_sended <= {byte_sended[6:0], 1'b0};
send_status <= 2'b11;
end
end
2'b11: begin //根據sck下降沿改變數據
miso <= byte_sended[7];
if(sck_falledge) ///---------------------------------------這里多移了一位
begin
//miso <= byte_sended[7];
byte_sended <= {byte_sended[6:0], 1'b0};
if(bit_sended_cnt == 4'b0111)
begin
send_status <= 2'b10;
bit_sended_cnt <= 4'b0000;
sending_flag <= 1'b0;
end
else
begin
bit_sended_cnt <= bit_sended_cnt+1;
end
end
end
2'b10: begin //數據發送完畢
send_status <= 2'b00;
//sending_flag <= 1'b0;
miso <= 1'b0;
end
endcase
end
end
end
經過實測,SCK頻率低於clk頻率8倍以上,通信可靠穩定,測試芯片為XC3S50-TQ144,平台為ISE,clk為25MHz。
