Commit e4a2d54a authored by Cristian Ciocaltea's avatar Cristian Ciocaltea Committed by Heiko Stuebner
Browse files

drm/bridge: dw-hdmi-qp: Add CEC support



Add support for the CEC interface of the Synopsys DesignWare HDMI QP TX
controller.

This is based on the downstream implementation, but rewritten on top of
the CEC helpers added recently to the DRM HDMI connector framework.

Also note struct dw_hdmi_qp_plat_data has been extended to include the
CEC IRQ number to be provided by the platform driver.

Co-developed-by: default avatarAlgea Cao <algea.cao@rock-chips.com>
Signed-off-by: default avatarAlgea Cao <algea.cao@rock-chips.com>
Co-developed-by: default avatarDerek Foreman <derek.foreman@collabora.com>
Signed-off-by: default avatarDerek Foreman <derek.foreman@collabora.com>
Reviewed-by: default avatarDmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Signed-off-by: default avatarCristian Ciocaltea <cristian.ciocaltea@collabora.com>
Signed-off-by: default avatarHeiko Stuebner <heiko@sntech.de>
Link: https://lore.kernel.org/r/20250903-rk3588-hdmi-cec-v4-1-fa25163c4b08@collabora.com
parent e485883c
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -61,6 +61,14 @@ config DRM_DW_HDMI_QP
	select DRM_KMS_HELPER
	select REGMAP_MMIO

config DRM_DW_HDMI_QP_CEC
	bool "Synopsis Designware QP CEC interface"
	depends on DRM_DW_HDMI_QP
	select DRM_DISPLAY_HDMI_CEC_HELPER
	help
	  Support the CEC interface which is part of the Synopsys
	  Designware HDMI QP block.

config DRM_DW_MIPI_DSI
	tristate
	select DRM_KMS_HELPER
+212 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@

#include <drm/bridge/dw_hdmi_qp.h>
#include <drm/display/drm_hdmi_helper.h>
#include <drm/display/drm_hdmi_cec_helper.h>
#include <drm/display/drm_hdmi_state_helper.h>
#include <drm/drm_atomic.h>
#include <drm/drm_atomic_helper.h>
@@ -26,6 +27,8 @@
#include <drm/drm_edid.h>
#include <drm/drm_modes.h>

#include <media/cec.h>

#include <sound/hdmi-codec.h>

#include "dw-hdmi-qp.h"
@@ -131,12 +134,28 @@ struct dw_hdmi_qp_i2c {
	bool			is_segment;
};

#ifdef CONFIG_DRM_DW_HDMI_QP_CEC
struct dw_hdmi_qp_cec {
	struct drm_connector *connector;
	int irq;
	u32 addresses;
	struct cec_msg rx_msg;
	u8 tx_status;
	bool tx_done;
	bool rx_done;
};
#endif

struct dw_hdmi_qp {
	struct drm_bridge bridge;

	struct device *dev;
	struct dw_hdmi_qp_i2c *i2c;

#ifdef CONFIG_DRM_DW_HDMI_QP_CEC
	struct dw_hdmi_qp_cec *cec;
#endif

