news 2026/10/26 4:39:27

STM32 UART2重定向printf/scanf实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 UART2重定向printf/scanf实战指南

简介:本资源是一份面向STM32初学者与嵌入式开发者的实战型UART通信教学程序,聚焦于UART2模块的printf格式化输出与scanf交互式输入功能实现,解决库函数环境下标准I/O重定向这一典型学习难点。压缩包共161个文件,含28个C源码(如stm32f10x_rcc.c、stm32f10x_uart.c等核心驱动)、29个头文件(h)、32个汇编文件(s)及若干编译中间文件(o、d、crf)和工程配置文件(uvproj、uvopt、sct等),完整保留Keil MDK-ARM神舟IV号开发板工程结构,总大小3.07MB。已有866人学习下载,说明其在实践教学中具备较强参考价值。读者可直接导入MDK工程编译运行,获取已验证的UART2初始化配置、_write()重定向实现、中断式接收缓冲区设计、简易scanf解析逻辑及配套串口调试方法,同时附带清晰的中文注释与模块化组织,便于理解底层通信机制并快速迁移至其他UART端口或项目场景。

1. 把 printf/scanf 搬进 STM32 串口2:不是魔术,是库函数级的底层重定向实操

你有没有试过在 STM32 上用printf("cnt=%d\r\n", i);调试,结果串口助手一片死寂?或者敲scanf("%s", buf);却卡在半路,连回车都吞不下去?这不是编译器玄学,而是标准 C 库的_write和_read两个黑匣子没被你亲手撬开。本资源是一份完整可运行的 STM32 神舟 IV 号开发板(基于 STM32F103VE)配套工程,专为 UART2 定制,采用标准外设库(Standard Peripheral Library)而非 HAL,所有重定向逻辑全部落在usart2_printf.c/h和syscalls.c里,无任何中间层封装、无宏替换陷阱、无隐式初始化依赖。它不教你怎么配时钟树,但会告诉你为什么USART2_IRQn必须手动使能、为什么__io_putchar里必须加超时等待、为什么scanf读字符串时getchar()会吃掉第一个字符——这些全是真实项目里翻车过三次才写进注释的血泪经验。适合正在调试传感器协议栈、需要快速验证数据收发逻辑、或被fputc/fgetc返回值搞晕的新手;也适合想确认自己重定向是否健壮、准备把串口调试模块抽成独立组件的熟手。


2. 从标准库到硬件寄存器:printf/scanf 重定向的三层映射原理与选型依据

2.1 标准 C 库的 IO 接口到底在调什么?

printf和scanf表面是格式化函数,底层实际依赖 Newlib(ARM GCC 默认 C 库)提供的弱符号系统:_write负责输出字节流,_read负责输入字节流。它们默认指向syscalls.c中的桩函数(stub),返回-1并置errno = ENOSYS。要让printf打印到 UART2,就必须强定义这两个函数,把字节流导向USART_SendData(USART2, ch);同理,scanf要读取,就得让_read从USART_ReceiveData(USART2)拿数据。这不是“改个函数名”就能搞定的事——_write的buf参数是char *,但USART_SendData一次只发一个uint16_t;_read的len是请求长度,而串口是字节流,你得自己缓存、判帧、处理超时。本资源选择完全接管_write/_read,而非仅重载fputc/fgetc,是因为后者无法支撑scanf的多字节解析(比如%d需要连续读直到非数字字符),且fputc在 Newlib 中仍会调用_write,绕一圈反而增加不可控路径。

2.2 为什么是 UART2?而不是 USART1 或 USART3?

