STM32 Development Env for Windows: VSCode + ARM GCC Toolchain + OpenOCD

In this tutorial I’ll shortly describe how I setup my development environment for the STM32 micros. STM32L433 will be used as an example but the following process shall be applicable to any other STM32 micro.

Step 1: Get the Toolchain

In order to make the whole development process portable (i.e. to allow other parties to build the same output binary images from the same source files) I use Docker build container. If you aren’t familiar to Docker then go ahead and read this article. After that install Docker Desktop on your computer.

Although not strictly necessary three advantages come with the Docker approach:

  • Docker offers virtualization that ensures that other people who use the same Docker image will get she same results no matter what underlying (Host) OS they use, etc..
  • Docker (if well configured) can make the firmware building process to go *much* faster than building within the HostOS due to the fact that we’ll be storing the build process half-products (like *.o files) within the file system of the container that resides in RAM
  • You don’t have to contaminate your Host OS with tools for building ARM projects

Next step would be to build the docker image on your machine. Since i’ve put the docker image to the (which is an image storing repository, something like git) the whole build process can be started by typing in:

docker pull twatorowski/gcc-arm-none-eabi

If you are interested in the details of this image then take a peek at

Step 2: We need something for debugging and uploading the firmware – OpenOCD.

Sadly enough If you, (like me) use Windows as your Host OS this step cannot be done in a Docker as Docker Desktop on Windows has no capability to forward the host’s USB to the containers so it can’t use the programming adapters like (STLink or any of the FTDI based JTAGs/SWDs). We need to install the OpenOCD within the Host OS.

Go to the and look for a release that’s applicable for you operating system (I chose this). Next, extract the zip contents to the directory of your choosing. The main executable is in the directory bin. Add this directory to the system PATH so that the openocd is accessible from command line in any directory.

Since this tutorial uses the STM32L433 as an example we may as well do a quick check whether the OpenOCD works. Find yourself a Nucleo board with any MCU from the STM32L4 familiy and type the following command:

openocd -f board/st_nucleo_l4.cfg

If the OpenOCD did not exit and your output looks something like this then you are good to go:

GNU MCU Eclipse OpenOCD, 64-bitOpen On-Chip Debugger 0.10.0+dev-00593-g23ad80df4 (2019-04-22-20:25)
Licensed under GNU GPL v2
For bug reports, read
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
adapter speed: 500 kHz
adapter_nsrst_delay: 100
none separate
srst_only separate srst_nogate srst_open_drain connect_deassert_srst
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 500 kHz
Info : STLINK V2J33M25 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.244649
Info : stm32l4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections

OpenOCD does not limit you to the STM32L4 family: if you are planning a project using other micro then take a peek under the scripts/boards directory and choose a script that suits you the most.

Step 3: Test connection between the OpenOCD and the GDB running inside a Docker container

This is just a test to tell whether the networking between the Docker containers ad you Host OS is working as expected. From the OpenOCD’s output provided above you can tell that the OpenOCD expects a connection from GDB on port 3333. That’s the default configuration and I’ll stick to it for the rest of this article.

Let’s begin with a fresh start of the OpenOCD, just like we did before (make sure that the target board you wish to develop on is connected!):

openocd -f board/st_nucleo_l4.cfg

Follow that by starting the container using the following command. You should be left with container’s shell:

docker run --rm --name test -it twatorowski/gcc-arm-none-eabi bash

Now let’s try to connect the GDB with the OpenOCD. In the shell start the gdb process:


Then within the running GDB type the following command that will establish the connection to the gdb-server part of the OpenOCD:

target remote host.docker.internal:3333

The address host.docker.internal points to your Host OS. If the output from all above looks something like this then everything is fine!

c:\>docker run --rm --name test -it gcc-arm-none-eabi sh
/ # arm-none-eabi-gdb
GNU gdb (GDB) 8.3
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-musl --target=arm-none-eabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
Find the GDB manual and other documentation resources online at:

For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) target remote host.docker.internal:3333
Remote debugging using host.docker.internal:3333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x08006c40 in ?? ()

You can test some basic commands here. For example, let’s try to crank up the Nucleo SWD interface clock speed to it’s maximum using the following command:

(gdb) monitor adapter_khz 4000
adapter speed: 4000 kHz

Step 4: Get the IDE – VSCode

This is as simple as obtaining installing the Visual Studio Code from here: Start the VSCode editor.

It is worth to install couple of plugins that make the development process pleasant. Here’s my list in the alphabetical order:

  • C/C++ – syntax highlighting and debugging capabilities.
  • Code Spell Checker – helps to avoid spelling mistakes in code and comments.
  • Cortex-Debug – ARM Cortex-M GDB Debugger support for VSCode, can display Peripherals, Core registers, Function Disassembly, Memory View and so on, BUT useful only when you have the arm-none-eabi-gdb installed in your host OS. Sadly enough does not support the gdb pipe launch which means that it cannot communicate with the gdb residing in Docker container.
  • Doxygen Documentation Generator – helps to create Doxygen headers for new files, functions, etc…
  • Head-File-Guard – Generator of the infamous #ifndef ... #define ... #endif header file guard
  • Linker Script – Language support (syntax coloring) for the linker scripts
  • TODO Highlight – Highlights comments that contain words like TODO: or FIXME:, can generate a list of TODOs and FIXMEs.

Step 5: Prepare a project within the VSCode that uses all we did above

Start by downloading the source code that you want to build. Let’s proceed with the STM32L433 startup project. Then go to it’s main directory.

git clone
cd startup-stm32l433

NOTE: Make sure that your project directory has no whitespace characters.

VSCode uses .vscode folder inside the main project directory to store all tasks (like Build, Clean) and launch configurations (like Debug). I’ve prepared all the files needed so let’s start with a clean directory and get these:

git clone .vscode

Now we are officially ready to start coding in VSCode. Start the VSCode in the project directory. After the VSCode has launched. If you press shift+ctrl+b you’ll see the picker with four options. Two of these must be invoked before anything gets uploaded (but only once in a life cycle of the VSCode) into the target device: Start OpenOCD and Start Docker.

In order to have the includes from the docker’s arm-none-eabi-gcc toolchain within the reach of VSCode a special task was prepared: Copy Includes This is to be executed only once per project and will extract all the *.h files to the project’s .includes directory. For the sake of Intellisense settings.json file was prepared so that files from the .includes can be resolved within the project.

After that whenever you’ll press F5 the firmware will get build, uploaded and the execution will start. Don’t forget to add breakpoints to see the debugging in action 🙂

If you want to change the name of the output *.elf file then don’t forget to update your .vscode/launch.json as well.

Happy debugging!