The core apps -- wm, net, repl and term -- are programmed into
flash alongside the kernel and are available with no sdcard attached.
Two problems, and the second is the bigger one.
Getting started. A freshly flashed board used to boot to a serial shell and nothing else, because every app lived on the sdcard. "Flash the board and you have a desktop" is a far better first five minutes than "flash the board, now go write an sdcard".
Iterating. Updating the core apps meant hand-driving xf in minicom
four times, about a minute of interactive work per cycle, with the
terminal tied up throughout. make dev-flash is now unattended and
takes seconds.
Flash is memory-mapped on this SOC, which is what makes the whole thing
cheap: loading an app from it is a memcpy, with no filesystem and no
SPI driver involved. It is also faster than the sdcard, which is
bit-banged SPI (sw/os/fs/fatfs/sdmm.c).
This is the third use of that same property. The BIOS already loads the
kernel this way (load_zeitlos(), sw/bios/bios.c), and sw/os/logo.c
reads the boot splash straight out of flash into VRAM so it costs no
main memory at all.
Writing flash is slow, but that happens once per build, unattended, as
part of make flash.
0x10000000 MEM_ROM base
0x100F0000 boot splash
0x10100000 kernel, 256KB
0x10140000 core apps <-- this
The archive format is deliberately minimal:
offset size field
0 4 magic "ZAR1"
4 4 count number of entries
8 8 reserved must be 0
16 ... entries[count], 24 bytes each:
0 16 name (NUL-padded)
16 4 offset from start of archive
20 4 size bytes, the whole ZEXE file
... the ZEXE files themselves, in entry order
The stored files are ZEXE (sw/common/zexec.h) verbatim, exactly as
each app's own Makefile produced them. tools/mkzar.py concatenates
them without re-encoding, so there is one executable format to keep
working rather than two.
Z_ZAR_FLASH_OFFSET in sw/os/zar.h and the offset in Makefile's
flash_apps target must agree. Nothing checks that they do, and a
mismatch presents as "no core apps in flash" rather than an error.
The resolution rule is one line:
if the filesystem has it, use that; otherwise use the flash copy.
fs_exec_info_any() and fs_load_exec_any() (sw/os/fs/fs.c) are the
single place that decides. Every path that starts a process goes
through them: sh.c's run, sh.c's init, and k_proc_run() --
which is what wm's dock calls, and what lets term launch on a
card-less board.
There is still exactly one name for term. run term behaves
identically whether it came from a card, from flash, with no card at
all, or after being killed and restarted.
The card keeps its executables in apps/, alongside docs/, ark/
and user/, rather than loose in the root. fs_exec_resolve() is
what knows that. For a bare name it tries, in order:
- the root --
term apps/--apps/term- the flash archive, under the bare name
A name containing / is taken literally and not searched at all,
so docs/term cannot resolve to apps/term.
Two consequences worth stating plainly:
-
Nothing above this function changed when the apps moved.
dock_candidates[]in wm, the extension table inztype.c,z_proc_stack_size_for()inkernel.h, pidreg registrations andrun termat the shell all still use bare names. The alternative -- writingapps/at every one of those call sites -- would have put a constant prefix in a dozen places where it carries no information, and spent 5 ofZ_ZAR_NAME_MAX's 16 bytes on it in the archive too. A prefix repeated everywhere belongs in the resolver instead. -
Root is searched before
apps/, deliberately. That preserves the shadowing rule above: the only way a file reaches the card root is somebody deliberately putting it there, soxf wmstill hot-swaps a single app during development exactly as it did beforeapps/existed. A card written before the move also still boots, since its root-level apps are found first. The cost is one failedf_openper launch on a normally-laid-out card.
This is not the drive-letter idea below under another name. It applies
to executable resolution only -- files are still opened by exact
path, and fs_open/size/read/write, ls, te, repl's file API
and tget/tput are all untouched. Data is addressed; programs are
resolved.
Drive letters (A: = flash, B: = sdcard) were considered and
rejected. They are a namespace solution to what is actually a
fallback question, and the cost lands everywhere:
- Every path-taking API would have to learn about drives:
fs_open/size/read/write,ls,te, repl's Scheme file API,tget/tput. - Flash is read-only, so writes to
A:need a new failure path in each of them. - Worst, callers like wm's dock would have to know which drive an app lives on -- precisely the thing they should not have to care about. Adding default-drive rules to avoid that just reintroduces the ambiguity drives were meant to remove.
If explicit selection is ever genuinely needed, a flash:term prefix
handled inside fs_exec_info_any() is a much smaller change than
teaching the whole filesystem API about drives. That would become more
attractive if Zeitlos ever grows a second real storage device (USB mass
storage, a network mount), which is when drives start earning their
keep.
A file on the card wins. That is deliberate and needs no version scheme,
timestamp comparison or precedence rules: the only way an app got onto
the card is somebody deliberately putting it there, so treating that as
intent is exactly right. It keeps xf wm working as a single-app
hot-swap during development.
To make that visible rather than mysterious:
initprints each app's source at boot (init: wm (flash)).runprints it too (loading term from flash).lslists flash apps in a separatein flash:section, skipping any shadowed by a real file, so what it shows matches whatrunwould actually launch.
This is a first-class path, not a fallback that happens to work.
sh.c's auto-init used to poll for up to AUTOINIT_TIMEOUT_TICKS
(~3s) waiting for a slow sdcard to become readable. On a board with no
card that answer will never change, so the flash case is checked first
and skips the poll entirely -- otherwise a card-less board would stall
for three seconds on every boot before the desktop appeared.
That check only decides when to call init(). init() still resolves
per app, so a card holding only wm still gets its wm from the card
and everything else from flash.
- Add or remove it in
Makefile'soutput/$(BOARD_LC)/apps.zarrule. - Rebuild and reflash:
make BOARD=<board> flash_apps.
The kernel accepts up to Z_ZAR_MAX_ENTRIES (32) entries. There is no
requirement that a core app also appear in wm's dock -- the dock should
only offer apps guaranteed to resolve, which in practice means core apps
(gpu3d was removed from it for exactly this reason: it only ever
exists on a card, so its icon was a dead button on most machines).
Note apps must emit ZEXE, not a raw objcopy binary. mkzar.py warns
if one does not; such a file still loads, but ships its .bss as
literal zeros and wastes flash.
$ make clean && make BOARD=obst dev-flash
dev-flash rebuilds and reflashes the kernel and core apps without
touching the bitstream. Both are flashed together deliberately:
sw/common/syscalls.def is compiled into kernel.bin and into every
app, and a binary built against a different copy calls the wrong kernel
handler for every syscall past the point they diverge -- which is not a
crash, just quietly wrong behaviour. See that file's own warning.
If anything under rtl/ changed, use make flash instead.