完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
由于项目中单片机的串口资源不够,所以使用SPI来代替串口,通信双方分别是Hi3516EV300和STM32L051,前者作为SPI主机,后者作为SPI从机。硬件连接关系如下图所示。
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。 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方式IDLE机制流程图 实际使用中,软件定时器的超时时间我们设为了1ms,因为在1Mbps的速率下,1ms内理论上可传输的数据量是131Byte,这个检测频率已经远远满足需要了。 4.半双工通信 第2章提到了在全双工方式下,不方便设置DMA的大小,因为全双工方式下,发送大小和接收大小强关联了。既然如此,我们使用半双工方式不就解决问题了吗?更何况主从机同时交互数据概率还是很小的,一般都是一问一答的形式(也不是完全没有同时交互的情况)。 这里指的半双工是指软件上的半双工,实际上硬件还是全双工的。也就是说主机发送的时候,接收到的任何数据都认为是垃圾数据,直接丢弃,从机亦然。这就涉及到主从机如何知道自己处于何种状态呢? 对于主机而言,在发送数据前需要判断一下NOTIFY引脚的状态,如果引脚为高电平,就说明从机当前在发送数据,主机在接收数据,此时主机不可发送数据,需要延时等待一会儿再尝试发送。 对于从机而言,在发送数据前需要判断一下当前是否在接收数据,如果DMA已接收的数据量不为0,就说明主机在发送数据,需要延时等待一会儿再尝试发送。从机需要发送多少数据量就把DMA大小设置为多大,这样发送成功后就会触发发送完成中断。另外需要注意,只要从机没有发送数据,就应该把DMA接收大小设置为256字节(256是本项目的情况,其他项目需要根据实际情况设置),以此来保证能接收主机随时可能发来的数据。 从机判断主机是否在发送的方法,目前用的是判断DMA已接收的数据量不为0。一开始用的方法是将CS引脚设为输入,根据CS引脚是否为低电平判断主机是否在发送,同时也可以根据CS脚状态代替定时器超时机制来判断接收是否结束,但我在尝试这种方式的时候,从机接收的数据有错位,原因暂未去深究。 软件上半双工的好处在于主机和从机都可以直接丢弃垃圾数据,不会对接收缓冲区造成影响,提高协议解析的效率;缺点在于不能同时交互数据,不过这点缺点相对于优点来说已经不值一提了。 最后实测的SPI通信波形如下。 主机发送从机接收数据波形 从机发送数据,实际上是通知主机来取数据。下面是从机发送主机接收的波形。由于项目中主机每次都固定读取4字节,所以最后可能会多读取了3个无用的字节。 从机发送主机接收波形 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口的输出能力来解决。 SPI信号过冲 |
|
|
|
只有小组成员才能发言,加入小组>>
3311 浏览 9 评论
2994 浏览 16 评论
3493 浏览 1 评论
9058 浏览 16 评论
4087 浏览 18 评论
1178浏览 3评论
605浏览 2评论
const uint16_t Tab[10]={0}; const uint16_t *p; p = Tab;//报错是怎么回事?
599浏览 2评论
用NUC131单片机UART3作为打印口,但printf没有输出东西是什么原因?
2335浏览 2评论
NUC980DK61YC启动随机性出现Err-DDR是为什么?
1896浏览 2评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-12-23 00:32 , Processed in 1.395010 second(s), Total 79, Slave 59 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号