串行外设接口(Serial Peripheral Interface, SPI)

目录

  • 1. 功能介绍
    • 1.1. 全双工 串行 同步
      • 波特率
      • 只能全双工
    • 1.2. 工作模式
      • Master/Slave Select Register(MSTR)
      • Clock Polarity(CPOL)与 Clock Phase(CPHA)
      • CPHA的意义
    • 1.3. SPI与UART的区别
  • 2. 架构
    • 2.1. spi
    • 2.2. baud_clk_gen
  • 3. 逻辑设计
    • 3.1. spi_master
    • 3.2. spi_slave
    • 3.3. spi
    • 3.4. spi_tb
  • 4. 测试
    • 4.1. spi_master与spi_slave1、spi_slave2的pingpong测试

《SPI Block Guide V04.01, Motorola, Inc》
SPI原理超详细讲解—值得一看
arduino教程-9. 串行外设接口(spi)
SPI通信总线原理及工作过程
《FPGA Verilog开发实战指南——基于Altera EP4CE10》


1. 功能介绍

串行外设接口(Serial Peripheral Interface, SPI)协议是Motorola提出的一种全双工、串行、同步通信协议,广泛用于 电可擦编程只读存储器(Electrically Erasable Programmable Read-Only Memory,EEPROM)、Flash、实时时钟(Real Time Clock,RTC)、数模转换器ADC、数字信号处理器DSP以及数字信号解码器上。

相比于UART,SPI的速率较快,但是缺陷与UART相同——没有握手机制确认数据是否接受,故数据可靠性存在缺陷

下面细讲

1.1. 全双工 串行 同步

可以通过介绍想一下SPI的信号啊,全双工串行说明可以同时单bit读写,所以一个SPI模块一定有两根线用于发送、接受。

而同步则说明SPI模块间通信是在同一个时钟域下,所以还有一个时钟信号。

如下图

串行外设接口(Serial Peripheral Interface, SPI)_第1张图片

其中SCK是同步时钟,MOSI(Master Output Slave Input)和MISO(Master Input Slave Output)是全双工串行总线,CSn则用于对Slave使能、低电平有效。而Master和Slave则表示主机和从机。

波特率

SPI速度比UART快就体现在波特率上,UART是在异步时钟常用的115.2kbps嘛最高可达3Mbps,而SPI由于工作在同步时钟的波特率从12.21kHZ~12.5MHZ不等。

别忘了SPI是同步串口,所以波特率是通过通过同步时钟保证的。

常用UART速率就是115200HZ,SPI则是3MHZ,I2C就是1MHZ左右。
而且UART数据位只能是5~7 bit,而SPI则可以任意
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,UART)

别忘了时钟频率/波特率 = 每bit变化的时钟周期数,对于SPI来说就是SCK每周期的时钟周期数

只能全双工

注意SPI是只能全双工工作,不存在半双工工作模式。

UART也是全双工,即发送的同时可以接收。但是呢UART也可以变成半双工,即发送的同时不接收,接收的时候可以不发送。
而SPI只能全双工的意思是,发送的同时必须接收,要么就既不发送也不接收,为什么呢?

这个与SPI的发送接收机制有关,实际上SPI的发送和接收是基于一个移位寄存器实现的。

如下图Master移位寄存器和Slave移位寄存器均在SCK时钟域下

串行外设接口(Serial Peripheral Interface, SPI)_第2张图片

工作过程是这样的:

● Master将/SS拉低开启与Slave通信

● Master和Slave各自将缓存Memory数据放入各自的移位寄存器中

● SCK每一拍将Master移位寄存器的一位通过MOSI传送至Slave移位寄存器的中,Slave移位寄存器的一位通过MISO传送至Master移位寄存器的中

● 经过8个SCK周期,实现了Master与Slave的数据交换。

本质上是Master通过/SS控制对Slave的读和写。
所以说Master只写不读,就不用采样交换之后的寄存器值。只读不写,就可以给Slave发空数据,然后采样交换之后的寄存器值。当然也可以又读又写。

1.2. 工作模式

之前的功能除了同步时钟,其他的与UART没什么区别,但是SPI可以配置成多种工作模式

Master/Slave Select Register(MSTR)

MSTR是SPI control register 1 的某一位,用于确定该SPI是Master还是Slave,也就是说SPI具有一主多从工作模式

一个Master SPI 可连接多个Slave SPI,Master通过片选信号线选择与哪几个Slave建立通信,如下图

