
During my classes at WRUST (Wrocław University of Science and Technology), I teach about Robot Controllers where most of the course related to embedded systems is centred around STM32 ecosystem. For quite some time, STM32CubeIDE software was used there. However, the version that is being used there is quite old (1.4.0). The newer versions of the software offer a lot of new features that are useful during debugging. On the other hand, any newer version than 1.4.0 has issues with remote debugging, which is a core part of RemoteLab. Fortunately enough, there is a good substitution that connects both words: new features and operational robustness. The solution is based on VS Code and uses cmake as a project build tool.
Before going into details how to set it up, let us investigate the root cause with STM32CubeIDE. Whenever you want to setup a remote debugging session, like in the figure below. You will receive an alert that the ST device verification process has failed.

Above in the debug configuration, we set the debug probe as ST-LINK (OpenOCD). OpenOCD is a debug server that runs on RemoteLab making ST dev boards available remotely. Access to each board is granted through individual port number, as in the case above 3008. After connecting to the board, you have a debug session established as if the development board were next to you. However, each version of STM32CubeIDE after 1.4.0 has some problems. Instead of establishing a debug session, you will receive an alert.

The above message informs about the failed verification stage of an original ST device (a number of Nucleo-L476RG boards are connected via RemoteLab). This issue was quite often encountered when using a STM32F103 clone microcontroller on so-called BluePill boards. The root cause for this problem was a difference in register read value that did not match expected value. There were workarounds which disabled the check (or compared against different values) through openocd scripts. In turn, this behaviour is not related to remote debugging session since openocd is already working on a RemoteLab server with an original board. Furthermore, when a debug session is established using a different IDE than STM32CubeIDE, everything works fine. In this blog post I am going to present to you how to overcome this specific issue and how to setup the environment to seamlessly work in both STM32CubeIDE and VS Code environments. Furthermore, it is still possible to work with STM32CubeIDE in version 1.4.0; however, the software (in this specific version) is no longer available to download.
Environment Setup
There are a few prerequisites; a list below:
- VS Code, as IDE of choice,
- Cortex-Debug plugin (marus25.cortex-debug, version 1.12.1),
- STM32 VS Code Extension plugin (mostly optional, stmicroelectronics.stm32-vscode-extension, version 2.1.1),
- STM32CubeCLT (en.st-stm32cubeclt_1.18.0_24403_20250225_1636_x86_64.exe, version 1.18.0),
- STM32CubeMX (optional, en.stm32cubemx-lin-v6-14-0.zip, version 6.14.0).
The above specific versions of the software were provided, so in case something does not work, it would be easy to recreate it. It is good to start with the installation of STM32CubeCLT. This is a command line tool set that contains the necessary tools to enable different environments to work with STM32 devices. In our case, VS Code.
In VS Code these two plugins must be installed: Cortex-Debug and STM32 VS Code Extension. Cortex-Debug extends the debugging capabilities of VS Code to support embedded system debugging. The STM32 VS Code Extension is the second plugin that adds additional integration between STM32CubeCLT and VS Code but is optional. When you would like to create an empty project from scratch or launch CubeMX from VS Code, then I would recommend to install it. Later, I will point out differences which make it necessary to install.
STM32CubeMX is software that allows you to automatically generate code based on visual configuration of MCU peripherals. It allows one to create STM32CubeIDE project, thus it is useful. Furthermore, STM32CubeIDE already has this software integrated but not as a standalone version.
Project setup
Here I assume following that you already have a STM32CubeIDE project generated and you would like to compile and run it locally, but also have possibility to deploy the exact same project remotely. At this point in time there are two options. You can either create an empty project with STM32 VS Code Extension, integrate it with your current Cube project, or you can you can manually add some files that will be generated by the plugin during creation of the project itself. I will focus on the second option since both of them overlap considerably and eventually additional changes are needed anyway.
Since VS Code integrates very well with CMake build projects, it is necessary to provide CMake relevant files. Each such project contains at least one CMakeList.txt file at the root of the project tree. This file explains how to build the project and its dependencies. Furthermore, there are two additional files that would be located in cmake directory in the root of the project. These two files are gcc-arm-none-eabi.cmake and vscode_generated.cmake. First one defines the toolset, what command should be use for C compiler or toolchain prefix. The second one defines which compiler flags should be used or where the source files are located. In the end, these three files could be merged into a single one: MakeList.txt. However, this is the project structure, as it would be generated with STM32 plugin from VS Code.
CMakeLists.txt
The file includes both files with the following commands:
include("cmake/gcc-arm-none-eabi.cmake")
and
include("cmake/vscode_generated.cmake")
Currently, the only thing to look for when trying to adjust for a different microcontroller is located below.
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
${symbols_SYMB}
$<$<COMPILE_LANGUAGE:C>: ${symbols_c_SYMB}>
$<$<COMPILE_LANGUAGE:CXX>: ${symbols_cxx_SYMB}>
$<$<COMPILE_LANGUAGE:ASM>: ${symbols_asm_SYMB}>
# Configuration specific
$<$<CONFIG:Debug>:DEBUG>
$<$<CONFIG:Release>: >
DEBUG
USE_HAL_DRIVER
STM32L476xx
)
target_compile_definitions() allows one to define compiler-specific definitions. The last three definitions are of the most interest. DEBUG will inform the build system that this is a debug target, so during compilation some debug-specific code will be enabled. The second is USE_HAL_DRIVER that enables usage of HAL libraries. Finally, there is STM32L476xx that defines the type of microcontroller that will be used. If configured incorrectly, it simply might not work because different microcontrollers have different features and memory layout.
A different command target_compile_options() allows one to enable compiler options. You might notice that the original file enables debug symbols for the Debug target. It is different from DEBUG provided with target_compile_definitions() and results in different compiled code.
Also, an important entry in the file is:
project(RemoteLab-Test)
The above defines the project name. When building the project in Debug configuration a firmware file will be created under build/RemoteLab-Test.elf or build/debug/RemoteLab-Test.elf if, for example, using presets with binaryDir option set.
gcc-arm-none-eabi.cmake
Besides what was already said about this file, it defines some variables like FLAGS which, in turn, are used in CMAKE_C_FLAGS and CMAKE_CXX_FLAGS (for C++ projects). Generally, it sets up and configures the cross-compilation compilation environment.
An additional remark is own to the toolchain configuration. If the toolchain path is inside PATH environment variable, then the below part can be skippet. However, if the toolchain you are using is not in the PATH environment variable or you have multiple toolchains then a slight modification to the file is required.
set(TOOLCHAIN_PREFIX "arm-none-eabi-")
The above line defines the prefix for the toolchain, but it will only work if it can be found. To not modify reset of toolchain file gcc-arm-none-eabi.cmake a slight modification needs to be introduced.
set(TOOLCHAIN_DIR "/c/ST/STM32CubeCLT_1.18.0/GNU-tools-for-STM32/bin/")
set(TOOLCHAIN_PREFIX "${TOOLCHAIN_DIR}arm-none-eabi-")
Thanks to this modification, CMAKE_C_COMPILER, CMAKE_CXX_COMPILER and others later in the file are not required to be altered. The advantage of this solution is that a specific toolchain can be provided. When compiling under different OS like Windows and Linux, keep in mind that the path to toolchain directory should be path format specific for a given operating system. Definitely, you should refrain from using backslashes on Windows when compiling within MSYS2 bash.
vscode_generated.cmake
This file configures the project in relation to the cmake project. There are a few specific variables that are being populated.
set(linker_script_SRC ${linker_script_SRC}
${CMAKE_CURRENT_SOURCE_DIR}/STM32L476RGTX_FLASH.ld
)
Sets the linker_script_SRC variable. This variable is in the turn used in CMakeLists.txt file for target_link_options() to set target linker options. It should hold the path of the linker script file. It is specific for each microcontroller, so if a different MCU is being used or the file is in a different location than the root of your project, the above should be adjusted.
Then the source files are held in sources_SRCS. The first starting script is given as
set(sources_SRCS ${sources_SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/Core/Startup/startup_stm32l476rgtx.s
)
Once again, if a different MCU is being used, the line should be adjusted to reflect the correct startup source file.
Then there are two lines which load all the source files in the project.
file(GLOB core_src "${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/*.c")
set(sources_SRCS ${sources_SRCS} ${core_src})
file(GLOB hal_src "${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32L4xx_HAL_Driver/Src/*.c")
set(sources_SRCS ${sources_SRCS} ${hal_src})
Since file(GLOB) is being used, the core_src variable will be populated with all occurrences of source file matching the provided pattern. The path is ignored, as it only searches for source files *.c at a given level. It does not perform a recursive search. So, if some additional source files are kept in e.g. external/Src then following two lines should be added.
file(GLOB external_src "${CMAKE_CURRENT_SOURCE_DIR}/external/Src/*.c")
set(sources_SRCS ${sources_SRCS} ${external_src})
Finally, the hal_src variable holds all HAL library source files. In the end, the sources_SRCS variable grows holding all source files necessary to build a given target.
To define where include files are located, a include_c_DIRS needs to be populated with all include paths. This variable is then used in CMakeFiles.txt under target_include_directories() function.
set(include_c_DIRS ${include_c_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/Core/Inc
${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32L4xx_HAL_Driver/Inc
${CMAKE_CURRENT_SOURCE_DIR}/Drivers/CMSIS/Device/ST/STM32L4xx/Include
${CMAKE_CURRENT_SOURCE_DIR}/Drivers/CMSIS/Include
${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32L4xx_HAL_Driver/Inc
${CMAKE_CURRENT_SOURCE_DIR}/Drivers/STM32L4xx_HAL_Driver/Inc/Legacy
)
Usually, the set of include directories is enough for a standard STM32 project. It extends the include search paths with all necessary entries related to CMSIS and HAL driver libraries.
Project Configuration
An archive with all the files mentioned can be found here. The files should be copied to the tree of the project root directory and adjusted if necessary. An example tree structure is shown in the figure below.
There is an additional file called CMakePresets.json. It is a file in JSON format that holds configuration and build presets. It allows you to easily switch between different configurations like debug and release.
Compilation without presets
cmake -S . -B build
The build directory will be created with the necessary configuration.
cmake --build build -j4
Then comes the compilation on build directory with 4 parallel jobs. The output binary is present under build/RemoteLab-Test.elf.
Compilation with presets
Presets simplify the process and allow multiple configuration present in the build directory simultaneously.
cmake -S . --preset debug
The build directory is being created with the debug subdirectory according to the debug preset inside CMakePresets.json.
cmake --build build -j4 --preset debug
The build process for the debug preset is conducted. The output binary file will be available under build/debug/RemoteLab-Test.elf.
Debugging
In VS Code, debug configuration is stored inside .vscode/launch.json file.
Typically, when creating the project using STM32 VS Code Extension plugin, the launch.json file will be populated with some default configurations like the one below:
{
"name": "Attach to Microcontroller - ST-Link",
"cwd": "${workspaceFolder}",
"type": "cortex-debug",
"executable": "${command:cmake.launchTargetPath}",
// Let CMake extension decide executable: "${command:cmake.launchTargetPath}"
// Or fixed file path: "${workspaceFolder}/path/to/filename.elf"
"request": "attach",
"servertype": "stlink",
"device": "STM32L476RGTx", //MCU used
"interface": "swd",
"serialNumber": "", //Set ST-Link ID if you use multiple at the same time
"runToEntryPoint": "main",
"svdFile": "${config:STM32VSCodeExtension.cubeCLT.path}/STMicroelectronics_CMSIS_SVD/STM32L476.svd",
"v1": false, //Change it depending on ST Link version
"serverpath": "${config:STM32VSCodeExtension.cubeCLT.path}/STLink-gdb-server/bin/ST-LINK_gdbserver",
"stm32cubeprogrammer":"${config:STM32VSCodeExtension.cubeCLT.path}/STM32CubeProgrammer/bin",
"stlinkPath": "${config:STM32VSCodeExtension.cubeCLT.path}/STLink-gdb-server/bin/ST-LINK_gdbserver",
"armToolchainPath": "${config:STM32VSCodeExtension.cubeCLT.path}/GNU-tools-for-STM32/bin",
"gdbPath":"${config:STM32VSCodeExtension.cubeCLT.path}/GNU-tools-for-STM32/bin/arm-none-eabi-gdb",
"serverArgs": [
"-m","0",
],
/* If you use external loader, add additional arguments */
//"serverArgs": ["--extload", "path/to/ext/loader.stldr"],
},
The default configuration allows us to upload firmware to MCU and start debug session. However, the core of this blog post is to discuss the remote debugging capabilities of VS Code in terms of STM32.
Remote Debugging
Remote debugging configuration looks similar, but there are a few exceptions.
{
"name": "Launch Program remote XXXX",
"cwd": "${workspaceRoot}",
"executable": "${workspaceFolder}/build/debug/RemoteLab-Test.elf",
"request": "launch",
"type": "cortex-debug",
"servertype": "external",
"gdbPath" : "arm-none-eabi-gdb",
"gdbTarget": "127.0.0.1:XXXX",
"device": "STM32L476RGTx",
"svdFile": "c:/ST/STM32CubeCLT_1.18.0/STMicroelectronics_CMSIS_SVD/STM32L476.svd",
"runToEntryPoint": "main",
"showDevDebugOutput": "raw"
}
The above configuration uses paths which are not dependent on STM32 VS Code Extension plugin. You will not see any references to “${config:STM32VSCodeExtension.cubeCLT.path}”, thus this configuration does not require the presence of the plugin in VS Code.
There are a few specific values, such as type where it was set to cortex-debug. This configuration makes use of cortex-debug plugin; hence it allows for remote debugging. The servertype option informs the plugin that the debug session will be a remote one, connecting to external resource rather than launching debug server locally. gdbPath defines path to the gdb application that comes with the CLT package, here a full path can be provided.
Finally, the most important thing is set with gdbTarget. It defines the endpoint target. Hence, if the gdb connection was tunnelled to a local host (127.0.0.1), it should be available under the XXXX port. The XXXX phrase should be replaced with actual port number.
The configuration is virtually the same as in the case of debugging Raspberry Pi Pico devices. Only changes were made to device and svdFile (and executable).
Final remarks
The benefit of the presented configuration is the simultaneous coexistence of two types of projects. The project can be developed using STM32CubeIDE locally or it can be developed using VS Code both locally and remotely. If one would want to fully switch to VS Code then it would be recommended to instal STM32CubeMX which is a visual-based code generator. CubeMX is integrated with CubeIDE, so there is no need to install it. However, when using VS Code, CubeMX should be installed to have a similar experience.
The files discussed throughout the post can be found here.
These files can be used as a template to easily adjust for your project. Generally speaking, since the project is now based on cmake, all features provided with cmake are now available!