目标

在 Windows 下脱离 Keil MDK 完成 STM32L4xxx 系列 MCU 的开发和调试工作。

网上很多教程在一些配置上解释的都有错误,或者根本没解释为什么要这样做,简单记录一下配置流程。

为什么不用 Keil?

Keil 狗都不用。

为什么不用 PlatformIO?

PlatformIO 是以开发板而非芯片为中心的 IDE,这就导致它更适合于 Arduino 这类开源硬件的开发,和实际的嵌入式开发需求不符合。PlatformIO 的开发者似乎也意识到了这个问题,在库里提供了部分 generic 配置文件,但覆盖面太窄了,并没有 STM32L4 的 generic 配置文件,手写.ini文件又太蠢了一些。

为什么不用 stm32-for-vscode 和 Cortex-Debug 插件?

第一次使用还是手动写写配置文件比较好,能熟悉一下每个工具。安装完这两个插件后就直接自动完成了所有工作,不利于前期学习。

大体流程

  1. 使用 STM32CubeMX 生成配置代码及 Makefile

  2. 使用 VS Code 编写程序

  3. 在 VS Code 中调用 make 完成交叉编译

  4. 使用 OpenOCD 完成程序的烧录和调试

flowchart

图中横线上方加粗的内容是脱离 Keil 开发和调试 STM32 程序必需的工具,横线下方为推荐的工具和插件。

准备工作

安装必需的工具:

  • STM32CubeMX
  • VS Code

除了上面两个之外,还需要安装:

  • make
  • Arm GNU Toolchain
  • Git Bash

Windows 并不提供 make 命令,而 Mingw-w64 包含了了mingw64-make.exe程序,因此可以通过安装 Mingw-w64 来安装 make:

  1. 将安装目录bin文件夹下的mingw64-make.exe复制一份,并重命名为make.exe

  2. bin文件夹添加到环境变量;

  3. 在命令行执行make -v,确认环境变量配置正确。

现在已经 2022 年了,网络上有不少互相抄来抄去教程,要求读者安装 GNU Arm Embedded Toolchain,然后甩一个旧版的下载链接。

但实际上 Arm GNU Toolchain 已经取代了 GNU Arm Embedded Toolchain,后者被 Arm 标记为 discontinued,没有特殊理由当然应优先选择最新的 Toolchain。

Arm GNU Toolchain Official Site1

环境配置

打开 STM32CubeMX,这里我们已经有了一个名为 Test 的工程,切换到 Project Manager,将 Toolchain/IDE 修改为 Makefile:

STM32CubeMX

修改完成后生成代码,在输出目录(我这里是E:\Test)中,可以看到目录下已经有了一个 Makefile 文件。

该目录打开命令行( powershell 或 cmd 均可),运行 make 命令,会得到下面的结果:

PS E:\Test> make
mkdir build
process_begin: CreateProcess(NULL, mkdir build, ...) failed.
make (e=2): 系统找不到指定的文件。
make: *** [Makefile:179: build] Error 2

这里产生了一个报错,提示没有找到 build 目录,为什么会产生这个错误我们在后面会解释,这里先不管它,在E:\Test目录下我们手动创建一个文件夹build,再次执行命令make

make

如果最终得到以下输出结果,则说明程序编译没有任何问题,Mingw-w64gcc-arm-none-eabi均已被正确安装。

arm-none-eabi-size build/Test.elf
   text    data     bss     dec     hex filename
   4384      32    1568    5984    1760 build/Test.elf
arm-none-eabi-objcopy -O ihex build/Test.elf build/Test.hex
arm-none-eabi-objcopy -O binary -S build/Test.elf build/Test.bin

如果尝试执行make clean,也会遇到类似的错误:

PS E:\Test> make clean
rm -fR build
process_begin: CreateProcess(NULL, rm -fR build, ...) failed.
make (e=2): 系统找不到指定的文件。
make: [Makefile:185: clean] Error 2 (ignored)

造成这一问题的原因和mkdir无法执行的原因相同,将在后面进行解释。

在 VS Code 中开发和编译程序

在调通前面的步骤之后,我们实际上已经可以脱离 Keil 环境完成 STM32 程序的编译,接下来还需要修改 VSCode 的配置,才能直接在 VSCode 中编译代码,并享受到 VSCode 提供的代码补全等功能。

首先我们在 VSCode 中打开项目文件夹E:\Test,我们将要修改下面三个文件的配置:

  • tasks.json
  • settings.json
  • c_cpp_properties.json

这三个文件夹将被放置在工程目录下 .vscode 文件夹中。

tasks.json

tasks.json文件告诉了 VSCode 如何按照上一节的步骤编译我们的 STM32 项目,为了修改该文件,按下快捷键ctrl+shift+p,输入configure task,点击“配置任务”,并选择“使用模板创建tasks.json文件”,模板则选择Others,然后将下面的内容填入到新创建的tasks.json文件中:

// tasks.json
{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Build",
            "type": "shell",
            "command": "make",
            "args": [
                "-j16"
            ],
            "problemMatcher": ["$gcc"],
            "group": {
              "kind": "build",
              "isDefault": true
            }
        }
    ]
}