而I2C、EMIF则是通过地址选择相应的Slave

串行外设接口(Serial Peripheral Interface, SPI)_第3张图片

注意MSTR决定了是MASTER SPI还是SLAVE SPI,如果是MASTER那么SCK、MOSI、CSn是输出,MISO是输入,如果是SLAVE那么SCK、MOSI、CSn是输入,MISO则是输出
片选不仅仅是选择了哪个Slave进行通信,片选信号拉低也是与该Slave通信开始的标志。

Clock Polarity(CPOL)与 Clock Phase(CPHA)

CPOL与CPHA都属于SPI control register 1,均为1bit,分别用于确定该SPI 作为Master时空闲(与任何Slave都没有通信)的SCK电平通信时对输入信号采样的SCK沿、对输出信号驱动的SCK沿

此处的采样是指将输入采样至移位寄存器,驱动是指将移位寄存器的值驱动给输出。
换句话说,CPHA决定的是 对输入采样并驱动给移位寄存器的SCK边沿 和 对移位寄存器采样并驱动给输出的SCK边沿

如下表

工作方式 CPOL CPHA MSTR 说明
SP0 1'b0 1'b0 1'b0 作为Master,空闲时SCK电平为低,上升沿MISO采样,下降沿MOSI驱动
1'b1 作为Slave,空闲时SCK电平为低,上升沿MOSI采样,下降沿MISO驱动
SP1 1'b0 1'b1 1'b0 作为Master,空闲时SCK电平为低,下降沿MISO采样,上升沿MOSI驱动
1'b1 作为Slave,空闲时SCK电平为低,下降沿MOSI采样,上升沿MISO驱动
SP2 1'b1 1'b0 1'b0 作为Master,空闲时SCK电平为高,下降沿MISO采样,上升沿MOSI驱动
1'b1 作为Slave,空闲时SCK电平为高,下降沿MOSI采样,上升沿MISO驱动
SP3 1'b1 1'b1 1'b0 作为Master,空闲时SCK电平为高,上升沿MOSI采样,下降沿MISO驱动
1'b1 作为Slave,空闲时SCK电平为高,上升沿MISO采样,下降沿MOSI驱动

假设SPI输入输出均为1bit,如下图所示,红色虚线表示SCK采样的边沿、绿色虚线表示SCK驱动的边沿

串行外设接口(Serial Peripheral Interface, SPI)_第4张图片

CPOL和CPHA需要MASTER SPI和SLAVE SPI统一
注意虽然输入输出都是1bit,但由于采样输入和驱动输出不在一个沿,所以移位寄存器得是2bit

实际上常用的工作模式是SP0和SP3,发送的数据则默认只是有效数据位,可配置为LSB发送或MSB发送

CPHA的意义

这里讲一下CPHA搞个采样驱动不同边沿有啥用。

如果使用的单沿采样驱动,此时采样输入和驱动输出在同一拍故移位寄存器就是1bit,那么从采样输入到移位寄存器、从移位寄存器到输出也是2拍,从速度上来说这个与CPHA模式相同。

实际上,CPHA模式的意义就是为了保证在信号中间时刻采样,以得到稳定的信号,利用SCK周期长的特点,让SCK的驱动沿和采样沿不是同一个。

我也一直在想同步时钟的话,SCK、MOSI和MISO的远距离传输必然会有信号偏斜的弊端。
UART是在baud_cnt为中间值时采样,与SPI在SCK中间沿采样异曲同工

如下图若在绿线处采样,那么就会采样到1,但由于延迟Slave那边的的MOSI.CK和MOSI.D会比Master这边的MOSI.CK和MOSI.Q晚一些。

但是Slave.MOSI.CK和Slave.MOSI.D慢的速度可能不同,会导致延迟之后红线处采样到0。但如果使用下降沿采样就会采样到1。

串行外设接口(Serial Peripheral Interface, SPI)_第5张图片

再说细一点,直接上那个熟悉的公式:

在这里插入图片描述

T s e t u p S I < T S C K + T S C K 2 S I − ( T S C K 2 M O + T C K 2 Q M O + T M O 2 S I ) (a) T^{SI}_{setup}TsetupSI<TSCK+TSCK2SI(TSCK2MO+TCK2QMO+TMO2SI)(a)

