STM32 零基础可移植教程 07:USART 串口打印,从 CubeMX 配置到 printf 输出
前面几篇我们已经做了 LED、蜂鸣器、按键轮询、按键消抖和外部中断。
到这里,板子已经能"看得见、听得到、按得动"。
但嵌入式调试里还有一个非常重要的能力:让板子把信息说出来。
比如你想知道:
-
程序有没有跑到某个位置;
-
按键事件到底有没有触发;
-
ADC 采样值是多少;
-
I2C 设备地址有没有应答;
-
某个变量为什么变成了奇怪的值。
这时候串口打印就很有用了。
这一篇只做一个明确目标:
bash
STM32 通过 USART 每秒打印一次 Hello 嵌入式小站
并且把 printf() 重定向到串口。后面再写 ADC、I2C、SPI、CAN 时,我们就可以直接打印调试信息。
本篇目标
最终现象:
bash
串口助手每 1 秒显示一行:
Hello 嵌入式小站, count = 1
Hello 嵌入式小站, count = 2
Hello 嵌入式小站, count = 3
...
本篇用到的外设:
bash
USART
GPIO Alternate Function
本篇跑通标准:
-
Keil 编译通过;
-
程序能下载到开发板;
-
串口助手能收到 STM32 打印的文本;
-
printf()能正常输出; -
能说清楚 TX、RX、GND 怎么接;
-
能说清楚波特率、数据位、停止位、校验位是什么意思。
本篇只讲串口发送和 printf() 打印,不讲串口接收、中断、DMA、不定长帧。那些后面单独讲。
准备工作
你需要准备:
|
项目
|
说明
|
| --- | --- |
|
STM32 开发板
|
任意 STM32 开发板
|
|
下载器
|
ST-LINK/V2 或板载 ST-LINK
|
|
USB 转 TTL 模块
|
如果开发板没有板载 USB 串口,需要外接
|
|
串口助手
|
任意常见串口工具都可以
|
|
原理图
|
确认 USART TX/RX 接到哪个引脚
|
如果你用的是 Nucleo、Discovery 或一些带 USB 串口芯片的开发板,可能板子已经把某个 USART 接到了板载 USB 串口。
如果你用的是 F103 最小系统板,通常需要外接 USB 转 TTL 模块。

硬件连接
串口最容易接错的是 TX 和 RX。
STM32 的 TX 是发送,USB 转 TTL 的 RX 是接收,所以要交叉连接:
|
STM32
|
USB 转 TTL
|
| --- | --- |
|
USART_TX
|
RXD
|
|
USART_RX
|
TXD
|
|
GND
|
GND
|
如果本篇只做 STM32 打印,理论上只接:
bash
STM32 TX -> USB 转 TTL RX
GND -> GND
也能看到输出。
但建议一开始就把 RX 也接好,后面讲串口接收时可以继续用。
注意电平:
bash
STM32 GPIO 通常是 3.3V TTL 电平
不要直接接 RS232 电平
普通 USB 转 TTL 模块一般支持 3.3V 或 5V,有些模块有跳帽或开关。给 STM32 接信号时,优先选择 3.3V 电平。

串口参数先看懂
我们先用最常见配置:
bash
115200, 8N1
它的意思是:
|
参数
|
值
|
说明
|
| --- | --- | --- |
|
Baud Rate
|
115200
|
每秒传输的符号数
|
|
Word Length
|
8 Bits
|
每个数据字节 8 位
|
|
Parity
|
None
|
不使用校验位
|
|
Stop Bits
|
1
|
1 个停止位
|
|
Flow Control
|
None
|
不使用硬件流控
|
串口两端参数必须一致。
STM32 配了 115200,串口助手也要选 115200。STM32 配 8N1,串口助手也要选 8N1。
如果两边不一致,常见现象就是乱码。
CubeMX 配置步骤
1. 复制上一篇工程
建议从上一篇 06_key_exti 或一个干净基础工程复制一份,改名为:
bash
07_usart_printf
这一篇不依赖按键,可以只保留 LED,也可以从基础工程开始。
2. 选择 USART 实例
常见 STM32F103 工程里,经常使用:
bash
USART1
默认引脚通常是:
bash
PA9 -> USART1_TX
PA10 -> USART1_RX
但不同芯片、不同开发板不一定一样。
在 CubeMX 左侧找到:
bash
Connectivity -> USART1
把 Mode 设置为:
bash
Asynchronous

