blob: c1647ef1dc15734af99d2a9d28c62bef906e93f2 [file] [log] [blame]
/*
* 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.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)
{
struct imx_hdp *hdp = container_of(cec, struct imx_hdp, cec);
struct hdp_mem *mem = &hdp->mem;
u32 addr = (offset << 2) + ADDR_HDP_CEC_BASE;
u32 value;
hdp->rw->read_reg(mem, addr, &value);
return value;
}
void cec_write(struct imx_cec_dev *cec, u32 offset, u32 value)
{
struct imx_hdp *hdp = container_of(cec, struct imx_hdp, cec);
struct hdp_mem *mem = &hdp->mem;
u32 addr = (offset << 2) + ADDR_HDP_CEC_BASE;
hdp->rw->write_reg(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 imx_hdp *hdp = container_of(cec, struct imx_hdp, cec);
struct device *dev = hdp->dev;
int ret;
/* Set CEC clock divider */
if (hdp->clks.clk_core)
cec->clk_div = clk_get_rate(hdp->clks.clk_core) / 100000;
else
/* Default HDMI core clock rate 133MHz */
cec->clk_div = 1330;
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");