[PATCH 00/10] Rockchip VOP2 support

This series adds VOP2 support to U-Boot and enable it for PineTab2.
HDMI should work flawlessly but there's still some timings issue with DW MIPI DSI, perhaps that's something to look into later.
Signed-off-by: Dang Huynh danct12@riseup.net --- Dang Huynh (8): video: rockchip: dw-mipi-dsi: Depend on CONFIG_VIDEO_BRIDGE video: Add BOE TH101MB31IG002-28A MIPI-DSI panel video: rockchip: Add VOP2 support video: rockchip: vop2: Add video bridge support dts: rockchip: rk356x: Prerelocate VOP in U-Boot proper configs: quartz64: Enable vidconsole video: rockchip: Add HDMI support for RK3568 configs: pinetab2-rk3566: Enable video and USB keyboard
Ondrej Jirman (2): video: rockchip: dw_mipi_dsi: Improve pixel clock calculations video: rockchip: dw_mipi_dsi: Proceed when external PHY is not defined
arch/arm/dts/rk356x-u-boot.dtsi | 4 + arch/arm/include/asm/arch-rockchip/vop_rk3568.h | 249 ++++++++++++ configs/pinetab2-rk3566_defconfig | 15 + drivers/video/Kconfig | 10 + drivers/video/Makefile | 1 + drivers/video/boe-th101mb31ig002-28a.c | 231 +++++++++++ drivers/video/rockchip/Kconfig | 2 +- drivers/video/rockchip/Makefile | 4 +- drivers/video/rockchip/dw_mipi_dsi_rockchip.c | 26 +- drivers/video/rockchip/rk3568_hdmi.c | 63 +++ drivers/video/rockchip/rk3568_vop.c | 214 ++++++++++ drivers/video/rockchip/rk_vop2.c | 519 ++++++++++++++++++++++++ drivers/video/rockchip/rk_vop2.h | 58 +++ include/configs/quartz64_rk3566.h | 5 +- 14 files changed, 1383 insertions(+), 18 deletions(-) --- base-commit: 6d41f0a39d6423c8e57e92ebbe9f8c0333a63f72 change-id: 20250114-vop2-pt2-755fb1991bb6
Best regards,

The driver is in video bridge class, so we must depend on it or the driver will fail to init.
Signed-off-by: Dang Huynh danct12@riseup.net --- drivers/video/rockchip/Kconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/video/rockchip/Kconfig b/drivers/video/rockchip/Kconfig index 01804dcb1cc8743e738d32540bea6c8539e51ab4..72fcd6715a69e522f2ba6e8de9c1a923106ca10f 100644 --- a/drivers/video/rockchip/Kconfig +++ b/drivers/video/rockchip/Kconfig @@ -71,7 +71,7 @@ config DISPLAY_ROCKCHIP_MIPI
config DISPLAY_ROCKCHIP_DW_MIPI bool "Rockchip Designware MIPI" - depends on VIDEO_ROCKCHIP + depends on VIDEO_ROCKCHIP && VIDEO_BRIDGE select VIDEO_DW_MIPI_DSI help Select the Designware MIPI DSI controller in use on some Rockchip

