LinCoding

【原创】详细解析FPGA与STM32的SPI通信(二)

1
阅读(14892)

【主题】:详细解析FPGA与STM32的SPI通信(二) 

【作者】:LinCoding

【时间】:2016.11.26

【声明】:转载、引用,请注明出处

       本篇文章承接——详细解析FPGA与STM32的SPI通信(一),真是内容有点多,不得不分成两篇文章来讲。上文说道用FPGA来模仿STM32发出的SPI的协议。

       1、SPI_Receiver模块的程序:

module spi_receiver
(
	input				clk,		//global clock
	input				rst_n,		//global reset
			
	input				spi_cs,
	input				spi_sck,
	input				spi_mosi,
	
	output	reg	[7:0]	        rxd_data,
	output	reg			rxd_flag
);

  第一部分是输入输出定义,没什么可说的,对于接收数据的模块,要增加接收完成标志信号,以便其他模块读取数据

//-----------------------------------
//synchronize the input signal
reg	spi_cs_r0,	spi_cs_r1;
reg	spi_sck_r0,	spi_sck_r1;
reg	spi_mosi_r0,spi_mosi_r1;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			spi_cs_r0	<= 1'b1;		spi_cs_r1	<= 1'b1;
                        spi_sck_r0	<= 1'b0;		spi_sck_r1	<= 1'b0;
                        spi_mosi_r0	<= 1'b0;		spi_mosi_r1	<= 1'b0;
		end
	else
		begin
			spi_cs_r0	<= spi_cs;		spi_cs_r1	<= spi_cs_r0;
                        spi_sck_r0	<= spi_sck;		spi_sck_r1	<= spi_sck_r0;
                        spi_mosi_r0	<= spi_mosi;	        spi_mosi_r1	<= spi_mosi_r0;			
		end
