SPI主从机DMA通信

目录

1.硬件方案

2.SPI通信问题

3.DMA超时检测机制

4.半双工通信

5.从机部分代码

6.注意事项


1.硬件方案

由于项目中单片机的串口资源不够,所以使用SPI来代替串口,通信双方分别是Hi3516EV300和STM32L051,前者作为SPI主机,后者作为SPI从机。硬件连接关系如下图所示。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3NTc1ODQx,size_16,color_FFFFFF,t_70

SPI主从机硬件连接关系

SPI通信需要由主机发起,也就是由主机产生CLK,从机被动应答,那么当从机需要主动发送数据的时候怎么办呢?办法就是用额外的引脚来告知主机来取数据,这个引脚在上图就是NOTIFY引脚。当NOTIFY引脚被从机拉高时,主机便产生CLK,这样从机就可以把数据发送出去了。

2.SPI通信问题

SPI是一种全双工的同步的通信总线,也就是说主机在发送数据的时候也在接收数据,反之亦然,主机在接收数据时候也在发送数据,从机亦是如此。这就意味着当从机向主机发送数据的时候,主机会返回一些无用的数据,例如0xFF,从机会收到0xFF,对从机来说,这些0xFF都是垃圾数据,这也是全双工通信的一个小缺点。

SPI通信我们选择了1Mbps,但实测STM32L0使用SPI单字节接收中断时,却无法承受这么高的速率,必须在字节与字节之间加一定的延时,如果不加延时的话,STM32L0会发生SPI ORE错误,即中断在处理当前字节的时候,下一个字节已经到来额,单片机来不及处理。这个延时我们取的是1ms,实际处理一个字节应该用不了1ms,这里的1ms是保守值。

发1个字节需要1ms,这也太蛋疼了!竟然比串口还慢!这显然是无法接受的。那么有没有办法去掉这1ms呢?答案是必然的,那就是用DMA。STM32L0只是无法处理过快的中断,硬件上还是支持1Mbps的速率的,否则单字节接收都接收不了才对。在STM32的HAL库中,有对SPI外设速率的相关注解。我们配置STM32L0的Fpclk为32MHz,查表可知中断方式下最大支持的频率为2MHz,DMA方式下最大支持的频率为16MHz。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3NTc1ODQx,size_16,color_FFFFFF,t_70

STM32HAL库对SPI速率的注解

使用DMA方式也有蛋疼的地方:

①. 接收:STM32的SPI不像UART一样有IDLE中断,STM32的UART+DMA可以实现用DMA接收不定长的数据,但是SPI不行啊!接收长度没有达到DMA指定的大小时是不会触发接收完成中断的。

②. 发送:例如从机需要发送10个字节,设置了DMA的size为10,也就意味着从机在此时只能接收10个字节的数据,如果主机发送了更多的数据,那么从机就GG了。

上面两个问题,最后分别使用了DMA超时检测机制、半双工通信方式解决。

3.DMA超时检测机制

第2章提到了STM32的SPI没有IDLE中断,硬件上不支持那我们可以用软件去实现!

通信协议上的规定的一帧数据不会超过256字节,那我们就设置DMA接收大小为256字节,这也就意味着主机发送一帧数据时从机不会产生DMA接收完成中断,但我们可以随时查询到DMA当前接收了多少个字节的数据,如果这个大小维持了一段都没有改变,那我们就可以认为数据已经接收完成了,这不就是软件实现IDLE检测机制吗?SPI IDLE机制大致的流程图如下。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3NTc1ODQx,size_16,color_FFFFFF,t_70

SPI DMA方式IDLE机制流程图

实际使用中,软件定时器的超时时间我们设为了1ms,因为在1Mbps的速率下,1ms内理论上可传输的数据量是131Byte,这个检测频率已经远远满足需要了。

4.半双工通信

第2章提到了在全双工方式下,不方便设置DMA的大小,因为全双工方式下,发送大小和接收大小强关联了。既然如此,我们使用半双工方式不就解决问题了吗?更何况主从机同时交互数据概率还是很小的,一般都是一问一答的形式(也不是完全没有同时交互的情况)。

这里指的半双工是指软件上的半双工,实际上硬件还是全双工的。也就是说主机发送的时候,接收到的任何数据都认为是垃圾数据,直接丢弃,从机亦然。这就涉及到主从机如何知道自己处于何种状态呢?

对于主机而言,在发送数据前需要判断一下NOTIFY引脚的状态,如果引脚为高电平,就说明从机当前在发送数据,主机在接收数据,此时主机不可发送数据,需要延时等待一会儿再尝试发送。

