Every chip is an island

There is no docker run for hardware.
Not because nobody tried. Embedded toolchain fragmentation is not an accident or a gap somebody forgot to fill. It is the accumulated output of decades of decisions that each made perfect sense to the company making them. Together, they produce an industry where "run this binary on that chip" is not a thing you can type.
Software got out of this. Compilers, then package managers, then containers, then CI, then cloud. Each layer swallowed a class of difference and handed back one interface. Embedded never had that. A 2025 survey of DevOps practice in firmware puts it dryly: "Unlike the standardized cloud DevOps stacks (e.g., Docker, Kubernetes), embedded pipelines depend heavily on vendor-specific compilers, simulators, and debuggers."1
Dryly is doing a lot of work in that sentence.
What embedded toolchain fragmentation actually looks like
Take a working blinky on an STM32F407 Discovery and move it to an nRF52840 DK. Not a port to a different architecture — both are Cortex-M4F. Same instruction set, same core vendor, same language.
The memory map changes: different flash origin, different RAM size, different reserved
holes. So the linker script changes, and the startup code with it. The vector table and
the interrupt list are per-device. The peripheral registers change completely — different
UART, different names, different bit meanings. The HAL changes, because ST ships
stm32f4xx-hal and Nordic's world is nrfx and neither has heard of the other. The clock
tree changes, and it is not a parameter, it is a different set of concepts.
Then you leave the source tree. The probe changes: ST-LINK on one board, J-Link on the other. The flashing tool changes. The board definition changes. On an RTOS, the driver bindings change. And at the end of it the board is a physical object somebody has to be holding.
Ten layers. Every one forks per vendor, and most fork per part number.

Why docker run works, and why there is no docker for firmware
This is the part people hand-wave, so let me not. docker run works because of two things
underneath it, and both are load-bearing.
The first is one kernel. A container is not a virtual machine — Docker uses Linux kernel namespaces to carve an isolated workspace out of the host you are already running on.2 The container brings its own userland and borrows the kernel's system call interface, which is stable, documented, and the same on every Linux box on earth. That is the single ABI the whole abstraction rests on.
The second is that CPU architecture is a short enum. When it varies, containers do not abstract it — they cheat. The OCI image index ships several manifests in one artifact, each tagged with a platform, and the client picks the matching one at pull time.3 That is not portability. That is building it N times and choosing. It works because N is small.
Now try both on a microcontroller.
There is no kernel, because there is no operating system. The thing your binary talks to is the silicon, and the ABI is the memory map plus the peripheral register set. That is not a detail underneath the abstraction. It is the abstraction's entire surface, and it differs on every part.
The cheat does not rescue you either. OCI's platform axis is a short enum. The embedded equivalent is a part number, times a board, times a wiring diagram — two boards carrying the same die can differ in crystal, external flash, and pin mux, and that is enough to make the same ELF behave differently.
docker run also assumes the machine is fungible. Any host with the right kernel will do.
A board is not fungible. It is one object, in one building, that one person is using right
now — the whole argument in why HIL benches won't scale in the AI
era.
Why each layer resisted standardization
It is tempting to write this off as inertia. It isn't. Every layer has a real reason, and most of them are economic rather than technical.
Differentiation is the product. A vendor sells you the peripheral set. Low-power modes, the DMA fabric, the crypto block, the way the timers chain — that is the pitch. A uniform interface across vendors turns the part into a commodity, which is exactly what nobody selling parts wants.
The silicon predates the software standard. Reference manuals get written by teams shipping a die on a tape-out schedule. The machine-readable description arrives later as a retrofit, inheriting whatever the hardware team already decided.
Support burden. A standard interface is a promise to support every combination built out of it. Vendors would rather bless one IDE and one probe and call the rest unsupported.
Certification. In ISO 26262 or IEC 61508 work, a qualified toolchain is an asset with paperwork attached. Swapping the compiler is not an afternoon, it is a project. Frozen layers do not converge.
And the memory map cannot be abstracted without lying. This one isn't about incentives. You cannot present 128 KB of RAM as 192 KB and call it an implementation detail. If a layer tries, it does not become portable. It becomes wrong.
What a lie at the memory-map layer looks like
We shipped that lie in our own examples repo and found it while writing this batch.
The memory.x in our stm32f4-discovery example declares RAM : ORIGIN = 0x20000000, LENGTH = 192K. cortex-m-rt puts the initial stack pointer at the top of RAM, so the
first word of the vector table is 0x20030000. On a real STM32F407 the contiguous block
at 0x20000000 is 128 KB and everything from 0x20020000 up is reserved.4 That stack
pointer is 64 KB into nothing.
cargo build --release is delighted. One ELF, one md5. Three machines:
| Machine | Result | Why |
|---|---|---|
Chiplab (Renode 1.16.1), stm32f4_discovery | boots, runs, prints, exits clean | its platform provides 256 KB contiguous at 0x20000000 |
QEMU 11.0.3, netduinoplus2 (STM32F405) | instant lockup before main | provides the silicon-correct 128 KB |
| Real STM32F407 | would bus-fault on the same push | 0x20020000+ is reserved per DS8626 |
QEMU's answer, verbatim:
qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1)
R00=00000000 R01=00000000 R02=00000000 R03=00000000
R12=00000000 R13=2002ffe0 R14=fffffff9 R15=0800359c
XPSR=41000003 -Z-- T handler
Three "correct" answers to the same binary, because each board model has its own idea of what memory exists. The simulator that "worked" was the wrong one. That is fragmentation you can see, and it is why firmware CI that stops at the linker proves less than the green checkmark suggests.
The partial escapes, and how far each one actually gets
Plenty of smart people have attacked this. Every one of these is real and useful. None of them composes into one call.
| Layer | What varies | Best standard or escape hatch | How far it actually gets |
|---|---|---|---|
| Core API | Cortex-M revision, NVIC, SysTick | CMSIS-Core5 | Solid, but Arm-only, and CMSIS "does not define standard peripherals" |
| Register description | Every peripheral, every bit | CMSIS-SVD6 | Machine-readable programmer's view — vendor-authored, quality varies |
| Device package | Startup code, headers, flash algorithms | Open-CMSIS-Pack7 | Real packaging; internal structure is still manufacturer-defined |
| Toolchain | Compiler, target triple, ABI flags | LLVM/GCC in a container | Fully solved and reproducible — for the build only |
| Linker + startup | Memory map, vector table | memory.x, vendor linker scripts | Per-part, hand-written, silently wrong when copied |
| HAL | Peripheral drivers | embedded-hal traits, Zephyr drivers | Trait-level portability; implementations stay per-vendor |
| Debug transport | SWD vs JTAG, probe vendor | OpenOCD8, probe-rs9 | Broad probe support; still needs a probe, a cable, a board |
| Flashing | Per-part flash algorithm | .FLM algorithms in CMSIS-Packs10 | Works when the vendor ships one; they "can be absent" |
| Board definition | Pins, clocks, peripherals present | Zephyr board porting11, PlatformIO12 | Excellent in tree; otherwise you are writing devicetree |
| Physical access | Who owns the board | nothing | Zero. A human plugs it in. |
Read the last column top to bottom. Every row is a partial win. Rust gets closest to the
container experience — cargo build then probe-rs run really is two commands, and
probe-rs speaks SWD and JTAG across Arm and RISC-V using flash algorithms lifted from
vendor packs.910 It is excellent. It also requires that the probe is attached, the
board is powered, the target is in the registry, and the vendor shipped a flash algorithm.
Four preconditions a Linux container has never once asked about.
Ten layers, nine partial fixes, and the tenth is a person.