From: Ondrej Jirman megi@xff.cz
Calculate burst mode overhead in one place for both internal and external PHY use case and exit if out of range, instead of ignoring the wrong value.
Signed-off-by: Ondrej Jirman megi@xff.cz Signed-off-by: Dang Huynh danct12@riseup.net --- drivers/video/rockchip/dw_mipi_dsi_rockchip.c | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-)
diff --git a/drivers/video/rockchip/dw_mipi_dsi_rockchip.c b/drivers/video/rockchip/dw_mipi_dsi_rockchip.c index fa512173510b1ee6f51e6269eb249e24d0e980f8..c47992dfb133cda029eba82e842ac824ceead64b 100644 --- a/drivers/video/rockchip/dw_mipi_dsi_rockchip.c +++ b/drivers/video/rockchip/dw_mipi_dsi_rockchip.c @@ -526,8 +526,6 @@ dw_mipi_dsi_get_lane_mbps(void *priv_data, struct display_timing *timings, struct udevice *dev = device->dev; struct dw_rockchip_dsi_priv *dsi = dev_get_priv(dev); int bpp; - unsigned long mpclk, tmp; - unsigned int target_mbps = 1000; unsigned int max_mbps = dppa_map[ARRAY_SIZE(dppa_map) - 1].max_mbps; unsigned long best_freq = 0; unsigned long fvco_min, fvco_max, fin, fout; @@ -544,30 +542,28 @@ dw_mipi_dsi_get_lane_mbps(void *priv_data, struct display_timing *timings, return bpp; }
- mpclk = DIV_ROUND_UP(timings->pixelclock.typ, 1000); - if (mpclk) { - /* take 1 / 0.8, since mbps must big than bandwidth of RGB */ - tmp = (mpclk * (bpp / lanes) * 10 / 8) / 1000; - if (tmp < max_mbps) - target_mbps = tmp; - else - dev_err(dsi->dsi_host, - "DPHY clock frequency is out of range\n"); + fout = timings->pixelclock.typ / MSEC_PER_SEC * bpp / lanes; + if (device->mode_flags & MIPI_DSI_MODE_VIDEO_BURST) + fout = fout * 12 / 10; + fout *= MSEC_PER_SEC; + + if (fout > max_mbps * USEC_PER_SEC) { + dev_err(dsi->dsi_host, "DPHY clock frequency is out of range\n"); + return -EINVAL; }
/* for external phy only the mipi_dphy_config is necessary */ if (generic_phy_valid(&dsi->phy)) { - phy_mipi_dphy_get_default_config(timings->pixelclock.typ * 10 / 8, + phy_mipi_dphy_get_default_config(fout / bpp * lanes, bpp, lanes, &dsi->phy_opts); - dsi->lane_mbps = target_mbps; + dsi->lane_mbps = DIV_ROUND_UP(fout, USEC_PER_SEC); *lane_mbps = dsi->lane_mbps;
return 0; }
fin = clk_get_rate(dsi->ref); - fout = target_mbps * USEC_PER_SEC;
/* constraint: 5Mhz <= Fref / N <= 40MHz */ min_prediv = DIV_ROUND_UP(fin, 40 * USEC_PER_SEC);

From: Ondrej Jirman megi@xff.cz
In this case the DM returns ENOENT, not ENODATA.
Signed-off-by: Ondrej Jirman megi@xff.cz Signed-off-by: Dang Huynh danct12@riseup.net --- drivers/video/rockchip/dw_mipi_dsi_rockchip.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/video/rockchip/dw_mipi_dsi_rockchip.c b/drivers/video/rockchip/dw_mipi_dsi_rockchip.c index c47992dfb133cda029eba82e842ac824ceead64b..95e825eb3d6de7ef2836fa029927034394486e9c 100644 --- a/drivers/video/rockchip/dw_mipi_dsi_rockchip.c +++ b/drivers/video/rockchip/dw_mipi_dsi_rockchip.c @@ -839,7 +839,7 @@ static int dw_mipi_dsi_rockchip_probe(struct udevice *dev) * NULL if it's not initialized. */ ret = generic_phy_get_by_name(dev, "dphy", &priv->phy); - if (ret && ret != -ENODATA) { + if (ret && ret != -ENOENT) { dev_err(dev, "failed to get mipi dphy: %d\n", ret); return ret; }

BOE TH101MB31IG002-28A is a MIPI-DSI panel used in the Pine64 PineTab2.
Signed-off-by: Dang Huynh danct12@riseup.net --- drivers/video/Kconfig | 10 ++ drivers/video/Makefile | 1 + drivers/video/boe-th101mb31ig002-28a.c | 231 +++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+)
diff --git a/drivers/video/Kconfig b/drivers/video/Kconfig index 3c3cebaacd02b847e2817d465123b088edfc70c0..bb76df7b7d5ef426b018f37160f4d0863f087f57 100644 --- a/drivers/video/Kconfig +++ b/drivers/video/Kconfig @@ -531,6 +531,16 @@ config VIDEO_BCM2835 that same resolution (or as near as possible) and 32bpp depth, so that U-Boot can access it with full colour depth.
+config VIDEO_LCD_BOE_TH101MB31IG002_28A + bool "BOE TH101MB31IG002-28A DSI LCD panel support" + depends on PANEL && BACKLIGHT + select VIDEO_MIPI_DSI + help + Say Y here if you want to enable support for BOE TH101MB31IG002-28A + panel. + + This panel has a 800x1280 resolution and uses 24 bit RGB per pixel. + config VIDEO_LCD_ENDEAVORU tristate "Endeavoru 720x1280 DSI video mode panel" depends on PANEL && BACKLIGHT diff --git a/drivers/video/Makefile b/drivers/video/Makefile index 5a00438ce0648effdcade42ee255f037c65b00d3..d41e3ad5750fc0de4451d7c0be0e9fc7074ce532 100644 --- a/drivers/video/Makefile +++ b/drivers/video/Makefile @@ -55,6 +55,7 @@ obj-$(CONFIG_VIDEO_EFI) += efi.o obj-$(CONFIG_VIDEO_IPUV3) += imx/ obj-$(CONFIG_VIDEO_IVYBRIDGE_IGD) += ivybridge_igd.o obj-$(CONFIG_VIDEO_LCD_ANX9804) += anx9804.o +obj-$(CONFIG_VIDEO_LCD_BOE_TH101MB31IG002_28A) += boe-th101mb31ig002-28a.o obj-$(CONFIG_VIDEO_LCD_ENDEAVORU) += endeavoru-panel.o obj-$(CONFIG_VIDEO_LCD_HIMAX_HX8394) += himax-hx8394.o obj-$(CONFIG_VIDEO_LCD_HITACHI_TX18D42VM) += hitachi_tx18d42vm_lcd.o diff --git a/drivers/video/boe-th101mb31ig002-28a.c b/drivers/video/boe-th101mb31ig002-28a.c new file mode 100644 index 0000000000000000000000000000000000000000..6182ac2f5fba0176cf28a662384e6e3a7b709e10 --- /dev/null +++ b/drivers/video/boe-th101mb31ig002-28a.c @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2023 Alexander Warnecke awarnecke002@hotmail.com + * Copyright (c) 2023 Manuel Traut manut@mecka.net + * Copyright (c) 2023 Dang Huynh danct12@riseup.net + */ + +#include <backlight.h> +#include <dm.h> +#include <mipi_dsi.h> +#include <panel.h> +#include <asm/gpio.h> +#include <dm/device_compat.h> +#include <linux/delay.h> +#include <power/regulator.h> + +struct th101mb31ig002_28a_panel_priv { + struct udevice *backlight; + struct udevice *reg_power; + struct gpio_desc enable; + struct gpio_desc reset; +}; + +static const struct display_timing boe_th101mb31ig002_default_timing = { + .pixelclock.typ = 73500000, + .hactive.typ = 800, + .hfront_porch.typ = 64, + .hback_porch.typ = 64, + .hsync_len.typ = 16, + .vactive.typ = 1280, + .vfront_porch.typ = 2, + .vback_porch.typ = 12, + .vsync_len.typ = 4, +}; + +#define dsi_dcs_write_seq(device, seq...) do { \ + static const u8 d[] = { seq }; \ + int ret; \ + ret = mipi_dsi_dcs_write_buffer(device, d, ARRAY_SIZE(d)); \ + if (ret < 0) \ + return ret; \ + } while (0) + +static int th101mb31ig002_28a_init_sequence(struct udevice *dev) +{ + struct mipi_dsi_panel_plat *plat = dev_get_plat(dev); + struct mipi_dsi_device *device = plat->device; + int ret; + + dsi_dcs_write_seq(device, 0xE0, 0xAB, 0xBA); + dsi_dcs_write_seq(device, 0xE1, 0xBA, 0xAB); + dsi_dcs_write_seq(device, 0xB1, 0x10, 0x01, 0x47, 0xFF); + dsi_dcs_write_seq(device, 0xB2, 0x0C, 0x14, 0x04, 0x50, 0x50, 0x14); + dsi_dcs_write_seq(device, 0xB3, 0x56, 0x53, 0x00); + dsi_dcs_write_seq(device, 0xB4, 0x33, 0x30, 0x04); + dsi_dcs_write_seq(device, 0xB6, 0xB0, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00); + dsi_dcs_write_seq(device, 0xB8, 0x05, 0x12, 0x29, 0x49, 0x48, 0x00, 0x00); + dsi_dcs_write_seq(device, 0xB9, 0x7C, 0x65, 0x55, 0x49, 0x46, 0x36, 0x3B, 0x24, 0x3D, + 0x3C, 0x3D, 0x5C, 0x4C, 0x55, 0x47, 0x46, 0x39, 0x26, 0x06, 0x7C, 0x65, + 0x55, 0x49, 0x46, 0x36, 0x3B, 0x24, 0x3D, 0x3C, 0x3D, 0x5C, 0x4C, 0x55, + 0x47, 0x46, 0x39, 0x26, 0x06); + dsi_dcs_write_seq(device, 0xC0, 0xFF, 0x87, 0x12, 0x34, 0x44, 0x44, 0x44, + 0x44, 0x98, 0x04, 0x98, 0x04, 0x0F, 0x00, 0x00, 0xC1); + dsi_dcs_write_seq(device, 0xC1, 0x54, 0x94, 0x02, 0x85, 0x9F, 0x00, 0x7F, 0x00, 0x54, + 0x00); + dsi_dcs_write_seq(device, 0xC2, 0x17, 0x09, 0x08, 0x89, 0x08, 0x11, 0x22, 0x20, 0x44, + 0xFF, 0x18, 0x00); + dsi_dcs_write_seq(device, 0xC3, 0x86, 0x46, 0x05, 0x05, 0x1C, 0x1C, 0x1D, 0x1D, 0x02, + 0x1F, 0x1F, 0x1E, 0x1E, 0x0F, 0x0F, 0x0D, 0x0D, 0x13, 0x13, 0x11, 0x11, + 0x00); + dsi_dcs_write_seq(device, 0xC4, 0x07, 0x07, 0x04, 0x04, 0x1C, 0x1C, 0x1D, 0x1D, 0x02, + 0x1F, 0x1F, 0x1E, 0x1E, 0x0E, 0x0E, 0x0C, 0x0C, 0x12, 0x12, 0x10, 0x10, + 0x00); + dsi_dcs_write_seq(device, 0xC6, 0x2A, 0x2A); + dsi_dcs_write_seq(device, 0xC8, 0x21, 0x00, 0x31, 0x42, 0x34, 0x16); + dsi_dcs_write_seq(device, 0xCA, 0xCB, 0x43); + dsi_dcs_write_seq(device, 0xCD, 0x0E, 0x4B, 0x4B, 0x20, 0x19, 0x6B, 0x06, 0xB3); + dsi_dcs_write_seq(device, 0xD2, 0xE3, 0x2B, 0x38, 0x00); + dsi_dcs_write_seq(device, 0xD4, 0x00, 0x01, 0x00, 0x0E, 0x04, 0x44, 0x08, 0x10, 0x00, + 0x00, 0x00); + dsi_dcs_write_seq(device, 0xE6, 0x80, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); + dsi_dcs_write_seq(device, 0xF0, 0x12, 0x03, 0x20, 0x00, 0xFF); + dsi_dcs_write_seq(device, 0xF3, 0x00); + + ret = mipi_dsi_dcs_exit_sleep_mode(device); + if (ret) + return ret; + + mdelay(120); + + ret = mipi_dsi_dcs_set_display_on(device); + if (ret) + return ret; + + return 0; +} + +static int th101mb31ig002_28a_panel_enable_backlight(struct udevice *dev) +{ + struct mipi_dsi_panel_plat *plat = dev_get_plat(dev); + struct mipi_dsi_device *device = plat->device; + struct th101mb31ig002_28a_panel_priv *priv = dev_get_priv(dev); + int ret; + + ret = mipi_dsi_attach(device); + if (ret < 0) { + printf("mipi_dsi_attach failed %d\n", ret); + return ret; + } + + ret = th101mb31ig002_28a_init_sequence(dev); + if (ret) { + printf("hx8394_init_sequence failed %d\n", ret); + return ret; + } + + if (priv->backlight) { + ret = backlight_enable(priv->backlight); + if (ret) { + printf("backlight enabled failed %d\n", ret); + return ret; + } + + backlight_set_brightness(priv->backlight, 60); + } + + mdelay(10); + + return 0; +} + +static int th101mb31ig002_28a_panel_get_display_timing(struct udevice *dev, + struct display_timing *timings) +{ + memcpy(timings, &boe_th101mb31ig002_default_timing, sizeof(*timings)); + + return 0; +} + +static int th101mb31ig002_28a_panel_of_to_plat(struct udevice *dev) +{ + struct th101mb31ig002_28a_panel_priv *priv = dev_get_priv(dev); + int ret; + + if (CONFIG_IS_ENABLED(DM_REGULATOR)) { + ret = device_get_supply_regulator(dev, "power-supply", + &priv->reg_power); + if (ret && ret != -ENOENT) { + dev_err(dev, "Warning: cannot get power supply\n"); + return ret; + } + } + + ret = uclass_get_device_by_phandle(UCLASS_PANEL_BACKLIGHT, dev, + "backlight", &priv->backlight); + if (ret) + dev_warn(dev, "failed to get backlight\n"); + + ret = gpio_request_by_name(dev, "enable-gpios", 0, &priv->enable, + GPIOD_IS_OUT); + if (ret) { + dev_err(dev, "Failed to get enable GPIO (%d)\n", ret); + if (ret != -ENOENT) + return ret; + } + + ret = gpio_request_by_name(dev, "reset-gpios", 0, &priv->reset, + GPIOD_IS_OUT); + if (ret) { + dev_err(dev, "Failed to get reset GPIO (%d)\n", ret); + if (ret != -ENOENT) + return ret; + } + + return 0; +} + +static int th101mb31ig002_28a_panel_probe(struct udevice *dev) +{ + struct th101mb31ig002_28a_panel_priv *priv = dev_get_priv(dev); + struct mipi_dsi_panel_plat *plat = dev_get_plat(dev); + int ret; + + if (CONFIG_IS_ENABLED(DM_REGULATOR) && priv->reg_power) { + ret = regulator_set_enable(priv->reg_power, true); + if (ret) + return ret; + } + + /* enable panel */ + dm_gpio_set_value(&priv->enable, 1); + mdelay(50); + + /* reset panel */ + dm_gpio_set_value(&priv->reset, 0); + udelay(100); + dm_gpio_set_value(&priv->reset, 1); + udelay(100); + dm_gpio_set_value(&priv->reset, 0); + mdelay(6); + + plat->lanes = 4; + plat->format = MIPI_DSI_FMT_RGB888; + plat->mode_flags = MIPI_DSI_MODE_VIDEO | + MIPI_DSI_MODE_VIDEO_BURST | + MIPI_DSI_MODE_EOT_PACKET | + MIPI_DSI_MODE_LPM; + + return 0; +} + +static const struct panel_ops th101mb31ig002_28a_panel_ops = { + .enable_backlight = th101mb31ig002_28a_panel_enable_backlight, + .get_display_timing = th101mb31ig002_28a_panel_get_display_timing, +}; + +static const struct udevice_id th101mb31ig002_28a_ids[] = { + { .compatible = "boe,th101mb31ig002-28a", }, + { /* sentinel */ } +}; + +U_BOOT_DRIVER(th101mb31ig002_28a_panel) = { + .name = "th101mb31ig002_28a_panel", + .id = UCLASS_PANEL, + .of_match = th101mb31ig002_28a_ids, + .ops = &th101mb31ig002_28a_panel_ops, + .of_to_plat = th101mb31ig002_28a_panel_of_to_plat, + .probe = th101mb31ig002_28a_panel_probe, + .plat_auto = sizeof(struct mipi_dsi_panel_plat), + .priv_auto = sizeof(struct th101mb31ig002_28a_panel_priv), +};

VOP2 (Video Output Processor v2) is a display controller on Rockchip SoCs. It can be found on RK3566/8 and RK3588.
This commit currently only supports RK3566/8.
Signed-off-by: Dang Huynh danct12@riseup.net --- arch/arm/include/asm/arch-rockchip/vop_rk3568.h | 249 +++++++++++++ drivers/video/rockchip/Makefile | 3 +- drivers/video/rockchip/rk3568_vop.c | 214 +++++++++++ drivers/video/rockchip/rk_vop2.c | 474 ++++++++++++++++++++++++ drivers/video/rockchip/rk_vop2.h | 58 +++ 5 files changed, 997 insertions(+), 1 deletion(-)
diff --git a/arch/arm/include/asm/arch-rockchip/vop_rk3568.h b/arch/arm/include/asm/arch-rockchip/vop_rk3568.h new file mode 100644 index 0000000000000000000000000000000000000000..bfcdf7a07cc275dc0ea48ae6a7889e8f2d8335cf --- /dev/null +++ b/arch/arm/include/asm/arch-rockchip/vop_rk3568.h @@ -0,0 +1,249 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2024 Dang Huynh danct12@riseup.net + * + * Based on vop_rk3288.h: + * Copyright (c) 2015 Google, Inc + * Copyright 2014 Rockchip Inc. + */ + +#ifndef _ASM_ARCH_VOP_RK3568_H +#define _ASM_ARCH_VOP_RK3568_H + +struct rk3568_vop_sysctrl { + u32 reg_cfg_done; + u32 version_info; + u32 autogating_ctrl; + u32 win_reg_cfg_done; + u32 axi_ctrl0; + u32 axi_hurry_ctrl0; + u32 axi_hurry_ctrl1; + u32 axi_outstanding_ctrl0; + u32 axi_outstanding_ctrl1; + u32 axi_lut_ctrl; + u32 dsp_en; + u32 dsp_ctrl; + u32 dsp_pol; + u32 pwr_ctrl; + u32 var_freq_ctrl; + u32 mmu_raddr_range; + u32 wb_ctrl0; + u32 wb_xspd; + u32 wb_yrgb_mst; + u32 wb_cbr_mst; + u32 otp_win; + u32 otp_mirr_ctrl; + u32 lut_port_sel; + u32 pwr_stable_ctrl; + u32 status0; + u32 status1; + u32 status2; + u32 status3; + u32 line_flag0; + u32 line_flag1; + u32 line_flag2; + u32 line_flag3; + u32 sys0_intr_en; + u32 sys0_intr_clr; + u32 sys0_intr_status; + u32 sys0_intr_status_raw; + u32 sys1_intr_en; + u32 sys1_intr_clr; + u32 sys1_intr_status; + u32 sys1_intr_status_raw; + u32 port0_intr_en; + u32 port0_intr_clr; + u32 port0_intr_status; + u32 port0_intr_status_raw; + u32 port1_intr_en; + u32 port1_intr_clr; + u32 port1_intr_status; + u32 port1_intr_status_raw; + u32 port2_intr_en; + u32 port2_intr_clr; + u32 port2_intr_status; + u32 port2_intr_status_raw; + u32 port3_intr_en; + u32 port3_intr_clr; + u32 port3_intr_status; + u32 port3_intr_status_raw; +}; + +check_member(rk3568_vop_sysctrl, port3_intr_status_raw, 0x00DC); + +struct rk3568_vop_overlay { + u32 overlay_ctrl; + u32 layer_sel; + u32 port_sel; +}; + +check_member(rk3568_vop_overlay, port_sel, 0x0008); + +struct rk3568_vop_post { + u32 dsp_ctrl; + u32 mipi_ctrl; + u32 color_ctrl; + u32 reserved2; + u32 lut_reserved[4]; + u32 reserved[3]; + u32 dsp_bg; + u32 prescan_htimings; + u32 dsp_hact_info; + u32 dsp_vact_info; + u32 scl_factor_yrgb; + u32 scl_ctrl; + u32 dsp_vact_info_f1; + u32 dsp_htotal_hs_end; + u32 dsp_hact_st_end; + u32 dsp_vtotal_vs_end; + u32 dsp_vact_st_end; + u32 dsp_vs_st_end_f1; + u32 dsp_vact_st_end_f1; +}; + +check_member(rk3568_vop_post, dsp_vact_st_end_f1, 0x005C); + +struct rk3568_vop_esmart { + u32 esmart_ctrl0; + u32 esmart_ctrl1; + u32 reserved0[2]; + u32 esmart_region0_mst_ctl; + u32 esmart_region0_mst_yrgb; + u32 esmart_region0_mst_cbcr; + u32 esmart_region0_vir; + u32 esmart_region0_act_info; + u32 esmart_region0_dsp_info; + u32 esmart_region0_dsp_offset; + u32 reserved1[1]; + u32 esmart_region0_scl_ctrl; + u32 esmart_region0_scl_factor_yrgb; + u32 esmart_region0_scl_factor_cbcr; + u32 esmart_region0_scl_offset; +}; + +check_member(rk3568_vop_esmart, esmart_region0_scl_offset, 0x003C); + +enum rockchip_fb_data_format_t { + ARGB8888 = 0, + RGB888 = 1, + RGB565 = 2, +}; + +enum vop_modes { + VOP_MODE_EDP = 0, + VOP_MODE_MIPI, + VOP_MODE_HDMI, + VOP_MODE_LVDS, + VOP_MODE_DP, +}; + +/* OFFSETS */ +#define VOP2_SYSREG_OFFSET 0x0 +#define VOP2_OVERLAY_OFFSET 0x0600 +#define VOP2_POST_OFFSET(n) 0x0c00 + ((n) * 0x100) +#define VOP2_CLUSTER_OFFSET(n) 0x1000 + ((n) * 0x200) +#define VOP2_ESMART_OFFSET(n) 0x1800 + ((n) * 0x200) + +/* System Registers */ +/* REG_CFG_DONE */ +#define M_GLOBAL_REGDONE (1 << 15) +#define M_LOAD_GLOBAL(x) (1 << ((x) & 3)) + +#define V_GLOBAL_REGDONE(x) (((x) & 1) << 15) +#define V_LOAD_GLOBAL(x, y) (((y) & 1) << ((x) & 3)) + +/* VERSION_INFO */ +#define M_FPGA_VERSION (0xffff << 16) +#define M_RTL_VERSION (0xffff) + +/* AUTO_GATING_CTRL */ +#define M_AUTO_GATING (1 << 31) +#define V_AUTO_GATING(x) (((x) & 1) << 31) + +/* DSP_INFACE_POL */ +#define M_DSP_INFACE_REGDONE (1 << 28) +#define V_DSP_INFACE_REGDONE(x) (((x) & 1) << 28) + +/* OTP_WIN_EN */ +#define M_OTP_WIN (1 << 0) +#define V_OTP_WIN(x) (((x) & 1) << 0) + +/* Overlay */ +/* OVERLAY_CTRL */ +#define M_LAYER_SEL_REGDONE_SEL (3 << 30) +#define M_LAYER_SEL_REGDONE_EN (1 << 28) +#define M_VP_OVERLAY_MODE(vp) (1 << ((vp) & 3)) + +#define V_LAYER_SEL_REGDONE_SEL(x) (((x) & 3) << 30) +#define V_LAYER_SEL_REGDONE_EN(x) (((x) & 1) << 28) +#define V_VP_OVERLAY_MODE(x, vp) (((x) & 1) << ((vp) & 3)) + +/* LAYER_SEL */ +#define V_LAYER_SEL(x, port) (((port) & 7) << 4 * ((x) & 7)) + +/* PORT_SEL */ +#define V_ESMART_SEL_PORT(x, vp) (((vp) & 3) << (24 + (2 * ((x) & 3)))) +#define V_PORT_MUX(x, vp) (((x) & 0xf) << 4 * ((vp) & 3)) + +/* Post processing */ +/* DSP_CTRL */ +#define M_POST_STANDBY (1 << 31) +#define M_POST_FP_STANDBY (1 << 30) +#define M_POST_BLACK (1 << 27) +#define M_POST_OUT_ZERO (1 << 26) +#define M_POST_LB_MODE (1 << 23) +#define M_PRE_DITHER_DOWN (1 << 16) +#define M_DSP_OUT_MODE (0xf) + +#define V_POST_STANDBY(x) (((x) & 1) << 31) +#define V_POST_FP_STANDBY(x) (((x) & 1) << 30) +#define V_POST_BLACK(x) (((x) & 1) << 27) +#define V_POST_OUT_ZERO(x) (((x) & 1) << 26) +#define V_POST_LB_MODE(x) (((x) & 1) << 23) +#define V_PRE_DITHER_DOWN(x) (((x) & 1) << 16) +#define V_DSP_OUT_MODE(x) ((x) & 0xf) + +/* COLOR_CTRL */ +#define M_POST_COLORBAR_MODE (1 << 1) +#define M_POST_COLORBAR_EN (1 << 0) + +#define V_POST_COLORBAR_MODE(x) (((x) & 1) << 1) +#define V_POST_COLORBAR_EN(x) (((x) & 1) << 0) + +/* DSP_BG */ +#define M_DSP_BG_RED (0x3f << 20) +#define M_DSP_BG_GREEN (0x3f << 10) +#define M_DSP_BG_BLUE (0x3f << 0) + +#define V_DSP_BG_RED(x) (((x) & 0x3f) << 20) +#define V_DSP_BG_GREEN(x) (((x) & 0x3f) << 10) +#define V_DSP_BG_BLUE(x) (((x) & 0x3f) << 0) + +/* ESMART */ +#define M_ESMART_REGION0_MST_EN (1 << 0) +#define V_ESMART_REGION0_DATA_FMT(x) (((x) & 0x16) << 1) + +/* VOP2_ESMART_REGION0_VIR */ +#define V_ARGB888_VIRWIDTH(x) (((x) & 0xffff) << 0) +#define V_RGB888_VIRWIDTH(x) ((((((x) * 3) >> 2) + ((x) % 3)) & 0xffff) << 0) +#define V_RGB565_VIRWIDTH(x) ((((x) / 2) & 0xffff) << 0) +#define YUV_VIRWIDTH(x) ((((x) / 4) & 0xffff) << 0) + +#define V_ACT_HEIGHT(x) (((x) & 0x1fff) << 16) +#define V_ACT_WIDTH(x) ((x) & 0x1fff) +#define V_DSP_HEIGHT(x) (((x) & 0x1fff) << 16) +#define V_DSP_WIDTH(x) ((x) & 0x1fff) +#define V_DSP_YST(x) (((x) & 0x1fff) << 16) +#define V_DSP_XST(x) ((x) & 0x1fff) + +#define V_HSYNC(x) (((x) & 0x1fff) << 0) /* hsync pulse width */ +#define V_HORPRD(x) (((x) & 0x1fff) << 16) /* horizontal period */ +#define V_VSYNC(x) (((x) & 0x1fff) << 0) +#define V_VERPRD(x) (((x) & 0x1fff) << 16) + +#define V_HEAP(x) (((x) & 0x1fff) << 0)/* horizontal active end */ +#define V_HASP(x) (((x) & 0x1fff) << 16)/* horizontal active start */ +#define V_VAEP(x) (((x) & 0x1fff) << 0) +#define V_VASP(x) (((x) & 0x1fff) << 16) + +#endif diff --git a/drivers/video/rockchip/Makefile b/drivers/video/rockchip/Makefile index f55beceebf118bbfc6f85b9edf7f64eaa13ffe62..2f89a979a2848733be5a6d05817ad76ce3ad3a34 100644 --- a/drivers/video/rockchip/Makefile +++ b/drivers/video/rockchip/Makefile @@ -4,10 +4,11 @@ # Wolfgang Denk, DENX Software Engineering, wd@denx.de.
ifdef CONFIG_VIDEO_ROCKCHIP -obj-y += rk_vop.o +obj-y += rk_vop.o rk_vop2.o obj-$(CONFIG_ROCKCHIP_RK3288) += rk3288_vop.o obj-$(CONFIG_ROCKCHIP_RK3328) += rk3328_vop.o obj-$(CONFIG_ROCKCHIP_RK3399) += rk3399_vop.o +obj-$(CONFIG_ROCKCHIP_RK3568) += rk3568_vop.o obj-$(CONFIG_DISPLAY_ROCKCHIP_EDP) += rk_edp.o obj-$(CONFIG_DISPLAY_ROCKCHIP_LVDS) += rk_lvds.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3288) += rk3288_hdmi.o diff --git a/drivers/video/rockchip/rk3568_vop.c b/drivers/video/rockchip/rk3568_vop.c new file mode 100644 index 0000000000000000000000000000000000000000..077b74d0b75a98222af24c48d092c2af871d5e63 --- /dev/null +++ b/drivers/video/rockchip/rk3568_vop.c @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2024 Dang Huynh danct12@riseup.net + * + * Based on rk3399_vop.c: + * Copyright (c) 2017 Theobroma Systems Design und Consulting GmbH + * Copyright (c) 2015 Google, Inc + * Copyright 2014 Rockchip Inc. + */ + +#include <clk.h> +#include <display.h> +#include <dm.h> +#include <log.h> +#include <regmap.h> +#include <video.h> +#include <asm/arch-rockchip/hardware.h> +#include <asm/global_data.h> +#include <linux/bitfield.h> +#include "rk_vop2.h" + +DECLARE_GLOBAL_DATA_PTR; + +#define M_MIPI1_INFACE_MUX (3 << 21) +#define M_LVDS_INFACE_MUX (3 << 18) +#define M_MIPI_INFACE_MUX (3 << 16) +#define M_EDP_INFACE_MUX (3 << 14) +#define M_HDMI_INFACE_MUX (3 << 10) +#define M_RGB_INFACE_MUX (3 << 8) + +#define V_MIPI1_INFACE_MUX(x) (((x) & 3) << 21) +#define V_LVDS_INFACE_MUX(x) (((x) & 3) << 18) +#define V_MIPI_INFACE_MUX(x) (((x) & 3) << 16) +#define V_EDP_INFACE_MUX(x) (((x) & 3) << 14) +#define V_HDMI_INFACE_MUX(x) (((x) & 3) << 10) +#define V_RGB_INFACE_MUX(x) (((x) & 3) << 8) + +#define M_MIPI_POL (0xf << 16) +#define M_EDP_POL (0xf << 12) +#define M_HDMI_POL (0xf << 4) +#define M_RGB_LVDS_POL (0xf << 0) + +#define V_MIPI_POL(x) (((x) & 0xf) << 16) +#define V_EDP_POL(x) (((x) & 0xf) << 12) +#define V_HDMI_POL(x) (((x) & 0xf) << 4) +#define V_RGB_LVDS_POL(x) (((x) & 0xf) << 0) + +#define M_MIPI1_OUT_EN (1 << 20) +#define M_BT656_OUT_EN (1 << 7) +#define M_BT1120_OUT_EN (1 << 6) +#define M_LVDS_OUT_EN (1 << 5) +#define M_MIPI_OUT_EN (1 << 4) +#define M_EDP_OUT_EN (1 << 3) +#define M_HDMI_OUT_EN (1 << 1) +#define M_RGB_OUT_EN (1 << 0) + +#define M_ALL_OUT_EN (M_MIPI1_OUT_EN | M_BT656_OUT_EN | M_BT1120_OUT_EN | M_LVDS_OUT_EN | \ + M_MIPI_OUT_EN | M_EDP_OUT_EN | M_HDMI_OUT_EN | M_RGB_OUT_EN) + +#define V_MIPI1_OUT_EN(x) (((x) & 1) << 20) +#define V_BT656_OUT_EN(x) (((x) & 1) << 7) +#define V_BT1120_OUT_EN(x) (((x) & 1) << 6) +#define V_LVDS_OUT_EN(x) (((x) & 1) << 5) +#define V_MIPI_OUT_EN(x) (((x) & 1) << 4) +#define V_EDP_OUT_EN(x) (((x) & 1) << 3) +#define V_HDMI_OUT_EN(x) (((x) & 1) << 1) +#define V_RGB_OUT_EN(x) (((x) & 1) << 0) + +static void rk3568_enable_output(struct udevice *dev, + enum vop_modes mode, u32 port) +{ + struct rk_vop2_priv *priv = dev_get_priv(dev); + struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET; + u32 reg; + + switch (mode) { + case VOP_MODE_EDP: + reg |= M_EDP_OUT_EN | V_EDP_INFACE_MUX(port); + break; + + case VOP_MODE_HDMI: + reg |= M_HDMI_OUT_EN | V_HDMI_INFACE_MUX(port); + break; + + case VOP_MODE_MIPI: + reg |= M_MIPI_OUT_EN | V_MIPI_INFACE_MUX(port); + break; + + case VOP_MODE_LVDS: + reg |= M_LVDS_OUT_EN | V_LVDS_INFACE_MUX(port); + break; + + default: + debug("%s: unsupported output mode %x\n", __func__, mode); + return; + } + + debug("%s: vop output 0x%08x\n", __func__, reg); + writel(reg, &sysctrl->dsp_en); +} + +static void rk3568_set_pin_polarity(struct udevice *dev, + enum vop_modes mode, u32 polarity) +{ + struct rk_vop2_priv *priv = dev_get_priv(dev); + struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET; + u32 reg; + + reg = M_DSP_INFACE_REGDONE; + + switch (mode) { + case VOP_MODE_EDP: + reg |= V_EDP_POL(polarity); + break; + + case VOP_MODE_HDMI: + reg |= V_HDMI_POL(polarity); + break; + + case VOP_MODE_MIPI: + reg |= V_MIPI_POL(polarity); + break; + + /* RGB and LVDS shares the same polarity */ + case VOP_MODE_LVDS: + reg |= V_RGB_LVDS_POL(polarity); + break; + + default: + debug("%s: unsupported output mode %x\n", __func__, mode); + return; + } + + debug("%s: vop polarity 0x%08x\n", __func__, reg); + writel(reg, &sysctrl->dsp_pol); +} + +static int rkvop_initialize(struct udevice *dev) +{ + struct rk_vop2_priv *priv = dev_get_priv(dev); + struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET; + struct clk aclk; + int ret; + + ret = clk_get_by_name(dev, "aclk", &aclk); + if (ret < 0) + return ret; + + ret = clk_enable(&aclk); + if (ret < 0) { + printf("Failed to enable aclk\n"); + return ret; + } + + debug("aclk rate: %ld\n", clk_get_rate(&aclk)); + + /* Enable OTP function */ + clrsetbits_le32(&sysctrl->otp_win, M_OTP_WIN, V_OTP_WIN(1)); + + writel(M_GLOBAL_REGDONE, &sysctrl->reg_cfg_done); + + /* Disable auto gating */ + clrsetbits_le32(&sysctrl->autogating_ctrl, M_AUTO_GATING, V_AUTO_GATING(0)); + + return 0; +} + +static int rk3568_vop_probe(struct udevice *dev) +{ + int ret; + + /* Before relocation we don't need to do anything */ + if (!(gd->flags & GD_FLG_RELOC)) + return 0; + + ret = rkvop_initialize(dev); + if (ret) + return ret; + + return rk_vop2_probe(dev); +} + +struct rkvop_platdata rk3568_platdata = { + .delay = 20, + .bg_dly = {42, 40, 40}, +}; + +struct rkvop_driverdata rk3568_driverdata = { + .features = VOP_FEATURE_OUTPUT_10BIT, + .set_pin_polarity = rk3568_set_pin_polarity, + .enable_output = rk3568_enable_output, + .platdata = &rk3568_platdata, +}; + +static const struct udevice_id rk3568_vop_ids[] = { + { .compatible = "rockchip,rk3566-vop", + .data = (ulong)&rk3568_driverdata }, + { .compatible = "rockchip,rk3568-vop", + .data = (ulong)&rk3568_driverdata }, + { } +}; + +static const struct video_ops rk3568_vop_ops = { +}; + +U_BOOT_DRIVER(rk3568_vop) = { + .name = "rk3568_vop", + .id = UCLASS_VIDEO, + .of_match = rk3568_vop_ids, + .ops = &rk3568_vop_ops, + .bind = rk_vop2_bind, + .probe = rk3568_vop_probe, + .priv_auto = sizeof(struct rk_vop2_priv), +}; diff --git a/drivers/video/rockchip/rk_vop2.c b/drivers/video/rockchip/rk_vop2.c new file mode 100644 index 0000000000000000000000000000000000000000..21751342de2f079d884b5247ade696efb95131c7 --- /dev/null +++ b/drivers/video/rockchip/rk_vop2.c @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2024 Dang Huynh danct12@riseup.net + * + * Based on rk_vop.c: + * Copyright (c) 2015 Google, Inc + * Copyright 2014 Rockchip Inc. + */ + +#include <clk.h> +#include <display.h> +#include <dm.h> +#include <dm/device_compat.h> +#include <edid.h> +#include <log.h> +#include <regmap.h> +#include <reset.h> +#include <syscon.h> +#include <video.h> +#include <asm/global_data.h> +#include <asm/gpio.h> +#include <asm/io.h> +#include <asm/arch-rockchip/clock.h> +#include <asm/arch-rockchip/vop_rk3568.h> +#include <dm/device-internal.h> +#include <dm/uclass-internal.h> +#include <efi.h> +#include <efi_loader.h> +#include <linux/bitops.h> +#include <linux/err.h> +#include <power/regulator.h> + +#include "rk_vop2.h" + +DECLARE_GLOBAL_DATA_PTR; + +enum vop_pol { + HSYNC_POSITIVE = 0, + VSYNC_POSITIVE = 1, + DEN_NEGATIVE = 2, + DCLK_INVERT = 3 +}; + +static void rkvop_cfg_regdone(struct rk3568_vop_sysctrl *sysctrl, int port) +{ + u32 reg; + + reg = M_GLOBAL_REGDONE; + + /* + * For RK3588, changes will only take effect when the same bit is + * leftshifted by 16. + */ + reg |= M_LOAD_GLOBAL(port) | M_LOAD_GLOBAL(port) << 16; + + writel(reg, &sysctrl->reg_cfg_done); +} + +static void rkvop_enable(struct udevice *dev, ulong fbbase, + int fb_bits_per_pixel, + const struct display_timing *edid, int port, + struct rkvop_platdata *platdata) +{ + struct rk_vop2_priv *priv = dev_get_priv(dev); + struct rk3568_vop_overlay *overlay = priv->regs + VOP2_OVERLAY_OFFSET; + struct rk3568_vop_esmart *esmart = priv->regs + VOP2_ESMART_OFFSET(0); + u32 reg; + u32 rgb_mode; + u32 hactive = edid->hactive.typ; + u32 vactive = edid->vactive.typ; + + debug("(%s, %s): esmart addr: 0x%p\n", dev_read_name(dev), __func__, esmart); + + writel(V_ACT_WIDTH(hactive - 1) | V_ACT_HEIGHT(vactive - 1), + &esmart->esmart_region0_act_info); + + /* Set offset to 0,0 */ + writel(0, &esmart->esmart_region0_dsp_offset); + + writel(V_DSP_WIDTH(hactive - 1) | + V_DSP_HEIGHT(vactive - 1), + &esmart->esmart_region0_dsp_info); + + switch (fb_bits_per_pixel) { + case 16: + rgb_mode = RGB565; + writel(V_RGB565_VIRWIDTH(hactive), &esmart->esmart_region0_vir); + break; + case 24: + rgb_mode = RGB888; + writel(V_RGB888_VIRWIDTH(hactive), &esmart->esmart_region0_vir); + break; + case 32: + default: + rgb_mode = ARGB8888; + writel(V_ARGB888_VIRWIDTH(hactive), &esmart->esmart_region0_vir); + break; + } + + writel(fbbase, &esmart->esmart_region0_mst_yrgb); + + writel(V_ESMART_REGION0_DATA_FMT(rgb_mode) | M_ESMART_REGION0_MST_EN, + &esmart->esmart_region0_mst_ctl); + + reg = readl(&overlay->overlay_ctrl) | M_LAYER_SEL_REGDONE_EN; + writel(reg, &overlay->overlay_ctrl); + + /* Set layer 0 to esmart0 */ + writel(V_LAYER_SEL(0, 2), &overlay->layer_sel); + + /* Set esmart to the destination video port */ + reg = V_ESMART_SEL_PORT(0, port); + + /* + * VOP2 requires every port mux to be configured. + * + * As U-Boot only supports singledisplay, we'll set all + * unused ports to set layer to 8 (disabled). + */ + for (int i = 0; i < 4; i++) { + if (i != port) + reg |= V_PORT_MUX(8, i); + } + + writel(reg, &overlay->port_sel); +} + +static void rkvop_set_pin_polarity(struct udevice *dev, + enum vop_modes mode, u32 polarity) +{ + struct rkvop_driverdata *ops = + (struct rkvop_driverdata *)dev_get_driver_data(dev); + + if (ops->set_pin_polarity) + ops->set_pin_polarity(dev, mode, polarity); +} + +static void rkvop_enable_output(struct udevice *dev, enum vop_modes mode, u32 port) +{ + struct rkvop_driverdata *ops = + (struct rkvop_driverdata *)dev_get_driver_data(dev); + + if (ops->enable_output) + ops->enable_output(dev, mode, port); +} + +static void rkvop_mode_set(struct udevice *dev, + const struct display_timing *edid, + enum vop_modes mode, int port, + struct rkvop_platdata *platdata) +{ + struct rk_vop2_priv *priv = dev_get_priv(dev); + struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET; + struct rk3568_vop_post *post = priv->regs + VOP2_POST_OFFSET(port); + struct rkvop_driverdata *data = + (struct rkvop_driverdata *)dev_get_driver_data(dev); + + debug("(%s, %s): port addr: 0x%p\n", dev_read_name(dev), __func__, post); + + u32 hactive = edid->hactive.typ; + u32 vactive = edid->vactive.typ; + u32 hsync_len = edid->hsync_len.typ; + u32 hback_porch = edid->hback_porch.typ; + u32 vsync_len = edid->vsync_len.typ; + u32 vback_porch = edid->vback_porch.typ; + u32 hfront_porch = edid->hfront_porch.typ; + u32 vfront_porch = edid->vfront_porch.typ; + int mode_flags; + u32 pin_polarity; + u32 reg; + + pin_polarity = BIT(DCLK_INVERT); + if (edid->flags & DISPLAY_FLAGS_HSYNC_HIGH) + pin_polarity |= BIT(HSYNC_POSITIVE); + if (edid->flags & DISPLAY_FLAGS_VSYNC_HIGH) + pin_polarity |= BIT(VSYNC_POSITIVE); + + rkvop_enable_output(dev, mode, port); + rkvop_set_pin_polarity(dev, mode, pin_polarity); + + mode_flags = 0; /* RGB888 */ + if ((data->features & VOP_FEATURE_OUTPUT_10BIT) && + mode == VOP_MODE_HDMI) + mode_flags = 15; /* RGBaaa */ + + reg = V_DSP_OUT_MODE(mode_flags); + + debug("(%s, %s): bg_dly: %d\n", + dev_read_name(dev), __func__, platdata->bg_dly[port]); + + writel(((platdata->bg_dly[port] + (hactive >> 1) - 1) << 16) | hsync_len, + &post->prescan_htimings); + + writel(V_HSYNC(hsync_len) | + V_HORPRD(hsync_len + hback_porch + hactive + hfront_porch), + &post->dsp_htotal_hs_end); + + writel(V_HEAP(hsync_len + hback_porch + hactive) | + V_HASP(hsync_len + hback_porch), + &post->dsp_hact_st_end); + + writel(V_VAEP(vsync_len + vback_porch + vactive) | + V_VASP(vsync_len + vback_porch), + &post->dsp_vact_st_end); + + writel(V_VSYNC(vsync_len) | + V_VERPRD(vsync_len + vback_porch + vactive + vfront_porch), + &post->dsp_vtotal_vs_end); + + writel(V_HEAP(hsync_len + hback_porch + hactive) | + V_HASP(hsync_len + hback_porch), + &post->dsp_hact_info); + + writel(V_VAEP(vsync_len + vback_porch + vactive) | + V_VASP(vsync_len + vback_porch), + &post->dsp_vact_info); + + /* No scaling */ + writel(0x10001000, &post->scl_factor_yrgb); + + writel(reg, &post->dsp_ctrl); + + rkvop_cfg_regdone(sysctrl, port); +} + +/** + * rk_display_init() - Try to enable the given display device + * + * This function performs many steps: + * - Finds the display device being referenced by @ep_node + * - Puts the VOP's ID into its uclass platform data + * - Probes the device to set it up + * - Reads the timing information (from EDID or panel) + * - Sets up the VOP clocks, etc. for the selected pixel clock and display mode + * - Enables the display (the display device handles this and will do different + * things depending on the display type) + * - Tells the uclass about the display resolution so that the console will + * appear correctly + * + * @dev: VOP device that we want to connect to the display + * @fbbase: Frame buffer address + * @vp_node: Device tree node to process + * Return: 0 if OK, -ve if something went wrong + */ +static int rk_display_init(struct udevice *dev, ulong fbbase, ofnode vp_node) +{ + struct video_priv *uc_priv = dev_get_uclass_priv(dev); + struct rkvop_driverdata *drvdata = + (struct rkvop_driverdata *)dev_get_driver_data(dev); + struct rkvop_platdata *platdata = + (struct rkvop_platdata *)drvdata->platdata; + ofnode ep_node; + int vop_id, port_id; + struct display_timing timing; + struct udevice *disp; + int ret; + u32 remote_phandle; + struct display_plat *disp_uc_plat; + enum video_log2_bpp l2bpp; + ofnode remote; + const char *compat; + char dclk_name[9]; + struct clk dclk; + + debug("%s(%s, 0x%lx, %s)\n", __func__, + dev_read_name(dev), fbbase, ofnode_get_name(vp_node)); + + port_id = ofnode_read_u32_default(vp_node, "reg", -1); + if (port_id < 0) { + debug("%s(%s): no video port id\n", __func__, dev_read_name(dev)); + return port_id; + } + + ep_node = ofnode_first_subnode(vp_node); + if (!ofnode_valid(ep_node)) { + debug("%s(%s): no valid subnode\n", __func__, dev_read_name(dev)); + return -EINVAL; + } + + ret = ofnode_read_u32(ep_node, "remote-endpoint", &remote_phandle); + if (ret) { + debug("%s(%s): no remote-endpoint\n", __func__, dev_read_name(dev)); + return ret; + } + + remote = ofnode_get_by_phandle(remote_phandle); + if (!ofnode_valid(remote)) + return -EINVAL; + + remote = ofnode_get_parent(remote); + if (!ofnode_valid(remote)) + return -EINVAL; + + /* + * The remote-endpoint references into a subnode of the encoder + * (i.e. HDMI, MIPI, etc.) with the DTS looking something like + * the following: + * + * hdmi: hdmi@fe0a0000 { + * ports { + * hdmi_in: port { + * hdmi_in_vp0: endpoint { ... }; + * } + * } + * } + * + * This isn't any different from how VOP1 works, so we'll adapt + * the same method of finding the display from the original code + * (find the enclosing device of "UCLASS_DISPLAY") + * + * We also look for UCLASS_VIDEO_BRIDGE so we can use the existing + * DW MIPI DSI driver for Rockchip. + */ + while (ofnode_valid(remote)) { + remote = ofnode_get_parent(remote); + if (!ofnode_valid(remote)) { + debug("%s(%s): no UCLASS_DISPLAY for remote-endpoint\n", + __func__, dev_read_name(dev)); + return -EINVAL; + } + + uclass_find_device_by_ofnode(UCLASS_DISPLAY, remote, &disp); + if (disp) + break; + }; + compat = ofnode_get_property(remote, "compatible", NULL); + if (!compat) { + debug("%s(%s): Failed to find compatible property\n", + __func__, dev_read_name(dev)); + return -EINVAL; + } + if (strstr(compat, "edp")) { + vop_id = VOP_MODE_EDP; + } else if (strstr(compat, "mipi")) { + vop_id = VOP_MODE_MIPI; + } else if (strstr(compat, "hdmi")) { + vop_id = VOP_MODE_HDMI; + } else if (strstr(compat, "rk3588-dp")) { + vop_id = VOP_MODE_DP; + } else if (strstr(compat, "lvds")) { + vop_id = VOP_MODE_LVDS; + } else { + debug("%s(%s): Failed to find vop mode for %s\n", + __func__, dev_read_name(dev), compat); + return -EINVAL; + } + debug("vop_id=%d\n", vop_id); + debug("port=%d\n", port_id); + + /* Get the video port clock and enable it */ + snprintf(dclk_name, sizeof(dclk_name), "dclk_vp%d", port_id); + ret = clk_get_by_name(dev, dclk_name, &dclk); + if (ret < 0) + return ret; + + ret = clk_enable(&dclk); + if (ret < 0) { + printf("Failed to enable %s\n", dclk_name); + return ret; + } + + disp_uc_plat = dev_get_uclass_plat(disp); + debug("Found device '%s', disp_uc_priv=%p\n", disp->name, disp_uc_plat); + if (display_in_use(disp)) { + debug(" - device in use\n"); + return -EBUSY; + } + + disp_uc_plat->source_id = vop_id; + disp_uc_plat->src_dev = dev; + + ret = device_probe(disp); + if (ret) { + debug("%s: device '%s' display won't probe (ret=%d)\n", + __func__, dev->name, ret); + return ret; + } + + ret = display_read_timing(disp, &timing); + if (ret) { + debug("%s: Failed to read timings\n", __func__); + return ret; + } + + /* Set clock rate on video port to display timings */ + ret = clk_set_rate(&dclk, timing.pixelclock.typ); + if (ret < 0) { + printf("Failed to set clock rate: %d\n", ret); + return ret; + } + + debug("%s(%s): %s clkrate %lu\n", __func__, dev_read_name(dev), + dclk_name, clk_get_rate(&dclk)); + + /* Set bitwidth for vop display according to vop mode */ + switch (vop_id) { + case VOP_MODE_EDP: + case VOP_MODE_MIPI: + case VOP_MODE_HDMI: + case VOP_MODE_DP: + case VOP_MODE_LVDS: + l2bpp = VIDEO_BPP32; + break; + default: + l2bpp = VIDEO_BPP16; + } + + rkvop_enable(dev, fbbase, 1 << l2bpp, &timing, port_id, platdata); + + rkvop_mode_set(dev, &timing, vop_id, port_id, platdata); + + ret = display_enable(disp, 1 << l2bpp, &timing); + if (ret) + return ret; + + uc_priv->xsize = timing.hactive.typ; + uc_priv->ysize = timing.vactive.typ; + uc_priv->bpix = l2bpp; + + debug("fb=%lx, size=%d %d\n", fbbase, + uc_priv->xsize, uc_priv->ysize); + + return 0; +} + +int rk_vop2_probe(struct udevice *dev) +{ + struct video_uc_plat *plat = dev_get_uclass_plat(dev); + struct rk_vop2_priv *priv = dev_get_priv(dev); + int ret = 0; + ofnode port, node; + + /* Before relocation we don't need to do anything */ + if (!(gd->flags & GD_FLG_RELOC)) + return 0; + +#ifdef CONFIG_EFI_LOADER + debug("Adding to EFI map %d @ %lx\n", plat->size, plat->base); + efi_add_memory_map(plat->base, plat->size, EFI_RESERVED_MEMORY_TYPE); +#endif + + priv->regs = dev_read_addr_ptr(dev); + + /* Try all the ports until we find one that works. */ + port = dev_read_subnode(dev, "ports"); + if (!ofnode_valid(port)) { + debug("%s(%s): 'port' subnode not found\n", + __func__, dev_read_name(dev)); + return -EINVAL; + } + + for (node = ofnode_first_subnode(port); + ofnode_valid(node); + node = dev_read_next_subnode(node)) { + ret = rk_display_init(dev, plat->base, node); + if (ret) + debug("Device failed: ret=%d\n", ret); + if (!ret) + break; + } + video_set_flush_dcache(dev, 1); + + return ret; +} + +int rk_vop2_bind(struct udevice *dev) +{ + struct video_uc_plat *plat = dev_get_uclass_plat(dev); + + plat->size = 4 * (CONFIG_VIDEO_ROCKCHIP_MAX_XRES * + CONFIG_VIDEO_ROCKCHIP_MAX_YRES); + + return 0; +} diff --git a/drivers/video/rockchip/rk_vop2.h b/drivers/video/rockchip/rk_vop2.h new file mode 100644 index 0000000000000000000000000000000000000000..096d925841556f0a7545f095e34be22a00b89881 --- /dev/null +++ b/drivers/video/rockchip/rk_vop2.h @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2017 Theobroma Systems Design und Consulting GmbH + */ + +#ifndef __RK_VOP2_H__ +#define __RK_VOP2_H__ + +#include <asm/arch-rockchip/vop_rk3568.h> + +struct rk_vop2_priv { + void *grf; + void *regs; +}; + +enum vop_features { + VOP_FEATURE_OUTPUT_10BIT = (1 << 0), +}; + +struct rkvop_platdata { + const u8 delay; + const u8 bg_dly[3]; /* VOP2 supports up to 4 video ports (0-3) */ +}; + +struct rkvop_driverdata { + /* configuration */ + u32 features; + void (*platdata); + /* block-specific setters/getters */ + void (*enable_output)(struct udevice *dev, enum vop_modes mode, u32 port); + void (*set_pin_polarity)(struct udevice *dev, enum vop_modes mode, u32 port); +}; + +/** + * rk_vop2_probe() - common probe implementation + * + * Performs the rk_display_init on each port-subnode until finding a + * working port (or returning an error if none of the ports could be + * successfully initialised). + * + * @dev: device + * Return: 0 if OK, -ve if something went wrong + */ +int rk_vop2_probe(struct udevice *dev); + +/** + * rk_vop2_bind() - common bind implementation + * + * Sets the plat->size field to the amount of memory to be reserved for + * the framebuffer: this is always + * (32 BPP) x VIDEO_ROCKCHIP_MAX_XRES x VIDEO_ROCKCHIP_MAX_YRES + * + * @dev: device + * Return: 0 (always OK) + */ +int rk_vop2_bind(struct udevice *dev); + +#endif

Hi Danq,
At 2025-01-16 17:15:54, "Dang Huynh" danct12@riseup.net wrote:
VOP2 (Video Output Processor v2) is a display controller on Rockchip SoCs. It can be found on RK3566/8 and RK3588.
This commit currently only supports RK3566/8.
Signed-off-by: Dang Huynh danct12@riseup.net
arch/arm/include/asm/arch-rockchip/vop_rk3568.h | 249 +++++++++++++ drivers/video/rockchip/Makefile | 3 +- drivers/video/rockchip/rk3568_vop.c | 214 +++++++++++ drivers/video/rockchip/rk_vop2.c | 474 ++++++++++++++++++++++++ drivers/video/rockchip/rk_vop2.h | 58 +++ 5 files changed, 997 insertions(+), 1 deletion(-)
diff --git a/arch/arm/include/asm/arch-rockchip/vop_rk3568.h b/arch/arm/include/asm/arch-rockchip/vop_rk3568.h new file mode 100644 index 0000000000000000000000000000000000000000..bfcdf7a07cc275dc0ea48ae6a7889e8f2d8335cf --- /dev/null +++ b/arch/arm/include/asm/arch-rockchip/vop_rk3568.h @@ -0,0 +1,249 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/*
- Copyright (c) 2024 Dang Huynh danct12@riseup.net
- Based on vop_rk3288.h:
- Copyright (c) 2015 Google, Inc
- Copyright 2014 Rockchip Inc.
- */
+#ifndef _ASM_ARCH_VOP_RK3568_H +#define _ASM_ARCH_VOP_RK3568_H
+struct rk3568_vop_sysctrl {
- u32 reg_cfg_done;
- u32 version_info;
- u32 autogating_ctrl;
- u32 win_reg_cfg_done;
- u32 axi_ctrl0;
- u32 axi_hurry_ctrl0;
- u32 axi_hurry_ctrl1;
- u32 axi_outstanding_ctrl0;
- u32 axi_outstanding_ctrl1;
- u32 axi_lut_ctrl;
- u32 dsp_en;
- u32 dsp_ctrl;
- u32 dsp_pol;
- u32 pwr_ctrl;
- u32 var_freq_ctrl;
- u32 mmu_raddr_range;
- u32 wb_ctrl0;
- u32 wb_xspd;
- u32 wb_yrgb_mst;
- u32 wb_cbr_mst;
- u32 otp_win;
- u32 otp_mirr_ctrl;
- u32 lut_port_sel;
- u32 pwr_stable_ctrl;
- u32 status0;
- u32 status1;
- u32 status2;
- u32 status3;
- u32 line_flag0;
- u32 line_flag1;
- u32 line_flag2;
- u32 line_flag3;
- u32 sys0_intr_en;
- u32 sys0_intr_clr;
- u32 sys0_intr_status;
- u32 sys0_intr_status_raw;
- u32 sys1_intr_en;
- u32 sys1_intr_clr;
- u32 sys1_intr_status;
- u32 sys1_intr_status_raw;
- u32 port0_intr_en;
- u32 port0_intr_clr;
- u32 port0_intr_status;
- u32 port0_intr_status_raw;
- u32 port1_intr_en;
- u32 port1_intr_clr;
- u32 port1_intr_status;
- u32 port1_intr_status_raw;
- u32 port2_intr_en;
- u32 port2_intr_clr;
- u32 port2_intr_status;
- u32 port2_intr_status_raw;
- u32 port3_intr_en;
- u32 port3_intr_clr;
- u32 port3_intr_status;
- u32 port3_intr_status_raw;
+};
+check_member(rk3568_vop_sysctrl, port3_intr_status_raw, 0x00DC);
+struct rk3568_vop_overlay {
- u32 overlay_ctrl;
- u32 layer_sel;
- u32 port_sel;
+};
+check_member(rk3568_vop_overlay, port_sel, 0x0008);
+struct rk3568_vop_post {
- u32 dsp_ctrl;
- u32 mipi_ctrl;
- u32 color_ctrl;
- u32 reserved2;
- u32 lut_reserved[4];
- u32 reserved[3];
- u32 dsp_bg;
- u32 prescan_htimings;
- u32 dsp_hact_info;
- u32 dsp_vact_info;
- u32 scl_factor_yrgb;
- u32 scl_ctrl;
- u32 dsp_vact_info_f1;
- u32 dsp_htotal_hs_end;
- u32 dsp_hact_st_end;
- u32 dsp_vtotal_vs_end;
- u32 dsp_vact_st_end;
- u32 dsp_vs_st_end_f1;
- u32 dsp_vact_st_end_f1;
+};
+check_member(rk3568_vop_post, dsp_vact_st_end_f1, 0x005C);
+struct rk3568_vop_esmart {
- u32 esmart_ctrl0;
- u32 esmart_ctrl1;
- u32 reserved0[2];
- u32 esmart_region0_mst_ctl;
- u32 esmart_region0_mst_yrgb;
- u32 esmart_region0_mst_cbcr;
- u32 esmart_region0_vir;
- u32 esmart_region0_act_info;
- u32 esmart_region0_dsp_info;
- u32 esmart_region0_dsp_offset;
- u32 reserved1[1];
- u32 esmart_region0_scl_ctrl;
- u32 esmart_region0_scl_factor_yrgb;
- u32 esmart_region0_scl_factor_cbcr;
- u32 esmart_region0_scl_offset;
+};
+check_member(rk3568_vop_esmart, esmart_region0_scl_offset, 0x003C);
+enum rockchip_fb_data_format_t {
- ARGB8888 = 0,
- RGB888 = 1,
- RGB565 = 2,
+};
+enum vop_modes {
- VOP_MODE_EDP = 0,
- VOP_MODE_MIPI,
- VOP_MODE_HDMI,
- VOP_MODE_LVDS,
- VOP_MODE_DP,
+};
+/* OFFSETS */ +#define VOP2_SYSREG_OFFSET 0x0 +#define VOP2_OVERLAY_OFFSET 0x0600 +#define VOP2_POST_OFFSET(n) 0x0c00 + ((n) * 0x100) +#define VOP2_CLUSTER_OFFSET(n) 0x1000 + ((n) * 0x200) +#define VOP2_ESMART_OFFSET(n) 0x1800 + ((n) * 0x200)
+/* System Registers */ +/* REG_CFG_DONE */ +#define M_GLOBAL_REGDONE (1 << 15) +#define M_LOAD_GLOBAL(x) (1 << ((x) & 3))
+#define V_GLOBAL_REGDONE(x) (((x) & 1) << 15) +#define V_LOAD_GLOBAL(x, y) (((y) & 1) << ((x) & 3))
+/* VERSION_INFO */ +#define M_FPGA_VERSION (0xffff << 16) +#define M_RTL_VERSION (0xffff)
+/* AUTO_GATING_CTRL */ +#define M_AUTO_GATING (1 << 31) +#define V_AUTO_GATING(x) (((x) & 1) << 31)
+/* DSP_INFACE_POL */ +#define M_DSP_INFACE_REGDONE (1 << 28) +#define V_DSP_INFACE_REGDONE(x) (((x) & 1) << 28)
+/* OTP_WIN_EN */ +#define M_OTP_WIN (1 << 0) +#define V_OTP_WIN(x) (((x) & 1) << 0)
+/* Overlay */ +/* OVERLAY_CTRL */ +#define M_LAYER_SEL_REGDONE_SEL (3 << 30) +#define M_LAYER_SEL_REGDONE_EN (1 << 28) +#define M_VP_OVERLAY_MODE(vp) (1 << ((vp) & 3))
+#define V_LAYER_SEL_REGDONE_SEL(x) (((x) & 3) << 30) +#define V_LAYER_SEL_REGDONE_EN(x) (((x) & 1) << 28) +#define V_VP_OVERLAY_MODE(x, vp) (((x) & 1) << ((vp) & 3))
+/* LAYER_SEL */ +#define V_LAYER_SEL(x, port) (((port) & 7) << 4 * ((x) & 7))
+/* PORT_SEL */ +#define V_ESMART_SEL_PORT(x, vp) (((vp) & 3) << (24 + (2 * ((x) & 3)))) +#define V_PORT_MUX(x, vp) (((x) & 0xf) << 4 * ((vp) & 3))
+/* Post processing */ +/* DSP_CTRL */ +#define M_POST_STANDBY (1 << 31) +#define M_POST_FP_STANDBY (1 << 30) +#define M_POST_BLACK (1 << 27) +#define M_POST_OUT_ZERO (1 << 26) +#define M_POST_LB_MODE (1 << 23) +#define M_PRE_DITHER_DOWN (1 << 16) +#define M_DSP_OUT_MODE (0xf)
+#define V_POST_STANDBY(x) (((x) & 1) << 31) +#define V_POST_FP_STANDBY(x) (((x) & 1) << 30) +#define V_POST_BLACK(x) (((x) & 1) << 27) +#define V_POST_OUT_ZERO(x) (((x) & 1) << 26) +#define V_POST_LB_MODE(x) (((x) & 1) << 23) +#define V_PRE_DITHER_DOWN(x) (((x) & 1) << 16) +#define V_DSP_OUT_MODE(x) ((x) & 0xf)
+/* COLOR_CTRL */ +#define M_POST_COLORBAR_MODE (1 << 1) +#define M_POST_COLORBAR_EN (1 << 0)
+#define V_POST_COLORBAR_MODE(x) (((x) & 1) << 1) +#define V_POST_COLORBAR_EN(x) (((x) & 1) << 0)
+/* DSP_BG */ +#define M_DSP_BG_RED (0x3f << 20) +#define M_DSP_BG_GREEN (0x3f << 10) +#define M_DSP_BG_BLUE (0x3f << 0)
+#define V_DSP_BG_RED(x) (((x) & 0x3f) << 20) +#define V_DSP_BG_GREEN(x) (((x) & 0x3f) << 10) +#define V_DSP_BG_BLUE(x) (((x) & 0x3f) << 0)
+/* ESMART */ +#define M_ESMART_REGION0_MST_EN (1 << 0) +#define V_ESMART_REGION0_DATA_FMT(x) (((x) & 0x16) << 1)
+/* VOP2_ESMART_REGION0_VIR */ +#define V_ARGB888_VIRWIDTH(x) (((x) & 0xffff) << 0) +#define V_RGB888_VIRWIDTH(x) ((((((x) * 3) >> 2) + ((x) % 3)) & 0xffff) << 0) +#define V_RGB565_VIRWIDTH(x) ((((x) / 2) & 0xffff) << 0) +#define YUV_VIRWIDTH(x) ((((x) / 4) & 0xffff) << 0)
+#define V_ACT_HEIGHT(x) (((x) & 0x1fff) << 16) +#define V_ACT_WIDTH(x) ((x) & 0x1fff) +#define V_DSP_HEIGHT(x) (((x) & 0x1fff) << 16) +#define V_DSP_WIDTH(x) ((x) & 0x1fff) +#define V_DSP_YST(x) (((x) & 0x1fff) << 16) +#define V_DSP_XST(x) ((x) & 0x1fff)
+#define V_HSYNC(x) (((x) & 0x1fff) << 0) /* hsync pulse width */ +#define V_HORPRD(x) (((x) & 0x1fff) << 16) /* horizontal period */ +#define V_VSYNC(x) (((x) & 0x1fff) << 0) +#define V_VERPRD(x) (((x) & 0x1fff) << 16)
+#define V_HEAP(x) (((x) & 0x1fff) << 0)/* horizontal active end */ +#define V_HASP(x) (((x) & 0x1fff) << 16)/* horizontal active start */ +#define V_VAEP(x) (((x) & 0x1fff) << 0) +#define V_VASP(x) (((x) & 0x1fff) << 16)
+#endif diff --git a/drivers/video/rockchip/Makefile b/drivers/video/rockchip/Makefile index f55beceebf118bbfc6f85b9edf7f64eaa13ffe62..2f89a979a2848733be5a6d05817ad76ce3ad3a34 100644 --- a/drivers/video/rockchip/Makefile +++ b/drivers/video/rockchip/Makefile @@ -4,10 +4,11 @@ # Wolfgang Denk, DENX Software Engineering, wd@denx.de.
ifdef CONFIG_VIDEO_ROCKCHIP -obj-y += rk_vop.o +obj-y += rk_vop.o rk_vop2.o obj-$(CONFIG_ROCKCHIP_RK3288) += rk3288_vop.o obj-$(CONFIG_ROCKCHIP_RK3328) += rk3328_vop.o obj-$(CONFIG_ROCKCHIP_RK3399) += rk3399_vop.o +obj-$(CONFIG_ROCKCHIP_RK3568) += rk3568_vop.o obj-$(CONFIG_DISPLAY_ROCKCHIP_EDP) += rk_edp.o obj-$(CONFIG_DISPLAY_ROCKCHIP_LVDS) += rk_lvds.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3288) += rk3288_hdmi.o diff --git a/drivers/video/rockchip/rk3568_vop.c b/drivers/video/rockchip/rk3568_vop.c new file mode 100644 index 0000000000000000000000000000000000000000..077b74d0b75a98222af24c48d092c2af871d5e63 --- /dev/null +++ b/drivers/video/rockchip/rk3568_vop.c @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0 +/*
- Copyright (c) 2024 Dang Huynh danct12@riseup.net
- Based on rk3399_vop.c:
- Copyright (c) 2017 Theobroma Systems Design und Consulting GmbH
- Copyright (c) 2015 Google, Inc
- Copyright 2014 Rockchip Inc.
- */
+#include <clk.h> +#include <display.h> +#include <dm.h> +#include <log.h> +#include <regmap.h> +#include <video.h> +#include <asm/arch-rockchip/hardware.h> +#include <asm/global_data.h> +#include <linux/bitfield.h> +#include "rk_vop2.h"
+DECLARE_GLOBAL_DATA_PTR;
+#define M_MIPI1_INFACE_MUX (3 << 21) +#define M_LVDS_INFACE_MUX (3 << 18) +#define M_MIPI_INFACE_MUX (3 << 16) +#define M_EDP_INFACE_MUX (3 << 14) +#define M_HDMI_INFACE_MUX (3 << 10) +#define M_RGB_INFACE_MUX (3 << 8)
+#define V_MIPI1_INFACE_MUX(x) (((x) & 3) << 21) +#define V_LVDS_INFACE_MUX(x) (((x) & 3) << 18) +#define V_MIPI_INFACE_MUX(x) (((x) & 3) << 16) +#define V_EDP_INFACE_MUX(x) (((x) & 3) << 14) +#define V_HDMI_INFACE_MUX(x) (((x) & 3) << 10) +#define V_RGB_INFACE_MUX(x) (((x) & 3) << 8)
+#define M_MIPI_POL (0xf << 16) +#define M_EDP_POL (0xf << 12) +#define M_HDMI_POL (0xf << 4) +#define M_RGB_LVDS_POL (0xf << 0)
+#define V_MIPI_POL(x) (((x) & 0xf) << 16) +#define V_EDP_POL(x) (((x) & 0xf) << 12) +#define V_HDMI_POL(x) (((x) & 0xf) << 4) +#define V_RGB_LVDS_POL(x) (((x) & 0xf) << 0)
+#define M_MIPI1_OUT_EN (1 << 20) +#define M_BT656_OUT_EN (1 << 7) +#define M_BT1120_OUT_EN (1 << 6) +#define M_LVDS_OUT_EN (1 << 5) +#define M_MIPI_OUT_EN (1 << 4) +#define M_EDP_OUT_EN (1 << 3) +#define M_HDMI_OUT_EN (1 << 1) +#define M_RGB_OUT_EN (1 << 0)
+#define M_ALL_OUT_EN (M_MIPI1_OUT_EN | M_BT656_OUT_EN | M_BT1120_OUT_EN | M_LVDS_OUT_EN | \
M_MIPI_OUT_EN | M_EDP_OUT_EN | M_HDMI_OUT_EN | M_RGB_OUT_EN)
+#define V_MIPI1_OUT_EN(x) (((x) & 1) << 20) +#define V_BT656_OUT_EN(x) (((x) & 1) << 7) +#define V_BT1120_OUT_EN(x) (((x) & 1) << 6) +#define V_LVDS_OUT_EN(x) (((x) & 1) << 5) +#define V_MIPI_OUT_EN(x) (((x) & 1) << 4) +#define V_EDP_OUT_EN(x) (((x) & 1) << 3) +#define V_HDMI_OUT_EN(x) (((x) & 1) << 1) +#define V_RGB_OUT_EN(x) (((x) & 1) << 0)
+static void rk3568_enable_output(struct udevice *dev,
enum vop_modes mode, u32 port)
+{
- struct rk_vop2_priv *priv = dev_get_priv(dev);
- struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET;
- u32 reg;
- switch (mode) {
- case VOP_MODE_EDP:
reg |= M_EDP_OUT_EN | V_EDP_INFACE_MUX(port);
break;
- case VOP_MODE_HDMI:
reg |= M_HDMI_OUT_EN | V_HDMI_INFACE_MUX(port);
break;
- case VOP_MODE_MIPI:
reg |= M_MIPI_OUT_EN | V_MIPI_INFACE_MUX(port);
break;
- case VOP_MODE_LVDS:
reg |= M_LVDS_OUT_EN | V_LVDS_INFACE_MUX(port);
break;
- default:
debug("%s: unsupported output mode %x\n", __func__, mode);
return;
- }
- debug("%s: vop output 0x%08x\n", __func__, reg);
- writel(reg, &sysctrl->dsp_en);
+}
+static void rk3568_set_pin_polarity(struct udevice *dev,
enum vop_modes mode, u32 polarity)
+{
- struct rk_vop2_priv *priv = dev_get_priv(dev);
- struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET;
- u32 reg;
- reg = M_DSP_INFACE_REGDONE;
- switch (mode) {
- case VOP_MODE_EDP:
reg |= V_EDP_POL(polarity);
break;
- case VOP_MODE_HDMI:
reg |= V_HDMI_POL(polarity);
break;
- case VOP_MODE_MIPI:
reg |= V_MIPI_POL(polarity);
break;
- /* RGB and LVDS shares the same polarity */
- case VOP_MODE_LVDS:
reg |= V_RGB_LVDS_POL(polarity);
break;
- default:
debug("%s: unsupported output mode %x\n", __func__, mode);
return;
- }
- debug("%s: vop polarity 0x%08x\n", __func__, reg);
- writel(reg, &sysctrl->dsp_pol);
+}
+static int rkvop_initialize(struct udevice *dev) +{
- struct rk_vop2_priv *priv = dev_get_priv(dev);
- struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET;
- struct clk aclk;
- int ret;
- ret = clk_get_by_name(dev, "aclk", &aclk);
- if (ret < 0)
return ret;
- ret = clk_enable(&aclk);
- if (ret < 0) {
printf("Failed to enable aclk\n");
return ret;
- }
- debug("aclk rate: %ld\n", clk_get_rate(&aclk));
- /* Enable OTP function */
- clrsetbits_le32(&sysctrl->otp_win, M_OTP_WIN, V_OTP_WIN(1));
- writel(M_GLOBAL_REGDONE, &sysctrl->reg_cfg_done);
- /* Disable auto gating */
- clrsetbits_le32(&sysctrl->autogating_ctrl, M_AUTO_GATING, V_AUTO_GATING(0));
- return 0;
+}
+static int rk3568_vop_probe(struct udevice *dev) +{
- int ret;
- /* Before relocation we don't need to do anything */
- if (!(gd->flags & GD_FLG_RELOC))
return 0;
- ret = rkvop_initialize(dev);
- if (ret)
return ret;
- return rk_vop2_probe(dev);
+}
+struct rkvop_platdata rk3568_platdata = {
- .delay = 20,
- .bg_dly = {42, 40, 40},
+};
+struct rkvop_driverdata rk3568_driverdata = {
- .features = VOP_FEATURE_OUTPUT_10BIT,
- .set_pin_polarity = rk3568_set_pin_polarity,
- .enable_output = rk3568_enable_output,
- .platdata = &rk3568_platdata,
+};
+static const struct udevice_id rk3568_vop_ids[] = {
- { .compatible = "rockchip,rk3566-vop",
.data = (ulong)&rk3568_driverdata },
- { .compatible = "rockchip,rk3568-vop",
.data = (ulong)&rk3568_driverdata },
- { }
+};
+static const struct video_ops rk3568_vop_ops = { +};
+U_BOOT_DRIVER(rk3568_vop) = {
- .name = "rk3568_vop",
- .id = UCLASS_VIDEO,
- .of_match = rk3568_vop_ids,
- .ops = &rk3568_vop_ops,
- .bind = rk_vop2_bind,
- .probe = rk3568_vop_probe,
- .priv_auto = sizeof(struct rk_vop2_priv),
+}; diff --git a/drivers/video/rockchip/rk_vop2.c b/drivers/video/rockchip/rk_vop2.c new file mode 100644 index 0000000000000000000000000000000000000000..21751342de2f079d884b5247ade696efb95131c7 --- /dev/null +++ b/drivers/video/rockchip/rk_vop2.c @@ -0,0 +1,474 @@ +// SPDX-License-Identifier: GPL-2.0 +/*
- Copyright (c) 2024 Dang Huynh danct12@riseup.net
- Based on rk_vop.c:
- Copyright (c) 2015 Google, Inc
- Copyright 2014 Rockchip Inc.
- */
+#include <clk.h> +#include <display.h> +#include <dm.h> +#include <dm/device_compat.h> +#include <edid.h> +#include <log.h> +#include <regmap.h> +#include <reset.h> +#include <syscon.h> +#include <video.h> +#include <asm/global_data.h> +#include <asm/gpio.h> +#include <asm/io.h> +#include <asm/arch-rockchip/clock.h> +#include <asm/arch-rockchip/vop_rk3568.h> +#include <dm/device-internal.h> +#include <dm/uclass-internal.h> +#include <efi.h> +#include <efi_loader.h> +#include <linux/bitops.h> +#include <linux/err.h> +#include <power/regulator.h>
+#include "rk_vop2.h"
+DECLARE_GLOBAL_DATA_PTR;
+enum vop_pol {
- HSYNC_POSITIVE = 0,
- VSYNC_POSITIVE = 1,
- DEN_NEGATIVE = 2,
- DCLK_INVERT = 3
+};
+static void rkvop_cfg_regdone(struct rk3568_vop_sysctrl *sysctrl, int port)
I suggest all the functions go with prefix vop2 instead of rkvop, distinguish its name from that of the first generation of rk_vop2.c.
+{
- u32 reg;
- reg = M_GLOBAL_REGDONE;
- /*
* For RK3588, changes will only take effect when the same bit is
* leftshifted by 16.
I think you mean rk3568 here.
*/
- reg |= M_LOAD_GLOBAL(port) | M_LOAD_GLOBAL(port) << 16;
- writel(reg, &sysctrl->reg_cfg_done);
+}
+static void rkvop_enable(struct udevice *dev, ulong fbbase,
int fb_bits_per_pixel,
const struct display_timing *edid, int port,
struct rkvop_platdata *platdata)
+{
- struct rk_vop2_priv *priv = dev_get_priv(dev);
- struct rk3568_vop_overlay *overlay = priv->regs + VOP2_OVERLAY_OFFSET;
- struct rk3568_vop_esmart *esmart = priv->regs + VOP2_ESMART_OFFSET(0);
- u32 reg;
- u32 rgb_mode;
- u32 hactive = edid->hactive.typ;
- u32 vactive = edid->vactive.typ;
- debug("(%s, %s): esmart addr: 0x%p\n", dev_read_name(dev), __func__, esmart);
- writel(V_ACT_WIDTH(hactive - 1) | V_ACT_HEIGHT(vactive - 1),
&esmart->esmart_region0_act_info);
- /* Set offset to 0,0 */
- writel(0, &esmart->esmart_region0_dsp_offset);
- writel(V_DSP_WIDTH(hactive - 1) |
V_DSP_HEIGHT(vactive - 1),
&esmart->esmart_region0_dsp_info);
- switch (fb_bits_per_pixel) {
- case 16:
rgb_mode = RGB565;
writel(V_RGB565_VIRWIDTH(hactive), &esmart->esmart_region0_vir);
break;
- case 24:
rgb_mode = RGB888;
writel(V_RGB888_VIRWIDTH(hactive), &esmart->esmart_region0_vir);
break;
- case 32:
- default:
rgb_mode = ARGB8888;
writel(V_ARGB888_VIRWIDTH(hactive), &esmart->esmart_region0_vir);
break;
- }
- writel(fbbase, &esmart->esmart_region0_mst_yrgb);
- writel(V_ESMART_REGION0_DATA_FMT(rgb_mode) | M_ESMART_REGION0_MST_EN,
&esmart->esmart_region0_mst_ctl);
- reg = readl(&overlay->overlay_ctrl) | M_LAYER_SEL_REGDONE_EN;
- writel(reg, &overlay->overlay_ctrl);
- /* Set layer 0 to esmart0 */
- writel(V_LAYER_SEL(0, 2), &overlay->layer_sel);
- /* Set esmart to the destination video port */
- reg = V_ESMART_SEL_PORT(0, port);
- /*
* VOP2 requires every port mux to be configured.
*
* As U-Boot only supports singledisplay, we'll set all
* unused ports to set layer to 8 (disabled).
*/
- for (int i = 0; i < 4; i++) {
if (i != port)
reg |= V_PORT_MUX(8, i);
- }
- writel(reg, &overlay->port_sel);
+}
+static void rkvop_set_pin_polarity(struct udevice *dev,
enum vop_modes mode, u32 polarity)
+{
- struct rkvop_driverdata *ops =
(struct rkvop_driverdata *)dev_get_driver_data(dev);
- if (ops->set_pin_polarity)
ops->set_pin_polarity(dev, mode, polarity);
+}
+static void rkvop_enable_output(struct udevice *dev, enum vop_modes mode, u32 port) +{
- struct rkvop_driverdata *ops =
(struct rkvop_driverdata *)dev_get_driver_data(dev);
- if (ops->enable_output)
ops->enable_output(dev, mode, port);
+}
+static void rkvop_mode_set(struct udevice *dev,
const struct display_timing *edid,
enum vop_modes mode, int port,
struct rkvop_platdata *platdata)
+{
- struct rk_vop2_priv *priv = dev_get_priv(dev);
- struct rk3568_vop_sysctrl *sysctrl = priv->regs + VOP2_SYSREG_OFFSET;
- struct rk3568_vop_post *post = priv->regs + VOP2_POST_OFFSET(port);
- struct rkvop_driverdata *data =
(struct rkvop_driverdata *)dev_get_driver_data(dev);
- debug("(%s, %s): port addr: 0x%p\n", dev_read_name(dev), __func__, post);
- u32 hactive = edid->hactive.typ;
- u32 vactive = edid->vactive.typ;
- u32 hsync_len = edid->hsync_len.typ;
- u32 hback_porch = edid->hback_porch.typ;
- u32 vsync_len = edid->vsync_len.typ;
- u32 vback_porch = edid->vback_porch.typ;
- u32 hfront_porch = edid->hfront_porch.typ;
- u32 vfront_porch = edid->vfront_porch.typ;
- int mode_flags;
- u32 pin_polarity;
- u32 reg;
- pin_polarity = BIT(DCLK_INVERT);
- if (edid->flags & DISPLAY_FLAGS_HSYNC_HIGH)
pin_polarity |= BIT(HSYNC_POSITIVE);
- if (edid->flags & DISPLAY_FLAGS_VSYNC_HIGH)
pin_polarity |= BIT(VSYNC_POSITIVE);
- rkvop_enable_output(dev, mode, port);
- rkvop_set_pin_polarity(dev, mode, pin_polarity);
- mode_flags = 0; /* RGB888 */
- if ((data->features & VOP_FEATURE_OUTPUT_10BIT) &&
mode == VOP_MODE_HDMI)
mode_flags = 15; /* RGBaaa */
- reg = V_DSP_OUT_MODE(mode_flags);
- debug("(%s, %s): bg_dly: %d\n",
dev_read_name(dev), __func__, platdata->bg_dly[port]);
- writel(((platdata->bg_dly[port] + (hactive >> 1) - 1) << 16) | hsync_len,
&post->prescan_htimings);
- writel(V_HSYNC(hsync_len) |
V_HORPRD(hsync_len + hback_porch + hactive + hfront_porch),
&post->dsp_htotal_hs_end);
- writel(V_HEAP(hsync_len + hback_porch + hactive) |
V_HASP(hsync_len + hback_porch),
&post->dsp_hact_st_end);
- writel(V_VAEP(vsync_len + vback_porch + vactive) |
V_VASP(vsync_len + vback_porch),
&post->dsp_vact_st_end);
- writel(V_VSYNC(vsync_len) |
V_VERPRD(vsync_len + vback_porch + vactive + vfront_porch),
&post->dsp_vtotal_vs_end);
- writel(V_HEAP(hsync_len + hback_porch + hactive) |
V_HASP(hsync_len + hback_porch),
&post->dsp_hact_info);
- writel(V_VAEP(vsync_len + vback_porch + vactive) |
V_VASP(vsync_len + vback_porch),
&post->dsp_vact_info);
- /* No scaling */
- writel(0x10001000, &post->scl_factor_yrgb);
- writel(reg, &post->dsp_ctrl);
- rkvop_cfg_regdone(sysctrl, port);
+}
+/**
- rk_display_init() - Try to enable the given display device
- This function performs many steps:
- Finds the display device being referenced by @ep_node
- Puts the VOP's ID into its uclass platform data
- Probes the device to set it up
- Reads the timing information (from EDID or panel)
- Sets up the VOP clocks, etc. for the selected pixel clock and display mode
- Enables the display (the display device handles this and will do different
things depending on the display type)
- Tells the uclass about the display resolution so that the console will
appear correctly
- @dev: VOP device that we want to connect to the display
- @fbbase: Frame buffer address
- @vp_node: Device tree node to process
- Return: 0 if OK, -ve if something went wrong
- */
+static int rk_display_init(struct udevice *dev, ulong fbbase, ofnode vp_node) +{
- struct video_priv *uc_priv = dev_get_uclass_priv(dev);
- struct rkvop_driverdata *drvdata =
(struct rkvop_driverdata *)dev_get_driver_data(dev);
- struct rkvop_platdata *platdata =
(struct rkvop_platdata *)drvdata->platdata;
- ofnode ep_node;
- int vop_id, port_id;
- struct display_timing timing;
- struct udevice *disp;
- int ret;
- u32 remote_phandle;
- struct display_plat *disp_uc_plat;
- enum video_log2_bpp l2bpp;
- ofnode remote;
- const char *compat;
- char dclk_name[9];
- struct clk dclk;
- debug("%s(%s, 0x%lx, %s)\n", __func__,
dev_read_name(dev), fbbase, ofnode_get_name(vp_node));
- port_id = ofnode_read_u32_default(vp_node, "reg", -1);
- if (port_id < 0) {
debug("%s(%s): no video port id\n", __func__, dev_read_name(dev));
return port_id;
- }
- ep_node = ofnode_first_subnode(vp_node);
- if (!ofnode_valid(ep_node)) {
debug("%s(%s): no valid subnode\n", __func__, dev_read_name(dev));
return -EINVAL;
- }
- ret = ofnode_read_u32(ep_node, "remote-endpoint", &remote_phandle);
- if (ret) {
debug("%s(%s): no remote-endpoint\n", __func__, dev_read_name(dev));
return ret;
- }
- remote = ofnode_get_by_phandle(remote_phandle);
- if (!ofnode_valid(remote))
return -EINVAL;
- remote = ofnode_get_parent(remote);
- if (!ofnode_valid(remote))
return -EINVAL;
- /*
* The remote-endpoint references into a subnode of the encoder
* (i.e. HDMI, MIPI, etc.) with the DTS looking something like
* the following:
*
* hdmi: hdmi@fe0a0000 {
* ports {
* hdmi_in: port {
* hdmi_in_vp0: endpoint { ... };
* }
* }
* }
*
* This isn't any different from how VOP1 works, so we'll adapt
* the same method of finding the display from the original code
* (find the enclosing device of "UCLASS_DISPLAY")
*
* We also look for UCLASS_VIDEO_BRIDGE so we can use the existing
* DW MIPI DSI driver for Rockchip.
*/
- while (ofnode_valid(remote)) {
remote = ofnode_get_parent(remote);
if (!ofnode_valid(remote)) {
debug("%s(%s): no UCLASS_DISPLAY for remote-endpoint\n",
__func__, dev_read_name(dev));
return -EINVAL;
}
uclass_find_device_by_ofnode(UCLASS_DISPLAY, remote, &disp);
if (disp)
break;
- };
- compat = ofnode_get_property(remote, "compatible", NULL);
- if (!compat) {
debug("%s(%s): Failed to find compatible property\n",
__func__, dev_read_name(dev));
return -EINVAL;
- }
- if (strstr(compat, "edp")) {
vop_id = VOP_MODE_EDP;
- } else if (strstr(compat, "mipi")) {
vop_id = VOP_MODE_MIPI;
- } else if (strstr(compat, "hdmi")) {
vop_id = VOP_MODE_HDMI;
- } else if (strstr(compat, "rk3588-dp")) {
vop_id = VOP_MODE_DP;
- } else if (strstr(compat, "lvds")) {
vop_id = VOP_MODE_LVDS;
- } else {
debug("%s(%s): Failed to find vop mode for %s\n",
__func__, dev_read_name(dev), compat);
return -EINVAL;
- }
- debug("vop_id=%d\n", vop_id);
- debug("port=%d\n", port_id);
- /* Get the video port clock and enable it */
- snprintf(dclk_name, sizeof(dclk_name), "dclk_vp%d", port_id);
- ret = clk_get_by_name(dev, dclk_name, &dclk);
- if (ret < 0)
return ret;
- ret = clk_enable(&dclk);
- if (ret < 0) {
printf("Failed to enable %s\n", dclk_name);
return ret;
- }
- disp_uc_plat = dev_get_uclass_plat(disp);
- debug("Found device '%s', disp_uc_priv=%p\n", disp->name, disp_uc_plat);
- if (display_in_use(disp)) {
debug(" - device in use\n");
return -EBUSY;
- }
- disp_uc_plat->source_id = vop_id;
- disp_uc_plat->src_dev = dev;
- ret = device_probe(disp);
- if (ret) {
debug("%s: device '%s' display won't probe (ret=%d)\n",
__func__, dev->name, ret);
return ret;
- }
- ret = display_read_timing(disp, &timing);
- if (ret) {
debug("%s: Failed to read timings\n", __func__);
return ret;
- }
- /* Set clock rate on video port to display timings */
- ret = clk_set_rate(&dclk, timing.pixelclock.typ);
- if (ret < 0) {
printf("Failed to set clock rate: %d\n", ret);
return ret;
- }
- debug("%s(%s): %s clkrate %lu\n", __func__, dev_read_name(dev),
dclk_name, clk_get_rate(&dclk));
- /* Set bitwidth for vop display according to vop mode */
- switch (vop_id) {
- case VOP_MODE_EDP:
- case VOP_MODE_MIPI:
- case VOP_MODE_HDMI:
- case VOP_MODE_DP:
- case VOP_MODE_LVDS:
l2bpp = VIDEO_BPP32;
break;
- default:
l2bpp = VIDEO_BPP16;
- }
- rkvop_enable(dev, fbbase, 1 << l2bpp, &timing, port_id, platdata);
- rkvop_mode_set(dev, &timing, vop_id, port_id, platdata);
- ret = display_enable(disp, 1 << l2bpp, &timing);
- if (ret)
return ret;
- uc_priv->xsize = timing.hactive.typ;
- uc_priv->ysize = timing.vactive.typ;
- uc_priv->bpix = l2bpp;
- debug("fb=%lx, size=%d %d\n", fbbase,
uc_priv->xsize, uc_priv->ysize);
- return 0;
+}
+int rk_vop2_probe(struct udevice *dev) +{
- struct video_uc_plat *plat = dev_get_uclass_plat(dev);
- struct rk_vop2_priv *priv = dev_get_priv(dev);
- int ret = 0;
- ofnode port, node;
- /* Before relocation we don't need to do anything */
- if (!(gd->flags & GD_FLG_RELOC))
return 0;
+#ifdef CONFIG_EFI_LOADER
- debug("Adding to EFI map %d @ %lx\n", plat->size, plat->base);
- efi_add_memory_map(plat->base, plat->size, EFI_RESERVED_MEMORY_TYPE);
+#endif
- priv->regs = dev_read_addr_ptr(dev);
- /* Try all the ports until we find one that works. */
- port = dev_read_subnode(dev, "ports");
- if (!ofnode_valid(port)) {
debug("%s(%s): 'port' subnode not found\n",
__func__, dev_read_name(dev));
return -EINVAL;
- }
- for (node = ofnode_first_subnode(port);
ofnode_valid(node);
node = dev_read_next_subnode(node)) {
ret = rk_display_init(dev, plat->base, node);
if (ret)
debug("Device failed: ret=%d\n", ret);
if (!ret)
break;
- }
- video_set_flush_dcache(dev, 1);
- return ret;
+}
+int rk_vop2_bind(struct udevice *dev) +{
- struct video_uc_plat *plat = dev_get_uclass_plat(dev);
- plat->size = 4 * (CONFIG_VIDEO_ROCKCHIP_MAX_XRES *
CONFIG_VIDEO_ROCKCHIP_MAX_YRES);
- return 0;
+} diff --git a/drivers/video/rockchip/rk_vop2.h b/drivers/video/rockchip/rk_vop2.h new file mode 100644 index 0000000000000000000000000000000000000000..096d925841556f0a7545f095e34be22a00b89881 --- /dev/null +++ b/drivers/video/rockchip/rk_vop2.h @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/*
- Copyright (c) 2017 Theobroma Systems Design und Consulting GmbH
- */
+#ifndef __RK_VOP2_H__ +#define __RK_VOP2_H__
+#include <asm/arch-rockchip/vop_rk3568.h>
+struct rk_vop2_priv {
- void *grf;
- void *regs;
+};
+enum vop_features {
- VOP_FEATURE_OUTPUT_10BIT = (1 << 0),
+};
+struct rkvop_platdata {
- const u8 delay;
- const u8 bg_dly[3]; /* VOP2 supports up to 4 video ports (0-3) */
+};
+struct rkvop_driverdata {
- /* configuration */
- u32 features;
- void (*platdata);
- /* block-specific setters/getters */
- void (*enable_output)(struct udevice *dev, enum vop_modes mode, u32 port);
- void (*set_pin_polarity)(struct udevice *dev, enum vop_modes mode, u32 port);
+};
+/**
- rk_vop2_probe() - common probe implementation
- Performs the rk_display_init on each port-subnode until finding a
- working port (or returning an error if none of the ports could be
- successfully initialised).
- @dev: device
- Return: 0 if OK, -ve if something went wrong
- */
+int rk_vop2_probe(struct udevice *dev);
+/**
- rk_vop2_bind() - common bind implementation
- Sets the plat->size field to the amount of memory to be reserved for
- the framebuffer: this is always
(32 BPP) x VIDEO_ROCKCHIP_MAX_XRES x VIDEO_ROCKCHIP_MAX_YRES
- @dev: device
- Return: 0 (always OK)
- */
+int rk_vop2_bind(struct udevice *dev);
Do you have tests with this u-boot video enabled then boot a linux kernel also with drm display enabled?
Because the u-boot video been enabled with no-mmu, when it runs into linux kernel with the window/plane you enable at u-boot stage, when linux kernel enable the iommu, there is a moment the window will read the memory through iommu, but the framebuffer you set in u-boot stage has not been mapped by iommu, there is a high probability of causing an IOMMU page fault.
+#endif
-- 2.48.1

