FPGA实现的SPI协议(一)—-SPI驱动

写在前面

SPI协议系列文章:

FPGA实现的SPI协议(一)—-SPI驱动

FPGA实现的SPI协议(二)—-基于SPI接口的FLASH芯片M25P16的使用

1、什么是SPI协议

SPI(Serial Peripheral Interface,串行外围设备接口)通讯协议,是 Motorola 公司提出的一种同步串行接口技术,是一种高速、全双工、同步通信总线,在芯片中只占用四根管脚用来控制及数据传输,广泛用于 EEPROM、Flash、RTC(实时时钟)、ADC(数模转换 器)、DSP(数字信号处理器)以及数字信号解码器上,是常用的、重要的低速通讯协议之一。

SPI 通讯协议的优点是支持全双工通信,通讯方式较为简单,且相对数据传输速率较快;缺点是没有指定的流控制,没有应答机制,在数据可靠性上有一定缺陷。

2、SPI协议详述

2.1、SPI协议物理层

SPI 通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分,根据从机设备的数量,SPI 通讯设备之间的连接方式可分为 一主一从和一主多从。

FPGA实现的SPI协议(一)----SPI驱动

FPGA实现的SPI协议(一)----SPI驱动

SPI总线传输只需要4根线就能完成,这四根线的作用分别如下:

  • SCK (Serial Clock):时钟信号线,用于同步通讯数据。由通讯主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不同
  • MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,数据方向由主机到从机
  • MISO (Master Input,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,数据方向由从机到主机
  • *CS (Chip Select):片选信号线。当有多个 SPI 从 设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同使用这 3 条总线;而每个从设备都有独立的片选信号线,即有多少个从设备,就有多少条片选信号线。相当于由SPI构成的通信系统中,通过CS片选信号来决定通信的从机设备是哪一台。通信期间低电平有效,表示对应从机被选中

2.2、SPI 协议层

SPI总线传输一共有4种模式,这4种模式分别由 时钟极性(CPOL,Clock Polarity)时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了SCK时钟信号空闲状态的电平,CPHA规定了数据是在SCK时钟的上升沿被采样还是下降沿被采样。

SPI总线的极性–时钟极性

时钟极性决定SPI总线空闲时的时钟信号是 高电平还是 低电平。CPOL = 1:表示空闲时是高电平;CPOL = 0:表示空闲时是低电平。

SPI总线的相位–时钟相位

时钟相位决定SPI总线从哪个跳变沿开始采样数据 CPHA = 0:在时钟信号SCK的第1个跳变沿采样;CPHA = 1:在时钟信号SCK的第2个跳变沿采样。

这四种模式的时序图如下图所示:

FPGA实现的SPI协议(一)----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(毕竟在下降沿采集数据的还是少)。下图描述了4种模式数据线MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系。

FPGA实现的SPI协议(一)----SPI驱动

2.3、SPI协议通信过程

下面以模式 0 为例,讲解一下 SPI 基本的通讯过程:

FPGA实现的SPI协议(一)----SPI驱动

SCK、MOSI、CS_N 信号均由主机控制产生, SCK 是时钟信号,用以同步数据,MOSI 是主机输出从机输入信号,主机通过此信号线传输数据给从机,CS_N 为片选信号,用以选定从机设备,低电平有效;而 MISO 的信号由 从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 CS_N 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。

在图中的标号1处,CS_N 信号线由高变低,是 SPI 通讯的起始信号。CS_N 是每 个从机各自独占的信号线,当从机在自己的 CS_N 线检测到起始信号后,就知道自己被主 机选中了,开始准备与主机通讯。在图中的标号6处,CS_N 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI 及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用MSB 先行模式。 MOSI 及 MISO 的数据在 SCK 的下降沿期间变化输出, 在 SCK 的上升沿时被采样。即在 SCK 的上升沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据”1″,为低电平时表示数据”0″。在其它时刻,数据无效,MOSI 及 MISO 为下一次表示数据做准备。

SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。

2.4、SPI协议的特性

  • SPI协议是一主多从的架构,通过片选信号CS来区分不同的从机(寻址方式)
  • SPI协议是一种同步(Synchronous)传输协议,通信双方通过主机生成的时钟信号SCK来作为数据交换的基准信号
  • SPI协议是一种全双工的串行通信协议,通信过程中主从双方均可进行数据交换
  • SPI协议具有4中通信模式,依据双方约定好的模式进行通信

2.5、SPI协议的 优势、劣势

优势:

  • 全双工串行通信
  • 简单的硬件结构
  • 高速数据传输速率(相比UART、IIC)
  • 灵活的数据传输方式,不限于8位,可以是任意大小的字

劣势

  • 仅支持一个主设备
  • 引脚略多(相比UART、IIC)
  • 没有硬件从机应答信号(主机可能在不知情的情况下无处发送)

3、驱动代码的设计实现

接下来实现的SPI驱动代码特性如下:MSB 先行;仅限模式0;每次传输8位(1个BYTE)。

3.1、接口定义与整体设计

SPI驱动的整体框图、输入输出信号如下所示:

FPGA实现的SPI协议(一)----SPI驱动

其中信号描述如下:

FPGA实现的SPI协议(一)----SPI驱动

该模块的使用方法如下:

  • 拉高SPI传输开始信号spi_start一个周期,同时发送要传输的数据给data_send,等待数据发送完成后,该模块会将发送完成标志信号 send_done拉高一个周期,标志一个BYTE的数据通过SPI总线发送给了从机
  • 同样的,当接收完成标志信号rec_done被该模块拉高后,则意味着,主机成功接收了一个BTYE从机发送过来的数据
  • 当主机希望结束这次传输时,可将SPI结束信号spi_end拉高一个周期,则该模块会在发送最后一个模块后结束SPI传输,这也意味着,如果没有结束到SPI结束信号,则SPI传输会一直进行,以便实现多个BYTE的SPI传输

3.2、Verilog代码

Verilog代码并不复杂,结合下图的SPI通信过程,可以发现以下要点:

  • SCK很适合使用系统时钟的4分频时钟,因为在一个SCK内需要对其进行4次操作
  • 分别使用生成的SCK的上升沿、下降沿对其移位发送数据、接收数据即可
  • 此外从下图可知,SPI的驱动非常适合使用状态机编写,有兴趣可以自己尝试一下

FPGA实现的SPI协议(一)----SPI驱动
timescale 1ns/1ns      //时间单位/精度
// 模式0
module spi_drive
(
// 系统接口
    input               sys_clk     ,           // 全局时钟50MHz
    input               sys_rst_n   ,           // 复位信号,低电平有效
// 用户接口
    input               spi_start   ,           // 发送传输开始信号,一个高电平
    input               spi_end     ,           // 发送传输结束信号,一个高电平
    input        [7:0]  data_send   ,           // 要发送的数据
    output  reg  [7:0]  data_rec    ,           // 接收到的数据
    output  reg         send_done   ,           // 主机发送一个字节完毕标志位
    output  reg         rec_done    ,           // 主机接收一个字节完毕标志位
// SPI物理接口
    input               spi_miso    ,           // SPI串行输入,用来接收从机的数据
    output  reg         spi_sclk    ,           // SPI时钟
    output  reg         spi_cs      ,           // SPI片选信号,低电平有效
    output  reg         spi_mosi                // SPI输出,用来给从机发送数据
);

reg [1:0]   cnt;                                //4分频计数器
reg [3:0]   bit_cnt_send;                       //发送计数器
reg [3:0]   bit_cnt_rec;                        //接收计数器
reg         spi_end_req;                        //结束请求

//4分频计数器
always @(posedge sys_clk or negedge sys_rst_n)begin
    if(!sys_rst_n)
        cnt <= 2'd0; else if(!spi_cs)begin if(cnt="=" 2'd3) cnt <="2'd0;" + 1'b1; end 生成spi_sclk时钟 always @(posedge sys_clk or negedge sys_rst_n)begin if(!sys_rst_n) spi_sclk 模式0默认为低电平 在spi传输过程中 2'd0 ) if (cnt="=" 2'd2) 生成片选信号spi_cs spi_cs 默认为高电平 if(spi_start) 开始spi准备传输,拉低片选信号 收到了spi结束信号,且结束了最近的一个byte if(spi_end_req && 2'd1 bit_cnt_rec="=" 4'd0)) 拉高片选信号,结束spi传输 生成结束请求信号(捕捉spi_end信号) spi_end_req 默认不使能 if(spi_cs) 结束spi传输后拉低请求 if(spi_end) 接收到spi结束信号后就把结束请求拉高 发送数据过程-------------------------------------------------------------------- 发送数据 if(!sys_rst_n)begin spi_mosi 模式0空闲 bit_cnt_send !spi_cs)begin 模式0的上升沿 发送数据移位 if(bit_cnt_send="=" 4'd7) 发送完8bit if(spi_cs)begin 非传输时间段 begin 发送数据标志 send_done 发送完了8bit数据 拉高一个周期,表示发送完成 接收数据过程-------------------------------------------------------------------- 接收数据spi_miso data_rec 2'd2 data_rec[7-bit_cnt_rec] 移位接收 if(bit_cnt_rec="=" 接收完了8bit 接收数据标志 rec_done 拉高一个周期,表示接收完成 endmodule< code></=>
