上一篇我們設計了 SPI 的狀態機,那麼我們今天要來引用 SPI 狀態機模塊來實現整個 SPI 的模塊,並撰寫 testbench 來驗證電路的正確性。
先來看看這個 SPI 模塊該有哪些輸入輸出腳吧:
輸入:
輸出:
module uasge_SPI_4wire(
en,
clock_50M,
rst_n,
CS,
mosi,
miso,
din,
dout,
SCLK,
finish
);
/*-------ports declaration------*/
input clock_50M;
input rst_n;
input miso;
input en;
input [15:0] din;
output [15:0] dout;
output CS;
output mosi;
output SCLK;
output finish;
reg [15:0] dout;
wire CS;
wire mosi;
wire SCLK;
wire finish;
宣告變數:
需要用到的變數有:
/*-------variables------*/
reg [3:0] counter;
reg [15:0] regdata_i;
reg [15:0] regdata_o;
reg tick_SPI;
reg SCLK_temp;//mux of SCLK_temp or 1'b1
reg [5:0] cnt_SPI; //1MHZ counter
wire countEN;
wire rstcount;
wire SHEN;
wire LDEN;
引用狀態機模組
/*-------module instantiate------*/
SPI U0(
.clk_sys(clock_50M),
.SCLK_temp(SCLK_temp),
.rst_n(rst_n),
.tick_SPI(tick_SPI),
.SCLK(SCLK),
.CS(CS),
.count(counter),
.countEN(countEN),
.rstcount(rstcount),
.ready(en),
.finish(finish),
.SHEN(SHEN),
.LDEN(LDEN)
);
cnt_SPI
/*-------1M counter------*/
always@(posedge clock_50M or negedge rst_n)begin
if(!rst_n)cnt_SPI <= 6'd0;
else begin
if(cnt_SPI<6'd49)cnt_SPI <= cnt_SPI + 6'd1;
else cnt_SPI <= 6'd0;
end
end
做出 SCLK_temp
/*-------make SCLK------*/
always@(posedge clock_50M or negedge rst_n)begin
if(!rst_n)begin
SCLK_temp <= 1'b0;
end
else begin
if(cnt_SPI == 6'd24) SCLK_temp <= 1'b0;
else if(cnt_SPI == 6'd49)SCLK_temp <= 1'b1;
else SCLK_temp <= SCLK_temp;
end
end
為什麼這邊是 24 跟 49 呢?因為 0-24 & 25-49 會剛好把 clk 切為一半,這樣寫就會分別在正緣且 cnt 為 24 & 49 時 SCLK_temp 反向。
tick_SPI
/*-------tick_SPI------*/
always@(posedge clock_50M or negedge rst_n)begin
if(!rst_n)tick_SPI <= 1'b0;
else begin
if(cnt_SPI==6'd48)tick_SPI <= 1'b1;
else tick_SPI <= 1'b0;
end
end
來解釋一下位甚麼這邊是 48,因為我想讓其他動作有在 SCLK 負緣時動作的效果,而 SCLK 會在 cnt = 49 時發生負緣,所以如果我想讓其他事情也在 cnt = 49 時動作,那麼我 tick 必須提前一個 clk 升起來才行,所以這裡才是 48 而非 49。
靠 rstcount&countEN 來控制 counter
/*-------ctrl counter------*/
always@(posedge clock_50M or negedge rst_n)begin
if(!rst_n)begin
counter <= 4'd0;
end
else begin
if(tick_SPI)begin
if(rstcount) counter <= 4'd0;
else if(countEN)counter <= counter + 4'd1;
else counter <= 4'd0;
end
else counter <= counter;
end
end
靠 LDEN&SHEN 來控制我們的 regdata_i
/*-------data in------*/
always@(posedge clock_50M or negedge rst_n)begin//SCLK's negedge
if(!rst_n)begin
regdata_i <= 16'd0;
end
else begin
if(tick_SPI)begin
if(LDEN) regdata_i <= din;
else if(SHEN)regdata_i <= {regdata_i[14:0], 1'b0};
else regdata_i <= regdata_i;
end
else regdata_i <= regdata_i;
end
end
這裡使用左移是因為 SPI 為 MSB 先傳。
蒐集 miso 的 data
/*-------collect miso data------*/
always@(posedge clock_50M or negedge rst_n)begin//SCLK's posedge
if(!rst_n)regdata_o <= 16'd0;
else begin
if(tick_SPI)begin
if(SHEN)regdata_o <= {regdata_o[14:0], miso};
else regdata_o <= regdata_o;
end
else begin
regdata_o <= regdata_o;
end
end
end
這邊來解釋一下為什麼用 SCLK 的負緣收資料,因為我設計的模組是預想外部輸入會在 SCLK 正緣時改變資料,所以為了確保資料的正確,要在 SCLK 的負緣收比較恰當。
於傳輸結束後將 dout 送出
/*-------data out------*/
always@(posedge clock_50M or negedge rst_n)begin
if(!rst_n)begin
dout <= 16'd0;
end
else begin
if(finish)dout <= regdata_o;
else dout <= dout;
end
end
endmodule
還有最後的 mosi 還沒處理
/*-------assign wire------*/
assign mosi = (SHEN)?(regdata_i[15]):(1'b0);
當沒有要傳資料時其實給什麼值都行,因為此時 SCLK 沒有在動作,也意味著 slave 端並不會收取資料。
Quartus 的 State Machine Viewer
`timescale 1ns / 1ns
module spi_tb();
reg sysClk, rst_n, tick_tx, miso;
reg [7:0]address, txdata;
reg [15:0]rxdata;
wire [15:0]data_get;
wire sclk, cs_n, mosi;
wire finish;
localparam Freq_i = 50000000;
localparam durTime = 1000;
integer i;
uasge_SPI_4wire UUT(
.en(tick_tx),
.clock_50M(sysClk),
.rst_n(rst_n),
.CS(cs_n),
.mosi(mosi),
.miso(miso),
.din({address,txdata}),
.dout(data_get),
.SCLK(sclk),
.finish(finish)
);
always@(posedge sclk, negedge rst_n)begin
if(!rst_n) miso <= 1'b0;
else if(i>=0&&i<16)miso <= rxdata[15-i];
else miso <= 1'b0;
end
always@(posedge sclk, negedge rst_n)begin
if(!rst_n)i <= 0;
else i <= i + 1;
end
always #10 sysClk = ~sysClk;
initial begin
sysClk = 0;
rst_n = 0;
tick_tx = 0;
address = 8'h7A;
txdata = 8'h81;
rxdata = 16'h4689;
rst_n = 0;
#1000 rst_n = 1;
#1000;
#1000 tick_tx = 1;
#1000 tick_tx = 0;
repeat(20) #durTime;
$stop;
end
endmodule
這裡的輸入資料 16 bit 是由 8 bit 的 address 以及 8 bit 的 txdata 所組成,
而 testbench 會將 rxdata 於 SCLK 的正緣依序由 MSB 傳出。
這邊可以看到 din 為 7a81,同樣為水藍色信號線也確實有在 SCLK 負緣時改變輸出值,而且值為 0111_1010_1000_0001,確實為 7a81。
再來是收的部分,可以看到粉紅色信號線是在 SCLK 正緣傳送 0100_0110_1000_1001(4689),而在 data_get 也確實在資料傳送結束時吐出 4689。
最後也來可以看一下 RTL Viewer
看起來沒有合成出過多冗餘的部分,還可以~
以上就是我們的 4 線 SPI 教學嘍~~~~