Landin Cortex-M execution profile source
Cortex-M execution profile

Cortex-M execution profile

ROADMAP.md R6.10 owns the selection and completion evidence. This environment runs small C/assembly controls, not Landin output. R6.20 adds layout and ABI planning with independent controls below; later items retain the memory model, encodings, backend and firmware startup.

Selected lanes and pins

ComponentExact selection
Probe hostnative Linux x86-64, Debian 13; the existing native runner is supported
CPU/startup laneQEMU 10.0.13, Debian 1:10.0.13+ds-0+deb13u1, -M microbit -accel tcg,thread=single
CorenRF51822 Cortex-M0, ARMv6-M, Thumb, little endian, 16 MHz model clock; no FPU, caches or exclusive-access instruction requirement
Peripheral laneRenode 1.17.0 Linux x86-64 portable, build 1.17.0+20260907gitf1dd1b4af, bundled .NET 8.0.12; prototype.repl with Cortex-M0 at 16 MIPS and NVIC
EABI compilerarm-none-eabi-gcc 14.2.1 20241119, Debian 15:14.2.rel1-1
Assembler/linkerGNU Arm binutils 2.44, Debian 2.44-3+23+b1
Debuggergdb-multiarch 16.3, Debian 16.3-1, loopback QEMU remote stub
Probe options-mcpu=cortex-m0 -mthumb -mfloat-abi=soft -mabi=aapcs -ffreestanding -fno-builtin -fno-omit-frame-pointer -g3 -O1 -nostdlib; warnings are errors

tools.lock.json pins URLs and SHA-256 for every downloaded package, including shared-library dependencies and GDB's Python support. The portable Renode archive SHA-256 is 4ba7c68b59e2447f188ef4b4b112fcccd2802582c460beedceb63b84e5605a5f. The installer verifies archives before extraction. Each probe verifies the installation inventory and versions before execution, checks it again afterward, and retains that inventory. Debian 13 supplies the base libc and Python; this is a supported host profile, not a hermetic operating-system image.

These C probe flags select base AAPCS soft-float transport and ELF32 EABI. R6.20 separately selects the internal Landin transport described in the target guide. The compiler describes Cortex-M0 layout but has no Cortex-M emitter.

Memory and device map

RegionQEMU CPU laneSynthetic Renode peripheral lane
Vectorsaddress 0, 48 words / 192 bytes, initial MSP 0x20004000; Thumb reset address in word 1identical probe image convention
Flashmachine provides 256 KiB at 0; linker limits probes to 32 KiB32 KiB mapped at 0
RAM16 KiB at 0x20000000identical
Probe stacktop 0x20004000; linker reserves upper 4 KiBidentical; this is a reservation, not a measured stack bound
NVIC / SysTick0xe000e000; 32 external IRQ entriessame address; synthetic device uses IRQ0
GPIONordic registers at 0x50000000prototype GPIO A at 0x40020000; mode, speed, alternate-function and image storage, input injection, output set/reset
UARTNordic UART at 0x40002000, transmitted bytes captured to a filesynthetic divisor at 0x40021000, received data at 0x40021004
DMAno prototype DMAcontroller status/clear at 0x40026000/04; stream 5 config/count/peripheral/memory at 0x40026058/5c/60/64

The prototype is a sketch combining M0 CPU pressure with vendor-like registers; it does not identify a real part matching that whole map. The Renode platform is deliberately named prototype, not micro:bit or STM32. GPIO B, unused streams and unspecified UART registers are absent. Unknown modeled-register accesses and invalid access directions throw; unsupported bus widths generate warnings which fail the runner. Explicit halfword support covers the 16-bit count and GPIO data, without automatic read/modify/write translation. The linker refuses initialized data (this probe startup only clears BSS), a wrong vector extent, flash overflow and BSS entering the reserved stack area.

Why QEMU plus Renode

Current official QEMU documentation identifies the micro:bit's M0 and Nordic devices. Selection also inspects the pinned SoC implementation and runs the controls below. Device presence does not prove the prototype's behavior: Nordic GPIO/UART addresses differ, clock registers include stubs, and this is not a prototype DMA model. QEMU retains the mandatory CPU/startup role and R6.90's QEMU obligation is unchanged.

Renode's official documentation and peripheral modeling guide provide composable devices, bus access, IRQ wiring and C# models loaded from source. Those are useful here because the same firmware can execute against an explicit, small contract model. An independent host-only register simulator would not exercise CPU loads/stores and the NVIC together. Extending QEMU's native device tree would require a separately maintained emulator build. For this bounded lane, Renode's runtime model loading is the selected route.

