前言:
前面的两篇文章都说关于发送的,HAL库发送数据可以调用现成的函数,而接收数据,现成函数不太好用。这里为了记录了一下自己参考了网上几个大佬的代码,整理了一下USART1 DMA方式接受数据的代码,这里亲测了一下,传输比较稳定,也没有出现发送数据过快导致串口反应不过来的情况。
正文开始:
Cubemx配置
这里跟上一篇博客一样,我就不再赘述了
额外注意的一点是记得勾选上Use MicroLIB
代码编写:
这里我是学习了大佬的博客,传送门
我稍微做了一下改进,这里还是沿用上一篇文章创建的两个文件 USART_DMA.c和USART.h
按照大佬博客里教的。
① 定义一个结构体变量:存放接收的字节数、数据数组。
② 开启DMA:让硬件自动接收数据放到缓存
③ 重写回调函数:当一帧数据接收好了,把缓存的数据,转存到全局结构体变量里,备用。
④ 在需要使用串口接收的地方,如在while中,判断接收字节数>0, 即为接收到新一帧数据了
在USART_DMA.h中,我们声明一下我们的结构体
typedef struct //声明一个结构体,方便管理变量
{ uint16_t ReceiveNum; //接受字节数;在中断回调函数中被自动赋值;只要字节数>0,即为接受到新一帧uint8_t ReceiveData[512]; //接受到的数据uint8_t BuffTemp[512]; //接受缓存;注意,这个数组只是一个缓存//临时缓存,在DMA空闲中断中将把一帧数据复制到ReceivedData[]
}myUATR_TypeDef;
然后在USART_DMA.c中定义一个自己的变量,如果要在其他.c文件引用的,extern一下就行
myUATR_TypeDef myUSART1 = {0}; //用来定义自己的变量
我采用的方法是,直接将extern myUATR_TypeDef myUSART1;放在USART_DMA.h里面。
开启DMA,让硬件自动接收数据,.
我们整个接收过程,仅使用到1个HAL库函数。只需在main()函数的初始化部分,调用HAL库函数:HAL_UARTEx_ReceiveToIdle_DMA (串口、缓存、字节数) ;
参数:串口、接收缓存区、最大接收字节数
作用:使能DMA、使能串口的空闲中断,正式进入接收状态。
我们需要在main.c中添加代码
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, myUSART1.BuffTemp, sizeof(myUSART1.BuffTemp)); // 开启DMA空闲中断
调用函数后,硬件就会立刻进入自动接收状态:从RX引脚接收到的数据,会逐个字节顺序存放到指定缓存中,这里我们指定的缓存是:myUSART1.BuffTemp
因为函数内部,开启了DMA中断、空闲中断,所以达成下列两个条件之一,就会触发中断:
① DMA接收的字节数,达到了参数中的最大值
② 串口发生空闲中断,即RX引脚,超过1字节的时间,没有新信号。
当上述中断产生时,硬件自动调用其相关的中断服务函数,再继而调用回调函数。
重写DMA空闲中断回调函数,(DMA完成中断、空闲中断,所调用的回调函数): HAL_UARTEx_RxEventCallback(串口,接收到的字节数);
弱函数定义在stm32xx_hal_gpio.c文件的底部。
/******************************************************************************* 函 数: HAL_UARTEx_RxEventCallback* 功 能: DMA+空闲中断回调函数* 参 数: UART_HandleTypeDef *huart // 触发的串口* uint16_t Size // 接收字节* 返回值: 无* 备 注: 1:这个是回调函数,不是中断服务函数。技巧:使用CubeMX生成的工程中,中断服务函数已被CubeMX安排妥当,我们只管重写回调函数* 2:触发条件:当DMA接收到指定字节数时,或产生空闲中断时,硬件就会自动调用本回调函数,无需进行人工调用;* 2:必须使用这个函数名称,因为它在CubeMX生成时,已被写好了各种函数调用、函数弱定义(在stm32xx_hal_uart.c的底部); 不要在原弱定义中增添代码,而是重写本函数* 3:无需进行中断标志的清理,它在被调用前,已有清中断的操作;* 4:生成的所有DMA+空闲中断服务函数,都会统一调用这个函数,以引脚编号作参数* 5:判断参数传进来的引脚编号,即可知道是哪个串口接收收了多少字节
******************************************************************************/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart == &huart1) // 判断串口{__HAL_UNLOCK(huart); // 解锁串口状态myUSART1.ReceiveNum = Size; // 把接收字节数,存入结构体xUSART1.ReceiveNum,以备使用memset(myUSART1.ReceiveData, 0, sizeof(myUSART1.ReceiveData)); // 清0前一帧的接收数据memcpy(myUSART1.ReceiveData, myUSART1.BuffTemp, Size); // 把新数据,从临时缓存中,复制到xUSART1.ReceiveData[], 以备使用HAL_UARTEx_ReceiveToIdle_DMA(&huart1, myUSART1.BuffTemp, sizeof(myUSART1.BuffTemp)); // 再次开启DMA空闲中断; 每当接收完指定长度,或者产生空闲中断时,就会来到这个}
}
(1)xUSART1.ReceiveNum = Size;
把接收的字节数,存入结构体 xUSART1.ReceiveNum,以备使用 。
在程序的其它地方,判断 ReceivNum > 0, 就能知道是否收到新一帧数据了。
(2)memset(xUSART1.ReceivedData, 0, sizeof(xUSART1.ReceivedData));
清0前一帧的数据缓存
(3) memcpy(xUSART1.ReceivedData, xUSART1.BuffTemp, Size);
把新数据,从临时缓存中,复制到xUSART1.ReceivedData[], 以备使用
从结构体和这段回调函数中,可以发现,这是一个双缓存的操作思路。 .ReceivedData:用于存放接收后完整的一帧数据,对外使用 。
.BuffTemp:用于DMA接收过程,是一个中间缓存。
(4)HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp));
再次开启DMA空闲中断,进入接收状态。
我们在main()函数的初始化部分,已调用过这个函数了,为什么要在回调函数中再次调用?
因为在DMA的中断服务函数里,会关闭DMA,即只接收一次。所以,在接收完一帧后,再次调用函数,就能让DMA开始工作接收下一帧。在这个位置调用 ,能让DMA不断地循环工作。
其实,在CubeMX配置中,DMA有一个选项 :Mode的circular, 可以让DMA进行连续地的工作,接收完成后,无需在回调函数里再次开启DMA
本篇的处理,是保存最后一帧数据。当有新一帧数据来了,会自动盖掉旧帧数据。
接下来,为了验证,到底这个项目的程序到底好事使不好使,我在这里写了一个测试函数,其中的myprintf函数是用了我上一篇文章的写法
void USART_test()
{if(myUSART1.ReceiveNum) //一旦接受到数据{myUSART1.ReceiveNum = 0; //将数据清零if(strcmp((char *)myUSART1.ReceiveData,"hello\r\n") == 0)//相等,返回 0;{num++;myprintf("接受次数%d\r\n",num);}}
}
将其放在while循环中
最终效果演示
在串口助手这里,简单设置一下 ,一定要注意的是,如果点了发送新行之后,一定不要再额外在第一个箭头的后面加回车了,否则是接受不到的。
接受的速度也很不错,没有出现过卡死的状况,100ms回应一次。