SPI通信方式的详解

Serial Peripheral Interface

SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步的通信总线。

  1. 连接图
    SPI通信方式的详解_第1张图片 CS/SS 片选信号,此信号有效表示此从机被选中通信;SCLK主机给从机的系统时钟;
    SDI/MISO主机输出给从机的数据;SDO/MOSI 从机输出给主机的数据;
    注意:三线的SPI是把SDI和SDO合并成了一个双向的SDIO(后章中有具体用法)
  2. SPI接口时序图
    SPI通信方式的详解_第2张图片
    CPOL=0;CPHA =0:
    主机在SCLK下降沿输出MOSI的数据,上升沿读取MISO上的数据
    从机在SCLK下降沿输出MISO的数据,上升沿读取MOSI上的数据(注意MISO第一个数据是在拉低CS信号就已经输出)
    CPOL=1,CPHA =1:
    主机在SCLK下降沿输出MOSI的数据,上升沿读取MISO上的数据
    从机在SCLK下降沿输出MISO的数据,上升沿读取MOSI上的数据(注意MISO第一个数据是在拉低CS信号就已经输出)
  3. SPI驱动代码
/***************************************************
*	Module Name		:	spi_master		   
*	Engineer		:	GUAN 
*	Target Device	:	EP4CE6E17C8
*	Tool versions	:	Quartus II 13.0
*	Create Date		:	2020-3-5
*	Revision		:	v1.0
*	Description		:	SPI主机控制器
***************************************************/
module spi_master
(
	input                       	sys_clk,	
	input                       	rst_n,
	output                      	cs_n,       //chip select (SPI mode) 
	output reg                   	sclk,      	//spi clock
	output                      	mosi,      	//spi master data output
	input                       	miso,      	//spi master input
	input                       	CPOL,
	input                       	CPHA,
	input     	[9:0]           	spi_div,
	input                       	cs_ctrl,
	input                       	wr_req,		
	output                      	wr_ack,
	input		[7:0]             	data_in,
	output		[7:0]             	data_out
);
localparam                   		IDLE        		= 0;
localparam                   		SCLK_RUN    		= 1;
localparam                   		ACK					= 2;
localparam                   		ACK_WAIT			= 3;
reg		[2:0]                    	cur_state ;
reg		[2:0]                  		next_state;
reg 	[9:0] 						clk_cnt;			// 分频时钟
reg 	[4:0] 						cnt;				// 计数
reg 	[7:0] 						mosi_shift;
reg 	[7:0] 						miso_shift;
reg									wr_ack_r;
wire								wr_ack_reg;
assign	cs_n 		= cs_ctrl;
assign	mosi 		= mosi_shift[7];	
assign	wr_ack 		= (cur_state == ACK);
assign	data_out 	=  miso_shift ;
//生成SPI的SCLK的二倍频率(25M)的驱动时钟用于驱动spi的操作
always @(posedge sys_clk or negedge rst_n) 
begin
    if(!rst_n) 
		clk_cnt <= 10'd0;
    else if(clk_cnt >= (spi_div-1)) 	
		clk_cnt <= 10'd0;
    else
       clk_cnt <= clk_cnt + 1'b1;
end

wire sclk_plus = (clk_cnt == (spi_div-1));
//同步时序描述状态转移
always @(posedge sys_clk or negedge rst_n) 
begin
    if(!rst_n) 
		cur_state <= IDLE;
	else
		cur_state <= next_state;
