串口调试 — printf 重定向与 USART 通信

串口调试 --- printf 重定向与 USART 通信

配套硬件 :DshanMCU-F407(STM32F407ZGT6)+ USB-TTL(HW-597)模块

学习目标 :学会用 printf 打印变量,这是嵌入式开发最重要的调试手段

核心思想:不用 LED 闪烁猜问题,直接看数据


目录

  1. 为什么要学串口调试?
  2. [什么是 USART/UART?](#什么是 USART/UART?)
  3. [引脚选择------为什么是 PB10 和 PC11?](#引脚选择——为什么是 PB10 和 PC11?)
  4. [USART 配置参数详解(115200 8N1)](#USART 配置参数详解(115200 8N1))
  5. 硬件接线
  6. [CubeMX 配置](#CubeMX 配置)
  7. 代码解读
  8. [fputc 重定向详解(核心难点)](#fputc 重定向详解(核心难点))
  9. [用 printf 调试------这才是重点!](#用 printf 调试——这才是重点!)
  10. 串口助手的设置
  11. 常见问题
  12. 扩展思考

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 新建工程

  1. 打开 STM32CubeMX → New Project
  2. 搜索 STM32F407ZGTx → 双击选中

6.2 引脚配置

引脚 设置 说明
PB10 USART3_TX 发送数据给电脑
PC11 USART3_RX 接收电脑发来的数据(本次实验不涉及)

6.3 USART3 参数配置

左边 Connectivity → USART3Parameter 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)功能很全------支持文件系统、异常处理、区域语言等。但这也意味着:

  1. 占用的 Flash 空间大
  2. 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 帧格式

相关推荐
M1582276905514 小时前
工业级 CAN 转以太网网关|SG-CANET-210/410,打通 CAN 与以太网,工业通信无边界
单片机·嵌入式硬件·php
爱搬砖的狮子14 小时前
编译appweb源代码
stm32·单片机·嵌入式硬件
hoiii18714 小时前
STM32 开发板上用 USART 实现 Modbus 协议控制设备的方案
stm32·单片机·嵌入式硬件
Lucky_ldy14 小时前
51单片机的学习上(结合中科协的个人自用笔记)
嵌入式硬件·学习·51单片机
全球通史14 小时前
Jetson Nano 双摄像头芯片检测视觉系统:小尺度难定位问题解决,从零开始实现教程说明
嵌入式硬件·算法·ubuntu·性能优化
崇山峻岭之间14 小时前
单片机RTC实验
单片机·嵌入式硬件·实时音视频
踏着七彩祥云的小丑15 小时前
嵌入式测试学习第 21 天:常见硬件故障现象:不开机、死机、串口无输出
单片机·嵌入式硬件
chuwengeileyan11 天前
过零比较器 proteus
嵌入式硬件
foundbug9991 天前
51单片机 PT100 温度测量程序
单片机·嵌入式硬件·51单片机