Hi Andy,
On Sunday, January 19, 2025 9:10:10 AM UTC Andy Yan wrote:
Hi Danq,
Do you have tests with this u-boot video enabled then boot a linux kernel also with drm display enabled?
Yes, I booted Linux with U-Boot VOP2 and Linux VOP2 enabled without any problem.
Because the u-boot video been enabled with no-mmu, when it runs into linux kernel with the window/plane you enable at u-boot stage, when linux kernel enable the iommu, there is a moment the window will read the memory through iommu, but the framebuffer you set in u-boot stage has not been mapped by iommu, there is a high probability of causing an IOMMU page fault.

Hi Dang,
On 2025/1/19 21:41, Dang Huynh wrote:
Hi Andy,
On Sunday, January 19, 2025 9:10:10 AM UTC Andy Yan wrote:
Hi Danq,
Do you have tests with this u-boot video enabled then boot a linux kernel also with drm display enabled?
Yes, I booted Linux with U-Boot VOP2 and Linux VOP2 enabled without any problem.
The power domain of the VOP is reset during the kernel boot process on RK356X, which effectively resets the VOP. If the PD is not reset when the VOP is active, it may not be able to reset the IOMMU which cause VOP fault. You can verify this by setting PD to always on. In addition, the RK3588 may have a similar problem.
Because the u-boot video been enabled with no-mmu, when it runs into linux kernel with the window/plane you enable at u-boot stage, when linux kernel enable the iommu, there is a moment the window will read the memory through iommu, but the framebuffer you set in u-boot stage has not been mapped by iommu, there is a high probability of causing an IOMMU page fault.

