尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Keil环境下单片机串口printf打印中文乱码解决全攻略

发布时间:2026/9/29 2:35:24

资讯中心
01
ARTICLE

Keil环境下单片机串口printf打印中文乱码解决全攻略

Keil环境下单片机串口printf打印中文乱码解决全攻略
1. 串口打印中文这件事为什么值得单独拎出来讲搞过单片机的人都有一个共同习惯调试阶段第一件事就是把串口打通然后用printf往电脑上怼信息。打印英文和数字从来不是问题一行printf(Hello\r\n)就能跑起来。可一旦你试着打印一句中文比如printf(温度过高\r\n)大概率会看到一串乱码或者干脆什么都不显示。这个问题在 Keil 环境下尤其常见因为 Keil 的默认编译器设置、编码格式、终端解码方式三者之间很容易对不上。我自己刚开始用 STC 单片机做项目的时候就踩过这个坑。当时用 Keil C51 写了个串口发送程序英文打印一切正常换成中文就变成了一堆问号或者方块。查了半天以为是波特率不对后来才发现问题出在字符编码和重定向实现上。所以这篇内容就是把这个过程完整拆开从原理到代码到排查一步步讲清楚怎么在 Keil 环境下用printf稳定地发送中文。这篇适合谁看如果你正在用 51 单片机、STM32 或者任何在 Keil 环境下开发的芯片需要通过串口输出中文调试信息那这篇内容可以直接抄作业。如果你只是刚接触串口通信也能从里面搞明白printf重定向到底是怎么回事为什么它不能直接用在单片机上。下面我会按照实际操作的顺序把每个环节的关键点和坑都讲透。2. 整体思路与方案选型为什么 printf 不能直接用在单片机上2.1 printf 在标准 C 里的工作方式在 PC 上写 C 语言程序printf之所以能把文字显示到屏幕上是因为操作系统提供了底层输出接口。标准库里的printf最终会调用一个叫fputc或者类似的底层函数把字符一个一个送到标准输出设备。这个过程在 PC 上是操作系统帮你兜底的你不需要关心字符怎么从内存跑到屏幕上。但单片机没有操作系统也没有标准输出设备。printf在 Keil 的 C 库里面依然存在但它需要一个“出口”。这个出口就是串口。你要做的事情就是告诉编译器当你调用printf的时候底层字符不要往别的地方送统统送到串口寄存器里去。这个动作就叫重定向。2.2 重定向的核心原理Keil 的 C 库无论是 C51 还是 ARM 的 MicroLIB都预留了一个底层字符输出函数。对于 C51 来说这个函数是putchar或者fputc取决于你用的库版本。对于 ARM 的 MicroLIB通常是fputc。你只需要在自己的代码里重新实现这个函数在里面把字符写入串口发送寄存器printf就能正常工作了。这里有一个关键点重定向函数的签名必须和库里面声明的一致。如果你写错了参数类型或者返回值类型编译器不会报错但链接的时候会找不到符号或者运行时行为异常。这是很多人第一次做重定向时容易忽略的地方。2.3 中文打印的特殊性英文和数字在 ASCII 编码下只占一个字节范围是 0 到 127。串口按字节发送终端按 ASCII 解码一一对应不会出问题。但中文不一样。中文在计算机里面通常用两个字节表示GBK 编码或者三个字节表示UTF-8 编码。如果你发送的是 GBK 编码的中文字节流但终端用 UTF-8 解码就会显示乱码。反过来也一样。所以中文打印的问题实际上分成两层第一层是printf重定向有没有成功第二层是发送端和接收端的编码格式有没有对齐。很多人只解决了第一层发现英文能打印了就以为大功告成结果中文还是乱的。下面我会把这两层都拆开讲。3. 核心细节解析编码、重定向与串口配置3.1 中文字符编码在 Keil 里的处理Keil 编辑器默认的源文件编码方式在不同版本里不太一样。Keil uVision5 在中文 Windows 系统下默认可能是 GB2312 或者 GBK。也就是说你在代码里写printf(温度过高\r\n)编译器看到的这几个中文字符在源文件里是以 GBK 编码存储的每个汉字占两个字节。当你编译的时候这些字节会被原封不动地放进字符串常量里。运行时printf把这些字节一个一个通过串口发出去。所以串口上传输的就是 GBK 编码的中文字节流。这时候如果你用电脑上的串口助手接收串口助手必须也设置为 GBK 解码才能正确显示中文。如果你用的是 UTF-8 解码就会看到乱码。这就是为什么有些人说“我代码没问题但就是乱码”——问题出在终端解码设置上。注意Keil 的编辑界面里中文注释和中文串口输出是两回事。注释在编译时会被丢弃不影响运行结果。但字符串常量里的中文会实实在在进入二进制文件。3.2 重定向函数的正确写法对于 STC 51 单片机常用的重定向函数是putchar。下面是一个可以直接用的实现#include reg52.h void UartInit(void) { SCON 0x50; // 串口工作方式1允许接收 TMOD 0x0F; TMOD | 0x20; // 定时器1工作方式28位自动重装 TH1 0xFD; // 波特率9600晶振11.0592MHz TL1 0xFD; TR1 1; // 启动定时器1 ES 1; // 打开串口中断如果不用中断可以关掉 EA 1; } void UartSendByte(unsigned char dat) { SBUF dat; while (!TI); TI 0; } char putchar(char c) { UartSendByte(c); return c; }这段代码里putchar就是重定向的关键。Keil C51 的库在调用printf时会逐个字符调用putchar。你在putchar里面把字符通过串口发出去printf就能正常工作了。对于 STM32 在 Keil MDK 环境下通常使用 MicroLIB重定向函数是fputc#include stm32f10x.h #include stdio.h int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }提示在 Keil MDK 里使用 MicroLIB 需要在工程选项的 Target 标签页里勾选 “Use MicroLIB”。如果不勾选标准库的重定向方式会不一样需要实现_sys_write等函数比较麻烦。所以建议直接勾选 MicroLIB。3.3 波特率与晶振的匹配问题串口通信对波特率的误差很敏感。51 单片机常用 11.0592MHz 晶振因为这个频率能让定时器产生的波特率误差接近零。如果你用 12MHz 晶振9600 波特率的误差会比较大可能导致通信不稳定。具体计算过程是这样的定时器1工作在方式2时波特率 (2^SMOD / 32) * (晶振频率 / (12 * (256 - TH1)))。以 11.0592MHz 晶振、SMOD0、波特率9600为例反推 TH1 256 - (11059200 / (384 * 9600)) 256 - 3 253 0xFD。这就是上面代码里 TH1 和 TL1 都设为 0xFD 的原因。如果你换成 12MHz 晶振同样的公式算出来 TH1 约等于 250.5不是整数实际波特率会有偏差。短距离调试可能看不出来但长时间传输或者高波特率下就容易丢数据。3.4 串口助手的编码设置电脑端的串口助手是接收端它的解码设置直接决定了你能不能看到正确的中文。常见的串口助手比如 SSCOM、XCOM、友善串口调试助手都有编码选项。你需要把它设置成和单片机发送端一致的编码。如果你在 Keil 里写的中文是 GBK 编码串口助手就选 GBK 或者 GB2312。如果你把 Keil 源文件转成了 UTF-8串口助手就选 UTF-8。两边必须一致。注意有些串口助手默认是 UTF-8有些默认是 GBK不同版本还不一样。所以每次换工具的时候先发一句中文测试一下确认编码设置正确再继续调试。3.5 发送缓冲区与中断的配合如果你用中断方式发送串口数据printf重定向的时候要注意缓冲区的问题。printf是阻塞式调用它会一直等到所有字符都发送完毕才返回。如果你在中断服务函数里面调用printf可能会导致中断嵌套或者死锁。我一般的做法是printf只用在主循环或者非中断上下文里。如果需要在中断里发数据就自己写一个环形缓冲区把数据存进去在主循环里慢慢发。这样既不影响中断响应速度也不会因为printf阻塞导致其他中断被延迟。4. 完整实操过程从新建工程到中文打印成功4.1 工程创建与基础配置打开 Keil uVision5新建一个 Project选择你的单片机型号。如果是 STC89C52就选 Atmel 的 AT89C52 或者直接选 STC 的器件库需要额外安装。如果是 STM32选对应的芯片型号。创建完工程后新建一个main.c文件添加到工程里。然后在工程选项里确认晶振频率设置正确。对于 51 单片机在 Target 标签页里把 Xtal 设为 11.0592MHz。对于 STM32在 Target 标签页里勾选 Use MicroLIB。4.2 串口初始化代码以 STC89C52 为例完整的串口初始化代码如下#include reg52.h #include stdio.h void UartInit(void) { SCON 0x50; // 0101 0000方式18位UART允许接收 TMOD 0x0F; // 清除定时器1模式位 TMOD | 0x20; // 定时器1方式28位自动重装 TH1 0xFD; // 波特率9600 TL1 0xFD; TR1 1; // 启动定时器1 ES 1; // 使能串口中断 EA 1; // 使能总中断 } void UartSendByte(unsigned char dat) { SBUF dat; while (!TI); TI 0; } char putchar(char c) { UartSendByte(c); return c; } void main(void) { UartInit(); printf(系统启动成功\r\n); printf(当前温度25.6℃\r\n); printf(中文测试你好世界\r\n); while (1) { // 主循环 } }这段代码编译下载后打开串口助手波特率设为9600数据位8停止位1无校验编码设为GBK就能看到中文正常显示了。4.3 STM32 版本的实现如果你用的是 STM32F103 在 Keil MDK 环境下串口初始化和重定向代码如下#include stm32f10x.h #include stdio.h void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // PA9 TX GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PA10 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; } int main(void) { USART1_Init(); printf(STM32 串口中文测试\r\n); printf(温度26.3℃ 湿度58%%\r\n); while (1) { } }注意printf里面的百分号需要写成%%才能输出一个百分号这是 C 语言格式化字符串的规则。4.4 验证步骤与预期结果下载程序后按以下步骤验证打开串口助手选择正确的 COM 口。波特率设为 9600数据位 8停止位 1无校验。编码设置为 GBK。打开串口按一下单片机复位键。观察接收窗口应该能看到“系统启动成功”、“当前温度25.6℃”、“中文测试你好世界”这几行文字。如果英文正常但中文乱码先检查串口助手的编码设置。如果连英文都不正常检查波特率和接线。如果什么都没有检查串口线是否接反TX 接 RXRX 接 TX。4.5 关键参数速查表参数项51单片机典型值STM32典型值说明晶振频率11.0592MHz8MHz外部51单片机建议用11.0592MHz波特率96009600调试阶段不建议超过115200定时器模式方式2不用定时器51用定时器1产生波特率重定向函数putcharfputc取决于编译器和库编码格式GBKGBK与串口助手保持一致数据位88标准配置停止位11标准配置校验位无无调试阶段通常不用校验5. 常见问题与排查技巧实录5.1 中文乱码的三种典型情况第一种英文正常中文显示为乱码。这几乎可以肯定是编码不匹配。Keil 源文件是 GBK串口助手是 UTF-8或者反过来。解决办法是把串口助手编码改成 GBK或者在 Keil 里把源文件转成 UTF-8但 Keil 对 UTF-8 的支持不太稳定不建议。第二种英文和中文都乱码。这说明波特率不对或者串口初始化有问题。先检查晶振频率和 TH1 的值是否匹配再检查串口助手的波特率设置。第三种中文显示为问号。这通常是串口助手不支持中文或者编码选项里选了一个不支持中文的字符集。换一个串口助手试试比如 SSCOM 或者 XCOM。5.2 printf 没有输出的排查顺序如果printf完全没反应按以下顺序排查确认串口初始化代码被调用了。有时候UartInit()写了但忘记在main里调用。确认重定向函数的名字和签名正确。C51 是putcharMDK 是fputc写错了不会报错但不会生效。确认 MicroLIB 已勾选STM32 在 Keil MDK 下。确认串口线接对了。TX 接 RXRX 接 TXGND 接 GND。用示波器或者逻辑分析仪看一下 TX 引脚有没有波形输出。如果没有波形说明单片机根本没发数据。5.3 中断方式发送的注意事项如果你用中断方式发送串口数据printf重定向的时候要注意putchar或fputc里面不要用中断发送直接用查询方式等待 TI 标志位最稳妥。因为printf本身是阻塞的它在等待每个字符发送完成如果你在putchar里又开了中断逻辑会变得很复杂容易出问题。我一般的做法是调试阶段直接用查询方式发送简单可靠。等产品定型了如果需要高效率传输再改成 DMA 或者中断加环形缓冲区的方式。5.4 常见问题速查表现象可能原因解决方法英文正常中文乱码编码不匹配串口助手改为GBK全部乱码波特率错误检查晶振和TH1值无任何输出重定向函数名错误C51用putcharMDK用fputc无任何输出未勾选MicroLIB工程选项里勾选输出断断续续波特率误差大换11.0592MHz晶振中文显示问号串口助手不支持换SSCOM或XCOM复位后第一行乱码上电瞬间电平不稳延时后再初始化串口打印浮点数异常未启用浮点支持检查printf库配置5.5 一个容易被忽略的坑字符串长度限制Keil C51 的printf对字符串长度有一定的限制特别是当你使用sprintf格式化到缓冲区的时候。如果你的中文句子很长可能会超出缓冲区导致溢出。我建议单次printf输出的内容不要超过 80 个字符如果需要输出长文本分成多次调用。另外C51 的printf默认不支持浮点数。如果你要打印温度值25.6直接用%f会输出错误的结果。解决办法是先把浮点数转成整数和小数两部分分别用%d打印或者使用sprintf手动格式化。float temp 25.6; int temp_int (int)temp; int temp_dec (int)((temp - temp_int) * 10); printf(温度%d.%d℃\r\n, temp_int, temp_dec);这样就能在不支持浮点打印的环境下正确输出小数了。6. 进阶技巧让串口调试更顺手6.1 封装一个带标签的调试打印宏在实际项目里直接写printf会让代码里到处都是格式化字符串不好管理。我习惯封装一个宏#define DEBUG_PRINT(fmt, ...) printf([DEBUG] fmt \r\n, ##__VA_ARGS__) #define INFO_PRINT(fmt, ...) printf([INFO] fmt \r\n, ##__VA_ARGS__) #define ERROR_PRINT(fmt, ...) printf([ERROR] fmt \r\n, ##__VA_ARGS__)这样打印出来的信息自带标签和换行串口助手里看起来一目了然。而且以后要关闭调试输出只需要把宏定义改成空就行不用一行一行删printf。6.2 用串口助手做简易数据监控除了看文字串口助手还可以用来监控数据变化。比如你可以在主循环里每隔一秒打印一次传感器数值然后在串口助手里观察数据的变化趋势。有些串口助手支持数据绘图功能可以把接收到的数值直接画成曲线对于调试温度、电压之类的模拟量非常直观。6.3 中文打印在 LCD 上的延伸串口中文打印搞明白之后你会发现同样的编码逻辑也适用于 LCD 显示。LCD 显示中文需要字模数据而字模的编码方式GBK 或 UTF-8和串口打印是同一套逻辑。你在 Keil 里怎么处理中文字符串在 LCD 上就怎么处理。所以把这个环节搞透后面做带中文界面的项目会轻松很多。6.4 关于编码转换的实用建议如果你的项目需要同时支持 GBK 和 UTF-8比如上位机用 UTF-8 而下位机用 GBK那就需要在发送前做编码转换。简单的做法是在 PC 端做转换单片机只管发 GBK。如果非要在单片机里转换就需要引入编码转换表会占用不少 Flash 空间。对于资源紧张的 51 单片机不建议这么做。我个人的经验是统一用 GBK。Keil 在中文 Windows 下默认就是 GBK串口助手也支持 GBK两边不用做任何转换最省事。除非你的项目有明确的 UTF-8 需求否则不要给自己找麻烦。6.5 调试完成后的代码清理产品定型之前记得把调试用的printf清理掉或者用宏开关关掉。串口打印会占用 CPU 时间特别是在高频率调用的时候会影响实时性。而且留着调试代码也有暴露内部信息的风险。我一般会在工程里定义一个DEBUG_ENABLE宏发布版本把这个宏关掉所有调试打印自动消失。#ifdef DEBUG_ENABLE #define DEBUG_PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__) #else #define DEBUG_PRINT(fmt, ...) #endif这样在开发阶段打开DEBUG_ENABLE发布的时候注释掉这一行定义所有调试输出就都没了代码也不用改。串口打印中文这件事说到底就是三个对齐重定向函数对齐、波特率对齐、编码格式对齐。三个都对了中文自然就出来了。我见过太多人卡在其中一个环节上反复检查代码却忽略了串口助手的编码设置。希望这篇内容能帮你少走点弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。