1.硬件方案
2.SPI通信问题
3.DMA超时检测机制
4.半双工通信
5.从机部分代码
由于项目中单片机的串口资源不够,所以使用SPI来代替串口,通信双方分别是Hi3516EV300和STM32L051,前者作为SPI主机,后者作为SPI从机。硬件连接关系如下图所示。
SPI通信需要由主机发起,也就是由主机产生CLK,从机被动应答,那么当从机需要主动发送数据的时候怎么办呢?办法就是用额外的引脚来告知主机来取数据,这个引脚在上图就是NOTIFY引脚。当NOTIFY引脚被从机拉高时,主机便产生CLK,这样从机就可以把数据发送出去了。
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。
使用DMA方式也有蛋疼的地方:
①. 接收:STM32的SPI不像UART一样有IDLE中断,STM32的UART+DMA可以实现用DMA接收不定长的数据,但是SPI不行啊!接收长度没有达到DMA指定的大小时是不会触发接收完成中断的。
②. 发送:例如从机需要发送10个字节,设置了DMA的size为10,也就意味着从机在此时只能接收10个字节的数据,如果主机发送了更多的数据,那么从机就GG了。
上面两个问题,最后分别使用了DMA超时检测机制、半双工通信方式解决。
第2章提到了STM32的SPI没有IDLE中断,硬件上不支持那我们可以用软件去实现!
通信协议上的规定的一帧数据不会超过256字节,那我们就设置DMA接收大小为256字节,这也就意味着主机发送一帧数据时从机不会产生DMA接收完成中断,但我们可以随时查询到DMA当前接收了多少个字节的数据,如果这个大小维持了一段都没有改变,那我们就可以认为数据已经接收完成了,这不就是软件实现IDLE检测机制吗?SPI IDLE机制大致的流程图如下。
实际使用中,软件定时器的超时时间我们设为了1ms,因为在1Mbps的速率下,1ms内理论上可传输的数据量是131Byte,这个检测频率已经远远满足需要了。
第2章提到了在全双工方式下,不方便设置DMA的大小,因为全双工方式下,发送大小和接收大小强关联了。既然如此,我们使用半双工方式不就解决问题了吗?更何况主从机同时交互数据概率还是很小的,一般都是一问一答的形式(也不是完全没有同时交互的情况)。
这里指的半双工是指软件上的半双工,实际上硬件还是全双工的。也就是说主机发送的时候,接收到的任何数据都认为是垃圾数据,直接丢弃,从机亦然。这就涉及到主从机如何知道自己处于何种状态呢?
对于主机而言,在发送数据前需要判断一下NOTIFY引脚的状态,如果引脚为高电平,就说明从机当前在发送数据,主机在接收数据,此时主机不可发送数据,需要延时等待一会儿再尝试发送。
对于从机而言,在发送数据前需要判断一下当前是否在接收数据,如果DMA已接收的数据量不为0,就说明主机在发送数据,需要延时等待一会儿再尝试发送。从机需要发送多少数据量就把DMA大小设置为多大,这样发送成功后就会触发发送完成中断。另外需要注意,只要从机没有发送数据,就应该把DMA接收大小设置为256字节(256是本项目的情况,其他项目需要根据实际情况设置),以此来保证能接收主机随时可能发来的数据。
从机判断主机是否在发送的方法,目前用的是判断DMA已接收的数据量不为0。一开始用的方法是将CS引脚设为输入,根据CS引脚是否为低电平判断主机是否在发送,同时也可以根据CS脚状态代替定时器超时机制来判断接收是否结束,但我在尝试这种方式的时候,从机接收的数据有错位,原因暂未去深究。
软件上半双工的好处在于主机和从机都可以直接丢弃垃圾数据,不会对接收缓冲区造成影响,提高协议解析的效率;缺点在于不能同时交互数据,不过这点缺点相对于优点来说已经不值一提了。
最后实测的SPI通信波形如下,从机发送20字节,用时235uS,主机每4字节处理一下数据,所以每4字节间有点小延时;主机发送17字节的情况,用时174us。
/* 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);
}
}