</code></pre>
<h1>4、Testbench及仿真结果</h1>
<h2>4.1、单个BYTE的仿真</h2>
<p>使用该SPI驱动,向从机发送单个BYTE数据8'b01010101,观察其仿真时序是否正确:</p>
<pre><code>//------------------------------------------------
//--SPI驱动仿真(模式0,1个BYTE)
//------------------------------------------------
 1ns/1ns      //&#x65F6;&#x95F4;&#x5355;&#x4F4D;/&#x7CBE;&#x5EA6;

//------------<模块及端口声明>----------------------------------------
module tb_spi_drive();
//&#x7CFB;&#x7EDF;&#x63A5;&#x53E3;
reg             sys_clk     ;           // &#x5168;&#x5C40;&#x65F6;&#x949F;50MHz
reg             sys_rst_n   ;           // &#x590D;&#x4F4D;&#x4FE1;&#x53F7;&#xFF0C;&#x4F4E;&#x7535;&#x5E73;&#x6709;&#x6548;
//&#x7528;&#x6237;&#x63A5;&#x53E3;
reg             spi_start   ;           // &#x53D1;&#x9001;&#x4F20;&#x8F93;&#x5F00;&#x59CB;&#x4FE1;&#x53F7;&#xFF0C;&#x4E00;&#x4E2A;&#x9AD8;&#x7535;&#x5E73;
reg             spi_end     ;           // &#x53D1;&#x9001;&#x4F20;&#x8F93;&#x7ED3;&#x675F;&#x4FE1;&#x53F7;&#xFF0C;&#x4E00;&#x4E2A;&#x9AD8;&#x7535;&#x5E73;
reg     [7:0]   data_send   ;           // &#x8981;&#x53D1;&#x9001;&#x7684;&#x6570;&#x636E;
wire    [7:0]   data_rec    ;           // &#x63A5;&#x6536;&#x5230;&#x7684;&#x6570;&#x636E;
wire            send_done   ;           // &#x4E3B;&#x673A;&#x53D1;&#x9001;&#x4E00;&#x4E2A;&#x5B57;&#x8282;&#x5B8C;&#x6BD5;&#x6807;&#x5FD7;&#x4F4D;
wire            rec_done    ;           // &#x4E3B;&#x673A;&#x63A5;&#x6536;&#x4E00;&#x4E2A;&#x5B57;&#x8282;&#x5B8C;&#x6BD5;&#x6807;&#x5FD7;&#x4F4D;
//SPI&#x7269;&#x7406;&#x63A5;&#x53E3;
reg             spi_miso    ;           // SPI&#x4E32;&#x884C;&#x8F93;&#x5165;&#xFF0C;&#x7528;&#x6765;&#x63A5;&#x6536;&#x4ECE;&#x673A;&#x7684;&#x6570;&#x636E;
wire            spi_sclk    ;           // SPI&#x65F6;&#x949F;
wire            spi_cs      ;           // SPI&#x7247;&#x9009;&#x4FE1;&#x53F7;
wire            spi_mosi    ;           // SPI&#x8F93;&#x51FA;&#xFF0C;&#x7528;&#x6765;&#x7ED9;&#x4ECE;&#x673A;&#x53D1;&#x9001;&#x6570;&#x636E;
//&#x4EFF;&#x771F;&#x7528;
reg     [3:0]   cnt_send    ;           //&#x53D1;&#x9001;&#x6570;&#x636E;&#x8BA1;&#x6570;&#x5668;&#xFF0C;0-15

