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).
357 lines
11 KiB
C
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 */
|
|
}
|