Add support for the MIPI DSI bridge driver that we have.
Signed-off-by: Dang Huynh danct12@riseup.net --- drivers/video/rockchip/rk_vop2.c | 89 ++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 22 deletions(-)
diff --git a/drivers/video/rockchip/rk_vop2.c b/drivers/video/rockchip/rk_vop2.c index 21751342de2f079d884b5247ade696efb95131c7..369892d32a6ad3b23b9cf1ddb4922c5df309d5c4 100644 --- a/drivers/video/rockchip/rk_vop2.c +++ b/drivers/video/rockchip/rk_vop2.c @@ -13,10 +13,12 @@ #include <dm/device_compat.h> #include <edid.h> #include <log.h> +#include <panel.h> #include <regmap.h> #include <reset.h> #include <syscon.h> #include <video.h> +#include <video_bridge.h> #include <asm/global_data.h> #include <asm/gpio.h> #include <asm/io.h> @@ -253,12 +255,14 @@ static int rk_display_init(struct udevice *dev, ulong fbbase, ofnode vp_node) int vop_id, port_id; struct display_timing timing; struct udevice *disp; + struct udevice *bridge; int ret; u32 remote_phandle; struct display_plat *disp_uc_plat; enum video_log2_bpp l2bpp; ofnode remote; const char *compat; + struct udevice *panel; char dclk_name[9]; struct clk dclk;
@@ -319,8 +323,10 @@ static int rk_display_init(struct udevice *dev, ulong fbbase, ofnode vp_node) return -EINVAL; }
+ uclass_find_device_by_ofnode(UCLASS_VIDEO_BRIDGE, remote, &bridge); + uclass_find_device_by_ofnode(UCLASS_DISPLAY, remote, &disp); - if (disp) + if (disp || bridge) break; }; compat = ofnode_get_property(remote, "compatible", NULL); @@ -359,27 +365,57 @@ static int rk_display_init(struct udevice *dev, ulong fbbase, ofnode vp_node) return ret; }
- disp_uc_plat = dev_get_uclass_plat(disp); - debug("Found device '%s', disp_uc_priv=%p\n", disp->name, disp_uc_plat); - if (display_in_use(disp)) { - debug(" - device in use\n"); - return -EBUSY; - } + if (bridge) { + /* video bridge detected, probe it */ + ret = device_probe(bridge); + if (ret) { + printf("%s: device '%s' bridge won't probe (ret=%d)\n", + __func__, dev->name, ret); + return ret; + }
- disp_uc_plat->source_id = vop_id; - disp_uc_plat->src_dev = dev; + /* Attach the DSI controller and the display to the bridge. */ + ret = video_bridge_attach(bridge); + if (ret) { + printf("Failed to attach video bridge: %d\n", ret); + return ret; + }
- ret = device_probe(disp); - if (ret) { - debug("%s: device '%s' display won't probe (ret=%d)\n", - __func__, dev->name, ret); - return ret; - } + /* Get the panel device (TODO: fetch it from the bridge somehow) */ + ret = uclass_first_device_err(UCLASS_PANEL, &panel); + if (ret) { + printf("Panel device error: %d\n", ret); + return ret; + }
- ret = display_read_timing(disp, &timing); - if (ret) { - debug("%s: Failed to read timings\n", __func__); - return ret; + ret = panel_get_display_timing(panel, &timing); + if (ret) { + debug("%s: Failed to read timings\n", __func__); + return ret; + } + } else { + disp_uc_plat = dev_get_uclass_plat(disp); + debug("Found device '%s', disp_uc_priv=%p\n", disp->name, disp_uc_plat); + if (display_in_use(disp)) { + debug(" - device in use\n"); + return -EBUSY; + } + + disp_uc_plat->source_id = vop_id; + disp_uc_plat->src_dev = dev; + + ret = device_probe(disp); + if (ret) { + debug("%s: device '%s' display won't probe (ret=%d)\n", + __func__, dev->name, ret); + return ret; + } + + ret = display_read_timing(disp, &timing); + if (ret) { + debug("%s: Failed to read timings\n", __func__); + return ret; + } }
/* Set clock rate on video port to display timings */ @@ -409,9 +445,18 @@ static int rk_display_init(struct udevice *dev, ulong fbbase, ofnode vp_node)
rkvop_mode_set(dev, &timing, vop_id, port_id, platdata);
- ret = display_enable(disp, 1 << l2bpp, &timing); - if (ret) - return ret; + if (bridge) { + /* Attach the DSI controller and the display to the bridge. */ + ret = video_bridge_set_backlight(bridge, 60); + if (ret) { + printf("Failed to start the video bridge: %d\n", ret); + return ret; + } + } else { + ret = display_enable(disp, 1 << l2bpp, &timing); + if (ret) + return ret; + }
uc_priv->xsize = timing.hactive.typ; uc_priv->ysize = timing.vactive.typ;

We need to prerelocate VOP2 memory so the driver can work. This will only support U-Boot proper.
Signed-off-by: Dang Huynh danct12@riseup.net --- arch/arm/dts/rk356x-u-boot.dtsi | 4 ++++ 1 file changed, 4 insertions(+)
diff --git a/arch/arm/dts/rk356x-u-boot.dtsi b/arch/arm/dts/rk356x-u-boot.dtsi index 0a0943b462a90826a05b3539bd19f77abf169944..413e3497404ee0ecc109c2386233ba5946113979 100644 --- a/arch/arm/dts/rk356x-u-boot.dtsi +++ b/arch/arm/dts/rk356x-u-boot.dtsi @@ -162,6 +162,10 @@ clock-frequency = <24000000>; };
+&vop { + bootph-some-ram; +}; + &uart2m0_xfer { bootph-all; };

