目录
SPI协议简介
1.高速传输,SPI作为三大低速总线(UART、IIc、SPI)之一,其传输速度是这个个中最快的一个。它是一种高速、全双工、同步串行通信总线。所谓高速,指的是传输速度,最高能达到几十M/s,具体速度取决于硬件实现和时钟频率。
2.单工、全双工、半双工可参照下面这个介绍:
3.同步通信,.同步指的是收发双方使用同一个时钟,在传输过程中,保证数据传输无误。主设备提供时钟信号,从设备根据时钟信号的上升沿或下降沿进行数据的采样和发送。
- SPI 的硬件接口相对简单,通常只需要四根线:串行时钟线(SCLK)、主设备输出从设备输入线(MOSI)、主设备输入从设备输出线(MISO)和从设备选择线(SS)。
- 这种简单的硬件接口使得 SPI 易于实现和集成到各种电子系统中,降低了系统的成本和复杂性。
SPI工作原理
SPI 的工作原理
-
主从设备结构
- SPI 通信通常涉及一个主设备和一个或多个从设备。主设备负责发起通信并控制时钟信号,从设备则根据主设备的指令进行数据的发送和接收。
- 主设备通过将从设备选择线(SS)拉低来选择要与之通信的从设备。一旦从设备被选中,主设备和从设备之间就可以通过 MOSI 和 MISO 线进行数据的传输。
-
数据传输过程
- 在 SPI 通信中,数据是以字节为单位进行传输的。主设备(Master)首先将数据写入发送缓冲区,然后在时钟信号的控制下,将数据逐位地通过 MOSI 线发送给从设备(Slave)。
- 从设备在时钟信号的同步下,通过 MISO 线将数据逐位地发送回主设备。主设备在接收数据的同时,可以继续发送下一个字节的数据。
- 数据传输的顺序通常是高位在前,低位在后。在每个时钟周期,主设备和从设备都会同时发送和接收一位数据,直到一个字节的数据传输完成。
-
时钟信号
- 时钟信号是 SPI 通信的关键,它由主设备提供,并用于同步数据的传输。时钟信号的频率可以根据需要进行设置,通常在几兆赫兹到几十兆赫兹之间。
- 时钟信号的极性和相位可以通过配置寄存器进行设置,以适应不同的从设备要求。例如,可以设置时钟信号在上升沿或下降沿进行数据采样,或者设置时钟信号的初始相位为高电平或低电平。
SPI总线传输一共有4中模式,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了SCK时钟信号空闲状态的电平,CPHA规定了数据是在SCK时钟的上升沿被采样还是下降沿被采样。这四种模式的时序图如下图所示:
模式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时序图
其中在SS#低电平时,发送数据有效。当处于模式0下,时钟极性和时钟相位都为0,则主机发送数据时(对应的为MOSI信号变化),在sclk的奇数沿采样数据data,在偶数沿数据发生变化
下图是其他三种模式数据线MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系图:
实验需求
这里以模式0状态下,编写SPI通信下的主机发送模块,实现发送8bit数据的verilog代码
模块图
模块介绍:PLL模块:分频产生所需要的时钟信号
spi_m模块:控制主机发送所需要的数据
顶层模块:将子模块封装起来
时序图
要想实现上文模式0的时序,最简单的办法还是设计一个状态机。为了方便说明,手搓了一个viso时序图:
由于是要用FPGA实现发送数据,所以这里FPGA是主机。
当FPGA通过SPI总线发送一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0(即cs#),表示准备开始发送数据,整个发送数据过程其实可以分为5个状态:
IDLE状态:当开始发送数据信号(str_flag==1'd1)时,数据跳转到S0
S0状态(可不设置这个状态):直接跳转到S1
S1状态:在此时指示数据进行传输
S2状态:CS#信号要拉高
S3状态:直接跳转到IDLE状态
一个字节数据发送完毕以后,产生一个发送完成标志位finish并把CS#/SS信号拉高完成一次发送。这里为了传输sck信号,设置了一个clk_en使能信号,目的是在传输数据时,把时钟信号也进行传输。
思路理清楚以后就可以直接编写Verilog代码了,spi_m模块的代码如下:
module spi_send(
input wire div_clk ,//分频后的时钟
input wire sys_rst_n ,//系统复位,低电平有效
input wire str_flag ,//发送开始信号
input wire [7:0]data ,//输入数据
//input wire data_width[7:0] ,//发送的数据位宽
output wire sck ,//发送时钟
output reg cs_n ,//片选信号,低电平有效
output reg mosi ,//主机输出从机输入
output reg finish //一帧完成信号
);
/****************define code********************/
parameter IDLE = 2'd0 ,
S0 = 2'd1 ,
S1 = 2'd2 ,
S2 = 2'd3 ,
S3 = 2'd4 ;
wire div_clk_n ;//对分频时钟取反
reg [3:0]cnt_bit ;//bit计数器0-8
reg clk_en ;//sck的使能信号
reg [2:0]state ;//状态机
/****************main code********************/
//describe div_clk
assign div_clk_n = ~div_clk ;
//describe cnt_bit
always@(posedge div_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
cnt_bit <= 4'd0 ;
else if(cnt_bit == 4'd8)
cnt_bit <= 4'd0 ;
else if(state == S1)
cnt_bit <= cnt_bit + 1'd1 ;
end
//describe clk_en
always@(posedge div_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
clk_en <= 1'd0 ;
else if((cnt_bit >= 4'd0) && (cnt_bit <= 4'd7)&&(state == S1))
clk_en <= 1'd1 ;
else
clk_en <= 1'd0 ;
end
//describe state
always@(posedge div_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
state <= 3'd0 ;
else case(state)
IDLE :
begin
if(str_flag)
state <= S0 ;
end
S0 :
begin
state <= S1 ;
end
S1 :
begin
if(cnt_bit == 4'd8)
state <= S2 ;
else
state <= state ;
end
S2 :
begin
state <= S3 ;
end
S3 :
begin
state <= IDLE ;
end
default : state <= IDLE ;
endcase
end
//describe sck
assign sck = (clk_en == 1'd1) ? div_clk_n : 1'd0 ;
//describe cs_n
always@(posedge div_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
cs_n <= 1'd1 ;
else if(str_flag)
cs_n <= 1'd0 ;
else if(state == S2)
cs_n <= 1'd1 ;
else
cs_n <= cs_n ;
end
//describe mosi
/* always@(posedge sck or negedge sys_rst_n)
begin
if(~sys_rst_n)
mosi <= 1'd0 ;
else if((clk_en)&&(~cs_n))
mosi <= data[7-cnt_bit] ;
else
mosi <= 1'd0 ;
end */
always@(posedge div_clk or negedge sys_rst_n)
if(sys_rst_n==1'b0)
mosi <= 1'b0;
else if(state==S1)
case(cnt_bit)
4'd0 : mosi <= data[7];
4'd1 : mosi <= data[6];
4'd2 : mosi <= data[5];
4'd3 : mosi <= data[4];
4'd4 : mosi <= data[3];
4'd5 : mosi <= data[2];
4'd6 : mosi <= data[1];
4'd7 : mosi <= data[0];
default : mosi <= 1'b0;
endcase
else
mosi <= 1'b0;
//describe finish
always@(posedge div_clk or negedge sys_rst_n)
begin
if(~sys_rst_n)
finish <= 1'd0 ;
else if(state == S2)
finish <= 1'd1 ;
else
finish <= 1'd0 ;
end
endmodule
整个代码的流程与之前分析的流程完全一致。接下来就对这个代码用ModelSim进行基本的仿真。这里继续编写一个testbench仿真文件:
`timescale 1ns/1ps
module top_tb();
reg sys_clk ;
reg sys_rst_n ;
reg str_flag ;
reg [7:0]data ;
wire sck ;
wire cs_n ;
wire mosi ;
wire finish ;
initial begin
str_flag = 1'd0 ;
sys_clk = 1'd0 ;
sys_rst_n = 1'd0 ;
#100.1
sys_rst_n = 1'd1 ;
data = 8'b11101110 ;//模拟数据输入为8'b11101110
#1000
str_flag = 1'd1 ;
#80
str_flag = 1'd0 ;//模拟数据开始传输信号
end
always #10 sys_clk = ~sys_clk ;
top top(
.sys_clk (sys_clk ) ,
.sys_rst_n(sys_rst_n) ,
.str_flag (str_flag ) ,
.data (data ) ,
.sck (sck ) ,
.cs_n (cs_n ) ,
.mosi (mosi ) ,
.finish (finish )
);
endmodule
ModelSim的仿真图如下图所示:
由图可以看到仿真得到的时序与SPI模式0的时序完全一致。
小结
1.如果外设芯片的数据位宽是16-bit或者32-bit怎么办?
上文已经完成了8-bit数据发送的例子,16-bit和32-bit可以照着葫芦画瓢,无非就是多增加几个状态而已。
2.发送数据的状态机和接收数据的状态机可以用移位的方式来做吗?
事实上那个状态机的发送8-bit数据和接收8-bit数据的部分只有一行代码是不同的,所以也可以用移位的方法来做,然后把偶数状态也可以整合到一起,这样写的代码会更短更精炼。但出于理解更容易的角度,还是分开写较好。
标签:SCK,clk,就够,sys,SPI,图文,数据,时钟 From: https://blog.csdn.net/weixin_60580395/article/details/142218155