RESEARCH.md: full lab notebook of display investigation

Chronological record of the research: source verification (coreboot
tables, arm64 Image header, depthcharge FIT loader), the first black-
screen attempt, the depth-charge-version forensics that identified the
handoff teardown (black fill + backlight GPIOs + OVL_EN off), the
register-level facts used for the revival fix, flash state, and a
post-fix diagnostic decision tree.
This commit is contained in:
vhaudiquet
2026-08-29 20:52:53 +02:00
parent 6e31a5566e
commit fc4292918f
+175
View File
@@ -0,0 +1,175 @@
# 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 = 0x0F00`, `DISP_REG_OVL0_2L_EN =
0x100C` (from 2021 `mtk_ddp.c`; 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 = 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:
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.
## Flash state
- `mmcblk0p1` reflashed with the revival payload:
sha256 `edb65b29191692186a6fde2058df05a1999c257e19b60cf01cf4ee63a6933237`,
verified on-device (`cmp` first 159,744 bytes + `vbutil_kernel --verify`
→ body verification succeeded).
- 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.
## Diagnostic decision tree (after this fix)
| observation | meaning |
|---|---|
| menu exits → black immediately, no color | stub ran but died in DTB/LBIO parse (or OVL revive didn't take) |
| colors appear but sequence stops early | fill/mask problem — stuck color identifies stage |
| red → yellow → green → blue, blue held | pipeline fully validated |
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 is provably non-zero at runtime
(depthcharge's menu rendered through it), even though current mainline
coreboot passes `fb_addr=0` for kukui. Reading the live LBIO record from
Linux is currently blocked (pmOS kernel: `IO_STRICT_DEVMEM` → `/dev/mem`
EPERM, no `/proc/kcore`, sysfs coreboot driver exposes only tags). If the
revival fix doesn't produce colors, next step is a stub-side diagnostic
(e.g., blink backlight GPIO as a debug signal) or kexec-ing a small
reader.
- qemu test updated to the same expectation (colors + held blue); it
exercises parser + fill logic, not the MMIO revival writes.