T h o l d S I < T S C K 2 M O + T C K 2 Q M O + T M O 2 S I − T S C K 2 S I (b) T^{SI}_{hold}TholdSI<TSCK2MO+TCK2QMO+TMO2SITSCK2SI(b)

首先 T s e t u p S I T^{SI}_{setup} TsetupSI一般可以满足,因为 T S C K T_{SCK} TSCK比较大,而且其他量相比于 T S C K T_{SCK} TSCK小很多。

但是 T h o l d S I T^{SI}_{hold} TholdSI则不好说,如果SCK走线时间和MOSI走线时间差距比较大的话,可能会违背。但如果对MO/CK和SI/CK在SCK不同沿的话, ( b ) (b) (b)式就会在左侧多一个 1 2 T S C K \frac{1}{2}T_{SCK} 21TSCK,就可以保证。

可以算一下验证,触发器建立时间约为5ns,保持时间约为25ns,12.5MHZ最快的SPI周期是160ns
最后一个问题,那么为什么平时的设计没怎么见到这种双沿驱动呢?因为成本,你看双沿模式是不是比单沿模式多一个触发器?

1.3. SPI与UART的区别

上述是SPI本身的特性,但如果SPI要与外部模块交互呢?Master肯定有一个外部的用户时钟clk输入,用于传入要发送的数据并根据波特率产生SCK对吧。

串行外设接口(Serial Peripheral Interface, SPI)_第6张图片

但是Slave也需要与外部模块进行数据交互,所以Slave也需要有一个外部的clk输入。这样的话一定会涉及到SCK时钟域到clk时钟域的相互同步,这样的话是不是有一点点像UART?

UART不就是串行全双工么?然后tx和rx是通过波特率保证的。
而上述的SLAVE SPI 加上单bit同步,其本质也是通过波特率保证MOSI和MISO的交互

那么这样的SPI与UART的区别在哪里?

全双工:SPI发送接收必须同时完成,而UART可以发送接收分离

跨时钟域: SPI涉及到clk域与SCK域的相互同步问题,UART涉及到txd到rxd的跨时钟域问题

波特率实现: SPI是通过clk分频成SCK保证波特率的,而UART是通过clk计数器保证SCK波特率

工作模式:SPI具备一主多从工作,UART则是点对点
SPI基于SCK不同沿采样和驱动,UART则是基于波特率计数器采样和驱动。

2. 架构

由于SPI根据不同的配置产生不同的工作模式,所以SPI实现就需要根据parameter值用generate生成不同的代码。

根据谁呢?根据MSTR判断是生成spi_master还是spi_slave,如下图

注意顶层的spi使用了APB接口
串行外设接口(Serial Peripheral Interface, SPI)_第7张图片

串行外设接口(Serial Peripheral Interface, SPI)_第8张图片

2.1. spi

Signal Direction Width(bits) Description
prstn input 1 复位信号
pclk input 1 SPI的用户时钟
paddr input PADDR_WIDTH 用于访问spi内部FIFO
pwrite input 1 1表示写,0表示读
psel input 1 是否对spi选通
penable input 1 APB使能
pwdata input PDATA_WIDTH 写数据
prdata input PDATA_WIDTH 读出的数据
pready output 1 usart准备标志
sck inout 1 波特率时钟,Master SPI为output、Slave SPI为input
mosi inout 1 SPI单bit通信端口,Master SPI为output、Slave SPI为input
miso inout 1 SPI单bit通信端口,Master SPI为input、Slave SPI为output
csn_i input CHIP_SEL_NUM 对Master/Slave SPI片选的控制信号
csn_o output CHIP_SEL_NUM Master SPI对Slave SPI的片选控制

之后是参数描述

Parameter Units Description
BAUD_RATE bit per second 设定的波特率
PCLK_FREQ HZ clk的时钟频率
PADDR_WIDTH bit 访问SPI内部FIFO的地址位宽
PDATA_WIDTH bit 写入or读出的数据位宽
CHIP_SEL_NUM bit Master SPI可片选Slave SPI的个数
MSTR bit 0表示Master SPI、1表示Slave SPI
CPOL bit 表示sck空闲时的电平
CPHA bit 用于确定sck对单bit输入采样沿和单bit输出的驱动沿
ASYNC_FIFO_WIDTH bit 可选,异步FIFO深度

2.2. baud_clk_gen

用于产生波特率时钟sck

Signal Direction Width(bits) Description
rstn input 1 复位信号
clk input 1 SPI的用户时钟
sck output 1 波特率时钟

