Files
krane-fb-stub/RESEARCH.md
T

30 KiB
Raw Blame History

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 = 0xb0 in /firmware/coreboot of the live FDT).
  • coreboot table (LBIO) at 0xffed9000, size 0x380; CBMEM region follows. Confirmed both by dtc -I dtb -O dts /sys/firmware/fdt (kernel's copy of depthcharge's fixed-up tree) and by sysfs (/sys/devices/platform/ffed9000.coreboot/coreboot7 carries MODALIAS=coreboot:t00000012 = LB_TAG_FRAMEBUFFER present).
  • Boot chain: BootROM → coreboot → TF-A BL31 → depthcharge → our payload.
  • Partition layout: mmcblk0p1 ChromeOS-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, built Mon 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_address is lb_uint64_t = __aligned(4) uint64_t (LB_ENTRY_ALIGN == 4). On LE AArch64 that is a plain 8-byte LE value at offset 8 of struct 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_record match the prompt. Records start at header_bytes, walked table_entries times.
  • Live cross-check: parsing the live /sys/firmware/fdt yields LBIO at 0xffed9000, size 0x380 — 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, x1x3 = 0, MMU off, DAIF masked.
  • image_size must be exact and little-endian (depthcharge checks the magic and uses image_size for 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, x1x3 = 0. → stub must be position-independent (build enforces zero dynamic relocations) and carry its own stack (16 KiB in-image).
  • Contradicts the prompt: fit.c skips 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 genuine mt8183-kukui-krane-sku176 DTB (config conf-27 of the running p1 FIT) purely as a config-selection key; depthcharge injects /firmware/coreboot at boot via src/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.
  • mkdepthcharge builds the FIT itself (mkimage -f auto -T kernel -O linux -C none -d <vmlinuz> -b <dtb>, then patches the kernel subimage to kernel_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:

  1. 69725330 (2020-09-22, "display: Black out screen in display_cleanup()", in R93): display_cleanup() at CleanupOnHandoff before jumping to the payload does:
    1. clear_screen(black) — erases the menu from the LBIO framebuffer;
    2. backlight_update(0) — drives DISP_PWM (GPIO 43) and EN_LCD_BL (GPIO 176) low;
    3. calls display_ops->stop → 2021-era mtk_display_stop(): OVL_EN=0, OVL0_2L_EN=0 — the overlay fetch engine is stopped.
  2. The DSI link, panel power and display MTCMOS remain up — panel/DSI poweroff only exists in 2025+ code (e9f50b81, 9be6dc52, Dec 2025). Also LB_TAG_PANEL_POWEROFF is 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 (2021 mtk_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; dout block at +0x140, 16 bytes per 32-pin group, set at +4 / rst at +8. Backlight pins: DISP_PWM = 43 (group 1, bit 11 → set reg 0x10005154), EN_LCD_BL = PERIPHERAL_EN13 = 176 (group 5, bit 16 → 0x10005194).
  • Panel: sku176 → (sku_id >> 4) & 0xF = 11BOE 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:

  1. OVL_EN = 1, OVL0_2L_EN = 1 (offsets from device-era mtk_ddp.c);
  2. GPIO dout.set writes for pins 43 and 176 (GPIO base 0x10005000, dout block +0x140, 16 B per 32-pin group — layout from depthcharge's mtk_gpio.c/mt8183.h);
  3. repaint — OVL_L0_ADDR still 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/mem LBIO read still blocked: pmOS kernel has CONFIG_STRICT_DEVMEM (confirmed in /boot/config), 0xffed9000 sits in fae00000-ffdfffff : reserved per /proc/iomemdd if=/dev/mem gives "Bad address". kexec remains the only live-read route; not needed — see below.
  • Open item resolved without hardware access: the runtime LBIO physical_address is provably non-zero. Depthcharge's device-era mtk_display_init() (src/drivers/video/mtk_ddp.c) programs OVL_L0_ADDR from lib_sysinfo.framebuffer.physical_address (i.e. the LBIO record) and the dev menu was visibly rendered through it; src/drivers/video/display.c also gates display handling on physical_address != 0. Mainline coreboot passes fb_addr=0 for 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

  • mmcblk0p1 reflashed with the Round-3 diagnostic payload: sha256 9d7859dc04dd9c445143f6b45600bb972cf5fc1497c06a4f9db549e3d2971e73, verified on-device (cmp first 159,744 bytes + vbutil_kernel --verify → body verification succeeded). Earlier: Round-1 edb65b29…, Round-2 9e7cf29d… (both functionally identical wrong-offset code; 41270e53… was a stale-tree rebuild caught by disassembly).
  • USB stick sda1 untouched throughout (sha256 watched: c4ee2044…).
  • eMMC backup: ~/mmcblk0p1-pmos-backup.img (sha256 20951a54…). 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:

  1. First instructions: OVL_EN=1, OVL0_2L_EN=1, backlight GPIOs on.
  2. Parse (<1 ms). Failure → 5 slow backlight blinks, spin.
  3. 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 24: 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

The Round-4 payload's diagnostic worked: 35 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's edid_fill_fb.c passes fb_addr through 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() reads OVL_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_dump blinks 5× then renders the bit-band dump AT THE SCANOUT ADDRESS; word 15 additionally carries the raw L0_ADDR value. Markers unchanged (0xC0DE0001 DTB stage, 0xC0DE0002|rc LBIO 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.

Round 10 — SUCCESS: full pipeline validated

Payload adbea06f…: red → yellow → green → blue, blue held. The depthcharge → custom-payload → display pipeline works end to end:

  1. depthcharge boots the dev-signed FIT from mmcblk0p1 and hands off (x0 = FDT buffer, x1-x3 = 0, MMU off);
  2. stage0 re-enables OVL (+0x000C / +0x100C) and backlight (GPIO dout 0x10005114 / 0x10005154);
  3. the LBIO table at the fixed 0xffed9000 parses (record: pa handled via OVL_L0_ADDR substitution = 0xFD536000);
  4. fills at the live scanout are visible.

Effective gotchas that cost nine rounds, all [REVERSED] or resolved: OVL_EN offset (0x0F00→0x000C), GPIO dout offset (0x140→0x100), LBIO pa=0 legitimacy (cbgfx rejects it; scanout comes from OVL_L0_ADDR), and the empty x0 FDT buffer (bypassed with the fixed table address; root cause still open and RELEVANT FOR U-BOOT).

Next phase: U-Boot as the FIT kernel payload. Handoff prompt for the next agent: /home/vhaudiquet/u-boot-krane-prompt.md (milestones: krane defconfig/board, Image-header wrapper + flash flow, framebuffer vidconsole reusing the validated revival/scanout code, then eMMC + bootflow; all work upstreamable; x0-FDT-zeros trap documented).

Recovery: power-cycle, boot USB (unchanged), dd if=mmcblk0p1-pmos-backup.img of=/dev/mmcblk0p1 bs=4M conv=fsync.

Open items

  • physical_address in the LBIO record — resolved in Round 2: provably non-zero at runtime (depthcharge's device-era mtk_display_init programs OVL_L0_ADDR from the LBIO record and the menu rendered through it; the /dev/mem read attempt failed on CONFIG_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.

Round 11 — U-Boot as payload: target, driver, first flash (result pending)

First mainline-U-Boot flash after the Round-10 pipeline validation.

Build (milestone 1)

  • Branch krane in /home/vhaudiquet/u-boot (mainline main @ 527115ef); commits 29aec558 (board/defconfig/MAINTAINERS), 923cc754 (dtc include order), f0470c9b (video driver).
  • configs/mt8183_kukui_krane_defconfig forked from pumpkin: keeps CONFIG_POSITION_INDEPENDENT=y, DEBUG_UART (0x11002000 @ 26 MHz, goes nowhere — absence of colors is not failure), MTK serial/MMC/WDT; drops fastboot/TPHY/mtu3/USB. CONFIG_BOOTDELAY=-1 so the first boot stops at the prompt instead of autobooting pmOS from p3.
  • DT: CONFIG_OF_UPSTREAM=y, CONFIG_DEFAULT_DEVICE_TREE="mediatek/ mt8183-kukui-krane-sku176"with OF_UPSTREAM the name must include the vendor dir; no arch/arm/dts registration needed.
  • New trap found while building: U-Boot's legacy include/dt-bindings/clock/mt8183-clk.h (BSP numbering, CLK_TOP_CLK26M=0) shadows the upstream dts/upstream/include/dt-bindings/clock/mt8183-clk.h (CLK_TOP_CLK26M=112) for OF_UPSTREAM builds — the krane tree failed with undefined CLK_CAM_LARB6, and even fixing those macros would have silently produced wrong clock numbers everywhere. Fix: scripts/Makefile.lib prefers dts/upstream/include when CONFIG_OF_UPSTREAM=y. Pumpkin's in-tree dts (different binding generation) keeps working because its config does not set OF_UPSTREAM.

Video driver (milestone 3, flashed before first reboot)

drivers/video/mt8183_scanout.c (CONFIG_VIDEO_MT8183_SCANOUT, checkpatch-clean), binds the upstream ovl0@14008000 node (mediatek,mt8183-disp-ovl; the two 2L overlays match a different compatible, so exactly one probe). probe():

  1. revival identical to the stub's stage0: OVL_EN=1 (+0x000C), OVL0_2L_EN=1 (+0x100C), GPIO dout set for pins 43/176;
  2. LBIO parse at the fixed 0xffed9000 (ported from the stub's Round-10-validated find_framebuffer; host regression test compiled the function standalone against lbio.bin: record parsed, bad signature rejected);
  3. scanout = LBIO pa if >= 0x40000000, else OVL_L0_ADDR (+0x0F40) (Round-9 ground truth: 0xFD536000); else probe fails gracefully;
  4. geometry from the record (xRGB 32bpp required; U-Boot's BPP32 console default composes pixels exactly as r<<16|g<<8|b — checked in video-uclass video_index_to_colour);
  5. both the coreboot table and the fb sit above the DTB's 2 GiB DRAM window (memory@40000000 is 0x80000000 long), so both regions are identity-mapped with mmu_map_region() first (MMIO < 0x20000000 is already mapped Device by arch/arm/mach-mediatek/armv8-mem-map.c); video_set_flush_dcache(true) for the console writes.

Revival happens BEFORE parsing, so "backlit black" = ran but parse/ probe failed, "dark" = never reached the driver — same decision tree as the stub. No autoboot: U-Boot banner + prompt should stay on panel.

Wrap + flash (milestone 2)

krane-fb-stub/build-uboot-payload.sh: 64-byte arm64 Image header (code0 b +0x40 = 0x14000010, image_size = 64 + len(u-boot.bin), flags bit3, magic at 0x38) prepended to u-boot.bin, mkdepthcharge with the krane DTB, vbutil_kernel --verify. (Header-bytes were checked with od after an edit mishap dropped the flags word — always re-verify the raw header bytes.) depthcharge decompresses the FIT and jumps to the first byte — U-Boot's _start lands at +0x40, PC-relative (PIC) so the arbitrary 2 MiB slot is fine.

  • Payload krane-uboot-payload.bin sha256 1fc74a0a…, flashed to mmcblk0p1 (dd 4M conv=fsync), cmp OK (589824 bytes), on-device vbutil_kernel --verify → body verification succeeded.
  • Backup mmcblk0p1-pmos-backup.img (sha256 20951a54…) copied onto the USB stick (sda3) — recovery no longer depends on eMMC.

Expected observation

Panel shows the U-Boot banner (white/light-gray text on black, portrait 1200×1920) and the prompt, frozen (BOOTDELAY=-1). Nothing on panel = boot path or early crash; backlit black = reached handoff but driver probe failed. The x0-FDT-zeros trap is bypassed: U-Boot uses its embedded DTB and never reads the handoff FDT.

Round 12 — first U-Boot boot: pitch black; diagnostic wrapper

Reboot with Round-11 payload 1fc74a0a…: pitch black, no backlight. Per the Round-11 decision tree the video driver's probe never ran (the OVL/backlight revival is its first act). So either the image never executed (but Round 10 proved the handoff path, and only the payload contents changed) or U-Boot died between entry and the video probe — a wide window (PIE fixup, relocation, DM scan, and notably initr_env (MMC/clock probe) runs before stdio_add_devices in board_r.c, i.e. before video probe and the banner).

Localization flash (payload 9955943c…)

The wrapper itself now carries the life sign, independent of U-Boot: uboot-wrapper.S (linked at +0x40 inside the Image, immediate-encoded PIC, no relocations) runs before U-Boot proper:

  1. OVL revival + backlight (identical writes to the stub's stage0);
  2. reads OVL_L0_ADDR, fills the scanout (guarded >= 0x40000000) with full-screen green (0x8ca000 bytes = 120019204);
  3. branches (imm26 patched at build time) to U-Boot's entry at +0x40+wrapper_len (116 bytes).

Layout verified by disassembly before flashing: header (code0 b +0x40, image_size 0x6d9ac, flags bit3, magic), wrapper instruction sequence, patched tail branch, U-Boot intact at +0xB4. Payload flashed, cmp + on-device vbutil verify OK.

Decision tree

observation meaning
green screen + backlight wrapper ran; U-Boot crashed before video probe
dark, no backlight wrapper never executed — handoff/boot-path problem with THIS image
U-Boot banner everything works (banner replaces the green)