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:
+175
@@ -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, x1–x3 = 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, 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.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.
|
||||
Reference in New Issue
Block a user