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 通讯设备之间的连接方式可分为一主一从和一主多从。
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个跳变沿采样。
这四种模式的时序图如下图所示:
- 模式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)位置和数据采样位置的关系。
2.3、SPI协议通信过程
下面以模式 0 为例,讲解一下 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驱动的整体框图、输入输出信号如下所示:
其中信号描述如下:
该模块的使用方法如下:
- 拉高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的驱动非常适合使用状态机编写,有兴趣可以自己尝试一下
`timescale 1ns/1ns //时间单位/精度
// 模式0
module spi_drive
(
// 系统接口input sys_clk , // 全局时钟50MHzinput 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)beginif(!sys_rst_n)cnt <= 2'd0; else if(!spi_cs)beginif(cnt == 2'd3)cnt <= 2'd0;elsecnt <= cnt + 1'b1; endelse cnt <= 2'd0;
end
// 生成spi_sclk时钟
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)spi_sclk <= 1'b0; //模式0默认为低电平 else if(!spi_cs)begin //在SPI传输过程中if(cnt == 2'd0 )spi_sclk <= 1'b0;else if (cnt == 2'd2)spi_sclk <= 1'b1;else spi_sclk <= spi_sclk; endelse spi_sclk <= 1'b0; //模式0默认为低电平
end
// 生成片选信号spi_cs
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)spi_cs <= 1'b1; //默认为高电平 else if(spi_start) //开始SPI准备传输,拉低片选信号spi_cs <= 1'b0;//收到了SPI结束信号,且结束了最近的一个BYTEelse if(spi_end_req && (cnt == 2'd1 && bit_cnt_rec == 4'd0))spi_cs <= 1'b1; //拉高片选信号,结束SPI传输
end
// 生成结束请求信号(捕捉spi_end信号)
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)spi_end_req <= 1'b0; //默认不使能 else if(spi_cs) spi_end_req <= 1'b0; //结束SPI传输后拉低请求else if(spi_end) spi_end_req <= 1'b1; //接收到SPI结束信号后就把结束请求拉高
end
// 发送数据过程--------------------------------------------------------------------// 发送数据
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginspi_mosi <= 1'b0; //模式0空闲bit_cnt_send <= 4'd0;endelse if(cnt == 2'd0 && !spi_cs)begin //模式0的上升沿spi_mosi <= data_send[7-bit_cnt_send]; //发送数据移位if(bit_cnt_send == 4'd7) //发送完8bitbit_cnt_send <= 4'd0;elsebit_cnt_send <= bit_cnt_send + 1'b1; endelse if(spi_cs)begin //非传输时间段spi_mosi <= 1'b0; //模式0空闲bit_cnt_send <= 4'd0;endelse beginspi_mosi <= spi_mosi;bit_cnt_send <= bit_cnt_send;end
end
// 发送数据标志
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)send_done <= 1'b0; else if(cnt == 2'd0 && bit_cnt_send == 4'd7) //发送完了8bit数据send_done <= 1'b1; //拉高一个周期,表示发送完成 else send_done <= 1'b0;
end// 接收数据过程--------------------------------------------------------------------// 接收数据spi_miso
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)begindata_rec <= 8'd0; bit_cnt_rec <= 4'd0;endelse if(cnt == 2'd2 && !spi_cs)begin //模式0的上升沿data_rec[7-bit_cnt_rec] <= spi_miso; //移位接收if(bit_cnt_rec == 4'd7) //接收完了8bitbit_cnt_rec <= 4'd0;elsebit_cnt_rec <= bit_cnt_rec + 1'b1; endelse if(spi_cs)begin bit_cnt_rec <= 4'd0;endelse begindata_rec <= data_rec;bit_cnt_rec <= bit_cnt_rec;end
end
// 接收数据标志
always @(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)rec_done <= 1'b0; else if(cnt == 2'd2 && bit_cnt_rec == 4'd7) //接收完了8bitrec_done <= 1'b1; //拉高一个周期,表示接收完成 else rec_done <= 1'b0;
endendmodule
4、Testbench及仿真结果
4.1、单个BYTE的仿真
使用该SPI驱动,向从机发送单个BYTE数据8‘b01010101,观察其仿真时序是否正确:
//------------------------------------------------
//--SPI驱动仿真(模式0,1个BYTE)
//------------------------------------------------
`timescale 1ns/1ns //时间单位/精度//------------<模块及端口声明>----------------------------------------
module tb_spi_drive();
//系统接口
reg sys_clk ; // 全局时钟50MHz
reg sys_rst_n ; // 复位信号,低电平有效
//用户接口
reg spi_start ; // 发送传输开始信号,一个高电平
reg spi_end ; // 发送传输结束信号,一个高电平
reg [7:0] data_send ; // 要发送的数据
wire [7:0] data_rec ; // 接收到的数据
wire send_done ; // 主机发送一个字节完毕标志位
wire rec_done ; // 主机接收一个字节完毕标志位
//SPI物理接口
reg spi_miso ; // SPI串行输入,用来接收从机的数据
wire spi_sclk ; // SPI时钟
wire spi_cs ; // SPI片选信号
wire spi_mosi ; // SPI输出,用来给从机发送数据
//仿真用
reg [3:0] cnt_send ; //发送数据计数器,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 beginsys_clk = 1'b0; //初始时钟为0sys_rst_n <= 1'b0; //初始复位spi_start <= 1'b0; data_send <= 8'd0; spi_miso <= 1'bz; spi_end <= 1'b0; #80 //80个时钟周期后sys_rst_n <= 1'b1; //拉高复位,系统进入工作状态#30 //30个时钟周期后拉高SPI开始信号,开始SPI传输spi_start <= 1'b1;data_send <= 8'b01010101;#20 spi_start <= 1'b0;@(posedge send_done) //一个BYTE发送完成spi_end <= 1'b1; #20 spi_end <= 1'b0; //拉高一个周期结束信号 end//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk; //系统时钟周期20nsendmodule
仿真结果如下:
可以看到,在拉高了spi_start开始传输信号后,主机开始发送数据,MOSI上的数据分别是01010101,发送完一个BYTE的数据后,send_done拉高。此时拉高结束信号spi_end,就终结了这次SPI传输,完成了单个BYTE的SPI传输。
4.2、多个BYTE的仿真
使用该SPI驱动,依次向从机发送数据8‘d0~8‘d10,观察其仿真时序是否正确:
//------------------------------------------------
//--SPI驱动仿真(模式0)
//------------------------------------------------
`timescale 1ns/1ns //时间单位/精度//------------<模块及端口声明>----------------------------------------
module tb_spi_drive();
//系统接口
reg sys_clk ; // 全局时钟50MHz
reg sys_rst_n ; // 复位信号,低电平有效
//用户接口
reg spi_start ; // 发送传输开始信号,一个高电平
reg spi_end ; // 发送传输结束信号,一个高电平
reg [7:0] data_send ; // 要发送的数据
wire [7:0] data_rec ; // 接收到的数据
wire send_done ; // 主机发送一个字节完毕标志位
wire rec_done ; // 主机接收一个字节完毕标志位
//SPI物理接口
reg spi_miso ; // SPI串行输入,用来接收从机的数据
wire spi_sclk ; // SPI时钟
wire spi_cs ; // SPI片选信号
wire spi_mosi ; // SPI输出,用来给从机发送数据
//仿真用
reg [3:0] cnt_send ; //发送数据计数器,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 beginsys_clk = 1'b0; //初始时钟为0sys_rst_n <= 1'b0; //初始复位spi_start <= 1'b0; data_send <= 8'd0; spi_miso <= 1'bz; spi_end <= 1'b0; #80 //80个时钟周期后sys_rst_n <= 1'b1; //拉高复位,系统进入工作状态#30 //30个时钟周期后拉高SPI开始信号,开始SPI传输spi_start <= 1'b1; #20 spi_start <= 1'b0;
endalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)begindata_send <= 8'd0; spi_end <= 1'b0; cnt_send <= 4'd0; endelse if(send_done)begin //数据发送完成 if(cnt_send == 4'd10)begin cnt_send <= 4'd0; spi_end <= 1'b1; //拉高结束标志,结束SPI传输过程 data_send <= 8'd0;endelse begincnt_send <= cnt_send + 4'd1; spi_end <= 1'b0; data_send <= data_send + 4'd1; //发送数据累加 endendelse begindata_send <= data_send;spi_end <= 1'b0; //其他时候保持SPI传输(不结束) end
end//------------<设置时钟>----------------------------------------------
always #10 sys_clk = ~sys_clk; //系统时钟周期20nsendmodule
仿真结果如下:
可以看到,在拉高了spi_start开始传输信号后,主机一直在发送数据,MOSI上的数据分别是8‘d0~8‘d10,每次发送一个BYTE的数据后,send_done即拉高一次。当结束信号spi_end被拉高后,就终结了这次SPI传输。
5、其他
- 需要注意的是,由于没有从机响应,所以MISO都是高阻态(蓝色)
- 下篇文章再结合从机(FLASH芯片)进行仿真验证接收数据功能
- 想要整个工程的朋友可以在评论区留下邮箱
FPGA实现的SPI协议(一)----SPI驱动相关推荐
- STM32F429入门(二十一):SPI协议及SPI读写FLASH
IIC主要用于通讯速率一般的场合,而SPI一般用于较高速的场合. 一.SPI协议简介 SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设 ...
- 18. SPI协议,spi转can,MCP2515裸机驱动详解
更多信息请关注公众号:一口Linux 十八.SPI.MCP2515 一.SPI概述 Serial Peripheral interface 通用串行外围设备接口 是Motorola首先在其MC68HC ...
- 基于SPI协议的Flash驱动控制-数据普通读操作
目录 Flash数据普通读操作 实现原理 verilog设计代码 verilog测试代码 Flash数据普通读操作 实现原理 将片选信号拉低,写入读操作指令,最少读取一个字节的数据,写入读指令后要写入 ...
- 基于SPI协议的Flash驱动控制-扇区擦除
目录 Flash扇区擦除 实现原理 verilog设计代码 verilog测试代码 Flash扇区擦除 实现原理 扇区的概念 Flash型号的数字代表容量,单位兆bit,如M25P16,此Flash的 ...
- linux内核添加spi驱动,Linux内核驱动之spi子系统spi协议.docx
Linux内核驱动之spi子系统spi协议 概况 SPI接口是摩托罗拉首先提出的全双工三线同步串行外围接口SCK,MOSI,MISO,采用主从模式(Master Slave)架构:支持多slave模式 ...
- 几种常用通信协议:IIC协议、SPI协议、UART协议
通信可以形象的比喻成两个人讲话:1.你说的别人得能听懂:双方约定信号的协议.2.你的语速别人得能接受:双方满足时序要求. 一.IIC协议: 2C串行总线一般有两根信号线,一根是双向的数据线SDA,另一 ...
- 【STM32】SPI协议通信详解
目录 一.SPI协议简介 二.SPI物理层 三.SPI协议层 1.通讯的起始和停止信号 2.数据有效性 3.时钟信号的相位和极性(重点) 四.SPI 特性及架构(重点) 1.通信引脚 2.时钟控制逻辑 ...
- 【嵌入式基础】基于IIC和SPI协议的温湿度采集与OLED显示
本文主要介绍IIC总线通信协议和SPI协议,并使用STM32系列芯片基于IIC协议实现AHT20温湿度传感器上位机数据采集,基于SPI协议实现OLED显示. 目录 一.IIC总线通信协议 1.IIC协 ...
- 基于IIC和SPI协议的温湿度采集与OLED显示
前言 环境 KEIL:5.0 FIyMcu:V0.993 开发板:STM32F103C8T6 器件 AHT-20 温湿度传感器 0.96英寸OLED屏幕 任务1内容 学习I2C总线通信协议,使用STM ...
最新文章
- Windows 7加域操作手册下
- python教程书籍-大牛推荐的10本学习 Python 的好书
- 设置响应主体格式php,Laravel如何实现适合Api的异常处理响应格式
- ⾼维特征的哈希技巧总结
- HttpClient的简单使用
- 连接oracle10g数据库免安装oracle客户端解决办法 (转载)
- github gists 101使代码共享漂亮
- 开博第一篇,聊聊 最基本的 “==” 与 “===”区别
- bootstrap上传图片可实现查看上一张图片和下一张图片_如何实现像人民日报微信推文一样的的点亮效果?...
- JetbrainsCrack-3.1-release-enc.jar 下载
- 标准差 php,标准偏差怎么算
- Dockerfile unable to access ‘.git/‘: Failed to connect to gitlab.com
- 显示12306服务器处理中正在排队,12306称能够解决技术问题 不与企业合作
- Struts2类型转换的说明及案例分析
- 浏览器查看,请在微信客户端打开链接
- JS制作蔡徐坤打篮球小游戏(鸡你太美?)
- 计算机专业 教师资格证,计算机专业可以报考广东教师资格证吗
- python如何识别特殊字符_python正则表达式--特殊字符
- Spark Streaming系列-5、应用案例: 百度搜索风云榜
- 【ESP32】13.DS18B20温度传感器实验(OneWire和DallasTemperature库)
热门文章
- linux 下使用isign 签名ipa包
- 【聚宽本地数据JQData】一个简单的股票回测策略
- 01Redis基础篇
- Nvidia jetson tx2 ubuntu16.04插入128GB内存卡报错:Unable to access/mount “128 GB Volume”
- ElasticSearch入门教程(1)
- 计算机休眠后黑屏打不开,电脑待机后黑屏打不开怎么办
- Go语言基础数据类型所占内存大小
- c语言读取midi文件举例子,c# – 使用NAudio从MIDI文件中读取音符
- OBS 相芯美颜 之 美颜SDK介绍
- termite:从零开始的go语言学习生活