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 链接器标志。

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

相关推荐
earthzhang20213 小时前
【1007】计算(a+b)×c的值
c语言·开发语言·数据结构·算法·青少年编程
杨枝甘露小码3 小时前
Python学习之基础篇
开发语言·python
清风6666663 小时前
基于单片机的元胞自动机仿真系统设计
单片机·嵌入式硬件·毕业设计·课程设计
千千道4 小时前
利用keil +RASC给瑞萨RA8D1编译烧写程序
单片机·嵌入式硬件·mcu·物联网
武文斌774 小时前
项目学习总结:LVGL图形参数动态变化、开发板的GDB调试、sqlite3移植、MQTT协议、心跳包
linux·开发语言·网络·arm开发·数据库·嵌入式硬件·学习
爱吃喵的鲤鱼4 小时前
仿mudou——Connection模块(连接管理)
linux·运维·服务器·开发语言·网络·c++
充哥单片机设计4 小时前
【STM32项目开源】基于STM32的智能家居安防系统
stm32·嵌入式硬件·智能家居
爱吃小胖橘4 小时前
Unity网络开发--超文本传输协议Http(1)
开发语言·网络·网络协议·http·c#·游戏引擎
郝学胜-神的一滴4 小时前
使用Linux的read和write系统函数操作文件
linux·服务器·开发语言·数据库·c++·程序人生·软件工程