神舟 IV 号板载资源明确:USART1 连接 USB 转串口芯片(CH340),用于下载和主调试通道;USART2 的 TX/RX 引脚(PA2/PA3)直接引出到 DB9 插座,物理上独立,专为外设通信或双机调试预留。若强行将printf绑定到 USART1,会导致下载时串口助手抢断通信,烧录失败率飙升;而 USART3 未焊接外围电路,悬空引脚易受干扰。本工程在usart2_init.c中严格按 RM0008 手册配置:

  • 时钟源:APB1 总线(PCLK1=36MHz),波特率 115200 →USARTDIV = (36000000)/(16×115200) ≈ 19.53→ 整数部分DIV_MANTISSA=19,小数部分DIV_FRACTION=8(即0x08);
  • 模式:8N1(8 数据位、无校验、1 停止位),硬件流控关闭;
  • 中断:仅使能USART_IT_RXNE(接收非空中断),禁用 TXE 中断——因为_write是阻塞式发送,靠轮询USART_GetFlagStatus(USART2, USART_FLAG_TXE)更可控,避免中断嵌套打乱printf的原子性。

提示:USART2的 GPIO 初始化必须在RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE)之后,且GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2)必须显式调用,否则 PA2/PA3 不会进入复用功能模式,这是新手最常漏写的一步。

2.3 库函数版 vs HAL 版:为什么这里坚持用标准外设库?

HAL 库虽有HAL_UART_Transmit和HAL_UART_Receive,但其内部状态机复杂,HAL_UART_Receive默认阻塞在HAL_UART_STATE_BUSY_RX,而_read要求非阻塞或带超时——HAL 的Timeout参数在底层仍依赖 SysTick,一旦printf正在发数据,SysTick 中断可能被抢占,导致scanf永久挂起。标准外设库的USART_ReceiveData()是纯寄存器操作,无状态依赖,配合while(!USART_GetFlagStatus(USART2, USART_FLAG_RXNE));可精确控制等待行为。本资源中usart2_printf.c的usart2_getchar_timeout()函数实现了10ms 超时机制:用SysTick_GetValue()计算流逝时间,超时返回-1,scanf自动放弃本次读取,避免整机卡死。这种细粒度控制,在 HAL 的抽象层下几乎无法安全实现。


3. 工程结构拆解与核心文件逐行注释:从初始化到重定向落地

3.1 工程目录与关键文件职责划分

本资源解压后为标准 KEIL MDK 工程结构,核心文件如下表所示:

文件路径功能说明关键技术点
USER/main.c主函数入口,调用usart2_init()并演示printf/scanfprintf输出前必须确保USART2已使能且NVIC_EnableIRQ(USART2_IRQn)已调用
HARDWARE/usart2/usart2_init.c/hUART2 初始化、发送/接收基础函数usart2_send_byte()内含while(!USART_GetFlagStatus(...))防止发送缓冲区溢出
HARDWARE/usart2/usart2_printf.c/hprintf/scanf重定向实现,含usart2_puts()、usart2_gets()usart2_gets()使用环形缓冲区(大小 64 字节),支持退格键\b删除
SYS/syscalls.c强定义_write和_read,桥接标准库与硬件_write中对\n自动补\r,适配 Windows 串口助手换行显示
STM32F10x_FWLib/src/stm32f10x_usart.c标准外设库源码(已编译进库),无需修改注意:本工程使用的是V3.5.0版本,与V3.0.0的USART_GetITStatus()参数顺序不同

注意:syscalls.c必须加入工程编译,且不能放在USE_STDPERIPH_DRIVER条件编译块内,否则链接时_write仍为弱符号,导致printf无声无息失败。

3.2syscalls.c:重定义_write与_read的完整实现

#include "stm32f10x.h" #include "usart2_printf.h" // _write: 将 buf 中 len 个字节输出到 USART2 int _write(int fd, char *ptr, int len) { int i; if (fd != 1 && fd != 2) return -1; // 只处理 stdout/stderr for (i = 0; i < len; i++) { if (*ptr == '\n') { // Windows 串口助手需 \r\n 换行 usart2_send_byte('\r'); } usart2_send_byte(*ptr++); } return i; // 返回实际写入字节数 } // _read: 从 USART2 读取最多 len 个字节到 buf int _read(int fd, char *ptr, int len) { int i; if (fd != 0) return -1; // 只处理 stdin for (i = 0; i < len; i++) { int ch = usart2_getchar_timeout(10); // 10ms 超时 if (ch == -1) break; // 超时则停止读取 *ptr++ = (char)ch; if (ch == '\r' || ch == '\n') break; // 遇到回车/换行结束 } return i; }

