STM32中printf的重定向详解

STM32中printf的重定向详解

​ 在STM32裸机开发中,可以将C标准库中的printf(默认情况下,在嵌入式工程中printf底层是空实现,当启用了半主机模式后,printf的输出可以被转发到调试器在PC上的终端窗口显示。)重定向到串口以实现调试信息的打印输出。由于不同库的实现方式不同,导致需要重写的函数各异。

ARMCC (Keil MDK) 环境下的重定向

​ 对于ARM的 MicroLib(比如Keil MDK的默认库)、及部分嵌入式C库。 其printf是直接调用fputc方法逐字符输出。因此,要在此类嵌入式C库的环境下重定向printf就需要重写fputc这个方法。

c 复制代码
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100); // STM32 HAL示例
	return ch;
}

​ 以上是该类printf重定向的核心片段,当在代码中调用printf()时,其底层便会调用fputc,进而利用串口进行信息输出。当然,这么做是不够的,还需要在keil中启用Microlib.

​ 如图所示,需要勾选Use MicroLIB选项。因为使用MicroLIB可避免标准C库的半主机依赖,使 printf 能在无调试器环境下独立运行。

它提供轻量级I/O接口,只需重写 fputc 就可以方便地将输出重定向至串口。但是勾选微库在某些情况下会影响运行的性能 。在不使用MicroLIB的情况下,我们可作如下修改来使重定向的printf成功运行:

c 复制代码
#if 1
#if (__ARMCC_VERSION >= 6010050)            /* 使用AC6编译器时 */
__asm(".global __use_no_semihosting\n\t");  /* 声明不使用半主机模式 */
__asm(".global __ARM_use_no_argv \n\t");    /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */

#else
/* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
#pragma import(__use_no_semihosting)
#endif

​ 首先,需要告诉编译器不要使用半主机模式,在这之后编译器便会寻找用户自定义的替代函数。

c 复制代码
struct __FILE
{
    int handle;
        
};
FILE __stdout;

​ 结构体 __FILE 和全局变量 FILE __stdout; 是标准C库中文件流的基础。这里我们只定义了一个空壳,这是为了防止 printf 等函数链接出错。

c 复制代码
int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}

当我们定义了__use_no_semihosting(也就是不使用半主机模式)后,就需要自己提供_ttywrch(int ch)_sys_exit(int x)_sys_command_string(char *cmd, int len)方法,否则就会出现链接失败。

  • _ttywrch(int ch):这也是底层的一个发送函数,但是此处我们并不使用他,所以给出一个无效实现。注意:他是ARM半主机模式中的一个函数。当你禁用半主机后,编译器/链接器要求这个符号必须存在。所以必须给予定义。
  • _sys_exit(int x):当程序结束时,这个方法会被调用。但是在嵌入式系统中,我们的主程序是不停止的,因此该方法我们也给予无效实现。
  • _sys_command_string(char *cmd, int len):该方法是处理与命令行相关的操作的,嵌入式环境下用不上,给予无效实现即可。

最终代码如下:

c 复制代码
#if 1
#if (__ARMCC_VERSION >= 6010050)           
__asm(".global __use_no_semihosting\n\t"); 
__asm(".global __ARM_use_no_argv \n\t");    

#else
#pragma import(__use_no_semihosting)
#endif

struct __FILE
{
    int handle;
        
};
FILE __stdout;

int _ttywrch(int ch)
{
    ch = ch;
    return ch;
}

void _sys_exit(int x)
{
    x = x;
}

char *_sys_command_string(char *cmd, int len)
{
    return NULL;
}

int fputc(int ch, FILE *f) {
  (void)f;

    HAL_USART_Transmit(&husart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);

    return ch;
  }
#endif

​ 如上,便是在不使用Keil中MicroLIB的情况下独立运行重定向的printf的方法。

GCC (STM32CubeIDE/ARM GCC) 环境下的重定向

​ 当使用GCC作为编译工具链时(如STM32CubeIDE),其printf底层是调用_write()这个方法来进行输出的。所以需要重写_write方法来进行串口重定向。

c 复制代码
#if defined(__GNUC__)
int _write(int fd, char *ptr, int len)
{
  HAL_USART_Transmit(&husart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
  
	return len;
}
#endif

以上是_write()方法的新实现,将_write方法实现后,只需要包含C标准的输入输出头文件后,便可调用printf利用串口进行打印。

可以发现GCC编译环境下不用在意半主机模式,这是因为GCC的Newlib-Nano库本身设计时就已经考虑了嵌入式环境,通常默认不依赖半主机模式,所以我们一般只需要重写 _write 即可。这与ARM编译器默认依赖半主机模式的行为不同。

当然该重定向并没有完整,此时是无法打印浮点数的,如果要让 printf 输出 %f 浮点数,需要在链接时加上 -u _printf_float

​ 当然凡事并不绝对,GCC环境下不用管半主机的说法也是要分情况的,在现代的工具链和IDE中,大部分情况下确实不需要显式处理。在出现特定的配置时,可能仍需要处理半主机的情况。所以在GCC环境下时,建议先从最简的重写_write()开始,若无法解决再一步步添加相关系统调用实现。

总结

  • Keil MDK:优先使用 MicroLib 并重写 fputc 是最简单的方法。若不能使用
    MicroLib,则需禁用半主机模式并实现一系列桩函数。
  • GCC (CubeIDE等):核心是重写 _write 函数。如需打印浮点数,记得添加 -u _printf_float 链接器标志。

核心思想其实都是将标准库的底层输出函数映射到硬件串口驱动上。

相关推荐
天竺鼠不该去劝架2 小时前
除了技术问题,哪些因素会导致自动化项目彻底失控?
经验分享
三易串口屏4 小时前
实验22 心跳曲线实验
stm32·tft屏·hmi·三易串口屏·嵌入式ui
isyangli_blog6 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008116 小时前
FastAPI APIRouter
开发语言·python
Benszen6 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木6 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充7 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~7 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6167 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang