video: add MT8183 scanout driver for firmware-initialized displays

Add a display driver for the MT8183 kukui family of ChromeOS devices
(e.g. the Lenovo IdeaPad Duet, google,krane), where the display
pipeline is already initialized and running when the payload is
handed off by depthcharge. depthcharge's display_cleanup() stops the
overlay engines and turns the backlight off before the jump, so the
driver only revives the pipeline: it re-enables the OVL0 engines
(OVL_EN @ +0x000c and OVL0_2L_EN @ +0x100c, offsets from depthcharge's
device-era mtk_ddp.c), drives the backlight GPIOs high (DISP_PWM,
pin 43, and EN_LCD_BL, pin 176, through the GPIO controller dout block
at 0x10005000+0x100), and renders the console into the live scanout
surface.

The geometry comes from the coreboot table framebuffer record, which
sits at the fixed kukui address 0xffed9000; when its physical_address
is 0 (what upstream coreboot 4.14 publishes on kukui), the live
scanout address is taken from the OVL layer 0 address register
(+0x0f40), which the firmware leaves in place across the handoff.

The driver binds to the existing ovl0 device tree node
(mediatek,mt8183-disp-ovl) and requires no display initialization.
This commit is contained in:
vhaudiquet
2026-08-30 01:27:34 +02:00
parent 923cc75439
commit f0470c9be5
3 changed files with 237 additions and 0 deletions
+13
View File
@@ -1022,6 +1022,19 @@ config VIDEO_MCDE_SIMPLE
before u-boot starts, and u-boot will simply render to the pre-
allocated frame buffer surface.
config VIDEO_MT8183_SCANOUT
bool "Enable MT8183 scanout driver for firmware-initialized displays"
depends on ARCH_MEDIATEK
help
Enables a display driver for the MediaTek MT8183 on boards where
the display pipeline has already been initialized by the boot
firmware (e.g. ChromeOS coreboot + depthcharge on the kukui
family). The driver revives the overlay engines and backlight that
the firmware disables before handoff, discovers the live scanout
surface from the coreboot table (with a fallback to the overlay
layer address register), and lets the standard vidconsole render
into it. No display initialization is performed.
config OSD
bool "Enable OSD support"
depends on DM
+1
View File
@@ -80,6 +80,7 @@ obj-$(CONFIG_VIDEO_LCD_SAMSUNG_S6E63M0) += samsung-s6e63m0.o
obj-$(CONFIG_VIDEO_MCDE_SIMPLE) += mcde_simple.o
obj-${CONFIG_VIDEO_MESON} += meson/
obj-${CONFIG_VIDEO_MIPI_DSI} += mipi_dsi.o
obj-$(CONFIG_VIDEO_MT8183_SCANOUT) += mt8183_scanout.o
obj-$(CONFIG_VIDEO_MVEBU) += mvebu_lcd.o
obj-$(CONFIG_VIDEO_MXS) += mxsfb.o videomodes.o
obj-$(CONFIG_VIDEO_NX) += nexell_display.o videomodes.o nexell/
+223
View File
@@ -0,0 +1,223 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* MT8183 display scanout driver for firmware-initialized pipelines.
*
* On the MT8183 kukui family of ChromeOS devices (e.g. the Lenovo IdeaPad
* Duet, "google,krane"), the display pipeline (MMSYS -> OVL0 -> OVL0_2L ->
* RDMA -> COLOR -> DSI -> panel) is fully initialized and running by the
* time control is passed to the next boot stage: coreboot (through
* libpayload/depthcharge) sets up the panel and programs the overlay scanout
* address. The payload, however, is handed over with the pipeline STOPPED:
* depthcharge's display_cleanup() disables the overlay engines and turns the
* backlight off right before jumping to the payload.
*
* This driver does not initialize any display hardware. It only revives the
* pipeline (re-enabling the overlay engines and the backlight), discovers
* the geometry and the live scanout address from the coreboot table
* framebuffer record (falling back to the OVL L0 layer address register,
* which still holds the address depthcharge used), and hands the surface to
* the video uclass so the standard vidconsole can render into it.
*/
#include <dm.h>
#include <video.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/unaligned.h>
#include <linux/kernel.h>
#include <linux/sizes.h>
/*
* OVL0 register offsets. Source: Linux drivers/gpu/drm/mediatek/
* mtk_disp_ovl.c (DISP_REG_OVL_EN) and device-era depthcharge
* src/drivers/video/mtk_ddp.c (ChromeOS R93, the generation shipped on
* kukui); both agree on 0x000c for OVL_EN. The 2L sub-engine enable
* (DISP_REG_OVL0_2L_EN) lives in the same register block on MT8183.
* The layer-0 source address register (DISP_REG_OVL_L0_ADDR) is not reset
* by depthcharge's stop() and holds the live scanout address.
*/
#define DISP_REG_OVL_L0_ADDR 0x0f40
#define DISP_REG_OVL_EN 0x000c
#define DISP_REG_OVL0_2L_EN 0x100c
/*
* Backlight GPIOs. The MT8183 GPIO controller is at 0x10005000; the dout
* block starts at +0x100 with 16 bytes per 32-pin group and set@+4 (layout
* of GpioRegs/GpioValRegs in device-era depthcharge src/drivers/gpio/
* mt8183.h). On kukui the backlight is driven by two dedicated GPIOs:
* DISP_PWM (pin 43) and EN_LCD_BL (PERIPHERAL_EN13, pin 176); depthcharge's
* kukui_backlight_update() drives both high to turn the backlight on.
*/
#define MTK_GPIO_BASE 0x10005000
#define MTK_GPIO_DOUT_SET(pin) (MTK_GPIO_BASE + 0x100 + ((pin) / 32) * 16 + 4)
#define MTK_GPIO_DOUT_BIT(pin) BIT((pin) % 32)
#define PAD_DISP_PWM 43
#define PAD_EN_LCD_BL 176
/*
* The coreboot table sits at a fixed address on kukui: 0xffed9000, size
* 0x380 (coreboot memlayout; confirmed on the device through
* /sys/firmware/fdt and the coreboot sysfs tags). It is deliberately
* hardcoded here rather than read from the handoff DTB: on this platform
* the FDT pointer passed at entry cannot be relied upon, and U-Boot boots
* with its own embedded control DTB which has no /firmware/coreboot node.
*/
#define COREBOOT_TABLE_ADDR 0xffed9000
#define LB_TAG_FRAMEBUFFER 0x12
/*
* Layout from coreboot src/commonlib/include/commonlib/coreboot_tables.h:
* 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 (record payload):
* physical_address @8 (4-byte-aligned LE u64), 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; size 40.
*
* physical_address == 0 is legitimate: upstream coreboot 4.14 (the
* generation shipped on kukui) publishes the framebuffer record with
* fb_addr=0. The live scanout address then comes from OVL_L0_ADDR.
*/
struct lb_framebuffer {
u32 tag;
u32 size;
u64 physical_address;
u32 x_resolution;
u32 y_resolution;
u32 bytes_per_line;
u8 bits_per_pixel;
u8 red_pos;
u8 red_size;
u8 green_pos;
u8 green_size;
u8 blue_pos;
u8 blue_size;
u8 reserved_pos;
u8 reserved_size;
};
static int find_framebuffer(u64 table, struct lb_framebuffer *fb)
{
void *base = (void *)(uintptr_t)table;
u32 header_bytes, entries, i;
void *rec;
if (get_unaligned_le32(base) != 0x4f49424c) /* "LBIO" */
return -ENOENT;
header_bytes = get_unaligned_le32(base + 4);
entries = get_unaligned_le32(base + 20);
if (header_bytes < 24 || header_bytes > 4096 ||
entries == 0 || entries > 4096)
return -EINVAL;
rec = base + header_bytes;
for (i = 0; i < entries; i++) {
u32 tag = get_unaligned_le32(rec);
u32 rsize = get_unaligned_le32(rec + 4);
if (rsize < 8)
return -EINVAL;
if (tag == LB_TAG_FRAMEBUFFER) {
if (rsize < sizeof(*fb))
return -EINVAL;
memcpy(fb, rec, sizeof(*fb));
fb->physical_address =
get_unaligned_le64(rec + 8);
return 0;
}
rec += rsize;
}
return -ENOENT;
}
static int mt8183_scanout_probe(struct udevice *dev)
{
struct video_uc_plat *plat = dev_get_uclass_plat(dev);
struct video_priv *uc_priv = dev_get_uclass_priv(dev);
struct lb_framebuffer fb;
fdt_addr_t ovl;
u64 addr;
int ret;
ovl = dev_read_addr(dev);
if (ovl == FDT_ADDR_T_NONE)
return log_msg_ret("ovl", -EINVAL);
/*
* Revive the pipeline: undo depthcharge's display_cleanup() by
* re-enabling the overlay engines and driving the backlight GPIOs
* high. No panel or DSI re-initialization is needed: the panel is
* powered and the DSI link stays up through the handoff.
*/
writel(1, ovl + DISP_REG_OVL_EN);
writel(1, ovl + DISP_REG_OVL0_2L_EN);
writel(MTK_GPIO_DOUT_BIT(PAD_DISP_PWM),
(void __iomem *)MTK_GPIO_DOUT_SET(PAD_DISP_PWM));
writel(MTK_GPIO_DOUT_BIT(PAD_EN_LCD_BL),
(void __iomem *)MTK_GPIO_DOUT_SET(PAD_EN_LCD_BL));
/*
* The coreboot table sits above the DRAM window described by the
* control DTB; map it before parsing.
*/
mmu_map_region(COREBOOT_TABLE_ADDR, SZ_4K, false);
ret = find_framebuffer(COREBOOT_TABLE_ADDR, &fb);
if (ret)
return log_msg_ret("lbio", ret);
if (fb.bits_per_pixel != 32 || fb.red_pos != 16 || fb.red_size != 8 ||
fb.green_pos != 8 || fb.green_size != 8 ||
fb.blue_pos != 0 || fb.blue_size != 8)
return log_msg_ret("fmt", -ENOTSUPP);
/*
* Use the address from the coreboot table when it is a plausible
* DRAM address (>= 1 GiB), otherwise fall back to the address the
* firmware actually programmed into the overlay, which it left in
* place across the handoff.
*/
addr = fb.physical_address;
if (addr < SZ_1G)
addr = readl(ovl + DISP_REG_OVL_L0_ADDR);
if (addr < SZ_1G)
return log_msg_ret("scanout", -ENODEV);
plat->base = addr;
plat->size = fb.bytes_per_line * fb.y_resolution;
/* The scanout surface is above the DTB DRAM window: map it. */
mmu_map_region(plat->base, ALIGN(plat->size, SZ_4K), false);
video_set_flush_dcache(dev, true);
uc_priv->bpix = VIDEO_BPP32;
uc_priv->xsize = fb.x_resolution;
uc_priv->ysize = fb.y_resolution;
uc_priv->line_length = fb.bytes_per_line;
printf("Video: MT8183 scanout %dx%d@32bpp at %llx\n",
uc_priv->xsize, uc_priv->ysize, (unsigned long long)plat->base);
return 0;
}
static const struct udevice_id mt8183_scanout_ids[] = {
{ .compatible = "mediatek,mt8183-disp-ovl" },
{ }
};
U_BOOT_DRIVER(mt8183_scanout) = {
.name = "mt8183_scanout",
.id = UCLASS_VIDEO,
.of_match = mt8183_scanout_ids,
.probe = mt8183_scanout_probe,
};