尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32C542 UART配置与printf重定向实战指南

STM32C542 UART配置与printf重定向实战指南 1. 从一颗新芯片开始为什么我选了STM32C542做UART调试先用一句话交代背景这次项目用的是STM32C542C系列相对大家更熟悉的F系列来说内核升级到了Cortex-M33主频能跑到110MHz左右外设也做了不少改动。第一部分我主要是点亮系统时钟、跑通基本GPIO到了第二部分已经在捣鼓中断和定时器了。今天这篇是第三篇核心内容就两个怎么把UART配置好以及怎么让printf老老实实从串口吐出来。这两个看起来是基本功但如果你是从F1、F4直接跳过来用C542的有几个细节值得提前知道不然会被一些“隐性差异”卡住半天。先说为什么UART配置是所有调试工作的起点。我这个板子没有板载调试器以外的交互通道跑着裸机代码最痛的不是逻辑写不出来而是看不到中间状态。你要么用SWD调试器打断点看变量要么用printf扔日志。但SWD打断点这招在某些场景下并不好用——比如时序敏感、正在和外部设备通信的中断服务函数里你断下来系统就死了问题不一定复现。而串口日志是异步的、非侵入式的逻辑跑飞之前能留下最后的遗言所以UART基本是嵌入式开发的“生命线”。这篇内容如果你正在用STM32C542或者其他C系列芯片可以直接照着抄作业。用老F系列的朋友也可以看很多思路是通的但有几个寄存器、时钟和HAL库版本的差异我会单独标志出来。我的开发环境如下芯片STM32C542R8T6开发工具STM32CubeIDE 1.17.x用的HAL库串口工具USB转TTL模块CH340串口助手推荐用MobaXterm或Vofa后面讲为什么调试器板载ST-Link硬件上我只做了一件非常简单的事把USART1的TX/RX接到USB转TTL模块再连到电脑。就这么点事中间其实藏了好几个值得说清楚的地方下面挨个讲。2. UART配置的核心思路和传统写法的差异2.1 C542的UART外设到底强在哪STM32C542上的UART和F1、F4不太一样。它内部用的是叫LPUART和USART的组合其中我们常用的USART支持了更多可配置能力比如可编程的数据位7、8、9位可选同步模式和单线半双工模式硬件流控RTS/CTSFIFO发送和接收各16字节注意不是所有C系列型号都有具体看参考手册的“USART implementation”章节过采样方式可选16倍或8倍过采样这些能力如果只是当普通日志口用确实没啥感觉。但后面如果你要接一个需要RS485收发控制脚的设备或者要跑一个要求高精度的DMX512协议那C系列硬件的可配置性优势就出来了。配置错误也不会立刻报错但底层的波特率误差会直接影响通信成功率。有一个差异一定要强调**C542的UART时钟源并不默认挂在APB2上你需要自己去查时钟树。**F1时代USART1在APB2USART2/3在APB1很多人已经背下来了。C542虽然也是类似分配但Cortex-M33带来的总线矩阵重构导致外设时钟的开关方式、复位方式都变了。如果你照搬旧工程的RCC时钟使能代码大概率编译能过运行起来串口就是不出数据。2.2 直接操作寄存器还是用HAL成年人全都要很多人一上来就纠结用寄存器还是HAL库我的观点**调试阶段用HAL库快速跑通稳定之后如果有性能瓶颈再针对性换寄存器。**这不是摸鱼这是效率最大化。举一个具体例子你用HAL库初始化完毕之后整个UART的发送接收其实都围绕着几个句柄和回调函数转非常方便。但到了低功耗模式切换、批量发送数据、DMA与中断协同工作的时候HAL库封装带来的额外函数调用开销和上下文切换就不可忽视了。C542主频110MHz看起来挺高但如果你在中断里用HAL_UART_Transmit做日志每发一个字节都要查状态标志、做超时判断、可能还要等待发送完成时间就浪费在这些“看不见”的环节里。所以我的实操经验是初始化、引脚复用、中断配置全部用HAL代码生成快不容易错。数据发送的“热路径”比如每秒几百次的日志输出直接操作寄存器或者使用HAL库提供的高级发送函数。具体到寄存器层面C542的USART发送有个标志位叫TXE发送数据寄存器空和TC发送完成很多初学者只判断TXE就急着写下一个字节结果最后一字节没发完就把外设关了数据尾丢了。处理方式是发完所有数据后必须等待TC置1再关或再切模式。这个坑后面会详细说。2.3 时钟树配置里的隐藏扣分项这块是我此次踩得最深的一个坑必须单独拿出来讲。STM32C542的UART挂在哪条总线上不是看“常识”而是要看两个东西一是参考手册里的时钟树图二是在CubeMX里实际生成出来的RCC配置。我这边用的是USART1CubeMX里默认给了APB2时钟源PCLK2。你看CubeMX的“Clock Configuration”页USART1的时钟输入默认是PCLK2而PCLK2最大110MHz。如果你用的外部晶振是8MHzPLL配置不当出来的PCLK2可能是55MHz或110MHz。这直接影响波特率计算。波特率计算公式如下对于USART发送/接收的时钟源为PCLK这里假设你选择的是PCLK2常用公式为BaudRate PCLK / (USARTDIV * 16)当过采样为16时USARTDIV是一个包含整数和小数部分的寄存器值。反过来推算USARTDIV PCLK / (BaudRate * 16)举个例子PCLK2 110MHz目标波特率115200那么USARTDIV 110000000 / (115200 * 16) 59.679...BRR寄存器里存的其实是USARTDIV乘以16之后的值也就是说整数部分是59小数部分是0.679*16≈10.86四舍五入取11。这样实际分频系数是59 11/16 59.6875实际波特率约为110000000 / (59.6875 * 16) ≈ 115183误差只有0.015%完全没问题。但如果PCLK2不是110MHz而是55MHz算出来误差可能到0.14%左右常规通信也能跑但长帧、高波特率下误码率会增加。所以我的建议是**把CubeMX时钟树截图截图串口波特率计算这一步自己手算一遍不要盲目信任库函数。**HAL库在初始化时确实会根据你填的波特率自动配BRR但它不会告诉你因为时钟源精度不足导致的误差有多大。另外注意C542的UART还允许使用PCLK、PLL2、PLL3、HSI、CSI或LSE作为内核时钟。在某些低功耗场景下你可能希望串口在STOP模式下还能接收那就要把UART时钟切到LSE或CSI。这一点L4系列用户应该很熟但从F1跳过来的人往往会忽略。3. 实操从CubeMX生成到第一个Hello World3.1 CubeMX里的具体配置项先说一下我的CubeMX配置步骤非常具体你可以照着点一遍。打开STM32CubeMX选择STM32C542R8T6进入“Pinout Configuration”页面左侧找到“Connectivity” - “USART1”勾选“Asynchronous”模式。此时芯片视图上PA9USART1_TX和PA10USART1_RX会自动被复用但注意C542的PA9/PA10不一定是你唯一的选择有些封装引脚更多USART1还能映射到PB6/PB7。查一下数据手册的AFIO表。在“Parameter Settings”里主要改这几项Baud Rate115200Word Length8 Bits含校验位选择NoneParityNoneStop Bits1其他保持默认即可在“NVIC Settings”页勾选“USART1 global interrupt”。如果你后面要用中断接收必须在这里把中断打开否则HAL_UART_Receive_IT会一直超时或永不触发回调。如果你要开FIFO去“Parameter Settings”下拉找到“FIFO Mode”。我试过开启后连续收发都更顺畅但有一个副作用用HAL库的接收中断回调时因为FIFO的存在会出现一次中断收到多个字节的情况。如果上层按“一字节一回调”的逻辑处理就要小心。裸机简单日志不需要开FIFO所以我没有开启。时钟树配置我选择外部高速时钟HSE 8MHz→ PLL倍频到系统时钟110MHz然后APB1分频到55MHz保证APB1外设不超频APB2不分频保持110MHz。USART1挂APB2波特率115200误差计算如上可以。CubeMX生成代码后工程里应该已经有了MX_USART1_UART_Init()函数代码如下HAL库版本生成的典型代码static void MX_USART1_UART_Init(void) { huart1.Instance USART1; 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; huart1.Init.OneBitSampling UART_ONE_BIT_SAMPLE_DISABLE; huart1.Init.ClockPrescaler UART_PRESCALER_DIV1; huart1.AdvancedInit.AdvFeatureInit UART_ADVFEATURE_NO_INIT; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); } }如果你用的是更新版本的HAL库你会发现结构体里多了ClockPrescaler和AdvancedInit字段。这不是你的程序错了是因为新HAL库把更多硬件特性暴露出来了没用到就初始化成默认。不需要手动删除。3.2 引脚复用与GPIO初始化不要手动再配一遍很多人会犯一个错误CubeMX生成代码后自己又在main函数里用HAL_GPIO_WritePin控制PA9或者自己改GPIO模式。结果调试了半天发现串口完全没输出。原因在于CubeMX已经为USART1生成了GPIO初始化代码内容是GPIO_InitStruct.Pin GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate GPIO_AF1_USART1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);注意里面的Alternate GPIO_AF1_USART1。这个值看起来只是枚举但如果你手动在别的工程里用老版本HAL库可能会把AF编号搞错。C542的USART1复用功能编号是AF1这个去参考手册的“Alternate function mapping”表查一下。如果AF编号错了你量引脚上是有电平的但它就是不走串口模块死活没有输出。再补一个细节GPIO速度设置成了GPIO_SPEED_FREQ_LOW。对于115200这种低速波特率LOW完全够用而且还能减少EMI。有些人看网上教程把GPIO速度全部设成HIGH那是针对SPI、SDIO这类高速接口的UART这边没必要设成LOW反而稳。3.3 第一个发送函数HAL_UART_TransmitCubeMX配置好、生成代码后在main函数里写一个最简单的发送测试int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); uint8_t test[] Hello from STM32C542\r\n; HAL_UART_Transmit(huart1, test, sizeof(test) - 1, 1000); while (1) { } }HAL_UART_Transmit最后一个参数是超时时间单位毫秒。这里传1000表示最多等1秒。如果串口模块有问题、引脚没接好、或者波特率完全错乱函数会返回HAL_TIMEOUT你可以用返回值判断然后处理错误。很多人一开始不检查返回值程序看起来是“跑飞”了实际是卡在某个外设等待上。实测下来这个最简单的调用在C542上是直接可用的没有任何额外的寄存器配置。如果你看到串口助手收到了乱码别急着改代码先查波特率、时钟树、USB转TTL模块的地线是否共地。这三个问题占了乱码故障的90%以上。4. printf重定向为什么直接改fputc会失效4.1 编译器与C库的三种重定向方案UART能发数据了但每次调用HAL_UART_Transmit要传长度和超时太麻烦。我们希望在代码里愉快地写printf(temp%d\r\n, temperature)这就需要重定向stdio的底层输出函数。但是**重定向printf并没有一个全平台通用的方案它和你的编译器、C库密切相关。**我在STM32C542上用了三种方式分别对应不同场景方案一Keil MDK MicroLIB如果你用的是Keil MDK并在Options for Target里勾选了“Use MicroLIB”那么重定向非常简单。在任意一个.c文件里写#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }MicroLIB是一个精简版C库它提供的printf会调用fputc来逐字符输出。注意超时时间我写的是0xFFFF意思是一直等防止高波特率下UART发送队列塞满时超时返回导致printf输出中断。MicroLIB模式下代码量小RAM占用少特别适合C542这种内部Flash只有64KB/128KB的型号。缺点是它不支持C99的一些高级特性比如snprintf的某些格式符支持不完整但在嵌入式日志打印场景完全够用。方案二ARM Compiler 6 标准C库不勾选MicroLIB如果你没有勾选MicroLIB用的默认标准C库则不能用fputc。因为ARM Compiler 6的标准库不直接使用fputc而是用更底层的一套机制。你需要自己实现_sys_write或在连接阶段使用“半主机semihosting”的重定向方式。有一个相对简洁的写法#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }这在AC5ARMCC下可行但到了AC6armclang下标准库与fputc的关联就变得不那么直接。你可能会发现编译能过但printf根本没有输出。此时建议使用__asm(.global __use_no_semihosting);来关闭半主机模式否则程序运行到printf时会跳到调试器的半主机通道表现为程序卡死。我在这里给出一段在AC6下经过实测的代码放在工程任意源文件里均可#include stdio.h #if defined(__ARMCC_VERSION) (__ARMCC_VERSION 6010050) // 关闭半主机模式防止printf进入调试通道 __asm(.global __use_no_semihosting); void _sys_exit(int x) { x x; } void _ttywrch(int ch) { (void)ch; } int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; } #endif注意_sys_exit和_ttywrch在标准库内部可能会被引用编译器要求在关闭半主机之后提供这些符号否则链接报错。这种做法在MDK AC5/AC6、STM32CubeIDE自带的GCC下也能用但GCC有一套完全不同的方法。方案三STM32CubeIDE / GCC环境下的syscall重写如果你和我一样用STM32CubeIDE底层工具链是arm-none-eabi-gcc那fputc不是不能用而是需要配合_write或_write_r函数。基于newlib的printf最终会调用_write系统调用所以重定向方式如下#include stdio.h int _write(int file, char *ptr, int len) { HAL_UART_Transmit(huart1, (uint8_t *)ptr, len, 0xFFFF); return len; }这段代码放到工程任意位置即可。它会接管所有底层字符输出printf、puts、fprintf都会走到这里。实测时发现一个细节如果你在中断里调用printfHAL_UART_Transmit会不断等待TXE标志可能和其他中断优先级产生竞争表现为输出偶尔丢字符。后面会讲解决方案。4.2 重定向printf后的缓冲区与浮点问题重定向之后还有一个被忽略的问题**printf默认是行缓冲还是全缓冲**在嵌入式C库中printf的缓冲行为和C库实现有关。MicroLIB下printf本质上是一次调用fputc输出一个字符每输出一个字符都要经过HAL库的发送函数速度慢但实时性好。GCC的newlib标准库在默认情况下stdout是全缓冲的只有在缓冲区满或者调用fflush时才会真正输出。你可能遇到过这种现象程序跑了一半串口上一个字符都没打印突然某个时刻整段输出全部涌出来。这就是全缓冲在起作用。解决方法有两种每次printf后手动fflush(stdout);最直接但会牺牲效率。在main开头调用setvbuf(stdout, NULL, _IONBF, 0);把stdout设为无缓冲。这样每个printf都会立刻输出到串口延迟最小。我个人偏好方案二。嵌入式日志最重要的是实时性宁可输出慢一点也不能让日志“攒着不发”。否则程序死机时缓冲区里那几个字节永远吐不出来你连现场都看不到。浮点格式化是另一个坑。printf里写%f在默认不启用浮点支持的情况下编译出来的程序会显示“0.000”或者什么都不显示。这不是你代码写错了而是C库为了节省空间默认不链接浮点格式化代码。解决办法Keil MDK MicroLIB会自动带上浮点格式化不需要特殊配置。STM32CubeIDE/GCC在链接选项里加-u _printf_float或者在CubeIDE的project properties - C/C Build - Settings - MCU GCC Linker - Miscellaneous里加入-u _printf_float否则printf里的%f就是无效的。如果不想动链接选项另一个办法是用整数运算自己换算小数部分比如int int_part (int)(value); int frac_part (int)((value - int_part) * 1000); printf(%d.%03d, int_part, frac_part);这个技巧适用于拿不到浮点printf的裸机环境。4.3 printf的线程安全与中断安全很多嵌入式工程师没意识到printf并不是中断安全的。假如你的主循环在调用printf同时串口接收中断里也调用了printf两个线程会同时操作huart1的发送状态轻则字符交错重则HAL_UART_Transmit内部状态机错乱导致后续所有发送全部卡死。解决思路有两种用一个互斥标志保护UART发送。在裸机环境下最简单的做法是定义一个全局变量volatile uint8_t uart_busy;发送前判断如果忙就等待或丢弃。把UART发送改成中断发送模式即调用HAL_UART_Transmit_IT在发送完成回调里清除标志。这样主循环和中断不会同时操作同一个外设因为中断发送是异步的不会阻塞调用方。对于C542这种主频110MHz的芯片如果日志量不大用方式1最简单同步阻塞式发送避免了状态机复杂度代价是CPU偶尔会被阻塞几十微秒但对日志来说完全可以接受。如果日志量很大比如每秒20KB建议使用DMA发送。HAL库对应是HAL_UART_Transmit_DMA。DMA发送需要配置DMA通道同时要处理发送完成回调。实测下来C542的DMA发送比F1爽很多因为DMA支持突发传输而且中断处理路径更快CPU负载显著下降。但DMA也会引入一个新坑如果数据更新太快上次DMA还没发完下次你又改了缓冲区内容会导致串口发出脏数据。规范的写法是用一个“发送中”标志配合双缓冲在一块缓冲区发送时往另一块缓冲区写入新数据。5. 核心环节实现轮询、中断和DMA三级跳5.1 轮询模式简单直接适合日志输出如果只是调试日志轮询模式最省心。核心函数就是HAL_UART_Transmit。我封装了一个log函数void log_msg(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); }这里有几个细节vsnprintf是C标准库提供的格式化输出函数比sprintf安全因为限制了目标缓冲区大小防止格式化超长导致栈溢出。缓冲区大小选择128字节。如果日志内容经常超过128建议改成分段发送比如先发送前半段再发后半段。不要无限增大栈数组虽然C542有挺大的SRAM但裸机工程里栈大小通常设置为0x400或0x800一个128字节的局部数组其实有点占空间。不过实测平时也够用。这个函数用起来很顺手log_msg(Temp: %d, Humi: %d\r\n, temp, humi);丢进中断里会有风险吗我在写日志不多的情况下实测过低负载下没问题但如果日志量大建议把log_msg包装一层中断安全锁。5.2 中断接收用回调函数处理单字节接收方面如果想做成“来一个字节处理一个字节”HAL库最标准的方式是uint8_t rx_byte; HAL_UART_Receive_IT(huart1, rx_byte, 1);然后在回调函数里处理void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { process_byte(rx_byte); HAL_UART_Receive_IT(huart1, rx_byte, 1); // 重新启用下一次接收 } }这个方式在C542上实测非常好用中断响应及时CPU占用低。但有一个必须注意的坑HAL库的UART接收中断是一次性的接收完指定的字节数后不会再自动开启下一次接收。如果你忘记在回调里再次调用HAL_UART_Receive_IT串口就只能收到第一个字节后面的数据全部丢失。这个坑在F1时代就有各型号都一样但我发现很多新手在C542上仍然会踩。另外如果你的接收数据是变长的比如一条命令的长度从几个字节到几十个字节不等怎么知道一条命令结束了呢常用的方式是空闲中断IDLE也就是串口线空闲一段时间后触发中断此时把缓冲区里的数据当一条完整消息处理。STM32C542的UART外设支持IDLE检测。但HAL库默认没有把IDLE中断暴露给用户你需要自己处理__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);然后在中断服务函数里判断void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); process_rx_line(); } }注意IDLE标志要在UART_IRQHandler之后判断因为HAL库在处理完自己的事件之后会清掉某些标志位。实测发现如果你调换了顺序IDLE标志可能已经被清零你就永远等不到“一条完整消息结束”的通知。5.3 DMA接收从“收到多少读多少”到“收满一帧再处理”如果你想在C542上做更大数据量的串口接收DMA是必须掌握的方式。DMA接收需要两个缓冲区一个给UART硬件搬运数据一个给应用程序处理数据。典型的应用就是双缓冲乒乓#define BUF_SIZE 256 uint8_t dma_buf1[BUF_SIZE]; uint8_t dma_buf2[BUF_SIZE]; HAL_UART_Receive_DMA(huart1, dma_buf1, BUF_SIZE);当DMA收到256字节后触发HAL_UART_RxCpltCallback此时你切换到dma_buf2继续接收同时处理dma_buf1中的数据。这个机制能保证数据不丢但要注意如果DMA接收256字节需要的时间很长而中途产生了空闲你可能希望先把已经收到的部分数据拿出来处理这时候就要结合空闲中断来做了。DMAIDLE这种组合在STM32圈子里是很经典的“不定长接收方案”网上很多代码但大多是针对F1/F4的。C542上DMA的请求映射和中断向量表不同建议直接看HAL库自动生成的DMA初始化代码不要手抄旧代码。另外C542在使用CubeMX生成DMA代码时要留意DMA的通道优先级设置。如果优先级太低在串口高速接收时可能被其他DMA请求抢占导致数据错位或丢失。5.4 发送完成回调的边界情况用DMA发送时HAL_UART_TxCpltCallback是最关键的回调。它标志着一帧数据已经完整发送出去。但要注意一个边界情况在C542上如果发送时开启了FIFO模式DMA发送完成中断触发时机可能比最后一个字节真正从引脚上发出去要早。因为数据还在FIFO里排队。如果你在回调里立刻切换GPIO模式比如RS485方向控制可能会截断最后一个字节。解决方法是在FIFO模式开启的情况下不要仅依赖DMA发送完成回调还要等待USART的TC标志。HAL库提供了一个函数HAL_UART_GetState你可以轮询状态直到不再处于HAL_UART_STATE_BUSY_TX或者直接读huart-gState。实测下来先清TC标志再等TC置位是靠谱的做法__HAL_UART_CLEAR_FLAG(huart1, UART_FLAG_TC); while (!__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC));这样写虽然有一点点阻塞但能保证最后一次数据确实从引脚送出去了。6. 实测遇到的高频问题乱码、卡死、输出格式异常一次说清6.1 串口完全没有输出的排查清单每次换新板子我都会按一个固定的顺序排查串口问题效率极高分享出来检查USB转TTL模块的TXD是否连接到MCU的RX引脚PA10RXD是否连接到MCU的TX引脚PA9。这两个接反是最大的坑很多模块上标了TXD/RXD但初学者容易把“TXD接TXD”想当然结果是完全没有任何输出。检查是否共地。USB转TTL模块的GND必须和STM32C542开发板的GND相连否则电平参考点不一致输出大概率乱码或完全无反应。测引脚电平。用示波器或万用表量PA9引脚如果板上电后引脚有跳变说明UART至少在工作如果一直是高电平可能是GPIO复用配置问题或时钟没使能。核对波特率。串口助手设置的波特率必须和初始化代码完全一致。如果你初始化时忘了填115200用了9600输出自然是乱码。检查cube生成的时钟树。用逻辑分析仪看TX引脚有没有脉冲没有则回查PCLK2是否正常输出时钟配置是否卡在SystemClock_Config里。检查是否被其他外设抢占引脚。比如PA9/PA10如果同时被配置成GPIO输出或别的复用功能UART也会失效。实测有一次我花了一个小时最后发现是CubeMX里不小心把PA9也勾选成了GPIO_Output。CubeMX允许一个引脚有多个复用选项但它在生成代码时不会警告你冲突。遇到问题先回头检查引脚配置是一个高效习惯。6.2 printf输出乱码、丢字符、重复字符乱码的根源绝大部分是波特率误差其次是电平不匹配。你手算过时钟配置后如果误差在0.5%以内不会乱码超过了2%长帧数据比如含很多字符的日志就会开始在中间位置出现错位表现为部分字符乱码。丢字符则常见于以下场景你在中断里调用printf发送阻塞时间太长导致其他中断被推迟看起来像是丢数据。你用的USB转TTL模块质量不好没有流控在高波特率下缓冲区溢出。你的程序在fputc里用的HAL_UART_Transmit超时时间太短比如10ms发送未完成就超时返回后续字节被丢弃。重复字符是FIFO和DMA配合时偶尔出现的主要原因是发送缓冲区被修改得太快DMA读取到的数据和实际想发的数据不一致导致同样的字节被发两次。改用双缓冲可以解决。6.3 printf输出了但开发板运行变慢这个问题容易被忽视printf是一个同步、阻塞调用尤其在你还没有用DMA的情况下HAL_UART_Transmit会等待每个字节发送完成。115200波特率下每字节大约86.8微秒一个100字节的日志就要8.7毫秒。如果你的日志打印频率是每10毫秒一条光printf就占了87%的CPU时间运行变慢就不奇怪了。优化方向有三个换更快的波特率比如460800或921600。C542的UART在8倍过采样模式下可以跑到更高波特率但需要匹配的是USB转TTL模块能不能稳定支持。减少日志量把调试日志分级只有开发阶段才打印全量信息。使用DMA发送把日志发送放到后台CPU只在发送启动时做一次配置。这是最终方案。6.4 经典但鲜有人提的一个坑PA9/PA10和SWD复用C542这颗芯片如果你的终端设备对引脚复用比较敏感注意PA13、PA14是SWDIO、SWCLK通常不能动。PA9/PA10本身不是调试口但在某些开发板设计中PA9可能连接了板载的其他功能比如LED、按键或CAN收发器导致你接USB转TTL时信号被拉低或干扰。我手上这块板子PA9位置附近同时焊了一个跳线帽一开始我还以为只是普通排针后来才发现它连着板载CAN收发器的TXD引脚。结果就是串口输出时有时无后来拔掉跳线帽就好了。遇到“串口输出奇怪地不稳定”的情况先查原理图看看PA9/PA10旁边还有什么外设。7. 一点小总结关于C542这份UART经验的延展做了这么多配置和实验我个人最大的感受是STM32C542的UART配置工具链已经非常成熟HAL库可以让你在5分钟内跑通基本收发但能走多远取决于你是否理解时钟树和底层标志位的语义。如果你接下来要在C542上做低功耗设备可以继续往两个方向探索一是用UART的WUFWakeup from Stop Mode功能让串口在STOP模式下保持可唤醒二是用LPUART替代普通USART这样在低功耗模式下依然能接收数据。这两个功能在C系列上都有硬件支持实际代码做法和F系列有一些差异等我把低功耗部分跑通了再单独写一篇。最后分享一个我自己的调试小习惯把开发板上的USART1固定当作调试串口所有日志默认都走这里应用程序的通信串口另找一组UART。这样代码里只需要一套printf重定向逻辑排查问题的时候也只需要盯一个串口非常省心。我试过最复杂的场景是“UART1接GPS模块UART2接4G模块同时用printf打日志到UART3”。如果不加区分三个串口的日志混在一起完全没法调试。用这套统一日志方案之后一切回归简单所有设备的数据都进同一个日志流按时间戳排序一屏看清楚系统状态。希望这篇UART配置与printf重定向的记录能帮你少踩几个坑。
返回列表