STM32 Cross Development Containers
Build, flash and debug CubeMX STM32 projects inside containers, from VS Code.
Your machine needs Docker and, on Windows, usbipd-win. The compiler, OpenOCD and
GDB live in the image — nothing is installed on the host, and a project builds
the same way on every machine that opens it.
The problem it removes
A CubeMX project carries a Makefile with an absolute path to whoever generated
it, a linker script, and an assumption that the right arm-none-eabi-gcc is
somewhere on PATH. Getting a colleague to a working breakpoint usually costs an
afternoon, and the result differs from yours in ways nobody notices until the
firmware does.
Declare the project once in stm32cdc.json, and the tasks, launch
configuration, IntelliSense settings and dev container are generated from it.
Your own files are never edited — CubeMX rewrites its Makefile, linker script and
sources whenever it regenerates, so everything the tool needs arrives from
outside. Regenerating from the .ioc stays a no-op.
Requirements
|
|
| Docker |
Desktop on Windows, or the engine on Linux |
| usbipd-win |
Windows only, to forward the debug probe into the container |
| Dev Containers |
the Microsoft extension, for opening a project in its container |
macOS is not supported: Docker Desktop there has no USB passthrough into
containers, so debugging cannot work.
Getting started
- Get the images — run
STM32CDC: Build images from source. The recipe
ships inside this extension, so no registry is involved. It downloads a
Debian base, its packages and the Arm toolchain; a few minutes, once.
- Open a CubeMX project and run
STM32CDC: Initialize project. It reads
the device from the .ioc and the target from the build files, asks only for
the probe and the image tags, and offers to fetch a matching SVD.
- Plug in the probe and run
STM32CDC: Attach debug probe. On Windows the
first use of each USB port raises a UAC prompt — binding needs it.
- Open the project in its dev container, then
Ctrl+Shift+B to build and
F5 to debug.
When something misbehaves, run STM32CDC: Diagnose environment before anything
else. It checks the two dozen things that actually go wrong with this stack and
names the command that fixes each one.
Prefer pulling to building? Set stm32cdc.imageRegistry to where the images
are published and run STM32CDC: Pull images. Pulling is faster and gives
everyone a byte-identical image; building is the path that works with no
registry at all.
What it supports
- Both CubeMX project shapes — Makefile and CMake, detected rather than
configured.
- Arm GNU Toolchain 14.3, pinned by version and SHA256.
- ST-Link, CMSIS-DAP and J-Link, with the probe chosen explicitly when
several are connected, remembered per workspace, and by device rather than by
USB port — so the choice survives a replug.
- Peripheral registers, with the SVD matched to your device and fetched on
request; CubeMX does not ship one.
Commands
| Command |
|
Initialize project |
Writes stm32cdc.json for a CubeMX project |
Project settings |
A form over that manifest |
Init / fix project configuration |
Regenerates the VS Code and container files |
Build project · Clean build output |
Through the generated tasks |
Attach · Detach · Select debug probe |
usbipd, including the elevation prompt |
Add peripheral register view (SVD) |
Fetches and installs the right SVD |
Open project in its dev container |
Creates the container, checking the probe first |
Pull images · Build images from source |
Either way of getting them |
Clean up containers left behind |
Stopped dev containers accumulate one per project |
Diagnose environment |
Start here when something is wrong |
All of them are also in the activity bar, grouped by what you are doing, with
the state on screen rather than behind a command: which project is active, which
build driver it uses, whether an SVD is configured, whether the probe is
forwarded right now.
Settings
| Setting |
|
stm32cdc.imageRegistry |
Where released images live, e.g. ghcr.io/acme; enables pulling instead of building |
stm32cdc.toolchainTag |
Toolchain part of the image tag; the version part comes from the extension |
stm32cdc.probes |
vid:pid values treated as debug probes — CMSIS-DAP ids differ per vendor, so add yours |
stm32cdc.svdRepository · stm32cdc.svdRef |
Where SVDs come from, pinned to a commit |
stm32cdc.wslDistro |
Distribution used for the usbipd attach |
stm32cdc.showStatusBar · stm32cdc.refreshIntervalSeconds |
Status bar and how often probe state is polled |
stm32cdc.json has a JSON schema registered for that filename, so completion,
hover documentation and error checking work without a network round trip.
Unknown fields are rejected — the point is to catch a misspelled key, which
otherwise fails silently by being ignored.
Known limitations
- One window reload the first time a dev container is created. VS Code
installs the debug extensions after the extension host has started, and
devcontainer.json has no way to ask for anything else.
- An extension can be installed in the container and still disabled, because
enablement lives in your client profile, keyed by extension id, and no API can
change it. Use Enable (Workspace), which scopes the change to that folder.
- amd64 only — the Dockerfile uses the x86_64 toolchain tarball.
- Locally built images are not byte-identical across machines: only the
toolchain is pinned by checksum, the Debian packages float. Pull published
images when that matters.
How it works, briefly
The extension runs on the host, always — usbipd is a Windows program and the
Docker CLI talks to the local daemon, and neither is reachable from inside a dev
container. It builds nothing itself: builds go through the generated VS Code
task, debugging through Cortex-Debug, and configuration through a generator that
lives in the builder image.
That last point is what keeps one build path rather than two. A task runs
wherever the window is — on the host for a local folder, inside the container for
a remote one — so the same command works in both without knowing which it is in.
Licence
MIT.