	struct {
		const struct dw_hdmi_qp_phy_ops *ops;
		void *data;
@@ -965,6 +984,179 @@ static int dw_hdmi_qp_bridge_write_infoframe(struct drm_bridge *bridge,
	}
}

#ifdef CONFIG_DRM_DW_HDMI_QP_CEC
static irqreturn_t dw_hdmi_qp_cec_hardirq(int irq, void *dev_id)
{
	struct dw_hdmi_qp *hdmi = dev_id;
	struct dw_hdmi_qp_cec *cec = hdmi->cec;
	irqreturn_t ret = IRQ_HANDLED;
	u32 stat;

	stat = dw_hdmi_qp_read(hdmi, CEC_INT_STATUS);
	if (stat == 0)
		return IRQ_NONE;

	dw_hdmi_qp_write(hdmi, stat, CEC_INT_CLEAR);

	if (stat & CEC_STAT_LINE_ERR) {
		cec->tx_status = CEC_TX_STATUS_ERROR;
		cec->tx_done = true;
		ret = IRQ_WAKE_THREAD;
	} else if (stat & CEC_STAT_DONE) {
		cec->tx_status = CEC_TX_STATUS_OK;
		cec->tx_done = true;
		ret = IRQ_WAKE_THREAD;
	} else if (stat & CEC_STAT_NACK) {
		cec->tx_status = CEC_TX_STATUS_NACK;
		cec->tx_done = true;
		ret = IRQ_WAKE_THREAD;
	}

	if (stat & CEC_STAT_EOM) {
		unsigned int len, i, val;

		val = dw_hdmi_qp_read(hdmi, CEC_RX_COUNT_STATUS);
		len = (val & 0xf) + 1;

		if (len > sizeof(cec->rx_msg.msg))
			len = sizeof(cec->rx_msg.msg);

		for (i = 0; i < 4; i++) {
			val = dw_hdmi_qp_read(hdmi, CEC_RX_DATA3_0 + i * 4);
			cec->rx_msg.msg[i * 4] = val & 0xff;
			cec->rx_msg.msg[i * 4 + 1] = (val >> 8) & 0xff;
			cec->rx_msg.msg[i * 4 + 2] = (val >> 16) & 0xff;
			cec->rx_msg.msg[i * 4 + 3] = (val >> 24) & 0xff;
		}

		dw_hdmi_qp_write(hdmi, 1, CEC_LOCK_CONTROL);

		cec->rx_msg.len = len;
		cec->rx_done = true;

		ret = IRQ_WAKE_THREAD;
	}

	return ret;
}

static irqreturn_t dw_hdmi_qp_cec_thread(int irq, void *dev_id)
{
	struct dw_hdmi_qp *hdmi = dev_id;
	struct dw_hdmi_qp_cec *cec = hdmi->cec;

	if (cec->tx_done) {
		cec->tx_done = false;
		drm_connector_hdmi_cec_transmit_attempt_done(cec->connector,
							     cec->tx_status);
	}

	if (cec->rx_done) {
		cec->rx_done = false;
		drm_connector_hdmi_cec_received_msg(cec->connector, &cec->rx_msg);
	}

	return IRQ_HANDLED;
}

static int dw_hdmi_qp_cec_init(struct drm_bridge *bridge,
			       struct drm_connector *connector)
{
	struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge);
	struct dw_hdmi_qp_cec *cec = hdmi->cec;

	cec->connector = connector;

	dw_hdmi_qp_write(hdmi, 0, CEC_TX_COUNT);
	dw_hdmi_qp_write(hdmi, ~0, CEC_INT_CLEAR);
	dw_hdmi_qp_write(hdmi, 0, CEC_INT_MASK_N);

	return devm_request_threaded_irq(hdmi->dev, cec->irq,
					 dw_hdmi_qp_cec_hardirq,
					 dw_hdmi_qp_cec_thread, IRQF_SHARED,
					 dev_name(hdmi->dev), hdmi);
}

static int dw_hdmi_qp_cec_log_addr(struct drm_bridge *bridge, u8 logical_addr)
{
	struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge);
	struct dw_hdmi_qp_cec *cec = hdmi->cec;

	if (logical_addr == CEC_LOG_ADDR_INVALID)
		cec->addresses = 0;
	else
		cec->addresses |= BIT(logical_addr) | CEC_ADDR_BROADCAST;

	dw_hdmi_qp_write(hdmi, cec->addresses, CEC_ADDR);

	return 0;
}

static int dw_hdmi_qp_cec_enable(struct drm_bridge *bridge, bool enable)
{
	struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge);
	unsigned int irqs;
	u32 swdisable;

	if (!enable) {
		dw_hdmi_qp_write(hdmi, 0, CEC_INT_MASK_N);
		dw_hdmi_qp_write(hdmi, ~0, CEC_INT_CLEAR);

		swdisable = dw_hdmi_qp_read(hdmi, GLOBAL_SWDISABLE);
		swdisable = swdisable | CEC_SWDISABLE;
		dw_hdmi_qp_write(hdmi, swdisable, GLOBAL_SWDISABLE);
	} else {
		swdisable = dw_hdmi_qp_read(hdmi, GLOBAL_SWDISABLE);
		swdisable = swdisable & ~CEC_SWDISABLE;
		dw_hdmi_qp_write(hdmi, swdisable, GLOBAL_SWDISABLE);

		dw_hdmi_qp_write(hdmi, ~0, CEC_INT_CLEAR);
		dw_hdmi_qp_write(hdmi, 1, CEC_LOCK_CONTROL);

		dw_hdmi_qp_cec_log_addr(bridge, CEC_LOG_ADDR_INVALID);

		irqs = CEC_STAT_LINE_ERR | CEC_STAT_NACK | CEC_STAT_EOM |
		       CEC_STAT_DONE;
		dw_hdmi_qp_write(hdmi, ~0, CEC_INT_CLEAR);
		dw_hdmi_qp_write(hdmi, irqs, CEC_INT_MASK_N);
	}

	return 0;
}