对于从机而言,在发送数据前需要判断一下当前是否在接收数据,如果DMA已接收的数据量不为0,就说明主机在发送数据,需要延时等待一会儿再尝试发送。从机需要发送多少数据量就把DMA大小设置为多大,这样发送成功后就会触发发送完成中断。另外需要注意,只要从机没有发送数据,就应该把DMA接收大小设置为256字节(256是本项目的情况,其他项目需要根据实际情况设置),以此来保证能接收主机随时可能发来的数据。

从机判断主机是否在发送的方法,目前用的是判断DMA已接收的数据量不为0。一开始用的方法是将CS引脚设为输入,根据CS引脚是否为低电平判断主机是否在发送,同时也可以根据CS脚状态代替定时器超时机制来判断接收是否结束,但我在尝试这种方式的时候,从机接收的数据有错位,原因暂未去深究。

软件上半双工的好处在于主机和从机都可以直接丢弃垃圾数据,不会对接收缓冲区造成影响,提高协议解析的效率;缺点在于不能同时交互数据,不过这点缺点相对于优点来说已经不值一提了。

最后实测的SPI通信波形如下。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3NTc1ODQx,size_16,color_FFFFFF,t_70

主机发送从机接收数据波形

从机发送数据,实际上是通知主机来取数据。下面是从机发送主机接收的波形。由于项目中主机每次都固定读取4字节,所以最后可能会多读取了3个无用的字节。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3NTc1ODQx,size_16,color_FFFFFF,t_70

从机发送主机接收波形

5.从机部分代码

/* SPI从机DMA设置 */
if ( hspi->Instance == SPI1 )
{
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_DMA1_CLK_ENABLE();
    
    hdma_spi1_tx.Instance = DMA1_Channel3;
    hdma_spi1_tx.Init.Request = DMA_REQUEST_1;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_spi1_tx);

    hdma_spi1_rx.Instance = DMA1_Channel2;
    hdma_spi1_rx.Init.Request = DMA_REQUEST_1;
    hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_rx.Init.Mode = DMA_NORMAL;
    hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_spi1_rx);

    __HAL_LINKDMA(hspi,hdmatx,hdma_spi1_tx);
    __HAL_LINKDMA(hspi,hdmarx,hdma_spi1_rx);

    HAL_NVIC_SetPriority(DMA1_Channel2_3_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel2_3_IRQn);

    HAL_NVIC_SetPriority(SPI1_IRQn, 3, 0);
    HAL_NVIC_EnableIRQ(SPI1_IRQn);
}
/* 从机通知主机取数据 */
static void _app_Spi_SlaveNotify(BOOL bSendFlag)
{
    s_bIsSending = bSendFlag;
    HAL_GPIO_WritePin(SPI_NOTIFY_PORT,SPI_NOTIFY_PIN,(GPIO_PinState)bSendFlag);	
}
/* s_u8SpiDmaTimer定时器的回调,实现从机DMA IDLE机制 */
static void app_Spi_RecvTimerHandle(void *p)
{
    static u8 s_u8LastDmaRxCount = 0;

    if(!s_bSpiEnable)
    {
        return;
    }

    /* 计算当前DMA接收了多少数据量 */
    u8 u8CurrDmaRxCount = sizeof(s_u8SpiTxRx) - __HAL_DMA_GET_COUNTER(&hdma_spi1_rx);

    /*  判断当前DMA已接收的数据量和上一次的数据量是否相等 */
    if(s_u8LastDmaRxCount != u8CurrDmaRxCount)
    {
        s_u8LastDmaRxCount = u8CurrDmaRxCount;	/* 更新上一次的数据量 */
        if(s_bIsSending)                        /* 如果是在发送,更新发送Tick */
            s_u32SendTick = GetTick();

        sys_Timer_Start(s_u8SpiDmaTimer);        /* 重启定时器,重新计时 */
    }
    /* 数据量维持不变且大于0,认为已经接收完毕 */
    else if(u8CurrDmaRxCount)				
    {
        if((s_bIsSending))    /* 从机当前处于发送状态 */
        {
            /* 发送完毕,拉低Notify引脚 */
            _app_Spi_SlaveNotify(FALSE);
        }
        else                  /* 从机当前处于接收状态 */
        {
            /* 将接收到的数据放入到接收队列中等待处理 */
            sys_Queue_Send(&g_stSpiMSgDecodeMng.stMsgQueue,
                           &s_u8SpiTxRx,
                           u8CurrDmaRxCount);
        }

        /* 停止DMA */
        HAL_SPI_DMAStop(&hspi1);

        /* 数据量记录清零 */  
        u8CurrDmaRxCount = s_u8LastDmaRxCount = 0;	   

        /* 缓存全部清为0xFF */ 
        memset(s_u8SpiTxRx,0xFF,sizeof(s_u8SpiTxRx));   

        /* 开启SPI DMA,注意大小为sizeof(s_u8SpiTxRx) */
        HAL_SPI_TransmitReceive_DMA(&hspi1,            
                    s_u8SpiTxRx,
                    s_u8SpiTxRx,
                    sizeof(s_u8SpiTxRx));

        /* 重启定时器,重新计时 */
        sys_Timer_Start(s_u8SpiDmaTimer);			    
    }
}
/* 从机发送接口 */
s32 app_Spi_Send(u8* pu8Data,u16 u16Len)
{
    s32 s32Ret = RET_OK;

    /* 要发送的数据写入发送队列,等待在合适的时机发送 */
    s32Ret = sys_Queue_Send(&s_stSendQueue, pu8Data, u16Len);
    return s32Ret;
}
static void app_Spi_SendTimerHandle(void *p)
{
    u8 u8SendQueueFillSize = 0,u8CurrDmaRxCount = 0;

    if(!s_bSpiEnable)
    {
        return ;
    }
    
    /* 查看发送队列里多少数据量 */
    u8SendQueueFillSize = sys_Queue_FillSize(&s_stSendQueue);

    /* 计算当前DMA已经接收的数据量 */
    u8CurrDmaRxCount = sizeof(s_u8SpiTxRx) - __HAL_DMA_GET_COUNTER(&hdma_spi1_rx);

    /* 最小发送长度符合要求,并且从机当前没有在接收数据,那么可以发送了*/
    if( (u8SendQueueFillSize >= 4) && (u8CurrDmaRxCount == 0))
    {
        /* 先停止DMA */
        HAL_SPI_DMAStop(&hspi1);

        /* 从发送队列中取出数据放到s_u8SpiTxRx数组中,实际取出的字节数为u8SendQueueFillSize */
        u8SendQueueFillSize = sys_Queue_Receivable(&s_stSendQueue, 
                                                   s_u8SpiTxRx ,
                                                   sizeof(s_u8SpiTxRx));

        /* 重新设置DMA发送,注意DMA大小为u8SendQueueFillSize */
        HAL_SPI_TransmitReceive_DMA(&hspi1,s_u8SpiTxRx,s_u8SpiTxRx,u8SendQueueFillSize );

        /* 通知主机来取数据,让从机把数据发出去 */
        _app_Spi_SlaveNotify(TRUE);

        /* 开启超时检测定时器 */
        sys_Timer_Start(s_u8SpiDmaTimer);
    }
}
/* SPI DMAz中断 */
void DMA1_Channel2_3_IRQHandler(void)
{
    /* 注意这里只需要rx就可以了,因为接收了多少字节就等于发送了多岁字节 */
    HAL_DMA_IRQHandler(&hdma_spi1_rx);
}