end
//组合逻辑判断状态转移条件
always@(*)
begin
	case(cur_state)
		IDLE:
			if(wr_req == 1'b1)
				next_state = SCLK_RUN;
			else
				next_state = IDLE;
		SCLK_RUN:
			if(cnt == 5'd17)
				next_state = ACK;
			else
				next_state = SCLK_RUN;
		//send one byte complete		
		ACK:
			next_state = ACK_WAIT;
		//wait for one clock cycle, to ensure that the cancel request signal
		//确保wr_req信号和cur_state==IDLE同时发生变化
		ACK_WAIT:
			next_state <= IDLE;
		default:
			next_state <= IDLE;
	endcase
end
//时序电路描述时钟控制信号的输出
always @(posedge sys_clk or negedge rst_n) begin
    //复位初始化
	if(!rst_n) begin
		sclk		<= CPOL;						
		//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
		//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
		cnt			<= 0;							
	end
	else if(cur_state != SCLK_RUN)
		cnt			<= 5'd0;
	else if(cur_state == SCLK_RUN && sclk_plus)
	begin 
		cnt        <= cnt +5'd1 ;
		case(cnt)
		5'd0:	sclk	<= CPOL;
		5'd1,5'd2,5'd3,5'd4,5'd5,5'd6,5'd7,5'd8,5'd9,5'd10,5'd11,5'd12,5'd13,5'd14,5'd15: 
				sclk	<= ~sclk; 
		5'd16: 	sclk	<= ~sclk;	  	
		endcase
	end 
end
//SPI主机数据输出
always@(posedge sys_clk or negedge rst_n)
begin
	if(!rst_n)
		mosi_shift <= 8'd0;
	else if((cur_state == SCLK_RUN) && (cnt==0))
		mosi_shift <= data_in;
	else if((cur_state == SCLK_RUN) && (cnt!=0) && sclk_plus)
		if(CPHA == 1'b0 && ((cnt!=5'd0)&&(cnt[0] == 1'b0)))    	
		//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出 cnt=cs_n/2/4/6/8/10/12/14    
			mosi_shift <= {mosi_shift[6:0],mosi_shift[7]};
		else if(CPHA == 1'b1 && ((cnt!=5'd1)&&(cnt[0] == 1'b1)))	
		//CPHA=1,在串行同步时钟的第一个跳变沿(上升或下降)数据被输出 cnt=cs_n/3/5/7/9/11/13/15 
			mosi_shift <= {mosi_shift[6:0],mosi_shift[7]};
end
//SPI主机数据采样
always@(posedge sys_clk or negedge rst_n)
begin
	if(!rst_n)
		miso_shift <= 8'd0;
	else if((cur_state == SCLK_RUN) && (cnt==0))
		miso_shift <= 8'h00;
	else if((cur_state == SCLK_RUN) && (cnt!=0) && sclk_plus)
		if(CPHA == 1'b0 && cnt[0] == 1'b1)//CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样  cnt=1/3/5/7/9/11/13/15
			miso_shift <= {miso_shift[6:0],miso};
		else if(CPHA == 1'b1 && ((cnt!=5'd0)&&(cnt[0] == 1'b0)))//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样  cnt=2/4/6/8/10/12/14/16
			miso_shift <= {miso_shift[6:0],miso};
end
endmodule 
/***************************************************
*	Module Name		:	spi_slave		   
*	Engineer		:	GUAN
*	Target Device	:	EP4CE6E17C8
*	Tool versions	:	Quartus II 13.0
*	Create Date		:	2020-3-5
*	Revision		:	v1.0
*	Description		:	SPI从机控制器
***************************************************/
module spi_slave
(
	input								clk,
	input 			 					rst_n,
	input 			 					cs_n,
	input           					sclk,
	input 		 	 					mosi,
	input                       		CPOL,
	input                       		CPHA,
	input 	  	[7:0]					slave_data_in,
	output wire		 					miso,
	output wire [7:0]					slave_data_out,
	output wire							rd_ack//recieve done,please transmit data
);
reg	[3:0]				rxd_cnt;	//从机接收计数器
reg	[3:0]				txd_cnt;	//从机发送计数器

reg [1:0]				sclk_r;
reg	[4:0]          		sclk_edge_cnt;
reg [7:0]				miso_shift;//从机输出数据寄存器
reg [7:0]				mosi_shift;//从机接收数据寄存器

reg	[1:0]				rd_ack_r;
reg						rd_ack_reg;
//捕获sclk的时钟边沿      
always@(posedge clk or negedge rst_n)
begin
    if(!rst_n)
	begin
		sclk_r[0] <= CPOL;   			//sclk of the idle state is high 
		sclk_r[1] <= CPOL;
		rd_ack_r[0]<= 1'b0;
		rd_ack_r[1]<= 1'b0;
	end
    else
	begin
		sclk_r[0] <= sclk;
		sclk_r[1] <= sclk_r[0];
		rd_ack_r[0]<= rd_ack_reg;
		rd_ack_r[1]<= rd_ack_r[0];
	end
end
assign sclk_n = ({sclk_r[0],sclk} == 2'b10);   //capture the sclk negedge
assign sclk_p = ({sclk_r[0],sclk} == 2'b01);   //capture the sclk posedge
assign rd_ack = ({rd_ack_r[1],rd_ack_r[0]} == 2'b01);
//SPI时钟SCLK的边沿计数器
always@(posedge clk or negedge rst_n)
begin
	if(!rst_n)
		sclk_edge_cnt <= 5'd0;
	else if(sclk_p ||sclk_n)
		sclk_edge_cnt <= sclk_edge_cnt + 5'd1;
	else if(rd_ack)
		sclk_edge_cnt <= 5'd0;
	else
		sclk_edge_cnt <= sclk_edge_cnt;
end
//SPI从机数据输出
always@(posedge clk or posedge rst_n)
begin
	if(!rst_n)
	begin 
		miso_shift	<= 8'd0;
		txd_cnt		<= 4'd0;
	end 
	//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
	//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样	
	else if ((CPOL == 1'b1) && (CPHA == 1'b1))
	begin    		 			
		if(cs_n || rd_ack)
			txd_cnt		<= 1'b0;
		else if((txd_cnt == 4'd0) && (!cs_n))
		begin
			miso_shift	<= slave_data_in;				
			txd_cnt		<=  txd_cnt + 1'b1;
		end
		else if(sclk_n)
		begin//SCLK下降沿输出MISO的数据,
			if((sclk_edge_cnt!= 5'd0) && (sclk_edge_cnt[0] == 1'b0))  
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件	sclk_edge_cnt=cs_n/2/4/6/8/10/12/14
				miso_shift	<= {miso_shift[6:0],miso_shift[7]};	
			txd_cnt	<=  txd_cnt + 1'b1;
		end 
	end 
	//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
	//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
	else if ((CPOL == 1'b1) && (CPHA == 1'b0))
	begin 
		if(cs_n || rd_ack)
			txd_cnt		<= 1'b0;
		else if((txd_cnt == 4'd0) && (!cs_n))
		begin
			miso_shift	<= slave_data_in;				
			txd_cnt		<=  txd_cnt + 1'b1;
		end
		else if(sclk_p)
		begin 	//SCLK上升沿输出MISO的数据,
			if(sclk_edge_cnt[0] == 1'b1)  								
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件  sclk_edge_cnt=cs_n/1/3/5/7/9/11/13
				miso_shift	<= {miso_shift[6:0],miso_shift[7]};	
			txd_cnt	<=  txd_cnt + 1'b1;
		end 
	end
	//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
	//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
	else if ((CPOL == 1'b0) && (CPHA == 1'b0))
	begin 						
		if(cs_n || rd_ack)
				txd_cnt		<= 1'b0;
		else if((txd_cnt == 4'd0) && (!cs_n))
		begin
			miso_shift	<= slave_data_in;				
			txd_cnt		<=  txd_cnt + 1'b1;
		end
		else if(sclk_n)
		begin 			//SCLK下降沿输出MISO的数据,
			if(sclk_edge_cnt[0] == 1'b1)									
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=cs_n/1/3/5/7/9/11/13
				miso_shift	<= {miso_shift[6:0],miso_shift[7]};	
			txd_cnt	<=  txd_cnt + 1'b1;
		end 																	
	end
	//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
	//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	else if ((CPOL == 1'b0) && (CPHA == 1'b1))
	begin 
		if(cs_n || rd_ack)
				txd_cnt		<= 1'b0;
		else if((txd_cnt == 4'd0) && (!cs_n))
		begin
			miso_shift	<= slave_data_in;				
			txd_cnt		<=  txd_cnt + 1'b1;
		end
		else if(sclk_p)
		begin 	//SCLK上升沿输出MISO的数据,
			if((sclk_edge_cnt!= 5'd0) && (sclk_edge_cnt[0] == 1'b0))	
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=cs_n/2/4/6/8/10/12/14
				miso_shift	<= {miso_shift[6:0],miso_shift[7]};	
			txd_cnt	<=  txd_cnt + 1'b1;
		end 	
	end
end 

assign miso = miso_shift[7];  

//SPI从机数据采样
always@(posedge clk or posedge rst_n)
begin
	if(!rst_n)
	begin
		mosi_shift	<= 8'd0;
		rxd_cnt		<= 4'd0;
		rd_ack_reg	<= 1'b0;
	end
	//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
	//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样	
	else if ((CPOL == 1'b1) && (CPHA == 1'b1))
	begin							
		if(cs_n)
			mosi_shift<= 8'h00;
		else if(rd_ack)
			rxd_cnt	<= 1'b0;
		else if(sclk_p && !cs_n)
		begin 				//上升沿采样
			if(sclk_edge_cnt[0] == 1'b1)					
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=1/3/5/7/9/11/13/15
				mosi_shift	<= {mosi_shift[6:0],mosi};
			rxd_cnt	<= rxd_cnt + 1'b1;
			if(rxd_cnt == 4'd7)
				rd_ack_reg	<= 1'b1;
			else
				rd_ack_reg	<= 1'b0;	
		end
	end
	//CPOL=1,串行同步时钟的空闲状态为高电平,也就是说第一个跳变沿为下降沿
	//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
	else if ((CPOL == 1'b1) && (CPHA == 1'b0))
	begin 
		if(cs_n)
			mosi_shift<= 8'h00;
		else if(rd_ack)
			rxd_cnt	<= 1'b0;
		else if(sclk_n && !cs_n)
		begin 						//下降沿采样
			if(sclk_edge_cnt[0] == 1'b0) //判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=0/2/4/6/8/10/12/14
				mosi_shift	<= {mosi_shift[6:0],mosi};
			rxd_cnt	<= rxd_cnt + 1'b1;
			if(rxd_cnt == 4'd7)
				rd_ack_reg	<= 1'b1;
			else
				rd_ack_reg	<= 1'b0;	
		end
	end
	//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
	//CPHA=0,在串行同步时钟的第二个跳变沿(上升或下降)数据被输出
	else if ((CPOL == 1'b0) && (CPHA == 1'b0))
	begin 
		if(cs_n)
			mosi_shift<= 8'h00;
		else if(rd_ack)
			rxd_cnt	<= 1'b0;
		else if(sclk_p && !cs_n)
		begin 						//上升沿采样
			if(sclk_edge_cnt[0] == 1'b0)					
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=0/2/4/6/8/10/12/14
				mosi_shift	<= {mosi_shift[6:0],mosi};
			rxd_cnt	<= rxd_cnt + 1'b1;
			if(rxd_cnt == 4'd7)
				rd_ack_reg	<= 1'b1;
			else
				rd_ack_reg	<= 1'b0;	
		end
	end
	//CPOL=0,串行同步时钟的空闲状态为低电平,也就是说第一个跳变沿为上升沿
	//CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	else if ((CPOL == 1'b0) && (CPHA == 1'b1))begin 
		if(cs_n)
			mosi_shift<= 8'h00;
		else if(rd_ack)
			rxd_cnt	<= 1'b0;
		else if(sclk_n && !cs_n)begin 						//下降沿采样
			if(sclk_edge_cnt[0] == 1'b1)					
			//判断值sclk_edge_cnt因延迟一拍故减去1作为条件 sclk_edge_cnt=1/3/5/7/9/11/13/15
				mosi_shift	<= {mosi_shift[6:0],mosi};
			rxd_cnt	<= rxd_cnt + 1'b1;
			if(rxd_cnt == 4'd7)
				rd_ack_reg	<= 1'b1;
			else
				rd_ack_reg	<= 1'b0;	
		end
	end
end

assign slave_data_out = rd_ack ? mosi_shift : slave_data_out ; 
  
endmodule 

若你驱动的设备为三线制(SDI和SDO合并成了一个双向的SDIO)的SPI,应该如何去驱动呢?请大家接着往下看,让我们直接在顶层例化中去完成:

spi_master u3(
	.sys_clk                   		(clk                  		),
	.rst_n	                       	(rst_n           		  	),
	.cs_n                         	(rtc_nce                	),
	.sclk                         	(rtc_sclk            	  	),
	.mosi                         	(mosi		               	),
	.miso                         	(rtc_io                 	),
	.spi_div                       	(16'd51				       	),  
	//sclk speed = clk speed /(spi_div+1)/2 
	.CPOL                          	(1'b0                	  	),
	.CPHA                          	(1'b0                 	  	),
	.ncs_ctrl                      	(ncs_ctrl               	),
	.wr_req                        	(spi_wr_req             	),
	.wr_ack                        	(spi_wr_ack             	),
	.data_in                       	(spi_send_data          	),
	.data_out                     	(spi_data_recv          	)
);

assign rtc_io    = ~rtc_io_dir ? mosi : 1'bz;

你可能感兴趣的:(FPGA之协议设计)