FPGA常用接口协议--SPI

前言

  SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司最早于1980年代推出的一种同步串行接口技术,其最早应用于M68系列微控制器与外围IC通信。SPI是一种高速的、全双工、同步的通信总线, 常用于MCU和 EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。现如今,SPI总线已经成为被广泛应用的一种数据传输方式,由于其简单的接口、灵活性和易用性,SPI 已成为一种标准,SPI被半导体制造商广泛应用于IC芯片。

  以下有部分内容摘自Motorola官方手册,如有理解差异,请参考原手册。

1 简介

  如图1-1所示为SPI结构框图,框图主要包含状态、控制和数据寄存器、移位逻辑控制、波特率发生器、主/从控制逻辑和端口控制逻辑。

1.1 概述

  SPI模块允许MCU和外围设备之间进行双工、同步、串行通信设备。软件可以轮询SPI状态标志,或者SPI操作可以由中断驱动,在上述SPI框图中体现了在MCU中SPI的驱动原理

1.2 特性

  SPI包括以下特性:
  •主模式和从模式
  •双向模式
  •从选择输出
  •具有CPU中断功能的模式故障错误标志
  •双缓冲数据寄存器
  •具有可编程极性和相位的串行时钟
  •等待模式下SPI操作的控制
  在MCU中,上述特性中相关模式选择通过控制寄存器控制,相关状态通过状态寄存器可以查询;在有功耗要求的场景下可以切换到等待模式或停止模式降低功耗关闭部分功能。

2 外部信号描述

  SPI通过4个外部引脚与外部设备通信,以下分别列出:

2.1 Master out/slave in (MOSI)

  当SPI模块被配置为“主”时,此引脚用于将数据传输出SPI模块,当SPI模块配置为“从”时,该引脚用于接收数据。

2.2 Master in/slave out (MISO)

  当SPI模块被配置为“从”时,此引脚用于将数据传输出SPI模块,当SPI模块配置为“主”时,该引脚用于接收数据。

2.3 Slave select ( SS ‾ \overline{\text{SS}} SS)

  该引脚用于将选择信号从SPI模块输出到另一外围设备,当其被配置为“主”时,数据传输将与该外围设备一起进行,当SPI被配置为“从”时,该引脚被用作接收从选择信号的输入。

2.4 Serial clock(SCK)

  该引脚用于输出SPI传输数据的时钟,或者在Slave的情况下接收时钟。

3 协议解析

  在MCU集成的SPI驱动中,MCU可以通过SPI驱动和外围设备之间进行双工、同步、串行通信设备,通过软件配置可以轮询SPI状态标志,或者SPI操作可以由中断驱动,当然FPGA也可以实现同样的功能,但在大多数时候,FPGA作为主机通过SPI固定的模式与外围设备通信,一些状态寄存器和配置寄存器可以配置为固定的值,简化了设计。

3.1 数据传输方式

  SPI为串行传输,发送数据时,在同步时钟下,一拍一拍的将数据串行移位发送,接收数据同样在同步时钟下,一拍一拍的将数据串行移位接收;在SPI传输期间。串行时钟(SCK)使两条串行数据线上的信息的移位和采样同步;片选信号允许选择单个从SPI设备,未被选择的从设备不会干扰SPI总线活动;有多个从设备时,可以通过片选信号分时选择与从设备通信。

3.2 时钟相位和极性控制

  SPI有不同的四种模式,通过控制同步时钟的极性和相位可以实现不同模式的切换,在与不同的外围设备通信时,可以切换不同的模式。
  CPOL时钟极性控制位指定有效的高或低时钟,(Motorola手册描述对传输格式没有显著影响),具体的极性选择与外围设备时序要求一致即可。
  CPHA时钟相位控制可以选择在时钟的哪个相位发送/采样数据。
  主SPI设备和通信从设备的时钟相位和极性应相同。在一些情况下,在传输之间改变相位和极性,以允许主设备与具有不同要求的外围从设备通信。

