串口属于串行通信,用一根 TX 发送、一根 RX 接收。
异步通信的难点在于:接收方不知道数据何时到来,必须靠起始位来同步。
上图:UART 串口帧结构及不同波特率/校验方式下的时序对比(来源:STM32入门教程 PPT 第112-113页)
空闲: 高电平
起始位: 1 位低电平
数据位: 5~8 位 (通常 8 位)
校验位: 可选 (奇校验/偶校验)
停止位: 1/1.5/2 位高电平
上图:UART 发送数据帧格式(来源:STM32入门教程 PPT 第117页)
以最常用的 8N1(8 数据位、无校验、1 停止位)为例,一帧共 10 位:
起始位(0) + D0 + D1 + D2 + D3 + D4 + D5 + D6 + D7 + 停止位(1)
发送时低位在前(LSB first),先发 D0,最后发 D7。
波特率(Baud Rate)定义每秒传输的符号数。在 UART 中,一个符号 = 1 位,所以比特率 = 波特率。
常用波特率:9600、115200。
波特率决定了每一位的时长:T = 1 / 波特率。例如 9600 时,每位约 104 μs。
两个设备通信时,必须TX 接 RX,RX 接 TX:
设备 A (TX) ────────── (RX) 设备 B
设备 A (RX) ────────── (TX) 设备 B
如果 TX 接 TX(RX 接 RX),双方都在发送,无法收到对方的信号。
STM32 的 USART 支持同步模式(需要额外时钟线),但最常用的还是异步模式(即 UART 模式)。
上图:STM32 USART 模块框图(来源:STM32入门教程 PPT 第115页)
USART 模块内部包含:
以 USART1 为例,默认引脚为 PA9 (TX) 和 PA10 (RX):
| USART 模块 | TX 引脚 | RX 引脚 |
|---|---|---|
| USART1 | PA9 | PA10 |
| USART2 | PA2 | PA3 |
| USART3 | PB10 | PB11 |
TX 引脚配置为复用推挽输出(AF_PP),RX 引脚配置为上拉输入(IPU)或浮空输入。
Asynchronous发送一个字节的核心操作:
USART_SendData(USART1, Byte); // 写入数据寄存器
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待发送完成
TXE 标志位:Transmit data register Empty。当 TXE = 1 时,表示 TDR 已空,可以写入下一个数据。
注意:TXE 只表示数据已从 TDR 转移到移位寄存器,如果还需要等待所有数据实际发送完成(包括停止位发出),应查询 TC 标志位(Transmission Complete):
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
void USART_Init(void) {
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
// PA9: TX - 复用推挽输出 CNF-10, MODE-11
GPIOA->CRH |= GPIO_CRH_MODE9;
GPIOA->CRH |= GPIO_CRH_CNF9_1; GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
// PA10: RX - 浮空输入 CNF-01, MODE-00
GPIOA->CRH &= ~GPIO_CRH_MODE10;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1; GPIOA->CRH |= GPIO_CRH_CNF10_0;
USART1->BRR = 0x271; // 72MHz/62500=115200
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
USART1->CR1 &= ~(USART_CR1_M | USART_CR1_PCE); // 8N1
USART1->CR2 &= ~USART_CR2_STOP; // 1 stop bit
}
void USART_SendChar(uint8_t ch) {
while ((USART1->SR & USART_SR_TXE) == 0) {}
USART1->DR = ch;
}
uint8_t USART_ReceiveChar(void) {
while ((USART1->SR & USART_SR_RXNE) == 0) {
if (USART1->SR & USART_SR_IDLE) return 0;
}
return USART1->DR;
}
uint8_t buffer[256];
uint16_t size = 0;
uint8_t isOver = 0;
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
buffer[size++] = USART1->DR;
} else if (USART1->SR & USART_SR_IDLE) {
USART1->DR; // 读DR清除IDLE标志
isOver = 1;
}
}
int fputc(int ch, FILE *f) {
USART_SendChar((uint8_t)ch);
return ch;
}
标准库:
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
HAL库:
UART_HandleTypeDef huart1;
void Serial_Init(void)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
}
// 发送单字节
void Serial_SendByte(uint8_t Byte) {
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length) {
for (uint16_t i = 0; i < Length; i++)
Serial_SendByte(Array[i]);
}
// 发送字符串
void Serial_SendString(char *String) {
for (uint8_t i = 0; String[i] != '\0'; i++)
Serial_SendByte(String[i]);
}
HAL库 发送等效:
// 发送单字节(阻塞超时模式)
void Serial_SendByte(uint8_t Byte)
{
HAL_UART_Transmit(&huart1, &Byte, 1, HAL_MAX_DELAY);
}
// 发送数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
HAL_UART_Transmit(&huart1, Array, Length, HAL_MAX_DELAY);
}
// 发送字符串
void Serial_SendString(char *String)
{
HAL_UART_Transmit(&huart1, (uint8_t *)String, strlen(String), HAL_MAX_DELAY);
}
数字需要转换为 ASCII 码再发送。例如数字 123 要分别发送 '1'、'2'、'3' 三个字符。
// 10 的 n 次方(内部函数)
uint32_t Serial_Pow(uint32_t X, uint32_t Y) {
uint32_t Result = 1;
while (Y--) Result *= X;
return Result;
}
// 发送数字(按十进制,每位转 ASCII)
void Serial_SendNumber(uint32_t Number, uint8_t Length) {
for (uint8_t i = 0; i < Length; i++)
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
方法一:重定向 fputc(需勾选 Use MicroLIB)
int fputc(int ch, FILE *f) {
Serial_SendByte(ch);
return ch;
}
之后可直接使用 printf()。
方法二:封装自己的 Printf 函数
#include <stdio.h>
#include <stdarg.h>
void Serial_Printf(char *format, ...) {
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
这样可以在多处使用而不受单一 printf 限制。
main.c 综合示例:
int main(void) {
OLED_Init();
Serial_Init();
// 1. 发送单字节 0x41 (即字符 'A')
Serial_SendByte(0x41);
// 2. 发送数组
uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
Serial_SendArray(MyArray, 4);
// 3. 发送字符串
Serial_SendString("\r\nNum1=");
// 4. 发送数字
Serial_SendNumber(111, 3);
// 5. 方法一:printf 重定向
printf("\r\nNum2=%d", 222);
// 6. 方法二:sprintf + 串口发送
char String[100];
sprintf(String, "\r\nNum3=%d", 333);
Serial_SendString(String);
// 7. 方法三:自定义 Printf
Serial_Printf("\r\nNum4=%d", 444);
while (1);
}
UART 接收有两种方式:
推荐使用中断接收。
RXNE:Read data register Not Empty。当 RXNE = 1 时,表示 RDR 中已收到一个字节,需要及时读取,否则下一个字节到来时会覆盖。
在 Serial_Init 中增加:
// 1. 开启串口接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 2. 配置 NVIC 分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 3. 配置 NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
完整代码(包含 GPIO、USART 和中断配置):
标准库:
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
HAL库:
UART_HandleTypeDef huart1;
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
void Serial_Init(void)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
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;
HAL_UART_Init(&huart1);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
HAL_NVIC_SetPriority(USART1_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
uint8_t Serial_RxData; // 接收的数据
uint8_t Serial_RxFlag; // 接收标志位
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
// 获取标志位(读取后自动清零)
uint8_t Serial_GetRxFlag(void) {
if (Serial_RxFlag == 1) {
Serial_RxFlag = 0;
return 1;
}
return 0;
}
// 获取数据
uint8_t Serial_GetRxData(void) {
return Serial_RxData;
}
HAL库 中断接收等效:
uint8_t rx_buffer; // 接收缓冲区
// 启动中断接收(在 main 中调用一次即可)
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
// 中断回调函数(HAL 自动调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
Serial_RxData = rx_buffer;
Serial_RxFlag = 1;
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1); // 重新启动接收
}
}
int main(void) {
OLED_Init();
OLED_ShowString(1, 1, "RxData:");
Serial_Init();
while (1) {
if (Serial_GetRxFlag() == 1) {
uint8_t RxData = Serial_GetRxData();
Serial_SendByte(RxData); // 回传
OLED_ShowHexNum(1, 8, RxData, 2); // OLED 显示
}
}
}
利用串口接收命令,控制 LED 闪烁速度:
uint16_t BlinkInterval = 500; // 默认 500ms
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
uint8_t cmd = USART_ReceiveData(USART1);
// 解析命令:控制 LED 闪烁间隔
if (cmd >= '0' && cmd <= '9')
BlinkInterval = (cmd - '0') * 100 + 100; // 100ms ~ 1000ms
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
通过串口发送特定格式的命令(如 LED1_ON、LED1_OFF),在中断中用状态机解析,可实现更复杂的控制。
在大量数据收发场景下,使用 DMA 可以释放 CPU 资源。HAL 库提供了 DMA 版本的收发函数,配合 USART 的硬件 DMA 请求使用。
DMA Settings 标签页中添加:
Memory to PeripheralPeripheral to MemoryNormal(或 Circular 用于连续接收), Increment Address 勾选// 发送数组(DMA 方式,非阻塞)
HAL_UART_Transmit_DMA(&huart1, TxBuffer, Size);
// 查询发送是否完成
while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, RxBuffer, BufferSize);
// 停止 DMA 接收
HAL_UART_DMAStop(&huart1);
// 阻塞式接收一个字节(超时等待)
uint8_t Serial_ReceiveByte(void)
{
uint8_t data;
HAL_UART_Receive(&huart1, &data, 1, HAL_MAX_DELAY);
return data;
}
| 功能 | 标准库 | HAL 库 |
|---|---|---|
| 发送字节 | USART_SendData() |
HAL_UART_Transmit() |
| 接收字节 | USART_ReceiveData() |
HAL_UART_Receive() |
| 中断发送 | USART_ITConfig(USART_IT_TXE) |
HAL_UART_Transmit_IT() |
| 中断接收 | USART_ITConfig(USART_IT_RXNE) |
HAL_UART_Receive_IT() |
| DMA 发送 | — | HAL_UART_Transmit_DMA() |
| DMA 接收 | — | HAL_UART_Receive_DMA() |
| 查询 TXE | USART_GetFlagStatus(USART_FLAG_TXE) |
__HAL_UART_GET_FLAG(UART_FLAG_TXE) |
| 查询 RXNE | USART_GetFlagStatus(USART_FLAG_RXNE) |
__HAL_UART_GET_FLAG(UART_FLAG_RXNE) |
| 使能 USART | USART_Cmd(ENABLE) |
HAL_UART_Init() 自动使能 |
| 中断使能 | USART_ITConfig() |
__HAL_UART_ENABLE_IT() |
发送方和接收方的波特率必须一致,否则收到乱码。常见错误:一方是 9600,另一方是 115200。
双设备通信时 TX 必须接 RX,如果 TX 接 TX,双方都在发送、无法接收。可尝试用 USB 转 TTL 模块在电脑上测试。
使用 printf 重定向必须:
fputc 函数如果未勾选 MicroLIB,会进入半主机模式导致程序卡死。
中断接收时,中断服务函数应尽快读取 RDR,否则下一个字节会覆盖当前数据。如果数据处理耗时较长,可采用环形缓冲区(Ring Buffer)。
STM32 的串口是 3.3V TTL 电平。如果需要与 RS232(±12V)设备通信,需加 MAX3232 电平转换芯片。与 USB 通信则需 CH340 / CP2102 等 USB 转 TTL 芯片。
9-1 串口发送\Hardware\Serial.c — 标准外设库串口发送(字节/数组/字符串/数字/printf)9-1 串口发送\User\main.c — 发送示例主程序9-2 串口发送+接收\Hardware\Serial.c — 增加中断接收9-2 串口发送+接收\User\main.c — 接收回传示例26-[STM32 HAL库][中断]串口中断接收实验 — 中断进阶:可变间隔闪烁本笔记综合整理自三套 STM32 教程:
- [STM32 HAL库] 串口基础 / 发送实验 / 接收实验 / 中断实验
- 铁头山羊 UART 系列(3.1~3.7)
- 江协科技 USART 系列(9-1~9-6)