摘要:本文以 STM32F103 系列和 USART1 为例,系统讲解串口通信的程序编写方法。内容涵盖寄存器方式与 HAL 库方式两种实现路径,包括单字符与指定长度数据的收发、printf 与 scanf 的串口重定向,以及通过串口指令控制 LED 的完整实验。文章同时对比了查询方式与中断方式的适用场景,并总结了收发配置一致、TX-RX 交叉接线等关键实践要点。
程序编写
本章以 STM32F103 系列、USART1 为例,介绍串口通信的程序编写方法。默认配置为:波特率 115200、8 位数据位、无校验、1 位停止位(8N1)。USART1 的 TX 引脚为 PA9,RX 引脚为 PA10。





注意:安装串口调试助手和 CH340
1、寄存器方式单字符收发
-
寄存器方式单字节发送

-
放到 while 上面验证没有问题

-
多数据发送验证
按照之前的程序会乱,所以做了优化
void UartSendBit(uint8_t ch)
{
while(!(USART1->SR & (1<<7))){}
USART1->DR=ch;
}

(1)GPIO 和 USART 初始化
void USART1_Init(void)
{
// 1. 使能 GPIOA 和 USART1 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能 GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // 使能 USART1 时钟
// 2. 配置 PA9(TX) 为复用推挽输出,50MHz
GPIOA->CRH &= ~(0xF << 4); // 清除 PA9 原有配置
GPIOA->CRH |= (0xB << 4); // PA9: 复用推挽输出, 50MHz
// 3. 配置 PA10(RX) 为浮空输入
GPIOA->CRH &= ~(0xF << 8); // 清除 PA10 原有配置
GPIOA->CRH |= (0x4 << 8); // PA10: 浮空输入
// 4. 设置波特率 115200 (PCLK2=72MHz)
// USARTDIV = 72000000 / (16 * 115200) = 39.0625
// DIV_Mantissa = 39, DIV_Fraction = 1
USART1->BRR = 0x271;
// 5. 配置 CR1: 8位数据, 无校验, 使能发送和接收
USART1->CR1 = 0; // 清零
USART1->CR1 |= USART_CR1_TE; // 使能发送
USART1->CR1 |= USART_CR1_RE; // 使能接收
USART1->CR1 |= USART_CR1_UE; // 使能 USART1
}
(2)指定长度数据接收
除了单字符接收,查询方式下还可以实现接收指定长度的数据块,或接收以特定结束符(如换行符 '\n')结尾的字符串。
/**
* @brief 接收指定长度的数据
* @param buf: 接收缓冲区指针
* @param len: 要接收的字节数
* @retval 实际接收到的字节数
*/
uint16_t USART1_ReceiveData(uint8_t *buf, uint16_t len)
{
uint16_t i = 0;
while (i < len)
{
// 等待接收数据寄存器非空
while (!(USART1->SR & USART_SR_RXNE));
buf[i++] = (uint8_t)(USART1->DR);
}
return i;
}
/**
* @brief 接收字符串,直到遇到结束符或缓冲区满
* @param buf: 接收缓冲区指针
* @param max_len: 缓冲区最大长度
* @param terminator: 结束符(如 '\n' 或 '\r')
* @retval 实际接收到的字符串长度(不含结束符)
*/
uint16_t USART1_ReceiveString(char *buf, uint16_t max_len, char terminator)
{
uint16_t i = 0;
uint8_t ch;
if (max_len == 0) return 0;
while (i < max_len - 1) // 预留1个字节给字符串结束符 '\0'
{
// 等待接收数据寄存器非空
while (!(USART1->SR & USART_SR_RXNE));
ch = (uint8_t)(USART1->DR);
if (ch == (uint8_t)terminator)
{
break; // 遇到结束符,停止接收
}
buf[i++] = (char)ch;
}
buf[i] = '\0'; // 添加字符串结束符
return i;
}
使用示例:
char rxBuf[50];
// 接收直到换行符 '\n',最多接收49个字符
uint16_t len = USART1_ReceiveString(rxBuf, sizeof(rxBuf), '\n');
// 将接收到的字符串回发
USART1_SendString("Received: ");
USART1_SendString(rxBuf);
USART1_SendString("\r\n");
查询方式接收的特点是阻塞等待:在数据未到达时,CPU 会停在 while 循环里等待,无法执行其他任务。因此查询方式适合简单的、对实时性要求不高的场景。如果需要在接收的同时处理其他任务,就需要使用中断方式(后续阶段讲解)。
2、HAL 方式串口收发
(1)HAL 库初始化
使用 STM32CubeMX 配置 USART1 后,生成的初始化代码如下(或手动编写):
UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200; // 波特率
huart1.Init.WordLength = UART_WORDLENGTH_8B; // 8位数据
huart1.Init.StopBits = UART_STOPBITS_1; // 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; // 16倍过采样
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
(2)HAL 库阻塞式发送
// 发送字符串
uint8_t txData[] = "Hello, UART!\r\n";
HAL_UART_Transmit(&huart1, txData, sizeof(txData)-1, HAL_MAX_DELAY);
// 发送单字节
uint8_t byte = 'A';
HAL_UART_Transmit(&huart1, &byte, 1, 100); // 超时100ms
(3)HAL 库阻塞式接收
uint8_t rxData[20];
// 阻塞接收指定长度数据,超时 5000ms
HAL_StatusTypeDef status = HAL_UART_Receive(&huart1, rxData, 10, 5000);
if (status == HAL_OK)
{
// 接收成功,处理 rxData
}
(4)串口重定向(重新定义数据传输的方向)
printf 为格式化输出函数
默认输出到终端
需求:使用 printf 输出数据到串口
__weak是什么
__weak是 弱定义(weak definition) 修饰符,用于标记一个函数或变量为弱符号。在 STM32 HAL 库中被大量使用。
核心作用:允许被用户重写
-
被
__weak修饰的函数提供一个默认实现 -
用户可以在自己的代码中定义一个同名函数(不带
__weak,即强符号) -
链接时,强符号会自动覆盖弱符号,从而调用用户自己的实现
串口重定向(注意在魔术棒工具里勾选微型 C 库)

