SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步的通信总线。
/***************************************************
* Module Name : spi_master
* Engineer : GUAN
* Target Device : EP4CE6E17C8
* Tool versions : Quartus II 13.0
* Create Date : 2020-3-5
* Revision : v1.0
* Description : SPI主机控制器
***************************************************/
module spi_master
(
input sys_clk,
input rst_n,
output cs_n, //chip select (SPI mode)
output reg sclk, //spi clock
output mosi, //spi master data output
input miso, //spi master input
input CPOL,
input CPHA,
input [9:0] spi_div,
input cs_ctrl,
input wr_req,
output wr_ack,
input [7:0] data_in,
output [7:0] data_out
);
localparam IDLE = 0;
localparam SCLK_RUN = 1;
localparam ACK = 2;
localparam ACK_WAIT = 3;
reg [2:0] cur_state ;
reg [2:0] next_state;
reg [9:0] clk_cnt; // 分频时钟
reg [4:0] cnt; // 计数
reg [7:0] mosi_shift;
reg [7:0] miso_shift;
reg wr_ack_r;
wire wr_ack_reg;
assign cs_n = cs_ctrl;
assign mosi = mosi_shift[7];
assign wr_ack = (cur_state == ACK);
assign data_out = miso_shift ;
//生成SPI的SCLK的二倍频率(25M)的驱动时钟用于驱动spi的操作
always @(posedge sys_clk or negedge rst_n)
begin
if(!rst_n)
clk_cnt <= 10'd0;
else if(clk_cnt >= (spi_div-1))
clk_cnt <= 10'd0;
else
clk_cnt <= clk_cnt + 1'b1;
end
wire sclk_plus = (clk_cnt == (spi_div-1));
//同步时序描述状态转移
always @(posedge sys_clk or negedge rst_n)
begin
if(!rst_n)
cur_state <= IDLE;
else
cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always@(*)
begin
case(cur_state)
IDLE:
if(wr_req == 1'b1)
next_state = SCLK_RUN;
else
next_state = IDLE;
SCLK_RUN:
if(cnt == 5'd17)
next_state = ACK;
else
next_state = SCLK_RUN;
//send one byte complete
ACK:
next_state = ACK_WAIT;
//wait for one clock cycle, to ensure that the cancel request signal
//确保wr_req信号和cur_state==IDLE同时发生变化
ACK_WAIT:
next_state <= IDLE;
default:
next_state <= IDLE;
endcase
end
//时序电路描述时钟控制信号的输出
always @(posedge sys_clk or negedge rst_n) begin
//复位初始化
if(!rst_n) begin
sclk <= CPOL;
//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
cnt <= 0;
end
else if(cur_state != SCLK_RUN)
cnt <= 5'd0;
else if(cur_state == SCLK_RUN && sclk_plus)
begin
cnt <= cnt +5'd1 ;
case(cnt)
5'd0: sclk <= CPOL;
5'd1,5'd2,5'd3,5'd4,5'd5,5'd6,5'd7,5'd8,5'd9,5'd10,5'd11,5'd12,5'd13,5'd14,5'd15:
sclk <= ~sclk;
5'd16: sclk <= ~sclk;
endcase
end
end
//SPI主机数据输出
always@(posedge sys_clk or negedge rst_n)
begin
if(!rst_n)
mosi_shift <= 8'd0;
else if((cur_state == SCLK_RUN) && (cnt==0))
mosi_shift <= data_in;
else if((cur_state == SCLK_RUN) && (cnt!=0) && sclk_plus)
if(CPHA == 1'b0 && ((cnt!=5'd0)&&(cnt[0] == 1'b0)))
//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出 cnt=cs_n/2/4/6/8/10/12/14
mosi_shift <= {mosi_shift[6:0],mosi_shift[7]};
else if(CPHA == 1'b1 && ((cnt!=5'd1)&&(cnt[0] == 1'b1)))
//CPHA=1,在串行同步时钟的第一个跳变沿(上升或下降)数据被输出 cnt=cs_n/3/5/7/9/11/13/15
mosi_shift <= {mosi_shift[6:0],mosi_shift[7]};
end
//SPI主机数据采样
always@(posedge sys_clk or negedge rst_n)
begin
if(!rst_n)
miso_shift <= 8'd0;
else if((cur_state == SCLK_RUN) && (cnt==0))
miso_shift <= 8'h00;
else if((cur_state == SCLK_RUN) && (cnt!=0) && sclk_plus)
if(CPHA == 1'b0 && cnt[0] == 1'b1)//CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样 cnt=1/3/5/7/9/11/13/15
miso_shift <= {miso_shift[6:0],miso};
else if(CPHA == 1'b1 && ((cnt!=5'd0)&&(cnt[0] == 1'b0)))//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样 cnt=2/4/6/8/10/12/14/16
miso_shift <= {miso_shift[6:0],miso};
end
endmodule
/***************************************************
* Module Name : spi_slave
* Engineer : GUAN
* Target Device : EP4CE6E17C8
* Tool versions : Quartus II 13.0
* Create Date : 2020-3-5
* Revision : v1.0
* Description : SPI从机控制器
***************************************************/
module spi_slave
(
input clk,
input rst_n,
input cs_n,
input sclk,
input mosi,
input CPOL,
input CPHA,
input [7:0] slave_data_in,
output wire miso,
output wire [7:0] slave_data_out,
output wire rd_ack//recieve done,please transmit data
);
reg [3:0] rxd_cnt; //从机接收计数器
reg [3:0] txd_cnt; //从机发送计数器
reg [1:0] sclk_r;
reg [4:0] sclk_edge_cnt;
reg [7:0] miso_shift;//从机输出数据寄存器
reg [7:0] mosi_shift;//从机接收数据寄存器
reg [1:0] rd_ack_r;
reg rd_ack_reg;
//捕获sclk的时钟边沿
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
sclk_r[0] <= CPOL; //sclk of the idle state is high
sclk_r[1] <= CPOL;
rd_ack_r[0]<= 1'b0;
rd_ack_r[1]<= 1'b0;
end
else
begin
sclk_r[0] <= sclk;
sclk_r[1] <= sclk_r[0];
rd_ack_r[0]<= rd_ack_reg;
rd_ack_r[1]<= rd_ack_r[0];
end
end
assign sclk_n = ({sclk_r[0],sclk} == 2'b10); //capture the sclk negedge
assign sclk_p = ({sclk_r[0],sclk} == 2'b01); //capture the sclk posedge
assign rd_ack = ({rd_ack_r[1],rd_ack_r[0]} == 2'b01);
//SPI时钟SCLK的边沿计数器
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
sclk_edge_cnt <= 5'd0;
else if(sclk_p ||sclk_n)
sclk_edge_cnt <= sclk_edge_cnt + 5'd1;
else if(rd_ack)
sclk_edge_cnt <= 5'd0;
else
sclk_edge_cnt <= sclk_edge_cnt;
end
//SPI从机数据输出
always@(posedge clk or posedge rst_n)
begin
if(!rst_n)
begin
miso_shift <= 8'd0;
txd_cnt <= 4'd0;
end
//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
else if ((CPOL == 1'b1) && (CPHA == 1'b1))
begin
if(cs_n || rd_ack)
txd_cnt <= 1'b0;
else if((txd_cnt == 4'd0) && (!cs_n))
begin
miso_shift <= slave_data_in;
txd_cnt <= txd_cnt + 1'b1;
end
else if(sclk_n)
begin//SCLK下降沿输出MISO的数据,
if((sclk_edge_cnt!= 5'd0) && (sclk_edge_cnt[0] == 1'b0))
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=cs_n/2/4/6/8/10/12/14
miso_shift <= {miso_shift[6:0],miso_shift[7]};
txd_cnt <= txd_cnt + 1'b1;
end
end
//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
else if ((CPOL == 1'b1) && (CPHA == 1'b0))
begin
if(cs_n || rd_ack)
txd_cnt <= 1'b0;
else if((txd_cnt == 4'd0) && (!cs_n))
begin
miso_shift <= slave_data_in;
txd_cnt <= txd_cnt + 1'b1;
end
else if(sclk_p)
begin //SCLK上升沿输出MISO的数据,
if(sclk_edge_cnt[0] == 1'b1)
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=cs_n/1/3/5/7/9/11/13
miso_shift <= {miso_shift[6:0],miso_shift[7]};
txd_cnt <= txd_cnt + 1'b1;
end
end
//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
else if ((CPOL == 1'b0) && (CPHA == 1'b0))
begin
if(cs_n || rd_ack)
txd_cnt <= 1'b0;
else if((txd_cnt == 4'd0) && (!cs_n))
begin
miso_shift <= slave_data_in;
txd_cnt <= txd_cnt + 1'b1;
end
else if(sclk_n)
begin //SCLK下降沿输出MISO的数据,
if(sclk_edge_cnt[0] == 1'b1)
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=cs_n/1/3/5/7/9/11/13
miso_shift <= {miso_shift[6:0],miso_shift[7]};
txd_cnt <= txd_cnt + 1'b1;
end
end
//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
else if ((CPOL == 1'b0) && (CPHA == 1'b1))
begin
if(cs_n || rd_ack)
txd_cnt <= 1'b0;
else if((txd_cnt == 4'd0) && (!cs_n))
begin
miso_shift <= slave_data_in;
txd_cnt <= txd_cnt + 1'b1;
end
else if(sclk_p)
begin //SCLK上升沿输出MISO的数据,
if((sclk_edge_cnt!= 5'd0) && (sclk_edge_cnt[0] == 1'b0))
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=cs_n/2/4/6/8/10/12/14
miso_shift <= {miso_shift[6:0],miso_shift[7]};
txd_cnt <= txd_cnt + 1'b1;
end
end
end
assign miso = miso_shift[7];
//SPI从机数据采样
always@(posedge clk or posedge rst_n)
begin
if(!rst_n)
begin
mosi_shift <= 8'd0;
rxd_cnt <= 4'd0;
rd_ack_reg <= 1'b0;
end
//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
else if ((CPOL == 1'b1) && (CPHA == 1'b1))
begin
if(cs_n)
mosi_shift<= 8'h00;
else if(rd_ack)
rxd_cnt <= 1'b0;
else if(sclk_p && !cs_n)
begin //上升沿采样
if(sclk_edge_cnt[0] == 1'b1)
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=1/3/5/7/9/11/13/15
mosi_shift <= {mosi_shift[6:0],mosi};
rxd_cnt <= rxd_cnt + 1'b1;
if(rxd_cnt == 4'd7)
rd_ack_reg <= 1'b1;
else
rd_ack_reg <= 1'b0;
end
end
//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
else if ((CPOL == 1'b1) && (CPHA == 1'b0))
begin
if(cs_n)
mosi_shift<= 8'h00;
else if(rd_ack)
rxd_cnt <= 1'b0;
else if(sclk_n && !cs_n)
begin //下降沿采样
if(sclk_edge_cnt[0] == 1'b0) //判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=0/2/4/6/8/10/12/14
mosi_shift <= {mosi_shift[6:0],mosi};
rxd_cnt <= rxd_cnt + 1'b1;
if(rxd_cnt == 4'd7)
rd_ack_reg <= 1'b1;
else
rd_ack_reg <= 1'b0;
end
end
//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
else if ((CPOL == 1'b0) && (CPHA == 1'b0))
begin
if(cs_n)
mosi_shift<= 8'h00;
else if(rd_ack)
rxd_cnt <= 1'b0;
else if(sclk_p && !cs_n)
begin //上升沿采样
if(sclk_edge_cnt[0] == 1'b0)
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=0/2/4/6/8/10/12/14
mosi_shift <= {mosi_shift[6:0],mosi};
rxd_cnt <= rxd_cnt + 1'b1;
if(rxd_cnt == 4'd7)
rd_ack_reg <= 1'b1;
else
rd_ack_reg <= 1'b0;
end
end
//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
else if ((CPOL == 1'b0) && (CPHA == 1'b1))begin
if(cs_n)
mosi_shift<= 8'h00;
else if(rd_ack)
rxd_cnt <= 1'b0;
else if(sclk_n && !cs_n)begin //下降沿采样
if(sclk_edge_cnt[0] == 1'b1)
//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=1/3/5/7/9/11/13/15
mosi_shift <= {mosi_shift[6:0],mosi};
rxd_cnt <= rxd_cnt + 1'b1;
if(rxd_cnt == 4'd7)
rd_ack_reg <= 1'b1;
else
rd_ack_reg <= 1'b0;
end
end
end
assign slave_data_out = rd_ack ? mosi_shift : slave_data_out ;
endmodule
若你驱动的设备为三线制(SDI和SDO合并成了一个双向的SDIO)的SPI,应该如何去驱动呢?请大家接着往下看,让我们直接在顶层例化中去完成:
spi_master u3(
.sys_clk (clk ),
.rst_n (rst_n ),
.cs_n (rtc_nce ),
.sclk (rtc_sclk ),
.mosi (mosi ),
.miso (rtc_io ),
.spi_div (16'd51 ),
//sclk speed = clk speed /(spi_div+1)/2
.CPOL (1'b0 ),
.CPHA (1'b0 ),
.ncs_ctrl (ncs_ctrl ),
.wr_req (spi_wr_req ),
.wr_ack (spi_wr_ack ),
.data_in (spi_send_data ),
.data_out (spi_data_recv )
);
assign rtc_io = ~rtc_io_dir ? mosi : 1'bz;