FPGA - SPI总线介绍以及通用接口模块设计

一,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

相关推荐
FakeOccupational1 天前
fpga系列 HDL : Microchip FPGA开发软件 Libero 中导出和导入引脚约束配置
fpga开发
贝塔实验室1 天前
LDPC 码的构造方法
算法·fpga开发·硬件工程·动态规划·信息与通信·信号处理·基带工程
Moonnnn.1 天前
【FPGA】时序逻辑计数器——仿真验证
fpga开发
三贝勒文子1 天前
Synopsys 逻辑综合之 ICG
fpga开发·eda·synopsys·时序综合
byte轻骑兵1 天前
【驱动设计的硬件基础】CPLD和FPGA
fpga开发·cpld
dadaobusi1 天前
看到一段SVA代码,让AI解释了一下
单片机·嵌入式硬件·fpga开发
G2突破手2591 天前
FMC、FMC+ 详解
fpga开发
fpga和matlab1 天前
FPGA时序约束分析4——Reg2Reg路径的建立时间与保持时间分析
fpga开发·reg2reg·建立时间·保持时间
高沉1 天前
2025华为海思数字IC面经
华为·fpga开发
伊宇韵1 天前
FPGA - GTX收发器-K码 以及 IBERT IP核使用
fpga开发