3. 确认 TX/RX 引脚
CubeMX 会自动把对应引脚设置成 USART 复用功能。
比如:
bash
PA9 -> USART1_TX
PA10 -> USART1_RX
你要做的是确认这两个引脚和你的开发板接线一致。
如果开发板板载 USB 串口接的是 USART2,那就不要硬用 USART1,要改成 USART2。
原则还是那句话:
bash
原理图接到哪个 USART,就配置哪个 USART

4. 配置 USART 参数
进入 USART 参数配置页面,设置:
|
配置项
|
推荐值
|
| --- | --- |
|
Baud Rate
|
115200
|
|
Word Length
|
8 Bits
|
|
Parity
|
None
|
|
Stop Bits
|
1
|
|
Data Direction
|
Receive and Transmit
|
|
Hardware Flow Control
|
None
|
|
Over Sampling
|
16 Samples
|
本篇只做发送,但建议保留 Receive and Transmit,后面讲接收可以继续用。

5. 本篇暂时不打开 USART 中断
这一篇只做阻塞发送:
bash
HAL_UART_Transmit()
所以暂时不需要打开 USART NVIC 中断。
后面讲串口接收、中断接收、DMA 接收时,我们再单独打开。
6. 生成 Keil 工程
配置完成后点击:
bash
GENERATE CODE
然后打开 Keil 工程,先编译一次。

Keil 工程生成和编译
打开 Keil 后,先编译:
bash
Build / F7
确认输出里没有错误:
bash
0 Error(s)

然后看一下 CubeMX 是否生成了串口句柄。
在此之前我们先解释一下串口句柄的概念,你可以把串口想象成一扇门(比如 COM1 是房门,COM2 是窗户)。
句柄就是这扇门的把手,或者更准确地说,是操作系统发给你的一把"数字钥匙"。
你拿到这把"钥匙"(句柄),就能对门做事情:开门(初始化串口)、送东西出去(发送数据)、收东西进来(接收数据)、关门(关闭串口)。
如果你没有这把钥匙,操作系统就不允许你碰这扇门。
在很多 CubeMX 工程里,你会看到类似:
bash
UART_HandleTypeDef huart1;
如果你用的是 USART2,可能是:
bash
UART_HandleTypeDef huart2;
在单片机中,没有操作系统管理"句柄"这个概念,但是CubeMX生成的结构体指针,起到了完全相同的作用 他也是一个"钥匙",包含了串口的所有状态和配置信息。
后面的代码默认用 huart1。如果你的工程是 huart2,需要改一个宏。
这个 huart1 变量(准确的说是它的指针 &huart1)就扮演了"串口句柄"的角色。 后续所有串口操作都要带上它:
bash
发送:HAL_UART_Transmit(&huart1, data, len, timeout);
接收:HAL_UART_Receive(&huart1, buffer, len, timeout);
完整代码
这一篇新增两个文件:
bash
Core/Inc/app_uart.h
Core/Src/app_uart.c
它们负责两件事:
-
封装字符串发送函数;
-
把 Keil 下常用的
printf()输出重定向到 USART。
1. 新建 Core/Inc/app_uart.h
在 Core/Inc 目录下新建:
bash
app_uart.h
写入下面代码:
bash
#ifndef APP_UART_H
#define APP_UART_H
#include "main.h"
#include <stdint.h>
void App_UART_Init(void);
void App_UART_Print(const char *text);
void App_UART_PrintBytes(const uint8_t *data, uint16_t len);
#endif
2. 新建 Core/Src/app_uart.c
在 Core/Src 目录下新建:
bash
app_uart.c
写入下面代码:
bash
#include "app_uart.h"
#include <stdio.h>
#include <string.h>
/*
* Default USART handle is huart1.
* If your project uses USART2, define APP_UART_HANDLE as huart2.
*/
#ifndef APP_UART_HANDLE
#define APP_UART_HANDLE huart1
#endif
extern UART_HandleTypeDef APP_UART_HANDLE;
void App_UART_Init(void)
{
}
void App_UART_Print(const char *text)
{
if (text == NULL)
{
return;
}
HAL_UART_Transmit(&APP_UART_HANDLE,
(uint8_t *)text,
(uint16_t)strlen(text),
HAL_MAX_DELAY);
}
void App_UART_PrintBytes(const uint8_t *data, uint16_t len)
{
if ((data == NULL) || (len == 0u))
{
return;
}
HAL_UART_Transmit(&APP_UART_HANDLE,
(uint8_t *)data,
len,
HAL_MAX_DELAY);
}
int fputc(int ch, FILE *f)
{
uint8_t data = (uint8_t)ch;
HAL_UART_Transmit(&APP_UART_HANDLE, &data, 1u, HAL_MAX_DELAY);
return ch;
}
这里有一个可移植点:
bash
#define APP_UART_HANDLE huart1
如果你用的是 USART2,就在 app_uart.c 里改成:
bash
#define APP_UART_HANDLE huart2
如果你用的是 USART3,就改成:
bash
#define APP_UART_HANDLE huart3
fputc() 是关键。Keil 工程里 printf() 最终会一个字符一个字符输出,我们在 fputc() 里调用 HAL_UART_Transmit(),这样 printf() 的内容就会从 USART 发出去。
3. 把 app_uart.c 加入 Keil 工程
在 Keil 工程树里右键:
bash
Application/User/Core
选择:
bash
Add Existing Files to Group 'Application/User/Core'
然后添加:
bash
Core/Src/app_uart.c

