摘要:本文以 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),避免数据丢失。