Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,110 @@
if ARCH_MXC || COMPILE_TEST
config DRM_IMX_LDB_HELPER
tristate
config DRM_IMX8MP_DW_HDMI_BRIDGE
tristate "Freescale i.MX8MP HDMI-TX bridge support"
depends on OF
depends on COMMON_CLK
select DRM_DW_HDMI
select OF_OVERLAY
select DRM_DISPLAY_CONNECTOR
imply DRM_IMX8MP_HDMI_PAI
imply DRM_IMX8MP_HDMI_PVI
imply PHY_FSL_SAMSUNG_HDMI_PHY
help
Choose this to enable support for the internal HDMI encoder found
on the i.MX8MP SoC.
config DRM_IMX8MP_DW_HDMI_BRIDGE_CONNECTOR_FIXUP
bool
default y
depends on DRM_IMX_LCDIF
depends on DRM_IMX8MP_DW_HDMI_BRIDGE
depends on OF
help
Modifies at early boot the live device tree of boards using the
i.MX8MP fsl,imx8mp-hdmi-tx adding a hdmi-connector node linked to
the hdmi-tx. This is needed to support bridge-connector usage in
the i.MX8MP LCDIF driver.
You need this if you use the i.MX8MP HDMI output and your board
device tree file does not have an hdmi-connector node connected
to it.
config DRM_IMX8MP_HDMI_PAI
tristate "Freescale i.MX8MP HDMI PAI bridge support"
depends on OF
select DRM_DW_HDMI
select REGMAP
select REGMAP_MMIO
help
Choose this to enable support for the internal HDMI TX Parallel
Audio Interface found on the Freescale i.MX8MP SoC.
config DRM_IMX8MP_HDMI_PVI
tristate "Freescale i.MX8MP HDMI PVI bridge support"
depends on OF
help
Choose this to enable support for the internal HDMI TX Parallel
Video Interface found on the Freescale i.MX8MP SoC.
config DRM_IMX8QM_LDB
tristate "Freescale i.MX8QM LVDS display bridge"
depends on OF
depends on COMMON_CLK
select DRM_IMX_LDB_HELPER
select DRM_KMS_HELPER
help
Choose this to enable the internal LVDS Display Bridge(LDB) found in
Freescale i.MX8qm processor. Official name of LDB is pixel mapper.
config DRM_IMX8QXP_LDB
tristate "Freescale i.MX8QXP LVDS display bridge"
depends on OF
depends on COMMON_CLK
select DRM_IMX_LDB_HELPER
select DRM_KMS_HELPER
help
Choose this to enable the internal LVDS Display Bridge(LDB) found in
Freescale i.MX8qxp processor. Official name of LDB is pixel mapper.
config DRM_IMX8QXP_PIXEL_COMBINER
tristate "Freescale i.MX8QM/QXP pixel combiner"
depends on OF
depends on COMMON_CLK
select DRM_KMS_HELPER
help
Choose this to enable pixel combiner found in
Freescale i.MX8qm/qxp processors.
config DRM_IMX8QXP_PIXEL_LINK
tristate "Freescale i.MX8QM/QXP display pixel link"
depends on OF
depends on IMX_SCU
select DRM_KMS_HELPER
help
Choose this to enable display pixel link found in
Freescale i.MX8qm/qxp processors.
config DRM_IMX8QXP_PIXEL_LINK_TO_DPI
tristate "Freescale i.MX8QXP pixel link to display pixel interface"
depends on OF
select DRM_KMS_HELPER
help
Choose this to enable pixel link to display pixel interface(PXL2DPI)
found in Freescale i.MX8qxp processor.
config DRM_IMX93_MIPI_DSI
tristate "Freescale i.MX93 specific extensions for Synopsys DW MIPI DSI"
depends on OF
depends on COMMON_CLK
select DRM_DW_MIPI_DSI
select GENERIC_PHY
select GENERIC_PHY_MIPI_DPHY
help
Choose this to enable MIPI DSI controller found in Freescale i.MX93
processor.
endif # ARCH_MXC || COMPILE_TEST
@@ -0,0 +1,12 @@
obj-$(CONFIG_DRM_IMX_LDB_HELPER) += imx-ldb-helper.o
obj-$(CONFIG_DRM_IMX8MP_DW_HDMI_BRIDGE) += imx8mp-hdmi-tx.o
obj-$(CONFIG_DRM_IMX8MP_DW_HDMI_BRIDGE_CONNECTOR_FIXUP) += imx8mp-hdmi-tx-connector-fixup.o \
imx8mp-hdmi-tx-connector-fixup.dtbo.o
obj-$(CONFIG_DRM_IMX8MP_HDMI_PAI) += imx8mp-hdmi-pai.o
obj-$(CONFIG_DRM_IMX8MP_HDMI_PVI) += imx8mp-hdmi-pvi.o
obj-$(CONFIG_DRM_IMX8QM_LDB) += imx8qm-ldb.o
obj-$(CONFIG_DRM_IMX8QXP_LDB) += imx8qxp-ldb.o
obj-$(CONFIG_DRM_IMX8QXP_PIXEL_COMBINER) += imx8qxp-pixel-combiner.o
obj-$(CONFIG_DRM_IMX8QXP_PIXEL_LINK) += imx8qxp-pixel-link.o
obj-$(CONFIG_DRM_IMX8QXP_PIXEL_LINK_TO_DPI) += imx8qxp-pxl2dpi.o
obj-$(CONFIG_DRM_IMX93_MIPI_DSI) += imx93-mipi-dsi.o
@@ -0,0 +1,230 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (C) 2012 Sascha Hauer, Pengutronix
* Copyright 2019,2020,2022 NXP
*/
#include <linux/export.h>
#include <linux/media-bus-format.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/regmap.h>
#include <drm/drm_bridge.h>
#include <drm/drm_of.h>
#include <drm/drm_print.h>
#include "imx-ldb-helper.h"
bool ldb_channel_is_single_link(struct ldb_channel *ldb_ch)
{
return ldb_ch->link_type == LDB_CH_SINGLE_LINK;
}
EXPORT_SYMBOL_GPL(ldb_channel_is_single_link);
bool ldb_channel_is_split_link(struct ldb_channel *ldb_ch)
{
return ldb_ch->link_type == LDB_CH_DUAL_LINK_EVEN_ODD_PIXELS ||
ldb_ch->link_type == LDB_CH_DUAL_LINK_ODD_EVEN_PIXELS;
}
EXPORT_SYMBOL_GPL(ldb_channel_is_split_link);
int ldb_bridge_atomic_check_helper(struct drm_bridge *bridge,
struct drm_bridge_state *bridge_state,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
ldb_ch->in_bus_format = bridge_state->input_bus_cfg.format;
ldb_ch->out_bus_format = bridge_state->output_bus_cfg.format;
return 0;
}
EXPORT_SYMBOL_GPL(ldb_bridge_atomic_check_helper);
void ldb_bridge_mode_set_helper(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
bool is_split = ldb_channel_is_split_link(ldb_ch);
if (is_split)
ldb->ldb_ctrl |= LDB_SPLIT_MODE_EN;
switch (ldb_ch->out_bus_format) {
case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
break;
case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
if (ldb_ch->chno == 0 || is_split)
ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH0_24;
if (ldb_ch->chno == 1 || is_split)
ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH1_24;
break;
case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
if (ldb_ch->chno == 0 || is_split)
ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH0_24 |
LDB_BIT_MAP_CH0_JEIDA;
if (ldb_ch->chno == 1 || is_split)
ldb->ldb_ctrl |= LDB_DATA_WIDTH_CH1_24 |
LDB_BIT_MAP_CH1_JEIDA;
break;
}
}
EXPORT_SYMBOL_GPL(ldb_bridge_mode_set_helper);
void ldb_bridge_enable_helper(struct drm_bridge *bridge)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
/*
* Platform specific bridge drivers should set ldb_ctrl properly
* for the enablement, so just write the ctrl_reg here.
*/
regmap_write(ldb->regmap, ldb->ctrl_reg, ldb->ldb_ctrl);
}
EXPORT_SYMBOL_GPL(ldb_bridge_enable_helper);
void ldb_bridge_disable_helper(struct drm_bridge *bridge)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
bool is_split = ldb_channel_is_split_link(ldb_ch);
if (ldb_ch->chno == 0 || is_split)
ldb->ldb_ctrl &= ~LDB_CH0_MODE_EN_MASK;
if (ldb_ch->chno == 1 || is_split)
ldb->ldb_ctrl &= ~LDB_CH1_MODE_EN_MASK;
regmap_write(ldb->regmap, ldb->ctrl_reg, ldb->ldb_ctrl);
}
EXPORT_SYMBOL_GPL(ldb_bridge_disable_helper);
int ldb_bridge_attach_helper(struct drm_bridge *bridge, struct drm_encoder *encoder,
enum drm_bridge_attach_flags flags)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
DRM_DEV_ERROR(ldb->dev,
"do not support creating a drm_connector\n");
return -EINVAL;
}
return drm_bridge_attach(encoder, ldb_ch->next_bridge, bridge,
DRM_BRIDGE_ATTACH_NO_CONNECTOR);
}
EXPORT_SYMBOL_GPL(ldb_bridge_attach_helper);
int ldb_init_helper(struct ldb *ldb)
{
struct device *dev = ldb->dev;
struct device_node *np = dev->of_node;
struct device_node *child;
int ret;
u32 i;
ldb->regmap = syscon_node_to_regmap(np->parent);
if (IS_ERR(ldb->regmap)) {
ret = PTR_ERR(ldb->regmap);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev, "failed to get regmap: %d\n", ret);
return ret;
}
for_each_available_child_of_node(np, child) {
struct ldb_channel *ldb_ch;
ret = of_property_read_u32(child, "reg", &i);
if (ret || i > MAX_LDB_CHAN_NUM - 1) {
ret = -EINVAL;
DRM_DEV_ERROR(dev,
"invalid channel node address: %u\n", i);
of_node_put(child);
return ret;
}
ldb_ch = ldb->channel[i];
ldb_ch->ldb = ldb;
ldb_ch->chno = i;
ldb_ch->is_available = true;
ldb_ch->np = child;
ldb->available_ch_cnt++;
}
return 0;
}
EXPORT_SYMBOL_GPL(ldb_init_helper);
int ldb_find_next_bridge_helper(struct ldb *ldb)
{
struct device *dev = ldb->dev;
struct ldb_channel *ldb_ch;
int ret, i;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
ldb_ch = ldb->channel[i];
if (!ldb_ch->is_available)
continue;
ldb_ch->next_bridge = devm_drm_of_get_bridge(dev, ldb_ch->np,
1, 0);
if (IS_ERR(ldb_ch->next_bridge)) {
ret = PTR_ERR(ldb_ch->next_bridge);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev,
"failed to get next bridge: %d\n",
ret);
return ret;
}
}
return 0;
}
EXPORT_SYMBOL_GPL(ldb_find_next_bridge_helper);
void ldb_add_bridge_helper(struct ldb *ldb)
{
struct ldb_channel *ldb_ch;
int i;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
ldb_ch = ldb->channel[i];
if (!ldb_ch->is_available)
continue;
ldb_ch->bridge.driver_private = ldb_ch;
ldb_ch->bridge.of_node = ldb_ch->np;
drm_bridge_add(&ldb_ch->bridge);
}
}
EXPORT_SYMBOL_GPL(ldb_add_bridge_helper);
void ldb_remove_bridge_helper(struct ldb *ldb)
{
struct ldb_channel *ldb_ch;
int i;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
ldb_ch = ldb->channel[i];
if (!ldb_ch->is_available)
continue;
drm_bridge_remove(&ldb_ch->bridge);
}
}
EXPORT_SYMBOL_GPL(ldb_remove_bridge_helper);
MODULE_DESCRIPTION("i.MX8 LVDS Display Bridge(LDB)/Pixel Mapper bridge helper");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,95 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Copyright 2019,2020,2022 NXP
*/
#ifndef __IMX_LDB_HELPER__
#define __IMX_LDB_HELPER__
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/of.h>
#include <linux/regmap.h>
#include <drm/drm_atomic.h>
#include <drm/drm_bridge.h>
#include <drm/drm_device.h>
#include <drm/drm_encoder.h>
#include <drm/drm_modeset_helper_vtables.h>
#define LDB_CH0_MODE_EN_TO_DI0 BIT(0)
#define LDB_CH0_MODE_EN_TO_DI1 (3 << 0)
#define LDB_CH0_MODE_EN_MASK (3 << 0)
#define LDB_CH1_MODE_EN_TO_DI0 BIT(2)
#define LDB_CH1_MODE_EN_TO_DI1 (3 << 2)
#define LDB_CH1_MODE_EN_MASK (3 << 2)
#define LDB_SPLIT_MODE_EN BIT(4)
#define LDB_DATA_WIDTH_CH0_24 BIT(5)
#define LDB_BIT_MAP_CH0_JEIDA BIT(6)
#define LDB_DATA_WIDTH_CH1_24 BIT(7)
#define LDB_BIT_MAP_CH1_JEIDA BIT(8)
#define LDB_DI0_VS_POL_ACT_LOW BIT(9)
#define LDB_DI1_VS_POL_ACT_LOW BIT(10)
#define MAX_LDB_CHAN_NUM 2
enum ldb_channel_link_type {
LDB_CH_SINGLE_LINK,
LDB_CH_DUAL_LINK_EVEN_ODD_PIXELS,
LDB_CH_DUAL_LINK_ODD_EVEN_PIXELS,
};
struct ldb;
struct ldb_channel {
struct ldb *ldb;
struct drm_bridge bridge;
struct drm_bridge *next_bridge;
struct device_node *np;
u32 chno;
bool is_available;
u32 in_bus_format;
u32 out_bus_format;
enum ldb_channel_link_type link_type;
};
struct ldb {
struct regmap *regmap;
struct device *dev;
struct ldb_channel *channel[MAX_LDB_CHAN_NUM];
unsigned int ctrl_reg;
u32 ldb_ctrl;
unsigned int available_ch_cnt;
};
#define bridge_to_ldb_ch(b) container_of(b, struct ldb_channel, bridge)
bool ldb_channel_is_single_link(struct ldb_channel *ldb_ch);
bool ldb_channel_is_split_link(struct ldb_channel *ldb_ch);
int ldb_bridge_atomic_check_helper(struct drm_bridge *bridge,
struct drm_bridge_state *bridge_state,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state);
void ldb_bridge_mode_set_helper(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode);
void ldb_bridge_enable_helper(struct drm_bridge *bridge);
void ldb_bridge_disable_helper(struct drm_bridge *bridge);
int ldb_bridge_attach_helper(struct drm_bridge *bridge, struct drm_encoder *encoder,
enum drm_bridge_attach_flags flags);
int ldb_init_helper(struct ldb *ldb);
int ldb_find_next_bridge_helper(struct ldb *ldb);
void ldb_add_bridge_helper(struct ldb *ldb);
void ldb_remove_bridge_helper(struct ldb *ldb);
#endif /* __IMX_LDB_HELPER__ */
@@ -0,0 +1,173 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2025 NXP
*/
#include <linux/bitfield.h>
#include <linux/component.h>
#include <linux/module.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <drm/bridge/dw_hdmi.h>
#include <sound/asoundef.h>
#define HTX_PAI_CTRL 0x00
#define ENABLE BIT(0)
#define HTX_PAI_CTRL_EXT 0x04
#define WTMK_HIGH_MASK GENMASK(31, 24)
#define WTMK_LOW_MASK GENMASK(23, 16)
#define NUM_CH_MASK GENMASK(10, 8)
#define WTMK_HIGH(n) FIELD_PREP(WTMK_HIGH_MASK, (n))
#define WTMK_LOW(n) FIELD_PREP(WTMK_LOW_MASK, (n))
#define NUM_CH(n) FIELD_PREP(NUM_CH_MASK, (n) - 1)
#define HTX_PAI_FIELD_CTRL 0x08
#define PRE_SEL GENMASK(28, 24)
#define D_SEL GENMASK(23, 20)
#define V_SEL GENMASK(19, 15)
#define U_SEL GENMASK(14, 10)
#define C_SEL GENMASK(9, 5)
#define P_SEL GENMASK(4, 0)
struct imx8mp_hdmi_pai {
struct regmap *regmap;
struct device *dev;
};
static void imx8mp_hdmi_pai_enable(struct dw_hdmi *dw_hdmi, int channel,
int width, int rate, int non_pcm,
int iec958)
{
const struct dw_hdmi_plat_data *pdata = dw_hdmi_to_plat_data(dw_hdmi);
struct imx8mp_hdmi_pai *hdmi_pai = pdata->priv_audio;
int val;
if (pm_runtime_resume_and_get(hdmi_pai->dev) < 0)
return;
/* PAI set control extended */
val = WTMK_HIGH(3) | WTMK_LOW(3);
val |= NUM_CH(channel);
regmap_write(hdmi_pai->regmap, HTX_PAI_CTRL_EXT, val);
/* IEC60958 format */
if (iec958) {
val = FIELD_PREP_CONST(P_SEL,
__bf_shf(IEC958_SUBFRAME_PARITY));
val |= FIELD_PREP_CONST(C_SEL,
__bf_shf(IEC958_SUBFRAME_CHANNEL_STATUS));
val |= FIELD_PREP_CONST(U_SEL,
__bf_shf(IEC958_SUBFRAME_USER_DATA));
val |= FIELD_PREP_CONST(V_SEL,
__bf_shf(IEC958_SUBFRAME_VALIDITY));
val |= FIELD_PREP_CONST(D_SEL,
__bf_shf(IEC958_SUBFRAME_SAMPLE_24_MASK));
val |= FIELD_PREP_CONST(PRE_SEL,
__bf_shf(IEC958_SUBFRAME_PREAMBLE_MASK));
} else {
/*
* The allowed PCM widths are 24bit and 32bit, as they are supported
* by aud2htx module.
* for 24bit, D_SEL = 0, select all the bits.
* for 32bit, D_SEL = 8, select 24bit in MSB.
*/
val = FIELD_PREP(D_SEL, width - 24);
}
regmap_write(hdmi_pai->regmap, HTX_PAI_FIELD_CTRL, val);
/* PAI start running */
regmap_write(hdmi_pai->regmap, HTX_PAI_CTRL, ENABLE);
}
static void imx8mp_hdmi_pai_disable(struct dw_hdmi *dw_hdmi)
{
const struct dw_hdmi_plat_data *pdata = dw_hdmi_to_plat_data(dw_hdmi);
struct imx8mp_hdmi_pai *hdmi_pai = pdata->priv_audio;
/* Stop PAI */
regmap_write(hdmi_pai->regmap, HTX_PAI_CTRL, 0);
pm_runtime_put_sync(hdmi_pai->dev);
}
static const struct regmap_config imx8mp_hdmi_pai_regmap_config = {
.reg_bits = 32,
.reg_stride = 4,
.val_bits = 32,
.max_register = HTX_PAI_FIELD_CTRL,
};
static int imx8mp_hdmi_pai_bind(struct device *dev, struct device *master, void *data)
{
struct platform_device *pdev = to_platform_device(dev);
struct dw_hdmi_plat_data *plat_data = data;
struct imx8mp_hdmi_pai *hdmi_pai;
struct resource *res;
void __iomem *base;
int ret;
hdmi_pai = devm_kzalloc(dev, sizeof(*hdmi_pai), GFP_KERNEL);
if (!hdmi_pai)
return -ENOMEM;
base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
if (IS_ERR(base))
return PTR_ERR(base);
hdmi_pai->regmap = devm_regmap_init_mmio_clk(dev, "apb", base,
&imx8mp_hdmi_pai_regmap_config);
if (IS_ERR(hdmi_pai->regmap)) {
dev_err(dev, "regmap init failed\n");
return PTR_ERR(hdmi_pai->regmap);
}
plat_data->enable_audio = imx8mp_hdmi_pai_enable;
plat_data->disable_audio = imx8mp_hdmi_pai_disable;
plat_data->priv_audio = hdmi_pai;
hdmi_pai->dev = dev;
ret = devm_pm_runtime_enable(dev);
if (ret < 0) {
dev_err(dev, "failed to enable PM runtime: %d\n", ret);
return ret;
}
return 0;
}
static const struct component_ops imx8mp_hdmi_pai_ops = {
.bind = imx8mp_hdmi_pai_bind,
};
static int imx8mp_hdmi_pai_probe(struct platform_device *pdev)
{
return component_add(&pdev->dev, &imx8mp_hdmi_pai_ops);
}
static void imx8mp_hdmi_pai_remove(struct platform_device *pdev)
{
component_del(&pdev->dev, &imx8mp_hdmi_pai_ops);
}
static const struct of_device_id imx8mp_hdmi_pai_of_table[] = {
{ .compatible = "fsl,imx8mp-hdmi-pai" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8mp_hdmi_pai_of_table);
static struct platform_driver imx8mp_hdmi_pai_platform_driver = {
.probe = imx8mp_hdmi_pai_probe,
.remove = imx8mp_hdmi_pai_remove,
.driver = {
.name = "imx8mp-hdmi-pai",
.of_match_table = imx8mp_hdmi_pai_of_table,
},
};
module_platform_driver(imx8mp_hdmi_pai_platform_driver);
MODULE_DESCRIPTION("i.MX8MP HDMI PAI driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (C) 2022 Pengutronix, Lucas Stach <kernel@pengutronix.de>
*/
#include <drm/drm_atomic_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_crtc.h>
#include <linux/bitfield.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_graph.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#define HTX_PVI_CTRL 0x0
#define PVI_CTRL_OP_VSYNC_POL BIT(18)
#define PVI_CTRL_OP_HSYNC_POL BIT(17)
#define PVI_CTRL_OP_DE_POL BIT(16)
#define PVI_CTRL_INP_VSYNC_POL BIT(14)
#define PVI_CTRL_INP_HSYNC_POL BIT(13)
#define PVI_CTRL_INP_DE_POL BIT(12)
#define PVI_CTRL_MODE_MASK GENMASK(2, 1)
#define PVI_CTRL_MODE_LCDIF 2
#define PVI_CTRL_EN BIT(0)
struct imx8mp_hdmi_pvi {
struct drm_bridge bridge;
struct device *dev;
void __iomem *regs;
};
static inline struct imx8mp_hdmi_pvi *
to_imx8mp_hdmi_pvi(struct drm_bridge *bridge)
{
return container_of(bridge, struct imx8mp_hdmi_pvi, bridge);
}
static int imx8mp_hdmi_pvi_bridge_attach(struct drm_bridge *bridge,
struct drm_encoder *encoder,
enum drm_bridge_attach_flags flags)
{
struct imx8mp_hdmi_pvi *pvi = to_imx8mp_hdmi_pvi(bridge);
return drm_bridge_attach(encoder, pvi->bridge.next_bridge,
bridge, flags);
}
static void imx8mp_hdmi_pvi_bridge_enable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct imx8mp_hdmi_pvi *pvi = to_imx8mp_hdmi_pvi(bridge);
struct drm_connector_state *conn_state;
struct drm_bridge_state *bridge_state;
const struct drm_display_mode *mode;
struct drm_crtc_state *crtc_state;
struct drm_connector *connector;
u32 bus_flags = 0, val;
bridge_state = drm_atomic_get_new_bridge_state(state, bridge);
connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder);
conn_state = drm_atomic_get_new_connector_state(state, connector);
crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc);
if (WARN_ON(pm_runtime_resume_and_get(pvi->dev)))
return;
mode = &crtc_state->adjusted_mode;
val = FIELD_PREP(PVI_CTRL_MODE_MASK, PVI_CTRL_MODE_LCDIF) | PVI_CTRL_EN;
if (mode->flags & DRM_MODE_FLAG_PVSYNC)
val |= PVI_CTRL_OP_VSYNC_POL | PVI_CTRL_INP_VSYNC_POL;
if (mode->flags & DRM_MODE_FLAG_PHSYNC)
val |= PVI_CTRL_OP_HSYNC_POL | PVI_CTRL_INP_HSYNC_POL;
if (pvi->bridge.next_bridge->timings)
bus_flags = pvi->bridge.next_bridge->timings->input_bus_flags;
else if (bridge_state)
bus_flags = bridge_state->input_bus_cfg.flags;
if (bus_flags & DRM_BUS_FLAG_DE_HIGH)
val |= PVI_CTRL_OP_DE_POL | PVI_CTRL_INP_DE_POL;
writel(val, pvi->regs + HTX_PVI_CTRL);
}
static void imx8mp_hdmi_pvi_bridge_disable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct imx8mp_hdmi_pvi *pvi = to_imx8mp_hdmi_pvi(bridge);
writel(0x0, pvi->regs + HTX_PVI_CTRL);
pm_runtime_put(pvi->dev);
}
static u32 *
imx8mp_hdmi_pvi_bridge_get_input_bus_fmts(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)
{
struct imx8mp_hdmi_pvi *pvi = to_imx8mp_hdmi_pvi(bridge);
struct drm_bridge *next_bridge = pvi->bridge.next_bridge;
struct drm_bridge_state *next_state;
if (!next_bridge->funcs->atomic_get_input_bus_fmts)
return NULL;
next_state = drm_atomic_get_new_bridge_state(crtc_state->state,
next_bridge);
return next_bridge->funcs->atomic_get_input_bus_fmts(next_bridge,
next_state,
crtc_state,
conn_state,
output_fmt,
num_input_fmts);
}
static const struct drm_bridge_funcs imx_hdmi_pvi_bridge_funcs = {
.attach = imx8mp_hdmi_pvi_bridge_attach,
.atomic_enable = imx8mp_hdmi_pvi_bridge_enable,
.atomic_disable = imx8mp_hdmi_pvi_bridge_disable,
.atomic_get_input_bus_fmts = imx8mp_hdmi_pvi_bridge_get_input_bus_fmts,
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
};
static int imx8mp_hdmi_pvi_probe(struct platform_device *pdev)
{
struct device_node *remote;
struct imx8mp_hdmi_pvi *pvi;
pvi = devm_drm_bridge_alloc(&pdev->dev, struct imx8mp_hdmi_pvi,
bridge, &imx_hdmi_pvi_bridge_funcs);
if (IS_ERR(pvi))
return PTR_ERR(pvi);
platform_set_drvdata(pdev, pvi);
pvi->dev = &pdev->dev;
pvi->regs = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(pvi->regs))
return PTR_ERR(pvi->regs);
/* Get the next bridge in the pipeline. */
remote = of_graph_get_remote_node(pdev->dev.of_node, 1, -1);
if (!remote)
return -EINVAL;
pvi->bridge.next_bridge = of_drm_find_and_get_bridge(remote);
of_node_put(remote);
if (!pvi->bridge.next_bridge)
return dev_err_probe(&pdev->dev, -EPROBE_DEFER,
"could not find next bridge\n");
pm_runtime_enable(&pdev->dev);
/* Register the bridge. */
pvi->bridge.of_node = pdev->dev.of_node;
pvi->bridge.timings = pvi->bridge.next_bridge->timings;
drm_bridge_add(&pvi->bridge);
return 0;
}
static void imx8mp_hdmi_pvi_remove(struct platform_device *pdev)
{
struct imx8mp_hdmi_pvi *pvi = platform_get_drvdata(pdev);
drm_bridge_remove(&pvi->bridge);
pm_runtime_disable(&pdev->dev);
}
static const struct of_device_id imx8mp_hdmi_pvi_match[] = {
{
.compatible = "fsl,imx8mp-hdmi-pvi",
}, {
/* sentinel */
}
};
MODULE_DEVICE_TABLE(of, imx8mp_hdmi_pvi_match);
static struct platform_driver imx8mp_hdmi_pvi_driver = {
.probe = imx8mp_hdmi_pvi_probe,
.remove = imx8mp_hdmi_pvi_remove,
.driver = {
.name = "imx-hdmi-pvi",
.of_match_table = imx8mp_hdmi_pvi_match,
},
};
module_platform_driver(imx8mp_hdmi_pvi_driver);
MODULE_DESCRIPTION("i.MX8MP HDMI TX Parallel Video Interface bridge driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Add an hdmi-connector node to boards using the imx8mp hdmi_tx which
* don't have one. This is needed for the i.MX LCDIF to work with
* DRM_BRIDGE_ATTACH_NO_CONNECTOR.
*
* Copyright (C) 2026 GE HealthCare
* Author: Luca Ceresoli <luca.ceresoli@bootlin.com>
*/
#include <linux/cleanup.h>
#include <linux/init.h>
#include <linux/of.h>
#include <linux/of_graph.h>
/* Embedded dtbo symbols created by cmd_wrap_S_dtb in scripts/Makefile.dtbs */
extern char __dtbo_imx8mp_hdmi_tx_connector_fixup_begin[];
extern char __dtbo_imx8mp_hdmi_tx_connector_fixup_end[];
static int __init imx8mp_hdmi_tx_connector_fixup_init(void)
{
struct device_node *soc __free(device_node) = NULL;
struct device_node *hdmi_tx __free(device_node) = NULL;
struct device_node *endpoint __free(device_node) = NULL;
void *dtbo_start;
u32 dtbo_size;
int ovcs_id;
int err;
soc = of_find_node_by_path("/soc@0");
if (!soc)
return 0;
/* This applies to i.MX8MP only, do nothing on other systems */
if (!of_device_is_compatible(soc, "fsl,imx8mp-soc"))
return 0;
hdmi_tx = of_find_node_by_path("/soc@0/bus@32c00000/hdmi@32fd8000");
if (!of_device_is_available(hdmi_tx))
return 0;
/* If endpoint exists, assume an hdmi-connector exists already */
endpoint = of_graph_get_endpoint_by_regs(hdmi_tx, 1, -1);
if (endpoint)
return 0;
/*
* Boards with an HDMI connector should describe it in a device
* tree node with compatible = "hdmi-connector".
*
* If you see this warning, it means such a node was not found and
* a fallback one is added using a device tree overlay. Please add
* one in your device tree, also describing the exact connector
* type (the added overlay assumes Type A as a fallback, but it
* might be wrong).
*
* This node is necessary for modern DRM, where bridge drivers do
* not create a connector (see the DRM_BRIDGE_ATTACH_NO_CONNECTOR
* flag). See https://docs.kernel.org/gpu/drm-kms-helpers.html for
* more info.
*/
pr_warn("Please add a hdmi-connector DT node for imx8mp-hdmi-tx.\n");
dtbo_start = __dtbo_imx8mp_hdmi_tx_connector_fixup_begin;
dtbo_size = __dtbo_imx8mp_hdmi_tx_connector_fixup_end -
__dtbo_imx8mp_hdmi_tx_connector_fixup_begin;
err = of_overlay_fdt_apply(dtbo_start, dtbo_size, &ovcs_id, NULL);
if (err)
err = of_overlay_remove(&ovcs_id);
return err;
}
subsys_initcall(imx8mp_hdmi_tx_connector_fixup_init);
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* DTS overlay adding an hdmi-connector node to boards using the imx8mp hdmi_tx
*
* Copyright (C) 2026 GE HealthCare
* Author: Luca Ceresoli <luca.ceresoli@bootlin.com>
*/
/dts-v1/;
/plugin/;
&{/} {
fixup-hdmi-connector {
compatible = "hdmi-connector";
label = "HDMI";
type = "a";
port {
fixup_hdmi_connector_in: endpoint {
remote-endpoint = <&hdmi_tx_out>;
};
};
};
};
&{/soc@0/bus@32c00000/hdmi@32fd8000/ports/port@1} {
hdmi_tx_out: endpoint {
remote-endpoint = <&fixup_hdmi_connector_in>;
};
};
@@ -0,0 +1,213 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (C) 2022 Pengutronix, Lucas Stach <kernel@pengutronix.de>
*/
#include <linux/clk.h>
#include <linux/component.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <drm/bridge/dw_hdmi.h>
#include <drm/drm_modes.h>
#include <drm/drm_of.h>
struct imx8mp_hdmi {
struct dw_hdmi_plat_data plat_data;
struct dw_hdmi *dw_hdmi;
struct clk *pixclk;
};
static enum drm_mode_status
imx8mp_hdmi_mode_valid(struct dw_hdmi *dw_hdmi, void *data,
const struct drm_display_info *info,
const struct drm_display_mode *mode)
{
struct imx8mp_hdmi *hdmi = (struct imx8mp_hdmi *)data;
long round_rate;
if (mode->clock < 13500)
return MODE_CLOCK_LOW;
if (mode->clock > 297000)
return MODE_CLOCK_HIGH;
round_rate = clk_round_rate(hdmi->pixclk, mode->clock * 1000);
/* imx8mp's pixel clock generator (fsl-samsung-hdmi) cannot generate
* all possible frequencies, so allow some tolerance to support more
* modes.
* Allow 0.5% difference allowed in various standards (VESA, CEA861)
* 0.5% = 5/1000 tolerance (mode->clock is 1/1000)
*/
if (abs(round_rate - mode->clock * 1000) > mode->clock * 5)
return MODE_CLOCK_RANGE;
/* We don't support double-clocked and Interlaced modes */
if ((mode->flags & DRM_MODE_FLAG_DBLCLK) ||
(mode->flags & DRM_MODE_FLAG_INTERLACE))
return MODE_BAD;
return MODE_OK;
}
static int imx8mp_hdmi_phy_init(struct dw_hdmi *dw_hdmi, void *data,
const struct drm_display_info *display,
const struct drm_display_mode *mode)
{
return 0;
}
static void imx8mp_hdmi_phy_disable(struct dw_hdmi *dw_hdmi, void *data)
{
}
static void im8mp_hdmi_phy_setup_hpd(struct dw_hdmi *hdmi, void *data)
{
/*
* Just release PHY core from reset, all other power management is done
* by the PHY driver.
*/
dw_hdmi_phy_gen1_reset(hdmi);
dw_hdmi_phy_setup_hpd(hdmi, data);
}
static const struct dw_hdmi_phy_ops imx8mp_hdmi_phy_ops = {
.init = imx8mp_hdmi_phy_init,
.disable = imx8mp_hdmi_phy_disable,
.setup_hpd = im8mp_hdmi_phy_setup_hpd,
.read_hpd = dw_hdmi_phy_read_hpd,
.update_hpd = dw_hdmi_phy_update_hpd,
};
static int imx8mp_dw_hdmi_bind(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct imx8mp_hdmi *hdmi = dev_get_drvdata(dev);
int ret;
ret = component_bind_all(dev, &hdmi->plat_data);
if (ret)
return dev_err_probe(dev, ret, "component_bind_all failed!\n");
hdmi->dw_hdmi = dw_hdmi_probe(pdev, &hdmi->plat_data);
if (IS_ERR(hdmi->dw_hdmi)) {
component_unbind_all(dev, &hdmi->plat_data);
return PTR_ERR(hdmi->dw_hdmi);
}
return 0;
}
static void imx8mp_dw_hdmi_unbind(struct device *dev)
{
struct imx8mp_hdmi *hdmi = dev_get_drvdata(dev);
dw_hdmi_remove(hdmi->dw_hdmi);
component_unbind_all(dev, &hdmi->plat_data);
}
static const struct component_master_ops imx8mp_dw_hdmi_ops = {
.bind = imx8mp_dw_hdmi_bind,
.unbind = imx8mp_dw_hdmi_unbind,
};
static int imx8mp_dw_hdmi_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct dw_hdmi_plat_data *plat_data;
struct component_match *match = NULL;
struct device_node *remote;
struct imx8mp_hdmi *hdmi;
hdmi = devm_kzalloc(dev, sizeof(*hdmi), GFP_KERNEL);
if (!hdmi)
return -ENOMEM;
plat_data = &hdmi->plat_data;
hdmi->pixclk = devm_clk_get(dev, "pix");
if (IS_ERR(hdmi->pixclk))
return dev_err_probe(dev, PTR_ERR(hdmi->pixclk),
"Unable to get pixel clock\n");
plat_data->mode_valid = imx8mp_hdmi_mode_valid;
plat_data->phy_ops = &imx8mp_hdmi_phy_ops;
plat_data->phy_name = "SAMSUNG HDMI TX PHY";
plat_data->priv_data = hdmi;
plat_data->phy_force_vendor = true;
plat_data->output_port = 1;
platform_set_drvdata(pdev, hdmi);
/* port@2 is for hdmi_pai device */
remote = of_graph_get_remote_node(pdev->dev.of_node, 2, 0);
if (!remote) {
hdmi->dw_hdmi = dw_hdmi_probe(pdev, plat_data);
if (IS_ERR(hdmi->dw_hdmi))
return PTR_ERR(hdmi->dw_hdmi);
} else {
drm_of_component_match_add(dev, &match, component_compare_of, remote);
of_node_put(remote);
return component_master_add_with_match(dev, &imx8mp_dw_hdmi_ops, match);
}
return 0;
}
static void imx8mp_dw_hdmi_remove(struct platform_device *pdev)
{
struct imx8mp_hdmi *hdmi = platform_get_drvdata(pdev);
struct device_node *remote;
remote = of_graph_get_remote_node(pdev->dev.of_node, 2, 0);
if (remote) {
of_node_put(remote);
component_master_del(&pdev->dev, &imx8mp_dw_hdmi_ops);
} else {
dw_hdmi_remove(hdmi->dw_hdmi);
}
}
static int imx8mp_dw_hdmi_pm_suspend(struct device *dev)
{
return 0;
}
static int imx8mp_dw_hdmi_pm_resume(struct device *dev)
{
struct imx8mp_hdmi *hdmi = dev_get_drvdata(dev);
dw_hdmi_resume(hdmi->dw_hdmi);
return 0;
}
static const struct dev_pm_ops imx8mp_dw_hdmi_pm_ops = {
SYSTEM_SLEEP_PM_OPS(imx8mp_dw_hdmi_pm_suspend, imx8mp_dw_hdmi_pm_resume)
};
static const struct of_device_id imx8mp_dw_hdmi_of_table[] = {
{ .compatible = "fsl,imx8mp-hdmi-tx" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8mp_dw_hdmi_of_table);
static struct platform_driver imx8mp_dw_hdmi_platform_driver = {
.probe = imx8mp_dw_hdmi_probe,
.remove = imx8mp_dw_hdmi_remove,
.driver = {
.name = "imx8mp-dw-hdmi-tx",
.of_match_table = imx8mp_dw_hdmi_of_table,
.pm = pm_ptr(&imx8mp_dw_hdmi_pm_ops),
},
};
module_platform_driver(imx8mp_dw_hdmi_platform_driver);
MODULE_DESCRIPTION("i.MX8MP HDMI encoder driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,589 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2020 NXP
*/
#include <linux/clk.h>
#include <linux/media-bus-format.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_graph.h>
#include <linux/phy/phy.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_connector.h>
#include <drm/drm_fourcc.h>
#include <drm/drm_of.h>
#include <drm/drm_print.h>
#include "imx-ldb-helper.h"
#define LDB_CH0_10BIT_EN BIT(22)
#define LDB_CH1_10BIT_EN BIT(23)
#define LDB_CH0_DATA_WIDTH_24BIT BIT(24)
#define LDB_CH1_DATA_WIDTH_24BIT BIT(26)
#define LDB_CH0_DATA_WIDTH_30BIT (2 << 24)
#define LDB_CH1_DATA_WIDTH_30BIT (2 << 26)
#define SS_CTRL 0x20
#define CH_HSYNC_M(id) BIT(0 + ((id) * 2))
#define CH_VSYNC_M(id) BIT(1 + ((id) * 2))
#define CH_PHSYNC(id) BIT(0 + ((id) * 2))
#define CH_PVSYNC(id) BIT(1 + ((id) * 2))
#define DRIVER_NAME "imx8qm-ldb"
struct imx8qm_ldb_channel {
struct ldb_channel base;
struct phy *phy;
};
struct imx8qm_ldb {
struct ldb base;
struct device *dev;
struct imx8qm_ldb_channel *channel[MAX_LDB_CHAN_NUM];
struct clk *clk_pixel;
struct clk *clk_bypass;
int active_chno;
};
static inline struct imx8qm_ldb_channel *
base_to_imx8qm_ldb_channel(struct ldb_channel *base)
{
return container_of(base, struct imx8qm_ldb_channel, base);
}
static inline struct imx8qm_ldb *base_to_imx8qm_ldb(struct ldb *base)
{
return container_of(base, struct imx8qm_ldb, base);
}
static void imx8qm_ldb_set_phy_cfg(struct imx8qm_ldb *imx8qm_ldb,
unsigned long di_clk,
bool is_split, bool is_slave,
struct phy_configure_opts_lvds *phy_cfg)
{
phy_cfg->bits_per_lane_and_dclk_cycle = 7;
phy_cfg->lanes = 4;
phy_cfg->differential_clk_rate = is_split ? di_clk / 2 : di_clk;
phy_cfg->is_slave = is_slave;
}
static int imx8qm_ldb_bridge_atomic_check(struct drm_bridge *bridge,
struct drm_bridge_state *bridge_state,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qm_ldb_channel *imx8qm_ldb_ch =
base_to_imx8qm_ldb_channel(ldb_ch);
struct imx8qm_ldb *imx8qm_ldb = base_to_imx8qm_ldb(ldb);
struct drm_display_mode *adj = &crtc_state->adjusted_mode;
unsigned long di_clk = adj->clock * 1000;
bool is_split = ldb_channel_is_split_link(ldb_ch);
union phy_configure_opts opts = { };
struct phy_configure_opts_lvds *phy_cfg = &opts.lvds;
int ret;
ret = ldb_bridge_atomic_check_helper(bridge, bridge_state,
crtc_state, conn_state);
if (ret)
return ret;
imx8qm_ldb_set_phy_cfg(imx8qm_ldb, di_clk, is_split, false, phy_cfg);
ret = phy_validate(imx8qm_ldb_ch->phy, PHY_MODE_LVDS, 0, &opts);
if (ret < 0) {
DRM_DEV_DEBUG_DRIVER(imx8qm_ldb->dev,
"failed to validate PHY: %d\n", ret);
return ret;
}
if (is_split) {
imx8qm_ldb_ch =
imx8qm_ldb->channel[imx8qm_ldb->active_chno ^ 1];
imx8qm_ldb_set_phy_cfg(imx8qm_ldb, di_clk, is_split, true,
phy_cfg);
ret = phy_validate(imx8qm_ldb_ch->phy, PHY_MODE_LVDS, 0, &opts);
if (ret < 0) {
DRM_DEV_DEBUG_DRIVER(imx8qm_ldb->dev,
"failed to validate slave PHY: %d\n",
ret);
return ret;
}
}
return ret;
}
static void
imx8qm_ldb_bridge_mode_set(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qm_ldb_channel *imx8qm_ldb_ch =
base_to_imx8qm_ldb_channel(ldb_ch);
struct imx8qm_ldb *imx8qm_ldb = base_to_imx8qm_ldb(ldb);
struct device *dev = imx8qm_ldb->dev;
unsigned long di_clk = adjusted_mode->clock * 1000;
bool is_split = ldb_channel_is_split_link(ldb_ch);
union phy_configure_opts opts = { };
struct phy_configure_opts_lvds *phy_cfg = &opts.lvds;
u32 chno = ldb_ch->chno;
int ret;
ret = pm_runtime_get_sync(dev);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to get runtime PM sync: %d\n", ret);
ret = phy_init(imx8qm_ldb_ch->phy);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to initialize PHY: %d\n", ret);
clk_set_rate(imx8qm_ldb->clk_bypass, di_clk);
clk_set_rate(imx8qm_ldb->clk_pixel, di_clk);
imx8qm_ldb_set_phy_cfg(imx8qm_ldb, di_clk, is_split, false, phy_cfg);
ret = phy_configure(imx8qm_ldb_ch->phy, &opts);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to configure PHY: %d\n", ret);
if (is_split) {
imx8qm_ldb_ch =
imx8qm_ldb->channel[imx8qm_ldb->active_chno ^ 1];
imx8qm_ldb_set_phy_cfg(imx8qm_ldb, di_clk, is_split, true,
phy_cfg);
ret = phy_configure(imx8qm_ldb_ch->phy, &opts);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to configure slave PHY: %d\n",
ret);
}
/* input VSYNC signal from pixel link is active low */
if (ldb_ch->chno == 0 || is_split)
ldb->ldb_ctrl |= LDB_DI0_VS_POL_ACT_LOW;
if (ldb_ch->chno == 1 || is_split)
ldb->ldb_ctrl |= LDB_DI1_VS_POL_ACT_LOW;
switch (ldb_ch->out_bus_format) {
case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
break;
case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
if (ldb_ch->chno == 0 || is_split)
ldb->ldb_ctrl |= LDB_CH0_DATA_WIDTH_24BIT;
if (ldb_ch->chno == 1 || is_split)
ldb->ldb_ctrl |= LDB_CH1_DATA_WIDTH_24BIT;
break;
}
ldb_bridge_mode_set_helper(bridge, mode, adjusted_mode);
if (adjusted_mode->flags & DRM_MODE_FLAG_NVSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL, CH_VSYNC_M(chno), 0);
else if (adjusted_mode->flags & DRM_MODE_FLAG_PVSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL,
CH_VSYNC_M(chno), CH_PVSYNC(chno));
if (adjusted_mode->flags & DRM_MODE_FLAG_NHSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL, CH_HSYNC_M(chno), 0);
else if (adjusted_mode->flags & DRM_MODE_FLAG_PHSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL,
CH_HSYNC_M(chno), CH_PHSYNC(chno));
}
static void imx8qm_ldb_bridge_atomic_enable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qm_ldb_channel *imx8qm_ldb_ch =
base_to_imx8qm_ldb_channel(ldb_ch);
struct imx8qm_ldb *imx8qm_ldb = base_to_imx8qm_ldb(ldb);
struct device *dev = imx8qm_ldb->dev;
bool is_split = ldb_channel_is_split_link(ldb_ch);
int ret;
clk_prepare_enable(imx8qm_ldb->clk_pixel);
clk_prepare_enable(imx8qm_ldb->clk_bypass);
/* both DI0 and DI1 connect with pixel link, so ok to use DI0 only */
if (ldb_ch->chno == 0 || is_split) {
ldb->ldb_ctrl &= ~LDB_CH0_MODE_EN_MASK;
ldb->ldb_ctrl |= LDB_CH0_MODE_EN_TO_DI0;
}
if (ldb_ch->chno == 1 || is_split) {
ldb->ldb_ctrl &= ~LDB_CH1_MODE_EN_MASK;
ldb->ldb_ctrl |= LDB_CH1_MODE_EN_TO_DI0;
}
if (is_split) {
ret = phy_power_on(imx8qm_ldb->channel[0]->phy);
if (ret)
DRM_DEV_ERROR(dev,
"failed to power on channel0 PHY: %d\n",
ret);
ret = phy_power_on(imx8qm_ldb->channel[1]->phy);
if (ret)
DRM_DEV_ERROR(dev,
"failed to power on channel1 PHY: %d\n",
ret);
} else {
ret = phy_power_on(imx8qm_ldb_ch->phy);
if (ret)
DRM_DEV_ERROR(dev, "failed to power on PHY: %d\n", ret);
}
ldb_bridge_enable_helper(bridge);
}
static void imx8qm_ldb_bridge_atomic_disable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qm_ldb_channel *imx8qm_ldb_ch =
base_to_imx8qm_ldb_channel(ldb_ch);
struct imx8qm_ldb *imx8qm_ldb = base_to_imx8qm_ldb(ldb);
struct device *dev = imx8qm_ldb->dev;
bool is_split = ldb_channel_is_split_link(ldb_ch);
int ret;
ldb_bridge_disable_helper(bridge);
if (is_split) {
ret = phy_power_off(imx8qm_ldb->channel[0]->phy);
if (ret)
DRM_DEV_ERROR(dev,
"failed to power off channel0 PHY: %d\n",
ret);
ret = phy_power_off(imx8qm_ldb->channel[1]->phy);
if (ret)
DRM_DEV_ERROR(dev,
"failed to power off channel1 PHY: %d\n",
ret);
} else {
ret = phy_power_off(imx8qm_ldb_ch->phy);
if (ret)
DRM_DEV_ERROR(dev, "failed to power off PHY: %d\n", ret);
}
clk_disable_unprepare(imx8qm_ldb->clk_bypass);
clk_disable_unprepare(imx8qm_ldb->clk_pixel);
pm_runtime_put(dev);
}
static const u32 imx8qm_ldb_bus_output_fmts[] = {
MEDIA_BUS_FMT_RGB666_1X7X3_SPWG,
MEDIA_BUS_FMT_RGB888_1X7X4_SPWG,
MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA,
MEDIA_BUS_FMT_FIXED,
};
static bool imx8qm_ldb_bus_output_fmt_supported(u32 fmt)
{
int i;
for (i = 0; i < ARRAY_SIZE(imx8qm_ldb_bus_output_fmts); i++) {
if (imx8qm_ldb_bus_output_fmts[i] == fmt)
return true;
}
return false;
}
static u32 *
imx8qm_ldb_bridge_atomic_get_input_bus_fmts(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)
{
struct drm_display_info *di;
const struct drm_format_info *finfo;
u32 *input_fmts;
if (!imx8qm_ldb_bus_output_fmt_supported(output_fmt))
return NULL;
*num_input_fmts = 1;
input_fmts = kmalloc_obj(*input_fmts);
if (!input_fmts)
return NULL;
switch (output_fmt) {
case MEDIA_BUS_FMT_FIXED:
di = &conn_state->connector->display_info;
/*
* Look at the first bus format to determine input format.
* Default to MEDIA_BUS_FMT_RGB888_1X36_CPADLO, if no match.
*/
if (di->num_bus_formats) {
finfo = drm_format_info(di->bus_formats[0]);
input_fmts[0] = finfo->depth == 18 ?
MEDIA_BUS_FMT_RGB666_1X36_CPADLO :
MEDIA_BUS_FMT_RGB888_1X36_CPADLO;
} else {
input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X36_CPADLO;
}
break;
case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
input_fmts[0] = MEDIA_BUS_FMT_RGB666_1X36_CPADLO;
break;
case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X36_CPADLO;
break;
default:
kfree(input_fmts);
input_fmts = NULL;
break;
}
return input_fmts;
}
static u32 *
imx8qm_ldb_bridge_atomic_get_output_bus_fmts(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)
{
*num_output_fmts = ARRAY_SIZE(imx8qm_ldb_bus_output_fmts);
return kmemdup(imx8qm_ldb_bus_output_fmts,
sizeof(imx8qm_ldb_bus_output_fmts), GFP_KERNEL);
}
static enum drm_mode_status
imx8qm_ldb_bridge_mode_valid(struct drm_bridge *bridge,
const struct drm_display_info *info,
const struct drm_display_mode *mode)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
bool is_single = ldb_channel_is_single_link(ldb_ch);
if (mode->clock > 300000)
return MODE_CLOCK_HIGH;
if (mode->clock > 150000 && is_single)
return MODE_CLOCK_HIGH;
return MODE_OK;
}
static const struct drm_bridge_funcs imx8qm_ldb_bridge_funcs = {
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.mode_valid = imx8qm_ldb_bridge_mode_valid,
.attach = ldb_bridge_attach_helper,
.atomic_check = imx8qm_ldb_bridge_atomic_check,
.mode_set = imx8qm_ldb_bridge_mode_set,
.atomic_enable = imx8qm_ldb_bridge_atomic_enable,
.atomic_disable = imx8qm_ldb_bridge_atomic_disable,
.atomic_get_input_bus_fmts =
imx8qm_ldb_bridge_atomic_get_input_bus_fmts,
.atomic_get_output_bus_fmts =
imx8qm_ldb_bridge_atomic_get_output_bus_fmts,
};
static int imx8qm_ldb_get_phy(struct imx8qm_ldb *imx8qm_ldb)
{
struct imx8qm_ldb_channel *imx8qm_ldb_ch;
struct ldb_channel *ldb_ch;
struct device *dev = imx8qm_ldb->dev;
int i, ret;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
imx8qm_ldb_ch = imx8qm_ldb->channel[i];
ldb_ch = &imx8qm_ldb_ch->base;
if (!ldb_ch->is_available)
continue;
imx8qm_ldb_ch->phy = devm_of_phy_get(dev, ldb_ch->np,
"lvds_phy");
if (IS_ERR(imx8qm_ldb_ch->phy)) {
ret = PTR_ERR(imx8qm_ldb_ch->phy);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev,
"failed to get channel%d PHY: %d\n",
i, ret);
return ret;
}
}
return 0;
}
static int imx8qm_ldb_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct imx8qm_ldb *imx8qm_ldb;
struct imx8qm_ldb_channel *imx8qm_ldb_ch;
struct ldb *ldb;
struct ldb_channel *ldb_ch;
struct device_node *port1, *port2;
int pixel_order;
int ret, i;
imx8qm_ldb = devm_kzalloc(dev, sizeof(*imx8qm_ldb), GFP_KERNEL);
if (!imx8qm_ldb)
return -ENOMEM;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
imx8qm_ldb->channel[i] =
devm_drm_bridge_alloc(dev, struct imx8qm_ldb_channel, base.bridge,
&imx8qm_ldb_bridge_funcs);
if (IS_ERR(imx8qm_ldb->channel[i]))
return PTR_ERR(imx8qm_ldb->channel[i]);
}
imx8qm_ldb->clk_pixel = devm_clk_get(dev, "pixel");
if (IS_ERR(imx8qm_ldb->clk_pixel)) {
ret = PTR_ERR(imx8qm_ldb->clk_pixel);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev,
"failed to get pixel clock: %d\n", ret);
return ret;
}
imx8qm_ldb->clk_bypass = devm_clk_get(dev, "bypass");
if (IS_ERR(imx8qm_ldb->clk_bypass)) {
ret = PTR_ERR(imx8qm_ldb->clk_bypass);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev,
"failed to get bypass clock: %d\n", ret);
return ret;
}
imx8qm_ldb->dev = dev;
ldb = &imx8qm_ldb->base;
ldb->dev = dev;
ldb->ctrl_reg = 0xe0;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++)
ldb->channel[i] = &imx8qm_ldb->channel[i]->base;
ret = ldb_init_helper(ldb);
if (ret)
return ret;
if (ldb->available_ch_cnt == 0) {
DRM_DEV_DEBUG_DRIVER(dev, "no available channel\n");
return 0;
}
if (ldb->available_ch_cnt == 2) {
port1 = of_graph_get_port_by_id(ldb->channel[0]->np, 1);
port2 = of_graph_get_port_by_id(ldb->channel[1]->np, 1);
pixel_order =
drm_of_lvds_get_dual_link_pixel_order(port1, port2);
of_node_put(port1);
of_node_put(port2);
if (pixel_order != DRM_LVDS_DUAL_LINK_ODD_EVEN_PIXELS) {
DRM_DEV_ERROR(dev, "invalid dual link pixel order: %d\n",
pixel_order);
return -EINVAL;
}
imx8qm_ldb->active_chno = 0;
imx8qm_ldb_ch = imx8qm_ldb->channel[0];
ldb_ch = &imx8qm_ldb_ch->base;
ldb_ch->link_type = pixel_order;
} else {
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
imx8qm_ldb_ch = imx8qm_ldb->channel[i];
ldb_ch = &imx8qm_ldb_ch->base;
if (ldb_ch->is_available) {
imx8qm_ldb->active_chno = ldb_ch->chno;
break;
}
}
}
ret = imx8qm_ldb_get_phy(imx8qm_ldb);
if (ret)
return ret;
ret = ldb_find_next_bridge_helper(ldb);
if (ret)
return ret;
platform_set_drvdata(pdev, imx8qm_ldb);
pm_runtime_enable(dev);
ldb_add_bridge_helper(ldb);
return ret;
}
static void imx8qm_ldb_remove(struct platform_device *pdev)
{
struct imx8qm_ldb *imx8qm_ldb = platform_get_drvdata(pdev);
struct ldb *ldb = &imx8qm_ldb->base;
ldb_remove_bridge_helper(ldb);
pm_runtime_disable(&pdev->dev);
}
static int imx8qm_ldb_runtime_suspend(struct device *dev)
{
return 0;
}
static int imx8qm_ldb_runtime_resume(struct device *dev)
{
struct imx8qm_ldb *imx8qm_ldb = dev_get_drvdata(dev);
struct ldb *ldb = &imx8qm_ldb->base;
/* disable LDB by resetting the control register to POR default */
regmap_write(ldb->regmap, ldb->ctrl_reg, 0);
return 0;
}
static const struct dev_pm_ops imx8qm_ldb_pm_ops = {
RUNTIME_PM_OPS(imx8qm_ldb_runtime_suspend, imx8qm_ldb_runtime_resume, NULL)
};
static const struct of_device_id imx8qm_ldb_dt_ids[] = {
{ .compatible = "fsl,imx8qm-ldb" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8qm_ldb_dt_ids);
static struct platform_driver imx8qm_ldb_driver = {
.probe = imx8qm_ldb_probe,
.remove = imx8qm_ldb_remove,
.driver = {
.pm = pm_ptr(&imx8qm_ldb_pm_ops),
.name = DRIVER_NAME,
.of_match_table = imx8qm_ldb_dt_ids,
},
};
module_platform_driver(imx8qm_ldb_driver);
MODULE_DESCRIPTION("i.MX8QM LVDS Display Bridge(LDB)/Pixel Mapper bridge driver");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:" DRIVER_NAME);
@@ -0,0 +1,732 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2020 NXP
*/
#include <linux/clk.h>
#include <linux/media-bus-format.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_graph.h>
#include <linux/phy/phy.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_connector.h>
#include <drm/drm_fourcc.h>
#include <drm/drm_of.h>
#include <drm/drm_print.h>
#include "imx-ldb-helper.h"
#define LDB_CH_SEL BIT(28)
#define SS_CTRL 0x20
#define CH_HSYNC_M(id) BIT(0 + ((id) * 2))
#define CH_VSYNC_M(id) BIT(1 + ((id) * 2))
#define CH_PHSYNC(id) BIT(0 + ((id) * 2))
#define CH_PVSYNC(id) BIT(1 + ((id) * 2))
#define DRIVER_NAME "imx8qxp-ldb"
struct imx8qxp_ldb_channel {
struct ldb_channel base;
struct phy *phy;
unsigned int di_id;
};
struct imx8qxp_ldb {
struct ldb base;
struct device *dev;
struct imx8qxp_ldb_channel *channel[MAX_LDB_CHAN_NUM];
struct clk *clk_pixel;
struct clk *clk_bypass;
struct drm_bridge *companion;
int active_chno;
};
static inline struct imx8qxp_ldb_channel *
base_to_imx8qxp_ldb_channel(struct ldb_channel *base)
{
return container_of(base, struct imx8qxp_ldb_channel, base);
}
static inline struct imx8qxp_ldb *base_to_imx8qxp_ldb(struct ldb *base)
{
return container_of(base, struct imx8qxp_ldb, base);
}
static void imx8qxp_ldb_bridge_destroy(struct drm_bridge *bridge)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct imx8qxp_ldb *imx8qxp_ldb;
if (!ldb_ch)
return;
imx8qxp_ldb = base_to_imx8qxp_ldb(ldb_ch->ldb);
drm_bridge_put(imx8qxp_ldb->companion);
}
static void imx8qxp_ldb_set_phy_cfg(struct imx8qxp_ldb *imx8qxp_ldb,
unsigned long di_clk, bool is_split,
struct phy_configure_opts_lvds *phy_cfg)
{
phy_cfg->bits_per_lane_and_dclk_cycle = 7;
phy_cfg->lanes = 4;
if (is_split) {
phy_cfg->differential_clk_rate = di_clk / 2;
phy_cfg->is_slave = !imx8qxp_ldb->companion;
} else {
phy_cfg->differential_clk_rate = di_clk;
phy_cfg->is_slave = false;
}
}
static int
imx8qxp_ldb_bridge_atomic_check(struct drm_bridge *bridge,
struct drm_bridge_state *bridge_state,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch =
base_to_imx8qxp_ldb_channel(ldb_ch);
struct imx8qxp_ldb *imx8qxp_ldb = base_to_imx8qxp_ldb(ldb);
struct drm_bridge *companion = imx8qxp_ldb->companion;
struct drm_display_mode *adj = &crtc_state->adjusted_mode;
unsigned long di_clk = adj->clock * 1000;
bool is_split = ldb_channel_is_split_link(ldb_ch);
union phy_configure_opts opts = { };
struct phy_configure_opts_lvds *phy_cfg = &opts.lvds;
int ret;
ret = ldb_bridge_atomic_check_helper(bridge, bridge_state,
crtc_state, conn_state);
if (ret)
return ret;
imx8qxp_ldb_set_phy_cfg(imx8qxp_ldb, di_clk, is_split, phy_cfg);
ret = phy_validate(imx8qxp_ldb_ch->phy, PHY_MODE_LVDS, 0, &opts);
if (ret < 0) {
DRM_DEV_DEBUG_DRIVER(imx8qxp_ldb->dev,
"failed to validate PHY: %d\n", ret);
return ret;
}
if (is_split && companion) {
ret = companion->funcs->atomic_check(companion,
bridge_state, crtc_state, conn_state);
if (ret)
return ret;
}
return ret;
}
static void
imx8qxp_ldb_bridge_mode_set(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb_channel *companion_ldb_ch;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch =
base_to_imx8qxp_ldb_channel(ldb_ch);
struct imx8qxp_ldb *imx8qxp_ldb = base_to_imx8qxp_ldb(ldb);
struct drm_bridge *companion = imx8qxp_ldb->companion;
struct device *dev = imx8qxp_ldb->dev;
unsigned long di_clk = adjusted_mode->clock * 1000;
bool is_split = ldb_channel_is_split_link(ldb_ch);
union phy_configure_opts opts = { };
struct phy_configure_opts_lvds *phy_cfg = &opts.lvds;
u32 chno = ldb_ch->chno;
int ret;
ret = pm_runtime_get_sync(dev);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to get runtime PM sync: %d\n", ret);
ret = phy_init(imx8qxp_ldb_ch->phy);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to initialize PHY: %d\n", ret);
ret = phy_set_mode(imx8qxp_ldb_ch->phy, PHY_MODE_LVDS);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to set PHY mode: %d\n", ret);
if (is_split && companion) {
companion_ldb_ch = bridge_to_ldb_ch(companion);
companion_ldb_ch->in_bus_format = ldb_ch->in_bus_format;
companion_ldb_ch->out_bus_format = ldb_ch->out_bus_format;
}
clk_set_rate(imx8qxp_ldb->clk_bypass, di_clk);
clk_set_rate(imx8qxp_ldb->clk_pixel, di_clk);
imx8qxp_ldb_set_phy_cfg(imx8qxp_ldb, di_clk, is_split, phy_cfg);
ret = phy_configure(imx8qxp_ldb_ch->phy, &opts);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to configure PHY: %d\n", ret);
if (chno == 0)
ldb->ldb_ctrl &= ~LDB_CH_SEL;
else
ldb->ldb_ctrl |= LDB_CH_SEL;
/* input VSYNC signal from pixel link is active low */
if (imx8qxp_ldb_ch->di_id == 0)
ldb->ldb_ctrl |= LDB_DI0_VS_POL_ACT_LOW;
else
ldb->ldb_ctrl |= LDB_DI1_VS_POL_ACT_LOW;
/*
* For split mode, settle input VSYNC signal polarity and
* channel selection down early.
*/
if (is_split)
regmap_write(ldb->regmap, ldb->ctrl_reg, ldb->ldb_ctrl);
ldb_bridge_mode_set_helper(bridge, mode, adjusted_mode);
if (adjusted_mode->flags & DRM_MODE_FLAG_NVSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL, CH_VSYNC_M(chno), 0);
else if (adjusted_mode->flags & DRM_MODE_FLAG_PVSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL,
CH_VSYNC_M(chno), CH_PVSYNC(chno));
if (adjusted_mode->flags & DRM_MODE_FLAG_NHSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL, CH_HSYNC_M(chno), 0);
else if (adjusted_mode->flags & DRM_MODE_FLAG_PHSYNC)
regmap_update_bits(ldb->regmap, SS_CTRL,
CH_HSYNC_M(chno), CH_PHSYNC(chno));
if (is_split && companion)
companion->funcs->mode_set(companion, mode, adjusted_mode);
}
static void imx8qxp_ldb_bridge_atomic_pre_enable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qxp_ldb *imx8qxp_ldb = base_to_imx8qxp_ldb(ldb);
struct drm_bridge *companion = imx8qxp_ldb->companion;
bool is_split = ldb_channel_is_split_link(ldb_ch);
clk_prepare_enable(imx8qxp_ldb->clk_pixel);
clk_prepare_enable(imx8qxp_ldb->clk_bypass);
if (is_split && companion)
companion->funcs->atomic_pre_enable(companion, state);
}
static void imx8qxp_ldb_bridge_atomic_enable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch =
base_to_imx8qxp_ldb_channel(ldb_ch);
struct imx8qxp_ldb *imx8qxp_ldb = base_to_imx8qxp_ldb(ldb);
struct drm_bridge *companion = imx8qxp_ldb->companion;
struct device *dev = imx8qxp_ldb->dev;
bool is_split = ldb_channel_is_split_link(ldb_ch);
int ret;
if (ldb_ch->chno == 0 || is_split) {
ldb->ldb_ctrl &= ~LDB_CH0_MODE_EN_MASK;
ldb->ldb_ctrl |= imx8qxp_ldb_ch->di_id == 0 ?
LDB_CH0_MODE_EN_TO_DI0 : LDB_CH0_MODE_EN_TO_DI1;
}
if (ldb_ch->chno == 1 || is_split) {
ldb->ldb_ctrl &= ~LDB_CH1_MODE_EN_MASK;
ldb->ldb_ctrl |= imx8qxp_ldb_ch->di_id == 0 ?
LDB_CH1_MODE_EN_TO_DI0 : LDB_CH1_MODE_EN_TO_DI1;
}
ldb_bridge_enable_helper(bridge);
ret = phy_power_on(imx8qxp_ldb_ch->phy);
if (ret)
DRM_DEV_ERROR(dev, "failed to power on PHY: %d\n", ret);
if (is_split && companion)
companion->funcs->atomic_enable(companion, state);
}
static void imx8qxp_ldb_bridge_atomic_disable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
struct ldb *ldb = ldb_ch->ldb;
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch =
base_to_imx8qxp_ldb_channel(ldb_ch);
struct imx8qxp_ldb *imx8qxp_ldb = base_to_imx8qxp_ldb(ldb);
struct drm_bridge *companion = imx8qxp_ldb->companion;
struct device *dev = imx8qxp_ldb->dev;
bool is_split = ldb_channel_is_split_link(ldb_ch);
int ret;
ret = phy_power_off(imx8qxp_ldb_ch->phy);
if (ret)
DRM_DEV_ERROR(dev, "failed to power off PHY: %d\n", ret);
ret = phy_exit(imx8qxp_ldb_ch->phy);
if (ret < 0)
DRM_DEV_ERROR(dev, "failed to teardown PHY: %d\n", ret);
ldb_bridge_disable_helper(bridge);
clk_disable_unprepare(imx8qxp_ldb->clk_bypass);
clk_disable_unprepare(imx8qxp_ldb->clk_pixel);
if (is_split && companion)
companion->funcs->atomic_disable(companion, state);
pm_runtime_put(dev);
}
static const u32 imx8qxp_ldb_bus_output_fmts[] = {
MEDIA_BUS_FMT_RGB666_1X7X3_SPWG,
MEDIA_BUS_FMT_RGB888_1X7X4_SPWG,
MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA,
MEDIA_BUS_FMT_FIXED,
};
static bool imx8qxp_ldb_bus_output_fmt_supported(u32 fmt)
{
int i;
for (i = 0; i < ARRAY_SIZE(imx8qxp_ldb_bus_output_fmts); i++) {
if (imx8qxp_ldb_bus_output_fmts[i] == fmt)
return true;
}
return false;
}
static u32 *
imx8qxp_ldb_bridge_atomic_get_input_bus_fmts(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)
{
struct drm_display_info *di;
const struct drm_format_info *finfo;
u32 *input_fmts;
if (!imx8qxp_ldb_bus_output_fmt_supported(output_fmt))
return NULL;
*num_input_fmts = 1;
input_fmts = kmalloc_obj(*input_fmts);
if (!input_fmts)
return NULL;
switch (output_fmt) {
case MEDIA_BUS_FMT_FIXED:
di = &conn_state->connector->display_info;
/*
* Look at the first bus format to determine input format.
* Default to MEDIA_BUS_FMT_RGB888_1X24, if no match.
*/
if (di->num_bus_formats) {
finfo = drm_format_info(di->bus_formats[0]);
input_fmts[0] = finfo->depth == 18 ?
MEDIA_BUS_FMT_RGB666_1X24_CPADHI :
MEDIA_BUS_FMT_RGB888_1X24;
} else {
input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
}
break;
case MEDIA_BUS_FMT_RGB666_1X7X3_SPWG:
input_fmts[0] = MEDIA_BUS_FMT_RGB666_1X24_CPADHI;
break;
case MEDIA_BUS_FMT_RGB888_1X7X4_SPWG:
case MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA:
input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X24;
break;
default:
kfree(input_fmts);
input_fmts = NULL;
break;
}
return input_fmts;
}
static u32 *
imx8qxp_ldb_bridge_atomic_get_output_bus_fmts(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)
{
*num_output_fmts = ARRAY_SIZE(imx8qxp_ldb_bus_output_fmts);
return kmemdup(imx8qxp_ldb_bus_output_fmts,
sizeof(imx8qxp_ldb_bus_output_fmts), GFP_KERNEL);
}
static enum drm_mode_status
imx8qxp_ldb_bridge_mode_valid(struct drm_bridge *bridge,
const struct drm_display_info *info,
const struct drm_display_mode *mode)
{
struct ldb_channel *ldb_ch = bridge->driver_private;
bool is_single = ldb_channel_is_single_link(ldb_ch);
if (mode->clock > 170000)
return MODE_CLOCK_HIGH;
if (mode->clock > 150000 && is_single)
return MODE_CLOCK_HIGH;
return MODE_OK;
}
static const struct drm_bridge_funcs imx8qxp_ldb_bridge_funcs = {
.destroy = imx8qxp_ldb_bridge_destroy,
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.mode_valid = imx8qxp_ldb_bridge_mode_valid,
.attach = ldb_bridge_attach_helper,
.atomic_check = imx8qxp_ldb_bridge_atomic_check,
.mode_set = imx8qxp_ldb_bridge_mode_set,
.atomic_pre_enable = imx8qxp_ldb_bridge_atomic_pre_enable,
.atomic_enable = imx8qxp_ldb_bridge_atomic_enable,
.atomic_disable = imx8qxp_ldb_bridge_atomic_disable,
.atomic_get_input_bus_fmts =
imx8qxp_ldb_bridge_atomic_get_input_bus_fmts,
.atomic_get_output_bus_fmts =
imx8qxp_ldb_bridge_atomic_get_output_bus_fmts,
};
static int imx8qxp_ldb_set_di_id(struct imx8qxp_ldb *imx8qxp_ldb)
{
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch =
imx8qxp_ldb->channel[imx8qxp_ldb->active_chno];
struct ldb_channel *ldb_ch = &imx8qxp_ldb_ch->base;
struct device_node *ep, *remote;
struct device *dev = imx8qxp_ldb->dev;
struct of_endpoint endpoint;
int ret;
ep = of_graph_get_endpoint_by_regs(ldb_ch->np, 0, -1);
if (!ep) {
DRM_DEV_ERROR(dev, "failed to get port0 endpoint\n");
return -EINVAL;
}
remote = of_graph_get_remote_endpoint(ep);
of_node_put(ep);
if (!remote) {
DRM_DEV_ERROR(dev, "failed to get port0 remote endpoint\n");
return -EINVAL;
}
ret = of_graph_parse_endpoint(remote, &endpoint);
of_node_put(remote);
if (ret) {
DRM_DEV_ERROR(dev, "failed to parse port0 remote endpoint: %d\n",
ret);
return ret;
}
imx8qxp_ldb_ch->di_id = endpoint.id;
return 0;
}
static int
imx8qxp_ldb_check_chno_and_dual_link(struct ldb_channel *ldb_ch, int link)
{
if ((link == DRM_LVDS_DUAL_LINK_ODD_EVEN_PIXELS && ldb_ch->chno != 0) ||
(link == DRM_LVDS_DUAL_LINK_EVEN_ODD_PIXELS && ldb_ch->chno != 1))
return -EINVAL;
return 0;
}
static int imx8qxp_ldb_parse_dt_companion(struct imx8qxp_ldb *imx8qxp_ldb)
{
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch =
imx8qxp_ldb->channel[imx8qxp_ldb->active_chno];
struct ldb_channel *ldb_ch = &imx8qxp_ldb_ch->base;
struct ldb_channel *companion_ldb_ch;
struct device_node *companion;
struct device_node *child;
struct device_node *companion_port = NULL;
struct device_node *port1, *port2;
struct device *dev = imx8qxp_ldb->dev;
const struct of_device_id *match;
u32 i;
int dual_link;
int ret;
/* Locate the companion LDB for dual-link operation, if any. */
companion = of_parse_phandle(dev->of_node, "fsl,companion-ldb", 0);
if (!companion)
return 0;
if (!of_device_is_available(companion)) {
DRM_DEV_ERROR(dev, "companion LDB is not available\n");
ret = -ENODEV;
goto out;
}
/*
* Sanity check: the companion bridge must have the same compatible
* string.
*/
match = of_match_device(dev->driver->of_match_table, dev);
if (!of_device_is_compatible(companion, match->compatible)) {
DRM_DEV_ERROR(dev, "companion LDB is incompatible\n");
ret = -ENXIO;
goto out;
}
for_each_available_child_of_node(companion, child) {
ret = of_property_read_u32(child, "reg", &i);
if (ret || i > MAX_LDB_CHAN_NUM - 1) {
DRM_DEV_ERROR(dev,
"invalid channel node address: %u\n", i);
ret = -EINVAL;
of_node_put(child);
goto out;
}
/*
* Channel numbers have to be different, because channel0
* transmits odd pixels and channel1 transmits even pixels.
*/
if (i == (ldb_ch->chno ^ 0x1)) {
companion_port = child;
break;
}
}
if (!companion_port) {
DRM_DEV_ERROR(dev,
"failed to find companion LDB channel port\n");
ret = -EINVAL;
goto out;
}
/*
* We need to work out if the sink is expecting us to function in
* dual-link mode. We do this by looking at the DT port nodes we are
* connected to. If they are marked as expecting odd pixels and
* even pixels than we need to enable LDB split mode.
*/
port1 = of_graph_get_port_by_id(ldb_ch->np, 1);
port2 = of_graph_get_port_by_id(companion_port, 1);
dual_link = drm_of_lvds_get_dual_link_pixel_order(port1, port2);
of_node_put(port1);
of_node_put(port2);
switch (dual_link) {
case DRM_LVDS_DUAL_LINK_ODD_EVEN_PIXELS:
ldb_ch->link_type = LDB_CH_DUAL_LINK_ODD_EVEN_PIXELS;
break;
case DRM_LVDS_DUAL_LINK_EVEN_ODD_PIXELS:
ldb_ch->link_type = LDB_CH_DUAL_LINK_EVEN_ODD_PIXELS;
break;
default:
ret = dual_link;
DRM_DEV_ERROR(dev,
"failed to get dual link pixel order: %d\n", ret);
goto out;
}
ret = imx8qxp_ldb_check_chno_and_dual_link(ldb_ch, dual_link);
if (ret < 0) {
DRM_DEV_ERROR(dev,
"unmatched channel number(%u) vs dual link(%d)\n",
ldb_ch->chno, dual_link);
goto out;
}
imx8qxp_ldb->companion = of_drm_find_and_get_bridge(companion_port);
if (!imx8qxp_ldb->companion) {
ret = -EPROBE_DEFER;
DRM_DEV_DEBUG_DRIVER(dev,
"failed to find bridge for companion bridge: %d\n",
ret);
goto out;
}
DRM_DEV_DEBUG_DRIVER(dev,
"dual-link configuration detected (companion bridge %pOF)\n",
companion);
companion_ldb_ch = bridge_to_ldb_ch(imx8qxp_ldb->companion);
companion_ldb_ch->link_type = ldb_ch->link_type;
out:
of_node_put(companion_port);
of_node_put(companion);
return ret;
}
static int imx8qxp_ldb_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct imx8qxp_ldb *imx8qxp_ldb;
struct imx8qxp_ldb_channel *imx8qxp_ldb_ch;
struct ldb *ldb;
struct ldb_channel *ldb_ch;
int ret, i;
imx8qxp_ldb = devm_kzalloc(dev, sizeof(*imx8qxp_ldb), GFP_KERNEL);
if (!imx8qxp_ldb)
return -ENOMEM;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
imx8qxp_ldb->channel[i] =
devm_drm_bridge_alloc(dev, struct imx8qxp_ldb_channel, base.bridge,
&imx8qxp_ldb_bridge_funcs);
if (IS_ERR(imx8qxp_ldb->channel[i]))
return PTR_ERR(imx8qxp_ldb->channel[i]);
}
imx8qxp_ldb->clk_pixel = devm_clk_get(dev, "pixel");
if (IS_ERR(imx8qxp_ldb->clk_pixel)) {
ret = PTR_ERR(imx8qxp_ldb->clk_pixel);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev,
"failed to get pixel clock: %d\n", ret);
return ret;
}
imx8qxp_ldb->clk_bypass = devm_clk_get(dev, "bypass");
if (IS_ERR(imx8qxp_ldb->clk_bypass)) {
ret = PTR_ERR(imx8qxp_ldb->clk_bypass);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev,
"failed to get bypass clock: %d\n", ret);
return ret;
}
imx8qxp_ldb->dev = dev;
ldb = &imx8qxp_ldb->base;
ldb->dev = dev;
ldb->ctrl_reg = 0xe0;
for (i = 0; i < MAX_LDB_CHAN_NUM; i++)
ldb->channel[i] = &imx8qxp_ldb->channel[i]->base;
ret = ldb_init_helper(ldb);
if (ret)
return ret;
if (ldb->available_ch_cnt == 0) {
DRM_DEV_DEBUG_DRIVER(dev, "no available channel\n");
return 0;
} else if (ldb->available_ch_cnt > 1) {
DRM_DEV_ERROR(dev, "invalid available channel number(%u)\n",
ldb->available_ch_cnt);
return -EINVAL;
}
for (i = 0; i < MAX_LDB_CHAN_NUM; i++) {
imx8qxp_ldb_ch = imx8qxp_ldb->channel[i];
ldb_ch = &imx8qxp_ldb_ch->base;
if (ldb_ch->is_available) {
imx8qxp_ldb->active_chno = ldb_ch->chno;
break;
}
}
imx8qxp_ldb_ch->phy = devm_of_phy_get(dev, ldb_ch->np, "lvds_phy");
if (IS_ERR(imx8qxp_ldb_ch->phy)) {
ret = PTR_ERR(imx8qxp_ldb_ch->phy);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev, "failed to get channel%d PHY: %d\n",
imx8qxp_ldb->active_chno, ret);
return ret;
}
ret = ldb_find_next_bridge_helper(ldb);
if (ret)
return ret;
ret = imx8qxp_ldb_set_di_id(imx8qxp_ldb);
if (ret)
return ret;
ret = imx8qxp_ldb_parse_dt_companion(imx8qxp_ldb);
if (ret)
return ret;
platform_set_drvdata(pdev, imx8qxp_ldb);
pm_runtime_enable(dev);
ldb_add_bridge_helper(ldb);
return 0;
}
static void imx8qxp_ldb_remove(struct platform_device *pdev)
{
struct imx8qxp_ldb *imx8qxp_ldb = platform_get_drvdata(pdev);
struct ldb *ldb = &imx8qxp_ldb->base;
ldb_remove_bridge_helper(ldb);
pm_runtime_disable(&pdev->dev);
}
static int imx8qxp_ldb_runtime_resume(struct device *dev)
{
struct imx8qxp_ldb *imx8qxp_ldb = dev_get_drvdata(dev);
struct ldb *ldb = &imx8qxp_ldb->base;
/* disable LDB by resetting the control register to POR default */
regmap_write(ldb->regmap, ldb->ctrl_reg, 0);
return 0;
}
static const struct dev_pm_ops imx8qxp_ldb_pm_ops = {
RUNTIME_PM_OPS(NULL, imx8qxp_ldb_runtime_resume, NULL)
};
static const struct of_device_id imx8qxp_ldb_dt_ids[] = {
{ .compatible = "fsl,imx8qxp-ldb" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8qxp_ldb_dt_ids);
static struct platform_driver imx8qxp_ldb_driver = {
.probe = imx8qxp_ldb_probe,
.remove = imx8qxp_ldb_remove,
.driver = {
.pm = pm_ptr(&imx8qxp_ldb_pm_ops),
.name = DRIVER_NAME,
.of_match_table = imx8qxp_ldb_dt_ids,
},
};
module_platform_driver(imx8qxp_ldb_driver);
MODULE_DESCRIPTION("i.MX8QXP LVDS Display Bridge(LDB)/Pixel Mapper bridge driver");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:" DRIVER_NAME);
@@ -0,0 +1,439 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2020 NXP
*/
#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <linux/media-bus-format.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_graph.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_print.h>
#define PC_CTRL_REG 0x0
#define PC_COMBINE_ENABLE BIT(0)
#define PC_DISP_BYPASS(n) BIT(1 + 21 * (n))
#define PC_DISP_HSYNC_POLARITY(n) BIT(2 + 11 * (n))
#define PC_DISP_HSYNC_POLARITY_POS(n) DISP_HSYNC_POLARITY(n)
#define PC_DISP_VSYNC_POLARITY(n) BIT(3 + 11 * (n))
#define PC_DISP_VSYNC_POLARITY_POS(n) DISP_VSYNC_POLARITY(n)
#define PC_DISP_DVALID_POLARITY(n) BIT(4 + 11 * (n))
#define PC_DISP_DVALID_POLARITY_POS(n) DISP_DVALID_POLARITY(n)
#define PC_VSYNC_MASK_ENABLE BIT(5)
#define PC_SKIP_MODE BIT(6)
#define PC_SKIP_NUMBER_MASK GENMASK(12, 7)
#define PC_SKIP_NUMBER(n) FIELD_PREP(PC_SKIP_NUMBER_MASK, (n))
#define PC_DISP0_PIX_DATA_FORMAT_MASK GENMASK(18, 16)
#define PC_DISP0_PIX_DATA_FORMAT(fmt) \
FIELD_PREP(PC_DISP0_PIX_DATA_FORMAT_MASK, (fmt))
#define PC_DISP1_PIX_DATA_FORMAT_MASK GENMASK(21, 19)
#define PC_DISP1_PIX_DATA_FORMAT(fmt) \
FIELD_PREP(PC_DISP1_PIX_DATA_FORMAT_MASK, (fmt))
#define PC_SW_RESET_REG 0x20
#define PC_SW_RESET_N BIT(0)
#define PC_DISP_SW_RESET_N(n) BIT(1 + (n))
#define PC_FULL_RESET_N (PC_SW_RESET_N | \
PC_DISP_SW_RESET_N(0) | \
PC_DISP_SW_RESET_N(1))
#define PC_REG_SET 0x4
#define PC_REG_CLR 0x8
#define DRIVER_NAME "imx8qxp-pixel-combiner"
enum imx8qxp_pc_pix_data_format {
RGB,
YUV444,
YUV422,
SPLIT_RGB,
};
struct imx8qxp_pc_channel {
struct drm_bridge bridge;
struct imx8qxp_pc *pc;
unsigned int stream_id;
};
struct imx8qxp_pc {
struct device *dev;
struct imx8qxp_pc_channel *ch[2];
struct clk *clk_apb;
void __iomem *base;
};
static inline u32 imx8qxp_pc_read(struct imx8qxp_pc *pc, unsigned int offset)
{
return readl(pc->base + offset);
}
static inline void
imx8qxp_pc_write(struct imx8qxp_pc *pc, unsigned int offset, u32 value)
{
writel(value, pc->base + offset);
}
static inline void
imx8qxp_pc_write_set(struct imx8qxp_pc *pc, unsigned int offset, u32 value)
{
imx8qxp_pc_write(pc, offset + PC_REG_SET, value);
}
static inline void
imx8qxp_pc_write_clr(struct imx8qxp_pc *pc, unsigned int offset, u32 value)
{
imx8qxp_pc_write(pc, offset + PC_REG_CLR, value);
}
static enum drm_mode_status
imx8qxp_pc_bridge_mode_valid(struct drm_bridge *bridge,
const struct drm_display_info *info,
const struct drm_display_mode *mode)
{
if (mode->hdisplay > 2560)
return MODE_BAD_HVALUE;
return MODE_OK;
}
static int imx8qxp_pc_bridge_attach(struct drm_bridge *bridge,
struct drm_encoder *encoder,
enum drm_bridge_attach_flags flags)
{
struct imx8qxp_pc_channel *ch = bridge->driver_private;
struct imx8qxp_pc *pc = ch->pc;
if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
DRM_DEV_ERROR(pc->dev,
"do not support creating a drm_connector\n");
return -EINVAL;
}
return drm_bridge_attach(encoder,
ch->bridge.next_bridge, bridge,
DRM_BRIDGE_ATTACH_NO_CONNECTOR);
}
static void
imx8qxp_pc_bridge_mode_set(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct imx8qxp_pc_channel *ch = bridge->driver_private;
struct imx8qxp_pc *pc = ch->pc;
u32 val;
int ret;
ret = pm_runtime_get_sync(pc->dev);
if (ret < 0)
DRM_DEV_ERROR(pc->dev,
"failed to get runtime PM sync: %d\n", ret);
ret = clk_prepare_enable(pc->clk_apb);
if (ret)
DRM_DEV_ERROR(pc->dev, "%s: failed to enable apb clock: %d\n",
__func__, ret);
/* HSYNC to pixel link is active low. */
imx8qxp_pc_write_clr(pc, PC_CTRL_REG,
PC_DISP_HSYNC_POLARITY(ch->stream_id));
/* VSYNC to pixel link is active low. */
imx8qxp_pc_write_clr(pc, PC_CTRL_REG,
PC_DISP_VSYNC_POLARITY(ch->stream_id));
/* Data enable to pixel link is active high. */
imx8qxp_pc_write_set(pc, PC_CTRL_REG,
PC_DISP_DVALID_POLARITY(ch->stream_id));
/* Mask the first frame output which may be incomplete. */
imx8qxp_pc_write_set(pc, PC_CTRL_REG, PC_VSYNC_MASK_ENABLE);
/* Only support RGB currently. */
val = imx8qxp_pc_read(pc, PC_CTRL_REG);
if (ch->stream_id == 0) {
val &= ~PC_DISP0_PIX_DATA_FORMAT_MASK;
val |= PC_DISP0_PIX_DATA_FORMAT(RGB);
} else {
val &= ~PC_DISP1_PIX_DATA_FORMAT_MASK;
val |= PC_DISP1_PIX_DATA_FORMAT(RGB);
}
imx8qxp_pc_write(pc, PC_CTRL_REG, val);
/* Only support bypass mode currently. */
imx8qxp_pc_write_set(pc, PC_CTRL_REG, PC_DISP_BYPASS(ch->stream_id));
clk_disable_unprepare(pc->clk_apb);
}
static void imx8qxp_pc_bridge_atomic_disable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct imx8qxp_pc_channel *ch = bridge->driver_private;
struct imx8qxp_pc *pc = ch->pc;
pm_runtime_put(pc->dev);
}
static const u32 imx8qxp_pc_bus_output_fmts[] = {
MEDIA_BUS_FMT_RGB888_1X36_CPADLO,
MEDIA_BUS_FMT_RGB666_1X36_CPADLO,
};
static bool imx8qxp_pc_bus_output_fmt_supported(u32 fmt)
{
int i;
for (i = 0; i < ARRAY_SIZE(imx8qxp_pc_bus_output_fmts); i++) {
if (imx8qxp_pc_bus_output_fmts[i] == fmt)
return true;
}
return false;
}
static u32 *
imx8qxp_pc_bridge_atomic_get_input_bus_fmts(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)
{
u32 *input_fmts;
if (!imx8qxp_pc_bus_output_fmt_supported(output_fmt))
return NULL;
*num_input_fmts = 1;
input_fmts = kmalloc_obj(*input_fmts);
if (!input_fmts)
return NULL;
switch (output_fmt) {
case MEDIA_BUS_FMT_RGB888_1X36_CPADLO:
input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X30_CPADLO;
break;
case MEDIA_BUS_FMT_RGB666_1X36_CPADLO:
input_fmts[0] = MEDIA_BUS_FMT_RGB666_1X30_CPADLO;
break;
default:
kfree(input_fmts);
input_fmts = NULL;
break;
}
return input_fmts;
}
static u32 *
imx8qxp_pc_bridge_atomic_get_output_bus_fmts(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)
{
*num_output_fmts = ARRAY_SIZE(imx8qxp_pc_bus_output_fmts);
return kmemdup(imx8qxp_pc_bus_output_fmts,
sizeof(imx8qxp_pc_bus_output_fmts), GFP_KERNEL);
}
static const struct drm_bridge_funcs imx8qxp_pc_bridge_funcs = {
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.mode_valid = imx8qxp_pc_bridge_mode_valid,
.attach = imx8qxp_pc_bridge_attach,
.mode_set = imx8qxp_pc_bridge_mode_set,
.atomic_disable = imx8qxp_pc_bridge_atomic_disable,
.atomic_get_input_bus_fmts =
imx8qxp_pc_bridge_atomic_get_input_bus_fmts,
.atomic_get_output_bus_fmts =
imx8qxp_pc_bridge_atomic_get_output_bus_fmts,
};
static int imx8qxp_pc_bridge_probe(struct platform_device *pdev)
{
struct imx8qxp_pc *pc;
struct imx8qxp_pc_channel *ch;
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct device_node *child, *remote;
u32 i;
int ret;
pc = devm_kzalloc(dev, sizeof(*pc), GFP_KERNEL);
if (!pc)
return -ENOMEM;
pc->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(pc->base))
return PTR_ERR(pc->base);
pc->dev = dev;
pc->clk_apb = devm_clk_get(dev, "apb");
if (IS_ERR(pc->clk_apb)) {
ret = PTR_ERR(pc->clk_apb);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev, "failed to get apb clock: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, pc);
pm_runtime_enable(dev);
for_each_available_child_of_node(np, child) {
ret = of_property_read_u32(child, "reg", &i);
if (ret || i > 1) {
ret = -EINVAL;
DRM_DEV_ERROR(dev,
"invalid channel(%u) node address\n", i);
goto free_child;
}
ch = devm_drm_bridge_alloc(dev, struct imx8qxp_pc_channel, bridge,
&imx8qxp_pc_bridge_funcs);
if (IS_ERR(ch)) {
ret = PTR_ERR(ch);
goto free_child;
}
pc->ch[i] = ch;
ch->pc = pc;
ch->stream_id = i;
remote = of_graph_get_remote_node(child, 1, 0);
if (!remote) {
ret = -ENODEV;
DRM_DEV_ERROR(dev,
"channel%u failed to get port1's remote node: %d\n",
i, ret);
goto free_child;
}
ch->bridge.next_bridge = of_drm_find_and_get_bridge(remote);
if (!ch->bridge.next_bridge) {
of_node_put(remote);
ret = -EPROBE_DEFER;
DRM_DEV_DEBUG_DRIVER(dev,
"channel%u failed to find next bridge: %d\n",
i, ret);
goto free_child;
}
of_node_put(remote);
ch->bridge.driver_private = ch;
ch->bridge.of_node = child;
drm_bridge_add(&ch->bridge);
}
return 0;
free_child:
of_node_put(child);
if (i == 1 && pc->ch[0] && pc->ch[0]->bridge.next_bridge)
drm_bridge_remove(&pc->ch[0]->bridge);
pm_runtime_disable(dev);
return ret;
}
static void imx8qxp_pc_bridge_remove(struct platform_device *pdev)
{
struct imx8qxp_pc *pc = platform_get_drvdata(pdev);
struct imx8qxp_pc_channel *ch;
int i;
for (i = 0; i < 2; i++) {
ch = pc->ch[i];
if (ch)
drm_bridge_remove(&ch->bridge);
}
pm_runtime_disable(&pdev->dev);
}
static int imx8qxp_pc_runtime_suspend(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct imx8qxp_pc *pc = platform_get_drvdata(pdev);
int ret;
ret = clk_prepare_enable(pc->clk_apb);
if (ret)
DRM_DEV_ERROR(pc->dev, "%s: failed to enable apb clock: %d\n",
__func__, ret);
/* Disable pixel combiner by full reset. */
imx8qxp_pc_write_clr(pc, PC_SW_RESET_REG, PC_FULL_RESET_N);
clk_disable_unprepare(pc->clk_apb);
/* Ensure the reset takes effect. */
usleep_range(10, 20);
return ret;
}
static int imx8qxp_pc_runtime_resume(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct imx8qxp_pc *pc = platform_get_drvdata(pdev);
int ret;
ret = clk_prepare_enable(pc->clk_apb);
if (ret) {
DRM_DEV_ERROR(pc->dev, "%s: failed to enable apb clock: %d\n",
__func__, ret);
return ret;
}
/* out of reset */
imx8qxp_pc_write_set(pc, PC_SW_RESET_REG, PC_FULL_RESET_N);
clk_disable_unprepare(pc->clk_apb);
return ret;
}
static const struct dev_pm_ops imx8qxp_pc_pm_ops = {
RUNTIME_PM_OPS(imx8qxp_pc_runtime_suspend, imx8qxp_pc_runtime_resume, NULL)
};
static const struct of_device_id imx8qxp_pc_dt_ids[] = {
{ .compatible = "fsl,imx8qm-pixel-combiner", },
{ .compatible = "fsl,imx8qxp-pixel-combiner", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8qxp_pc_dt_ids);
static struct platform_driver imx8qxp_pc_bridge_driver = {
.probe = imx8qxp_pc_bridge_probe,
.remove = imx8qxp_pc_bridge_remove,
.driver = {
.pm = pm_ptr(&imx8qxp_pc_pm_ops),
.name = DRIVER_NAME,
.of_match_table = imx8qxp_pc_dt_ids,
},
};
module_platform_driver(imx8qxp_pc_bridge_driver);
MODULE_DESCRIPTION("i.MX8QM/QXP pixel combiner bridge driver");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:" DRIVER_NAME);
@@ -0,0 +1,411 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2020,2022 NXP
*/
#include <linux/firmware/imx/svc/misc.h>
#include <linux/media-bus-format.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_graph.h>
#include <linux/platform_device.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_print.h>
#include <dt-bindings/firmware/imx/rsrc.h>
#define DRIVER_NAME "imx8qxp-display-pixel-link"
#define PL_MAX_MST_ADDR 3
#define PL_MAX_NEXT_BRIDGES 2
struct imx8qxp_pixel_link {
struct drm_bridge bridge;
struct device *dev;
struct imx_sc_ipc *ipc_handle;
u8 stream_id;
u8 dc_id;
u32 sink_rsc;
u32 mst_addr;
u8 mst_addr_ctrl;
u8 mst_en_ctrl;
u8 mst_vld_ctrl;
u8 sync_ctrl;
};
static void imx8qxp_pixel_link_enable_mst_en(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle, pl->sink_rsc,
pl->mst_en_ctrl, true);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to enable DC%u stream%u pixel link mst_en: %d\n",
pl->dc_id, pl->stream_id, ret);
}
static void imx8qxp_pixel_link_enable_mst_vld(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle, pl->sink_rsc,
pl->mst_vld_ctrl, true);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to enable DC%u stream%u pixel link mst_vld: %d\n",
pl->dc_id, pl->stream_id, ret);
}
static void imx8qxp_pixel_link_enable_sync(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle, pl->sink_rsc,
pl->sync_ctrl, true);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to enable DC%u stream%u pixel link sync: %d\n",
pl->dc_id, pl->stream_id, ret);
}
static int imx8qxp_pixel_link_disable_mst_en(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle, pl->sink_rsc,
pl->mst_en_ctrl, false);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to disable DC%u stream%u pixel link mst_en: %d\n",
pl->dc_id, pl->stream_id, ret);
return ret;
}
static int imx8qxp_pixel_link_disable_mst_vld(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle, pl->sink_rsc,
pl->mst_vld_ctrl, false);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to disable DC%u stream%u pixel link mst_vld: %d\n",
pl->dc_id, pl->stream_id, ret);
return ret;
}
static int imx8qxp_pixel_link_disable_sync(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle, pl->sink_rsc,
pl->sync_ctrl, false);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to disable DC%u stream%u pixel link sync: %d\n",
pl->dc_id, pl->stream_id, ret);
return ret;
}
static void imx8qxp_pixel_link_set_mst_addr(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx_sc_misc_set_control(pl->ipc_handle,
pl->sink_rsc, pl->mst_addr_ctrl,
pl->mst_addr);
if (ret)
DRM_DEV_ERROR(pl->dev,
"failed to set DC%u stream%u pixel link mst addr(%u): %d\n",
pl->dc_id, pl->stream_id, pl->mst_addr, ret);
}
static int imx8qxp_pixel_link_bridge_attach(struct drm_bridge *bridge,
struct drm_encoder *encoder,
enum drm_bridge_attach_flags flags)
{
struct imx8qxp_pixel_link *pl = bridge->driver_private;
if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
DRM_DEV_ERROR(pl->dev,
"do not support creating a drm_connector\n");
return -EINVAL;
}
return drm_bridge_attach(encoder,
pl->bridge.next_bridge, bridge,
DRM_BRIDGE_ATTACH_NO_CONNECTOR);
}
static void
imx8qxp_pixel_link_bridge_mode_set(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct imx8qxp_pixel_link *pl = bridge->driver_private;
imx8qxp_pixel_link_set_mst_addr(pl);
}
static void imx8qxp_pixel_link_bridge_atomic_enable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct imx8qxp_pixel_link *pl = bridge->driver_private;
imx8qxp_pixel_link_enable_mst_en(pl);
imx8qxp_pixel_link_enable_mst_vld(pl);
imx8qxp_pixel_link_enable_sync(pl);
}
static void imx8qxp_pixel_link_bridge_atomic_disable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct imx8qxp_pixel_link *pl = bridge->driver_private;
imx8qxp_pixel_link_disable_mst_en(pl);
imx8qxp_pixel_link_disable_mst_vld(pl);
imx8qxp_pixel_link_disable_sync(pl);
}
static const u32 imx8qxp_pixel_link_bus_output_fmts[] = {
MEDIA_BUS_FMT_RGB888_1X36_CPADLO,
MEDIA_BUS_FMT_RGB666_1X36_CPADLO,
};
static bool imx8qxp_pixel_link_bus_output_fmt_supported(u32 fmt)
{
int i;
for (i = 0; i < ARRAY_SIZE(imx8qxp_pixel_link_bus_output_fmts); i++) {
if (imx8qxp_pixel_link_bus_output_fmts[i] == fmt)
return true;
}
return false;
}
static u32 *
imx8qxp_pixel_link_bridge_atomic_get_input_bus_fmts(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)
{
u32 *input_fmts;
if (!imx8qxp_pixel_link_bus_output_fmt_supported(output_fmt))
return NULL;
*num_input_fmts = 1;
input_fmts = kmalloc_obj(*input_fmts);
if (!input_fmts)
return NULL;
input_fmts[0] = output_fmt;
return input_fmts;
}
static u32 *
imx8qxp_pixel_link_bridge_atomic_get_output_bus_fmts(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)
{
*num_output_fmts = ARRAY_SIZE(imx8qxp_pixel_link_bus_output_fmts);
return kmemdup(imx8qxp_pixel_link_bus_output_fmts,
sizeof(imx8qxp_pixel_link_bus_output_fmts), GFP_KERNEL);
}
static const struct drm_bridge_funcs imx8qxp_pixel_link_bridge_funcs = {
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.attach = imx8qxp_pixel_link_bridge_attach,
.mode_set = imx8qxp_pixel_link_bridge_mode_set,
.atomic_enable = imx8qxp_pixel_link_bridge_atomic_enable,
.atomic_disable = imx8qxp_pixel_link_bridge_atomic_disable,
.atomic_get_input_bus_fmts =
imx8qxp_pixel_link_bridge_atomic_get_input_bus_fmts,
.atomic_get_output_bus_fmts =
imx8qxp_pixel_link_bridge_atomic_get_output_bus_fmts,
};
static int imx8qxp_pixel_link_disable_all_controls(struct imx8qxp_pixel_link *pl)
{
int ret;
ret = imx8qxp_pixel_link_disable_mst_en(pl);
if (ret)
return ret;
ret = imx8qxp_pixel_link_disable_mst_vld(pl);
if (ret)
return ret;
return imx8qxp_pixel_link_disable_sync(pl);
}
static int imx8qxp_pixel_link_find_next_bridge(struct imx8qxp_pixel_link *pl)
{
struct device_node *np = pl->dev->of_node;
struct device_node *port;
u32 port_id;
bool found_port = false;
int reg;
for (port_id = 1; port_id <= PL_MAX_MST_ADDR + 1; port_id++) {
port = of_graph_get_port_by_id(np, port_id);
if (!port)
continue;
if (of_device_is_available(port)) {
found_port = true;
of_node_put(port);
break;
}
of_node_put(port);
}
if (!found_port) {
DRM_DEV_ERROR(pl->dev, "no available output port\n");
return -ENODEV;
}
for (reg = 0; reg < PL_MAX_NEXT_BRIDGES; reg++) {
struct device_node *remote __free(device_node) =
of_graph_get_remote_node(np, port_id, reg);
if (!remote)
continue;
if (!of_device_is_available(remote->parent)) {
DRM_DEV_DEBUG(pl->dev,
"port%u endpoint%u remote parent is not available\n",
port_id, reg);
continue;
}
if (!pl->bridge.next_bridge) {
/* Select the first bridge by default... */
pl->bridge.next_bridge = of_drm_find_and_get_bridge(remote);
if (!pl->bridge.next_bridge)
return -EPROBE_DEFER;
} else if (of_property_present(remote, "fsl,companion-pxl2dpi")) {
/* ... but prefer the companion PXL2DPI if present */
drm_bridge_put(pl->bridge.next_bridge);
pl->bridge.next_bridge = of_drm_find_and_get_bridge(remote);
if (!pl->bridge.next_bridge)
return -EPROBE_DEFER;
}
}
pl->mst_addr = port_id - 1;
return 0;
}
static int imx8qxp_pixel_link_bridge_probe(struct platform_device *pdev)
{
struct imx8qxp_pixel_link *pl;
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
int ret;
pl = devm_drm_bridge_alloc(dev, struct imx8qxp_pixel_link, bridge,
&imx8qxp_pixel_link_bridge_funcs);
if (IS_ERR(pl))
return PTR_ERR(pl);
ret = imx_scu_get_handle(&pl->ipc_handle);
if (ret) {
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev, "failed to get SCU ipc handle: %d\n",
ret);
return ret;
}
ret = of_property_read_u8(np, "fsl,dc-id", &pl->dc_id);
if (ret) {
DRM_DEV_ERROR(dev, "failed to get DC index: %d\n", ret);
return ret;
}
ret = of_property_read_u8(np, "fsl,dc-stream-id", &pl->stream_id);
if (ret) {
DRM_DEV_ERROR(dev, "failed to get DC stream index: %d\n", ret);
return ret;
}
pl->dev = dev;
pl->sink_rsc = pl->dc_id ? IMX_SC_R_DC_1 : IMX_SC_R_DC_0;
if (pl->stream_id == 0) {
pl->mst_addr_ctrl = IMX_SC_C_PXL_LINK_MST1_ADDR;
pl->mst_en_ctrl = IMX_SC_C_PXL_LINK_MST1_ENB;
pl->mst_vld_ctrl = IMX_SC_C_PXL_LINK_MST1_VLD;
pl->sync_ctrl = IMX_SC_C_SYNC_CTRL0;
} else {
pl->mst_addr_ctrl = IMX_SC_C_PXL_LINK_MST2_ADDR;
pl->mst_en_ctrl = IMX_SC_C_PXL_LINK_MST2_ENB;
pl->mst_vld_ctrl = IMX_SC_C_PXL_LINK_MST2_VLD;
pl->sync_ctrl = IMX_SC_C_SYNC_CTRL1;
}
/* disable all controls to POR default */
ret = imx8qxp_pixel_link_disable_all_controls(pl);
if (ret)
return ret;
ret = imx8qxp_pixel_link_find_next_bridge(pl);
if (ret)
return ret;
platform_set_drvdata(pdev, pl);
pl->bridge.driver_private = pl;
pl->bridge.of_node = np;
drm_bridge_add(&pl->bridge);
return ret;
}
static void imx8qxp_pixel_link_bridge_remove(struct platform_device *pdev)
{
struct imx8qxp_pixel_link *pl = platform_get_drvdata(pdev);
drm_bridge_remove(&pl->bridge);
}
static const struct of_device_id imx8qxp_pixel_link_dt_ids[] = {
{ .compatible = "fsl,imx8qm-dc-pixel-link", },
{ .compatible = "fsl,imx8qxp-dc-pixel-link", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8qxp_pixel_link_dt_ids);
static struct platform_driver imx8qxp_pixel_link_bridge_driver = {
.probe = imx8qxp_pixel_link_bridge_probe,
.remove = imx8qxp_pixel_link_bridge_remove,
.driver = {
.of_match_table = imx8qxp_pixel_link_dt_ids,
.name = DRIVER_NAME,
},
};
module_platform_driver(imx8qxp_pixel_link_bridge_driver);
MODULE_DESCRIPTION("i.MX8QXP/QM display pixel link bridge driver");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:" DRIVER_NAME);
@@ -0,0 +1,475 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2020 NXP
*/
#include <linux/firmware/imx/svc/misc.h>
#include <linux/media-bus-format.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_graph.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <drm/drm_atomic_state_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_of.h>
#include <drm/drm_print.h>
#include <dt-bindings/firmware/imx/rsrc.h>
#define PXL2DPI_CTRL 0x40
#define CFG1_16BIT 0x0
#define CFG2_16BIT 0x1
#define CFG3_16BIT 0x2
#define CFG1_18BIT 0x3
#define CFG2_18BIT 0x4
#define CFG_24BIT 0x5
#define DRIVER_NAME "imx8qxp-pxl2dpi"
struct imx8qxp_pxl2dpi {
struct regmap *regmap;
struct drm_bridge bridge;
struct drm_bridge *companion;
struct device *dev;
struct imx_sc_ipc *ipc_handle;
u32 sc_resource;
u32 in_bus_format;
u32 out_bus_format;
u32 pl_sel;
};
#define bridge_to_p2d(b) container_of(b, struct imx8qxp_pxl2dpi, bridge)
static int imx8qxp_pxl2dpi_bridge_attach(struct drm_bridge *bridge,
struct drm_encoder *encoder,
enum drm_bridge_attach_flags flags)
{
struct imx8qxp_pxl2dpi *p2d = bridge->driver_private;
if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
DRM_DEV_ERROR(p2d->dev,
"do not support creating a drm_connector\n");
return -EINVAL;
}
return drm_bridge_attach(encoder,
p2d->bridge.next_bridge, bridge,
DRM_BRIDGE_ATTACH_NO_CONNECTOR);
}
static void imx8qxp_pxl2dpi_bridge_destroy(struct drm_bridge *bridge)
{
struct imx8qxp_pxl2dpi *p2d = bridge->driver_private;
if (!p2d)
return;
drm_bridge_put(p2d->companion);
}
static int
imx8qxp_pxl2dpi_bridge_atomic_check(struct drm_bridge *bridge,
struct drm_bridge_state *bridge_state,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
struct imx8qxp_pxl2dpi *p2d = bridge->driver_private;
p2d->in_bus_format = bridge_state->input_bus_cfg.format;
p2d->out_bus_format = bridge_state->output_bus_cfg.format;
return 0;
}
static void
imx8qxp_pxl2dpi_bridge_mode_set(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct imx8qxp_pxl2dpi *p2d = bridge->driver_private;
struct imx8qxp_pxl2dpi *companion_p2d;
int ret;
ret = pm_runtime_get_sync(p2d->dev);
if (ret < 0)
DRM_DEV_ERROR(p2d->dev,
"failed to get runtime PM sync: %d\n", ret);
ret = imx_sc_misc_set_control(p2d->ipc_handle, p2d->sc_resource,
IMX_SC_C_PXL_LINK_SEL, p2d->pl_sel);
if (ret)
DRM_DEV_ERROR(p2d->dev,
"failed to set pixel link selection(%u): %d\n",
p2d->pl_sel, ret);
switch (p2d->out_bus_format) {
case MEDIA_BUS_FMT_RGB888_1X24:
regmap_write(p2d->regmap, PXL2DPI_CTRL, CFG_24BIT);
break;
case MEDIA_BUS_FMT_RGB666_1X24_CPADHI:
regmap_write(p2d->regmap, PXL2DPI_CTRL, CFG2_18BIT);
break;
default:
DRM_DEV_ERROR(p2d->dev,
"unsupported output bus format 0x%08x\n",
p2d->out_bus_format);
}
if (p2d->companion) {
companion_p2d = bridge_to_p2d(p2d->companion);
companion_p2d->in_bus_format = p2d->in_bus_format;
companion_p2d->out_bus_format = p2d->out_bus_format;
p2d->companion->funcs->mode_set(p2d->companion, mode,
adjusted_mode);
}
}
static void imx8qxp_pxl2dpi_bridge_atomic_disable(struct drm_bridge *bridge,
struct drm_atomic_commit *state)
{
struct imx8qxp_pxl2dpi *p2d = bridge->driver_private;
pm_runtime_put(p2d->dev);
if (p2d->companion)
p2d->companion->funcs->atomic_disable(p2d->companion, state);
}
static const u32 imx8qxp_pxl2dpi_bus_output_fmts[] = {
MEDIA_BUS_FMT_RGB888_1X24,
MEDIA_BUS_FMT_RGB666_1X24_CPADHI,
};
static bool imx8qxp_pxl2dpi_bus_output_fmt_supported(u32 fmt)
{
int i;
for (i = 0; i < ARRAY_SIZE(imx8qxp_pxl2dpi_bus_output_fmts); i++) {
if (imx8qxp_pxl2dpi_bus_output_fmts[i] == fmt)
return true;
}
return false;
}
static u32 *
imx8qxp_pxl2dpi_bridge_atomic_get_input_bus_fmts(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)
{
u32 *input_fmts;
if (!imx8qxp_pxl2dpi_bus_output_fmt_supported(output_fmt))
return NULL;
*num_input_fmts = 1;
input_fmts = kmalloc_obj(*input_fmts);
if (!input_fmts)
return NULL;
switch (output_fmt) {
case MEDIA_BUS_FMT_RGB888_1X24:
input_fmts[0] = MEDIA_BUS_FMT_RGB888_1X36_CPADLO;
break;
case MEDIA_BUS_FMT_RGB666_1X24_CPADHI:
input_fmts[0] = MEDIA_BUS_FMT_RGB666_1X36_CPADLO;
break;
default:
kfree(input_fmts);
input_fmts = NULL;
break;
}
return input_fmts;
}
static u32 *
imx8qxp_pxl2dpi_bridge_atomic_get_output_bus_fmts(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)
{
*num_output_fmts = ARRAY_SIZE(imx8qxp_pxl2dpi_bus_output_fmts);
return kmemdup(imx8qxp_pxl2dpi_bus_output_fmts,
sizeof(imx8qxp_pxl2dpi_bus_output_fmts), GFP_KERNEL);
}
static const struct drm_bridge_funcs imx8qxp_pxl2dpi_bridge_funcs = {
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.attach = imx8qxp_pxl2dpi_bridge_attach,
.destroy = imx8qxp_pxl2dpi_bridge_destroy,
.atomic_check = imx8qxp_pxl2dpi_bridge_atomic_check,
.mode_set = imx8qxp_pxl2dpi_bridge_mode_set,
.atomic_disable = imx8qxp_pxl2dpi_bridge_atomic_disable,
.atomic_get_input_bus_fmts =
imx8qxp_pxl2dpi_bridge_atomic_get_input_bus_fmts,
.atomic_get_output_bus_fmts =
imx8qxp_pxl2dpi_bridge_atomic_get_output_bus_fmts,
};
static int
imx8qxp_pxl2dpi_get_available_ep_from_port(struct imx8qxp_pxl2dpi *p2d,
u32 port_id,
struct device_node **ep)
{
struct device_node *port;
int ret = 0;
int ep_cnt;
*ep = NULL;
port = of_graph_get_port_by_id(p2d->dev->of_node, port_id);
if (!port) {
DRM_DEV_ERROR(p2d->dev, "failed to get port@%u\n", port_id);
return -ENODEV;
}
ep_cnt = of_get_available_child_count(port);
if (ep_cnt == 0) {
DRM_DEV_ERROR(p2d->dev, "no available endpoints of port@%u\n",
port_id);
ret = -ENODEV;
goto out;
} else if (ep_cnt > 1) {
DRM_DEV_ERROR(p2d->dev,
"invalid available endpoints of port@%u\n",
port_id);
ret = -EINVAL;
goto out;
}
*ep = of_get_next_available_child(port, NULL);
if (!*ep) {
DRM_DEV_ERROR(p2d->dev,
"failed to get available endpoint of port@%u\n",
port_id);
ret = -ENODEV;
goto out;
}
out:
of_node_put(port);
return ret;
}
static int imx8qxp_pxl2dpi_find_next_bridge(struct imx8qxp_pxl2dpi *p2d)
{
struct device_node *ep __free(device_node) = NULL;
int ret;
ret = imx8qxp_pxl2dpi_get_available_ep_from_port(p2d, 1, &ep);
if (ret)
return ret;
struct device_node *remote __free(device_node) = of_graph_get_remote_port_parent(ep);
if (!remote || !of_device_is_available(remote)) {
DRM_DEV_ERROR(p2d->dev, "no available remote\n");
return -ENODEV;
} else if (!of_device_is_available(remote->parent)) {
DRM_DEV_ERROR(p2d->dev, "remote parent is not available\n");
return -ENODEV;
}
p2d->bridge.next_bridge = of_drm_find_and_get_bridge(remote);
if (!p2d->bridge.next_bridge)
return -EPROBE_DEFER;
return 0;
}
static int imx8qxp_pxl2dpi_set_pixel_link_sel(struct imx8qxp_pxl2dpi *p2d)
{
struct device_node *ep;
struct of_endpoint endpoint;
int ret;
ret = imx8qxp_pxl2dpi_get_available_ep_from_port(p2d, 0, &ep);
if (ret)
return ret;
ret = of_graph_parse_endpoint(ep, &endpoint);
if (ret) {
DRM_DEV_ERROR(p2d->dev,
"failed to parse endpoint of port@0: %d\n", ret);
goto out;
}
p2d->pl_sel = endpoint.id;
out:
of_node_put(ep);
return ret;
}
static int imx8qxp_pxl2dpi_parse_dt_companion(struct imx8qxp_pxl2dpi *p2d)
{
struct imx8qxp_pxl2dpi *companion_p2d;
struct device *dev = p2d->dev;
struct device_node *companion;
struct device_node *port1, *port2;
const struct of_device_id *match;
int dual_link;
int ret = 0;
/* Locate the companion PXL2DPI for dual-link operation, if any. */
companion = of_parse_phandle(dev->of_node, "fsl,companion-pxl2dpi", 0);
if (!companion)
return 0;
if (!of_device_is_available(companion)) {
DRM_DEV_ERROR(dev, "companion PXL2DPI is not available\n");
ret = -ENODEV;
goto out;
}
/*
* Sanity check: the companion bridge must have the same compatible
* string.
*/
match = of_match_device(dev->driver->of_match_table, dev);
if (!of_device_is_compatible(companion, match->compatible)) {
DRM_DEV_ERROR(dev, "companion PXL2DPI is incompatible\n");
ret = -ENXIO;
goto out;
}
p2d->companion = of_drm_find_and_get_bridge(companion);
if (!p2d->companion) {
ret = -EPROBE_DEFER;
DRM_DEV_DEBUG_DRIVER(p2d->dev,
"failed to find companion bridge: %d\n",
ret);
goto out;
}
companion_p2d = bridge_to_p2d(p2d->companion);
/*
* We need to work out if the sink is expecting us to function in
* dual-link mode. We do this by looking at the DT port nodes that
* the next bridges are connected to. If they are marked as expecting
* even pixels and odd pixels than we need to use the companion PXL2DPI.
*/
port1 = of_graph_get_port_by_id(p2d->bridge.next_bridge->of_node, 1);
port2 = of_graph_get_port_by_id(companion_p2d->bridge.next_bridge->of_node, 1);
dual_link = drm_of_lvds_get_dual_link_pixel_order(port1, port2);
of_node_put(port1);
of_node_put(port2);
if (dual_link < 0) {
ret = dual_link;
DRM_DEV_ERROR(dev, "failed to get dual link pixel order: %d\n",
ret);
goto out;
}
DRM_DEV_DEBUG_DRIVER(dev,
"dual-link configuration detected (companion bridge %pOF)\n",
companion);
out:
of_node_put(companion);
return ret;
}
static int imx8qxp_pxl2dpi_bridge_probe(struct platform_device *pdev)
{
struct imx8qxp_pxl2dpi *p2d;
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
int ret;
p2d = devm_drm_bridge_alloc(dev, struct imx8qxp_pxl2dpi, bridge,
&imx8qxp_pxl2dpi_bridge_funcs);
if (IS_ERR(p2d))
return PTR_ERR(p2d);
p2d->regmap = syscon_node_to_regmap(np->parent);
if (IS_ERR(p2d->regmap)) {
ret = PTR_ERR(p2d->regmap);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev, "failed to get regmap: %d\n", ret);
return ret;
}
ret = imx_scu_get_handle(&p2d->ipc_handle);
if (ret) {
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(dev, "failed to get SCU ipc handle: %d\n",
ret);
return ret;
}
p2d->dev = dev;
ret = of_property_read_u32(np, "fsl,sc-resource", &p2d->sc_resource);
if (ret) {
DRM_DEV_ERROR(dev, "failed to get SC resource %d\n", ret);
return ret;
}
ret = imx8qxp_pxl2dpi_find_next_bridge(p2d);
if (ret)
return ret;
ret = imx8qxp_pxl2dpi_set_pixel_link_sel(p2d);
if (ret)
return ret;
ret = imx8qxp_pxl2dpi_parse_dt_companion(p2d);
if (ret)
return ret;
platform_set_drvdata(pdev, p2d);
pm_runtime_enable(dev);
p2d->bridge.driver_private = p2d;
p2d->bridge.of_node = np;
drm_bridge_add(&p2d->bridge);
return ret;
}
static void imx8qxp_pxl2dpi_bridge_remove(struct platform_device *pdev)
{
struct imx8qxp_pxl2dpi *p2d = platform_get_drvdata(pdev);
drm_bridge_remove(&p2d->bridge);
pm_runtime_disable(&pdev->dev);
}
static const struct of_device_id imx8qxp_pxl2dpi_dt_ids[] = {
{ .compatible = "fsl,imx8qxp-pxl2dpi", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx8qxp_pxl2dpi_dt_ids);
static struct platform_driver imx8qxp_pxl2dpi_bridge_driver = {
.probe = imx8qxp_pxl2dpi_bridge_probe,
.remove = imx8qxp_pxl2dpi_bridge_remove,
.driver = {
.of_match_table = imx8qxp_pxl2dpi_dt_ids,
.name = DRIVER_NAME,
},
};
module_platform_driver(imx8qxp_pxl2dpi_bridge_driver);
MODULE_DESCRIPTION("i.MX8QXP pixel link to DPI bridge driver");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:" DRIVER_NAME);
@@ -0,0 +1,915 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2022,2023 NXP
*/
#include <linux/bitfield.h>
#include <linux/bits.h>
#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/math.h>
#include <linux/media-bus-format.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/phy/phy.h>
#include <linux/phy/phy-mipi-dphy.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
#include <drm/bridge/dw_mipi_dsi.h>
#include <drm/drm_bridge.h>
#include <drm/drm_mipi_dsi.h>
#include <drm/drm_modes.h>
/* DPHY PLL configuration registers */
#define DSI_REG 0x4c
#define CFGCLKFREQRANGE_MASK GENMASK(5, 0)
#define CFGCLKFREQRANGE(x) FIELD_PREP(CFGCLKFREQRANGE_MASK, (x))
#define CLKSEL_MASK GENMASK(7, 6)
#define CLKSEL_STOP FIELD_PREP(CLKSEL_MASK, 0)
#define CLKSEL_GEN FIELD_PREP(CLKSEL_MASK, 1)
#define CLKSEL_EXT FIELD_PREP(CLKSEL_MASK, 2)
#define HSFREQRANGE_MASK GENMASK(14, 8)
#define HSFREQRANGE(x) FIELD_PREP(HSFREQRANGE_MASK, (x))
#define UPDATE_PLL BIT(17)
#define SHADOW_CLR BIT(18)
#define CLK_EXT BIT(19)
#define DSI_WRITE_REG0 0x50
#define M_MASK GENMASK(9, 0)
#define M(x) FIELD_PREP(M_MASK, ((x) - 2))
#define N_MASK GENMASK(13, 10)
#define N(x) FIELD_PREP(N_MASK, ((x) - 1))
#define VCO_CTRL_MASK GENMASK(19, 14)
#define VCO_CTRL(x) FIELD_PREP(VCO_CTRL_MASK, (x))
#define PROP_CTRL_MASK GENMASK(25, 20)
#define PROP_CTRL(x) FIELD_PREP(PROP_CTRL_MASK, (x))
#define INT_CTRL_MASK GENMASK(31, 26)
#define INT_CTRL(x) FIELD_PREP(INT_CTRL_MASK, (x))
#define DSI_WRITE_REG1 0x54
#define GMP_CTRL_MASK GENMASK(1, 0)
#define GMP_CTRL(x) FIELD_PREP(GMP_CTRL_MASK, (x))
#define CPBIAS_CTRL_MASK GENMASK(8, 2)
#define CPBIAS_CTRL(x) FIELD_PREP(CPBIAS_CTRL_MASK, (x))
#define PLL_SHADOW_CTRL BIT(9)
/* display mux control register */
#define DISPLAY_MUX 0x60
#define MIPI_DSI_RGB666_MAP_CFG GENMASK(7, 6)
#define RGB666_CONFIG1 FIELD_PREP(MIPI_DSI_RGB666_MAP_CFG, 0)
#define RGB666_CONFIG2 FIELD_PREP(MIPI_DSI_RGB666_MAP_CFG, 1)
#define MIPI_DSI_RGB565_MAP_CFG GENMASK(5, 4)
#define RGB565_CONFIG1 FIELD_PREP(MIPI_DSI_RGB565_MAP_CFG, 0)
#define RGB565_CONFIG2 FIELD_PREP(MIPI_DSI_RGB565_MAP_CFG, 1)
#define RGB565_CONFIG3 FIELD_PREP(MIPI_DSI_RGB565_MAP_CFG, 2)
#define LCDIF_CROSS_LINE_PATTERN GENMASK(3, 0)
#define RGB888_TO_RGB888 FIELD_PREP(LCDIF_CROSS_LINE_PATTERN, 0)
#define RGB888_TO_RGB666 FIELD_PREP(LCDIF_CROSS_LINE_PATTERN, 6)
#define RGB565_TO_RGB565 FIELD_PREP(LCDIF_CROSS_LINE_PATTERN, 7)
#define MHZ(x) ((x) * 1000000UL)
#define REF_CLK_RATE_MAX MHZ(64)
#define REF_CLK_RATE_MIN MHZ(2)
#define FOUT_MAX MHZ(1250)
#define FOUT_MIN MHZ(40)
#define FVCO_DIV_FACTOR MHZ(80)
#define MBPS(x) ((x) * 1000000UL)
#define DATA_RATE_MAX_SPEED MBPS(2500)
#define DATA_RATE_MIN_SPEED MBPS(80)
#define M_MAX 625UL
#define M_MIN 64UL
#define N_MAX 16U
#define N_MIN 1U
struct imx93_dsi {
struct device *dev;
struct regmap *regmap;
struct clk *clk_pixel;
struct clk *clk_ref;
struct clk *clk_cfg;
struct dw_mipi_dsi *dmd;
struct dw_mipi_dsi_plat_data pdata;
union phy_configure_opts phy_cfg;
unsigned long ref_clk_rate;
u32 format;
};
struct dphy_pll_cfg {
u32 m; /* PLL Feedback Multiplication Ratio */
u32 n; /* PLL Input Frequency Division Ratio */
};
struct dphy_pll_vco_prop {
unsigned long max_fout;
u8 vco_cntl;
u8 prop_cntl;
};
struct dphy_pll_hsfreqrange {
unsigned long max_mbps;
u8 hsfreqrange;
};
/* DPHY Databook Table 3-13 Charge-pump Programmability */
static const struct dphy_pll_vco_prop vco_prop_map[] = {
{ 55, 0x3f, 0x0d },
{ 82, 0x37, 0x0d },
{ 110, 0x2f, 0x0d },
{ 165, 0x27, 0x0d },
{ 220, 0x1f, 0x0d },
{ 330, 0x17, 0x0d },
{ 440, 0x0f, 0x0d },
{ 660, 0x07, 0x0d },
{ 1149, 0x03, 0x0d },
{ 1152, 0x01, 0x0d },
{ 1250, 0x01, 0x0e },
};
/* DPHY Databook Table 5-7 Frequency Ranges and Defaults */
static const struct dphy_pll_hsfreqrange hsfreqrange_map[] = {
{ 89, 0x00 },
{ 99, 0x10 },
{ 109, 0x20 },
{ 119, 0x30 },
{ 129, 0x01 },
{ 139, 0x11 },
{ 149, 0x21 },
{ 159, 0x31 },
{ 169, 0x02 },
{ 179, 0x12 },
{ 189, 0x22 },
{ 204, 0x32 },
{ 219, 0x03 },
{ 234, 0x13 },
{ 249, 0x23 },
{ 274, 0x33 },
{ 299, 0x04 },
{ 324, 0x14 },
{ 349, 0x25 },
{ 399, 0x35 },
{ 449, 0x05 },
{ 499, 0x16 },
{ 549, 0x26 },
{ 599, 0x37 },
{ 649, 0x07 },
{ 699, 0x18 },
{ 749, 0x28 },
{ 799, 0x39 },
{ 849, 0x09 },
{ 899, 0x19 },
{ 949, 0x29 },
{ 999, 0x3a },
{ 1049, 0x0a },
{ 1099, 0x1a },
{ 1149, 0x2a },
{ 1199, 0x3b },
{ 1249, 0x0b },
{ 1299, 0x1b },
{ 1349, 0x2b },
{ 1399, 0x3c },
{ 1449, 0x0c },
{ 1499, 0x1c },
{ 1549, 0x2c },
{ 1599, 0x3d },
{ 1649, 0x0d },
{ 1699, 0x1d },
{ 1749, 0x2e },
{ 1799, 0x3e },
{ 1849, 0x0e },
{ 1899, 0x1e },
{ 1949, 0x2f },
{ 1999, 0x3f },
{ 2049, 0x0f },
{ 2099, 0x40 },
{ 2149, 0x41 },
{ 2199, 0x42 },
{ 2249, 0x43 },
{ 2299, 0x44 },
{ 2349, 0x45 },
{ 2399, 0x46 },
{ 2449, 0x47 },
{ 2499, 0x48 },
{ 2500, 0x49 },
};
static void dphy_pll_write(struct imx93_dsi *dsi, unsigned int reg, u32 value)
{
int ret;
ret = regmap_write(dsi->regmap, reg, value);
if (ret < 0)
dev_err(dsi->dev, "failed to write 0x%08x to pll reg 0x%x: %d\n",
value, reg, ret);
}
static inline unsigned long data_rate_to_fout(unsigned long data_rate)
{
/* Fout is half of data rate */
return data_rate / 2;
}
static int
dphy_pll_get_configure_from_opts(struct imx93_dsi *dsi,
struct phy_configure_opts_mipi_dphy *dphy_opts,
struct dphy_pll_cfg *cfg)
{
struct device *dev = dsi->dev;
unsigned long fin = dsi->ref_clk_rate;
unsigned long fout;
unsigned long best_fout = 0;
unsigned int fvco_div;
unsigned int min_n, max_n, n, best_n = UINT_MAX;
unsigned long m, best_m = 0;
unsigned long min_delta = ULONG_MAX;
unsigned long delta;
u64 tmp;
if (dphy_opts->hs_clk_rate < DATA_RATE_MIN_SPEED ||
dphy_opts->hs_clk_rate > DATA_RATE_MAX_SPEED) {
dev_dbg(dev, "invalid data rate per lane: %lu\n",
dphy_opts->hs_clk_rate);
return -EINVAL;
}
fout = data_rate_to_fout(dphy_opts->hs_clk_rate);
/* DPHY Databook 3.3.6.1 Output Frequency */
/* Fout = Fvco / Fvco_div = (Fin * M) / (Fvco_div * N) */
/* Fvco_div could be 1/2/4/8 according to Fout range. */
fvco_div = 8UL / min(DIV_ROUND_UP(fout, FVCO_DIV_FACTOR), 8UL);
/* limitation: 2MHz <= Fin / N <= 8MHz */
min_n = DIV_ROUND_UP_ULL((u64)fin, MHZ(8));
max_n = DIV_ROUND_DOWN_ULL((u64)fin, MHZ(2));
/* clamp possible N(s) */
min_n = clamp(min_n, N_MIN, N_MAX);
max_n = clamp(max_n, N_MIN, N_MAX);
dev_dbg(dev, "Fout = %lu, Fvco_div = %u, n_range = [%u, %u]\n",
fout, fvco_div, min_n, max_n);
for (n = min_n; n <= max_n; n++) {
/* M = (Fout * N * Fvco_div) / Fin */
m = DIV_ROUND_CLOSEST(fout * n * fvco_div, fin);
/* check M range */
if (m < M_MIN || m > M_MAX)
continue;
/* calculate temporary Fout */
tmp = m * fin;
do_div(tmp, n * fvco_div);
if (tmp < FOUT_MIN || tmp > FOUT_MAX)
continue;
delta = abs(fout - tmp);
if (delta < min_delta) {
best_n = n;
best_m = m;
min_delta = delta;
best_fout = tmp;
}
}
if (best_fout) {
cfg->m = best_m;
cfg->n = best_n;
dev_dbg(dev, "best Fout = %lu, m = %u, n = %u\n",
best_fout, cfg->m, cfg->n);
} else {
dev_dbg(dev, "failed to find best Fout\n");
return -EINVAL;
}
return 0;
}
static void dphy_pll_clear_shadow(struct imx93_dsi *dsi)
{
/* Reference DPHY Databook Figure 3-3 Initialization Timing Diagram. */
/* Select clock generation first. */
dphy_pll_write(dsi, DSI_REG, CLKSEL_GEN);
/* Clear shadow after clock selection is done a while. */
fsleep(1);
dphy_pll_write(dsi, DSI_REG, CLKSEL_GEN | SHADOW_CLR);
/* A minimum pulse of 5ns on shadow_clear signal. */
fsleep(1);
dphy_pll_write(dsi, DSI_REG, CLKSEL_GEN);
}
static unsigned long dphy_pll_get_cfgclkrange(struct imx93_dsi *dsi)
{
/*
* DPHY Databook Table 4-4 System Control Signals mentions an equation
* for cfgclkfreqrange[5:0].
*/
return (clk_get_rate(dsi->clk_cfg) / MHZ(1) - 17) * 4;
}
static u8
dphy_pll_get_hsfreqrange(struct phy_configure_opts_mipi_dphy *dphy_opts)
{
unsigned long mbps = dphy_opts->hs_clk_rate / MHZ(1);
int i;
for (i = 0; i < ARRAY_SIZE(hsfreqrange_map); i++)
if (mbps <= hsfreqrange_map[i].max_mbps)
return hsfreqrange_map[i].hsfreqrange;
return 0;
}
static u8 dphy_pll_get_vco(struct phy_configure_opts_mipi_dphy *dphy_opts)
{
unsigned long fout = data_rate_to_fout(dphy_opts->hs_clk_rate) / MHZ(1);
int i;
for (i = 0; i < ARRAY_SIZE(vco_prop_map); i++)
if (fout <= vco_prop_map[i].max_fout)
return vco_prop_map[i].vco_cntl;
return 0;
}
static u8 dphy_pll_get_prop(struct phy_configure_opts_mipi_dphy *dphy_opts)
{
unsigned long fout = data_rate_to_fout(dphy_opts->hs_clk_rate) / MHZ(1);
int i;
for (i = 0; i < ARRAY_SIZE(vco_prop_map); i++)
if (fout <= vco_prop_map[i].max_fout)
return vco_prop_map[i].prop_cntl;
return 0;
}
static int dphy_pll_update(struct imx93_dsi *dsi)
{
int ret;
ret = regmap_update_bits(dsi->regmap, DSI_REG, UPDATE_PLL, UPDATE_PLL);
if (ret < 0) {
dev_err(dsi->dev, "failed to set UPDATE_PLL: %d\n", ret);
return ret;
}
/*
* The updatepll signal should be asserted for a minimum of four clkin
* cycles, according to DPHY Databook Figure 3-3 Initialization Timing
* Diagram.
*/
fsleep(10);
ret = regmap_update_bits(dsi->regmap, DSI_REG, UPDATE_PLL, 0);
if (ret < 0) {
dev_err(dsi->dev, "failed to clear UPDATE_PLL: %d\n", ret);
return ret;
}
return 0;
}
static int dphy_pll_configure(struct imx93_dsi *dsi, union phy_configure_opts *opts)
{
struct dphy_pll_cfg cfg = { 0 };
u32 val;
int ret;
ret = dphy_pll_get_configure_from_opts(dsi, &opts->mipi_dphy, &cfg);
if (ret) {
dev_err(dsi->dev, "failed to get phy pll cfg %d\n", ret);
return ret;
}
dphy_pll_clear_shadow(dsi);
/* DSI_REG */
val = CLKSEL_GEN |
CFGCLKFREQRANGE(dphy_pll_get_cfgclkrange(dsi)) |
HSFREQRANGE(dphy_pll_get_hsfreqrange(&opts->mipi_dphy));
dphy_pll_write(dsi, DSI_REG, val);
/* DSI_WRITE_REG0 */
val = M(cfg.m) | N(cfg.n) | INT_CTRL(0) |
VCO_CTRL(dphy_pll_get_vco(&opts->mipi_dphy)) |
PROP_CTRL(dphy_pll_get_prop(&opts->mipi_dphy));
dphy_pll_write(dsi, DSI_WRITE_REG0, val);
/* DSI_WRITE_REG1 */
dphy_pll_write(dsi, DSI_WRITE_REG1, GMP_CTRL(1) | CPBIAS_CTRL(0x10));
ret = clk_prepare_enable(dsi->clk_ref);
if (ret < 0) {
dev_err(dsi->dev, "failed to enable ref clock: %d\n", ret);
return ret;
}
/*
* At least 10 refclk cycles are required before updatePLL assertion,
* according to DPHY Databook Figure 3-3 Initialization Timing Diagram.
*/
fsleep(10);
ret = dphy_pll_update(dsi);
if (ret < 0) {
clk_disable_unprepare(dsi->clk_ref);
return ret;
}
return 0;
}
static void dphy_pll_clear_reg(struct imx93_dsi *dsi)
{
dphy_pll_write(dsi, DSI_REG, 0);
dphy_pll_write(dsi, DSI_WRITE_REG0, 0);
dphy_pll_write(dsi, DSI_WRITE_REG1, 0);
}
static int dphy_pll_init(struct imx93_dsi *dsi)
{
int ret;
ret = clk_prepare_enable(dsi->clk_cfg);
if (ret < 0) {
dev_err(dsi->dev, "failed to enable config clock: %d\n", ret);
return ret;
}
dphy_pll_clear_reg(dsi);
return 0;
}
static void dphy_pll_uninit(struct imx93_dsi *dsi)
{
dphy_pll_clear_reg(dsi);
clk_disable_unprepare(dsi->clk_cfg);
}
static void dphy_pll_power_off(struct imx93_dsi *dsi)
{
dphy_pll_clear_reg(dsi);
clk_disable_unprepare(dsi->clk_ref);
}
static int imx93_dsi_get_phy_configure_opts(struct imx93_dsi *dsi,
const struct drm_display_mode *mode,
union phy_configure_opts *phy_cfg,
u32 lanes, u32 format)
{
struct device *dev = dsi->dev;
int bpp;
int ret;
bpp = mipi_dsi_pixel_format_to_bpp(format);
if (bpp < 0) {
dev_dbg(dev, "failed to get bpp for pixel format %d\n", format);
return -EINVAL;
}
ret = phy_mipi_dphy_get_default_config(mode->clock * MSEC_PER_SEC, bpp,
lanes, &phy_cfg->mipi_dphy);
if (ret < 0) {
dev_dbg(dev, "failed to get default phy cfg %d\n", ret);
return ret;
}
return 0;
}
static enum drm_mode_status
imx93_dsi_validate_mode(struct imx93_dsi *dsi, const struct drm_display_mode *mode)
{
struct drm_bridge *dmd_bridge = dw_mipi_dsi_get_bridge(dsi->dmd);
struct drm_bridge *last_bridge __free(drm_bridge_put) =
drm_bridge_chain_get_last_bridge(dmd_bridge->encoder);
if ((last_bridge->ops & DRM_BRIDGE_OP_DETECT) &&
(last_bridge->ops & DRM_BRIDGE_OP_EDID)) {
unsigned long pixel_clock_rate = mode->clock * 1000;
unsigned long rounded_rate;
/* Allow +/-0.5% pixel clock rate deviation */
rounded_rate = clk_round_rate(dsi->clk_pixel, pixel_clock_rate);
if (rounded_rate < pixel_clock_rate * 995 / 1000 ||
rounded_rate > pixel_clock_rate * 1005 / 1000) {
dev_dbg(dsi->dev, "failed to round clock for mode " DRM_MODE_FMT "\n",
DRM_MODE_ARG(mode));
return MODE_NOCLOCK;
}
}
return MODE_OK;
}
static enum drm_mode_status
imx93_dsi_validate_phy(struct imx93_dsi *dsi, const struct drm_display_mode *mode,
unsigned long mode_flags, u32 lanes, u32 format)
{
union phy_configure_opts phy_cfg;
struct dphy_pll_cfg cfg = { 0 };
struct device *dev = dsi->dev;
int ret;
ret = imx93_dsi_get_phy_configure_opts(dsi, mode, &phy_cfg, lanes,
format);
if (ret < 0) {
dev_dbg(dev, "failed to get phy cfg opts %d\n", ret);
return MODE_ERROR;
}
ret = dphy_pll_get_configure_from_opts(dsi, &phy_cfg.mipi_dphy, &cfg);
if (ret < 0) {
dev_dbg(dev, "failed to get phy pll cfg %d\n", ret);
return MODE_NOCLOCK;
}
return MODE_OK;
}
static enum drm_mode_status
imx93_dsi_mode_valid(void *priv_data, const struct drm_display_mode *mode,
unsigned long mode_flags, u32 lanes, u32 format)
{
struct imx93_dsi *dsi = priv_data;
struct device *dev = dsi->dev;
enum drm_mode_status ret;
ret = imx93_dsi_validate_mode(dsi, mode);
if (ret != MODE_OK) {
dev_dbg(dev, "failed to validate mode " DRM_MODE_FMT "\n",
DRM_MODE_ARG(mode));
return ret;
}
ret = imx93_dsi_validate_phy(dsi, mode, mode_flags, lanes, format);
if (ret != MODE_OK) {
dev_dbg(dev, "failed to validate phy for mode " DRM_MODE_FMT "\n",
DRM_MODE_ARG(mode));
return ret;
}
return MODE_OK;
}
static bool imx93_dsi_mode_fixup(void *priv_data,
const struct drm_display_mode *mode,
struct drm_display_mode *adjusted_mode)
{
struct imx93_dsi *dsi = priv_data;
unsigned long pixel_clock_rate;
unsigned long rounded_rate;
pixel_clock_rate = mode->clock * 1000;
rounded_rate = clk_round_rate(dsi->clk_pixel, pixel_clock_rate);
memcpy(adjusted_mode, mode, sizeof(*mode));
adjusted_mode->clock = rounded_rate / 1000;
dev_dbg(dsi->dev, "adj clock %d for mode " DRM_MODE_FMT "\n",
adjusted_mode->clock, DRM_MODE_ARG(mode));
return true;
}
static u32 *imx93_dsi_get_input_bus_fmts(void *priv_data,
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)
{
u32 *input_fmts, input_fmt;
*num_input_fmts = 0;
switch (output_fmt) {
case MEDIA_BUS_FMT_RGB888_1X24:
case MEDIA_BUS_FMT_RGB666_1X18:
case MEDIA_BUS_FMT_FIXED:
input_fmt = MEDIA_BUS_FMT_RGB888_1X24;
break;
case MEDIA_BUS_FMT_RGB565_1X16:
input_fmt = output_fmt;
break;
default:
return NULL;
}
input_fmts = kmalloc_obj(*input_fmts);
if (!input_fmts)
return NULL;
input_fmts[0] = input_fmt;
*num_input_fmts = 1;
return input_fmts;
}
static int imx93_dsi_phy_init(void *priv_data)
{
struct imx93_dsi *dsi = priv_data;
unsigned int fmt = 0;
int ret;
switch (dsi->format) {
case MIPI_DSI_FMT_RGB888:
fmt = RGB888_TO_RGB888;
break;
case MIPI_DSI_FMT_RGB666:
fmt = RGB888_TO_RGB666;
regmap_update_bits(dsi->regmap, DISPLAY_MUX,
MIPI_DSI_RGB666_MAP_CFG, RGB666_CONFIG2);
break;
case MIPI_DSI_FMT_RGB666_PACKED:
fmt = RGB888_TO_RGB666;
regmap_update_bits(dsi->regmap, DISPLAY_MUX,
MIPI_DSI_RGB666_MAP_CFG, RGB666_CONFIG1);
break;
case MIPI_DSI_FMT_RGB565:
fmt = RGB565_TO_RGB565;
regmap_update_bits(dsi->regmap, DISPLAY_MUX,
MIPI_DSI_RGB565_MAP_CFG, RGB565_CONFIG1);
break;
}
regmap_update_bits(dsi->regmap, DISPLAY_MUX, LCDIF_CROSS_LINE_PATTERN, fmt);
ret = dphy_pll_init(dsi);
if (ret < 0) {
dev_err(dsi->dev, "failed to init phy pll: %d\n", ret);
return ret;
}
ret = dphy_pll_configure(dsi, &dsi->phy_cfg);
if (ret < 0) {
dev_err(dsi->dev, "failed to configure phy pll: %d\n", ret);
dphy_pll_uninit(dsi);
return ret;
}
return 0;
}
static void imx93_dsi_phy_power_off(void *priv_data)
{
struct imx93_dsi *dsi = priv_data;
dphy_pll_power_off(dsi);
dphy_pll_uninit(dsi);
}
static int
imx93_dsi_get_lane_mbps(void *priv_data, const struct drm_display_mode *mode,
unsigned long mode_flags, u32 lanes, u32 format,
unsigned int *lane_mbps)
{
struct imx93_dsi *dsi = priv_data;
union phy_configure_opts phy_cfg;
struct device *dev = dsi->dev;
int ret;
ret = imx93_dsi_get_phy_configure_opts(dsi, mode, &phy_cfg, lanes,
format);
if (ret < 0) {
dev_dbg(dev, "failed to get phy cfg opts %d\n", ret);
return ret;
}
*lane_mbps = DIV_ROUND_UP(phy_cfg.mipi_dphy.hs_clk_rate, USEC_PER_SEC);
memcpy(&dsi->phy_cfg, &phy_cfg, sizeof(phy_cfg));
dev_dbg(dev, "get lane_mbps %u for mode " DRM_MODE_FMT "\n",
*lane_mbps, DRM_MODE_ARG(mode));
return 0;
}
/* High-Speed Transition Times */
struct hstt {
unsigned int maxfreq;
struct dw_mipi_dsi_dphy_timing timing;
};
#define HSTT(_maxfreq, _c_lp2hs, _c_hs2lp, _d_lp2hs, _d_hs2lp) \
{ \
.maxfreq = (_maxfreq), \
.timing = { \
.clk_lp2hs = (_c_lp2hs), \
.clk_hs2lp = (_c_hs2lp), \
.data_lp2hs = (_d_lp2hs), \
.data_hs2lp = (_d_hs2lp), \
} \
}
/* DPHY Databook Table A-4 High-Speed Transition Times */
static const struct hstt hstt_table[] = {
HSTT(80, 21, 17, 15, 10),
HSTT(90, 23, 17, 16, 10),
HSTT(100, 22, 17, 16, 10),
HSTT(110, 25, 18, 17, 11),
HSTT(120, 26, 20, 18, 11),
HSTT(130, 27, 19, 19, 11),
HSTT(140, 27, 19, 19, 11),
HSTT(150, 28, 20, 20, 12),
HSTT(160, 30, 21, 22, 13),
HSTT(170, 30, 21, 23, 13),
HSTT(180, 31, 21, 23, 13),
HSTT(190, 32, 22, 24, 13),
HSTT(205, 35, 22, 25, 13),
HSTT(220, 37, 26, 27, 15),
HSTT(235, 38, 28, 27, 16),
HSTT(250, 41, 29, 30, 17),
HSTT(275, 43, 29, 32, 18),
HSTT(300, 45, 32, 35, 19),
HSTT(325, 48, 33, 36, 18),
HSTT(350, 51, 35, 40, 20),
HSTT(400, 59, 37, 44, 21),
HSTT(450, 65, 40, 49, 23),
HSTT(500, 71, 41, 54, 24),
HSTT(550, 77, 44, 57, 26),
HSTT(600, 82, 46, 64, 27),
HSTT(650, 87, 48, 67, 28),
HSTT(700, 94, 52, 71, 29),
HSTT(750, 99, 52, 75, 31),
HSTT(800, 105, 55, 82, 32),
HSTT(850, 110, 58, 85, 32),
HSTT(900, 115, 58, 88, 35),
HSTT(950, 120, 62, 93, 36),
HSTT(1000, 128, 63, 99, 38),
HSTT(1050, 132, 65, 102, 38),
HSTT(1100, 138, 67, 106, 39),
HSTT(1150, 146, 69, 112, 42),
HSTT(1200, 151, 71, 117, 43),
HSTT(1250, 153, 74, 120, 45),
HSTT(1300, 160, 73, 124, 46),
HSTT(1350, 165, 76, 130, 47),
HSTT(1400, 172, 78, 134, 49),
HSTT(1450, 177, 80, 138, 49),
HSTT(1500, 183, 81, 143, 52),
HSTT(1550, 191, 84, 147, 52),
HSTT(1600, 194, 85, 152, 52),
HSTT(1650, 201, 86, 155, 53),
HSTT(1700, 208, 88, 161, 53),
HSTT(1750, 212, 89, 165, 53),
HSTT(1800, 220, 90, 171, 54),
HSTT(1850, 223, 92, 175, 54),
HSTT(1900, 231, 91, 180, 55),
HSTT(1950, 236, 95, 185, 56),
HSTT(2000, 243, 97, 190, 56),
HSTT(2050, 248, 99, 194, 58),
HSTT(2100, 252, 100, 199, 59),
HSTT(2150, 259, 102, 204, 61),
HSTT(2200, 266, 105, 210, 62),
HSTT(2250, 269, 109, 213, 63),
HSTT(2300, 272, 109, 217, 65),
HSTT(2350, 281, 112, 225, 66),
HSTT(2400, 283, 115, 226, 66),
HSTT(2450, 282, 115, 226, 67),
HSTT(2500, 281, 118, 227, 67),
};
static int imx93_dsi_phy_get_timing(void *priv_data, unsigned int lane_mbps,
struct dw_mipi_dsi_dphy_timing *timing)
{
struct imx93_dsi *dsi = priv_data;
struct device *dev = dsi->dev;
int i;
for (i = 0; i < ARRAY_SIZE(hstt_table); i++)
if (lane_mbps <= hstt_table[i].maxfreq)
break;
if (i == ARRAY_SIZE(hstt_table)) {
dev_err(dev, "failed to get phy timing for lane_mbps %u\n",
lane_mbps);
return -EINVAL;
}
*timing = hstt_table[i].timing;
dev_dbg(dev, "get phy timing for %u <= %u (lane_mbps)\n",
lane_mbps, hstt_table[i].maxfreq);
return 0;
}
static const struct dw_mipi_dsi_phy_ops imx93_dsi_phy_ops = {
.init = imx93_dsi_phy_init,
.power_off = imx93_dsi_phy_power_off,
.get_lane_mbps = imx93_dsi_get_lane_mbps,
.get_timing = imx93_dsi_phy_get_timing,
};
static int imx93_dsi_host_attach(void *priv_data, struct mipi_dsi_device *device)
{
struct imx93_dsi *dsi = priv_data;
dsi->format = device->format;
return 0;
}
static const struct dw_mipi_dsi_host_ops imx93_dsi_host_ops = {
.attach = imx93_dsi_host_attach,
};
static int imx93_dsi_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct imx93_dsi *dsi;
int ret;
dsi = devm_kzalloc(dev, sizeof(*dsi), GFP_KERNEL);
if (!dsi)
return -ENOMEM;
dsi->regmap = syscon_regmap_lookup_by_phandle(np, "fsl,media-blk-ctrl");
if (IS_ERR(dsi->regmap)) {
ret = PTR_ERR(dsi->regmap);
dev_err(dev, "failed to get block ctrl regmap: %d\n", ret);
return ret;
}
dsi->clk_pixel = devm_clk_get(dev, "pix");
if (IS_ERR(dsi->clk_pixel))
return dev_err_probe(dev, PTR_ERR(dsi->clk_pixel),
"failed to get pixel clock\n");
dsi->clk_cfg = devm_clk_get(dev, "phy_cfg");
if (IS_ERR(dsi->clk_cfg))
return dev_err_probe(dev, PTR_ERR(dsi->clk_cfg),
"failed to get phy cfg clock\n");
dsi->clk_ref = devm_clk_get(dev, "phy_ref");
if (IS_ERR(dsi->clk_ref))
return dev_err_probe(dev, PTR_ERR(dsi->clk_ref),
"failed to get phy ref clock\n");
dsi->ref_clk_rate = clk_get_rate(dsi->clk_ref);
if (dsi->ref_clk_rate < REF_CLK_RATE_MIN ||
dsi->ref_clk_rate > REF_CLK_RATE_MAX) {
dev_err(dev, "invalid phy ref clock rate %lu\n",
dsi->ref_clk_rate);
return -EINVAL;
}
dev_dbg(dev, "phy ref clock rate: %lu\n", dsi->ref_clk_rate);
dsi->dev = dev;
dsi->pdata.max_data_lanes = 4;
dsi->pdata.mode_valid = imx93_dsi_mode_valid;
dsi->pdata.mode_fixup = imx93_dsi_mode_fixup;
dsi->pdata.get_input_bus_fmts = imx93_dsi_get_input_bus_fmts;
dsi->pdata.phy_ops = &imx93_dsi_phy_ops;
dsi->pdata.host_ops = &imx93_dsi_host_ops;
dsi->pdata.priv_data = dsi;
platform_set_drvdata(pdev, dsi);
dsi->dmd = dw_mipi_dsi_probe(pdev, &dsi->pdata);
if (IS_ERR(dsi->dmd))
return dev_err_probe(dev, PTR_ERR(dsi->dmd),
"failed to probe dw_mipi_dsi\n");
return 0;
}
static void imx93_dsi_remove(struct platform_device *pdev)
{
struct imx93_dsi *dsi = platform_get_drvdata(pdev);
dw_mipi_dsi_remove(dsi->dmd);
}
static const struct of_device_id imx93_dsi_dt_ids[] = {
{ .compatible = "fsl,imx93-mipi-dsi", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx93_dsi_dt_ids);
static struct platform_driver imx93_dsi_driver = {
.probe = imx93_dsi_probe,
.remove = imx93_dsi_remove,
.driver = {
.of_match_table = imx93_dsi_dt_ids,
.name = "imx93_mipi_dsi",
},
};
module_platform_driver(imx93_dsi_driver);
MODULE_DESCRIPTION("Freescale i.MX93 MIPI DSI driver");
MODULE_AUTHOR("Liu Ying <victor.liu@nxp.com>");
MODULE_LICENSE("GPL");