tan flash's alif_mram_jlink backend ("Flow D": J-Link straight over SWD,
no SE-UART) burns a signed ATOC into an Alif Ensemble part's on-die MRAM.
Producing that signature is Alif's own job, done by the Alif Security Toolkit
(SETOOLS) app-gen-toc step — tan does not sign anything itself; it drives
app-gen-toc for you when it can find it, and refuses loudly, naming exactly
what it tried, when it cannot (tan-cli#365).
SETOOLS is license-gated and obtained directly from Alif. Neither tan
nor alp-sdk redistributes it. Get it from the Alif developer portal under
your own Alif account, then point tan at the directory you installed it
into — the sections below cover how.
Two shapes matter, depending on host OS:
- Linux bundle:
app-release-exec-linux-SE_FW_x.y.z— the one executabletanlooks for inside it isapp-gen-toc(west flash's ownalif_flashrunner, for the SE-UART path, looks forapp-write-mramseparately; Flow D here never does). Runningapp-gen-tocwritesapp-package-map.txt, its own build report — not another executable, and not somethingtansearches for the way it searches for the tool. - Windows: a genuine Windows SETOOLS install ships
app-gen-toc.exeinstead of the bare Linux name;tanlooks for both.
tan flash accepts three ways to say where SETOOLS lives. Highest
precedence wins outright — a lower source is never consulted once a higher
one resolves:
--setools-dir <path>— a flag ontan flashitself. The one durable, discoverable-from---helpway to pin this per invocation, regardless of shell session or manifest state.SETOOLS_DIR=<path>— an environment variable. Survives acrosstan buildruns (unlike the manifest field below), but is scoped to whatever shell/session set it.flash_args.setools_dirinbuild/system-manifest.yaml— lowest precedence, and not durable:tan buildregenerates this file on every run (python/tan/commands/build/manifest.py), and alp-sdk's own emit carries nosetools_dirkey at all. A hand-edit here is silently overwritten by your next build. Prefer the flag or the environment variable for anything you want to survive a rebuild; treat this field as build-owned, not a place to hand-author a durable setting.
If none of the three resolves, tan flash refuses with a message naming all
three sources, in this same order, and how to set each one — it never
searches the filesystem for a plausible SETOOLS install: a wrong SETOOLS
silently signing against the wrong part is worse than tan refusing outright.
When a Flow D entry has no atoc/atoc_address yet (an AEN801 slot0 slice's
manifest today typically carries only jlink_flash_device and
slot0_load_address — alp-sdk's emit does not sign anything itself),
tan flash drives one app-gen-toc sign step for you:
- copies the build's raw
.bininto<SETOOLS_DIR>/build/images/; - writes an app-only ATOC config to
<SETOOLS_DIR>/build/config/— no"DEVICE"key: the on-module factory device config is already correct for your part, and this step must not overwrite it; - runs
app-gen-toc, insideSETOOLS_DIR, against that config; - reads the resulting ATOC's MRAM placement back out of
<SETOOLS_DIR>/build/app-package-map.txt. This file is APPEND-mode — the accumulated sign record for the whole install, including hand-runs you did outsidetan— sotannever truncates or deletes it (tan-cli#373): it records the file's size and mtime beforehand and refuses if either is unchanged after a zero exit (a soft failure that would otherwise read back a stale, unrelated address as if it were fresh), and separately confirms<SETOOLS_DIR>/build/AppTocPackage.bin(which — unlike the map — IS overwritten whole every run, so there is no history in it to protect) was actually rewritten before trusting either.
A successful sign names which SETOOLS install did it (--setools-dir,
SETOOLS_DIR, or flash_args.setools_dir — see setools.source in tan flash's own output), not only a failed one.
Under --dry-run none of this touches your SETOOLS install or spawns
app-gen-toc at all — tan flash --dry-run prints what it would sign and
stops there.
If you already resolved a signature yourself — an explicit flash_args.atoc
flash_args.atoc_address, orflash_args.atoc_mappointing at your ownapp-package-map.txt— none of the above runs;tanuses what you gave it verbatim.
On a bench carrying more than one J-Link, flash_args.jlink_serial picks a
probe by serial only — JLinkExe has no USB-port selector. Some OEM J-Link
probes ship with a cloned serial number shared across more than one
physical unit, in which case jlink_serial alone cannot tell two probes
apart, even when set: a wrong-board write is now possible even with a serial
pinned. flash_args.expect_dpidr (paired with flash_args.jlink_device) is
the real per-silicon discriminator for this case — tan reads it back on
connect, before ever writing MRAM, and refuses when it doesn't match. Set
both when your bench has more than one probe, or when a shared/cloned serial
is a possibility; do not rely on jlink_serial alone to disambiguate.
flash_args.expect_dpidr must be a full 32-bit SW-DP ID — 8 hex digits
(an optional 0x/0X prefix doesn't count towards the 8). tan refuses a
shorter value outright, at plan time (so it surfaces under --dry-run, not
only on a real write): a truncated ID like 0x2477, or 0x477 — ARM's own
JEP106 designer field, shared by every ARM SW-DP — would otherwise match more
than one board and silently disarm the wrong-board guard (tan-cli#795).
flash_args.expect_dpidr is optional, so a write with none set proceeds
unguarded. Since tan-cli#609 the flash.dpidr-preflight-unarmed warning covers
every method tan itself composes a J-Link Commander session for — today
Flow D (alif_mram_jlink) alone. The coverage is a table
(DPIDR_GUARD_COVERAGE) pinned to the backend registry by a gate, so a new
backend has to declare which side it is on instead of inheriting silence.
It reached only the (now-removed) swd_probe backend before #609, and that
was measured, not theoretical: a real AEN MRAM write through tan flash on
2026-08-10 emitted ISSUES = [] — no wrong-board guard and no signal that
there was none — on a bench where one J-Link serial is cloned across two
probes.
What each path emits:
- Flow D (
alif_mram_jlink) — raises the warning. The remedy names BOTH keys, because Flow D pairsexpect_dpidrwithflash_args.jlink_device(the live-core attach profile, notjlink_flash_device). - Every other method (
zephyr_west_flash,baremetal_cmake_flash,yocto_wic*,xspi_flashwriter) — raises nothing, becausetancomposes no probe session there forexpect_dpidrto arm. That is not a safety claim about those methods;west flash's own runner, for one, may well drive a J-Link, andtanhas no view into how it selects a probe.
An unattended bench reads no warnings, and the openocd/pyocd arm emits none to
read, so tan flash also honours an env switch: with
ALP_FLASH_REQUIRE_DPIDR=1 exported, a real write whose DPIDR preflight
would not run fails the entry before anything is spawned
(flash.entry-failed) instead of proceeding. Unset — the default — nothing
changes.
Its scope is the same table as the advisory (tan-cli#609): Flow D today. It was
swd_probe-only when tan-cli#589 shipped it (that backend was removed by
tan-cli#732), which left the AEN MRAM path — the genuine customer flash path
of the two, the GD32 bridge being factory-programmed by Alp Lab — outside both
halves of the guard. On Flow D the refusal fires ahead of the SETOOLS
auto-sign, not merely ahead of the write:
app-gen-toc appends a block to build/app-package-map.txt and rewrites
build/AppTocPackage.bin whole, and tan-cli#512 measured a wrong-board abort
that correctly left slot0 byte-identical and still left the SETOOLS install
mutated.
The policy belongs to the host, not to the manifest. Export it on a factory or
bench machine, where a wrong-board write is expensive and nobody is watching;
leave it unset on a customer machine, where a bricked-bridge recovery must not
be blocked by a metadata field alp-sdk has not populated yet. It is read as the
exact string 1, the same as ALP_FLASH_FORCE.
Two things it does not do: it does not apply to --dry-run (a preview
writes nothing), and it does not make expect_dpidr mandatory in metadata. No
shipped alp-sdk preset carries a SW-DP ID today, and tan is forbidden from
deriving one — until metadata populates the field, exporting this variable
refuses these writes rather than guarding them.
A Flow D write does not add an entry to the ATOC. It loadbins a new ATOC over
the old one, replacing the entire table — and, unlike Flow A over the
SE-UART, there is no channel to ask the part what is resident first. So every
boot entry already in MRAM that your new ATOC does not name (an A32 boot chain,
an HP-core app, a diagnostic image) is silently delisted by the write. The
Secure Enclave then reports [SES] ATOC ok, because from its point of view the
table it was handed is perfectly valid — nothing in the transcript says
anything was lost.
Because tan cannot enumerate what it is about to replace, it asks you to
say you accept it. A confirmed Flow D write refuses
(flash.atoc-replacement-unacknowledged, exit 1) unless one of exactly two
spellings acknowledges the replacement:
--atoc-unqueryableontan flash(and ontan run --flash);flash_args.atoc_unqueryable: trueinbuild/system-manifest.yaml— fortan flash, which reads that file as it stands. It is not an option fortan run --flash: every such run regeneratesbuild/system-manifest.yamlfrom the planner before flashing, and the planner composesflash_argsfrom the keys it knows, so a hand-added acknowledgement is overwritten by the same command that then asks for it. Ontan run, pass the flag.
This is the same guard alp-sdk#2025 put on the bench scripts themselves —
scripts/bench/aen/flash-jlink.sh, flash-jlink-hp.sh and
flash-jlink-mramxip.sh refuse with exit 8 without the identical flag.
Three properties worth knowing:
- It is not
--confirm, and never an alias for it.--confirmmeans "yes, write"; this means "yes, I accept that the entire ATOC is replaced". A confirmed run still refuses without it — including anALP_FLASH_FORCE=1bench, deliberately. - There is no environment variable, on purpose. Unlike
ALP_FLASH_FORCEandALP_FLASH_REQUIRE_DPIDR, which are properties of the host, this is a statement about this write's ATOC. An env var would be exported once into a shell profile or a CI job and then acknowledge every future write, including the unattended ones — which is exactly what alp-sdk#2025's own header warns against when it says the Flow D flag must never be merged or aliased with Flow A's--replace-atoc. - Previews still preview. The refusal fires only where the write would
really proceed (confirm gate armed and not
--dry-run).tan flash --dry-runand an unconfirmed run both still report what they would do — and their message now states the whole-ATOC replacement, so you read it before arming the write rather than after.
A present-but-null or non-boolean flash_args.atoc_unqueryable is refused at
plan time — under --dry-run, and before any SETOOLS spawn — rather than read
as an absent key, so a mistyped acknowledgement is never quietly the same as no
acknowledgement.
On Flow D the refusal fires ahead of the SETOOLS auto-sign (same placement, and
same reason, as the wrong-board refusal above: app-gen-toc writes into your
SETOOLS install, and a refusal that fires after it has already run is a refusal
that did not prevent the mutation), and after the ALP_FLASH_REQUIRE_DPIDR
gate — writing the right table to the wrong board is the worse of the two
failures.
This is a Flow D guard, and only a Flow D guard. A slice that stays on
zephyr_west_flash — no jlink_flash_device on its flash_args, so west flash picks the board.cmake default alif_flash runner and burns the ATOC
over the SE-UART (Flow A) — writes with no acknowledgement and no warning
today. Every published Ensemble variant in alp-sdk metadata carries
debug.jlink_flash_device, so a planner-emitted AEN manifest dispatches Flow D
and is guarded; a hand-written or legacy manifest without that key is not.
That gap is deliberately left open rather than closed by widening this flag:
alp-sdk#2025's own header says the Flow D flag must never be merged or aliased
with Flow A's --replace-atoc, precisely because an operator on a no-SE-UART
slot passes the Flow D one on every run — and a flag that answered both gates
would silence the one that can query the part first. Flow A's answer is a
query-based check (upstream's bench_atoc_replace_guard), which is its own
piece of work.
tan flash no longer has a local-write path for the E1M-X V2N/V2M SoMs' GD32
bridge supervisor MCU (the swd_probe backend, removed by tan-cli#732 — GD32
programming is separating out of tan entirely). The GD32's field-update
path is untouched and stays: helper_firmware[].update_channel: alp_ota_spi_bridge (protocol v0.6 Path A, slot-A/B application bootloader
with commit and rollback, over the bridge link rather than SWD), which alp-sdk
still emits and tan still projects into build/system-manifest.yaml. A
project that previously relied on tan flash --helper gd32_bridge for a local
SWD write (recovering a bricked bridge, say) has no in-tree tan replacement
as of this change; that gap is tracked separately, not silently dropped — see
tan-cli#610 (needs-silicon, the still-open contradiction over the GD32
bridge's own SW-DP ID), whose premise — settling expect_dpidr for a tan flash write to the GD32 — no longer applies now that tan has no such write
to arm, but stays open rather than closed over: the underlying SW-DP ID
contradiction is a real, unresolved bench fact that whatever tool ends up
programming the GD32 will still need.
docs/adr/— architecture decisions this backend follows (no new hardware fact invented intan; every identifier above comes fromflash_args, which alp-sdk'smetadata/**populates).- tan-cli#353, #365, #366, #367, #368, #369, #373 — the issues this doc and the surrounding fixes answer.
- tan-cli#520, #589, #609 — the wrong-board SW-DP ID guard: the preflight itself, the opt-in strict switch, and making both method-independent.
- tan-cli#732 — removed the
swd_probeflash backend (GD32 programming separating out oftan); #610 above is the open follow-up it leaves. - tan-cli#1252 —
--atoc-unqueryable: a Flow D write replaces the whole ATOC and cannot enumerate what is resident first, so the replacement must be acknowledged. Ports alp-sdk#2025 (PR alp-sdk#2029), which put the same refusal on the AEN bench scripts.