484 lines
12 KiB
C
484 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Tegra CEC implementation
|
|
*
|
|
* The original 3.10 CEC driver using a custom API:
|
|
*
|
|
* Copyright (c) 2012-2015, NVIDIA CORPORATION. All rights reserved.
|
|
*
|
|
* Conversion to the CEC framework and to the mainline kernel:
|
|
*
|
|
* Copyright 2016-2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/err.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/io.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/pm.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_platform.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/clk/tegra.h>
|
|
|
|
#include <media/cec-notifier.h>
|
|
|
|
#include "tegra_cec.h"
|
|
|
|
#define TEGRA_CEC_NAME "tegra-cec"
|
|
|
|
struct tegra_cec {
|
|
struct cec_adapter *adap;
|
|
struct device *dev;
|
|
struct clk *clk;
|
|
void __iomem *cec_base;
|
|
struct cec_notifier *notifier;
|
|
int tegra_cec_irq;
|
|
bool rx_done;
|
|
bool tx_done;
|
|
int tx_status;
|
|
u8 rx_buf[CEC_MAX_MSG_SIZE];
|
|
u8 rx_buf_cnt;
|
|
u32 tx_buf[CEC_MAX_MSG_SIZE];
|
|
u8 tx_buf_cur;
|
|
u8 tx_buf_cnt;
|
|
};
|
|
|
|
static inline u32 cec_read(struct tegra_cec *cec, u32 reg)
|
|
{
|
|
return readl(cec->cec_base + reg);
|
|
}
|
|
|
|
static inline void cec_write(struct tegra_cec *cec, u32 reg, u32 val)
|
|
{
|
|
writel(val, cec->cec_base + reg);
|
|
}
|
|
|
|
static void tegra_cec_error_recovery(struct tegra_cec *cec)
|
|
{
|
|
u32 hw_ctrl;
|
|
|
|
hw_ctrl = cec_read(cec, TEGRA_CEC_HW_CONTROL);
|
|
cec_write(cec, TEGRA_CEC_HW_CONTROL, 0);
|
|
cec_write(cec, TEGRA_CEC_INT_STAT, 0xffffffff);
|
|
cec_write(cec, TEGRA_CEC_HW_CONTROL, hw_ctrl);
|
|
}
|
|
|
|
static irqreturn_t tegra_cec_irq_thread_handler(int irq, void *data)
|
|
{
|
|
struct device *dev = data;
|
|
struct tegra_cec *cec = dev_get_drvdata(dev);
|
|
|
|
if (cec->tx_done) {
|
|
cec_transmit_attempt_done(cec->adap, cec->tx_status);
|
|
cec->tx_done = false;
|
|
}
|
|
if (cec->rx_done) {
|
|
struct cec_msg msg = {};
|
|
|
|
msg.len = cec->rx_buf_cnt;
|
|
memcpy(msg.msg, cec->rx_buf, msg.len);
|
|
cec_received_msg(cec->adap, &msg);
|
|
cec->rx_done = false;
|
|
cec->rx_buf_cnt = 0;
|
|
}
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static irqreturn_t tegra_cec_irq_handler(int irq, void *data)
|
|
{
|
|
struct device *dev = data;
|
|
struct tegra_cec *cec = dev_get_drvdata(dev);
|
|
u32 status, mask;
|
|
|
|
status = cec_read(cec, TEGRA_CEC_INT_STAT);
|
|
mask = cec_read(cec, TEGRA_CEC_INT_MASK);
|
|
|
|
status &= mask;
|
|
|
|
if (!status)
|
|
return IRQ_HANDLED;
|
|
|
|
if (status & TEGRA_CEC_INT_STAT_TX_REGISTER_UNDERRUN) {
|
|
dev_err(dev, "TX underrun, interrupt timing issue!\n");
|
|
|
|
tegra_cec_error_recovery(cec);
|
|
cec_write(cec, TEGRA_CEC_INT_MASK,
|
|
mask & ~TEGRA_CEC_INT_MASK_TX_REGISTER_EMPTY);
|
|
|
|
cec->tx_done = true;
|
|
cec->tx_status = CEC_TX_STATUS_ERROR;
|
|
return IRQ_WAKE_THREAD;
|
|
}
|
|
|
|
if ((status & TEGRA_CEC_INT_STAT_TX_ARBITRATION_FAILED) ||
|
|
(status & TEGRA_CEC_INT_STAT_TX_BUS_ANOMALY_DETECTED)) {
|
|
tegra_cec_error_recovery(cec);
|
|
cec_write(cec, TEGRA_CEC_INT_MASK,
|
|
mask & ~TEGRA_CEC_INT_MASK_TX_REGISTER_EMPTY);
|
|
|
|
cec->tx_done = true;
|
|
if (status & TEGRA_CEC_INT_STAT_TX_BUS_ANOMALY_DETECTED)
|
|
cec->tx_status = CEC_TX_STATUS_LOW_DRIVE;
|
|
else
|
|
cec->tx_status = CEC_TX_STATUS_ARB_LOST;
|
|
return IRQ_WAKE_THREAD;
|
|
}
|
|
|
|
if (status & TEGRA_CEC_INT_STAT_TX_FRAME_TRANSMITTED) {
|
|
cec_write(cec, TEGRA_CEC_INT_STAT,
|
|
TEGRA_CEC_INT_STAT_TX_FRAME_TRANSMITTED);
|
|
|
|
if (status & TEGRA_CEC_INT_STAT_TX_FRAME_OR_BLOCK_NAKD) {
|
|
tegra_cec_error_recovery(cec);
|
|
|
|
cec->tx_done = true;
|
|
cec->tx_status = CEC_TX_STATUS_NACK;
|
|
} else {
|
|
cec->tx_done = true;
|
|
cec->tx_status = CEC_TX_STATUS_OK;
|
|
}
|
|
return IRQ_WAKE_THREAD;
|
|
}
|
|
|
|
if (status & TEGRA_CEC_INT_STAT_TX_FRAME_OR_BLOCK_NAKD)
|
|
dev_warn(dev, "TX NAKed on the fly!\n");
|
|
|
|
if (status & TEGRA_CEC_INT_STAT_TX_REGISTER_EMPTY) {
|
|
if (cec->tx_buf_cur == cec->tx_buf_cnt) {
|
|
cec_write(cec, TEGRA_CEC_INT_MASK,
|
|
mask & ~TEGRA_CEC_INT_MASK_TX_REGISTER_EMPTY);
|
|
} else {
|
|
cec_write(cec, TEGRA_CEC_TX_REGISTER,
|
|
cec->tx_buf[cec->tx_buf_cur++]);
|
|
cec_write(cec, TEGRA_CEC_INT_STAT,
|
|
TEGRA_CEC_INT_STAT_TX_REGISTER_EMPTY);
|
|
}
|
|
}
|
|
|
|
if (status & TEGRA_CEC_INT_STAT_RX_START_BIT_DETECTED) {
|
|
cec_write(cec, TEGRA_CEC_INT_STAT,
|
|
TEGRA_CEC_INT_STAT_RX_START_BIT_DETECTED);
|
|
cec->rx_done = false;
|
|
cec->rx_buf_cnt = 0;
|
|
}
|
|
if (status & TEGRA_CEC_INT_STAT_RX_REGISTER_FULL) {
|
|
u32 v;
|
|
|
|
cec_write(cec, TEGRA_CEC_INT_STAT,
|
|
TEGRA_CEC_INT_STAT_RX_REGISTER_FULL);
|
|
v = cec_read(cec, TEGRA_CEC_RX_REGISTER);
|
|
if (cec->rx_buf_cnt < CEC_MAX_MSG_SIZE)
|
|
cec->rx_buf[cec->rx_buf_cnt++] = v & 0xff;
|
|
if (v & TEGRA_CEC_RX_REGISTER_EOM) {
|
|
cec->rx_done = true;
|
|
return IRQ_WAKE_THREAD;
|
|
}
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int tegra_cec_adap_enable(struct cec_adapter *adap, bool enable)
|
|
{
|
|
struct tegra_cec *cec = adap->priv;
|
|
|
|
cec->rx_buf_cnt = 0;
|
|
cec->tx_buf_cnt = 0;
|
|
cec->tx_buf_cur = 0;
|
|
|
|
cec_write(cec, TEGRA_CEC_HW_CONTROL, 0);
|
|
cec_write(cec, TEGRA_CEC_INT_MASK, 0);
|
|
cec_write(cec, TEGRA_CEC_INT_STAT, 0xffffffff);
|
|
cec_write(cec, TEGRA_CEC_SW_CONTROL, 0);
|
|
|
|
if (!enable)
|
|
return 0;
|
|
|
|
cec_write(cec, TEGRA_CEC_INPUT_FILTER, (1U << 31) | 0x20);
|
|
|
|
cec_write(cec, TEGRA_CEC_RX_TIMING_0,
|
|
(0x7a << TEGRA_CEC_RX_TIM0_START_BIT_MAX_LO_TIME_SHIFT) |
|
|
(0x6d << TEGRA_CEC_RX_TIM0_START_BIT_MIN_LO_TIME_SHIFT) |
|
|
(0x93 << TEGRA_CEC_RX_TIM0_START_BIT_MAX_DURATION_SHIFT) |
|
|
(0x86 << TEGRA_CEC_RX_TIM0_START_BIT_MIN_DURATION_SHIFT));
|
|
|
|
cec_write(cec, TEGRA_CEC_RX_TIMING_1,
|
|
(0x35 << TEGRA_CEC_RX_TIM1_DATA_BIT_MAX_LO_TIME_SHIFT) |
|
|
(0x21 << TEGRA_CEC_RX_TIM1_DATA_BIT_SAMPLE_TIME_SHIFT) |
|
|
(0x56 << TEGRA_CEC_RX_TIM1_DATA_BIT_MAX_DURATION_SHIFT) |
|
|
(0x40 << TEGRA_CEC_RX_TIM1_DATA_BIT_MIN_DURATION_SHIFT));
|
|
|
|
cec_write(cec, TEGRA_CEC_RX_TIMING_2,
|
|
(0x50 << TEGRA_CEC_RX_TIM2_END_OF_BLOCK_TIME_SHIFT));
|
|
|
|
cec_write(cec, TEGRA_CEC_TX_TIMING_0,
|
|
(0x74 << TEGRA_CEC_TX_TIM0_START_BIT_LO_TIME_SHIFT) |
|
|
(0x8d << TEGRA_CEC_TX_TIM0_START_BIT_DURATION_SHIFT) |
|
|
(0x08 << TEGRA_CEC_TX_TIM0_BUS_XITION_TIME_SHIFT) |
|
|
(0x71 << TEGRA_CEC_TX_TIM0_BUS_ERROR_LO_TIME_SHIFT));
|
|
|
|
cec_write(cec, TEGRA_CEC_TX_TIMING_1,
|
|
(0x2f << TEGRA_CEC_TX_TIM1_LO_DATA_BIT_LO_TIME_SHIFT) |
|
|
(0x13 << TEGRA_CEC_TX_TIM1_HI_DATA_BIT_LO_TIME_SHIFT) |
|
|
(0x4b << TEGRA_CEC_TX_TIM1_DATA_BIT_DURATION_SHIFT) |
|
|
(0x21 << TEGRA_CEC_TX_TIM1_ACK_NAK_BIT_SAMPLE_TIME_SHIFT));
|
|
|
|
cec_write(cec, TEGRA_CEC_TX_TIMING_2,
|
|
(0x07 << TEGRA_CEC_TX_TIM2_BUS_IDLE_TIME_ADDITIONAL_FRAME_SHIFT) |
|
|
(0x05 << TEGRA_CEC_TX_TIM2_BUS_IDLE_TIME_NEW_FRAME_SHIFT) |
|
|
(0x03 << TEGRA_CEC_TX_TIM2_BUS_IDLE_TIME_RETRY_FRAME_SHIFT));
|
|
|
|
cec_write(cec, TEGRA_CEC_INT_MASK,
|
|
TEGRA_CEC_INT_MASK_TX_REGISTER_UNDERRUN |
|
|
TEGRA_CEC_INT_MASK_TX_FRAME_OR_BLOCK_NAKD |
|
|
TEGRA_CEC_INT_MASK_TX_ARBITRATION_FAILED |
|
|
TEGRA_CEC_INT_MASK_TX_BUS_ANOMALY_DETECTED |
|
|
TEGRA_CEC_INT_MASK_TX_FRAME_TRANSMITTED |
|
|
TEGRA_CEC_INT_MASK_RX_REGISTER_FULL |
|
|
TEGRA_CEC_INT_MASK_RX_START_BIT_DETECTED);
|
|
|
|
cec_write(cec, TEGRA_CEC_HW_CONTROL, TEGRA_CEC_HWCTRL_TX_RX_MODE);
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_cec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr)
|
|
{
|
|
struct tegra_cec *cec = adap->priv;
|
|
u32 state = cec_read(cec, TEGRA_CEC_HW_CONTROL);
|
|
|
|
if (logical_addr == CEC_LOG_ADDR_INVALID)
|
|
state &= ~TEGRA_CEC_HWCTRL_RX_LADDR_MASK;
|
|
else
|
|
state |= TEGRA_CEC_HWCTRL_RX_LADDR((1 << logical_addr));
|
|
|
|
cec_write(cec, TEGRA_CEC_HW_CONTROL, state);
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_cec_adap_monitor_all_enable(struct cec_adapter *adap,
|
|
bool enable)
|
|
{
|
|
struct tegra_cec *cec = adap->priv;
|
|
u32 reg = cec_read(cec, TEGRA_CEC_HW_CONTROL);
|
|
|
|
if (enable)
|
|
reg |= TEGRA_CEC_HWCTRL_RX_SNOOP;
|
|
else
|
|
reg &= ~TEGRA_CEC_HWCTRL_RX_SNOOP;
|
|
cec_write(cec, TEGRA_CEC_HW_CONTROL, reg);
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
|
|
u32 signal_free_time_ms, struct cec_msg *msg)
|
|
{
|
|
bool retry_xfer = signal_free_time_ms == CEC_SIGNAL_FREE_TIME_RETRY;
|
|
struct tegra_cec *cec = adap->priv;
|
|
unsigned int i;
|
|
u32 mode = 0;
|
|
u32 mask;
|
|
|
|
if (cec_msg_is_broadcast(msg))
|
|
mode = TEGRA_CEC_TX_REG_BCAST;
|
|
|
|
cec->tx_buf_cur = 0;
|
|
cec->tx_buf_cnt = msg->len;
|
|
|
|
for (i = 0; i < msg->len; i++) {
|
|
cec->tx_buf[i] = mode | msg->msg[i];
|
|
if (i == 0)
|
|
cec->tx_buf[i] |= TEGRA_CEC_TX_REG_START_BIT;
|
|
if (i == msg->len - 1)
|
|
cec->tx_buf[i] |= TEGRA_CEC_TX_REG_EOM;
|
|
if (i == 0 && retry_xfer)
|
|
cec->tx_buf[i] |= TEGRA_CEC_TX_REG_RETRY;
|
|
}
|
|
|
|
mask = cec_read(cec, TEGRA_CEC_INT_MASK);
|
|
cec_write(cec, TEGRA_CEC_INT_MASK,
|
|
mask | TEGRA_CEC_INT_MASK_TX_REGISTER_EMPTY);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct cec_adap_ops tegra_cec_ops = {
|
|
.adap_enable = tegra_cec_adap_enable,
|
|
.adap_log_addr = tegra_cec_adap_log_addr,
|
|
.adap_transmit = tegra_cec_adap_transmit,
|
|
.adap_monitor_all_enable = tegra_cec_adap_monitor_all_enable,
|
|
};
|
|
|
|
static int tegra_cec_probe(struct platform_device *pdev)
|
|
{
|
|
struct device *hdmi_dev;
|
|
struct tegra_cec *cec;
|
|
struct resource *res;
|
|
int ret = 0;
|
|
|
|
hdmi_dev = cec_notifier_parse_hdmi_phandle(&pdev->dev);
|
|
|
|
if (IS_ERR(hdmi_dev))
|
|
return PTR_ERR(hdmi_dev);
|
|
|
|
cec = devm_kzalloc(&pdev->dev, sizeof(struct tegra_cec), GFP_KERNEL);
|
|
|
|
if (!cec)
|
|
return -ENOMEM;
|
|
|
|
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
|
|
|
if (!res) {
|
|
dev_err(&pdev->dev,
|
|
"Unable to allocate resources for device\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
if (!devm_request_mem_region(&pdev->dev, res->start, resource_size(res),
|
|
pdev->name)) {
|
|
dev_err(&pdev->dev,
|
|
"Unable to request mem region for device\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
cec->tegra_cec_irq = platform_get_irq(pdev, 0);
|
|
|
|
if (cec->tegra_cec_irq <= 0)
|
|
return -EBUSY;
|
|
|
|
cec->cec_base = devm_ioremap(&pdev->dev, res->start,
|
|
resource_size(res));
|
|
|
|
if (!cec->cec_base) {
|
|
dev_err(&pdev->dev, "Unable to grab IOs for device\n");
|
|
return -EBUSY;
|
|
}
|
|
|
|
cec->clk = devm_clk_get(&pdev->dev, "cec");
|
|
|
|
if (IS_ERR_OR_NULL(cec->clk)) {
|
|
dev_err(&pdev->dev, "Can't get clock for CEC\n");
|
|
return -ENOENT;
|
|
}
|
|
|
|
ret = clk_prepare_enable(cec->clk);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "Unable to prepare clock for CEC\n");
|
|
return ret;
|
|
}
|
|
|
|
/* set context info. */
|
|
cec->dev = &pdev->dev;
|
|
|
|
platform_set_drvdata(pdev, cec);
|
|
|
|
ret = devm_request_threaded_irq(&pdev->dev, cec->tegra_cec_irq,
|
|
tegra_cec_irq_handler, tegra_cec_irq_thread_handler,
|
|
0, "cec_irq", &pdev->dev);
|
|
|
|
if (ret) {
|
|
dev_err(&pdev->dev,
|
|
"Unable to request interrupt for device\n");
|
|
goto err_clk;
|
|
}
|
|
|
|
cec->adap = cec_allocate_adapter(&tegra_cec_ops, cec, TEGRA_CEC_NAME,
|
|
CEC_CAP_DEFAULTS | CEC_CAP_MONITOR_ALL |
|
|
CEC_CAP_CONNECTOR_INFO,
|
|
CEC_MAX_LOG_ADDRS);
|
|
if (IS_ERR(cec->adap)) {
|
|
ret = -ENOMEM;
|
|
dev_err(&pdev->dev, "Couldn't create cec adapter\n");
|
|
goto err_clk;
|
|
}
|
|
|
|
cec->notifier = cec_notifier_cec_adap_register(hdmi_dev, NULL,
|
|
cec->adap);
|
|
if (!cec->notifier) {
|
|
ret = -ENOMEM;
|
|
goto err_adapter;
|
|
}
|
|
|
|
ret = cec_register_adapter(cec->adap, &pdev->dev);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "Couldn't register device\n");
|
|
goto err_notifier;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err_notifier:
|
|
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
|
|
err_adapter:
|
|
cec_delete_adapter(cec->adap);
|
|
err_clk:
|
|
clk_disable_unprepare(cec->clk);
|
|
return ret;
|
|
}
|
|
|
|
static int tegra_cec_remove(struct platform_device *pdev)
|
|
{
|
|
struct tegra_cec *cec = platform_get_drvdata(pdev);
|
|
|
|
clk_disable_unprepare(cec->clk);
|
|
|
|
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
|
|
cec_unregister_adapter(cec->adap);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
static int tegra_cec_suspend(struct platform_device *pdev, pm_message_t state)
|
|
{
|
|
struct tegra_cec *cec = platform_get_drvdata(pdev);
|
|
|
|
clk_disable_unprepare(cec->clk);
|
|
|
|
dev_notice(&pdev->dev, "suspended\n");
|
|
return 0;
|
|
}
|
|
|
|
static int tegra_cec_resume(struct platform_device *pdev)
|
|
{
|
|
struct tegra_cec *cec = platform_get_drvdata(pdev);
|
|
|
|
dev_notice(&pdev->dev, "Resuming\n");
|
|
|
|
return clk_prepare_enable(cec->clk);
|
|
}
|
|
#endif
|
|
|
|
static const struct of_device_id tegra_cec_of_match[] = {
|
|
{ .compatible = "nvidia,tegra114-cec", },
|
|
{ .compatible = "nvidia,tegra124-cec", },
|
|
{ .compatible = "nvidia,tegra210-cec", },
|
|
{},
|
|
};
|
|
|
|
static struct platform_driver tegra_cec_driver = {
|
|
.driver = {
|
|
.name = TEGRA_CEC_NAME,
|
|
.of_match_table = of_match_ptr(tegra_cec_of_match),
|
|
},
|
|
.probe = tegra_cec_probe,
|
|
.remove = tegra_cec_remove,
|
|
|
|
#ifdef CONFIG_PM
|
|
.suspend = tegra_cec_suspend,
|
|
.resume = tegra_cec_resume,
|
|
#endif
|
|
};
|
|
|
|
module_platform_driver(tegra_cec_driver);
|
|
|
|
MODULE_DESCRIPTION("Tegra HDMI CEC driver");
|
|
MODULE_AUTHOR("NVIDIA CORPORATION");
|
|
MODULE_AUTHOR("Cisco Systems, Inc. and/or its affiliates");
|
|
MODULE_LICENSE("GPL v2");
|