summaryrefslogtreecommitdiff
path: root/drivers/mxc/hdp-cec/imx-hdp-cec.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/mxc/hdp-cec/imx-hdp-cec.c')
-rw-r--r--drivers/mxc/hdp-cec/imx-hdp-cec.c313
1 files changed, 313 insertions, 0 deletions
diff --git a/drivers/mxc/hdp-cec/imx-hdp-cec.c b/drivers/mxc/hdp-cec/imx-hdp-cec.c
new file mode 100644
index 000000000000..25c2d0c34730
--- /dev/null
+++ b/drivers/mxc/hdp-cec/imx-hdp-cec.c
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2017 NXP
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+#include <linux/clk.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/timer.h>
+#include <linux/version.h>
+#include <linux/workqueue.h>
+#include <linux/kthread.h>
+#include <media/cec.h>
+#include <soc/imx8/soc.h>
+
+#include "imx-hdp-cec.h"
+
+#define CEC_NAME "hdp-cec"
+
+#define REG_ADDR_OFF 1
+
+#define MAX_LA_IDX 4
+#define MAX_LA_VAL 15
+
+/**
+ * Maximum number of Messages in the RX Buffers.
+ */
+# define CEC_MAX_RX_MSGS 2
+
+#define set_la F_MY_LOG_ADDR0
+#define get_la F_MY_LOG_ADDR0_RD
+#define set_la_valid F_LOG_ADDR_VALID0
+#define get_la_valid F_LOG_ADDR_VALID0_RD
+
+u32 cec_read(struct imx_cec_dev *cec, u32 offset)
+{
+ u32 addr = (offset << 2) + ADDR_HDP_CEC_BASE;
+ u32 value;
+
+ cec->rw->read_reg(cec->mem, addr, &value);
+ return value;
+}
+
+void cec_write(struct imx_cec_dev *cec, u32 offset, u32 value)
+{
+ u32 addr = (offset << 2) + ADDR_HDP_CEC_BASE;
+
+ cec->rw->write_reg(cec->mem, addr, value);
+}
+
+void cec_clear_rx_buffer(struct imx_cec_dev *cec)
+{
+ cec_write(cec, RX_CLEAR_BUF, 1);
+ cec_write(cec, RX_CLEAR_BUF, 0);
+}
+
+void cec_set_divider(struct imx_cec_dev *cec)
+{
+ /* Set clock divider */
+ if (cec->clk_div == 0) {
+ dev_warn(cec->dev,
+ "Warning. Clock divider is 0. Changing to 1.\n");
+ cec_write(cec, CLK_DIV_MSB, 0);
+ cec_write(cec, CLK_DIV_LSB, 1);
+ } else {
+ cec_write(cec, CLK_DIV_MSB,
+ (cec->clk_div >> 8) & 0xFF);
+ cec_write(cec, CLK_DIV_LSB, cec->clk_div & 0xFF);
+ }
+}
+
+u32 cec_read_message(struct imx_cec_dev *cec)
+{
+ struct cec_msg *msg = &cec->msg;
+ int len;
+ int i;
+
+ cec_write(cec, RX_MSG_CMD, CEC_RX_READ);
+
+ len = cec_read(cec, RX_MSG_LENGTH);
+ msg->len = len + 1;
+ dev_dbg(cec->dev, "RX MSG len =%d\n", len);
+
+ /* Read RX MSG bytes */
+ for (i = 0; i < msg->len; ++i) {
+ msg->msg[i] = (u8) cec_read(cec, RX_MSG_DATA1 + (i * REG_ADDR_OFF));
+ dev_dbg(cec->dev, "RX MSG[%d]=0x%x\n", i, msg->msg[i]);
+ }
+
+ cec_write(cec, RX_MSG_CMD, CEC_RX_STOP);
+
+ return true;
+}
+
+u32 cec_write_message(struct imx_cec_dev *cec, struct cec_msg *msg)
+{
+ u8 i;
+
+ cec_write(cec, TX_MSG_CMD, CEC_TX_STOP);
+
+ if (msg->len > CEC_MAX_MSG_SIZE) {
+ dev_err(cec->dev, "Invalid MSG size!\n");
+ return -EINVAL;
+ }
+
+ /* Write Message to register */
+ for (i = 0; i < msg->len; ++i) {
+ cec_write(cec, TX_MSG_HEADER + (i * REG_ADDR_OFF),
+ msg->msg[i]);
+ }
+ /* Write Message Length (payload + opcode) */
+ cec_write(cec, TX_MSG_LENGTH, msg->len - 1);
+
+ cec_write(cec, TX_MSG_CMD, CEC_TX_TRANSMIT);
+
+ return true;
+}
+
+void cec_abort_tx_transfer(struct imx_cec_dev *cec)
+{
+ cec_write(cec, TX_MSG_CMD, CEC_TX_ABORT);
+ cec_write(cec, TX_MSG_CMD, CEC_TX_STOP);
+}
+
+u32 imx_cec_set_logical_addr(struct imx_cec_dev *cec, u32 la)
+{
+ u8 i;
+ u8 la_reg;
+
+ if (la >= MAX_LA_VAL) {
+ dev_err(cec->dev, "Error logical Addr\n");
+ return -EINVAL;
+ }
+
+ for (i = 0; i < MAX_LA_IDX; ++i) {
+ la_reg =
+ cec_read(cec, LOGICAL_ADDRESS_LA0 + (i * REG_ADDR_OFF));
+
+ if (get_la_valid(la_reg))
+ continue;
+
+ if (get_la(la_reg) == la) {
+ dev_warn(cec->dev, "Warning. LA already in use.\n");
+ return true;
+ }
+
+ la = set_la(la) | set_la_valid(1);
+
+ cec_write(cec, LOGICAL_ADDRESS_LA0 + (i * REG_ADDR_OFF), la);
+ return true;
+ }
+
+ dev_warn(cec->dev, "All LA in use\n");
+
+ return false;
+}
+
+static int cec_poll_worker(void *_cec)
+{
+ struct imx_cec_dev *cec = (struct imx_cec_dev *)_cec;
+ int num_rx_msgs, i;
+ int sts;
+
+ for (;;) {
+ if (kthread_should_stop())
+ break;
+
+ /* Check TX State */
+ sts = cec_read(cec, TX_MSG_STATUS);
+ switch (sts) {
+ case CEC_STS_SUCCESS:
+ cec_transmit_done(cec->adap, CEC_TX_STATUS_OK, 0, 0, 0,
+ 0);
+ cec_write(cec, TX_MSG_CMD, CEC_TX_STOP);
+ break;
+ case CEC_STS_ERROR:
+ cec_write(cec, TX_MSG_CMD, CEC_TX_STOP);
+ cec_transmit_done(cec->adap,
+ CEC_TX_STATUS_MAX_RETRIES |
+ CEC_TX_STATUS_ERROR, 0, 0, 0, 1);
+ break;
+ case CEC_STS_BUSY:
+ default:
+ break;
+ }
+
+ /* Check RX State */
+ sts = cec_read(cec, RX_MSG_STATUS);
+ num_rx_msgs = cec_read(cec, NUM_OF_MSG_RX_BUF);
+ switch (sts) {
+ case CEC_STS_SUCCESS:
+ if (num_rx_msgs == 0xf)
+ num_rx_msgs = CEC_MAX_RX_MSGS;
+
+ if (num_rx_msgs > CEC_MAX_RX_MSGS) {
+ dev_err(cec->dev, "Error rx msg num %d\n",
+ num_rx_msgs);
+ cec_clear_rx_buffer(cec);
+ break;
+ }
+
+ /* Rx FIFO Depth 2 RX MSG */
+ for (i = 0; i < num_rx_msgs; i++) {
+ cec_read_message(cec);
+ cec->msg.rx_status = CEC_RX_STATUS_OK;
+ cec_received_msg(cec->adap, &cec->msg);
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (!kthread_should_stop())
+ schedule_timeout_idle(20);
+ }
+
+ return 0;
+}
+
+static int imx_cec_adap_enable(struct cec_adapter *adap, bool enable)
+{
+ struct imx_cec_dev *cec = adap->priv;
+
+ if (enable) {
+ cec_write(cec, DB_L_TIMER, 0x10);
+ cec_set_divider(cec);
+ } else {
+ cec_set_divider(cec);
+ }
+
+ return 0;
+}
+
+static int imx_cec_adap_log_addr(struct cec_adapter *adap, u8 addr)
+{
+ struct imx_cec_dev *cec = adap->priv;
+
+ imx_cec_set_logical_addr(cec, addr);
+ return 0;
+}
+
+static int imx_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
+ u32 signal_free_time, struct cec_msg *msg)
+{
+ struct imx_cec_dev *cec = adap->priv;
+
+ cec_write_message(cec, msg);
+
+ return 0;
+}
+
+static const struct cec_adap_ops imx_cec_adap_ops = {
+ .adap_enable = imx_cec_adap_enable,
+ .adap_log_addr = imx_cec_adap_log_addr,
+ .adap_transmit = imx_cec_adap_transmit,
+};
+
+int imx_cec_register(struct imx_cec_dev *cec)
+{
+ struct device *dev = cec->dev;
+ int ret;
+
+ cec->adap = cec_allocate_adapter(&imx_cec_adap_ops, cec,
+ CEC_NAME,
+ CEC_CAP_PHYS_ADDR | CEC_CAP_LOG_ADDRS |
+ CEC_CAP_TRANSMIT | CEC_CAP_PASSTHROUGH
+ | CEC_CAP_RC, 1, dev);
+ ret = PTR_ERR_OR_ZERO(cec->adap);
+ if (ret)
+ return ret;
+ ret = cec_register_adapter(cec->adap);
+ if (ret) {
+ cec_delete_adapter(cec->adap);
+ return ret;
+ }
+
+ cec->dev = dev;
+
+ cec->cec_worker = kthread_create(cec_poll_worker, cec, "hdp-cec");
+ if (IS_ERR(cec->cec_worker))
+ dev_err(cec->dev, "failed create hdp cec thread\n");
+
+ wake_up_process(cec->cec_worker);
+
+ dev_dbg(dev, "CEC successfuly probed\n");
+ return 0;
+}
+
+int imx_cec_unregister(struct imx_cec_dev *cec)
+{
+ if (cec->cec_worker) {
+ kthread_stop(cec->cec_worker);
+ cec->cec_worker = NULL;
+ }
+ cec_unregister_adapter(cec->adap);
+ return 0;
+}
+
+MODULE_AUTHOR("Sandor.Yu@NXP.com");
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("NXP IMX HDP CEC driver");