【原创】详细解析FPGA与STM32的SPI通信(二)
1赞【主题】:详细解析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三个数,可以看到,都可以完美检测到。
这里有一个问题需要注意:
能否把上述代码的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
理想很美好,感觉可以了,看仿真吧:
结果只能识别第一个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,这在按键消抖的实验中已经用过了,见以下仿真图:
=====================================================
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的下降沿作为标志,这是为何?请看仿真图:
可以看到当FPGA给STM32发送数据时,STM32会在SCK的上升沿进行读取,如果FPGA仅仅在SCK的下降沿进行设置数据的话,SCK的第一个上升沿,由于FPGA还没有设置数据,导致STM32采到的高电平,也就是无论发什么数据,8位数据的最高位都是1,这是不合理的,因此,第一个数据必须在CS变为低电平的时候就设置好,之后在SCK的下降沿设置,这样可以完美发送8位数据!
如图所示,示波器实时采集到的数据,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
写成上述代码,一点问题没有,但是不简洁,因此推荐第一种,事实上,在笔者的按键消抖中,就是第一种用法!
最后呢,一切都是那么完美,完美的时序,完美的结果!