Stock STM32 models were evaluated, not assumed faithful from a board list. The shipped binary identifies source f1dd1b4af; its Infrastructure submodule is cd4b002aac2398994c4a61a34a8b31b5700facf2, distinct from the release tag's submodule. The inspected DMA implementation implements request-driven copies and completion IRQs but tags CIRC, HTIE and TEIE. It does not supply the required circular reload or half/error behavior. The UART implementation can signal DMA reception; stock platform wiring alone does not establish that route. stock.repl explicitly connects it. stock.py demonstrates two received bytes reaching RAM, completion status/clear, GPIO set/reset and zero remaining count despite CIRC. Its expected warnings identify tagged CIRC and unclocked UART reception: this control proves no baud or idle-line behavior.

Executable routes and limits

Prototype pressureRoute and current evidenceLimit / semantic owner
Reset, zeroed static storage, vector table and kept handlersstart.S, memory.ld, QEMU GDB initial MSP/PC/vector assertions, step, reset and second boothandwritten environment startup; Landin sections/keep/entry R6.60
Traps, exceptions, timer and wait-for-interruptcpu.c: SVC, PendSV, SysTick/WFI, IRQ0; fault_probe UDF reaches HardFault with IPSR 3no cycle timing, every fault class or general priority/nesting proof; R6.50/R6.60
Debugger controlQEMU GDB breakpoint, step, memory/register inspection, resume, reset, rerun; exact log assertionsC/assembly debug only; Landin source debug/stack evidence R6.100
GPIO whole images, indexed packed fields, AF selection, reserved bitsperipheral.c: explicit mask/shift RMW, retained high bits, AF image, set/reset; model input injectionstored image behavior, not electrical pins/pull/speed/alternate routing; packed-language legality R6.40
UART configuration and bytesQEMU exact TX bytes; stock UART-to-DMA control; synthetic Feed writes its DR and transfers one supplied bytesynthetic divisor is storage only; no serial line, baud accuracy, FIFO overrun or framing/parity model
DMA descriptor, escaping static buffer, enable handoffperipheral.c supplies a static ordinary byte array and descriptor; feed while disabled leaves it untouchedC pointer handoff only; Landin origin refusals and complete driver R6.90
DMA count, increment, circular wrap and CPU visibilityfive ordered feeds into four bytes, checked halfword count 4/2/4/3, wrap replaces first byte, CPU reads the transferred bytesmodel serializes byte copy before count/status; no asynchronous bus races or cache-coherence proof
Completion, half-transfer and transfer errormodel status 0x20/40/80, enables in config bits 1/2/3, NVIC IRQ0, firmware records three IRQs and clears statussynthetic status convention follows sketch stream 5; not stock STM32 status layout
Critical section and event collectionPRIMASK masks delivery while DMA updates RAM/count; restoring it permits pending completion handlerproves this model's mask/delivery behavior, not Landin happens-before; R6.30
One-clears and access modesfirmware clears pending bits; model-checks.py proves writing zero preserves error, one clears it, read-only/write-only/unknown accesses refuseother access modes are absent; normative register rules remain R6.40/R6.80
Bad descriptor, bounds, unsupported directionmodel controls reject bad destination, count above 65535 and unsupported direction; no transfer occursno physical bus fault/overrun timing; driver buffer_empty, buffer_too_big, bad_baud and recovery execute in R6.90 over this lane
Ordinary-slice read, head/tail and short outputcurrent lane exposes count and circular RAM for the complete derived driverR6.90 must execute its available/read cases and failure oracles; this environment is not that driver
SVD-derived types, encoded values, named refusalschecked-in model and map are explicit device inputs; malformed access controls already executeR6.40 packed encodings; R6.70 noreturn; R6.80 generated .ldn fixtures; general generator stays with companion tooling

Feeds happen while virtual execution is paused, followed by a fixed 1 ms virtual run and a checked firmware stage. No host sleep decides firmware success. Only the debugger socket startup uses a bounded readiness poll. The Renode time framework is execution scheduling, not physical bus timing. The model's transfer order is an explicit harness assumption. The ordinary C buffer is reloaded across opaque memory-clobber barriers; that is no definition of Landin DMA visibility. R6.30 must define races, tearing, atomics, compiler/hardware barriers and cache maintenance, with models for behaviors this cacheless M0 profile cannot expose. Supplemental hardware can test actual bus/interrupt timing, electrical behavior and a real device's DMA visibility; it cannot replace the emulator gates or by itself establish language concurrency semantics.

Reproduction and evidence

On the documented native Linux host, with Python 3 and dpkg-deb:

python3 environments/cortex-m/setup.py
python3 environments/cortex-m/test.py
python3 environments/cortex-m/run.py --output /absolute/new/evidence-directory

The default private tool root is ~/work/.cortex-m; --tools selects another installation. Setup refuses an existing root; install deliberately into a new root to change tools. No sudo, host package replacement, Linux container on the Mac, Nix job or compiler dependency is introduced. Missing tools, wrong pins/host, subprocess failures, deadline expiration, model errors, unexpected warnings and absent assertions all fail. Renode returning zero after a script error is insufficient: the runner requires the unique final marker and scans its log for failures. The stock control allows only its two known warnings.

