Files
krane-fb-stub/RESEARCH.md
T

55 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)

Round 13 — green screen: U-Boot crashes pre-video-probe; init checkpoints

Reboot with 9955943c…: full-screen green, stable — the wrapper ran and U-Boot proper died between entry and the video probe (the video uclass would have video_cleared to black for the banner). The crash window is wide: PIE fixup, relocation, DM scan, and notably initr_env/initr_mmc run BEFORE stdio_add_devices in board_r.c.

Checkpoint flash (payload fc48d83b…)

U-Boot itself now paints checkpoints into the live scanout from board code (mt8183.c, TEMPORARY, gated on CONFIG_BOARD_EARLY_INIT_F/R, dcache_status()-guarded with mmu_map_region+flush for the post-relocation one):

band checkpoint position
full green wrapper before U-Boot (Round 12)
orange top board_early_init_f after initf_dm, before get_clocks/timer/serial/dram_init
blue bottom dram_init_banksize after dram_init, right before relocation
red top board_early_init_r post-relocation, before dm/mmc/env/console/video

Read: last paint reached = code survived through that checkpoint; the crash/hang is in the next window. Success = banner on black.

Round 14 — all green decoded: U-Boot's 4K-alignment requirement

Reboot with fc48d83b…: all green, no bands — U-Boot hung before board_early_init_f. Root cause found in start.S: with CONFIG_POSITION_INDEPENDENT, U-Boot verifies adr _start & 0xfff == 0 (it uses ADRP+ADD with lo12 relocations during the PIE fixup) and otherwise loops in wfi forever — a silent hang, no exception. Our wrapper pushed U-Boot's entry to load_base+0xB4: misaligned by construction.

Fix (payload faee130f…)

build-uboot-payload.sh now pads the wrapper so U-Boot starts at the next 4 KiB boundary (0x1000) inside the 2 MiB-aligned image; wrapper tail branch patched accordingly (0x140003d4 → 0x1000, verified by disassembly). Keep this invariant for every future layout change.

Reboot with faee130f… (4K-aligned U-Boot): all black, no backlight. Unexpected: the wrapper is byte-identical to Rounds 12/13 except its tail-branch immediate, and it demonstrably painted green in both. Black without any wrapper trace was only ever explained by "payload never ran". Candidates for a genuine regression: none found in the wrapper path; no U-Boot driver can touch the backlight GPIOs (no MTK GPIO driver in-tree, PINCTRL off); nothing before the video probe touches display HW. Working hypothesis: the Round-14 boot transiently did not hand off (or the report reflects a state we cannot yet distinguish). Note: a build slip was caught before flashing — the diagnostic hooks were gc-section'ed out because .config was stale; defconfig was re-run and the hook symbols verified with nm (board_early_init_f, board_early_init_r, dram_init_banksize, misc_init_r all present).

Round-15 payload (b1c5c380…, flashed, cmp + vbutil OK)

Maximally distinguishable phases:

  • wrapper: magenta full screen + backlight, then 3 slow backlight blinks (~250 ms each via the arch timer), then branch to U-Boot. Repeating blink trains = WDT reset loop; a single train then colors = wrapper ran; nothing at all = never ran.
  • U-Boot checkpoints unchanged: orange top (board_early_init_f), blue bottom (dram_init_banksize), red top (board_early_init_r).
  • new: yellow band + backlight re-assert in misc_init_r (CONFIG_MISC_INIT_R) — proves full init past the banner stage.

Decision tree

observation meaning
nothing, no blink payload not handed off (depthcharge-side)
blink train once, then magenta persists U-Boot crashed before board_early_init_f (PIE fixup / early board_init_f / initf_dm)
blink train repeating periodically reset loop (WDT) — identify the phase by the colors that flicker
magenta + orange/blue/red/yellow bands crash in the next window after the last band
banner on black success

Round 16 — bisecting the R14/R15 handoff failure

R15 (b1c5c380…): nothing at all — no magenta, no blink train. Since the wrapper + header bytes at 0x40 are functionally identical across R13 (ran) and R14/R15 (did not run), the failure is depthcharge-side. Only depthcharge-visible structural change between R13 and R14: the mkdepthcharge body grew from 0x80000 to 0x81000 when the 4 KiB alignment padding landed (R15: 0x82000). Verified the device-era depthcharge (R93, 0.0.22-10566-g8417b00957) sources are not locally available (clone is mainline-only; googlesource fetch failed); mainline KERNEL_SIZE is 0x20000000, so a 512 KiB constraint has no known mechanism yet — but the correlation is 2-for-2.

