Files
krane-fb-stub/main.c
T
vhaudiquet edb21f2e94 stub: render failure dump as giant hex digits
Round 8: the Round-7 bit-band dump was visible at the L0_ADDR scanout
(both the scanout substitution and the render path work), but 32 coarse
squares per row proved unreadable from a photo. Replace the rendering
with giant hex digits: one u32 per line, 8 digits of a 3x5 font scaled
x10 (30x50 px), white on the black background depthcharge left.
Readable in any panel orientation, transcribable as text.

Same data and markers as before; word 15 still carries the raw
OVL_L0_ADDR value. Payload 10760e3c... flashed and verified.
2026-08-30 00:22:09 +02:00

456 lines
14 KiB
C

/*
* krane-fb-stub — freestanding, position-independent payload main.
*
* Success path: red 2s -> yellow 2s -> green 2s -> blue held.
* Failure path: 5 slow backlight blinks, then a bit-band diagnostic
* dump rendered into the live scanout buffer (see
* fail_dump below); decode offline from a photo.
*
* Runtime facts established on the device (see RESEARCH.md):
* - depthcharge enters with x0 = flattened DTB (fixed up), x1-x3 = 0,
* MMU off (src/arch/arm/boot64.c: handoff(fdt, 0, 0, 0)).
* - The dev menu was drawn via cbgfx, which REJECTS pa == 0, so the
* runtime framebuffer address is the one programmed into
* OVL_L0_ADDR (0x14008F40) by mtk_display_init() — read it instead
* of trusting the LBIO record.
* - OVL_EN is at +0x000C (NOT 0x0F00), GPIO dout block at +0x0100
* (NOT 0x0140); both earlier offsets were misreads, [REVERSED] in
* RESEARCH.md.
*/
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.
*/
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 tolerated: upstream coreboot 4.14 kukui passes
* fb_addr=0, and the true scanout address then comes from OVL_L0_ADDR
* (see read_scanout_addr / cmain). The geometry fields are always valid.
*/
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;
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 ----------------------------------------------------
*
* Device-era depthcharge (v0.0.22-10476/10566) display_cleanup() at
* CleanupOnHandoff runs before jumping here:
* 1. disable_graphics_buffer() + clear_screen(black)
* 2. backlight_update(false) — GPIO 43 (DISP_PWM), GPIO 176 (EN_LCD_BL)
* 3. mtk_display_stop() — OVL_EN=0 at +0x000C, OVL0_2L_EN=0 at
* +0x100C
* We undo 2 and 3. OVL_L0_ADDR is not touched by stop(), so the scanout
* address survives; re-enabling the engines resumes fetching from it.
*/
#define DISP_OVL0_BASE 0x14008000u
#define DISP_REG_OVL_EN 0x000Cu /* mtk_ddp.c, both mt8173/mt8183 */
#define DISP_REG_OVL_L0_ADDR 0x0F40u /* live scanout address */
#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; 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;
}
/* The scanout framebuffer address depthcharge actually used: programmed
* into OVL_L0_ADDR by mtk_display_init() and left there by stop(). This
* is ground truth — the LBIO record may disagree. */
static u32 read_scanout_addr(void)
{
return rd32le((const void *)(DISP_OVL0_BASE + DISP_REG_OVL_L0_ADDR));
}
/* Plausible MT8183 DRAM address (qemu: unmapped read yields 0 / -1). */
static int plausible_fb(u32 a)
{
return a >= 0x40000000u;
}
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)
{
/*
* First instructions after entry, before any parsing: undo the
* display teardown (OVL engines + backlight) so that anything we
* paint is visible. No MMIO reads here.
*/
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));
}
/* ---- failure diagnostics -------------------------------------------------
*
* The parse failed. Paint the failing values as giant hex digits directly
* into the live scanout buffer (read from OVL_L0_ADDR — the same buffer
* the menu was displayed from and clear_screen() blackened at handoff).
* No parse result is needed to find it. One word per line, 8 digits of
* 3x5 cells scaled x10 (30x50 px), MSB nibble first, white on the black
* background depthcharge left. Readable in any panel orientation.
* Line 0 is the marker 0xC0DE00xx identifying the failure stage.
*/
#define DIAG_FB_WIDTH 1200 /* menu geometry, 32bpp xRGB */
#define DIAG_DIGIT_SCALE 10 /* 3x5 font -> 30x50 px */
static const u16 hexfont[16] = {
0b111101101101111, /* 0 */
0b010110010010111, /* 1 */
0b111001111100111, /* 2 */
0b111001011001111, /* 3 */
0b101101111001001, /* 4 */
0b111100111001111, /* 5 */
0b111100111101111, /* 6 */
0b111001001010010, /* 7 */
0b111101111101111, /* 8 */
0b111101111001111, /* 9 */
0b111101111101101, /* A */
0b100100111101111, /* b */
0b111100100100111, /* C */
0b001001111101111, /* d */
0b111100111100111, /* E */
0b111100111100100, /* F */
};
static void fill_rect(u32 base, u32 x, u32 y, u32 w, u32 h, u32 c)
{
volatile u32 *fb = (volatile u32 *)(u64)base;
for (u32 yy = 0; yy < h; yy++) {
volatile u32 *row = fb + (u64)(y + yy) * DIAG_FB_WIDTH + x;
for (u32 xx = 0; xx < w; xx++)
row[xx] = c;
}
}
static void render_diag(u32 base, const u32 *d, int n)
{
const u32 S = DIAG_DIGIT_SCALE;
for (int i = 0; i < n; i++) {
u32 y0 = 30 + (u32)i * 70;
if (y0 + 50 > 1920)
break;
for (u32 j = 0; j < 8; j++) {
u32 nib = (d[i] >> (28 - 4 * j)) & 0xF;
u16 g = hexfont[nib];
u32 x0 = 40 + j * 4 * S;
for (u32 gy = 0; gy < 5; gy++)
for (u32 gx = 0; gx < 3; gx++)
if ((g >> (14 - gy * 3 - gx)) & 1)
fill_rect(base,
x0 + gx * S,
y0 + gy * S,
S, S, 0x00FFFFFFu);
}
}
}
static void fail_dump(u32 marker, const u32 *words, int n)
{
u32 diag[16];
for (int i = 0; i < 16; i++)
diag[i] = 0;
diag[0] = marker;
for (int i = 0; i < n && i < 14; i++)
diag[1 + i] = words[i];
blink_backlight(5); /* "I ran" signal */
u32 l0 = read_scanout_addr();
if (plausible_fb(l0))
diag[15] = l0;
render_diag(l0, diag, 16);
halt();
}
static void checkpoint(const struct fbinfo *f, int r, int g, int b)
{
fill_screen(f, mkcolor(f, r, g, b));
delay_ms(2000);
}
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)) {
/* Dump the DTB header words + the x0 pointer itself. */
const volatile u32 *dt = (const volatile u32 *)dtb;
u32 w[6] = {
(u32)dtb, (u32)(dtb >> 32),
dt[0], dt[1], dt[2], dt[3],
};
fail_dump(0xC0DE0001u, w, 6);
}
u32 l0 = read_scanout_addr();
int rc = find_framebuffer(lbio_addr, &fb);
if (rc) {
/* Dump the LBIO location + raw table header + raw words at
* several offsets + the live scanout address. */
const volatile u32 *t = (const volatile u32 *)lbio_addr;
u32 w[13] = {
(u32)lbio_addr, (u32)(lbio_addr >> 32), lbio_size,
t[0], t[1], t[5], /* magic, header_bytes, entries */
t[6], t[7], /* rec0 tag, size (if hb=24) */
t[8], t[9], /* raw words at +32, +36 */
t[16], t[17], /* raw words at +64, +68 */
l0,
};
fail_dump(0xC0DE0002u | (u32)rc, w, 13);
}
/* The LBIO record may not carry the real scanout address; the OVL
* L0_ADDR register does (depthcharge programmed it for the menu). */
if (!fb.pa && plausible_fb(l0))
fb.pa = l0;
/* 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 */
}