Verilog实现SPI通信协议驱动设计

SPI通信协议原理

串行外围设备接口(SPI)是微控制器和外围IC(移位寄存器、SRAM等)之间广泛使用的接口。SPI是一种同步、全双工、主从式接口。来自主机或者从机的数据在clk上升沿或下降沿同步,主机和从机可以通过MOSI、MISO线路同时传输数据。SPI接口可以是3线式(SCLK、CS、DIO)或者4线式(SCLK、CS、MOSI、MISO)。


全双工:接口可以同时接收和发送数据(双倍速率),与iic相比,支持更高的时钟频率,SCLK范围在0.8-3.2Mhz之间。
半双工:接口任意时刻只能接收或者发送数据。


四根线的解释:

SCLK:由主机产生的时钟信号线,
.
CS:片选信号(低有效),该信号用来选择从机;
.
数据线:DIO表示主从机之间传输的数据线;MOSI表示数据从主机到从机,MISO反之。
.


如下为主从机间的四线式SPI接口,一机一从。
在这里插入图片描述

当主机提供多个单独片选CS信号时,即可达到一主多从的效果,如下所示:
在这里插入图片描述

SPI的四种通信方式

CPOL(时钟极性)和CPHA(时钟相位)来共同控制SPI的通信方式。其中:

CPOL决定SPI总线空闲时SCLK的电平;(0:空闲状态时SCLK低电平,1:空闲状态时SCLK高电平)
.
CPHA决定数据是上升沿还是下降沿采样;【0:第一个沿采样,1:第二个沿采样】

因此四种通信方式如下,常用0和3:
在这里插入图片描述
四种通信模式的详解:

mode0:CPOL= 0,CPHA=0。SCLK串行时钟线空闲时为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
mode1:CPOL= 0,CPHA=1。SCLK串行时钟线空闲时为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
mode2:CPOL= 1,CPHA=0。SCLK串行时钟线空闲时为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
mode3:CPOL= 1,CPHA=1。SCLK串行时钟线空闲时为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换

SPI时序图

以Mode0为例:

当CS信号低电平时MOSI、MISO信号线有效,于是在SCK的每个时钟周期传输一位的数据。
1时刻:CS由高变低,为SPI通信的起始标志,对应从机被选中;
6时刻:CS由低变高,通信结束标志,对应从机选中取消;
2时刻(偶数时刻):数据在上升沿采样,此时位于数据中间位置,最稳定。
3时刻(奇数时刻):数据在下降沿切换,便于在下一上升沿的时候数据能正确被采集。
总的来说,这种方式能保证数据被正常采集到,也符合触发器建立和保持时间的要求,数据在上升沿之前提前到达以及在上升沿之后继续保持一段时间。
在这里插入图片描述

SPI每次输出传输以8或者16位为单位,每次传输的单位数不受限制。(UART为8位)
.
注意:数据传输的时候,MSB和LSB先行均可,但要保证两个SPI通信设备之间一致,一般采用MSB先行的方式。

SPI通讯协议的应用

现进行SPI与ADC进行通信,主要的接口如下:
其中Channel用来表示ADC的8个通道,这里无需过多关注,重点完成SPI驱动控制的时序设计。根据控制模块来生成对应的SPI信号,从而实现SPI与ADC的通信。
在这里插入图片描述
根据下面的时序图,来完成SPI控制器设计。
在这里插入图片描述

重点:如果理解计数器的产生?
由于数据的采样和切换是在上升沿、下降沿进行的,因此我们来产生SCLK_cnt计数器,用来计数SCLK的边沿,此外每一个边沿产生一个cnt_flag信号,简单理解就是每半个周期产生一个脉冲cnt_flag。SCLK的范围是0.8-3.2Mhz,因此我们取该范围内,若为2.5MHZ,因此一个SCLK周期是400ns,那么半个周期是200ns,由于系统时钟50Mhz,因此200ns / 20ns =10,每计数10次,表示一个cnt_flag脉冲信号。
在这里插入图片描述


代码编写

在进行具体协议代码编写的时候,一般有状态机和线性序列机两种方式,分情况来使用。
比如IIC,有着多种开始、器件地址传输以及应答等状态,那么需要用状态机来设计,而SPI、UART即可用线性序列机,状态少且能用计数器来完成,下面代码就是用线性序列机完成SPI的方法。

verilog代码

module  spi_drive(
    input clk,
	 input rst_n,
	 input start,
	 input [2:0] Channel,
	 input DOUT,
	 
	 output reg DIN,
	 output reg CS_N,
    output reg SCLK,
	 output reg [11:0] data,
    output reg done	 
	 
);
    reg en;
	 reg [2:0] r_channel;
	 reg [3:0] cnt;
	 reg cnt_flag;
	 reg [5:0] SCLK_CNT; //假设共有33个SCLK
    reg [11:0] r_data;
//转换使能信号(计数器的累计使能等)
always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     en <= 1'b0;
	 else if(start)
	     en <= 1'b1;
	 else if(done)
	     en <= 1'b0;
	 else
	     en <= en; 
	     
 // r_channel
 always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     r_channel <= 1'd0;
	  else if(start)
	     r_channel <= Channel; 
	  else
	     r_channel <= r_channel;

//产生时序图,SCLK 0.8 - 3.2Mhz,取SCLK=2.5Mhz

//cnt
 always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     cnt <= 4'd0;
	  else if(en) begin
	      if(cnt == 4'd9)
			    cnt <= 4'd0;
	      else
			    cnt <= cnt + 1'b1; 
		  end
	  else
	     cnt <= 4'd0;
		  
//产生cnt_flag
 always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     cnt_flag <= 0;
	  else if(cnt == 4'd9)
	     cnt_flag <= 1; 
	  else
	     cnt_flag <= 0;
		  
//产生SCLK_CNT
 always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     SCLK_CNT <= 6'd0;
	  else if(en) begin
	      if(SCLK_CNT == 6'd33)
			    SCLK_CNT <= 6'd0;
	      else if(cnt_flag)
			    SCLK_CNT <= SCLK_CNT + 1'b1; 
			else
			    SCLK_CNT <= SCLK_CNT ; 
		  end
	  else
	     SCLK_CNT <= 6'd0;
		  
//线性序列机
 always @ (posedge clk or negedge rst_n)
    if(!rst_n)begin
	     DIN  <= 1'b1;
	     CS_N <= 1'b1; //低电平有效
	     SCLK <= 1'b1;
		end
	  else if(en)begin
	      case(SCLK_CNT)
			    6'd0:begin CS_N <= 1'b0; end
				 6'd1:begin SCLK <= 1'b0; DIN  <= 1'b0;end
				 6'd2:begin SCLK <= 1'b1; end
				 6'd3:begin SCLK <= 1'b0; end
				 6'd4:begin SCLK <= 1'b1; end
				 6'd5:begin SCLK <= 1'b0; DIN  <= r_channel[2];end
				 6'd6:begin SCLK <= 1'b1; end
				 6'd7:begin SCLK <= 1'b0; DIN  <= r_channel[1];end
				 6'd8:begin SCLK <= 1'b1; end
				 6'd9:begin SCLK <= 1'b0; DIN  <= r_channel[0];end
				 6'd10,6'd12,6'd14,6'd16,6'd18,6'd20,6'd22,6'd24,6'd26,6'd28,6'd30,6'd32:
				 begin SCLK <= 1'b1; r_data  <= {r_data[10:0],DOUT};end  //上升沿采样,高电平移位操作
				 6'd11,6'd13,6'd15,6'd17,6'd19,6'd21,6'd23,6'd25,6'd27,6'd29,6'd31:
				 begin SCLK <= 1'b0;end //低电平期间数据保持
				 6'd33:begin CS_N <= 1'b1; end
				 default:begin CS_N <= 1'b1; end
			endcase
	  end
	  else begin
	     DIN  <= 1'b1;
	     CS_N <= 1'b1; //低电平有效
	     SCLK <= 1'b1;
		end
	     
//done信号

always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     done <= 1'b0;
	 else if(SCLK_CNT == 6'd33)
	     done <= 1'b1;
	 else
	     done <= 1'b0;

//data信号

always @ (posedge clk or negedge rst_n)
    if(!rst_n)
	     data <= 1'b0;
	 else if(SCLK_CNT == 6'd33)
	     data <= r_data;
	 else
	     data <= data;

endmodule

tb测试文件

不是很会写tb测试文件,但也能分析分析,凑合看下吧~或者也可以用其他人写的tb文件来测试

`timescale 1ns/1ns

module spi_drive_tb;

    reg clk;
	 reg rst_n;
	 reg start;
	 reg [2:0] Channel;
	 reg DOUT;
	 
	 wire DIN;
	 wire CS_N;
    wire SCLK;
	 wire [11:0] data;
    wire done;	

spi_drive spi_drive(
    .clk(clk),
	 .rst_n(rst_n),
	 .start(start),
	 .Channel(Channel),
	 .DOUT(DOUT),

	 .DIN(DIN),
	 .CS_N(CS_N),
    .SCLK(SCLK),
	 .data(data),
    .done(done)	 
	 
);

//产生50Mhz的时钟频率
initial clk = 1'b1;
always #10 clk = ~clk;  

initial begin
    rst_n = 1'b0;
	 Channel = 0;
	 start = 0;
	 DOUT = 0;
	 #100;
	 rst_n = 1'b1;
	 #10;
	 start = 1;
	 #100;
	 Channel = 3; 
	 #100;
	 DOUT = 1;
	 #10;
	 DOUT = 0;	 
	 #50;
	 DOUT = 1;
	 #40;
	 DOUT = 0;
	 #50;
	 DOUT = 1;	 
	 #10;
	 DOUT = 1;	
	#50000;
	$stop;
end

endmodule 

波形仿真

重点观察所设计的几个信号是否正确。
en:当start信号为高电平的时候,en信号拉高,直到done信号出现,en才变成低电平。

cnt_flag:每cnt_flag出现一个高脉冲,则SCLK_CNT计数器加1。

SCLK:从仿真中可看到,SCLK的一个时钟周期刚好为400ns。同时其高低变化和verilog代码中描述的一致,计数为奇数的时候为低电平,计数为偶数的时候为高电平。

DIN,CS_N信号跟SCLK_CNT的计数值相关。复位的时候二者都是高电平。然后
当SCLK_CNT=0的时候,CS_N低电平,DIN高电平;
当SCLK_CNT=1的时候,CS_N低电平,DIN低电平……都是根据SPI时序或者说verilog处的描述来的。

done:由于这里提前设定传输33个数据拉高,因此计数到33的时候,done拉高.

data:计数到33,将数据开始输出,否则数据保持。
在这里插入图片描述

学习博文
学习视频,讲真的很好理解

Verilog可以用来实现SPI(Serial Peripheral Interface)协议。SPI是一种串行通信协议,用于在芯片之间进行数据交换。下面是一个简单的Verilog模块,用于实现SPI主设备的发送功能: ```verilog module spi_master ( input wire clk, input wire reset, output wire cs, output wire sclk, output wire mosi ); reg [7:0] data_out; reg [3:0] bit_counter; reg active; reg data_valid; assign cs = ~active; always @(posedge clk or posedge reset) begin if (reset) begin active <= 0; bit_counter <= 4'b0; data_out <= 8'b0; sclk <= 1'b0; mosi <= 1'b0; data_valid <= 1'b0; end else if (active) begin case (bit_counter) 0: begin mosi <= 1'b0; // Start bit sclk <= 1'b0; end 9: begin mosi <= 1'b1; // Stop bit sclk <= 1'b0; end default: begin mosi <= data_out[bit_counter-1]; sclk <= 1'b1; end endcase bit_counter <= bit_counter + 1; end else if (data_valid) begin active <= 1'b1; bit_counter <= 4'b0; end end always @(posedge clk or posedge reset) begin if (reset) begin data_valid <= 1'b0; end else if (active && (bit_counter == 9)) begin data_valid <= 1'b0; end else if (~active && ~data_valid) begin data_valid <= 1'b1; end end endmodule ``` 这个Verilog模块定义了一个SPI主设备,包含了时钟信号(`clk`)、复位信号(`reset`)、从设备选择信号(`cs`)、时钟信号(`sclk`)和输出数据信号(`mosi`)。 模块内部使用了一些寄存器和状态变量来实现SPI传输的逻辑。当`active`为1时,表示当前正在进行SPI传输。`bit_counter`用于计数传输的位数。`data_out`是要发送的数据。模块的行为由时钟信号和复位信号驱动,根据不同的状态和计数值,控制输出信号的值。 请注意,这只是一个简单的示例,实际使用时需要根据具体的SPI设备和通信协议进行适当的修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Fighting_FPGA

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

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

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

打赏作者

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

抵扣说明:

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

余额充值