引言:SPI是常用的板級通信協議,在FPGA板級通信中,許多重要的從器件都對SPI協議有所支持。因此,掌握SPI通信的FPGA片上實現對於FPGA工程開發具有重要的意義。本文設計了一個基於SPI模式0的主機通信控制器,系統性闡明瞭SPI設計的全流程。希望本文的設計能夠對更多的人有所幫助。
一、SPI通信協議簡介
1.物理層引線
SPI通信的最小結構爲一主一從結構,主機向從機提供信號發送接收時鐘SCLK。在物理實現中,主機與從機之間存有四根引線,即MOSI(主收從發)、MISO(主發從收)、SCK(通信時鐘)、nSS(從機片選信號)。其中,通過在主機上增加片選信號輸出端的數目,可以使得主機控制更多的從機,實現一主多從的SPI通信。
2.SPI四種模式
SPI通信協議規定了4鍾工作模式,在實際應用中應當保證主機和從機工作在相同的工作模式下。SPI工作模式通過時鐘極性CPOL和時鐘相位CPHA聯合指定。其中沒時鐘極性CPOL指定SCK在空閒狀態時的電平,時鐘極性CPHA指定在SCK的何種邊緣進行數據採樣。其標識如下:
CPOL=1 | CPOL=0 | |
CPHA=1 | MODE3:SCK空閒爲高,上升沿採樣 | MODE1:SCK空閒爲低,下降沿採樣 |
CPHA-0 | MODE2:SCK空閒爲高,下降沿採樣 | MODE0:SCK空閒爲低,上升沿採樣 |
其中,SPI中最常用的時模式0和模式2.
二、FPGA實現設計
由於我們設計的目標爲SPI模式0主機控制器,因此我需要實現在SCK上升沿時對數據進行採樣。由於在時鐘上升沿進行數據採樣,那麼,接收/發送狀態機的狀態應當早於SCK時鐘上升沿提前準備好接收/發送狀態。
對於發送狀態機,必須在SCK時鐘上升沿到來前將需發送的數據在MOSI信號線上準備好。因此其狀態的跳變應當較時鐘上升沿到來更早。因此,本文設定發送狀態機的在SCK時鐘的下降沿進行狀態變換,在SCK的低電平中心將需發送的數據壓至MOSI線上。
對於接收狀態機,爲了保證狀態跳變的一致性,也採用SCK下降沿進行狀態跳變時刻。接收狀態機在SCK上升沿時將數據採集至接收緩衝口,並在採集完一字節數據後生成信號標誌脈衝。這個脈衝將保持一個SCK週期以供FPGA其餘模塊識別。
對於SPI主機控制器,另一個重要的模塊是稱之爲control的控制模塊,它是用來生成SCK時鐘與nSS片選信號的模塊。由於本文設計只有一個從機,因此只單純的控制nSS的電平,不對多條nSS進行選擇控制。SCK時鐘的生成也是依靠狀態機,其機理爲當狀態機處於IDLE空閒狀態時,檢測到讀/寫請求,在時鐘相位變爲下降沿時進入工作狀態,並輸出8個完整的SCK時鐘。此後,狀態機轉入STOP狀態,並在一個週期內檢測是否有新的請求信號到來,若無抵達空閒狀態,若有,轉入工作狀態。
本文設計的代碼如下,如果要修改SPI工作模式,請根據頭文件的相位定義修改代碼:
module SPI(
clk,rst_n,miso,mosi,sclk,ss_n,wr,rd,data_wr,data_rd,wr_req,rd_req
);
input clk; //時鐘信號,設定爲50MHz
input rst_n; //異步復位同步釋放
//SPI接口信號
input miso; //主收從發信號
output mosi; //主發從收信號
output sclk; //SPI發送接收時鐘SCLK
output ss_n; //SPI從設備片選信號
//功能指示與數據傳輸信號
output wr; //SPI主機發送空閒信號
output rd; //SPI主機接收緩衝區數據可讀信號
input[7:0]data_wr; //需通過SPI主機發送的數據
input wr_req; //SPI主機數據發送請求信號
input rd_req; //SPI主機讀讀從機數據請求信號
output[7:0]data_rd; //SPI主機接收機緩衝區數據
//////////////////////////////////////////////////////////////////////////////////
wire[8:0]phase; //SCLK時鐘相位值
wire[3:0]cnt; //發送/接收字節計數器
//////////////////////////////////////////////////////////////////////////////////
//控制模塊:片選信號及SCLK時鐘信號控制
control U1(
.clk(clk), //時鐘信號
.rst_n(rst_n), //全局復位信號
.sclk(sclk), //SCLK時鐘信號
.ss_n(ss_n), //從機片選信號
.phase(phase), //SCLK時鐘相位信息
.wr_req(wr_req), //主機寫請求
.rd_req(wr_req), //主機讀請求
.cnt(cnt) //字節計數器
);
//////////////////////////////////////////////////////////////////////////////////
//發送模塊
spi_tx U2(
.clk(clk), //
.rst_n(rst_n), //
.wr_req(wr_req), //寫請求信號
.data_wr(data_wr), //寫數據
.phase(phase), //SCLK相位
.wr(wr), //SPI主機發送空閒信號
.mosi(mosi), //主發從收信號
.cnt(cnt) //字節計數器
);
//////////////////////////////////////////////////////////////////////////////////
//接收模塊
spi_rx U3(
.clk(clk), //
.rst_n(rst_n), //
.rd_req(rd_req), //讀請求信號
.data_rd(data_rd), //讀數據
.phase(phase), //SCLK相位
.rd(rd), //SPI主機接收緩衝可讀信號
.miso(miso), //主收從發信號
.cnt(cnt) //字節計數器
);
//////////////////////////////////////////////////////////////////////////////////
endmodule
`include "global_definition.v"
module control(
clk,rst_n,sclk,ss_n,phase,wr_req,rd_req,cnt
);
input clk;
input rst_n;
output reg sclk; //SCLK時鐘信號
output reg ss_n; //從機片選信號
output reg[8:0]phase; //SCLK時鐘相位
input wr_req; //主機寫請求
input rd_req; //主機讀請求
output cnt; //字節計數器
///////////////////////////////////////////////////////////////////////////////////
reg[1:0]state; //狀態機
reg[1:0]next_state; //下一狀態
//狀態機狀態變換儘可能貼合BCD碼變換順序,以減少電路成本並降低組合邏輯風險
parameter IDLE = 2'b00; //空閒狀態
parameter WORK = 2'b01; //工作狀態
parameter STOP = 2'b11; //停止狀態
reg[3:0]cnt; //並串轉換計數器
///////////////////////////////////////////////////////////////////////////////////
//相位計數器
always@(posedge clk or negedge rst_n)
if(!rst_n) phase <= 9'd0;
else if(phase == 9'd499) phase <= 9'd0; //分頻數500,SPI時鐘速率100KHz
else phase <= phase + 9'd1;
///////////////////////////////////////////////////////////////////////////////////
//狀態機轉移
always@(posedge clk or negedge rst_n)
if(!rst_n) state <= IDLE;
else if(`SCLK_NEG)state <= next_state; //狀態只在SCLK時鐘下降沿時改變
else state <= state;
///////////////////////////////////////////////////////////////////////////////////
//狀態組合判定
always@(wr_req or rd_req or cnt)
case(state)
IDLE : if(wr_req||rd_req) next_state <= WORK; //空閒狀態遭遇讀/寫請求,下次狀態發生改變
else next_state <= IDLE;
WORK : if(cnt == 4'd8) next_state <= STOP; //發送/接收一字節數據後,狀態轉移至STOP
else next_state <= WORK;
STOP : if(wr_req||rd_req) next_state <= WORK; //在STOP狀態下,若觀測到請求,則繼續工作,否則休眠
else next_state <= IDLE;
default : next_state <= IDLE;
endcase
///////////////////////////////////////////////////////////////////////////////////
//狀態機輸出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
sclk <= 1'b0;
ss_n <= 1'b1;
cnt <= 4'd0;
end
else begin
case(state)
IDLE : begin
sclk <= 1'b0;
ss_n <= 1'b1;
cnt <= 4'd0;
end
WORK : begin
ss_n <= 1'b0;
if(`SCLK_POS)begin //時鐘上升沿
sclk <= 1'b1; //SCLK時鐘拉高
cnt <= cnt + 4'd1; //字節計數器加一
end
else if(`SCLK_NEG)sclk <= 1'b0; //
else sclk <= sclk;
end
STOP : begin
cnt <= 4'd0;
sclk <= 1'b0;
ss_n <= 1'b1;
end
default : begin
cnt <= 4'd0;
sclk <= 1'b0;
ss_n <= 1'b1;
end
endcase
end
///////////////////////////////////////////////////////////////////////////////////
endmodule
`include "global_definition.v"
module spi_tx(
clk,rst_n,wr_req,data_wr,phase,wr,mosi,cnt
);
input clk;
input rst_n;
input wr_req; //主機寫請求信號
input[7:0]data_wr; //寫數據
input[8:0]phase; //SCLK相位
output reg wr; //寫空閒標誌
output reg mosi; //主發從收信號
input[3:0]cnt; //字節計數器
////////////////////////////////////////////////////////////////////
reg[1:0]state;
reg[1:0]next_state;
parameter IDLE = 2'b00;
parameter WORK = 2'b01;
parameter STOP = 2'b11;
////////////////////////////////////////////////////////////////////
//狀態機轉移
always@(posedge clk or negedge rst_n)
if(!rst_n) state <= IDLE;
else if(`SCLK_NEG) state <= next_state;
////////////////////////////////////////////////////////////////////
//狀態組合判定
always@(wr_req or cnt)
case(state)
IDLE : if(wr_req) next_state <= WORK;
else next_state <= IDLE;
WORK : if(cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
STOP : if(wr_req) next_state <= WORK;
else next_state <= IDLE;
default : state <= IDLE;
endcase
////////////////////////////////////////////////////////////////////
//狀態機輸出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
mosi <= 1'b0;
wr <= 1'b1;
end
else begin
case(state)
IDLE : begin
mosi <= 1'b0;
wr <= 1'b1;
end
WORK : begin //在SCLK下降沿狀態改變後,緊跟
if(`SCLK_LOW)begin //SCLK低電平時將數據放置總線上
case(cnt)
4'd0 : mosi <= data_wr[7];
4'd1 : mosi <= data_wr[6];
4'd2 : mosi <= data_wr[5];
4'd3 : mosi <= data_wr[4];
4'd4 : mosi <= data_wr[3];
4'd5 : mosi <= data_wr[2];
4'd6 : mosi <= data_wr[1];
4'd7 : mosi <= data_wr[0];
default : mosi <= 1'b0;
endcase
end
wr <= 1'b0;
end
STOP : begin
wr <= 1'b1;
mosi <= 1'b0;
end
default : begin
wr <= 1'b1;
mosi <= 1'b0;
end
endcase
end
////////////////////////////////////////////////////////////////////
endmodule
`include "global_definition.v"
module spi_rx(
clk,rst_n,rd_req,data_rd,phase,rd,miso,cnt
);
input clk;
input rst_n;
input rd_req; //主機讀請求信號
output reg[7:0]data_rd; //讀出數據
input[8:0]phase; //SCLK相位
output reg rd; //讀數據有效標誌
input miso; //主收從發信號
input[3:0]cnt; //字節計數器
////////////////////////////////////////////////////////////////////
reg[1:0]state;
reg[1:0]next_state;
parameter IDLE = 2'b00;
parameter WORK = 2'b01;
parameter STOP = 2'b11;
////////////////////////////////////////////////////////////////////
//狀態機轉移
always@(posedge clk or negedge rst_n)
if(!rst_n) state <= IDLE;
else if(`SCLK_NEG) state <= next_state;
////////////////////////////////////////////////////////////////////
//狀態組合判定
always@(rd_req or cnt)
case(state)
IDLE : if(rd_req) next_state <= WORK;
else next_state <= IDLE;
WORK : if(cnt == 4'd8) next_state <= STOP;
else next_state <= WORK;
STOP : if(rd_req) next_state <= WORK;
else next_state <= IDLE;
default : state <= IDLE;
endcase
////////////////////////////////////////////////////////////////////
//狀態機輸出
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
rd <= 1'b0;
data_rd <= 8'd0;
end
else begin
case(state)
IDLE : begin
rd <= 1'b0;
data_rd <= 8'd0;
end
WORK : begin //在SCLK下降沿狀態改變後,緊跟
if(`SCLK_POS)begin //SCLK上升沿時讀取總線上數據
case(cnt)
4'd0 : data_rd[7] <= miso;
4'd1 : data_rd[6] <= miso;
4'd2 : data_rd[5] <= miso;
4'd3 : data_rd[4] <= miso;
4'd4 : data_rd[3] <= miso;
4'd5 : data_rd[2] <= miso;
4'd6 : data_rd[1] <= miso;
4'd7 : data_rd[0] <= miso;
default : data_rd <= 8'd0;
endcase
end
rd <= 1'b0;
end
STOP : begin
rd <= 1'b1;
end
default : begin
rd <= 1'b0;
data_rd <= 8'd0;
end
endcase
end
////////////////////////////////////////////////////////////////////
endmodule
頭文件如下:
`define SCLK_POS (phase == 9'd499) //定義SCLK上升沿
`define SCLK_HIG (phase == 9'd124) //定義SCLK高電平
`define SCLK_NEG (phase == 9'd249) //定義SCLK下降沿
`define SCLK_LOW (phase == 9'd374) //定義SCLK低電平