只看如何用VSCODE开发stm32看左侧目录跳过去即可!其余内容是连续课程
公共层------日志输出打印
添加目录 加载头文件
在MDK-ARM文件夹中新建common

新建日志头文件和主类

添加GROUP组并将头文件添加到工程中
点击魔术棒-C/C++-包含路径

将common文件夹放进来,会把文件夹中的h头文件加入

熟悉vscode开发c类型项目的都知道,还需要把文件放进CMakeLists.txt中才能被编译,因此在这里也需要添加编译文件,把common文件添加为类

新建common类,点击Addfiles,把c h都包含进去

注意要把allfiles选上才会展示所有的文件。


如何在vscode开发Keil项目
我们需要现在vscode安装这个

然后配置Uv4Path的路径
按"ctrl+,"打开设置搜索keil

C51这一项不用改,MDK UV4.exe path要修改为自己的UV4地址

如何快速得到UV4的位置呢?
桌面右击Keil uVision5 然后点击打开文件所在位置

选中exe,shift+ctrl+c复制路径,然后粘贴到里面,记得去掉引号

然后下滑到这一项,将值这里改为上面地址去掉UV4/UV4.exe的父地址:
"E:\Project\stm32\keil5core\UV4\UV4.exe"-》"E:\Project\stm32\keil5core"

然后就能打开了

如果点击这个打开uVision项目还是打不开怎么办?
把版本往下调就可以了


打印HELLOWORLD
编辑commom中的debug.h文件:
cpp
#ifndef __COM_DEBUG_H
#define __COM_DEBUG_H
//使用串口重定向视线日志输出打印
#include "usart.h"
#endif /* __COM_DEBUG_H */
在APPLICATION/USER/CORE的main.c中的USER CODE BEGIN END2之间添加日志打印:
cpp
/* USER CODE BEGIN 2 */
//使用串口打印信息
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello World!\r\n", 13, 1000);
/* USER CODE END 2 */
点击编译

显示这个就是OK了:
bash
*** Using Compiler 'V5.06 update 6 (build 750)', folder: 'E:\Project\stm32\keil5core\ARM\ARMCC\Bin'
Build target 'Location_hal'
compiling main.c...
compiling stm32f1xx_it.c...
compiling gpio.c...
compiling usart.c...
compiling stm32f1xx_hal.c...
compiling Com_debug.c...
compiling stm32f1xx_hal_cortex.c...
compiling stm32f1xx_hal_pwr.c...
compiling stm32f1xx_hal_msp.c...
compiling stm32f1xx_hal_dma.c...
compiling stm32f1xx_hal_rcc_ex.c...
compiling stm32f1xx_hal_flash.c...
compiling stm32f1xx_hal_gpio_ex.c...
compiling stm32f1xx_hal_gpio.c...
compiling stm32f1xx_hal_rcc.c...
compiling stm32f1xx_hal_uart.c...
compiling system_stm32f1xx.c...
compiling stm32f1xx_hal_exti.c...
compiling stm32f1xx_hal_flash_ex.c...
linking...
Program Size: Code=4512 RO-data=312 RW-data=16 ZI-data=1096
FromELF: creating hex file...
"Location_hal\Location_hal.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:03
* 终端将被任务重用,按任意键关闭。
接上stm32,再点击烧录

显示这个就是正常:
bash
> E:\Project\stm32\keil5core\UV4\UV4.exe -f e:\stm32\project\Location\Location_hal\MDK-ARM\Location_hal.uvprojx -j0 -t Location_hal -o e:\stm32\project\Location\Location_hal\MDK-ARM\.vscode\uv4.log
Load "Location_hal\\Location_hal.axf"
Erase Done.
Programming Done.
Verify OK.
Application running ...
Flash Load finished at 11:24:03
* 终端将被任务重用,按任意键关闭。
将stm32接入usb-ttl
【STM32 usb-ttl连接使用方法-哔哩哔哩】 https://b23.tv/wtD1g1Y

打开串口助手sscom

按下BOOT重启就有输出了:

封装日志打印函数
在debug.h中先添加printf的依赖------stdio.h
cpp
#ifndef __COM_DEBUG_H
#define __COM_DEBUG_H
//使用串口重定向视线日志输出打印
#include "usart.h"
#include "stdio.h"
#endif /* __COM_DEBUG_H */
在debug.c中做重定向:
printf底层调用fputc函数,对每个字节调用一次fputc函数:
cpp
#include "Com_debug.h"
//编写重定向方法
int fputc(int ch, FILE *f){
//printf底层调用fputc函数,对每个字节调用一次fputc函数
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 1000);
return ch;
}
然后在main.c中include一下,要写在USERCODE之间,否则每次更新会被删掉
cpp
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "Com_debug.h"
/* USER CODE END Includes */
然后修改HELLOWORLD的方法为封装后的方法:
依然在USERCODE2之间
cpp
/* USER CODE BEGIN 2 */
//使用串口打印信息
//HAL_UART_Transmit(&huart1, (uint8_t*)"Hello World!\r\n", 13, 1000);
printf("Hello World!\r\n");
/* USER CODE END 2 */
重新编译烧录即可(可以看出,在高波特率下有一定概率出现乱码)

优化打印函数------同时打印文件名和行号
我们不知道日志来自哪一行,因此希望加上行号和文件名
在debug.h中:
cpp
#ifndef __COM_DEBUG_H
#define __COM_DEBUG_H
//使用串口重定向视线日志输出打印
#include "usart.h"
#include "stdio.h"
#include "string.h"
#include "stdarg.h"
//使用宏定义替换printf函数
#define debug_printf(format, ...) printf("[%s:%d]" format,__FILE__,__LINE__, ##__VA_ARGS__)
#endif /* __COM_DEBUG_H */
首先引入stdarg,这样才能使用系统自定义的FILE LINE的硬编码参数代表文件名和行号
逐部分解析
1. #define debug_printf(format, ...)
- 定义一个名为
debug_printf的宏; format是第一个参数,通常是格式化字符串(如"x = %d\n");...表示"可变参数"------你可以传 0 个、1 个或多个额外参数(就像printf一样)。
2. printf("[%s:%d]" format, __FILE__, __LINE__, ##__VA_ARGS__)
这是宏展开后实际执行的代码:
| 部分 | 含义 |
|---|---|
"[%s:%d]" format |
字符串拼接!C 语言会自动把相邻字符串字面量合并。例如: 如果 format 是 "value=%d\n",那么整体变成: "[%s:%d]value=%d\n" |
__FILE__ |
预定义宏 ,编译时自动替换为当前源文件的路径(如 "main.c" 或 "com_debug.c") |
__LINE__ |
预定义宏,编译时自动替换为当前代码行号(整数) |
##__VA_ARGS__ |
将 ... 中传入的实际参数展开,并传递给 printf (## 是 GCC/Clang/MSVC 支持的扩展,用于处理"零参数"情况) |
🧪 举个例子
假设你在 main.c 第 25 行写了:
1debug_printf("sensor value: %d, status: %s\n", 42, "OK");
经过预处理器展开后,等价于:
1printf("[%s:%d]sensor value: %d, status: %s\n", "main.c", 25, 42, "OK");
运行时输出可能为:
1[main.c:25]sensor value: 42, status: OK
✅ 一眼就知道这条日志是从哪个文件、哪一行打印的!
cpp
/* USER CODE BEGIN 2 */
//使用串口打印信息
//HAL_UART_Transmit(&huart1, (uint8_t*)"Hello World!\r\n", 13, 1000);
printf("Hello World!");
debug_printf("Hello World!");
/* USER CODE END 2 */
嘉立创和淘宝的发票要报销
避免重定义宏是什么?
这里的"避免重定义"并不是指避免 #define __COM_DEBUG_H 这个宏被重复定义 ,而是通过这个宏来防止整个头文件的内容被多次包含 ,从而间接避免头文件内部的其他内容(如函数、变量、类型、其他宏)被重复定义。
具体解释如下:
-
目的 :
当多个
.c文件或嵌套的头文件都#include "com_debug.h"时,预处理器会把该头文件的内容插入多次。如果没有保护,就会导致:- 结构体/类重定义
- 函数重定义
- 变量重定义
- 其他宏重定义(如果头文件里还有
#define MAX_SIZE 100等)
-
机制(Include Guard / 头文件守卫):
- 第一次包含
com_debug.h时,__COM_DEBUG_H未定义 → 进入#ifndef块 → 定义__COM_DEBUG_H→ 编译头文件内容。 - 第二次再包含时,
__COM_DEBUG_H已定义 →#ifndef条件为假 → 跳过整个块(直到#endif)→ 头文件内容不会被再次处理。
- 第一次包含