Xilinx-Verilog-学习笔记(16):SPI总线

本文介绍了SPI总线的基本概念及特点,详细分析了使用Verilog语言设计SPI总线的过程,包括1Mhz SPI时钟的产生、内存数据读取与发送等功能模块的设计。

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

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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值