STM32 可移植教程 01:VSCode 环境搭建 + 点亮 LED(实战篇)

STM32 可移植教程 01:VSCode 环境搭建 + 点亮 LED(实战篇)

很多人学 STM32,第一个门槛不是 C 语言,也不是 GPIO,而是开发环境。

你用 Keil 写过几篇教程之后,应该已经感觉到:Keil 的编辑器确实不太顺手。代码补全基本没有,跳转定义经常失效,界面风格还停留在十几年前。写几十行代码还行,写几百行就开始难受了。

那能不能用 VSCode 来写 STM32?能。

而且不只是写代码------VSCode 可以负责编辑、编译、下载、调试一整套流程。Keil 退回到只管"编译和下载"的角色,现在连这两个角色都可以交给更现代的工具链:

text 复制代码
CubeMX(配引脚、生成 Makefile 工程)
  → VSCode(写代码)
    → arm-none-eabi-gcc(编译)
      → OpenOCD(下载、调试)

这篇教程就做一件事:把上面这条工具链从头到尾跑通,并且让你亲手点亮一个 LED。 环境配置和 GPIO 点灯合并成一篇,因为在 VSCode 工具链里,最好的验证方式就是直接跑一个完整的外设工程。

环境通了,LED 闪了,后面蜂鸣器、按键、串口、定时器全都顺了。

还有一个更重要的原因促使我换掉 Keil:手敲代码。

学 STM32,复制粘贴跑通的代码,和一行一行自己敲出来的代码,学到的东西差很远。敲错、报错、排查、修正------这个循环才是真正在学。但 Keil 的编辑器太简陋了,没有像样的代码补全,跳转定义经常失效,在这种环境下逼自己手敲代码,不是锻炼,是折磨。

VSCode 就不一样。IntelliSense 在你敲 HAL_GPIO_ 的时候自动弹出候选,Ctrl+点击直接跳到函数定义,红色波浪线在你编译之前就告诉你哪里写错了。手敲代码从一个痛苦的过程变成了一件顺手的事------你仍然在逐行思考每一行在干什么,但编辑器在帮你省掉那些没有学习价值的机械动作。

所以本系列教程有一个不同于大多数教程的习惯:代码会完整展示在文章里,但不会提供现成的 .c/.h 文件让你直接下载复制。你要对着文章一行一行敲。VSCode 会让你敲得很顺。

本篇目标

看完并照着做完这一篇,你至少要完成这几件事:

  • 安装 ARM GCC 编译器(arm-none-eabi-gcc);
  • 安装 OpenOCD(用于 ST-Link 下载和调试);
  • 安装 GNU Make(构建工具);
  • 安装 VS Code 和必备扩展;
  • 用 CubeMX 创建一个 Makefile 工程;
  • 学会 VSCode 的三个工程配置文件(tasks.json、launch.json、c_cpp_properties.json);
  • 在 VSCode 里一键编译;
  • 用 OpenOCD 下载程序到开发板;
  • 让一个 LED 每 500 ms 闪烁一次。

本篇的跑通标准很简单:

text 复制代码
VSCode 里按 Ctrl+Shift+B 编译成功
终端里执行 make flash 下载成功
开发板上的 LED 以 500 ms 间隔稳定闪烁

准备工作

你需要准备这些东西:

项目 说明
电脑 Windows 10 / Windows 11
开发板 任意 STM32 开发板,只要板上有 LED 或你外接一个 LED
下载器 ST-LINK/V2 或板载 ST-LINK
USB 线 一定要是数据线,不要只用充电线
软件 下面工具安装部分会逐个下载安装
原理图 最好能找到开发板 LED 那一小块原理图

为了让教程可移植,我不会把步骤死绑在某一块开发板上。

你手上可以是 STM32F103C8T6 最小系统板、STM32F407 开发板、Nucleo 板、或者公司自己的 STM32 板子。本文以 STM32F103ZET6 做演示,但凡是和具体芯片、具体板子有关的地方,我都会提醒你应该改哪里。

先说清楚:这些工具分别干什么

从 Keil 切到 VSCode 之后,原来 Keil 一个软件做的事情,现在分给了四个工具。新手最容易困惑的就是"为什么装这么多东西"。

可以这样理解:

工具 原来 Keil 里对应什么 干什么
CubeMX Keil 里也能配引脚但不好用 图形化配置芯片引脚、时钟、外设,生成初始化代码和 Makefile
arm-none-eabi-gcc ARMCC 编译器(Keil 内置) 把 C 代码编译成 ARM 机器码
OpenOCD Keil 内置的 ST-Link Debugger 通过 ST-Link 把程序下载到芯片,以及调试
GNU Make Keil 的工程管理和构建系统 按照 Makefile 规则调用编译器,管理编译顺序
VS Code Keil 编辑器 写代码、代码补全、跳转定义、集成终端

一句话记住:

CubeMX 负责把工程搭起来,VSCode 负责写代码,ARM GCC 负责编译,OpenOCD 负责下载。

第一步:安装 ARM GNU Toolchain

ARM GNU Toolchain 是 ARM 官方维护的开源编译器套件,核心就是 arm-none-eabi-gcc

下载入口:

text 复制代码
https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads

在页面里找到 Windows 版本,一般文件名类似:

text 复制代码
arm-gnu-toolchain-13.3.rel1-mingw-w64-i686-arm-none-eabi.exe

