目录
1.简介
2.串口和UART
2.1串口的简介
2.2UART的简介
2.3UART通信协议
2.3.1波特率
2.3.2空闲位
2.3.3起始位
2.3.4数据位
2.3.5校验位
2.3.6停止位
3.STM32的UART
4.HAL库中常用的操作UART的函数
4.1UART初始化函数 -- HAL_UART_Init
4.2硬件初始化回调函数 -- HAL_UART_MspInit
4.3发送数据函数 -- HAL_UART_Transmit
4.4接收数据的函数 -- HAL_UART_Receive
4.5使能中断源 -- __HAL_UART_ENABLE_IT
4.6获取UART的状态标志 -- __HAL_UART_GET_FLAG
4.7清除指定的 UART 状态标志位 -- __HAL_UART_CLEAR_FLAG
5.发送接收数据实验 并 打开串口printf功能
5.1硬件连接
5.2代码编写
5.2.1配置UART
5.2.2中断函数
5.2.3重定义fputc -- 支持printf函数
6.整体代码 && 实验现象
6.1uart1.h
6.2uart1.c
6.3main.c
6.4实验现象
1.简介
这个文章会介绍串口通信,然后编写代码完成单片机通过串口发送到电脑,电脑发送数据到单片机的过程。
作者使用的开发板是正点原子的精英版,写文章用于记录学习和经验分享。
2.串口和UART
2.1串口的简介
串口(Serial Port) 是一种常见的数据通信接口,用于实现设备之间的串行通信(Serial Communication)。所谓“串行”,是指数据是按 一位一位的顺序进行发送或接收的,与并行通信(一次性传输多个比特)相对。
2.2UART的简介
UART(Universal Asynchronous Receiver/Transmitter),UART 全称是通用异步收发器,是一种硬件模块,用于实现异步串行通信。它是实现串口通信的核心逻辑电路。
UART也是一种广泛应用于嵌入式系统中的异步串行通信协议,通过定义数据帧格式和波特率实现设备之间的点对点数据传输。它仅需两根信号线(TXD发送、RXD接收),无需共享时钟,具有结构简单、使用方便、兼容性强等优点,常用于调试输出、传感器通信、模块交互等场景。
设备1的TX(发送端)接入设备2的RX(接收端),设备1的RX(接收端)接入设备2的(发送端),这样就实现了全双工通信。也可以单工通信,就是一发一收,接通一路TX、RX即可。
2.3UART通信协议
UART的一个完整的数据帧通常包括起始位、数据位、可选的校验位和停止位,通信双方必须保持波特率和数据格式一致才能正常工作。
2.3.1波特率
波特率(Baud Rate) 是串行通信中的一个关键参数,用于表示每秒传输的信号变化次数,单位是 bps(bits per second) 。在 UART 通信中,波特率决定了数据传输的速度。
在 UART 通信中,发送端和接收端必须设置相同的波特率,否则会导致数据接收不到或者接收内容与发送的内容不一致的问题。
2.3.2空闲位
空闲位(Idle State) 是指当串口未发送或接收任何数据时,数据线(通常是 TXD 或 RXD)处于高电平状态。数据帧以一个起始位(Start Bit,低电平)开始,空闲位作为帧与帧之间的间隔,确保接收方能正确识别下一帧数据。
2.3.3起始位
起始位(Start Bit) 是 UART 数据帧中的第一个信号位,用于通知接收端:一帧数据即将开始传输。电平状态固定为低电平(0);长度为 1 位。
2.3.4数据位
数据位(Data Bits) 是 UART 数据帧中承载实际信息的部分,表示一次通信中传输的有效数据内容。数据的长度通常为 5 到 8 位,8位用的最广泛,因为正好是1字节的大小。
发送的顺序是低位先发(LSB First)。例如字符 'A' 的 ASCII 码为:0x41(十六进制)二进制表示为:01000001 UART 发送顺序(LSB 先发):1 0 0 0 0 0 1 0
2.3.5校验位
校验位(Parity Bit)是 UART 数据帧中一个可选的错误检测位,用于在通信过程中简单判断数据是否出错。校验位的位置在数据位之后、停止位之前。可以选择无校验(None)、偶校验(Even)、奇校验(Odd)。
校验方式 | 要求总“1”个数为... | 示例(数据位:01000001 → 有两个“1”) |
---|---|---|
偶校验(Even) | 偶数个“1” | 校验位 = 0(使总数仍为偶数) |
奇校验(Odd) | 奇数个“1” | 校验位 = 1(使总数变为奇数) |
接收端收到后会重新计算并比对校验位,若不一致则判定为传输错误。即使数据吃错,也不会纠正错误,就仅仅简单的校验而已。
2.3.6停止位
停止位(Stop Bit) 是 UART 数据帧中的最后一个部分,用于标识一次数据传输的结束,同时为下一次通信提供时间间隔。电平状态固定为高电平(1);长度可选为 1 位、1.5 位 或 2 位。
3.STM32的UART
作者使用的是STM32F103ZET6为微控制器的开发板,该微控制器内置了3个通用同步/异步收发器(USART1、USART2和USART3),和2个通用异步收发器(UART4和UART5)。
UART 只支持异步串行通信,而USART 是 UART 的“升级版”,既支持异步也支持同步通信,功能更强大。在这里就把USART当作UART用就好。
附上框图,虽然我没咋看懂。
4.HAL库中常用的操作UART的函数
打开HAL库的stm32f1xx_hal_uart.c文件。
4.1UART初始化函数 -- HAL_UART_Init
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
这个函数是 STM32 HAL 库中用于 初始化 UART 外设 的核心函数。主要配置使用哪个UART、波特率、数据位、校验位、停止位、CTS/RTS硬件流控位和收发模式。
函数的参数 UART_HandleTypeDef *huart 是指向一个 UART 句柄结构体的指针,该结构体包含了 UART 的配置信息和运行状态。
函数的返回值(HAL_StatusTypeDef):
返回值 | 含义 |
---|---|
HAL_OK | 初始化成功 |
HAL_ERROR | 初始化失败(如参数错误、硬件故障) |
HAL_BUSY | UART 正在被使用 |
HAL_TIMEOUT | 操作超时 |
4.2硬件初始化回调函数 -- HAL_UART_MspInit
__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart);
这个函数是 HAL 库中 UART 的底层硬件初始化函数,必须由用户根据实际硬件平台实现,用于配置 GPIO、时钟、中断等。这是一个公共的函数,通过HAL_UART_Init函数进行调用。同时也是虚函数,意思是要我们自己写函数的内容。
函数的参数 UART_HandleTypeDef *huart 是指向一个 UART 句柄结构体的指针,该结构体包含了 UART 的配置信息和运行状态。
4.3发送数据函数 -- HAL_UART_Transmit
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
该函数用于 通过 UART 以阻塞方式发送一组数据(一个字节数组),直到全部发送完成或发生错误/超时。使用简单,适合 不使用中断或 DMA 的场景。
参数说明:
参数名 | 类型 | 说明 |
---|---|---|
huart | UART_HandleTypeDef* | UART 句柄指针,指向已配置好的 UART 实例(如 &huart1 ) |
pData | uint8_t* | 指向要发送数据的缓冲区(字节数组) |
Size | uint16_t | 要发送的数据长度(字节数) |
Timeout | uint32_t | 等待发送完成的最大时间(单位:ms),设为 HAL_MAX_DELAY 表示无限等待 |
4.4接收数据的函数 -- HAL_UART_Receive
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
该函数用于 通过 UART 以阻塞方式接收指定数量的字节数据。会一直等待直到:接收到指定数量的数据(Size 字节)或发生错误 / 超时。
参数说明:
huart | UART_HandleTypeDef* | UART 句柄指针(如 &huart1 ) |
pData | uint8_t* | 指向接收缓冲区的指针(用于存储接收到的数据) |
Size | uint16_t | 要接收的数据长度(字节数) |
Timeout | uint32_t | 接收等待最大时间(单位:ms),设为 HAL_MAX_DELAY 表示无限等待 |
4.5使能中断源 -- __HAL_UART_ENABLE_IT
__HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)
这是一个宏函数。该宏用于直接使能 UART 的某个中断源。
参数说明:
__HANDLE__:就是 UART的句柄指针 UART_HandleTypeDef* 。
__INTERRUPT__:中断源
宏定义 | 中断名称 | 触发条件 | 是否常用 |
---|---|---|---|
UART_IT_CTS | CTS 变化中断 | 当 CTS 引脚电平变化时触发(硬件流控相关) | ❌ 较少使用 |
UART_IT_LBD | LIN 断点检测中断 | 检测到 LIN 总线断点标志(用于 LIN 协议) | ❌ 特定场景 |
UART_IT_TXE | 发送缓冲区空中断 | 当发送寄存器为空,可以写入新数据时触发 | ✅ 常用 |
UART_IT_TC | 发送完成中断 | 整个数据帧传输完成后触发 | ✅ 常用 |
UART_IT_RXNE | 接收缓冲区非空中断 | 当收到一个字节数据时触发 | ✅ 最常用 |
UART_IT_IDLE | 空闲总线检测中断 | 当 UART 接收线上检测到空闲时触发(常用于接收不定长数据帧) | ✅ 高级应用 |
UART_IT_PE | 校验错误中断 | 接收到的数据发生奇偶校验错误时触发 | ⚠️ 错误检测 |
UART_IT_ERR | 错误中断(综合) | 包括帧错误、噪声错误、溢出错误等 | ⚠️ 错误处理 |
4.6获取UART的状态标志 -- __HAL_UART_GET_FLAG
__HAL_UART_GET_FLAG(__HANDLE__, __FLAG__)
用于读取 UART 状态标志位的宏,常用于轮询或中断中判断事件是否发生,是实现串口通信控制和错误检测的关键工具。
参数说明:
__HANDLE__:就是 UART的句柄指针 UART_HandleTypeDef* 。
__FLAG__:
宏定义 | 对应标志 | 描述 |
---|---|---|
UART_FLAG_CTS | CTS 标志位 | CTS 引脚状态变化(硬件流控) |
UART_FLAG_LBD | LIN 断点标志 | LIN 总线断点检测 |
UART_FLAG_TXE | 发送缓冲区空标志 | 表示可以写入新数据 |
UART_FLAG_TC | 发送完成标志 | 整帧数据发送完成 |
UART_FLAG_RXNE | 接收缓冲区非空标志 | 表示已接收到一个字节 |
UART_FLAG_IDLE | 空闲总线标志 | UART 总线空闲检测(常用于不定长接收) |
UART_FLAG_ORE | 溢出错误标志 | 接收缓冲区溢出 |
UART_FLAG_NE | 噪声错误标志 | 接收到噪声干扰 |
UART_FLAG_FE | 帧错误标志 | 数据格式错误 |
UART_FLAG_PE | 校验错误标志 | 奇偶校验失败 |
4.7清除指定的 UART 状态标志位 -- __HAL_UART_CLEAR_FLAG
__HAL_UART_CLEAR_FLAG(__HANDLE__, __FLAG__)
该宏用于 清除指定的 UART 状态标志位。在某些中断或轮询操作中,需要手动清除标志以避免重复触发或状态错误。
参数说明:
__HANDLE__:就是 UART的句柄指针 UART_HandleTypeDef* 。
__FLAG__:
宏定义 | 对应标志 | 是否需要手动清除 | 说明 |
---|---|---|---|
UART_FLAG_CTS | CTS 标志 | ✅ 是 | 清除 CTS 中断标志 |
UART_FLAG_LBD | LIN 断点标志 | ✅ 是 | 清除 LIN 检测标志 |
UART_FLAG_TC | 发送完成标志 | ✅ 是 | 清除发送完成标志 |
UART_FLAG_IDLE | 空闲总线标志 | ✅ 是 | 常用于接收不定长数据帧后需清除 |
UART_FLAG_RXNE | 接收缓冲区非空标志 | ❌ 否 | 读取 DR 寄存器自动清除 |
UART_FLAG_TXE | 发送缓冲区空标志 | ❌ 否 | 写入 DR 自动清除 |
UART_FLAG_ORE | 溢出错误标志 | ✅ 是 | 清除溢出标志 |
UART_FLAG_NE | 噪声错误标志 | ✅ 是 | 清除噪声错误标志 |
UART_FLAG_FE | 帧错误标志 | ✅ 是 | 清除帧错误标志 |
UART_FLAG_PE | 校验错误标志 | ✅ 是 | 清除奇偶校验错误标志 |
5.发送接收数据实验 并 打开串口printf功能
5.1硬件连接
一般地,STM32处理器的PA9和PA10就是对应的USART的TX和RX引脚。而且带有type-c USB线直接连接到电脑上进行调试的功能。如果读者使用的是最小系统板,就用USB转TTL再进行连线到核心板就好,功能是一样的。
5.2代码编写
5.2.1配置UART
使用上面介绍的HAL_UART_Init配置UART1。
UART_HandleTypeDef g_uart1_handle; //UART1句柄
/*** @brief 串口1初始化函数* @param baudrate: 波特率, 根据自己需要设置波特率值* @retval 无*/
void uart1_init(unsigned int baudrate)
{g_uart1_handle.Instance = USART1; //配置的是USART1g_uart1_handle.Init.BaudRate = baudrate; //波特率g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; //数据位为8位g_uart1_handle.Init.StopBits = UART_STOPBITS_1; //停止位为1位g_uart1_handle.Init.Parity = UART_PARITY_NONE; //无校验位g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //不使用CTS/RTS硬件流控位g_uart1_handle.Init.Mode = UART_MODE_TX_RX; //UART发送和接收HAL_UART_Init(&g_uart1_handle); //调用初始化函数进行初始化
}
再使用HAL_UART_MspInit进行底层硬件初始化,进行打开时钟,配置GPIO和打开中断。
对于发送数据引脚PA9,要求是推挽复用输出模式,上拉模式,速度中速高速都行。
对于 接收数据引脚PA10,要求是复用输入模式,上拉模式。
要使用HAL_NVIC_EnableIRQ函数打开USART1的中断,并设置中断优先级,然后再使用宏函数打开具体的UART中断。这里打开的是空闲中断和接收中断。
*** @brief UART底层初始化函数* @param huart: UART句柄类型指针* @note 此函数会被HAL_UART_Init()调用* 完成时钟使能,引脚配置,中断配置* @retval 无*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){/* 1.打开时钟 */__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/* 2.配置GPIO */GPIO_InitTypeDef gpio_handle = {0};gpio_handle.Pin = UART1_TX_PIN;gpio_handle.Mode = GPIO_MODE_AF_PP;gpio_handle.Pull = GPIO_PULLUP;gpio_handle.Speed = GPIO_SPEED_FREQ_MEDIUM;HAL_GPIO_Init(UART1_TX_PORT, &gpio_handle);gpio_handle.Pin = UART1_RX_PIN;gpio_handle.Mode = GPIO_MODE_AF_INPUT;gpio_handle.Pull = GPIO_PULLUP;HAL_GPIO_Init(UART1_RX_PORT, &gpio_handle);/* 3.打开中断 */HAL_NVIC_SetPriority(USART1_IRQn, 2, 2);HAL_NVIC_EnableIRQ(USART1_IRQn);__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); //打开接收中断__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); //打开空闲中断}
}
5.2.2中断函数
打开了接收中断之后,当接收到数据时,就跳到中断函数进行接收数据。当接收完成后,即进入空闲中断。在空闲中断中可进行其他操作。
这里的逻辑比较简单,当触发了接收缓冲区非空中断(接收到1字节数据),就进行接收数据的操作。数据一字节一字节的拷贝进g_uart1_rx_buf数组中,用于存储接收的数据。接收中断的标志会自动的清除。
当触发空闲总线检测中断(数据接收完成),就进行打印操作,并且清空用于存储接收数据的数组。然后要手动的清除空闲中断标志,否则程序会一直在这里。
/*** @brief 串口1中断服务函数* @note 在此使用接收中断,实现不定长数据收发* @param 无* @retval 无*/
void USART1_IRQHandler(void)
{unsigned char recv_data;if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_RXNE) != RESET){if(g_uart1_rx_len >= sizeof(g_uart1_rx_buf))g_uart1_rx_len = 0;HAL_UART_Receive(&g_uart1_handle, &recv_data, 1, 1000);g_uart1_rx_buf[g_uart1_rx_len++] = recv_data;}if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_IDLE) != RESET){printf("recv:%s\r\n", g_uart1_rx_buf);uart1_clear_rx_buf();__HAL_UART_CLEAR_IDLEFLAG(&g_uart1_handle);}
}
5.2.3重定义fputc -- 支持printf函数
fputc() 是 printf() 的底层输出函数通过重写该函数,可以将 printf() 输出重定向到串口,便于调试和日志输出。
/*** @brief 重定义fputc函数* @note printf函数最终会通过调用fputc输出字符串到串口*/
int fputc(int ch, FILE *f)
{while ((USART1->SR & 0X40) == 0); /* 等待上一个字符发送完成 */USART1->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */return ch;
}
USART1->SR 是 UART 的状态寄存器(Status Register);0x40 对应的是 TXE(Transmit Data Register Empty)标志位。当 TXE=1 表示 DR 寄存器为空,即可以写入新数据。将字符 ch 写入数据寄存器(Data Register),开始通过USART1向外发送。
6.整体代码 && 实验现象
6.1uart1.h
主要进行了UART1的引脚宏定义和函数声明。
#ifndef __UART1_H__
#define __UART1_H__#include "stdio.h"
#include "sys.h"/**********************宏定义**************************************/
/* 引脚定义 */
#define UART1_TX_PORT GPIOA
#define UART1_TX_PIN GPIO_PIN_9#define UART1_RX_PORT GPIOA
#define UART1_RX_PIN GPIO_PIN_10/* UART收发缓冲大小 */
#define UART1_RX_BUF_SIZE 128
#define UART1_TX_BUF_SIZE 64/* 错误代码 */
#define UART1_EOK 0 /* 没有错误 */
#define UART1_ERROR 1 /* 通用错误 */
#define UART1_ETIMEOUT 2 /* 超时错误 */
#define UART1_EINVAL 3 /* 参数错误 *//**********************函数声明**************************************/
void uart1_init(unsigned int baudrate); /* 串口初始化函数 */#endif
6.2uart1.c
操作USART1的函数。
#include "sys.h"
#include "uart1.h"
#include "string.h"/* 全局变量 */
unsigned char g_uart1_rx_buf[UART1_RX_BUF_SIZE]; //保存接收的数据
unsigned short g_uart1_rx_len = 0; //接收数据的长度
UART_HandleTypeDef g_uart1_handle; //UART1句柄/*** @brief 重定义fputc函数* @note printf函数最终会通过调用fputc输出字符串到串口*/
int fputc(int ch, FILE *f)
{while ((USART1->SR & 0X40) == 0); /* 等待上一个字符发送完成 */USART1->DR = (uint8_t)ch; /* 将要发送的字符 ch 写入到DR寄存器 */return ch;
}/*** @brief 串口1初始化函数* @param baudrate: 波特率, 根据自己需要设置波特率值* @retval 无*/
void uart1_init(unsigned int baudrate)
{g_uart1_handle.Instance = USART1; //配置的是USART1g_uart1_handle.Init.BaudRate = baudrate; //波特率g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B; //数据位为8位g_uart1_handle.Init.StopBits = UART_STOPBITS_1; //停止位为1位g_uart1_handle.Init.Parity = UART_PARITY_NONE; //无校验位g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; //不使用CTS/RTS硬件流控位g_uart1_handle.Init.Mode = UART_MODE_TX_RX; //UART发送和接收HAL_UART_Init(&g_uart1_handle); //调用初始化函数进行初始化
}/*** @brief UART底层初始化函数* @param huart: UART句柄类型指针* @note 此函数会被HAL_UART_Init()调用* 完成时钟使能,引脚配置,中断配置* @retval 无*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){/* 1.打开时钟 */__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/* 2.配置GPIO */GPIO_InitTypeDef gpio_handle = {0};gpio_handle.Pin = UART1_TX_PIN;gpio_handle.Mode = GPIO_MODE_AF_PP;gpio_handle.Pull = GPIO_PULLUP;gpio_handle.Speed = GPIO_SPEED_FREQ_MEDIUM;HAL_GPIO_Init(UART1_TX_PORT, &gpio_handle);gpio_handle.Pin = UART1_RX_PIN;gpio_handle.Mode = GPIO_MODE_AF_INPUT;gpio_handle.Pull = GPIO_PULLUP;HAL_GPIO_Init(UART1_RX_PORT, &gpio_handle);/* 3.打开中断 */HAL_NVIC_SetPriority(USART1_IRQn, 2, 2);HAL_NVIC_EnableIRQ(USART1_IRQn);__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); //打开接收中断__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE); //打开空闲中断}
}/*** @brief UART1接收缓冲区清除* @param 无* @retval 无*/
void uart1_clear_rx_buf(void)
{memset(g_uart1_rx_buf, 0, sizeof(g_uart1_rx_buf));g_uart1_rx_len = 0;
}/*** @brief 串口1中断服务函数* @note 在此使用接收中断,实现不定长数据收发* @param 无* @retval 无*/
void USART1_IRQHandler(void)
{unsigned char recv_data;if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_RXNE) != RESET){if(g_uart1_rx_len >= sizeof(g_uart1_rx_buf))g_uart1_rx_len = 0;HAL_UART_Receive(&g_uart1_handle, &recv_data, 1, 1000);g_uart1_rx_buf[g_uart1_rx_len++] = recv_data;}if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_IDLE) != RESET){printf("recv:%s\r\n", g_uart1_rx_buf);uart1_clear_rx_buf();__HAL_UART_CLEAR_IDLEFLAG(&g_uart1_handle);}
}
6.3main.c
在主函数中调用uart1_init函数,传递的参数115200是波特率。
#include "main.h"int main(void)
{HAL_Init(); /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */uart1_init(115200);while(1){delay_ms(10);}
}
6.4实验现象
首先在KEIL5中魔术棒中勾选 Use MicroLIB选项。
写好程序之后下载程序到开发板,然后打开电脑的串口调试助手,并且按照上面配置的波特率、停止位、数据位、校验位打开串口。
之后发送数据,对应的接收区会有 recv:[数据]返回。就正常了。