如果忘了这一步,可能会报:
bash
undefined symbol App_UART_Init
undefined symbol App_UART_Print
或者 printf() 没有按预期走你的重定向。
main.c 调用方式
1. Includes 区域
找到:
bash
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
改成:
bash
/* USER CODE BEGIN Includes */
#include "app_uart.h"
#include <stdio.h>
/* USER CODE END Includes */
注意:printf() 需要包含:
bash
#include <stdio.h>
2. 初始化区域
确保 CubeMX 生成的串口初始化函数已经被调用。
如果用 USART1,通常是:
bash
MX_USART1_UART_Init();
如果用 USART2,可能是:
bash
MX_USART2_UART_Init();
然后在 USER CODE BEGIN 2 里添加:
bash
/* USER CODE BEGIN 2 */
App_UART_Init();
printf("USART printf test start\r\n");
/* USER CODE END 2 */
App_UART_Init() 现在是空函数,保留它是为了以后扩展,比如初始化环形缓冲区、日志等级、串口接收状态机。
3. while 循环区域
找到:
bash
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
改成:
bash
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
static uint32_t count = 0;
count++;
printf("Hello STM32, count = %lu\r\n", count);
HAL_Delay(1000);
/* USER CODE END 3 */
}
这里的 \r\n 是换行。
很多串口助手在 Windows 下更习惯:
bash
\r\n
如果只写 \n,有些工具可能只换行不回到行首,看起来排版有点怪。
Keil 里建议打开 MicroLIB
在 Keil 里使用 printf() 时,建议打开 MicroLIB。
进入:
bash
Options for Target -> Target
勾选:
bash
Use MicroLIB

如果你不勾 MicroLIB,有些工程也能工作,但新手阶段容易遇到半主机、库函数重定向、链接配置这些额外问题。
本系列先采用最容易跑通的方式。
串口助手设置
打开串口助手,选择 USB 转 TTL 对应的 COM 口。
参数设置为:
bash
Baud Rate: 115200
Data Bits: 8
Stop Bits: 1
Parity: None
Flow Control: None
如果你不知道是哪个 COM 口,可以打开 Windows 设备管理器,看"端口 COM 和 LPT"。


