On 8/26/26 8:09 AM, Julien Stephan wrote:
> Add a UCLASS_VIDEO driver for the HDMI TX v2 IP found on the MT8188.
>
> On probe, the driver waits for a plugged and powered display, reads
> its EDID through the DDC bus to select the preferred mode, configures
> the HDMI controller and PHY accordingly, and then programs all the
> components of the vdosys1 pipeline (MDP RDMA, padding, VPP merge,
> ETHDR mixer, mutex, DPI1) to scan out the framebuffer. The
> framebuffer lives in a dedicated reserved-memory region referenced by
> the "memory-region" property of the hdmi node.
>
> Signed-off-by: Pavlo Yadvychuk <[email protected]>
> Signed-off-by: Julien Stephan <[email protected]>
> ---
> drivers/video/Kconfig | 1 +
> drivers/video/Makefile | 1 +
> drivers/video/mediatek/Kconfig | 21 +
> drivers/video/mediatek/Makefile | 16 +
> drivers/video/mediatek/mtk_hdmi.c | 1102
> +++++++++++++++++++++++++++++++++++++
> 5 files changed, 1141 insertions(+)
>
> diff --git a/drivers/video/Kconfig b/drivers/video/Kconfig
> index 15000e21840..cb3ba6a6f63 100644
> --- a/drivers/video/Kconfig
> +++ b/drivers/video/Kconfig
> @@ -770,6 +770,7 @@ config VIDEO_LCD_SPI_MISO
> option takes a string in the format understood by 'sunxi_name_to_gpio'
> function, e.g. PH1 for pin 1 of port H.
>
> +source "drivers/video/mediatek/Kconfig"
> source "drivers/video/meson/Kconfig"
>
> config VIDEO_MVEBU
> diff --git a/drivers/video/Makefile b/drivers/video/Makefile
> index 082b8967982..a20011b9b17 100644
> --- a/drivers/video/Makefile
> +++ b/drivers/video/Makefile
> @@ -92,5 +92,6 @@ obj-$(CONFIG_VIDEO_SEPS525) += seps525.o
> obj-$(CONFIG_VIDEO_ZYNQMP_DPSUB) += zynqmp/
>
> obj-y += bridge/
> +obj-y += mediatek/
> obj-y += sunxi/
> obj-y += tegra/
> diff --git a/drivers/video/mediatek/Kconfig b/drivers/video/mediatek/Kconfig
> new file mode 100644
> index 00000000000..54cf1b11fdf
> --- /dev/null
> +++ b/drivers/video/mediatek/Kconfig
> @@ -0,0 +1,21 @@
> +# SPDX-License-Identifier: GPL-2.0+
> +#
> +# Copyright (C) 2026 BayLibre, SAS
> +#
> +# Author: Julien Stephan <[email protected]>
> +
> +config VIDEO_MEDIATEK_HDMI
> + bool "Video HDMI Support for Mediatek SoCs"
> + depends on VIDEO && ARCH_MEDIATEK
> + depends on DM_I2C && PHY && POWER_DOMAIN
> + select I2C_EDID
> + select MISC
> + select PHY_MTK_HDMI
> + select MTK_POWER_DOMAIN
> + select SYS_I2C_DDC_MTK
> + help
> + Enable support for the HDMI TX output found on recent MediaTek
> + SoCs such as the MT8188. The driver reads the EDID of the
> + connected display, selects its preferred mode and drives the
> + vdosys1 display pipeline (MDP RDMA, padding, merge, ETHDR
> + mixer, DPI) to scan out the U-Boot framebuffer over HDMI.
> diff --git a/drivers/video/mediatek/Makefile b/drivers/video/mediatek/Makefile
> new file mode 100644
> index 00000000000..70da9d9662a
> --- /dev/null
> +++ b/drivers/video/mediatek/Makefile
> @@ -0,0 +1,16 @@
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Copyright (c) 2026 MediaTek Inc.
> +#
> +# Author: Julien Stephan <[email protected]>
> +
> +obj-$(CONFIG_VIDEO_MEDIATEK_HDMI) += \
> + mtk_hdmi.o \
> + mtk_disp_comp.o \
> + mtk_dpi.o \
> + mtk_mdp_rdma.o \
> + mtk_disp_merge.o \
> + mtk_ethdr.o \
> + mtk_disp_mutex.o \
> + mtk_disp_padding.o \
> + mtk_smi_larb.o
> diff --git a/drivers/video/mediatek/mtk_hdmi.c
> b/drivers/video/mediatek/mtk_hdmi.c
> new file mode 100644
> index 00000000000..a37fe44b295
> --- /dev/null
> +++ b/drivers/video/mediatek/mtk_hdmi.c
> @@ -0,0 +1,1102 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (c) 2026 BayLibre, SAS
> + * Author: Julien Stephan <[email protected]>
> + */
> +
> +#include <asm/io.h>
> +#include <asm/system.h>
Still used?
> +#include <asm/unaligned.h>
> +#include <clk.h>
> +#include <dm.h>
> +#include <dm/device_compat.h>
> +#include <dm/ofnode.h>
> +#include <edid.h>
> +#include <errno.h>
> +#include <generic-phy.h>
> +#include <i2c.h>
> +#include <linux/delay.h>
> +#include <linux/hdmi.h>
> +#include <video.h>
> +
> +#include "mtk_disp_comp.h"
> +#include "mtk_disp_merge.h"
> +#include "mtk_ethdr.h"
> +#include "mtk_disp_mutex.h"
> +#include "mtk_disp_padding.h"
> +#include "mtk_dpi.h"
> +#include "mtk_mdp_rdma.h"
> +
> +/* HDMI TX register definitions */
> +#define AVI_DIS (0)
> +#define AVI_DIS_WR (0)
> +#define AVI_EN (0x1)
> +#define AVI_EN_WR BIT(16)
> +#define AVI_RPT_DIS (0x0)
> +#define AVI_RPT_EN (0x1)
> +
> +#define C422_C420_CONFIG_BYPASS BIT(5)
> +#define C422_C420_CONFIG_ENABLE BIT(4)
> +#define C422_C420_CONFIG_OUT_CB_OR_CR BIT(6)
> +#define C444_C422_CONFIG_ENABLE (0x1)
> +
> +#define DEEPCOLOR_MODE_10BIT BIT(8)
> +#define DEEPCOLOR_MODE_12BIT GENMASK(9, 8)
> +#define DEEPCOLOR_MODE_16BIT GENMASK(10, 8)
> +#define DEEPCOLOR_MODE_8BIT (0)
These are confusing and don't quite match linux. Should be a field with
GENMASK(9, 8) and values 0, 1, 2, 3.
> +#define DEEPCOLOR_MODE_MASKBIT GENMASK(10, 8)
> +#define DEEPCOLOR_PAT_EN BIT(12)
> +#define DEEP_COLOR_ADD BIT(4)
> +
> +#define HDMI2_OFF (0)
> +#define HDMI2_ON BIT(2)
> +
> +#define HDMITX_SW_HPD BIT(29)
> +#define HDMITX_SW_RSTB BIT(31)
> +
> +#define HDMI_MODE_DVI (0)
> +#define HDMI_MODE_HDMI BIT(3)
> +
> +#define HDMI_YUV420_MODE BIT(10)
> +
> +#define HPD_DDC_STATUS 0xC60
> +#define PORD_PIN_STA BIT(5)
> +#define HPD_PIN_STA BIT(4)
> +
> +#define NULL_PKT_EN BIT(2)
> +#define NULL_PKT_VSYNC_HIGH_EN BIT(3)
> +
> +#define OUTPUT_FORMAT_DEMUX_420_ENABLE BIT(10)
> +
> +#define REG_VMUTE_EN BIT(16)
> +
> +#define SCR_OFF 0
> +#define SCR_ON BIT(4)
> +
> +#define SPD_DIS 0
> +#define SPD_DIS_WR 0
> +#define SPD_EN BIT(1)
> +#define SPD_EN_WR BIT(17)
> +#define SPD_RPT_DIS 0
> +#define SPD_RPT_EN BIT(1)
> +
> +#define TOP_CFG00 0x000
> +#define TOP_CFG01 0x004
> +#define TOP_INFO_EN 0x01C
> +#define TOP_INFO_RPT 0x020
> +#define TOP_AVI_HEADER 0x024
> +#define TOP_AVI_PKT00 0x028
> +#define TOP_AVI_PKT01 0x02C
> +#define TOP_AVI_PKT02 0x030
> +#define TOP_AVI_PKT03 0x034
> +#define TOP_AVI_PKT04 0x038
> +#define TOP_AVI_PKT05 0x03C
> +#define TOP_INT_MASK00 0x1B0
> +#define TOP_INT_MASK01 0x1B4
These MASK names don't match Linux.
> +#define TOP_MISC_CTLR 0x1A4
> +#define TOP_SPDIF_HEADER 0x054
> +#define TOP_SPDIF_PKT00 0x058
> +#define TOP_SPDIF_PKT01 0x05C
> +#define TOP_SPDIF_PKT02 0x060
> +#define TOP_SPDIF_PKT03 0x064
> +#define TOP_SPDIF_PKT04 0x068
> +#define TOP_SPDIF_PKT05 0x06C
> +#define TOP_SPDIF_PKT06 0x070
> +#define TOP_SPDIF_PKT07 0x074
> +#define TOP_VMUTE_CFG1 0x1C8
> +
> +#define VID_DOWNSAMPLE_CONFIG 0x8F0
This doesn't match upstream Linux (but does match downstream). We should
confirm which is correct.
> +#define VID_OUT_FORMAT 0x8FC
> +
> +#define ABIST_ENABLE BIT(31)
> +
> +#define HDCP_TOP_CTRL 0xC00
> +#define HDMI_CONFIG 0xEA0
> +
...
> +struct mtk_hdmi {
> + struct udevice *dev;
> + struct udevice *ddc_bus;
> + struct udevice *dpi1;
> + struct udevice *merge3;
> + struct udevice *merge5;
> + struct udevice *ethdr;
> + struct udevice *mutex;
> + struct udevice *padding4;
> + struct udevice *padding5;
> + struct udevice *rdma4;
> + struct udevice *rdma5;
> + struct udevice *larb2;
> + struct udevice *larb3;
> + struct phy phy;
> + fdt_addr_t regs;
Should be `void __iomem *`?
> + struct clk_bulk clk_bulk;
> + u64 support_csp_depth;
Unused field.
> + u64 set_csp_depth;
> + enum hdmi_colorspace csp;
> + enum hdmi_color_depth color_depth;
> + enum hdmi_colorimetry colorimetry;
> + struct display_timing mode;
> +};
> +
...
> +static void mtk_hdmi_hw_vid_black(struct mtk_hdmi *hdmi, bool black)
> +{
> + if (black)
> + mtk_hdmi_update(hdmi, TOP_VMUTE_CFG1, REG_VMUTE_EN,
> REG_VMUTE_EN);
> + else
> + mtk_hdmi_update(hdmi, TOP_VMUTE_CFG1, 0, REG_VMUTE_EN);
Can we simplify this like mtk_hdmi_enable_hdmi_mode()?
There are a couple more cases like this below too.
> +}
> +
...
> +static inline void mtk_hdmi_enable_scrambling(struct mtk_hdmi *hdmi,
> + bool enable)
> +{
> + udelay(150);
Odd to have a delay before doing something.
Should this be moved to mtk_hdmi_change_video_resolution()?
> +
> + if (enable)
> + mtk_hdmi_update(hdmi, TOP_CFG00, SCR_ON | HDMI2_ON,
> + SCR_ON | HDMI2_ON);
> + else
> + mtk_hdmi_update(hdmi, TOP_CFG00, SCR_OFF | HDMI2_OFF,
> + SCR_ON | HDMI2_ON);
> +}
> +
...
> +static void mtk_hdmi_output_set_display_mode(struct mtk_hdmi *hdmi,
> + struct display_timing *mode)
> +{
> + unsigned long link_rate = mode->pixelclock.typ;
> + int ret;
> +
> + ret = generic_phy_configure(&hdmi->phy, &link_rate);
> + if (ret)
> + dev_err(hdmi->dev, "Setting clock=%u failed: %d\n",
> + mode->pixelclock.typ, ret);
Should propagate error or add comment to explain why it is safe to ignore.
> +
> + mtk_hdmi_change_video_resolution(hdmi);
> +}
> +
...
> +static int mtk_hdmi_setup_avi_infoframe(struct mtk_hdmi *hdmi, u8 *buffer,
> + size_t bufsz,
> + struct display_timing *mode)
> +{
> + u8 checksum;
> + int i;
> +
> + if (bufsz < HDMI_AVI_INFOFRAME_SIZE)
> + return -EINVAL;
> +
> + memset(buffer, 0, HDMI_AVI_INFOFRAME_SIZE);
> +
> + /* AVI InfoFrame header */
> + buffer[0] = 0x82; /* AVI InfoFrame type */
> + buffer[1] = 0x02; /* Version */
> + buffer[2] = 0x0D; /* Length (13 bytes) */
> +
> + /* Data byte 1: Scan info, bar info, active format info, RGB/YCC */
> + switch (hdmi->csp) {
> + case HDMI_COLORSPACE_YUV422:
> + buffer[4] = 0x20;
> + break;
> + case HDMI_COLORSPACE_YUV444:
> + buffer[4] = 0x40;
> + break;
> + case HDMI_COLORSPACE_YUV420:
> + buffer[4] = 0x60;
> + break;
> + default:
> + buffer[4] = 0x00;
> + break;
> + }
> +
> + /* Data byte 4: Video Identification Code (VIC) */
> + if (mode->hactive.typ == 1920 && mode->vactive.typ == 1080)
> + buffer[7] = 16; /* 1920x1080@60Hz */
> + else if (mode->hactive.typ == 1280 && mode->vactive.typ == 720)
> + buffer[7] = 4; /* 1280x720@60Hz */
> + else if (mode->hactive.typ == 720 && mode->vactive.typ == 480)
> + buffer[7] = 2; /* 720x480@60Hz */
> + else
> + buffer[7] = 0; /* Unknown/unsupported timing */
Should we also be validating the refresh rate here instead of assuming 60Hz?
> +
> + /*
> + * Data byte 2: picture aspect ratio, active portion same as
> + * picture. VIC 2 is a 4:3 mode, the others are 16:9.
> + */
> + buffer[5] = buffer[7] == 2 ? 0x18 : 0x28;
> +
> + /* Data byte 3: Colorimetry, picture scaling */
> + switch (hdmi->colorimetry) {
> + case HDMI_COLORIMETRY_ITU_709:
> + buffer[6] = 0x80;
> + break;
> + case HDMI_COLORIMETRY_ITU_601:
> + buffer[6] = 0x40;
> + break;
> + default:
> + buffer[6] = 0x00;
> + break;
> + }
> +
> + /* Data byte 5: Pixel repetition */
> + buffer[8] = 0x00; /* No pixel repetition */
> +
> + /* checksum over the full frame, so that the total sums to zero */
> + for (checksum = 0, i = 0; i < HDMI_AVI_INFOFRAME_SIZE; i++)
> + checksum += buffer[i];
> + buffer[3] = 0x100 - checksum;
> +
> + return 0;
> +}
> +
> +static void mtk_hdmi_hw_avi_infoframe(struct mtk_hdmi *hdmi, u8 *buf, u8 len)
> +{
len is unused.
> + /* Disable AVI InfoFrame first */
> + mtk_hdmi_update(hdmi, TOP_INFO_EN, AVI_DIS_WR | AVI_DIS,
> + AVI_EN_WR | AVI_EN);
> + mtk_hdmi_update(hdmi, TOP_INFO_RPT, AVI_RPT_DIS, AVI_RPT_EN);
> +
> + /* Write AVI InfoFrame header */
> + mtk_hdmi_write(hdmi, TOP_AVI_HEADER, get_unaligned_le24(&buf[0]));
> +
> + /* Write AVI InfoFrame data packets */
> + mtk_hdmi_write(hdmi, TOP_AVI_PKT00, get_unaligned_le32(&buf[3]));
> + mtk_hdmi_write(hdmi, TOP_AVI_PKT01, get_unaligned_le24(&buf[7]));
> + mtk_hdmi_write(hdmi, TOP_AVI_PKT02, get_unaligned_le32(&buf[10]));
> + mtk_hdmi_write(hdmi, TOP_AVI_PKT03, get_unaligned_le24(&buf[14]));
> +
> + /* Clear remaining packets */
> + mtk_hdmi_write(hdmi, TOP_AVI_PKT04, 0);
> + mtk_hdmi_write(hdmi, TOP_AVI_PKT05, 0);
> +
> + /* Enable AVI InfoFrame */
> + mtk_hdmi_update(hdmi, TOP_INFO_RPT, AVI_RPT_EN, AVI_RPT_EN);
> + mtk_hdmi_update(hdmi, TOP_INFO_EN, AVI_EN_WR | AVI_EN,
> + AVI_EN_WR | AVI_EN);
> +}
> +
> +static void mtk_hdmi_hw_spd_infoframe(struct mtk_hdmi *hdmi, u8 *buf, u8 len)
> +{
ditto
> + /* Disable SPD InfoFrame first */
> + mtk_hdmi_update(hdmi, TOP_INFO_EN, SPD_DIS_WR | SPD_DIS,
> + SPD_EN_WR | SPD_EN);
> + mtk_hdmi_update(hdmi, TOP_INFO_RPT, SPD_RPT_DIS, SPD_RPT_EN);
> +
> + /* Write SPD InfoFrame header */
> + mtk_hdmi_write(hdmi, TOP_SPDIF_HEADER, get_unaligned_le24(&buf[0]));
> +
> + /* Write SPD InfoFrame data packets */
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT00, get_unaligned_le32(&buf[3]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT01, get_unaligned_le24(&buf[7]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT02, get_unaligned_le32(&buf[10]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT03, get_unaligned_le24(&buf[14]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT04, get_unaligned_le32(&buf[17]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT05, get_unaligned_le24(&buf[21]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT06, get_unaligned_le32(&buf[24]));
> + mtk_hdmi_write(hdmi, TOP_SPDIF_PKT07, buf[28]);
> +
> + /* Enable SPD InfoFrame */
> + mtk_hdmi_update(hdmi, TOP_INFO_RPT, SPD_RPT_EN, SPD_RPT_EN);
> + mtk_hdmi_update(hdmi, TOP_INFO_EN, SPD_EN_WR | SPD_EN,
> + SPD_EN_WR | SPD_EN);
> +}
> +
...
> +static int mtk_hdmi_read_edid(struct mtk_hdmi *hdmi, u8 *buf, int buf_size)
> +{
> + struct udevice *chip;
> + int ret;
> +
> + if (!hdmi->ddc_bus || buf_size < EDID_SIZE)
> + return -EINVAL;
> +
> + ret = i2c_get_chip(hdmi->ddc_bus, EDID_ADDR, 1, &chip);
> + if (ret)
> + return ret;
> +
> + ret = dm_i2c_read(chip, 0, buf, EDID_SIZE);
> + if (ret) {
> + dev_err(hdmi->dev, "failed to read EDID: %d\n", ret);
> + return ret;
> + }
> +
> + /* read the extension block, if any */
> + if (buf[0x7e] != 0 && buf_size >= EDID_EXT_SIZE) {
0x7e should probably be a offsetof() or cast to struct edid1_info * and use
the actual field.
> + ret = dm_i2c_read(chip, EDID_SIZE, buf + EDID_SIZE,
> + buf_size - EDID_SIZE);
> + if (ret)
> + dev_warn(hdmi->dev,
> + "error reading extended EDID block\n");
> + }
> +
> + return 0;
> +}
> +
...
> +static int mtk_hdmi_get_comp_by_alias(struct udevice *dev, const char *alias,
> + struct udevice **compp)
> +{
> + struct udevice *comp;
> + ofnode node;
> + int ret;
> +
> + node = ofnode_get_aliases_node(alias);
> + if (!ofnode_valid(node)) {
> + dev_err(dev, "cannot find alias %s\n", alias);
> + return -ENODEV;
> + }
> +
> + ret = uclass_get_device_by_ofnode(UCLASS_MISC, node, &comp);
> + if (ret) {
> + dev_err(dev, "cannot get %s: %d\n", alias, ret);
> + return ret;
> + }
> +
> + ret = mtk_disp_comp_enable(comp);
> + if (ret)
> + return ret;
> +
> + /* only publish the handle once its clocks are on, so the error */
> + /* unwind can rely on a non-NULL pointer meaning "enabled" */
nit: change to single comment
> + *compp = comp;
> +
> + return 0;
> +}
> +