static int dw_hdmi_qp_cec_transmit(struct drm_bridge *bridge, u8 attempts,
				   u32 signal_free_time, struct cec_msg *msg)
{
	struct dw_hdmi_qp *hdmi = dw_hdmi_qp_from_bridge(bridge);
	unsigned int i;
	u32 val;

	for (i = 0; i < msg->len; i++) {
		if (!(i % 4))
			val = msg->msg[i];
		if ((i % 4) == 1)
			val |= msg->msg[i] << 8;
		if ((i % 4) == 2)
			val |= msg->msg[i] << 16;
		if ((i % 4) == 3)
			val |= msg->msg[i] << 24;

		if (i == (msg->len - 1) || (i % 4) == 3)
			dw_hdmi_qp_write(hdmi, val, CEC_TX_DATA3_0 + (i / 4) * 4);
	}

	dw_hdmi_qp_write(hdmi, msg->len - 1, CEC_TX_COUNT);
	dw_hdmi_qp_write(hdmi, CEC_CTRL_START, CEC_TX_CONTROL);

	return 0;
}
#else
#define dw_hdmi_qp_cec_init NULL
#define dw_hdmi_qp_cec_enable NULL
#define dw_hdmi_qp_cec_log_addr NULL
#define dw_hdmi_qp_cec_transmit NULL
#endif /* CONFIG_DRM_DW_HDMI_QP_CEC */

static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = {
	.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
	.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
@@ -979,6 +1171,10 @@ static const struct drm_bridge_funcs dw_hdmi_qp_bridge_funcs = {
	.hdmi_audio_startup = dw_hdmi_qp_audio_enable,
	.hdmi_audio_shutdown = dw_hdmi_qp_audio_disable,
	.hdmi_audio_prepare = dw_hdmi_qp_audio_prepare,
	.hdmi_cec_init = dw_hdmi_qp_cec_init,
	.hdmi_cec_enable = dw_hdmi_qp_cec_enable,
	.hdmi_cec_log_addr = dw_hdmi_qp_cec_log_addr,
	.hdmi_cec_transmit = dw_hdmi_qp_cec_transmit,
};

static irqreturn_t dw_hdmi_qp_main_hardirq(int irq, void *dev_id)
@@ -1093,6 +1289,22 @@ struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev,
	hdmi->bridge.hdmi_audio_dev = dev;
	hdmi->bridge.hdmi_audio_dai_port = 1;

#ifdef CONFIG_DRM_DW_HDMI_QP_CEC
	if (plat_data->cec_irq) {
		hdmi->bridge.ops |= DRM_BRIDGE_OP_HDMI_CEC_ADAPTER;
		hdmi->bridge.hdmi_cec_dev = dev;
		hdmi->bridge.hdmi_cec_adapter_name = dev_name(dev);

		hdmi->cec = devm_kzalloc(hdmi->dev, sizeof(*hdmi->cec), GFP_KERNEL);
		if (!hdmi->cec)
			return ERR_PTR(-ENOMEM);

		hdmi->cec->irq = plat_data->cec_irq;
	} else {
		dev_warn(dev, "Disabled CEC support due to missing IRQ\n");
	}
#endif

	ret = devm_drm_bridge_add(dev, &hdmi->bridge);
	if (ret)
		return ERR_PTR(ret);
+14 −0
Original line number Diff line number Diff line
@@ -488,9 +488,23 @@
#define AUDPKT_VBIT_OVR0				0xf24
/* CEC Registers */
#define CEC_TX_CONTROL					0x1000
#define CEC_CTRL_CLEAR					BIT(0)
#define CEC_CTRL_START					BIT(0)
#define CEC_STATUS					0x1004
#define CEC_STAT_DONE					BIT(0)
#define CEC_STAT_NACK					BIT(1)
#define CEC_STAT_ARBLOST				BIT(2)
#define CEC_STAT_LINE_ERR				BIT(3)
#define CEC_STAT_RETRANS_FAIL				BIT(4)
#define CEC_STAT_DISCARD				BIT(5)
#define CEC_STAT_TX_BUSY				BIT(8)
#define CEC_STAT_RX_BUSY				BIT(9)
#define CEC_STAT_DRIVE_ERR				BIT(10)
#define CEC_STAT_EOM					BIT(11)
#define CEC_STAT_NOTIFY_ERR				BIT(12)
#define CEC_CONFIG					0x1008
#define CEC_ADDR					0x100c
#define CEC_ADDR_BROADCAST				BIT(15)
#define CEC_TX_COUNT					0x1020
#define CEC_TX_DATA3_0					0x1024
#define CEC_TX_DATA7_4					0x1028
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ struct dw_hdmi_qp_plat_data {
	const struct dw_hdmi_qp_phy_ops *phy_ops;
	void *phy_data;
	int main_irq;
	int cec_irq;
};

struct dw_hdmi_qp *dw_hdmi_qp_bind(struct platform_device *pdev,