资讯动态

PY32F002B串口printf重定向实操:从点灯到调试效率提升

发布时间:2026/9/28 20:50:35 来源:尧图企业网站定制
不少人拿到 PY32F002B 开发板第一件事就是点灯。点完之后呢就不知道该干什么了。其实对于刚接触 32 位 MCU 的人来说串口通信是最值得先打通的一环而把 printf 重定向到串口又是这一环里最实用的一步。我用这块几块钱级别的小板子实测了一遍从接线、建工程到重定向、排查乱码把整套流程踩通了。这篇文章就是我的实操记录适合刚拿到 PY32F002B、或者从 STM32 转过来的朋友。看完你就能在串口助手上看到熟悉的 Hello PY32F002B并且知道它背后到底是怎么工作的。1. 核心思路拆解与硬件方案选型1.1 为什么选 PY32F002B 做串口 printf 实战PY32F002B 属于灵动微电子的 PY32F0 系列内核是 ARM Cortex-M0定位就是低成本、低功耗、小资源。它便宜到什么程度一块最小系统板价格通常只有几块钱到十几块钱比起当年动辄几十块的 STM32F103 核心板入门门槛低了一大截。但它又是正儿八经的 32 位 MCU不是 51 那种老架构开发方式也和 STM32 高度接近。选它做串口 printf 实战有几个理由。第一外设够简单USART 数量少实际常用就是 USART1具体以数据手册为准环境够纯粹不会被多路串口、DMA、中断一堆概念绕晕。第二官方和社区给的 HAL 库模板非常接近 STM32 的 HAL 风格你在这里学到的 GPIO 配置、UART 发送、printf 重定向迁移到 STM32/GD32 几乎零成本。第三因为便宜随便折腾不心疼烧坏了也不怎么肉疼。不过它不是万能的后面我会专门讲它的坑尤其是 Flash 和 RAM 很紧张这一点。但也正因为紧张你被迫学会“省着用”这对入门来说反而是好事。很多朋友一上来就玩 STM32资源多到不需要思考最后代码里 printf 满天飞、浮点数随便打到了小资源芯片上就各种翻车。先在小芯片上练一圈后面做大项目会稳很多。1.2 开发板与 USB 转串口模块怎么搭配市面上的 PY32F002B 开发板大致分两类。一类是纯核心板/最小系统板板子上只有 MCU、电源 LDO、排针没有 USB 转串口需要你另买一个 USB 转 TTL 模块。另一类是带板载 USB 转串口的全功能板常见方案是 CH340N 或 CP2102插一根 USB 数据线电脑就能识别出 COM 口直接用。如果是纯核心板接线口诀是“TX 接 RXRX 接 TXGND 必须共地”。具体到 PY32F002B串口默认是 USART1一般在 PA2 和 PA3 上PA2 是发送PA3 是接收。USB 转 TTL 模块的 RXD 接开发板 PA2TXD 接开发板 PA3GND 接 GND。别接成两个 TX 对 TX那样什么都没有。这里有一个容易忽略的点PY32F002B 是 3.3V 逻辑很多 USB 转 TTL 模块虽然有 3.3V/5V 跳线但如果你用 5V 供电、3.3V 逻辑的模式没问题如果模块本身只能输出 5V 的 TXD直接接到 PA3 上长期跑有风险。建议选支持 3.3V 逻辑的模块或者先用板载 3.3V 供电再共地避免电平不匹配。我自己最常用的组合是 ST-Link 下载程序 CH340 模块看串口ST-Link 只负责 SWD 烧录不碰串口。1.3 和其他常见单片机的串口体验对比这里做一个横向对比方便你判断自己是不是选对了方向。STM32F103C8T6 是 Cortex-M3主频 72MHzFlash 64KBUSART 有 3 个以前是入门首选但现在价格也没有绝对优势了。STM32F030 系列是和 PY32F002B 定位接近的低成本 M0但价格、供货一般不如国产小芯片。STC89C52 是 51 内核UART 只有一个下载还要冷启动用起来确实有点“古老”。ESP32 自带 USB 转串口写代码直接 Serial.print但那是另一个赛道WiFi/蓝牙那一套复杂得多。PY32F002B 的优势就在这里M0 核开发逻辑和主流 ARM 生态一致但价格便宜、封装小、功耗低。缺点是 Flash/RAM 小、外设少、主频低不适合复杂应用。对于想用最低成本把“单片机程序到底是怎么跑起来的”搞明白的人它几乎是同价位里最合适的。尤其是串口通信这一步熟练掌握之后后面换任何一颗 MCU核心思路都差不多。芯片/开发板内核主频典型Flash典型RAMUSART数量上手难度PY32F002BCortex-M024MHz max几KB~十几KB级2KB级1路常用低STM32F103C8T6Cortex-M372MHz64KB20KB3路中STM32F030C8Cortex-M048MHz64KB8KB2路以上中低STC89C525112MHz左右8KB512B1路低但冷启动下载麻烦ESP32 DevKitXtensa双核240MHz4MB以上320KB多路中高2. 环境搭建与工程模板不踩坑的第一步2.1 开发环境选择与安装Windows 下最省事的就是 Keil MDK 5 加灵动官方的 PACK 包。在 Pack 安装器里搜 PY32F0或者直接装官方提供的 Device Family Pack装完就能在芯片列表里看到对应型号。然后装 USB 转串口驱动CH340 装 WCH 官方驱动CP2102 装 Silicon Labs 驱动。驱动没装好插线后会识别成未知设备很多新手在这里卡半天。Linux 下喜欢命令行的人可以用 arm-none-eabi-gcc 交叉编译链加社区模板配合 py32-openocd 这类开源工具下载。如果有人非要把开发板挂到 Ubuntu 虚拟机里交叉编译操作也不复杂但你得先确保虚拟机识别到了 USB 设备并且给 openocd 配好权限。新手阶段建议先用 Keil等把流程跑通了再折腾工具链。开发工具不宜贪多能跑通一个就先用一个后面有精力再扩展。2.2 创建工程别从零开始直接改官方例程第一次接触 PY32F002B不建议从零建工程。官方固件库里已经有完整的 USART 例程直接把它复制出来改是最省力、最不容易出错的路径。步骤大致是这样。先到灵动微官网或官方代码托管仓库找 PY32F0xx 固件库压缩包解压后看 Examples 或 Projects 目录。找一个和串口相关的例程比如 USART_TxRx 或 PrintfDemo。复制整个工程文件夹不要在原工程上改防止搞乱原始代码。打开 Keil 工程确认处理器型号选了 PY32F002B 对应那颗然后再看 Options for Target 里的 Debug 设置选 ST-Link 或者你手上的调试器。编译下载打开串口助手正常就能看到输出。这中间最值得强调的一点是PY32 系列用的 Flash 下载算法和 STM32 不一样很多朋友用 STM32 的工程格式去套编译过了但下载失败报错类似 Cannot access target 或者 Flash Algorithm 不对。解决方法是装好灵动的 PACK 后在 Utilities 或 Flash Download 页面里选择对应芯片的 Flash 算法。如果实在找不到去看看官方模板工程里 Utilities 页是怎么选的照着抄。2.3 串口引脚与最小系统接线图下面画一个最简接线图这对纯核心板用户很有用USB转TTL模块 PY32F002B核心板 RXD --------- PA2 (USART1_TX) TXD --------- PA3 (USART1_RX) GND --------- GND板载串口芯片的板子就不用管这套了USB 一插就行。注意 PA2/PA3 这两个引脚在 PY32F002B 上默认复用为 USART1但代码里必须在 GPIO 初始化时配置成 AF_PP 并指定复用功能号 AF1_USART1否则引脚不会连到串口外设上。这个坑和 STM32 一模一样忘了配复用所有配置看起来都对但就是不输出。3. printf 重定向原理、代码与参数计算3.1 printf 输出背后的完整链路先花两分钟理解一下 printf 的原理后面遇到问题才好排查。printf 是 C 标准库函数它把格式化的数据写到标准输出 stdout。在 PC 上stdout 默认是屏幕所以 printf 结果显示在终端在单片机裸机环境里并没有屏幕这个东西stdout 是“悬空”的。重定向要做的事情就是把 stdout 的底层写操作接到 UART 上。在 Keil 的 ARMCC/MicroLIB 环境下编译器会调用 fputc(c, f) 作为字符输出函数在 GCC 的 newlib 环境下底层是 _write(file, ptr, len)。所以你只需要实现这两个函数之一把字符通过 HAL_UART_Transmit 发到串口printf 的路线就通了。很多人问“为什么我 include 了 stdio.h 还是不行”就是因为只 include 了头文件没做这个底层重定向。这里顺带回答一个经常被纠结的问题printf 和 HAL_UART_Transmit 到底选哪个直接 HAL_UART_Transmit 当然能发数据但没有格式化能力。你临时调试要看一个变量的十六进制值还得自己转字符串或者一个字符一个字符地发太麻烦。printf 的 %d、%x、%02X 甚至 %.2f改一行代码就能搞定调试效率完全不同。代价是引入标准库的格式化代码占用 Flash 和 RAM这个后面会详细说。3.2 Keil MDK 下的重定向完整代码先给一个能直接跑的代码模板。我在工程里一般把串口初始化和重定向分开一个放 usart.c一个放 retarget.cmain.c 只负责调接口。usart.c#include usart.h UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_USART1_CLK_ENABLE(); /* PA2: USART1_TX, PA3: USART1_RX */ GPIO_InitStruct.Pin GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF1_USART1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength USART_WORDLENGTH_8B; huart1.Init.StopBits USART_STOPBITS_1; huart1.Init.Parity USART_PARITY_NONE; huart1.Init.Mode USART_MODE_TX_RX; huart1.Init.HardwareFlowControl USART_HARDWAREFLOWCONTROL_NONE; huart1.Init.OverSampling USART_OVERSAMPLING_16; HAL_UART_Init(huart1); }retarget.c#include stdio.h #include usart.h #if defined(__CC_ARM) || (defined(__ARMCC_VERSION) (__ARMCC_VERSION 6010050)) #pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) { x x; } int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; } #endif如果你在 Keil 里勾选了 Use MicroLIB其实只需要 fputc 这一个函数上面那一段 __use_no_semihosting 和 _sys_exit 是为了不勾 MicroLIB 时避免因为半主机模式导致程序死在启动阶段。两种方式我实测都能跑建议新手直接勾 MicroLIB省代码空间也省事。MicroLIB 的 printf 对浮点支持有限但整数和字符串输出完全够用。然后 main.c 里这样写#include main.h UART_HandleTypeDef huart1; int main(void) { HAL_Init(); MX_USART1_UART_Init(); printf(PY32F002B UART printf test\r\n); printf(SystemCoreClock %d Hz\r\n, (int)SystemCoreClock); while (1) { printf(Hello PY32F002B\r\n); HAL_Delay(1000); } }注意 printf 字符串后面加 \r\n不要只加 \n。很多串口工具对裸 \r 或裸 \n 处理不一致加上 \r\n 显示最整齐。这是个小细节能省不少看着“输出错位”的烦恼。3.3 GCC 环境下的重定向实现在 Linux 或 macOS 下用 arm-none-eabi-gcc标准库是 newlib重定向函数换成 _write 即可int _write(int file, char *ptr, int len) { HAL_UART_Transmit(huart1, (uint8_t *)ptr, len, 0xFFFF); return len; }newlib 会自己在内部调用这个符号把 printf 的输出一截一截传过来。也就是说fputc 一次发一个字符_write 一次可能发一整段字符串效率反而更高一些。这段代码在 GCC 环境下要注意不要和 retarget.c 里的 Keil 专用代码混在一起编译建议用宏区分平台。用 newlib 的时候一个额外问题是浮点支持。如果你链接命令里用了 --specsnano.specs 来裁剪体积printf 默认不带浮点解析遇到 %.2f 会输出空或者不支持。解决办法是链接时加 -u _printf_float。代价是代码体积变大其实和 Keil MicroLIB 的情况类似。对 PY32F002B 这种小 Flash 芯片强烈建议能不用浮点就不用浮点把整数拆开打印能用 %d 解决就别上 %.2f。3.4 波特率计算与内部时钟误差分析HAL 库的 HAL_UART_Init 会自动帮你计算 USART 波特率寄存器 BRR 的值可以不用手算但建议知道它怎么来的。标准的异步串口公式是波特率 fck / (16 * USARTDIV)对于 PY32F002B 内部 HSI 为 24MHz、目标波特率 115200 的情况USARTDIV 24000000 / (16 * 115200) 13.0208整数部分是 13小数部分在寄存器里用 4bit 左右表示实际算出来的波特率和目标值误差很小大约千分之一点六。这个误差对 115200 来说完全没问题因为在 16 倍过采样下双方时钟误差容忍度通常在 2% 到 3% 左右。问题容易出在 HSI 这个内部 RC 振荡器本身。它的精度一般有限而且不同批次、不同温度下会漂。如果你的板子没有外部晶振全靠内部 HSI串口助手那边的 USB 转 TTL 模块用的是精度很高的晶振两边可能因为时钟偏差表现为高波特率偶尔乱码、低波特率正常。遇到这种问题我给你的第一个排查动作永远是把波特率从 115200 改成 9600 试试。如果 9600 正常而 115200 乱码那基本就是时钟误差问题不是代码逻辑问题。极端情况下可以微调波特率数值比如电脑端设 120000 来匹配芯片实际输出但这是应急手段治标不治本。4. 常见问题与排查技巧实录4.1 printf 中文乱码的 3 个根因很多人一搜串口问得最多的就是“printf 中文乱码”。我实测总结下来中文乱码分三种情况。一是源文件编码和串口工具编码不一致。Keil 默认编辑器保存的文件可能带 BOM也可能按 GB2312/GBK 存而串口助手上选择的是 UTF-8两边对不上中文自然乱成一团。解决方法是把编码统一。最简单先用纯英文串测通再把串口工具切到 UTF-8或者把源码用编辑器另存为不带 BOM 的 UTF-8看你用的串口工具支持哪种就统一用哪种。二是波特率误差带来的整体错乱。这种现象不只是中文乱码英文也乱有时候看起来像“乱码但偶尔能认出几个字符”。先降波特率排除。三是 TX/RX 接反或者接触不良。这时往往完全没输出不会“乱码”。但如果接触不良导致电平不稳定会出现零星乱字符。重新插拔然后做回环测试把 PA2 和 PA3 直接短接代码发什么就收什么能回环就说明串口外设没问题问题在外部接线。现象优先怀疑项处置方法完全没输出接线反了、没下载进程序TX/RX 对调确认烧录成功英文乱码波特率、时钟误差降到 9600检查时钟配置只有中文乱码编码不匹配统一源码与串口工具编码偶尔丢字卡顿中断优先级/缓冲溢出不要在中断里 printf用队列缓冲4.2 PY32F002B 的硬性资源限制与避坑PY32F002B 的 Flash 和 RAM 非常小这是它最需要正视的坑。以常见型号为例代码空间在几 KB 到十几 KB 这个量级RAM 更是捉襟见肘。printf 本身就占空间尤其一旦用到浮点格式化代码体积会明显膨胀。我实际测过一个工程加了一行 printf(%.2f, value)Flash 占用立马上涨了好几 KB差点超容量。解决方向有三个。第一多用整数格式化少用浮点。第二使用 MicroLIB 或者 GCC nano newlib 裁剪标准库。第三去掉不用的 HAL 模块不用的外设初始化代码删干净编译器优化等级开到 -Os。这些都是常规操作但能明显缓解容量焦虑。另外别把 printf 塞进中断服务函数里。HAL_UART_Transmit 是阻塞发送在中断里调用会让中断挂在那里高优先级中断被低优先级逻辑拖住系统行为会变得非常诡异。我见过有人直接在 UART 接收中断里回显一串 printf结果接收中断被阻塞后续字节全丢。更好的做法是中断里只放一个标志或者把数据塞进环形队列printf 放到主循环里处理。还有一个下载相关的坑SWD 引脚尽量不要复用成普通 IO尤其是调试器要用的那两根。一旦你把这些引脚配置成了不接调试器的状态下一次烧录可能连不上芯片。PY32 不像老 51 有专门的 ISP 串口下载模式救砖步骤相对麻烦一些所以新手尽量保住下载口。4.3 串口调试的实用工具与效率技巧串口工具不强求推荐几个我用着顺手的Windows 下的 XCOM、MobaXterm 自带的串口会话Linux 下直接用 screen 或 minicom都行。驱动和工具安装包尽量去官方渠道下载避免第三方打包网站捆绑乱七八糟的东西。效率技巧方面有几个值得记一下。第一在开机启动 printf 里打印编译日期和版本号。有时候串口数据很多不确定固件是不是最新版一条 printf([%s] build\r\n,DATE) 能省很多查版本的时间。第二用逻辑分析仪抓 TX 引脚可以直接量出真实波特率判断是不是内部 RC 偏差太大。这个比瞎猜靠谱得多。第三串口助手设置里尽量选显示时间戳结合程序里的 HAL_GetTick()能快速定位程序卡在哪一步。5. 从 printf 到交互式调试下一步可以怎么玩5.1 串口接收中断让开发板能“听”你说话printf 打通之后很自然会想往开发板发数据控制它。这个时候启用接收中断就行关键代码就几行uint8_t rx_byte 0; /* 主循环初始化后启动接收 */ HAL_UART_Receive_IT(huart1, rx_byte, 1); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { /* 收一个回显一个相当于本地串口回环 */ HAL_UART_Transmit(huart1, rx_byte, 1, 0xFFFF); HAL_UART_Receive_IT(huart1, rx_byte, 1); } }难点在于如果你收到的命令长度不固定就不能太天真地一个字节一个字节处理。最常见做法是维护一个接收缓冲区和计数变量在回调里把字节写入缓冲等收到 \r 或 \n 再解析整行命令。这套思路在所有 MCU 上都是通用的不局限于 PY32F002B学会了一次性受用很久。5.2 用 printf 搭建轻量级日志系统printf 裸用时间长了会发现代码里到处都是 printf后期想关掉某个模块的日志很痛苦。这时候可以包一层日志宏#define LOG_LEVEL_ERROR 0 #define LOG_LEVEL_INFO 1 #define LOG_LEVEL_DEBUG 2 #define LOG_E(fmt, ...) printf([E] fmt \r\n, ##__VA_ARGS__) #define LOG_I(fmt, ...) printf([I] fmt \r\n, ##__VA_ARGS__) #define LOG_D(fmt, ...) printf([D] fmt \r\n, ##__VA_ARGS__)想关掉调试日志把 LOG_D 改成空宏就行不用删代码。再进一步还可以在宏里加 HAL_GetTick() 时间戳方便后期看执行时序。注意这种可变参数宏写法在 GCC 和 ARMCC 下都支持但别在中断函数里调用这些日志宏原因前面讲过了。5.3 适合零基础复刻的迷你项目串口打通后有两个很适合拿来练手的小项目。一个是“按键事件日志”按键按下去串口打印出按下时间、按键编号和持续周期。这个项目能练会外部中断、消抖和 printf 组合使用。另一个是“传感器数据串口监测”接一个常见的 DHT11 或者 DS18B20把温湿度数值通过 printf 打印出来串口助手上能看到实时数据变化。再进阶一点收到特定字符串就切换采集周期这已经有一点“交互式命令行”的雏形了。我在实际折腾 PY32F002B 的时候最大的感受是不要因为它便宜、资源小就小看它串口 printf 这个能力一旦打通调试效率会直接上一个台阶。芯片资源紧张反而逼着我把代码写得更克制改掉了一上来就浮点满天飞的坏毛病。这个习惯后来在做其他项目时帮了我大忙。新手朋友完全可以照着这个路径走先把串口玩明白再去折腾接收、中断、通信协议一步步来基础就扎实了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