2.2 SPI协议的FPGA实现

2020-06-30 10:21:56 浏览数 (1)

2.2.1 SPI总线规范简介

  SPI(Serial Peripheral Interface,串行外围设备接口),是Motorola公司提出的一种同步串行接口技术,是一种高速、全双工、同步通信总线,在芯片中只占用四根管脚用来控制及数据传输,广泛用于EEPROM、Flash、RTC(实时时钟)、ADC(数模转换器)、DSP(数字信号处理器)以及数字信号解码器上。SPI通信的速度很容易达到好几兆bps,所以可以用SPI总线传输一些未压缩的音频以及压缩的视频。   下图是只有2个chip利用SPI总线进行通信的结构图

        图2 23 两个chip利用SPI总线进行通信的结构图    可知SPI总线传输只需要4根线就能完成,这四根线的作用分别如下:   SCK(Serial Clock):SCK是串行时钟线,作用是Master向Slave传输时钟信号,控制数据交换的时机和速率;   MOSI(Master Out Slave in):在SPI Master上也被称为Tx-channel,作用是SPI主机给SPI从机发送数据;   CS/SS(Chip Select/Slave Select):作用是SPI Master选择与哪一个SPI Slave通信,低电平表示从机被选中(低电平有效);   MISO(Master In Slave Out):在SPI Master上也被称为Rx-channel,作用是SPI主机接收SPI从机传输过来的数据;

  SPI总线主要有以下几个特点:   1、 采用主从模式(Master-Slave)的控制方式,支持单Master多Slave。SPI规定了两个SPI设备之间通信必须由主设备Master来控制从设备Slave。也就是说,如果FPGA是主机的情况下,不管是FPGA给芯片发送数据还是从芯片中接收数据,写Verilog逻辑的时候片选信号CS与串行时钟信号SCK必须由FPGA来产生。同时一个Master可以设置多个片选(Chip Select)来控制多个Slave。SPI协议还规定Slave设备的clock由Master通过SCK管脚提供给Slave,Slave本身不能产生或控制clock,没有clock则Slave不能正常工作。单Master多Slave的典型结构如下图所示

            图2 24 单Master多Slave的典型结构   2、 SPI总线在传输数据的同时也传输了时钟信号,所以SPI协议是一种同步(Synchronous)传输协议。Master会根据将要交换的数据产生相应的时钟脉冲,组成时钟信号,时钟信号通过时钟极性(CPOL)和时钟相位(CPHA)控制两个SPI设备何时交换数据以及何时对接收数据进行采样,保证数据在两个设备之间是同步传输的。   3、 SPI总线协议是一种全双工的串行通信协议,数据传输时高位在前,低位在后。SPI协议规定一个SPI设备不能在数据通信过程中仅仅充当一个发送者(Transmitter)或者接受者(Receiver)。在片选信号CS为0的情况下,每个clock周期内,SPI设备都会发送并接收1 bit数据,相当于有1 bit数据被交换了。数据传输高位在前,低位在后(MSB first)。SPI主从结构内部数据传输示意图如下图所示

              图2 25 SPI主从结构内部数据传输示意图   SPI总线传输的模式:   SPI总线传输一共有4中模式,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了SCK时钟信号空闲状态的电平,CPHA规定了数据是在SCK时钟的上升沿被采样还是下降沿被采样。这四种模式的时序图如下图所示:

        图2 26 SPI四种模式的时序图   模式0:CPOL= 0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换   模式1:CPOL= 0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换   模式2:CPOL= 1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换   模式3:CPOL= 1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换   其中比较常用的模式是模式0和模式3。为了更清晰的描述SPI总线的时序,下面展现了模式0下的SPI时序图

              图2 27 模式0下的SPI时序图   上图清晰的表明在模式0下,在空闲状态下,SCK串行时钟线为低电平,当SS被主机拉低以后,数据传输开始,数据线MOSI和MISO的数据切换(Toggling)发生在时钟的下降沿(上图的黑色虚线),而数据线MOSI和MISO的数据的采样(Sampling)发生在数据的正中间(上图中的灰色实线)。下图清晰的描述了其他三种模式数据线MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系图

图2 28 MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系图