Discriminating flash (payload d418252a…)

R13 layout exactly (wrapper v1 = green fill, no blinks, U-Boot @0xB4) but with R14's body size (0x81000, via the larger R15 U-Boot):

observation conclusion
green (persistent) body size innocent; the 0x1000 offset somehow guilty — re-bisect
black FIT body > 0x80000 breaks this depthcharge — fix = keep FIT under 512 KiB (minimal FDT subimage) with U-Boot @0x1000

Round 17 — R16 green decodes body-size theory; total-size bisect

R16 (R13 layout, body 0x81000): green — body size ruled out (R16 and R14 share body 0x81000). Updated correlation: total wrapped-image size / image_size field: ran ≤ 449772, dead ≥ 453432. Device-era sources recovered far enough to rule out boot64/fit_decompress size checks (kukui r93/12573-B code identical modulo cosmetics; bounds 0x5f800000/0x60000000/0x80000000 — kend≈0x4006xxxx passes; the loader is DETERMINISTIC: first CB_MEM_RAM range, kstart=0x40000000 — the "random 2 MiB slot" note from Phase 1 was mainline behavior, [REVERSED] for the device).

Test A (payload a27f0354…, flashed, cmp+vbutil OK)

R16's exact known-good content (wrapper v1 green, U-Boot @0xB4, contiguous — wfi hang expected) + 8 KiB zero tail padding → total 457964, image_size=457964, body 0x83000. Isolates TOTAL SIZE from the 0x1000-offset/padding variable:

observation conclusion
green total size innocent → the 0x1000 alignment padding/placement is the killer (re-test padding content)
black total size (image_size value) alone gates the handoff

Round 18 — total size ruled out; wrapper-only isolator

R17 (R16 content + 8 KiB tail pad, total 457964 > dead 453432): green — total size ruled out. The only remaining structural difference between ran (R12/13/16/17: U-Boot contiguous at 0xB4) and dead (R14/15: U-Boot at 0x1000 behind interior zero pad): the interior pad + shifted placement. Also discovered: the U-Boot binary does NOT start at _start — arch/arm/cpu/u-boot.lds places 0x40 bytes of .vectors first, so link _start = 0x4C000140 (all prior alignment math shifts by 0x40; the PIE requirement is runtime _start ≡ link _start mod 4K AND start.S's wfi check demands runtime _start ≡ 0 mod 4K, so link _start must be 4K-aligned too).

Test B (payload 9485c59e…, flashed, cmp+vbutil OK, body 0x14000)

4 KiB image: header + wrapper v3 (magenta + 3 blinks + ~5 s hold) + zeros to 0x1000 + NO U-Boot, tail branch patched to self (hang):

observation conclusion
magenta + blinks + hang interior pad innocent; the trigger is U-Boot content/placement at 0x1000
black, nothing depthcharge rejects the padded image before any execution

Round 19 — Test B decoded: wrapper runs in padded layout; correct PIE alignment + post-clear checkpoint

Test B (9485c59e…): wrapper color + 3 blinks + color held — the wrapper runs fine even in the padded/0x1000 layout, with NO U-Boot present. Depthcharge is fully exonerated: it copies and jumps every payload. Combined with R14/R15 (black), the story closes: U-Boot (at 0x1000, aligned) RAN in R14/R15, reached the video probe and its video_clear() (black over the wrapper color, backlight back on), and died somewhere between the clear and a visible banner. The user's "no backlight" was a backlit-black misread (matches R5's physics). (The R15 blink train was evidently too brief to register.)

Alignment math corrected ([REVERSED] earlier notes)