编译、下载和验证
代码加完后,先编译:
bash
Build / F7
没有错误后下载:
bash
Download
打开串口助手,正常情况下你应该能看到:
bash
USART printf test start
Hello STM32, count = 1
Hello STM32, count = 2
Hello STM32, count = 3
如果 Keil 下载后程序没有自动运行,但你按复位键后能看到串口输出,那说明程序本身没问题,还是之前说过的下载后自动复位运行问题。
移植到其他板子的修改点
这篇的移植点主要有 7 个。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
|
USART 实例
|
不同板子板载串口可能接 USART1/2/3
|
CubeMX Connectivity
|
|
TX/RX 引脚
|
不同芯片复用引脚不同
|
CubeMX Pinout 和原理图
|
|
串口句柄
| huart1
、huart2、huart3 不同
| APP_UART_HANDLE |
|
波特率
|
两端必须一致
|
CubeMX USART 参数和串口助手
|
|
时钟
|
波特率依赖串口时钟
|
CubeMX Clock Configuration
|
|
电平
|
STM32 是 TTL 电平,不是 RS232 电平
|
硬件连接
|
|
GND
|
串口通信必须共地
|
开发板和 USB 转 TTL
|
换板子的推荐顺序:
-
看原理图,确认板载 USB 串口接到哪个 USART;
-
如果外接 USB 转 TTL,确认你接的是哪个 TX/RX 引脚;
-
CubeMX 配置对应 USART 为 Asynchronous;
-
设置 115200、8N1;
-
根据实际句柄修改
APP_UART_HANDLE; -
串口助手选择相同参数;
-
编译下载,看是否能收到
Hello STM32。
常见问题排查
1. 串口助手完全没输出
优先检查:
|
优先检查
|
具体方法
|
| --- | --- |
|
程序是否运行
|
下载后按复位键,看是否开始输出
|
|
TX/RX 是否接反
|
STM32 TX 接 USB-TTL RX
|
|
GND 是否共地
|
STM32 GND 和 USB-TTL GND 必须相连
|
|
COM 口是否选对
|
设备管理器查看端口号
|
|
波特率是否一致
|
STM32 和串口助手都设为 115200
|
| app_uart.c
是否加入工程
|
Keil 工程树确认
|
2. 串口乱码
常见原因:
-
波特率不一致;
-
串口助手数据位/停止位/校验位不一致;
-
系统时钟配置不对,导致串口实际波特率偏差;
-
USB 转 TTL 电平不匹配;
-
GND 没接好或接触不良。
先把两边都设成:
bash
115200, 8N1
如果还是乱码,再回 CubeMX 看 Clock Configuration 有没有红色错误。
3. 编译报 huart1 未定义
说明你的工程里没有 huart1。
可能原因:
-
你配置的是 USART2,所以句柄是
huart2; -
你配置的是 USART3,所以句柄是
huart3; -
CubeMX 没有生成 USART 初始化代码;
-
app_uart.c里APP_UART_HANDLE没改。
解决方法:
打开 CubeMX 生成的串口初始化文件或 main.c,找到实际句柄,比如:
bash
UART_HandleTypeDef huart2;
然后把 app_uart.c 里的宏改成:
bash
#define APP_UART_HANDLE huart2
4. printf() 没输出,但 App_UART_Print() 能输出
说明串口发送本身是通的,问题在 printf() 重定向。
优先检查:
-
app_uart.c里是否有fputc(); -
app_uart.c是否加入 Keil 工程; -
main.c是否包含<stdio.h>; -
Keil 是否勾选
Use MicroLIB; -
工程里是否有另一个
fputc()或重定向函数冲突。
5. 输出不换行
建议使用:
bash
printf("Hello STM32\r\n");
不要只写:
bash
printf("Hello STM32\n");
有些串口助手对 \n 的显示不够友好, \r\n 更稳。
6. 中文输出乱码
新手阶段建议先用英文和数字输出。
中文涉及源码编码、串口助手编码显示、终端字体等问题,容易把简单问题复杂化。
先确认英文:
bash
Hello STM32
能稳定输出,再考虑中文。
本篇小结
这一篇我们完成了 STM32 串口打印的第一步。
你现在至少应该知道:
-
串口 TX/RX 要交叉连接;
-
STM32 和串口助手参数必须一致;
-
常用参数是
115200, 8N1; -
CubeMX 里 USART 要选择 Asynchronous;
-
HAL_UART_Transmit()可以阻塞发送数据; -
fputc()可以把 Keil 下的printf()重定向到串口; -
换 USART1/2/3 时要改
APP_UART_HANDLE; -
串口没输出时,先查运行、接线、COM 口、波特率和 GND。
下一篇我们继续沿着串口往下走:
STM32 串口接收一个字节:先把 RX 中断跑通。
有了接收能力以后,板子就不只是能"说话",还能听电脑发来的命令。