Round 9, hex dump decoded:
x0 = 0x5F800000 (= _fit_fdt_start, depthcharge's own FDT
buffer; the kernel reserves 5f800000-5f815fff for it)
dt[0..3] = all zeros — no FDT magic at payload entry
OVL_L0_ADDR = 0xFD536000 (real scanout, top-of-DRAM reserved region)
The device-era boot path flattens the fixed-up tree into
_fit_fdt_start and hands off that pointer. Why the kernel sees a valid
FDT there while the payload sees zeros is open (leading suspicion:
cache flush behavior differing with the ~30 MB pmOS kernel vs the
18 KB stub).
For the stub this is moot: the coreboot table address is a memlayout
constant on this board (0xffed9000, confirmed by the kernel's coreboot
driver, sysfs tags and /sys/firmware/fdt). find_coreboot_reg failure
now falls back to it instead of halting; the stub no longer depends on
the DTB. find_framebuffer failure still dumps the raw LBIO bytes.
Payload adbea06f... flashed and verified. host_test and qemu_test pass.
462 lines
14 KiB
C
462 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);
|
|
}
|
|
|
|
#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 */
|
|
}
|