news 2026/9/7 13:27:42

STM32串口printf重定向全攻略:从CubeMX到Keil5一步到位

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32串口printf重定向全攻略:从CubeMX到Keil5一步到位

简介:一套基于STM32CubeMX生成的STM32F103C8T6标准工程,配套KEIL5开发环境,核心演示串口输出功能与printf函数重定向封装,适合刚接触STM32的嵌入式初学者,也方便开发者快速搭建带打印调试能力的项目骨架。包内共139个文件,以C源码与H头文件、编译中间文件O/AXF/HEX、CubeMX配置的IOC文件及KEIL工程文件为主,整体约3.45MB,文件结构完整,既能直接编译烧录,也能对照学习外设初始化与底层封装思路。目前已有1920人学习下载,工程中针对串口发送做了printf重定向,可直接用标准格式化输出到调试助手,省去逐个字节发送的麻烦。通过这套源码,读者可以理解STM32的HAL库串口配置流程与printf重定向原理,并将封装方法复用到其他项目;同时保留的HEX文件也便于快速验证功能。 做 STM32 开发,绕不开一套组合拳:CubeMX 生成工程、Keil5 编译烧录、串口往外打调试信息。最近我把 stm32f103c8t6 的串口输出和 printf 函数封装完整走了一遍,从 CubeMX 配置到 Keil5 工程设置,再到自己写一个可靠好用的打印函数,过程中踩了不少坑,也整理出几套可以直接抄作业的写法。这篇文章就把完整思路、关键代码和排查经验一次性说清楚。

这套内容适合两类人:一是刚拿到 C8T6 最小系统板、还没搞明白串口该怎么玩的初学者;二是已经会点灯、但 printf 一打印就乱码或者不输出、被重定向折腾到怀疑人生的朋友。串口调试永远值得花时间打磨,它不光是往电脑上发几个字符的事,后面接 GPS 模块、接 4G 模组、接各类传感器,全靠同一套底层逻辑。

1. 项目核心思路与方案选型

1.1 标题里那些关键词到底在说什么

先说 STM32CUEB,其实大家口口相传的准确名字是 STM32CubeMX,一个图形化配置工具,用来初始化芯片引脚、时钟、外设,然后直接生成工程代码。很多教程里把它简写或写错,搜索时两个名字都能搜到,但下载软件时认准 CubeMX 就行。

KEIL5 指的是 Keil MDK-ARM 5.x,目前 STM32 开发最主流的 IDE 之一。stm32f103c8t6 是意法半导体经典的 M3 内核芯片,64KB Flash、20KB RAM,LQFP48 封装,某宝上的“最小系统板”大多数就是这颗料。串口输出就是用 USART 外设把数据发到电脑端串口助手。printf 函数封装,则是把 C 库的 printf 重定向到底层串口,让调试代码像在电脑上写控制台程序一样方便。

这一连串词串起来的完整工作流程是:CubeMX 配置 USART1 和时钟,生成 MDK-ARM 工程,再用 Keil5 编译、下载,最后通过重定义 fputc 或者自封装打印函数,让程序里的 printf 能直接出现在电脑屏幕上。

1.2 为什么我用 CubeMX + HAL 而不是标准外设库

现在网上还能搜到很多基于标准库的 stm32f103c8t6 例程,代码确实简洁,寄存器也看得清清楚楚。但我建议新项目直接用 HAL 库,原因很简单:CubeMX 生成的工程已经把时钟树、引脚复用、中断优先级这些最容易出错的地方全部处理好了,你只需要在图形界面里勾选配置。对于 C8T6 这种入门板子,HAL 库多出来的那点资源开销完全不用担心。

更重要的是生态问题。现在 ST 官方主推 HAL,后续如果你想换 F4、F7、H7,或者用 CubeMX 配合 FreeRTOS、LVGL 等中间件,全是同一套框架。标准库学完还得再学一遍 HAL,不如一步到位。Keil5 在这里的作用就是编译器和调试器,CubeMX 负责生成代码,两个工具配合好,后面所有工程都是这套流程。

2. 环境准备与工程搭建全流程

2.1 Keil5 安装和芯片支持包这个坑

Keil5 和 Keil4 最大的区别是“支持包”机制:MDK 核心只是一个 IDE 骨架,具体支持哪颗芯片,要单独安装 Device Family Pack。很多人装上 Keil5 后打开工程提示 “Device not found”,就是因为没装 STM32F1 的支持包。