Now that we have VOP2 support, we should enable vidconsole.
Signed-off-by: Dang Huynh danct12@riseup.net --- include/configs/quartz64_rk3566.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/include/configs/quartz64_rk3566.h b/include/configs/quartz64_rk3566.h index dfe0fee94cdb043cbca558e90fb203245534509a..bf391458219b4f2371626d0ccf98af2a3bc279ce 100644 --- a/include/configs/quartz64_rk3566.h +++ b/include/configs/quartz64_rk3566.h @@ -3,7 +3,10 @@ #ifndef __QUARTZ64_RK3566_H #define __QUARTZ64_RK3566_H
-#define ROCKCHIP_DEVICE_SETTINGS +#define ROCKCHIP_DEVICE_SETTINGS \ + "stdin=serial,usbkbd\0" \ + "stdout=serial,vidconsole\0" \ + "stderr=serial,vidconsole\0"
#include <configs/rk3568_common.h>

HDMI on RK3568 is mostly simplified, all this does is enabling DDC for display timings and HPD.
Signed-off-by: Dang Huynh danct12@riseup.net --- drivers/video/rockchip/Makefile | 1 + drivers/video/rockchip/rk3568_hdmi.c | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+)
diff --git a/drivers/video/rockchip/Makefile b/drivers/video/rockchip/Makefile index 2f89a979a2848733be5a6d05817ad76ce3ad3a34..b751c969b46f1933e91a6c0434f31227a709d8e5 100644 --- a/drivers/video/rockchip/Makefile +++ b/drivers/video/rockchip/Makefile @@ -14,6 +14,7 @@ obj-$(CONFIG_DISPLAY_ROCKCHIP_LVDS) += rk_lvds.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3288) += rk3288_hdmi.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3328) += rk3328_hdmi.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3399) += rk3399_hdmi.o +obj-hdmi-$(CONFIG_ROCKCHIP_RK3568) += rk3568_hdmi.o obj-$(CONFIG_DISPLAY_ROCKCHIP_HDMI) += rk_hdmi.o $(obj-hdmi-y) obj-mipi-$(CONFIG_ROCKCHIP_RK3288) += rk3288_mipi.o obj-mipi-$(CONFIG_ROCKCHIP_RK3399) += rk3399_mipi.o diff --git a/drivers/video/rockchip/rk3568_hdmi.c b/drivers/video/rockchip/rk3568_hdmi.c new file mode 100644 index 0000000000000000000000000000000000000000..e0dae919c89d685c145373c8520c52c760f60864 --- /dev/null +++ b/drivers/video/rockchip/rk3568_hdmi.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright (c) 2024 Dang Huynh danct12@riseup.net + */ + +#include <display.h> +#include <dm.h> +#include <dw_hdmi.h> +#include <edid.h> +#include <asm/arch-rockchip/hardware.h> +#include <asm/arch-rockchip/grf_rk3568.h> +#include "rk_hdmi.h" + +#define RK3568_IO_DDC_IN_MSK ((3 << 14) | (3 << (14 + 16))) + +static int rk3568_hdmi_enable(struct udevice *dev, int panel_bpp, + const struct display_timing *edid) +{ + struct rk_hdmi_priv *priv = dev_get_priv(dev); + + return dw_hdmi_enable(&priv->hdmi, edid); +} + +static int rk3568_hdmi_of_to_plat(struct udevice *dev) +{ + struct rk_hdmi_priv *priv = dev_get_priv(dev); + struct dw_hdmi *hdmi = &priv->hdmi; + + hdmi->i2c_clk_high = 0x71; + hdmi->i2c_clk_low = 0x76; + + return rk_hdmi_of_to_plat(dev); +} + +static int rk3568_hdmi_probe(struct udevice *dev) +{ + struct rk_hdmi_priv *priv = dev_get_priv(dev); + struct rk3568_grf *grf = priv->grf; + + writel(RK3568_IO_DDC_IN_MSK, &grf->vo_con1); + + return rk_hdmi_probe(dev); +} + +static const struct dm_display_ops rk3568_hdmi_ops = { + .read_edid = rk_hdmi_read_edid, + .enable = rk3568_hdmi_enable, +}; + +static const struct udevice_id rk3568_hdmi_ids[] = { + { .compatible = "rockchip,rk3568-dw-hdmi" }, + { } +}; + +U_BOOT_DRIVER(rk3568_hdmi_rockchip) = { + .name = "rk3568_hdmi_rockchip", + .id = UCLASS_DISPLAY, + .of_match = rk3568_hdmi_ids, + .ops = &rk3568_hdmi_ops, + .of_to_plat = rk3568_hdmi_of_to_plat, + .probe = rk3568_hdmi_probe, + .priv_auto = sizeof(struct rk_hdmi_priv), +};

