Minimal freestanding arm64 binary that depthcharge boots as a kernel: parses depthcharge's /firmware/coreboot DTB node, walks the coreboot table, finds LB_TAG_FRAMEBUFFER, and paints red/yellow/green/blue checkpoints (~2s each) into the live boot-splash framebuffer. Verified against coreboot tables header, Linux arm64 booting.rst, and depthcharge's fit.c/boot64.c. Host parser tests pass against the live /sys/firmware/fdt; full red->yellow->green->blue sequence verified end-to-end under qemu-system-aarch64; packed image verifies with the ChromeOS devkeys.
9.7 KiB
Task: Build and test a minimal ARM64 "hello framebuffer" stub for a Lenovo IdeaPad Duet (MT8183 / Krane)
Goal
Produce the smallest possible standalone ARM64 binary that depthcharge (the ChromeOS firmware bootloader) can load as if it were a Linux kernel, which does nothing but locate the already-initialized boot-splash framebuffer and fill it with solid colors at distinct checkpoints. This validates the whole depthcharge → custom-payload pipeline before any real U-Boot bring-up work begins. No U-Boot involved yet — this is a from-scratch freestanding binary, as small and dependency-free as possible.
Hardware / firmware facts already confirmed on this exact device — do not re-derive these
- Device: Lenovo IdeaPad Duet Chromebook. SoC: MediaTek MT8183. Board: Google "krane", specifically krane sku176.
- Confirmed from the live devicetree
/firmware/corebootnode on this exact unit (dumped viadtc -I dtb -O dtson/sys/firmware/fdtunder the currently-running kernel):Withfirmware { ranges; coreboot { ram-code = <0x06>; sku-id = <0xb0>; /* = 176 decimal, confirms sku176 */ board-id = <0x06>; reg = <0x00 0xffed9000 0x00 0x380 0x00 0xffed9000 0x00 0x127000>; compatible = "coreboot"; }; };#address-cells = 2, #size-cells = 2, thisregdecodes as two (address, size) pairs:- coreboot table (LBIO): address
0xffed9000, size0x380(896 bytes) - CBMEM area: address
0xffed9000, size0x127000(~1.15 MB)
- coreboot table (LBIO): address
- Firmware boot chain on this device: Boot ROM → coreboot → TF-A BL31 → depthcharge → "kernel" (whatever is packed into the ChromeOS kernel partition). Reference: https://trustedfirmware-a.readthedocs.io/en/latest/plat/mt8183.html
- Partition layout on
/dev/mmcblk0:p1= type "ChromeOS kernel" (GUIDFE3A2A5D-4F32-41A7-B725-ACCC3285A309), small. This is what depthcharge actually boots — verified viafutility vbutil_kernel --verify /dev/mmcblk0p1 --verbose.p2= ext4, mounted/bootin the Ubuntu rootfs — not read by firmware at boot time, just staging space.p3= ext4, Ubuntu root filesystem.
- The device currently shows the ChromeOS dev-mode boot menu (USB/internal
boot choice) before handoff, confirming coreboot's
panel_krane.c(https://github.com/coreboot/coreboot/blob/main/src/mainboard/google/kukui/panel_krane.c) has already brought up the MIPI-DSI panel and a linear framebuffer is live and DMA'd by the time any payload we supply would run. dev_boot_usbshould be confirmed as1(crossystem dev_boot_usb) before any USB-boot testing.
Coreboot table format — verify against source before trusting field order
Reference header: src/commonlib/include/commonlib/coreboot_tables.h in
https://github.com/coreboot/coreboot (clone this repo locally and grep it —
do not trust field order from this prompt without checking).
Known so far:
struct lb_header {
uint8_t signature[4]; /* "LBIO" */
uint32_t header_bytes;
uint32_t header_checksum;
uint32_t table_bytes;
uint32_t table_checksum;
uint32_t table_entries;
};
struct lb_record {
uint32_t tag;
uint32_t size;
};
#define LB_TAG_FRAMEBUFFER 0x12
struct lb_framebuffer {
uint32_t tag;
uint32_t size;
uint64_t physical_address; /* verify: coreboot uses lb_uint64_t, a
32-bit-aligned split hi/lo struct on
some versions — confirm packing */
uint32_t x_resolution;
uint32_t y_resolution;
uint32_t bytes_per_line;
uint8_t bits_per_pixel;
uint8_t red_mask_pos;
uint8_t red_mask_size;
uint8_t green_mask_pos;
uint8_t green_mask_size;
uint8_t blue_mask_pos;
uint8_t blue_mask_size;
uint8_t reserved_mask_pos;
uint8_t reserved_mask_size;
uint8_t orientation;
/* struct lb_framebuffer_flags flags; possible trailing padding —
confirm exact struct size against sizeof() in the real header */
};
Action required: before writing the parser, confirm (a) whether
physical_address is a plain uint64_t or coreboot's split lb_uint64_t
(hi/lo 32-bit halves) in the version you clone, and (b) the exact total
struct size/padding, by reading the header directly and/or writing a tiny
host-side C program that does printf("%zu\n", sizeof(struct lb_framebuffer))
against the real, included header.
What the stub must do
- Entry point conforms to the arm64 Linux kernel boot protocol (this is
what depthcharge expects to jump into): entered with MMU and D-cache off,
x0= physical address of a DTB blob,x1–x3= 0. The binary itself must also carry a valid arm64 "Image" header (magicARM\x64at byte offset 0x38,code0/code1branch-past-header instructions,text_offset,image_size, etc.) — confirm the exact header layout against the Linux kernel'sDocumentation/arch/arm64/booting.rst(orarch/arm64/kernel/head.S) in a clonedtorvalds/linuxtree, since depthcharge's loader is written to accept real Linux Image binaries and the packing step below (mkdepthcharge) will otherwise choke or misinterpret the payload. - Parse the DTB at
x0(a small/partial hand-rolled parser is fine — you only need to find one node — but usinglibfdtif it's easy to statically link in a freestanding way is also acceptable) to locate/firmware/corebootand extract the firstregpair (LBIO table address + size). Do not hardcode0xffed9000/0x380in the shipped stub logic — read them from the DTB at runtime, since this is the general mechanism; the constants above are only for your own manual verification/testing during development. - Verify the
"LBIO"signature at that address, walklb_recordentries usingheader_bytes/table_entries, find the one withtag == LB_TAG_FRAMEBUFFER (0x12). - Using
physical_address,x_resolution,y_resolution,bytes_per_line,bits_per_pixel: fill the entire visible framebuffer with a single solid color, per the checkpoint sequence below, with a fixed delay (spin-loop is fine, no timer driver needed) between each so a human watching the screen can see each stage.
Checkpoint color convention (in order, ~2 seconds each, hold last color forever)
- Red — stub entered and running (proves depthcharge jumped here correctly and code is executing).
- Yellow — DTB parsed,
/firmware/corebootnode found. - Green — LBIO signature verified, framebuffer record found.
- Blue — framebuffer fill of the previous checkpoint colors succeeded (i.e., blue only appears if red/yellow/green were each visibly, correctly drawn — this is your final "everything worked" signal).
- If any step fails, halt on the last successful color (infinite loop, do not proceed) rather than showing a "fail" color — the stuck color itself tells us which stage broke.
Build and packaging
- Toolchain:
aarch64-linux-gnu-gcc(freestanding:-ffreestanding -nostdlib -static, no libc), or hand-written assembly if simpler given the small scope. - Package with
mkdepthcharge(from https://github.com/alpernebbi/depthcharge-tools, wrapsmkimage/vbutil_kernel) targeting arm64, using the ChromeOS devkeys (/usr/share/vboot/devkeys/kernel.keyblock+kernel_data_key.vbprivk) already trusted by this device's firmware. Confirm againstmkdepthcharge --helpand its source whether a raw freestanding binary needs to be wrapped in a FIT/uImage first (viamkimage) or can be passed directly — do not assume. - Do not pass a real DTB for depthcharge to hand to us for real — we want
depthcharge's own normal DTB selection behavior (it already knows how to
pick/pass the correct krane DTB when booting a "kernel"), so build the FIT
the same way
mkdepthchargewould for a normal kernel+dtb pair, just with our stub binary in place of vmlinuz and no initramfs needed.
Safety rules — non-negotiable
- Never write the packed image directly to
/dev/mmcblk0p1. Every test cycle targets a USB stick first (dev_boot_usb=1must be set; boot via the ChromeOS dev-mode menu's USB option). Only after a stub has been confirmed working from USB — visually, by a human watching the screen — should writing to internal storage even be discussed, and that should be a separate, explicitly-confirmed step, not something done automatically as part of a build/test loop. - Keep the current, known-working pmOS kernel image backed up on the USB stick itself (not only on the internal eMMC), so recovery never depends on anything that might get overwritten.
- If a build step, packing step, or the arm64 Image header requirements are ambiguous or contradicted by what you find in source, stop and report the ambiguity rather than guessing and flashing — getting the on-disk format wrong here produces a hang with zero diagnostic output, which is the one failure mode this whole exercise exists to avoid.
- Treat every numeric fact in this prompt (struct layouts, offsets, table address) as "reported, needs verification against source," not as ground truth — cross-check each against the actual cloned coreboot/Linux trees before relying on it in code.
Deliverables
- Stub source (assembly + minimal C, or pure assembly), with comments explaining each stage against the checkpoint list above.
- Build script producing the raw binary.
- Packing command(s) producing a bootable USB disk image.
- A short README noting which of the "verify against source" items above were checked, what was found, and any deviations from this prompt's assumptions.