之后是参数描述

Parameter Units Description
BAUD_RATE bit per second 设定的波特率
CLK_FREQ HZ clk的时钟频率
CPOL bit 表示sck空闲时的电平

3. 逻辑设计

3.1. spi_master

串行外设接口(Serial Peripheral Interface, SPI)逻辑设计部分 - spi_master

3.2. spi_slave

串行外设接口(Serial Peripheral Interface, SPI)逻辑设计部分 - spi_slave

3.3. spi

整个顶层模块如下

module spi#(
	parameter	BAUD_RATE			=	12500000,
	parameter	PCLK_FREQ			=	50000000,
	parameter	PADDR_WIDTH			=	32,
	parameter	PDATA_WIDTH			=	32,
	parameter	CHIP_SEL_NUM		=	3,
	parameter	MSTR				=	0,
	parameter	CPOL				=	0,
	parameter	CPHA				=	0,
	parameter	ASYNC_FIFO_WIDTH	=	4096
	)(
		input						prstn,
		input						pclk,
		
		input	[PADDR_WIDTH-1:0]	paddr,
		input						pwrite,
		input						psel,
		input						penable,
		input	[PDATA_WIDTH-1:0]	pwdata,
		output	[PDATA_WIDTH-1:0]	prdata,
		output						pready,
		
		inout						sck,
		inout						mosi,
		inout						miso,
		input	[CHIP_SEL_NUM-1:0]	csn_i,
		output	[CHIP_SEL_NUM-1:0]	csn_o
	);

localparam TX_DATA_ADDR = 32'h0000_0000_0000_1000;
localparam RX_DATA_ADDR = TX_DATA_ADDR + 32'h4;
localparam CSN_I_ADDR = TX_DATA_ADDR + 32'h8;

reg		[PDATA_WIDTH-1:0]		tx_data;	
reg 							tx_data_val; 
wire 							tx_fifo_full;	
reg 							rx_req;		
wire	[PDATA_WIDTH-1:0]		rx_data;		
wire 							rx_data_val;	
wire 							rx_fifo_empty;
reg		[PDATA_WIDTH-1:0]		prdata_r;
reg 							pready_r;

	
always@(*) begin
	if(psel && penable && pwrite && paddr == TX_DATA_ADDR) begin
		tx_data = pwdata;
		tx_data_val = 1'b1;
	end
	else begin
		tx_data = 'd0;
		tx_data_val = 1'b0;
	end
end

always@(*) begin
	if(psel && penable && !pwrite && paddr == RX_DATA_ADDR) 
		prdata_r = rx_data;
	else if(psel && penable && !pwrite && paddr == TX_DATA_ADDR)
		prdata_r = tx_data;
	else if(psel && penable && !pwrite && paddr == CSN_I_ADDR)
		prdata_r = csn_i;
	else
		prdata_r = 'd0;
end

assign prdata = prdata_r;

always@(posedge pclk or negedge prstn) begin
	if(!prstn)
		rx_req <= 1'b0;
	else if(psel && !pwrite && paddr == RX_DATA_ADDR) begin
		if(!penable)
			rx_req <= 1'b1;
		else if(rx_fifo_empty)
			rx_req <= 1'b1;
		else
			rx_req <= 1'b0;
	end
	else
		rx_req <= 1'b0;
end

always@(*) begin
	if(psel && penable && pwrite && paddr == TX_DATA_ADDR) 
		pready_r = !tx_fifo_full;
	else if(psel && penable && !pwrite && paddr == RX_DATA_ADDR) 
		pready_r = rx_data_val;
	else if(psel && penable && pwrite && paddr == CSN_I_ADDR) 
		pready_r = 1'b1;
	else
		pready_r = 1'b0;
end

assign pready = pready_r;


generate if(!MSTR) begin

reg		[CHIP_SEL_NUM-1:0]		csn_i_r;