安装时注意两项:

  1. 安装路径不要带中文、空格和特殊符号,推荐类似 C:\arm-gnu-toolchainD:\Embedded\Tools\arm-gnu-toolchain
  2. 安装到最后一步时,一定要勾选 "Add path to environment variable" 。这样装完后在终端里直接敲 arm-none-eabi-gcc 就能用。

装完之后验证一下。打开终端(Win+R 输入 cmd,或者 PowerShell),输入:

bash 复制代码
arm-none-eabi-gcc --version

如果看到类似下面的输出,说明装好了:

text 复制代码
arm-none-eabi-gcc.exe (Arm GNU Toolchain 15.2.Rel1 (Build arm-15.86)) 15.2.1 20251203

如果提示 'arm-none-eabi-gcc' is not recognized,说明 PATH 没生效。两个可能原因:安装时没勾选"Add to PATH",或者安装后没有重新打开终端。先去检查系统环境变量里有没有 <安装路径>\bin,没有的话手动加上,然后重新打开终端。

第二步:安装 OpenOCD

OpenOCD(Open On-Chip Debugger)是开源调试工具,通过 ST-Link 和芯片通信,负责下载程序和调试。

下载入口(xpack 预编译版,免安装):

text 复制代码
https://github.com/xpack-dev-tools/openocd-xpack/releases

找到最新的 Windows 版本,一般文件名类似:

text 复制代码
xpack-openocd-0.12.0-3-win32-x64.zip

下载后解压到一个固定目录,比如 C:\openocdD:\Embedded\Tools\openocd

然后把 <解压目录>\bin 加入系统 PATH。例如如果你解压到 D:\Embedded\Tools\openocd\xpack-openocd-0.12.0-6-win32-x64\xpack-openocd-0.12.0-6,就要把它的 bin 子目录加到 PATH。

装完之后验证:

bash 复制代码
openocd --version

期望看到类似:

text 复制代码
xPack Open On-Chip Debugger 0.12.0+dev-01850-geb6f2745b-dirty (2025-02-07-10:08)

第三步:安装 GNU Make

Make 是构建工具,负责按照 CubeMX 生成的 Makefile 来调用编译器。

如果你的系统已经有 make(可以试试 make --version),就不用再装。没有的话:

方案一(推荐):用 winget 安装。

bash 复制代码
winget install GnuWin32.Make

方案二:去 https://gnuwin32.sourceforge.net/packages/make.htm 下载安装包。

装完后把 C:\Program Files (x86)\GnuWin32\bin 加入 PATH。

验证:

bash 复制代码
make --version

期望看到类似:

text 复制代码
GNU Make 3.81

给不喜欢折腾 PATH 的读者

如果你不想手动加 PATH,也可以把这三个工具的 bin 目录路径记下来,后续在 VSCode 的 tasks.json 里用绝对路径。但长期来看,加到 PATH 里是最省事的。

第四步:安装 VS Code 和必备扩展

VS Code 下载入口:

text 复制代码
https://code.visualstudio.com/

安装时记得勾选 "Add to PATH" ,这样在终端里可以直接敲 code 打开 VSCode。

装完后打开 VSCode,点左侧的扩展图标(或者按 Ctrl+Shift+X),搜索并安装以下 4 个扩展:

扩展名 作者 用途
C/C++ Microsoft 代码补全、语法高亮、跳转定义、IntelliSense
Cortex-Debug marus25 调试 STM32、查看寄存器和变量
ARM Assembly dan-c-underwood ARM 汇编语法高亮(看 startup 文件用)
LinkerScript Zixuan Wang 链接脚本(.ld 文件)语法高亮

不装 Cortex-Debug 也能编译和下载,但后面调试时你会需要它。建议一次装齐。

验证 VSCode 安装:在终端里输入:

bash 复制代码
code --version

期望看到:

text 复制代码
1.xx.x

第五步:三个工具都装好的自检清单

在继续之前,请确认这四条命令在终端里都能正常输出(不是报错):

bash 复制代码
arm-none-eabi-gcc --version
openocd --version
make --version
code --version

四条都通了,说明工具链已就绪。我们开始创建第一个工程。

第六步:用 CubeMX 创建 Makefile 工程

现在我们用 CubeMX 创建一个 LED 点灯工程。和以前 Keil 版教程最大的区别只有一步:Toolchain 选 Makefile 而不是 MDK-ARM

6.1 新建工程,选芯片

打开 STM32CubeMX,点击:

text 复制代码
File → New Project

在芯片选择页面输入你的芯片型号。我演示用的是 STM32F103ZET6,你手上是什么芯片就搜什么。

双击芯片型号,进入配置界面。

6.2 配置调试接口(非常重要)

在左侧找到:

text 复制代码
System Core → SYS

把 Debug 设置为:

text 复制代码
Serial Wire

为什么必须做这一步: 如果你忘了开 Serial Wire,后面程序可能会把 SWD 调试引脚(PA13/PA14)当普通 GPIO 用了。一旦下载进去,ST-Link 就再也连不上芯片了,只能靠 BOOT0 拉高来恢复。这是新手最常见的"一次性连接"事故。

6.3 配置 LED 引脚为 GPIO 输出

在右侧芯片引脚图上,找到你开发板的 LED 引脚。我的演示板 LED 接在 PB5,你可以根据你的原理图选择。

点击该引脚,在弹出的功能列表中选择:

text 复制代码
GPIO_Output

然后在引脚上右键,选择 Enter User Label,输入:

