ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PY32F002B开发板串口调试与printf重定向实战指南

PY32F002B开发板串口调试与printf重定向实战指南 1. 为什么一块几块钱的PY32F002B开发板值得折腾串口打印PY32F002B这颗芯片这两年在低成本嵌入式圈子里热度不低ARM Cortex-M0内核主频能跑到24MHz封装小、价格便宜做小家电控制、简单传感器节点、LED驱动这类活儿非常合适。但很多朋友拿到开发板的第一反应是怎么让它把信息打印出来没有printf调试基本靠猜点个灯还行一旦涉及变量状态、寄存器值、流程走向整个人就抓瞎了。串口调试就是解决这个问题的。把PY32F002B的UART配置好重定向printf到串口你就能在串口调试助手里看到程序运行时的各种信息变量值、函数进出、错误码一目了然。这篇内容就是围绕“PY32F002B开发板串口调试”这个核心从零开始把printf输出的完整链路讲清楚包括时钟配置、GPIO复用、UART初始化、重定向实现、常见坑点排查。适合刚接触PY32系列的新手也适合从STM32转过来、想快速上手国产芯片的开发者。我手上这块PY32F002B开发板是常见的核心板形态引出全部IO带一个Micro USB接口只供电不带串口芯片所以需要外接一个USB转TTL模块。如果你用的是带CH340或CP2102的板子那就更省事直接一根USB线搞定。下面所有操作和代码都基于PY32F002B的官方SDK和Keil MDK环境其他IDE思路一致。2. 动手前的硬件与软件准备清单2.1 硬件连接别小看那几根杜邦线PY32F002B的UART引脚是复用的默认情况下PA0和PA1可以配置为UART1的TX和RX但具体要看你的开发板原理图。我手上这块板子PA0接了LEDPA1空着PA2和PA3引出来了所以我把UART1重映射到PA2TX和PA3RX。这一步必须查原理图确认不能凭感觉。USB转TTL模块选CH340G或CP2102都行几块钱一个。接线就三根模块的TX接板子的RX模块的RX接板子的TXGND对GND。注意TX和RX必须交叉我见过太多人直连然后纳闷为什么没输出。供电方面如果板子已经通过USB供电那TTL模块的VCC就不要接了只接GND和两根信号线避免双电源冲突。注意有些USB转TTL模块的IO电平是5V的PY32F002B是3.3V器件长期接5V TX到3.3V RX可能会损伤引脚。建议选带3.3V/5V跳线的模块或者串一个1k电阻限流。我实测CH340G在3.3V供电下输出高电平约3.1V可以直接用。2.2 软件环境SDK、Keil和串口助手PY32F002B的官方SDK可以从厂商官网或GitHub仓库获取里面包含启动文件、外设驱动库、例程。Keil MDK需要安装PY32F002B的器件支持包Pack如果没有可以选ARM Cortex-M0的通用配置手动添加启动文件和链接脚本。串口调试助手我用的是SSCOM界面简单支持HEX显示和定时发送够用了。也有人用XCOM或者串口调试助手专业版功能大同小异选顺手的就行。编译环境配置好之后先跑一个点灯例程确认工具链没问题再开始改串口。这一步能排除很多低级错误比如下载算法选错、时钟配置不对导致芯片根本没跑起来。3. 串口初始化的核心细节与时钟计算3.1 时钟树配置波特率准不准全看这里PY32F002B默认使用内部HSI 8MHz RC振荡器也可以通过PLL倍频到24MHz。串口波特率的精度直接依赖系统时钟如果时钟偏了波特率就会偏表现为串口助手收到乱码。所以第一步是确认系统时钟频率。我选择HSI 8MHz直接作为系统时钟不启用PLL这样功耗低、配置简单。UART1挂载在APB总线上APB时钟默认等于系统时钟即8MHz。波特率计算公式是BRR fCK / baudrate对于8MHz时钟和115200波特率BRR 8000000 / 115200 ≈ 69.44。PY32的UART波特率寄存器是16位整数加4位小数实际写入值需要按手册格式计算。不过用SDK库函数UART_Init时只需要传入波特率结构体库会自动算。但你要知道背后的逻辑万一通信不正常可以回头检查时钟配置。实操心得如果你用HSI 8MHz跑115200误差大约0.16%完全在容忍范围内。但如果用内部RC的24MHzHSI经过PLLRC振荡器本身精度可能只有±1%累积误差可能导致通信不稳定。建议高波特率时用外部晶振或者降低波特率到9600。3.2 GPIO复用配置AF模式别配错PY32F002B的GPIO复用功能通过AFR寄存器选择。以PA2和PA3为例需要配置为复用推挽输出TX和浮空输入或上拉输入RX。具体步骤使能GPIOA时钟和UART1时钟。配置PA2为复用推挽输出速度选高或中。配置PA3为浮空输入或上拉输入。设置AFR寄存器将PA2和PA3映射到UART1的TX和RX功能。这里有个容易踩的坑PY32的AFR寄存器编号和STM32不同不能照搬STM32的代码。必须查PY32F002B的参考手册找到UART1_TX和UART1_RX对应的AF编号。我查到的资料显示PA2对应AF4PA3对应AF4但不同封装可能不一样务必以手册为准。// GPIO复用配置示例基于SDK库 GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_UART1_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_2; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF4_UART1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_AF_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);3.3 UART参数设置数据位、停止位、校验位标准配置是8位数据位、1位停止位、无校验、无硬件流控。波特率115200。这些参数在串口助手里要一一对应否则收到的就是乱码或者根本收不到。UART_HandleTypeDef huart1; huart1.Instance UART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart1);初始化完成后可以先发一个字节测试比如HAL_UART_Transmit(huart1, (uint8_t*)A, 1, 100)串口助手收到“A”就说明链路通了。这一步别跳过直接上printf容易把问题复杂化。4. printf重定向的三种实现方式与代码实操4.1 方法一重写fputc最常用Keil MDK默认使用MicroLIB时printf最终会调用fputc来输出字符。我们只需要重写fputc把字符通过UART发出去。#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t*)ch, 1, 0xFFFF); return ch; }然后在Keil的Target选项中勾选“Use MicroLIB”。如果不勾选标准库的printf会依赖半主机模式导致程序卡在BKPT指令。这是新手最常遇到的坑之一现象是程序下载后不运行或者串口没输出。注意MicroLIB不支持浮点printf如果你要打印float需要自己实现浮点转字符串或者改用标准库并重写_write。我一般用整数放大法比如把电压值乘以1000变成毫伏用%d打印省事又可靠。4.2 方法二重写_write标准库方案如果不使用MicroLIB就需要重写_write函数并且可能需要关闭半主机模式。在代码里添加#include stdio.h #include rt_misc.h int _write(int fd, char *ptr, int len) { HAL_UART_Transmit(huart1, (uint8_t*)ptr, len, 0xFFFF); return len; } __asm(.global __use_no_semihosting\n\t);这种方式支持浮点但代码体积会大一些。对于PY32F002B这种Flash只有20KB的芯片能用MicroLIB就用MicroLIB省空间。4.3 方法三自己实现printf变体最轻量如果连MicroLIB都不想用可以自己写一个简单的串口打印函数支持可变参数#include stdarg.h #include string.h void uart_printf(const char *fmt, ...) { char buf[128]; 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), 0xFFFF); }这种方式不依赖任何库代码可控但功能有限不支持宽度、精度等高级格式。适合资源极度紧张的场景。4.4 三种方式对比与选型建议方式代码体积浮点支持依赖适用场景重写fputc MicroLIB小不支持MicroLIB资源紧张整数打印重写_write 标准库大支持标准库需要浮点Flash充足自定义uart_printf最小不支持无极简应用固定格式我一般首选第一种Flash占用小配置简单。如果项目确实需要打印浮点再换第二种。第三种适合做bootloader或者极简测试代码。5. 串口调试中那些让人抓狂的坑与排查方法5.1 串口助手收到乱码乱码是最常见的现象原因通常有三个波特率不匹配、时钟配置错误、数据位/停止位不对。排查顺序确认串口助手的波特率和代码里一致都是115200。用示波器或者逻辑分析仪测TX引脚波形计算实际波特率。如果没有仪器可以临时把波特率降到9600看是否正常。如果9600正常而115200乱码基本是时钟精度问题。检查系统时钟配置确认HSI频率和PLL倍频系数。PY32F002B的HSI出厂校准值在寄存器里但如果你改了时钟配置没更新就会偏。我遇到过一次代码里写的115200但系统时钟实际是4MHzHSI分频了结果BRR算出来差一倍串口助手收到全是乱码。后来把时钟配置改回8MHz就好了。5.2 printf没有输出程序卡死如果程序下载后没反应串口也没输出先检查是否勾选了MicroLIB。没勾选的话printf会触发半主机程序停在BKPT。其次检查UART初始化是否成功可以在初始化后点个灯确认程序跑到了主循环。还有一种情况是TX引脚配置成了普通GPIO而不是复用功能或者AF编号写错。用万用表测TX引脚电压空闲时应该是高电平3.3V。如果一直是低电平说明引脚配置有问题。5.3 打印中文乱码串口助手默认是ASCII显示中文需要UTF-8或GBK编码。Keil源文件默认是GBK但串口助手如果选UTF-8就会乱码。解决办法要么源文件存为UTF-8要么串口助手切换编码。我一般统一用UTF-8避免跨平台问题。另外MicroLIB对中文支持没问题但要注意字符串长度别超过缓冲区。5.4 常见问题速查表现象可能原因解决方法无输出MicroLIB未勾选勾选Use MicroLIB无输出TX/RX接反交叉接线乱码波特率不匹配统一波特率乱码时钟配置错误检查HSI/PLL配置卡死半主机模式重写_write或勾选MicroLIB中文乱码编码不一致统一UTF-8偶尔丢数据发送过快无缓冲加延时或DMA6. 进阶技巧用DMA和环形缓冲提升串口输出效率6.1 为什么需要DMAprintf是阻塞式发送每打一个字符都要等UART发送完成。如果主循环里频繁打印会严重拖慢程序运行。比如你在1ms定时中断里打印调试信息阻塞发送可能导致中断响应延迟影响实时性。DMA方式可以把数据搬到UART发送寄存器CPU不用管发完了再中断通知。这样printf几乎不占用CPU时间。PY32F002B支持DMA但通道资源有限需要查手册确认UART1_TX对应哪个DMA通道。6.2 环形缓冲空闲中断接收发送用DMA接收可以用环形缓冲加空闲中断。串口接收中断里把数据存入环形缓冲主循环慢慢处理。空闲中断用来判断一帧数据结束。这种方式适合接收不定长指令比如AT指令或者自定义协议。#define RX_BUF_SIZE 128 uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_head 0, rx_tail 0; void UART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_RXNE)) { uint8_t data (uint8_t)(huart1.Instance-DR 0xFF); uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { rx_buf[rx_head] data; rx_head next; } } if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 一帧接收完成置标志位 } }实操心得PY32F002B的UART空闲中断标志清除需要先读SR再读DR顺序不能错否则清不掉。我在这上面卡了半天后来查手册才发现。6.3 用DMA发送printf数据把printf的输出先格式化到缓冲区然后启动DMA发送。注意DMA发送期间缓冲区不能被修改所以要么用双缓冲要么等发送完成再格式化下一帧。char dma_tx_buf[256]; void uart_dma_printf(const char *fmt, ...) { va_list args; va_start(args, fmt); int len vsnprintf(dma_tx_buf, sizeof(dma_tx_buf), fmt, args); va_end(args); HAL_UART_Transmit_DMA(huart1, (uint8_t*)dma_tx_buf, len); }这种方式适合低频打印比如每秒几次。如果打印频率很高还是建议用环形缓冲加DMA循环模式。7. 从串口调试延伸出去的几个实用方向串口调通之后这块板子的玩法就多了。你可以用串口接收上位机指令控制GPIO输出做简单的命令解析器。也可以把传感器数据通过串口上报配合串口调试助手的数据记录功能做简易的数据采集。甚至可以用串口做IAP升级通过上位机发送固件数据Bootloader接收后写入Flash。我最近一个项目就是用PY32F002B做温湿度采集串口每5秒打印一次数据格式是“T25.3,H60.2”上位机用Python脚本读取串口并存入CSV文件。整个链路简单可靠成本不到十块钱。如果你想让交互更友好可以移植一个轻量级的命令行shell比如Letter Shell支持Tab补全和历史命令。不过PY32F002B的RAM只有3KB移植时要裁剪功能只保留核心解析。我试过在2KB RAM的芯片上跑精简版shell勉强够用但要注意栈深度。最后分享一个我常用的调试技巧在串口输出里加时间戳。用一个1ms的SysTick计数器每次printf前打印当前计数值这样能直观看到各段代码的执行时间定位性能瓶颈非常方便。比如printf([%lu] Sensor init done\n, HAL_GetTick());这样一眼就能看出初始化花了多少毫秒比盲猜强多了。
返回列表