@page docs_adr_index Architecture Decision Records
This directory captures the why behind significant architectural choices in the Alp SDK. Each ADR is a short markdown file describing one decision: the context that forced it, the alternatives considered, the choice made, and the consequences (good and bad).
ADRs are append-only — a decision is never edited, only superseded by a later ADR that links back to it. The index below lists the current set in chronological order.
# NNNN. <title>
Status: Proposed | Accepted | Superseded by NNNN | Deprecated
Date: YYYY-MM-DD
## Context
What forced the decision. What problem we were facing.
## Decision
What we chose.
## Alternatives
What else we considered, and why we didn't pick them.
## Consequences
Good: ...
Bad / costs: ...
| ADR | Title | Status |
|---|---|---|
| 0001 | Why Alp SDK wraps Zephyr (and why the wrapper stays thin) | Accepted |
| 0002 | alp_last_error() + compile-time SoC capability validation |
Accepted — amended 2026-08-27 to record the ALP_ERR_NOT_READY/ALP_ERR_INVAL convention measured across the dispatch layer and correct a scope misreading; the decision itself unchanged |
| 0003 | Wrap 12 peripheral classes at v0.2, not just I2C/SPI/GPIO/UART | Accepted — partially superseded by 0023 (Ethernet-into-<alp/iot.h> row only; 0023 itself is Proposed) |
| 0004 | E1M-spec instance counts as the portability bound | Accepted |
| 0005 | alp-sdk vs alp-studio repo boundary (dual-use acid test) | Accepted |
| 0006 | Secure boot (vendor-native) + secure OTA (MCUboot + Mender) | Accepted, partially superseded (2026-05-11 amendment: Mender replaces RAUC on Linux side; AEN-Zephyr OTA client choice pending) |
| 0007 | Wave-2 DSP: pipeline stages, not standalone primitives | Accepted |
| 0008 | GPU2D: portable shim under <alp/gpu2d.h> even for single-silicon |
Accepted |
| 0009 | Mender Zephyr client deferred to v1.1 | Accepted |
| 0010 | Heterogeneous OS orchestration: Zephyr + Yocto as peers, not alternatives | Accepted |
| 0011 | Intra-family portability is load-bearing; cross-form-factor portability is intentionally not a goal | Accepted |
| 0012 | alp-sdk developer host is cross-platform; Linux required ONLY for Yocto | Accepted |
| 0013 | TF-M trust boundary on AEN: TrustZone-M split on M55-HP, not a dedicated M55-HE | Accepted |
| 0014 | Deterministic, versioned build-plan-v1 contract between planning and execution |
Accepted — runtime mechanism superseded by 0020's Python-port amendment; contract remains the reference/parity and --plan-from seam |
| 0015 | CC3501E bridge firmware is embedded in alp-sdk (like gd32-bridge), not a separate repo | Accepted |
| 0016 | Cross-core peripheral proxy wire schema: the GD32 bridge command frame carried over RPMsg (A55→M33→GD32) | Proposed |
| 0017 | alp-sdk rides over the vendor SDK — no rewritten vendor drivers; Tier-1 upstream-native / Tier-1.5 in-tree-thin (Apache HAL / upstream core) / Tier-2 vendor-SDK-consumed / Tier-3 SE-mediated | Accepted (amended 2026-06-15) |
| 0018 | Curated third-party libraries: one libraries: flag in the project YAML, capability-gated manifests under metadata/libraries/, Tier A curated / Tier B recipe-only, never forked |
Accepted |
| 0019 | Vendor a verbatim e1m-spec pinout snapshot under metadata/e1m/ (generated by sync_e1m_spec.py, gated offline by check_e1m_pinout.py) so per-SoM pinout tables are cross-checked against the standard; supersedes PR #582's hand-transcribed footprint |
Accepted |
| 0020 | A standalone tan CLI owns the command surface and execution; Python Tan now also carries the relocated in-process planner, while alp-sdk retains the reference emitters; the six west alp-{build,flash,image,size,renode,clean} extensions + fan_out retired (read-only west alp-emit remains); supersedes 0014 mechanism + 84-87 |
Accepted — amended 2026-08-03 for the Python port; only migrate, lock, and quality still forward to west; the cross-repo repository_dispatch trigger shipped as .github/workflows/dispatch-tan-parity.yml (see ADR Amendment, 2026-08-26) — partially superseded by 0022 (the Rust-executor language clause, and — after 0022's 2026-08-07 withdrawal was itself superseded by its Amendment 2 on 2026-08-17 — the renode command-surface entries as well; the plans-vs-executes split, three-repo boundary, and contract still live) |
| 0021 | Toolchain provisioning: pin upstream URLs + sha256 rather than rehosting binaries, key ~/.alp/toolchains/ by artifact not SDK version, signed package manager for small host tools + hermetic archives for the large compilers, absolute-path injection instead of PATH mutation, tiered consent (A no-elevation / B elevation / C licence-gated), and per-lane onboarding (prebuilt-blink ‖ Zephyr-on-M ‖ Yocto container); automates what 0012 documented |
Proposed |
| 0022 | tan ships as Python (PyInstaller freeze of alp-tan), not Rust, from v0.5.0; Renode is retired completely — the command surface, alp-sdk's four pr-renode-* CI gates and their Renode v1.16.1 pin, the metadata/renode/ models, the tests/renode/ fixtures and examples/aen/aen-sim-vision — with no replacement simulator; narrowly supersedes 0020's Rust-executor language clause + its renode command-surface entries |
Accepted — amended twice: 2026-08-07 withdrew the Renode-retirement clause, then Amendment 2 (2026-08-17) superseded that withdrawal, re-instated the retirement and widened it to alp-sdk's own Renode CI + assets and to examples/aen/aen-sim-vision (Decision point 4 resolved: deleted); diagnostics.sim_console is retained; the Python-executor clause shipped and stands throughout |
| 0023 | Ethernet stays out of the <alp/*> surface — compile-time capability + form-factor port identity + ring-2 PHY chip drivers is the whole contract; no <alp/net.h>; link state may return additively if alp-studio needs it; narrowly supersedes ADR 0003's Ethernet-into-<alp/iot.h> row |
Proposed |
| 0024 | V2N/V2M ADC, PWM, DAC, counter, and qenc stay served exclusively by the GD32 bridge — no native RZ/V2N leg, because no SoC pin reaches an E1M-standard analog/counter pad; the six-surface HiL quarantine has mostly moved on and is not the driving concern | Proposed |
| 0025 | V2N DMAC0 stays CM33-exclusive (whole unit) — the CM33 FSP reserves channels 0-1 for the SCI7 SPI DMA fast path, channels 2-7 are unclaimed margin, and Linux must never re-enable &dmac0; no Linux-side per-channel mask is fabricated for lack of evidence one works |
Proposed |
| 0026 | Tan owns the planner outright -- alp-sdk's scripts/alp_orchestrate/ is retired as a second producer and python/tan/planner/ becomes the single implementation; alp-sdk keeps metadata/schemas/examples (hardware truth) and build-plan-v1 survives as the seam; amends 0020's 2026-08-03 amendment clause 3 only |
Accepted — amended 2026-08-30: the decision stands, but migration step 3's deletion unit was wrong as scoped (scripts/alp_orchestrate/ is planner + emitters + the board.yaml loader whose SdkRevisionNotBuildable gate refuses unbuildable silicon, and is imported by validate_board_yaml.py, the script tan spawns on its own default validate path), a missing split step is inserted before it, the surviving-oracle checks are re-homed to tan-cli because parity-seam1.yml diffs the frozen oracle against a LIVE SDK emit, clause 4's frozen-crates/ bullet is struck as already stale (no top-level crates/ in tan-cli as of b9aa697), and the parity axis may be DELETED only once no alp-sdk release tan still accepts carries the second producer; decided 2026-09-04 (§C/§D): alp-sdk keeps the configure-time --emit zephyr-conf call via a surviving emitter core, not repointed at tan, and stays canonical for rendered-artefact bytes, so tan consumes plan-embedded bytes instead of re-implementing the renderers |
| 0027 | board.yaml storage regions are declared by role: (settings/app_data/log/ota_cache), a closed enum the planner resolves to a concrete region per SoM; flash_device: survives as an explicit, non-portable PIN and a pinned region missing after a SoM swap is a REFUSAL naming the region, the SoM and the replacing role -- never a silent relocation of persisted data |
Proposed |
| 0028 | Tan owns the model engine -- proposes retiring scripts/alp_model/ in favor of python/tan/model/ as the single implementation; alp-sdk keeps the NPU truth (metadata/npu_ops/, metadata/model_zoo/, metadata/schemas/, the inference_arena_sram_kib budgets) and the on-device .alpmodel reader, and the .alpmodel container format survives as the seam; applies 0026's hardware-truth-vs-consumer principle to the second duplicated surface |
Proposed |
| 0029 | Cross-repo pins (tan-cli's fork-audit hashes, alp-sdk's propagated Zephyr version, SUPPORTED_CLI_VERSION, PINNED_SDK_TAG) become typed lock-manifest entries (value / intent: tracking or frozen / reason-issue link / proof reference / site list) instead of a literal plus a prose comment; acceptance moves from spelling-agreement to a property gate -- no pin entry moves without a green run on the new value, via a new pin-move-verify repository_dispatch to alplabai/alp-e2e |
Proposed |
| 0030 | E1M-AEN SE firmware: the SERAM image on the module and the hal_alif se_services library are one versioned pair that moves together, and E1M-AEN tracks Alif's latest released SERAM (floor and current baseline v110); a module below the floor is unsupported and untriageable because an API break admits any behaviour, so se_service_get_se_revision() is checked before any other AEN SE investigation |
Accepted |
| 0031 | The CC3501E and GD32 bridge firmwares live in their own public repositories, each with its own CI, security policy and protected main; the wire contract stays SINGLE-SOURCED -- neither mirrors the protocol header, both compile alp-sdk's canonical one via an explicit ALP_SDK_ROOT, and firmware-side CI fails on drift. alp-sdk keeps the host halves. Supersedes 0015 |
Accepted |
| 0032 | Vendor data the vendor PERMITS us to redistribute but that cannot sit under Apache-2.0 (e.g. the Alif Ensemble SVDs) is carried in a segregated subtree -- metadata/svd/<vendor>/ for register descriptions, vendors/<name>/ for vendored source -- with the vendor's unmodified licence beside it and a NOTICE entry naming it. That split is load-bearing: check_public_private.py:76-82 excludes vendors/ from the leak scan while metadata/ is a scan root, and an SVD is dense register and pad detail, so it belongs on the scanned side. The title deliberately avoids "licence-gated", since this repo already uses that term for the opposite case (SETOOLS/BSPs, not redistributed) and ADRs are append-only, so the filename would have locked the inversion in; the term keeps its existing meaning where the document does use it. The NOTICE half of the pattern is convention rather than enforcement: nothing reads NOTICE and seven vendored components already have no entry, so Decision 3 repairs a drift rather than continuing a practice. NOT by extending library-v1.schema.json license.enum, which gates only what a project selects through libraries: and whose ADR 0018 guard would be widened to admit something that never travels that path. The root NOTICE currently states Alif/Renesas/NXP/DEEPX vendor material is NOT redistributed, so the carve-out is an amendment to a published representation and lands with the first file it describes, never ahead of it. Extends 0018 |
Proposed |
| 0033 | The CC3501E wire protocol is versioned MAJOR.MINOR and only MAJOR gates the link. MAJOR moves only when an unchanged host would be MISREAD (reusing a reserved byte or flag bit, changing framing or a struct layout); everything additive is MINOR and does NOT refuse a link. Features are discovered with CMD_GET_CAPABILITIES (0x06) rather than inferred from a version number -- the bitmap is composed from the same compile-time switches that decide which HAL bodies link, so it reports what a BUILD implements rather than what its number implies. The raw integer went v5 -> v9 in one week, forcing a lockstep reflash each time, when only v7 and v8 could actually misread an old host; v6 and v9 were additive and cost customers a reflash for nothing. Retroactive mapping v5=1.0, v6=1.1, v7=2.0, v8=3.0, v9=3.1; v9's 3.1 was BYTE-IDENTICAL to what shipped as v8, but since the #2035 CRC-trailer bump (RESP_OK 0x00 -> 0x5A plus a CRC-16 trailer) the current wire is 4.0 -- see the ledger in the ADR body, which is the source of truth for the current version. MAJOR 0 is reserved so a pre-scheme firmware answering with a raw integer is distinguishable from a corrupt one. Costs one final breaking bump, deliberately taken now at one customer rather than at ten |
Proposed |
| 0034 | alp-sdk DECLARES support policy as versioned schema-backed data, and exactly ONE normative evaluator turns a declared policy into a verdict; consumers call it or carry a hash-pinned audited port, never a prose re-derivation. The evaluator lives in alp-sdk scripts/ because 96 configure-time CMakeLists.txt invocations and the four west alp-* commands reach load_board_yaml with no tan in the process -- so a plain west build keeps its refusals. Declared policy is constrained (exact values only, derived closed codomain, no catch-all, totality proved by a gate rather than by ordering) so the evaluator stays a small total lookup and a port is provable. A refusal that enforces a SUPPORT decision must say so rather than assert a hardware limit: the Cortex-A -> Yocto / Cortex-M -> Zephyr pairing is a product choice, not physics -- upstream Zephyr v4.4.1 ships arch/arm/core/cortex_a_r/. Narrows 0026 clause 2's justification, not its decision |
Proposed |
| 0035 | micro-ROS cross-core transport on the RZ/V2N CM33<->A55 pair: recommends a custom XRCE transport over the ADR 0016 RPMsg channel (A55 bridge feeding an unmodified micro-ROS agent) over UDP on a virtual net or XRCE serial-over-RPMsg; no custom RMW is needed; bench-gated, maintainer decides | Proposed |
Write one when the decision:
- Is hard to reverse (changes the public API shape, the build system, the OS-pivot story, the way the studio talks to the SDK).
- Has a non-obvious "why" — six months from now the next contributor will look at the code and ask "why isn't this done the simpler way?"
- Closes off an alternative path that someone could plausibly try again ("we considered X, here's why we didn't do it").
Don't write one for ordinary feature work or routine bugfixes — the changelog is the right place for those.