text 复制代码
LED

为什么设置 User Label: User Label 会给引脚起一个有意义的名字。CubeMX 生成代码时,会在 main.h 里自动生成 LED_GPIO_PortLED_Pin 两个宏。你的应用代码里直接用 LED_GPIO_PortLED_Pin,不用记 GPIOBGPIO_PIN_5。换了板子,只要在 CubeMX 里重新给新引脚设置 User Label = LED,代码不用改。

6.4 配置 GPIO 输出参数

在左侧找到:

text 复制代码
System Core → GPIO

找到你刚才配置的 LED 引脚,确认以下参数:

参数 为什么
GPIO output level Low 初始化时 LED 默认熄灭
GPIO mode Output Push Pull 推挽输出,能提供足够的驱动电流
GPIO Pull-up/Pull-down No pull-up and no pull-down LED 电路已经确定了电平,不需要上下拉
Maximum output speed Low 点灯不需要高速翻转,Low 就够了

6.5 时钟配置

新手阶段先不折腾时钟。CubeMX 默认用 HSI(内部高速 RC 振荡器,8 MHz),点灯绰绰有余。

后面做串口、定时器、ADC 等对时钟精度有要求的外设时,我们再去配置 HSE 和 PLL。

6.6 工程管理器配置(关键步骤)

切换到:

text 复制代码
Project Manager

填写以下内容:

选项 说明
Project Name 01_led_gpio 工程名,见名知义
Project Location 你的工程根目录,比如 D:\STM32_Project 建议一个固定目录
Toolchain / IDE Makefile 这是和 Keil 版教程唯一不同的地方!
其他选项 保持默认 不用改

为什么选 Makefile 而不是 MDK-ARM: CubeMX 选择 Makefile 后,会生成一个 GNU Make 格式的 Makefile,这个 Makefile 可以被 ARM GCC 编译器直接使用。VSCode 的 tasks.json 通过调用 make 来编译,整套流程都基于这个 Makefile。

然后点击右上角:

text 复制代码
GENERATE CODE

生成成功后,你的工程目录应该是这样:

text 复制代码
01_led_gpio/
├── Core/
│   ├── Inc/
│   │   └── main.h              # 包含 LED_GPIO_Port 和 LED_Pin 宏
│   └── Src/
│       ├── main.c              # 主程序
│       ├── stm32f1xx_hal_msp.c # HAL 底层初始化
│       ├── stm32f1xx_it.c      # 中断服务函数
│       └── system_stm32f1xx.c  # 系统时钟初始化
├── Drivers/
│   ├── CMSIS/                  # ARM Cortex-M 内核文件
│   └── STM32F1xx_HAL_Driver/   # STM32 HAL 库
├── Makefile                    # 构建规则(自动生成)
├── STM32F103ZETx_FLASH.ld     # 链接脚本(自动生成)
└── 01_led_gpio.ioc            # CubeMX 工程文件

第七步:VSCode 打开工程,配置三个 JSON 文件

现在用 VSCode 打开刚才生成的工程目录。

在终端里进入工程目录,执行:

bash 复制代码
code D:\STM32_Project\01_led_gpio

或者在 VSCode 里:File → Open Folder,选择 01_led_gpio 目录。

工程打开后,我们要在 .vscode 目录下创建三个配置文件。这三个文件是 VSCode 管理工程的核心。

先说说这三个文件的关系:

文件 干什么 什么时候用
tasks.json 定义任务:编译(make)、清理(make clean)等 Ctrl+Shift+B 编译时
launch.json 定义调试配置:用什么调试器、下载什么文件 F5 开始调试时
c_cpp_properties.json 告诉 IntelliSense 去哪找头文件、用哪个编译器 写代码时(代码补全、跳转)

Ctrl+Shift+P,输入 task,选择 Tasks: Configure Default Build Task ,然后选 Create tasks.json file from templateOthers 。这会创建 .vscode/tasks.json

把下面的内容替换进去:

json 复制代码
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "command": "make",
            "args": [
                "-j8"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": {
                "owner": "cpp",
                "fileLocation": [
                    "relative",
                    "${workspaceFolder}"
                ],
                "pattern": {
                    "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
                    "file": 1,
                    "line": 2,
                    "column": 3,
                    "severity": 4,
                    "message": 5
                }
            }
        },
        {
            "label": "clean",
            "type": "shell",
            "command": "make",
            "args": [
                "clean"
            ]
        },
        {
            "label": "flash",
            "type": "shell",
            "command": "openocd",
            "args": [
                "-f", "interface/stlink.cfg",
                "-f", "target/stm32f1x.cfg",
                "-c", "program ${workspaceFolder}/build/${workspaceFolderBasename}.elf verify reset exit"
            ]
        }
    ]
}

每段干什么用:

  • build 任务 :执行 make -j8-j8 表示同时用 8 个线程编译,大大加快编译速度。isDefault: true 意味着按 Ctrl+Shift+B 时直接执行这个任务。
  • problemMatcher:用正则匹配 ARM GCC 的错误/警告输出格式,这样编译出错时,VSCode 的 Problems 面板里会列出错误,点击能直接跳到对应行。
  • clean 任务 :执行 make clean,清理编译产物。如果你想从头完整重新编译,先 clean 再 build。
  • flash 任务 :用 OpenOCD 通过 ST-Link 把编译好的程序下载到芯片。stm32f1x.cfg 是 STM32F1 系列的配置文件,如果你的芯片是 STM32F4,要改成 stm32f4x.cfg;如果是 STM32G4,要改成 stm32g4x.cfg

