一、SPI简介
SPI(Serial Peripheral Interface,串行外围设备接口)是一种高速、全双工、同步通信总线。SPI 通讯协议的优点是支持全双工通信,通讯方式较为简单,且相对数据传输速率较快;缺点是没有指定的流控制,没有应答机制,在数据可靠性上有一定缺陷。
来自主机或者从机的数据在clk上升沿或下降沿同步,主机和从机可以通过MOSI、MISO线路同时传输数据
。SPI接口可以是3线式(SCLK、CS、DIO)或者4线式(SCLK、CS、MOSI、MISO)
SPI采用主从控制模式,通常由一个主模块和一个或多个从模块组成(不支持多主机),来自主机或者从机的数据在clk上升沿或下降沿同步,一般使用四条线进行通信SCLK、CS、MOSI、MISO) 。
MISO ( Master Input Slave Output ) : 主设备数据输入,从设备数据输出;
MOSI ( Master Output Slave Input ) : 主设备数据输出,从设备数据输入;
SCLK ( Serial Clock ) : 时钟信号,由主设备产生;
CS/SS ( Chip Select/Slave Select ) : 从设备片选信号,由主设备控制,通常低电平有效。
二、SPI四种通信方式
SPI总线在传输数据的同时也传输了时钟信号,时钟信号通过时钟极性(CPOL)和时钟相位(CPHA)控制两个SPI设备何时交换数据以及何时对接收数据进行采样,保证数据在两个设备之间同步传输。
时钟极性(Clock Polarity, CPOL/CKP),它是指时钟信号在空闲状态下是高电平还是低电平,当时钟空闲时为低电平即 CPOL=0,反之则 CPOL=1。
时钟相位(Clock Phase, CPHA/CKE),它是指时钟信号开始有效的第一个边沿和数据的关系。当时钟信号有效的第一个边沿处于数据稳定期的正中间时定义CPHA=0,反之时钟信号有效的第一个边沿不处于数据稳定期的正中间定义CPHA=1。所以在时钟信号SCK的第一个跳变沿采样即CPHA=0,再时钟信号SCK的第二个跳变沿采样为CPHA=1。
那么根据SPI的时钟极性和时钟相位特性可以设置4种不同的SPI通信操作模式:
SPI模式 | CPOL | CPHA | 空闲时SCK时钟 | 采样时刻 |
0 | 0 | 0 | 低电平 | 第1个边沿(奇) |
1 | 0 | 1 | 低电平 | 第2个边沿(偶) |
2 | 1 | 0 | 高电平 | 第1个边沿(奇) |
3 | 1 | 1 | 高电平 | 第2个边沿(偶) |
Mode0:CKP=0,CPHA =0:当空闲态时,SCK处于低电平,数据采样是在第1个边沿,即SCK由低电平到高电平的跳变,所以数据采样是在上升沿(准备数据),(发送数据)数据发送是在下降沿。
Mode1:CKP=0,CPHA=1:当空闲态时,SCK处于低电平,数据发送是在第2个边沿,即SCK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
Mode2:CKP=1,CPHA=0:当空闲态时,SCK处于高电平,数据采集是在第1个边沿,即SCK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
Mode3:CKP=1,CPHA=1:当空闲态时,SCK处于高电平,数据发送是在第2个边沿,即SCK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
SPI协议规定一个SPI设备不能在数据通信过程中仅仅充当一个发送者(Transmitter)或者接受者(Receiver)。在片选信号CS为0的情况下,每个clock周期内,SPI设备都会发送并接收1bit数据,相当于有1bit数据被交换。
MOSI及 MISO的数据在SCK的上升沿期间变化输出,在 SCK的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO为下一次表示数据做准备。
SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。
SPI通信过程
主机将8位二进制数据10101100发送给从机,从机将8位二进制数据11001010发送给主机。
首先主机要将从机片选信号SS拉低此时代表从机被选中,在此之前 SCK 信号要始终处于低电平。SS拉低后时钟信号SCK开始启动,此时主机和从机的数据开始交互。
时钟 | 主机数据 | MOSI | 从机数据 | MISO |
初始低电平 | 10101100 | 初始值0 | 11001010 | 初始值0 |
1 | 0101100X | 1 | 1001010X | 1 |
0 | 01011001 | 1 | 10010101 | 1 |
1 | 1011001X | 0 | 0010101X | 1 |
0 | 10110011 | 0 | 00101010 | 1 |
1 | 0110011X | 1 | 0101010X | 0 |
0 | 01100110 | 1 | 01010101 | 0 |
1 | 1100110X | 0 | 1010101X | 0 |
0 | 11001100 | 0 | 10101010 | 0 |
1 | 1001100X | 1 | 0101010X | 1 |
0 | 10011001 | 1 | 01010101 | 1 |
1 | 0011001X | 1 | 1010101X | 0 |
0 | 00110010 | 1 | 10101011 | 0 |
1 | 0110010X | 0 | 0101011X | 1 |
0 | 01100101 | 0 | 01010110 | 1 |
1 | 1100101X | 0 | 1010110X | 0 |
0 | 11001010 | 0 | 10101100 | 0 |
上表中时钟信号1代表时钟上升沿,0代表下降沿。
采用SPI通信Model 0,所以第一个时钟上升沿到来时完成数据采样,主机将要发送的数据最高位放到 MOSI上,而从机也将要发送的数据最高位放到 MISO上(保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用MSB 先行模式),当第一个时钟的下降沿到来时主机把MISO上的数据存入移位寄存器中,而从机把 MOSI上的数据存入移位寄存器中,这样主机从机就完成了1bit 数据的交互。
当第二个时钟上升沿到来时,重复第一个时钟的操作就完成了 2bit 数据的交互,当 8 个时钟过去后,主机和从机就完成了 8bit 数据交互,此时主机中的数据由10101100 变成了11001010,而从机中的数据也由 11001010变成了10101100。
三、SPI协议实现
通过状态机的方式实现SPC协议Model 0
发送:
当FPGA通过SPI总线往从机发送一个字节(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总线从从机中接收一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始接收数据,整个接收数据过程其实也可以分为16个状态,但是与发送过程不同的是,为了保证接收到的数据准确,必须在数据的正中间采样,接收过程的每个状态执行的操作为:
状态0:SCK为0,不锁存MISO上的数据
状态1:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[7]
状态2:SCK为0,不锁存MISO上的数据
状态3:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[6]
状态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信号拉高完成一次数据的接收。
module spi_module
(
input I_clk , // 全局时钟50MHz
input I_rst_n , // 复位信号,低电平有效
input I_rx_en , // 读使能信号
input I_tx_en , // 发送使能信号
input [7:0] I_data_in , // 要发送的数据
output reg [7:0] O_data_out , // 接收到的数据
output reg O_tx_done , // 发送一个字节完毕标志位
output reg O_rx_done , // 接收一个字节完毕标志位
// 四线标准SPI信号定义
input I_spi_miso , // SPI串行输入,用来接收从机的数据
output reg O_spi_sck , // SPI时钟
output reg O_spi_cs , // SPI片选信号
output reg O_spi_mosi // SPI输出,用来给从机发送数据
);
reg [3:0] R_tx_state ;
reg [3:0] R_rx_state ;
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_tx_state <= 4'd0 ;
R_rx_state <= 4'd0 ;
O_spi_cs <= 1'b1 ;
O_spi_sck <= 1'b0 ;
O_spi_mosi <= 1'b0 ;
O_tx_done <= 1'b0 ;
O_rx_done <= 1'b0 ;
O_data_out <= 8'd0 ;
end
else if(I_tx_en) // 发送使能信号打开的情况下
begin
O_spi_cs <= 1'b0 ; // 把片选CS拉低
case(R_tx_state)
4'd1, 4'd3 , 4'd5 , 4'd7 ,
4'd9, 4'd11, 4'd13, 4'd15 : //整合奇数状态
begin
O_spi_sck <= 1'b1 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd0: // 发送第7位
begin
O_spi_mosi <= I_data_in[7] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd2: // 发送第6位
begin
O_spi_mosi <= I_data_in[6] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd4: // 发送第5位
begin
O_spi_mosi <= I_data_in[5] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd6: // 发送第4位
begin
O_spi_mosi <= I_data_in[4] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd8: // 发送第3位
begin
O_spi_mosi <= I_data_in[3] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd10: // 发送第2位
begin
O_spi_mosi <= I_data_in[2] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd12: // 发送第1位
begin
O_spi_mosi <= I_data_in[1] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd14: // 发送第0位
begin
O_spi_mosi <= I_data_in[0] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b1 ;
end
default:R_tx_state <= 4'd0 ;
endcase
end
else if(I_rx_en) // 接收使能信号打开的情况下
begin
O_spi_cs <= 1'b0 ; // 拉低片选信号CS
case(R_rx_state)
4'd0, 4'd2 , 4'd4 , 4'd6 ,
4'd8, 4'd10, 4'd12, 4'd14 : //整合偶数状态
begin
O_spi_sck <= 1'b0 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
end
4'd1: // 接收第7位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[7] <= I_spi_miso ;
end
4'd3: // 接收第6位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[6] <= I_spi_miso ;
end
4'd5: // 接收第5位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[5] <= I_spi_miso ;
end
4'd7: // 接收第4位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[4] <= I_spi_miso ;
end
4'd9: // 接收第3位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[3] <= I_spi_miso ;
end
4'd11: // 接收第2位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[2] <= I_spi_miso ;
end
4'd13: // 接收第1位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[1] <= I_spi_miso ;
end
4'd15: // 接收第0位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b1 ;
O_data_out[0] <= I_spi_miso ;
end
default:R_rx_state <= 4'd0 ;
endcase
end
else
begin
R_tx_state <= 4'd0 ;
R_rx_state <= 4'd0 ;
O_tx_done <= 1'b0 ;
O_rx_done <= 1'b0 ;
O_spi_cs <= 1'b1 ;
O_spi_sck <= 1'b0 ;
O_spi_mosi <= 1'b0 ;
O_data_out <= 8'd0 ;
end
end
endmodule
参考文献:
正点原子、野火FPGA
【接口时序】4、SPI总线的原理与Verilog实现 - jgliu - 博客园 (cnblogs.com)