#include <stdio.h>
int fputc(int ch,FILE *f)
{
while(!(USART1->SR & (1<<7))){}
USART1->DR=ch;
return ch;
}
#include "gpio.h"和#include <stdio.h>区别
C 语言 #include 预处理指令的两种头文件包含方式,核心区别在于查找路径的顺序不同。
|
写法 |
查找顺序 |
典型用途 |
|
|
先在当前源文件所在目录查找,找不到再去系统标准库目录查找 |
项目自己的头文件 |
|
|
直接在系统标准库目录(编译器指定的 include 路径)中查找 |
C 标准库 / 第三方库头文件 |
结合 STM32 项目理解
-
#include "gpio.h":gpio.h是 STM32CubeMX 生成的、属于你自己项目的头文件(里面是 GPIO 初始化函数声明,如MX_GPIO_Init()),和你的.c源文件在同一个工程目录下,所以用双引号。 -
#include <stdio.h>:stdio.h是 C 标准库提供的头文件(里面声明了printf、scanf等标准输入输出函数),由编译器/工具链自带,放在系统 include 目录中,不在你的项目文件夹里,所以用尖括号。
补充说明
-
双引号是"兜底"的:用
"gpio.h"时,如果当前目录找不到,编译器最终也会去系统目录找,所以理论上标准库头文件也能用双引号包含——但不推荐,因为这会降低查找效率,也不符合编码规范。 -
尖括号不会找当前目录:用
<stdio.h>时,编译器不会去当前源文件目录查找,直接走系统路径。 -
编码规范约定俗成:
-
自己写的 / 项目内的头文件 → 双引号
"xxx.h" -
标准库 / 第三方库头文件 → 尖括号
<xxx.h>
-
简单记:双引号找"自家的",尖括号找"公家的"。
scanf 重定向
int fgetc(FILE *f)
{
uint8_t ch;
while(!(USART1->SR & (1<<5))){}
ch=USART1->DR;
return ch;
}
重定向之后注意在串口输入数据后添加‘空格’

