35 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.
Round 10 — SUCCESS: full pipeline validated
Payload adbea06f…: red → yellow → green → blue, blue held. The
depthcharge → custom-payload → display pipeline works end to end:
- depthcharge boots the dev-signed FIT from
mmcblk0p1and hands off (x0 = FDT buffer, x1-x3 = 0, MMU off); - stage0 re-enables OVL (+0x000C / +0x100C) and backlight (GPIO dout 0x10005114 / 0x10005154);
- the LBIO table at the fixed 0xffed9000 parses (record: pa handled via OVL_L0_ADDR substitution = 0xFD536000);
- 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_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.
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
kranein/home/vhaudiquet/u-boot(mainline main @527115ef); commits29aec558(board/defconfig/MAINTAINERS),923cc754(dtc include order),f0470c9b(video driver). configs/mt8183_kukui_krane_defconfigforked from pumpkin: keepsCONFIG_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=-1so 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 upstreamdts/upstream/include/dt-bindings/clock/mt8183-clk.h(CLK_TOP_CLK26M=112) for OF_UPSTREAM builds — the krane tree failed with undefinedCLK_CAM_LARB6, and even fixing those macros would have silently produced wrong clock numbers everywhere. Fix:scripts/Makefile.libprefersdts/upstream/includewhenCONFIG_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():
- revival identical to the stub's stage0:
OVL_EN=1(+0x000C),OVL0_2L_EN=1(+0x100C), GPIO dout set for pins 43/176; - LBIO parse at the fixed 0xffed9000 (ported from the stub's
Round-10-validated
find_framebuffer; host regression test compiled the function standalone againstlbio.bin: record parsed, bad signature rejected); - scanout = LBIO pa if >= 0x40000000, else
OVL_L0_ADDR(+0x0F40) (Round-9 ground truth: 0xFD536000); else probe fails gracefully; - 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); - 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 byarch/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.binsha2561fc74a0a…, flashed tommcblk0p1(dd 4M conv=fsync),cmpOK (589824 bytes), on-devicevbutil_kernel --verify→ body verification succeeded. - Backup
mmcblk0p1-pmos-backup.img(sha25620951a54…) 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:
- OVL revival + backlight (identical writes to the stub's stage0);
- reads OVL_L0_ADDR, fills the scanout (guarded >= 0x40000000) with full-screen green (0x8ca000 bytes = 120019204);
- 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.
Round 15 — all black again (Round 14 anomaly); blink-train wrapper
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 |