一、UART基本原理
1. 基本介绍
从直接上手应用的角度来说,我们无非就是想要实现发送与接收这两个基本功能,再在此基础上实现其他的一些应用,比如什么时候发送一些数据,对接收到的数据应该做一些什么处理等等,而如何实现收发功能常用的基本就三个:
1. 普通阻塞式收发
- 应用:直接在程序运行的过程中,在你想要发送数据的地方调用发送函数,需要接收数据的地方调用接收函数,函数会一直阻塞程序,直到数据发送或接收完成。
- 优点:简单
- 缺点:效率低,实时性差
- 场景:进行普通的收发测试
2. 中断模式收发
- 应用:在配置好后,使用中断的方式对数据直接进行收发,对应的回调函数里编写相关中断处理逻辑,当数据发送或接收完成时,会触发中断处理函数,然后这个中断处理函数会调用相应的回调函数进行处理。
- 优点:提高 CPU 利用率,实时性较好
- 缺点:稍微复杂一点点,如果频繁中断会影响性能(正常情况下基本不会)
- 场景:对单个数据的处理及时性要求极高,且数据传输量较小、频率较低时使用
3. DMA模式收发
- 应用:与使用中断进行收发类似,但是不占用 CPU 资源,直接通过DMA通道访问存储器数据,使用DMA的方式对数据进行收发,然后在对应的回调函数里编写相关中断处理逻辑。
- 优点:高效的数据传输,减轻 CPU 负担
- 缺点:稍微复杂一点
- 场景:需要快速传输大量数据,并且对数据的整体传输效率和系统资源利用率要求较高时使用
(中断模式下每次数据传输都会触发一次中断,CPU 需要频繁地响应中断并执行中断服务程序,而DMA 控制器独立于 CPU 工作,在数据传输过程中,CPU可以继续执行其他任务,无需一直关注数据传输的过程。)
接下来简单熟悉一下UART的基本知识,实际应用可以直接从第二章阅读
1.1 应用场景与特点
通常应用于传输速率要求不高的情况下,如PC与单片机之间通信,方便查看数据
优点: 方便简单
缺点: 不能远距离传输信号,通信速度慢,不能一对多通信
1.2 电平标准
TTL电平:+3.3V或+5V表示逻辑1,0V表示逻辑0
RS232电平:-3 ~ -15V表示逻辑1,+3 ~ +15V表示逻辑0
RS485电平:两线压差+2 ~ +6V表示逻辑1,-2 ~ -6V表示逻辑0(差分信号)
本篇为普通TTL电平标准
1.3 串口参数
- 波特率:串口通信的速率
- 起始位:标志一个数据帧的开始,固定为低电平
- 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
- 校验位:用于数据验证,根据数据位计算得来
- 停止位:用于数据帧间隔,固定为高电平
2. 通信协议
2.1 基本结构
2.2 帧格式
二、CubeMX配置
以下三种方式选择自己需要的配置好后直接去代码章节编写程序即可
1. 普通阻塞式配置
步骤 1:打开 STM32CubeMX 并创建新项目
步骤 2:配置 RCC(复位和时钟控制)
步骤 3:配置 USART
普通阻塞收发配置这些即可
2. 中断模式配置
步骤4:启用中断
3. DMA模式配置
步骤5:配置DMA
三、代码实现
在HAL库里已经有各种库函数可以完成接收发送等等工作,在CubeMX里配置好后,对应的代码就已经生成了,我们需要做的其实就是对这些函数进行包装或者直接使用,并在对应的回调函数里编写自己需要的逻辑处理,这样基本就可以解决绝大多数使用UART的需求了,常用的函数如下:
/* 初始化 */
HAL_UART_Init // 初始化UART,根据传入的UART_HandleTypeDef结构体配置UART的各项参数,如波特率、数据位、停止位等
/* 收发功能 */
HAL_UART_Transmit // 阻塞式发送,程序会在此函数处等待,直到指定数量的数据发送完成或者超时才会继续执行后续代码
HAL_UART_Receive // 阻塞式接收,程序会阻塞在该函数,直到接收到指定数量的数据或者超时
HAL_UART_Transmit_IT // 中断模式发送,启动UART发送操作,数据发送完成后会触发相应的发送完成回调函数(HAL_UART_TxCpltCallback),在发送过程中CPU可以处理其他任务
HAL_UART_Receive_IT // 中断模式接收,启动UART接收操作,接收到指定数量的数据后会触发相应的接收完成回调函数(HAL_UART_RxCpltCallback),在接收过程中CPU可处理其他事务
HAL_UART_Transmit_DMA // DMA模式发送,利用DMA(直接内存访问)控制器进行数据发送,无需CPU干预,数据发送完成会触发相应回调,提高数据传输效率
HAL_UART_Receive_DMA // DMA模式接收,使用DMA控制器进行数据接收,将接收到的数据直接传输到指定的内存区域,接收完成触发相应回调,减少CPU负担
HAL_UARTEx_ReceiveToIdle_DMA
/* 中断服务 */
HAL_UART_IRQHandler // UART中断处理函数,在UART的中断服务程序中调用,负责处理UART的各种中断事件,如接收中断、发送中断、错误中断等
HAL_UART_TxCpltCallback // 发送完成回调函数,当使用中断模式或DMA模式发送数据完成时被调用,可在此函数中编写发送完成后的处理逻辑
HAL_UART_RxCpltCallback // 接收完成回调函数,当中断模式或DMA模式下接收到指定数量的数据时被调用,可用于处理接收到的数据
HAL_UART_ErrorCallback // 错误回调函数,当UART通信过程中出现错误(如奇偶校验错误、帧错误等)时被调用,可在此进行错误处理操作
HAL_UARTEx_RxEventCallback
/* 其他 */
HAL_UART_GetError // 获取UART当前的错误状态,返回值表示具体的错误类型,可用于判断UART通信是否出现异常
HAL_UART_DMAResume // 恢复UART的DMA传输操作,当DMA传输被暂停后,可调用此函数恢复传输
HAL_UART_DMAPause // 暂停UART的DMA传输操作,在需要暂时停止DMA传输时使用
HAL_UART_DMAStop // 停止UART的DMA传输操作,结束当前正在进行的DMA传输任务
建议新建代码文件,把程序放在里面,方便移植,重复使用
1. 普通阻塞模式
1.1 重定向收发函数
为了方便我们使用,可以将发送与接收功能重写在 fputc
和 fgetc
函数里,实现使用 printf
和 scanf
进行串口数据的收发
1.2 Keil配置
1.3 重写 fput() 函数与 fgetc() 函数
My_Usart.c
#include "My_Usart.h"
#include "usart.h"
#include <stdio.h>
#include <string.h>
// 重定向printf输出到UART
int fputc(int ch, FILE *f)
{
// 通过UART10发送单个字符
HAL_StatusTypeDef status = HAL_UART_Transmit(&huart10, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
if (status != HAL_OK) { return -1; } // 发送失败返回-1
return ch; // 发送成功返回字符
}
// 重定向scanf输入从UART
int fgetc(FILE *f)
{
uint8_t ch;
// 通过UART10接收单个字符
HAL_StatusTypeDef status = HAL_UART_Receive(&huart10, &ch, 1, HAL_MAX_DELAY);
if (status != HAL_OK) { return -1; } // 接收失败返回-1
return (int)ch; // 接收成功返回字符
}
My_Usart.h
#ifndef MY_USART_H
#define MY_USART_H
#include "main.h"
/* 这些后面会使用,如果只用阻塞模式这些不声明也行 */
// 声明全局变量
extern uint8_t rx_buffer[RX_BUFFER_SIZE]; // 接收缓冲区
extern uint16_t rx_index; // 当前接收位置索引
#endif
1.4 功能测试
main.c
/* USER CODE BEGIN Includes */
#include "My_Usart.h" // 添加头文件
/* USER CODE END Includes */
/* USER CODE BEGIN 2 */
char data[RX_BUFFER_SIZE];
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
while (1)
{
printf("Please enter some information: \n");
if (scanf("%s", data) == 1) {
printf("You entered: %s\r\n", data);
} else {
printf("Invalid input! Please try again.\r\n");
// 清空输入缓冲区
while (getchar() != '\n');
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
串口助手
进行测试
2. 中断模式
2.1 添加错误处理函数
My_Usart.c
// UART错误处理函数
void UART_Error_Handler(UART_HandleTypeDef *huart)
{
// 检查并处理各种UART错误
if (huart->ErrorCode != HAL_UART_ERROR_NONE)
{
switch (huart->ErrorCode)
{
case HAL_UART_ERROR_PE: // 奇偶校验错误
printf("UART Parity Error detected on UART%d!\n", (huart == &huart10) ? 1 : 0);
break;
case HAL_UART_ERROR_NE: // 噪声错误
printf("UART Noise Error detected on UART%d!\n", (huart == &huart10) ? 1 : 0);
break;
case HAL_UART_ERROR_FE: // 帧错误
printf("UART Frame Error detected on UART%d!\n", (huart == &huart10) ? 1 : 0);
break;
case HAL_UART_ERROR_ORE: // 溢出错误
printf("UART Overrun Error detected on UART%d!\n", (huart == &huart10) ? 1 : 0);
break;
case HAL_UART_ERROR_DMA: // DMA错误
printf("UART DMA Error detected on UART%d!\n", (huart == &huart10) ? 1 : 0);
break;
default: // 未知错误
printf("Unknown UART Error detected on UART%d!\n", (huart == &huart10) ? 1 : 0);
break;
}
// 复位UART并重新初始化
HAL_UART_DeInit(huart);
HAL_UART_Init(huart);
// 重新启动接收中断
uint8_t dummy;
HAL_UART_Receive_IT(huart, &dummy, 1);
}
}
2.2 添加中断接收处理逻辑
My_Usart.c
// 接收缓冲区及索引
uint8_t rx_buffer[RX_BUFFER_SIZE]; // UART接收数据缓冲区
uint16_t rx_index = 0; // 当前接收位置索引
// 初始化UART接收
void My_USART_Init(void)
{
// 启动UART10中断接收
HAL_StatusTypeDef status = HAL_UART_Receive_IT(&huart10, &rx_buffer[rx_index], 1);
if (status != HAL_OK) {
UART_Error_Handler(&huart10); // 初始化失败处理错误
}
}
// UART接收逻辑处理
void My_USART_Receive_Logic(void)
{
// 检查是否接收到结束字符
if (rx_buffer[rx_index] == END_CHAR) {
rx_buffer[rx_index] = '\0'; // 添加字符串结束符
printf("Received data: %s\n", rx_buffer); // 打印完整接收数据
rx_index = 0; // 重置接收索引
}
// 检查是否接收到"Scanf"命令
else if (strcmp((char *)rx_buffer, "Scanf") == 0) {
rx_buffer[rx_index] = '\0'; // 添加字符串结束符
printf("接收成功!\n"); // 打印接收成功信息
rx_index = 0; // 重置接收索引
}
else {
rx_index++; // 移动接收索引
// 检查缓冲区是否溢出
if (rx_index >= RX_BUFFER_SIZE) {
printf("Buffer overflow! Resetting buffer.\n"); // 缓冲区溢出警告
rx_index = 0; // 重置接收索引
}
}
// 继续接收下一个字符
HAL_StatusTypeDef status = HAL_UART_Receive_IT(&huart10, &rx_buffer[rx_index], 1);
if (status != HAL_OK) {
UART_Error_Handler(&huart10); // 接收失败处理错误
}
}
// 最后在UART接收完成中断回调函数里调用
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 检查是否为UART10的中断
if (huart == &huart10)
{
My_USART_Receive_Logic(); // 调用接收逻辑处理函数
}
}
2.3 功能测试
My_Usart.h
自定义END_CHAR
为接收数据结束符(比如需要发送“123”,输入“123”然后回车,再发送,接收函数My_USART_Receive_Logic
接收到数据时检测到结束符时存入当前数据,然后重置接收)
// 定义常量
#define RX_BUFFER_SIZE 256 // 接收缓冲区大小
#define END_CHAR 0x0D // 数据结束字符(回车)
// 添加函数声明
void My_USART_Init(void); // 初始化UART接收
void My_USART_Receive_Logic(void); // 接收数据处理逻辑
void UART_Error_Handler(UART_HandleTypeDef *huart); // UART错误处理
main.c
中断模式下进行数据接收无需在主程序里进行操作,只要接收到数据就会中断到对应的函数,储存数据并进行自定义的操作
/* USER CODE BEGIN 2 */
My_USART_Init(); // 初始化中断接收
char *tx_data = "Printf\n"; // 要发送的字符串
printf("%s", tx_data);
/* USER CODE END 2 */
串口助手
进行测试
3. DMA模式
3.1 HAL_UARTEx_ReceiveToIdle_DMA 接收函数
HAL_UART_Receive_DMA
和HAL_UARTEx_ReceiveToIdle_DMA
- HAL_UART_Receive_DMA: 以接收到指定数量的数据为停止条件,适用于固定长度数据的接收,使用 HAL_UART_RxCpltCallback 作为接收完成回调函数。
- HAL_UARTEx_ReceiveToIdle_DMA: 既可以在接收到指定数量的数据时停止,也可以在检测到空闲线路时停止,更适合不定长数据的接收,使用
HAL_UARTEx_RxEventCallback 作为接收事件回调函数。
3.2 HAL_UARTEx_RxEventCallback 回调函数
首先需要认识并区分一下HAL_UART_RxCpltCallback
和HAL_UARTEx_RxEventCallback
这两个回调函数
HAL_UART_RxCpltCallback
原理: 这是一个用于处理 UART 接收完成事件的回调函数。当使用 HAL_UART_Receive_DMA 或 HAL_UART_Receive_IT 函数启动 UART
接收,并且接收到指定数量的数据时,该回调函数就会被触发。它属于基础的接收完成回调函数,在传统的 UART 接收处理中较为常用。
场景: 常用于按固定长度接收数据的场景,比如你预先知道要接收的数据长度是多少,在启动接收时指定了这个长度,当接收到这么多数据后,就可以在该回调函数中对数据进行处理。
HAL_UARTEx_RxEventCallback
原理: 这是一个扩展的 UART 接收事件回调函数。当使用 HAL_UARTEx_ReceiveToIdle_DMA 函数启动 UART
接收时,在检测到空闲线路(即一帧数据接收完毕)或者接收到指定数量的数据时,该回调函数会被触发。它提供了更灵活的接收处理方式,特别是对于不定长数据的接收。
场景: 适用于接收不定长数据的场景,比如在通信协议中,每一帧数据的长度可能不同,通过检测空闲线路可以判断一帧数据是否接收完毕,然后在回调函数中根据实际接收到的数据长度进行处理。
所以我们在使用DMA通道进行uart传输数据时,最好使用HAL_UARTEx_ReceiveToIdle_DMA
进行数据接收,然后在 HAL_UARTEx_RxEventCallback
里编写接收后的逻辑处理,但是除了检测到空闲线路和接收到指定数量的数据时会触发这个回调函数外,接收到一半数量的数据时也会触发,所以在我们不需要这个功能的情况下,需要调用__HAL_DMA_DISABLE_IT(&hdma_usart10_rx, DMA_IT_HT);
手动关闭该触发条件(&hdma_usart10_rx
为配置的UART对应的DMA通道,DMA_IT_HT
为DMA接收到一半数量中断功能)
3.3 DMA接收功能实现
My_Usart.c
// 初始化UART接收
void My_USART_Init(void)
{
// 启动UART10 DMA接收
HAL_StatusTypeDef status = HAL_UARTEx_ReceiveToIdle_DMA(&huart10, rx_buffer, RX_BUFFER_SIZE);
// 关闭DMA过半中断
__HAL_DMA_DISABLE_IT(&hdma_usart10_rx, DMA_IT_HT);
if (status != HAL_OK)
{
UART_Error_Handler(&huart10); // 初始化失败处理错误
}
}
// UART接收逻辑处理
void My_USART_DMARx_Logic(void)
{
HAL_UART_Transmit_DMA(&huart10, rx_buffer, RX_BUFFER_SIZE);
HAL_StatusTypeDef status = HAL_UARTEx_ReceiveToIdle_DMA(&huart10, rx_buffer, RX_BUFFER_SIZE);
if (status != HAL_OK)
{
UART_Error_Handler(&huart10); // 接收失败处理错误
}
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart10)
{
My_USART_DMARx_Logic(); // 调用接收逻辑处理函数
}
}
My_Usart.h
void My_USART_DMARx_Logic(void); // 接收数据处理函数声明
3.4 功能测试
串口助手
进行测试
总结
新手求喷,第一次写博客,一个UART写了这么久,起初就是想要回顾一下知识然后记录一下方便日后回顾,再提升提升写作能力,算是边学边写,结果在这个过程中发现了一些之前没注意的知识点,同时也系统的构建了这整个知识体系框架,费了点功夫但确实对自己学习知识有很好的帮助,而且也有可能帮到其他人,以后有时间会继续写下去。
有不足的地方或者有任何问题请直接发在评论区或者私信,我看到了会第一时间回复并更改!