stub: stage-0 diagnostic — prove handoff before parsing

Round 3: still black with the corrected OVL_EN offset. All device-era
display_cleanup steps are now provably undone, and init_screen()
sequencing proves the menu was drawn into the LBIO physical_address
(OVL_L0_ADDR == pa), so revival + fills are correct on paper. The
unexcluded branch: the payload may never execute — internal boot of a
custom p1 image was never baseline-proven on this device.

Diagnostic payload (9d7859dc...):
- first instructions: OVL_EN=1, OVL0_2L_EN=1, backlight on (no parse)
- parse failure: 5 slow backlight blinks, spin
- parse success: red -> yellow -> green -> blue, blue held

Decision tree: colors = pipeline validated; blinks on black = parse
failed at runtime; nothing = payload never handed off.

An earlier design read OVL_L0_ADDR pre-parse; abandoned: unassigned
MMIO reads data-abort on qemu -M virt (killed the test) and would do
the same on hardware with a gated display clock. The stub now has no
MMIO reads.

host_test and qemu_test pass; flashed and verified on device.
This commit is contained in:
vhaudiquet
2026-08-29 22:01:59 +02:00
parent 10fb741163
commit dca94d627c
4 changed files with 94 additions and 23 deletions
+47 -4
View File
@@ -194,20 +194,63 @@ disassembly before reboot), flashed to `mmcblk0p1`, `cmp` +
## Flash state
- `mmcblk0p1` reflashed with the revival payload:
sha256 `9e7cf29d3cddb83cda957a469ff5fb0dea70a2d67c9a6cc0a1f1497b19bc8756`,
- `mmcblk0p1` reflashed with the Round-3 diagnostic payload:
sha256 `9d7859dc04dd9c445143f6b45600bb972cf5fc1497c06a4f9db549e3d2971e73`,
verified on-device (`cmp` first 159,744 bytes + `vbutil_kernel --verify`
→ body verification succeeded). (Round-1 payload was `edb65b29…`.)
→ 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.
## 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) |
| black, no colors, no blink | payload never handed off — boot-path problem (vboot/mkdepthcharge), investigate p1 boot |
| black + 5 slow backlight blinks | 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 |
Binary file not shown.
BIN
View File
Binary file not shown.
+47 -19
View File
@@ -248,6 +248,7 @@ static void fill_screen(const struct fbinfo *f, u64 val)
#define DISP_OVL0_BASE 0x14008000u
#define DISP_REG_OVL_EN 0x000Cu /* mtk_ddp.c, both mt8173/mt8183 */
#define DISP_REG_OVL_L0_ADDR 0x0F40u /* scanout address, survives stop */
#define DISP_REG_OVL0_2L_EN 0x100Cu /* 2021 mtk_ddp.c */
#define GPIO_BASE 0x10005000u
@@ -263,14 +264,40 @@ static void wr32(u64 addr, u32 val)
*(volatile u32 *)addr = val;
}
/* Undo depthcharge's display_cleanup() so our fills are visible. */
static void display_revive(void)
/* ---- main checkpoint sequence ------------------------------------------- */
/*
* The krane panel (BOE TV101WUM_NL6) is 1200x1920 xRGB8888 — the menu
* buffer depthcharge scanned out (OVL_L0_ADDR, programmed from the LBIO
* record's physical_address) has exactly that geometry. So painting the
* parsed LBIO pa IS repainting the still-registered scanout buffer.
* Note: we deliberately never READ OVL registers — MMIO reads to a gated
* display module can data-abort (qemu -M virt aborts on such reads too,
* which killed an earlier stage-0 design); writes are always safe.
*/
static void blink_backlight(int times)
{
/* Same buffer depthcharge scanned out for the menu. */
for (int i = 0; i < times; i++) {
wr32(GPIO_DOUT_SET(PAD_DISP_PWM), 0);
wr32(GPIO_DOUT_SET(PAD_EN_LCD_BL), 0);
delay_ms(300);
wr32(GPIO_DOUT_SET(PAD_DISP_PWM), GPIO_DOUT_BIT(PAD_DISP_PWM));
wr32(GPIO_DOUT_SET(PAD_EN_LCD_BL), GPIO_DOUT_BIT(PAD_EN_LCD_BL));
delay_ms(300);
}
}
static void stage0(void)
{
/*
* Stage 0 — first instructions after entry, BEFORE any parsing.
* Backlight on + OVL engine re-enabled (undoing depthcharge's
* cleanup). No reads, no parse: if even this never becomes visible,
* the payload was never handed off (boot-path problem), not a
* display-revival problem.
*/
wr32(DISP_OVL0_BASE + DISP_REG_OVL_EN, 1);
wr32(DISP_OVL0_BASE + DISP_REG_OVL0_2L_EN, 1);
/* Backlight on (both pins), matching kukui_backlight_update(true). */
wr32(GPIO_DOUT_SET(PAD_DISP_PWM), GPIO_DOUT_BIT(PAD_DISP_PWM));
wr32(GPIO_DOUT_SET(PAD_EN_LCD_BL), GPIO_DOUT_BIT(PAD_EN_LCD_BL));
}
@@ -283,33 +310,34 @@ static void checkpoint(const struct fbinfo *f, int r, int g, int b)
delay_ms(2000);
}
/* Parse failure: black screen + 5 slow backlight blinks, then spin. */
static void parse_fail(void)
{
blink_backlight(5);
halt();
}
void cmain(u64 dtb)
{
struct fbinfo fb;
u64 lbio_addr;
u32 lbio_size;
/*
* Parse everything first (instantaneous). On failure halt unpainted:
* depthcharge blacked the screen and disabled the OVL engine, so the
* failure signature is a black screen right after the menu.
*/
stage0();
if (find_coreboot_reg((const void *)dtb, &lbio_addr, &lbio_size))
halt(); /* black screen: DTB parse failed */
parse_fail(); /* 5 blinks: DTB parse failed */
if (find_framebuffer(lbio_addr, &fb))
halt(); /* black screen: LBIO/fb record bad */
parse_fail(); /* 5 blinks: LBIO/fb record bad */
/* Re-enable scanout + backlight, then paint. */
display_revive();
/* Checkpoint 1: red — we entered, parsed, revived the display. */
/* Checkpoint 1: red — parsed; LBIO framebuffer record is sane. */
checkpoint(&fb, 1, 0, 0); /* red */
/* Checkpoint 2: yellow — DTB stage. */
/* Checkpoint 2: yellow. */
checkpoint(&fb, 1, 1, 0); /* yellow */
/* Checkpoint 3: green — LBIO + framebuffer record found. */
/* Checkpoint 3: green. */
checkpoint(&fb, 0, 1, 0); /* green */
/* Checkpoint 4: blue — all earlier fills visibly succeeded. */
/* Checkpoint 4: blue — hold. */
checkpoint(&fb, 0, 0, 1); /* blue */
halt(); /* hold blue forever */