3.3 CPHA = 0时传输形式

  从如下图中看到,当CPOL(时钟极性控制)为0时,IDLE(空闲)状态,时钟保持为0,当CPOL(时钟极性控制)为1时,IDLE(空闲)状态,时钟保持为1。
  SCK第一个边沿前tL个时钟周期拉低SS(片选)信号,此时发送第一个数据,从图中可以看出,发送数据在第0、2、4、6、7、10、12、14个时钟边沿时,即偶数沿;采样数据在第1、3、5、7、9、11、13、15个时钟边沿时,即奇数沿;数据可以是MSB first(高位在前),也可以LSB first(低位在前),在FPGA实现中一般为高位在前,具体与从设备一致即可。
  在发送/采样结束之后,在最后一个SCK沿后再等待tT时间回到IDLE(空闲)状态,IDLE(空闲)状态至少保持tI 再进行下一次传输。
  从图中看出:tL、tT、tI最少保持二分之一个时钟周期。

3.4 CPHA = 1时传输形式

  从如下图中看到,同样,当CPOL(时钟极性控制)为0时,IDLE(空闲)状态,时钟保持为0,当CPOL(时钟极性控制)为1时,IDLE(空闲)状态,时钟保持为1。
  SCK第一个边沿前tL个时钟周期拉低SS(片选)信号,在第一个时钟沿时发送第一个数据,从图中可以看出,发送数据在第1、3、5、7、9、11、13、15个时钟边沿时,即奇数沿;采样数据在第2、4、6、8、10、12、14、16个时钟边沿时,即偶数沿;数据可以是MSB first(高位在前),也可以LSB first(低位在前),在FPGA实现中一般为高位在前,具体与从设备一致即可。
  在发送/采样结束之后,在最后一个SCK沿后再等待tT时间回到IDLE(空闲)状态,IDLE(空闲)状态至少保持tI 再进行下一次传输。
  从图中看出:tL、tT、tI最少保持二分之一个时钟周期。

4 Verilog描述

4.1 SPI主机代码实现

  在实际FPGA应用中,为了对第1章节中框图中的SPI结构进行简化,通常只需其中的串行数据移位控制、波特率生成,同时为了应用于不同的外围设备,可以将SPI模式作为参数进行传递。
  通常通过FPGA控制SPI通信应用中,主要为FLASH读写控制,外围器件寄存器读写,以上操作一般为炒作指令+地址+数据的形式,对于不同类型或不同大小的外围设备,指令长度、地址长度、寄存器长度灯都可能不一致,为了更好的适应不同的器件,如下代码可以控制指令长度、地址长度、寄存器可变的场景,方便在控制不同的外围设备时移植。


 //