always@(posedge pclk or negedge prstn) begin
	if(!prstn)
		csn_i_r <= {CHIP_SEL_NUM{1'b1}};
	else if(psel && (!penable) && pwrite && paddr == CSN_I_ADDR) 
		csn_i_r <= pwdata;
end

baud_clk_gen#(
	.BAUD_RATE				(BAUD_RATE		),
	.CPOL					(CPOL	  		),
	.CLK_FREQ				(PCLK_FREQ 		)
	)u_baud_clk_gen(
		.rstn				(prstn			),
		.clk				(pclk			),
		.sck				(sck			)
	);
	
spi_master#(
	.CHIP_SEL_NUM			(CHIP_SEL_NUM		),
	.DATA_WIDTH				(PDATA_WIDTH		),
	.ASYNC_FIFO_WIDTH		(ASYNC_FIFO_WIDTH	),
	.CPOL					(CPOL				),
	.CPHA					(CPHA				)
	)u_spi_master(
		.rstn				(prstn				),
		.clk				(pclk				),

		.tx_data			(tx_data			),
		.tx_data_val		(tx_data_val		),
		.tx_fifo_full		(tx_fifo_full		),
		.rx_req				(rx_req				),
		.rx_data			(rx_data			),
		.rx_data_val		(rx_data_val		),
		.rx_fifo_empty		(rx_fifo_empty		),

		.sck				(sck				),
		.mosi				(mosi				),
		.miso				(miso				),
		.csn_i				(csn_i_r			),
		.csn_o				(csn_o				)
	);
	
end
else begin

spi_slave#(
	.CHIP_SEL_NUM			(CHIP_SEL_NUM		),
	.DATA_WIDTH				(PDATA_WIDTH		),
	.ASYNC_FIFO_WIDTH		(ASYNC_FIFO_WIDTH	),
	.CPOL					(CPOL				),
	.CPHA					(CPHA				)
	)u_spi_slave(
		.rstn				(prstn				),
		.clk				(pclk				),

		.tx_data			(tx_data			),
		.tx_data_val		(tx_data_val		),
		.tx_fifo_full		(tx_fifo_full		),
		.rx_req				(rx_req				),
		.rx_data			(rx_data			),
		.rx_data_val		(rx_data_val		),
		.rx_fifo_empty		(rx_fifo_empty		),

		.sck				(sck				),
		.mosi				(mosi				),
		.miso				(miso				),
		.csn				(csn_i[0]			)
	);


end

endgenerate

endmodule

3.4. spi_tb

