linux/drivers/gpu/drm/bridge/synopsys/dw-hdmi-cec.c
<<
>>
Prefs
   1// SPDX-License-Identifier: GPL-2.0-only
   2/*
   3 * Designware HDMI CEC driver
   4 *
   5 * Copyright (C) 2015-2017 Russell King.
   6 */
   7#include <linux/interrupt.h>
   8#include <linux/io.h>
   9#include <linux/module.h>
  10#include <linux/platform_device.h>
  11#include <linux/sched.h>
  12#include <linux/slab.h>
  13
  14#include <drm/drm_edid.h>
  15
  16#include <media/cec.h>
  17#include <media/cec-notifier.h>
  18
  19#include "dw-hdmi-cec.h"
  20
  21enum {
  22        HDMI_IH_CEC_STAT0       = 0x0106,
  23        HDMI_IH_MUTE_CEC_STAT0  = 0x0186,
  24
  25        HDMI_CEC_CTRL           = 0x7d00,
  26        CEC_CTRL_START          = BIT(0),
  27        CEC_CTRL_FRAME_TYP      = 3 << 1,
  28        CEC_CTRL_RETRY          = 0 << 1,
  29        CEC_CTRL_NORMAL         = 1 << 1,
  30        CEC_CTRL_IMMED          = 2 << 1,
  31
  32        HDMI_CEC_STAT           = 0x7d01,
  33        CEC_STAT_DONE           = BIT(0),
  34        CEC_STAT_EOM            = BIT(1),
  35        CEC_STAT_NACK           = BIT(2),
  36        CEC_STAT_ARBLOST        = BIT(3),
  37        CEC_STAT_ERROR_INIT     = BIT(4),
  38        CEC_STAT_ERROR_FOLL     = BIT(5),
  39        CEC_STAT_WAKEUP         = BIT(6),
  40
  41        HDMI_CEC_MASK           = 0x7d02,
  42        HDMI_CEC_POLARITY       = 0x7d03,
  43        HDMI_CEC_INT            = 0x7d04,
  44        HDMI_CEC_ADDR_L         = 0x7d05,
  45        HDMI_CEC_ADDR_H         = 0x7d06,
  46        HDMI_CEC_TX_CNT         = 0x7d07,
  47        HDMI_CEC_RX_CNT         = 0x7d08,
  48        HDMI_CEC_TX_DATA0       = 0x7d10,
  49        HDMI_CEC_RX_DATA0       = 0x7d20,
  50        HDMI_CEC_LOCK           = 0x7d30,
  51        HDMI_CEC_WKUPCTRL       = 0x7d31,
  52};
  53
  54struct dw_hdmi_cec {
  55        struct dw_hdmi *hdmi;
  56        const struct dw_hdmi_cec_ops *ops;
  57        u32 addresses;
  58        struct cec_adapter *adap;
  59        struct cec_msg rx_msg;
  60        unsigned int tx_status;
  61        bool tx_done;
  62        bool rx_done;
  63        struct cec_notifier *notify;
  64        int irq;
  65};
  66
  67static void dw_hdmi_write(struct dw_hdmi_cec *cec, u8 val, int offset)
  68{
  69        cec->ops->write(cec->hdmi, val, offset);
  70}
  71
  72static u8 dw_hdmi_read(struct dw_hdmi_cec *cec, int offset)
  73{
  74        return cec->ops->read(cec->hdmi, offset);
  75}
  76
  77static int dw_hdmi_cec_log_addr(struct cec_adapter *adap, u8 logical_addr)
  78{
  79        struct dw_hdmi_cec *cec = cec_get_drvdata(adap);
  80
  81        if (logical_addr == CEC_LOG_ADDR_INVALID)
  82                cec->addresses = 0;
  83        else
  84                cec->addresses |= BIT(logical_addr) | BIT(15);
  85
  86        dw_hdmi_write(cec, cec->addresses & 255, HDMI_CEC_ADDR_L);
  87        dw_hdmi_write(cec, cec->addresses >> 8, HDMI_CEC_ADDR_H);
  88
  89        return 0;
  90}
  91
  92static int dw_hdmi_cec_transmit(struct cec_adapter *adap, u8 attempts,
  93                                u32 signal_free_time, struct cec_msg *msg)
  94{
  95        struct dw_hdmi_cec *cec = cec_get_drvdata(adap);
  96        unsigned int i, ctrl;
  97
  98        switch (signal_free_time) {
  99        case CEC_SIGNAL_FREE_TIME_RETRY:
 100                ctrl = CEC_CTRL_RETRY;
 101                break;
 102        case CEC_SIGNAL_FREE_TIME_NEW_INITIATOR:
 103        default:
 104                ctrl = CEC_CTRL_NORMAL;
 105                break;
 106        case CEC_SIGNAL_FREE_TIME_NEXT_XFER:
 107                ctrl = CEC_CTRL_IMMED;
 108                break;
 109        }
 110
 111        for (i = 0; i < msg->len; i++)
 112                dw_hdmi_write(cec, msg->msg[i], HDMI_CEC_TX_DATA0 + i);
 113
 114        dw_hdmi_write(cec, msg->len, HDMI_CEC_TX_CNT);
 115        dw_hdmi_write(cec, ctrl | CEC_CTRL_START, HDMI_CEC_CTRL);
 116
 117        return 0;
 118}
 119
 120static irqreturn_t dw_hdmi_cec_hardirq(int irq, void *data)
 121{
 122        struct cec_adapter *adap = data;
 123        struct dw_hdmi_cec *cec = cec_get_drvdata(adap);
 124        unsigned int stat = dw_hdmi_read(cec, HDMI_IH_CEC_STAT0);
 125        irqreturn_t ret = IRQ_HANDLED;
 126
 127        if (stat == 0)
 128                return IRQ_NONE;
 129
 130        dw_hdmi_write(cec, stat, HDMI_IH_CEC_STAT0);
 131
 132        if (stat & CEC_STAT_ERROR_INIT) {
 133                cec->tx_status = CEC_TX_STATUS_ERROR;
 134                cec->tx_done = true;
 135                ret = IRQ_WAKE_THREAD;
 136        } else if (stat & CEC_STAT_DONE) {
 137                cec->tx_status = CEC_TX_STATUS_OK;
 138                cec->tx_done = true;
 139                ret = IRQ_WAKE_THREAD;
 140        } else if (stat & CEC_STAT_NACK) {
 141                cec->tx_status = CEC_TX_STATUS_NACK;
 142                cec->tx_done = true;
 143                ret = IRQ_WAKE_THREAD;
 144        }
 145
 146        if (stat & CEC_STAT_EOM) {
 147                unsigned int len, i;
 148
 149                len = dw_hdmi_read(cec, HDMI_CEC_RX_CNT);
 150                if (len > sizeof(cec->rx_msg.msg))
 151                        len = sizeof(cec->rx_msg.msg);
 152
 153                for (i = 0; i < len; i++)
 154                        cec->rx_msg.msg[i] =
 155                                dw_hdmi_read(cec, HDMI_CEC_RX_DATA0 + i);
 156
 157                dw_hdmi_write(cec, 0, HDMI_CEC_LOCK);
 158
 159                cec->rx_msg.len = len;
 160                smp_wmb();
 161                cec->rx_done = true;
 162
 163                ret = IRQ_WAKE_THREAD;
 164        }
 165
 166        return ret;
 167}
 168
 169static irqreturn_t dw_hdmi_cec_thread(int irq, void *data)
 170{
 171        struct cec_adapter *adap = data;
 172        struct dw_hdmi_cec *cec = cec_get_drvdata(adap);
 173
 174        if (cec->tx_done) {
 175                cec->tx_done = false;
 176                cec_transmit_attempt_done(adap, cec->tx_status);
 177        }
 178        if (cec->rx_done) {
 179                cec->rx_done = false;
 180                smp_rmb();
 181                cec_received_msg(adap, &cec->rx_msg);
 182        }
 183        return IRQ_HANDLED;
 184}
 185
 186static int dw_hdmi_cec_enable(struct cec_adapter *adap, bool enable)
 187{
 188        struct dw_hdmi_cec *cec = cec_get_drvdata(adap);
 189
 190        if (!enable) {
 191                dw_hdmi_write(cec, ~0, HDMI_CEC_MASK);
 192                dw_hdmi_write(cec, ~0, HDMI_IH_MUTE_CEC_STAT0);
 193                dw_hdmi_write(cec, 0, HDMI_CEC_POLARITY);
 194
 195                cec->ops->disable(cec->hdmi);
 196        } else {
 197                unsigned int irqs;
 198
 199                dw_hdmi_write(cec, 0, HDMI_CEC_CTRL);
 200                dw_hdmi_write(cec, ~0, HDMI_IH_CEC_STAT0);
 201                dw_hdmi_write(cec, 0, HDMI_CEC_LOCK);
 202
 203                dw_hdmi_cec_log_addr(cec->adap, CEC_LOG_ADDR_INVALID);
 204
 205                cec->ops->enable(cec->hdmi);
 206
 207                irqs = CEC_STAT_ERROR_INIT | CEC_STAT_NACK | CEC_STAT_EOM |
 208                       CEC_STAT_DONE;
 209                dw_hdmi_write(cec, irqs, HDMI_CEC_POLARITY);
 210                dw_hdmi_write(cec, ~irqs, HDMI_CEC_MASK);
 211                dw_hdmi_write(cec, ~irqs, HDMI_IH_MUTE_CEC_STAT0);
 212        }
 213        return 0;
 214}
 215
 216static const struct cec_adap_ops dw_hdmi_cec_ops = {
 217        .adap_enable = dw_hdmi_cec_enable,
 218        .adap_log_addr = dw_hdmi_cec_log_addr,
 219        .adap_transmit = dw_hdmi_cec_transmit,
 220};
 221
 222static void dw_hdmi_cec_del(void *data)
 223{
 224        struct dw_hdmi_cec *cec = data;
 225
 226        cec_delete_adapter(cec->adap);
 227}
 228
 229static int dw_hdmi_cec_probe(struct platform_device *pdev)
 230{
 231        struct dw_hdmi_cec_data *data = dev_get_platdata(&pdev->dev);
 232        struct dw_hdmi_cec *cec;
 233        int ret;
 234
 235        if (!data)
 236                return -ENXIO;
 237
 238        /*
 239         * Our device is just a convenience - we want to link to the real
 240         * hardware device here, so that userspace can see the association
 241         * between the HDMI hardware and its associated CEC chardev.
 242         */
 243        cec = devm_kzalloc(&pdev->dev, sizeof(*cec), GFP_KERNEL);
 244        if (!cec)
 245                return -ENOMEM;
 246
 247        cec->irq = data->irq;
 248        cec->ops = data->ops;
 249        cec->hdmi = data->hdmi;
 250
 251        platform_set_drvdata(pdev, cec);
 252
 253        dw_hdmi_write(cec, 0, HDMI_CEC_TX_CNT);
 254        dw_hdmi_write(cec, ~0, HDMI_CEC_MASK);
 255        dw_hdmi_write(cec, ~0, HDMI_IH_MUTE_CEC_STAT0);
 256        dw_hdmi_write(cec, 0, HDMI_CEC_POLARITY);
 257
 258        cec->adap = cec_allocate_adapter(&dw_hdmi_cec_ops, cec, "dw_hdmi",
 259                                         CEC_CAP_LOG_ADDRS | CEC_CAP_TRANSMIT |
 260                                         CEC_CAP_RC | CEC_CAP_PASSTHROUGH,
 261                                         CEC_MAX_LOG_ADDRS);
 262        if (IS_ERR(cec->adap))
 263                return PTR_ERR(cec->adap);
 264
 265        /* override the module pointer */
 266        cec->adap->owner = THIS_MODULE;
 267
 268        ret = devm_add_action(&pdev->dev, dw_hdmi_cec_del, cec);
 269        if (ret) {
 270                cec_delete_adapter(cec->adap);
 271                return ret;
 272        }
 273
 274        ret = devm_request_threaded_irq(&pdev->dev, cec->irq,
 275                                        dw_hdmi_cec_hardirq,
 276                                        dw_hdmi_cec_thread, IRQF_SHARED,
 277                                        "dw-hdmi-cec", cec->adap);
 278        if (ret < 0)
 279                return ret;
 280
 281        cec->notify = cec_notifier_get(pdev->dev.parent);
 282        if (!cec->notify)
 283                return -ENOMEM;
 284
 285        ret = cec_register_adapter(cec->adap, pdev->dev.parent);
 286        if (ret < 0) {
 287                cec_notifier_put(cec->notify);
 288                return ret;
 289        }
 290
 291        /*
 292         * CEC documentation says we must not call cec_delete_adapter
 293         * after a successful call to cec_register_adapter().
 294         */
 295        devm_remove_action(&pdev->dev, dw_hdmi_cec_del, cec);
 296
 297        cec_register_cec_notifier(cec->adap, cec->notify);
 298
 299        return 0;
 300}
 301
 302static int dw_hdmi_cec_remove(struct platform_device *pdev)
 303{
 304        struct dw_hdmi_cec *cec = platform_get_drvdata(pdev);
 305
 306        cec_unregister_adapter(cec->adap);
 307        cec_notifier_put(cec->notify);
 308
 309        return 0;
 310}
 311
 312static struct platform_driver dw_hdmi_cec_driver = {
 313        .probe  = dw_hdmi_cec_probe,
 314        .remove = dw_hdmi_cec_remove,
 315        .driver = {
 316                .name = "dw-hdmi-cec",
 317        },
 318};
 319module_platform_driver(dw_hdmi_cec_driver);
 320
 321MODULE_AUTHOR("Russell King <rmk+kernel@armlinux.org.uk>");
 322MODULE_DESCRIPTION("Synopsys Designware HDMI CEC driver for i.MX");
 323MODULE_LICENSE("GPL");
 324MODULE_ALIAS(PLATFORM_MODULE_PREFIX "dw-hdmi-cec");
 325