/* * 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); } #define LBIO_FALLBACK_ADDR 0xffed9000ull /* coreboot memlayout, fixed */ #define LBIO_FALLBACK_SIZE 0x380 void cmain(u64 dtb) { struct fbinfo fb; u64 lbio_addr; u32 lbio_size; stage0(); /* * NOTE: on this device the FDT buffer depthcharge hands off (x0 = * _fit_fdt_start = 0x5F800000) reads as all zeros at payload entry, * so the DTB parse fails (RESEARCH.md Round 9). The coreboot table * address, however, is fixed by coreboot's memlayout and confirmed * by the running kernel (sysfs + /sys/firmware/fdt), so fall back * to it instead of relying on the DTB. */ if (find_coreboot_reg((const void *)dtb, &lbio_addr, &lbio_size)) { lbio_addr = LBIO_FALLBACK_ADDR; lbio_size = LBIO_FALLBACK_SIZE; } 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 */ }