From 9d47d3aef28e00b7c99140301bff6640593ca6dc Mon Sep 17 00:00:00 2001 From: Shunqing Chen Date: Mon, 29 Nov 2021 09:50:10 +0800 Subject: [PATCH] media: rockchip: hdmirx: add cec support Signed-off-by: Shunqing Chen Change-Id: Idcfb5be184e6b1aa4818421626b7c24f1409e18d --- .../media/platform/rockchip/hdmirx/Makefile | 2 +- .../platform/rockchip/hdmirx/rk_hdmirx.c | 38 +++ .../platform/rockchip/hdmirx/rk_hdmirx.h | 23 ++ .../platform/rockchip/hdmirx/rk_hdmirx_cec.c | 288 ++++++++++++++++++ .../platform/rockchip/hdmirx/rk_hdmirx_cec.h | 46 +++ 5 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.c create mode 100644 drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.h diff --git a/drivers/media/platform/rockchip/hdmirx/Makefile b/drivers/media/platform/rockchip/hdmirx/Makefile index 42798c3a3cbe..d989c89c2379 100644 --- a/drivers/media/platform/rockchip/hdmirx/Makefile +++ b/drivers/media/platform/rockchip/hdmirx/Makefile @@ -1,4 +1,4 @@ # SPDX-License-Identifier: GPL-2.0 -rockchip-hdmirx-objs := rk_hdmirx.o +rockchip-hdmirx-objs := rk_hdmirx.o rk_hdmirx_cec.o obj-$(CONFIG_VIDEO_ROCKCHIP_HDMIRX) += rockchip-hdmirx.o diff --git a/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.c b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.c index 63de27137e13..3203fa8363fd 100644 --- a/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.c +++ b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.c @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -34,6 +36,7 @@ #include #include #include "rk_hdmirx.h" +#include "rk_hdmirx_cec.h" static int debug; module_param(debug, int, 0644); @@ -174,6 +177,8 @@ struct rk_hdmirx_dev { struct hdmirx_audiostate audio_state; hdmi_codec_plugged_cb plugged_cb; struct device *codec_dev; + struct hdmirx_cec *cec; + struct cec_notifier *cec_notifier; }; static bool tx_5v_power_present(struct rk_hdmirx_dev *hdmirx_dev); @@ -2418,6 +2423,11 @@ static irqreturn_t hdmirx_5v_det_irq_handler(int irq, void *dev_id) return IRQ_HANDLED; } +static const struct hdmirx_cec_ops hdmirx_cec_ops = { + .write = hdmirx_writel, + .read = hdmirx_readl, +}; + static int hdmirx_parse_dt(struct rk_hdmirx_dev *hdmirx_dev) { struct device *dev = hdmirx_dev->dev; @@ -2598,6 +2608,7 @@ static int hdmirx_probe(struct platform_device *pdev) struct v4l2_ctrl_handler *hdl; struct resource *res; int ret, irq; + struct hdmirx_cec_data cec_data; hdmirx_dev = devm_kzalloc(dev, sizeof(*hdmirx_dev), GFP_KERNEL); if (!hdmirx_dev) @@ -2724,6 +2735,28 @@ static int hdmirx_probe(struct platform_device *pdev) schedule_delayed_work(&hdmirx_dev->delayed_work_hotplug, msecs_to_jiffies(2000)); + + hdmirx_dev->cec_notifier = cec_notifier_conn_register(dev, NULL, NULL); + if (!hdmirx_dev->cec_notifier) { + ret = -ENOMEM; + goto err_hdl; + } + + irq = platform_get_irq_byname(pdev, "cec"); + if (irq < 0) { + dev_err(dev, "get hdmi cec irq failed!\n"); + cec_notifier_conn_unregister(hdmirx_dev->cec_notifier); + ret = irq; + goto err_hdl; + } + + cec_data.hdmirx = hdmirx_dev; + cec_data.dev = hdmirx_dev->dev; + cec_data.ops = &hdmirx_cec_ops; + cec_data.irq = irq; + cec_data.edid = edid_init_data; + hdmirx_dev->cec = rk_hdmirx_cec_register(&cec_data); + dev_info(dev, "%s driver probe ok!\n", dev_name(dev)); return 0; @@ -2757,6 +2790,11 @@ static int hdmirx_remove(struct platform_device *pdev) clk_bulk_disable_unprepare(hdmirx_dev->num_clks, hdmirx_dev->clks); reset_control_assert(hdmirx_dev->reset); + if (hdmirx_dev->cec) + rk_hdmirx_cec_unregister(hdmirx_dev->cec); + if (hdmirx_dev->cec_notifier) + cec_notifier_conn_unregister(hdmirx_dev->cec_notifier); + video_unregister_device(&hdmirx_dev->stream.vdev); v4l2_ctrl_handler_free(&hdmirx_dev->hdl); v4l2_device_unregister(&hdmirx_dev->v4l2_dev); diff --git a/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.h b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.h index 9c57edac6f79..c50b5692d30e 100644 --- a/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.h +++ b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx.h @@ -261,7 +261,19 @@ #define VMON_STATUS7 0x1598 #define VMON_ILACE_DETECT BIT(4) +#define CEC_TX_CONTROL 0x2000 +#define CEC_STATUS 0x2004 +#define CEC_CONFIG 0x2008 +#define RX_AUTO_DRIVE_ACKNOWLEDGE BIT(9) #define CEC_ADDR 0x200c +#define CEC_TX_COUNT 0x2020 +#define CEC_TX_DATA3_0 0x2024 +#define CEC_RX_COUNT_STATUS 0x2040 +#define CEC_RX_DATA3_0 0x2044 +#define CEC_LOCK_CONTROL 0x2054 +#define CEC_RXQUAL_BITTIME_CONFIG 0x2060 +#define CEC_RX_BITTIME_CONFIG 0x2064 +#define CEC_TX_BITTIME_CONFIG 0x2068 #define DMA_CONFIG1 0x4400 #define UV_WID_MASK GENMASK(31, 28) @@ -324,6 +336,17 @@ #define MAINUNIT_INTVEC_INDEX 0x5000 #define MAINUNIT_0_INT_STATUS 0x5010 +#define CECRX_NOTIFY_ERR BIT(12) +#define CECRX_EOM BIT(11) +#define CECTX_DRIVE_ERR BIT(10) +#define CECRX_BUSY BIT(9) +#define CECTX_BUSY BIT(8) +#define CECTX_FRAME_DISCARDED BIT(5) +#define CECTX_NRETRANSMIT_FAIL BIT(4) +#define CECTX_LINE_ERR BIT(3) +#define CECTX_ARBLOST BIT(2) +#define CECTX_NACK BIT(1) +#define CECTX_DONE BIT(0) #define MAINUNIT_0_INT_MASK_N 0x5014 #define MAINUNIT_0_INT_CLEAR 0x5018 #define MAINUNIT_0_INT_FORCE 0x501c diff --git a/drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.c b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.c new file mode 100644 index 000000000000..e86216f1c33a --- /dev/null +++ b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.c @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (c) 2021 Rockchip Electronics Co. Ltd. + * + * Author: Shunqing Chen + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rk_hdmirx.h" +#include "rk_hdmirx_cec.h" + +static void hdmirx_cec_write(struct hdmirx_cec *cec, int reg, u32 val) +{ + cec->ops->write(cec->hdmirx, reg, val); +} + +static u32 hdmirx_cec_read(struct hdmirx_cec *cec, int reg) +{ + return cec->ops->read(cec->hdmirx, reg); +} + +static void hdmirx_cec_update_bits(struct hdmirx_cec *cec, int reg, u32 mask, + u32 data) +{ + u32 val = hdmirx_cec_read(cec, reg) & ~mask; + + val |= (data & mask); + hdmirx_cec_write(cec, reg, val); +} + +static int hdmirx_cec_log_addr(struct cec_adapter *adap, u8 logical_addr) +{ + struct hdmirx_cec *cec = cec_get_drvdata(adap); + + if (logical_addr == CEC_LOG_ADDR_INVALID) + cec->addresses = 0; + else + cec->addresses |= BIT(logical_addr) | BIT(15); + + hdmirx_cec_write(cec, CEC_ADDR, cec->addresses); + + return 0; +} + +static int hdmirx_cec_transmit(struct cec_adapter *adap, u8 attempts, + u32 signal_free_time, struct cec_msg *msg) +{ + struct hdmirx_cec *cec = cec_get_drvdata(adap); + u32 data[4] = {0}; + int i, data_len, msg_len; + + msg_len = msg->len; + if (msg->len > 16) + msg_len = 16; + if (msg_len <= 0) + return 0; + + hdmirx_cec_write(cec, CEC_TX_COUNT, msg_len - 1); + for (i = 0; i < msg_len; i++) + data[i / 4] |= msg->msg[i] << (i % 4) * 8; + + data_len = msg_len / 4 + 1; + for (i = 0; i < data_len; i++) + hdmirx_cec_write(cec, CEC_TX_DATA3_0 + i * 4, data[i]); + + hdmirx_cec_write(cec, CEC_TX_CONTROL, 0x1); + + return 0; +} + +static irqreturn_t hdmirx_cec_hardirq(int irq, void *data) +{ + struct cec_adapter *adap = data; + struct hdmirx_cec *cec = cec_get_drvdata(adap); + u32 stat = hdmirx_cec_read(cec, CEC_INT_STATUS); + irqreturn_t ret = IRQ_HANDLED; + u32 val; + + if (stat == 0) + return IRQ_NONE; + + hdmirx_cec_write(cec, CEC_INT_CLEAR, stat); + + if (stat & CECTX_LINE_ERR) { + cec->tx_status = CEC_TX_STATUS_ERROR; + cec->tx_done = true; + ret = IRQ_WAKE_THREAD; + } else if (stat & CECTX_DONE) { + cec->tx_status = CEC_TX_STATUS_OK; + cec->tx_done = true; + ret = IRQ_WAKE_THREAD; + } else if (stat & CECTX_NACK) { + cec->tx_status = CEC_TX_STATUS_NACK; + cec->tx_done = true; + ret = IRQ_WAKE_THREAD; + } + + if (stat & CECRX_EOM) { + unsigned int len, i; + + val = hdmirx_cec_read(cec, CEC_RX_COUNT_STATUS); + /* rxbuffer locked status */ + if ((val & 0x80)) + return ret; + + len = (val & 0xf) + 1; + if (len > sizeof(cec->rx_msg.msg)) + len = sizeof(cec->rx_msg.msg); + + for (i = 0; i < len; i++) { + if (i % 4 == 0) + val = hdmirx_cec_read(cec, CEC_RX_DATA3_0 + i / 4 * 4); + cec->rx_msg.msg[i] = (val >> ((i % 4) * 8)) & 0xff; + } + + cec->rx_msg.len = len; + smp_wmb(); /* receive RX msg */ + cec->rx_done = true; + hdmirx_cec_write(cec, CEC_LOCK_CONTROL, 0x1); + + ret = IRQ_WAKE_THREAD; + } + + return ret; +} + +static irqreturn_t hdmirx_cec_thread(int irq, void *data) +{ + struct cec_adapter *adap = data; + struct hdmirx_cec *cec = cec_get_drvdata(adap); + + if (cec->tx_done) { + cec->tx_done = false; + cec_transmit_attempt_done(adap, cec->tx_status); + } + if (cec->rx_done) { + cec->rx_done = false; + smp_rmb(); /* RX msg has been received */ + cec_received_msg(adap, &cec->rx_msg); + } + + return IRQ_HANDLED; +} + +static int hdmirx_cec_enable(struct cec_adapter *adap, bool enable) +{ + struct hdmirx_cec *cec = cec_get_drvdata(adap); + + if (!enable) { + hdmirx_cec_write(cec, CEC_INT_MASK_N, 0); + hdmirx_cec_write(cec, CEC_INT_CLEAR, 0); + if (cec->ops->disable) + cec->ops->disable(cec->hdmirx); + } else { + unsigned int irqs; + + hdmirx_cec_log_addr(cec->adap, CEC_LOG_ADDR_INVALID); + if (cec->ops->enable) + cec->ops->enable(cec->hdmirx); + hdmirx_cec_update_bits(cec, GLOBAL_SWENABLE, CEC_ENABLE, CEC_ENABLE); + + irqs = CECTX_LINE_ERR | CECTX_NACK | CECRX_EOM | CECTX_DONE; + hdmirx_cec_write(cec, CEC_INT_MASK_N, irqs); + } + + return 0; +} + +static const struct cec_adap_ops hdmirx_cec_ops = { + .adap_enable = hdmirx_cec_enable, + .adap_log_addr = hdmirx_cec_log_addr, + .adap_transmit = hdmirx_cec_transmit, +}; + +static void hdmirx_cec_del(void *data) +{ + struct hdmirx_cec *cec = data; + + cec_delete_adapter(cec->adap); +} + +struct hdmirx_cec *rk_hdmirx_cec_register(struct hdmirx_cec_data *data) +{ + struct hdmirx_cec *cec; + int ret; + + unsigned int irqs; + + if (!data) + return NULL; + + /* + * Our device is just a convenience - we want to link to the real + * hardware device here, so that userspace can see the association + * between the HDMI hardware and its associated CEC chardev. + */ + cec = devm_kzalloc(data->dev, sizeof(*cec), GFP_KERNEL); + if (!cec) + return NULL; + + cec->dev = data->dev; + cec->irq = data->irq; + cec->ops = data->ops; + cec->hdmirx = data->hdmirx; + cec->edid = (struct edid *)data->edid; + + hdmirx_cec_update_bits(cec, GLOBAL_SWENABLE, CEC_ENABLE, CEC_ENABLE); + hdmirx_cec_update_bits(cec, CEC_CONFIG, RX_AUTO_DRIVE_ACKNOWLEDGE, + RX_AUTO_DRIVE_ACKNOWLEDGE); + + hdmirx_cec_write(cec, CEC_TX_COUNT, 0); + hdmirx_cec_write(cec, CEC_INT_MASK_N, 0); + hdmirx_cec_write(cec, CEC_INT_CLEAR, ~0); + + cec->adap = cec_allocate_adapter(&hdmirx_cec_ops, cec, "rk-hdmirx", + CEC_CAP_LOG_ADDRS | CEC_CAP_TRANSMIT | + CEC_CAP_RC | CEC_CAP_PASSTHROUGH, + CEC_MAX_LOG_ADDRS); + if (IS_ERR(cec->adap)) { + dev_err(cec->dev, "cec adap allocate failed!\n"); + return NULL; + } + + /* override the module pointer */ + cec->adap->owner = THIS_MODULE; + + ret = devm_add_action(cec->dev, hdmirx_cec_del, cec); + if (ret) { + cec_delete_adapter(cec->adap); + return NULL; + } + + cec->notify = cec_notifier_cec_adap_register(cec->dev, + NULL, cec->adap); + if (!cec->notify) { + dev_err(cec->dev, "cec notify register failed!\n"); + return NULL; + } + + ret = cec_register_adapter(cec->adap, cec->dev); + if (ret < 0) { + dev_err(cec->dev, "cec register adapter failed!\n"); + cec_notifier_cec_adap_unregister(cec->notify, cec->adap); + return NULL; + } + + cec_s_phys_addr_from_edid(cec->adap, cec->edid); + + ret = devm_request_threaded_irq(cec->dev, cec->irq, + hdmirx_cec_hardirq, + hdmirx_cec_thread, IRQF_ONESHOT, + "rk_hdmirx_cec", cec->adap); + if (ret) { + dev_err(cec->dev, "cec irq request failed!\n"); + cec_notifier_cec_adap_unregister(cec->notify, cec->adap); + dev_err(cec->dev, "request cec irq thread failed!\n"); + return NULL; + } + + irqs = CECTX_LINE_ERR | CECTX_NACK | CECRX_EOM | CECTX_DONE; + hdmirx_cec_write(cec, CEC_INT_MASK_N, irqs); + + /* + * CEC documentation says we must not call cec_delete_adapter + * after a successful call to cec_register_adapter(). + */ + devm_remove_action(cec->dev, hdmirx_cec_del, cec); + + return cec; +} + +void rk_hdmirx_cec_unregister(struct hdmirx_cec *cec) +{ + if (!cec) + return; + + cec_notifier_cec_adap_unregister(cec->notify, cec->adap); + cec_unregister_adapter(cec->adap); +} diff --git a/drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.h b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.h new file mode 100644 index 000000000000..ce6331a09d5d --- /dev/null +++ b/drivers/media/platform/rockchip/hdmirx/rk_hdmirx_cec.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright (c) 2021 Rockchip Electronics Co. Ltd. + * + * Author: Shunqing Chen + */ + +#ifndef __RK_HDMIRX_CEC_H__ +#define __RK_HDMIRX_CEC_H__ + +struct rk_hdmirx_dev; + +struct hdmirx_cec_ops { + void (*write)(struct rk_hdmirx_dev *hdmirx_dev, int reg, u32 val); + u32 (*read)(struct rk_hdmirx_dev *hdmirx_dev, int reg); + void (*enable)(struct rk_hdmirx_dev *hdmirx); + void (*disable)(struct rk_hdmirx_dev *hdmirx); +}; + +struct hdmirx_cec_data { + struct rk_hdmirx_dev *hdmirx; + const struct hdmirx_cec_ops *ops; + struct device *dev; + int irq; + u8 *edid; +}; + +struct hdmirx_cec { + struct rk_hdmirx_dev *hdmirx; + struct device *dev; + const struct hdmirx_cec_ops *ops; + u32 addresses; + struct cec_adapter *adap; + struct cec_msg rx_msg; + unsigned int tx_status; + bool tx_done; + bool rx_done; + struct cec_notifier *notify; + int irq; + struct edid *edid; +}; + +struct hdmirx_cec *rk_hdmirx_cec_register(struct hdmirx_cec_data *data); +void rk_hdmirx_cec_unregister(struct hdmirx_cec *cec); + +#endif /* __DW_HDMI_RX_CEC_H__ */