重写 _write 能让 printf 打印到串口,因其是 newlib 中 printf 的底层输出入口;重实现时需判断 fd==1/2、逐字调用 UART 发送、返回实际字节数,避免递归调用或阻塞。

为什么重写 _write 就能让 printf 打印到串口
printf 在裸机或半裸机嵌入式环境(如 ARM Cortex-M + CMSIS、无完整 libc)中,底层依赖 newlib 的 _write 系统调用。它不是直接操作硬件,而是把格式化后的字符串交给 _write(int fd, char *ptr, int len)。只要重实现这个函数,把 ptr 中的 len 个字节逐字发给 UART,printf 自然就“转向”串口了。
注意:这仅适用于使用 newlib(尤其是 newlib-nano)的编译配置;若用的是 picolibc 或自定义 libc,接口可能叫 write 或需额外 hook __io_putchar。
怎么写一个安全可用的 _write 实现
核心是避免阻塞主逻辑、适配你的 UART 驱动,并处理好标准输出(fd == 1)和标准错误(fd == 2)——通常都映射到同一串口。
- 必须检查
fd:只处理fd == 1或fd == 2,其他 fd 直接返回 -1(表示不支持) - 不能在
_write里调用printf或任何依赖_write的函数,否则递归崩溃 - 推荐用轮询发送(最简单),但若 UART 支持中断+TX complete 标志,可加超时等待,避免死等
- 返回值必须是实际写入字节数(
len),不能返回 0 或负数(除非出错)
示例(基于 STM32 HAL):
立即学习“C++免费学习笔记(深入)”;
int _write(int fd, char *ptr, int len) {
if ((fd != 1) && (fd != 2)) return -1;
for (int i = 0; i < len; i++) {
while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX) {
// 可选:加超时防止卡死
}
HAL_UART_Transmit(&huart1, (uint8_t*)&ptr[i], 1, HAL_MAX_DELAY);
}
return len;
}
常见报错和踩坑点
链接失败、printf 无输出、程序卡死——八成出在这几处:
-
_write声明没加extern "C":C++ 编译器会 name mangling,导致链接时找不到符号 → 必须包在extern "C" { ... }里 - 没关 newlib 的 syscall stub 检查:在
arm-none-eabi-gcc链接时要加-u _printf_float(如果用了浮点)和--specs=nano.specs(启用 newlib-nano) - UART 初始化太晚:
_write可能在main()之前就被__libc_init_array调用(比如全局对象构造里用了printf)→ 确保 UART 在main前已初始化,或禁用全局构造中的日志 - 缓冲区未刷新:newlib 默认行缓冲,遇到
\n才触发_write;想立即看到输出,得调用fflush(stdout)或用setvbuf(stdout, NULL, _IONBF, 0)关闭缓冲
比 _write 更轻量的替代方案
如果只是调试打印,不想动 libc 层,可以直接重定义 __io_putchar(Keil/ARMCC)或 __putchar(GCC + newlib-nano)。很多 printf 实现会优先查这个弱符号:
int __io_putchar(int ch) {
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
这个函数更窄、更可控,但只对单字符有效;含格式化的 printf 仍需走 _write。两者可以共存,__io_putchar 供简易 putchar 用,_write 供 printf 底层调用。
真正麻烦的从来不是写几行发送代码,而是确认你的 toolchain 用的是哪个 libc、哪个 syscall 接口、是否启用了 nano 版本、以及 printf 调用链在哪一层被截断——这些细节不摸清,光抄代码只会让串口永远沉默。


















