《SPI Block Guide V04.01, Motorola, Inc》
SPI原理超详细讲解—值得一看
arduino教程-9. 串行外设接口(spi)
SPI通信总线原理及工作过程
《FPGA Verilog开发实战指南——基于Altera EP4CE10》
串行外设接口(Serial Peripheral Interface, SPI)协议是Motorola提出的一种全双工、串行、同步通信协议,广泛用于 电可擦编程只读存储器(Electrically Erasable Programmable Read-Only Memory,EEPROM)、Flash、实时时钟(Real Time Clock,RTC)、数模转换器ADC、数字信号处理器DSP以及数字信号解码器上。
相比于UART,SPI的速率较快,但是缺陷与UART相同——没有握手机制确认数据是否接受,故数据可靠性存在缺陷
下面细讲
可以通过介绍想一下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每周期的时钟周期数
注意SPI是只能全双工工作,不存在半双工工作模式。
UART也是全双工,即发送的同时可以接收。但是呢UART也可以变成半双工,即发送的同时不接收,接收的时候可以不发送。
而SPI只能全双工的意思是,发送的同时必须接收,要么就既不发送也不接收,为什么呢?
这个与SPI的发送接收机制有关,实际上SPI的发送和接收是基于一个移位寄存器实现的。
如下图Master移位寄存器和Slave移位寄存器均在SCK时钟域下
工作过程是这样的:
● Master将/SS拉低开启与Slave通信
● Master和Slave各自将缓存Memory数据放入各自的移位寄存器中
● SCK每一拍将Master移位寄存器的一位通过MOSI传送至Slave移位寄存器的中,Slave移位寄存器的一位通过MISO传送至Master移位寄存器的中
● 经过8个SCK周期,实现了Master与Slave的数据交换。
本质上是Master通过/SS控制对Slave的读和写。
所以说Master只写不读,就不用采样交换之后的寄存器值。只读不写,就可以给Slave发空数据,然后采样交换之后的寄存器值。当然也可以又读又写。
之前的功能除了同步时钟,其他的与UART没什么区别,但是SPI可以配置成多种工作模式
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通信开始的标志。
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驱动的边沿
CPOL和CPHA需要MASTER SPI和SLAVE SPI统一
注意虽然输入输出都是1bit,但由于采样输入和驱动输出不在一个沿,所以移位寄存器得是2bit
实际上常用的工作模式是SP0和SP3,发送的数据则默认只是有效数据位,可配置为LSB发送或MSB发送
这里讲一下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 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 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
最后一个问题,那么为什么平时的设计没怎么见到这种双沿驱动呢?因为成本,你看双沿模式是不是比单沿模式多一个触发器?
上述是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则是基于波特率计数器采样和驱动。
由于SPI根据不同的配置产生不同的工作模式,所以SPI实现就需要根据parameter值用generate生成不同的代码。
根据谁呢?根据MSTR判断是生成spi_master还是spi_slave,如下图
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深度 |
用于产生波特率时钟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空闲时的电平 |
串行外设接口(Serial Peripheral Interface, SPI)逻辑设计部分 - spi_master
串行外设接口(Serial Peripheral Interface, SPI)逻辑设计部分 - spi_slave
整个顶层模块如下
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
`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
测试的内容如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其他取值可用相同的方法进行测试。