Hi Dang,
On 2025/1/16 17:15, Dang Huynh wrote:
HDMI on RK3568 is mostly simplified, all this does is enabling DDC for display timings and HPD.
Signed-off-by: Dang Huynh danct12@riseup.net
drivers/video/rockchip/Makefile | 1 + drivers/video/rockchip/rk3568_hdmi.c | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+)
diff --git a/drivers/video/rockchip/Makefile b/drivers/video/rockchip/Makefile index 2f89a979a2848733be5a6d05817ad76ce3ad3a34..b751c969b46f1933e91a6c0434f31227a709d8e5 100644 --- a/drivers/video/rockchip/Makefile +++ b/drivers/video/rockchip/Makefile @@ -14,6 +14,7 @@ obj-$(CONFIG_DISPLAY_ROCKCHIP_LVDS) += rk_lvds.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3288) += rk3288_hdmi.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3328) += rk3328_hdmi.o obj-hdmi-$(CONFIG_ROCKCHIP_RK3399) += rk3399_hdmi.o +obj-hdmi-$(CONFIG_ROCKCHIP_RK3568) += rk3568_hdmi.o obj-$(CONFIG_DISPLAY_ROCKCHIP_HDMI) += rk_hdmi.o $(obj-hdmi-y) obj-mipi-$(CONFIG_ROCKCHIP_RK3288) += rk3288_mipi.o obj-mipi-$(CONFIG_ROCKCHIP_RK3399) += rk3399_mipi.o diff --git a/drivers/video/rockchip/rk3568_hdmi.c b/drivers/video/rockchip/rk3568_hdmi.c new file mode 100644 index 0000000000000000000000000000000000000000..e0dae919c89d685c145373c8520c52c760f60864 --- /dev/null +++ b/drivers/video/rockchip/rk3568_hdmi.c @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0+ +/*
- Copyright (c) 2024 Dang Huynh danct12@riseup.net
- */
+#include <display.h> +#include <dm.h> +#include <dw_hdmi.h> +#include <edid.h> +#include <asm/arch-rockchip/hardware.h> +#include <asm/arch-rockchip/grf_rk3568.h> +#include "rk_hdmi.h"
+#define RK3568_IO_DDC_IN_MSK ((3 << 14) | (3 << (14 + 16)))
+static int rk3568_hdmi_enable(struct udevice *dev, int panel_bpp,
const struct display_timing *edid)
+{
- struct rk_hdmi_priv *priv = dev_get_priv(dev);
- return dw_hdmi_enable(&priv->hdmi, edid);
+}
+static int rk3568_hdmi_of_to_plat(struct udevice *dev) +{
- struct rk_hdmi_priv *priv = dev_get_priv(dev);
- struct dw_hdmi *hdmi = &priv->hdmi;
- hdmi->i2c_clk_high = 0x71;
- hdmi->i2c_clk_low = 0x76;
- return rk_hdmi_of_to_plat(dev);
+}
+static int rk3568_hdmi_probe(struct udevice *dev) +{
- struct rk_hdmi_priv *priv = dev_get_priv(dev);
- struct rk3568_grf *grf = priv->grf;
- writel(RK3568_IO_DDC_IN_MSK, &grf->vo_con1);
- return rk_hdmi_probe(dev);
+}
+static const struct dm_display_ops rk3568_hdmi_ops = {
- .read_edid = rk_hdmi_read_edid,
- .enable = rk3568_hdmi_enable,
+};
+static const struct udevice_id rk3568_hdmi_ids[] = {
- { .compatible = "rockchip,rk3568-dw-hdmi" },
- { }
+};
+U_BOOT_DRIVER(rk3568_hdmi_rockchip) = {
- .name = "rk3568_hdmi_rockchip",
- .id = UCLASS_DISPLAY,
- .of_match = rk3568_hdmi_ids,
- .ops = &rk3568_hdmi_ops,
- .of_to_plat = rk3568_hdmi_of_to_plat,
- .probe = rk3568_hdmi_probe,
- .priv_auto = sizeof(struct rk_hdmi_priv),
+};
Enable the "avdd-0v9" and "avdd-1v8" regulators as we did in the Kernel is a good idea.