复制上面这段内容时记得根据电脑实际配置修改args中的-j16参数,将16替换为CPU实际的线程数,其它内容保持不变,保存。

随后,点击顶部菜单栏“终端”,“运行生成任务”,可以看到底部终端中 VSCode 将自动执行make -j16命令(尽管依然会出现错误,“系统找不到指定的文件”)。

settings.json

前面提到了,手动执行 make 时,产生了如下的错误:

mkdir build
process_begin: CreateProcess(NULL, mkdir build, ...) failed.
make (e=2): 系统找不到指定的文件。
make: *** [Makefile:179: build] Error 2

这四行的意思很简单,第一行尝试执行了mkdir build命令来创建文件夹build,但第二行告诉我们mkdir build命令执行失败,如果我们看一下 Makefile文件,可以看到,build文件夹是编译结果的输出文件夹,如果这个文件夹未能正确创建,自然也就无法进行后续的编译操作。

#######################################
# paths
#######################################
# Build path
BUILD_DIR = build

# 第 178 行
$(BUILD_DIR):
    mkdir $@

此时在工程目录手动创建build文件夹虽然能够绕过问题,但为什么mkdir build会执行失败呢?问题就隐含在错误输出的第二行:

process_begin: CreateProcess(NULL, mkdir build, ...) failed.

这里告诉我们,程序尝试通过CreateProcess()函数来创建一个新的进程,然而在 Windows 下,mkdir并非一个可执行文件(或者说,Windows 下并没有mkdir.exe),而仅仅是cmd.exe的一个内置命令,自然也就无法直接通过CreateProcess()来调用。

类似的,rmmv等 Linux 下可以随意调用的命令,在 Windows 下都无法直接通过CreateProcess()调用,这也是为什么无法直接运行make clean:

#######################################
# clean up
#######################################
clean:
    -rm -fR $(BUILD_DIR)

这一问题的解决办法也很简单,在安装 Git 的同时我们同时也安装了 Git Bash,而 Git Bash 提供了大量 Unix 命令(如rmcpmv),只要在 Git Bash 下执行 make 就不会遇到这类问题了。

那么如何修改 VSCode 工作区的默认终端环境为 Git Bash 呢?只需要修改工作区的setting.json文件即可达到目的。按下ctrl+shift+p,输入open Workspace Settings (JSON),然后向其中填入:

// settings.json
{
    "terminal.integrated.defaultProfile.windows": "Git Bash"
}

重新启动 VSCode 工作区,点击顶部菜单栏“终端”,“运行生成任务”,可以看到 STM32 工程已被正确编译。

c_cpp_properties.json

在正确填写task.jsonsettings.json后,虽然我们已经可以绕开命令行操作,直接在 VSCode 中编译整个项目,但在 VSCode 中打开任意源文件(如main.c),仍会看到大量的红色波浪线,代码补全等功能依然无法使用。这就需要修改c_cpp_properties.json,使 VSCode IntelliSense 在正常工作。

// c_cpp_properties.json
{
    "configurations": [
        {
            "name": "STM32L475xx",
            "includePath": [
                "${workspaceFolder}/**"
            ],
            "defines": [
                "USE_HAL_DRIVER",
                "STM32L475xx"
            ],
            "compilerPath": "C:/Program Files/mingw-w64/bin/gcc.exe",
            "cStandard": "c17",
            "cppStandard": "c++17",
            "intelliSenseMode": "gcc-x64",
            "configurationProvider": "ms-vscode.makefile-tools"
        }
    ],
    "version": 4
}

这里要重点注意的主要是definescompilerPath两个选项(至于name随便填就好了),defines实际上就是在 Keil 中填写的全局宏定义标识符(Preprocessor Symbols -> Define):

Keil Defines

compilerPath则是我们之前安装的 Mingw-w64 编译器的路径,按实际情况填写就行。

一些文章在配置c_cpp_properties.json时,还修改了includePath

"includePath": [
    "${workspaceFolder}/**",
    "${workspaceFolder}/Core/Inc",
    "${workspaceFolder}/Drivers/STM32L4xx_HAL_Driver/Inc",
    "${workspaceFolder}/Drivers/STM32L4xx_HAL_Driver/Inc/Legacy",
    "${workspaceFolder}/Drivers/CMSIS/Device/ST/STM32L4xx/Include",
    "${workspaceFolder}/Drivers/CMSIS/Include"
],

这些文章把 Keil 中填写的 Include Paths 填入到了c_cpp_properties.json里,这种写法纯属画蛇添足,因为${workspaceFolder}/**就已经告诉了 VSCode 要递归遍历项目文件夹下的所有文件及其子文件,在后面手动添加的这些内容没有任何意义,正确的做法是要么只写第一行,要么不写第一行。

简单总结

  • tasks.json:用于配置 VSCode 调用外部工具完成编译工作;
  • settings.json:在该文件中修改工作区内使用的 terminal,以消除 make 命令执行时遇到的各种问题;
  • c_cpp_properties.json:用于配置 VSCode 的智能感知插件,使其能正确解析工作区内所有代码,实现代码补全等功能。