前言
SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司最早于1980年代推出的一种同步串行接口技术,其最早应用于M68系列微控制器与外围IC通信。SPI是一种高速的、全双工、同步的通信总线, 常用于MCU和 EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。现如今,SPI总线已经成为被广泛应用的一种数据传输方式,由于其简单的接口、灵活性和易用性,SPI 已成为一种标准,SPI被半导体制造商广泛应用于IC芯片。
以下有部分内容摘自Motorola官方手册,如有理解差异,请参考原手册。
1 简介
如图1-1所示为SPI结构框图,框图主要包含状态、控制和数据寄存器、移位逻辑控制、波特率发生器、主/从控制逻辑和端口控制逻辑。
![](https://i-blog.csdnimg.cn/blog_migrate/1dc61c87c78bd7fc91458d5e2a17e16b.png)
1.1 概述
SPI模块允许MCU和外围设备之间进行双工、同步、串行通信设备。软件可以轮询SPI状态标志,或者SPI操作可以由中断驱动,在上述SPI框图中体现了在MCU中SPI的驱动原理
1.2 特性
SPI包括以下特性:
•主模式和从模式
•双向模式
•从选择输出
•具有CPU中断功能的模式故障错误标志
•双缓冲数据寄存器
•具有可编程极性和相位的串行时钟
•等待模式下SPI操作的控制
在MCU中,上述特性中相关模式选择通过控制寄存器控制,相关状态通过状态寄存器可以查询;在有功耗要求的场景下可以切换到等待模式或停止模式降低功耗关闭部分功能。
2 外部信号描述
SPI通过4个外部引脚与外部设备通信,以下分别列出:
2.1 Master out/slave in (MOSI)
当SPI模块被配置为“主”时,此引脚用于将数据传输出SPI模块,当SPI模块配置为“从”时,该引脚用于接收数据。
2.2 Master in/slave out (MISO)
当SPI模块被配置为“从”时,此引脚用于将数据传输出SPI模块,当SPI模块配置为“主”时,该引脚用于接收数据。
2.3 Slave select ( SS ‾ \overline{\text{SS}} SS)
该引脚用于将选择信号从SPI模块输出到另一外围设备,当其被配置为“主”时,数据传输将与该外围设备一起进行,当SPI被配置为“从”时,该引脚被用作接收从选择信号的输入。
2.4 Serial clock(SCK)
该引脚用于输出SPI传输数据的时钟,或者在Slave的情况下接收时钟。
3 协议解析
在MCU集成的SPI驱动中,MCU可以通过SPI驱动和外围设备之间进行双工、同步、串行通信设备,通过软件配置可以轮询SPI状态标志,或者SPI操作可以由中断驱动,当然FPGA也可以实现同样的功能,但在大多数时候,FPGA作为主机通过SPI固定的模式与外围设备通信,一些状态寄存器和配置寄存器可以配置为固定的值,简化了设计。
3.1 数据传输方式
SPI为串行传输,发送数据时,在同步时钟下,一拍一拍的将数据串行移位发送,接收数据同样在同步时钟下,一拍一拍的将数据串行移位接收;在SPI传输期间。串行时钟(SCK)使两条串行数据线上的信息的移位和采样同步;片选信号允许选择单个从SPI设备,未被选择的从设备不会干扰SPI总线活动;有多个从设备时,可以通过片选信号分时选择与从设备通信。
3.2 时钟相位和极性控制
SPI有不同的四种模式,通过控制同步时钟的极性和相位可以实现不同模式的切换,在与不同的外围设备通信时,可以切换不同的模式。
CPOL时钟极性控制位指定有效的高或低时钟,(Motorola手册描述对传输格式没有显著影响),具体的极性选择与外围设备时序要求一致即可。
CPHA时钟相位控制可以选择在时钟的哪个相位发送/采样数据。
主SPI设备和通信从设备的时钟相位和极性应相同。在一些情况下,在传输之间改变相位和极性,以允许主设备与具有不同要求的外围从设备通信。
3.3 CPHA = 0时传输形式
从如下图中看到,当CPOL(时钟极性控制)为0时,IDLE(空闲)状态,时钟保持为0,当CPOL(时钟极性控制)为1时,IDLE(空闲)状态,时钟保持为1。
SCK第一个边沿前tL个时钟周期拉低SS(片选)信号,此时发送第一个数据,从图中可以看出,发送数据在第0、2、4、6、7、10、12、14个时钟边沿时,即偶数沿;采样数据在第1、3、5、7、9、11、13、15个时钟边沿时,即奇数沿;数据可以是MSB first(高位在前),也可以LSB first(低位在前),在FPGA实现中一般为高位在前,具体与从设备一致即可。
在发送/采样结束之后,在最后一个SCK沿后再等待tT时间回到IDLE(空闲)状态,IDLE(空闲)状态至少保持tI 再进行下一次传输。
从图中看出:tL、tT、tI最少保持二分之一个时钟周期。
![](https://i-blog.csdnimg.cn/blog_migrate/d6e9f315cd5641a82efd45da299ef589.png)
3.4 CPHA = 1时传输形式
从如下图中看到,同样,当CPOL(时钟极性控制)为0时,IDLE(空闲)状态,时钟保持为0,当CPOL(时钟极性控制)为1时,IDLE(空闲)状态,时钟保持为1。
SCK第一个边沿前tL个时钟周期拉低SS(片选)信号,在第一个时钟沿时发送第一个数据,从图中可以看出,发送数据在第1、3、5、7、9、11、13、15个时钟边沿时,即奇数沿;采样数据在第2、4、6、8、10、12、14、16个时钟边沿时,即偶数沿;数据可以是MSB first(高位在前),也可以LSB first(低位在前),在FPGA实现中一般为高位在前,具体与从设备一致即可。
在发送/采样结束之后,在最后一个SCK沿后再等待tT时间回到IDLE(空闲)状态,IDLE(空闲)状态至少保持tI 再进行下一次传输。
从图中看出:tL、tT、tI最少保持二分之一个时钟周期。
![](https://i-blog.csdnimg.cn/blog_migrate/58007943ac013d7159655c6dee94baa4.png)
4 Verilog描述
4.1 SPI主机代码实现
在实际FPGA应用中,为了对第1章节中框图中的SPI结构进行简化,通常只需其中的串行数据移位控制、波特率生成,同时为了应用于不同的外围设备,可以将SPI模式作为参数进行传递。
通常通过FPGA控制SPI通信应用中,主要为FLASH读写控制,外围器件寄存器读写,以上操作一般为炒作指令+地址+数据的形式,对于不同类型或不同大小的外围设备,指令长度、地址长度、寄存器长度灯都可能不一致,为了更好的适应不同的器件,如下代码可以控制指令长度、地址长度、寄存器可变的场景,方便在控制不同的外围设备时移植。
//
//
// Author: NZ
// Create Date: 2022/10/17 21:31:30
// Design Name:spi_master.v
// Description:
// Revision:
// Revision 0.01 - File Created
//
//
`timescale 1ns/1ns
module spi_master
#(
parameter CPOL = 1'b0 ,
parameter CPHA = 1'b0 ,
parameter SYSCLK_FREQ = 100_000_000 , //system clk
parameter SPICLK_FREQ = 10_000_000 , //spi clk;
parameter MAX_CMDWIDTH = 8'd8 , //max command width,include instr and address
parameter MAX_DATAWIDTH = 8'd32 //the max data width,
)
(
input wire i_rst_n , //system reset ,acitive low
input wire i_clk , //input system clock ,100Mhz
input wire i_wr_en , //spi write enable,at least one system clock cycle,active high
input wire i_rd_en , //spi read enable,at least one system clock cycle,active high
input wire[7:0] i_byte_len , //Actual length of read or write operation data
input wire[MAX_CMDWIDTH-1:0] i_cmd_word , //Command data
input wire[MAX_DATAWIDTH-1:0] i_wr_data , //Data to write
input wire i_spi_miso , //spi device to fpga
output wire o_spi_mosi , //fpga to spi device
output wire o_spi_csn , //spi chip select
output wire o_spi_sck , //spi clock
output wire[MAX_DATAWIDTH-1:0] o_rd_data , //spi read data
output wire o_spi_rdy , //if this signal is assert,spi master is idle
output wire o_operate_end //After one operation, assert that at least one clock cycle is valid
);
/*************************************************************************/
/******* constant ****/
/*************************************************************************/
localparam CLK_DIV = SYSCLK_FREQ/SPICLK_FREQ ;
localparam ALLDATAWIDTH = MAX_CMDWIDTH+MAX_DATAWIDTH ;
localparam S_IDLE = 3'd0 ;
localparam S_WRITE = 3'd1 ;
localparam S_READ = 3'd2 ;
localparam S_WRTRANSITION = 3'd3 ;
localparam S_RDTRANSITION = 3'd4 ;
localparam S_OPEND = 3'd5 ;
localparam S_HOLD = 3'd6 ;
/*************************************************************************/
/******* reg / wire ******/
/*************************************************************************/
reg wr_start_dly1,wr_start_dly2 ;
reg rd_start_dly1,rd_start_dly2 ;
wire wr_start_rise ;
wire rd_start_rise ;
reg spi_flag ;
reg write_or_read ;
wire spi_valid_flag ;
reg spi_cmd_flag ;
wire spi_wr_flag ;
wire spi_rd_flag ;
reg spi_rdy ;
reg [7:0] sck_cnt ;
wire sck ;
reg cs_n ;
wire spi_mosi ;
wire [7:0] bit_cnt ;
reg [ALLDATAWIDTH-1:0] spi_wr_data ;
reg [7:0] hlafclk_cnt ;
reg [10:0] wr_len ;
reg [10:0] rd_len ;
reg operate_end ;
reg [10:0] rd_cnt ;
reg [2:0] state ;
reg [MAX_DATAWIDTH-1:0] rd_data ;
/*************************************************************************/
/******* spi write or read enable edge ******/
/*************************************************************************/
assign wr_start_rise = wr_start_dly1 & (~wr_start_dly2);
assign rd_start_rise = rd_start_dly1 & (~rd_start_dly2);
always @(posedge i_clk)begin
if(!i_rst_n)begin
wr_start_dly1 <= 1'b0;
wr_start_dly2 <= 1'b0;
rd_start_dly1 <= 1'b0;
rd_start_dly2 <= 1'b0;
end
else begin
wr_start_dly1 <= i_wr_en;
wr_start_dly2 <= wr_start_dly1;
rd_start_dly1 <= i_rd_en;
rd_start_dly2 <= rd_start_dly1;
end
end
/*********************************************************************/
/***** spi write or read operation *****/
/*********************************************************************/
always @(posedge i_clk)begin
if(!i_rst_n)begin
spi_flag <= 1'b0 ;
operate_end <= 1'b0 ;
cs_n <= 1'b1 ;
spi_wr_data <= 0 ;
wr_len <= 11'd0 ;
rd_len <= 11'd0 ;
spi_rdy <= 1'b0 ;
write_or_read <= 1'b0 ;
state <= S_IDLE ;
end
else begin
case(state)
S_IDLE : begin
if(wr_start_rise)begin
state <= S_WRITE;
spi_rdy <= 1'b0;
write_or_read <= 1'b0;
wr_len <= {i_byte_len,3'b000};
spi_wr_data <= {i_cmd_word,i_wr_data};
end
else if(rd_start_rise)begin
state <= S_READ;
spi_rdy <= 1'b0;
write_or_read <= 1'b1;
rd_len <= {i_byte_len,3'b000};
spi_wr_data <= {i_cmd_word,{MAX_DATAWIDTH{1'b0}}};
end
else begin
spi_rdy <= 1'b1;
operate_end <= 1'b0;
state <= S_IDLE;
end
end
S_WRITE : begin
if(i_byte_len==8'd0)begin
state <= S_OPEND;
end
else if(hlafclk_cnt==wr_len<<1 && sck_cnt==(CLK_DIV>>1)-1)begin
spi_flag <= 1'b0;
cs_n <= 1'b1;
wr_len <= 11'd0;
state <= S_OPEND;
end
else begin
spi_flag <= 1'b1;
cs_n <= 1'b0;
state <= S_WRITE;
end
end
S_READ : begin
if(i_byte_len==8'd0)begin
state <= S_OPEND;
end
else if(hlafclk_cnt==rd_len<<1 && sck_cnt==(CLK_DIV>>1)-1)begin
spi_flag <= 1'b0 ;
cs_n <= 1'b1 ;
wr_len <= 11'd0;
rd_len <= 11'd0;
state <= S_OPEND;
end
else begin
spi_flag <= 1'b1 ;
cs_n <= 1'b0 ;
wr_len <= MAX_CMDWIDTH ;
end
end
S_OPEND : begin
spi_flag <= 1'b0;
wr_len <= 11'd0;
rd_len <= 11'd0;
operate_end <= 1'b1;
state <= S_IDLE;
end
default : begin
state <= S_IDLE;
end
endcase
end
end
always @(*)begin
if(!i_rst_n)begin
spi_cmd_flag <= 1'b0 ;
end
else if(state == S_WRITE | state == S_READ) begin
if(bit_cnt>MAX_CMDWIDTH-1'b1)begin
spi_cmd_flag <= 1'b0;
end
else begin
spi_cmd_flag <= 1'b1;
end
end
else begin
spi_cmd_flag <= 1'b0;
end
end
assign spi_wr_flag = (spi_flag==1'b1 && write_or_read==1'b0)?~spi_cmd_flag:1'b0;
assign spi_rd_flag = (spi_flag==1'b1 && write_or_read==1'b1)?~spi_cmd_flag:1'b0;
/*************************************************************************/
/******* spi clk gen ******/
/*************************************************************************/
always @(posedge i_clk)begin
if(!i_rst_n)begin
sck_cnt <= 8'd0;
end
else if(spi_flag && sck_cnt==(CLK_DIV>>1)-1'b1)begin
sck_cnt <= 0;
end
else if(spi_flag) begin
sck_cnt <= sck_cnt + 1'b1;
end
else begin
sck_cnt <= 0;
end
end
assign sck = (spi_flag)?
CPOL?~hlafclk_cnt[0]:hlafclk_cnt[0]
:CPOL;
always @(posedge i_clk)begin
if(!i_rst_n)begin
hlafclk_cnt <= 0;
end
else if(cs_n==0 && hlafclk_cnt==i_byte_len<<(3+1) && sck_cnt==(CLK_DIV>>1)-1)begin
hlafclk_cnt <= 0;
end
else if(cs_n==0 && sck_cnt==(CLK_DIV>>1)-1)begin
hlafclk_cnt <= hlafclk_cnt + 1'b1;
end
else if(cs_n==1)begin
hlafclk_cnt <= 0;
end
end
/*************************************************************************/
/******* spi Write ******/
/*************************************************************************/
assign bit_cnt = hlafclk_cnt>>1 ;
assign spi_valid_flag = hlafclk_cnt>>1 ;
assign spi_mosi = spi_flag?spi_wr_data[ALLDATAWIDTH - 1'b1 - bit_cnt]:1'b0;
/*********************************************************/
/***** spi read *****/
/*********************************************************/
always @(posedge i_clk)begin
if(!i_rst_n)begin
rd_cnt <= 11'd0;
rd_data <= {MAX_DATAWIDTH{1'b0}};
end
else if(spi_rd_flag==1'b0)begin
rd_cnt <= 11'd0;
end
else if(spi_rd_flag && sck_cnt==(CLK_DIV>>1)-1'b1)begin
rd_cnt <= rd_cnt + 1'b1;
rd_data[MAX_DATAWIDTH-1'b1-rd_cnt] <= i_spi_miso;
end
else begin
rd_data <= rd_data;
end
end
/*************************************************************************/
//************** OUTPUT **************//
/*************************************************************************/
assign o_spi_mosi = spi_mosi ;
assign o_spi_csn = cs_n ;
assign o_spi_sck = sck ;
assign o_spi_rdy = spi_rdy ;
assign o_operate_end = operate_end ;
assign o_rd_data = rd_data ;
endmodule
4.2 Testbench
//
//
// Author: NZ
// Create Date: 2022/10/17 21:31:30
// Design Name:spi_master_tb.v
// Description:
// Revision:
// Revision 0.01 - File Created
//
//
`timescale 1ns/1ns
module spi_master_tb();
reg i_rst_n ;
reg i_clk ;
reg i_wr_start ;
reg i_rd_start ;
reg[7:0] i_byte_len ;
reg[7:0] i_cmd_word ;
reg[31:0] i_wr_data ;
reg i_spi_sdo ;
wire o_spi_sdi ;
wire o_spi_rdy ;
wire o_spi_csn ;
wire o_spi_clk ;
wire[31:0] o_rd_data ;
wire o_operate_end ;
reg[31:0] rd_data ;
always #5 i_clk = ~i_clk;
// 初始化
initial
begin
i_clk = 0;
i_rst_n = 0;
#100;
i_rst_n = 1;
end
task spi_write(
input [7:0] cmd_word ,
input [7:0] byte_len ,
input [255:0] spi_wr_data
);
integer i ;
reg[7:0] data_temp ;
begin
i_cmd_word = cmd_word ;
i_byte_len = byte_len ;
i_wr_data = spi_wr_data ;
i_wr_start = 1;
#20
i_wr_start = 0;
#3000;
end
endtask
initial
begin
i_spi_sdo = 1'b1;
i_wr_start = 1'b0;
i_rd_start = 1'b0;
i_cmd_word = 8'b0;
i_byte_len = 8'b0;
i_wr_data = 32'b0;
rd_data = 32'b0;
#100;
spi_write(8'h55,8'h02,32'ha5a22a78);
#300;
spi_write(8'haa,8'h03,32'ha5a22a78);
#300;
$stop;
end
always #100 i_spi_sdo = $random;
//***************************************************
// 例化顶层文件
spi_master
#(
.CPOL (0 ),
.CPHA (0 ),
.MAX_CMDWIDTH (8 ),
.MAX_DATAWIDTH (32 )
)
spi_master_inst(
.i_rst_n (i_rst_n ),
.i_clk (i_clk ),
.i_wr_en (i_wr_start ),
.i_rd_en (i_rd_start ),
.i_byte_len (i_byte_len ),
.i_cmd_word (i_cmd_word ),
.i_wr_data (i_wr_data ),
.i_spi_miso (i_spi_sdo ),
.o_spi_mosi (o_spi_sdi ),
.o_spi_rdy (o_spi_rdy ),
.o_spi_csn (o_spi_csn ),
.o_spi_sck (o_spi_clk ),
.o_rd_data (o_rd_data ),
.o_operate_end (o_operate_end )
);
endmodule
4.3 仿真
如下图所示:将模式设置为CPOL=0,CPHA=0,此时,将最大数据位宽设置为32bit,命令+数据设置有效字节为2byte,命令放在最先发送的字节,命令字节设置为0x55,从图中看出在片选信号拉低时发送第一bit数据,然后依次发送,分别为0、1、0、1、0、1、0、1,紧接着发送数据,由于定义为32bit 数据,从高字节开始,即有效位为为最高字节0xa5,与协议中模式0对比时钟极性为0,数据在偶数个时钟发送,与协议一致
有兴趣可以尝试更改SPI模式或字节长度查看是否满足其他模式时序要求
参考文档:SPI Block Guide V04.01