【精品博文】详细解析FPGA与STM32的SPI通信(二)

一个双肩背包

有多难?

戳一下试试看!

→_→

长摁识别

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

【作者】:LinCoding

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

本篇文章承接——详细解析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 signalreg 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; endendreg [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 capturewire 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 MOSIreg [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; endend

第三部分就是进行数据的采样,看图说话,笔者在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清零!因此没有办法,只能写成最开始那种形式!

//-----------------------------------//outputalways @ ( 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; endend

第四部分是同步输出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 signalreg 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; endendwire mcu_cs = spi_cs_r1;wire mcu_write_flag = ( ~spi_sck_r0 & spi_sck_r1) ? 1'b1 : 1'b0; //sck negedge capturewire mcu_write_done = ( spi_cs_r0 & ~spi_cs_r1 ) ? 1'b1 : 1'b0; //cs posedge capturewire mcu_write_start = ( ~spi_cs_r0 & spi_cs_r1 ) ? 1'b1 : 1'b0; //cs negedge capture

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

//-----------------------------------//FSM: encodelocalparam T_IDLE = 2'd0;localparam T_START = 2'd1;localparam T_SEND = 2'd2;localparam SPI_MISO_DEFAULT = 1'b1;//-----------------------------------//transfer FSMreg [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 endcaseend

第三部分就是长长的发送状态机了,首先在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,一切正常!

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

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

//-----------------------------------//outputalways @ ( 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

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

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

(0)

相关推荐