STM32 / Cortex-M Workflow (Preview)
STM32 (and Cortex-M in general) is a Champion-tier preview in esphome.cloud. The flash / monitor / debug-probe surface is in espctl today; the cloud build executors land tier by tier.
Status (2026-05-24). What works locally now: discover and claim a USB debug probe, flash a pre-built Cortex-M flash bundle via
probe-rs, and stream RTT over the same probe. What is still rolling out: the three remote-build paths (Path A / B / C) that produce those flash bundles in the cloud. Local-ELF + cloud archive (Path C) is the first to land. NUCLEO-G431RB is the first board family; F7 / H7 / L4 follow.
Tier gating
| Tier | STM32 access |
|---|---|
| Hobby / Maker / Master | ESP32 only |
| Champion + | STM32 build executor unlocked |
| Self-hosted | All targets (your own runners) |
The pricing page at esphome.cloud/pricing has the current executor matrix.
The three paths
| Path | Source | Where it builds | Cloud does |
|---|---|---|---|
| A · CMake + arm-gcc | CMake project (structurally compatible with CubeMX output; you run the CubeMX GUI locally) | Remote agent | Configure + build with arm-gcc-none-eabi + STM32CubeG4 HAL, sign, package as flash bundle |
| B · Cargo + embassy-rs | Cargo project, #[no_std], embassy-rs | Remote agent | cargo build --release against the target triple, sign, package |
| C · Local ELF + cloud archive | You compile the ELF on your machine | Local for ELF; remote for archive | arm-none-eabi-objcopy to produce raw flash images + sign + package |
esphome.cloud explicitly does not ship a desktop IDE or GUI. CubeMX, CubeIDE, Keil, PlatformIO all stay on your machine. The cloud only takes:
- a CMake / Cargo project (Path A / B), or
- a finished ELF (Path C).
The output is the same in all three cases: a flash_bundle.tar.gz
that espctl flash can consume.
Internally the three paths surface in espctl skills (once shipped)
as three named recipes — cortexm_cube_build (Path A),
cortexm_embassy_build (Path B), cortexm_rust_elf (Path C) — so
an agent can pick the right one without text matching.
What each path produces
Whichever path produces the bundle, the cloud emits the same set of
artifacts inside flash_bundle.tar.gz:
| Artifact | Purpose |
|---|---|
firmware.elf | Unstripped application ELF. Retained for espctl elf <build_id> retrieval and for RTT debugging on the user’s side. |
firmware.bin | Raw flash image at the linker-script base address. |
firmware.hex | Intel HEX form of the same image. |
manifest.json | Flash-bundle manifest — chip, probe_rs_chip, flash segments + offsets, SHA-256 hashes, build_provenance. |
sbom.json | Toolchain pins (Path A / B) or empty toolchain section (Path C — you supplied the ELF) plus declared project dependencies. |
The whole bundle is signed before delivery. espctl flash and
espctl monitor --bundle consume any of the three path outputs
interchangeably.
Toolchain pinning + reproducibility goals
The remote build store pins toolchain versions per store release and reuses them across every Cortex-M build in that release. You do not pick them per-build — picking is per-store, on the maintainer side.
| Component | Pinned version (current store release) |
|---|---|
arm-none-eabi-gcc (Path A) | 13.2.0 |
Rust + cargo (Path B) | 1.85.0 |
| STM32CubeG4 HAL (Path A, NUCLEO-G431RB) | bundled per store release |
probe-rs (client-side, used by espctl flash / monitor --rtt) | tracks the version baked into your local espctl |
Reproducibility goal:
- L1 — text section byte-identical across rebuilds with the same source tree and toolchain pin. Mandated.
- L2 — full flash image byte-identical across rebuilds. The
goal is 90% of production scenarios.
SOURCE_DATE_EPOCHis honored; remaining 10% are usually third-party crates with embedded timestamps or non-determinstic config emission.
Pin shifts only happen across store-manifest revisions, and a pin shift is published in the store changelog before it lands.
CubeMX project layout — zero CMakeLists.txt edits
For Path A, the standard CubeMX-generated CMakeLists.txt works
as-is. The toolchain file the cloud injects attaches the
firmware.elf → firmware.bin / firmware.hex + signing pipeline to
every executable target automatically, via CMake’s
CMAKE_PROJECT_INCLUDE_BEFORE hook. You do not need to add
add_cortexm_firmware(...) calls.
If your project already defines its own CMAKE_PROJECT_INCLUDE or
CMAKE_PROJECT_INCLUDE_BEFORE, the cloud appends to it — your
existing hooks are preserved, not replaced.
Net effect: a freshly exported CubeMX project from a CubeG4 template compiles and packages on the cloud with zero modifications.
What works today (local Cortex-M surface)
Everything below works right now against any pre-built Cortex-M flash bundle, regardless of how the bundle was produced.
1. Provision the host
Make probe-rs able to talk to debug probes without root:
espctl provision-host --dry-run # print the plan
espctl provision-host # apply it (Phase 3+; Phase 0 is dry-run only)
On Linux this writes udev rules; on Windows it prints a Zadig
pointer; macOS works out of the box. See
CLI Utilities — espctl provision-host.
2. List connected probes
espctl probes list # table
espctl probes list --json # for piping into an agent
Returns vendor / product / serial / VID:PID and the probe-rs
identifier you’ll pass to --probe. See
CLI Utilities — espctl probes.
3. Flash a Cortex-M bundle
espctl flash flash_bundle.tar.gz \
--probe auto # or VID:PID[:SERIAL]
--chip STM32G431RBTx # override bundle's probe_rs_chip if your variant differs
# --no-verify # skip probe-rs verify step
espctl flash reads the bundle’s FlashTarget:
Esp32 { .. }→ dispatches through pure-Rustespflash(USB-serial, requires--port).CortexM { probe_rs_chip, elf, .. }→ dispatches throughprobe-rs(USB, no serial port).--probe,--chip, and--no-verifyapply;--portand--baudare ignored.
ADR-007 defines the FlashTarget enum. See
Firmware & Flash.
4. Monitor over RTT
espctl monitor --bundle flash_bundle.tar.gz # mode auto-resolves to RTT from the bundle
espctl monitor --rtt --chip STM32G431RBTx # explicit RTT without a bundle
When --bundle is supplied, espctl monitor reads the bundle’s
FlashTarget and:
CortexM→ defaults to RTT mode, auto-extracts the ELF forprobe-rs attach.Esp32→ defaults to UART mode (existing serial-port flow).
Force a specific mode with --rtt or --uart. --rtt and --uart
are mutually exclusive.
What is rolling out (the cloud build paths)
The three paths land tier by tier. The remote build endpoints share
the same auth flow, signing pipeline, and flash-bundle output
schema with the ESP32 / ESP-IDF path; the difference is the
toolchain image and the bundle’s FlashTarget field.
When a path is live, the public surface looks like:
# Path A or B — remote build (planned)
espctl build --target stm32g431 --remote # CMake or Cargo project in cwd
# → flash_bundle.tar.gz
# Path C — local ELF + cloud archive (planned)
espctl build --rust-elf path/to/local.elf \
--target stm32g431 # objcopy + sign + package
# → flash_bundle.tar.gz
Today, espctl build --rust-elf produces ESP32-S3 bundles
(espflash save-image --merge); the same flag will accept an
--target stm32* once Path C lands.
Until then, you can hand-produce a Cortex-M flash bundle from any
toolchain that emits an ELF, then run espctl flash against it.
The flash_bundle crate defines the
manifest schema.
Path C details — espctl build --rust-elf for Cortex-M (rolling out)
When Path C opens for Cortex-M, the contract on the wire looks like
this (visible via espctl skills --format json once shipped):
| Field | Value / limit | Notes |
|---|---|---|
elf_blob max size | 16 MiB | Anything larger rejects at submission. |
| Pre-flight ELF validation | ELF magic + 32-bit class + little-endian + e_machine == EM_ARM (40) | Fails fast with a structured error if you upload a non-ELF / 64-bit ELF / big-endian ELF / non-ARM ELF. Note: VFP / soft-float ABI flags in e_flags are not checked — thumbv6m soft-float and thumbv7em-eabihf both pass. |
| Memory limit (archive-only) | 512 MiB | Enough for arm-none-eabi-objcopy + Intel HEX + signing + packaging. |
| Wall-clock timeout | 60 s | An archive-only job that runs longer is killed. |
Manifest build_provenance | "client_supplied" | Lets buyers / verifiers see at a glance that the ELF was not produced by the cloud toolchain. |
SBOM toolchain section | empty | The cloud only knows what objcopy did. Fill in your source-side toolchain via the build_meta field (free text, ≤ 64 KiB) if you want it captured. |
Bundle integrity (the signature + hashes) is identical to Path A / B.
The difference is provenance: a Path C bundle says “Aegis
processed this ELF”, whereas a Path A / B bundle says “Aegis built
this from source inside its pinned toolchain”. Downstream verifiers
and the deposit ledger record both states distinctly.
Hardware first-class support
Champion preview ships with first-class support for:
- STM32G4 family — beginning with NUCLEO-G431RB. Path A templates target STM32CubeG4 HAL.
Roadmap:
- STM32F7 / STM32H7 / STM32L4 — order subject to user demand.
Anything outside the supported set works under Path C (local ELF
- cloud archive) —
arm-none-eabi-objcopyis target-family agnostic, the bundle format isprobe_rs_chip-driven, so anythingprobe-rssupports can be flashed and monitored.
Build → Deposit → Review still applies
The three-phase loop in Typical Workflow is the same for Cortex-M as for ESP32:
- Build → produces
flash_bundle.tar.gz(any of the three paths). - Deposit →
espctl deposit add <build_id>writes the(requirement, code, physical verification)triple to~/maker-assets/. STM32 builds land in the same triple ledger as ESP32 builds. - Review → the
MakerStatsSnapshotpushed to/ops/funnel-reviewhas ahardwareMix[]field — STM32 platforms show up there alongsideesp32-s3etc. See Weekly Maker Asset Review.
See also
- CLI Utilities —
espctl probes,espctl provision-host,--jsonsemantics. - Firmware & Flash —
espctl flash,espctl monitor,FlashTargetdispatch (ADR-007). - Pricing — the executor matrix and Champion tier specifics.
- System Overview — where the remote build agent sits and how flash bundles are signed.