//------------<例化spi驱动模块(模式0)>----------------------------------------
spi_drive   spi_drive_inst(
    .sys_clk        (sys_clk    ),
    .sys_rst_n      (sys_rst_n  ),

    .spi_start      (spi_start  ),
    .spi_end        (spi_end    ),
    .data_send      (data_send  ),
    .data_rec       (data_rec   ),
    .send_done      (send_done  ),
    .rec_done       (rec_done   ),

    .spi_miso       (spi_miso   ),
    .spi_sclk       (spi_sclk   ),
    .spi_cs         (spi_cs     ),
    .spi_mosi       (spi_mosi   )
);

//------------<设置初始测试条件>----------------------------------------
initial begin
    sys_clk = 1'b0;                     //&#x521D;&#x59CB;&#x65F6;&#x949F;&#x4E3A;0
    sys_rst_n <= 1'b0; 初始复位 spi_start <="1'b0;" data_send spi_miso spi_end #80 80个时钟周期后 sys_rst_n 拉高复位,系统进入工作状态 #30 30个时钟周期后拉高spi开始信号,开始spi传输 #20 @(posedge send_done) 一个byte发送完成 拉高一个周期结束信号 end ------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;                  //&#x7CFB;&#x7EDF;&#x65F6;&#x949F;&#x5468;&#x671F;20ns

