串口
前言
不定长数据接收的原理及其解决的方法
在 STM32 中。USART 发送接收有三种基本方式,轮询、中断和 DMA。
轮询方式为堵塞模式,使用超时管理机制。它每次接收一个字节,在规定时间内接收固定长度的数据。在对于某些数据不固定长度接收的数据,轮询的方式有时候不够灵活。
中断的方式为每一个字节都中断一次,当时比较消耗系统资源。特别是HAL库中,从中断到回调函数运行了不少的程序,频繁的中断很可能造成数据溢出。为了避免这个问题,我们使用指定接收一定长度的数据,再调用回调函数,这会让我们可以接收大数据,但是这种情况则造成了,要求每次的包是固定长度。
为了解决以上一些问题,网上最常用的办法是使用空闲中断,即在串口空闲的时候,触发一次中断,通知内核,本次运输完成了。数据传输过程为了尽量不占用CPU的处理数据时间,所以就使用DMA接收串口的数据。
什么是串口空闲
一般我们串口接收的时候都是使用的RXNE,接收到一个字节数据就进入一次中断,然后把它放入缓存,但是数据量很大的时候会频繁进入中断影响单片机的时效性。这时就可以使用到IDLE空闲中断,即在接收到一段数据后在一定的时间检测到没有数据到来,就认为此时串口总线空闲便产生一个空闲中断。
空闲中断的标志也就是停止位,一个上升沿
轮询和中断方式的简单实现 介绍
轮询
生成
STM32CubeMX 配置 STM32F407时钟树和烧录方式
API代码
串口阻塞式发送数据,使用超时管理机制 HAL_UART_Transmit()
串口阻塞式接收数据,使用超时管理机制 HAL_UART_Receive()
中断
生成
在上面的基础上,在NVIC中使能串口中断,并配置优先级。
串口中断模式发送 HAL_UART_Transmit_IT(),发送完会回调 HAL_UART_TxCpltCalback()
串口中断模式接收 HAL_UART_Receive_IT(),接收完会回调 HAL_UART_RxCpltCallback()
每个字节进一次中断,在 HAL_UART_IRQHandler() 中由HAL库自行处理。
对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用
总结:使用中断方式收发串口的流程
1、调用函数开启一次中断收发的流程
2、每发送或接收到一个字节会进入中断,由HAL库进行处理,用户程序无需参与
3、HAL调用发送或接收完成回调函数
拓展:其他常用串口中断
- UART_IT_TXE 发送寄存器空中断,开启后当一个字节传送完毕就会进入中断,HAL中断方式发送依赖该中断。
- UART_IT_RXE 接收寄存器非空中断,开启后当接收到一个字节会进入中断,HAL库中断方式接收依赖该中断。
DMA
DMA(Direct memory access),即直接存储器访问。用于在外设与存储器之间以及存储器与存储器之间提供一种高速数据传输的方式。它在开始发送和接收完成数据时会给CPU相应的信号或者中断,在数据传输过程中无需CPU参与,通过硬件方式为RAM与I/O设备提供一条直接传送数据的通道。
DMA发送函数
HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
函数主要功能是以DAM模式发送pData指针指向的数据中固定长度的数据,并同时设置和使能DMA中断,具体怎么设置和使能中断的,打开此函数源码会发现下面这个函数。
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
此函数是启动DMA传输并启用中断。从函数源码可以看出此函数首先判断DMA传输状态是否是Ready:
- 如果是Ready则使能了DMA三个中断:DMA 半传输,DMA传输完成和DMA传输出错。如果发送数据正常,进入2次进入DMA中断(DMA 半传输和DMA传输完成);错误的话进入DMA传输出错中断;从这里也能看出HAL库比标准库严谨但效率低,DMA 半传输中断如果觉得效率低可以在程序中屏蔽掉,这样数据正常发送完成就只会进入一次DMA完成中断,三种DMA中断其实是同一个函数
HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
此函数功能是处理DMA中断请求,主要工作是清除中断标志位,改写DMA的状态,只有把状态改成HAL_DMA_STATE_READY,下一次才能正常使用DMA功能,否则会进入 HAL_BUSY状态 - 如果不是Ready状态,则进入HAL_BUSY状态,这也是为什么连续使用
HAL_UART_Transmit_DMA()
函数发送数据,第二次会发不出来数据,而且第二次函数会进入HAL_BUSY状态,所以要想使用HAL_UART_Transmit_DMA()
函数连续发送数据,相邻两次之间要有延时间隔或者检测DMA数据是否完成。
DMA 接收函数
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数说明:此函数的功能在DMA模式下接收大量数据,同时设置DMA线和哪个串口外设连接,以及将DMA线接收到的数据搬 *pData对应地内存中,和上面DMA发送函数一样,此函数同时具有设置和使能DMA中断的功能。可以看出此函数不仅是一个接收函数同时也是一个初始化函数,在main()之前调用,初始化串口和DMA连接和DMA接收BUF,以及设置和使能中断。如果DMA模式设置成循环模式时,只需设置这一次,如果DMA模式设置成正常模式时,每次读取完数据后需要再从新设置一次(就是再调用一次此函数),分析函数源码会发现此函数内部同样会调用以下两个函数,使用方法和分析和发送类似。
-
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
-
HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);
主要说说HAL_UART_Receive_DMA()
,怎么配合IDLE串口空闲中断使用,main()函数之前一般调用此函数,一个主要目的是指明DMA传输串口数据存到指定的地方。一般情况我们会开辟一个全局变量的缓存
extern uint8_t receive_buff[BUFFER_SIZE]
。比如函数初始化为HAL_UART_Receive_DMA(&huart2, (uint8_t*)receive_buff, BUFFER_SIZE)
; 就是设置串口2接收到数据通过DMA线直接到receive_buff
中了,配合串口空闲中断,当进入串口空闲中断,说明一帧数据已接收完成。我们读取receive_buff相应长度的数据就是此次接收一帧的数据,这里还需要再介绍一个函数; -
__HAL_DMA_GET_COUNTER(__HANDLE__)
此函数的功能:获取当前DMA通道传输中receive_buff[BUFFER_SIZE]
缓存还剩余多少个数据单元。这样就能算出这一帧数据到底接收了多少单元的数据(数据长度=缓存总长度-缓存剩余的长度),
length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
空闲中断
空闲中断介绍
空闲中断是接受数据后出现一个byte的高电平(空闲)状态,就会触发空闲中断.并不是空闲就会一直中断,准确的说应该是上升沿(停止位)后一个byte,如果一直是低电平是不会触发空闲中断的(会触发break中断)。所以为了减少误进入串口空闲中断,串口RX的IO管脚建议设置成Pull-up<上拉模式>,串口空闲中断只是接收的数据时触发,发送时不触发。
# STM32系列-串口-uart-软件引脚内部上拉 或者 外部电阻上拉-原因问题的搜寻
但是我本人通过软件,把RX配置成PULL UP之后,串口就无法正常工作了
空闲中断使用
串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发生,则认为串口空闲了,进入相应的串口中断。在中断内清除空闲中断标志位和调用串口回调函数,在回调函数内处理读取,判断,处理接收的数据。处理完数据以后我再把串口中断打开重复上面的流程,就可以完整的接收一帧一帧的数据。同时利用空闲中断也可以省去很多的的判断。
实现DMA+IDLE
生成
在前面的基础上,因为我使用了USART1的DMA发送和接收数据,首先是将UART1_RX和UART1_TX映射到DMA对应的通道上,对应的配置就是在CubeMX配置就是添加相应的DMA通道。再设置DMA几个参数。
- DMA模式,DMA有循环模式和正常模式2种模式。
(1) DMA接收如果用 循环模式,只需调用一次接收函数即可,重复调用也只有第一次调用有效 若是 正常模式,需要在每次接收完成后重复调用
(2) DMA发送要用 正常模式,需在每次发送时重复调用 还要打开UART中断,否则即使重复调用DMA发送函数也只有第一次发送有效,其余不会执行(UART发送状态一直处于 BUSY) 若是 循环模式,则会连续不断地发送,不会停止。 - 地址增加
(1)在传输数据时,可以配置指向寄存器数据的指针是否自动向后递增。
(2)通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。
我这里将RX和TX 都设置为了 Normal
如果把 Rx 设置为Circular,就要再配置个循环队列来存储数据
我本人理解是,若采用循环队列,数据实时性得不到保证,就没有采用这种方法了
普通模式和循环模式的区别在于,普通模式下,DMA只会接收一次数据,接收完成后就会停止,需要接收时再开启;而循环模式下,DMA会一直接收数据,直到接收缓存区满或者手动停止。
根据自己需求定模式,如果是数据有间隔,空闲中断的这种情况下,处理一帧数据,建议用Normal,有很多例程用Circular模式的情况下处理一帧数据,还要在中断中使用函数HAL_UART_DMAStop(); 来停止此次接收,处理数据后再重新启动接收,就显得有点多次两举了,如果用Circular模式,没有用HAL_UART_DMAStop停止接收的话,DMA会一直循环填充数据,size为最大,因为数据是循环填充的,所以数据的位置不定,看上去是乱的。
关于禁用DMA传输的半传输中断,在空闲中断接收数据时,因为是不定长,假如在开启接收最大长度为128,那么当接收到64字节数据的时候就会触发半传输中断,虽然半传输中断的回调函数是HAL_UART_RxHalfCpltCallback(); 但是测试发现传输一半字节长度也会触发接收事件回调函数HAL_UARTEx_RxEventCallback(); 所以,还是建议关闭半传输中断。
代码
大多数人介绍的方法(我没成功)
准备工作
#define UART_BUFFSIZE 256
typedef struct{
uint16_t Uart_SendLens; //待发送数据长度
uint16_t Uart_RecvLens; //接收到的数据长度
uint8_t Uart_SentBuff[UART_BUFFSIZE]; // 发送数组
uint8_t Uart_RecvBuff[UART_BUFFSIZE]; // 接收数组
void (*Send_function)(uint8_t data); // 发送函数指针
void (*Analysis_function)(uint8_t data); // 解析函数指针
}UART_STR;
UART_STR Uart1_Str;
开启空闲中断和DMA
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, Uart1_Str.Uart_RecvBuff, sizeof(Uart1_Str.Uart_RecvBuff)-1);
/* USER CODE END USART1_Init 2 */
}
File stm32f4xx_it.c 添加函数,
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 */
if (__HAL_UART_GET_FLAG(&huart1, UART_IT_IDLE) != RESET){
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清楚空闲中断标志位
HAL_UART_DMAStop(&huart1); // 停止DMA传输
Uart1_Str.Uart_RecvLens = UART_BUFFSIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 接收的数据长度 = DMA接收通道总长度 - 剩余长度
HAL_UART_Transmit_DMA(&huart1, Uart1_Str.Uart_RecvBuff, Uart1_Str.Uart_RecvLens); // 将接收到的数据打印出来
HAL_UART_Receive_DMA(&huart1, Uart1_Str.Uart_RecvBuff, sizeof(Uart1_Str.Uart_RecvBuff)-1); // 重新开启DMA接收(如果是把RX配置成Normal)
}
/* USER CODE END USART1_IRQn 1 */
}
本人使用这种方式,经过调试发现连空闲中断都没有进去。而大多数人都是成功实现的,不知道问题出在哪里
本人成功的方法(2024.2)
在hal库空闲中断接收函数API
在stm32f4xx_hal_uart.h中
//在阻塞模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout);
//在中断模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
//在DMA模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
//使用空闲中断时的接收回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
准备工作
#define UART_BUFFSIZE 256
typedef struct{
uint16_t Uart_SendLens; //待发送数据长度
uint16_t Uart_RecvLens; //接收到的数据长度
uint8_t Uart_SentBuff[UART_BUFFSIZE]; // 发送数组
uint8_t Uart_RecvBuff[UART_BUFFSIZE]; // 接收数组
void (*Send_function)(uint8_t data); // 发送函数指针
void (*Analysis_function)(uint8_t data); // 解析函数指针
}UART_STR;
UART_STR Uart1_Str;
调用 HAL_UARTEx_ReceiveToIdle_IT()
在使用HAL_UARTEx_ReceiveToIdle_DMA时,如果出现错误,即函数返回值=HAL_ERROR时,会被重置为HAL_UART_RECEPTION_STANDARD 标准接收模式,即中断回调函数为HAL_UART_RxCpltCallback,触发条件为当接收的数据满启动时指定的Size,如启动接收时指定接收长度为128字节,当接收到128字节时就会触发一次中断(如果DMA为循环模式)。
这个错误可能出现的情况:开启状态下已经触发了一个中断,比如溢出或者重启时别的设备发送了一串数据,触发了一个空闲中断,同时有数据在串口的缓冲区,就会导致开启HAL_UARTEx_ReceiveToIdle_DMA时产生一个错误返回值,同时被置为标准接收模式。
解决办法:清空接收缓冲区(不清空的话,依旧会返回HAL_ERROR),同时清除错误标志。调用HAL_UART_AbortReceive函数可以同时满足停止DMA传输、清空接收缓冲区、清除标志。
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
uint8_t Error_code_uart1;
// 开启DMA传输UART空闲中断中接收的数据,并在接收到UART空闲中断后停止传输
Error_code_uart1 = HAL_UARTEx_ReceiveToIdle_DMA(&huart1, Uart1_Str.Uart_RecvBuff, sizeof(Uart1_Str)-1);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
// 如果有错误的话
if (Error_code_uart1 != HAL_OK)
{
// 清空缓冲,置位一些标志,具体看函数内部
HAL_UART_AbortReceive(&huart1);
// 重启接收
Error_code_uart1 = HAL_UARTEx_ReceiveToIdle_DMA(&huart1, Uart1_Str.Uart_RecvBuff, sizeof(Uart1_Str)-1);
}
/* USER CODE END USART1_Init 2 */
}
在回调函数 HAL_UARTEx_RxEventCallback()中做相应处理
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *UartHandle, uint16_t Size) {
if (UartHandle == &huart1) {
/******************************************************************************
* 功能:DMA接收,空闲中断
*****************************************************************************/
//重启接收
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)Uart1_Str.Uart_RecvBuff, Size);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, Uart1_Str.Uart_RecvBuff, sizeof(Uart1_Str) - 1);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
}
}
后续的开发,可以通过互斥信号量,在数据解析的时候,不允许数据接收。
参考
【STM32F4+CubeMX零基础快速入门】串口收发全攻略
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送
CubeMX生成的中断函数HAL_UART_IRQHandler(*huart1)如何使用。