`timescale 1ns/1ps

module spi_tb();

parameter BAUD_RATE			=	3000000;	
parameter PCLK_FREQ			=	50000000;			
parameter CHIP_SEL_NUM		=	2;	
parameter PADDR_WIDTH		=	32;	
parameter PDATA_WIDTH		=	32;								
parameter CPOL				=	0;				
parameter CPHA				=	0;			
parameter ASYNC_FIFO_WIDTH	=	4096;

parameter SPI_TX_DATA_ADDR = 32'h0000_0000_0000_1000;
parameter SPI_RX_DATA_ADDR = SPI_TX_DATA_ADDR + 32'h4;
parameter SPI_MASTER_CSN_I_ADDR = SPI_TX_DATA_ADDR + 32'h8;


logic 								prstn;			
logic 								pclk;
			
logic		[PADDR_WIDTH-1:0]		paddr_mst;
logic								pwrite_mst;
logic								psel_mst;
logic								penable_mst;
logic		[PDATA_WIDTH-1:0]		pwdata_mst;
logic		[PDATA_WIDTH-1:0]		prdata_mst;
logic								pready_mst;
	
logic		[PADDR_WIDTH-1:0]		paddr_slv1;
logic								pwrite_slv1;
logic								psel_slv1;
logic								penable_slv1;
logic		[PDATA_WIDTH-1:0]		pwdata_slv1;
logic		[PDATA_WIDTH-1:0]		prdata_slv1;
logic								pready_slv1;

logic		[PADDR_WIDTH-1:0]		paddr_slv2;
logic								pwrite_slv2;
logic								psel_slv2;
logic								penable_slv2;
logic		[PDATA_WIDTH-1:0]		pwdata_slv2;
logic		[PDATA_WIDTH-1:0]		prdata_slv2;
logic								pready_slv2;
		
wire 								sck;			
wire 								mosi;
wire 								miso;
logic 								miso_r;			
wire 								miso_slv1;	
wire 								miso_slv2;					
logic 		[CHIP_SEL_NUM-1:0]		csn_o;			


initial begin
	pclk = 0;
	forever #10 pclk = !pclk;					//50MHZ
end

initial begin
	prstn = 1;
	#50 prstn 	= 0;
	#50 prstn 	= 1;
end

initial begin
	paddr_mst = 'd0;
	pwrite_mst = 1'b0;
	psel_mst = 1'b0;
	penable_mst = 1'b0;
	pwdata_mst = 'd0;
	
	paddr_slv1 = 'd0;
	pwrite_slv1 = 1'b0;
	psel_slv1 = 1'b0;
	penable_slv1 = 1'b0;
	pwdata_slv1 = 'd0;
	
	paddr_slv2 = 'd0;
	pwrite_slv2 = 1'b0;
	psel_slv2 = 1'b0;
	penable_slv2 = 1'b0;
	pwdata_slv2 = 'd0;
	
	#300;
	spi_pingpong_test();
end


task spi_pingpong_test();
	fork
		begin
			spi_master_sequence(SPI_TX_DATA_ADDR,1'b1,32'd1);
			spi_master_sequence(SPI_TX_DATA_ADDR,1'b1,32'd2);
		end
		begin
			spi_slave1_sequence(SPI_TX_DATA_ADDR,1'b1,32'd5);
			spi_slave1_sequence(SPI_TX_DATA_ADDR,1'b1,32'd6);
		end
	join
	
	@(posedge pclk);
	#1;
	spi_master_sequence(SPI_MASTER_CSN_I_ADDR,1'b1,32'b10);
	fork
		begin
			spi_master_sequence(SPI_RX_DATA_ADDR,1'b0,32'd0);
			spi_master_sequence(SPI_RX_DATA_ADDR,1'b0,32'd0);
		end
		begin
			spi_slave1_sequence(SPI_RX_DATA_ADDR,1'b0,32'd0);
			spi_slave1_sequence(SPI_RX_DATA_ADDR,1'b0,32'd0);
		end
	join
	@(posedge sck);
	@(posedge sck);
	@(posedge pclk);
	#1;
	spi_master_sequence(SPI_MASTER_CSN_I_ADDR,1'b1,32'b11);
	
	fork
		begin
			spi_master_sequence(SPI_TX_DATA_ADDR,1'b1,32'd3);
			spi_master_sequence(SPI_TX_DATA_ADDR,1'b1,32'd4);
		end
		begin
			spi_slave2_sequence(SPI_TX_DATA_ADDR,1'b1,32'd7);
			spi_slave2_sequence(SPI_TX_DATA_ADDR,1'b1,32'd8);
		end
	join
	
	@(posedge pclk);
	#1;
	spi_master_sequence(SPI_MASTER_CSN_I_ADDR,1'b1,32'b01);
	fork
		begin
			spi_master_sequence(SPI_RX_DATA_ADDR,1'b0,32'd0);
			spi_master_sequence(SPI_RX_DATA_ADDR,1'b0,32'd0);
		end
		begin
			spi_slave2_sequence(SPI_RX_DATA_ADDR,1'b0,32'd7);
			spi_slave2_sequence(SPI_RX_DATA_ADDR,1'b0,32'd8);
		end
	join
	@(posedge pclk);
	#1;
	spi_master_sequence(SPI_MASTER_CSN_I_ADDR,1'b1,32'b11);
	
	
endtask

task spi_master_sequence(input bit [PADDR_WIDTH-1:0] addr,input bit write,input bit [PDATA_WIDTH-1:0] wdata);
	@(posedge pclk);
	#1;
	paddr_mst = addr;
	pwrite_mst = write;
	psel_mst = 1'b1;
	penable_mst = 1'b0;
	if(pwrite_mst)
		pwdata_mst = wdata;
	@(posedge pclk);
	#1;
	penable_mst = 1'b1;
	forever begin
		@(posedge pclk);
		if(psel_mst && penable_mst && pready_mst) begin
			case(paddr_mst)
				SPI_TX_DATA_ADDR:
					if(pwrite_mst)
						$display("spi master has successfully written data %d into transfer FIFO",pwdata_mst);
					else
						$display("spi master has written %d last time",pwdata_mst);
				SPI_RX_DATA_ADDR:
					if(pwrite_mst)
						$display("spi master: this read-only register can not be written.");
					else
						$display("spi master has successfully read data %d from receive FIFO",prdata_mst);
				SPI_MASTER_CSN_I_ADDR:
					if(pwrite_mst)
						$display("spi master has successfully selected %b",pwdata_mst[CHIP_SEL_NUM-1:0]);
					else
						$display("spi master has select %b last time",pwdata_mst[CHIP_SEL_NUM-1:0]);
				default:
					$display("spi master: paddr is set wrong");
			endcase
			#1;
			psel_mst = 1'b0;
			break;
		end
	end
endtask

task spi_slave1_sequence(input bit [PADDR_WIDTH-1:0] addr,input bit write,input bit [PDATA_WIDTH-1:0] wdata);
	@(posedge pclk);
	#1;
	paddr_slv1 = addr;
	pwrite_slv1 = write;
	psel_slv1 = 1'b1;
	penable_slv1 = 1'b0;
	if(pwrite_slv1)
		pwdata_slv1 = wdata;
	@(posedge pclk);
	#1;
	penable_slv1 = 1'b1;
	forever begin
		@(posedge pclk);
		if(psel_slv1 && penable_slv1 && pready_slv1) begin
			case(paddr_slv1)
				SPI_TX_DATA_ADDR:
					if(pwrite_slv1)
						$display("spi slave1 has successfully written data %d into transfer FIFO",pwdata_slv1);
					else
						$display("spi slave1 has written %d last time",pwdata_slv1);
				SPI_RX_DATA_ADDR:
					if(pwrite_slv1)
						$display("spi slave1: this read-only register can not be written.");
					else
						$display("spi slave1 has successfully read data %d from receive FIFO",prdata_slv1);
				default:
					$display("spi slave1: paddr is set wrong");
			endcase
			#1;
			psel_slv1 = 1'b0;
			break;
		end
	end
endtask

task spi_slave2_sequence(input bit [PADDR_WIDTH-1:0] addr,input bit write,input bit [PDATA_WIDTH-1:0] wdata);
	@(posedge pclk);
	#1;
	paddr_slv2 = addr;
	pwrite_slv2 = write;
	psel_slv2 = 1'b1;
	penable_slv2 = 1'b0;
	if(pwrite_slv2)
		pwdata_slv2 = wdata;
	@(posedge pclk);
	#1;
	penable_slv2 = 1'b1;
	forever begin
		@(posedge pclk);
		if(psel_slv2 && penable_slv2 && pready_slv2) begin
			case(paddr_slv2)
				SPI_TX_DATA_ADDR:
					if(pwrite_slv2)
						$display("spi slave1 has successfully written data %d into transfer FIFO",pwdata_slv2);
					else
						$display("spi slave1 has written %d last time",pwdata_slv2);
				SPI_RX_DATA_ADDR:
					if(pwrite_slv2)
						$display("spi slave1: this read-only register can not be written.");
					else
						$display("spi slave1 has successfully read data %d from receive FIFO",prdata_slv2);
				default:
					$display("spi slave1: paddr is set wrong");
			endcase
			#1;
			psel_slv2 = 1'b0;
			break;
		end
	end
endtask


spi#(
	.BAUD_RATE			(BAUD_RATE			),
	.PCLK_FREQ			(PCLK_FREQ			),
	.CHIP_SEL_NUM		(CHIP_SEL_NUM		),
	.PADDR_WIDTH		(PADDR_WIDTH		),
	.PDATA_WIDTH		(PDATA_WIDTH		),
	.MSTR				(0					),
	.CPOL				(CPOL				),
	.CPHA				(CPHA				),
	.ASYNC_FIFO_WIDTH	(ASYNC_FIFO_WIDTH	)
	)u_spi_master(
		.prstn				(prstn					),
		.pclk				(pclk					),
		.paddr				(paddr_mst				),
		.pwrite				(pwrite_mst				),
		.psel				(psel_mst				),
		.penable			(penable_mst			),
		.pwdata				(pwdata_mst				),
		.prdata				(prdata_mst				),
		.pready				(pready_mst				),
		
		.sck				(sck					),
		.mosi				(mosi					),
		.miso				(miso					),
		.csn_o				(csn_o					),
		.csn_i				(						)
	);


always@(*) begin
	case(csn_o)
		2'b10:
			miso_r = miso_slv1;
		2'b01:
			miso_r = miso_slv2;
		default: miso_r = 1'b1;
	endcase
end

assign miso = miso_r;

spi#(
	.BAUD_RATE			(BAUD_RATE			),
	.PCLK_FREQ			(PCLK_FREQ			),
	.CHIP_SEL_NUM		(CHIP_SEL_NUM		),
	.PADDR_WIDTH		(PADDR_WIDTH		),
	.PDATA_WIDTH		(PDATA_WIDTH		),
	.MSTR				(1					),
	.CPOL				(CPOL				),
	.CPHA				(CPHA				),
	.ASYNC_FIFO_WIDTH	(ASYNC_FIFO_WIDTH	)
	)u_spi_slave1(
		.prstn				(prstn					),
		.pclk				(pclk					),
		.paddr				(paddr_slv1				),
		.pwrite				(pwrite_slv1			),
		.psel				(psel_slv1				),
		.penable			(penable_slv1			),
		.pwdata				(pwdata_slv1			),
		.prdata				(prdata_slv1			),
		.pready				(pready_slv1			),
		
		.sck				(sck					),
		.mosi				(mosi					),
		.miso				(miso_slv1				),
		.csn_o				(						),
		.csn_i				(csn_o[0]				)
	);
	

spi#(
	.BAUD_RATE			(BAUD_RATE			),
	.PCLK_FREQ			(PCLK_FREQ			),
	.CHIP_SEL_NUM		(CHIP_SEL_NUM		),
	.PADDR_WIDTH		(PADDR_WIDTH		),
	.PDATA_WIDTH		(PDATA_WIDTH		),
	.MSTR				(1					),
	.CPOL				(CPOL				),
	.CPHA				(CPHA				),
	.ASYNC_FIFO_WIDTH	(ASYNC_FIFO_WIDTH	)
	)u_spi_slave2(
		.prstn				(prstn					),
		.pclk				(pclk					),
		.paddr				(paddr_slv2				),
		.pwrite				(pwrite_slv2			),
		.psel				(psel_slv2				),
		.penable			(penable_slv2			),
		.pwdata				(pwdata_slv2			),
		.prdata				(prdata_slv2			),
		.pready				(pready_slv2			),
		
		.sck				(sck					),
		.mosi				(mosi					),
		.miso				(miso_slv2				),
		.csn_o				(						),
		.csn_i				(csn_o[1]				)
	);


endmodule

4. 测试

4.1. spi_master与spi_slave1、spi_slave2的pingpong测试

测试的内容如spi_tb代码所写,先是将spi_master与spi_slave1进行1v1通信,然后是spi_master与spi_slave2进行1v1通信

参数设定如下:

parameter BAUD_RATE			=	3000000;			//SCK频率	
parameter PCLK_FREQ			=	50000000;			//用户时钟
parameter CHIP_SEL_NUM		=	2;					//spi master可片选的spi slave数量
parameter PADDR_WIDTH		=	32;					//地址位宽
parameter PDATA_WIDTH		=	32;					//数据位宽
parameter CPOL				=	0;					//CPOL
parameter CPHA				=	0;					//CPHA
parameter ASYNC_FIFO_WIDTH	=	4096;				//异步FIFO深度

parameter SPI_TX_DATA_ADDR = 32'h0000_0000_0000_1000;			//spi内部发送FIFO的访问地址
parameter SPI_RX_DATA_ADDR = SPI_TX_DATA_ADDR + 32'h4;			//spi内部接收FIFO的访问地址
parameter SPI_MASTER_CSN_I_ADDR = SPI_TX_DATA_ADDR + 32'h8;		//spi_master内部片选寄存器csn_i的访问地址

首先给出transcript信息,数据交换正确

串行外设接口(Serial Peripheral Interface, SPI)_第9张图片

再看部分信号波形,在复位刚刚结束时,先将要传输的数据分别写入各自的FIFO内,spi master和spi slave不断去读,直到读出要发送的数据并将其写入各自的移位寄存器,在此期间片选信号为高。

之后片选信号拉低,表示开始传输,spi master直接从IDLE进入TRANS状态。由于CPOL和CPHA均为0,上升沿采样、下降沿驱动,故bit_cnt下降沿减1、移位寄存器在sck上升沿和下降沿均会变化。

串行外设接口(Serial Peripheral Interface, SPI)_第10张图片

当bit_cnt为0时传输结束,虽然此时csn_i依然为2’b10,但csn_o自动置2’b11。spi master先进入FIFO_WRITE状态将获得的数据写入rx_async_fifo,再在IDLE状态从tx_async_fifo读出新数据。

串行外设接口(Serial Peripheral Interface, SPI)_第11张图片

spi slave也是先写再读,整个过程与spi master类似不再赘述,可自行仿真。

CPOL和CPHA其他取值可用相同的方法进行测试。

你可能感兴趣的:(标准总线接口协议,数字IC,IC验证,fpga开发)