想在嵌入式里用 printf,又不想拉来一大堆 libc,堆栈还狂飙?今天给大家安利一个神奇的小家伙——nanoprintf,全篇走口语化,避开那种教科书腔,保证听起来像隔壁老王在给你唠嗑。
什么是 nanoprintf?
简单说,它就是一个超轻量级的 snprintf/vsnprintf 实现,纯 C99 单头文件,无 malloc,用不到 100 字节栈空间。编译出来也就 580–2800 字节(针对 Cortex-M0,取决于你开不开浮点、二进制等功能)。它目的是纠结在“能跑”的最小化,而不是条理清晰的卷福式代码。
它为啥牛?痛点全击破
-
• 嵌入式项目里,
<stdio.h>常常带一堆 syscalls 或者文件描述符,根本用不上; -
• 想把日志直接发串口、发总线,自己绑个 callback 就行,零 syscall;
-
• 想要更小?禁掉浮点、二进制、写回(
%n)、大长度修饰符,编译体积还会再缩水; -
• 兼容性超好,Clang/GCC/MSVC + UBsan/ASan 都过,32/64 位都打过包;
-
• 最核心的格式化逻辑都在一个
npf_vpprintf里,虽然看着像大杂烩,但确实省空间。
核心 API 速览
|
函数 |
用法 & 作用 |
|
npf_snprintf(buf,len,fmt,…) |
跟标准 |
|
npf_vsnprintf(buf,len,fmt,va) |
支持 |
|
npf_pprintf(putc,ctx,fmt,…) |
每打个字符就回调 |
|
npf_vpprintf(putc,ctx,fmt,va) |
带 |
简单上手:代码演示
看图不如写码,咱直接上例子。
// 在某个 .c 里先开实现
#define NANOPRINTF_IMPLEMENTATION
// 以下都可按需开启/关闭
// #define NANOPRINTF_USE_FLOAT_FORMAT_SPECIFIERS 0
// #define NANOPRINTF_USE_BINARY_FORMAT_SPECIFIERS 1
#include "path/to/nanoprintf.h"
// 串口打印
void uart_putc(char c, void* ctx){
// 你自己的串口发送函数
my_uart_send((uint8_t)c);
}
void log_to_uart(void){
npf_pprintf(uart_putc,NULL,
"温度: %d°C, 电压: %.2fV\n",
read_temp(), read_voltage());
}
// 缓冲区写入
void build_msg(char* buf, size_t len){
int used = npf_snprintf(buf, len,
"ID:%04d 状态:%s", device_id(), status_str());
// 注意返回值 used 是“如果够大能写多少”,不含 '\0'
}
// 解析格式串(进阶)
npf_parse_fmt("Hello %08x",---); // 仅解析不输出
优缺点一览表
|
优点 |
缺点 |
|
单文件头,Drop-in 即用 |
源码可读性差,全塞在 |
|
零堆分配、100B 栈,Cortex-M0 下仅几百字节 |
不支持 |
|
支持按需编译,浮点、二进制、写回等功能可选 |
宽字符 |
|
Clang/GCC/MSVC + UBsan/ASan 无警告 |
配置宏一旦显式指定,就所有宏都得显式指定 |
|
并发安全(只要你 callback 做好保护) |
没有自带 |
适用场景 & 小结
-
• Bootloader、固件里,空间极度紧张,却想保留基本
printf能力; -
• 串口、I2C、CAN 总线等无文件概念,只要回调每字节发出去;
-
• 想 DIY 你的轻量级日志库、RPC 框架,或者只要偶尔往缓冲区写格式化字符串;
-
• 如果你不需要科幻的科学计数、区域化、wchar,那真没啥理由不用。
总的来说,nanoprintf 就是那种“能死磕到最小,别奢求漂亮、模块化”的实用主义作品。要是你也是追求极简、上游戏搞固件,就赶紧把它丢进项目里,用 #define NANOPRINTF_IMPLEMENTATION 一键带走。项目地址在这儿求 Star!
项目地址:https://github.com/charlesnicholson/nanoprintf