一、简介
SPI(Serial Peripheral Interface)串行外设接口,是一种高速、全双工、同步的串行通信总线。SPI最初由Motorola在2000年提出,Motorola所定义的SPI标准并未被任何国际委员会定义为统一标准,但业界以此为基础广泛引用成为事实标准。不过不同半导体公司的实施细节可能有所不同,导致业界没有统一的SPI标准,这些区别体现在寄存器设置、信号定义、数据格式等,具体应用需要参考特定器件手册。
因此后文在尽量全面介绍该协议的同时,主要对最常用的方案进行总结。
SPI以主从方式工作,一般存在一个主设备和多个从设备,主设备通过选择不同的从设备进行数据传输和接收。SPI通常用于EEPROM,FLASH,AD转换器等芯片进行数据交互。
SPI通信协议的基本原理是利用时钟信号的同步作用,实现数据在多个设备之间的传递,主设备通过时钟线来规定数据的传输数据,并利用数据线进行数据的传输。。SPI通信一般由四根线或者五根线组成,包括时钟线(CLK)、数据线(MOSI或MISO)、主机选择线(SS或Chip Select, CS)与从机选择线(Chip Enable, CE),常用方案为四根线,主机选择线(CS)也用于从机使能(CE)。
图1-1 motorala SPI Block Diagram
二、基本协议
SPI以主从方式工作,一般存在一个主设备和多个从设备,需要至少4根信号线,事实上3根也可以(单向传输时)。四根信号线为:
(1)SCLK - Serial CLK,时钟信号,由主设备产生;
(2)CS - Chip Select,片选/从设备使能信号,由主设备控制;
(3)MOSI - Master Out Slave In,主设备数据输出,从设备数据输入;
(4)MISO - Master In Slave Out,主设备输入,从设备输出。
SPI通信过程中,CS信号首先使能(高电平或者低电平,根据从机datasheet而定)指定的从设备,然后MOSI和MISO(MSB或者LSB发送先后,传输字的字长,根据从机datasheet而定)基于SCLK信号(极性和相位根据从机datasheet而定)进行数据传输(交换);数据传输时,一个字节传输完后无需应答即可开始下一个字节的传送;CS信号变为非使能状态后,传输结束。注:常用方案为CS低电平使能,MSB先发送,8bit或16bit。
SCLK是SPI数据传输的基础,因此其非常重要,SCLK信号只有主设备进行控制,从设备不能控制SCLK信号线。SPI数据传输方式与SCLK的极性(CPOL)和相位(CPHA)两个因素有关:CPOL表示SCLK空闲时候的状态,CPOL为0表示空闲时SCLK为低电平,CPOL为1表示空闲时SCLK为高电平;CPHA表示传输采样时刻,CPHA为0表示每个周期的第一个时钟沿采样,CPHA为1表示每个周期的第二个时钟沿采样。根据SCLK两种不同因素的组合,SPI总线分成下图2-1四种不同的工作模式(mode),其黑色代表clk信号,其初始状态几位clk的空闲状态,红色线则代表数据采样的时刻。SPI配置模式则有00(0),01(1),10(2),11(3)四种情况,最常用的时Mode0和mode3。对于一个特定的从设备来说,一般在出场时就会将其设计为某种特定的工作模式;我们在使用该从设备的时候,需要对主设备的时钟极性和相位进行配置,使其与从设备的工作模式保持一致,否则无法进行通信。
图2-1 SPI 传输模式
图2-2为motorola SPI Block Guide V03.06中CHPA=0的时序图,其片选信号先于时钟信号半个周期有效,片选后数据立即出现在MOSI/MISO管脚,数据在第一个时钟边沿采样(锁存),在第二个时钟边沿,将锁存的数据写入到移位寄存器中,经过16个时钟边沿后,MOSI数据线立即失效,MISO继续有效至低有效片选信号拉高,8位数据全部写入移位寄存器内,完成主从设备数据交换。个人理解这种方式更适合主设备向从设备写入数据。
注意图2-2中的三个时间要求:
minimum leading time:片选信号有效沿到第一个时钟沿的最小时间间隔,我理解为此处是为了满足触发器的建立时间(寄存器的写入时间);
minimum trailing time:数据传输完成时钟沿到片选信号恢复空闲状态边沿的最小时间间隔,我理解此处时为了满足出发其的保持时间;
minimum ilding time:两次传输之间时钟处于空闲状态的最小时间。
图2-2 CHPA = 0
图2-3为motorola SPI Block Guide V03.06中CHPA=1的时序图,其片选信号先于时钟信号半个周期有效,片选后MISO数据立即有效,MOSI数据在第一个时钟边沿有效,数据在第一个时钟边沿传输,在第二个时钟边沿,将对数据采样,经过16个时钟边沿后,8位数据全部写入移位寄存器内,完成主从设备数据交换。个人理解这种方式更适合主设备读从设备数据。
图2-3 CHPA = 1
三、SPI通信实现方案
3.1 一主一从
图3-1 一主一从连接
3.2 一主多从(主设备多位片选信号)
每个从设备都需要单独的片选信号,主设备每次只能选择其中一个从设备进行通信。因为所有从设备的SCK、MOSI、MISO都是连在一起的,未被选中从设备的MISO要表现为高阻状态(Hi-Z)以避免数据传输错误。由于每个设备都需要单独的片选信号,如果需要的片选信号过多,可以使用译码器产生所有的片选信号。
图3-2 一主多从连接
3.3 一主多从(主设备一位片选信号,菊花链)
数据信号经过主从设备所有的移位寄存器构成闭环。数据通过主设备发送(绿色线)经过从设备返回(蓝色线)到主设备。在这种方式下,片选和时钟同时接到所有从设备,通常用于移位寄存器和LED驱动器。注意,菊花链方式的主设备需要发送足够长的数据以确保数据送达到所有从设备。切记主设备所发送的第一个数据需(移位)到达菊花链中最后一个从设备。
菊花链式连接常用于仅需主设备发送数据而不需要接收返回数据的场合,如LED驱动器。在这种应用下,主设备MISO可以不连。如果需要接收从设备的返回数据,则需要连接主设备的MISO形成闭环。同样地,切记要发送足够多的接收指令以确保数据(移位)送达主设备。
图3-3 菊花链连接
四、Verilog实现 SPI mode0 master
4.1 时钟产生模块
module sclk_gen
#(
parameter EVEN_DIV_CNT_VALUE = 4
)
(
input sys_clk,
input rst_n,
input clkgen_en,
output reg spi_clk,
output spi_neg_clk
);
reg [log2(EVEN_DIV_CNT_VALUE-1):0] even_div_cnt;
reg clkgen_en_delay1;
reg clkgen_en_delay2;
always@(posedge sys_clk )begin
clkgen_en_delay1<= clkgen_en;
clkgen_en_delay2<= clkgen_en_delay1;
end
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
even_div_cnt <= { log2(EVEN_DIV_CNT_VALUE){1'b0}};
else if(clkgen_en == 1'b1)
begin
if(even_div_cnt == (EVEN_DIV_CNT_VALUE-1))
even_div_cnt <= { log2(EVEN_DIV_CNT_VALUE){1'b0}};
else
even_div_cnt <= even_div_cnt + 1;
end
else
even_div_cnt <= { log2(EVEN_DIV_CNT_VALUE){1'b0}};
end
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
spi_clk <= 1'b0;
else if(even_div_cnt == (EVEN_DIV_CNT_VALUE-1))
spi_clk <= ~spi_clk;
else
;
end
assign spi_neg_clk =(clkgen_en /*|| clkgen_en_delay2*/)? ~spi_clk:1'b0;
function integer log2;
input [31:0] size;
integer i;
begin
log2 = 1;
for(i=0;2**i < size;i=i+1)
log2 = i+1;
end
endfunction
endmodule
4.2 master 实现
///
//Function : implemention of spi mode 0
//Authour: liuchj
//Description: SPI (Serial Peripheral Interface) Master
// support:
// 1. single chip-select capability,
// 2. use SPI mode0
// 3. arbitrary length byte transfers.
// //4. Big_Endian or Little_Endian select
//Parameters: CLK_DIV_CNT_VALUE - set the value of the SPI clock generate counter
// DATA_WIDTH - input data,output data and trans data length
//
// Note: 1. If multiple CS signals are needed, will need to use different module,
// OR multiplex the CS from this at a higher level.
// 2. SPI CLK is even divided by sys_clk
// version: 1.0
///
module spi_master
#(
parameter CLK_DIV_CNT_VALUE = 2,
parameter DATA_WIDTH = 8
)
(
input sys_clk,
input rst_n,
input spi_start,
input [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_recv,
output reg data_recv_vld,
output spi_clk,
input spi_miso,
output reg spi_mosi,
output reg spi_csn
);
parameter IDLE = 2'd0;
parameter START = 2'd1;
parameter TRANS = 2'd2;
parameter END = 2'd3;
reg [1:0] c_state;
reg [1:0] n_state;
wire idle2start_flag ;
wire trans2end_flag;
wire end2idle_flag;
reg clkgen_en;
//--------------- spi_clk generate ---------------//
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
clkgen_en<=1'b0;
else if(c_state == END)
clkgen_en <= 1'b0;
else if(c_state == START)
clkgen_en <= 1'b1;
end
wire spi_neg_clk;
sclk_gen
#(
.EVEN_DIV_CNT_VALUE(CLK_DIV_CNT_VALUE)
)
u0_sclk_gen(
.sys_clk(sys_clk),
.rst_n(rst_n),
.clkgen_en(clkgen_en),
.spi_clk(spi_clk),
.spi_neg_clk(spi_neg_clk)
);
//-------------- spi master state machine ------------//
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
c_state <= IDLE;
else
c_state <=n_state;
end
always@(*)begin
case(c_state)
IDLE:
begin
if(idle2start_flag == 1'b1 )
n_state = START;
else
n_state = c_state;
end
START:
begin
if(1)
n_state = TRANS;
else
n_state = c_state;
end
TRANS:
begin
if(trans2end_flag ==1'b1 )
n_state = END;
else
n_state = c_state;
end
END:
begin
if(end2idle_flag )
n_state = IDLE;
else
n_state = c_state;
end
default: n_state = IDLE;
endcase
end
reg spi_start_delay;
reg [log2(DATA_WIDTH-1):0] trans_cnt;
reg [log2(DATA_WIDTH-1):0] spiclk_cnt;
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
spi_start_delay <= 1'b0;
else
spi_start_delay <= spi_start;
end
assign idle2start_flag = ((~spi_start) && spi_start_delay) && (c_state == IDLE);
assign trans2end_flag = spiclk_cnt == 4'd9 && c_state == TRANS;
assign end2idle_flag = spiclk_cnt == 4'd9 && (c_state == END);
reg spi_busy;
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
spi_busy <= 1'b0;
else if(c_state != IDLE )
spi_busy <= 1'b1;
else
spi_busy <= 1'b0;
end
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
spi_csn <= 1'b1;
else if(c_state == START)
spi_csn <= 1'b0;
else if(c_state == END)
spi_csn <= 1'b1;
end
always@(posedge spi_neg_clk or negedge rst_n)begin
if(!rst_n)
trans_cnt <= {log2(DATA_WIDTH){1'b0}};
else if(c_state == TRANS && trans_cnt < DATA_WIDTH)
trans_cnt <= trans_cnt + 1;
else if(trans_cnt == DATA_WIDTH )
trans_cnt <= {log2(DATA_WIDTH){1'b0}};
end
reg spi_neg_clk_delay1;
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
spi_neg_clk_delay1 <= 1'b0;
else if(clkgen_en == 1 )
spi_neg_clk_delay1 <= spi_neg_clk;
end
always@(posedge sys_clk or negedge rst_n)begin
if(!rst_n)
spiclk_cnt <= {log2(DATA_WIDTH){1'b0}};
else if(clkgen_en == 1)
begin
if( (~spi_neg_clk_delay1 && spi_neg_clk))
spiclk_cnt <= spiclk_cnt + 1;
end
else
spiclk_cnt <= {log2(DATA_WIDTH){1'b0}};
end
reg [DATA_WIDTH-1:0] spi_data;
always@(posedge sys_clk or negedge rst_n )begin
if(!rst_n)
spi_data <= {DATA_WIDTH{1'b0}};
else if(idle2start_flag)
spi_data <= data_in;
else
;
end
always@(posedge spi_neg_clk or negedge rst_n )begin
if(!rst_n)
spi_mosi <= 1'b0;
else if(c_state == TRANS)
spi_mosi <= spi_data[DATA_WIDTH - trans_cnt -1] ;
else
spi_mosi <= 1'b0;
end
always@(posedge spi_clk or negedge rst_n )begin
if(!rst_n)
data_recv <= {log2(DATA_WIDTH){1'b0}};
else if(c_state == TRANS)
data_recv <= {spi_miso,data_recv[DATA_WIDTH-1:1]};
end
always@(posedge sys_clk or negedge rst_n )begin
if(!rst_n)
data_recv_vld <= 1'b0;
else if(c_state == START)
data_recv_vld <= 1'b0;
else if(c_state == END)
data_recv_vld <= 1'b1;
end
function integer log2;
input [31:0] size;
integer i;
begin
log2 = 1;
for(i=0;2**i < size;i=i+1)
log2 = i+1;
end
endfunction
endmodule
4.3 testbench
`timescale 1ns/1ps
module spi_master_tb;
reg sys_clk ;
reg rst_n ;
reg spi_start ;
reg [7:0] data_in ;
wire data_recv_vld ;
wire spi_clk ;
reg spi_miso ;
wire spi_mosi ;
wire spi_csn ;
always
#50 sys_clk = ~sys_clk;
initial begin
sys_clk = 1'b0;
rst_n = 1'b0;
#100 rst_n = 1'b1;
end
initial begin
data_in = 8'h55;
end
initial begin
spi_start = 1'b0;
#160 spi_start = 1'b1;
#100 spi_start = 1'b0;
#4500 spi_start = 1'b1;
#100 spi_start = 1'b0;
end
initial begin
spi_miso = 1;
#4760 spi_miso = 0;
end
spi_master
#(
.CLK_DIV_CNT_value(4),
.DATA_WIDTH(8)
) u0_spi_master
(
.sys_clk ( sys_clk ) ,
.rst_n ( rst_n ) ,
.spi_start ( spi_start ) ,
.data_in ( data_in ) ,
.data_recv_vld ( data_recv_vld ) ,
.spi_clk ( spi_clk ) ,
.spi_miso ( spi_miso ) ,
.spi_mosi ( spi_mosi ) ,
.spi_csn ( spi_csn )
);
endmodule
4.4 功能仿真(by vivado)
具体实现时可在相应的引脚进行上拉或者下拉约束以避免数据传完后空闲状态下的X态。
五、参考资料
1. SPI总线协议 – 第22条军规 (wangdali.net)
2. SPI协议详解(图文并茂+超详细) - 知乎 (zhihu.com)
3. SPI总线协议及SPI时序图详解 - Ady Lee - 博客园 (cnblogs.com)
4. 创客学院 经典总线协议相关视频
5. Motorala SPI Block Guide V03.06