如果你的芯片不是 STM32F1 系列 ,flask 任务里的 target/stm32f1x.cfg 要换成你对应系列的配置文件。常见的有:

  • STM32F0 → stm32f0x.cfg
  • STM32F1 → stm32f1x.cfg
  • STM32F4 → stm32f4x.cfg
  • STM32F7 → stm32f7x.cfg
  • STM32G0 → stm32g0x.cfg
  • STM32G4 → stm32g4x.cfg
  • STM32L4 → stm32l4x.cfg

然后创建调试配置。点击左侧调试图标(虫子图标),点击 create a launch.json file ,选择 Cortex Debug。把下面的内容替换进去:

json 复制代码
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug (OpenOCD)",
            "cwd": "${workspaceFolder}",
            "executable": "${workspaceFolder}/build/${workspaceFolderBasename}.elf",
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "device": "STM32F103ZE",
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f1x.cfg"
            ],
            "svdFile": "",
            "preLaunchTask": "build",
            "runToEntryPoint": "main"
        }
    ]
}

每段干什么用:

  • name:调试配置的名称,在 VSCode 调试面板的下拉菜单里显示。
  • executable :指向编译生成的 .elf 文件。CubeMX 生成的 Makefile 会把编译产物放在 build/ 目录下,文件名和工程名一致。
  • type: cortex-debug:使用 Cortex-Debug 扩展作为调试器。
  • servertype: openocd:调试后端用 OpenOCD。
  • device: STM32F103ZE :告诉 Cortex-Debug 目标芯片型号。这个要改成你自己的芯片型号 ,比如 STM32F103C8STM32F407ZGSTM32G431RB
  • configFiles:OpenOCD 的配置文件,和 tasks.json 里的 flash 任务一致。
  • preLaunchTask: build:每次按 F5 开始调试前,先自动执行 build 任务编译一次。
  • runToEntryPoint: main :下载后自动运行到 main() 函数入口处暂停。
  • svdFile:先留空。SVD 文件能让 Cortex-Debug 显示外设寄存器,后面可以配置。

最后创建 IntelliSense 配置文件。按 Ctrl+Shift+P,输入 c/c++,选择 C/C++: Edit Configurations (JSON)。把下面的内容替换进去:

json 复制代码
{
    "configurations": [
        {
            "name": "STM32",
            "includePath": [
                "${workspaceFolder}/Core/Inc",
                "${workspaceFolder}/Drivers/CMSIS/Device/ST/STM32F1xx/Include",
                "${workspaceFolder}/Drivers/CMSIS/Include",
                "${workspaceFolder}/Drivers/STM32F1xx_HAL_Driver/Inc",
                "${workspaceFolder}/Drivers/STM32F1xx_HAL_Driver/Inc/Legacy"
            ],
            "defines": [
                "STM32F103xE",
                "USE_HAL_DRIVER"
            ],
            "compilerPath": "arm-none-eabi-gcc",
            "intelliSenseMode": "gcc-arm",
            "cStandard": "c11"
        }
    ],
    "version": 4
}

每段干什么用:

  • includePath :告诉 IntelliSense 去哪里找头文件。这样你写代码时 #include "app_led.h" 能找到文件,Ctrl+点击 能跳转。如果你用的是其他芯片系列(如 STM32F4),要把路径里的 STM32F1xx 改成 STM32F4xx
  • defines :预定义宏,和 Makefile 里的一致。STM32F103xE 要根据你的芯片型号改------比如 F103C8 是 STM32F103xB,F407ZG 是 STM32F407xx。这个值可以在 CubeMX 生成代码时看到,或在 Makefile 里搜索 -DSTM32F
  • compilerPath :编译器路径。如果你已经把 arm-none-eabi-gcc 加入了 PATH,这里写名字就行。如果 PATH 没生效,要写完整路径,比如 D:/Embedded/Tools/arm-gnu-toolchain/bin/arm-none-eabi-gcc.exe
  • intelliSenseMode: gcc-arm:告诉 IntelliSense 按 ARM GCC 的语法规则来检查代码。

三个 JSON 文件配置完之后,你的 .vscode 目录应该是这样:

text 复制代码
.vscode/
├── tasks.json
├── launch.json
└── c_cpp_properties.json

第八步:硬件连接

在写代码之前,先把 LED 搞清楚。

不同开发板的 LED 引脚可能不一样:

开发板类型 常见 LED 引脚示例
STM32F103C8T6 最小系统板 常见是 PC13
Nucleo-F103RB 常见是 PA5
正点原子 Mini / Elite 常见是 PE5 / PB5
普中 F103 开发板 可能是 PB5 / PE5 等

不要死记这些引脚。你真正要做的是:看原理图,找到 LED 对应的 GPIO。

LED 的有效电平也要注意:

两种常见接法:

text 复制代码
接法 A(推挽输出高电平亮):
  STM32 GPIO 引脚 → 限流电阻 → LED → GND
  输出高电平 → LED 亮

接法 B(推挽输出低电平亮):
  3.3V → 限流电阻 → LED → STM32 GPIO 引脚
  输出低电平 → LED 亮

这不是小细节。 很多新手代码对了、配置对了,LED 就是不亮,最后发现只是有效电平搞反了。

本文演示板用的是 PB5,接法 A(高电平亮)。你的板子可能不同,后面代码里有一个宏专门处理这个问题。

