STM32中printf的重定向详解
STM32中printf
的重定向详解
在STM32裸机开发中,可以将C标准库中的printf
(默认情况下,在嵌入式工程中printf
底层是空实现,当启用了半主机模式后,printf
的输出可以被转发到调试器在PC上的终端窗口显示。)重定向到串口以实现调试信息的打印输出。由于不同库的实现方式不同,导致需要重写的函数各异。
ARMCC (Keil MDK) 环境下的重定向
对于ARM的 MicroLib
(比如Keil MDK
的默认库)、及部分嵌入式C库。 其printf
是直接调用fputc
方法逐字符输出。因此,要在此类嵌入式C库的环境下重定向printf
就需要重写fputc
这个方法。
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
成功运行:
#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
首先,需要告诉编译器不要使用半主机模式,在这之后编译器便会寻找用户自定义的替代函数。
struct __FILE
{int handle;};
FILE __stdout;
结构体 __FILE
和全局变量 FILE __stdout;
是标准C库中文件流的基础。这里我们只定义了一个空壳,这是为了防止 printf
等函数链接出错。
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)
:该方法是处理与命令行相关的操作的,嵌入式环境下用不上,给予无效实现即可。
最终代码如下:
#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)
#endifstruct __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方法来进行串口重定向。
#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
链接器标志。
核心思想其实都是将标准库的底层输出函数映射到硬件串口驱动上。