• 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133

    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 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    波形仿真

    重点观察所设计的几个信号是否正确。
    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,将数据开始输出,否则数据保持。
    在这里插入图片描述

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

  • 相关阅读:
    Copy
    第二十章:多线程
    AndroidStudio jni C++与Java互相调用
    解决问题:Python使用seaborn可视化热力图heatmap、旋转X轴刻度文本标签45度后、发生文本平移、重叠问题解决
    3D激光线轮廓传感器市场需求,预计2029年将达到734.86百万美元。
    对敏感操作的二次认证 —— 详解 Sa-Token 二级认证
    论文超详细精读|八千字:DGNN
    Volatile 可以保证什么特性?有什么作用?
    【Linux】第十九站:进程替换
    大数据+大模型的尽头——数据分析师的未来会怎样?
  • 原文地址:https://blog.csdn.net/H19981118/article/details/125666438