After Renode exits, the runner removes only its verified empty renode.config.lock coordination file before inventorying evidence. Native export omits lock files, so recording that transient file would correctly fail export verification. Nonempty or linked replacements fail instead.

Each subprocess has a 30-second deadline, GDB 20 seconds, and QEMU startup three seconds; timeout cleanup terminates the owned process group, escalating after two seconds. Each run needs a new output directory and retains failure logs as well as successes. Evidence includes exact commands, version replies, installed file hashes, input hashes, linker maps, ELF headers/attributes, sizes, ELFs, generated GDB/Monitor scripts, logs, UART bytes and result.json.

The Linux acceptance documents job executes both the failure controls and these live probes from the same committed archive as every native job. job.py copies its cortex-m evidence into the exported, hash-verified bundle. Thus the annotated dual-native approval binds the environment's actual run, not only this document or a development transcript. Darwin retains native compiler/workload/LLDB evidence and does not impersonate this probe host. R6.10 selects routine scope with debugger coverage because it adds debugger control checks and changes acceptance commands/evidence retention. This is not the full R6.100 milestone matrix. Nix CI and scheduler/cache/resume work retain R5.51's dispositions.

The retained development result is indexed by validation.json. Its successful probe images contain 643 and 836 text bytes respectively, no initialized data and 24 BSS bytes each. These are environment-control sizes, not measurements of the future Landin driver or a stack-usage guarantee.

R6.20 layout and ABI evidence

The same mandatory run.py now also builds probes/abi.c and abi.S with the original pinned flags, startup and memory limits, then executes them in QEMU. Renode remains a separate peripheral lane; its result is never counted as ABI evidence. No library or tool pin changes. The additional ELF, disassembly, macro dump, GDB script/log, measured JSON and both layout contract inputs are retained in the existing exported cortex-m directory. The original Renode lock-file cleanup and its regression controls remain required.

Three independent comparisons meet:

  1. The Ada cortex ABI suite computes scalar, aggregate, variant, evidence, any and multiple-result layouts and physical call plans from compiler packages, comparing with compiler/tests/cortex-m.contract. It also checks eight existing source fixtures through lowering and entry/direct/indirect planning under Cortex-M0 and synthetic-32, comparing detailed neutral IR. These are compiler-host tests, not target execution.
  2. abi.py compares the contract with every original synthetic-32 row in layout.targets, without regenerating that golden. GCC independently lays out C control structures, unions and tables. QEMU/GDB reads their emitted measurements and compares them with the contract. Static assertions also cover data/code pointers, slices, ILP32, plain-char policy and 64-bit alignment. The C union is a representation control, not C semantics for a Landin variant.
  3. Independently written assembly captures actual GCC-produced r0–r3, incoming sp and stack argument words. Assertions compare meaningful words with the compiler plan for double-word gaps, split 12-byte records, narrow stack arguments, soft floats, float-only records, promoted varargs and result-pointer displacement. Separate hand callers check GCC callees, and C callers check hand returns/callbacks, including 3/4/5-byte records and integer/double 64-bit results. Padding and skipped registers are unspecified and are not asserted as values.

The Landin convention has a separate handwritten witness: hidden aggregate result and direct/parent evidence addresses, an address-passed value, a stacked u64, successful and failing r12 outcomes, r0/r1 scalar results, multiple-result storage, and erased data/table dispatch through Thumb code pointers. GDB checks the nested r11 frame records, saved lr, chain termination and aligned sp; firmware checks callee-save registers and restoration. These execute the selected contract with real M0 instructions; they are not compiler-generated Landin calls or Landin source-debugging acceptance.

Compiler refusal/boundary controls cover wrong targets, unavailable C and emission capabilities, narrow extension, the four-byte C result threshold, empty C carriers, unpromoted varargs, declared C errors, stack-budget rounding and a target-overflowing argument without materializing it. Source checks cover pointer-sized integer/address overflow and the array byte-extent boundary. The Python controls refuse duplicate/missing/invalid contract rows, synthetic mismatches, missing markers, subprocess errors and timeouts. They preserve the R6.10 lock cleanup refusal for linked or nonempty replacements.

Every subprocess retains the original deadlines (GDB 20 seconds, build/tools 30, debugger readiness three) and owned process-group cleanup. The final exact-archive Linux documents job repeats all lanes and retains tool/input hashes alongside the new ABI artifacts. Its ordinary dual-native approval binds these results to the same revision as the hosted checks. Development results are indexed separately in abi-validation.json; validation.json keeps its historical R6.10 meaning.

These bounded probes establish neither floating arithmetic helpers, general unwind support, a complete C language ABI surface, firmware stack bounds, physical hardware behavior nor a Cortex-M compiler backend. R6.50 must consume the plans with native selection and frame code; R6.60 must implement image placement/startup; R6.100 must establish Landin debugging and stack evidence. R6.30/R6.40 retain concurrency and invalid packed encodings. Existing resource, evidence, scheduling, Nix and general-generator dispositions are unchanged.