Files
krane-fb-stub/main.c
T
vhaudiquet da4aaca1c2 stub: accept LBIO physical_address == 0 — it is the real record here
Round 5: the Round-4 diagnostic blinks decoded as parse_fail. Root cause
confirmed in coreboot 4.14 source (kukui mainboard.c + edid_fill_fb.c):
the LBIO framebuffer record genuinely has physical_address = 0 — the
menu renders through DRAM address 0, which the never-stopped OVL scans
out (display_init_required() is false, so depthcharge registers no
display ops and its handoff cleanup's backlight/stop calls are no-ops).

Round-2's 'menu proves pa != 0' reasoning was wrong; the original
open-item note was right. The stub's !fb->pa rejection caused every
parse failure. find_framebuffer now accepts pa == 0 and the stub paints
at address 0, the same region depthcharge's own cleanup black-fills.

host_test gained a pa==0 regression test; qemu_test unchanged. Payload
b25d9132... flashed and verified (cmp + vbutil).
2026-08-29 23:09:37 +02:00

357 lines
11 KiB
C

/*
* krane-fb-stub — freestanding, position-independent payload main.
*
* Checkpoint sequence (from the task spec):
* 1. RED — stub entered and running (depthcharge jumped here)
* 2. YELLOW — DTB parsed, /firmware/coreboot node found
* 3. GREEN — "LBIO" signature verified, LB_TAG_FRAMEBUFFER record found
* 4. BLUE — previous fills visibly succeeded; hold blue forever
* On any failure: spin forever on the LAST SUCCESSFUL color.
*
* Deviation from the prompt (documented in README): the framebuffer
* address is only knowable after parsing the DTB and the coreboot table,
* so "red on entry" is physically impossible. Parsing happens first
* (fast, <1ms); if any parse stage fails the screen keeps showing
* depthcharge's own output (the dev-mode menu), which is an even
* stronger "stuck at stage N" signal than a stuck color.
*/
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;
/* ---- platform helpers --------------------------------------------------- */
static void halt(void)
{
for (;;)
__asm__ volatile("wfe");
}
#ifdef __aarch64__
/* Generic timer read — not a timer driver, just two system registers. */
static void delay_ms(unsigned ms)
{
u64 frq, t;
__asm__ volatile("mrs %0, cntfrq_el0" : "=r"(frq));
if (!frq) { /* paranoia: fallback spin */
volatile u64 n = (u64)ms * 200000;
while (n--)
;
return;
}
__asm__ volatile("mrs %0, cntpct_el0" : "=r"(t));
u64 end = t + (frq / 1000) * (u64)ms;
do {
__asm__ volatile("mrs %0, cntpct_el0" : "=r"(t));
} while (t < end);
}
#else
/* host test build */
static void delay_ms(unsigned ms) { (void)ms; }
#endif
static u32 rd32le(const void *p) { return *(volatile const u32 *)p; }
static u32 rd32be(const void *p) { return __builtin_bswap32(*(volatile const u32 *)p); }
static int streq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
/* ---- FDT parsing -------------------------------------------------------- */
#define FDT_MAGIC 0xd00dfeedu
#define FDT_BEGIN_NODE 1
#define FDT_END_NODE 2
#define FDT_PROP 3
#define FDT_NOP 4
#define FDT_END 9
struct fbinfo {
u64 pa;
u32 xres, yres, bpl;
u8 bpp, rpos, rsize, gpos, gsize, bpos, bsize;
};
/*
* Walk the FDT at `dt`, find /firmware/coreboot, return reg pair #1
* (coreboot table address + size). Cell counts tracked from
* #address-cells/#size-cells properties; krane root is 2/2 and
* depthcharge's fixup adds them on /firmware as well.
*/
static int find_coreboot_reg(const void *dt, u64 *addr, u32 *size)
{
const u8 *base = (const u8 *)dt;
const u8 *sbase = base + rd32be(base + 12); /* off_dt_strings */
const u32 *p = (const u32 *)(base + rd32be(base + 8)); /* off_dt_struct */
if (rd32be(base) != FDT_MAGIC)
return -1;
int depth = 0;
const char *path[8];
u32 ac = 2, sc = 2; /* krane root: #address/size-cells = 2/2 */
for (;;) {
u32 tok = rd32be(p); p++;
if (tok == FDT_BEGIN_NODE) {
const char *name = (const char *)p;
while (*(volatile const u8 *)p) /* skip name + NUL */
p = (const u32 *)((const u8 *)p + 1);
p = (const u32 *)(((u64)p + 4) & ~3ull); /* + pad */
if (depth < 8)
path[depth] = name;
depth++;
} else if (tok == FDT_END_NODE) {
depth--;
} else if (tok == FDT_PROP) {
u32 len = rd32be(p); p++;
u32 nameoff = rd32be(p); p++;
const char *pname = (const char *)(sbase + nameoff);
if (len == 4 && streq(pname, "#address-cells"))
ac = rd32be(p);
else if (len == 4 && streq(pname, "#size-cells"))
sc = rd32be(p);
else if (depth == 3 && streq(path[1], "firmware") &&
streq(path[2], "coreboot") && streq(pname, "reg") &&
len >= 16 && ac == 2 && sc == 2) {
/* first (address, size) pair, big-endian cells */
*addr = (u64)rd32be(p) << 32 | rd32be(p + 1);
*size = (u64)rd32be(p + 2) << 32 | rd32be(p + 3);
return 0;
}
p = (const u32 *)((const u8 *)p + ((len + 3) & ~3ull));
} else if (tok == FDT_NOP) {
continue;
} else { /* FDT_END or garbage */
return -1;
}
}
}
/* ---- coreboot table walk ------------------------------------------------ */
#define LB_TAG_FRAMEBUFFER 0x12
/*
* Layout verified against coreboot
* src/commonlib/include/commonlib/coreboot_tables.h (see README):
* struct lb_header: sig[4] "LBIO", header_bytes, header_checksum,
* table_bytes, table_checksum, table_entries (LE u32)
* struct lb_record: tag, size
* struct lb_framebuffer, offsets from record start (record = tag, size, ...):
* physical_address @8 (lb_uint64_t = 4-byte-aligned u64, little-endian)
* x_resolution @16, y_resolution @20, bytes_per_line @24,
* bits_per_pixel @28, red_pos/size @29/30, green @31/32,
* blue @33/34, reserved @35/36, orientation @37, flags @38, pad @39
* sizeof(struct lb_framebuffer) == 40 (host-verified with real header).
*
* **physical_address == 0 is LEGITIMATE on this device**: coreboot 4.14
* kukui calls fb_new_framebuffer_info_from_edid(edid, 0) with no carveout,
* so the LBIO record really says pa=0 and depthcharge's UI draws its menu
* into DRAM address 0, which the (never-stopped) OVL scans out. Do NOT
* reject it; paint at address 0 — the same 9.2 MB region depthcharge's own
* display_cleanup black-fills at handoff.
*/
static int find_framebuffer(u64 table, struct fbinfo *fb)
{
const u8 *h = (const u8 *)table;
if (rd32le(h) != 0x4F49424Cu) /* "LBIO", 'L' = lowest byte */
return -1;
u32 header_bytes = rd32le(h + 4);
u32 entries = rd32le(h + 20);
if (header_bytes < 24 || header_bytes > 4096 ||
entries == 0 || entries > 4096)
return -1;
const u8 *rec = h + header_bytes;
for (u32 i = 0; i < entries; i++) {
u32 tag = rd32le(rec);
u32 rsize = rd32le(rec + 4);
if (rsize < 8)
return -1;
if (tag == LB_TAG_FRAMEBUFFER) {
if (rsize < 40)
return -1;
/* pa == 0 is legitimate here (see block comment). */
fb->pa = (u64)rd32le(rec + 8) |
(u64)rd32le(rec + 12) << 32;
fb->xres = rd32le(rec + 16);
fb->yres = rd32le(rec + 20);
fb->bpl = rd32le(rec + 24);
fb->bpp = rec[28];
fb->rpos = rec[29]; fb->rsize = rec[30];
fb->gpos = rec[31]; fb->gsize = rec[32];
fb->bpos = rec[33]; fb->bsize = rec[34];
if (!fb->xres || !fb->yres || !fb->bpl)
return -1;
if (fb->bpp % 8 || fb->bpp < 8 || fb->bpp > 32)
return -1;
if (fb->bpl < (u32)fb->xres * (fb->bpp / 8))
return -1;
if (fb->xres > 16384 || fb->yres > 16384)
return -1;
return 0;
}
rec += rsize;
}
return -1;
}
/* ---- framebuffer fill --------------------------------------------------- */
static u64 mkcolor(const struct fbinfo *f, int r, int g, int b)
{
u64 v = 0;
if (r && f->rsize && f->rsize < 32)
v |= (u64)((1u << f->rsize) - 1) << f->rpos;
if (g && f->gsize && f->gsize < 32)
v |= (u64)((1u << f->gsize) - 1) << f->gpos;
if (b && f->bsize && f->bsize < 32)
v |= (u64)((1u << f->bsize) - 1) << f->bpos;
return v;
}
static void fill_screen(const struct fbinfo *f, u64 val)
{
u32 bppb = f->bpp / 8;
for (u32 y = 0; y < f->yres; y++) {
volatile u8 *row = (volatile u8 *)f->pa + (u64)y * f->bpl;
for (u32 x = 0; x < f->xres; x++) {
volatile u8 *px = row + (u64)x * bppb;
switch (bppb) {
case 4: *(volatile u32 *)px = (u32)val; break;
case 2: *(volatile u16 *)px = (u16)val; break;
case 1: *px = (u8)val; break;
case 3:
px[0] = (u8)val;
px[1] = (u8)(val >> 8);
px[2] = (u8)(val >> 16);
break;
}
}
}
}
/* ---- display revival ----------------------------------------------------
*
* Verified against the DEVICE's depthcharge (v0.0.22-10476, 2021/2022):
* display_cleanup() runs at CleanupOnHandoff before jumping here:
* 1. clear_screen(black) — LBIO framebuffer painted black
* 2. backlight_update(false) — GPIO 43 (DISP_PWM) and GPIO 176
* (EN_LCD_BL) driven low
* 3. mtk_display_stop() — OVL_EN=0 and OVL0_2L_EN=0
* The DSI link, panel and display MTCMOS stay up (no panel poweroff in this
* firmware generation). So the stub just needs to undo exactly those three
* steps — no DSI/panel re-init required.
*/
#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
/* GpioRegs (mt8183.h): dir[6]@0x000, rsv00[160], dout[6]@0x100,
* rsv01[160], din[6]@0x200; GpioValRegs = 16 B: val@0, set@4, rst@8.
* **[REVERSED]** Round-1 note claimed dout@0x140 — wrong, backlight never
* fired in any round; read back from the device-era header. */
#define GPIO_DOUT_SET(pin) (GPIO_BASE + 0x100 + ((pin) / 32) * 16 + 4)
#define GPIO_DOUT_RST(pin) (GPIO_BASE + 0x100 + ((pin) / 32) * 16 + 8)
#define GPIO_DOUT_BIT(pin) (1u << ((pin) % 32))
#define PAD_DISP_PWM 43 /* DISP_PWM */
#define PAD_EN_LCD_BL 176 /* PERIPHERAL_EN13 */
static void wr32(u64 addr, u32 val)
{
*(volatile u32 *)addr = val;
}
/* ---- 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)
{
for (int i = 0; i < times; i++) {
wr32(GPIO_DOUT_RST(PAD_DISP_PWM), GPIO_DOUT_BIT(PAD_DISP_PWM));
wr32(GPIO_DOUT_RST(PAD_EN_LCD_BL), GPIO_DOUT_BIT(PAD_EN_LCD_BL));
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);
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));
}
/* ---- main checkpoint sequence ------------------------------------------- */
static void checkpoint(const struct fbinfo *f, int r, int g, int b)
{
fill_screen(f, mkcolor(f, r, g, 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;
stage0();
if (find_coreboot_reg((const void *)dtb, &lbio_addr, &lbio_size))
parse_fail(); /* 5 blinks: DTB parse failed */
if (find_framebuffer(lbio_addr, &fb))
parse_fail(); /* 5 blinks: LBIO/fb record bad */
/* Checkpoint 1: red — parsed; LBIO framebuffer record is sane. */
checkpoint(&fb, 1, 0, 0); /* red */
/* Checkpoint 2: yellow. */
checkpoint(&fb, 1, 1, 0); /* yellow */
/* Checkpoint 3: green. */
checkpoint(&fb, 0, 1, 0); /* green */
/* Checkpoint 4: blue — hold. */
checkpoint(&fb, 0, 0, 1); /* blue */
halt(); /* hold blue forever */
}