第九步:完整代码

说完硬件,开始写代码。

先说一个重要习惯:尽量对着文章敲代码,不要直接复制粘贴。

复制粘贴看起来省时间,实际上丢了两次学习机会。第一次是你敲错的时候------拼错 GPIO_PIN_SET、漏了 #endif、忘了在 Makefile 里加源文件------排查这些错误的过程,才是真正在学。第二次是你逐行读代码的时候------复制粘贴你不会读,手敲你会自然地在脑子里过一遍每一行在干什么。

如果你实在卡住了、想对照一下完整的源文件,每篇教程的配套资料里都提供了完整源码。但建议先自己敲,敲不通再去看。

和原 Keil 版教程一样,代码放在 Core 目录下:

  • Core/Inc/app_led.h --- LED 头文件(自己新建)
  • Core/Src/app_led.c --- LED 实现文件(自己新建)

为什么要单独抽 app_led.h / app_led.c,而不是直接写在 main.c 里?

三个原因:

  1. main.c 是 CubeMX 自动生成的,重新生成时你写在里面的代码可能被覆盖(除非写在 USER CODE BEGIN/END 之间)。
  2. 后面做蜂鸣器、按键、串口时,每个外设一个文件,工程结构清晰。
  3. 换板子、换芯片时,外设模块的文件直接复用,改几个宏就行。

app_led.h

创建文件 Core/Inc/app_led.h,内容如下:

c 复制代码
#ifndef APP_LED_H
#define APP_LED_H

#include "main.h"

void App_LED_Init(void);
void App_LED_On(void);
void App_LED_Off(void);
void App_LED_Toggle(void);

#endif

app_led.c

创建文件 Core/Src/app_led.c,内容如下:

c 复制代码
#include "app_led.h"

/*
 * LED_GPIO_Port and LED_Pin are generated by CubeMX in main.h.
 * If your board uses another LED pin, change the pin and label in CubeMX.
 */
#ifndef LED_GPIO_Port
#error "LED_GPIO_Port is not defined. Set the LED pin User Label to LED in CubeMX."
#endif

#ifndef LED_Pin
#error "LED_Pin is not defined. Set the LED pin User Label to LED in CubeMX."
#endif

/*
 * Many STM32 boards use active-low LEDs (GPIO_PIN_RESET turns LED ON).
 *
 * If your LED is active-high (GPIO_PIN_SET turns LED ON), change:
 *   APP_LED_ON_LEVEL  -> GPIO_PIN_SET
 *   APP_LED_OFF_LEVEL -> GPIO_PIN_RESET
 */
#ifndef APP_LED_ON_LEVEL
#define APP_LED_ON_LEVEL   GPIO_PIN_SET
#endif

#ifndef APP_LED_OFF_LEVEL
#define APP_LED_OFF_LEVEL  GPIO_PIN_RESET
#endif

void App_LED_Init(void)
{
    App_LED_Off();
}

void App_LED_On(void)
{
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, APP_LED_ON_LEVEL);
}

void App_LED_Off(void)
{
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, APP_LED_OFF_LEVEL);
}

void App_LED_Toggle(void)
{
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

这个文件的关键可以这样理解:

  1. #ifndef LED_GPIO_Port + #error --- 如果你在 CubeMX 里忘了给 LED 引脚设置 User Label,编译时会直接报错提醒你。这是一种"让代码自己说话"的写法。
  2. APP_LED_ON_LEVELAPP_LED_OFF_LEVEL --- 通过 #ifndef 实现可覆盖的默认值。本文默认高电平亮(GPIO_PIN_SET 亮灯),如果你的板子是低电平亮,不需要改这个文件,而是在 CubeMX 生成的 main.h 里,或者在自己的 app_config.h 里提前定义这两个宏。
  3. App_LED_Init() 调用 App_LED_Off() --- 初始化时默认熄灭 LED。
  4. App_LED_On/Off/Toggle --- 都基于 HAL_GPIO_WritePinHAL_GPIO_TogglePin,代码短到不需要注释,函数名已经说明了行为。

把你的源文件加入编译

CubeMX 生成的 Makefile 默认只会编译它自己生成的那几个 C 文件 (main.c、stm32f1xx_it.c 等)。你自己加的 app_led.c 需要手动告诉 Makefile。

打开工程根目录的 Makefile,找到这两行(大约在第 60-80 行附近):

makefile 复制代码
# C sources
C_SOURCES =  \
Core/Src/main.c \
Core/Src/stm32f1xx_hal_msp.c \
...

C_SOURCES 列表里加入你自己的源文件:

makefile 复制代码
# C sources
C_SOURCES =  \
Core/Src/main.c \
Core/Src/stm32f1xx_hal_msp.c \
Core/Src/stm32f1xx_it.c \
Core/Src/system_stm32f1xx.c \
Core/Src/app_led.c \
...

后面每新增一个 .c 文件,都要在这里加一行。换个角度看,这不是负担------它让你对"工程里有哪些源文件"有清晰的掌控。

第十步:main.c 调用方式

main.c 是 CubeMX 自动生成的,我们只在这三个标记之间写自己的代码:

  • /* USER CODE BEGIN Includes */ ...... /* USER CODE END Includes */
  • /* USER CODE BEGIN 2 */ ...... /* USER CODE END 2 */
  • /* USER CODE BEGIN WHILE */ ...... /* USER CODE END WHILE */

为什么要遵守 USER CODE 区域的边界? CubeMX 重新生成代码时,会保留 USER CODE 区域里的内容,覆盖区域外的内容。所以你自己写的代码放在这三个区域里最安全。

1. Includes 区域

Core/Src/main.c 中找到:

c 复制代码
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

在这两行之间加入:

c 复制代码
/* USER CODE BEGIN Includes */
#include "app_led.h"
/* USER CODE END Includes */

2. 初始化区域

Core/Src/main.c 中找到:

c 复制代码
/* USER CODE BEGIN 2 */

/* USER CODE END 2 */

在这两行之间加入(注意放在 MX_GPIO_Init() 之后,因为 LED 用到了 GPIO):

c 复制代码
/* USER CODE BEGIN 2 */
App_LED_Init();
/* USER CODE END 2 */

3. while 循环区域

Core/Src/main.c 中找到:

c 复制代码
/* USER CODE BEGIN WHILE */

/* USER CODE END WHILE */

在这两行之间加入 LED 闪烁逻辑:

c 复制代码
/* USER CODE BEGIN WHILE */
while (1)
{
    App_LED_Toggle();
    HAL_Delay(500);

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    /* USER CODE END 3 */
}

等一下------你会发现上面这段代码跨了三个区域。这是因为 CubeMX 生成的 while 循环结构是这样的:

c 复制代码
while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    /* USER CODE END 3 */
}