//
// Author: NZ
// Create Date: 2022/10/17 21:31:30
// Design Name:spi_master.v
// Description:
// Revision:
// Revision 0.01 - File Created
//
//
`timescale 1ns/1ns

module spi_master
#(
	parameter	CPOL		        =	1'b0			,
	parameter	CPHA		        =	1'b0			,
	parameter	SYSCLK_FREQ         =	100_000_000     ,   //system clk
	parameter   SPICLK_FREQ         = 	10_000_000	    ,	//spi clk;
	parameter   MAX_CMDWIDTH 	    = 	8'd8			,	//max command width,include instr and address
	parameter   MAX_DATAWIDTH 	    = 	8'd32				//the max data width,
)
(
    input   wire           			i_rst_n     		,  	//system reset ,acitive low		     
    input   wire            		i_clk    			,   //input system clock ,100Mhz
	input	wire					i_wr_en			    ,	//spi write enable,at least one system clock cycle,active high					
	input	wire					i_rd_en			    ,	//spi read enable,at least one system clock cycle,active high
	input	wire[7:0]				i_byte_len			,	//Actual length of read or write operation data
	input	wire[MAX_CMDWIDTH-1:0]  i_cmd_word			,	//Command data
	input	wire[MAX_DATAWIDTH-1:0]	i_wr_data			,	//Data to write 
	input	wire					i_spi_miso			,	//spi device to fpga

	output	wire					o_spi_mosi			,	//fpga to spi device 
	output	wire					o_spi_csn			,	//spi chip select
	output	wire					o_spi_sck			,	//spi clock 
	output	wire[MAX_DATAWIDTH-1:0]	o_rd_data			,	//spi read data
    output	wire					o_spi_rdy			,	//if this signal is assert,spi master is idle	   
	output	wire 					o_operate_end			//After one operation, assert that at least one clock cycle is valid
);
        
        
/*************************************************************************/
/*******                        constant     			        	  ****/       
/*************************************************************************/     
	localparam 	CLK_DIV			= 	SYSCLK_FREQ/SPICLK_FREQ			;
	localparam 	ALLDATAWIDTH    = 	MAX_CMDWIDTH+MAX_DATAWIDTH      ;


	localparam 	S_IDLE			= 	3'd0			                ;
	localparam 	S_WRITE			= 	3'd1			                ;
	localparam 	S_READ			= 	3'd2			                ;
	localparam 	S_WRTRANSITION  = 	3'd3			                ;
	localparam 	S_RDTRANSITION  = 	3'd4			                ;
	localparam 	S_OPEND			= 	3'd5			                ;
	localparam 	S_HOLD			= 	3'd6			                ;
         
/*************************************************************************/
/*******                    reg / wire       						******/       
/*************************************************************************/     
	reg						    wr_start_dly1,wr_start_dly2	;			
	reg						    rd_start_dly1,rd_start_dly2	;		
	wire					    wr_start_rise				;		
	wire					    rd_start_rise				;		
	reg						    spi_flag				    ; 
	reg						    write_or_read               ;     
	wire						spi_valid_flag              ; 
	reg						    spi_cmd_flag				;
	wire					    spi_wr_flag					;
	wire					    spi_rd_flag					;
	reg					        spi_rdy					    ;
    reg		[7:0]			    sck_cnt	         			;
	wire					    sck							;
	reg						    cs_n						;
	wire                        spi_mosi                    ;	
    wire	[7:0]			    bit_cnt         			;
    reg     [ALLDATAWIDTH-1:0]  spi_wr_data    	            ;
    reg		[7:0]			    hlafclk_cnt                 ;    

    reg		[10:0]			    wr_len         				;
    reg		[10:0]			    rd_len         				;
    reg                         operate_end         		;
    reg		[10:0]			    rd_cnt         				;
    reg		[2:0]			    state         				;    
	reg     [MAX_DATAWIDTH-1:0] rd_data					    ;         


/*************************************************************************/
/*******                  spi write or read enable edge             ******/       
/*************************************************************************/ 
assign	  wr_start_rise = wr_start_dly1 & (~wr_start_dly2);
assign	  rd_start_rise = rd_start_dly1 & (~rd_start_dly2);

always @(posedge i_clk)begin
    if(!i_rst_n)begin
        wr_start_dly1 <= 1'b0;
        wr_start_dly2 <= 1'b0;
        rd_start_dly1 <= 1'b0;
        rd_start_dly2 <= 1'b0;
    end
	else begin
        wr_start_dly1 <= i_wr_en;
        wr_start_dly2 <= wr_start_dly1;
        rd_start_dly1 <= i_rd_en;
        rd_start_dly2 <= rd_start_dly1;
    end
end 

/*********************************************************************/
/*****                  spi write or read operation              *****/
/*********************************************************************/ 
always @(posedge i_clk)begin
    if(!i_rst_n)begin 
		spi_flag	        <= 1'b0		;
		operate_end		    <= 1'b0		;
		cs_n			    <= 1'b1		;
		spi_wr_data		    <= 0		;		
        wr_len			    <= 11'd0    ;
        rd_len			    <= 11'd0    ;
        spi_rdy			    <= 1'b0     ;
        write_or_read       <= 1'b0     ;
		state			    <= S_IDLE	;
	end
	else begin
		case(state)
		S_IDLE : begin 
			if(wr_start_rise)begin
				state	        <= S_WRITE;
                spi_rdy         <= 1'b0;
                write_or_read           <= 1'b0;
				wr_len          <= {i_byte_len,3'b000};
				spi_wr_data     <= {i_cmd_word,i_wr_data};                
			end
			else if(rd_start_rise)begin
				state	        <= S_READ;
                spi_rdy         <= 1'b0;
                write_or_read           <= 1'b1;
				rd_len			<= {i_byte_len,3'b000};                  
				spi_wr_data 	<= {i_cmd_word,{MAX_DATAWIDTH{1'b0}}};  
			end
			else begin
                spi_rdy         <= 1'b1;
                operate_end     <= 1'b0;
				state           <= S_IDLE;
			end
		end       
		S_WRITE : begin
			if(i_byte_len==8'd0)begin
				state 	        <= S_OPEND;
			end
			else if(hlafclk_cnt==wr_len<<1 && sck_cnt==(CLK_DIV>>1)-1)begin 
				spi_flag	    <= 1'b0;
				cs_n			<= 1'b1; 
				wr_len			<= 11'd0;
				state 			<= S_OPEND;
			end
			else begin
				spi_flag 	    <= 1'b1;
				cs_n			<= 1'b0;
                state 			<= S_WRITE;
			end
			end
		S_READ : begin
			if(i_byte_len==8'd0)begin
				state <= S_OPEND;
			end
			else if(hlafclk_cnt==rd_len<<1 && sck_cnt==(CLK_DIV>>1)-1)begin
				spi_flag	    <= 1'b0	;
				cs_n			<= 1'b1	;				
				wr_len 			<= 11'd0;
				rd_len 			<= 11'd0;
				state			<= S_OPEND;
			end
			else begin
				spi_flag 	    <= 1'b1			;
				cs_n			<= 1'b0			;					
				wr_len			<= MAX_CMDWIDTH	;
			end
			end
		S_OPEND : begin
			spi_flag		    <= 1'b0;          
			wr_len				<= 11'd0;
			rd_len				<= 11'd0;
            operate_end		    <= 1'b1;
            state			    <= S_IDLE;
        end
		default : begin
			state	<= S_IDLE;
			end
		endcase
	end
end 	




always @(*)begin
    if(!i_rst_n)begin 
		spi_cmd_flag	<= 1'b0		;
	end
    else if(state == S_WRITE | state == S_READ) begin
        if(bit_cnt>MAX_CMDWIDTH-1'b1)begin
            spi_cmd_flag    <= 1'b0;
        end
        else begin
            spi_cmd_flag    <= 1'b1; 
        end
    end
    else begin
       spi_cmd_flag    <= 1'b0; 
    end  
end

assign spi_wr_flag = (spi_flag==1'b1 && write_or_read==1'b0)?~spi_cmd_flag:1'b0;
assign spi_rd_flag = (spi_flag==1'b1 && write_or_read==1'b1)?~spi_cmd_flag:1'b0;




/*************************************************************************/
/*******                     spi clk gen      						******/       
/*************************************************************************/ 
always	@(posedge i_clk)begin
	if(!i_rst_n)begin
		sck_cnt <= 8'd0;
	end
	else if(spi_flag && sck_cnt==(CLK_DIV>>1)-1'b1)begin
		sck_cnt <= 0;
	end		
	else if(spi_flag) begin
		sck_cnt <= sck_cnt + 1'b1;
	end
	else begin
		sck_cnt <= 0;
	end
end 

assign sck =    (spi_flag)?
                CPOL?~hlafclk_cnt[0]:hlafclk_cnt[0]
                :CPOL;

always @(posedge i_clk)begin
    if(!i_rst_n)begin
        hlafclk_cnt <= 0;
    end
    else if(cs_n==0 && hlafclk_cnt==i_byte_len<<(3+1) && sck_cnt==(CLK_DIV>>1)-1)begin
        hlafclk_cnt <= 0;
    end
    else if(cs_n==0 && sck_cnt==(CLK_DIV>>1)-1)begin
        hlafclk_cnt <= hlafclk_cnt + 1'b1;
    end
    else if(cs_n==1)begin
        hlafclk_cnt <= 0;
    end
end


/*************************************************************************/
/*******                      spi Write      						******/       
/*************************************************************************/ 
 
assign bit_cnt          = hlafclk_cnt>>1 ; 

assign spi_valid_flag   = hlafclk_cnt>>1 ; 

assign spi_mosi         = spi_flag?spi_wr_data[ALLDATAWIDTH - 1'b1 - bit_cnt]:1'b0;



/*********************************************************/
/*****                  spi read                     *****/
/*********************************************************/ 
always	@(posedge i_clk)begin
	if(!i_rst_n)begin 
		rd_cnt		<= 11'd0;
		rd_data     <= {MAX_DATAWIDTH{1'b0}};
	end
    else if(spi_rd_flag==1'b0)begin
        rd_cnt		<= 11'd0;
    end
	else if(spi_rd_flag && sck_cnt==(CLK_DIV>>1)-1'b1)begin
		rd_cnt		<= rd_cnt + 1'b1;
		rd_data[MAX_DATAWIDTH-1'b1-rd_cnt] <= i_spi_miso;
    end
	else begin
        rd_data <= rd_data;
	end
end 

/*************************************************************************/ 
//**************				OUTPUT					   **************//
/*************************************************************************/ 
   
assign  o_spi_mosi      =   spi_mosi    ;
assign  o_spi_csn       =   cs_n        ;
assign  o_spi_sck       =   sck         ;
assign  o_spi_rdy       =   spi_rdy     ;
assign  o_operate_end   =   operate_end ;
assign  o_rd_data       =   rd_data     ;

 
endmodule 


4.2 Testbench

 //
//
// Author: NZ
// Create Date: 2022/10/17 21:31:30
// Design Name:spi_master_tb.v
// Description:
// Revision:
// Revision 0.01 - File Created
//
//
`timescale 1ns/1ns

