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.
200 lines
9.7 KiB
Markdown
200 lines
9.7 KiB
Markdown
# 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:
|
||
```c
|
||
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, `x1`–`x3` = 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.
|