下面我将以模式0为例用Verilog编写SPI通信的代码。编写SPI通信的Verilog代码并利用ModelSim进行时序仿真   Verilog编写的SPI模块除了进行SPI通信的四根线以外还要包括一些时钟、复位、使能、并行的输入输出以及完成标志位。其框图如下所示:

            图 2 29 SPI协议建模框图 其中:

代码语言:javascript复制
I_clk是系统时钟;
I_rst_n是系统复位;
I_tx_en是主机给从机发送数据的使能信号,当I_tx_en为1时主机才能给从机发送数据;
I_rx _en是主机从从机接收数据的使能信号,当I_rx_en为1时主机才能从从机接收数据;
I_data_in是主机要发送的并行数据;
O_data_out是把从机接收回来的串行数据并行化以后的并行数据;
O_tx_done是主机给从机发送数据完成的标志位,发送完成后会产生一个高脉冲;
O_rx_done是主机从从机接收数据完成的标志位,接收完成后会产生一个高脉冲;
I_spi_miso、O_spi_cs、O_spi_sck和O_spi_mosi是标准SPI总线协议规定的四根线;

  要想实现上文模式0的时序,最简单的办法还是设计一个状态机。为了方便说明,这里把模式0的时序再在下面贴一遍

                图2 30 模式0下的SPI时序图   发送:当FPGA通过SPI总线往QSPI Flash中发送一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始发送数据,整个发送数据过程其实可以分为16个状态:

  • 状态0:SCK为0,MOSI为要发送的数据的最高位,即I_data_in[7]
  • 状态1:SCK为1,MOSI保持不变
  • 状态2:SCK为0,MOSI为要发送的数据的次高位,即I_data_in[6]
  • 状态3:SCK为1,MOSI保持不变
  • 状态4:SCK为0,MOSI为要发送的数据的下一位,即I_data_in[5]
  • 状态5:SCK为1,MOSI保持不变
  • 状态6:SCK为0,MOSI为要发送的数据的下一位,即I_data_in[4]
  • 状态7:SCK为1,MOSI保持不变
  • 状态8:SCK为0,MOSI为要发送的数据的下一位,即I_data_in[3]
  • 状态9:SCK为1,MOSI保持不变
  • 状态10:SCK为0,MOSI为要发送的数据的下一位,即I_data_in[2]
  • 状态11:SCK为1,MOSI保持不变
  • 状态12:SCK为0,MOSI为要发送的数据的下一位,即I_data_in[1]
  • 状态13:SCK为1,MOSI保持不变
  • 状态14:SCK为0,MOSI为要发送的数据的最低位,即I_data_in[0]
  • 状态15:SCK为1,MOSI保持不变

  一个字节数据发送完毕以后,产生一个发送完成标志位O_tx_done并把CS/SS信号拉高完成一次发送。通过观察上面的状态可以发现状态编号为奇数的状态要做的操作实际上是一模一样的,所以写代码的时候为了精简代码,可以把状态号为奇数的状态全部整合到一起。   接收:当FPGA通过SPI总线从QSPI Flash中接收一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始接收数据,整个接收数据过程其实也可以分为16个状态,但是与发送过程不同的是,为了保证接收到的数据准确,必须在数据的正中间采样,也就是说模式0时序图中灰色实线的地方才是代码中锁存数据的地方,所以接收过程的每个状态执行的操作为:

  • 状态0:SCK为0,不锁存MISO上的数据
  • 状态1:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[7]
  • 状态2:SCK为0,不锁存MISO上的数据
  • 状态4:SCK为0,不锁存MISO上的数据
  • 状态5:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[5]
  • 状态6:SCK为0,不锁存MISO上的数据
  • 状态7:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[4]
  • 状态8:SCK为0,不锁存MISO上的数据
  • 状态9:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[3]
  • 状态10:SCK为0,不锁存MISO上的数据
  • 状态11:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[2]
  • 状态12:SCK为0,不锁存MISO上的数据
  • 状态13:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[1]
  • 状态14:SCK为0,不锁存MISO上的数据
  • 状态15:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[0]

  一个字节数据接收完毕以后,产生一个接收完成标志位O_rx_done并把CS/SS信号拉高完成一次数据的接收。通过观察上面的状态可以发现状态编号为偶数的状态要做的操作实际上是一模一样的,所以写代码的时候为了精简代码,可以把状态号为偶数的状态全部整合到一起。而这一点刚好与发送过程的状态刚好相反。 思路理清楚以后就可以直接编写Verilog代码了,spi_module模块的代码如下:               代码 2 9 spi_module模块的代码