正确做法是:先安装 MDK-ARM(版本选 5.30 左右都行,太新或太旧偶尔会有兼容问题),安装路径不要带中文;然后在 Pack Installer 里找到 STMicroelectronics 目录下的 STM32F1xx Series,把对应 Device Pack 装上。也可以直接在官网下载 Keil.STM32F1xx_DFP.x.x.x.pack 离线包,双击导入,速度更快。装好后新建工程时 Device 列表里能看到 STM32F103C8,就说明支持包生效了。

C8T6 这个型号在 Keil 里显示为 STM32F103C8,Target 界面的晶振设置要看清,标准最小系统板外部晶振多数是 8MHz,但你用的时钟源必须和 CubeMX 里的 RCC 配置一致,否则后面串口乱码的第一个嫌疑点就埋下了。

2.2 CubeMX 配置串口的最小步骤

打开 CubeMX,新建工程,选择芯片 STM32F103C8Tx,这一点没什么可说的。进入 Pinout & Configuration 界面后,有几处必须配置:

第一,SYS -> Debug 选 Serial Wire。这颗芯片默认 SWD 引脚是可以复用的,不把它设置成调试口,程序第一次烧进去后 ST-Link 可能就连不上了,只能按复位键碰运气。养成好习惯,新建工程先把 SWD 打开。

第二,RCC -> HSE 选 Crystal/Ceramic Resonator。如果板子上有 8MHz 晶振,这里就对应外部高速晶振;如果你的板子实际没焊晶振,那就只能靠内部 HSI,时钟树就完全不同了,不求 72MHz 满速也能跑。

第三,USART1 模式选 Asynchronous,默认引脚是 PA9(TX)和 PA10(RX),波特率我习惯设 115200,8 位数据、无校验、1 位停止位,CH340 串口助手最常见的参数。之后去 Clock Configuration 里把系统时钟配置到 72MHz,CubeMX 会自动计算分频系数。

最后在 Project Manager 里设置工程名和路径,路径不能有中文;Toolchain 选 MDK-ARM V5,Code Generator 里建议勾选 “Generate peripheral initialization as a pair of .c/.h files per peripheral”,这样每个外设独立成文件,工程结构清爽得多。

2.3 Keil5 打开工程后的三个关键设置

CubeMX 生成的是 MDK-ARM 文件夹里的 .uvprojx 工程,双击就能打开。但先别急着编译,三个设置确认好再动手:

第一个,Options for Target -> C/C++ 页面,Verify 一下 Define 里有没有 USE_HAL_DRIVER 和 STM32F103C8Tx。通常 CubeMX 会自动生成,但如果你的工程是从别的模板改的,这两个宏缺失会编译出一大堆底层错误。Include Paths 也要检查,确保包含了 Core、Drivers 下的头文件目录。

第二个,Options for Target -> Debug 页面,选择你实际的下载器。ST-Link 就选 ST-Link Debugger,J-Link 就选 J-Link,设置里还要确认 Flash Download 的编程算法是 STM32F10x Med-density Flash 64K。C8T6 是 Medium-density,选错算法下载时会报地址越界。

第三个,Target 页面里勾上 Use MicroLIB。这个选项对串口重定向至关重要,稍后细说。不勾也能编过,但很多标准库函数在嵌入式环境里会牵扯到半主机模式(semihosting),导致程序卡死在奇怪的地方。用 MicroLIB 能省掉至少一半烦恼。

3. printf 函数封装的三种硬核方案

3.1 为什么说“重定向”是串口调试的基石

单片机里原本没有 printf 这个概念,printf 是 C 标准库提供的格式化输出函数,它最终是通过 fputc 这个底层字符输出函数把一个个字符送出去的。在电脑上,fputc 写到屏幕;在单片机上,它的默认实现可能是写到调试器、或者什么都不干。所以要让 printf 走串口发出去,本质就是重新实现 fputc,让它调用 HAL_UART_Transmit。

但如果不想依赖标准库,也可以自己写一个格式化函数。两种思路各有优势,我实际项目里往往会同时备着:调试阶段用标准库 printf,代码里又有自己的 uart_printf 供中断或者特殊场景调用。

3.2 方案一:标准库重定向 fputc + MicroLIB

在 Keil5 环境下,最经典的做法是新建一个 retarget.c 或者在 main.c 顶部加一段代码:

#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }

这段代码里 huart1 是 CubeMX 生成的全局串口句柄。HAL_UART_Transmit 的第三个参数是超时时间,单位是 ms,0xFFFF 表示最长等 65 秒,正常发送一个字节用不了那么久,但这样写能防止极端情况阻塞死。编译时需要勾选 Target -> Use MicroLIB,原因前面说了:标准库的重定向在 MDK 里默认带半主机依赖,MicroLIB 是专门为嵌入式裁剪的轻量库,重定向后不容易出幺蛾子。

用的时候直接printf("hello\r\n");即可。注意我习惯在字符串末尾加\r\n而不是只加\n,因为很多串口助手只认\r\n才换行,不加\r的话文字会叠在同一行。

3.3 方案二:不依赖标准库的 vsnprintf 封装

如果你不想动标准库,或者在一些不能用 MicroLIB 的工程里,可以用 vsnprintf 自己封装一个。vsnprintf 是标准 C 库里的格式化函数,把格式化后的结果写进一个字符数组,然后再由你决定用什么方式发出去。封装代码如下:

#include <stdarg.h> #include <stdio.h> #include "usart.h" void UART1_Printf(const char *fmt, ...) { static char buf[256]; va_list args; uint16_t len; va_start(args, fmt); len = vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); if (len > sizeof(buf) - 1) { len = sizeof(buf) - 1; } HAL_UART_Transmit(&huart1, (uint8_t *)buf, len, 0xFFFF); }

这里用 static 数组而不是栈上数组,是为了避免在某些栈空间紧张的配置下崩掉。如果格式化字符串本身内容不可控,比如来自网络或用户的输入,vsnprintf 会自动截断,比 sprintf 安全得多。这个函数的用法和 printf 一模一样,只是函数名换成 UART1_Printf,括号里的格式化参数照写:UART1_Printf("temp: %.2f C\r\n", temp);

3.4 三种方案到底怎么选

标准库里其实还有多个重定向入口,除了 fputc,也可以从半主机模式层面改造,比如实现_sys_write、关闭 semihosting 等。但在 MDK5 + HAL 的现代写法里,fputc 重定向是最稳定、最不容易出问题的路径。自封装方案的优势是不依赖具体 IDE,换到 GCC 或 IAR 也通用,并且能随意指定串口句柄。如果你手上有两个串口,一个打印一个通信,自封装方案就能自由切换,而标准库重定向只能管一个默认输出流。

结合经验,下面这张表是我常用的选择依据:

方案依赖优点缺点适用场景
fputc + MicroLIBMDK + HAL写完直接用 printf,最常见换了 GDB/IDE 要重新适配Keil5 日常调试
vsnprintf 封装C 库 + HAL可指定串口,截断安全,跨编译器每次多一次缓冲拷贝多串口项目、量产工程
直接 HAL_UART_TransmitHAL最底层,最可控没有格式化能力中断里发固定帧

4. 完整代码实现与串口验证记录

4.1 CubeMX 生成文件里的核心代码位置

CubeMX 生成工程后,串口初始化主要在 usart.c 的 MX_USART1_UART_Init 里,时钟初始化在 main.c 的 SystemClock_Config 里,用户代码写在 main 函数预留的 USER CODE 区。我不建议去改初始化函数,CubeMX 重新生成代码时会覆盖它们;要加的代码都放在标记之间。

串口发送的核心函数是 HAL_UART_Transmit,它的原型是:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

注意 Size 是 uint16_t,最多 65535 字节,数据指针指向 uint8_t 数组。printf 产生的字符数组是 char 类型,转换成 uint8_t 指针即可。这也是很多新手最容易忽略的细节:类型不匹配编译告警,然后稀里糊涂把数据截断了。

4.2 一个可以直接跑通的完整 main 示例

下面是一个最简示例,把重定向和测试输出都放在 main.c 的 USER CODE 区:

#include "main.h" #include "usart.h" #include <stdio.h> UART_HandleTypeDef huart1; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); int count = 0; while (1) { printf("count = %d\r\n", count++); HAL_Delay(500); } }

这段代码烧进去后,打开串口助手,波特率 115200,就能看到每秒两次的计数输出。如果没看到数据,先把 TX/RX 两根杜邦线反接试一下,再检查 COM 口号是不是被其他软件占用了。

