This tutorial explains steps to effectively develop and debug STM32 application in Visual Studio Code using CMake build generator, Ninja build tool and GCC compiler.
Things you will learn
How to install and setup all tools
How to create new STM32 project with STM32CubeMX or STM32CubeIDE tools
How to install and setup recommended extensions for Visual Studio Code for easier development
How to setup CMake lists and CMake presets
How to generate build system for compiler
How to compile the project with GCC
How to flash and debug application to the STM32 target
This tutorial is using Windows operating system. Similar procedure will apply for Linux and MAC operating system.
The tools installation tutorial will help you to understand the necessary tools required to work with STM32 and is generally good for beginners to get up to speed and correctly understand necessary requirements.
Since some time, ST has a new STM32CubeCLT software tool, that includes the build tools necessary for vscode, includes Ninja build system and CMake build generator. STM32CubeCLT (Command Line Tools) is a simple and easy way to quickly get up to speed for vscode development. It will also setup your build environment variables (Path in case of Windows) automatically, allowing you to invoke ninja, cmake or other commands directly from command line tool.
By installing the STM32CubeCLT, you won't get STM32CubeIDE graphical tool, nor you will get any MCU configuration tool capability, rather just command line tools to invoke build and debug, normally from within vscode. You will still need to separately install vscode and necessary extensions.
First step is to install STM32CubeIDE, that will be used to easily start new STM32 project and it comes with integrated STM32CubeMX tool - allowing us graphical configuration.
STM32CubeIDE also provides necessary tools needed later for VSCode development
ARM none eabi GCC compiler
ST-LINK GDBServer for debugging
STM32CubeProgrammer tool for code downloading and respective ST-Link drivers
Folder with STM32 SVD files
Drivers for ST-Link
Environmental path setup
3
paths should be added to environmental settings from STM32CubeIDE installation, one path for each of above-mentioned tools.
In case of my computer, using STM32CubeIDE 1.8 (updated through eclipse, hence my actual installation path is still showing version 1.0.2
) paths are defined as:
GCC compiler: c:STSTM32CubeIDE_1.0.2STM32CubeIDEpluginscom.st.stm32cube.ide.mcu.externaltools.gnu-tools-for-stm32.9-2020-q2-update.win32_2.0.0.202105311346toolsbin
ST-Link GDB server: c:STSTM32CubeIDE_1.0.2STM32CubeIDEpluginscom.st.stm32cube.ide.mcu.externaltools.stlink-gdb-server.win32_2.0.100.202109301221toolsbin
STM32Cube Programmer CLI: c:STSTM32CubeIDE_1.0.2STM32CubeIDEpluginscom.st.stm32cube.ide.mcu.externaltools.cubeprogrammer.win32_2.0.100.202110141430toolsbin
Your paths may differ at version numbers
Verify correct path setup, run:
arm-none-eabi-gcc --version STM32_Programmer_CLI --version ST-LINK_gdbserver --version
That should produce output similar to the picture below
This step is not necessary if you have installed the build tools with STM32CubeCLT
Download and install CMake.
Installation wizard will ask you to add CMake to environmental paths. Select the option or add bin
folder of CMake installation folder to environmental path.
This step is not necessary if you have installed the build tools with STM32CubeCLT
Download Ninja build system from Github releases page. It comes as portable executable, without need to install anything. However it must be visible at environment level, like all previous tools.
Verify CMake and Ninja installation, run:
cmake --version ninja --version
Output shall be something similar to
Download and install VSCode. Once installed and opened, window will look similar to the one below.
Visual Studio Code is lightweight text editor with capability to enlarge it using extensions.
List of useful extensions for STM32 development using CMake:
ms-vscode.cpptools
: Syntax highlighting and other core features for C/C++ development
ms-vscode.cmake-tools
: CMake core tools, build system generator tool
twxs.cmake
: CMake color highlighting
marus25.cortex-debug
: Cortex-M debugging extension, mandatory for STM32 debug from VSCode
dan-c-underwood.arm
: ARM Assembly syntax highlighter
zixuanwang.linkerscript
: GCC Linker script syntax highlighter
You can install them by copying below commands in VSCode's internal terminal window.
code --install-extension ms-vscode.cpptools code --install-extension ms-vscode.cmake-tools code --install-extension twxs.cmake code --install-extension marus25.cortex-debug code --install-extension dan-c-underwood.arm code --install-extension zixuanwang.linkerscript
Go to Terminal -> New Terminal to open new terminal window
Alternative way is to use Extension search GUI and manually install from there.
At this point, all the tools are properly installed - you are on the right track towards success.
Fundamental requirement to move forward is to have a working project that will be converted to CMake and developed in VSCode. For this purpose, I will guide you through simple new project creation using STM32CubeMX or STM32CubeIDE software tools.
You can skip this part, if you already have your project to work on.
I used STM32CubeIDE tool and STM32H735G-DK board for this demo.
Open STM32CubeIDE and start new project
Select STM32 MCU - I selected STM32H735IG which is used on STM32H735G-DK board
Select project name and path, then create project and wait for Pinout view to open
Our task is to have a simple project that will toggle leds. LEDs are connected to PC2
and PC3
respectively, active LOW. Pins can be configured in output push-pull or open-drain mode
Set pins as outputs with optional labels as LED1
and LED2
respectively
If you are using STM32CubeMX
, go to Project manager, set project name and be sure STM32CubeIDE
is selected as Toolchain
.
Go to advanced settings and select LL
as drivers for generated code
We are using LL drivers for the sake of simplicity in this tutorial
Re-generate the project by pressing red button or by saving the project with CTRL + S
shortcut
Project is now (re)generated. Yellow highlighted files are sources to build. Blue is linker script.
That's it for the first run, we are ready to compile. Hit CTRL + B
or click on hammer icon to start.STM32CubeIDE will compile the project, you should see similar as on picture below. It is now ready for flashing the MCU's flash and start debugging.
This is end of first part, where we successfully created our project. At this point we consider project being ready to be transferred to CMake-based build system.
You can continue your development with STM32CubeIDE in the future, add new sources, modify code, compile, flash the binary and debug directly the microcontroller. This is preferred STM32 development studio, developed and maintained by STMicroelectronics.
It is expected that project to develop in VSCode has been created. We will move forward for GCC compiler, but others could be used too.
With release of Visual Studio Code, many developers use the tool for many programming languages and fortunately can also develop STM32 applications with single tool. If you are one of developers liking VSCode, most elegant way to move forward is to transfer STM32CubeIDE-based project to CMake, develop code in VSCode and compile with Ninja build system using GCC compiler. It is fast and lightweight.
Development in VSCode is for intermediate or experienced users. I suggest to all STM32 beginners to stay with STM32CubeIDE development toolchain. It will be very easy to move forward and come to VSCode topic later.
Every CMake-based application requires CMakeLists.txt
file in the root directory, that describes the project and provides input information for build system generation.
Root
CMakeLists.txt
file is sometimes called top-level CMake file
Essential things described in CMakeLists.txt
file:
Toolchain information, such as GCC configuration with build flags
Project name
Source files to build with compiler, C, C++ or Assembly files
List of include paths for compiler to find functions, defines, ... (-I
)
Linker script path
Compilation defines, or sometimes called preprocessor defines (-D
)
Cortex-Mxx and floating point settings for instruction set generation
Visual Studio Code has been installed and will be used as further file editor.
Find your generated project path and open folder with VSCode:
Option 1: Go to the folder with explorer, then right click and select Open in Code
.
Option 2: Alternatively, open VScode as new empty solution and add folder to it manually. Use File -> Open Folder...
to open folder
Option 3: Go to folder with cmd or powershell tool and run code .
Final result should look similar to the one below
CMake needs to be aware about Toolchain we would like to use to finally compile the project with. As same toolchain is usually reused among different projects, it is advised to create this part in separate file for easier reuse. These are generic compiler settings and not directly linked to projects itself.
A simple .cmake
file can be used and later reused among your various projects. I am using name cmake/gcc-arm-none-eabi.cmake
for this tutorial and below is its example:
set(CMAKE_SYSTEM_NAME Generic)set(CMAKE_SYSTEM_PROCESSOR arm)# Some default GCC settings# arm-none-eabi- must be part of path environmentset(TOOLCHAIN_PREFIX arm-none-eabi-)set(FLAGS "-fdata-sections -ffunction-sections --specs=nano.specs -Wl,--gc-sections")set(CPP_FLAGS "-fno-rtti -fno-exceptions -fno-threadsafe-statics")# Define compiler settingsset(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc ${FLAGS})set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++ ${FLAGS} ${CPP_FLAGS})set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy)set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size)set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf")set(CMAKE_EXECUTABLE_SUFFIX_C ".elf")set(CMAKE_EXECUTABLE_SUFFIX_CXX ".elf")set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
Create a file in the cmake/
folder of root project directory.
If CMake highlighter plugin is installed, VSCode will nicely highlight CMake commands for you
Toolchain setup is complete. You can freely close the file and move to next step.
We need to create main CMakeLists.txt
, also called root CMake file.
Make sure you really name it
CMakeLists.txt
with correct upper and lowercase characters.
I prepared simple template file for you, that can be reused for all of your projects in the future. You will just need to change things like project name, source files, include paths, etc.
cmake_minimum_required(VERSION 3.22)# Setup compiler settingsset(CMAKE_C_STANDARD 11)set(CMAKE_C_STANDARD_REQUIRED ON)set(CMAKE_C_EXTENSIONS ON)set(CMAKE_CXX_STANDARD 20)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS ON)set(PROJ_PATH ${CMAKE_CURRENT_SOURCE_DIR})message("Build type: " ${CMAKE_BUILD_TYPE})## Core project settings#project(your-project-name)enable_language(C CXX ASM)## Core MCU flags, CPU, instruction set and FPU setup# Needs to be set properly for your MCU#set(CPU_PARAMETERS -mthumb# This needs attention to properly set for used MCU-mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard )# Set linker scriptset(linker_script_SRC ${PROJ_PATH}/path-to-linker-script.ld)set(EXECUTABLE ${CMAKE_PROJECT_NAME})## List of source files to compile#set(sources_SRCS# Put here your source files, one in each line, relative to CMakeLists.txt file location)## Include directories#set(include_path_DIRS# Put here your include dirs, one in each line, relative to CMakeLists.txt file location)## Symbols definition#set(symbols_SYMB# Put here your symbols (preprocessor defines), one in each line# Encapsulate them with double quotes for safety purpose)# Executable filesadd_executable(${EXECUTABLE} ${sources_SRCS})# Include pathstarget_include_directories(${EXECUTABLE} PRIVATE ${include_path_DIRS})# Project symbolstarget_compile_definitions(${EXECUTABLE} PRIVATE ${symbols_SYMB})# Compiler optionstarget_compile_options(${EXECUTABLE} PRIVATE${CPU_PARAMETERS}-Wall -Wextra -Wpedantic -Wno-unused-parameter# Full debug configuration-Og -g3 -ggdb )# Linker optionstarget_link_options(${EXECUTABLE} PRIVATE-T${linker_script_SRC}${CPU_PARAMETERS}-Wl,-Map=${CMAKE_PROJECT_NAME}.map --specs=nosys.specs -u _printf_float # STDIO float formatting support-Wl,--start-group -lc -lm -lstdc++ -lsupc++ -Wl,--end-group -Wl,--print-memory-usage )# Execute post-build to print sizeadd_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_SIZE} $<TARGET_FILE:${EXECUTABLE}> )# Convert output to hex and binaryadd_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${EXECUTABLE}> ${EXECUTABLE}.hex )# Convert to bin file -> add conditional check?add_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${EXECUTABLE}> ${EXECUTABLE}.bin )
Source files are the same as in STM32CubeIDE project. You can check previous image with highlighted sources in yellow color.
Symbols and include paths can be found in STM32CubeIDE under project settings. 2
pictures below are showing how it is in the case of demo project.
Cortex-Mxx setup needs a special attention, especially with floating point setup.
For STM32H735xx
, settings should be set as below.
set(CPU_PARAMETERS -mthumb -mcpu=cortex-m7 # Set Cortex-M CPU-mfpu=fpv5-d16 # Set Floating point type-mfloat-abi=hard # Hardware ABI mode)
General rule for settings would be as per table below
STM32 Family | -mcpu | -mfpu | -mfloat-abi |
---|---|---|---|
STM32F0 | cortex-m0 | Not used | soft |
STM32F1 | cortex-m3 | Not used | soft |
STM32F2 | cortex-m3 | Not used | soft |
STM32F3 | cortex-m4 | fpv4-sp-d16 | hard |
STM32F4 | cortex-m4 | fpv4-sp-d16 | hard |
STM32F7 SP | cortex-m7 | fpv5-sp-d16 | hard |
STM32F7 DP | cortex-m7 | fpv5-d16 | hard |
STM32G0 | cortex-m0plus | Not used | soft |
STM32C0 | cortex-m0plus | Not used | soft |
STM32G4 | cortex-m4 | fpv4-sp-d16 | hard |
STM32H5 | cortex-m33 | fpv5-sp-d16 | hard |
STM32H7 | cortex-m7 | fpv5-d16 | hard |
STM32L0 | cortex-m0plus | Not used | soft |
STM32L1 | cortex-m3 | Not used | soft |
STM32L4 | cortex-m4 | fpv4-sp-d16 | hard |
STM32L5 | cortex-m33 | fpv5-sp-d16 | hard |
STM32U0 | cortex-m0plus | Not used | soft |
STM32U5 | cortex-m33 | fpv5-sp-d16 | hard |
STM32WB | cortex-m4 | fpv4-sp-d16 | hard |
STM32WBA | cortex-m33 | fpv5-sp-d16 | hard |
STM32WL CM4 | cortex-m4 | Not used | soft |
STM32WL CM0 | cortex-m0plus | Not used | soft |
This table is a subject of potential mistakes, not tested with GCC compiler for all lines. For
STM32F7
, go to STM32F7xx official site and check if your device has single or double precision FPU, then apply settings accordingly. Products list is not exhaustive.
Final CMakeLists.txt
file after source files, include paths, MCU core settings and defines are set:
cmake_minimum_required(VERSION 3.22)# Setup compiler settingsset(CMAKE_C_STANDARD 11)set(CMAKE_C_STANDARD_REQUIRED ON)set(CMAKE_C_EXTENSIONS ON)set(CMAKE_CXX_STANDARD 20)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS ON)set(PROJ_PATH ${CMAKE_CURRENT_SOURCE_DIR})message("Build type: " ${CMAKE_BUILD_TYPE})## Core project settings#project(STM32H735G-DK-LED) # Modifiedenable_language(C CXX ASM)## Core MCU flags, CPU, instruction set and FPU setup# Needs to be set properly for your MCU#set(CPU_PARAMETERS -mthumb# This needs attention to properly set for used MCU-mcpu=cortex-m7 # Modified-mfpu=fpv5-d16 # Modified-mfloat-abi=hard # Modified)# Set linker scriptset(linker_script_SRC ${PROJ_PATH}/STM32H735IGKX_FLASH.ld) # Modifiedset(EXECUTABLE ${CMAKE_PROJECT_NAME})## List of source files to compile#set(sources_SRCS # Modified${PROJ_PATH}/Core/Src/main.c${PROJ_PATH}/Core/Src/stm32h7xx_it.c${PROJ_PATH}/Core/Src/syscalls.c${PROJ_PATH}/Core/Src/sysmem.c${PROJ_PATH}/Core/Src/system_stm32h7xx.c${PROJ_PATH}/Core/Startup/startup_stm32h735igkx.s${PROJ_PATH}/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_exti.c${PROJ_PATH}/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_gpio.c${PROJ_PATH}/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_pwr.c${PROJ_PATH}/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_rcc.c${PROJ_PATH}/Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_ll_utils.c )## Include directories#set(include_path_DIRS # Modified${PROJ_PATH}/Core/Inc${PROJ_PATH}/Drivers/STM32H7xx_HAL_Driver/Inc${PROJ_PATH}/Drivers/CMSIS/Device/ST/STM32H7xx/Include${PROJ_PATH}/Drivers/CMSIS/Include )## Symbols definition#set(symbols_SYMB # Modified"DEBUG""STM32H735xx""USE_FULL_LL_DRIVER""HSE_VALUE=25000000")# Executable filesadd_executable(${EXECUTABLE} ${sources_SRCS})# Include pathstarget_include_directories(${EXECUTABLE} PRIVATE ${include_path_DIRS})# Project symbolstarget_compile_definitions(${EXECUTABLE} PRIVATE ${symbols_SYMB})# Compiler optionstarget_compile_options(${EXECUTABLE} PRIVATE${CPU_PARAMETERS}-Wall -Wextra -Wpedantic -Wno-unused-parameter# Full debug configuration-Og -g3 -ggdb )# Linker optionstarget_link_options(${EXECUTABLE} PRIVATE-T${linker_script_SRC}${CPU_PARAMETERS}-Wl,-Map=${CMAKE_PROJECT_NAME}.map --specs=nosys.specs -u _printf_float # STDIO float formatting support-Wl,--start-group -lc -lm -lstdc++ -lsupc++ -Wl,--end-group -Wl,--print-memory-usage )# Execute post-build to print sizeadd_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_SIZE} $<TARGET_FILE:${EXECUTABLE}> )# Convert output to hex and binaryadd_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${EXECUTABLE}> ${EXECUTABLE}.hex )# Convert to bin file -> add conditional check?add_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${EXECUTABLE}> ${EXECUTABLE}.bin )
In VSCode, well highlighted, it looks like this
CMakePresets.json
is a special file, available since CMake 3.18
and provides definition for user configuration, similar to debug and release configuration known in eclipse. Having this file allows developer to quickly change between debug and release mode, or even between bootloader and main application, that is a common use case in embedded applications.
This tutorial will not focus on details about the file, rather here is the provided template file
File describes:
Path to build directory for each build configuration
Default build type for each configuration (Debug, Release, ...)
Path to .cmake toolchain descriptor
4
presets are configured in the template for each of the default CMake configurations
{"version": 3,"configurePresets": [ {"name": "default","hidden": true,"generator": "Ninja","binaryDir": "${sourceDir}/build/${presetName}","toolchainFile": "${sourceDir}/cmake/gcc-arm-none-eabi.cmake","cacheVariables": {"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"} }, {"name": "Debug","inherits": "default","cacheVariables": {"CMAKE_BUILD_TYPE": "Debug"} }, {"name": "RelWithDebInfo","inherits": "default","cacheVariables": {"CMAKE_BUILD_TYPE": "RelWithDebInfo"} }, {"name": "Release","inherits": "default","cacheVariables": {"CMAKE_BUILD_TYPE": "Release"} }, {"name": "MinSizeRel","inherits": "default","cacheVariables": {"CMAKE_BUILD_TYPE": "MinSizeRel"} } ] }
Always up-to-date file is available in
templates/CMakePresets.json
We have configured CMake with project information and are now ready to run the CMake commands.
VSCode comes with CMake Tools plugin - a great helper for CMake commands. When installed, several options are available at the bottom of the VSCode active window
As you can see, there is no Configuration Preset selected.
If you do not see such information, hit
CTRl + ALT + P
and runCMake: Quick Start
command.
Next step is to select current preset. Click on No Configure Preset Selected to open a window on top side and select your preset. I selected debug for the sake of this tutorial.
When selected, text will change to selected preset label.
Now that preset is active, every time user will modify CMakeLists.txt
file, thanks to CMake-Tools extension, VSCode will automatically invoke build generation command to apply new changes.
Our project is ready for building and linking. Unless CMake build generation step failed, we should have build directory ready to invoke ninja build system.
Next step is to hit Build button - as indicated with green rectangle. CMake will run commands:
Run build generator for selected preset
Actually build code with Ninja
If it builds well, final step on the output is print of memory use with different sections.
As a result, we got some output in build/<presetname>/
directory:
project-name.elf
file with complete executable information
project-name.hex
HEX file
project-name.bin
BIN file
project-name.map
map file
In default configuration, .hex
and .bin
files are not generated nor memory usage is displayed.
Our prepared CMakeLists.txt
file includes POST_BUILD
options, to execute additional commands after successful build.
Code is already in your CMakeLists.txt
file, so no need to do anything, just observe.
It executes command to:
Print used size of each region + final executable memory consumption
Generate .hex
file from executable
Generate .bin
file from executable
# Execute post-build to print sizeadd_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_SIZE} $<TARGET_FILE:${EXECUTABLE}> )# Convert output to hex and binaryadd_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${EXECUTABLE}> ${EXECUTABLE}.hex )# Convert to bin file -> add conditional check?add_custom_command(TARGET ${EXECUTABLE} POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${EXECUTABLE}> ${EXECUTABLE}.bin )
To disable
.bin
file generation, simply deletePOST_BUILD
line for.bin
and regenerate CMake build system commands. Generating.bin
files may have a negative effect when memory is split between internal and external flash memories. It may generate very large files (>= 2GB
) with plenty of non-used zeros.
There is a list of useful commands to keep in mind during project development:
Build changes
Clean project
Re-build project, with clean first
Flash project
Its easy to forget full syntax, rather let's create .vscode/tasks.json
file with commands list, for quick run:
{ "version": "2.0.0", "tasks": [ {"type": "cppbuild","label": "Build project","command": "cmake","args": ["--build", "${command:cmake.buildDirectory}", "-j", "8"],"options": {"cwd": "${workspaceFolder}"},"problemMatcher": ["$gcc"],"group": {"kind": "build","isDefault": true} }, {"type": "shell","label": "Re-build project","command": "cmake","args": ["--build", "${command:cmake.buildDirectory}", "--clean-first", "-v", "-j", "8"],"options": {"cwd": "${workspaceFolder}"},"problemMatcher": ["$gcc"], }, {"type": "shell","label": "Clean project","command": "cmake","args": ["--build", "${command:cmake.buildDirectory}", "--target", "clean"],"options": {"cwd": "${workspaceFolder}"},"problemMatcher": [] }, {"type": "shell","label": "CubeProg: Flash project (SWD)","command": "STM32_Programmer_CLI","args": ["--connect","port=swd","--download", "${command:cmake.launchTargetPath}","-hardRst"],"options": {"cwd": "${workspaceFolder}"},"problemMatcher": [] }, {"type": "shell","label": "CubeProg: Flash project with defined serial number (SWD) - you must set serial number first",