/* SPI发送和接收完成中断回调函数,实际本项目的发送会触发该回调,而不是定时器超时 */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
    /* 判断当前是否处于发送状态 */
    if(s_bIsSending && s_bSpiEnable)
    {
        /* 通知主机数据发送完毕 */
        _app_Spi_SlaveNotify(FALSE);	

        HAL_SPI_DMAStop(&hspi1);
        memset(s_u8SpiTxRx,0xFF,sizeof(s_u8SpiTxRx));

        /* 重新设置DMA,注意DMA大小为sizeof(s_u8SpiTxRx) */
        HAL_SPI_TransmitReceive_DMA(&hspi1,s_u8SpiTxRx,s_u8SpiTxRx,sizeof(s_u8SpiTxRx));	

        /* 重启Idle检测定时器 */
        sys_Timer_Start(s_u8SpiDmaTimer);
    }
}
/* 检测Notify引脚是否一直拉高,避免因为主机有BUG导致从机Nofity一直拉高 */
void app_Spi_SendCheck(void)
{
    /* 发送状态下超过500ms没有发送数据,拉低Notify引脚 */
    if((s_bIsSending == TRUE) && (PastTick(s_u32SendTick) >= 500))
    {	
        _app_Spi_SlaveNotify(FALSE);
    }
}

6.注意事项

如果SPI总线没有阻抗匹配,可能会出现信号过冲问题,如下图所示。可以在信号线上串入小电阻(例如22欧姆)来解决,或者通过软件降低IO口的输出能力来解决。

watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3NTc1ODQx,size_16,color_FFFFFF,t_70

SPI信号过冲

  • 6
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dokin丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值