The U-Boot file does NOT start at _start: a 4-byte stub precedes it (nm: link _start = CONFIG_TEXT_BASE + 4). Two constraints must hold simultaneously:

  1. start.S wfi check: runtime _start ≡ 0 (mod 4K);
  2. PIE fixup (adrp+add lo12): runtime _start ≡ link _start (mod 4K). ⇒ link _start must be 4K-aligned: CONFIG_TEXT_BASE=0x4C000FFC → link _start=0x4C001000; U-Boot file placed at image offset 0xEC0 → runtime _start=0x40001000. Verified: nm shows 0x4c001000; the payload script now asserts it against u-boot.sym. (Earlier R14/R15 layouts had runtime _start page offset 0x40 → wfi-hang — but green should have persisted; the observed black is only explained by the video_clear path, which means R14's U-Boot PASSED the wfi check... [OPEN] the R14/R15 images had link _start=0x4C000140 (page offset 0x140) and runtime 0x1040 (0x40) — mismatched mod 4K by 0x100, so even past the wfi check the PIE fixup would corrupt pointers; either way the video_clear observation stands as the only black-producing mechanism.)

Round 19 payload (1f598c7a…, flashed, cmp+vbutil OK)

  • CONFIG_TEXT_BASE=0x4C000FFC, U-Boot file at image 0xEC0 (correct alignment for both constraints);
  • new checkpoint: WHITE band painted at the END of video_post_probe (after the vidconsole child is bound+probed) — post-clear deaths are now distinguishable from banner-stage deaths;
  • all earlier checkpoints retained (orange/blue/red bands, misc_init_r yellow).

Decision tree

observation meaning
magenta + blinks + magenta persists U-Boot died BEFORE the video probe (bands show how far)
black + white band video probe completed; died between post_probe and banner
black, no white band died inside video_post_probe after video_clear (bind/probe of vidconsole)
banner (white on black) works

Round 20 — post-probe black decoded: U-Boot reaches video_clear; two-band instrumentation

Round 19 (1f598c7a…): wrapper color + blinks, then black, no white band. Decoded: U-Boot runs (correct alignment), reaches the video uclass video_clear() (the only black-painter — that's the blue→black transition), and dies/ends before the white band at the end of video_post_probe. The window contains only: colours setup, vidconsole bind ("vidconsole0"), device_probe of the console child, cyclic registration. A GRACEFUL failure there (bind/probe error → video probe fails) leaves exactly the observed state: cleared black, no banner (goes nowhere — no console), backlight on. Also noted: CONFIG_CYCLIC=y makes video-uclass line 732's gd->flags && GD_FLG_RELOC typo live (harmless-looking cyclic_register every 10 ms).

Round 20 payload (90f0de99…, flashed, cmp+vbutil OK)

Two-band instrumentation inside video_post_probe:

  • cyan band right after video_clear() — visible ⇒ the clear and the fb mapping work, death is later in this function or after;
  • white band after the vidconsole child is bound+probed (existing).

Decision tree

observation meaning
black only (no cyan) died between clear and cyan — video_set_default_colors/... (near-impossible)
cyan only death/graceful-failure in bind or probe of vidconsole0
cyan + white died between post_probe end and the banner (jumptable/console_init_r/puts)
banner works

Round 21 — [REVERSED R19/R20] root cause: build script patched delay's ret, not the tail branch

User report R20 ("blue fill, one blink, black, backlight off, ~1 s, NO 5 s hold") exposed a build bug that invalidates the R19/R20 interpretation — U-Boot never executed at all in R19/R20:

  • The wrapper's tail b . is NOT the last instruction: v3 puts the delay subroutine (8 insns incl. ret) after it. The script patched wrapper[-4:] = delay's ret, overwriting it with the branch.
  • Effect: the FIRST blink's bl delay never returns — it branches to the U-Boot file's first word (0x00000000 = UDF) with the backlight in the blink-OFF phase → screen black, backlight off, hang. Blue fill + one dark blink = exactly the R19/R20 observations. The "reached video_clear" decode of R19/R20 is [REVERSED]; the cyan/white bands were never reached (kept in the tree — still useful).
  • Test B (R18) was immune because its wrapper-only image left b . as the self-branch (patching ret was harmless there). R12R17 ran because wrapper v1/v2 had the tail branch as the last instruction.

Fixes (payload cff0e0e9…, flashed, cmp+vbutil OK)

  1. Script now SEARCHES the wrapper for the single b . (0x14000000) and asserts exactly one occurrence; patch target = the real branch.
  2. Placement recomputed from the ELF: _start is NOT at file offset 0 — the image leads with 0xFFC bytes (vectors/stub) so link _start = 0x4C001000 = file offset 0x1000 (TEXT_BASE 0x4C000FFC). U-Boot file placed at image 0x1000 → runtime _start = 0x40002000 (4K-aligned); PIE delta = -0xBFFFF000 (4K-aligned). Both alignment constraints hold for the first time in one layout.
  3. Verified in the packed image: b .@0x118 → 0x140007ba → target image 0x2000 = runtime 0x40002000 ✓; ret@0x138 intact ✓.

Expected behavior

Blue fill → 3 blinks (~1.5 s) → 5 s blue hold → U-Boot takes over. If U-Boot boots: banner (white on black). If it dies early, the orange/blue/red board-init bands and the post-clear cyan / post-probe white bands localize it (Round 19/20 instrumentation still in place).

Round 22 — wrapper fully healthy; U-Boot dies before board_init_f's first band

Round 21 (cff0e0e9…): blue, 3 blinks, blue held forever — the wrapper now runs its complete sequence (fill, blink train, 5 s hold) and jumps to 0x40002000. Blue persisting = nothing in U-Boot painted anything: death (or non-execution) BEFORE board_early_init_f's orange band. start.S review: the 4K wfi check passes (runtime _start 0x40002000); the PIE fixup applies the full delta to relative relocs and our delta (-0xBFFFF000) is 4K-aligned, so adrp+lo12 stays valid; crt0's SP (INIT_SP_RELATIVE, adrp __bss_start + 0x80000) is also 4K-delta-safe. So the hang is somewhere in [PIE fixup .. initcalls before board_early_init_f] — a path no payload has ever reached on this board (R12R17 all died at the 4K wfi check).

Round 22 payload (21c3f792…, flashed, cmp+vbutil OK)

New checkpoint: green band (y=400) painted at the very top of board_init_f (common/board_f.c, temporary) — before the entire initcall sequence.

Decision tree

observation death localized to
blue hold only (no green) start.S / crt0 / PIE fixup (pure asm window)
green only initcalls before early_init_f: fdtdec_setup (embedded DTB!), log, bootstage, bloblist, arch/mach_cpu_init, initf_dm (clock driver probes)
green + orange after early_init_f, before dram_init_banksize
green + orange + blue after dram_init_banksize, before relocation

Round 23 — [REVERSED R22 analysis] true root cause: 4-byte linker fill before _start skews the PIE fixup

R22 (21c3f792…): blue hold only — board_init_f's green band never painted. Root cause found in the disassembly, not guessed:

  • start.o's input .text section is 8-byte aligned, but CONFIG_TEXT_BASE=0x4C000FFC is only 4-aligned → ld inserts a 4-byte FILL at the start of .text → _start lands at TEXT_BASE+4, while _TEXT_BASE (the word start.S's PIE fixup loads as the link base) and __image_copy_start remain at TEXT_BASE.
  • start.S: pie_fixup: adr x0,_start; ldr x1,_TEXT_BASE; subs x9,x0,x1 → x9 = true_delta + 4 → EVERY relative relocation skewed by 4 → corrupted gd/function/fdt pointers → death before board_init_f. (The adrp + #:lo12: pairs used for the rela bounds also assume delta ≡ 0 mod 4K — with the skew, both mechanisms break.)
  • This retroactively explains the R12R22 alignment confusion: the "+4" was never a stub/vectors mystery — it was a linker fill that appears iff CONFIG_TEXT_BASE is not 8-aligned.
  • [REVERSED] the "vectors/stub bytes precede _start" note from R18/19; the 0xFFC leading bytes in old u-boot.bin dumps were the fill plus ELF file-offset artifacts, and the R22 "dies in pure asm" conclusion was wrong in mechanism (it died IN the fixup's corrupted pointers).

Fix (payload 18cf3e93…, flashed, cmp+vbutil OK)

  • CONFIG_TEXT_BASE=0x4C001000 (8-aligned) → no fill: _start == __image_copy_start == _TEXT_BASE == 0x4C001000, file offset 0 = b reset (verified in u-boot-nodtb.bin and in the packed image at 0x1000).
  • File placed at image offset 0x1000 → runtime _start = 0x40001000, start.S 4K check passes; PIE delta = -0xC000000 (4K-aligned).
  • Payload script now derives the file offset from __image_copy_start (not ELF section offsets) and ASSERTS _start == __image_copy_start so a fill regression fails at build time instead of on device.

Expected

blue → 3 blinks → 5 s hold → green band (board_init_f) → orange/blue/ red bands → cyan/white (video probe) → banner.

Round 24 — C environment alive; video probe is the failing stage

Round 23 (18cf3e93…): blue → blinks → hold → bands. The user reported (top to bottom): two red-ish stripes, yellow, green — i.e. board_init_f (green), board_early_init_f (orange, reads red-ish), board_early_init_r (red), misc_init_r (yellow) ALL ran; dram_init blue is at the very bottom (y=1820, easy to miss). CRITICAL: the screen was NOT cleared and no cyan/white band appeared → the video uclass post_probe (whose FIRST action after colour setup is video_clear) never ran → the video driver's probe FAILED or the device never bound. Everything else (banner needs the vidconsole) follows from that. Note: the "yellow" seen may be either misc_init_r (y=300) or the post-clear cyan band (0x00ffff00 = yellow in xRGB, y=260) — irrelevant since no clear happened.

Round 24 payload (03fc7b4d…, flashed, cmp+vbutil OK)

New checkpoints (mt8183_scanout.c + video-uclass.c video_post_bind):

  • purple (y=470): a video device got BOUND (driver matched DT)
  • magenta (y=500): driver probe entered
  • gray (y=620): geometry resolved (coreboot LBIO parsed + scanout addr valid)
  • red bands at failure exits: y=780 (ovl reg read failed), y=800 (coreboot LBIO not found), y=820 (pixel format mismatch), y=840 (no plausible scanout address)

Decision tree

observation meaning
no purple video driver never bound (DT compatible / DM scan issue)
purple, no magenta bound but probe not called (uclass sequencing)
magenta + red@800 coreboot table/LBIO not found (map/parse)
magenta + red@820 LBIO found but format mismatch
magenta + red@840 no plausible scanout address
magenta + gray geometry OK; failure later (clear/probe tail)
cyan/white appear video_clear + post_probe reached — back on the R19/20 tree

Round 25 — [REVERSED] not a probe failure: the video device was never probed (console env wiring)

Round 24 (03fc7b4d…): purple, no magenta, no red error bands — the driver matched the DT node and bound, but device_probe was never called, and none of the probe's error paths ran. Root cause is in common code, not the driver:

  • stdio_add_devices()'s probe-all loop over UCLASS_VIDEO is compiled OUT when CONFIG_SYS_CONSOLE_IS_IN_ENV=y — and that defaults to y via CONSOLE_MUX (inherited from the pumpkin defconfig fork).
  • With SYS_CONSOLE_IS_IN_ENV, console_init_r() reads env stdout/stderr and muxes via iomux_doenv(). There was NO environment (and the env backend was MMC — inherited, wrong for a depthcharge payload), so the names were NULL, console_search_dev() never saw the string "vidconsole", the lazy stdio_probe_device(name, UCLASS_VIDEO) never fired, and the console fell back to serial only. The video device was never probed → no clear, no cyan/white, no banner. Screen keeps the init bands.

Fix (payload e4bdd875…, flashed, cmp+vbutil OK)

  • CONFIG_ENV_IS_NOWHERE=y (drops the MMC env dependency inherited from pumpkin — correct for a payload stage, deterministic default env);
  • new CONFIG_ENV_SOURCE_FILE="krane" + board/mediatek/mt8183/krane.env: stdin=serial stdout=serial,vidconsole stderr=serial,vidconsole so console_init_r() → iomux_doenv() → console_search_dev ("vidconsole") → stdio_probe_device → probes the video device (driver probe → clear → vidconsole registered) → banner muxed to serial + screen.
  • R24 instrumentation kept for this round.

Expected

blue → blinks → hold → green/orange/blue/red/yellow bands → magenta (probe entered) → screen CLEARS black → gray (geometry) → cyan/white (video clear/probe) → white-on-black banner (or a red failure band: y=800 lbio, y=820 fmt, y=840 scanout).

Round 26 — video probe SUCCEEDED (clear ran); post-console hang reset via WDT; WDT disabled for observability

Round 25 (e4bdd875…): bands → screen cleared black → reset back to the depthcharge menu. Decode:

  • The clear proves console env wiring now works: krane.env's stdout=serial,vidconsole → console_init_r → iomux_doenv → console_search_dev("vidconsole") → stdio_probe_device → video driver probed (magenta/gray painted, then erased by the full-screen video_clear) → post-probe cyan/white should have painted too (thin, y=200/260 — possibly missed in the crash window).
  • The reset is the MTK watchdog: CONFIG_WDT_MTK autostarts a 60 s watchdog at initr_watchdog (early board_r); it is serviced by the cyclic scheduler only once main_loop() runs. A hang anywhere between the video probe (still in board_r console init) and the first schedule() → 60 s → WDT reset → depthcharge menu. User's "immediately" = unattended reset.

Round 26 payload (08ee5c1f…, flashed, cmp+vbutil OK)

  • # CONFIG_WATCHDOG_AUTOSTART is not set — the WDT is probed but not started, so a hang now freezes the screen with bands visible instead of resetting. (Temporary bring-up measure; upstream will want the WDT on.)
  • New checkpoint: light-gray band (y=900) at main_loop entry (common/main.c) — everything before it completed.

Decision tree

observation meaning
black screen stays black, no reset, no gray hang between video probe and main_loop (console tail / late board_r / initr_mmc...)
black + gray band, frozen main_loop reached, hang in preboot/cli_init/cli_loop — inspect further
banner + prompt (white on black) WORKING

Round 27 — pink band in board_late_init; WDT-off reset anomaly needs timing

Round 26 (08ee5c1f…): user reports "same as before": bands → black (clear) → menu. Ambiguity: the report may describe the R25 payload (WDT autostart ON, 60 s → reset explains it) or the R26 payload (WDT autostart OFF — then the reset source is NOT U-Boot's watchdog: coreboot's mtk_wdt_init DISABLES the WDT, depthcharge doesn't arm it, so a reset would need another mechanism). The black→menu GAP will discriminate: ~55-65 s ⇒ watchdog; a few seconds ⇒ something else.

The missing light-gray band (main_loop entry, y=900) keeps the death window at [misc_init_r (yellow, AFTER console_init_r!) .. main_loop]. Refined understanding of the observed order: the video probe runs INSIDE console_init_r (lazy stdio_probe_device via iomux), so the screen clears BEFORE misc_init_r's yellow paints — yellow on black is expected, and the pre-clear bands (green/orange/red + probe's magenta/gray/cyan/white) are erased by the clear. Board_r tail: console_init_r → console_announce_r (first vidconsole TEXT!) → arch/misc_init_r → interrupt/timer init → board_late_init → EVT_LAST_STAGE_INIT → initr_mem → run_main_loop. No text has ever been reported — the first vidconsole puts (console_announce banner or "In:/Out:/Err:") may itself be the crash point.

Round 27 payload (948d5e2b…, flashed, cmp+vbutil OK)

  • New pink band (y=860) in board_late_init (CONFIG_BOARD_LATE_INIT enabled). Sequence of late checkpoints: yellow (misc_init_r) → pink (board_late_init) → light-gray (main_loop).
  • WDT still off (autostart=n).

Round 28 — reset mechanism identified: sync abort → panic → PSCI reset; PANIC_HANG to capture the dump

Round 26/27 analysis (user: reset in <1 s after the black screen, WDT autostart already off). The reset mechanism is now proven from source, not guessed:

  • coreboot's mtk_wdt_init() DISABLES the MTK WDT (clears WDT_ENABLE); depthcharge never touches it; U-Boot's mtk_wdt_probe() even stops it. Hardware watchdog ruled out.
  • arch/arm/mach-mediatek/mt8183/init.c: reset_cpu() → psci_system_reset() (SMC into BL31) — instant warm reset.
  • lib/panic.c: CONFIG_PANIC_HANG unset → panic() → do_reset() → reset_cpu() → PSCI.
  • arch/arm/lib/interrupts_64.c: do_sync() → show_regs → panic("Resetting CPU ...").

⇒ ANY synchronous abort after the video probe = instant reset to the depthcharge menu, <1 s. Matches the observation exactly. (Also retroactively plausible for earlier "resets".)

Round 28 payload (d1e78b0f…, flashed, cmp+vbutil OK)

CONFIG_PANIC_HANG=y — on an abort, U-Boot now prints the full exception dump ("Synchronous Abort" handler, esr, FAR, registers, PC) through the vidconsole and HANGS with the text visible instead of resetting. Plus R27's pink (board_late_init, y=860) and R26's light-gray (main_loop, y=900) bands.

Ask to the user

Reboot, select Internal storage. Expect: bands → black → then either frozen black with WHITE TEXT (transcribe it! esr/FAR/ELR tell us the exact fault), or a hang at some band stage. Report the band stack (yellow/pink/gray) and any text.