Now that we have VOP2 support, let's enable it and support the built in USB keyboard.
Signed-off-by: Dang Huynh danct12@riseup.net --- configs/pinetab2-rk3566_defconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+)
diff --git a/configs/pinetab2-rk3566_defconfig b/configs/pinetab2-rk3566_defconfig index 45e63b42d192dc172f24e071b09c8203675965d8..b250d8d12e9d5e045e28db4d455850f94514321f 100644 --- a/configs/pinetab2-rk3566_defconfig +++ b/configs/pinetab2-rk3566_defconfig @@ -23,7 +23,10 @@ CONFIG_FIT_VERBOSE=y CONFIG_SPL_FIT_SIGNATURE=y CONFIG_SPL_LOAD_FIT=y CONFIG_LEGACY_IMAGE_FORMAT=y +CONFIG_USE_PREBOOT=y CONFIG_DEFAULT_FDT_FILE="rockchip/rk3566-pinetab2-v2.0.dtb" +CONFIG_PRE_CONSOLE_BUFFER=y +CONFIG_SYS_CONSOLE_ENV_OVERWRITE=y # CONFIG_DISPLAY_CPUINFO is not set CONFIG_DISPLAY_BOARDINFO_LATE=y CONFIG_SPL_MAX_SIZE=0x40000 @@ -57,6 +60,8 @@ CONFIG_BUTTON=y CONFIG_BUTTON_ADC=y CONFIG_BUTTON_GPIO=y CONFIG_SPL_CLK=y +CONFIG_CLK_CCF=y +CONFIG_CLK_COMPOSITE_CCF=y # CONFIG_USB_FUNCTION_FASTBOOT is not set CONFIG_ROCKCHIP_GPIO=y CONFIG_SYS_I2C_ROCKCHIP=y @@ -69,6 +74,7 @@ CONFIG_MMC_SDHCI_SDMA=y CONFIG_MMC_SDHCI_ROCKCHIP=y CONFIG_SPI_FLASH_SFDP_SUPPORT=y CONFIG_SPI_FLASH_SILICONKAISER=y +CONFIG_PHY_ROCKCHIP_INNO_DSIDPHY=y CONFIG_PHY_ROCKCHIP_INNO_USB2=y CONFIG_PHY_ROCKCHIP_NANENG_COMBOPHY=y CONFIG_SPL_PINCTRL=y @@ -94,6 +100,7 @@ CONFIG_USB_OHCI_HCD=y CONFIG_USB_OHCI_GENERIC=y CONFIG_USB_DWC3=y CONFIG_USB_DWC3_GENERIC=y +CONFIG_USB_KEYBOARD=y CONFIG_USB_HOST_ETHER=y CONFIG_USB_ETHER_ASIX=y CONFIG_USB_ETHER_ASIX88179=y @@ -101,4 +108,12 @@ CONFIG_USB_ETHER_RTL8152=y CONFIG_USB_GADGET=y CONFIG_USB_GADGET_DOWNLOAD=y CONFIG_USB_FUNCTION_ROCKUSB=y +CONFIG_VIDEO=y +CONFIG_CONSOLE_ROTATION=y +CONFIG_VIDEO_LCD_BOE_TH101MB31IG002_28A=y +CONFIG_DISPLAY=y +CONFIG_VIDEO_ROCKCHIP=y +CONFIG_DISPLAY_ROCKCHIP_HDMI=y +CONFIG_DISPLAY_ROCKCHIP_DW_MIPI=y +CONFIG_VIDEO_DT_SIMPLEFB=y CONFIG_ERRNO_STR=y

