本文需要用到HAL库的HAL_UARTEx_ReceiveToIdle_DMA()函数,如果编辑器提示找不到函数,可以尝试更新HAL库至最新版本。
串口接收不定长数据是串口的常见应用。最近的项目需要用到modbus协议,由于不经常使用HAL库,配置串口接收时遇到了一些问题。在此记录一下,希望能帮助到一些人。
串口的接收常见的方法有一下两种:
方法一:
传统方法。具体参考:STM32 HAL CubeMX 串口IDLE接收空闲中断+DMA_Z小旋的博客
方法二:
利用HAL库的 HAL_UARTEx_ReceiveToIdle_DMA()函数,代码简洁。
本文采用的是方法二。
打开STM32CubeMX,开始配置程序。
打开串口接收DMA,模式选择Normal。
点击侧边栏的NVIC选项
取消选中 Force DMA channels Interrupts,否则DMA不可自定义DMA中断优先级。
我这里配置成14,防止抢占其他更重要的中断。如果没有这个需要保持默认的0即可。
函数中会把接收类型设置成HAL_UART_RECEPTION_TOIDLE
,然后开启DMA接收,清除一次IDLEF
标志位,重新开启IDLEF
标志位
开启标志位后,如果串口中断来临就会执行中断处理函数void USART1_IRQHandler(void)
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //重新开启串口空闲中断和DMA接收,一定要放在这里
/* USER CODE END USART1_IRQn 1 */
}
HAL_UART_IRQHandler(&huart1)
中会判断各种中断类型,并执行对应的操作
主要关注其中关于IDLE中断的部分
首先判断串口的接收类型 ReceptionType,在HAL_UARTEx_ReceiveToIdle_DMA()
中已经被设为HAL_UART_RECEPTION_TOIDLE
清除相关中断的标志位(标志位具体功能对照参考手册查看)
最后调用事件回调函数HAL_UARTEx_RxEventCallback();
这个函数默认被定义成__weak 需要我们重新实现
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
这里为了方便演示在main.c中重新实现
uint8_t rx_buffer[BUF_SIZE]; // 创建接收缓存,大小为BUF_SIZE
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
cnt = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
HAL_UART_Transmit(&huart1, rx_buffer, cnt, 0xffff); //将接受到的数据再发回上位机
memset(rx_buffer, 0, cnt);
}
}
也可以直接用形参Size替换cnt,这是HAL库定义好的,两者操作和作用都一样:表示DMA一次接收到了多少字节的数据。
在main函数中,while循环前加入函数 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);
否则无法完成第一次接收。
测试结果如下:
单片机发回了相同的数据。
大部分文章中,习惯把HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //重新开启串口空闲中断和DMA接收
放在重新实现的函数void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
中,
而不是本文中放在void USART1_IRQHandler(void)
中的方式。
也就是
本文:
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, BUF_SIZE); //本文中选择放置在此处
/* USER CODE END USART1_IRQn 1 */
}
区别于:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
cnt = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
HAL_UART_Transmit(&huart1, rx_buffer, cnt, 0xffff);
memset(rx_buffer, 0, cnt);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //其他大部分文章放置于此处
}
}
放置在回调函数HAL_UARTEx_RxEventCallback
中似乎也说的通:产生串口空闲中断时,会调用回调函数,并在其中重新开启串口空闲中断和DMA,等待下一次串口空闲中断来临。但是这样做会不会有bug?
实际测试一下:
首先,串口初始化时波特率配置成 9600bps
第一次发送:波特率正确 9600bps 发送消息后收到正确的回复
第二次发送: 波特率错误 115200bps 发送消息后收不到回复
第三次发送 波特率改回正确的9600bps 发送后依然收不到回复
以上就是模拟波特率不小心设置错误的情况,居然产生了严重的问题,即使改回正确的波特率依然无法收到回复。
说明这种做法存在一定的风险。
经过大量的排查,最后确定问题就出在函数 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE)
放置的位置。
波特率正确的正常情况下
USART的CR1
寄存器中,IDLEIE位
打开,ISR寄存器中IDLE
位关闭
同时对应DMA通道寄存器的情况如下
目前收发正常
也能正常进入回调函数
接着改成错误的波特率,收不到回复
并且仿真未在回调函数中的断点处停下,说明不再进入回调函数
查看DMA寄存器,发现DMA没有正确打开(与之前的截图对比)
查看USART寄存器发现也未正确开启
串口波特率错误时,不再进入回调函数。一轮接收结束时,串口空闲中断与DMA均被关闭,而两者的重启在回调函数中通过
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);
开启。这就导致波特率错误时,无法接收后续的新数据。
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);
必须放在void USART1_IRQHandler(void)
中。
这样即使接收错误,也能重新开启串口空闲中断和DMA,不影响下次接收。