逻辑说明与参数说明:

  • _write的fd参数是文件描述符,1代表stdout,2代表stderr,0是stdin;本实现只响应fd==1或2,其他返回-1,符合 POSIX 规范;
  • usart2_send_byte()内部调用while(!USART_GetFlagStatus(USART2, USART_FLAG_TXE));确保发送寄存器空闲,再写入USART_SendData(USART2, ch),避免数据覆盖;
  • _read中usart2_getchar_timeout(10)是关键:它用SysTick_GetValue()获取当前滴答值,循环检测USART_GetFlagStatus(USART2, USART_FLAG_RXNE),若 10ms 内无数据到达,则返回-1,scanf解析器据此判断输入结束;
  • scanf("%s", buf)实际会多次调用_read,每次读 1 字节,因此超时机制必须精细到毫秒级,否则长字符串输入会异常中断。

3.3usart2_printf.c:printf/scanf辅助函数的健壮性设计

// usart2_gets: 读取一行(支持退格删除),存入 buf,最大长度 len-1 char* usart2_gets(char *buf, int len) { int i = 0; int ch; while (i < len - 1) { ch = usart2_getchar_timeout(10); if (ch == -1) continue; // 超时跳过 if (ch == '\r' || ch == '\n') { // 回车或换行结束 buf[i] = '\0'; break; } else if (ch == '\b' || ch == 0x7F) { // 退格键(ASCII 0x08 或 0x7F) if (i > 0) { i--; usart2_send_byte('\b'); // 发送退格 usart2_send_byte(' '); // 清屏 usart2_send_byte('\b'); // 回退光标 } } else if (ch >= ' ' && ch <= '~') { // 可见字符 buf[i++] = (char)ch; usart2_send_byte((uint8_t)ch); } } return buf; }

逻辑说明与参数说明:

  • usart2_gets()是scanf内部调用的底层函数,len参数必须传入缓冲区真实大小(如char buf[32]则传32),函数自动在末尾写\0,防止溢出;
  • 退格键处理分三步:先删buf[i],再发\b让光标左移,再发空格擦除原字符,最后再发\b回退光标——这是为了让串口助手显示效果与终端一致;
  • 对ch == 0x7F的判断是兼容 macOS/Linux 终端的退格码(Delete 键),Windows 串口助手通常发0x08,双保险;
  • usart2_send_byte()在发送退格和空格时,同样经过TXE等待,确保字符顺序不乱。

4. 避坑指南:UART2 重定向中五个真实踩过的坑与根治方案

4.1 现象:printf("Hello")无输出,但usart2_send_byte('H')能发出去

原因:_write函数未被链接器识别,仍是 Newlib 的弱符号桩函数。常见于syscalls.c未加入工程编译列表,或文件编码为 UTF-8 with BOM(KEIL 无法正确解析)。
解决:右键工程 → “Options for Target” → “C/C++” → 确认syscalls.c在 “Source Group” 中;用 Notepad++ 将syscalls.c编码转为 “UTF-8 without BOM”;编译后查看 map 文件,搜索_write,确认其地址不再是0x00000000。

4.2 现象:scanf("%d", &num)输入数字后卡住,printf后续语句不执行

原因:scanf读取%d时,会一直等待非数字字符(如空格、回车),但_read的超时时间(10ms)太短,导致scanf认为“输入流已结束”,返回0,num未赋值,程序逻辑错乱。
解决:在main.c中调用scanf前,先清空接收缓冲区:while(USART_GetFlagStatus(USART2, USART_FLAG_RXNE) != RESET) USART_ReceiveData(USART2);;或修改_read中的超时为100(100ms),适配人工输入节奏。

4.3 现象:串口助手显示乱码(如?、``),但用逻辑分析仪看波形正常

