一、SPI基础知识梳理
1、SPI的组成
a、四线组成
一般情况下SPI是由四根线组成的全双工高速通信总线。总线一般命名为“cs_n”、“sclk”、“mosi”和“miso”,其中“cs_n”是片选信号;“sclk”是数据时钟;“mosi”是主机数据输出,从机数据输入;“miso”是主机数据输入,从机数据输出。
四线制SPI,一主一从的连接方式如下图:
四线制SPI,一主多从的连接方式如下图:
四线制SPI,菊花链拓扑连接示意如下图:
b、三线组成
SPI也可由三线组成,分别为“cs_n”、“sclk”和“sdio”。“cs_n”是片选信号;“sclk”是数据时钟;“sdio”是双向数据线。
三线制SPI,一主一从的连接方式如下图:
三线制SPI,一主多从的连接示意图如下:
2、SPI的工作模式及时序关系
a、SPI的工作模式
SPI有四种工作模式,由“CPOL”和“CPHA"决定。
CPOL:时钟极性,定义了数据时钟的空闲电平
CPOL=0:表示时钟空闲电平为低电平
CPOL=1:表示时钟空闲电平为高电平
CPHA:时钟相位,定义了数据被采样的时刻
CPHA=0:表示在时钟的第一个跳变沿采集数据
CPHA=1:表示在时钟的第二个跳变沿采集数据
四种模式分别如下
Mode0:“CPOL”=“0”,“CPHA”=“0”,表示时钟空闲时为低电平,在时钟的第一个跳变沿采集数据,通俗也称为“00模式”。
Mode1:“CPOL”=“0”,“CPHA”=“1”,表示时钟空闲时为低电平,在时钟的第二个跳变沿采集数据,通俗也称为“01模式”。
Mode2:“CPOL”=“1”,“CPHA”=“0”,表示时钟空闲时为高电平,在时钟的第一个跳变沿采集数据,通俗也称为“10模式”。
Mode3:“CPOL”=“1”,“CPHA”=“0”,表示时钟空闲时为高电平,在时钟的第二个跳变沿采集数据,通俗也称为“11模式”。
Mode0和Mode3为常用模式
b、SPI的工作时序
SPI的数据时钟一般为100K、400K和3.4M。其实这三种时钟速率只是参考速率,例如某一个器件的手册说自己支持100K速率的SPI,但实际在100K左右的时钟频率也能正常工作。
时序图按照三线制和四线制分类展示,其中四线制时序分为全双工应用场景和半双工应用场景。数据位宽假设为8bit。
三线制SPI工作时序图
1)Mode0时序图
CPOL=0,CPHA=0,时钟空闲为低电平,在第一个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
2)Mode1时序图
CPOL=0,CPHA=1,时钟空闲为低电平,在第二个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
3) Mode2时序图
CPOL=1,CPHA=0,时钟空闲为高电平,在第一个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
4)Mode3时序图
CPOL=1,CPHA=1,时钟空闲为高电平,在第二个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
四线制工作时序图
虽然说SPI是一种全双工通信总线,但是也存在半双工的应用场景。所以下面分为全双工和半双工两种场景介绍时序图
全双工应用场景
1)Mode0时序图
CPOL=0,CPHA=0,时钟空闲为低电平,在第一个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
2)Mode1时序图
CPOL=0,CPHA=1,时钟空闲为低电平,在第二个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
3)Mode2时序图
CPOL=1,CPHA=0,时钟空闲为高电平,在第一个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
4)Mode3时序图
CPOL=1,CPHA=1,时钟空闲为高电平,在第二个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
半双工应用场景
1)Mode0时序图
CPOL=0,CPHA=0,时钟空闲为低电平,在第一个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
2)Mode1时序图
CPOL=0,CPHA=1,时钟空闲为低电平,在第二个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
3)Mode2时序图
CPOL=1,CPHA=0,时钟空闲为高电平,在第一个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
4)Mode3时序图
CPOL=1,CPHA=1,时钟空闲为高电平,在第二个时钟跳变沿采集数据时序图如下。其中“MO-x”表示主机输出的数据,“SO-x”表示从机输出的数据。
二、FPGA实现SPI主机功能
1、思路分析
a、目标
1、实现多应用场景的SPI主机收发数据功能(仅实现数据收发功能,作为底层驱动模块,上层应用根据项目开发)
2、自适应用户时钟
3、模式可配置。
可配置内容包括:
1、三线制或者四线制
2、数据位宽
3、Mode类型
4、数据大小端
5、SPI速率
可配置项中三线制或者四线制直接决定模块端口的数量和类型,所以按照“线制”进行第一次分类。
在四线制场景下,全双工和半双工两种应该的端口也不相同,所以按照全双工和半双工进行第二次分类。
综上,模块功能分为三种模式,分别为“三线制SPI”、“四线制全双工SPI”和“四线制半双工SPI”。
在这三种模式分类下,进行功能分类,分别是数据位宽、Mode类型、数据大小端和SPI速率。
b、实现方法
针对“三线制SPI”、“四线制全双工SPI”和“四线制半双工SPI”三类采取宏定义方式,进行实现。
在三类模式基础下采取参数方式实现对数据位宽、Mode类型、数据大小端和SPI速率的配置。
2、SPI时序拆分
a、SPI时序
不管是三线制SPI还是四线制SPI我们都可机将一个完整的SPI工作时序拆分为一个个单独时钟周期,而且在每个单独的时钟周期内时钟和数据的逻辑相同。
在一个独立时钟周期内时钟和数据的行为逻辑受CPOL和CPHA控制。
进一步将一个独立的时钟周期拆分为四个部分,如下图(四线制全双工为例,其它场景可同理推导):
假设图中为SPI通信的第一和第二个时钟周期。T0-T4为一个时钟周期,T1、T2、T3将一个时钟周期分为四部分。
先关注“CPHA”
主机端采集数据:
“MISO”上的数据不管CPHA是“0”或者是“1”数据在T2至T4时间段内总是保持稳定,则主机可以忽略“CPHA”的值在T3时刻对“MISO”的数据采样。
主句端输出数据:
“MOSI”上的数据理论上在“T0”或者“T2”时刻发生改变,从机在“T2”或者“T4”时刻采样数据。则主机只需要保证“MOSI”上的数据在T1时刻改变就可以保证从机采样到正确的数据。
再来看“CPOL”
“CPOL”只是决定空闲时钟的极性。不管“CPOL”是什么值,时钟总是在“T2”时刻和“T4”时刻发生翻转。
综上,理想后的时序如下图:
主机采样数据总是发生在“T3”时刻,
主机输出数据总是发生在“T1”时刻,
这样就能保证“CPHA”的两种时序都正常工作。
时钟总是在“T2”时刻和“T4”时刻发生一次翻转,
3、模型端口设计
a、三线模式
spi_modu #(
.CLK_FREQ ('d50 ), // 模块时钟频率 |单位:Mhz
.SPI_FREQ ('d100 ), // SPI通信频率 |单位:khz
.DATA_SIDE ("MSB" ), // MSB:大端模式 LSB:小端模式
.BIT_NUM ('d8 ), // 数据长度 |单位bit
.CPHA ('d0 ), // 时钟相位
.CPOL ('d0 ) // 时钟极性
) your_instance_name (
.clk (clk ), // input wire [0 :0] |模块时钟输入
.reset (reset ), // input wire [0 :0] |模块复位输入 |0:无效 1:复位
.send_en (send_en ), // input wire [0 :0] |发送数据使能
.send_data (send_data ), // input wire [BIT_NUM-1:0] |发送数据输入
.send_done (send_done ), // output reg [0 :0] |发送数据结束标志
.recv_en (recv_en ), // input wire [0 :0] |接收数据使能
.recv_vld (recv_vld ), // output reg [0 :0] |接收数据有效标志
.recv_data (recv_data ), // output reg [BIT_NUM-1:0] |接收数据输出
.recv_done (recv_done ), // output reg [0 :0] |接收数据结束标志
.comm_done (comm_done ), // output reg [0 :0] |通信结束标志
.spi_cs_n (spi_cs_n ), // output reg [0 :0] |设备片选
.spi_sclk (spi_sclk ), // output reg [0 :0] |通信时钟
.spi_sdio (spi_sdio ) // inout wire [0 :0] |数据输入输出
);
b、四线全双工模式
spi_modu #(
.CLK_FREQ ('d50 ), // 模块时钟频率 |单位:Mhz
.SPI_FREQ ('d100 ), // SPI通信频率 |单位:khz
.DATA_SIDE ("MSB" ), // MSB:大端模式 LSB:小端模式
.BIT_NUM ('d8 ), // 数据长度 |单位bit
.CPHA ('d0 ), // 时钟相位
.CPOL ('d0 ) // 时钟极性
) your_instance_name (
.clk (clk ), // input wire [0 :0] |模块时钟输入
.reset (reset ), // input wire [0 :0] |模块复位输入 |0:无效 1:复位
.swop_en (swop_en ), // input wire [0 :0] |开始交换使能
.send_data (send_data ), // input wire [BIT_NUM-1:0] |发送数据输入
.recv_vld (recv_vld ), // output reg [0 :0] |接收数据有效标志
.recv_data (recv_data ), // output reg [BIT_NUM-1:0] |接收数据输出
.swop_done (swop_done ), // output reg [0 :0] |交换结束标志
.comm_done (comm_done ), // output reg [0 :0] |通信结束标志
.spi_cs_n (spi_cs_n ), // output reg [0 :0] |设备片选
.spi_sclk (spi_sclk ), // output reg [0 :0] |通信时钟
.spi_mosi (spi_mosi ), // output reg [0 :0] |数据输出
.spi_miso (spi_miso ) // input wire [0 :0] |数据输入
);
c、四线半双工模式
spi_modu #(
.CLK_FREQ ('d50 ), // 模块时钟频率 |单位:Mhz
.SPI_FREQ ('d100 ), // SPI通信频率 |单位:khz
.DATA_SIDE ("MSB" ), // MSB:大端模式 LSB:小端模式
.BIT_NUM ('d8 ), // 数据长度 |单位bit
.CPHA ('d0 ), // 时钟相位
.CPOL ('d0 ) // 时钟极性
) your_instance_name (
.clk (clk ), // input wire [0 :0] |模块时钟输入
.reset (reset ), // input wire [0 :0] |模块复位输入 |0:无效 1:复位
.send_en (send_en ), // input wire [0 :0] |发送数据使能
.send_data (send_data ), // input wire [BIT_NUM-1:0] |发送数据输入
.send_done (send_done ), // output reg [0 :0] |发送数据结束标志
.recv_en (recv_en ), // input wire [0 :0] |接收数据使能
.recv_vld (recv_vld ), // output reg [0 :0] |接收数据有效标志
.recv_data (recv_data ), // output reg [BIT_NUM-1:0] |接收数据输出
.recv_done (recv_done ), // output reg [0 :0] |接收数据结束标志
.comm_done (comm_done ), // output reg [0 :0] |通信结束标志
.spi_cs_n (spi_cs_n ), // output reg [0 :0] |设备片选
.spi_sclk (spi_sclk ), // output reg [0 :0] |通信时钟
.spi_mosi (spi_mosi ), // output reg [0 :0] |数据输出
.spi_miso (spi_miso ) // input wire [0 :0] |数据输入
);
3、代码实现
a、头文件代码
`ifndef spi_mode_vh
`define spi_mode_vh
//==========================================
//SPI线制定义 |三线制 或者 四线制
//==========================================
// `define four_wire 1 //四线制SPI申明
// `define three_wire 1 //三线制SPI申明
`ifdef four_wire
//==========================================
//SPI类型定义 |全双工类型 或者 半双工类型
//==========================================
// `define half_duplex 1 //半双工类型
// `define full_duplex 1 //全双工类型
`endif
`endif
在头文件里面根据需求取消注释即可,例如需要四线制全双工模式则取消四线制定义和全双工类型定义的注释,如下:
`ifndef spi_mode_vh
`define spi_mode_vh
//==========================================
//SPI线制定义 |三线制 或者 四线制
//==========================================
`define four_wire 1 //四线制SPI申明
// `define three_wire 1 //三线制SPI申明
`ifdef four_wire
//==========================================
//SPI类型定义 |全双工类型 或者 半双工类型
//==========================================
// `define half_duplex 1 //半双工类型
`define full_duplex 1 //全双工类型
`endif
`endif
b、代码
代码内容请访问
链接:FPGA实现SPI协议、verilog编程(代码篇)