OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 4931|回复: 3

分享自己的FPGA与STM32的SPI通信

[复制链接]

4

主题

22

帖子

0

精华

新手上路

积分
46
金钱
46
注册时间
2016-3-26
在线时间
4 小时
发表于 2016-11-26 17:42:10 | 显示全部楼层 |阅读模式
这是笔者今天写的博客,第一时间发到了ChinaAET上,所以图片会有水印,不过不影响大家阅读!
【主题】:详细解析FPGA与STM32的SPI通信(一)
【作者】:LinCoding
【时间】:2016.11.26
     昨天把SPI彻底的又搞了一遍,感觉之前学STM32时学的SPI只是皮毛,这次学习FPGA时候,才真正算是把SPI吃透了。
     As we all know, SPI有四种模式,但是STM32与FPGA通信的话推荐使用SPI_CPOL_Low和SPI_CPHA_1Edge这个模式,也就是时钟信号线空闲为低,上升沿采样,因为这样更加适合FPGA进行处理。
     使用SPI要注意以下几点:
     1、时钟和片选是由主机提供,从机只负责接收。
     2、对STM32来说,片选可选用硬件或软件,具体有什么区别请看数据手册。
     3、SPI的通信速率不能太快,否则数据会出错。
     4、如果进行双工通信的话,主机必须在发送完数据后多发送一个8位数据,笔者通常发送0xFF,这样从机才能将最后一个有效数据发过来,具体原因请百度,不多解释。
      先说第一点和第二点:   
主机给从机发数据进行单工通信的话很简单,因为时钟和片选是由主机提供的。
     但是,从机给主机发数据进行单工通信的时,由于时钟和片选是由主机提供的,主机并不知道从机什么时候给主机发,也就不知道什么时候给从机提供时钟信号和片选信号,当然了,也不会知道从机什么时候已经发完数据,什么时候停止提供时钟信号和片选信号。那这该怎么办?
同样,如果是双工通信,且从机所发的数据比主机数量多,主机发完以后如果停止提供片选和时钟信号的话,从机剩余的数据就无法传送过来,那这又该怎么办?
      笔者想到了一个解决办法,就是增加一根从机与主机的连线,普通IO口即可,这根线平时为高,当从机准备向主机发数据时,把这根线拉低,主机检测到这根线拉低,则提供片选和时钟信号,当从机发完数据以后,把这根线拉高,主机检测到这根线拉高,则主机停止提供片选和时钟信号线。
      这样就完美的解决了这一问题。
      再说第三点,速率问题:
      对STM32来说,笔者使用的是SPI2,而SPI2硬件是挂在APB1总线上,也就是36Mhz低速外设总线,这样根据硬件的设定,速率可以在以下这四种中进行选择,
SPI_BaudRatePrescaler_2   2分频   -- 18Mhz
SPI_BaudRatePrescaler_8   8分频   -- 4.5Mhz
SPI_BaudRatePrescaler_16  16分频  -- 2.25Mhz
SPI_BaudRatePrescaler_256 256分频 -- 140.625Khz

但是要注意,速率太快会出错!笔者FPGA使用100Mhz速率,按说可以采样到18Mhz的数据,但是,当STM32的SPI设置为18Mhz时会出错,4.5Mhz及以下均没有问题。当笔者将FPGA提高到200Mhz时,SPI的18Mhz仍然不行,因此笔者怀疑,频率太高的话对布局布线要求较高,而笔者采用的是杜邦线进行连接,应该是使用杜邦线不能满足那么高频率的要求。
      好了,基本上就是上面四个注意点,剩下的就是时序问题了,下面开始解析整个系统的程序:
      STM32的程序相对简单,那就先说STM32的程序吧。
void SPI2_Init(void)
{
         GPIO_InitTypeDef GPIO_InitStructure;
         SPI_InitTypeDef  SPI_InitStructure;
         RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
         RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,  ENABLE);      
         
         GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
         GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
         GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB
         GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_12;
         GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
         GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
         GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB
         GPIO_SetBits(GPIOB, GPIO_Pin_12);   //CS
         GPIO_ResetBits(GPIOB, GPIO_Pin_13); //SCK
         GPIO_SetBits(GPIOB, GPIO_Pin_14);   //MISO
         GPIO_ResetBits(GPIOB, GPIO_Pin_15); //MOSI
         
         SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  
         SPI_InitStructure.SPI_Mode = SPI_Mode_Master;               
         SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
         SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;         
         SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
         SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
         SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
         SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
         SPI_InitStructure.SPI_CRCPolynomial = 7;
         SPI_Init(SPI2, &SPI_InitStructure);
         SPI2_SetSpeed(SPI_BaudRatePrescaler_8);
         SPI_Cmd(SPI2, ENABLE);
}

笔者使用的是原子的例程,使用库函数进行配置是很简单的,注意的是,STM32设置为SPI_Mode_Master,SPI_DataSize_8b,SPI_CPOL_Low,SPI_CPHA_1Edge,SPI_NSS_Soft,还有就是SPI_FirstBit_MSB,这样就OK了!
int main(void)
{         
         u8 i = 0;
         u8 dataTemp;
         delay_init();            
         NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
         uart_init(9600);         
         LED_Init();                                
         SPI2_Init();      
         while(1)
         {
                 for ( i=0; i < 255; i++ )
                 {
                          dataTemp = SPI2_ReadWriteByte(i);
                          printf("%d\r\n",dataTemp);
                          delay_ms(100);
                 }
                 dataTemp = SPI2_ReadWriteByte(0xFF);
                 printf("%d\r\n",dataTemp);
                 delay_ms(100);
         }
}

主函数中就是循环发送0~255,并且接收FPGA发来的数据,打印到串口进行显示。可以看到我在发送完数据以后,特地多发了一个0xFF。
笔者用示波器将STM32发出的数据抓了一下,见下图:
                              
为了图片好看,使用STM32只发送了一个0xAA,笔者采用了SPI_BaudRatePrescaler_256256分频 -- 140.625Khz,可以看到示波器抓到的频率为140.5Khz,没差多少,数据是10101010也就是0xAA啦,下一步,就是使用FPGA模仿这个时序了。

正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

4

主题

22

帖子

0

精华

新手上路

积分
46
金钱
46
注册时间
2016-3-26
在线时间
4 小时
 楼主| 发表于 2016-11-26 17:42:43 | 显示全部楼层
【主题】:详细解析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
写成上述代码,一点问题没有,但是不简洁,因此推荐第一种,事实上,在笔者的按键消抖中,就是第一种用法!

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

回复 支持 1 反对 0

使用道具 举报

4

主题

22

帖子

0

精华

新手上路

积分
46
金钱
46
注册时间
2016-3-26
在线时间
4 小时
 楼主| 发表于 2016-11-26 17:47:30 | 显示全部楼层
在这里格式不是很好。。。。图片也没粘贴上去,感兴趣的直接看我博客吧!

http://blog.chinaaet.com/LinCoding
回复 支持 反对

使用道具 举报

1

主题

13

帖子

0

精华

初级会员

Rank: 2

积分
58
金钱
58
注册时间
2018-12-14
在线时间
12 小时
发表于 2019-9-26 10:31:54 | 显示全部楼层
楼主你好,看了您的帖子,很受益。能分享一下FPGA和STM32通过SPI通信的完整源码吗?顶层代码出现问题了不能通信,希望用您的来参考一下。394864937@qq.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-6-8 14:16

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表