代码语言:javascript复制
1.	//****************************************************************************//
2.	//# @Author: 碎碎思
3.	//# @Date:   2019-04-18 20:57:46
4.	//# @Last Modified by:   zlk
5.	//# @WeChat Official Account: OpenFPGA
6.	//# @Last Modified time: 2019-04-18 21:52:43
7.	//# Description:
8.	//# @Modification History: 2019-04-18 21:52:43
9.	//# Date                By             Version             Change Description:
10.	//# ========================================================================= #
11.	//# 2019-04-18 21:52:43
12.	//# ========================================================================= #
13.	//# |                                                                       | #
14.	//# |                                OpenFPGA                               | #
15.	//****************************************************************************//
16.	`timescale 1ns/ 1ps
17.	module spi
18.	(
19.	    input CLOCK, RESET,// 全局时钟50MHz/复位信号,低电平有效
20.	    input               I_rx_en     , // 读使能信号
21.	    input               I_tx_en     , // 发送使能信号
22.	    input        [7:0]  iData   , // 要发送的数据
23.	    output  reg  [7:0]  oData  , // 接收到的数据
24.	    output  reg         iDone   , // 发送一个字节完毕标志位
25.	    output  reg         oDone   , // 接收一个字节完毕标志位
26.	      
27.	    // 四线标准SPI信号定义
28.	    input               I_spi_miso  , // SPI串行输入,用来接收从机的数据
29.	    output  reg         O_spi_sck   , // SPI时钟
30.	    output  reg         O_spi_cs    , // SPI片选信号
31.	    output  reg         O_spi_mosi    // SPI输出,用来给从机发送数据
32.	);
33.	//****************************************//
34.	  
35.	reg [3:0]   R_tx_state      ;
36.	reg [3:0]   R_rx_state      ;
37.	  
38.	//****************************************//
39.	always @(posedge CLOCK or negedge RESET)
40.	begin
41.	    if(!RESET)
42.	        begin
43.	            R_tx_state  <=  4'd0    ;
44.	            R_rx_state  <=  4'd0    ;
45.	            O_spi_cs    <=  1'b1    ;
46.	            O_spi_sck   <=  1'b0    ;
47.	            O_spi_mosi  <=  1'b0    ;
48.	            iDone   <=  1'b0    ;
49.	            oDone   <=  1'b0    ;
50.	            oData  <=  8'd0    ;
51.	        end
52.	    else if(I_tx_en) // 发送使能信号打开的情况下
53.	        begin
54.	            O_spi_cs    <=  1'b0    ; // 把片选CS拉低
55.	            case(R_tx_state)
56.	                4'd1, 4'd3 , 4'd5 , 4'd7  ,
57.	                4'd9, 4'd11, 4'd13, 4'd15 : //整合奇数状态
58.	                    begin
59.	                        O_spi_sck   <=  1'b1                ;
60.	                        R_tx_state  <=  R_tx_state   1'b1   ;
61.	                        iDone   <=  1'b0                ;
62.	                    end
63.	                4'd0:    // 发送第7位
64.	                    begin
65.	                        O_spi_mosi  <=  iData[7]        ;
66.	                        O_spi_sck   <=  1'b0                ;
67.	                        R_tx_state  <=  R_tx_state   1'b1   ;
68.	                        iDone   <=  1'b0                ;
69.	                    end
70.	                4'd2:    // 发送第6位
71.	                    begin
72.	                        O_spi_mosi  <=  iData[6]        ;
73.	                        O_spi_sck   <=  1'b0                ;
74.	                        R_tx_state  <=  R_tx_state   1'b1   ;
75.	                        iDone   <=  1'b0                ;
76.	                    end
77.	                4'd4:    // 发送第5位
78.	                    begin
79.	                        O_spi_mosi  <=  iData[5]        ;
80.	                        O_spi_sck   <=  1'b0                ;
81.	                        R_tx_state  <=  R_tx_state   1'b1   ;
82.	                        iDone   <=  1'b0                ;
83.	                    end
84.	                4'd6:    // 发送第4位
85.	                    begin
86.	                        O_spi_mosi  <=  iData[4]        ;
87.	                        O_spi_sck   <=  1'b0                ;
88.	                        R_tx_state  <=  R_tx_state   1'b1   ;
89.	                        iDone   <=  1'b0                ;
90.	                    end
91.	                4'd8:    // 发送第3位
92.	                    begin
93.	                        O_spi_mosi  <=  iData[3]        ;
94.	                        O_spi_sck   <=  1'b0                ;
95.	                        R_tx_state  <=  R_tx_state   1'b1   ;
96.	                        iDone   <=  1'b0                ;
97.	                    end
98.	                4'd10:    // 发送第2位
99.	                    begin
100.	                        O_spi_mosi  <=  iData[2]        ;
101.	                        O_spi_sck   <=  1'b0                ;
102.	                        R_tx_state  <=  R_tx_state   1'b1   ;
103.	                        iDone   <=  1'b0                ;
104.	                    end
105.	                4'd12:    // 发送第1位
106.	                    begin
107.	                        O_spi_mosi  <=  iData[1]        ;
108.	                        O_spi_sck   <=  1'b0                ;
109.	                        R_tx_state  <=  R_tx_state   1'b1   ;
110.	                        iDone   <=  1'b0                ;
111.	                    end
112.	                4'd14:    // 发送第0位
113.	                    begin
114.	                        O_spi_mosi  <=  iData[0]        ;
115.	                        O_spi_sck   <=  1'b0                ;
116.	                        R_tx_state  <=  R_tx_state   1'b1   ;
117.	                        iDone   <=  1'b1                ;
118.	                    end
119.	                default:R_tx_state  <=  4'd0                ;
120.	            endcase
121.	        end
122.	    else if(I_rx_en) // 接收使能信号打开的情况下
123.	        begin
124.	            O_spi_cs    <=  1'b0        ; // 拉低片选信号CS
125.	            case(R_rx_state)
126.	                4'd0, 4'd2 , 4'd4 , 4'd6  ,  4'd8, 4'd10, 4'd12, 4'd14 : //整合偶数状态
127.	                    begin
128.	                        O_spi_sck   <=  1'b0;
129.	                        R_rx_state  <=  R_rx_state   1'b1;
130.	                        oDone   <=  1'b0;
131.	                    end
132.	                4'd1:    // 接收第7位
133.	                    begin
134.	                        O_spi_sck       <=  1'b1                ;
135.	                        R_rx_state      <=  R_rx_state   1'b1   ;
136.	                        oDone       <=  1'b0                ;
137.	                        oData[7]   <=  I_spi_miso          ;
138.	                    end
139.	                4'd3:    // 接收第6位
140.	                    begin
141.	                        O_spi_sck       <=  1'b1                ;
142.	                        R_rx_state      <=  R_rx_state   1'b1   ;
143.	                        oDone       <=  1'b0                ;
144.	                        oData[6]   <=  I_spi_miso          ;
145.	                    end
146.	                4'd5:    // 接收第5位
147.	                    begin
148.	                        O_spi_sck       <=  1'b1                ;
149.	                        R_rx_state      <=  R_rx_state   1'b1   ;
150.	                        oDone       <=  1'b0                ;
151.	                        oData[5]   <=  I_spi_miso          ;
152.	                    end
153.	                4'd7:    // 接收第4位
154.	                    begin
155.	                        O_spi_sck       <=  1'b1                ;
156.	                        R_rx_state      <=  R_rx_state   1'b1   ;
157.	                        oDone       <=  1'b0                ;
158.	                        oData[4]   <=  I_spi_miso          ;
159.	                    end
160.	                4'd9:    // 接收第3位
161.	                    begin
162.	                        O_spi_sck       <=  1'b1                ;
163.	                        R_rx_state      <=  R_rx_state   1'b1   ;
164.	                        oDone       <=  1'b0                ;
165.	                        oData[3]   <=  I_spi_miso          ;
166.	                    end
167.	                4'd11:    // 接收第2位
168.	                    begin
169.	                        O_spi_sck       <=  1'b1                ;
170.	                        R_rx_state      <=  R_rx_state   1'b1   ;
171.	                        oDone       <=  1'b0                ;
172.	                        oData[2]   <=  I_spi_miso          ;
173.	                    end
174.	                4'd13:    // 接收第1位
175.	                    begin
176.	                        O_spi_sck       <=  1'b1                ;
177.	                        R_rx_state      <=  R_rx_state   1'b1   ;
178.	                        oDone       <=  1'b0                ;
179.	                        oData[1]   <=  I_spi_miso          ;
180.	                    end
181.	                4'd15:    // 接收第0位
182.	                    begin
183.	                        O_spi_sck       <=  1'b1                ;
184.	                        R_rx_state      <=  R_rx_state   1'b1   ;
185.	                        oDone       <=  1'b1                ;
186.	                        oData[0]   <=  I_spi_miso          ;
187.	                    end
188.	                default:R_rx_state  <=  4'd0                    ;
189.	            endcase
190.	        end
191.	    else
192.	        begin
193.	            R_tx_state  <=  4'd0    ;
194.	            R_rx_state  <=  4'd0    ;
195.	            iDone   <=  1'b0    ;
196.	            oDone   <=  1'b0    ;
197.	            O_spi_cs    <=  1'b1    ;
198.	            O_spi_sck   <=  1'b0    ;
199.	            O_spi_mosi  <=  1'b0    ;
200.	            oData  <=  8'd0    ;
201.	        end
202.	end
203.	  
204.	endmodule

  整个代码的流程与之前分析的流程完全一致。接下来就对这个代码用ModelSim进行基本的仿真。由于接收部分不再硬件上不太好测,所以这里只对发送部分进行测试,接收部分等把代码下载到板子里面以后用ChipScope抓接收部分时序就一清二楚了。 发射部分的测试激励代码如下:                   代码 2 10 spi_module仿真代码

代码语言:javascript复制
1.	`timescale 1ns / 1ps
2.	  
3.	module tb_spi_module;
4.	  
5.	    // Inputs
6.	    reg I_clk;
7.	    reg I_rst_n;
8.	    reg I_rx_en;
9.	    reg I_tx_en;
10.	    reg [7:0] I_data_in;
11.	    reg I_spi_miso;
12.	  
13.	    // Outputs
14.	    wire [7:0] O_data_out;
15.	    wire O_tx_done;
16.	    wire O_rx_done;
17.	    wire O_spi_sck;
18.	    wire O_spi_cs;
19.	    wire O_spi_mosi;
20.	  
21.	    // Instantiate the Unit Under Test (UUT)
22.	    spi_module uut (
23.	        .I_clk       (I_clk         ),
24.	        .I_rst_n     (I_rst_n    ),
25.	        .I_rx_en      (I_rx_en    ),
26.	        .I_tx_en      (I_tx_en    ),
27.	        .I_data_in     (I_data_in   ),
28.	        .O_data_out    (O_data_out  ),
29.	        .O_tx_done    (O_tx_done    ),
30.	        .O_rx_done    (O_rx_done    ),
31.	        .I_spi_miso   (I_spi_miso   ),
32.	        .O_spi_sck    (O_spi_sck    ),
33.	        .O_spi_cs    (O_spi_cs    ),
34.	        .O_spi_mosi   (O_spi_mosi   )
35.	    );
36.	  
37.	    initial begin
38.	        // Initialize Inputs
39.	        I_clk = 0;
40.	        I_rst_n = 0;
41.	        I_rx_en = 0;
42.	        I_tx_en = 1;
43.	        I_data_in = 8'h00;
44.	        I_spi_miso = 0;
45.	  
46.	        // Wait 100 ns for global reset to finish
47.	        #100;
48.	        I_rst_n = 1;
49.	  
50.	    end
51.	      
52.	    always #10 I_clk = ~I_clk ;
53.	      
54.	    always @(posedge I_clk or negedge I_rst_n)
55.	    begin
56.	         if(!I_rst_n)
57.	            I_data_in <= 8'h00;
58.	         else if(I_data_in == 8'hff)
59.	            begin
60.	                I_data_in <= 8'hff;
61.	                I_tx_en <= 0;
62.	            end
63.	         else if(O_tx_done)
64.	            I_data_in <= I_data_in   1'b1 ;
65.	    end
66.	        
67.	endmodule

   ModelSim的仿真图如下图所示:

                      图 2 31 ModelSim仿真波形   由图可以看到仿真得到的时序与SPI模式0的时序完全一致。

0 人点赞