SPI从机DMA方式通信调试总结

目录

1.硬件方案

2.SPI通信问题

3.DMA超时检测机制

4.半双工通信

5.从机部分代码


1.硬件方案

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

SPI从机DMA方式通信调试总结_第1张图片 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。

SPI从机DMA方式通信调试总结_第2张图片 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机制大致的流程图如下。

SPI从机DMA方式通信调试总结_第3张图片 SPI DMA方式IDLE机制流程图

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

4.半双工通信

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

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

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

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

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

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

最后实测的SPI通信波形如下,从机发送20字节,用时235uS,主机每4字节处理一下数据,所以每4字节间有点小延时;主机发送17字节的情况,用时174us。

SPI从机DMA方式通信调试总结_第4张图片 从机发送时的波形

 

SPI从机DMA方式通信调试总结_第5张图片 主机发送时的波形

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);
    }
}

 

你可能感兴趣的:(STM32)