(5)常用 HAL 库函数一览
|
函数 |
功能 |
|---|---|
|
HAL_UART_Init() |
初始化 UART 外设 |
|
HAL_UART_Transmit() |
阻塞式发送数据 |
|
HAL_UART_Receive() |
阻塞式接收数据 |
|
HAL_UART_Transmit_IT() |
中断方式发送数据 |
|
HAL_UART_Receive_IT() |
中断方式接收数据 |
|
HAL_UART_Transmit_DMA() |
DMA 方式发送数据 |
|
HAL_UART_Receive_DMA() |
DMA 方式接收数据 |
|
HAL_UART_RxCpltCallback() |
接收完成回调函数 |
|
HAL_UART_TxCpltCallback() |
发送完成回调函数 |
3、串口重定向
串口重定向是指将 C 标准库的 printf、scanf 等函数的输入输出重定向到串口,这样就可以像在 PC 上一样使用 printf 打印调试信息。
(1)printf 重定向(HAL 库方式)
#include <stdio.h>
// 重写 fputc 函数,将 printf 输出重定向到串口
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
// 使用示例
printf("System Start...\r\n");
printf("ADC Value = %d\r\n", adcValue);
printf("Temperature = %.2f C\r\n", temp);
(2)printf 重定向(寄存器方式)
#include <stdio.h>
int fputc(int ch, FILE *f)
{
while (!(USART1->SR & USART_SR_TXE)); // 等待发送寄存器空
USART1->DR = (uint8_t)ch; // 发送字符
return ch;
}
(3)scanf 重定向(可选)
int fgetc(FILE *f)
{
while (!(USART1->SR & USART_SR_RXNE)); // 等待接收数据
return (int)USART1->DR; // 读取并返回
}
注意事项:
1. 使用 printf 前需要在工程设置中勾选 "Use MicroLIB"(Keil)或启用 newlib-nano(GCC),否则程序会因堆栈不足而跑飞。
2. printf 中的浮点数输出(%f)在某些编译环境下需要额外配置,否则可能不显示。
3. 建议在输出字符串末尾加上 "\r\n",这样串口助手显示时会换行且光标回到行首。
4、串口控制实验
本实验通过串口发送指令控制开发板上的 LED,并在串口返回状态信息。
(1)实验功能
-
发送字符
'1':点亮 LED -
发送字符
'0':熄灭 LED -
发送字符
's':查询 LED 当前状态 -
其他字符:提示"未知指令"
(2)寄存器方式实现
#include "stm32f10x.h"
uint8_t ledState = 0; // LED 状态: 0=灭, 1=亮
void LED_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // 使能 GPIOC 时钟
GPIOC->CRH &= ~(0xF << 20); // 清除 PC13 配置
GPIOC->CRH |= (0x3 << 20); // PC13: 推挽输出 50MHz
GPIOC->BSRR = GPIO_BSRR_BS13; // LED 默认熄灭(高电平)
}
void LED_On(void) { GPIOC->BRR = GPIO_BRR_BR13; ledState = 1; }
void LED_Off(void) { GPIOC->BSRR = GPIO_BSRR_BS13; ledState = 0; }
int main(void)
{
uint8_t cmd;
LED_Init();
USART1_Init(); // 使用前面定义的初始化函数
USART1_SendString("=== UART Control LED ===\r\n");
USART1_SendString("Send '1' to turn ON, '0' to turn OFF, 's' for status\r\n");
while (1)
{
cmd = USART1_ReceiveByte(); // 阻塞接收一个字符
switch (cmd)
{
case '1':
LED_On();
USART1_SendString("[OK] LED ON\r\n");
break;
case '0':
LED_Off();
USART1_SendString("[OK] LED OFF\r\n");
break;
case 's':
if (ledState)
USART1_SendString("[INFO] LED is ON\r\n");
else
USART1_SendString("[INFO] LED is OFF\r\n");
break;
default:
USART1_SendString("[ERR] Unknown command\r\n");
break;
}
}
}
(3)HAL 库中断方式实现
#include "main.h"
#include <stdio.h>
UART_HandleTypeDef huart1;
uint8_t rxByte; // 接收单字节缓冲
uint8_t ledState = 0; // LED 状态
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)
{
switch (rxByte)
{
case '1':
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
ledState = 1;
printf("[OK] LED ON\r\n");
break;
case '0':
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
ledState = 0;
printf("[OK] LED OFF\r\n");
break;
case 's':
printf("[INFO] LED is %s\r\n", ledState ? "ON" : "OFF");
break;
default:
printf("[ERR] Unknown command: %c\r\n", rxByte);
break;
}
// 重新开启接收中断
HAL_UART_Receive_IT(&huart1, &rxByte, 1);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
printf("=== UART Control LED (HAL IT) ===\r\n");
printf("Send '1'=ON, '0'=OFF, 's'=status\r\n");
// 开启接收中断
HAL_UART_Receive_IT(&huart1, &rxByte, 1);
while (1)
{
// 主循环可做其他任务,串口接收由中断处理
}
}
(4)实验步骤
-
将开发板的 USART1(PA9-TX、PA10-RX)通过 USB 转串口模块连接到电脑。
-
打开串口助手(如 XCOM、SecureCRT、Putty),设置波特率 115200、8 位数据位、无校验、1 位停止位。
-
烧录程序并复位开发板,串口助手应显示欢迎信息。
-
在串口助手中发送字符
'1',观察 LED 点亮并收到 "[OK] LED ON"。 -
发送字符
'0',观察 LED 熄灭并收到 "[OK] LED OFF"。 -
发送字符
's',查看当前 LED 状态。
实验要点总结:
1. 串口通信的核心是收发双方配置一致(波特率、数据位、校验位、停止位)。
2. 接线遵循TX-RX 交叉、GND 共地原则。
3. 查询方式简单但占用 CPU;中断方式效率高,适合实际项目。
4. printf 重定向是嵌入式调试的重要手段,务必掌握。
5. 接收数据时注意处理溢出错误(ORE),避免数据丢失。
1320



被折叠的 条评论
为什么被折叠?



