目录
UART通信协议的介绍
实现串口数据发送
CubeMX配置
printf重定向代码编写
实现串口数据接收
轮询方式实现串口数据接收
接收单个字符
接收不定长字符串(接收的字符串以\n结尾)
中断方式实现串口数据接收
CubeMX配置
UART中断方式接收数据函数是一次性的
HAL_UART_Receive_IT() 调用位置
接收单个字符
接收不定长字符串
UART通信协议的介绍
UART (Universal Asynchronous Receiver/Transmitter) 是一种串行异步全双工通信协议。
UART通信的核心思想是异步和串行,全双工。
-
异步 (Asynchronous):通信双方不需要共享一个时钟信号来同步数据传输。取而代之的是,通过起始位 (Start Bit) 标记数据帧的开始,通过停止位 (Stop Bit) 标记数据帧的结束,以及预先约定好的波特率 (Baud Rate) 来协调数据的采样时间。
-
串行 (Serial):数据一位一位地按顺序在线路上传输,而不是并行多位同时传输。这使得只需要两根线。
异步详细介绍
发送方: 按照预设的波特率,精确地控制每个数据位、起始位和停止位的持续时间,并将其串行发送出去。
接收方: 同样按照预设的波特率,在检测到起始位后,精确地计算每个数据位的采样时间点,从而正确读取数据。
进行异步通信,通信的每一方都必须拥有自己的内部时钟,如果没有各自的内部时钟,发送方就无法知道何时发送下一个比特,接收方也无法知道何时去读取数据线上的电平。
通信双方的波特率必须相同,否则将导致数据错乱。
使用串口助手进行串口通信的时候,是满足这里的要求的,电脑上面有时钟,同时开发板内部的UART也有时钟。
串行,全双工详细介绍
标准的UART通信通常只需要两根信号线:
TX (Transmit):发送数据线,用于发送数据。
RX (Receive):接收数据线,用于接收数据。
连接方式是交叉连接:发送方的TX连接到接收方的RX,发送方的RX连接到接收方的TX。
由于发送和接收使用不同的物理线路,它们互不干扰,所以发送方可以在向接收方发送数据的同时,接收方也可以向发送方发送数据。UART协议是全双工通信协议
实现串口数据发送
CubeMX配置
配置好时钟源之后,选择USART1之后选择模式为异步模式就可以了,剩下的配置不需要动。
选完之后我们会发现这里多了两个选中的引脚, TX(发送),RX(接收)。
如果选择同步通信方式的话还会有一根时钟线用于同步操作。
在USART协议中我们其实是可以选择使用同步方式(synchronous)或者异步方式(Asynchronous)进行数据的发送和接收的 ,不过一般异步方式用的多,同步方式通信一般会选择其他类型协议如 SPI、I²C。
printf重定向代码编写
串口调试助手在没有显示屏的嵌入式系统中有着很好的应用,可以利用函数重定向功能,调用printf函数,将开发板中获取到的数据通过串口刷出到PC,为程序调试和串口通信提供了很大的便利。
我们不能直接调用printf将数据输出到串口,要想实现printf重定向功能,还需要进行重写一下fputc函数。
为什么 printf 默认不能输出到串口?
在 C 标准库中,
printf()
的本质是将格式化后的字符串输出到一个默认的标准输出设备,这个设备在 PC 上通常是终端(屏幕),但在 MCU(如 STM32)里:
没有默认的标准输出设备(没有屏幕、没有操作系统);
所以
printf()
其实调用的是底层函数如fputc()
、_write()
,但这些在裸机环境下是 空实现 或 报错实现。
fputc的函数原型照抄就行,这里的第二个参数用不到,不需要处理。
fputc函数的作用是输出单个字符,所以这里使用HAL_UART_Transmit实现fputc输出字符的时候,参数选择字节数为1就行。这样实现出来的输出字符串到串口助手的效果非常好,还很简便,不需要我们判断发送到串口的字符串长度逻辑(printf帮我们做了,我们只需要实现单个字符发送就可以!)
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);return ch;
}
HAL_UART_Transmit函数介绍
- UART_HandleTypeDef *huart (第一个参数):
这是一个指向 UART_HandleTypeDef 结构体的指针。这个结构体包含了特定 UART 外设的所有配置信息和状态。
- uint8_t *pData (第二个参数):
这是一个指向要发送数据的缓冲区的指针。uint8_t 表示数据是以字节(8位)的形式传输。函数会从这个地址开始读取数据并发送出去。
- uint16_t Size (第三个参数):
这是一个 uint16_t 类型的变量,表示要发送的数据的字节数(长度)。
- uint32_t Timeout (第四个参数):
- 这是一个 uint32_t 类型的变量,表示数据发送操作的超时时间,单位是毫秒(ms)。
- 如果在指定的时间内数据没有发送完成,函数将返回 HAL_TIMEOUT。
- 使用 HAL_MAX_DELAY 表示无限等待,直到数据发送完成(或发生错误),这在不希望有超时限制的情况下很常用。
由于使用了printf这个函数,所以我们需要在main.c文件中包含对应的头文件:
#include "stdio.h"
/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */uint16_t cnt=0;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */printf("this is a message from stm32 cnt : %d\n", cnt++);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
Keil中对文件勾选配置:Use MicroLIB
效果展示:
实现串口数据接收
轮询方式实现串口数据接收
接收单个字符
uint8_t receive_data;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */HAL_UART_Receive(&huart1, &receive_data, 1, HAL_MAX_DELAY);printf("receive data: %c\r\n", receive_data);/* USER CODE BEGIN 3 */}
接收不定长字符串(接收的字符串以\n结尾)
uint8_t receive_data;uint8_t receive_buf[256] = {0};uint8_t index = 0;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */HAL_UART_Receive(&huart1, &receive_data, 1, HAL_MAX_DELAY);if(receive_data != '\n'){receive_buf[index++] = receive_data;}else{receive_buf[index++] = '\0';printf("receive data: %s\r\n", receive_buf);index = 0;memset(receive_buf, 0, sizeof(receive_buf));}/* USER CODE BEGIN 3 */}
代码介绍
这段代码是典型的使用 HAL 库的轮询(阻塞)模式接收串口数据,并以换行符 \n 作为结束标志 的实现。
这段代码的主要功能是:
- 在无限循环 while(1) 中,阻塞式地等待 接收 UART1 的一个字节数据。
- 如果接收到的字节不是换行符 \n,则将其存储到 receive_buf 缓冲区中。
- 如果接收到换行符 \n,则认为一帧数据接收完毕:
- 在缓冲区末尾添加字符串结束符 \0。
- 通过 printf 打印接收到的字符串。
- 重置 index 为 0,准备接收下一帧数据。
- 清空 receive_buf 缓冲区。
代码优缺点:
优点
简单易懂: 对于初学者来说,这种轮询加阻塞的模式是最容易理解和实现的。逻辑直接,容易跟踪。
便于调试: 由于是阻塞式的,每次接收一个字节,单步调试时可以清晰地看到数据流。
处理明确的结束符: 使用
\n
作为帧结束符是文本协议中常见且有效的方式,使得数据包的边界清晰。缺点
CPU 效率极低(最主要的问题):
HAL_UART_Receive(&huart1, &receive_data, 1, HAL_MAX_DELAY); 这行代码会无限期阻塞,直到接收到一个字节。这意味着在没有数据到达时,CPU 会一直停在这里等待,无法执行其他任何任务。
这对于任何需要做其他事情(比如控制LED、读取传感器、处理按键等)的实时嵌入式系统来说都是不可接受的。CPU 的大部分时间都会被浪费在等待串口数据上。
注意看,我们在使用串口助手向开发板发送数据的时候,需要加上一个换行符,表示发送的这串字符串结束了。开发板内部的串口处理程序,顺序收到/n之后,根据判断逻辑就会输出缓冲区积攒的字符串。
中断方式实现串口数据接收
CubeMX配置
相比于上面采用非中断模式来实现串口数据接收,采用中断方式的话,需要将Uart1对应的中断打开。
UART中断方式接收数据函数是一次性的
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数作用:启动 UART1 的中断接收,期望接收 uint16_t Size 个字节
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 推荐每次只接收一个字节
虽然调用一次函数可以选择接收多个字节数据,但是我们通常选择调用一次UART中断接收数据函数只接收一个字节,会在中断回调里处理并再次启动接收函数。
为什么说UART 中断接收函数为何说是“一次性的”?
- 这句话的意思是,当你调用
HAL_UART_Receive_IT(&huart, pData, Size);
这个函数后,它只会启动一次中断接收过程,并等待接收指定数量(Size
)的字节。
- 一旦 UART 接收到这
Size
个字节的数据,这个函数所启动的当前接收任务就完成了。UART 接收中断会相应地被触发,执行你的回调函数HAL_UART_RxCpltCallback()。
- 但是,此时 UART 并不会自动开始接收下一组数据。 如果你想继续接收数据,你需要再次调用
HAL_UART_Receive_IT()
来“重新武装”UART 硬件,让它准备好接收下一批数据,通常我们会选择在回调函数HAL_UART_RxCpltCallback中再次启动接收函数。
HAL_UART_Receive_IT() 调用位置
HAL_UART_Receive_IT() 函数推荐在以下几个地方调用:
1. main() 函数的初始化部分
这是最常见和推荐的调用位置。 在你的 main.c 文件中,通常在所有硬件初始化完成(例如 MX_GPIO_Init()、MX_USARTx_UART_Init() 等)之后,while(1) 无限循环之前,调用一次 HAL_UART_Receive_IT() 来启动串口的首次接收。
int main(void)
{// ... 系统初始化 ...HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();// ... 其他外设初始化 .../* USER CODE BEGIN 2 */// 启动 UART1 的中断接收,期望接收 SOME_BUFFER_SIZE 个字节// 或者通常只接收一个字节,在中断回调里处理并再次启动HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 推荐每次只接收一个字节/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */
2. UART 接收完成回调函数 HAL_UART_RxCpltCallback() 中
// 这个函数是弱声明,你需要重写它
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1){// 假设这里处理了接收到的数据 rx_buffer// Process_Received_Data(rx_buffer);// 重新启动下一次中断接收HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 再次启动接收一个字节}
}
接收单个字符
让我们用一个小栗子来进行实现一下中断接收单个字符吧。
代码功能:
这里我们想要实现的是通过串口助手,每次发送一个字符1/2/3/4来控制LED1/2/3/4,比如发送一个1,LED1电平就翻转,再次输入1LED1电平就再次翻转。LED2/3/4同理。
这里为了展现出中断方式接收数据不阻塞主程序执行,我们在main函数中还持续用向串口发送字符串"hello world"
代码实现如下:
在main.c中定义成一个全局变量
/* USER CODE BEGIN PV */uint8_t receive_data;//存放UART1接收到的数据
/* USER CODE END PV */函数声明部分
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
int fputc(int ch, FILE *f);
/* USER CODE END PFP */main函数里面
/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN 2 */HAL_UART_Receive_IT(&huart1, &receive_data, 1);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */printf("hello world\r\n");HAL_Delay(1000);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}main函数中定义的函数的具体实现
/* USER CODE BEGIN 4 */
int fputc(int ch, FILE *f)
{HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);return ch;
}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){if(huart->Instance == USART1){if(receive_data == '1'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_10);}else if(receive_data == '2'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_2);}else if(receive_data == '3'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_1);}else if(receive_data == '4'){HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);}}HAL_UART_Receive_IT(&huart1, &receive_data, 1);
}
/* USER CODE END 4 */
代码编写时候需要注意的就是:
- HAL_UART_Receive_IT()需要在main函数初始化的时候调用,
- 还需要在 HAL_UART_RxCpltCallback中调用
接收不定长字符串
代码实现思路:
- 每次只接收一个字节: 在中断模式下,将 HAL_UART_Receive_IT() 的 Size 参数设置为 1。这样每接收到一个字节,就会触发一次接收完成中断。
- 使用缓冲区 (Buffer): 在中断服务程序中,将接收到的每一个字节存入缓冲区。
- 在主循环中判断数据完整性: 主循环会不断地检查缓冲区中是否有完整的字符串帧(通常通过查找特定的结束符,如 \n )。
- 重新启动接收: 在每次接收完成中断回调函数中,都需要再次调用 HAL_UART_Receive_IT() 来“重新武装”串口,准备接收下一个字节。
CubeMX配置
上面已经说过了,主要需要注意的是记得在NVIC 设置中,勾选开启对应 UART 全局中断的 Enabled。
代码实现
额外包含头文件
#include "stdio.h"
#include "string.h"main.c中定义的全局变量
/*
这里定义成全局变量的原因:
回调函数的实现以及main函数中都要用到这些变量
*/
/* USER CODE BEGIN PV */uint8_t receive_data;uint8_t receive_buf[256] = {0};uint8_t receive_buf_index = 0;
/* USER CODE END PV */函数声明
/* USER CODE BEGIN PFP */
int fputc(int ch, FILE* f);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
/* USER CODE END PFP */main函数中代码
HAL_UART_Receive_IT(&huart1, &receive_data, 1);/* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE */printf("hello world\r\n");HAL_Delay(1000);/* USER CODE BEGIN 3 */}代码实现部分
int fputc(int ch, FILE* f){HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY); return ch;
}void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){if(huart->Instance == USART1){if(receive_data != '\n'){receive_buf[receive_buf_index++] = receive_data;}else{receive_buf[receive_buf_index++] = '\0';printf("receive data: %s\r\n", receive_buf);receive_buf_index = 0;memset(receive_buf, 0, sizeof(receive_buf));}HAL_UART_Receive_IT(&huart1, &receive_data, 1);}
}