这里要多说一句:HAL_Delay 默认用的是 SysTick,而 printf 里的 HAL_UART_Transmit 又是阻塞发送。在高波特率下阻塞时间非常短,但如果你在主程序和中断里同时调用 HAL_UART_Transmit,可能出现数据交错。后面项目变复杂了,建议给串口发送加一个互斥标志,或者在中断里只使用不等待发送完毕的 DMA 方式,把“打印”和“实时响应”解耦开。

4.3 验证过程中的关键现象记录

我第一次验证时遇到的现象很有代表性:程序能跑,LED 能闪,就是串口没输出。排查了很久发现是 CubeMX 工程生成时路径带有中文,Keil 的编译器头文件引用出现异常,但报错又不够明确。重新把工程放到纯英文路径后,一切正常。所以工程路径、用户名叫中文这种环境问题,遇到怪毛病时可以第一时间怀疑。

第二个现象是输出全乱码。当时波特率设为 115200,但 CubeMX 时钟树里 HSE 填的是 25MHz,而板上实际是 8MHz 晶振。HSE 差了三倍多,导致 USART 波特率发生偏移,当然乱码。改成 8MHz 后问题消失。这个坑属于 F103C8T6 最小系统板的经典坑,尤其是从淘宝买的板子,标注的晶振值必须要和实物一致,最好直接看原理图确认。

5. 常见问题与排查技巧实录

5.1 printf 不输出但工程编译正常

这是新手问得最多的问题。编译没问题,烧录成功,串口助手不开或没反应。依次检查三件事:第一,串口助手是否真的打开了正确的 COM 号,拔插 USB 转 TTL 后在设备管理器里看新增的 COM 口;第二,接线是否交叉连接,板子的 TX 要接 USB 转 TTL 的 RX,如果接成 TX 对 TX,数据是发不出去的;第三,代码里是否真的调用了 printf,放在循环外面的语句只执行一次,看过就没下文了。

如果以上都没问题,就要检查重定向代码有没有被编译器优化掉,或者根本没编进工程。CubeMX 生成工程后可能没有把 retarget.c 加入编译,需要在 Keil5 左侧项目管理器里确认文件前面的方框是勾选的。另外,用标准库重定向时不要忘记勾选 Use MicroLIB,不勾选时也会出现 printf 调用了但串口没消息的现象,原因是底层出口不是你的 fputc。

5.2 串口中断只收到一次就再也不进中断

这个坑出现在你用 HAL 库做中断接收时。HAL_UART_Receive_IT 做的是“一次一收”的逻辑:你每次调用它,就只能接收一个字节进中断,处理完回调后如果不重新调用,中断通道就关闭了。正确写法是在回调函数里再调一次:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 处理收到的数据 rx_data HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 继续接收下一字节 } }

如果数据量大、间隔很短,这种逐字节中断方式还容易丢失数据,因为中断频繁打断主程序。更省心的方案是用空闲中断(IDLE)加 DMA,收到一帧后再一次性处理,这对串口日志记录、GPS 数据处理都更友好。C8T6 的 RAM 只有 20KB,DMA 缓冲要规划好,一般一帧 256 字节以内问题不大。

5.3 烧录失败和调试器连不上的几种原因

“No target connected”是另一个高频报错。遇到时先检查 ST-Link/J-Link 的接线:VDD、GND、SWDIO、SWCLK 四根线,SWDIO 和 SWCLK 是交叉很容易接反。确认板上供电正常,有些最小系统板用 ST-Link 供电时要把跳线帽设置正确。

其次是 Flash Download 算法选择错误,如果编程算法里没有 STM32F10x Med-density 128K 或 64K 的选项,就在 Flash Download 页面点 Add 添加。再有就是目标板进入低功耗或引脚被复用了,如果之前烧过把 SWD 引脚改成普通 GPIO 的程序,此时必须按住复位键,在点击下载的瞬间松开,让芯片复位后立刻被调试器连接。

5.4 printf 打印浮点数出不来

调试传感器时经常要打印 float,比如温度、电压。有些旧版 MDK 配合 MicroLIB 时,printf 的 %f 格式化可能输出空白或者显示 “f” 之类的字符,原因是嵌入式裁剪版库对浮点格式化的支持不完整。解决办法有两个方向:一是确认 Target 页面里的 MicroLIB 勾选状态,并尝试更新 MDK 到较新版本;二是干脆不用 %f,自己手工把浮点拆成整数和小数部分:

float temp = 26.53f; int int_part = (int)temp; int frac_part = (int)((temp - int_part) * 100); printf("temp = %d.%02d\r\n", int_part, frac_part);

