Plan: Microcontroller development (STM32 Cortex-M, Espressif RISC-V)
Status — 2026-06-17 — first last-mile image formats landed
Kit's core compile→link→object pipeline is already in good shape for bare-metal
MCU work — the hard part (an embedded-grade linker with full MEMORY/SECTIONS/
AT> linker-script support, --gc-sections, freestanding ET_EXEC) is done,
and both 32-bit backends produce correct -O0/-O1 code. The gaps are concentrated
in last-mile workflow (startup stubs, target docs, on-target debug) and a
few defaults footguns. Intel HEX and Motorola S-record output are now
covered by kit image --format ... and kit objcopy -O ....
This doc owns the MCU workflow integration roadmap. The ARM backend
follow-ons that MCU work depends on (hard-float, .ARM.attributes, GNU numeric
local labels, the kit-compiled reset stub) are tracked in
ARM32.md — referenced here by MCU impact, not duplicated. The RV32
backend contract lives in ../RUNTIME.md; the linker-script
surface in LINKER.md / ../LINK.md.
Scope
In scope (the two named ecosystems, minus Xtensa):
- STMicro STM32 on Cortex-M3 (ARMv7-M) and Cortex-M4/M4F/M7 (ARMv7E-M): STM32 F1/L1/F2 today (soft-float, correct — these have no FPU); F3/F4/L4/G4/ F7/H7 compile soft-float now and gain the FPU once ARM hard-float lands.
- Espressif RISC-V parts: ESP32-C2/C3 (RV32IMC), C6/H2 (RV32IMAC), P4 (RV32IMAFC). The C extension is already supported, which is what these need.
Explicitly out of scope (for now):
- ARMv6-M / Cortex-M0 / M0+ (STM32 F0/L0/G0/C0). The backend emits
ARMv7-M-only ops (hardware
SDIV/UDIV,MOVW/MOVT, wide branches, full IT) that ARMv6-M lacks. Deferred — see the ARM32.md follow-on list. - Xtensa (classic ESP32 / ESP32-S2 / S3, ESP8266). No backend; out of scope. Espressif support here means their RISC-V silicon only.
- ARMv8-M / TrustZone (Cortex-M23/M33; STM32 L5/U5/H5). Untested, unmodeled.
§0 — Target coverage matrix
| Vendor / family | Core (ISA) | Status |
|---|---|---|
| STM32 F1/L1/F2 | Cortex-M3 (ARMv7-M) | Works — soft-float, correct (no FPU on part) |
| STM32 F3/F4/L4/G4 | Cortex-M4(F) (ARMv7E-M) | Compiles soft-float only; FPv4-SP idle until §4 |
| STM32 F7/H7 | Cortex-M7 (ARMv7E-M) | Same — soft-float; no FPU/DP-FPU use |
| STM32 F0/L0/G0/C0 | Cortex-M0/M0+ (ARMv6-M) | Out of scope (ARMv7-M-only insns) |
| STM32 L5/U5/H5 | Cortex-M33 (ARMv8-M) | Out of scope (untested) |
| ESP32-C2 / -C3 | RV32IMC | Works — soft-float by default now (§1); add -march=rv32imc to drop the A ext |
| ESP32-C6 / -H2 | RV32IMAC | Works (-march=rv32imac) |
| ESP32-P4 | RV32IMAFC | Works; single-float hard, double soft |
| ESP32 / -S2 / -S3, ESP8266 | Xtensa | Out of scope (no backend) |
§1 — Footguns to fix first (cheap, high-impact)
These are correctness/UX traps that bite before any feature work and are nearly free to fix.
- RV32 default profile no longer assumes an FPU. The rv32 default was
rv32imafc/ilp32f(hardware single-float on by default), so a forgotten-march/-mabisilently emittedFLW/FSWthat fault on a no-FPU part (ESP32-C3 = rv32imc). Flipped torv32imac/ilp32soft-float (rv64_target_feature_defaultsinsrc/arch/riscv/arch.cno longer setsFfor rv32). The FPU is opt-in via-march=rv32imafc -mabi=ilp32f(the explicit flag fully overrides defaults and still emitsfadd.s— verified). Because the static predefined-macro table can encode only one float profile (andrt/lib/corokeys on__riscv_flen), the float macros (__riscv_float_abi_*,__riscv_flen,__riscv_fdiv,__riscv_fsqrt) are now emitted per resolvedfloat_abiby a newArchImpl.float_predefineshook (kit_compiler_arch_float_predefines), so the preprocessor always agrees with the codegen ABI — this also fixed a latent bug where the soft runtime's coro layer was built with__riscv_flen=32(FP save/restore in soft code) and where rv64lp64soft builds got__riscv_float_abi_double. Tested:test-smoke-rv32(ilp32 + ilp32f lanes, -O0/-O1, qemu) 7/0,test-cg-api,test-abi-classify,test-smoke-rv64all green. - Document the canonical invocations per part. A short table (this part →
-target … -march=… -mabi=…) in the user docs, so the C3/C6/P4 ISA-string differences are spelled out rather than rediscovered. - RV32 atomics without the
Aextension. ESP32-C2/C3 are RV32IMC (noA); C6/H2/P4 haveA. Kit declares__riscv_atomicand provides 4-byte lock-free / 8-byte spinlock fallbacks. On a no-Asingle-core part the lock-free 4-byte path needs verification (it should degrade to the runtime helper, not emitlr.w/sc.w). Confirm-march=rv32imcroutes atomics through the runtime, and that the spinlock fallback is interrupt- safe enough for single-core use. (src/cg/atomic.c,../RUNTIME.md:116.)
§2 — Flashing: object → image formats
The flash workflow needs an image format the flasher ingests. Today
objcopy -O binary works (driver/cmd/objcopy.c:226, via
KIT_IMAGE_FORMAT_BIN) and this implementation adds Intel HEX / S-record support.
objcopy -O ihex(Intel HEX). Many ST-Link/third-party flashers and bootloaders consume.hex. Bounded: a new encoder alongside the BIN path insrc/obj/image.c(records: data / extended-linear-address / EOF; checksum per line) plus the-Otoken indriver/cmd/objcopy.c:206.objcopy -O srec(Motorola S-record). Same shape (S0/S1-3/S7-9), parallel encoder. Lower priority than ihex but cheap once ihex exists.- Espressif note (no work):
esptool.pyconsumes a plain ELF (orelf2image) and produces the bootloader/partition/app image with its header + checksum itself. Kit's freestandingET_EXECELF is sufficient for the C-series — no custom Espressif image format needed. Verify the round-trip and document it; do not build an image packer.
§3 — Startup: crt0 / vector table
The runtime ships no crt0 by design (../RUNTIME.md:240), so today every
firmware hand-writes the reset handler, vector table, .data copy-from-flash,
.bss zero, and stack setup. Correct reference stubs already exist in the test
harness (test/smoke/arm32.sh:70, test/smoke/rv32.sh,
test/link/harness/start.c) — the work is promoting a reusable, overridable
startup into the shippable runtime so users start from working code, not a blank
page.
- Generic Cortex-M startup + minimal vector table — reset handler that
sets MSP from
_estack, copies.data(LMA→VMA), zeroes.bss, runs.init_array, callsmain; a weak default vector table with overridable handler slots. Overridable so a vendor table wins when present. - Generic RV32 startup — set
sp/gp, the TLS TCB +tpprologue (../RUNTIME.md:248),.data/.bss,.init_array,main. Single-thread TLS-as-static is fine for MCU. - A starter linker script per regime — a parameterized
MEMORY{FLASH,RAM}+SECTIONS{}template (with_sdata/_edata/_sbss/_ebss/_estack,.isr_vectorKEEP,.dataAT>FLASH) that the §3 startup matches. Kit's linker already supports every directive this needs (src/link/link_script.c). - Depends on (ARM): the kit-compiled reset stub in ARM32.md (currently
clang-assembled; blocked on a few
kit asforms). When that lands, the ARM startup above is fully kit-built end to end.
§4 — ARM hard-float (FPv4-SP / FPv5)
Cortex-M4F/M7 (STM32 F4/F7/H7 — very common) have FPUs that current soft-float
codegen leaves idle. This is an ARM backend item, owned by
ARM32.md ("Hard-float (FPv4-SP, arm-none-eabihf): VFP codegen +
softfp/hard axis"). Listed here only for MCU prioritization: it is the largest
single performance lever for the STM32 mid/high range. Needs the VFP register
file, hard-float AAPCS variant, and -mfloat-abi/-mfpu plumbing.
§5 — SDK / toolchain interop
To use kit alongside vendor SDKs (CMSIS, STM32 HAL, ESP-IDF) rather than fully standalone, kit objects must link cleanly with GNU-toolchain libraries.
.ARM.attributes— without the build-attributes section, GNUldrejects mixing kit.owith vendor.aon ABI grounds. Owned by ARM32.md (src/obj/elf/emit.c); a hard gate for SDK interop on ARM.- GNU numeric local labels (
1:/1f/1b) inkit as— needed to assemble typical vendor startup.sand the AEABI helper layer. Owned by ARM32.md. - Vendor linker-script compatibility. Kit's linker-script parser is a
GNU-ld subset; it rejects
GROUP/INPUT/INCLUDE/STARTUP/SEARCH_DIR(src/link/link_script.c:1683). Real STM32CubeIDE / ESP-IDF.ldscripts use some of these. Run kit's linker against a representative set of vendor scripts, then add the directives that actually appear (likelyINCLUDEandGROUPfirst). Track the concrete misses in LINKER.md.
§6 — On-target debug
kit dbg is an in-process JIT debugger (KitDebugSession); it cannot speak
the GDB remote serial protocol to an OpenOCD/J-Link/probe-rs server, so it can't
debug on real silicon today.
- Pragmatic path (verify, near-free): kit already produces ELF + DWARF
for Thumb-2/RV32 (
make test-dwarf). Confirm stockarm-none-eabi-gdb/riscv32-elf-gdb+ OpenOCD load kit's DWARF and step kit-built firmware correctly. If that works, on-target debug is unblocked via external gdb and the rest is optional. - Native path (larger, separate effort): a
kit dbg target remote :3333GDB-remote client mode so kit's own debugger drives hardware. Defer until the external-gdb path is confirmed working; this is a real subsystem, not a quick add. Coordinate with DEBUG.md.
§7 — Runtime polish (optional, demand-driven)
- Float
printf— the callback formatter is integer-only (rt/lib/stdio/printf.c).%f/%gis a common embedded ask (and a common explicit refusal for size). Make it an opt-in lane, not the default. - Thin semihosting helper — a small
kit/semihosting.hwrappingSYS_WRITE0/SYS_EXIT(BKPT #0xABon ARM,ebreaksequence on RV32) so users don't hand-roll the trap. The printf callback already plugs into this. Useful for QEMU bring-up and early board debug.
Ordering
Roughly by leverage-per-effort:
- §1 footguns (RV32 soft-float default, per-part doc) — hours, prevents silent bad images.
- §2
objcopy -O ihex— small, unblocks STM32 flashing. - §3 startup + starter linker script — removes the biggest "blank page" barrier.
- §6 verify external-gdb on-target debug — near-free if it works.
- §4 ARM hard-float (via ARM32.md) — biggest perf lever for STM32 F4/F7/H7.
- §5 SDK interop (
.ARM.attributes, GNU labels, vendor.ld) — gates using kit with existing vendor code. - §7 polish, §6 native remote-debug — demand-driven.