简介一套基于瑞萨 e2studio 开发环境的完整工程代码面向使用瑞萨 RA 系列单片机的嵌入式开发者重点讲解 printf 与 sprintf 打印函数的重定向与格式化输出实现支持直接导入 e2studio 进行编译、烧录与调试。资源包共 169 个文件包括大量 C 源文件与头文件、e2studio 工程配置文件cproject、pincfg、launch、链接脚本 ld、makefile 工程构建脚本以及 J-Link 调试所需的 elf、srec、jlink 等文件同时保留编译中间产物 o 和 d便于查看依赖关系与目标文件信息压缩包整体大小约 1.3MB体量轻巧适合快速获取与部署。工程内附有 r_sci_uart.c 等串口底层驱动源码清晰展示 UART 初始化、数据发送、printf 重定向等关键实现结合 sprintf 的格式化处理范例能够帮助读者深入理解瑞萨 RA 芯片上的打印函数工作机制。已有 1146 人学习下载适合初学者对照配套博客与视频教程实践也可作为实际项目中串口调试功能的基础模板减少重复开发工作。1. 为什么瑞萨 RA 单片机的 printf 不能直接当串口用在 x86 上调用 printf结果出现在控制台窗口在瑞萨 RA 这种 MCU 上没有控制台printf 并不知道该把字符送往哪个引脚。第一次在 e2studio 里调用 printf 如果没有任何输出不用怀疑驱动没创建多半是没有把标准输出重定向到已初始化的 SCI 串口。RA 的 UART 外设由 FSP 生成 r_sci_uart.c 来管理真正把字节发出去的是 R_SCI_UART_Write而不是 printf 本身。因此 printf、sprintf 能否工作取决于你如何让标准库调用底层 UART 接口。下文从 FSP 的 UART 配置开始给出 e2studio 下 printf、sprintf 的完整落地路径同时覆盖%f浮点打印、中文乱码以及 GCC 工具链特有的重定向方式。2. 先打通 R_SCI_UARTprintf 的地基在驱动而不是 stdioprintf 作为 C 标准库函数不感知波特率、引脚和时钟它最终要调用底层驱动才能把字节送出。e2studio 上这个底层就是 FSP 配置生成的 r_sci_uart.c 驱动。首先要保证 UART 实例可用下面是配置到初始化全流程。2.1 FSP 里配置 UART 外设和引脚在 e2studio 工程中双击 configuration.xml进入 Stacks 页面点击 Add - New Stack - Connectivity - UART(r_sci_uart)。生成实例名默认是 g_uart0控制块名为 g_uart0_ctrl配置块名为 g_uart0_cfg。这里用的是 e2studio 自带的 FSP 配置视图如果你单独用 RASC 生成工程参数和寄存器设置完全一致。随后在 Properties 中设置串口参数常用配置如下。参数典型值说明Module Nameg_uart0FSP 生成的驱动实例名Channel0选择 SCI0和引脚约束对应Baud Rate115200与串口工具保持一致Data Bits8标准异步串口数据位ParityNone无校验和大多数模块默认一致Stop Bits1停止位Interrupt Callbackuart_callback收/发完成回调可先留空TX PinP200根据硬件原理图分配RX PinP201同上这里最容易踩坑的是引脚分配RA 的 SCI 外设引脚是固定复用组比如 SCI0 只能用 P200/P201 或 P109/P110。如果板子上串口接的是 P109你还要把 P109 配置成 TX 并打开 P110 为 RX。配置好后点 Generate Project ContentFSP 会重新生成 r_sci_uart.c、r_sci_uart.h、bsp_clocks.c、system.c 等文件。标题中列出的那一批文件就是生成后被工程引用到的核心文件。2.2 确认驱动已 Open并理解三个关键 APIFSP 生成的代码通常会在 R_BSP_WarmStart 或 system_init 阶段调用 R_SCI_UART_Open因此 main 里可以直接写。但如果你换了 UART 实例或者配置过手动初始化必须在第一次打印前执行fsp_err_t err R_SCI_UART_Open(g_uart0_ctrl, g_uart0_cfg); if (err ! FSP_SUCCESS) { __BKPT(0); }这段代码的作用是让驱动进入 ready 状态绑好中断、时钟和引脚。err 返回值如果不等于 FSP_SUCCESS后续调用 Write 会返回 FSP_ERR_NOT_OPEN。此时再查 printf 输出毫无意义问题出在初始化。接着是三个和打印强相关的 APIR_SCI_UART_Write(g_uart0_ctrl, p_src, bytes)向串口发送 bytes 个字节p_src 必须是有效内存指针。R_SCI_UART_GetStatus(g_uart0_ctrl, NULL)查询当前驱动是否忙。返回 FSP_ERR_IN_USE 时说明上一帧还没有发送完。R_SCI_UART_Read(g_uart0_ctrl, dst, bytes)读取接收到的字节可以配合 fgetc 实现 scanf 或者简单的命令行解析。发送前需要保证上一次发送已经停止因为 SCI 驱动的状态机是同一时间只处理一帧发送。常见的轮询代码while (R_SCI_UART_GetStatus(g_uart0_ctrl, NULL) FSP_ERR_IN_USE) { }提示R_SCI_UART_GetStatus 返回 FSP_ERR_IN_USE 并不代表硬件故障它只是表示上一帧发送还在进行。这个 while 循环会阻塞等待直到移位寄存器把最后一位发完。2.3 初始化失败的定位方法如果配置完仍不稳定先检查 FSP 的 Pins 页面是否把同一引脚配成其他外设例如误把 P200 同时配置成 ADCFSP 会报冲突。然后检查 R_SCI_UART_Open 返回的错误码常见的是 FSP_ERR_INVALID_ARGUMENT 或 FSP_ERR_ALREADY_OPEN。前者通常来自引脚配置错误后者来自你手动重复初始化。再不行就把中断回调打开在回调里判断 uart_event 是否等于 UART_EVENT_TRANSFER_ABORTED 或 UART_EVENT_ERR_OVERRUN。这一步能区分是配置问题还是数据链路问题。3. 把 printf 和 sprintf 接到串口上UART 驱动准备好后需要让标准库的输出函数走进 R_SCI_UART_Write。e2studio 默认工具链是 arm-none-eabi-gcc newlib和 Keil 的 ARM Compiler 不同它的底层出口是 _write 而不是 fputc。先讲 _write再讲兼容层。3.1 newlib 下重定向 _writeGCC 的 printf 函数内部是调用 _write(fd, buf, size) 来输出字符fd 为 1 表示 stdout2 表示 stderr。实现方式如下#include stdio.h #include stdint.h #include hal_data.h int _write(int fd, char *pBuffer, int size) { if (fd 1 || fd 2) { R_SCI_UART_Write(g_uart0_ctrl, (uint8_t *)pBuffer, (uint32_t)size); while (R_SCI_UART_GetStatus(g_uart0_ctrl, NULL) FSP_ERR_IN_USE) { } return size; } return -1; }这段代码的核心是让标准库认为系统已支持标准输出。fd 非 1/2 的调用直接返回 -1避免误导其他文件操作的调用。R_SCI_UART_Write 是异步提交所以发送后要等到 GetStatus 不再返回 FSP_ERR_IN_USE否则连续多次 printf 时前一个缓冲区可能还没发完驱动就接收了新请求。这里的 size 是以字节为单位的UTF-8 中文一个汉字占 3 字节这不影响 _write 逻辑。把这段代码放到任何 .c 文件中即可。需要注意工程里只能出现一次 _write 定义如果 FSP 或者第三方库已经实现过链接时会报 multiple definition此时直接删掉库里的那个实现。3.2 兼容 fputc 的写法从 IAR 或 Keil 工程迁移过来的代码通常会看到 fputc 重定向。在 GCC 链接器没有特别指定的情况下fputc 不会自动被 printf 调用但为了兼容老代码和中间件建议保留一个 fputc 转发层int fputc(int ch, FILE *f) { char c (char)ch; return (_write(1, c, 1) 1) ? ch : EOF; }fputc 实际上是一个字符一个字符地调用 _write每次都要等待一帧发送完成速度比直接 _write 慢很多所以项目里如果大量打印优先让数据路径走 _write。这个函数更多是为了满足 printf 和 puts 之外的中间件符号需求。3.3 sprintf 不需要重定向但不能裸奔sprintf 和 printf 不同它只把格式化结果写到 char 数组中不涉及 UART。真正发送时还要你自己调用 Write。典型代码char log_buf[64]; uint16_t adc_val 1234; sprintf(log_buf, adc:%04d\r\n, adc_val); R_SCI_UART_Write(g_uart0_ctrl, (uint8_t *)log_buf, strlen(log_buf));这里 %04d 会按四位左补零输出比如 34 会打印成 0034\r\n 是为了让串口终端正确换行。strlen 返回的是不含结尾 \0 的字节数因此 Write 不会多送那个空字节。如果不想多扫一遍字符串可以用 sprintf 的返回值int len sprintf(log_buf, adc:%04d\r\n, adc_val); R_SCI_UART_Write(g_uart0_ctrl, (uint8_t *)log_buf, len);当有多个字符串拼接时优先用 snprintf 限制最大长度防止缓冲区越界。下表是几个输出函数的对比函数输出目标重定向要求典型场景printfstdout - _write - UART需要 _write短调试信息直接发送putsstdout - _write - UART需要 _write固定字符串自动加换行sprintfchar[]不依赖串口拼日志/协议报文snprintfchar[]不依赖串口带长度限制的拼接vsnprintfchar[]不依赖串口封装自定义可变参数打印sprintf 的缓冲区长度必须足够容纳格式化结果。比如一个 uint32_t 最大值 4294967295 占 10 位加上前缀和换行就至少 15 字节很多初学者只分配 8 字节结果写穿栈区。遇到这个现象时先检查每个 sprintf 的目标缓冲区。4. 编译告警、中文乱码与隐藏的链接配置printf 在串口上能出字只是第一步实际项目中常碰到三类问题隐式声明告警、中文乱码、浮点格式不生效。这三个问题都不在打印函数本身而在编译环境和链接配置。4.1 隐式声明告警的成因Keil 工程移植到 e2studio 时经常出现这条 warning: #223-D: function printf declared implicitly。这是 ARM Compiler 提示 printf 没有原型。原因是 printf 属于标准库函数使用时必须包含 stdio.h。没有原型时编译器按 C89 规则默认 int printf()导致参数类型检查失效对于 printf 这种可变参数函数可能会发生栈上参数错位。在 e2studio 的 GCC 下对应提示是 implicit declaration of function printf。解决办法只有一个在任何使用 printf 的 .c 文件头部加入#include stdio.h如果你使用的是 C要包含 或者在 extern C 中声明。此外fputc 和 _write 这两个符号涉及 FILE、EOF 和 va_list 类型不包含 stdio.h、stdarg.h 会引发一连串奇怪的告警排错时先确认包含关系。4.2 中文乱码是编码链不一致不是波特率问题printf(温度:%d\r\n, temp); 在 e2studio 编译后输出到串口工具出现乱码这里 95% 是编码问题。源码文件按 UTF-8 保存时字符串字面量在 Flash 里就是 UTF-8 字节序。如果你的串口终端使用 GBK 解码中文字符就全部错位。反过来也一样源码为 GBK、终端为 UTF-8也会乱码。第一检查 e2studio 右下角的文件编码应该是 UTF-8。第二检查编译器执行字符集GCC 默认会用 UTF-8 解析字符常量。如果你确实需要 GBK在工程属性中添加编译参数-fexec-charsetGBK这样字符串字面量最终以 GBK 存储在 Flash 中配合使用 GBK 的串口助手就能正常显示。更推荐的做法是把串口工具统一设置为 UTF-8这样无需改编译器参数。第三检查串口工具例如 SSCOM 的字符集选择或者 XShell 的 Encoding。还要注意UTF-8 中文是变长的printf 输出的是字节流_write 不会也无需区分中英文。使用 sprintf 进行中文拼接时缓冲区大小最好用 sizeof 而不是肉眼估个数比如char msg[32]然后写成snprintf(msg, sizeof(msg), 节点%d上线, id);。4.3 浮点打印需要显式开启 _printf_floatnewlib-nano 为了减小代码体积默认不实现 printf 的浮点格式化。你写 printf(%f\r\n, 3.14) 时结果可能是一个空字符串也可能原样输出 %f。这不是瑞萨驱动问题而是链接器没有拉入浮点格式化函数。在 e2studio 中依次打开项目属性 - C/C Build - Settings - GNU ARM Cross C Linker - Miscellaneous在 Linker flags 中添加-u _printf_floatGCC 看到 -u 后会把符号 _printf_float 强制拉入链接进而引入浮点格式支持。如果还需要 scanf 读取浮点就增加-u _scanf_float添加后编译出的固件会变大通常增加几千字节到十几 KB栈也可能增加约 1 KB 到 2 KB。如果程序里只用 sprintf 不用 printf也要加 _printf_float因为格式化引擎是共享的。4.4 浮点打印触发 HardFault 时怎么查启用浮点打印后如果稍复杂的格式化场景直接 HardFault优先怀疑栈空间不足。newlib 的格式化函数在构造输出字符串时会消耗不少栈尤其是长字符串加浮点数。FSP 工程默认栈大小通常在 startup.c 中定义也可以链接脚本里通过 __STACK_SIZE 指定。可以在链接器参数中加入-Wl,--defsym__STACK_SIZE0x2000把栈从默认的 0x400 或 0x1000 加大到 0x2000再验证 HardFault 是否消失。另一个隐藏点是用 sprintf 到局部 char buf[128]这个数组本身就占 128 字节栈多个嵌套函数调用后会放大栈深度。建议把大型缓冲区改为 static或者直接封装成一个专用的打印函数见下一章。5. 用 vsnprintf 封装一个不会越界的最小打印接口经过前面的配置printf 和 sprintf 都能用了。但项目进入业务逻辑后直接裸用 sprintf 的越界风险和大块局部数组并不适合所有场景。更稳的做法是把可变参数封装成带长度限制的 debug_printf让调用方无法轻易写穿缓冲区。5.1 debug_printf 的完整实现#include stdarg.h #include stdio.h #include stdint.h #include hal_data.h void debug_printf(const char *fmt, ...) { char buf[128]; va_list args; int len; va_start(args, fmt); len vsnprintf(buf, sizeof(buf), fmt, args); va_end(args); if (len 0) { return; } if (len (int)sizeof(buf)) { len (int)sizeof(buf) - 1; } R_SCI_UART_Write(g_uart0_ctrl, (uint8_t *)buf, (uint32_t)len); while (R_SCI_UART_GetStatus(g_uart0_ctrl, NULL) FSP_ERR_IN_USE) { } }vsnprintf 的作用是先把格式化结果写入 buf并且最多写入 sizeof(buf) 字节。它返回的是格式化后完整字符串的长度而不是实际写入长度所以返回值为负表示编码错误必须退出。当 len 大于等于缓冲区大小时说明输出被截断发送部分只发前 sizeof(buf)-1 个字节保证不会把未写入的内存发出去。调用方式与 printf 一致debug_printf(ra6m5 adc%d temp%.2f\r\n, adc_raw, temperature);这是可变参数在嵌入式里的一个常见落点用底层标准库函数组合出一个自己的打印入口。5.2 在中断上下文调用的边界条件debug_printf 内部是同步等待 UART 发送完成。如果放在定时器中断或者 SCI 接收回调里调用可能和主循环中的打印互相等待或者长时间阻塞中断导致实时性下降。此时不要在中断里做格式化只把一个标志置位主循环检测后统一打印volatile uint8_t g_print_flag 0; void sci_callback(uart_callback_args_t *p_args) { if (p_args-event UART_EVENT_RX_CHAR) { g_print_flag 1; } }主循环中检查标志后调用 debug_printf这是结构化调试最常用的做法。如果一定要在中断里输出需要把 buf 改为 static 或全局数组并且舍弃等待游标直接用 R_SCI_UART_Write 提交防止本次中断返回时缓冲区内容还留在栈上已被覆盖。前者保证数据完整性后者保证时序你只需要按使用场景二选一。实际使用时还可以把 debug_printf 的缓冲区大小抽成宏或者将 buf 换成链接脚本里的一块专用于调试的数组这样在串口 DMA 模式下可以直接让外设访问静态地址进一步降低 CPU 占用。本文还有配套的精品资源点击获取