串口调试 --- printf 重定向与 USART 通信
配套硬件 :DshanMCU-F407(STM32F407ZGT6)+ USB-TTL(HW-597)模块
学习目标 :学会用 printf 打印变量,这是嵌入式开发最重要的调试手段
核心思想:不用 LED 闪烁猜问题,直接看数据
目录
- 为什么要学串口调试?
- [什么是 USART/UART?](#什么是 USART/UART?)
- [引脚选择------为什么是 PB10 和 PC11?](#引脚选择——为什么是 PB10 和 PC11?)
- [USART 配置参数详解(115200 8N1)](#USART 配置参数详解(115200 8N1))
- 硬件接线
- [CubeMX 配置](#CubeMX 配置)
- 代码解读
- [fputc 重定向详解(核心难点)](#fputc 重定向详解(核心难点))
- [用 printf 调试------这才是重点!](#用 printf 调试——这才是重点!)
- 串口助手的设置
- 常见问题
- 扩展思考
1. 为什么要学串口调试?


1.1 没有串口之前你是怎么 debug 的?
方式 1:LED 闪烁
- 亮灭亮灭 → 程序活着
- 一直亮 → 卡死了
- 一闪而过 → 看不清
方式 2:猜
- 你觉得是这里错了,改一改,烧进去试试
- 不行再猜
- 效率极低
1.2 有了串口 printf 之后
c
printf("距离 = %d cm\r\n", distance); // 直接看距离值
printf("中断触发了,count = %d\r\n", count); // 看中断次数
printf("进入函数 A:param = %d\r\n", param); // 看函数是否执行了
你"看见"了变量内部的值,不用猜。
1.3 06-4 课程为什么讲 OLED 调试
06-4 的题眼是"调试手段 "------LED 能给你的信息只有 1 位(亮/灭),OLED/printf 能给你成千上万字符的信息。你学会的是一种"把变量值说出来"的方法,不管用 OLED 还是串口,本质一样。
2. 什么是 USART/UART?
2.1 名字拆解
UART = Universal Asynchronous Receiver / Transmitter
(通用异步收发器)
USART = Universal Synchronous / Asynchronous Receiver / Transmitter
(通用同步/异步收发器)
USART 比 UART 多一个同步功能(有时钟线),但大多数时候我们把它当 UART 用------异步,两根线(TX/RX)。
2.2 UART 通信物理层
F407(USART3) USB-TTL 模块 电脑
PB10 (TX) ─────────── RXD ───→ 串口芯片 → USB → 串口助手
PC11 (RX) ──────────── TXD ←──串口芯片 ← USB ← 串口助手
GND ────────────── GND 共地,统一参考电平
TX 接 RX、RX 接 TX(交叉连接),GND 一定要共地。
2.3 UART 一帧数据的结构
空闲(高电平)
│
▼
┌─────────┬───────────────┬───┬────────┐
│ 起始位 │ 8 位数据 │校 │ 停止位 │
│ (低) │ LSB→MSB │验 │ (高) │
└─────────┴───────────────┴───┴────────┘
1 bit 8 bit 无 1 bit
从前面看:
BIT0 BIT1 BIT2 ... BIT7
┌──┐ ┌┐ ┌┐ ┌┐
TX ────高─────┘ └──┘└──┘└──────┘└────── 高 ─────
起始 停止
关键:没有时钟线,靠收发双方约定好"每秒传多少位"(波特率)。
3. 引脚选择------为什么是 PB10 和 PC11?
3.1 STM32 的 Alternate Function 表
F407 的 USART 不止一组,外设模块可以通过不同的 GPIO 引脚连接到芯片内部。具体哪个引脚可以当哪个外设用,看芯片手册的 Alternate Function 表:
USART3_TX 有 3 个可选引脚:
PB10 ─── AF7(Alternate Function 7)
PC10 ─── AF7
PD8 ─── AF7
USART3_RX 有 3 个可选引脚:
PB11 ─── AF7
PC11 ─── AF7
PD9 ─── AF7
3.2 为什么最终选了 PB10 和 PC11?
查 P3 排针引出的空闲引脚:
| 候选 | P3 排针上有没有? | 结论 |
|---|---|---|
| PB10 | 有 | ✅ 做 TX |
| PB11 | N/A | ❌ 排针上没有 |
| PC10 | N/A | ❌ 排针上没有 |
| PC11 | 有 | ✅ 做 RX |
| PD8/PD9 | N/A | ❌ 排针上没有 |
不是"随便选的",是从 Alternate Function 表找到可用引脚 → 再和排针对比 → 选出都能用的组合。这是硬件工程师的基本功,也是你在 CubeMX 配引脚时经常要做的事。
⭐⭐ SHOULD KNOW:CubeMX 可以直接帮你看------当你选择 USART3 时,它会自动高亮所有可用的 TX/RX 引脚(绿色标出可选,橙色标出已选)。
4. USART 配置参数详解(115200 8N1)
4.1 Baud Rate(波特率)
波特率 = 每秒钟传输的"符号数",单位 bps(bit per second)
115200 bps → 每秒传 115200 位
115200 是怎么来的?
115200 = 115.2 Kbps ≈ 11.52 KB/s(有效数据)
↓
假设你传 1000 字节的数据:
用时 ≈ 1000 / 11520 ≈ 0.087 秒
为什么用 115200 而不是其他值?
| 波特率 | 优点 | 缺点 |
|---|---|---|
| 9600 | 兼容老设备 | 太慢,传 1KB 要 1 秒 |
| 115200 | 速度适中,大部分设备支持 | --- |
| 921600 | 极快 | 线长了容易乱码 |
115200 是嵌入式开发最常用的波特率,能兼顾速度和稳定性。
⚠️ 重要:收发双方的波特率必须一致。你设 115200,电脑串口助手也要设 115200。否则收到的数据是乱码。
4.2 Word Length(数据位)
8 bit → 每帧传输 8 位,刚好 1 个字节
这是最常用的设置,因为你要传的数据都是以字节为单位的
4.3 Parity(校验位)
None(无校验):
不添加校验位,10 位 = 起始 1 + 数据 8 + 停止 1
Even(偶校验):
校验位自动调整,使数据中 1 的个数为偶数
11 位 = 起始 1 + 数据 8 + 校验 1 + 停止 1
Odd(奇校验):
同上,但使 1 的个数为奇数
为什么 None? 如果你只是调试用,不需要校验。校验位多一位,有效传输率降低。串口线短(<2 米),出错概率极低。
4.4 Stop Bits(停止位)
1 bit → 停止位 1 位
停止位告诉接收方"这一帧结束了"
8N1 的完整含义:
8 = 8 data bits(数据位 8 位)
N = No Parity(无校验)
1 = 1 stop bit(停止位 1 位)
总共:1(起始)+ 8(数据)+ 0(校验)+ 1(停止)= 10 位每字符
4.5 面试会问的问题
问:波特率 115200,8N1,每秒实际传多少字节?
答 :每字节需要 10 位(1 起始 + 8 数据 + 1 停止)。每秒传 115200 / 10 = 11520 字节。
5. 硬件接线
5.1 接线表
| USB-TTL(HW-597) | F407 | 说明 |
|---|---|---|
| TXD | PC11 (USART3_RX) | 发→收,交叉 |
| RXD | PB10 (USART3_TX) | 收→发,交叉 |
| GND | GND | 共地,必须接 |
| 5V 或 3V3 | 不接 | USB-TTL 由 USB 供电,F407 由自己的电源供电 |
注意:TX 接 RX,RX 接 TX,不要接反。
5.2 USB-TTL 电脑端
USB-TTL 插入电脑 → 查看设备管理器(Windows)-> 端口(COM 和 LPT)→ 看出现哪个 COM 口(如 COM3、COM4)。
这个 COM 口号后面打开串口助手时要选。
6. CubeMX 配置
6.1 新建工程
- 打开 STM32CubeMX → New Project
- 搜索
STM32F407ZGTx→ 双击选中
6.2 引脚配置
| 引脚 | 设置 | 说明 |
|---|---|---|
| PB10 | USART3_TX | 发送数据给电脑 |
| PC11 | USART3_RX | 接收电脑发来的数据(本次实验不涉及) |
6.3 USART3 参数配置
左边 Connectivity → USART3 → Parameter Settings:
| 参数 | 设置值 | 说明 |
|---|---|---|
| Mode | Asynchronous | 异步模式(只有 TX/RX,无时钟) |
| Baud Rate | 115200 | 每秒传 115200 位 |
| Word Length | 8 Bit | 每帧传 8 位 |
| Parity | None | 无校验 |
| Stop Bits | 1 | 停止位 1 位 |
6.4 NVIC 配置
左边菜单 → NVIC → 找到 USART3 global interrupt → 打勾(本次实验用查询方式,不勾也能工作,但建议勾上习惯就好)
6.5 时钟配置
Clock Configuration → HCLK = 168 MHz。
时钟树里 USART3 挂载在 APB1 上(42MHz),CubeMX 会自动计算分频因子使波特率误差最小。
6.6 生成工程
Project Manager:
- Project Name:
uart_debug - Toolchain/IDE:MDK-ARM
- 点击 GENERATE CODE
7. 代码解读
7.1 完整代码
c
/* USER CODE BEGIN Includes */
#include <stdio.h> // printf、sprintf 等函数的声明
/* USER CODE END Includes */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART3_UART_Init();
/* USER CODE BEGIN 2 */
int count = 0; // 定义一个计数器
/* USER CODE END 2 */
while (1)
{
count++; // 每次循环加 1
// 打印字符串
printf("Hello from STM32F407!\r\n");
// 打印变量的值
printf("count = %d\r\n", count);
HAL_Delay(1000); // 每秒打印一次
}
}
7.2 printf 格式化输出速查表
c
int val = 42;
float pi = 3.14159f;
printf("%d\r\n", val); // %d = 十进制整数 → "42"
printf("%x\r\n", val); // %x = 十六进制 → "2a"
printf("%u\r\n", val); // %u = 无符号十进制 → "42"
printf("%.2f\r\n", pi); // %.2f = 小数后2位 → "3.14"
printf("val = %d, pi = %.2f\r\n", val, pi); // 多个变量
// 字符串
char name[] = "STM32";
printf("chip: %s\r\n", name); // %s = 字符串 → "chip: STM32"
注意 :打印
float类型需要开启 MicroLIB,或换用sprintf加整数转换。
7.3 \r\n 是什么?
\r = 回车(CR)→ 光标回到行首
\n = 换行(LF)→ 光标移到下一行
\r\n 合起来 = 回车换行 → 新的一行从头开始
不同串口助手的处理不同:
- 只写
\n:有的助手不换行 - 只写
\r:光标到行首但不换行,覆盖之前的内容 \r\n:最保险的写法
8. fputc 重定向详解(核心难点)
8.1 为什么需要重定向?
printf 本身不知道"往哪发"
printf 是 C 语言标准库函数,它只负责把数据格式化成一个字符串(比如把 count=42 变成 "count = 42\r\n")。但它不关心这个字符串最终去哪------屏幕?文件?串口?
fputc 是 printf 的"底层出口"
每次 printf 调用,内部都会多次调 fputc(一次发一个字符):
printf("Hello\r\n");
↓ 内部等价于
fputc('H', stdout); // stdout = 标准输出
fputc('e', stdout);
fputc('l', stdout);
fputc('l', stdout);
fputc('o', stdout);
fputc('\r', stdout);
fputc('\n', stdout);
8.2 重定向代码做了什么
c
int fputc(int ch, FILE *f) // 这行是 C 库规定的格式,不能改
{
HAL_UART_Transmit(&huart3, // 通过 USART3 发出去
(uint8_t *)&ch, // 要发的字符(转成字节指针)
1, // 发 1 个字节
1000); // 超时时间 1000ms
return ch; // 返回发出去的字符
}
代码解读:
int ch:要发送的字符(虽然是 int 类型,但只存储了一个字符)FILE *f:流指针,C 库的格式要求,我们不用管它(uint8_t *)&ch:把 ch 的地址转成 uint8_t 指针,因为 HAL_UART_Transmit 要求接收 uint8_t* 类型return ch:返回发送的字符,表示发送成功
所以整体流程是:
printf("count = %d\r\n", count);
↓
C 库格式化 → 得到字符串 "count = 42\r\n"
↓
逐个字符调 fputc → fputc('c', stdout)、fputc('o', stdout)...
↓
fputc 内部调用 HAL_UART_Transmit → 通过 USART3 发出去
↓
USB-TTL 模块收到 → 通过 USB 传给电脑
↓
串口助手显示:"count = 42"
8.3 什么是 FILE *f?
FILE 是 C 标准库中用来表示"流(stream)"的类型。常见的流:
stdin → 标准输入(键盘)
stdout → 标准输出(屏幕/串口)
stderr → 标准错误(屏幕)
在嵌入式系统中,我们重定向 fputc,让 stdout 从"显示器"改成"USART3 串口"。所以 FILE *f 参数直接忽略。
8.4 什么是 MicroLIB?
为什么需要 MicroLIB?
Keil 默认带的 C 库(ARM C Library)功能很全------支持文件系统、异常处理、区域语言等。但这也意味着:
- 占用的 Flash 空间大
- printf 重定向配置非常复杂(要设置
syscall.c等)
MicroLIB 是 ARM 提供的一个精简版 C 运行库:
- 体积小(几十 KB)
- printf 重定向直接写 fputc 就行,不需要额外配置
- 支持绝大部分常用功能(printf、sprintf、strlen 等)
副作用:
- 不支持浮点 printf(
%f)------但可以用sprintf加整数转换绕过 - 不能同时使用标准的文件操作(fopen/fread 等)
开启方法 :Keil → Options for Target → Target → 勾选 Use MicroLIB
9. 用 printf 调试------这才是重点!
这一节演示调试(debug)的核心:看变量内部的值
9.1 第一个真实调试场景:看中断触发了多少次
结合之前的 PIR 实验,不需要实际接 PIR 硬件就能演示原理。用 while 里的软件自增变量来模拟。
在 /* USER CODE BEGIN 2 */(MX 初始化完成后)添加:
c
/* USER CODE BEGIN 2 */
int count = 0;
/* USER CODE END 2 */
在 while(1) 循环里:
c
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
count++;
// 每次都打印
printf("程序运行了 %d 次循环\r\n", count);
// 每 100 次打印一次(减少刷屏)
if (count % 100 == 0)
{
printf("================== 已经跑了 %d 次 ==================\r\n", count);
}
HAL_Delay(10); // 10ms 一次
}
9.2 第二个真实调试场景:看传感器的 ADC 值
c
// 假设你用 ADC 读取了光敏模块的值
uint32_t adc_value = HAL_ADC_GetValue(&hadc1);
// 调试打印
printf("ADC value = %d (0~4095)\r\n", adc_value);
// 也可以打印十六进制
printf("ADC hex = 0x%04X\r\n", adc_value);
// 根据值做简单判断
if (adc_value < 500)
{
printf("▶ 光线很暗!\r\n");
}
else if (adc_value < 2000)
{
printf("▶ 光线一般\r\n");
}
else
{
printf("▶ 光线充足\r\n");
}
9.3 第三个场景:跟踪函数的执行路径
c
void my_function(int param)
{
printf("[DEBUG] 进入 my_function, param = %d\r\n", param);
if (param > 100)
{
printf("[DEBUG] 分支 A 执行\r\n");
// 分支 A 的代码
}
else
{
printf("[DEBUG] 分支 B 执行\r\n");
// 分支 B 的代码
}
printf("[DEBUG] 离开 my_function\r\n");
}
在串口助手上你会看到:
[DEBUG] 进入 my_function, param = 50
[DEBUG] 分支 B 执行
[DEBUG] 离开 my_function
这就是 debug------你知道程序走了哪条路、参数是什么,不用靠猜。
9.4 调试系统时间
c
// 打印系统运行时间
printf("系统运行了 %lu ms\r\n", HAL_GetTick());
// 测量函数执行时间
uint32_t start = HAL_GetTick();
my_function();
uint32_t end = HAL_GetTick();
printf("my_function 耗时 %lu ms\r\n", end - start);
10. 串口助手的设置
10.1 常用串口助手
Windows 推荐:
- SSCOM(友善串口助手)------ 小巧,够用
- MobaXterm ------ 可以切换十六进制显示
10.2 设置步骤
① USB-TTL 插入电脑
② 设备管理器 → 查看 COM 口号(如 COM3)
③ 打开串口助手
├── 端口号:COM3(和你设备管理器一致)
├── 波特率:115200
├── 数据位:8
├── 校验位:None
├── 停止位:1
└── 打开串口
如果选错 COM 口 :会提示"端口被占用"或"端口不存在"
如果波特率不对:收到乱码
10.3 两个常见操作
DTR 和 RTS:
大部分串口助手的 DTR 默认勾选,会导致 STM32 复位
如果 STM32 反复重启 → 取消勾选 DTR
HEX 显示 vs 文本显示:
想看数值 → 文本显示(能看到 "count = 42")
想看原始字节 → HEX 显示(能看到 0x63 0x6F 0x75...)
11. 常见问题
问题 ①:什么也收不到
排查步骤:
① TX → RX 接反了?(检查接线)
② GND 接了吗?(必须共地)
③ 串口助手 COM 口选对了吗?(设备管理器确认)
④ 串口助手波特率设对了吗?(必须 115200)
⑤ 代码里勾选 MicroLIB 了吗?
⑥ fputc 写对了吗?(注意函数名是 fputc 不是 fputs)
问题 ②:收到乱码
原因 1:波特率不匹配
确认 STM32 设 115200,串口助手也设 115200
原因 2:时钟配置不对
CubeMX 时钟树配错 → USART 波特率计算不准
→ HCLK 必须 = 168 MHz
原因 3:USB-TTL 模块坏了
换一个模块试试
问题 ③:程序烧录后没反应
检查 Keil 的 Debug 设置:
① Options for Target → Debug → 选 ST-Link
② Utilities → 选 ST-Link
③ 点击 Settings → Flash Download → 勾选 Reset and Run
(烧录后自动复位运行,不用按复位键)
问题 ④:为什么一定要用 MicroLIB?
不用 MicroLIB:
你必须实现 _sys_open、_sys_write、_sys_close 等多个底层函数
printf 才能工作
非常麻烦
用 MicroLIB:
只实现一个 fputc 就够
轻量、简单
12. 扩展思考
12.1 如何从电脑发数据给 F407?
c
// 在中断回调里接收
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3)
{
// rx_byte 就是电脑发来的数据
printf("收到: 0x%02X (%c)\r\n", rx_byte, rx_byte);
}
}
12.2 多个 printf 重定向?
如果要同时用 USART1 和 USART3 打印:
c
// 重定向到 USART3
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 1000);
return ch;
}
// 自定义函数,用 USART1 发
void debug_printf_USART1(const char *fmt, ...)
{
char buf[256];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 1000);
}
12.3 sprintf + LCD 显示
c
char display_buf[32];
sprintf(display_buf, "距离: %d cm", distance);
// 把这个字符串显示到 LCD 上(等你的 LCD 有了文字显示功能后)
编写日期 :2026年5月25日
适用硬件 :DshanMCU-F407(STM32F407ZGT6)+ USB-TTL(HW-597)模块
配套视频 :07-3-1 UART 串口编程查询方式
前置知识 :已掌握 GPIO 输出/输入、定时器
关键概念:fputc 重定向、MicroLIB、波特率、8N1 帧格式