"scaffold β write β build β flash β verify" β STM32F4 firmware, programmatically, no IDE GUI, agent-drivable.
A self-contained scaffold + MCP server for generating, building, flashing, and observing STM32F4 firmware programmatically β designed to be driven by an autonomous agent that auto-modifies a robot's firmware at runtime, without ever opening the STM32CubeIDE GUI. It borrows the compiler, programmer, and driver libraries bundled inside STM32CubeIDE, so one IDE install supplies everything.
- Target MCU: STM32F407VG (CortexβM4F) β retargetable via
stm32.config.json - Reference board: STM32F407GβDISC1 (onβboard STβLINK/V2, 4 user LEDs on PD12β15)
- Toolchain: GNU Tools for STM32 (GCC 14.3.1, binutils 2.44), bundled in
C:\ST\STM32CubeIDE_2.1.1\STM32CubeIDE - HAL/CMSIS: referenced from the installed pack
STM32Cube_FW_F4_V1.28.3 - Build systems: GNU Make and CMake+Ninja (both verified, identical output)
- Flash/upload: STM32CubeProgrammer CLI v2.22.0 over STβLINK/SWD
- Observe (HIL): serial/VCP and live memory over SWD (oneβshot + continuous OpenOCD streaming)
- MCP: Python server in
mcp/exposing 23 tools (create / build / flash / serial / liveβmemory)
π Brand new to STM32 or this project? Start with
InstructionsGuideForDummies.mdβ a complete, zeroβassumptions walkthrough from installing everything β loading code β building β flashing β verifying on a real STM32F407GβDISC1.
| Stage | Capability |
|---|---|
| Scaffold | Clone a complete, buildable project (default firmware = an LEDβchase blinky) to any location. |
| Author | Drop generated .c/.h into Core/Src and User/Src β the build autoβglobs them, no Makefile surgery. |
| Build | Bundled arm-none-eabi-gcc β .elf + .hex + .bin + .map, via Make or CMake+Ninja. |
| Flash | Upload to the MCU via STM32_Programmer_CLI over STβLINK/SWD, with verify + reset. |
| Observe | Read/stream live RAM, peripherals, or named ELF symbols over SWD without halting the target; talk to the board's serial VCP. |
| Retarget | Switch to another STM32F4 part by editing stm32.config.json + startup/linker files. |
The HAL/CMSIS driver sources are referenced from the firmware pack (not copied), so each project copy is only a few KB. The full HAL is compiled into every build, so any peripheral the agent uses already links β no perβproject Makefile changes.
STM32TemplateProject/
ββ README.md # this file
ββ InstructionsGuideForDummies.md # step-by-step beginner manual (install β flash β verify)
ββ stm32.config.json # SINGLE SOURCE OF TRUTH (MCU, tool paths, flash/serial/swd opts)
ββ config.mk # Make-side defaults (mirror of the JSON)
ββ Makefile # GNU Make build (make / make flash / make clean / β¦)
ββ CMakeLists.txt # CMake build
ββ cmake/
β ββ gcc-arm-none-eabi.cmake # CMake toolchain file (arm-none-eabi)
ββ tools/
β ββ discover_toolchain.ps1 # find bundled tool paths (robust to IDE updates)
β ββ vendor_hal.ps1 # optionally copy HAL/CMSIS in for self-containment
ββ mcp/
β ββ stm32_mcp_server.py # the MCP server β 23 tools <-- integrate with your agent
β ββ smoke_test.py # local createβbuild smoke test
β ββ requirements.txt / pyproject.toml
β ββ claude_desktop_config.example.json
β ββ README.md # full MCP tool reference
ββ build/ # build outputs (.elf/.hex/.bin/.map) [git-ignored]
ββ project/ # the STM32 firmware source tree (agent fills this)
ββ Core/{Inc,Src,Startup}/ # main.c, IT, MSP, system, startup
ββ User/{Inc,Src}/ # application modules go here
ββ Drivers/ # empty (HAL/CMSIS referenced from the pack)
ββ STM32F407VGTX_FLASH.ld # linker script (1 MB FLASH @0x08000000, 128 KB RAM)
create_project write_source build flash / observe
ββββββββββββββ ββββββββββββ βββββ βββββββββββββββ
clone template β Core/Src/main.c β arm-none-eabi-gcc β STM32_Programmer_CLI
to <dest>/<name> User/Src/*.c (make | cmake) over ST-LINK/SWD
β β
.elf .hex .bin .map LEDs chase Β· SWD reads
serial VCP Β· live memory
- The template is a complete, buildable project (default = an LEDβchase blinky) plus the build glue (Make/CMake) and the MCP server.
- The MCP
create_projectcopies the whole template to a new location. - The agent calls
write_sourceto drop generated.c/.hfiles intoproject/. buildruns the bundled GCC β produces.elf+.hex+.bin.flashuploads the binary to the MCU;serial_*/read_memory/live_memory_*then prove it's running on real silicon.
Tool paths are autoβdiscovered by globbing the STM32CubeIDE plugins/ directory,
so the server keeps working after IDE updates rename the versionβstamped folders.
Values in stm32.config.json > toolchain override discovery.
Open a terminal in this folder. Tool dirs are baked into config.mk; override if your
IDE path differs (see tools/discover_toolchain.ps1, which prints the exact paths).
# build (GNU Make)
& "C:\ST\STM32CubeIDE_2.1.1\STM32CubeIDE\plugins\com.st.stm32cube.ide.mcu.externaltools.make.win32_2.2.100.202601091506\tools\bin\make.exe"
# -> build\stm32app.elf / .hex / .bin / .map (+ prints size)
# build (CMake + Ninja)
$cmake = "C:\ST\STM32CubeIDE_2.1.1\STM32CubeIDE\plugins\com.st.stm32cube.ide.mcu.externaltools.cmake.win32_1.1.101.202603101401\tools\bin\cmake.exe"
& $cmake -S . -B build-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-arm-none-eabi.cmake
& $cmake --build build-cmake
# flash (with an ST-LINK + board attached)
& "...\make.exe" flashMake targets: make Β· make flash Β· make erase Β· make reset Β· make size Β·
make clean Β· make print-CONFIG.
Tip: put the gcc
tools\binand themakeplugin'stools\binonPATH(the latter supplies thesh/mkdir/rmthe recipes need) and you can just typemake,make flash, etc. Full PATH setup is in the Instruction Guide.
Install + register the server (details in mcp/README.md):
cd C:\Development\STM32TemplateProject\mcp
python -m venv .venv ; .\.venv\Scripts\Activate.ps1 # optional
pip install -r requirements.txt # mcp + pyserialRegister it with your MCP client (e.g. Claude Desktop β merge
mcp/claude_desktop_config.example.json into %APPDATA%\Claude\claude_desktop_config.json),
then a typical agent flow is:
create_project(name="leg_ctrl", dest_parent="C:/robot/fw") -> project_dir
write_source(project_dir, "Core/Src/main.c", "<generated firmware>")
write_source(project_dir, "User/Src/motor.c", "...") # optional modules
build_and_flash(project_dir) # -> programs the MCU
live_memory_start('["g_blink_count","g_led_index"]', project_dir=project_dir, interval_ms=250)
# ... watch the counters move on the running board ...
live_memory_stop(session_id)
All tools return a JSONβable dict with ok/returncode; none raise. write_source
/read_source refuse paths that escape project/.
Environment
| Tool | Purpose |
|---|---|
get_config |
Resolved config + discovered toolchain paths + toolchain_ok flag. |
discover_toolchain_tool(ide_root="") |
Locate gcc/make/cmake/ninja/programmer in a CubeIDE install. |
Project lifecycle
| Tool | Purpose |
|---|---|
create_project(name, dest_parent, overwrite=False) |
Clone the template to <dest_parent>/<name>. Returns project_dir. |
write_source(project_dir, rel_path, content) |
Write/overwrite a file under project/ (e.g. Core/Src/main.c). |
read_source(project_dir, rel_path) |
Read a file back. |
list_sources(project_dir) |
List everything under project/. |
clean(project_dir, system="make") |
Remove the build output dir. |
Build & flash
| Tool | Purpose |
|---|---|
build(project_dir, system="make", jobs=8, clean_first=False) |
Compile+link+objcopy β .elf/.hex/.bin; returns artifacts + size. |
list_artifacts(project_dir, system="make") |
Paths to the latest build outputs. |
flash(project_dir, system="make", binary="bin") |
Upload via STM32_Programmer_CLI (STβLINK/SWD). |
build_and_flash(project_dir, system="make", jobs=8) |
Build then flash in one call (flashes only on build success). |
erase(project_dir="") / reset(project_dir="") |
Massβerase / hardwareβreset the MCU. |
Serial / VCP (HIL)
| Tool | Purpose |
|---|---|
serial_list_ports() |
List COM ports; flags STβLINK VCP ports (USB VID 0x0483). |
serial_connect(port, baud=0) |
Open a VCP/UART (e.g. COM7). Baud from serial config if 0. |
serial_send(port, data, read_response=True, β¦) |
Send a line, optionally read the reply. |
serial_read(port, timeout=2.0, β¦) |
Read async board output (boot banner / logs). |
serial_disconnect(port) |
Close the connection. |
Live memory over SWD (HIL)
| Tool | Purpose |
|---|---|
read_memory(address|symbol, project_dir, β¦, count, width) |
Read live RAM/peripherals of a running target (HOTPLUG, no reset). |
write_memory(value, address|symbol, β¦) |
Write live memory of a running target. |
live_memory_start(variables, project_dir, interval_ms=500, β¦) |
Stream variables continuously via a persistent OpenOCD β session_id + JSONL. |
live_memory_read(session_id, last_n=10) |
Most recent samples from the ring buffer. |
live_memory_stop(session_id) |
Stop the session, kill OpenOCD, release the STβLINK. |
After flashing, the agent can talk to and observe the running board:
-
Serial / VCP β
serial_connect("COM7")βserial_send(port, "PING")βserial_disconnect(port). Baud / lineβending defaults come from theserialblock instm32.config.json. Needspyserial; the server still starts without it and the serial tools return a clear "not installed" message. -
Live memory over SWD β
read_memory/write_memoryuseSTM32_Programmer_CLIinmode=HOTPLUG, which attaches to a running target without resetting it. Address a raw location (address="0x20000000") or a variable by name (symbol="g_blink_count", resolved from the build's.elfvia the bundledarm-none-eabi-nm). These are oneβshot (~0.5β1 s each). -
Continuous streaming β for subβsecond polling,
live_memory_*runs a persistent OpenOCD connected over its TCL RPC port and polls your variables on a background thread β without halting or resetting the target. Samples land in a ring buffer (live_memory_read) and a JSONL file.
β οΈ Two rules for live monitoring:
- A live session owns the STβLINK β
live_memory_stopit beforeflash,read_memory, orwrite_memory(only one consumer of the probe at a time). A serial VCP and SWD memory access can run together (separate USB interfaces).- It uses ST's
interface/stlink-dap.cfg(thedapdirectdriver). The olderhlastlink.cfgtrips an "infinite eval recursion" bug in ST'sswj-dp.tclwith this OpenOCD build βstm32.config.json > swd.openocd_interface_cfgis set accordingly. Leave it.
- Plug into the miniβUSB (STβLINK) port β it powers and programs the board.
- Four user LEDs on GPIOD: PD12 green Β· PD13 orange Β· PD14 red Β· PD15 blue
(defined in
project/Core/Inc/main.h). - The default
main.cruns on the internal HSI 16 MHz (no PLL) so it's correct on any F407 board regardless of crystal, and "chases" the four LEDs while incrementing twovolatilecounters (g_blink_count,g_led_index) that the liveβmemory tools watch over SWD. Production firmware should configure the PLL for full 168 MHz.
Edit stm32.config.json (mcu + firmware_pack), swap the startup file in
project/Core/Startup/ and the .ld linker script, and adjust -mcpu/-mfpu in
config.mk / the CMake cache if the core differs. Both build systems read those values.
For families other than F4 you'd also point firmware_pack at the matching
STM32Cube_FW pack and update the CMSIS include paths. (Full steps in the
Instruction Guide, PART J.)
makebuild: 95 objects βstm32app.elf(text 4328 / data 20 / bss 1572),.hex,.bin, exit 0cmake+ninjabuild: identical firmware size, exit 0- MCP
create_projectβbuild: exit 0, all artifacts produced - MCP
flash: correctly invokes CubeProgrammer v2.22.0 (errors cleanly when no probe attached) - Endβtoβend HIL on an STM32F407GβDISC1: build β flash β liveβmonitor
g_blink_countincrementing while the LED chase runs on PD12β15 β clean probe release
| You want to⦠| Edit / read |
|---|---|
| Change the program | project/Core/Src/main.c |
| Add a code module | project/User/Src/*.c + project/User/Inc/*.h |
| Turn on a peripheral | project/Core/Inc/stm32f4xx_hal_conf.h |
| Change MCU / tool paths / flash opts | stm32.config.json (MCP) + config.mk (Make) |
| Discover tool paths on a new machine | tools/discover_toolchain.ps1 |
| Make a project self-contained (vendor HAL) | tools/vendor_hal.ps1 |
| Full beginner walkthrough | InstructionsGuideForDummies.md |
| MCP tool deep-dive | mcp/README.md |
| Symptom | Fix |
|---|---|
arm-none-eabi-gcc: No such file |
Wrong TOOLCHAIN_BIN in config.mk β reβrun tools/discover_toolchain.ps1. |
fatal error: stm32f4xx_hal.h: No such file |
Wrong/absent FW_PACK β install the FW_F4 pack (see the Guide, PART A2). |
undefined reference to HAL_xxx_... |
Enable that module's #define HAL_xxx_MODULE_ENABLED in stm32f4xx_hal_conf.h. |
Recipe fails on mkdir/rm/sh |
Add the make plugin's tools\bin to PATH (it ships those helpers). |
flash β "No STLink detected" |
Use the miniβUSB port; check the driver in Device Manager; STM32_Programmer_CLI -l. |
| Live tools β "probe busy" | live_memory_stop first β only one STβLINK consumer at a time. |
A fuller troubleshooting table lives in
InstructionsGuideForDummies.md#troubleshooting.