| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright (C) 2025 Icenowy Zheng <uwu@icenowy.me> | |
| */ | |
| #include <linux/of.h> | |
| #include <linux/regmap.h> | |
| #include <uapi/linux/media-bus-format.h> | |
| #include <drm/drm_atomic.h> | |
| #include <drm/drm_atomic_helper.h> | |
| #include <drm/drm_bridge.h> | |
| #include <drm/drm_bridge_connector.h> | |
| #include <drm/drm_connector.h> | |
| #include <drm/drm_encoder.h> | |
| #include <drm/drm_of.h> | |
| #include <drm/drm_print.h> | |
| #include <drm/drm_simple_kms_helper.h> | |
| #include "vs_bridge.h" | |
| #include "vs_bridge_regs.h" | |
| #include "vs_crtc.h" | |
| #include "vs_dc.h" | |
| static int vs_bridge_attach(struct drm_bridge *bridge, | |
| struct drm_encoder *encoder, | |
| enum drm_bridge_attach_flags flags) | |
| { | |
| struct vs_bridge *vbridge = drm_bridge_to_vs_bridge(bridge); | |
| return drm_bridge_attach(encoder, vbridge->next_bridge, | |
| bridge, flags); | |
| } | |
| struct vsdc_dp_format { | |
| u32 linux_fmt; | |
| bool is_yuv; | |
| u32 vsdc_fmt; | |
| }; | |
| static struct vsdc_dp_format vsdc_dp_supported_fmts[] = { | |
| /* default to RGB888 */ | |
| { MEDIA_BUS_FMT_FIXED, false, VSDC_DISP_DP_CONFIG_FMT_RGB888 }, | |
| { MEDIA_BUS_FMT_RGB888_1X24, false, VSDC_DISP_DP_CONFIG_FMT_RGB888 }, | |
| { MEDIA_BUS_FMT_RGB565_1X16, false, VSDC_DISP_DP_CONFIG_FMT_RGB565 }, | |
| { MEDIA_BUS_FMT_RGB666_1X18, false, VSDC_DISP_DP_CONFIG_FMT_RGB666 }, | |
| { MEDIA_BUS_FMT_RGB101010_1X30, | |
| false, VSDC_DISP_DP_CONFIG_FMT_RGB101010 }, | |
| { MEDIA_BUS_FMT_UYVY8_1X16, true, VSDC_DISP_DP_CONFIG_YUV_FMT_UYVY8 }, | |
| { MEDIA_BUS_FMT_UYVY10_1X20, true, VSDC_DISP_DP_CONFIG_YUV_FMT_UYVY10 }, | |
| { MEDIA_BUS_FMT_YUV8_1X24, true, VSDC_DISP_DP_CONFIG_YUV_FMT_YUV8 }, | |
| { MEDIA_BUS_FMT_YUV10_1X30, true, VSDC_DISP_DP_CONFIG_YUV_FMT_YUV10 }, | |
| { MEDIA_BUS_FMT_UYYVYY8_0_5X24, | |
| true, VSDC_DISP_DP_CONFIG_YUV_FMT_UYYVYY8 }, | |
| { MEDIA_BUS_FMT_UYYVYY10_0_5X30, | |
| true, VSDC_DISP_DP_CONFIG_YUV_FMT_UYYVYY10 }, | |
| }; | |
| static u32 *vs_bridge_atomic_get_output_bus_fmts_dpi(struct drm_bridge *bridge, | |
| struct drm_bridge_state *bridge_state, | |
| struct drm_crtc_state *crtc_state, | |
| struct drm_connector_state *conn_state, | |
| unsigned int *num_output_fmts) | |
| { | |
| u32 *output_fmts; | |
| *num_output_fmts = 2; | |
| output_fmts = kcalloc(*num_output_fmts, sizeof(*output_fmts), | |
| GFP_KERNEL); | |
| if (!output_fmts) | |
| return NULL; | |
| /* TODO: support more DPI output formats */ | |
| output_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24; | |
| output_fmts[1] = MEDIA_BUS_FMT_FIXED; | |
| return output_fmts; | |
| } | |
| static u32 *vs_bridge_atomic_get_output_bus_fmts_dp(struct drm_bridge *bridge, | |
| struct drm_bridge_state *bridge_state, | |
| struct drm_crtc_state *crtc_state, | |
| struct drm_connector_state *conn_state, | |
| unsigned int *num_output_fmts) | |
| { | |
| u32 *output_fmts; | |
| unsigned int i; | |
| *num_output_fmts = ARRAY_SIZE(vsdc_dp_supported_fmts); | |
| output_fmts = kcalloc(*num_output_fmts, sizeof(*output_fmts), | |
| GFP_KERNEL); | |
| if (!output_fmts) | |
| return NULL; | |
| for (i = 0; i < *num_output_fmts; i++) | |
| output_fmts[i] = vsdc_dp_supported_fmts[i].linux_fmt; | |
| return output_fmts; | |
| } | |
| static bool vs_bridge_out_dp_fmt_supported(u32 out_fmt) | |
| { | |
| unsigned int i; | |
| for (i = 0; i < ARRAY_SIZE(vsdc_dp_supported_fmts); i++) | |
| if (vsdc_dp_supported_fmts[i].linux_fmt == out_fmt) | |
| return true; | |
| return false; | |
| } | |
| static u32 *vs_bridge_atomic_get_input_bus_fmts_dp(struct drm_bridge *bridge, | |
| struct drm_bridge_state *bridge_state, | |
| struct drm_crtc_state *crtc_state, | |
| struct drm_connector_state *conn_state, | |
| u32 output_fmt, | |
| unsigned int *num_input_fmts) | |
| { | |
| if (!vs_bridge_out_dp_fmt_supported(output_fmt)) { | |
| *num_input_fmts = 0; | |
| return NULL; | |
| } | |
| return drm_atomic_helper_bridge_propagate_bus_fmt(bridge, bridge_state, | |
| crtc_state, | |
| conn_state, | |
| output_fmt, | |
| num_input_fmts); | |
| } | |
| static int vs_bridge_atomic_check_dp(struct drm_bridge *bridge, | |
| struct drm_bridge_state *bridge_state, | |
| struct drm_crtc_state *crtc_state, | |
| struct drm_connector_state *conn_state) | |
| { | |
| if (!vs_bridge_out_dp_fmt_supported(bridge_state->output_bus_cfg.format)) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| static void vs_bridge_enable_common(struct vs_crtc *crtc, | |
| struct drm_bridge_state *br_state) | |
| { | |
| struct vs_dc *dc = crtc->dc; | |
| unsigned int output = crtc->id; | |
| regmap_clear_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_DAT_POL); | |
| regmap_assign_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_DE_POL, | |
| br_state->output_bus_cfg.flags & | |
| DRM_BUS_FLAG_DE_LOW); | |
| regmap_assign_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_CLK_POL, | |
| br_state->output_bus_cfg.flags & | |
| DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE); | |
| regmap_set_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_DE_EN | | |
| VSDC_DISP_PANEL_CONFIG_DAT_EN | | |
| VSDC_DISP_PANEL_CONFIG_CLK_EN); | |
| regmap_set_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_RUNNING); | |
| regmap_clear_bits(dc->regs, VSDC_DISP_PANEL_START, | |
| VSDC_DISP_PANEL_START_MULTI_DISP_SYNC); | |
| regmap_set_bits(dc->regs, VSDC_DISP_PANEL_START, | |
| VSDC_DISP_PANEL_START_RUNNING(output)); | |
| regmap_set_bits(dc->regs, VSDC_DISP_PANEL_CONFIG_EX(crtc->id), | |
| VSDC_DISP_PANEL_CONFIG_EX_COMMIT); | |
| } | |
| static void vs_bridge_atomic_enable_dpi(struct drm_bridge *bridge, | |
| struct drm_atomic_commit *state) | |
| { | |
| struct vs_bridge *vbridge = drm_bridge_to_vs_bridge(bridge); | |
| struct drm_bridge_state *br_state = | |
| drm_atomic_get_new_bridge_state(state, bridge); | |
| struct vs_crtc *crtc = vbridge->crtc; | |
| struct vs_dc *dc = crtc->dc; | |
| unsigned int output = crtc->id; | |
| regmap_clear_bits(dc->regs, VSDC_DISP_DP_CONFIG(output), | |
| VSDC_DISP_DP_CONFIG_DP_EN); | |
| regmap_write(dc->regs, VSDC_DISP_DPI_CONFIG(output), | |
| VSDC_DISP_DPI_CONFIG_FMT_RGB888); | |
| vs_bridge_enable_common(crtc, br_state); | |
| } | |
| static void vs_bridge_atomic_enable_dp(struct drm_bridge *bridge, | |
| struct drm_atomic_commit *state) | |
| { | |
| struct vs_bridge *vbridge = drm_bridge_to_vs_bridge(bridge); | |
| struct drm_bridge_state *br_state = | |
| drm_atomic_get_new_bridge_state(state, bridge); | |
| struct vs_crtc *crtc = vbridge->crtc; | |
| struct vs_dc *dc = crtc->dc; | |
| unsigned int output = crtc->id; | |
| u32 dp_fmt; | |
| unsigned int i; | |
| for (i = 0; i < ARRAY_SIZE(vsdc_dp_supported_fmts); i++) { | |
| if (vsdc_dp_supported_fmts[i].linux_fmt == | |
| br_state->output_bus_cfg.format) | |
| break; | |
| } | |
| if (WARN_ON_ONCE(i == ARRAY_SIZE(vsdc_dp_supported_fmts))) | |
| return; | |
| dp_fmt = vsdc_dp_supported_fmts[i].vsdc_fmt; | |
| dp_fmt |= VSDC_DISP_DP_CONFIG_DP_EN; | |
| regmap_write(dc->regs, VSDC_DISP_DP_CONFIG(output), dp_fmt); | |
| regmap_assign_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_YUV, | |
| vsdc_dp_supported_fmts[i].is_yuv); | |
| vs_bridge_enable_common(crtc, br_state); | |
| } | |
| static void vs_bridge_atomic_disable(struct drm_bridge *bridge, | |
| struct drm_atomic_commit *state) | |
| { | |
| struct vs_bridge *vbridge = drm_bridge_to_vs_bridge(bridge); | |
| struct vs_crtc *crtc = vbridge->crtc; | |
| struct vs_dc *dc = crtc->dc; | |
| unsigned int output = crtc->id; | |
| regmap_clear_bits(dc->regs, VSDC_DISP_PANEL_START, | |
| VSDC_DISP_PANEL_START_MULTI_DISP_SYNC | | |
| VSDC_DISP_PANEL_START_RUNNING(output)); | |
| regmap_clear_bits(dc->regs, VSDC_DISP_PANEL_CONFIG(output), | |
| VSDC_DISP_PANEL_CONFIG_RUNNING); | |
| regmap_set_bits(dc->regs, VSDC_DISP_PANEL_CONFIG_EX(crtc->id), | |
| VSDC_DISP_PANEL_CONFIG_EX_COMMIT); | |
| } | |
| static const struct drm_bridge_funcs vs_dpi_bridge_funcs = { | |
| .attach = vs_bridge_attach, | |
| .atomic_enable = vs_bridge_atomic_enable_dpi, | |
| .atomic_disable = vs_bridge_atomic_disable, | |
| .atomic_get_input_bus_fmts = drm_atomic_helper_bridge_propagate_bus_fmt, | |
| .atomic_get_output_bus_fmts = vs_bridge_atomic_get_output_bus_fmts_dpi, | |
| .atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state, | |
| .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state, | |
| .atomic_create_state = drm_atomic_helper_bridge_create_state, | |
| }; | |
| static const struct drm_bridge_funcs vs_dp_bridge_funcs = { | |
| .attach = vs_bridge_attach, | |
| .atomic_enable = vs_bridge_atomic_enable_dp, | |
| .atomic_disable = vs_bridge_atomic_disable, | |
| .atomic_check = vs_bridge_atomic_check_dp, | |
| .atomic_get_input_bus_fmts = vs_bridge_atomic_get_input_bus_fmts_dp, | |
| .atomic_get_output_bus_fmts = vs_bridge_atomic_get_output_bus_fmts_dp, | |
| .atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state, | |
| .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state, | |
| .atomic_create_state = drm_atomic_helper_bridge_create_state, | |
| }; | |
| static int vs_bridge_detect_output_interface(struct device_node *of_node, | |
| unsigned int output) | |
| { | |
| int ret; | |
| struct device_node *remote; | |
| remote = of_graph_get_remote_node(of_node, output, | |
| VSDC_OUTPUT_INTERFACE_DPI); | |
| if (remote) { | |
| ret = VSDC_OUTPUT_INTERFACE_DPI; | |
| } else { | |
| remote = of_graph_get_remote_node(of_node, output, | |
| VSDC_OUTPUT_INTERFACE_DP); | |
| if (remote) | |
| ret = VSDC_OUTPUT_INTERFACE_DP; | |
| else | |
| ret = -ENODEV; | |
| } | |
| if (remote) | |
| of_node_put(remote); | |
| return ret; | |
| } | |
| struct vs_bridge *vs_bridge_init(struct drm_device *drm_dev, | |
| struct vs_crtc *crtc) | |
| { | |
| unsigned int output = crtc->id; | |
| struct vs_bridge *bridge; | |
| struct drm_bridge *next; | |
| enum vs_bridge_output_interface intf; | |
| const struct drm_bridge_funcs *bridge_funcs; | |
| int ret, enctype; | |
| intf = vs_bridge_detect_output_interface(drm_dev->dev->of_node, | |
| output); | |
| if (intf == -ENODEV) { | |
| drm_dbg(drm_dev, "Skipping output %u\n", output); | |
| return NULL; | |
| } | |
| next = devm_drm_of_get_bridge(drm_dev->dev, drm_dev->dev->of_node, | |
| output, intf); | |
| if (IS_ERR(next)) { | |
| ret = PTR_ERR(next); | |
| if (ret != -EPROBE_DEFER) | |
| drm_err(drm_dev, | |
| "Cannot get downstream bridge of output %u\n", | |
| output); | |
| return ERR_PTR(ret); | |
| } | |
| if (intf == VSDC_OUTPUT_INTERFACE_DPI) | |
| bridge_funcs = &vs_dpi_bridge_funcs; | |
| else | |
| bridge_funcs = &vs_dp_bridge_funcs; | |
| bridge = devm_drm_bridge_alloc(drm_dev->dev, struct vs_bridge, base, | |
| bridge_funcs); | |
| if (IS_ERR(bridge)) | |
| return ERR_PTR(PTR_ERR(bridge)); | |
| bridge->crtc = crtc; | |
| bridge->intf = intf; | |
| bridge->next_bridge = next; | |
| if (intf == VSDC_OUTPUT_INTERFACE_DPI) | |
| enctype = DRM_MODE_ENCODER_DPI; | |
| else | |
| enctype = DRM_MODE_ENCODER_NONE; | |
| bridge->enc = drmm_plain_encoder_alloc(drm_dev, NULL, enctype, NULL); | |
| if (IS_ERR(bridge->enc)) { | |
| drm_err(drm_dev, | |
| "Cannot initialize encoder for output %u\n", output); | |
| ret = PTR_ERR(bridge->enc); | |
| return ERR_PTR(ret); | |
| } | |
| bridge->enc->possible_crtcs = drm_crtc_mask(&crtc->base); | |
| ret = devm_drm_bridge_add(drm_dev->dev, &bridge->base); | |
| if (ret) { | |
| drm_err(drm_dev, | |
| "Cannot add bridge for output %u\n", output); | |
| return ERR_PTR(ret); | |
| } | |
| ret = drm_bridge_attach(bridge->enc, &bridge->base, NULL, | |
| DRM_BRIDGE_ATTACH_NO_CONNECTOR); | |
| if (ret) { | |
| drm_err(drm_dev, | |
| "Cannot attach bridge for output %u\n", output); | |
| return ERR_PTR(ret); | |
| } | |
| bridge->conn = drm_bridge_connector_init(drm_dev, bridge->enc); | |
| if (IS_ERR(bridge->conn)) { | |
| drm_err(drm_dev, | |
| "Cannot create connector for output %u\n", output); | |
| ret = PTR_ERR(bridge->conn); | |
| return ERR_PTR(ret); | |
| } | |
| return bridge; | |
| } |