After ExitBootServices, firmware's console is gone, and Glint needs a
driver of its own to be seen at all. The plan's answer is the second kind of
provider under Concept in PLAN.md: an upstream Zephyr
driver compiled into Glint through compat/zephyr, as Run modes there
states for native mode's console. Track Z builds the smallest seam that
makes this work. Zephyr's PL011 driver (on QEMU virt) and its NS16550
driver (on q35, by I/O port) are compiled unmodified, run in poll mode, and
stand behind Glint's console protocols, so that Phase 2's native system
table (2.5) is built on them and its exit (2.6) prints through them.
The evidence for this track is docs/zephyr-survey.md.
Its probe compiled both drivers, linked them into EFI images and printed
through them on both sides of ExitBootServices. It found that the hard
part is the toolchain, not the API: Glint's images were built for
*-unknown-windows, which is LLP64 and MSVC-flavoured, while Zephyr assumes
LP64 GNU C. Zephyr's own source states the assumption, in
include/zephyr/kernel.h: "Zephyr currently assumes the size of a couple
standard types to simplify print string formats." The track therefore
started by moving Glint's images to ELF (Z.1), checks the drivers Phase 6
will want under that build, and only then writes the generators. The
generators and shim follow the survey's section 6 ("What a minimal early 6.8
should contain", "What a minimal early 6.9 should contain"). Which Phase 6
slices extend them is stated once, under What Track Z leaves below.
Zephyr is pinned at release v4.4.2, commit
dccb09599635bdff17633fa7e9dab014b91dce90. The source is an external tree,
not a copy in the checkout: as Track T does with QEMU_SRC, the build finds
it through ZEPHYR_SRC and checks that it is that commit, against the record
in third_party/zephyr/PIN.md (Z.2), which is the one place the pin is
written down in the repository. Zephyr's build system is not used (Build
constraints). Each generator follows Track T's rule: it works from the data
Zephyr ships and the document that specifies it, not from the Python program
or its output. The oracle is Zephyr itself: its unchanged headers must accept
the generated ones, and its drivers must compile with warnings as errors and
run. Every Zephyr name stays inside compat/zephyr and the generated
headers; Glint's core sees only EFI-shaped protocols. The track cites
Zephyr's own documentation and source in place of the development text, as
rule 2 allows for tracks.
The track runs after Track T. Z.1 to Z.9 are merged; they were refined with
Phase 2 (rule 9). Three constraints fix their order against
Phase 2. Z.1 came before any Phase 2 slice added code to images built the
old way. Z.3 lands before 2.4a, because 2.4a's spec checks Glint's
devicetree reader against cleave on the virt devicetree that Z.3 records.
Z.9 lands before 2.5, which builds native mode's system table on Z.9's
provider. Otherwise Z.4–Z.9 may interleave with 2.2–2.4. From Z.2 on, what
differs between the architectures is data (a devicetree, a symbol table), so
each slice lands for both at once. Z.1 was different: its entry code in
boot/crt0.S, its relocation check and its calling-convention thunks are
code that differs by architecture, under #ifdef, though one linker script
serves both. Sizes are non-spec lines, as in rule 2.
Slices
Z.1 ELF images with a PE header. Merged as check-in 8b4357fa. This
slice changed how Glint's two images are built, and nothing they do. Glint's
Crystal and C are compiled for aarch64-unknown-linux-gnu and
x86_64-unknown-linux-gnu, freestanding and position-independent, instead
of *-unknown-windows, and tools/glint_build.cr links them with ld.lld
as an ELF shared object under one linker script, boot/efi.lds, then copies
out the loadable bytes with llvm-objcopy. The PE32+ header is not written
by the build driver: it is assembled in boot/crt0.S, at the front of the
image, from symbols the linker script defines (the sizes and offsets of
.text, .data and .reloc). Its .reloc directory holds one empty block,
which is all firmware needs, because the real fix-ups are ELF RELATIVE
relocations: _start in crt0.S calls glint_relocate in
c/efi_reloc.c, which applies them, refusing any other relocation type,
before it calls efi_main. This is the method Linux's arm64 Image
(arch/arm64/kernel/efi-header.S) and U-Boot's EFI applications
(arch/arm/lib/crt0_aarch64_efi.S, elf_aarch64_efi.lds) use. On x86_64,
UEFI's functions use the Microsoft x64 calling convention (UEFI 2.10 §2.3.4)
while ELF code uses System V. Firmware's entry is converted in crt0.S,
which moves the two arguments from rcx and rdx to rdi and rsi, and
every call into UEFI goes through one boundary: c/efi_boundary.c holds one
small C function per firmware call, declared ms_abi on x86_64, and
boot/efi_boundary.cr routes Crystal's calls through it in the images and
straight to the fake table in specs. On aarch64 both sides use AAPCS64, so
the same functions compile as plain calls. It was demonstrated by
re-proving Phase 1 under the new build: bin/glint-run aarch64 --expect
"Hello from C via Glint" (and x86_64) boots at rung 3,
spec/efi_image_spec.cr checks the PE32+ header, subsystem 10, .reloc
and no imports at rung 2, and spec/elf_dialect_spec.cr checks that a C
long and Crystal's LibC::Long are 8 bytes on both targets. It came first
because the survey found the object format to be the riskiest assumption
(risk 1), and because every Phase 2 slice adds code to these images; the
generator slices that follow run on the Linux host and do not need them
until Z.8 links drivers in. Under ELF, Zephyr's C compiles as Zephyr's own
build compiles it: the survey's two patches and two MSVC-countering flags
are no longer needed, and a linker script can give Zephyr's section-sorted
tables a home (risk 3). One C dialect for Glint and Zephyr, stated and
checked, is Clarity; removing the patches and flags is Parsimony. It came to
297 lines added and 32 removed, over ~200 because the header, the
relocation step and the thunks only boot together: an image missing any one
of them does not load.
Z.2 Checking the Phase 6 drivers first. Merged as check-in 1e8246d1. The survey's section 6 asks for
one cheap check "before any generator is written": compile the drivers Phase
6 is likely to want with the pointer-cast warnings promoted to errors
(-Werror=pointer-to-int-cast -Werror=int-to-pointer-cast), and with
-Wshorten-64-to-32 as a warning whose every instance is recorded. A
narrowing from a fixed-width type, such as uint64_t to uint32_t, is
the same on every ABI and only noted; one whose source type is long,
size_t, uintptr_t or another width-dependent type is what this check
exists to find. The first run found the former in uart_pl011.c (the
baud divisor), which Zephyr's own build does not warn about. This slice
adds third_party/zephyr/PIN.md
(release, commit, how to fetch the tree, and an empty list of patches),
tools/zephyr/source.cr, which finds ZEPHYR_SRC and checks its commit
against PIN.md, and tools/zephyr/flags.cr, the one list of flags every
Zephyr object is built with. spec/zephyr/width_spec.cr then compiles, for
both architectures, uart_pl011.c and uart_ns16550.c with
CONFIG_UART_INTERRUPT_DRIVEN, drivers/virtio/virtio_mmio.c,
virtio_pci.c, virtqueue.c and drivers/disk/nvme/*.c, against
hand-written generated headers kept as spec data in spec/fixtures/zephyr/,
as the survey's probe did. The fixtures come in two configurations per
architecture. irq/ is the one this check uses: interrupt-driven, with
interrupt cells in its devicetree header. poll/ is the configuration the
first console runs in: the survey's 18-line autoconf.h without
UART_INTERRUPT_DRIVEN, and the two syscall headers the poll path needs.
Z.5 compiles against poll/ until Z.6 and Z.7 generate their own. The slice
also lists the inline assembly that could be wrong-sized on LP64. The method
is clang's own: for each target, clang -M with Z.2's flags names the
headers each listed driver includes, and the spec takes those under
include/zephyr/arch/ (the arm64 and common headers for aarch64, the x86
and common headers for x86_64). It then runs clang -fsyntax-only -Xclang
-ast-dump=json on each driver and keeps every inline assembly statement
(GCCAsmStmt) in those headers that has an operand whose type is long or
unsigned long. The list goes in PIN.md under Inline assembly with long
operands, one line per operand, sorted by header and line:
include/zephyr/arch/x86/intel64/arch.h:61 arch_irq_lock output0 "=g"
unsigned long, that is, header and line, enclosing function, operand,
constraint and C type. The spec checks that the list it finds equals the one
recorded. On LP64 such operands are 64 bits, which is what the assembly
expects, so the list is a record to review when the pin moves, not a list of
patches. It is demonstrated at rung 1 by ZEPHYR_SRC=… crystal spec
spec/zephyr/width_spec.cr. It comes right after Z.1 because it tests Z.1's
claim on the code Phase 6 will actually use, and because a failure here
should reach the maintainer before any generator is written against an
assumption that does not hold. Showing the assumption rather than trusting
it is Demonstrability. It is about 150 lines; the fixtures are spec data and
not counted.
Z.3 Reading Zephyr's bindings, and the build devicetrees. Merged as check-in 26a1c624. Zephyr's
drivers read their devicetree through macros, and the macros carry each
property in a form set by the node's binding, a YAML file in
dts/bindings/. Zephyr's documentation puts it this way: "The build system
uses bindings both to validate devicetree nodes and to convert the
devicetree's contents into the generated devicetree_generated.h header
file" (doc/build/dts/bindings-intro.rst). This slice adds
tools/zephyr/bindings.cr, which reads bindings with Crystal's YAML,
following doc/build/dts/bindings-syntax.rst: it merges include: files,
records each property's type (int, boolean, string, string-array, array,
phandle, phandles, phandle-array), its default and whether it is required,
and matches nodes to bindings by compatible. It also adds the build
devicetree for each machine class, as the plan's principle 6 describes, and
keeps every input to them in compat/zephyr/dts/, since they are build
inputs and not spec data. For virt, the input is QEMU's own devicetree,
recorded by this slice as compat/zephyr/dts/qemu-virt.dtb from the
project-built QEMU with exactly the machine arguments glint-run passes
(the runner gives one method for both, plus dumpdtb=…), so a slice that
changes them, such as turning on virtualization=on for EL2, regenerates
the file. compat/zephyr/dts/qemu-virt-overlay.dts
relabels its UART arm,sbsa-uart, the maintainer's choice: in that mode
Zephyr's driver keeps the baud rate and format U-Boot already set, and reads
no clock or speed property (survey section 4). The overlay is applied by
this slice's tools/zephyr/build_dt.cr, because cleave at Glint's pin
compiles overlays (/plugin/, with __fixups__, as dtc 1.7 does) but has
no counterpart of fdtoverlay to apply one, and its compiler reads source,
not a DTB. build_dt.cr reads the recorded DTB with Cleave::Fdt, compiles
the overlay with Cleave::Dts and Cleave::Dtc, and for each fragment that
names its target by target-path sets the fragment's properties on that
node. It refuses a fragment that targets a phandle, and any overlay with
__fixups__, since a DTB dumped by QEMU carries no labels; so the overlay
names its node by path (&{/pl011@9000000}). Cleave's compiler emits a
target-path fragment for a path reference, as dtc 1.7 does
(add_orphan in src/cleave/dtc.cr), so the applier needs nothing new
from cleave. For q35,
which has no devicetree, compat/zephyr/dts/pc-com1.dts writes the PC's
COM1 as data (ns16550, port 0x3f8, io-mapped, reg-shift,
clock-frequency = <1843200>, current-speed = <115200>, and no
interrupts), as Zephyr's own dts/x86/intel/atom.dtsi does. A spec loads
the bindings of every node in both trees from ZEPHYR_SRC, checks the
merged types of the UART properties, rejects a node that lacks a required
property, and checks that the overlay changed only the UART's compatible.
With QEMU_SRC, it also dumps the devicetree again and checks that it is
byte-identical to the recorded one, so a change of QEMU pin shows up here.
It is demonstrated at rung 1 by crystal spec spec/zephyr/bindings_spec.cr.
It opens the generator because every macro the next two slices write
depends on a property's type, and it must land before 2.4a, whose spec reads
the recorded virt devicetree; the file is made once and serves both.
Reading Zephyr's bindings as data, through cleave's existing devicetree
code, keeps each mechanism in one place, which is Parsimony. It is about 230
lines, plus about 40 lines of devicetree source.
Z.4 Node and property macros. Merged as check-in 4183503f. This slice adds
tools/zephyr/devicetree_header.cr, which writes the first half of
devicetree_generated.h: the per-node and per-property macro families that
the survey's section 4 lists for the two drivers. For each node it writes
_PATH, _FULL_NAME, _PARENT, _EXISTS, _STATUS_okay,
_COMPAT_MATCHES_<compat>, _REG_NUM, _REG_IDX_0_EXISTS,
_REG_IDX_0_VAL_ADDRESS and _REG_IDX_0_VAL_SIZE. For each property its
binding declares it writes _P_<prop> and _P_<prop>_EXISTS, with booleans
always present as 0 or 1. For compatible it adds _P_compatible_LEN and
_P_compatible_IDX_<i> with its _STRING_TOKEN, _STRING_UPPER_TOKEN and
_EXISTS forms, and for clocks the phandle reference
_P_clocks_IDX_<i>_PH. It also writes the helper DT_DEBRACKET_INTERNAL.
The names follow doc/build/dts/macros.bnf, "An RFC 7405 ABNF grammar for
devicetree macros", which is the specification this generator works from.
Zephyr's own include/zephyr/devicetree.h, used unchanged, is the oracle: a
spec compiles a small C file of _Static_asserts through the real header
with clang -fsyntax-only (for example, that DT_REG_ADDR of the virt
UART is 0x9000000 and its first compatible token is arm_sbsa_uart, and
that COM1's io_mapped is 1). It is demonstrated at rung 1 by crystal spec
spec/zephyr/devicetree_header_spec.cr. It follows Z.3 because each macro
family is chosen by property type. Judging the generator by Zephyr's header
rather than by Python's output is Demonstrability. It is about 240 lines.
That passes ~200 because the eight property types are one table: until every
type the two trees use is written, devicetree.h cannot evaluate any
property, so a smaller split would leave a half that cannot be checked.
Z.5 Ordinals, instances and iteration. Merged as check-in aab5992c. Zephyr's device model needs more
than per-node facts. Every C file that includes device.h expands
DT_FOREACH_OKAY_HELPER over the whole tree (include/zephyr/device.h), and
drivers find their instances through DT_INST_FOREACH_STATUS_OKAY. This
slice extends devicetree_header.cr with the rest of the survey's section 4
list: dependency ordinals (_ORD and _ORD_STR_SORTABLE, in an order where
a node follows its parent and the nodes its phandles name), instance
identifiers (DT_N_INST_<i>_<compat>), and the global families
DT_FOREACH_HELPER, DT_FOREACH_OKAY_HELPER, DT_COMPAT_HAS_OKAY_<compat>,
DT_N_INST_<compat>_NUM_OKAY, DT_FOREACH_OKAY_<compat>,
DT_FOREACH_OKAY_VARGS_<compat>, DT_FOREACH_OKAY_INST_<compat> and
DT_FOREACH_OKAY_INST_VARGS_<compat>. Its acceptance test is the one the
survey set: uart_pl011.c and uart_ns16550.c compile with -Werror (Z.2's recorded
narrowings excepted) against the generated devicetree header. They compile in poll mode, since
the generated header has no interrupt cells: autoconf.h and the syscall
headers are still Z.2's hand-written poll/ fixtures, which leave
CONFIG_UART_INTERRUPT_DRIVEN undefined. It is demonstrated at rung 1 by
ZEPHYR_SRC=… crystal spec spec/zephyr/devicetree_header_spec.cr. It comes
after Z.4 because ordinals and iteration are built over the node macros.
With it, the devicetree generator is complete for poll-mode UARTs. Stopping
at what the two drivers reach is Parsimony. It is about 200 lines.
Z.6 autoconf.h from a checked table. Merged as check-in 5c8819ca. Zephyr's drivers also read
configuration symbols. As Zephyr's documentation says, "The output from
Kconfig is a header file autoconf.h with macros that can be tested at build
time" (doc/build/kconfig/index.rst). Zephyr's Kconfig tree expects a board
and a SoC, which Glint does not have (principle 6; survey risk 6). So this
slice does not run that tree. It adds a short symbol table per architecture,
compat/zephyr/config/aarch64.cr and x86_64.cr, holding the symbols the
survey's section 4 lists. Both tables hold SERIAL, SERIAL_HAS_DRIVER,
SERIAL_INIT_PRIORITY (50), 64BIT and ATOMIC_OPERATIONS_BUILTIN, and,
because kernel.h reads them, NUM_PREEMPT_PRIORITIES,
NUM_COOP_PRIORITIES, MP_MAX_NUM_CPUS,
SYS_CLOCK_TICKS_PER_SEC, SYS_CLOCK_HW_CYCLES_PER_SEC and
SYS_CLOCK_MAX_TIMEOUT_DAYS, with SYS_CLOCK_EXISTS, on which the hardware cycle rate
depends. PRIVILEGED_STACK_SIZE is in the aarch64 table only, as 2048: Kconfig
gives it a value there from arch/arm64/core/Kconfig, and none on x86_64. The aarch64 table adds ARM64,
UART_PL011 and UART_PL011_SBSA; the x86_64 table adds X86, X86_64,
MAX_IRQ_LINES, UART_NS16550 and UART_NS16550_VARIANT_NS16550. The
DT_HAS_<compat>_ENABLED symbols come from the build devicetree, not the
table. The survey probe's value is written beside each symbol; where Kconfig
would pick another for a symbol with no visible prompt, Kconfig's value wins,
and the spec checks it. Both tables also name the symbols that must stay
absent (UART_INTERRUPT_DRIVEN, UART_ASYNC_API, PINCTRL, RESET,
CLOCK_CONTROL, PM_DEVICE, MMU, USERSPACE, LOG). Each table is closed
under select, with one exception, chosen by the maintainer: on x86_64,
X86_64 selects X86_MMU and so MMU, and the table overrides it to keep
MMU unset as on aarch64. The spec prints the override whenever it runs. The slice adds
tools/zephyr/autoconf.cr, which writes the header as Kconfig does, leaving
a disabled boolean undefined, not 0. The DT_HAS_<compat>_ENABLED symbols
are computed from the build devicetree (Z.3), the job Zephyr's
gen_driver_kconfig_dts.py does. The table is checked rather than trusted,
with Cleave::Kconfig's Zephyr dialect, which reads Kconfiglib's grammar,
its source forms and its preprocessor functions (cleave 42316ae7, added in
answer to docs/upstream/cleave-zephyr-kconfig-dialect.md). Zephyr's tree
expects a board, so the spec does not load it from the top. It loads each
file that defines a table symbol under a two-line root that sources
Kconfig.constants and then the file, following the file's own source
lines as Kconfiglib would: drivers/serial/Kconfig, Kconfig.pl011 and
Kconfig.ns16550, arch/Kconfig, arch/arm64/core/Kconfig,
arch/x86/Kconfig, kernel/Kconfig and kernel/Kconfig.smp. The one file
Zephyr's CMake would have generated, $(KCONFIG_BINARY_DIR)/arch/Kconfig, is
an empty stand-in, since the architecture files are loaded on their own.
Through cleave's functions hook the spec answers
$(dt_compat_any_has_prop,…), the one devicetree function the serial files
call, from the build devicetrees. The three calls ask
whether an ns16550 node has interrupts or pinctrl-0, and whether an
arm,pl011 node has pinctrl-0; for these trees all three are n, since
COM1 has neither property and virt's UART is arm,sbsa-uart. Every other $(dt_…) call
(arch/Kconfig has eight, for SRAM, flash and cache-line sizes) gets
cleave's default, Kconfiglib's answer with no devicetree, and the spec fails
if a table symbol's definition contains one. It then checks that each table symbol is defined in
one of those files with the table's type, and that its depends on holds,
for a symbol defined in several places, as the OR of every definition's
dependencies, each default applying under its own definition's conditions,
when evaluated with the table's symbols at their values, the DT_HAS_*
symbols from the build devicetree, and every symbol outside the table as
n, which is how Kconfig treats a symbol that is not set. The survey also
names UART_LOG_LEVEL for the NS16550; it is defined by the logging
template with depends on LOG, so the table leaves it out, and Z.7's
compile shows whether the driver needs it with LOG unset. It is
demonstrated at rung 1 by ZEPHYR_SRC=… crystal spec
spec/zephyr/autoconf_spec.cr. It needs only Z.3, for the DT_HAS_*
symbols, so it may land before Z.4 and Z.5; Z.7 needs all three. A short
table that every reader can see, checked against the files that define it,
is Clarity; the full tree belongs to Track S, which has a Zephyr board. It
is about 170 lines.
Z.7 Syscall stubs. Merged as check-in 4eb32a10. Zephyr's headers declare many functions with
__syscall and then include a generated zephyr/syscalls/<header>.h. Glint
never enables Zephyr's user mode, and Zephyr's documentation states what
then happens: "If CONFIG_USERSPACE is not enabled, all APIs just directly
call the implementation function" (doc/kernel/usermode/syscalls.rst).
This slice adds tools/zephyr/syscalls.cr. It finds the headers the build
includes with clang rather than from a fixed list: for each driver in the
source list, clang -M -MG with Z.2's flags names the header closure,
counting the not-yet-generated zephyr/syscalls/*.h as dependencies rather
than failing on them. For every header in the closure that includes one of
those, it reads the __syscall declarations and writes the matching
zephyr/syscalls/<header>.h with an inline wrapper per declaration that
calls z_impl_<name>; it also writes empty syscall_list.h,
kobj-types-enum.h and heap_constants.h. Its demonstration is that both
UART drivers now compile with -Werror in poll mode from generated headers
alone, with none of Z.2's hand-written ones: ZEPHYR_SRC=… crystal spec
spec/zephyr/syscalls_spec.cr at rung 1. Z.2's fixtures stay, for the Phase
6 drivers the generators do not yet cover. It comes after Z.5 and Z.6
because that compile needs their headers. Writing stubs that call straight
through, instead of building any system-call machinery, is Parsimony. It is
about 140 lines.
Z.8 The minimal shim runtime. Merged as check-in 36aeae9c. Compiled drivers must now be linked into
Glint and started. Zephyr's documentation gives the job: "The device model
is responsible for initializing all the drivers configured into the system"
(doc/kernel/drivers/index.rst). Zephyr does this by walking linker
sections; Glint generates a table instead (survey section 4, risk 3). This
slice adds tools/zephyr/device_table.cr, which writes glint_devices.c, a
table of DEVICE_DT_GET for each enabled node whose compatible a built
driver claims, in ordinal order. Beside each device the table records the
compatible and first reg address it was built from, so that 2.6 can
compare the firmware's description with the class the image was built for.
It adds compat/zephyr/device.cr, with the init runner (call init, store a
positive errno in init_res, set initialized, as do_device_init in
kernel/device.c does) and z_impl_device_is_ready, and
compat/zephyr/libc.cr, with memcpy and memset as Crystal exports. The
only kernel object the two drivers reach needs no code from Glint: the
NS16550 takes k_spin_lock and k_spin_unlock around each register access,
and those are Zephyr's own inline functions in include/zephyr/spinlock.h,
used unchanged, over arch_irq_lock; on one CPU, masking interrupts is all a
spinlock must do. Finally, tools/zephyr/sources.cr holds the source list,
and tools/glint_build.cr compiles the listed drivers into both images. It
is demonstrated at two rungs. At rung 1, a spec compiles
spec/fixtures/zephyr/test_device.c for the Linux host with headers
generated from spec/fixtures/zephyr/test.dts, a test devicetree with two
nodes of the compatible vnd,device-with-props. That compatible's binding
is Zephyr's own, dts/bindings/test/vnd,device-with-props.yaml, read from
ZEPHYR_SRC like every other binding, so the test needs no binding of
Glint's. The test driver's init succeeds for one instance and fails with
-EIO for the other, and the spec runs the init runner and checks
init_res, initialized and device_is_ready. The host is x86_64 and
LP64, so it uses the x86_64 table, and the test driver calls nothing that
needs privilege. At rung 2, both images link with the real drivers, and a
spec checks that, before linking, their only undefined symbols are the ones
the survey found (memcpy for the PL011, z_impl_device_is_ready for the
NS16550). The runtime does not initialise any device at boot: 2.5's console
bring-up does that, after the exit. It comes after Z.7 because it links
objects compiled from generated headers alone. Implementing Zephyr's API in
Crystal, rather than copying Zephyr's kernel, is Loose Coupling. It is about
220 lines. That passes ~200 because a driver cannot link without the
runtime, and the runtime cannot be shown in the images without the build
step, so either half alone would need a temporary stub.
Z.9 Zephyr's UART as a console provider. Merged as check-in 710533c9. This slice adds
compat/zephyr/console.cr, which wraps a Zephyr UART device as Glint's
console protocols, and compat/zephyr/status.cr, the one table that maps
Zephyr's results to EFI_STATUS (risk 7): negative errno values, the -1
of poll_in, and the positive errno that init_res holds. The table lands
here because this is its first user. SIMPLE_TEXT_OUTPUT.OutputString
sends each UCS-2 character through uart_poll_out encoded as UTF-8
(callers send \r\n, as UEFI expects); an unpaired surrogate goes out as
?. Reset succeeds. The calls a UART cannot honour, such as
SetAttribute and ClearScreen, return EFI_UNSUPPORTED.
SIMPLE_TEXT_INPUT.ReadKeyStroke calls uart_poll_in, which "is a
non-blocking function and returns a character or -1 when no valid data is
available" (doc/hardware/peripherals/uart.rst); its "nothing yet" becomes
EFI_NOT_READY, which Phase 1's Console.read_line already polls past.
Bytes become keys with a scan code of 0, following the maintainer's rules
for serial input: UTF-8 is decoded, so 0x20–0x7E and each complete
multi-byte sequence for a character in the Basic Multilingual Plane become
that character, and an invalid or out-of-plane sequence is dropped; carriage return as Enter,
and a line feed as Enter unless it directly follows a carriage return, in
which case it is ignored; 0x08 and 0x7F as Backspace; an escape and the
control sequence it starts are read to the sequence's final byte and
dropped, since Phase 1's shell uses no arrow keys; other control bytes are
dropped. poll_out waits for the FIFO with no timeout (survey section 2),
which is acceptable for a console. Its demonstration runs Phase 1's own
keyboard and shell specs through Zephyr's API, without changing them. A spec
compiles spec/fixtures/zephyr/fake_uart.c, a Zephyr UART driver whose
poll calls read and write buffers, for the Linux host, as a node of the
compatible vnd,serial added to Z.8's test devicetree; its binding is again
Zephyr's own, dts/bindings/test/vnd,serial.yaml. spec/support/fake_efi.cr
learns one option, the environment variable GLINT_FAKE_CONSOLE=zephyr:
with it set, the fake table's ConIn and ConOut are this provider in
front of the fake UART, script_keys fills the UART's receive buffer, and
text reads its transmit buffer. spec/zephyr/console_spec.cr runs
crystal spec spec/keyboard_spec.cr spec/shell_spec.cr in a child process
with the variable set and checks that the same examples pass, then checks
the byte-to-key rules and the status table directly. The two Phase 1 files
are not edited. That is rung 1, crystal spec spec/zephyr/console_spec.cr;
at rung 2 both images link the provider. Its rung-3 evidence is 2.6's
first boot through it: printing through a second console during a hosted
boot would need a mechanism that would then be removed. It comes last
because it wraps a device that Z.8 makes linkable. A Zephyr driver behind
the same protocol as the firmware's console, with no call site changed, is
the plan's Loose Coupling in its smallest form. It is about 170 lines.
Track demo: Phase 2's exit demonstration (2.6). bin/glint-run aarch64
--type native --expect "Mode: native" (and x86_64) leaves boot services,
prints Left boot services and the lines after it through Zephyr's PL011
and NS16550 drivers, compiled unmodified from the pinned release, and the
shell keeps answering through them. Within the track, the last
demonstration is Z.9's: Phase 1's keyboard and shell specs passing through
Zephyr's UART API.
Z.10 Uninitialised data the loader zero-fills. Merged as check-in 1bf86292. A maintenance slice to
Z.1's layout, found while preparing 2.6. .bss sat inside .data, so each
image stored its zero-initialised data as zeros: 2.3's 16 KiB map buffer
now, and 2.6's 64 KiB stack next. UEFI loaders zero-fill a section's
VirtualSize past its SizeOfRawData, as U-Boot does
(lib/efi_loader/efi_image_loader.c:1024–1027). So .bss becomes its own
NOBITS section, and the PE header gives .data a virtual size larger than
its raw size. Storing only what is not zero is Parsimony, and a spec that
checks the header fields against the file keeps it Demonstrable.
What Track Z leaves
This is the one place the plan says who extends Track Z. Z.3–Z.9 come to about 1,370 lines, within the survey's estimate of 1,150–1,550 for this scope; Z.1 (297 as merged) and Z.2 add about 450. The track leaves generators limited to what two poll-mode UARTs reach, a shim with one device table and no kernel object of Glint's own, and one console provider. Three Phase 6 slices extend them in place, and no other slice does:
- 6.3 adds interrupts:
interruptscells in the devicetree generator,IRQ_CONNECTandirq_enablein the shim, and a receive ring in Z.9's provider, with the UART drivers rebuilt withCONFIG_UART_INTERRUPT_DRIVEN; - 6.8 grows the generators for the first storage provider:
ranges, PCI andchosenin the devicetree generator (aliases and node labels only if that driver reaches them), the provider's symbols in theautoconf.htable, the retirement of Z.2's fixtures for that driver, and anio-mappedflag in Z.8's device table, so that 3.2's page-table layout can tell port I/O from memory-mapped devices (until then x86_64 maps no device, and a spec fails if a second device appears); - 6.9 grows the shim:
k_sem,k_mutex,k_work,k_timerand logging.
Pinctrl, clocks and resets stay avoided by the build devicetree, as the
survey's section 3 advises, because firmware has already configured the
UART's pins and clock. None of the three Phase 6 slices takes them. They
are needed first by a machine class whose UART cannot be used as firmware
left it. The first such slice is Track S's S.2, whose emulated board names
its UART's pins (The devicetree on a small target there). Late binding (principle 6)
is not placed in a slice either. CONFIG_MMU stays unset: Glint's own page
tables (3.2) map the registers that Z.8's device table records, at their
physical addresses, so the drivers need no k_mem_map_phys_bare. The
survey's section 6 lists the rest.
References
- The pinned Zephyr tree:
include/zephyr/kernel.h(the LP64 assumption, Z.1–Z.2);doc/build/dts/bindings-intro.rstandbindings-syntax.rst(Z.3);doc/build/dts/macros.bnf(Z.4–Z.5);doc/build/kconfig/index.rstand the Kconfig files Z.6 names (Z.6);doc/kernel/usermode/syscalls.rst(Z.7);doc/kernel/drivers/index.rst,kernel/device.candinclude/zephyr/spinlock.h(Z.8);doc/hardware/peripherals/uart.rstandinclude/zephyr/drivers/uart.h(Z.9);dts/bindings/test/(Z.8, Z.9). docs/zephyr-survey.md: the interface map, the probe, the minimal shim surface, the size estimate and the risks this track answers.- The UEFI specification 2.10 (PE32+ images, the x64 calling convention),
with Linux's
arch/arm64/kernel/efi-header.Sand U-Boot'sarch/arm/lib/crt0_aarch64_efi.Sandarch/x86/lib/crt0_x86_64_efi.Sas precedent for an ELF program with a PE header (Z.1). - cleave:
Cleave::Dts,Cleave::Dtc,Cleave::FdtandCleave::Kconfig(Z.3, Z.6). - Rationale: like Tracks T and P, this track builds a tool and a seam, not a chapter of the operating system, so it cites the documents of the project it connects to. Working from Zephyr's specifications and data keeps each generator to what the data requires (Parsimony). Judging it by Zephyr's own headers and drivers makes every claim checkable (Demonstrability). Implementing Zephyr's API behind EFI-shaped protocols, without copying Zephyr's kernel, keeps the seam replaceable (Loose Coupling).