end
reg	[3:0]	rxd_cnt /*synthesis noprune*/;
wire	mcu_cs	= spi_cs_r1;
wire	mcu_data= spi_mosi_r1;
wire	mcu_read_flag = ( spi_sck_r0 & ~spi_sck_r1) ? 1'b1 : 1'b0;	//sck posedge capture
wire	mcu_read_done = ( spi_cs_r0 & ~spi_cs_r1 & (rxd_cnt == 4'd8) ) ? 1'b1 : 1'b0;	

       第二部分是一个重点:

 首先,由于FPGA作为从机,接收STM32所发出的CS,SCK和MOSI信号,因此对于此类异步信号,需要利用主时钟做同步处理,最常用的方法就是打两拍,这在按键消抖的文章中有讲过。

       其次,由于STM32的SPI模式选择为SPI_CPOL_Low和SPI_CPHA_1Edge这个模式,因此要在SCK时钟的上升沿进行采样,所以定义了mcu_read_flag这个信号,以捕获SCK的上升沿。

       最后,还要知道8位的数据什么时候读取完毕了,根据上篇文章中示波器中的图,可以采用CS的上升沿作为数据读取完毕标志,因此定义了mcu_read_done信号,来监测CS的上升沿,但是由于STM32在复位阶段会有CS的抖动,因此最好加上rxd_cnt==8这个条件,以使得数据准确无误!

//-----------------------------------
//sample input MOSI
reg		[7:0]	rxd_data_r;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			rxd_cnt		<= 4'd0;
			rxd_data_r	<= 8'd0;
		end
	else if ( ! mcu_cs )
		if ( mcu_read_flag )
			begin
				rxd_data_r[3'd7-rxd_cnt]	<= mcu_data;
				rxd_cnt				<= rxd_cnt + 1'b1;
			end
		else
			begin
				rxd_data_r			<= rxd_data_r;
				rxd_cnt				<= rxd_cnt;
			end
	else
		begin			
			rxd_data_r	<= rxd_data_r;
			rxd_cnt		<= 4'd0;
		end
end

 第三部分就是进行数据的采样,看图说话,笔者在testbench中发了0xaa,0x55和0xff三个数,可以看到,都可以完美检测到。

blob.png

 这里有一个问题需要注意:

 能否把上述代码的else if 部分改写成以下代码?

	else if ( mcu_read_flag && ! mcu_cs )
		begin
			rxd_data_r[3'd7-rxd_cnt]	<= mcu_data;
			rxd_cnt				<= rxd_cnt + 1'b1;
		end
	else
		begin			
			rxd_data_r	                <= rxd_data_r;
			rxd_cnt		                <= rxd_cnt;
		end

 这样看起来使得代码很简洁,但是却没有地方写rxd_cnt  <= 4'd0;使得rxd_cnt无法恢复初值。因此笔者修改如下:

	else if ( mcu_read_flag && ! mcu_cs )
		if ( rxd_cnt < 4'd8 )
		    begin
			rxd_data_r[3'd7-rxd_cnt]	<= mcu_data;
			rxd_cnt				<= rxd_cnt + 1'b1;
		    end
		else
		    begin
			rxd_data_r	<= rxd_data_r;
			rxd_cnt		<= 4'd0;
		    end    
	else
		begin			
			rxd_data_r	<= rxd_data_r;
			rxd_cnt		<= rxd_cnt;
		end

 理想很美好,感觉可以了,看仿真吧:

blob.png

      结果只能识别第一个0xaa,因为缺少一个mcu_read_flag把rxd_cnt清零!因此没有办法,只能写成最开始那种形式!

//-----------------------------------
//output
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			rxd_data	<= 8'd0;
			rxd_flag	<= 1'b0;
		end
	else if ( mcu_read_done )
		begin
			rxd_data	<= rxd_data_r;
			rxd_flag	<= 1'b1;
		end
	else
		begin
			rxd_data	<= rxd_data;
			rxd_flag	<= 1'b0;
		end
end

 第四部分是同步输出rxd_data和rxd_flag,这在按键消抖的实验中已经用过了,见以下仿真图:

blob.png

=====================================================

 2、下面是SPI_Transfer模块的程序:

module spi_transfer
(
	input				clk,		//global clock
	input				rst_n,		//global reset
			
	input				spi_cs,
	input				spi_sck,
	output	reg			spi_miso,	
	
	input				txd_en,
	input		[7:0]	        txd_data,	
	output	reg			txd_flag
);

 第一部分是输入输出定义,需要说明的是对于发送类的模块,无论是串口发送,SPI发送,都需要发送使能信号,如本例中的txd_en。

当然了,有发送使能,大家会想到什么?

是使用状态机的IDLE来等待使能信号的到来!笔者在——《详细解析74HC595驱动程序》这篇文章中说过!因此写Verilog程序只要掌握了相应的套路,模式,其实一点也不难!当然,就像接收模块的rxd_flag一样,少不了发送完成标志信号txd_flag,以供其他模块使用。

//-----------------------------------
//synchronize the input signal
reg	spi_cs_r0,	spi_cs_r1;
reg	spi_sck_r0,	spi_sck_r1;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			spi_cs_r0	<= 1'b1;		spi_cs_r1	<= 1'b1;
                        spi_sck_r0	<= 1'b0;		spi_sck_r1	<= 1'b0;
		end
	else
		begin
			spi_cs_r0	<= spi_cs;		spi_cs_r1	<= spi_cs_r0;
                        spi_sck_r0	<= spi_sck;		spi_sck_r1	<= spi_sck_r0;	
		end
end

wire	mcu_cs	= spi_cs_r1;
wire	mcu_write_flag = ( ~spi_sck_r0 & spi_sck_r1) ? 1'b1 : 1'b0;	//sck negedge capture
wire	mcu_write_done = ( spi_cs_r0 & ~spi_cs_r1 ) ? 1'b1 : 1'b0;	//cs posedge capture
wire	mcu_write_start = ( ~spi_cs_r0 & spi_cs_r1 ) ? 1'b1 : 1'b0;	//cs negedge capture

 第二部分和spi_receiver的那部分类似,就不多做介绍了!

//-----------------------------------
//FSM: encode
localparam	T_IDLE	= 2'd0;
localparam	T_START	= 2'd1;
localparam	T_SEND	= 2'd2;
localparam	SPI_MISO_DEFAULT = 1'b1;

//-----------------------------------
//transfer FSM
reg	[1:0]	txd_state;
reg	[3:0]	txd_cnt /*synthesis noprune*/;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		begin
			txd_cnt		<= 4'd0;
			spi_miso	<= SPI_MISO_DEFAULT;
			txd_state	<= T_IDLE;
		end
	else
		case ( txd_state )
			T_IDLE:
				begin
					txd_cnt		<= 4'd0;
					spi_miso	<= SPI_MISO_DEFAULT;
					if ( txd_en )
						txd_state	<= T_START;
					else
						txd_state	<= T_IDLE;
				end
			T_START:
				begin
					if ( mcu_write_start )
						begin
						    spi_miso <= txd_data[3'd7-txd_cnt[2:0]];
						    txd_cnt  <= txd_cnt + 1'b1;
						    txd_state<= T_SEND;
						end
					else
						begin
						    spi_miso <= spi_miso;
						    txd_cnt  <= txd_cnt;
						    txd_state<= T_START;
						end
				end
			T_SEND:
				begin
					if ( mcu_write_done )
						txd_state	<= T_IDLE;
					else
						txd_state	<= T_SEND;
					
					if ( ! mcu_cs )
						if ( mcu_write_flag )
						    begin
						        if ( txd_cnt < 4'd8 )
							begin
						    spi_miso  <= txd_data[3'd7-txd_cnt[2:0]];
						    txd_cnt   <= txd_cnt + 1'b1;
							end
						        else
							begin
						            spi_miso  <= 1'b1;
						            txd_cnt   <= txd_cnt;
							end
						    end
						else
							begin
								spi_miso	<= spi_miso;
								txd_cnt		<= txd_cnt;
							end
					else
					    begin
						spi_miso		<= SPI_MISO_DEFAULT;
						txd_cnt			<= 4'd0;
					    end
				end
			default:
				begin
					txd_cnt		<= 4'd0;
					spi_miso	<= SPI_MISO_DEFAULT;
					txd_state	<= T_IDLE;
				end
		endcase
end

 第三部分就是长长的发送状态机了,首先在IDLE态等待使能信号的到来,使能信号到来之后,进入发送状态。

 有一点需要注意,笔者的发送状态,第一位数据的发送时以CS信号的下降沿作为标志,之后的数据发送均以SCK的下降沿作为标志,这是为何?请看仿真图:

blob.png

可以看到当FPGA给STM32发送数据时,STM32会在SCK的上升沿进行读取,如果FPGA仅仅在SCK的下降沿进行设置数据的话,SCK的第一个上升沿,由于FPGA还没有设置数据,导致STM32采到的高电平,也就是无论发什么数据,8位数据的最高位都是1,这是不合理的,因此,第一个数据必须在CS变为低电平的时候就设置好,之后在SCK的下降沿设置,这样可以完美发送8位数据!

blob.png

       如图所示,示波器实时采集到的数据,3号通道的是MOSI,4号通道的是MISO,可见MOSI此时正在发送的是01010111,也就是87,而MISO此时发送的是01010110,也就是86,一切正常!

//-----------------------------------
//output
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		txd_flag	<= 1'b0;
	else
		txd_flag	<= mcu_write_done;
end

 最后一部分是产生txd_flag信号,虽然很简单,但是笔者还是要说两句,为何不写成以下形式呢?

//-----------------------------------
//output
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		txd_flag	<= 1'b0;
	else if ( mcu_write_done )
		txd_flag	<= 1'b1;
	else
	        txd_flag        <= 1'b0;
end

 写成上述代码,一点问题没有,但是不简洁,因此推荐第一种,事实上,在笔者的按键消抖中,就是第一种用法!


最后呢,一切都是那么完美,完美的时序,完美的结果!

blob.png