使用 VSCode 给STM32配置一个串口 printf 工程
gcc 重定向 printf 和 keil 不一样。
文件准备
-
先从以前的工程中拷过一份串口的代码来,然后在 main 函数中初始化串口并 print 一个数据吧。
-
新添加的文件需要添加到 Markfile 文件中,否则编译肯定会报错的。同时为了 vscode 不报错也把 include 路径在 c_cpp_properties.json 中放一份。
.h
文件路径 ->Makefile
+c_cpp_properties.json
.c
文件 ->Makefile
-
之后还需要在 stm32f1xx_hal_conf.h 删除 ...uart.h 和 ...usart.h 的注释,然后把 HAL 库里串口的 .c 文件添加到 Makefile .
-
编译通过,但是下载进去果然不行,串口没出来任何东西。这是因为 gcc 和 Keil 关于 printf() 函数底层的实现不一样。在 Keil 中需要重定向的是 fputc() 函数,但是在 GCC 中不太一样。
重定向_Printf
已知在 GCC 中想要使用 printf() 函数是需要重定向 _write() 函数的。
想要知道在 GCC 中怎么用,最好看看官方怎么说,别管哪个官方说的总会比私人说的靠谱。先试着找一下 ST 固件库的示例工程中有没有用到 printf() 的。很幸运的是官方提供了示例工程,在固件库的
...\STM32Cube_FW_F1_V1.8.0\Projects\STM32F103RB-Nucleo\Examples\UART\UART_Printf
目录中可以拿到它(每一种芯片的目录下应该都有对应的这个例程)。-
打开示例工程的目录,可以看到里面有一个 readme.txt , 把 .txt 给它重命名为 .md 用 vscode 打开我们就可以十分清晰的看到它的介绍信息了。不难发现这个工程其实是让单片机连接超级终端用的。既然要连接超级终端那想必除了要实现 printf() 还要实现 scanf() 和其他的东东吧。不过我们目前只关心 printf(), 剩下的以后再收拾。
-
我们需要用到这个工程里的两个文件,
main.c
和syscalls.c
. main.c 的重要性没什么可说的,而 syscalls 翻译过来是系统调用的意思,因此我觉得所有的底层应该都是在这里实现的。-
首先看他的 main.c 文件,把所有干扰视线的注释删除掉可以发现其中除了各模块初始化等我们熟悉的代码外就多了以下两段内容。
...... /* 在我们的工程中 __GNUC__ 肯定是定义了的,因此这一段其实就只有 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch) 生效了 */
/* Private function prototypes -----------------------------------------------*/
#ifdef __GNUC__
/* With GCC, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */ ...... /* 这里写了 PUTCHAR_PROTOTYPE ,这不就是上面定义的那个宏吗,也就是说这里重写了 int __io_putchar(int ch) 这个函数,但是这跟 printf() 有什么关系呢? */
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
} ...... -
接下来打开 syscalls.c 文件,这个文件在
\Examples\UART\UART_Printf\SW4STM32
目录下(SW4STM32 基于GCC的STM32的编译调试工具链,看到 GCC 就觉得它很可爱)。打开后果然发现这里面重写了 _write() 函数,而且其内容就是上文重写的 __io_putchar(int ch) 那个函数,到这里一切疑问就迎刃而解了。__attribute__((weak)) int _write(int file, char *ptr, int len)
{
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
__io_putchar(*ptr++);
}
return len;
}
-
在搞明白原理之后我们只需用 main.c 中重定向 __io_putchar(int ch) 的部分源码替换掉 usart.c 在 Keil 中重定向 printf() 的那部分代码,然后将 syscalls.c 文件添加到工程并添加到 Makefile 文件中使其编译就可以正常使用 printf() 函数了。这里其实不把 syscalls.c 整个文件拿过来只要重写 _write() 的那部分也可以,但是看在这个文件体积也不大的份上还是把他拿过来吧,万一以后用上也方便。
-
好了,不出意外的话现在我们就可以正常的使用 printf() 了,试验一下吧。
-
关于无法打印浮点数的问题。试了试好像确实没办法打印浮点数,用 sprintf() 也不行。不过问题不大,在 Makefile 文件中找到 LDFLAGS 选项然后在里面添加
-u _printf_float
参数就可以了,添加以后printf() 和 sprintf() 正常使用。 -
Ps.日常写程序时常用的除了 printf() 还有 sprintf() 和 sscanf() 这两个数字和字符串互转的函数,前面说了 sprintf() 已经可以正常用了,那么 sprintf() 呢?其实一样的道理, sprintf() 默认也是不能转浮点数的,但是在 Makefile 里对应的加一句
-u _scanf_float
就万事大吉了。
使用_printf_需要注意的地方
printf() 只有在检测到 '\n' 时才会从缓存区把数据发出去,因此在数据结尾一定要有 '\n', 否则数据是肯定传不出去的。这次滞留的数据有可能会在下次发送带 '\n' 的数据时随它一块过去,当然也有可能被覆盖,因此记得'\n'. 如果真有什么特殊需求不能用 '\n' 的话在发完数据之后就要运行一次
fflush(stdout)
强制刷新一次输出流,这样数据也是能发出去的。编码问题。VSCode 默认使用的编码是 UTF-8 因此如果你的输出有中文的话请找一个支持 UTF-8 的串口助手查看,否则肯定会乱码,实测 Windows 应用商店里的 串口调试助手 可用。虽然 VSCode 也能改成 GB2312 编码,但我劝你还是忘记那个糟糕的东西吧。
刚才又发现 vscode 一个莫名奇妙的问题,他说我的串口句柄(一个变量)没定义,扯淡我明明定义了。后来试了一下把 main.c 中最后一项 include
#include "stdio.h"
移到顶端就没问题了。C/C++这个插件也是莫名其妙,渣渣。
总结
本篇介绍在 GCC 中重定向 printf() 方法,也顺便解答了从之前的 Keil 工程中将文件移植到 GCC 项目使用的问题,总结起来步骤大概分为以下几个:
复制文件。把文件复制到工程目录下你喜欢的地方。
添加 includepath. 这一步需要分别在 Makefile 和 c_cpp_properties.json 文件中添加,已添加的就不用重复添加了。
添加 C_SOURCES . 在 Makefile 中添加你新引入的 C 文件的路径。不添加不一定出错,但添加上好。
-
添加外设的 HAL 库文件。虽然 CubeMX 生成的工程中包含了完整的 HAL 库,但默认这些库文件并没有全部编译,比如我们默认生成的只配置了灯的工程自然就不会去编译串口、ADC等这些不相干的库文件,因此当我们需要使用串口时就需要手动把它包含进来了。
- 首先确定你的工程中的确有串口相关的 HAL 库文件,一般在
\Drivers\STM32F1xx_HAL_Driver\Src
目录下。 - 然后去
stm32f1xx_hal_conf.h
文件中取消掉#define HAL_UART_MODULE_ENABLED
和#define HAL_USART_MODULE_ENABLED
这两个宏的注释。 - 最后在 Makrfile 的 C_SOURCES 中添加上串口的 HAL 库 C 文件。
- 首先确定你的工程中的确有串口相关的 HAL 库文件,一般在
引入声明了初始化串口函数的 .h 文件,然后在 main() 函数中初始化串口并 printf() 一个数据试试。
为了更好的使用 printf() 和 sscanf()、sprintf() 可以在 Makefile 中添加
-u _printf_float
和-u _scanf_float
,这样就可以实现浮点数的转换了。使用 printf() 记得以 '\n' 结尾或使用 fflush(stdout) .