原因:PC 端串口助手波特率设置错误,或USARTDIV计算偏差。本工程按 PCLK1=36MHz 计算,若实际系统时钟被误配为 72MHz(如SystemInit()中PLL倍频错误),则实际波特率为72e6/(16×19.53)≈230400,与 115200 差一倍。
解决:用示波器测 PA2 引脚,量一个字符宽度(10 bit),计算实际波特率;检查system_stm32f10x.c中RCC_CFGR配置,确认PLLMUL和HPRE设置;重新计算DIV_MANTISSA和DIV_FRACTION。

4.4 现象:printf输出中文(如"温度:%d℃")显示为方块或乱码

原因:printf是 ASCII 字节流函数,不支持 GB2312/UTF-8 编码。"℃"是 Unicode 字符(U+2103),在源文件中以 UTF-8 编码存储为0xE2 0x84 0x83三个字节,但usart2_send_byte()一次只发一个字节,串口助手按单字节解释,自然乱码。
解决:禁用源文件 UTF-8 编码,改用 ANSI(GBK);或彻底放弃printf输出中文,改用usart2_puts("温度:"); usart2_printf_num(temp); usart2_puts("℃");分段发送。

4.5 现象:scanf读取字符串时,第一个字符总是丢失(如输入"abc",buf中为"bc\0")

原因:scanf("%s", buf)内部会先调用_read读一个字符判断是否空白,这个字符被消耗但未存入buf。若此时USART2的 RXNE 标志已被之前操作置位(如调试时发过命令),_read第一次就立即返回该字符,导致scanf误判。
解决:在main()开头强制清空一次接收缓冲区:while(USART_GetFlagStatus(USART2, USART_FLAG_RXNE)) USART_ReceiveData(USART2);;或在usart2_init()最后加一句USART_ClearFlag(USART2, USART_FLAG_RXNE);。


5. 实战验证四步法:从烧录到交互式调试的完整闭环

5.1 烧录前必检清单:五项硬性条件缺一不可

在将.hex或.axf文件烧录进神舟 IV 号前,请逐项核对以下条件,任一缺失都会导致printf/scanf失效:

检查项正确值验证方法不满足后果
1. RCC 时钟配置RCC->CFGR &= ~(uint32_t)RCC_CFGR_PPRE1;(PCLK1 = HCLK)查system_stm32f10x.c第 212 行波特率计算错误,输出乱码
2. GPIO 复用功能GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2)查usart2_init.c第 48 行PA2/PA3 为普通 IO,无信号输出
3. NVIC 中断使能NVIC_EnableIRQ(USART2_IRQn)查main.c第 56 行scanf无法触发接收中断,永远等待
4. USART2 使能USART_Cmd(USART2, ENABLE)查usart2_init.c第 72 行寄存器全锁死,SendData无效
5. syscalls.c 编译工程左侧栏可见syscalls.c,且编译日志有syscalls.o编译后看 Output 窗口_write为弱符号,printf静默失败

提示:KEIL 编译日志中搜索Warning: L6314W表示符号未定义,此时务必检查syscalls.c是否在编译列表中。

5.2 串口助手配置与交互式调试技巧

使用 XCOM(推荐)、SSCOM 或 Tera Term 进行测试,配置必须严格匹配:

参数必须值说明
波特率115200与usart2_init.c中USARTDIV计算一致
数据位8USART_WordLength_8b
校验位NoneUSART_Parity_No
停止位1USART_StopBits_1
流控None硬件流控未连接,开启会导致握手失败

交互式调试技巧:

  • 验证printf:在main()中插入printf("STM32 UART2 Test OK!\r\n");,烧录后应立即看到该字符串;
  • 验证scanf数字:输入123后按回车,程序应能正确解析并打印num = 123;
  • 验证scanf字符串:输入hello(无空格),scanf("%s", buf)后printf("Got: %s\r\n", buf)应输出Got: hello;
  • 压力测试:连续快速输入1234567890(10 个数字),观察是否丢字符——本工程环形缓冲区为 64 字节,可承受约 6 次连续输入不丢。