实际上,最干净的写法是这样的:

c 复制代码
/* USER CODE BEGIN WHILE */
while (1)
{
    App_LED_Toggle();
    HAL_Delay(500);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    /* USER CODE END 3 */
}

关键习惯: App_LED_Toggle()HAL_Delay(500) 写在 /* USER CODE END WHILE */ 之前。这样 CubeMX 重新生成时,你的代码不会被覆盖。

第十一步:工程思考------不要用 HAL_Delay 锁住 CPU

上面的闪烁代码能跑,但它有一个隐藏的问题。

c 复制代码
while (1)
{
    App_LED_Toggle();
    HAL_Delay(500);   // CPU 在这空等 500ms,什么事都干不了
}

HAL_Delay(500) 不是一个"定时器",它是一个死循环。CPU 在里面不停地数数,数满 500ms 才能出去。这 500ms 里 CPU 不能做任何其他事情。

现在工程里只有 LED,你还感觉不到问题。但后面加上按键、串口、蜂鸣器、定时器之后,如果你敢在 while 循环里写一个 HAL_Delay(500),就意味着:

  • 按键扫面每 500ms 才能跑一次------按键响应迟钝;
  • 串口数据每 500ms 才能处理一次------数据可能丢;
  • 蜂鸣器响完之前整个系统卡住不动。

阻塞的本质:CPU 只有一个,你让它空等,所有功能就一起等。

改法:用 HAL_GetTick() 看一眼时间就走

STM32 的 HAL 库在系统启动时就开始维护一个毫秒计数器,HAL_GetTick() 返回从开机到现在的毫秒数。这个函数不阻塞------它只是读一个变量,读完立刻返回。

用它来改造 LED 闪烁:

c 复制代码
/* USER CODE BEGIN WHILE */
while (1)
{
    static uint32_t last_tick = 0;

    if (HAL_GetTick() - last_tick >= 500)
    {
        last_tick = HAL_GetTick();
        App_LED_Toggle();
    }

    // 这里可以放其他事情,每一轮循环都会执行
    // App_Key_Scan();
    // App_UART_Process();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

    /* USER CODE END 3 */
}

这个代码在做什么:

  1. HAL_GetTick() - last_tick >= 500:看一眼表,"距离上次翻转过去 500ms 了吗?"
  2. 没到 500ms:跳过 if 块,继续往下走。这一轮循环可能只花了几十微秒。
  3. 到了 500ms:更新 last_tick,翻转 LED。然后继续往下走。
  4. 下次循环回来再问一遍。

HAL_Delay 是人等车------你站那不动,干等。HAL_GetTick 是你路过站台看一眼------车没来就先干别的,下次路过再瞄一眼。

验证方法

代码改成非阻塞版本后,LED 闪烁效果和之前一模一样------每 500ms 翻转一次。区别在于:CPU 在两次翻转之间的 500ms 里是自由身,可以干其他事了。

现在你可能还没加其他功能,所以视觉上没有区别。但后面每加上一个外设------按键、串口、蜂鸣器------你都会回到这个非阻塞写法上来,因为你会发现再也没有 HAL_Delay 的位置了。

这一篇你只需要记住一句话:HAL_Delay 让 CPU 傻等,HAL_GetTick 让 CPU 可以同时做多件事。 这个意识比你调通任何外设都重要。

第十二步:编译

代码写好了,现在试编译。

在 VSCode 里按:

text 复制代码
Ctrl+Shift+B

这相当于执行 make -j8。终端面板里会显示编译过程。

首次编译可能比较慢,因为 HAL 库的所有源文件都要编译一遍。后续改了代码再编译,Makefile 只会编译你改动过的文件,很快。

编译成功的标志: 终端输出最后几行没有 Error 字样,并且在 build/ 目录下生成了 01_led_gpio.elf01_led_gpio.hex01_led_gpio.bin

text 复制代码
arm-none-eabi-size build/01_led_gpio.elf
   text    data     bss     dec     hex filename
   3200      12    1572    4784    12b0 build/01_led_gpio.elf

