Files
krane-fb-stub/krane-fb-stub-agent-prompt.md
vhaudiquet 64e19c213a krane-fb-stub: arm64 framebuffer test payload for depthcharge
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.
2026-08-29 19:21:28 +02:00

9.7 KiB
Raw Permalink Blame History

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/coreboot node on this exact unit (dumped via dtc -I dtb -O dts on /sys/firmware/fdt under the currently-running kernel):
    firmware {
        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";
        };
    };
    
    With #address-cells = 2, #size-cells = 2, this reg decodes as two (address, size) pairs:
    • coreboot table (LBIO): address 0xffed9000, size 0x380 (896 bytes)
    • CBMEM area: address 0xffed9000, size 0x127000 (~1.15 MB)
  • 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" (GUID FE3A2A5D-4F32-41A7-B725-ACCC3285A309), small. This is what depthcharge actually boots — verified via futility vbutil_kernel --verify /dev/mmcblk0p1 --verbose.
    • p2 = ext4, mounted /boot in 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_usb should be confirmed as 1 (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

  1. 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, x1x3 = 0. The binary itself must also carry a valid arm64 "Image" header (magic ARM\x64 at byte offset 0x38, code0/code1 branch-past-header instructions, text_offset, image_size, etc.) — confirm the exact header layout against the Linux kernel's Documentation/arch/arm64/booting.rst (or arch/arm64/kernel/head.S) in a cloned torvalds/linux tree, 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.
  2. Parse the DTB at x0 (a small/partial hand-rolled parser is fine — you only need to find one node — but using libfdt if it's easy to statically link in a freestanding way is also acceptable) to locate /firmware/coreboot and extract the first reg pair (LBIO table address + size). Do not hardcode 0xffed9000/0x380 in 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.
  3. Verify the "LBIO" signature at that address, walk lb_record entries using header_bytes/table_entries, find the one with tag == LB_TAG_FRAMEBUFFER (0x12).
  4. 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)

  1. Red — stub entered and running (proves depthcharge jumped here correctly and code is executing).
  2. Yellow — DTB parsed, /firmware/coreboot node found.
  3. Green — LBIO signature verified, framebuffer record found.
  4. 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).
  5. 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, wraps mkimage/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 against mkdepthcharge --help and its source whether a raw freestanding binary needs to be wrapped in a FIT/uImage first (via mkimage) 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 mkdepthcharge would for a normal kernel+dtb pair, just with our stub binary in place of vmlinuz and no initramfs needed.

Safety rules — non-negotiable

  1. Never write the packed image directly to /dev/mmcblk0p1. Every test cycle targets a USB stick first (dev_boot_usb=1 must 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.
  2. 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.
  3. 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.
  4. 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

  1. Stub source (assembly + minimal C, or pure assembly), with comments explaining each stage against the checkpoint list above.
  2. Build script producing the raw binary.
  3. Packing command(s) producing a bootable USB disk image.
  4. 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.