Xilinx-Verilog-学习笔记(16):SPI总线
一、SPI总线
1、什么是SPI总线?
SPI是串行外设接口(Serial Peripheral Interface),是一种高速的、全双工、同步的通信总线。
由于没有应答机制确认是否接收到数据,因此SPI总线与IIC总线相比,在数据可靠性上有一定的缺陷。
2、时序特点
(1)数据线介绍
SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是SDI(数据输入)、SDO(数据输出)、SCLK(时钟)、CS(片选)。
SDI是主机(FPGA一般做主机)向从机发送数据的线;SDO为从机向主机发送的数据的线。一般SDI发送的是一些控制指令,SDO发送的是一些反馈信息; SCLK为主设备提供的时钟信号;CS为片选信号,选择信号是否有效。
对于三线型,数据线为SDIO,是一种双向的。
(2)写时序
这里暂不考虑控制位和数据位。可以看出,写数据是按照spi的时钟节拍完成的,在时钟上升沿时写入数据。
(3)读时序
同样在时钟上升沿时,读取数据。
这里在读写时,尽量使得时钟的上升沿与数据的中心位置对齐,目的是保证对数据采样的稳定性。
二、verilog代码设计
1、任务要求
(1)设计1Mhz的SPI时钟频率用于SPI总线。
(2)设计位宽为16位、深度为32位的内存,用于提供数据。
(3)通过SPI总线读取内存中的32个数据,并进行分析。
系统上电首先为IDLE状态,然后来了work_en标志,使得系统进入到WAIT状态,在该状态下等待系统准备好后发送wait_end标志,进入到READ_MEM状态,选择需要读取的内存地址,并读取1个数据到移位寄存器中。读取完成后进入到WRITE_REG状态,将移位寄存器中的数据一位一位地移入到SPI总线的SDI数据线上,发送出去。发送完一个数据,则通过shift_end标志进入到WAIT状态,等待下一次读取和发送,直到完成最后一个数据,发送data_end标志,表示整体读写任务结束,整个系统进入到STOP状态。
2、design文件分析
最终实现的效果是,当spi_csn片选信号拉低后,即进入到W_REG写寄存器状态。那么按照spi_clk时钟节拍,上升沿时对shift_buf移位寄存器中的值进行采样(此时在中间位置,采样最可靠),每次将移位寄存器中的最高位放在SPI_SDI数据线上发送出去。从图中可以看出,在写寄存器状态下,共经历了16个时钟周期,即16个上升沿,因此向总线发送了16位的数据,恰好是一个数据的位宽。发送完成后,片选信号拉高,结束发送。
综合来看,一共经历了32次WAIT、R_MEM和W_REG状态,即读取地址32次。正好内存存储了32个16位宽的数据,读取完成后,整个状态进入到STOP状态。
下面详细分析具体每一部分如何实现的:
(1)如何得到1Mhz的SPI总线时钟
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
div_cnt <= 5'd0;
else if( div_cnt == H_DIV_CYC )
div_cnt <= 'd0;
else
div_cnt <= div_cnt + 1'b1;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
clk_p <= 1'b0;
else if(div_cnt == H_DIV_CYC)
clk_p <= ~clk_p;
assign clk_n=~clk_p;
用于进行分频,通过分频计数器div_cnt的方法,数25个时钟周期翻转一次,则50个时钟周期完成一个分频周期。最终得到的clk_p频率为原系统时钟的五十分之一,系统时钟为50Mhz,则分频后的时钟周期为1Mhz。clk_n为clk_p取反。
clk_n用于采集数据,clk_p用于生成数据
(2)如何将时钟clk_p作为SPI总线时钟
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
pose_flag <= 1'b0;
else if(clk_p == 1'b0 && div_cnt == H_DIV_CYC)
pose_flag <= 1'b1;
else pose_flag <= 1'b0;
这里pose_flag表示clk_p上升沿到来的时候,是一个标志位。因此要在clk_p为低的时候进行判断,且计数器div_cnt应该记到24时,下一个计数器变为0的时候刚好是clk_p为高的那个上升沿。每次判断pose_flag则相当于always @(posedge clk_p) 的效果。
注:分频时钟不允许做寄存器的触发时钟,也就是不能写在always块的触发列表中。
(3)WAIT状态
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
wait_cnt <= 'd0;
else if(state == WAIT && pose_flag == 1'b1)
wait_cnt <= wait_cnt + 1'b1;
else if(state != WAIT)
wait_cnt <= 4'd0;
WAIT状态下,通过wait_cnt计数器,按照clk_p时钟节拍记8拍,然后再进入到READ_MEM状态。(推测此状态可能用来延时一小段时间等待系统准备好)用于分割两次命令中间等待的时间。
(4)READ_MEM状态
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
r_addr <= 'd0;
else if(state == R_MEM)
r_addr <= r_addr + 1'b1;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
data_end <= 1'b0;
else if(state == R_MEM && (&r_addr) == 1'b1)//等效于r_addr == 5'd31
data_end <= 1'b1;
READ_MEM状态是用来读取内存地址中数据的,在每次进入到READ_MEM状态下,地址都向后移动一个以获取新的16位的数据,并将地址中的数据r_data赋给shift_reg移位寄存器。r_addr计数器用于记录地址信息的,当地址到达第32个时,系统状态进入到STOP状态,标志读取任务完成。
(5)W_REG状态
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
shift_cnt <= 'd0;
else if(state == W_REG && pose_flag == 1'b1)
shift_cnt <= shift_cnt + 1'b1;
else if( state != W_REG)
shift_cnt <= 4'd0;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
shift_buf<='d0;
else if(state == R_MEM)
shift_buf <= r_data;
else if(state == W_REG && pose_flag == 1'b1)
shift_buf <= {shift_buf[14:0],1'b1};
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
sdi<=1'b0;
else if(state == W_REG)
sdi<=shift_buf[15];
else
sdi <= 1'b0;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
csn <= 1'b1;
else if(state == W_REG)
csn <= 1'b0;
else
csn <= 1'b1;
WRITE_REG状态是用来把移位寄存器的数据通过SDI数据线发送出去,具体发送方法是按照spi_clk的时钟节拍,一位一位地将移位寄存器中的数据放到SDI线上。每次取移位寄存器的最高位,每次时钟时钟上升沿到来时,移位寄存器都将高位移出,低位用1来填充。最终用所有1填充满移位寄存器后,表示移位寄存器中的16位数据完全发送出去。
(6)数据从哪来,到哪去?
ram_16x32_sr ram_16x32_sr_inst (
.address ( r_addr ),//读地址
.clock ( sclk ),
.data ( 16'd0 ),//写数据
.wren ( wren ),//写使能高有效,读使能低有效
.q ( r_data )//读数据
);
通过例化一个位宽为16,深度为32的RAM(内部已赋值),将RAM的读端连接到r_data上以读取RAM中的数据,RAM的地址有r_addr控制,时钟为sclk控制。
task rec_spi();
integer i,j;
begin
for (i=0;i<32;i=i+1)begin
for(j=0;j<16;j=j+1)begin
@(posedge spi_clk);
shift_buf = {shift_buf[14:0],spi_sdi};
if(j==15 && shift_buf == send_mem[i])
$display("ok data index is %d rec_d=%d send_d=%d",i,shift_buf,send_mem[i]);
else if(j== 15)
$display("error");
end
end
end
endtask
endmodule
在testbench中,通过将数据线上的数据一位一位地存到shift_buf中,将shift_buf中的数据与mem保存的mif文件对比,以确认是否成功读取数据。
3、design、testbench、do文件的完整版
design文件
module spi_ctrl(
input wire sclk,//系统时钟50Mhz
input wire rst_n,
input wire work_en,//触发配置操作的使能
output reg conf_end,
output wire spi_clk,//50-60mhz
output wire spi_sdi,
output wire spi_csn,
input wire spi_sdo//读输入管脚不进行编程
);
parameter IDLE = 5'b0_0001;
parameter WAIT = 5'b0_0010;
parameter R_MEM= 5'b0_0100;
parameter W_REG= 5'b0_1000;
parameter STOP = 5'b1_0000;
parameter H_DIV_CYC = 5'd25-1;
reg [4:0] state;//状态机的寄存器变量,编码方式采用独热码
reg [4:0] div_cnt;
reg clk_p=1'b0;
wire clk_n;
reg pose_flag;
reg [3:0] wait_cnt;
reg [3:0] shift_cnt;
reg [4:0] r_addr;
wire [15:0] r_data;
wire wren;
reg [15:0] shift_buf;
reg data_end;
reg sdi;
reg csn;
reg tck;
//分频计数器
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
div_cnt <= 5'd0;
else if( div_cnt == H_DIV_CYC )
div_cnt <= 'd0;
else
div_cnt <= div_cnt + 1'b1;
//分频时钟不允许做寄存器的触发时钟,也就是不能写在always块的触发列表中
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
clk_p <= 1'b0;
else if(div_cnt == H_DIV_CYC)
clk_p <= ~clk_p;
assign clk_n=~clk_p;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
pose_flag <= 1'b0;
else if(clk_p == 1'b0 && div_cnt == H_DIV_CYC)
pose_flag <= 1'b1;
else pose_flag <= 1'b0;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
wait_cnt <= 'd0;
else if(state == WAIT && pose_flag == 1'b1)
wait_cnt <= wait_cnt + 1'b1;
else if(state != WAIT)
wait_cnt <= 4'd0;
//fsm
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
state <= IDLE;
else case (state)
IDLE :if(work_en == 1'b1)
state <= WAIT;
WAIT :if(wait_cnt[3] == 1'b1)
state <= R_MEM;
R_MEM: state <=W_REG;
W_REG: if(shift_cnt == 4'd15 && pose_flag == 1'b1 && data_end != 1'b1)
state <= WAIT;
else if(shift_cnt == 4'd15 && pose_flag == 1'b1 && data_end == 1'b1)
state <= STOP;
STOP: state <= STOP;
default : state <= IDLE;
endcase
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
shift_cnt <= 'd0;
else if(state == W_REG && pose_flag == 1'b1)
shift_cnt <= shift_cnt + 1'b1;
else if( state != W_REG)
shift_cnt <= 4'd0;
//读mem的地址产生
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
r_addr <= 'd0;
else if(state == R_MEM)
r_addr <= r_addr + 1'b1;
//data_end 最后一个需要移位数据
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
data_end <= 1'b0;
else if(state == R_MEM && (&r_addr) == 1'b1)//等效于r_addr == 5'd31
data_end <= 1'b1;
assign wren =1'b0;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
shift_buf<='d0;
else if(state == R_MEM)
shift_buf <= r_data;
else if(state == W_REG && pose_flag == 1'b1)
shift_buf <= {shift_buf[14:0],1'b1};
//数据的输出
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
sdi<=1'b0;
else if(state == W_REG)
sdi<=shift_buf[15];
else
sdi <= 1'b0;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
csn <= 1'b1;
else if(state == W_REG)
csn <= 1'b0;
else
csn <= 1'b1;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
tck<=1'b0;
else if(state == W_REG )
tck<=clk_n;
else
tck<=1'b0;
assign spi_clk = tck;
assign spi_csn = csn;
assign spi_sdi = sdi;
always @(posedge sclk or negedge rst_n)
if(rst_n == 1'b0)
conf_end <= 1'b0;
else if(state == STOP)
conf_end <= 1'b1;
ram_16x32_sr ram_16x32_sr_inst (
.address ( r_addr ),//读地址
.clock ( sclk ),
.data ( 16'd0 ),//写数据
.wren ( wren ),//写使能高有效,读使能低有效
.q ( r_data )//读数据
);
endmodule
testbench文件
`timescale 1ns/1ns
module tb_ex_spi;
reg sclk,rst_n;
reg work_en;
wire spi_clk,sclk_csn,spi_sdi;
reg [15:0] send_mem [31:0];
reg [15:0] shift_buf;
initial begin
rst_n =0;
sclk =0;
#100;
rst_n =1;
end
initial begin
work_en =0;
#150
work_en =1;
end
initial begin
$readmemb("dac_ini_16x32.mif",send_mem);
end
always #10 sclk = ~sclk;
initial begin
rec_spi();
end
spi_ctrl spi_ctrl_inst(
.sclk (sclk),//系统时钟50Mhz
.rst_n (rst_n),
.work_en (work_en),//触发配置操作的使能
.conf_end (),
.spi_clk (spi_clk),//50-60mhz
.spi_sdi (spi_sdi),
.spi_csn (spi_csn),
.spi_sdo ()//读输入管脚不进行编程
);
task rec_spi();
integer i,j;
begin
for (i=0;i<32;i=i+1)begin
for(j=0;j<16;j=j+1)begin
@(posedge spi_clk);
shift_buf = {shift_buf[14:0],spi_sdi};
if(j==15 && shift_buf == send_mem[i])
$display("ok data index is %d rec_d=%d send_d=%d",i,shift_buf,send_mem[i]);
else if(j== 15)
$display("error");
end
end
end
endtask
endmodule
do文件
quit -sim
.main clear
vlib work
vmap work work
vlog ./tb_ex_spi.v
vlog ./../design/spi_ctrl.v
vlog ./../quartus_prj/ipcore_dir/ram_16x32_sr.v
vlog ./altera_lib/*.v
vsim -voptargs=+acc work.tb_ex_spi
#创建虚拟信号
virtual type {
{01 IDLE}
{02 WAIT}
{04 R_MEM}
{08 W_REG}
{10 STOP}
} vir_new_signal
add wave tb_ex_spi/spi_ctrl_inst/*
add wave tb_ex_spi/rec_spi/*
virtual function {(vir_new_signal)tb_ex_spi/spi_ctrl_inst/state} new_state
add wave -color blue tb_ex_spi/spi_ctrl_inst/new_state
run 300us