Zephyr survey: Glint's platform interface and Zephyr's drivers
This survey records what was found before Track Z was planned. Where it numbers Phase 2's slices or says hosted mode stays the default, the plan has since moved on: the exit is slice 2.6, native mode is the default from 2.10, and the seam is Track Z (
docs/phases/Z-zephyr-seam.md). The findings about Zephyr itself stand.
Glint reaches hardware only through an EFI-shaped platform interface
(PLAN.md, Concept). Today every protocol behind it comes from firmware.
The plan's second kind of provider is upstream Zephyr C drivers compiled into
Glint through compat/zephyr (principle 5), with the generators that replace
Zephyr's Python build tools (slice 6.8), the shim (6.9) and the first
provider (6.10). This survey checks, before that work starts, how well
Glint's protocols map onto Zephyr's driver APIs. It also checks what Zephyr
drivers expect from the kernel underneath them, and what the smallest useful
Zephyr seam needs: Zephyr's PL011 and NS16550 UART drivers in poll mode,
carrying Glint's console across ExitBootServices in Phase 2.
Paths in include/, drivers/, kernel/, scripts/, dts/ and subsys/
are in the Zephyr tree at the pinned release. Line numbers refer to that
release. Glint paths are given from the checkout root.
For readers new to Glint or Zephyr
Glint's side. A protocol is a C struct of function pointers found by
GUID through LocateProtocol (boot/protocol.cr, boot/efi.cr). Calls
are synchronous: the caller passes its own buffer, the call returns when the
work is done or reports EFI_NOT_READY, and every result is an
EFI_STATUS (boot/console.cr polls ReadKeyStroke, and
fs/efi_file.cr opens and reads files through SIMPLE_FILE_SYSTEM).
Nothing calls back into Glint. Under U-Boot nothing uses interrupts either.
Zephyr's side. A driver instance is a struct device
(include/zephyr/device.h:513): a name, a config pointer (usually
read-only), a data pointer, a state record and a pointer to a class API
struct such as struct uart_driver_api
(include/zephyr/drivers/uart/uart_internal.h:39). Instances are created at
compile time by DEVICE_DT_DEFINE (device.h:259), one per
devicetree node with status = "okay" and a matching compatible. Every
property a driver reads comes from macros in a generated header,
devicetree_generated.h, and every option from a generated autoconf.h.
Zephyr's kernel then runs each device's init function in an order set by
linker sections, and drivers call kernel services (k_spin_lock, k_sem,
k_work, IRQ_CONNECT, logging) as they need them.
The seam. compat/zephyr is where Glint supplies those generated
headers and kernel services, so that unmodified Zephyr driver sources
compile and run, and where each driver is wrapped as an EFI-shaped protocol.
The plan intends Glint to implement Zephyr's API, not to copy Zephyr's
kernel implementation.
Summary
- The pin is Zephyr v4.4.2, commit
dccb09599635bdff17633fa7e9dab014b91dce90. Pull request #117311 (the MSPM0 I2C driver) is not in it. - A probe (described in section 4 and the appendix) compiled both
drivers/serial/uart_pl011.canddrivers/serial/uart_ns16550.cunmodified against Zephyr's own headers, a hand-writtendevicetree_generated.h, an 18-lineautoconf.hand two syscall stub headers. It linked each into a PE32+ EFI image with Glint'sld.lld -flavor linkmethod. Under the project's QEMU 11.1.2 and U-Boot 2026.07, each image printed through Zephyr's driver both before and afterExitBootServices. After compilation the PL011 object needs onlymemcpyfrom outside. The NS16550 object needs onlyz_impl_device_is_ready. - The hard part is the toolchain, not the API. Glint builds PE/COFF objects
for
*-unknown-windows, which is LLP64 (longis 32 bits) and turns on clang's MSVC preprocessor compatibility. Zephyr assumes LP64 GNU C. The probe hit three failures from this: a build assertion, a macro that silently evaluated wrong, and a stack corruption at run time. Each has a small fix. This is the riskiest assumption and should be settled first (section 6). - Basic I/O need not be lost. The console maps almost directly onto the
UART poll API. Block devices, the network and timers map with adapters,
and the network is the worst fit. The core services the two UART drivers
need in poll mode are tiny. The large items are the three generators,
above all
devicetree_generated.h.
1. The pin
| Item | Value |
|---|---|
| Release | v4.4.2, the latest release on GitHub's releases page (VERSION: 4.4.2) |
| Commit | dccb09599635bdff17633fa7e9dab014b91dce90 ("release: Zephyr 4.4.2"); the annotated tag object is 671f64aa79924606253238f801c494c84b02c2a0 |
| Clone | git clone --depth 1 --branch v4.4.2 https://github.com/zephyrproject-rtos/zephyr.git /workspace/zephyr-src |
| PR #117311 ("drivers: i2c: i2c_mspm0: add MSPM0 I2C driver") | Not in v4.4.2. The pull request is still open and unmerged, and drivers/i2c/ has no i2c_mspm0.c. It is not in v4.5.0-rc3 either |
Three facts about this pin bear on later work:
- The Zepto board is not in v4.4.2. The release has the MSPM0L1117 SoC
(
soc/ti/mspm0/mspm0l/Kconfig.soc:19,dts/arm/ti/mspm0/l/mspm0l1117.dtsi) and the MSPM0 UART driver (drivers/serial/uart_mspm0.c). Thebeagleconnect_zeptoboard that Track S builds on was added onmainafter the 4.4 branch, and it is present atv4.5.0-rc3. MSPM0 drivers also need TI's driverlib from the separatehal_tiwest module (uart_mspm0.c:20, pinned atwest.yml:275–277), so Track S needs a second pinned source tree. - There is no virtio-blk driver in v4.4.2. Slice 6.10 names
"virtio-blk on QEMU first".
drivers/virtio/holds only the transports (virtio_mmio.c,virtio_pci.c,virtqueue.c). The virtio users in v4.4.2 aredrivers/ethernet/eth_virtio_net.c,drivers/serial/uart_virtio_console.canddrivers/entropy/entropy_virtio.c. Adisk_accessdriver,drivers/disk/virtio_blk.c, is onmainand inv4.5.0-rc3. NVMe (drivers/disk/nvme/) is in v4.4.2 and runs on QEMUq35. - v4.5.0 is at its third release candidate. Both UART drivers,
device.h,uart.handkernel.hdiffer betweenv4.4.2andv4.5.0-rc3. So v4.4.2 serves the Phase 2 work. Slice 6.10 and Track S want the next release, and the probe in the appendix should be repeated at each pin change.
2. Interface map
"Fit" means: direct (a one-to-one call with only status translation), adapter (Glint code reshapes calls or data but the meaning carries over) or mismatch (the two sides disagree about who drives the work, who owns memory or what exists at all).
| Glint protocol | Zephyr API that would provide it | Fit | Specific mismatches |
|---|---|---|---|
Console output (SIMPLE_TEXT_OUTPUT.OutputString) |
uart_poll_out (include/zephyr/drivers/uart.h:397) |
adapter, thin | One byte per call against a NUL-terminated CHAR16 string, so the adapter converts UCS-2 and \n. SetAttribute, SetCursorPosition, QueryMode and ClearScreen have no UART equivalent; the adapter can emit ANSI sequences or return EFI_UNSUPPORTED. poll_out spins until the FIFO has room, with no timeout (uart_pl011.c:224, uart_ns16550.c:986) |
Console input (SIMPLE_TEXT_INPUT.ReadKeyStroke, WaitForKey) |
uart_poll_in (uart.h:359); later the interrupt API, uart_irq_callback_user_data_set (uart.h:710) and uart_fifo_read (uart.h:524) |
direct for ReadKeyStroke; adapter for keys and events |
poll_in returns 0 or −1 for "nothing yet", which maps to EFI_NOT_READY. Terminal escape sequences (arrows, Delete) must become EFI scan codes. WaitForKey is an EFI event, which Glint must supply itself. In interrupt mode the callback runs in interrupt context and the driver keeps no receive buffer, so the adapter owns a ring buffer |
SERIAL_IO (SetAttributes, SetControl, GetControl, Read, Write, Mode) |
uart_configure / uart_config_get (uart.h:434, 451; need CONFIG_UART_USE_RUNTIME_CONFIGURE), uart_line_ctrl_set / _get (uart.h:942, 957; CONFIG_UART_LINE_CTRL), poll, interrupt or async transfer |
adapter with mismatches | The enumerations differ. EFI parity counts Default=0, None=1, Even=2, Odd=3; Zephyr counts NONE=0, ODD=1, EVEN=2 (uart.h:80). EFI stop bits are Default=0, One=1, OneFive=2, Two=3; Zephyr's are 0.5=0, 1=1, 1.5=2, 2=3 (uart.h:89). Data bits are a count in EFI and an enum in Zephyr (uart.h:97). Zephyr has no "Default". Baud is UINT64 in EFI and uint32_t in Zephyr (uart.h:122). EFI's Timeout (µs) and ReceiveFifoDepth have no Zephyr field. Read/Write take a buffer and a timeout; Zephyr offers bytes (poll), FIFO callbacks (interrupt) or callback-completed transfers (async). In SBSA mode the PL011 refuses configure with -ENOTSUP (uart_pl011.c:274–278) |
Async UART (for SERIAL_IO under load) |
uart_callback_set, uart_tx, uart_rx_enable, uart_rx_buf_rsp (uart.h:754, 775, 834, 882); events in struct uart_event (uart.h:293) |
mismatch | Buffers stay owned by the driver until UART_TX_DONE or UART_RX_BUF_RELEASED. The driver asks for the next receive buffer from interrupt context. Timeouts need k_work_delayable (the NS16550 async path, uart_ns16550.c:306, 317). An EFI-shaped synchronous Read must block on a semaphore, which needs a scheduler (Phase 8), or poll for completion |
BLOCK_IO (ReadBlocks, WriteBlocks, FlushBlocks, Media) |
disk_access_read / _write / _ioctl (include/zephyr/storage/disk_access.h:82, 100, 134), backed by struct disk_operations (include/zephyr/drivers/disk.h:116); NVMe in drivers/disk/nvme/; sdhc_request (include/zephyr/drivers/sdhc.h:322) is one layer lower |
adapter with mismatches | Sector numbers and counts are uint32_t, so a 512-byte-sector disk tops out at 2 TiB, against EFI's 64-bit EFI_LBA. Disks are found by name string and registered at run time (disk_access_register, disk.h:134), not by handle. EFI passes a byte count, Zephyr a sector count. EFI's IoAlign meets driver DMA rules and cache handling (NVMe calls k_mem_phys_addr ten times). MediaId, MediaChanged and removable media have only DISK_STATUS_*. Calls block the caller while the driver waits on a k_sem. An sdhc driver is not a disk: it needs subsys/sd and drivers/disk/sdmmc_subsys.c above it |
SIMPLE_FILE_SYSTEM |
fs_open, fs_opendir (include/zephyr/fs/fs.h:315, 518) over FatFs (the separate fatfs west module, west.yml:144–146) or LittleFS |
mismatch | This is a subsystem with a mount table and path strings, not a driver, and its FAT code lives outside the Zephyr tree. Glint should keep its own FAT over BLOCK_IO (Phase 10) and not map this protocol to Zephyr |
SIMPLE_NETWORK (Transmit, Receive, GetStatus) |
struct ethernet_api (include/zephyr/net/ethernet.h:538), whose send takes a struct net_pkt (line 599); received frames are pushed with net_recv_data (include/zephyr/net/net_core.h:105). Example: drivers/ethernet/eth_virtio_net.c (ETH_NET_DEVICE_DT_INST_DEFINE at line 245, net_recv_data at 172) |
mismatch, the worst | SNP is pull: the caller polls Receive with its own buffer. A Zephyr Ethernet driver pushes each frame into Zephyr's network stack from interrupt or thread context, in net_pkt/net_buf buffers from Zephyr's pools. The device must also be a net_if, which is registered in an iterable linker section (include/zephyr/net/net_if.h:3512). The adapter must implement net_pkt allocation, net_recv_data and a stub net_if, plus a frame queue |
GRAPHICS_OUTPUT (QueryMode, SetMode, Blt, FrameBufferBase) |
display_write (include/zephyr/drivers/display.h:543) with struct display_buffer_descriptor (line 302), display_get_framebuffer (line 416); QEMU-relevant drivers are drivers/display/display_qemu_ramfb.c and display_intel_multibootfb.c |
adapter | GOP's four Blt operations become rectangle writes and reads. Pixel formats differ: GOP's 32-bit BGRX against Zephyr's bitmask enum (PIXEL_FORMAT_ARGB_8888, RGB_888, RGB_565, …, display.h:49–107). There is no mode setting, since the resolution comes from the devicetree. A linear framebuffer exists only where the driver exposes one |
Timers (CreateEvent, SetTimer, WaitForEvent, Stall) |
the system-timer driver interface sys_clock_set_timeout, sys_clock_elapsed, sys_clock_announce (include/zephyr/drivers/timer/system_timer.h:157, 221, 205) with drivers/timer/arm_arch_timer.c, apic_tsc.c, hpet.c; counter_set_channel_alarm (include/zephyr/drivers/counter.h:816) for hardware counters; k_timer_start, k_busy_wait (include/zephyr/kernel.h:1969, 741) for drivers |
mismatch of direction | Zephyr's timer drivers exist to drive Zephyr's own tick: they call sys_clock_announce into kernel/timeout.c. Glint owns the Arm generic timer and the LAPIC itself (slice 6.2, principle 6). So compat/zephyr consumes Glint's time: it implements k_busy_wait, k_uptime_get, k_timer and k_work_delayable on Glint's timers and links no Zephyr timer driver. Units differ: EFI uses 100 ns, Zephyr uses ticks (CONFIG_SYS_CLOCK_TICKS_PER_SEC) or counter ticks that wrap at a top value, and counter alarms call back in interrupt context, where EFI uses events and task priority levels |
Page allocation (AllocatePages, FreePages, AllocatePool) |
no driver API. Drivers consume k_malloc (kernel.h:6409), k_heap_alloc (kernel.h:6178), k_mem_map (include/zephyr/kernel/mm.h:192), k_mem_phys_addr (include/zephyr/kernel/internal/mm.h:116) and k_mem_map_phys_bare (kernel/internal/mm.h:225) |
inverse direction | Nothing in Zephyr provides pages to Glint. Glint provides memory to Zephyr drivers: compat/zephyr implements these over Glint's page allocator (Phase 3), including DMA-safe allocation, virtual-to-physical translation and uncached device mappings (K_MEM_CACHE_NONE) |
| I2C master (Track S; the plan shapes it on the UEFI PI I2C master protocol) | i2c_transfer with struct i2c_msg (include/zephyr/drivers/i2c.h:871, 185), i2c_transfer_cb (line 925) |
adapter, close | A PI StartRequest with a request packet of operations maps onto an array of i2c_msg. PI completes through an optional event; Zephyr is synchronous or callback-based |
3. The core services drivers expect
The table separates three verdicts. Implement means compat/zephyr
provides the API, with declarations in C headers and implementations as
Crystal fun exports. In most rows the header is Zephyr's own and only the
out-of-line functions are Glint's. Stub means a trivial body or an empty
generated file is enough. Avoid means the need disappears through driver
choice, devicetree content or Kconfig. "Copy" (linking Zephyr's kernel/
sources) is not proposed for any row. Where a Zephyr header holds the API as
static inline code over Zephyr internals, implementing the API means
replacing that header, and the row says so.
| Service | What drivers use | Verdict for Phase 2 (UART, poll) | Verdict for Phase 6 and later |
|---|---|---|---|
| Device model | struct device (device.h:513), struct device_state (device.h:458), DEVICE_DT_DEFINE (device.h:259) via Z_DEVICE_DEFINE (device.h:1317), DEVICE_DT_GET (device.h:317), DEVICE_API (device.h:1365) |
Use Zephyr's header unchanged. Implement the init runner with the semantics of do_device_init (kernel/device.c:18–50: call ops.init, store the positive errno in state->init_res, set state->initialized) and z_impl_device_is_ready (kernel/device.c:186) |
Same. Avoid device_get_binding, DEVICE_API_IS (device.h:1387), device_handle_get (device.h:563) and CONFIG_DEVICE_DEPS, which all walk the linker-built device list |
| Init levels | PRE_KERNEL_1, PRE_KERNEL_2, POST_KERNEL, APPLICATION with a priority and a devicetree-ordinal sub-priority, encoded in section names (include/zephyr/init.h:82–95, Z_INIT_ENTRY_SECTION at init.h:111) and sorted by the linker script (include/zephyr/linker/linker-defs.h:70–74, run by z_sys_init_run_level at kernel/init.c:220) |
Implement ordering with a generated device table in devicetree dependency order. Both UART drivers are PRE_KERNEL_1 at CONFIG_SERIAL_INIT_PRIORITY |
Ordering across classes needs level and priority, which live in driver macros and not in the devicetree. Read them from the section names of the compiled objects (.z_init_PRE_KERNEL_1_P_50_SUB_00002_) and generate the table. Avoid SYS_INIT users |
| Devicetree macros | include/zephyr/devicetree.h (5,915 lines, macros only) over devicetree_generated.h, made by scripts/dts/gen_defines.py (1,245 lines) on edtlib.py (3,694 lines) and dtlib.py (2,304 lines) |
Implement the generator in Crystal on cleave's devicetree code, plus Zephyr's YAML bindings. Use devicetree.h unchanged |
The generator grows to interrupts with named cells, ranges, pinctrl, chosen, aliases and node labels |
| Kconfig | autoconf.h; DT_HAS_<compat>_ENABLED symbols from scripts/dts/gen_driver_kconfig_dts.py:18–21; $(dt_*) functions (39 of them in scripts/kconfig/kconfigfunctions.py, e.g. dt_compat_enabled at line 817, dt_compat_any_has_prop at line 881) |
Implement autoconf.h from a Crystal table of about 15 to 20 symbols per architecture, checked against the Kconfig files that define them |
The full tree needs a board and SoC (Kconfig.zephyr), which Glint does not have by principle 6. Keep the table for native Glint. Solve the full tree only for Track S, where Zephyr's own board exists |
| Syscalls | __syscall prototypes (637 declarations in 89 headers) and the generated zephyr/syscalls/*.h (template at scripts/build/gen_syscalls.py:229–310) |
Implement a stub generator for CONFIG_USERSPACE=n: each wrapper calls z_impl_<name>. Empty syscall_list.h, kobj-types-enum.h and heap_constants.h suffice |
Unchanged. Glint never enables Zephyr userspace |
k_spinlock |
k_spin_lock / k_spin_unlock / K_SPINLOCK (include/zephyr/spinlock.h:181, 301, 440); static inline over arch_irq_lock |
Use as is: on one CPU, masking interrupts is exactly slice 6.9's planned stand-in. NS16550 takes it in poll_in and poll_out (uart_ns16550.c:958, 984); PL011 does not |
Phase 7 maps it to Glint's locks. Because it is inline, that means replacing spinlock.h in compat/zephyr |
k_mutex, k_sem |
k_mutex_lock, k_sem_take (kernel.h:3525, 3743), syscalls to z_impl_* |
not needed | Implement in Crystal. Before Phase 8 there are no threads, so a blocking take must spin with interrupts enabled. Block and network drivers depend on this |
| Work queues, timers | k_work_init, k_work_submit, k_work_schedule (kernel.h:3942, 4007, 4341), k_timer_start (kernel.h:1969) |
not needed | Implement on Glint's timer (6.2) and a deferred-work list. Async UART, NVMe and virtio-net use them |
| Threads | k_thread_create (kernel.h:444) |
not needed | Avoid drivers and subsystems that start their own threads (the USB host stack does: subsys/usb/host/usbh_core.c:195). Phase 8 can implement it if one is worth having |
IRQ_CONNECT, irq_enable |
include/zephyr/irq.h:48, 298. On arm64, ARCH_IRQ_CONNECT (include/zephyr/arch/arm64/irq.h:85–89) places a struct _isr_list in section .intList (include/zephyr/sw_isr_table.h:282–285), which scripts/build/gen_isr_tables.py turns into the vector table. On x86-64 it calls arch_irq_connect_dynamic (include/zephyr/arch/x86/intel64/arch.h:89–93) |
Avoid: CONFIG_UART_INTERRUPT_DRIVEN=n removes every reference |
Implement arch_irq_connect_dynamic, arch_irq_enable and z_arm64_irq_priority_set on 6.1's dispatcher. On arm64 the static .intList path must become a generated table, read from the objects' .intList sections, or the dynamic path |
| Logging | LOG_MODULE_REGISTER (include/zephyr/logging/log.h:771; NS16550 calls it unconditionally at uart_ns16550.c:45) |
Avoid: with CONFIG_LOG unset it registers nothing (log.h:723–728) and LOG_ERR is (void)0 (log.h:857) |
Route to Glint's console through CONFIG_LOG_CUSTOM_HEADER (log.h:836–839), which includes a Glint-supplied zephyr_custom_log.h |
| pinctrl | PINCTRL_DT_INST_DEFINE, pinctrl_apply_state (include/zephyr/drivers/pinctrl.h:257, 345) |
Avoid: no pinctrl-0 in the build devicetree, CONFIG_PINCTRL unset |
Needed for SoC UARTs in native mode, e.g. UART_NS16550_TI_K3 selects PINCTRL (drivers/serial/Kconfig.ns16550:71). This is where No BSPs meets Zephyr's SoC drivers |
| clock_control | clock_control_on, clock_control_get_rate (include/zephyr/drivers/clock_control.h:173, 274) |
Avoid by devicetree: the PL011's clocks phandle points to a fixed-clock (uart_pl011.c:713–717, 805–807); the NS16550 has clock-frequency (uart_ns16550.c:2061–2072) |
As pinctrl |
| reset | reset_line_toggle_dt (include/zephyr/drivers/reset.h:375) |
Avoid: no resets property, CONFIG_RESET unset |
As pinctrl |
| Power management | pm_device_driver_init |
Avoid: with CONFIG_PM_DEVICE unset it is inline (include/zephyr/pm/device.h:814–830) and calls the driver's own action for TURN_ON then RESUME. For the NS16550 that is uart_ns16550_configure |
Keep unset |
| Device MMIO | DEVICE_MMIO_ROM, DEVICE_MMIO_MAP, DEVICE_MMIO_GET (include/zephyr/sys/device_mmio.h:265, 320–327) |
Avoid with CONFIG_MMU unset: the driver uses the physical address directly (device_mmio.h:37–39), which matches firmware's identity map |
With CONFIG_MMU=y, device_map calls k_mem_map_phys_bare (device_mmio.h:97–104), which Glint implements on its Phase 3 page tables. The probe confirmed this is then the PL011's only new external symbol |
| Iterable sections and other linker tricks | STRUCT_SECTION_ITERABLE (include/zephyr/sys/iterable_sections.h:42–44, section names from include/zephyr/toolchain/gcc.h:197–201), .z_devstate (device.h:947–949), .z_init_*, .intList, net_if_dev |
Avoid iteration: generate tables instead. The sections still compile and link on COFF, and nothing reads them | Grows with every subsystem. See risk 3 |
| Atomics | include/zephyr/sys/atomic.h with CONFIG_ATOMIC_OPERATIONS_BUILTIN |
header only, compiler builtins | same |
| C library | memcpy, memset; headers from lib/libc/minimal/include |
Stub: Glint supplies the two functions | grows with drivers |
4. The UART probe in detail
PL011 (drivers/serial/uart_pl011.c, compatibles arm,pl011 and arm,sbsa-uart)
- Headers. The direct includes are
string.h,zephyr/kernel.h,zephyr/arch/cpu.h,zephyr/init.h,zephyr/device.h,zephyr/drivers/uart.h,zephyr/sys/device_mmio.h,zephyr/sys/barrier.h,zephyr/irq.hand the localuart_pl011_registers.h(lines 12–35).pinctrl.h,reset.h,clock_control.handcmsis_compiler.hare included only under their Kconfig symbols.kernel.hbrings in the arch headers, so the closure is about 150 Zephyr headers (clang -M). All are used unchanged. - Macros.
DT_DRV_COMPATwithDT_INST_FOREACH_STATUS_OKAY(lines 814, 850);DT_ANY_COMPAT_HAS_PROP_STATUS_OKAYandDT_HAS_COMPAT_STATUS_OKAY(lines 37–40, 79–91);DEVICE_DT_INST_DEFINE(lines 810, 842);DEVICE_MMIO_ROM,_RAM,_ROM_INIT,_MAPand theget_uarthelper (uart_pl011_registers.h:40);DT_INST_PROPforcurrent_speed,hw_flow_controlandfifo_disable(lines 784–807);DT_INST_STRING_TOKEN_BY_IDX(n, compatible, 0), which pastes the first compatible into a function name such aspwr_on_arm_pl011(lines 684–693);DT_INST_CLOCKS_CTLR,DT_NODE_HAS_COMPAT(…, fixed_clock)andDT_INST_PROP_BY_PHANDLE(n, clocks, clock_frequency)(lines 713–717, 805–807);DEVICE_API(uart, …)(line 547);FIELD_PREP,BIT,COND_CODE_1,IF_ENABLED;barrier_dmem_fence_fullandbarrier_isync_fence_full. - Devicetree properties.
compatible,reg,status,clocks(whose first phandle must be afixed-clockwithclock-frequency),current-speed,hw-flow-controlandfifo-disable.interruptsandinterrupt-namesare needed only with interrupts. The binding (dts/bindings/serial/arm,pl011.yaml, includinguart-controller.yaml) markscurrent-speedoptional, but the driver reads it withDT_INST_PROP(line 797), so a node without it does not compile. QEMU'svirtnode, built bycreate_uartinhw/arm/virt.c, hascompatible,reg,interrupts,clocks(to/apb-pclk, a 24 MHzfixed-clock) andclock-names, but nocurrent-speed. Glint's build devicetree therefore needs an overlay that adds it, or that relabels the nodearm,sbsa-uart. In SBSA mode,pl011_initskips baud, format and FIFO setup and trusts what firmware left (lines 611–650), and the SBSA instance reads no clock or speed property (lines 829–848). That suits a UART that U-Boot has already configured. - Kconfig.
UART_PL011(drivers/serial/Kconfig.pl011:4–12, which depends onDT_HAS_ARM_PL011_ENABLED || DT_HAS_ARM_SBSA_UART_ENABLED),UART_PL011_SBSA(lines 16–18) when SBSA is used,SERIAL,SERIAL_HAS_DRIVER,SERIAL_INIT_PRIORITY(default 50,drivers/serial/Kconfig:34–36); for the architectureARM64,64BIT,ATOMIC_OPERATIONS_BUILTIN; and, forkernel.halone,NUM_PREEMPT_PRIORITIES,NUM_COOP_PRIORITIES,MP_MAX_NUM_CPUS,PRIVILEGED_STACK_SIZE,SYS_CLOCK_TICKS_PER_SEC,SYS_CLOCK_HW_CYCLES_PER_SECandSYS_CLOCK_MAX_TIMEOUT_DAYS. These must be absent:UART_INTERRUPT_DRIVEN,UART_ASYNC_API,PINCTRL,RESET,CLOCK_CONTROL,PM_DEVICE,MMU,USERSPACEandLOG. (As in Kconfig's ownautoconf.h, a disabled boolean is left undefined, not set to 0. Several drivers test with#ifdef.) - Kernel calls. None in the poll path:
pl011_poll_inandpl011_poll_out(lines 204, 218) touch only registers.pl011_init(lines 583–675) callsmemcpy(line 349). - Linker sections produced.
._device.static.1_50_,._uart_driver_api.static.pl011_driver_api_noirq_,.z_devstateand.z_init_PRE_KERNEL_1_P_50_SUB_<ordinal>_. - External symbols after compilation:
memcpy.
NS16550 (drivers/serial/uart_ns16550.c, I/O-port access)
- Headers. In addition to the PL011's list, these are unconditional:
zephyr/linker/sections.h,zephyr/drivers/clock_control.h,zephyr/pm/device.h,zephyr/pm/policy.h,zephyr/sys/sys_io.h,zephyr/spinlock.h,zephyr/drivers/serial/uart_ns16550.handzephyr/logging/log.h(lines 22–43). The closure is 151 headers, 100,109 lines. - I/O ports are a devicetree property, not a Kconfig symbol. v4.4.2 has
no
UART_NS16550_ACCESS_IOPORT. The driver derivesUART_NS16550_IOPORT_ENABLEDfrom any okay node with the booleanio-mapped(lines 73–86, bindingdts/bindings/serial/ns16550.yaml). It then usessys_in8/sys_out8, which are inlineinb/outbfrom the x86 arch headers (lines 404–447,get_portat 487). The port lives in a union with the MMIO record in astatic constconfig (lines 336–361, 2095–2115). - Macros.
LOG_MODULE_REGISTER(line 45);DT_ANY_INST_HAS_PROP_STATUS_OKAYforpcp,dlf,dmasandresets(lines 47–57);DT_ANY_INST_ON_BUS_STATUS_OKAY(pcie);DT_INST_PROP_OR;DT_INST_REG_ADDR;DT_INST_ON_BUS;DEVICE_DT_INST_DEFINE(line 2111);PM_DEVICE_DT_INST_DEFINEandPM_DEVICE_DT_INST_GET, both empty with PM off;DEVICE_API(line 1892). - Devicetree properties.
compatible = "ns16550",reg = <0x3f8 …>,io-mapped,reg-shift(required by the binding),clock-frequency(1,843,200 for a PC COM port),current-speed(read with a default of 0, which skips the divisor),hw-flow-controlandloopback. Withoutclock-frequencythe driver needsclocks, soDEVICE_DT_GETof a clock controller andclock_control(lines 2061–2072). Withoutio-mappedit uses MMIO. On a PCIe node it needsCONFIG_PCIEand dynamic interrupts (line 51,UART_NS16550_IRQ_CONFIG_PCIEat line 1955). QEMU'sq35has no devicetree at all, so Glint writes this node as data: the PC's standard COM1, as Zephyr's owndts/x86/intel/atom.dtsi:49–58does. - Kconfig.
UART_NS16550(drivers/serial/Kconfig.ns16550:3–6, depending onDT_HAS_NS16550_ENABLED), the variant choice (line 41,UART_NS16550_VARIANT_NS16550),UART_LOG_LEVEL(used byLOG_MODULE_REGISTER),SERIAL,SERIAL_HAS_DRIVER,SERIAL_INIT_PRIORITY; for the architectureX86,X86_64,64BIT,ATOMIC_OPERATIONS_BUILTINandMAX_IRQ_LINES(include/zephyr/arch/x86/arch.h:229); and the samekernel.hset as the PL011. The same symbols must be absent. - Kernel calls.
k_spin_lock/k_spin_unlockaround every register access (lines 958, 984), inline overarch_irq_lock.device_is_readyinuart_ns16550_configure(line 636) is reached only withoutclock-frequency, but it is compiled in, so the syscall wrapper andz_impl_device_is_readymust exist.pm_device_driver_init(line 943) callsuart_ns16550_pm_action(line 815), which callsuart_ns16550_configure(line 587). That sets the divisor, line control and FIFOs, so this driver reprograms the UART that firmware set up. - Linker sections: as for the PL011.
- External symbols after compilation:
z_impl_device_is_ready.
What the probe showed
Both drivers were compiled with clang for Glint's two targets (the flags
are in the appendix). Each was linked with a 52-line C file standing in for
compat/zephyr into a PE32+ application, using the same ld.lld -flavor
link /subsystem:efi_application step as tools/glint_build.cr. The images
were booted from an ESP written by Glint::Run::ESP under the project's
QEMU and U-Boot builds:
zephyr PL011: poll_out in boot services (aarch64, QEMU virt)
zephyr PL011: poll_out after ExitBootServices
zephyr NS16550: poll_out in boot services (x86_64, QEMU q35)
zephyr NS16550: poll_out after ExitBootServices
The stand-in for the shim is small enough to quote. It shows the device table that replaces linker-section iteration:
static const struct device *const glint_devices[] = { DEVICE_DT_GET(PROBE_NODE) };
static void glint_zephyr_init(void)
{
for (size_t i = 0; i < ARRAY_SIZE(glint_devices); i++) {
const struct device *dev = glint_devices[i];
int r = dev->ops.init != NULL ? dev->ops.init(dev) : 0;
dev->state->init_res = (uint8_t)(r < 0 ? -r : r);
dev->state->initialized = true;
}
}
Getting there exposed four toolchain findings. The first three needed fixes, which the probe applied:
include/zephyr/kernel.h:37assertssizeof(intptr_t) == sizeof(long), which fails on LLP64. The probe removed that line in an overlay copy ofkernel.h.- On
*-unknown-windows, clang defines_MSC_VERand not__GNUC__.include/zephyr/toolchain.h:49–50still pickstoolchain/llvm.hfrom__llvm__, but 16 Zephyr headers test__GNUC__directly (toolchain/gcc.h:22,85,145,244;sys/rb.hfalls back toalloca.h), so-fgnuc-version=4.2.1is needed. With it added, the default-fms-compatibilityremains, and its preprocessor makesIS_EMPTY()return 0 (include/zephyr/sys/util_macro.h).DT_ANY_INST_HAS_PROP_STATUS_OKAY(resets)then evaluated to 1 for a node with noresets, so the NS16550 silently compiled in its reset path and referencedreset_line_toggle. Adding-fno-ms-compatibilityfixed it. This failure gives no error. - On x86-64,
arch_irq_lock(include/zephyr/arch/x86/intel64/arch.h:57–64) doespushfq; cli; popqinto anunsigned long. On LLP64 that is 4 bytes, so the 8-bytepopqoverwrote half of the return address. The first character printed, then the machine triple-faulted (QEMU-d int: page fault at RIP 0x2). Wideningkeytounsigned long longfixed it. - lld-link keeps each Zephyr section as a separate 4 KiB-aligned PE section,
with its name cut to eight characters (
._device,._uart_d,.z_devst,.z_init_). This is harmless but wasteful./merge:folded.z_devstateand.z_init_…into.dataand.rdata; it did not fold._device.static.1_50_or._uart_driver_api.static.…in this test.
Minimal shim surface for the two drivers in poll mode
| Kind | Items |
|---|---|
| Generated headers | zephyr/devicetree_generated.h; zephyr/autoconf.h; zephyr/syscalls/{device,uart,kernel,clock,errno_private,kobject,log_msg,reset}.h (only device.h and uart.h need real wrappers here: device_is_ready, uart_poll_in, uart_poll_out); empty zephyr/syscall_list.h, zephyr/kobj-types-enum.h, zephyr/heap_constants.h |
| Devicetree macro families the two drivers reach | per node: _PATH, _FULL_NAME, _PARENT, _ORD, _ORD_STR_SORTABLE, _EXISTS, _STATUS_okay, _COMPAT_MATCHES_<compat>, _REG_NUM, _REG_IDX_0_EXISTS, _REG_IDX_0_VAL_ADDRESS, _REG_IDX_0_VAL_SIZE, _P_<prop> and _P_<prop>_EXISTS for every binding property (booleans always present as 0 or 1), _P_compatible_IDX_<i>[_STRING_TOKEN|_STRING_UPPER_TOKEN|_EXISTS], _P_compatible_LEN, _P_clocks_IDX_<i>_PH; identifiers DT_N_INST_<i>_<compat>; global DT_FOREACH_HELPER, DT_FOREACH_OKAY_HELPER (used by device.h:1352 in every file), 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>, DT_FOREACH_OKAY_INST_VARGS_<compat>; DT_DEBRACKET_INTERNAL. Names follow scripts/dts/gen_defines.py:72–104, 235–256, 338–388, 458–475, 680–760, 813–831 and 1024–1116 |
| Functions Glint provides | z_impl_device_is_ready; memcpy, memset; the device-table init runner; later k_mem_map_phys_bare (with CONFIG_MMU) |
| Patches to the pinned tree | kernel.h:37 (the LP64 assertion); arch/x86/intel64/arch.h:59 (key width). Both are one line, kept as separate patch files as principle 5 requires |
| Compile flags | -fno-ms-compatibility -fgnuc-version=4.2.1 -std=gnu11 -ffreestanding -fno-builtin -mno-red-zone -imacros autoconf.h -D__ZEPHYR__=1 -DKERNEL -fno-strict-aliasing -fno-common (the last four follow Zephyr's CMakeLists.txt:151–152, 376 and cmake/compiler/gcc/compiler_flags.cmake:156, 236; the probe used -include and needed none of the four to pass), plus -I for the generated directory, include/, lib/libc/minimal/include and drivers/serial |
Estimated size (non-spec lines, Crystal unless stated):
| Piece | Early (Phase 2) | Notes |
|---|---|---|
devicetree_generated.h generator: DTB or DTS through cleave, YAML bindings with include: merging, property types (int, boolean, string, string-array, array, phandle, phandles, phandle-array), defaults, dependency ordinals, the macro families above |
600–800 | Python reference is about 5,000 lines (gen_defines.py + edtlib.py); a full Crystal port is likely 2,000–2,500 |
Build devicetree inputs: QEMU virt (the 2.4 fixture) plus an overlay; PC COM1 node |
about 30 (DTS) | |
autoconf.h from a symbol table, checked against the Kconfig files with Cleave::Kconfig |
120–180 | full tree with Kconfig.dts, rsource/osource, $(dt_*): 800–1,200 |
| Syscall stub generator | 120–150 | |
Shim runtime: device table emitter and init runner, z_impl_device_is_ready, status and errno mapping |
100–150 | |
Console provider: SIMPLE_TEXT_OUTPUT and SIMPLE_TEXT_INPUT over uart_poll_out/uart_poll_in |
120–160 | |
Build glue in tools/glint_build.cr: Zephyr source list, flags, patch application |
60–100 | |
| Total | about 1,150–1,550 | five to seven slices at the plan's ~200-line target; the devicetree generator alone needs two or three |
For scale, the probe's hand-written inputs were a 70-line and a 45-line
devicetree header, a 14-line and an 18-line autoconf.h, two 3- to 5-line
syscall headers and the 52-line stand-in.
What is hard in these two drivers
- Section-based device lists.
DEVICE_DT_DEFINEplaces the device in._device.static.<level>_<prio>_and its init entry in.z_init_<level>_P_<prio>_SUB_<ordinal>_. Zephyr's linker script sorts them and provides_device_list_start/_end(include/zephyr/linker/common-rom/common-rom-kernel-devices.ld:23,linker-defs.h:70–74). lld-link has no linker script. Glint generates a table instead, and any driver that walks the list fails at link time with an undefined_device_list_start, which is a loud failure. - Init ordering. The level and priority are arguments in the driver's
C source, not devicetree data, so a devicetree-only generator cannot
know them. Ordering by dependency ordinal is enough for two UARTs. Later,
read the
.z_init_…section names from the compiled objects. - Inline functions over generated macros. Every file expands
DT_FOREACH_OKAY_HELPERthroughZ_MAYBE_DEVICE_DECLARE_INTERNAL(device.h:1348–1352), so the generated header must be complete for the whole tree, not only for the driver being built.DEVICE_DT_GETof a phandle target (a clock controller) becomes a link-time dependency on that device. The macro graph is correct only under the GNU preprocessor (problem 2 above). - pinctrl and clocks. Avoided by the devicetree: a
fixed-clockfor the PL011,clock-frequencyfor the NS16550, and nopinctrl-0orresets. QEMU's PL011 node needs one added property or the SBSA relabel.
5. Risks to the mapping
Risk 1: object format and C dialect (the biggest). Zephyr's C assumes
LP64 and GNU C. Glint's images are PE/COFF from *-unknown-windows, which
is LLP64 and MSVC-compatible by default. The probe hit an assertion, a
silent macro misevaluation and a runtime stack corruption, and it was
driving only two small drivers. Other latent cases are visible in the
headers: ROUND_UP/ROUND_DOWN cast pointers to unsigned long
(include/zephyr/sys/util.h:318–326), which truncates addresses above 4 GiB
on LLP64; atomic_t is long (include/zephyr/sys/atomic_types.h:15);
and the arch headers use unsigned long 49 times, some in inline assembly
that no warning can check. There are two ways to resolve it:
(a) keep COFF, compile Zephyr with -fno-ms-compatibility
-fgnuc-version=4.2.1, carry small LP64 patches, and build all Zephyr code
with -Werror=pointer-to-int-cast -Werror=int-to-pointer-cast
-Werror=shorten-64-to-32, replacing arch inline assembly that holds
long with shim code; or (b) build Zephyr objects, or the whole kernel, as
LP64 ELF with ld.lld and a linker script, and write the PE header with a
Crystal tool, as Linux's arm64 Image does. Option (b) also gives Zephyr
its linker script (risk 3), but it changes Glint's build and Crystal
target. x86_64-pc-cygwin is LP64 COFF, but it has no aarch64
counterpart, so it does not solve the problem.
Risk 2: compile-time devicetree against run-time discovery. Zephyr fixes
addresses, instance counts and clock wiring when it compiles. Glint finds
its hardware at boot (slice 2.4) and promises to run on any EBBR or x86 UEFI
machine without board code. A build devicetree per machine class (QEMU
virt, PC COM1) works if Glint checks at run time that the firmware's
description agrees with the build node: same compatible, same reg. If it
does not agree, Glint stays in hosted mode (slice 2.6 already has that
decision point). Later, CONFIG_MMU=y routes every MMIO driver's base
through k_mem_map_phys_bare, where Glint can substitute the address it
discovered. That is late binding at one choke point, without patching
const config structs (the NS16550's config is static const,
uart_ns16550.c:2098).
Risk 3: linker sections and generated tables. Device lists, init
levels, DEVICE_API_IS, device_get_binding, the arm64 interrupt table
(.intList and gen_isr_tables.py), net_if lists and log modules all
rely on ELF linker-script sorting with start and end symbols. lld-link for
EFI applications has no linker script. Each new subsystem therefore needs
either a generated table built from object-file section names, or a
COFF-specific section scheme. In COFF, lld-link sorts .name$suffix
sections alphabetically within a group, which can play the role of
SORT(). The hook is __TOOLCHAIN_CUSTOM__
(include/zephyr/toolchain.h:36–40), but Z_INIT_ENTRY_SECTION
(init.h:111) and .z_devstate (device.h:949) name sections directly
and would still need patches.
Risk 4: who drives the work. EFI protocols are synchronous and use
caller-owned buffers. Zephyr's interrupt and async APIs call back from
interrupt context and own buffers until a completion event, and block and
network drivers wait on k_sem, which needs threads. Before Phase 8,
compat/zephyr can only spin in k_sem_take. So the 6.10 provider runs
with a spinning semaphore until Phase 8, and its benchmark should say so.
Risk 5: the network stack. No Ethernet driver stands alone. Wrapping
one as SIMPLE_NETWORK means imitating net_pkt, net_buf and net_if
(section 2). Prefer, in order: Glint's own virtio-net (Phase 12, a standard
device under principle 6); the Linux guest; and only then a Zephyr Ethernet
driver behind a deliberately small net_pkt imitation.
Risk 6: Kconfig without a board. Zephyr's Kconfig tree expects a board and SoC. Native Glint has none. Keep a short, checked symbol table for native Glint. Run the full tree only in Track S, where Zephyr's own board exists.
Risk 7: error conventions. Zephyr returns negative errno values, plus
−1 from poll_in and positive errno values in device_state.init_res.
EFI returns EFI_STATUS with the high bit set. Write one mapping table in
the shim and use it everywhere.
Keeping the seam thin. These choices avoid pulling in more of Zephyr's core:
- Use the poll API until Phase 6, and the interrupt API before the async API.
- Leave
CONFIG_UART_INTERRUPT_DRIVEN,UART_ASYNC_API,PINCTRL,RESET,CLOCK_CONTROL,PM,PM_DEVICE,USERSPACE,LOG,DEVICE_DEPSand, until Phase 3,MMUunset. - Choose devicetree nodes with fixed clocks and no
pinctrl-0,resetsordmas. Usearm,sbsa-uartwhere firmware already configured the UART. Avoid PCIe-attached UARTs, which needCONFIG_PCIEand dynamic interrupts. - Avoid drivers that start threads, call
device_get_bindingorDEVICE_API_IS, or register with a subsystem (network, USB host,subsys/sd). - Keep every Zephyr name inside
compat/zephyrand the generated headers. Glint's core sees only EFI-shaped protocols, built by one Crystal adapter per class, with one status-mapping table.
6. Recommendation
What a minimal early 6.8 should contain. These pieces come forward into Phase 2 ahead of slice 2.5:
- the syscall stub generator, plus the three empty generated headers;
autoconf.hfrom a per-architecture Crystal table (the symbols listed in section 4), with a spec that parsesdrivers/serial/Kconfig.pl011,Kconfig.ns16550anddrivers/serial/KconfigwithCleave::Kconfigand checks that every symbol exists and that itsdepends onholds;- a
devicetree_generated.hgenerator limited to the macro families in section 4, reading the QEMUvirtDTB from 2.4's fixture with a Glint overlay, and a PC COM1 node written as data. Its specs run on Linux against cleave, and its acceptance test is that both drivers compile with-Werror.
What a minimal early 6.9 should contain:
- the build of
third_party/zephyrat v4.4.2 with the two patches and the flags above; - the generated device table, the init runner,
z_impl_device_is_readyandmemcpy/memsetas Crystal exports; - a console provider:
SIMPLE_TEXT_OUTPUTandSIMPLE_TEXT_INPUToveruart_poll_out/uart_poll_in, installed by 2.5's native system table in place ofboot/uart/pl011.crandboot/uart/ns16550.cr.
The Phase 2 demonstration is 2.5's own: bin/glint-run aarch64 --expect
"Left boot services" (and x86_64), with the line written by Zephyr's
driver. Hosted mode stays the default, so the native path is chosen by 2.6
only when the firmware's UART matches the build node, or when it is
explicitly selected.
What to leave for Phase 6: IRQ_CONNECT, irq_enable and the
interrupt-driven UART (6.3); every k_* object beyond the spinlock; the
async API; logging through LOG_CUSTOM_HEADER; pinctrl, clocks and resets;
the full devicetree generator (interrupt cells, ranges, pinctrl,
chosen) and the full Kconfig tree; block and network providers. Before
6.10 is refined, also move to v4.5.0 for drivers/disk/virtio_blk.c, or
choose NVMe or virtio-console as the first provider. CONFIG_MMU with
k_mem_map_phys_bare belongs with Phase 3's page tables.
What to check first. The riskiest assumption is that Zephyr C built
for Glint's PE/COFF LLP64 target behaves as it does on LP64. The two
drivers here pass with two patches and two flags. The cheap next check,
before any generator is written, is to compile the drivers Phase 6 is
likely to want with the warnings in risk 1 promoted to errors:
uart_pl011.c and uart_ns16550.c with interrupts, virtio_mmio.c,
virtio_pci.c, virtqueue.c, drivers/disk/nvme/*.c and, at v4.5.0,
virtio_blk.c. Hand-written generated headers are enough for this, as the
probe shows. Also grep the arm64 and x86-64 arch headers for inline
assembly that writes into a long. If the list is short, choose option
(a) of risk 1. If it is not, decide on the ELF build path before slice 2.5.
7. USB host: what Zephyr offers, and the guest-facing shim
The maintainer has set the direction: Glint owns every xHCI controller and the whole USB tree behind it, including hubs and every device class. xHCI is a standard, so it falls under principle 6. Guests reach USB devices only through a shim, and the keyboard is a trusted path to Glint even while a guest owns the display. This section informs that plan and changes nothing above.
What v4.4.2 has. The host controller API is
include/zephyr/drivers/usb/uhc.h: struct uhc_api at line 297 and
struct uhc_transfer at line 120, which carries a net_buf. Events arrive
through uhc_event_cb_t (line 219). The controller drivers in
drivers/usb/uhc/ are uhc_max3421e.c (an SPI-attached controller), NXP's
uhc_mcux_ehci.c, uhc_mcux_ip3516hs.c, uhc_mcux_khci.c and
uhc_mcux_ohci.c (all over NXP's SDK), and uhc_virtual.c. There is no
xHCI host driver. The only xHCI references in the tree are constants for
Intel's audio-offload link (drivers/uaol/uaol_intel_adsp.c:40). The host
stack in subsys/usb/host/ (usbh_core.c, usbh_device.c, usbh_ch9.c,
usbh_desc.c, usbh_class.c, about 1,550 lines) is marked experimental
(subsys/usb/host/Kconfig:5–8, drivers/usb/uhc/Kconfig:5–8). It has no
hub support and one class driver, video (class/usbh_uvc.c). It has no
host-side HID, mass storage or network class. It runs two threads fed by
message queues (usbh_core.c:22–41, 195). subsys/usb/host/usbip.c is a
USB/IP server: it exports host-attached devices over TCP and needs the
whole network stack (subsys/usb/host/Kconfig.usbip). The HID headers in
include/zephyr/usb/class/ are for the device side.
Is any of it worth using? No, not for this direction. Glint would still
have to write the xHCI driver, hub support and every class driver, and it
would take on threads, message queues and net_buf to use an experimental
core. Glint's own xHCI driver is the cleaner fit, built in Crystal from the
xHCI specification as Glint's other standard devices are. Each class then
needs its own driver in Glint: hubs; HID in boot protocol for the
keyboard; mass storage as Bulk-Only Transport with SCSI; and CDC-ECM or NCM
for network adapters. The boot protocol's fixed 8-byte keyboard report
needs no report-descriptor parser, which keeps the trusted path small
enough to audit. Glint sees every keystroke first and reserves a key chord
for itself before anything reaches a guest. In hosted mode, U-Boot's USB
keyboard already arrives through SIMPLE_TEXT_INPUT.
The guest-facing shim. There are three options:
| Option | What the guest sees | For | Against |
|---|---|---|---|
Emulated xHCI, as QEMU's hw/usb/hcd-xhci.c (about 3,700 lines) |
a USB controller with real device descriptors, driven by Linux's xhci-hcd and class drivers |
any class; no guest changes | the largest emulation; Glint must filter forwarded HID traffic to keep the trusted path; isochronous timing and device ownership shared between Glint and guest |
| Per-class paravirtual devices: virtio-input for keyboard and mouse, virtio-blk for storage (6.6), virtio-net for USB network adapters (Phase 12) | ordinary virtio devices, backed by Glint's class drivers through BLOCK_IO and the network protocol |
fits Glint's existing virtio model; the guest-visible surface is small; Glint stays in control, and the trusted keyboard path is natural because Glint injects events; virtio-input's device side is small (QEMU's hw/input/virtio-input*.c totals about 1,150 lines) |
only classes Glint has a driver for are reachable; the guest sees a virtio device, not "a USB stick" |
USB/IP-style forwarding of USB requests over vsock (Phase 9) to Linux's vhci-hcd |
a virtual host controller in the guest, attached by the usbip user-space tool |
any class with one mechanism and no xHCI emulation | needs guest user space and setup; not available during guest boot; adds latency; the keyboard must be kept out of it |
The middle row is the natural default for keyboards, storage and network. USB/IP-style forwarding is a fallback for classes Glint has no driver for. Emulated xHCI is worth building only for a guest without virtio drivers.
Appendix: reproducing the probe
The commands assume the clone at /workspace/zephyr-src, and
gen/ holding the hand-written generated headers (section 4 lists their
contents). The overlay directory ovl/ holds zephyr/kernel.h without
line 37 and, for x86-64, zephyr/arch/x86/intel64/arch.h with key
declared unsigned long long. These were per-session files and are not in
the checkout.
clang -target aarch64-unknown-windows # or x86_64-unknown-windows
-fno-ms-compatibility -fgnuc-version=4.2.1 -ffreestanding -fno-builtin
-std=gnu11 -O2 -include gen/zephyr/autoconf.h -Iovl -Igen
-I/workspace/zephyr-src/include
-I/workspace/zephyr-src/lib/libc/minimal/include
-I/workspace/zephyr-src/drivers/serial
-c /workspace/zephyr-src/drivers/serial/uart_pl011.c # or uart_ns16550.c
ld.lld -flavor link /subsystem:efi_application /entry:efi_main /nodefaultlib
/out:BOOTAA64.EFI main.obj driver.obj
main.c defines memcpy, memset and z_impl_device_is_ready, the
device table and init runner quoted in section 4, and an efi_main. That
efi_main prints through uart_poll_out, calls GetMemoryMap and
ExitBootServices (retrying once on a stale key), and prints again. The
ESP image was written by Glint::Run::ESP (src/glint/run/esp.cr) with
cleave's Fat16. QEMU was started with the arguments that
src/glint/run/runner.cr uses for each architecture, with
/workspace/glint-qbuild and /workspace/glint-ubuild as the QEMU and
U-Boot build directories.