5.3 用逻辑分析仪抓包:定位物理层与协议层问题

当软件逻辑看似无误但通信仍失败时,必须用 Saleae Logic 16 或类似设备抓取 PA2(TX)和 PA3(RX)引脚波形。重点关注三点:

  1. 起始位低电平宽度:应为1 / 115200 ≈ 8.68μs,若为4.34μs,说明波特率被设为 230400;
  2. 数据位顺序:LSB 在前,'H'(0x48 =01001000)应显示为0 0 0 0 1 0 0 1(反序);
  3. 帧间隔:连续两个字符间应有至少 1 位空闲时间(1/115200 s),若粘连,说明usart2_send_byte()未等待TXE标志。

抓包发现波形正确但 PC 端收不到,大概率是 PC 串口助手驱动问题(尤其 CH340 在 Win11 下需手动更新为 V3.5.2021.07.01 驱动);若波形本身错误,则回到第 5.1 节检查时钟和 GPIO。


6. 进阶技巧:把 UART2 重定向封装成可移植模块,并适配多串口场景

6.1 模块化改造:从硬编码到可配置接口

原始工程中所有USART2相关寄存器和引脚都是硬编码,不利于移植到其他型号(如 STM32F407 的 USART6)。我将其重构为usart_port.h/c,核心是定义usart_port_t结构体:

// usart_port.h typedef struct { USART_TypeDef* usart; uint32_t rcc_usart; uint32_t rcc_gpio; GPIO_TypeDef* gpio_port; uint16_t gpio_pin_tx; uint16_t gpio_pin_rx; uint8_t gpio_af; uint32_t baudrate; } usart_port_t; extern const usart_port_t usart2_port; extern void usart_port_init(const usart_port_t* port); extern int usart_port_write(const usart_port_t* port, char *ptr, int len); extern int usart_port_read(const usart_port_t* port, char *ptr, int len);
// usart_port.c const usart_port_t usart2_port = { .usart = USART2, .rcc_usart = RCC_APB1PERIPH_USART2, .rcc_gpio = RCC_APB2PERIPH_GPIOA, .gpio_port = GPIOA, .gpio_pin_tx = GPIO_Pin_2, .gpio_pin_rx = GPIO_Pin_3, .gpio_af = GPIO_AF_USART2, .baudrate = 115200 }; void usart_port_init(const usart_port_t* port) { RCC_APB2PeriphClockCmd(port->rcc_gpio, ENABLE); RCC_APB1PeriphClockCmd(port->rcc_usart, ENABLE); // ... 后续初始化代码全部用 port-> 成员访问 }

好处:新增 USART3 只需定义usart3_port结构体,调用usart_port_init(&usart3_port)即可,_write/_read中通过全局指针current_port切换目标,无需改任何重定向逻辑。

6.2 多串口共存方案:printf到 UART2,debug_log到 UART1

实际项目中常需分离功能:UART2 接传感器,UART1 接 PC 调试。此时不能让_write绑死一个串口。我的做法是:

  1. 在syscalls.c中定义static const usart_port_t* current_stdout = &usart2_port;;
  2. 提供void set_stdout_port(const usart_port_t* port)函数,运行时切换;
  3. _write改为return usart_port_write(current_stdout, ptr, len);;
  4. 在main()开头调用set_stdout_port(&usart1_port),调试阶段用 UART1;
  5. 在传感器协议初始化后,调用set_stdout_port(&usart2_port),切回 UART2。

这样printf始终可用,只是目标可变,比写两套printf_uart1/printf_uart2函数干净得多。

6.3scanf的安全增强:添加输入长度限制与非法字符过滤

原始scanf("%s", buf)极易因超长输入导致缓冲区溢出。我在usart2_printf.c中增加了usart2_scanf_safe():

