一,SPI总线
1,SPI总线概述
SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。串行外设接口总线(SPI )**,是一种高速的,全双工,同步的通信总线,**并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。
SPI系统可直接与各个厂家生产的多种标准外围器件接口,它只需4条线:串行时钟线(SCK)、主机输入/从机输出数据线(MISO)、主机输出/从机输入数据线(MOSI) 和低电平有效的从机选择线(NSS)。
(1)MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
(2)MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
(3)SCK:串口时钟,作为主设备的输出,从设备的输入。
(4)NSS:从设备选择。这是一个可选的引脚,用来选择主/从设备。它的功能是用来作为片选引脚,让主设备可以单独地与特定从设备通信,避免数据线上的冲突。
SPI是一个环形总线结构,由NSS、SCK、MISO、MOSI构成,NSS引脚设置为输入,MOSI引脚相互连接,MISO引脚相互连接,数据在主和从之间串行地传输(MSB位在前)。
2,电路连接
下图表示基本的SPI设备 连接示意图。片选信号NSS通常低电平有效。SPI数据传输原理是基于主从设备内部移位寄存器 的数据交换。在主设备SCK的控制下,待传数据由各自设备的数据寄存器 (Data Register)传输到移位寄存器(Shift Register),再通过MOSI和MISO信号线完成主从设备间的数据交换。
主从设备间数据交换逻辑示意图如下图所示:
3,硬件拓扑
(1)单主机单从机
(2)单主机多从机(片选方式)
每个从设备都需要单独的片选信号,主设备每次只能选择其中一个从设备进行通信。因为所有从设备的SCK、MOSI、MISO都是连在一起的,未被选中从设备的MISO要表现为高阻状态(Hi-Z)以避免数据传输错误。由于每个设备都需要单独的片选信号,如果需要的片选信号过多,可以使用译码器产生所有的片选信号。
(3)菊花链方式
数据信号经过主从设备所有的移位寄存器构成闭环。数据通过主设备发送 (绿色线)经过从设备返回 (蓝色线)到主设备。在这种方式下,片选和时钟同时接到所有从设备,通常用于移位寄存器和LED驱动器。注意,菊花链方式的主设备需要发送足够长的数据以确保数据送达到所有从设备。切记主设备所发送的第一个数据需(移位)到达菊花链中最后一个从设备。
菊花链式连接常用于仅需主设备发送数据而不需要接收返回数据的场合,如LED驱动器。在这种应用下,主设备MISO可以不连。如果需要接收从设备的返回数据,则需要连接主设备的MISO形成闭环。同样地,切记要发送足够多的接收指令以确保数据(移位)送达主设备
4,SPI传输模式
通过设置控制寄存器SPICR1 中的CPOL(时钟极性) 和CPHA(时钟相位 ) ,将SPI可以分成四种传输模式。
CPOL ,即C lock Pol arity,决定时钟空闲时的电平为高或低。对于SPI数据传输格式没有显著影响。
1 = 时钟低电平时有效,空闲时为高
0 = 时钟高电平时有效,空闲时为低
CPHA ,即C lock Pha se,定义SPI数据传输的两种基本模式。
1 = 数据采样发生在时钟(SCK)偶数(2,4,6,...,16)边沿(包括上下边沿)
0 = 数据采样发生在时钟(SCK)奇数(1,3,5,...,15)边沿(包括上下边沿)
四种模式如下图所示:
先看第一列 两张图(CPHA = 0 ),采样发生在第一个时钟跳变沿,即数据采样发生在SCK奇数边沿;
再看第二列 (CPHA =1 ),采样发生在第二个时钟跳变沿,即数据采样发生在SCK偶数边沿。
第一行 两张图(CPOL = 0 ),SCK空闲状态为低电平,
第二行 两张图(CPOL = 1 ),SCK空闲状态为高电平。
主从设备进行SPI通讯时,要确保它们的传输模式设置相同。对于某些场合,可能需要调整CPOL/CPHA设置以满足设备特定要求。
5,SPI时序图
CPHA = 0
- 有些器件在片选后数据立即出现在MOSI/MISO管脚,数据锁存于第一个时钟边沿
- 片选SS先于SCK半个时钟有效
- 在SCK的第二个时钟边沿,上个时钟边沿锁存的数据写入移位寄存器(MSB或LSB)
- 以此类推,数据在奇数边沿锁存,在偶数边沿写入移位寄存器
- 经过16个时钟边沿后,串行传输的数据全部写入(并行的)移位寄存器,完成主从设备的数据交换
CPHA = 1
- 有些设备要求数据输出在SCK第一个时钟边沿之后,数据锁存于第二个时钟边沿
- 片选SS先于SCK半个时钟有效
- 在SCK的第三个时钟边沿,上个时钟边沿锁存的数据写入移位寄存器(MSB或LSB)
- 以此类推,数据在偶数边沿锁存,在奇数边沿写入移位寄存器
- 经过16个时钟边沿后,串行传输的数据全部写入(并行的)移位寄存器,完成主从设备的数据交换
二,SPI 通用接口用户端模块-Verilog代码设计
1,SPI通用接口用户端设计框图
分析:
1,设计分频计数器(div_cnt)产生SCLK
2,设计比特计数器(bit_cnt) sdo变化
3,设计字节计数器(byte_cnt)
4,定义一个send_data 一直向右移位 把最低位send_data[0]给sdo
5,send_data向右移位,把最低位给sdo,在start信号为高的时候 ,把cmd赋值给send_data
6,CS在start信号为高的时候拉高 ,发完所有数据拉高
2,根据简单分析编写代码
// -----------------------------------------------------------------------------
// Author : RLG
// File : spi_master.v
// Create : 2024-03-17 14:35:00
// Revise : 2024-03-17 16:20:40
// -----------------------------------------------------------------------------
`timescale 1ns / 1ps
module spi_master #(
parameter SYS_CLK_FRWQ = 50000000,
parameter SPI_CLK_FREQ = 12500000,
parameter ADDR_WIDTH = 24
)
(
input clk ,
input reset ,
//SPI的物理接口
output reg spi_sck ,
output reg spi_cs ,
output spi_sdo ,
input spi_sdi ,
//SPI的用户接口
input spi_start ,
input [7:0] spi_cmd ,
input [ADDR_WIDTH-1:0] spi_addr ,
input [11:0] spi_length ,
output reg spi_busy ,
output reg spi_wr_req ,
input [7:0] spi_wr_data ,
output reg spi_rd_vld ,
output reg [7:0] spi_rd_data
);
localparam DIV_CNT_MAX = SYS_CLK_FRWQ / SPI_CLK_FREQ - 1; //只需计算一次 复位之前已经计算好了
localparam DIV_CNT_MAX_HALE = DIV_CNT_MAX / 2;
//定义3个计数器
reg [$clog2(DIV_CNT_MAX) - 1 :0] div_cnt ; //$clog2函数自动计算最小位宽
reg [7:0] bit_cnt ;
reg [12:0] byte_cnt ;
reg [11:0] spi_length_d0;
reg [ADDR_WIDTH-1:0] spi_addr_d0 ;
reg [7:0] send_data;
//锁存spi_length
always @(posedge clk )
if(spi_start) begin
spi_length_d0 <= spi_length;
end
//分频计数器
always @(posedge clk ) begin
if(reset)
div_cnt <= 0;
else if(spi_cs)
div_cnt <= 0;
else if(div_cnt == DIV_CNT_MAX)
div_cnt <= 0;
else if(~spi_cs)
div_cnt <= div_cnt + 1;
end
//比特计数器
always @(posedge clk ) begin
if(reset)
bit_cnt <= 0;
else if(spi_cs)
bit_cnt <= 0;
else if(bit_cnt == 7 &&div_cnt == DIV_CNT_MAX )
bit_cnt <= 0;
else if (div_cnt == DIV_CNT_MAX)
bit_cnt <= bit_cnt + 1;
end
//字节计数器
always @(posedge clk ) begin
if(reset)
byte_cnt <= 0;
else if(spi_cs)
byte_cnt <= 0;
else if(byte_cnt == (spi_length_d0 + ADDR_WIDTH/8) && div_cnt == DIV_CNT_MAX && bit_cnt == 7)
byte_cnt <= 0;
else if (div_cnt == DIV_CNT_MAX && bit_cnt == 7)
byte_cnt <= byte_cnt + 1;
end
//片选信号
always @(posedge clk)
if(reset)
spi_cs <= 1'b1;
else if(byte_cnt ==(spi_length_d0 + ADDR_WIDTH/8) && div_cnt == DIV_CNT_MAX && bit_cnt == 7)
spi_cs <= 1'b1;
else if(spi_start)
spi_cs <= 1'b0;
//SCK时钟信号
always @(posedge clk ) begin
if(reset)
spi_sck <= 0;
else if(div_cnt == DIV_CNT_MAX)
spi_sck <= 0;
else if(div_cnt == DIV_CNT_MAX_HALE )
spi_sck <= 1;
end
//spi_addr_d0
always @(posedge clk) begin
if (spi_start) begin
spi_addr_d0 <= spi_addr;
end
else if (byte_cnt <= ADDR_WIDTH/8 && div_cnt == DIV_CNT_MAX && bit_cnt == 7) begin
spi_addr_d0 <= spi_addr_d0 >> 8;
end
else begin
spi_addr_d0 <= spi_addr_d0;
end
end
//send_data
always @(posedge clk) begin
if (reset) begin
send_data <= 0;
end
else if (spi_start) begin
send_data <= spi_cmd;
end
else if(byte_cnt <= ADDR_WIDTH/8 -1 && div_cnt == DIV_CNT_MAX && bit_cnt == 7) begin
send_data <= spi_addr_d0[7:0]; //发送地址
end
else if(byte_cnt >= ADDR_WIDTH/8 && div_cnt == DIV_CNT_MAX && bit_cnt == 7) begin
send_data <= spi_wr_data;
end
else begin
send_data <= send_data >> 1;
end
end
assign spi_sdo = send_data[0];
always @(posedge clk ) begin
if (reset)
spi_wr_req <= 0;
else if (~spi_cs && byte_cnt >= ADDR_WIDTH/8 && div_cnt == DIV_CNT_MAX - 2 && bit_cnt == 7) begin
spi_wr_req <= 1;
end
else begin
spi_wr_req <= 0;
end
end
always @(posedge clk) begin
if (reset)
spi_rd_data <= 0;
else if (byte_cnt >= ADDR_WIDTH/8 + 1 && div_cnt == DIV_CNT_MAX )
spi_rd_data <= {spi_sdi,spi_rd_data[7:1]};
else
spi_rd_data <= spi_rd_data;
end
always @(posedge clk) begin
if (byte_cnt >= ADDR_WIDTH/8 + 1 && div_cnt == DIV_CNT_MAX - 2 && bit_cnt == 7) begin
spi_rd_vld <= 1;
end
else begin
spi_rd_vld <= 0;
end
end
always @(posedge clk ) begin
spi_busy <= ~spi_cs;
end
endmodule
3,编写测试文件:
// -----------------------------------------------------------------------------
// Author : RLG
// File : tb_spi_master.v
// Create : 2024-03-17 15:47:19
// Revise : 2024-03-17 15:57:44
// -----------------------------------------------------------------------------
`timescale 1ns / 1ps
module tb_spi_master();
parameter SYS_CLK_FRWQ = 50000000;
parameter SPI_CLK_FREQ = 12500000;
parameter ADDR_WIDTH = 24;
reg clk ;
reg reset ;
wire spi_sck ;
wire spi_cs ;
wire spi_sdo ;
wire spi_sdi = 1 ;
reg spi_start ;
wire [7:0] spi_cmd = 8'h0a ;
wire [ADDR_WIDTH-1:0] spi_addr = 24'haabbcc;
wire [11:0] spi_length = 5 ;
wire spi_busy ;
wire spi_wr_req ;
wire [7:0] spi_wr_data = 8'haa ;
wire spi_rd_vld ;
wire [7:0] spi_rd_data ;
spi_master #(
.SYS_CLK_FRWQ(SYS_CLK_FRWQ),
.SPI_CLK_FREQ(SPI_CLK_FREQ),
.ADDR_WIDTH(ADDR_WIDTH)
) inst_spi_master (
.clk (clk),
.reset (reset),
.spi_sck (spi_sck),
.spi_cs (spi_cs),
.spi_sdo (spi_sdo),
.spi_sdi (spi_sdi),
.spi_start (spi_start),
.spi_cmd (spi_cmd),
.spi_addr (spi_addr),
.spi_length (spi_length),
.spi_busy (spi_busy),
.spi_wr_req (spi_wr_req),
.spi_wr_data (spi_wr_data),
.spi_rd_vld (spi_rd_vld),
.spi_rd_data (spi_rd_data)
);
// initial clk = 0;
// always #10 clk = ~clk;
initial begin
clk = 0;
forever #(10)
clk = ~clk;
end
initial begin
reset = 1;
#200;
reset = 0;
end
initial begin
spi_start <= 0;
#290;
spi_start <= 1;
#20;
spi_start <= 0;
#8000;
$stop;
end
endmodule
4,仿真波形
spi_addr = 24'haabbcc(给从机的地址) 移位传输 ,分别在
byte_cnt == 1时 传输8'hcc
byte_cnt == 2时 传输8'hbb
byte_cnt == 3时 传输8'hcc