What an agent would actually need
An agent writing firmware needs the same thing a container runtime gives a web developer: one call that takes a binary and a target name and returns what happened. Not an IDE, not a plugin, not a wizard. One call, and a transcript.
Every layer above has to be closed for that call to exist, and most of them are somebody's product boundary. The one nobody has closed is hardware access for CI — the physical layer, where a person is holding the board. That is the whole premise of hardware becoming an API, and it is the layer we are building at Veecle with Chiplab. It currently exposes seven boards. Seven. That number tells you exactly how early this is and how little I am claiming.
Is a standard embedded development environment even possible?
Partly. The build side is already solved — a container with the right cross-compiler is reproducible and boring. The register and packaging side converges slowly through CMSIS, SVD, and probe-rs, because machine-readable descriptions are the one thing vendors gain from publishing.
Two layers will not converge on their own. The memory map, because it is the silicon, and lying about it produces a simulator that passes tests real hardware fails. And physical access, because no standard ever made a board plug itself in.
The escape hatches keep improving. The last mile stays a person. Every chip stays an island until somebody runs a ferry to all of them.
Update, 2026-08-04: our own memory map, corrected
The linker script in that three-way comparison was ours and it was wrong. It is fixed:
RAM is 128K at 0x20000000, matching the F407's actual contiguous SRAM, and the extra
64K of CCM stays where the silicon puts it, at 0x10000000, on a bus DMA cannot reach.
Rebuilt, both machines print Hello world! — Chiplab with SP = 0x20020000, and QEMU's
netduinoplus2, which would not boot the old binary at all.
Which is the layer argument in one file. One chip, one address range, three different opinions about what exists there, and the only way to settle it was to read the datasheet by hand. No standard was going to do that for us.
Sources
Footnotes
-
Katapara and Sharma, "Embedded DevOps: A Survey on the Application of DevOps Practices in Embedded Software and Firmware Development," arXiv:2507.00421, 2025. https://arxiv.org/html/2507.00421v1 ↩
-
Docker, "What is Docker?" — see "The underlying technology" on namespaces and Linux kernel features. https://docs.docker.com/get-started/docker-overview/ ↩
-
Open Container Initiative, "OCI Image Index Specification." https://github.com/opencontainers/image-spec/blob/main/image-index.md ↩
-
STMicroelectronics STM32F405xx/407xx datasheet (DS8626), memory map: SRAM 112 KB at 0x20000000, 0x2002 0000 – 0x3FFF FFFF reserved, CCM data RAM at 0x1000 0000. https://hangpersonal.com/wp-content/uploads/2024/10/STM32F407-Memory-Mapping.pdf ↩
-
Arm, "CMSIS — Common Microcontroller Software Interface Standard," general documentation. https://arm-software.github.io/CMSIS_6/latest/General/index.html ↩
-
Open-CMSIS-Pack, "CMSIS-SVD Format" specification. https://open-cmsis-pack.github.io/svd-spec/main/svd_Format_pg.html ↩
-
Open-CMSIS-Pack specification, "Delivery Mechanism for Software Packs." https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/index.html ↩
-
OpenOCD, "About" — debug adapters, transports, JTAG and SWD. https://openocd.org/doc/html/About.html ↩
-
probe-rs, "About probe-rs." https://probe.rs/docs/overview/about-probe-rs/ ↩ ↩2
-
probe-rs, "CMSIS Packs" knowledge base — pack structure is manufacturer-defined and flash algorithms "can be absent if the manufacturer chooses so." https://probe.rs/docs/knowledge-base/cmsis-packs/ ↩ ↩2
-
Zephyr Project, "Board Porting Guide." https://docs.zephyrproject.org/latest/hardware/porting/board_porting.html ↩
-
PlatformIO, "Boards." https://docs.platformio.org/en/latest/boards/index.html ↩