endmodule</=></设置初始测试条件></例化spi驱动模块(模式0)></模块及端口声明>

仿真结果如下:

FPGA实现的SPI协议(一)----SPI驱动

可以看到,在拉高了spi_start开始传输信号后,主机开始发送数据,MOSI上的数据分别是01010101,发送完一个BYTE的数据后,send_done拉高。此时拉高结束信号spi_end,就终结了这次SPI传输,完成了单个BYTE的SPI传输。

4.2、多个BYTE的仿真

使用该SPI驱动,依次向从机发送数据8’d0~8’d10,观察其仿真时序是否正确:

//------------------------------------------------
//--SPI&#x9A71;&#x52A8;&#x4EFF;&#x771F;&#xFF08;&#x6A21;&#x5F0F;0&#xFF09;
//------------------------------------------------
`timescale 1ns/1ns      //&#x65F6;&#x95F4;&#x5355;&#x4F4D;/&#x7CBE;&#x5EA6;

//------------<模块及端口声明>----------------------------------------
module tb_spi_drive();
//&#x7CFB;&#x7EDF;&#x63A5;&#x53E3;
reg             sys_clk     ;           // &#x5168;&#x5C40;&#x65F6;&#x949F;50MHz
reg             sys_rst_n   ;           // &#x590D;&#x4F4D;&#x4FE1;&#x53F7;&#xFF0C;&#x4F4E;&#x7535;&#x5E73;&#x6709;&#x6548;
//&#x7528;&#x6237;&#x63A5;&#x53E3;
reg             spi_start   ;           // &#x53D1;&#x9001;&#x4F20;&#x8F93;&#x5F00;&#x59CB;&#x4FE1;&#x53F7;&#xFF0C;&#x4E00;&#x4E2A;&#x9AD8;&#x7535;&#x5E73;
reg             spi_end     ;           // &#x53D1;&#x9001;&#x4F20;&#x8F93;&#x7ED3;&#x675F;&#x4FE1;&#x53F7;&#xFF0C;&#x4E00;&#x4E2A;&#x9AD8;&#x7535;&#x5E73;
reg     [7:0]   data_send   ;           // &#x8981;&#x53D1;&#x9001;&#x7684;&#x6570;&#x636E;
wire    [7:0]   data_rec    ;           // &#x63A5;&#x6536;&#x5230;&#x7684;&#x6570;&#x636E;
wire            send_done   ;           // &#x4E3B;&#x673A;&#x53D1;&#x9001;&#x4E00;&#x4E2A;&#x5B57;&#x8282;&#x5B8C;&#x6BD5;&#x6807;&#x5FD7;&#x4F4D;
wire            rec_done    ;           // &#x4E3B;&#x673A;&#x63A5;&#x6536;&#x4E00;&#x4E2A;&#x5B57;&#x8282;&#x5B8C;&#x6BD5;&#x6807;&#x5FD7;&#x4F4D;
//SPI&#x7269;&#x7406;&#x63A5;&#x53E3;
reg             spi_miso    ;           // SPI&#x4E32;&#x884C;&#x8F93;&#x5165;&#xFF0C;&#x7528;&#x6765;&#x63A5;&#x6536;&#x4ECE;&#x673A;&#x7684;&#x6570;&#x636E;
wire            spi_sclk    ;           // SPI&#x65F6;&#x949F;
wire            spi_cs      ;           // SPI&#x7247;&#x9009;&#x4FE1;&#x53F7;
wire            spi_mosi    ;           // SPI&#x8F93;&#x51FA;&#xFF0C;&#x7528;&#x6765;&#x7ED9;&#x4ECE;&#x673A;&#x53D1;&#x9001;&#x6570;&#x636E;
//&#x4EFF;&#x771F;&#x7528;
reg     [3:0]   cnt_send    ;           //&#x53D1;&#x9001;&#x6570;&#x636E;&#x8BA1;&#x6570;&#x5668;&#xFF0C;0-15

//------------<例化spi驱动模块(模式0)>----------------------------------------
spi_drive   spi_drive_inst(
    .sys_clk        (sys_clk    ),
    .sys_rst_n      (sys_rst_n  ),

    .spi_start      (spi_start  ),
    .spi_end        (spi_end    ),
    .data_send      (data_send  ),
    .data_rec       (data_rec   ),
    .send_done      (send_done  ),
    .rec_done       (rec_done   ),

    .spi_miso       (spi_miso   ),
    .spi_sclk       (spi_sclk   ),
    .spi_cs         (spi_cs     ),
    .spi_mosi       (spi_mosi   )
);

//------------<设置初始测试条件>----------------------------------------
initial begin
    sys_clk = 1'b0;                     //&#x521D;&#x59CB;&#x65F6;&#x949F;&#x4E3A;0
    sys_rst_n <= 1'b0; 初始复位 spi_start <="1'b0;" data_send spi_miso spi_end #80 80个时钟周期后 sys_rst_n 拉高复位,系统进入工作状态 #30 30个时钟周期后拉高spi开始信号,开始spi传输 #20 end always@(posedge sys_clk or negedge sys_rst_n)begin if(!sys_rst_n)begin cnt_send else if(send_done)begin 数据发送完成 if(cnt_send="=" 4'd10)begin 拉高结束标志,结束spi传输过程 begin + 4'd1; 发送数据累加 其他时候保持spi传输(不结束) ------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk;                  //&#x7CFB;&#x7EDF;&#x65F6;&#x949F;&#x5468;&#x671F;20ns

endmodule</=></设置初始测试条件></例化spi驱动模块(模式0)></模块及端口声明>

仿真结果如下:

FPGA实现的SPI协议(一)----SPI驱动

可以看到,在拉高了spi_start开始传输信号后,主机一直在发送数据,MOSI上的数据分别是8’d0~8’d10,每次发送一个BYTE的数据后,send_done即拉高一次。当结束信号spi_end被拉高后,就终结了这次SPI传输。

5、其他

  • 需要注意的是,由于没有从机响应,所以MISO都是高阻态(蓝色)
  • 下篇文章再结合从机(FLASH芯片)进行仿真验证接收数据功能
  • 想要整个工程的朋友可以在评论区留下邮箱

Original: https://blog.csdn.net/wuzhikaidetb/article/details/120984325
Author: 孤独的单刀
Title: FPGA实现的SPI协议(一)—-SPI驱动

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/93576/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

发表回复

登录后才能评论
免费咨询
免费咨询
扫码关注
扫码关注
联系站长

站长Johngo!

大数据和算法重度研究者!

持续产出大数据、算法、LeetCode干货,以及业界好资源!

2022012703491714

微信来撩,免费咨询:xiaozhu_tec

分享本页
返回顶部