路 线:
- 【verilog实战】同步FIFO的设计与功能验证(附源码)
- 【Verilog实战】异步FIFO设计和功能验证(附源码)
- 【Verilog实战】UART通信协议,半双工通信方式(附源码)
- 【Verilog实战】SPI协议接口设计和功能验证(附源码)
- 【Verilog实战】AMBA 3 APB接口设计和功能验证(附源码)
- 【Verilog实战】AMBA AHB接口设计和功能验证(附源码)
- 【Verilog实战】AMBA AXI接口设计和功能验证(附源码)
- 【Verilog实战】UART2APB bridge 设计和功能验证(附源码)
- 【Verilog实战】AHB2APB bridge 设计和功能验证(附源码)
☛ 博文的代码附Bug解决方法 or 点我直接从“我的资源”里下载
SPI 协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。和UART协议类似,都属于片外协议,但UART协议是通用的异步串行通信接口。
SPI 协议使用 3 条总线及N条片选线,3 条总线分别为 SCLK、MOSI、MISO,片选线为CS,其中3条总线是多个从设备共用的,cs是每一个从机有一条。它们的作用介绍如下:
(1)CS
全称:Chip Selection,从设备选择信号线,常称为片选信号线,也称为 NSS、SS。 SPI 协议中没有设备地址,它使用 cs 信号线来寻址,当主机要选择从设备时,把该从设备的 cs 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以SPI通讯以 cs 线置低电平为开始信号,以 cs 线被拉高作为结束信号。
(2)SCLK
全称:Serial Clock。时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,两个设备之间通讯时,通讯速率受限于低速设备。
(3)MOSI
全称:Master Output, Slave Input。主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
(4)MISO
全称:Master Input,Slave Output。主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
(1)通信模式
SPI工作模式 | CPOL | CPHA | 空闲时的SCLK电平 | 采样时刻 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 偶数边沿 |
SPI 一共有四种通讯模式,它们的主要区别是总线空闲时 SCK 的时钟状态以及数据采样时刻。工作模式由“时钟极性 CPOL”和“时钟相位 CPHA”决定。
(2)时序
根据 SCLK 在空闲状态时的电平,分为两种情况。CPOL=0时,SCLK信号线在空闲状态为低电平;CPOL=1时,空闲状态为高电平。
无论 CPOL=0,还是=1,因为配置的时钟相位 CPHA=0,在图中可以看到,采样时刻都是在 SCLK 的奇数边沿。注意当 CPOL=0 的时候,时钟的奇数边沿是上升沿,而 CPOL=1 的时候,时钟的奇数边沿是下降沿。所以 SPI 的采样时刻不是由上升/下降沿决定的。
类似地,当 CPHA=1 时,不受 CPOL 的影响,数据信号在 SCK 的偶数边沿被采样。
(3)基本通信过程
起始和结束信号
标号1处,CS 信号线(NNS)由高变低,是 SPI 通讯的起始信号。CS 信号线(NNS) 是每个从机各自独占的信号线,当从机在自己的片选信号线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。标号6处,CS 信号线(NNS) 由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。
数据有效性
SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或 LSB先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用图中的 MSB 先行模式。
图中的2~5标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK的下降沿时被采样。即在 SCK的下降沿时刻,MOSI 及 MISO的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效。
注意:SPI 每次数据的单位数不受限制
上游模块实现通过SPI接口与下游进行并行数据的交互,完成了并串互转和串行数据传输与采集的工作。
Signal Name | Width | Direction | Description |
---|---|---|---|
clk_i | 1 | input | system clock,100MHz |
rst_n_i | 1 | input | system reset signal |
cmd_in | 12 | input | 输入命令,[11]: write/read; [10:8]: addr; [7:0]: 写数据 or 无意义 |
cmd_rdy | 1 | output | 输入命令ready |
cmd_vld_i | 1 | input | 输入命令valid |
sclk | 1 | output | spi时钟,10MHz |
cs | 1 | output | spi片选 |
mosi | 1 | output | spi串行数据,master发,slave收 |
miso | 1 | input | spi串行数据,master收,slave发 |
read_vld_o | 1 | output | 读数据valid |
read_data_o | 8 | output | 读数据 |
(1)Write timing
(2)Read Timing
/*-- modified by xlinxdu, 2022/05/20
-- clk_i : 100MHz
-- sclk_o: 10MHz
-- sclk : 0(CPOL)
-- mosi : posedge sclk(CPHA)
-- receive_en:receive signal
-- cmd_in = 12bit;[11]:r/w;[10:8]:addr;[7:0]:data
*/
module spi
#(
parameter CMD_RW_WIDTH = 1,
parameter CMD_ADDR_WIDTH = 3,
parameter CMD_DATA_WIDTH = 8,
parameter CMD_IN_WIDTH = CMD_RW_WIDTH + CMD_ADDR_WIDTH +
CMD_DATA_WIDTH,
parameter CLK_CNT_WIDTH = 4,
parameter BIT_CNT_WIDTH = 4,
parameter DLY_CNT_WIDTH = 2,//delay counter
parameter DLY_CNT = 2,//delay counter of clk
parameter FDC = 10
)(
//-- system signal
input clk_i ,
input rst_n_i,
//-- mosi
input [CMD_IN_WIDTH-1:0] cmd_in ,
input cmd_vld_i ,
output reg cmd_rdy_o ,
output reg sclk_o ,
output reg cs_o ,
output reg mosi_o ,
//-- miso
input miso_i ,
output reg read_vld_o ,
output reg [CMD_DATA_WIDTH-1:0] read_data_o
);
reg [CMD_IN_WIDTH-1 :0] cmd_in_buf ;
reg start_flag ;
reg [CLK_CNT_WIDTH-1:0] clk_cnt ;
reg [BIT_CNT_WIDTH-1:0] bit_cnt ;
reg [DLY_CNT_WIDTH-1:0] dly_cnt ;
reg dly_en ;
reg [CMD_ADDR_WIDTH-1:0] rd_data_buf;
wire wr_end_flag ;
wire rd_stop_flag;
wire rd_end_flag ;
reg receive_en ;
/*-----------------------------------------------\
-- updata cmd_rdy_o --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
cmd_rdy_o <= 1'b1;
end
else if (cmd_vld_i) begin
cmd_rdy_o <= 1'b0;
end
else if(wr_end_flag || read_vld_o) begin
cmd_rdy_o <= 1'b1;
end
end
/*-----------------------------------------------\
-- updata cmd_in_buf --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
cmd_in_buf <= {(CMD_IN_WIDTH){1'b0}};
end
else if (cmd_vld_i) begin
cmd_in_buf <= cmd_in;
end
end
/*-----------------------------------------------\
-- Generate a flag to start the transfer --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
start_flag <= 1'b0;
end
else if (cmd_vld_i) begin
start_flag <= 1'b1;
end
else begin
start_flag <= 1'b0;
end
end
/*-----------------------------------------------\
-- Chip Selection --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
cs_o <= 1'b1;
end
else if (start_flag) begin
cs_o <= 1'b0;
end
else if(dly_en && (dly_cnt == (DLY_CNT-1)))begin
cs_o <= 1'b0;
end
else if(wr_end_flag || rd_stop_flag) begin
cs_o <= 1'b1;
end
end
/*-----------------------------------------------\
-- Frequency divider --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
clk_cnt <= {(CLK_CNT_WIDTH){1'b0}};
end
else if (clk_cnt == (FDC-1)) begin
clk_cnt <= {(CLK_CNT_WIDTH){1'b0}};
end
else if(!cs_o) begin
clk_cnt <= clk_cnt + 1'b1;
end
end
/*-----------------------------------------------\
-- updata sclk --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
sclk_o <= 1'b0;
end
else if(clk_cnt == (FDC/2-1)) begin
sclk_o <= 1'b1;
end
else if(clk_cnt == (FDC-1)) begin
sclk_o <= 1'b0;
end
end
/*-----------------------------------------------\
-- Generate of write end flag ,read stop flag
and read end flag --
\-----------------------------------------------*/
assign wr_end_flag = (!cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) && (bit_cnt == (CMD_IN_WIDTH-1)));
assign rd_stop_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) && (bit_cnt == (CMD_RW_WIDTH+CMD_ADDR_WIDTH-1)));
assign rd_end_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) && (bit_cnt == (CMD_DATA_WIDTH-1)));
/*-----------------------------------------------\
-- bit counter --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
bit_cnt <= {(BIT_CNT_WIDTH){1'b0}};
end
else if(wr_end_flag)begin
bit_cnt <= {(BIT_CNT_WIDTH){1'b0}};
end
else if(!receive_en && rd_stop_flag)begin
bit_cnt <= {(BIT_CNT_WIDTH){1'b0}};
end
else if(receive_en && rd_end_flag)begin
bit_cnt <= {(BIT_CNT_WIDTH){1'b0}};
end
else if (!cs_o && clk_cnt == (FDC-1)) begin
bit_cnt <= bit_cnt + 1'b1;
end
end
/*-----------------------------------------------\
-- updata MOSI --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
mosi_o <= 1'b0;
end
else if (!cs_o) begin
if(!cmd_in_buf[CMD_IN_WIDTH-1])begin
mosi_o <= cmd_in_buf[bit_cnt];
end
else begin
mosi_o <= cmd_in_buf[CMD_DATA_WIDTH + bit_cnt];
end
end
else begin
mosi_o <= 1'b0;
end
end
/*-----------------------------------------------\
-- delay enable --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
dly_en <= 1'b0;
end
else if (rd_stop_flag) begin
dly_en <= 1'b1;
end
else if(dly_cnt == (DLY_CNT-1))begin
dly_en <= 1'b0;
end
end
/*-----------------------------------------------\
-- delay counter --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
dly_cnt <= {(DLY_CNT_WIDTH){1'b0}};
end
else if (dly_en) begin
dly_cnt <= dly_cnt + 1'b1;
end
else begin
dly_cnt <= {(DLY_CNT_WIDTH){1'b0}};
end
end
/*-----------------------------------------------\
-- receive enable --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
receive_en <= 1'b0;
end
else if(dly_cnt == (DLY_CNT-1)) begin
receive_en <= 1'b1;
end
else if(rd_end_flag) begin
receive_en <= 1'b0;
end
end
/*-----------------------------------------------\
-- updata rd_data_buf of miso --
\-----------------------------------------------*/
always @ (posedge sclk_o or negedge rst_n_i) begin
if (!rst_n_i) begin
rd_data_buf <= {(CMD_DATA_WIDTH){1'b0}};
end
else if (receive_en) begin
rd_data_buf[bit_cnt] <= miso_i;
end
end
/*-----------------------------------------------\
-- updata read valid --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
read_vld_o <= 1'b0;
end
else if (rd_end_flag) begin
read_vld_o <= 1'b1;
end
else begin
read_vld_o <= 1'b0;
end
end
/*-----------------------------------------------\
-- updata read_data_o --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
read_data_o <= {(CMD_DATA_WIDTH){1'b0}};
end
else if (read_vld_o) begin
read_data_o <= rd_data_buf;
end
end
endmodule
`timescale 1ns/1ps
module tb_spi ;
reg clk_i ;
reg rst_n_i ;
reg [11:0] cmd_in ;
reg cmd_vld_i;
wire cmd_rdy_o;
wire sclk_o ;
wire cs_o ;
wire mosi_o ;
reg miso_i ;
wire read_vld_o ;
wire [7:0] read_data_o;
initial begin
clk_i = 0;
end
always #5 clk_i = ~clk_i;
initial begin
rst_n_i = 1;
cmd_vld_i = 0;
cmd_in = 12'b0011_0001_1001;//16'h318
#5 rst_n_i = 0;
#5 rst_n_i = 1;
cmd_vld_i=1;
#10 cmd_vld_i =0;
cmd_in = 12'b1010_0011_0010;//16'hA32
#1500 ;
cmd_vld_i=1;
#10 cmd_vld_i =0;
end
initial begin
miso_i = 0;
#1955 miso_i = 1;
#100 miso_i = 0;
#100 miso_i = 1;
#100 miso_i = 0;
#100 miso_i = 1;
#100 miso_i = 1;
#100 miso_i = 0;
#100 miso_i = 1;
#100;
end
spi tb_spi(
.clk_i (clk_i ),
.rst_n_i (rst_n_i ),
.cmd_in (cmd_in ),
.cmd_vld_i (cmd_vld_i ),
.cmd_rdy_o (cmd_rdy_o ),
.sclk_o (sclk_o ),
.cs_o (cs_o ),
.mosi_o (mosi_o ),
.miso_i (miso_i ),
.read_vld_o (read_vld_o ),
.read_data_o(read_data_o)
);
initial begin
#4500 $finish ;
$fsdbDumpfile("spi.fsdb");
$fsdbDumpvars ;
$fsdbDumpMDA ;
end
endmodule
bug1:在进行读操作时,mosi发送完读写指令+读地址(标号1阶段)后,又重复发送了一遍(标号2阶段),标号3阶段出现了不定态。
定位到更新mosi代码块
/*-----------------------------------------------\
-- updata MOSI --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
mosi_o <= 1'b0;
end
else if (!cs_o) begin
if(!cmd_in_buf[CMD_IN_WIDTH-1])begin
mosi_o <= cmd_in_buf[bit_cnt];
end
else begin
mosi_o <= cmd_in_buf[CMD_DATA_WIDTH + bit_cnt];
end
end
else begin
mosi_o <= 1'b0;
end
end
分析:由代码块可以知道,在读操作阶段,因为不是写就会不断执行,并且出现不定态的原因是在读操作中,在cs有效阶段(cs==0),会有两次cs拉低,第一次是发送读写控制位+读地址位,此时bit_cnt计数到3(0~3);第二次拉低是因为miso要反馈一个8位的串行数据流,此时,mosi应该不输出,逻辑错误。
/*-----------------------------------------------\
-- updata MOSI --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
mosi_o <= 1'b0;
end
else if (!cs_o) begin
if(!cmd_in_buf[CMD_IN_WIDTH-1])begin
mosi_o <= cmd_in_buf[bit_cnt];
end
else if(!receive_en)begin
mosi_o <= cmd_in_buf[CMD_DATA_WIDTH + bit_cnt];
end
end
else begin
mosi_o <= 1'b0;
end
end
更正:把读阶段的mosi触发条件改为只有非反馈数据阶段触发,即receive_en==0时触发。
bug2:在miso反馈数据阶段,cs_o片选信号被中断过,导致cs_o被中断是因为产生了一个在读操作中,发送读写控制位+读地址后才会产生的高电平信号(rd_stop_flag)。这个rd_stop_flag是用来指示在读操作中,mosi已经发送完读写控制位和读地址位。
定位到rd_stop_flag更新代码块
assign rd_stop_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) &&
(bit_cnt == (CMD_RW_WIDTH+CMD_ADDR_WIDTH-1)));
更改触发条件,只在miso非反馈数据阶段触发。
assign rd_stop_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) &&
(bit_cnt == (CMD_RW_WIDTH+CMD_ADDR_WIDTH-1)) && !receive_en);
bug3:读出数据缓存只有3位,而本来读出数据应该是8位
定位,发现是在定义的时候,用错参数
reg [CMD_ADDR_WIDTH-1:0] rd_data_buf;
更改
reg [CMD_DATA_WIDTH-1:0] rd_data_buf;
bug4:mosi输出延迟了一拍
定位到mosi更新代码块
/*-----------------------------------------------\
-- updata MOSI --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
mosi_o <= 1'b0;
end
else if (!cs_o) begin
if(!cmd_in_buf[CMD_IN_WIDTH-1])begin
mosi_o <= cmd_in_buf[bit_cnt];
end
else if(!receive_en)begin
mosi_o <= cmd_in_buf[CMD_DATA_WIDTH + bit_cnt];
end
end
else begin
mosi_o <= 1'b0;
end
end
分析:由代码块可以看到,mosi的更新是在cs片选信号拉低之后,再更新,但实际应该是mosi和cs同一时刻开始更新,逻辑错误。
更改:引入一个mosi的读写使能信号rw_en,在cmd_vld_i有效时拉高,依此作为mosi输出的触发条件,即可完成与cs同步输出。
在cmd_vld_i有效时,更新待发数据缓存cmd_in_buf,拉低cmd_rdy,并且生成一个写操作开始发送标志,打开发送使能,下一个时钟周期开始传输12bit数据,低位数据先行。在发完的前一个时钟周期生成结束写操作发送标志,并且拉高cmd_rdy信号。
读操作,在cmd_vld有效时,将待发数据写入缓存,之后开始发送读写控制位和地址位,一共4bit数据。发送结束前一周期生成rd_stop_flag信号,并开始启动延时,等待从模块反馈数据。等待时间事先已规定好,这里是两个时钟周期,并且可以配置。延时结束后,开始通过sclk采集miso反馈的数据,一共8bit,采集完成后,生成一个read_vld高电平,通过这个高电平,更新read_data_o。
仿真时序和规定时序在两个cmd_in之后不一致,功能验证不通过。
补充bug1:在dly_cnt中,多记数一个,造成不必要的信号变化,定位到delay counter代码块
/*-----------------------------------------------\
-- delay counter --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
dly_cnt <= {(DLY_CNT_WIDTH){1'b0}};
end
else if (dly_en) begin
dly_cnt <= dly_cnt + 1'b1;
end
else begin
dly_cnt <= {(DLY_CNT_WIDTH){1'b0}};
end
end
更改清零的优先级。
/*-----------------------------------------------\
-- delay counter --
\-----------------------------------------------*/
always @ (posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
dly_cnt <= {(DLY_CNT_WIDTH){1'b0}};
end
else if(dly_cnt == (DLY_CNT-1)) begin
dly_cnt <= {(DLY_CNT_WIDTH){1'b0}};
end
else if (dly_en) begin
dly_cnt <= dly_cnt + 1'b1;
end
end
补充bug2:在没有新的数据来时,即传输完两次后,没新的cmd_vld来临,可是模块miso还在工作,延时计数器也打开了,经过分析,cs_o被拉低了,cs_o受dly_en和dly_cnt共同影响,而影响dly_en的是rd_stop_flag信号。因此更改这几个标志信号,使其在cmd_rdy_o低电平时间段才可以产生,而不是全时间段不断产生。
/*-----------------------------------------------\
-- Generate of write end flag ,read stop flag
and read end flag --
\-----------------------------------------------*/
assign wr_end_flag = (!cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) && (bit_cnt == (CMD_IN_WIDTH-1)));
assign rd_stop_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) &&
(bit_cnt == (CMD_RW_WIDTH+CMD_ADDR_WIDTH-1)) && !receive_en);
assign rd_end_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (clk_cnt == (FDC-1) && (bit_cnt == (CMD_DATA_WIDTH-1)));
更改为:
/*-----------------------------------------------\
-- Generate of write end flag ,read stop flag
and read end flag --
\-----------------------------------------------*/
assign wr_end_flag = (!cmd_in_buf[CMD_IN_WIDTH-1]) && (!cmd_rdy_o) &&
(clk_cnt == (FDC-1) && (bit_cnt == (CMD_IN_WIDTH-1)));
assign rd_stop_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (!cmd_rdy_o) && (clk_cnt == (FDC-1) &&
(bit_cnt == (CMD_RW_WIDTH+CMD_ADDR_WIDTH-1)) && !receive_en);
assign rd_end_flag = (cmd_in_buf[CMD_IN_WIDTH-1]) && (!cmd_rdy_o) &&
(clk_cnt == (FDC-1) && (bit_cnt == (CMD_DATA_WIDTH-1)));
更改后,测试仿真时序和原来时序一致,功能验证通过。
作者:xlinxdu
版权:本文版权归作者所有
转载:未经作者允许,禁止转载,转载必须保留此段声明,必须在文章中给出原文连接。