module spi_master_tb();


	reg			i_rst_n     		;	
	reg			i_clk    			;
	reg			i_wr_start			;
	reg			i_rd_start			;
	reg[7:0]	i_byte_len			;
	reg[7:0]	i_cmd_word			;
	reg[31:0]	i_wr_data			;
	reg			i_spi_sdo			;

	wire		o_spi_sdi			;
	wire		o_spi_rdy			;
	wire		o_spi_csn			;
	wire		o_spi_clk			;
	wire[31:0]	o_rd_data			;
	wire		o_operate_end		;
    reg[31:0]	rd_data             ;




always #5  i_clk = ~i_clk;

// 初始化
initial
begin
    i_clk = 0;
    i_rst_n = 0;
    #100;
    i_rst_n = 1;	
    end 




task	spi_write(
	input	[7:0]	cmd_word		,	
	input	[7:0]	byte_len		,	
	input	[255:0]	spi_wr_data
);
	integer i ;
	reg[7:0]	data_temp	;
	
begin	
	i_cmd_word		= 	cmd_word	;
	i_byte_len		= 	byte_len	;
	i_wr_data 		= spi_wr_data	;
	i_wr_start = 1;
	#20 
		i_wr_start = 0;
    #3000;
end
endtask	


initial
begin
    i_spi_sdo 		= 1'b1;
    i_wr_start 		= 1'b0;
    i_rd_start 		= 1'b0;
    i_cmd_word 		= 8'b0;
    i_byte_len 		= 8'b0;
    i_wr_data 		= 32'b0;
    rd_data 		= 32'b0;
    #100;
	spi_write(8'h55,8'h02,32'ha5a22a78);
    #300;        
	spi_write(8'haa,8'h03,32'ha5a22a78);
    #300;
	$stop;
    end 
	

always  #100 i_spi_sdo = $random;
	


//***************************************************
// 例化顶层文件
spi_master 
#(
    .CPOL               (0                  ),
    .CPHA               (0                  ),
    .MAX_CMDWIDTH       (8                  ),
    .MAX_DATAWIDTH      (32                 )
)
spi_master_inst(
	.i_rst_n     		(i_rst_n     		),
	.i_clk    			(i_clk    			),
	.i_wr_en			(i_wr_start			),
	.i_rd_en			(i_rd_start			),
	.i_byte_len			(i_byte_len			),
	.i_cmd_word			(i_cmd_word			),
	.i_wr_data			(i_wr_data			),
	.i_spi_miso			(i_spi_sdo			),

	.o_spi_mosi			(o_spi_sdi			),
	.o_spi_rdy			(o_spi_rdy			),
	.o_spi_csn			(o_spi_csn			),
	.o_spi_sck			(o_spi_clk			),
	.o_rd_data			(o_rd_data			),
	.o_operate_end		(o_operate_end		)
);
 
	
	
endmodule

4.3 仿真

  如下图所示:将模式设置为CPOL=0,CPHA=0,此时,将最大数据位宽设置为32bit,命令+数据设置有效字节为2byte,命令放在最先发送的字节,命令字节设置为0x55,从图中看出在片选信号拉低时发送第一bit数据,然后依次发送,分别为0、1、0、1、0、1、0、1,紧接着发送数据,由于定义为32bit 数据,从高字节开始,即有效位为为最高字节0xa5,与协议中模式0对比时钟极性为0,数据在偶数个时钟发送,与协议一致
在这里插入图片描述

  有兴趣可以尝试更改SPI模式或字节长度查看是否满足其他模式时序要求

  欢迎相互讨论交流!!!

参考文档:SPI Block Guide V04.01

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值