如果编译报错,最常见的原因和解决方法:

错误 可能原因 解决方法
make: command not found make 不在 PATH 里 检查 make 是否安装并加入 PATH
arm-none-eabi-gcc: command not found 编译器不在 PATH 里 检查 ARM GCC 是否安装并加入 PATH
undefined reference to App_LED_Init app_led.c 没加入编译 在 Makefile 的 C_SOURCES 里加上 Core/Src/app_led.c
fatal error: app_led.h: No such file or directory app_led.h 路径不对 检查文件是否在 Core/Inc/
Makefile 语法错误 编辑 Makefile 时多加/少加了反斜杠 \ C_SOURCES 每行以 \ 结尾,最后一行不要 \

第十三步:下载

编译成功之后,把程序下载到开发板。

先确认三件事:

  1. 开发板已经通过 ST-Link 连接到电脑。
  2. 设备管理器里能正常识别 ST-Link(没有黄色感叹号)。
  3. USB 线是数据线(不是只能充电的线)。

在 VSCode 的终端里执行:

bash 复制代码
make flash

前提是你的 tasks.json 里已经配置了 flash 任务(见第七步)。这个命令会调用 OpenOCD,通过 ST-Link 下载 build/01_led_gpio.elf 到芯片。

也可以在 VSCode 里按 Ctrl+Shift+P,输入 run task,选择 flash

下载成功的标志: 终端输出最后几行出现:

text 复制代码
** Programming Finished **
** Verify Started **
** Verified OK **
shutdown command invoked

下载成功后,按一下开发板的复位键(如果板上有),或者程序会自动运行。你应该能看到:

text 复制代码
LED 以 500 ms 间隔闪烁(亮半秒、灭半秒,一直循环)

第十四步:调试(可选,但建议试一试)

上面用 make flash 只是下载。如果想调试------比如想看变量值、单步执行、设断点------就要用到 Cortex-Debug。

按 F5(或点左侧调试面板的绿色播放按钮),VSCode 会:

  1. 先编译(因为 preLaunchTask: build);
  2. 启动 OpenOCD 连接到 ST-Link;
  3. 下载程序到芯片;
  4. 暂停在 main() 函数入口;
  5. 显示调试工具栏(继续、暂停、单步、跳出、重启、停止)。

你可以试一个简单的调试操作:

  1. App_LED_Toggle() 所在行点一下行号左侧,打一个红色断点。
  2. 按 F5 开始调试,程序会在断点处停住。
  3. 按 F10 单步执行,观察 LED 的变化。
  4. 按 F5 继续运行。

调试体验,VSCode + Cortex-Debug 明显比 Keil 强------变量窗口、寄存器窗口、调用堆栈窗口都可以自由拖拽排列,代码浏览也快很多。

如果按 F5 调试不成功,先检查:

现象 优先检查
调试按钮是灰的 Cortex-Debug 扩展没装
报 "Unable to start debugging" launch.json 里的 device 名称不对,要和芯片型号匹配
OpenOCD 连不上 ST-Link 接线、驱动、SWD 配置
程序下载后不运行 launch.json 里没配 runToEntryPoint,或者需要手动按 Continue

移植到其他板子的修改点

换一块开发板,LED 照样能跑。你需要改的地方:

要改的地方 为什么要改 在哪里改
芯片型号 不同芯片外设和引脚复用不同 CubeMX 新建工程时重新选芯片
LED 引脚 每块板子的 LED 接到不同 GPIO CubeMX Pinout 界面,给新引脚设 User Label = LED
有效电平 有的板子高电平亮,有的低电平亮 app_led.cAPP_LED_ON_LEVEL / APP_LED_OFF_LEVEL
STM32 系列 F1/F4/G4 的 HAL 库路径不同 c_cpp_properties.json 里 includePath;tasks.json 里 OpenOCD target cfg
芯片具体型号 不同型号的 FLASH/RAM 大小不同 Makefile 里的 LDSCRIPT(链接脚本),CubeMX 会自动生成对应 .ld
HSE 频率 不同板子的外部晶振可能不同 CubeMX Clock Configuration(后续用外部时钟时才需要)

记住一个原则:网上教程用哪块板不重要,重要的是你知道自己的芯片型号、LED 接到哪个引脚、有效电平是什么。

常见问题排查

1. 终端里 arm-none-eabi-gcc 报 "not recognized"

优先检查 具体做法
安装没勾选 PATH 重新安装 ARM GCC,勾选 "Add to PATH",或手动加 bin 目录到系统环境变量
终端没重启 把当前终端关掉,重新打开
安装路径有问题 PATH 里 bin 目录的路径要和实际安装路径一致

2. make 编译报 "make: command not found"

优先检查 具体做法
make 没装或没加 PATH 用 winget 安装或手动加 C:\Program Files (x86)\GnuWin32\bin 到 PATH
终端没重启 重启终端

3. OpenOCD 连不上芯片

按这个顺序查:

顺序 检查项 说明
1 USB 线 确认是数据线,不是只能充电的线
2 ST-Link 驱动 设备管理器中不能有黄色感叹号
3 供电 目标板要有电(3.3V)
4 共地 ST-Link 的 GND 和开发板 GND 要连通
5 SWD 接线 SWDIO(PA13)、SWCLK(PA14)不要接反
6 CubeMX SYS Debug 必须设置为 Serial Wire
7 OpenOCD 配置文件 F1 系列用 stm32f1x.cfg,F4 系列用 stm32f4x.cfg

