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

本文深入介绍了SPI(Serial Peripheral Interface)协议,包括其全双工、串行同步的特性,详细阐述了波特率、工作模式(如CPOL和CPHA)以及与UART的区别。此外,还探讨了SPI的架构和逻辑设计,并提供了测试案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

《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模块间通信是在同一个时钟域下,所以还有一个时钟信号。

如下图

在这里插入图片描述

其中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每周期的时钟周期数

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

在这里插入图片描述

注意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 说明
SP01'b01'b01'b0作为Master,空闲时SCK电平为低,上升沿MISO采样,下降沿MOSI驱动
1'b1作为Slave,空闲时SCK电平为低,上升沿MOSI采样,下降沿MISO驱动
SP11'b01'b11'b0作为Master,空闲时SCK电平为低,下降沿MISO采样,上升沿MOSI驱动
1'b1作为Slave,空闲时SCK电平为低,下降沿MOSI采样,上升沿MISO驱动
SP21'b11'b01'b0作为Master,空闲时SCK电平为高,下降沿MISO采样,上升沿MOSI驱动
1'b1作为Slave,空闲时SCK电平为高,下降沿MOSI采样,上升沿MISO驱动
SP31'b11'b11'b0作为Master,空闲时SCK电平为高,上升沿MOSI采样,下降沿MISO驱动
1'b1作为Slave,空闲时SCK电平为高,上升沿MISO采样,下降沿MOSI驱动

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

在这里插入图片描述

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。

在这里插入图片描述

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

在这里插入图片描述

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}<T_{SCK}+T_{SCK2SI}-(T_{SCK2MO}+T^{MO}_{CK2Q}+T_{MO2SI}) \tag{a} 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}<T_{SCK2MO}+T^{MO}_{CK2Q}+T_{MO2SI}-T_{SCK2SI} \tag{b} 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对吧。

在这里插入图片描述

但是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接口
在这里插入图片描述

在这里插入图片描述

2.1. spi

Signal Direction Width(bits) Description
prstninput1复位信号
pclkinput1SPI的用户时钟
paddrinputPADDR_WIDTH用于访问spi内部FIFO
pwriteinput11表示写,0表示读
pselinput1是否对spi选通
penableinput1APB使能
pwdatainputPDATA_WIDTH写数据
prdatainputPDATA_WIDTH读出的数据
preadyoutput1usart准备标志
sckinout1波特率时钟,Master SPI为output、Slave SPI为input
mosiinout1SPI单bit通信端口,Master SPI为output、Slave SPI为input
misoinout1SPI单bit通信端口,Master SPI为input、Slave SPI为output
csn_iinputCHIP_SEL_NUM对Master/Slave SPI片选的控制信号
csn_ooutputCHIP_SEL_NUMMaster SPI对Slave SPI的片选控制

之后是参数描述

Parameter Units Description
BAUD_RATEbit per second设定的波特率
PCLK_FREQHZclk的时钟频率
PADDR_WIDTHbit访问SPI内部FIFO的地址位宽
PDATA_WIDTHbit写入or读出的数据位宽
CHIP_SEL_NUMbitMaster SPI可片选Slave SPI的个数
MSTRbit0表示Master SPI、1表示Slave SPI
CPOLbit表示sck空闲时的电平
CPHAbit用于确定sck对单bit输入采样沿和单bit输出的驱动沿
ASYNC_FIFO_WIDTHbit可选,异步FIFO深度

2.2. baud_clk_gen

用于产生波特率时钟sck

Signal Direction Width(bits) Description
rstninput1复位信号
clkinput1SPI的用户时钟
sckoutput1波特率时钟

之后是参数描述

Parameter Units Description
BAUD_RATEbit per second设定的波特率
CLK_FREQHZclk的时钟频率
CPOLbit表示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信息,数据交换正确

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值