这个方法简单可靠,嵌入式里大量使用。要注意负数取整可能对小数部分有影响,处理前先把绝对值算清楚。

6. 把这套串口能力复用到更多场景

6.1 给 printf 加时间戳和分级日志

串口打点和裸 printf 最大的区别,是日志信息能不能帮你定位问题。我后期给串口封装加上了时间和级别两个维度:每次打印自动带上毫秒级计数,并区分 DEBUG、INFO、ERROR。这样程序跑飞或者流程异常时,能直接从时间戳上看出卡在哪个阶段。

实现思路并不复杂,关键是封装一个统一的日志入口。底层输出仍然用前面写的 UART1_Printf,只是在格式化前把时间戳前缀拼进去。实际工程里还可以用条件编译控制 DEBUG 版本把日志全量输出,量产版本只保留 ERROR,通过一个宏开关切换。

6.2 把串口数据记录成文本文件

很多新玩法关注“串口数据记录仪,把数据输出为文本形式”。最简单的方式是串口助手自带的“保存日志”功能,把收到的所有内容直接存成 txt。若想更自动化,可以用 Python 写一段脚本读 COM 口,按时间戳追加到文件的每一行。比如数据格式定义为temp=26.5 hum=50.3\r\n,脚本按行解析后还能进一步绘制曲线或者入库。

如果单片机自己有 SD 卡,也可以让 C8T6 把格式化好的字符串通过 FATFS 写到 txt 里,这就是一个离线记录仪的核心。关键仍然是格式化数据的统一出口,所以一开始把 UART1_Printf 这种封装做好,后面接什么通道都方便。

6.3 同一个串口框架对接各种通信模组

串口外壳写好了,后面就顺畅了。用 C8T6 对接 ESP01S WiFi 模组、HX711 称重芯片、MT6701 磁编码器,甚至 4G 模组做 MQTT 上云,底层都是这些知识点:CubeMX 配置对应的串口,发送时打包成帧,接收时用中断/DMA 解析协议。你在 printf 重定向上省下来的时间,正好可以投入到协议解析和滤波算法这些真正有含金量的部分。

以 4G 模组为例,AT 指令交互本质上就是“单片机发字符串给模块,模块回字符串给单片机”。你要做的只是把 UART1_Printf 换成发往 4G 串口的 SendString,再把模块返回数据接进接收解析状态机,一整套通信链路就能搭起来。

最后说点个人体会。串口和 printf 这种看起来最基础的东西,反而是整个嵌入式开发生命周期里使用率最高的调试手段。现在我接任何新板子,第一件事就是点亮 LED,第二件事就是搞定串口打印。这两个地基打牢,后面移植代码、调传感器、排查问题都会顺手很多。特别是 printf 封装,早一点养成自己的统一风格,后续几十个项目都会受益。建议你花一个晚上把本文的方案在 C8T6 上完整跑通,不止跑通,还要故意制造几个故障再亲手解决它们,这些经验比任何教程都记得牢。

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

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

2026年9月GEO贴牌厂家哪家好?贴牌公司五大核心实力横评

一、GEO贴牌赛道爆发&#xff0c;但95%的服务商不是源头2026年&#xff0c;生成式引擎优化&#xff08;GEO&#xff09;从概念验证全面进入商业化爆发期。据中国互联网络信息中心&#xff08;CNNIC&#xff09;数据&#xff0c;国内生成式AI用户规模已突破5.15亿。Gartner预测&…

作者头像 李华
网站建设 2026/9/7 13:23:18

汇川PLC EtherCAT总线配置实战:从硬件组网到故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:23:11

Ryzen 7 7800X3D配RTX 5070装机指南:从3D缓存到DLSS的实战验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:21:06

分布式缓存实战指南:读写流程、一致性方案与故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 13:19:56

Matlab实现二维热传导有限差分:从方程到代码全解析

简介&#xff1a;二维热传导方程是材料科学、电子设备散热分析等工程与物理场景中的常见模型&#xff0c;但由于解析解通常难以获得&#xff0c;常需借助数值方法求解。这份Matlab代码资源正是基于有限差分法&#xff0c;将连续的热传导方程离散到二维网格上&#xff0c;并利用…

作者头像 李华
网站建设 2026/9/7 13:19:53

腾讯混元Hy4架构跃迁:从295B到770B MoE的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华