4. LED 不亮

优先检查 具体做法
程序是否真的下载进去了 看 make flash 的输出,确认 Verified OK
引脚对不对 在 CubeMX 里确认 LED 引脚和你原理图上的 GPIO 一致
有效电平对不对 高电平亮的 LED,APP_LED_ON_LEVEL 应该是 GPIO_PIN_SET;低电平亮的应该是 GPIO_PIN_RESET
LED 极性 万用表二极管档直接测一下 LED 是否完好,方向是否焊对
限流电阻 检查是否焊接正常,阻值是否合适
User Label 是否设置 在 CubeMX 里确认 LED 引脚设置了 User Label = "LED"
代码是否被编译进去了 检查 MakefileC_SOURCES 里是否包含 Core/Src/app_led.c

5. 编译报 "undefined reference to App_LED_xxx"

这说明 app_led.c 没有被编译。去 MakefileC_SOURCES 列表里加上 Core/Src/app_led.c \

6. IntelliSense 报错(红色波浪线),但编译通过了

IntelliSense 的配置(c_cpp_properties.json)和实际编译用的是不同配置。优先信任编译结果。

优先检查 具体做法
includePath 是否正确 对照实际目录结构检查路径
defines 是否正确 STM32F103xE / USE_HAL_DRIVER 是否包含
intelliSenseMode 应该是 gcc-arm
重启 IntelliSense Ctrl+Shift+P → C/C++: Reset IntelliSense Database

7. 下载后芯片再也连不上了

很可能是你把 SWD 调试引脚(PA13/PA14)用成了普通 GPIO。

解决方法:

  1. 把 BOOT0 拉高(接 3.3V),按复位键。
  2. 用 CubeProgrammer 或 OpenOCD 重新连接、擦除芯片。
  3. 把 BOOT0 恢复到低电平。
  4. 在 CubeMX 里确保 SYS → Debug → Serial Wire 是打开的。

建议你建立一个固定工程目录

新手阶段不要把工程丢得到处都是。建议建一个目录:

text 复制代码
D:\STM32_Project

以后每一篇教程一个文件夹:

text 复制代码
D:\STM32_Project
├── 01_led_gpio
├── 02_buzzer_gpio
├── 03_key_input
├── 04_key_debounce
├── 05_key_exti
├── 06_usart_printf
└── ...

如果你习惯用 Git,也可以从第一篇就养成习惯:

bash 复制代码
cd D:\STM32_Project\01_led_gpio
git init
git add .
git commit -m "01: VSCode env setup + LED GPIO"

不会 Git 也没关系,先把工程跑通最重要。

本篇小结

这一篇我们做了三件事:装好了全新的 VSCode 开发环境,点亮了第一个 LED,还学会了一个比点灯本身更重要的习惯------不让 CPU 傻等

你可以按这张清单确认一下:

检查项 是否完成
arm-none-eabi-gcc 能正常输出版本号
openocd 能正常输出版本号
make 能正常输出版本号
VS Code 安装了 C/C++、Cortex-Debug 扩展
CubeMX 能生成 Makefile 工程
VSCode 三个 JSON 配置文件就位
Ctrl+Shift+B 编译通过(0 Error)
make flash 下载(Verified OK)
LED 以 500 ms 间隔闪烁(非阻塞写法)
能说清楚 HAL_Delay 和 HAL_GetTick 的区别

做到最后一条,你的新工具链就已经打通了,而且一上手就写了一个真正工程级的 LED 闪烁------不用 HAL_Delay,用 HAL_GetTick 看一眼时间就走。后面加按键、串口、蜂鸣器,这个非阻塞的骨架直接复用。

以后每一篇教程的业务代码,和原来 Keil 版的完全一样------CubeMX 配置、HAL 调用、app_xxx.h/c 封装、移植方式------换的只是编辑器、编译器和调试器的使用方式。每篇后面都会像这篇一样,在基础功能跑通之后,加一节工程思考,往实战方向多走一步。

下一篇预告

LED 亮了之后,下一篇我们加上蜂鸣器:让板子响一声,同时 LED 翻转一次。

STM32 零基础可移植教程 02:蜂鸣器响一声,LED 跟着翻转一次。

相关推荐
guygg881 小时前
STM32正交编码器接口指南
stm32·单片机·嵌入式硬件
Mars-xq1 小时前
VSCode 开发 Android 时,类、方法无法跳转
android·ide·vscode
lin135380675732 小时前
AH810L输入 48~54V 转 5V/100mA 完整方案
嵌入式硬件·物联网
星夜夏空992 小时前
STM32单片机学习(36) —— RTC
stm32·单片机·学习
森利威尔电子-2 小时前
森利威尔 SL3042 | 9V-120V 宽压输入 1.25-50V 可调输出 峰值 10A 电源芯片
单片机·嵌入式硬件·电源芯片·降压恒压芯片
金线银线还是铜线?2 小时前
国产微能量收集PMIC芯片MF9005/MF9006如何选型?
嵌入式硬件·物联网·太阳能
Mars-xq2 小时前
VSCode 开发Android 新手必装插件清单
android·ide·vscode
xskukuku8 小时前
使用VSCode配置C语言运行环境
c语言·ide·vscode
Szime11 小时前
高速 ADC 国产替代选型:通信、雷达、仪器仪表项目要看哪些参数?
单片机·嵌入式硬件·fpga开发