(转载)原文链接:https://blog.csdn.net/qq_39344192/article/details/131470735
UART是一种异步串行通信接口,常用于通过串口与外部设备进行通信。它通过发送和接收数据帧来实现数据传输,使用起来相对简单。UART通常包含发送器(Transmitter)和接收器(Receiver),通过两根信号线(传输线)进行双向通信。
UART协议将一长串数据切成很多固定长度的小段,分别发送。每小段数据前后会加上一些附加数据以保证通信的实时性和准确性,最后形成的每个小段叫做一个数据包——即1帧数据。
UART通信的两个设备间,以上因素必须完全一致才能实现数据通信。
UART轮询收发时,CPU会不断检测串口的状态位来判断数据收发的情况。
UART轮询收发是一种简单直接的UART通信方式,它具有以下优点和缺点:
优点
缺点
UART轮询收发适用于简单的、对实时性要求不高的低速通信场景。但在对实时性、效率和灵活性要求较高的应用中,中断或DMA方式可能更加适合。在选择UART通信方式时,需要根据具体应用需求进行权衡和选择。
初始化UART参数:首先,需要对UART进行初始化,包括波特率(Baud rate)、数据位数、校验位、停止位等参数的设置。这些参数决定了数据的传输格式。
初始化函数 | HAL_UART_Init(UART_HandleTypeDef *huart); |
---|---|
功能 | 根据串口句柄指定的参数进行串口初始化。 |
入口参数 | huart :串口句柄的地址指针。 |
返回值 | HAL 状态值。 |
说明 | 使用CubeMX配置工程时,初始化代码会自动生成,我们不需要再对串口进行初始化。 |
发送数据:要发送数据,首先将待发送的数据写入UART发送缓冲区,然后调用轮询发送函数。在轮询方式下,单片机会一直检查是否完成发送,直到超过设定时间或数据发送完成。
轮询发送函数 | HAL_UART_Transmit(UART_Handle TypeDef *huart, uint 8_t *pData, uint 16_t Size, uint 32_t Timeout); |
---|---|
功能 | 在轮询方式下发送一定数量的数据。 |
入口参数 | huart :串口句柄的地址。pData :待发送数据的首地址。Size :发送的字节数。Timeout :超时等待时间, 以毫秒为单位。 |
返回值 | HAL 状态值。 |
接收数据:要接收数据,需要从UART接收缓冲区读取数据。同样,在轮询方式下,单片机会一直检查是否完成接收,直到超过设定时间或接收到所有数据。
轮询接收函数 | HAL_UART_Receive(UART_Handle TypeDef *huart, uint 8_t *pData, uint 16_t Size, uint 32_t Timeout); |
---|---|
功能 | 在轮询方式下接收一定数量的数据。 |
入口参数 | huart :串口句柄的地址。pData :存放数据的首地址。Size :接收数据的字节数。Timeout :超时等待时间, 以毫秒为单位。 |
返回值 | HAL 状态值。 |
使用UART协议,向手机循环发送“Hello World”
语句。
USART包括UART与USRT。我们使用USART的UART模式,即Asynchronous。
单片机 | 蓝牙模块 |
---|---|
RX | TX |
TX | RX |
5V | VCC |
GND | GND |
在main.c中添加如下语句:
/* 添加至引用区 */
#include "string.h" // strlen函数依赖该头文件
/* 添加至变量定义区*/
char txBuffer[] = "Hello World"; // 需要发送的数据
/* 写在main()的while(1)循环内 */
while(1)
{
HAL_UART_Transmit(&huart1, (uint8_t *)txBuffer, strlen(txBuffer), 1000);
HAL_Delay(1000);
}
优点
缺点
UART中断收发相较于UART轮询收发,提高了系统的效率,但是遇到大量、高速的数据传输时仍然会对CPU的性能产生影响。
使能中断后,每收发1帧数据后,UART会触发UART全局中断。此时程序会进入到中断处理函数 USART1_IRQHandler()
。在这个函数中,又调用了HAL库的中断处理函数HAL_UART_IRQHandler(&huart1)
,该函数会通过判断中断类型来决定调用哪个函数。
如果收发完成 -> 进入发送/接收回调函数;
如果产生错误 -> 进入错误回调函数;
如果未收发完 -> 继续发送/接收。
简而言之,每完成一帧数据的收发,都会调用一次中断处理函数,但只有当收发完成时才会调用发送/接收回调函数。在实际操作中,我们一般不需要对中断处理函数HAL_UART_IRQHandler()
进行修改。我们将收发完成时的操作逻辑写在回调函数中即可。
中断发送函数 | HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); |
---|---|
功能 | 在中断方式下发送一定数量的数据。 |
入口参数 | huart :串口句柄的地址。pData :发送数据的首地址。Size :发送数据的字节数。 |
返回值 | HAL 状态值。 |
发送回调函数 | HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); |
---|---|
功能 | 本函数会在中断发送完成时被调用。 |
入口参数 | huart :串口句柄的地址。 |
返回值 | 无 |
中断接收函数 | HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); |
---|---|
功能 | 在中断方式下接收一定数量的数据。 |
入口参数 | huart :串口句柄的地址。pData :存放数据的首地址。Size :接收数据的字节数。 |
返回值 | HAL 状态值。 |
接收回调函数 | HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); |
---|---|
功能 | 本函数会在中断接收完成时被调用。 |
入口参数 | huart :串口句柄的地址。 |
返回值 | 无 |
在stm32f1xx_hal_uart.c
文件中,我们可以找到回调函数的声明方式(下以接收回调函数为例)。
关注到函数声明前带有__weak
修饰,我们一般将这种函数称之为“弱函数”。含有__weak
标识的函数,我们可以自行声明一个同名函数,最终编译器在编译的时候会选择我们自己定义的函数。如果我们没有声明该函数,则会调用有__weak
修饰的函数。因此,我们只需要在合适的位置自己声明一个回调函数即可,不必在该文件中进行修改。
通过蓝牙向单片机发送三个英文字母,单片机将大小写翻转后发回。
在上一节配置的基础上,我们打开UART1的全局中断。
我们在main.c
中添加如下代码
/* 添加至宏定义区 */
#define rxDataLen 3 //接收三个字节的数据
/* 添加至变量定义区*/
char rxBuffer[rxDataLen]; // 存储接收到的数据的数组
int main()
{
/* 添加至while(1)循环前*/
HAL_UART_Receive_IT(&huart1, (uint8_t *)rxBuffer, rxDataLen); // 中断方式接收三个字节数据
while(1) // 注意不要将中断接收写在while循环内
{
HAL_Delay(1);
}
}
在完成一次接收后,我们需要将接收到的字母大小写翻转并输出。我们需要在接收回调函数中实现这一功能,定义接收回调函数如下。
// 接收回调函数(由于需要覆盖原先的弱函数,本函数名称不能更改)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1) // 如果是串口1
{
for(int i = 0; i < rxDataLen; i++)
{
if(rxBuffer[i] >= 'a' && rxBuffer[i] <= 'z')
{
rxBuffer[i] += 'A' - 'a';
}
else if(rxBuffer[i] >= 'A' && rxBuffer[i] <= 'Z')
{
rxBuffer[i] -= 'A' - 'a';
}
}
HAL_UART_Transmit_IT(&huart1, (uint8_t *)rxBuffer, rxDataLen);
}
// 使用HAL_UART_Receive_IT()时,只会进入一次中断。因此需要在回调函数内再次调用该函数
HAL_UART_Receive_IT(&huart1, (uint8_t *)rxBuffer, rxDataLen);
}
DMA(Direct Memory Access,直接内存访问)是一种计算机系统中的数据传输技术,它允许外设直接访问寄存器,而无需通过CPU的干预。DMA技术可以提高数据传输的效率和系统性能,减轻CPU的负担。
简单点来说,DMA收发与轮询、中断都有所不同。在收发数据时,CPU只需告诉DMA数据的来源以及目的地址等信息即可,而无需参与中间的所有传输过程(例如中断收发时每收发一帧数据都会进入中断处理函数,而DMA只在接收完成等少数时刻触发中断,大大降低了CPU的压力)。
空闲中断(Idle Interrupt)是UART通信中的一种中断类型。当UART处于空闲状态(没有接收到数据)且持续时间超过一个帧的传输时间时,空闲中断会触发,调用上节提到的函数USART1_IRQHandler
。空闲中断可以用于检测数据帧的结束或接收数据的完成。
使能UART的特定中断(宏) | __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__); |
---|---|
参数 | __HANDLE__ :指向UART句柄的指针;__INTERRUPT__ :要使能的中断标志位。 |
说明 | 本节会使用到空闲中断,即UART_IT_IDLE |
DMA发送函数 | HAL_UART_Transmit_DMA(UART_Handle TypeDef *huart, uint 8_t *pData, uint 16_t Size); |
---|---|
入口参数 | huart :串口句柄的地址。pData :发送数据的首地址。Size :发送数据的字节数。 |
DMA接收函数 | HAL_UART_Receive_DMA(UART_Handle TypeDef *huart, uint 8_t *pData, uint 16_t Size); |
---|---|
入口参数 | huart :串口句柄的地址。pData :存放数据的首地址。Size :接收数据的字节数。 |
禁用DMA传输(宏) | __HAL_DMA_DISABLE(_HANDLE_); |
---|---|
参数 | __HANDLE__ :指向UART句柄的指针 |
使能DMA传输(宏) | __HAL_DMA_ENABLE(_HANDLE_); |
---|---|
参数 | __HANDLE__ :指向UART句柄的指针 |
设置DMA传输计数器值(宏) | __HAL_DMA_SET_COUNTER(__HANDLE__, __COUNTER__); |
---|---|
参数 | __HANDLE__ :指向UART句柄的指针__COUNTER__ :要设置的计数器值。 |
获取DMA传输的计数器值(宏) | __HAL_DMA_GET_COUNTER(__HANDLE__); |
---|---|
参数 | __HANDLE__ :指向UART句柄的指针 |
宏定义 | #define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR) |
说明 | 这个宏需要我们自行定义。 我们可以利用这个宏定义来获取接收到的数据的长度。 接收到的数据长度 = 设置的长度 - 当前DMA计数器的值,即 rxLen = rxBufferLen - __HAL_DMA_GET_COUNTER(__HANDLE__); |
结合DMA以及空闲中断,实现接收不定长度的一段数据,发送这一数据的长度。
参考前两节,配置好UART以及全局中断。我们还需要开启UART的DMA接收。
在本节中,我们将代码按用途划分为不同函数,逐一实现。先定义如下全局变量以及宏变量。
#define rxBufferLen 40 // 接收数据的最大长度
#define __HAL_DMA_SET_COUNTER(__HANDLE__, __COUNTER__) ((__HANDLE__)->Instance->CNDTR = (uint16_t)(__COUNTER__))
char rxBuffer[rxBufferLen]; // 用于存放接收到的数据
程序运行之初,我们需要先对DMA接收进行初始化,该初始化函数应当在main()函数内调用一次。
void UART_InitDMAReceive() // 初始化函数
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲中断标志位
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 开启UART的空闲中断
HAL_UART_Receive_DMA(&huart1, (uint8_t*)rxBuffer, rxBufferLen ); // 启动DMA接收
}
每当发生空闲中断或接收到的数据超过缓存区大小时,会产生一次中断。我们首先需要自行编写中断回调函数。
void UART_DMAIdleCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
// 停止DMA接收
__HAL_DMA_DISABLE(huart->hdmarx);
// 计算接收到的数据长度
uint8_t dataLen = rxBufferLen - __HAL_DMA_GET_COUNTER(huart->hdmarx) + '0';
// 发送数据长度,使用轮询方法
HAL_UART_Transmit(&huart1, &dataLen, 1, 10);
// 重启DMA接收
__HAL_DMA_SET_COUNTER(huart->hdmarx, rxBufferLen); // 重设DMA计数器
__HAL_DMA_ENABLE(huart->hdmarx); // 使能DMA接收
}
}
我们在中断处理函数内添加如下内容(该函数位于文件stm32g4xx_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_FLAG_IDLE) != RESET) // 判断中断是否为空闲中断
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲中断标志位
UART_DMAIdleCallback(&huart1); // 空闲回调函数
}
/* USER CODE END USART1_IRQn 1 */
}