Hi Dang,
On 2025-01-16 10:15, Dang Huynh wrote:
Now that we have VOP2 support, let's enable it and support the built in USB keyboard.
Signed-off-by: Dang Huynh danct12@riseup.net
configs/pinetab2-rk3566_defconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+)
diff --git a/configs/pinetab2-rk3566_defconfig b/configs/pinetab2-rk3566_defconfig index 45e63b42d192dc172f24e071b09c8203675965d8..b250d8d12e9d5e045e28db4d455850f94514321f 100644 --- a/configs/pinetab2-rk3566_defconfig +++ b/configs/pinetab2-rk3566_defconfig @@ -23,7 +23,10 @@ CONFIG_FIT_VERBOSE=y CONFIG_SPL_FIT_SIGNATURE=y CONFIG_SPL_LOAD_FIT=y CONFIG_LEGACY_IMAGE_FORMAT=y +CONFIG_USE_PREBOOT=y
Please try to avoid adding USE_PREBOOT because it will slow down boot by several seconds thanks to it force running "usb start". For normal use starting usb is typically not needed. We should try and delay usb start until it is really needed.
CONFIG_DEFAULT_FDT_FILE="rockchip/rk3566-pinetab2-v2.0.dtb" +CONFIG_PRE_CONSOLE_BUFFER=y +CONFIG_SYS_CONSOLE_ENV_OVERWRITE=y # CONFIG_DISPLAY_CPUINFO is not set CONFIG_DISPLAY_BOARDINFO_LATE=y CONFIG_SPL_MAX_SIZE=0x40000 @@ -57,6 +60,8 @@ CONFIG_BUTTON=y CONFIG_BUTTON_ADC=y CONFIG_BUTTON_GPIO=y CONFIG_SPL_CLK=y +CONFIG_CLK_CCF=y +CONFIG_CLK_COMPOSITE_CCF=y
Is CCF and COMPOSITE_CCF needed?
# CONFIG_USB_FUNCTION_FASTBOOT is not set CONFIG_ROCKCHIP_GPIO=y CONFIG_SYS_I2C_ROCKCHIP=y @@ -69,6 +74,7 @@ CONFIG_MMC_SDHCI_SDMA=y CONFIG_MMC_SDHCI_ROCKCHIP=y CONFIG_SPI_FLASH_SFDP_SUPPORT=y CONFIG_SPI_FLASH_SILICONKAISER=y +CONFIG_PHY_ROCKCHIP_INNO_DSIDPHY=y CONFIG_PHY_ROCKCHIP_INNO_USB2=y CONFIG_PHY_ROCKCHIP_NANENG_COMBOPHY=y CONFIG_SPL_PINCTRL=y @@ -94,6 +100,7 @@ CONFIG_USB_OHCI_HCD=y CONFIG_USB_OHCI_GENERIC=y CONFIG_USB_DWC3=y CONFIG_USB_DWC3_GENERIC=y +CONFIG_USB_KEYBOARD=y CONFIG_USB_HOST_ETHER=y CONFIG_USB_ETHER_ASIX=y CONFIG_USB_ETHER_ASIX88179=y @@ -101,4 +108,12 @@ CONFIG_USB_ETHER_RTL8152=y CONFIG_USB_GADGET=y CONFIG_USB_GADGET_DOWNLOAD=y CONFIG_USB_FUNCTION_ROCKUSB=y +CONFIG_VIDEO=y +CONFIG_CONSOLE_ROTATION=y +CONFIG_VIDEO_LCD_BOE_TH101MB31IG002_28A=y +CONFIG_DISPLAY=y +CONFIG_VIDEO_ROCKCHIP=y +CONFIG_DISPLAY_ROCKCHIP_HDMI=y +CONFIG_DISPLAY_ROCKCHIP_DW_MIPI=y +CONFIG_VIDEO_DT_SIMPLEFB=y
I do not see a simple-framebuffer compatible, is VIDEO_DT_SIMPLEFB needed?
Regards, Jonas
CONFIG_ERRNO_STR=y

Hi Jonas,
On Sunday, January 19, 2025 2:08:37 PM UTC Jonas Karlman wrote:
Hi Dang,
On 2025-01-16 10:15, Dang Huynh wrote:
+CONFIG_CLK_CCF=y +CONFIG_CLK_COMPOSITE_CCF=y
Is CCF and COMPOSITE_CCF needed?
Yes, it's required to turn on and set video port clock. COMPOSITE_CCF shouldn't be needed.
+CONFIG_VIDEO_DT_SIMPLEFB=y
I do not see a simple-framebuffer compatible, is VIDEO_DT_SIMPLEFB needed?
It shoudln't be needed, whoops.
Regards, Jonas
participants (4)
-
Andy Yan
-
Chaoyi Chen
-
Dang Huynh
-
Jonas Karlman