共计 1509 个字符,预计需要花费 4 分钟才能阅读完成。
在嵌入式开发中,printf 函数是我们调试和输出信息的重要工具。但在 C51 这样的资源受限环境中,标准 printf 往往会带来不小的性能负担。今天就来分享一下我在实际项目中优化 printf 函数的经验。

背景痛点
标准 printf 在 C51 上使用时,常常会遇到两个主要问题:
- ROM 占用过大:完整版的 printf 可能会占用 2KB 以上的代码空间
- RAM 消耗严重:特别是使用浮点数格式时,会消耗大量堆栈空间
通过查看 Keil 生成的 map 文件,可以看到典型的占用情况:
MODULE INFORMATION: STATIC OVERLAYABLE
CODE size = 2356
XDATA size = 256
技术方案对比
针对这个问题,常见的解决方案有三种:
- 完整库链接
- 优点:功能完整,使用方便
-
缺点:占用资源多,不适合小内存设备
-
putchar 重定向
- 优点:节省空间,实现简单
-
缺点:功能受限,需要手动实现部分功能
-
sprintf 定制
- 优点:灵活性高,可精确控制输出
- 缺点:需要更多开发工作量
核心实现
putchar 重定向实现
最有效的优化方式之一就是重定向 putchar 函数。下面是具体实现步骤:
- 首先在代码中定义 putchar 函数:
char putchar(char c) {while (!TI); // 等待发送完成
TI = 0; // 清除发送标志
SBUF = c; // 写入发送缓冲区
return c;
}
- 初始化串口(以 9600 波特率为例):
void UART_Init() {
SCON = 0x50; // 模式 1,允许接收
TMOD |= 0x20; // 定时器 1 工作在模式 2
TH1 = 0xFD; // 9600 波特率 @11.0592MHz
TR1 = 1; // 启动定时器 1
TI = 1; // 初始置位 TI,使能第一次发送
}
精简版 printf 实现
如果还需要更进一步的优化,可以实现一个精简版的 printf:
void simple_printf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
while (*fmt) {if (*fmt == '%') {
fmt++;
switch (*fmt) {
case 'd': {int val = va_arg(ap, int);
// 简化版数字输出实现
break;
}
case 'x': {unsigned val = va_arg(ap, unsigned);
// 简化版十六进制输出
break;
}
default:
putchar(*fmt);
}
} else {putchar(*fmt);
}
fmt++;
}
va_end(ap);
}
性能验证
优化前后的对比数据:
| 方案 | CODE 大小 | XDATA 使用 |
|---|---|---|
| 标准 printf | 2356 字节 | 256 字节 |
| putchar 重定向 | 112 字节 | 32 字节 |
| 精简版 printf | 348 字节 | 32 字节 |
在 115200 波特率下,优化后的方案可以达到约 11KB/ s 的输出速度,完全满足大多数调试需求。
避坑指南
在实际使用中,有几个常见的坑需要注意:
- 多任务环境下的缓冲区共享
- 建议使用互斥锁保护共享资源
-
或者为每个任务分配独立的输出缓冲区
-
浮点数输出的堆栈溢出
- 尽量避免在资源受限的系统使用浮点输出
-
如果必须使用,确保有足够的堆栈空间
-
中断安全
- 在中断服务程序中调用 printf 要特别小心
- 建议使用队列缓冲机制
延伸思考
这套方案不仅适用于 UART 输出,稍作修改就可以适配其他显示设备,比如 OLED 屏幕。只需要修改 putchar 的实现,将字符输出到显示缓冲区即可。
进一步优化的方向:
- 使用宏替换可变参数,进一步减少调用开销
- 实现更高效的整数转字符串算法
- 添加环形缓冲区减少等待时间
通过以上优化,我们可以在不牺牲太多功能的前提下,显著降低 printf 的资源占用。这些技巧在资源受限的嵌入式系统中特别有用,希望能对大家的项目开发有所帮助。
正文完