// 安全版 scanf,max_len 为 buf 最大容量 int usart2_scanf_safe(char *buf, int max_len, const char* prompt) { if (prompt) usart2_puts(prompt); int i = 0; int ch; while (i < max_len - 1) { ch = usart2_getchar_timeout(100); if (ch == -1) continue; if (ch == '\r' || ch == '\n') break; if (ch == '\b' || ch == 0x7F) { if (i > 0) { i--; usart2_backspace(); } } else if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '-') { // 只允许字母数字下划线横杠 buf[i++] = (char)ch; usart2_send_byte((uint8_t)ch); } } buf[i] = '\0'; return i; }

参数说明:max_len必须传入sizeof(buf),函数内部自动留一位给\0;prompt为可选提示字符串(如"Input ID:");非法字符(如@#$%)直接忽略,不回显,避免用户困惑。

从那以后我每次做串口交互模块,都强制走一遍这四步:先用逻辑分析仪抓 TX 波形确认物理层 OK,再用usart2_send_byte()单字节验证寄存器层,然后跑通usart2_puts()确认字符串层,最后才接入printf/scanf。少跳任何一环,后面三天都在找那个本该一眼看到的NVIC_EnableIRQ漏写。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 4:39:16

VSCode处理超大目录的性能优化与问题解决

1. VSCode处理超大目录时的性能陷阱解析当你在VSCode中打开一个包含数万文件的超大目录时&#xff0c;编辑器会立即启动后台索引进程。这个看似简单的操作背后&#xff0c;实际上触发了三个关键机制&#xff1a;文件系统遍历、内容索引构建和元数据缓存。我曾在处理一个包含12万…

作者头像 李华
网站建设 2026/10/26 4:39:10

AI芯片软硬件协同设计:从数据搬运瓶颈到架构范式演进

1. 为什么“AI芯片的软硬件设计”不是两个独立模块&#xff0c;而是一体化工程“AI芯片的软硬件设计”这个标题&#xff0c;乍看像教科书目录里的一章——仿佛只要把“硬件设计”和“软件设计”两套知识拼在一起&#xff0c;就能做出一块能跑大模型的芯片。我刚入行那会儿也这么…

作者头像 李华
网站建设 2026/10/26 4:38:38

Maven核心:pom.xml文件结构与依赖管理详解

1. Maven与pom.xml核心概念解析作为Java项目构建的事实标准&#xff0c;Maven的核心在于其项目对象模型&#xff08;POM&#xff09;文件。这个XML格式的pom.xml文件&#xff0c;本质上是一个项目蓝图&#xff0c;定义了从依赖管理到构建流程的所有关键元素。我见过太多开发者只…

作者头像 李华
网站建设 2026/10/26 4:37:54

工业边缘计算平台选型指南:贴合产线的四大硬指标

1. 边缘计算平台推荐&#xff1a;不是选“云的缩小版”&#xff0c;而是挑“现场的决策大脑”最近帮某高校实验室搭建工业互联网边缘实训环境时&#xff0c;好几个老师反复问&#xff1a;“市面上这么多标着‘边缘计算平台’的产品&#xff0c;到底哪个能真正在产线设备旁跑起来…

作者头像 李华
网站建设 2026/10/26 4:37:52

STM32 USART2重定向printf/scanf避坑指南(神舟IV号专用)

简介&#xff1a;本资源是一份面向STM32初学者与嵌入式开发者的实战型UART通信教学程序&#xff0c;聚焦于UART2模块的printf格式化输出与scanf交互式输入功能实现&#xff0c;解决库函数环境下标准I/O重定向这一典型学习难点。压缩包共161个文件&#xff0c;含28个C源码&#…

作者头像 李华
网站建设 2026/10/26 4:37:35

Git仓库搭建、代码上传与多人协作实战(TortoiseGit/SVN)

1. 先想清楚&#xff1a;这套“平台仓库 Git 两个小乌龟”到底解决什么问题我见过太多人卡在这一步&#xff1a;代码写完了&#xff0c;想传到托管平台上&#xff0c;打开网页一看&#xff0c;满屏的git init、git remote、ssh-keygen&#xff0c;直接放弃&#xff1b;或者勉…

作者头像 李华