Round 9, hex dump decoded:
x0 = 0x5F800000 (= _fit_fdt_start, depthcharge's own FDT
buffer; the kernel reserves 5f800000-5f815fff for it)
dt[0..3] = all zeros — no FDT magic at payload entry
OVL_L0_ADDR = 0xFD536000 (real scanout, top-of-DRAM reserved region)
The device-era boot path flattens the fixed-up tree into
_fit_fdt_start and hands off that pointer. Why the kernel sees a valid
FDT there while the payload sees zeros is open (leading suspicion:
cache flush behavior differing with the ~30 MB pmOS kernel vs the
18 KB stub).
For the stub this is moot: the coreboot table address is a memlayout
constant on this board (0xffed9000, confirmed by the kernel's coreboot
driver, sysfs tags and /sys/firmware/fdt). find_coreboot_reg failure
now falls back to it instead of halting; the stub no longer depends on
the DTB. find_framebuffer failure still dumps the raw LBIO bytes.
Payload adbea06f... flashed and verified. host_test and qemu_test pass.
22 KiB
Research log — krane display pipeline & stub bring-up
This file is the running lab notebook for the krane-fb-stub project: every investigation done against the actual sources (coreboot, Linux, depthcharge — both mainline trees and the device-era code as recovered from the SPI flash version strings), what was found, and how each finding changed the payload. Ordered chronologically; conclusions marked [REVERSED] were later contradicted by deeper evidence.
Phase 0 — Task framing
Goal: smallest freestanding arm64 binary that depthcharge boots as a Linux kernel, locates the boot-splash framebuffer, and paints checkpoint colors (red → yellow → green → blue, ~2 s each, hold blue), to validate the depthcharge → custom-payload pipeline before any U-Boot bring-up.
Device ground truth (verified live):
- Lenovo IdeaPad Duet, MT8183, board
google,krane, sku176 (sku-id = 0xb0in/firmware/corebootof the live FDT). - coreboot table (LBIO) at
0xffed9000, size0x380; CBMEM region follows. Confirmed both bydtc -I dtb -O dts /sys/firmware/fdt(kernel's copy of depthcharge's fixed-up tree) and by sysfs (/sys/devices/platform/ffed9000.coreboot/coreboot7carriesMODALIAS=coreboot:t00000012= LB_TAG_FRAMEBUFFER present). - Boot chain: BootROM → coreboot → TF-A BL31 → depthcharge → our payload.
- Partition layout:
mmcblk0p1ChromeOS-kernel (what depthcharge boots internally),sda1(USB stick, pmOS) is the currently-running system. - Device firmware strings extracted from the 8 MB SPI (
flashrom -p internal):v1.9308_26_0.0.22-10476-g8c7859a3c9(RO, built ~Feb 2021)v1.9308_26_0.0.22-10566-g8417b00957, builtMon May 30 03:26:46 UTC 2022(RW — the slot that actually runs) Both are ChromeOS depthcharge ≈ v0.0.22 (R93-era, early 2021/2022).
Phase 1 — Format verification (all checked against source, not the prompt)
coreboot tables (src/commonlib/include/commonlib/coreboot_tables.h)
physical_addressislb_uint64_t=__aligned(4) uint64_t(LB_ENTRY_ALIGN == 4). On LE AArch64 that is a plain 8-byte LE value at offset 8 ofstruct lb_framebuffer— the "hi/lo split" is an alignment trick, not separate fields.sizeof(struct lb_framebuffer) == 40(host-compiled probe against the real header). Offsets: tag 0, size 4, physical_address 8, xres 16, yres 20, bytes_per_line 24, bpp 28, red 29/30, green 31/32, blue 33/34, reserved 35/36, orientation 37, flags 38, pad 39.lb_header/lb_recordmatch the prompt. Records start atheader_bytes, walkedtable_entriestimes.- Live cross-check: parsing the live
/sys/firmware/fdtyields LBIO at0xffed9000, size0x380— matches the values confirmed by hand with dtc.
arm64 Image header (Linux booting.rst + depthcharge boot64.c)
- 64-byte header, magic
ARM\x64(0x644d5241) at 0x38, code0 must branch past the header; entry with x0 = DTB, x1–x3 = 0, MMU off, DAIF masked. image_sizemust be exact and little-endian (depthcharge checks the magic and usesimage_sizefor KASLR slot math).
depthcharge mainline (as of this repo's clone)
boot64.c: kernel image is decompressed to a random 2 MiB-aligned slot; the Image magic is verified; entry = first byte; x0 = flat DTB, x1–x3 = 0. → stub must be position-independent (build enforces zero dynamic relocations) and carry its own stack (16 KiB in-image).- Contradicts the prompt:
fit.cskips FIT configs without an FDT subimage ("config %s has no FDT, skipping") and gives up with no match. So the packed FIT must contain a DTB. We embed the genuinemt8183-kukui-krane-sku176DTB (config conf-27 of the running p1 FIT) purely as a config-selection key; depthcharge injects/firmware/corebootat boot viasrc/boot/coreboot.c::install_coreboot_data(adds#address-cells,#size-cells,ranges,compatible,reg), so the runtime DTB at x0 always has the node we parse. mkdepthchargebuilds the FIT itself (mkimage -f auto -T kernel -O linux -C none -d <vmlinuz> -b <dtb>, then patches the kernel subimage tokernel_noload), so a raw arm64 Image goes in directly — no manual uImage wrapping. Verified: the packed image has the same shape as the working pmOS FIT (kernel_noload+flat_dt, compression none).
Round 1 (eMMC flash, first attempt) — BLACK SCREEN, menu vanished
Observed: depthcharge dev-menu exits → black screen, nothing ever painted.
Had to force power-cycle; recovered via USB boot (backup ~/mmcblk0p1-pmos- backup.img, sha256 20951a54…, taken before the first flash).
Investigation
The black screen was initially read as "stub crashed". The decisive evidence came from identifying the exact firmware versions in the SPI dump:
- depthcharge
v1.9308_26_0.0.22-10566-g8417b00957(RW slot, 2022-05-30) — i.e. ChromeOS R93-era (2021/2022), not mainline 2026. - coreboot
4.14-era (built ~Feb 2021 / May 2022).
Checking the chromiumos/depthcharge git history (git log -S) for the code
that ran on the device:
69725330(2020-09-22, "display: Black out screen in display_cleanup()", in R93):display_cleanup()atCleanupOnHandoffbefore jumping to the payload does:clear_screen(black)— erases the menu from the LBIO framebuffer;backlight_update(0)— drivesDISP_PWM(GPIO 43) andEN_LCD_BL(GPIO 176) low;- calls
display_ops->stop→ 2021-eramtk_display_stop():OVL_EN=0,OVL0_2L_EN=0— the overlay fetch engine is stopped.
- The DSI link, panel power and display MTCMOS remain up — panel/DSI
poweroff only exists in 2025+ code (
e9f50b81,9be6dc52, Dec 2025). AlsoLB_TAG_PANEL_POWEROFFis emitted only by skywalker boards.
Conclusion: the stub did run, painted red→yellow→green→blue and halted on blue — into a framebuffer nothing was scanning, with the backlight off. "Painted in the dark." The prompt's premise ("framebuffer is live and DMA'd by the time any payload runs") is false for this firmware generation.
Key register facts established (all from device-era sources)
- OVL0 base
0x14008000;DISP_REG_OVL_EN = 0x000C,DISP_REG_OVL0_2L_EN = 0x100C(2021mtk_ddp.c— [REVERSED] this log first claimed 0x0F00 for OVL_EN; see Round 2). The 0xF40 "L0_ADDR" register still holds the menu's scanout address after stop — it is not reset. - GPIO controller at
0x10005000;doutblock at +0x140, 16 bytes per 32-pin group,setat +4 /rstat +8. Backlight pins:DISP_PWM = 43(group 1, bit 11 → set reg0x10005154),EN_LCD_BL = PERIPHERAL_EN13 = 176(group 5, bit 16 →0x10005194). - Panel: sku176 →
(sku_id >> 4) & 0xF = 11→ BOE TV101WUM_NL6 (1200×1920, RGB888, 4 lanes, pixel clock 159.425 MHz → DSI data rate ≈ 956 Mbps). Not needed for the fix (panel stays initialized).
The fix (main.c::display_revive())
Minimal inverse of the three teardown steps, executed before painting:
OVL_EN = 1,OVL0_2L_EN = 1(offsets from device-era mtk_ddp.c);- GPIO
dout.setwrites for pins 43 and 176 (GPIO base0x10005000, dout block +0x140, 16 B per 32-pin group — layout from depthcharge'smtk_gpio.c/mt8183.h); - repaint —
OVL_L0_ADDRstill holds the address depthcharge used for the menu, which is the same LBIO-record address our parser extracts.
No DSI/panel re-init, no MTCMOS, no clock touching.
Round 2 (post-revival reboot) — STILL BLACK: wrong OVL_EN offset
Observed: reboot into internal drive → pitch black after depthcharge boots
mmcblk0p1. No colors. p1 verified intact afterwards (first 159,744 bytes
hash to the payload; vbutil_kernel --verify on-device OK), so the stub ran.
Live-environment re-checks
/dev/memLBIO read still blocked: pmOS kernel hasCONFIG_STRICT_DEVMEM(confirmed in/boot/config),0xffed9000sits infae00000-ffdfffff : reservedper/proc/iomem→dd if=/dev/memgives "Bad address". kexec remains the only live-read route; not needed — see below.- Open item resolved without hardware access: the runtime LBIO
physical_addressis provably non-zero. Depthcharge's device-eramtk_display_init()(src/drivers/video/mtk_ddp.c) programsOVL_L0_ADDRfromlib_sysinfo.framebuffer.physical_address(i.e. the LBIO record) and the dev menu was visibly rendered through it;src/drivers/video/display.calso gates display handling onphysical_address != 0. Mainline coreboot passesfb_addr=0for kukui (fb_new_framebuffer_info_from_edid(edid, 0)), but the device-era coreboot published a real address.
Root cause
The stub's display_revive() re-enabled the overlay at the wrong register:
it wrote OVL_EN at 0x14008000 + 0x0F00, but the actual offset in
depthcharge's mtk_ddp.c — confirmed in the 2021 revision (497450b4,
Jan 2021, inside the RO build window), the current tree, the merge commit
that unified mt8173/mt8183 (74376061, 2019), and Linux
drivers/gpu/drm/mediatek/mtk_disp_ovl.c — is 0x000C.
The Round-1 "0x0F00 from device-era mtk_ddp.c" note was a misread
(0x0F00-era MT8173 offset / confusion with the 0x0F40 L0_ADDR neighborhood).
Consequence: depthcharge's stop wrote OVL_EN=0 at 0x000C, the stub
revived at 0x0F00 (a different, wrong register), the overlay engine stayed
disabled, and every fill landed in a buffer nothing scanned out. Painting in
the dark, again.
Fix
main.c::display_revive(): DISP_REG_OVL_EN corrected to 0x000C.
OVL0_2L_EN = 0x100C was already correct. Backlight GPIO writes were
correct (match kukui_backlight_update).
Verification
payload krane-fb-stub-payload.bin sha256 9e7cf29d… (an intermediate
41270e53… rebuild was flashed once but built from the stale WIP tree
before the fix was synced into it — same wrong code as Round 1; caught by
disassembly before reboot), flashed to mmcblk0p1, cmp +
vbutil_kernel --verify OK. Awaiting reboot.
Flash state
mmcblk0p1reflashed with the Round-3 diagnostic payload: sha2569d7859dc04dd9c445143f6b45600bb972cf5fc1497c06a4f9db549e3d2971e73, verified on-device (cmpfirst 159,744 bytes +vbutil_kernel --verify→ body verification succeeded). Earlier: Round-1edb65b29…, Round-29e7cf29d…(both functionally identical wrong-offset code;41270e53…was a stale-tree rebuild caught by disassembly).- USB stick
sda1untouched throughout (sha256 watched:c4ee2044…). - eMMC backup:
~/mmcblk0p1-pmos-backup.img(sha25620951a54…). Copy it onto the USB stick before rebooting so recovery never depends on eMMC.
Round 3 — still black with corrected offset; diagnostic stub
Reboot with payload 9e7cf29d… (correct OVL_EN=0x000C): still pitch
black. Fact check: all four teardown steps of device-era display_cleanup
are now provably undone — clear_screen(black) (repainted), the backlight
GPIOs (matches kukui_backlight_update), OVL_EN/OVL0_2L_EN (correct
offsets), and disable_graphics_buffer() (a libpayload double-buffer
helper, no hardware effect). Device-era init_screen() sequencing
(ui/display.c) proves the dev menu was drawn into the LBIO
physical_address and scanned out via OVL_L0_ADDR = same address, so the
stub's fills go to the right buffer. Revival logic is correct on paper —
yet black.
The unexcluded branch: the stub may never execute. "Menu vanished" only proves depthcharge reached handoff — cleanup runs unconditionally. And the internal-boot path was never baseline-proven: pmOS always ran from USB; no custom image has ever been observed booting from p1 on this device. Every black screen so far is equally consistent with "payload never ran".
Diagnostic design (payload 9d7859dc…)
Payload now paints/blinks BEFORE trusting any parsing:
- First instructions:
OVL_EN=1,OVL0_2L_EN=1, backlight GPIOs on. - Parse (<1 ms). Failure → 5 slow backlight blinks, spin.
- Success → red → yellow → green → blue, blue held.
Decision tree: colors = pipeline validated; 5 blinks on black = parse failed at runtime; nothing at all = payload never handed off (boot path).
Abandoned: OVL_L0_ADDR stage-0 read
An earlier design read OVL_L0_ADDR (0x14008F40) pre-parse to paint the
menu buffer without knowing pa. Rejected: on qemu -M virt, READS of
unassigned MMIO data-abort (writes are silently dropped) — it killed the
test instantly; the same risk exists on real hardware if a display clock
gate is off, and a fault there is indistinguishable from "never ran". The
stub now contains no MMIO reads at all.
Round 4 — still black, no backlight: GPIO dout offset was wrong too
Reboot with the Round-3 diagnostic payload (9d7859dc…): pitch black, no
blinks, no backlight at all. Stage-0 turns the backlight on as its
first act, and the backlight demonstrably works via these same GPIOs (the
dev menu had it) — so either the payload never ran, or the GPIO writes
missed. Re-derived the register layout from the device-era header
(src/drivers/gpio/mt8183.h) instead of the Round-1 notes:
GpioRegs: dir[6]@0x000, rsv00[160], dout[6]@0x100, rsv01[160],
din[6]@0x200; GpioValRegs = 16 B (val@0, set@4, rst@8)
The Round-1 note's dout @ +0x140 was wrong — actual offset +0x100.
Consequences of the old addresses: the "pin 43 set" write to 0x10005154
actually set bit 11 of group 5 = pin 172 (I2S1_MCK); the "pin 176 set"
write to 0x10005194 hit a reserved region. Correct set/rst addresses:
pin 43 → 0x10005114/0x10005118, pin 176 → 0x10005154/0x10005158.
The Round-3 blink was additionally broken: it wrote 0 to a set register
(a no-op) instead of writing the bit to the rst register. Both fixed.
This also reframes Rounds 2–4: the OVL revival (correct offsets) and the
fills may have been working the whole time — with the backlight never
enabled, an LCD shows nothing regardless of what is scanned out. The only
proven failures were the two register-offset misreads in the Round-1
research notes (OVL_EN 0x0F00, dout 0x140), both now [REVERSED] and
fixed; the stub itself has never been disproven.
Payload 55ccc1bd… flashed (cmp + vbutil OK). host/qemu tests pass.
Diagnostic decision tree (after this fix)
| observation | meaning |
|---|---|
| black, no colors, no blink | payload never handed off — boot-path problem (vboot/mkdepthcharge), investigate p1 boot |
| black + 5 slow backlight blinks (NOW FUNCTIONAL — see Round 4) | stub ran, parse failed at runtime |
| colors appear but sequence stops early | fill/mask problem — stuck color identifies stage |
| red → yellow → green → blue, blue held | pipeline fully validated |
Round 5 — blinks decoded: LBIO physical_address really IS 0
The Round-4 payload's diagnostic worked: 3–5 backlight blinks then steady
backlit black = parse_fail(). The stub runs, stage-0 works, the GPIO fix
works — the parse rejected the LBIO framebuffer record. Root cause found
in the actual coreboot 4.14 source (fetched from the 4.14 tag):
src/mainboard/google/kukui/mainboard.c(4.14) is identical to mainline:fb_new_framebuffer_info_from_edid(edid, 0)— and 4.14'sedid_fill_fb.cpassesfb_addrthrough verbatim, no carveout, no allocation. The LBIO record on this device genuinely has physical_address = 0.
How the menu still renders: libpayload cbgfx draws into
phys_to_virt(pa) = DRAM address 0, and coreboot's display pipeline scans
out address 0. With pa = 0, depthcharge's display_init_required() is
false, so board.c never registers display ops — meaning at handoff
display_cleanup runs but backlight_update and stop are no-ops:
the OVL was never stopped and the backlight never disabled by
depthcharge. The "menu vanishing" was just the 9.2 MB black fill at
address 0 (safe for the payload: depthcharge itself does it after the
kernel slot is chosen, and the stub demonstrably survived every round).
Round-2's "open item resolved" reasoning was wrong — menu rendering
does NOT prove pa != 0, because address 0 works as a framebuffer region.
The original Phase-1 note ("mainline coreboot passes fb_addr=0 for kukui")
was correct all along. All five black screens so far trace to the stub's
!fb->pa rejection of a legitimate record (plus the two Round-1 register
offset misreads, which additionally kept the backlight dark).
Fix: find_framebuffer accepts pa == 0 and the stub paints at address
0 — the buffer the still-running OVL scans. OVL revival and backlight
writes are kept but are now believed to be redundant on this firmware.
Payload b25d9132… flashed (cmp + vbutil OK). host_test gained a pa==0
regression test; qemu_test unchanged (its synthetic record uses pa!=0).
Round 6 — pa==0 accepted but parse STILL fails: bit-band diagnostic dump
Reboot with b25d9132… (pa==0 accepted): 5 blinks again — parse_fail
persists, and with pa==0 accepted the failure is somewhere else in
find_coreboot_reg or find_framebuffer. Every local reproduction passes:
host_test parses the live fdt (21/21 checks incl. the new pa==0 test),
qemu end-to-end passes, and the runtime tree shape is provably identical
to the live fdt (same fixup code; krane DTB has no pre-existing /firmware
node; root cells 2/2). The remaining unknowns are the actual runtime
values — what is really at x0 and what the real LBIO bytes are.
Key enabler: on failure the scanout address is KNOWN without any parse — DRAM address 0 (Round 5; the visible menu erase proves the OVL scans it). So the stub can render diagnostic data directly on the panel.
New failure path (payload e619166a…)
fail_dump(): 5 blinks (execution proof, same signature), then paint a
bit-band dump into address 0: one row per u32, 32 cells of 32×32 px, MSB
first, white=1 / black=0, rows every 64 px. Word 0 = marker:
0xC0DE0001— find_coreboot_reg failed; words 1-2 = x0 pointer, words 3-6 = raw DTB header (magic, totalsize, off_struct, off_strings, off_mem_rsvmap).0xC0DE0002 | rc— find_framebuffer failed; words 1-3 = LBIO addr/size, words 4-6 = raw table magic/header_bytes/entries, words 7-12 = raw words at table+24..+68 (first record headers).
User photographs the panel; values are decoded offline.
Round 7 — diag at address 0 invisible: cbgfx rejects pa==0
Reboot with e619166a…: 5 blinks, then no bit-bands — the render
into DRAM address 0 never reached the panel. That kills the "OVL scans
address 0" corollary of Round 5, and with it the whole address-0 theory:
the device-era libpayload cbgfx_init() explicitly REJECTS
physical_address == 0 (CBGFX_ERROR_FRAMEBUFFER_ADDR). The menu could
never have been drawn through a zero pa — so the runtime framebuffer
address is NON-zero, and the LBIO record's pa (whatever it holds) is not
necessarily what the panel scans.
The authoritative scanout address is OVL_L0_ADDR (0x14008F40): it is
programmed by depthcharge's mtk_display_init() for the menu and left
in place by stop(). Round 5's "OVL never stopped / backlight never
disabled" reasoning is also [REVERSED] — with display ops registered,
display_cleanup really did disable the OVL and kill the backlight
(which is why Round 4's "no backlight at all" was observed).
Payload cebb9b1b… (full file rewrite)
read_scanout_addr()readsOVL_L0_ADDR; guarded as plausible DRAM (>= 0x40000000).- Success path: if the LBIO record's pa is 0, substitute the scanout address, then paint checkpoints into the buffer the OVL actually scans (after stage0 re-enables the engines and backlight).
- Failure path:
fail_dumpblinks 5× then renders the bit-band dump AT THE SCANOUT ADDRESS; word 15 additionally carries the raw L0_ADDR value. Markers unchanged (0xC0DE0001DTB stage,0xC0DE0002|rcLBIO stage).
The OVL register READ is device-only (qemu -M virt aborts on reads to unassigned MMIO; qemu never takes the failure/substitution path, so the test is unaffected).
Round 8 — hex-digit diagnostic dump
The bit-band dump WAS visible at the L0_ADDR scanout (proving both the
scanout substitution and the render path), but 32 coarse squares per row
proved unreadable from a photo, and the apparent row/column confusion
made transcription unreliable. The dump rendering is replaced with
giant hex digits: one u32 per line, 8 digits of a 3x5 cell font
scaled x10 (30x50 px), MSB nibble first, white on depthcharge's black
background. Readable in any panel orientation, transcribable as text.
Same data, same markers (0xC0DE0001 DTB stage, 0xC0DE0002|rc LBIO
stage, word 15 = raw OVL_L0_ADDR value).
Payload 10760e3c… flashed (cmp + vbutil OK). host_test and qemu_test
pass.
Round 9 — dump decoded: x0's FDT buffer is empty; LBIO fallback
The hex dump worked. Transcription (C0DE0001 = DTB stage failure):
x0 = 0x5F800000
dt[0..3] = 0, 0, 0, 0 (no FDT magic — buffer is zeros)
OVL_L0_ADDR = 0xFD536000 (real scanout, top-of-DRAM
reserved region, matches
/proc/iomem fae00000-ffdfffff)
x0 = _fit_fdt_start (device-era src/arch/arm/fit.c: the flattened tree
is placed in depthcharge's own _fit_fdt_start.._fit_fdt_end buffer and
that pointer is handed off). The kernel later reserves exactly
5f800000-5f815fff for its FDT (size ≈ the live fdt's 86738 bytes), so
the pointer is right — but for the payload boot the buffer content reads
as zeros. Why the kernel sees a valid FDT there while the payload sees
zeros is still OPEN (cache/flush difference vs the pmOS kernel boot is
the leading suspicion; the pmOS kernel is ~30 MB vs our 18 KB stub —
decompression footprint differs). For the stub this does not matter:
Fallback added: if find_coreboot_reg fails, use the fixed coreboot
table address 0xffed9000 (coreboot memlayout constant on this board;
confirmed by the kernel's own coreboot driver, sysfs tags, and
/sys/firmware/fdt). The stub no longer depends on the DTB at all.
find_framebuffer failure still dumps the raw LBIO bytes.
Payload adbea06f… flashed (cmp + vbutil OK). host_test and qemu_test
pass.
Recovery: power-cycle, boot USB (unchanged), dd if=mmcblk0p1-pmos-backup.img of=/dev/mmcblk0p1 bs=4M conv=fsync.
Open items
physical_addressin the LBIO record — resolved in Round 2: provably non-zero at runtime (depthcharge's device-eramtk_display_initprogramsOVL_L0_ADDRfrom the LBIO record and the menu rendered through it; the/dev/memread attempt failed onCONFIG_STRICT_DEVMEM+ reserved-RAM, "Bad address", not EPERM as first assumed).- qemu test updated to the same expectation (colors + held blue); it exercises parser + fill logic, not the MMIO revival writes.