Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# SPDX-License-Identifier: GPL-2.0-only
config CEC_CORE
tristate
config CEC_NOTIFIER
bool
config CEC_PIN
bool
menu "CEC support"
config MEDIA_CEC_RC
bool "HDMI CEC RC integration"
depends on CEC_CORE && RC_CORE
depends on CEC_CORE=m || RC_CORE=y
help
Pass on CEC remote control messages to the RC framework.
config CEC_PIN_ERROR_INJ
bool "Enable CEC error injection support"
depends on CEC_PIN && DEBUG_FS
help
This option enables CEC error injection using debugfs.
menuconfig MEDIA_CEC_SUPPORT
bool
prompt "HDMI CEC drivers"
default y if MEDIA_SUPPORT && !MEDIA_SUPPORT_FILTER
help
Enable support for HDMI CEC (Consumer Electronics Control),
which is an optional HDMI feature.
Say Y when you have an HDMI receiver, transmitter or a USB CEC
adapter that supports HDMI CEC.
if MEDIA_CEC_SUPPORT
source "drivers/media/cec/i2c/Kconfig"
source "drivers/media/cec/platform/Kconfig"
source "drivers/media/cec/usb/Kconfig"
endif
endmenu
+2
View File
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0
obj-y += core/ i2c/ platform/ usb/
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: GPL-2.0
cec-objs := cec-core.o cec-adap.o cec-api.o
ifeq ($(CONFIG_CEC_NOTIFIER),y)
cec-objs += cec-notifier.o
endif
ifeq ($(CONFIG_CEC_PIN),y)
cec-objs += cec-pin.o
endif
ifeq ($(CONFIG_CEC_PIN_ERROR_INJ),y)
cec-objs += cec-pin-error-inj.o
endif
obj-$(CONFIG_CEC_CORE) += cec.o
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,701 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* cec-api.c - HDMI Consumer Electronics Control framework - API
*
* Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kmod.h>
#include <linux/ktime.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/string.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#include <media/cec-pin.h>
#include "cec-priv.h"
#include "cec-pin-priv.h"
static inline struct cec_devnode *cec_devnode_data(struct file *filp)
{
struct cec_fh *fh = filp->private_data;
return &fh->adap->devnode;
}
/* CEC file operations */
static __poll_t cec_poll(struct file *filp,
struct poll_table_struct *poll)
{
struct cec_fh *fh = filp->private_data;
struct cec_adapter *adap = fh->adap;
__poll_t res = 0;
poll_wait(filp, &fh->wait, poll);
if (!cec_is_registered(adap))
return EPOLLERR | EPOLLHUP | EPOLLPRI;
mutex_lock(&adap->lock);
if (adap->is_configured &&
adap->transmit_queue_sz < CEC_MAX_MSG_TX_QUEUE_SZ)
res |= EPOLLOUT | EPOLLWRNORM;
if (fh->queued_msgs)
res |= EPOLLIN | EPOLLRDNORM;
if (fh->total_queued_events)
res |= EPOLLPRI;
mutex_unlock(&adap->lock);
return res;
}
static bool cec_is_busy(const struct cec_adapter *adap,
const struct cec_fh *fh)
{
bool valid_initiator = adap->cec_initiator && adap->cec_initiator == fh;
bool valid_follower = adap->cec_follower && adap->cec_follower == fh;
/*
* Exclusive initiators and followers can always access the CEC adapter
*/
if (valid_initiator || valid_follower)
return false;
/*
* All others can only access the CEC adapter if there is no
* exclusive initiator and they are in INITIATOR mode.
*/
return adap->cec_initiator ||
fh->mode_initiator == CEC_MODE_NO_INITIATOR;
}
static long cec_adap_g_caps(struct cec_adapter *adap,
struct cec_caps __user *parg)
{
struct cec_caps caps = {};
strscpy(caps.driver, adap->devnode.dev.parent->driver->name,
sizeof(caps.driver));
strscpy(caps.name, adap->name, sizeof(caps.name));
caps.available_log_addrs = adap->available_log_addrs;
caps.capabilities = adap->capabilities;
caps.version = LINUX_VERSION_CODE;
if (copy_to_user(parg, &caps, sizeof(caps)))
return -EFAULT;
return 0;
}
static long cec_adap_g_phys_addr(struct cec_adapter *adap,
__u16 __user *parg)
{
u16 phys_addr;
mutex_lock(&adap->lock);
phys_addr = adap->phys_addr;
mutex_unlock(&adap->lock);
if (copy_to_user(parg, &phys_addr, sizeof(phys_addr)))
return -EFAULT;
return 0;
}
static int cec_validate_phys_addr(u16 phys_addr)
{
int i;
if (phys_addr == CEC_PHYS_ADDR_INVALID)
return 0;
for (i = 0; i < 16; i += 4)
if (phys_addr & (0xf << i))
break;
if (i == 16)
return 0;
for (i += 4; i < 16; i += 4)
if ((phys_addr & (0xf << i)) == 0)
return -EINVAL;
return 0;
}
static long cec_adap_s_phys_addr(struct cec_adapter *adap, struct cec_fh *fh,
bool block, __u16 __user *parg)
{
u16 phys_addr;
long err;
if (!(adap->capabilities & CEC_CAP_PHYS_ADDR))
return -ENOTTY;
if (copy_from_user(&phys_addr, parg, sizeof(phys_addr)))
return -EFAULT;
err = cec_validate_phys_addr(phys_addr);
if (err)
return err;
mutex_lock(&adap->lock);
if (cec_is_busy(adap, fh))
err = -EBUSY;
else
__cec_s_phys_addr(adap, phys_addr, block);
mutex_unlock(&adap->lock);
return err;
}
static long cec_adap_g_log_addrs(struct cec_adapter *adap,
struct cec_log_addrs __user *parg)
{
struct cec_log_addrs log_addrs;
mutex_lock(&adap->lock);
/*
* We use memcpy here instead of assignment since there is a
* hole at the end of struct cec_log_addrs that an assignment
* might ignore. So when we do copy_to_user() we could leak
* one byte of memory.
*/
memcpy(&log_addrs, &adap->log_addrs, sizeof(log_addrs));
if (!adap->is_configured)
memset(log_addrs.log_addr, CEC_LOG_ADDR_INVALID,
sizeof(log_addrs.log_addr));
mutex_unlock(&adap->lock);
if (copy_to_user(parg, &log_addrs, sizeof(log_addrs)))
return -EFAULT;
return 0;
}
static long cec_adap_s_log_addrs(struct cec_adapter *adap, struct cec_fh *fh,
bool block, struct cec_log_addrs __user *parg)
{
struct cec_log_addrs log_addrs;
long err = -EBUSY;
if (!(adap->capabilities & CEC_CAP_LOG_ADDRS))
return -ENOTTY;
if (copy_from_user(&log_addrs, parg, sizeof(log_addrs)))
return -EFAULT;
log_addrs.flags &= CEC_LOG_ADDRS_FL_ALLOW_UNREG_FALLBACK |
CEC_LOG_ADDRS_FL_ALLOW_RC_PASSTHRU |
CEC_LOG_ADDRS_FL_CDC_ONLY;
mutex_lock(&adap->lock);
if (!adap->is_claiming_log_addrs && !adap->is_configuring &&
(!log_addrs.num_log_addrs || !adap->is_configured) &&
!cec_is_busy(adap, fh)) {
err = __cec_s_log_addrs(adap, &log_addrs, block);
if (!err)
log_addrs = adap->log_addrs;
}
mutex_unlock(&adap->lock);
if (err)
return err;
if (copy_to_user(parg, &log_addrs, sizeof(log_addrs)))
return -EFAULT;
return 0;
}
static long cec_adap_g_connector_info(struct cec_adapter *adap,
struct cec_log_addrs __user *parg)
{
int ret = 0;
if (!(adap->capabilities & CEC_CAP_CONNECTOR_INFO))
return -ENOTTY;
mutex_lock(&adap->lock);
if (copy_to_user(parg, &adap->conn_info, sizeof(adap->conn_info)))
ret = -EFAULT;
mutex_unlock(&adap->lock);
return ret;
}
static long cec_transmit(struct cec_adapter *adap, struct cec_fh *fh,
bool block, struct cec_msg __user *parg)
{
struct cec_msg msg = {};
long err = 0;
if (!(adap->capabilities & CEC_CAP_TRANSMIT))
return -ENOTTY;
if (copy_from_user(&msg, parg, sizeof(msg)))
return -EFAULT;
mutex_lock(&adap->lock);
if (adap->log_addrs.num_log_addrs == 0)
err = -EPERM;
else if (adap->is_configuring && !msg_is_raw(&msg))
err = -ENONET;
else if (cec_is_busy(adap, fh))
err = -EBUSY;
else
err = cec_transmit_msg_fh(adap, &msg, fh, block);
mutex_unlock(&adap->lock);
if (err)
return err;
if (copy_to_user(parg, &msg, sizeof(msg)))
return -EFAULT;
return 0;
}
/* Called by CEC_RECEIVE: wait for a message to arrive */
static int cec_receive_msg(struct cec_fh *fh, struct cec_msg *msg, bool block)
{
u32 timeout = msg->timeout;
int res;
do {
mutex_lock(&fh->lock);
/* Are there received messages queued up? */
if (fh->queued_msgs) {
/* Yes, return the first one */
struct cec_msg_entry *entry =
list_first_entry(&fh->msgs,
struct cec_msg_entry, list);
list_del(&entry->list);
*msg = entry->msg;
kfree(entry);
fh->queued_msgs--;
mutex_unlock(&fh->lock);
/* restore original timeout value */
msg->timeout = timeout;
return 0;
}
/* No, return EAGAIN in non-blocking mode or wait */
mutex_unlock(&fh->lock);
/* Return when in non-blocking mode */
if (!block)
return -EAGAIN;
if (msg->timeout) {
/* The user specified a timeout */
res = wait_event_interruptible_timeout(fh->wait,
fh->queued_msgs,
msecs_to_jiffies(msg->timeout));
if (res == 0)
res = -ETIMEDOUT;
else if (res > 0)
res = 0;
} else {
/* Wait indefinitely */
res = wait_event_interruptible(fh->wait,
fh->queued_msgs);
}
/* Exit on error, otherwise loop to get the new message */
} while (!res);
return res;
}
static long cec_receive(struct cec_adapter *adap, struct cec_fh *fh,
bool block, struct cec_msg __user *parg)
{
struct cec_msg msg = {};
long err;
if (copy_from_user(&msg, parg, sizeof(msg)))
return -EFAULT;
err = cec_receive_msg(fh, &msg, block);
if (err)
return err;
msg.flags = 0;
if (copy_to_user(parg, &msg, sizeof(msg)))
return -EFAULT;
return 0;
}
static long cec_dqevent(struct cec_adapter *adap, struct cec_fh *fh,
bool block, struct cec_event __user *parg)
{
struct cec_event_entry *ev = NULL;
u64 ts = ~0ULL;
unsigned int i;
unsigned int ev_idx;
long err = 0;
mutex_lock(&fh->lock);
while (!fh->total_queued_events && block) {
mutex_unlock(&fh->lock);
err = wait_event_interruptible(fh->wait,
fh->total_queued_events);
if (err)
return err;
mutex_lock(&fh->lock);
}
/* Find the oldest event */
for (i = 0; i < CEC_NUM_EVENTS; i++) {
struct cec_event_entry *entry =
list_first_entry_or_null(&fh->events[i],
struct cec_event_entry, list);
if (entry && entry->ev.ts <= ts) {
ev = entry;
ev_idx = i;
ts = ev->ev.ts;
}
}
if (!ev) {
err = -EAGAIN;
goto unlock;
}
list_del(&ev->list);
if (copy_to_user(parg, &ev->ev, sizeof(ev->ev)))
err = -EFAULT;
if (ev_idx >= CEC_NUM_CORE_EVENTS)
kfree(ev);
fh->queued_events[ev_idx]--;
fh->total_queued_events--;
unlock:
mutex_unlock(&fh->lock);
return err;
}
static long cec_g_mode(struct cec_adapter *adap, struct cec_fh *fh,
u32 __user *parg)
{
u32 mode = fh->mode_initiator | fh->mode_follower;
if (copy_to_user(parg, &mode, sizeof(mode)))
return -EFAULT;
return 0;
}
static long cec_s_mode(struct cec_adapter *adap, struct cec_fh *fh,
u32 __user *parg)
{
u32 mode;
u8 mode_initiator;
u8 mode_follower;
bool send_pin_event = false;
long err = 0;
if (copy_from_user(&mode, parg, sizeof(mode)))
return -EFAULT;
if (mode & ~(CEC_MODE_INITIATOR_MSK | CEC_MODE_FOLLOWER_MSK)) {
dprintk(1, "%s: invalid mode bits set\n", __func__);
return -EINVAL;
}
mode_initiator = mode & CEC_MODE_INITIATOR_MSK;
mode_follower = mode & CEC_MODE_FOLLOWER_MSK;
if (mode_initiator > CEC_MODE_EXCL_INITIATOR ||
mode_follower > CEC_MODE_MONITOR_ALL) {
dprintk(1, "%s: unknown mode\n", __func__);
return -EINVAL;
}
if (mode_follower == CEC_MODE_MONITOR_ALL &&
!(adap->capabilities & CEC_CAP_MONITOR_ALL)) {
dprintk(1, "%s: MONITOR_ALL not supported\n", __func__);
return -EINVAL;
}
if (mode_follower == CEC_MODE_MONITOR_PIN &&
!(adap->capabilities & CEC_CAP_MONITOR_PIN)) {
dprintk(1, "%s: MONITOR_PIN not supported\n", __func__);
return -EINVAL;
}
/* Follower modes should always be able to send CEC messages */
if ((mode_initiator == CEC_MODE_NO_INITIATOR ||
!(adap->capabilities & CEC_CAP_TRANSMIT)) &&
mode_follower >= CEC_MODE_FOLLOWER &&
mode_follower <= CEC_MODE_EXCL_FOLLOWER_PASSTHRU) {
dprintk(1, "%s: cannot transmit\n", __func__);
return -EINVAL;
}
/* Monitor modes require CEC_MODE_NO_INITIATOR */
if (mode_initiator && mode_follower >= CEC_MODE_MONITOR_PIN) {
dprintk(1, "%s: monitor modes require NO_INITIATOR\n",
__func__);
return -EINVAL;
}
/* Monitor modes require CAP_NET_ADMIN */
if (mode_follower >= CEC_MODE_MONITOR_PIN && !capable(CAP_NET_ADMIN))
return -EPERM;
mutex_lock(&adap->lock);
/*
* You can't become exclusive follower if someone else already
* has that job.
*/
if ((mode_follower == CEC_MODE_EXCL_FOLLOWER ||
mode_follower == CEC_MODE_EXCL_FOLLOWER_PASSTHRU) &&
adap->cec_follower && adap->cec_follower != fh)
err = -EBUSY;
/*
* You can't become exclusive initiator if someone else already
* has that job.
*/
if (mode_initiator == CEC_MODE_EXCL_INITIATOR &&
adap->cec_initiator && adap->cec_initiator != fh)
err = -EBUSY;
if (!err) {
bool old_mon_all = fh->mode_follower == CEC_MODE_MONITOR_ALL;
bool new_mon_all = mode_follower == CEC_MODE_MONITOR_ALL;
if (old_mon_all != new_mon_all) {
if (new_mon_all)
err = cec_monitor_all_cnt_inc(adap);
else
cec_monitor_all_cnt_dec(adap);
}
}
if (!err) {
bool old_mon_pin = fh->mode_follower == CEC_MODE_MONITOR_PIN;
bool new_mon_pin = mode_follower == CEC_MODE_MONITOR_PIN;
if (old_mon_pin != new_mon_pin) {
send_pin_event = new_mon_pin;
if (new_mon_pin)
err = cec_monitor_pin_cnt_inc(adap);
else
cec_monitor_pin_cnt_dec(adap);
}
}
if (err) {
mutex_unlock(&adap->lock);
return err;
}
if (fh->mode_follower == CEC_MODE_FOLLOWER)
adap->follower_cnt--;
if (mode_follower == CEC_MODE_FOLLOWER)
adap->follower_cnt++;
if (send_pin_event) {
struct cec_event ev = {
.flags = CEC_EVENT_FL_INITIAL_STATE,
};
ev.event = adap->cec_pin_is_high ? CEC_EVENT_PIN_CEC_HIGH :
CEC_EVENT_PIN_CEC_LOW;
cec_queue_event_fh(fh, &ev, 0);
}
if (mode_follower == CEC_MODE_EXCL_FOLLOWER ||
mode_follower == CEC_MODE_EXCL_FOLLOWER_PASSTHRU) {
adap->passthrough =
mode_follower == CEC_MODE_EXCL_FOLLOWER_PASSTHRU;
adap->cec_follower = fh;
} else if (adap->cec_follower == fh) {
adap->passthrough = false;
adap->cec_follower = NULL;
}
if (mode_initiator == CEC_MODE_EXCL_INITIATOR)
adap->cec_initiator = fh;
else if (adap->cec_initiator == fh)
adap->cec_initiator = NULL;
fh->mode_initiator = mode_initiator;
fh->mode_follower = mode_follower;
mutex_unlock(&adap->lock);
return 0;
}
static long cec_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct cec_fh *fh = filp->private_data;
struct cec_adapter *adap = fh->adap;
bool block = !(filp->f_flags & O_NONBLOCK);
void __user *parg = (void __user *)arg;
if (!cec_is_registered(adap))
return -ENODEV;
switch (cmd) {
case CEC_ADAP_G_CAPS:
return cec_adap_g_caps(adap, parg);
case CEC_ADAP_G_PHYS_ADDR:
return cec_adap_g_phys_addr(adap, parg);
case CEC_ADAP_S_PHYS_ADDR:
return cec_adap_s_phys_addr(adap, fh, block, parg);
case CEC_ADAP_G_LOG_ADDRS:
return cec_adap_g_log_addrs(adap, parg);
case CEC_ADAP_S_LOG_ADDRS:
return cec_adap_s_log_addrs(adap, fh, block, parg);
case CEC_ADAP_G_CONNECTOR_INFO:
return cec_adap_g_connector_info(adap, parg);
case CEC_TRANSMIT:
return cec_transmit(adap, fh, block, parg);
case CEC_RECEIVE:
return cec_receive(adap, fh, block, parg);
case CEC_DQEVENT:
return cec_dqevent(adap, fh, block, parg);
case CEC_G_MODE:
return cec_g_mode(adap, fh, parg);
case CEC_S_MODE:
return cec_s_mode(adap, fh, parg);
default:
return -ENOTTY;
}
}
static int cec_open(struct inode *inode, struct file *filp)
{
struct cec_devnode *devnode =
container_of(inode->i_cdev, struct cec_devnode, cdev);
struct cec_adapter *adap = to_cec_adapter(devnode);
struct cec_fh *fh = kzalloc_obj(*fh);
/*
* Initial events that are automatically sent when the cec device is
* opened.
*/
struct cec_event ev = {
.event = CEC_EVENT_STATE_CHANGE,
.flags = CEC_EVENT_FL_INITIAL_STATE,
};
unsigned int i;
int err;
if (!fh)
return -ENOMEM;
INIT_LIST_HEAD(&fh->msgs);
INIT_LIST_HEAD(&fh->xfer_list);
for (i = 0; i < CEC_NUM_EVENTS; i++)
INIT_LIST_HEAD(&fh->events[i]);
mutex_init(&fh->lock);
init_waitqueue_head(&fh->wait);
fh->mode_initiator = CEC_MODE_INITIATOR;
fh->adap = adap;
err = cec_get_device(adap);
if (err) {
kfree(fh);
return err;
}
filp->private_data = fh;
/* Queue up initial state events */
ev.state_change.phys_addr = adap->phys_addr;
ev.state_change.log_addr_mask = adap->log_addrs.log_addr_mask;
ev.state_change.have_conn_info =
adap->conn_info.type != CEC_CONNECTOR_TYPE_NO_CONNECTOR;
cec_queue_event_fh(fh, &ev, 0);
#ifdef CONFIG_CEC_PIN
if (adap->pin && adap->pin->ops->read_hpd &&
!adap->devnode.unregistered) {
err = adap->pin->ops->read_hpd(adap);
if (err >= 0) {
ev.event = err ? CEC_EVENT_PIN_HPD_HIGH :
CEC_EVENT_PIN_HPD_LOW;
cec_queue_event_fh(fh, &ev, 0);
}
}
if (adap->pin && adap->pin->ops->read_5v &&
!adap->devnode.unregistered) {
err = adap->pin->ops->read_5v(adap);
if (err >= 0) {
ev.event = err ? CEC_EVENT_PIN_5V_HIGH :
CEC_EVENT_PIN_5V_LOW;
cec_queue_event_fh(fh, &ev, 0);
}
}
#endif
mutex_lock(&devnode->lock);
mutex_lock(&devnode->lock_fhs);
list_add(&fh->list, &devnode->fhs);
mutex_unlock(&devnode->lock_fhs);
mutex_unlock(&devnode->lock);
return 0;
}
/* Override for the release function */
static int cec_release(struct inode *inode, struct file *filp)
{
struct cec_devnode *devnode = cec_devnode_data(filp);
struct cec_adapter *adap = to_cec_adapter(devnode);
struct cec_fh *fh = filp->private_data;
unsigned int i;
mutex_lock(&adap->lock);
if (adap->cec_initiator == fh)
adap->cec_initiator = NULL;
if (adap->cec_follower == fh) {
adap->cec_follower = NULL;
adap->passthrough = false;
}
if (fh->mode_follower == CEC_MODE_FOLLOWER)
adap->follower_cnt--;
if (fh->mode_follower == CEC_MODE_MONITOR_PIN)
cec_monitor_pin_cnt_dec(adap);
if (fh->mode_follower == CEC_MODE_MONITOR_ALL)
cec_monitor_all_cnt_dec(adap);
mutex_unlock(&adap->lock);
mutex_lock(&devnode->lock);
mutex_lock(&devnode->lock_fhs);
list_del(&fh->list);
mutex_unlock(&devnode->lock_fhs);
mutex_unlock(&devnode->lock);
/* Unhook pending transmits from this filehandle. */
mutex_lock(&adap->lock);
while (!list_empty(&fh->xfer_list)) {
struct cec_data *data =
list_first_entry(&fh->xfer_list, struct cec_data, xfer_list);
data->blocking = false;
data->fh = NULL;
list_del_init(&data->xfer_list);
}
mutex_unlock(&adap->lock);
mutex_lock(&fh->lock);
while (!list_empty(&fh->msgs)) {
struct cec_msg_entry *entry =
list_first_entry(&fh->msgs, struct cec_msg_entry, list);
list_del(&entry->list);
kfree(entry);
}
for (i = CEC_NUM_CORE_EVENTS; i < CEC_NUM_EVENTS; i++) {
while (!list_empty(&fh->events[i])) {
struct cec_event_entry *entry =
list_first_entry(&fh->events[i],
struct cec_event_entry, list);
list_del(&entry->list);
kfree(entry);
}
}
mutex_unlock(&fh->lock);
kfree(fh);
cec_put_device(adap);
filp->private_data = NULL;
return 0;
}
const struct file_operations cec_devnode_fops = {
.owner = THIS_MODULE,
.open = cec_open,
.unlocked_ioctl = cec_ioctl,
.compat_ioctl = cec_ioctl,
.release = cec_release,
.poll = cec_poll,
};
@@ -0,0 +1,445 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* cec-core.c - HDMI Consumer Electronics Control framework - Core
*
* Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <linux/debugfs.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kmod.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/types.h>
#include "cec-priv.h"
#define CEC_NUM_DEVICES 256
#define CEC_NAME "cec"
/*
* 400 ms is the time it takes for one 16 byte message to be
* transferred and 5 is the maximum number of retries. Add
* another 100 ms as a margin. So if the transmit doesn't
* finish before that time something is really wrong and we
* have to time out.
*
* This is a sign that something it really wrong and a warning
* will be issued.
*/
#define CEC_XFER_TIMEOUT_MS (5 * 400 + 100)
int cec_debug;
module_param_named(debug, cec_debug, int, 0644);
MODULE_PARM_DESC(debug, "debug level (0-2)");
static bool debug_phys_addr;
module_param(debug_phys_addr, bool, 0644);
MODULE_PARM_DESC(debug_phys_addr, "add CEC_CAP_PHYS_ADDR if set");
static dev_t cec_dev_t;
/* Active devices */
static DEFINE_MUTEX(cec_devnode_lock);
static DECLARE_BITMAP(cec_devnode_nums, CEC_NUM_DEVICES);
static struct dentry *top_cec_dir;
/* dev to cec_devnode */
#define to_cec_devnode(cd) container_of(cd, struct cec_devnode, dev)
/* Called when the last user of the cec device exits. */
static void cec_devnode_release(struct device *cd)
{
struct cec_devnode *devnode = to_cec_devnode(cd);
mutex_lock(&cec_devnode_lock);
/* Mark device node number as free */
clear_bit(devnode->minor, cec_devnode_nums);
mutex_unlock(&cec_devnode_lock);
cec_delete_adapter(to_cec_adapter(devnode));
}
static const struct bus_type cec_bus_type = {
.name = CEC_NAME,
};
/*
* Register a cec device node
*
* The registration code assigns minor numbers and registers the new device node
* with the kernel. An error is returned if no free minor number can be found,
* or if the registration of the device node fails.
*
* Zero is returned on success.
*
* Note that if the cec_devnode_register call fails, the release() callback of
* the cec_devnode structure is *not* called, so the caller is responsible for
* freeing any data.
*/
static int __must_check cec_devnode_register(struct cec_devnode *devnode,
struct module *owner)
{
int minor;
int ret;
/* Part 1: Find a free minor number */
mutex_lock(&cec_devnode_lock);
minor = find_first_zero_bit(cec_devnode_nums, CEC_NUM_DEVICES);
if (minor == CEC_NUM_DEVICES) {
mutex_unlock(&cec_devnode_lock);
pr_err("could not get a free minor\n");
return -ENFILE;
}
set_bit(minor, cec_devnode_nums);
mutex_unlock(&cec_devnode_lock);
devnode->minor = minor;
devnode->dev.bus = &cec_bus_type;
devnode->dev.devt = MKDEV(MAJOR(cec_dev_t), minor);
devnode->dev.release = cec_devnode_release;
dev_set_name(&devnode->dev, "cec%d", devnode->minor);
device_initialize(&devnode->dev);
/* Part 2: Initialize and register the character device */
cdev_init(&devnode->cdev, &cec_devnode_fops);
devnode->cdev.owner = owner;
kobject_set_name(&devnode->cdev.kobj, "cec%d", devnode->minor);
devnode->registered = true;
ret = cdev_device_add(&devnode->cdev, &devnode->dev);
if (ret) {
devnode->registered = false;
pr_err("%s: cdev_device_add failed\n", __func__);
goto clr_bit;
}
return 0;
clr_bit:
mutex_lock(&cec_devnode_lock);
clear_bit(devnode->minor, cec_devnode_nums);
mutex_unlock(&cec_devnode_lock);
return ret;
}
/*
* Unregister a cec device node
*
* This unregisters the passed device. Future open calls will be met with
* errors.
*
* This function can safely be called if the device node has never been
* registered or has already been unregistered.
*/
static void cec_devnode_unregister(struct cec_adapter *adap)
{
struct cec_devnode *devnode = &adap->devnode;
struct cec_fh *fh;
mutex_lock(&devnode->lock);
/* Check if devnode was never registered or already unregistered */
if (!devnode->registered || devnode->unregistered) {
mutex_unlock(&devnode->lock);
return;
}
devnode->registered = false;
devnode->unregistered = true;
mutex_lock(&devnode->lock_fhs);
list_for_each_entry(fh, &devnode->fhs, list)
wake_up_interruptible(&fh->wait);
mutex_unlock(&devnode->lock_fhs);
mutex_unlock(&devnode->lock);
mutex_lock(&adap->lock);
__cec_s_phys_addr(adap, CEC_PHYS_ADDR_INVALID, false);
__cec_s_log_addrs(adap, NULL, false);
// Disable the adapter (since adap->devnode.unregistered is true)
cec_adap_enable(adap);
mutex_unlock(&adap->lock);
cdev_device_del(&devnode->cdev, &devnode->dev);
put_device(&devnode->dev);
}
#ifdef CONFIG_DEBUG_FS
static ssize_t cec_error_inj_write(struct file *file,
const char __user *ubuf, size_t count, loff_t *ppos)
{
struct seq_file *sf = file->private_data;
struct cec_adapter *adap = sf->private;
char *buf;
char *line;
char *p;
buf = memdup_user_nul(ubuf, min_t(size_t, PAGE_SIZE, count));
if (IS_ERR(buf))
return PTR_ERR(buf);
p = buf;
while (p && *p) {
p = skip_spaces(p);
line = strsep(&p, "\n");
if (!*line || *line == '#')
continue;
if (!call_op(adap, error_inj_parse_line, line)) {
kfree(buf);
return -EINVAL;
}
}
kfree(buf);
return count;
}
static int cec_error_inj_show(struct seq_file *sf, void *unused)
{
struct cec_adapter *adap = sf->private;
return call_op(adap, error_inj_show, sf);
}
static int cec_error_inj_open(struct inode *inode, struct file *file)
{
return single_open(file, cec_error_inj_show, inode->i_private);
}
static const struct file_operations cec_error_inj_fops = {
.open = cec_error_inj_open,
.write = cec_error_inj_write,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
#endif
struct cec_adapter *cec_allocate_adapter(const struct cec_adap_ops *ops,
void *priv, const char *name, u32 caps,
u8 available_las)
{
struct cec_adapter *adap;
int res;
#ifndef CONFIG_MEDIA_CEC_RC
caps &= ~CEC_CAP_RC;
#endif
if (WARN_ON(!caps))
return ERR_PTR(-EINVAL);
if (WARN_ON(!ops))
return ERR_PTR(-EINVAL);
if (WARN_ON(!available_las || available_las > CEC_MAX_LOG_ADDRS))
return ERR_PTR(-EINVAL);
adap = kzalloc_obj(*adap);
if (!adap)
return ERR_PTR(-ENOMEM);
strscpy(adap->name, name, sizeof(adap->name));
adap->phys_addr = CEC_PHYS_ADDR_INVALID;
adap->cec_pin_is_high = true;
adap->log_addrs.cec_version = CEC_OP_CEC_VERSION_2_0;
adap->log_addrs.vendor_id = CEC_VENDOR_ID_NONE;
adap->capabilities = caps | CEC_CAP_REPLY_VENDOR_ID;
if (debug_phys_addr)
adap->capabilities |= CEC_CAP_PHYS_ADDR;
adap->needs_hpd = caps & CEC_CAP_NEEDS_HPD;
adap->available_log_addrs = available_las;
adap->sequence = 0;
adap->ops = ops;
adap->priv = priv;
mutex_init(&adap->lock);
INIT_LIST_HEAD(&adap->transmit_queue);
INIT_LIST_HEAD(&adap->wait_queue);
init_waitqueue_head(&adap->kthread_waitq);
/* adap->devnode initialization */
INIT_LIST_HEAD(&adap->devnode.fhs);
mutex_init(&adap->devnode.lock_fhs);
mutex_init(&adap->devnode.lock);
adap->kthread = kthread_run(cec_thread_func, adap, "cec-%s", name);
if (IS_ERR(adap->kthread)) {
pr_err("cec-%s: kernel_thread() failed\n", name);
res = PTR_ERR(adap->kthread);
kfree(adap);
return ERR_PTR(res);
}
#ifdef CONFIG_MEDIA_CEC_RC
if (!(caps & CEC_CAP_RC))
return adap;
/* Prepare the RC input device */
adap->rc = rc_allocate_device(RC_DRIVER_SCANCODE);
if (!adap->rc) {
pr_err("cec-%s: failed to allocate memory for rc_dev\n",
name);
kthread_stop(adap->kthread);
kfree(adap);
return ERR_PTR(-ENOMEM);
}
snprintf(adap->input_phys, sizeof(adap->input_phys),
"%s/input0", adap->name);
adap->rc->device_name = adap->name;
adap->rc->input_phys = adap->input_phys;
adap->rc->input_id.bustype = BUS_CEC;
adap->rc->input_id.vendor = 0;
adap->rc->input_id.product = 0;
adap->rc->input_id.version = 1;
adap->rc->driver_name = CEC_NAME;
adap->rc->allowed_protocols = RC_PROTO_BIT_CEC;
adap->rc->priv = adap;
adap->rc->map_name = RC_MAP_CEC;
adap->rc->timeout = MS_TO_US(550);
#endif
return adap;
}
EXPORT_SYMBOL_GPL(cec_allocate_adapter);
int cec_register_adapter(struct cec_adapter *adap,
struct device *parent)
{
int res;
if (IS_ERR_OR_NULL(adap))
return 0;
if (WARN_ON(!parent))
return -EINVAL;
adap->owner = parent->driver->owner;
adap->devnode.dev.parent = parent;
if (!adap->xfer_timeout_ms)
adap->xfer_timeout_ms = CEC_XFER_TIMEOUT_MS;
#ifdef CONFIG_MEDIA_CEC_RC
if (adap->capabilities & CEC_CAP_RC) {
adap->rc->dev.parent = parent;
res = rc_register_device(adap->rc);
if (res) {
pr_err("cec-%s: failed to prepare input device\n",
adap->name);
rc_free_device(adap->rc);
adap->rc = NULL;
return res;
}
}
#endif
res = cec_devnode_register(&adap->devnode, adap->owner);
if (res) {
#ifdef CONFIG_MEDIA_CEC_RC
rc_unregister_device(adap->rc);
rc_free_device(adap->rc);
adap->rc = NULL;
#endif
return res;
}
dev_set_drvdata(&adap->devnode.dev, adap);
#ifdef CONFIG_DEBUG_FS
if (!top_cec_dir)
return 0;
adap->cec_dir = debugfs_create_dir(dev_name(&adap->devnode.dev),
top_cec_dir);
debugfs_create_devm_seqfile(&adap->devnode.dev, "status", adap->cec_dir,
cec_adap_status);
if (!adap->ops->error_inj_show || !adap->ops->error_inj_parse_line)
return 0;
debugfs_create_file("error-inj", 0644, adap->cec_dir, adap,
&cec_error_inj_fops);
#endif
return 0;
}
EXPORT_SYMBOL_GPL(cec_register_adapter);
void cec_unregister_adapter(struct cec_adapter *adap)
{
if (IS_ERR_OR_NULL(adap))
return;
#ifdef CONFIG_MEDIA_CEC_RC
/* Note: rc_unregister also calls rc_free */
rc_unregister_device(adap->rc);
adap->rc = NULL;
#endif
debugfs_remove_recursive(adap->cec_dir);
#ifdef CONFIG_CEC_NOTIFIER
cec_notifier_cec_adap_unregister(adap->notifier, adap);
#endif
cec_devnode_unregister(adap);
}
EXPORT_SYMBOL_GPL(cec_unregister_adapter);
void cec_delete_adapter(struct cec_adapter *adap)
{
if (IS_ERR_OR_NULL(adap))
return;
if (adap->kthread_config)
kthread_stop(adap->kthread_config);
kthread_stop(adap->kthread);
if (adap->ops->adap_free)
adap->ops->adap_free(adap);
#ifdef CONFIG_MEDIA_CEC_RC
rc_free_device(adap->rc);
#endif
kfree(adap);
}
EXPORT_SYMBOL_GPL(cec_delete_adapter);
/*
* Initialise cec for linux
*/
static int __init cec_devnode_init(void)
{
int ret = alloc_chrdev_region(&cec_dev_t, 0, CEC_NUM_DEVICES, CEC_NAME);
if (ret < 0) {
pr_warn("cec: unable to allocate major\n");
return ret;
}
#ifdef CONFIG_DEBUG_FS
top_cec_dir = debugfs_create_dir("cec", NULL);
if (IS_ERR_OR_NULL(top_cec_dir)) {
pr_warn("cec: Failed to create debugfs cec dir\n");
top_cec_dir = NULL;
}
#endif
ret = bus_register(&cec_bus_type);
if (ret < 0) {
debugfs_remove_recursive(top_cec_dir);
unregister_chrdev_region(cec_dev_t, CEC_NUM_DEVICES);
pr_warn("cec: bus_register failed\n");
return -EIO;
}
return 0;
}
static void __exit cec_devnode_exit(void)
{
debugfs_remove_recursive(top_cec_dir);
bus_unregister(&cec_bus_type);
unregister_chrdev_region(cec_dev_t, CEC_NUM_DEVICES);
}
subsys_initcall(cec_devnode_init);
module_exit(cec_devnode_exit)
MODULE_AUTHOR("Hans Verkuil <hverkuil@kernel.org>");
MODULE_DESCRIPTION("Device node registration for cec drivers");
MODULE_LICENSE("GPL");
@@ -0,0 +1,256 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* cec-notifier.c - notify CEC drivers of physical address changes
*
* Copyright 2016 Russell King.
* Copyright 2016-2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <linux/export.h>
#include <linux/platform_device.h>
#include <linux/string.h>
#include <linux/slab.h>
#include <linux/i2c.h>
#include <linux/list.h>
#include <linux/kref.h>
#include <linux/of_platform.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
#include <drm/drm_edid.h>
struct cec_notifier {
struct mutex lock;
struct list_head head;
struct kref kref;
struct device *hdmi_dev;
struct cec_connector_info conn_info;
const char *port_name;
struct cec_adapter *cec_adap;
u16 phys_addr;
};
static LIST_HEAD(cec_notifiers);
static DEFINE_MUTEX(cec_notifiers_lock);
/**
* cec_notifier_get_conn - find or create a new cec_notifier for the given
* device and connector tuple.
* @hdmi_dev: device that sends the events.
* @port_name: the connector name from which the event occurs
*
* If a notifier for device @dev already exists, then increase the refcount
* and return that notifier.
*
* If it doesn't exist, then allocate a new notifier struct and return a
* pointer to that new struct.
*
* Return NULL if the memory could not be allocated.
*/
static struct cec_notifier *
cec_notifier_get_conn(struct device *hdmi_dev, const char *port_name)
{
struct cec_notifier *n;
mutex_lock(&cec_notifiers_lock);
list_for_each_entry(n, &cec_notifiers, head) {
if (n->hdmi_dev == hdmi_dev &&
(!port_name ||
(n->port_name && !strcmp(n->port_name, port_name)))) {
kref_get(&n->kref);
mutex_unlock(&cec_notifiers_lock);
return n;
}
}
n = kzalloc_obj(*n);
if (!n)
goto unlock;
n->hdmi_dev = hdmi_dev;
if (port_name) {
n->port_name = kstrdup(port_name, GFP_KERNEL);
if (!n->port_name) {
kfree(n);
n = NULL;
goto unlock;
}
}
n->phys_addr = CEC_PHYS_ADDR_INVALID;
mutex_init(&n->lock);
kref_init(&n->kref);
list_add_tail(&n->head, &cec_notifiers);
unlock:
mutex_unlock(&cec_notifiers_lock);
return n;
}
static void cec_notifier_release(struct kref *kref)
{
struct cec_notifier *n =
container_of(kref, struct cec_notifier, kref);
list_del(&n->head);
kfree(n->port_name);
kfree(n);
}
static void cec_notifier_put(struct cec_notifier *n)
{
mutex_lock(&cec_notifiers_lock);
kref_put(&n->kref, cec_notifier_release);
mutex_unlock(&cec_notifiers_lock);
}
struct cec_notifier *
cec_notifier_conn_register(struct device *hdmi_dev, const char *port_name,
const struct cec_connector_info *conn_info)
{
struct cec_notifier *n = cec_notifier_get_conn(hdmi_dev, port_name);
if (!n)
return n;
mutex_lock(&n->lock);
n->phys_addr = CEC_PHYS_ADDR_INVALID;
if (conn_info)
n->conn_info = *conn_info;
else
memset(&n->conn_info, 0, sizeof(n->conn_info));
if (n->cec_adap) {
if (!n->cec_adap->adap_controls_phys_addr)
cec_phys_addr_invalidate(n->cec_adap);
cec_s_conn_info(n->cec_adap, conn_info);
}
mutex_unlock(&n->lock);
return n;
}
EXPORT_SYMBOL_GPL(cec_notifier_conn_register);
void cec_notifier_conn_unregister(struct cec_notifier *n)
{
if (!n)
return;
mutex_lock(&n->lock);
memset(&n->conn_info, 0, sizeof(n->conn_info));
n->phys_addr = CEC_PHYS_ADDR_INVALID;
if (n->cec_adap) {
if (!n->cec_adap->adap_controls_phys_addr)
cec_phys_addr_invalidate(n->cec_adap);
cec_s_conn_info(n->cec_adap, NULL);
}
mutex_unlock(&n->lock);
cec_notifier_put(n);
}
EXPORT_SYMBOL_GPL(cec_notifier_conn_unregister);
struct cec_notifier *
cec_notifier_cec_adap_register(struct device *hdmi_dev, const char *port_name,
struct cec_adapter *adap)
{
struct cec_notifier *n;
if (WARN_ON(!adap))
return NULL;
n = cec_notifier_get_conn(hdmi_dev, port_name);
if (!n)
return n;
mutex_lock(&n->lock);
n->cec_adap = adap;
adap->conn_info = n->conn_info;
adap->notifier = n;
if (!adap->adap_controls_phys_addr)
cec_s_phys_addr(adap, n->phys_addr, false);
mutex_unlock(&n->lock);
return n;
}
EXPORT_SYMBOL_GPL(cec_notifier_cec_adap_register);
void cec_notifier_cec_adap_unregister(struct cec_notifier *n,
struct cec_adapter *adap)
{
if (!n)
return;
mutex_lock(&n->lock);
adap->notifier = NULL;
n->cec_adap = NULL;
mutex_unlock(&n->lock);
cec_notifier_put(n);
}
EXPORT_SYMBOL_GPL(cec_notifier_cec_adap_unregister);
void cec_notifier_set_phys_addr(struct cec_notifier *n, u16 pa)
{
if (n == NULL)
return;
mutex_lock(&n->lock);
n->phys_addr = pa;
if (n->cec_adap && !n->cec_adap->adap_controls_phys_addr)
cec_s_phys_addr(n->cec_adap, n->phys_addr, false);
mutex_unlock(&n->lock);
}
EXPORT_SYMBOL_GPL(cec_notifier_set_phys_addr);
/*
* Note: In the drm subsystem, prefer calling (if possible):
*
* cec_notifier_set_phys_addr(n, connector->display_info.source_physical_address);
*/
void cec_notifier_set_phys_addr_from_edid(struct cec_notifier *n,
const struct edid *edid)
{
u16 pa = CEC_PHYS_ADDR_INVALID;
if (n == NULL)
return;
if (edid && edid->extensions)
pa = cec_get_edid_phys_addr((const u8 *)edid,
EDID_LENGTH * (edid->extensions + 1), NULL);
cec_notifier_set_phys_addr(n, pa);
}
EXPORT_SYMBOL_GPL(cec_notifier_set_phys_addr_from_edid);
struct device *cec_notifier_parse_hdmi_phandle(struct device *dev)
{
struct platform_device *hdmi_pdev;
struct device *hdmi_dev = NULL;
struct device_node *np;
np = of_parse_phandle(dev->of_node, "hdmi-phandle", 0);
if (!np) {
dev_err(dev, "Failed to find HDMI node in device tree\n");
return ERR_PTR(-ENODEV);
}
hdmi_pdev = of_find_device_by_node(np);
if (hdmi_pdev)
hdmi_dev = &hdmi_pdev->dev;
#if IS_REACHABLE(CONFIG_I2C)
if (!hdmi_dev) {
struct i2c_client *hdmi_client = of_find_i2c_device_by_node(np);
if (hdmi_client)
hdmi_dev = &hdmi_client->dev;
}
#endif
of_node_put(np);
if (!hdmi_dev)
return ERR_PTR(-EPROBE_DEFER);
/*
* Note that the device struct is only used as a key into the
* cec_notifiers list, it is never actually accessed.
* So we decrement the reference here so we don't leak
* memory.
*/
put_device(hdmi_dev);
return hdmi_dev;
}
EXPORT_SYMBOL_GPL(cec_notifier_parse_hdmi_phandle);
@@ -0,0 +1,403 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright 2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <linux/delay.h>
#include <linux/sched/types.h>
#include <linux/seq_file.h>
#include <linux/slab.h>
#include <media/cec-pin.h>
#include "cec-pin-priv.h"
struct cec_error_inj_cmd {
unsigned int mode_offset;
int arg_idx;
const char *cmd;
};
static const struct cec_error_inj_cmd cec_error_inj_cmds[] = {
{ CEC_ERROR_INJ_RX_NACK_OFFSET, -1, "rx-nack" },
{ CEC_ERROR_INJ_RX_LOW_DRIVE_OFFSET,
CEC_ERROR_INJ_RX_LOW_DRIVE_ARG_IDX, "rx-low-drive" },
{ CEC_ERROR_INJ_RX_ADD_BYTE_OFFSET, -1, "rx-add-byte" },
{ CEC_ERROR_INJ_RX_REMOVE_BYTE_OFFSET, -1, "rx-remove-byte" },
{ CEC_ERROR_INJ_RX_ARB_LOST_OFFSET,
CEC_ERROR_INJ_RX_ARB_LOST_ARG_IDX, "rx-arb-lost" },
{ CEC_ERROR_INJ_TX_NO_EOM_OFFSET, -1, "tx-no-eom" },
{ CEC_ERROR_INJ_TX_EARLY_EOM_OFFSET, -1, "tx-early-eom" },
{ CEC_ERROR_INJ_TX_ADD_BYTES_OFFSET,
CEC_ERROR_INJ_TX_ADD_BYTES_ARG_IDX, "tx-add-bytes" },
{ CEC_ERROR_INJ_TX_REMOVE_BYTE_OFFSET, -1, "tx-remove-byte" },
{ CEC_ERROR_INJ_TX_SHORT_BIT_OFFSET,
CEC_ERROR_INJ_TX_SHORT_BIT_ARG_IDX, "tx-short-bit" },
{ CEC_ERROR_INJ_TX_LONG_BIT_OFFSET,
CEC_ERROR_INJ_TX_LONG_BIT_ARG_IDX, "tx-long-bit" },
{ CEC_ERROR_INJ_TX_CUSTOM_BIT_OFFSET,
CEC_ERROR_INJ_TX_CUSTOM_BIT_ARG_IDX, "tx-custom-bit" },
{ CEC_ERROR_INJ_TX_SHORT_START_OFFSET, -1, "tx-short-start" },
{ CEC_ERROR_INJ_TX_LONG_START_OFFSET, -1, "tx-long-start" },
{ CEC_ERROR_INJ_TX_CUSTOM_START_OFFSET, -1, "tx-custom-start" },
{ CEC_ERROR_INJ_TX_LAST_BIT_OFFSET,
CEC_ERROR_INJ_TX_LAST_BIT_ARG_IDX, "tx-last-bit" },
{ CEC_ERROR_INJ_TX_LOW_DRIVE_OFFSET,
CEC_ERROR_INJ_TX_LOW_DRIVE_ARG_IDX, "tx-low-drive" },
{ 0, -1, NULL }
};
u16 cec_pin_rx_error_inj(struct cec_pin *pin)
{
u16 cmd = CEC_ERROR_INJ_OP_ANY;
/* Only when 18 bits have been received do we have a valid cmd */
if (!(pin->error_inj[cmd] & CEC_ERROR_INJ_RX_MASK) &&
pin->rx_bit >= 18)
cmd = pin->rx_msg.msg[1];
return (pin->error_inj[cmd] & CEC_ERROR_INJ_RX_MASK) ? cmd :
CEC_ERROR_INJ_OP_ANY;
}
u16 cec_pin_tx_error_inj(struct cec_pin *pin)
{
u16 cmd = CEC_ERROR_INJ_OP_ANY;
if (!(pin->error_inj[cmd] & CEC_ERROR_INJ_TX_MASK) &&
pin->tx_msg.len > 1)
cmd = pin->tx_msg.msg[1];
return (pin->error_inj[cmd] & CEC_ERROR_INJ_TX_MASK) ? cmd :
CEC_ERROR_INJ_OP_ANY;
}
bool cec_pin_error_inj_parse_line(struct cec_adapter *adap, char *line)
{
static const char *delims = " \t\r";
struct cec_pin *pin = adap->pin;
unsigned int i;
bool has_pos = false;
char *p = line;
char *token;
char *comma;
u64 *error;
u8 *args;
bool has_op;
u8 op;
u8 mode;
u8 pos;
p = skip_spaces(p);
token = strsep(&p, delims);
if (!strcmp(token, "clear")) {
memset(pin->error_inj, 0, sizeof(pin->error_inj));
pin->rx_toggle = pin->tx_toggle = false;
pin->rx_no_low_drive = false;
pin->tx_ignore_nack_until_eom = false;
pin->tx_custom_pulse = false;
pin->tx_custom_low_usecs = CEC_TIM_CUSTOM_DEFAULT;
pin->tx_custom_high_usecs = CEC_TIM_CUSTOM_DEFAULT;
pin->tx_glitch_low_usecs = CEC_TIM_GLITCH_DEFAULT;
pin->tx_glitch_high_usecs = CEC_TIM_GLITCH_DEFAULT;
pin->tx_glitch_falling_edge = false;
pin->tx_glitch_rising_edge = false;
return true;
}
if (!strcmp(token, "rx-clear")) {
for (i = 0; i <= CEC_ERROR_INJ_OP_ANY; i++)
pin->error_inj[i] &= ~CEC_ERROR_INJ_RX_MASK;
pin->rx_toggle = false;
pin->rx_no_low_drive = false;
return true;
}
if (!strcmp(token, "tx-clear")) {
for (i = 0; i <= CEC_ERROR_INJ_OP_ANY; i++)
pin->error_inj[i] &= ~CEC_ERROR_INJ_TX_MASK;
pin->tx_toggle = false;
pin->tx_ignore_nack_until_eom = false;
pin->tx_custom_pulse = false;
pin->tx_custom_low_usecs = CEC_TIM_CUSTOM_DEFAULT;
pin->tx_custom_high_usecs = CEC_TIM_CUSTOM_DEFAULT;
pin->tx_glitch_low_usecs = CEC_TIM_GLITCH_DEFAULT;
pin->tx_glitch_high_usecs = CEC_TIM_GLITCH_DEFAULT;
pin->tx_glitch_falling_edge = false;
pin->tx_glitch_rising_edge = false;
return true;
}
if (!strcmp(token, "rx-no-low-drive")) {
pin->rx_no_low_drive = true;
return true;
}
if (!strcmp(token, "tx-ignore-nack-until-eom")) {
pin->tx_ignore_nack_until_eom = true;
return true;
}
if (!strcmp(token, "tx-custom-pulse")) {
pin->tx_custom_pulse = true;
cec_pin_start_timer(pin);
return true;
}
if (!strcmp(token, "tx-glitch-falling-edge")) {
pin->tx_glitch_falling_edge = true;
return true;
}
if (!strcmp(token, "tx-glitch-rising-edge")) {
pin->tx_glitch_rising_edge = true;
return true;
}
if (!p)
return false;
p = skip_spaces(p);
if (!strcmp(token, "tx-custom-low-usecs")) {
u32 usecs;
if (kstrtou32(p, 0, &usecs) || usecs > 10000000)
return false;
pin->tx_custom_low_usecs = usecs;
return true;
}
if (!strcmp(token, "tx-custom-high-usecs")) {
u32 usecs;
if (kstrtou32(p, 0, &usecs) || usecs > 10000000)
return false;
pin->tx_glitch_high_usecs = usecs;
return true;
}
if (!strcmp(token, "tx-glitch-low-usecs")) {
u32 usecs;
if (kstrtou32(p, 0, &usecs) || usecs > 100)
return false;
pin->tx_glitch_low_usecs = usecs;
return true;
}
if (!strcmp(token, "tx-glitch-high-usecs")) {
u32 usecs;
if (kstrtou32(p, 0, &usecs) || usecs > 100)
return false;
pin->tx_glitch_high_usecs = usecs;
return true;
}
comma = strchr(token, ',');
if (comma)
*comma++ = '\0';
if (!strcmp(token, "any")) {
has_op = false;
error = pin->error_inj + CEC_ERROR_INJ_OP_ANY;
args = pin->error_inj_args[CEC_ERROR_INJ_OP_ANY];
} else if (!kstrtou8(token, 0, &op)) {
has_op = true;
error = pin->error_inj + op;
args = pin->error_inj_args[op];
} else {
return false;
}
mode = CEC_ERROR_INJ_MODE_ONCE;
if (comma) {
if (!strcmp(comma, "off"))
mode = CEC_ERROR_INJ_MODE_OFF;
else if (!strcmp(comma, "once"))
mode = CEC_ERROR_INJ_MODE_ONCE;
else if (!strcmp(comma, "always"))
mode = CEC_ERROR_INJ_MODE_ALWAYS;
else if (!strcmp(comma, "toggle"))
mode = CEC_ERROR_INJ_MODE_TOGGLE;
else
return false;
}
token = strsep(&p, delims);
if (p) {
p = skip_spaces(p);
has_pos = !kstrtou8(p, 0, &pos);
}
if (!strcmp(token, "clear")) {
*error = 0;
return true;
}
if (!strcmp(token, "rx-clear")) {
*error &= ~CEC_ERROR_INJ_RX_MASK;
return true;
}
if (!strcmp(token, "tx-clear")) {
*error &= ~CEC_ERROR_INJ_TX_MASK;
return true;
}
for (i = 0; cec_error_inj_cmds[i].cmd; i++) {
const char *cmd = cec_error_inj_cmds[i].cmd;
unsigned int mode_offset;
u64 mode_mask;
int arg_idx;
bool is_bit_pos = true;
if (strcmp(token, cmd))
continue;
mode_offset = cec_error_inj_cmds[i].mode_offset;
mode_mask = CEC_ERROR_INJ_MODE_MASK << mode_offset;
arg_idx = cec_error_inj_cmds[i].arg_idx;
if (mode_offset == CEC_ERROR_INJ_RX_ARB_LOST_OFFSET) {
if (has_op)
return false;
if (!has_pos)
pos = 0x0f;
is_bit_pos = false;
} else if (mode_offset == CEC_ERROR_INJ_TX_ADD_BYTES_OFFSET) {
if (!has_pos || !pos)
return false;
is_bit_pos = false;
}
if (arg_idx >= 0 && is_bit_pos) {
if (!has_pos || pos >= 160)
return false;
if (has_op && pos < 10 + 8)
return false;
/* Invalid bit position may not be the Ack bit */
if ((mode_offset == CEC_ERROR_INJ_TX_SHORT_BIT_OFFSET ||
mode_offset == CEC_ERROR_INJ_TX_LONG_BIT_OFFSET ||
mode_offset == CEC_ERROR_INJ_TX_CUSTOM_BIT_OFFSET) &&
(pos % 10) == 9)
return false;
}
*error &= ~mode_mask;
*error |= (u64)mode << mode_offset;
if (arg_idx >= 0)
args[arg_idx] = pos;
return true;
}
return false;
}
static void cec_pin_show_cmd(struct seq_file *sf, u32 cmd, u8 mode)
{
if (cmd == CEC_ERROR_INJ_OP_ANY)
seq_puts(sf, "any,");
else
seq_printf(sf, "0x%02x,", cmd);
switch (mode) {
case CEC_ERROR_INJ_MODE_ONCE:
seq_puts(sf, "once ");
break;
case CEC_ERROR_INJ_MODE_ALWAYS:
seq_puts(sf, "always ");
break;
case CEC_ERROR_INJ_MODE_TOGGLE:
seq_puts(sf, "toggle ");
break;
default:
seq_puts(sf, "off ");
break;
}
}
int cec_pin_error_inj_show(struct cec_adapter *adap, struct seq_file *sf)
{
struct cec_pin *pin = adap->pin;
unsigned int i, j;
seq_puts(sf, "# Clear error injections:\n");
seq_puts(sf, "# clear clear all rx and tx error injections\n");
seq_puts(sf, "# rx-clear clear all rx error injections\n");
seq_puts(sf, "# tx-clear clear all tx error injections\n");
seq_puts(sf, "# <op> clear clear all rx and tx error injections for <op>\n");
seq_puts(sf, "# <op> rx-clear clear all rx error injections for <op>\n");
seq_puts(sf, "# <op> tx-clear clear all tx error injections for <op>\n");
seq_puts(sf, "#\n");
seq_puts(sf, "# RX error injection settings:\n");
seq_puts(sf, "# rx-no-low-drive do not generate low-drive pulses\n");
seq_puts(sf, "#\n");
seq_puts(sf, "# RX error injection:\n");
seq_puts(sf, "# <op>[,<mode>] rx-nack NACK the message instead of sending an ACK\n");
seq_puts(sf, "# <op>[,<mode>] rx-low-drive <bit> force a low-drive condition at this bit position\n");
seq_puts(sf, "# <op>[,<mode>] rx-add-byte add a spurious byte to the received CEC message\n");
seq_puts(sf, "# <op>[,<mode>] rx-remove-byte remove the last byte from the received CEC message\n");
seq_puts(sf, "# any[,<mode>] rx-arb-lost [<poll>] generate a POLL message to trigger an arbitration lost\n");
seq_puts(sf, "#\n");
seq_puts(sf, "# TX error injection settings:\n");
seq_puts(sf, "# tx-ignore-nack-until-eom ignore early NACKs until EOM\n");
seq_puts(sf, "# tx-custom-low-usecs <usecs> define the 'low' time for the custom pulse\n");
seq_puts(sf, "# tx-custom-high-usecs <usecs> define the 'high' time for the custom pulse\n");
seq_puts(sf, "# tx-custom-pulse transmit the custom pulse once the bus is idle\n");
seq_puts(sf, "# tx-glitch-low-usecs <usecs> define the 'low' time for the glitch pulse\n");
seq_puts(sf, "# tx-glitch-high-usecs <usecs> define the 'high' time for the glitch pulse\n");
seq_puts(sf, "# tx-glitch-falling-edge send the glitch pulse after every falling edge\n");
seq_puts(sf, "# tx-glitch-rising-edge send the glitch pulse after every rising edge\n");
seq_puts(sf, "#\n");
seq_puts(sf, "# TX error injection:\n");
seq_puts(sf, "# <op>[,<mode>] tx-no-eom don't set the EOM bit\n");
seq_puts(sf, "# <op>[,<mode>] tx-early-eom set the EOM bit one byte too soon\n");
seq_puts(sf, "# <op>[,<mode>] tx-add-bytes <num> append <num> (1-255) spurious bytes to the message\n");
seq_puts(sf, "# <op>[,<mode>] tx-remove-byte drop the last byte from the message\n");
seq_puts(sf, "# <op>[,<mode>] tx-short-bit <bit> make this bit shorter than allowed\n");
seq_puts(sf, "# <op>[,<mode>] tx-long-bit <bit> make this bit longer than allowed\n");
seq_puts(sf, "# <op>[,<mode>] tx-custom-bit <bit> send the custom pulse instead of this bit\n");
seq_puts(sf, "# <op>[,<mode>] tx-short-start send a start pulse that's too short\n");
seq_puts(sf, "# <op>[,<mode>] tx-long-start send a start pulse that's too long\n");
seq_puts(sf, "# <op>[,<mode>] tx-custom-start send the custom pulse instead of the start pulse\n");
seq_puts(sf, "# <op>[,<mode>] tx-last-bit <bit> stop sending after this bit\n");
seq_puts(sf, "# <op>[,<mode>] tx-low-drive <bit> force a low-drive condition at this bit position\n");
seq_puts(sf, "#\n");
seq_puts(sf, "# <op> CEC message opcode (0-255) or 'any'\n");
seq_puts(sf, "# <mode> 'once' (default), 'always', 'toggle' or 'off'\n");
seq_puts(sf, "# <bit> CEC message bit (0-159)\n");
seq_puts(sf, "# 10 bits per 'byte': bits 0-7: data, bit 8: EOM, bit 9: ACK\n");
seq_puts(sf, "# <poll> CEC poll message used to test arbitration lost (0x00-0xff, default 0x0f)\n");
seq_puts(sf, "# <usecs> microseconds (0-10000000, default 1000)\n");
seq_puts(sf, "\nclear\n");
for (i = 0; i < ARRAY_SIZE(pin->error_inj); i++) {
u64 e = pin->error_inj[i];
for (j = 0; cec_error_inj_cmds[j].cmd; j++) {
const char *cmd = cec_error_inj_cmds[j].cmd;
unsigned int mode;
unsigned int mode_offset;
int arg_idx;
mode_offset = cec_error_inj_cmds[j].mode_offset;
arg_idx = cec_error_inj_cmds[j].arg_idx;
mode = (e >> mode_offset) & CEC_ERROR_INJ_MODE_MASK;
if (!mode)
continue;
cec_pin_show_cmd(sf, i, mode);
seq_puts(sf, cmd);
if (arg_idx >= 0)
seq_printf(sf, " %u",
pin->error_inj_args[i][arg_idx]);
seq_puts(sf, "\n");
}
}
if (pin->rx_no_low_drive)
seq_puts(sf, "rx-no-low-drive\n");
if (pin->tx_ignore_nack_until_eom)
seq_puts(sf, "tx-ignore-nack-until-eom\n");
if (pin->tx_glitch_falling_edge)
seq_puts(sf, "tx-glitch-falling-edge\n");
if (pin->tx_glitch_rising_edge)
seq_puts(sf, "tx-glitch-rising-edge\n");
if (pin->tx_custom_pulse)
seq_puts(sf, "tx-custom-pulse\n");
if (pin->tx_custom_low_usecs != CEC_TIM_CUSTOM_DEFAULT)
seq_printf(sf, "tx-custom-low-usecs %u\n",
pin->tx_custom_low_usecs);
if (pin->tx_custom_high_usecs != CEC_TIM_CUSTOM_DEFAULT)
seq_printf(sf, "tx-custom-high-usecs %u\n",
pin->tx_custom_high_usecs);
if (pin->tx_glitch_low_usecs != CEC_TIM_GLITCH_DEFAULT)
seq_printf(sf, "tx-glitch-low-usecs %u\n",
pin->tx_glitch_low_usecs);
if (pin->tx_glitch_high_usecs != CEC_TIM_GLITCH_DEFAULT)
seq_printf(sf, "tx-glitch-high-usecs %u\n",
pin->tx_glitch_high_usecs);
return 0;
}
@@ -0,0 +1,260 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* cec-pin-priv.h - internal cec-pin header
*
* Copyright 2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#ifndef LINUX_CEC_PIN_PRIV_H
#define LINUX_CEC_PIN_PRIV_H
#include <linux/types.h>
#include <linux/atomic.h>
#include <media/cec-pin.h>
#define call_pin_op(pin, op, arg...) \
((pin && pin->ops->op && !pin->adap->devnode.unregistered) ? \
pin->ops->op(pin->adap, ## arg) : 0)
#define call_void_pin_op(pin, op, arg...) \
do { \
if (pin && pin->ops->op && \
!pin->adap->devnode.unregistered) \
pin->ops->op(pin->adap, ## arg); \
} while (0)
enum cec_pin_state {
/* CEC is off */
CEC_ST_OFF,
/* CEC is idle, waiting for Rx or Tx */
CEC_ST_IDLE,
/* Tx states */
/* Pending Tx, waiting for Signal Free Time to expire */
CEC_ST_TX_WAIT,
/* Low-drive was detected, wait for bus to go high */
CEC_ST_TX_WAIT_FOR_HIGH,
/* Drive CEC low for the start bit */
CEC_ST_TX_START_BIT_LOW,
/* Drive CEC high for the start bit */
CEC_ST_TX_START_BIT_HIGH,
/* Generate a start bit period that is too short */
CEC_ST_TX_START_BIT_HIGH_SHORT,
/* Generate a start bit period that is too long */
CEC_ST_TX_START_BIT_HIGH_LONG,
/* Drive CEC low for the start bit using the custom timing */
CEC_ST_TX_START_BIT_LOW_CUSTOM,
/* Drive CEC high for the start bit using the custom timing */
CEC_ST_TX_START_BIT_HIGH_CUSTOM,
/* Drive CEC low for the 0 bit */
CEC_ST_TX_DATA_BIT_0_LOW,
/* Drive CEC high for the 0 bit */
CEC_ST_TX_DATA_BIT_0_HIGH,
/* Generate a bit period that is too short */
CEC_ST_TX_DATA_BIT_0_HIGH_SHORT,
/* Generate a bit period that is too long */
CEC_ST_TX_DATA_BIT_0_HIGH_LONG,
/* Drive CEC low for the 1 bit */
CEC_ST_TX_DATA_BIT_1_LOW,
/* Drive CEC high for the 1 bit */
CEC_ST_TX_DATA_BIT_1_HIGH,
/* Generate a bit period that is too short */
CEC_ST_TX_DATA_BIT_1_HIGH_SHORT,
/* Generate a bit period that is too long */
CEC_ST_TX_DATA_BIT_1_HIGH_LONG,
/*
* Wait for start of sample time to check for Ack bit or first
* four initiator bits to check for Arbitration Lost.
*/
CEC_ST_TX_DATA_BIT_1_HIGH_PRE_SAMPLE,
/* Wait for end of bit period after sampling */
CEC_ST_TX_DATA_BIT_1_HIGH_POST_SAMPLE,
/* Generate a bit period that is too short */
CEC_ST_TX_DATA_BIT_1_HIGH_POST_SAMPLE_SHORT,
/* Generate a bit period that is too long */
CEC_ST_TX_DATA_BIT_1_HIGH_POST_SAMPLE_LONG,
/* Drive CEC low for a data bit using the custom timing */
CEC_ST_TX_DATA_BIT_LOW_CUSTOM,
/* Drive CEC high for a data bit using the custom timing */
CEC_ST_TX_DATA_BIT_HIGH_CUSTOM,
/* Drive CEC low for a standalone pulse using the custom timing */
CEC_ST_TX_PULSE_LOW_CUSTOM,
/* Drive CEC high for a standalone pulse using the custom timing */
CEC_ST_TX_PULSE_HIGH_CUSTOM,
/* Start low drive */
CEC_ST_TX_LOW_DRIVE,
/* Rx states */
/* Start bit low detected */
CEC_ST_RX_START_BIT_LOW,
/* Start bit high detected */
CEC_ST_RX_START_BIT_HIGH,
/* Wait for bit sample time */
CEC_ST_RX_DATA_SAMPLE,
/* Wait for earliest end of bit period after sampling */
CEC_ST_RX_DATA_POST_SAMPLE,
/* Wait for CEC to go low (i.e. end of bit period) */
CEC_ST_RX_DATA_WAIT_FOR_LOW,
/* Drive CEC low to send 0 Ack bit */
CEC_ST_RX_ACK_LOW,
/* End of 0 Ack time, wait for earliest end of bit period */
CEC_ST_RX_ACK_LOW_POST,
/* Wait for CEC to go high (i.e. end of bit period */
CEC_ST_RX_ACK_HIGH_POST,
/* Wait for earliest end of bit period and end of message */
CEC_ST_RX_ACK_FINISH,
/* Start low drive */
CEC_ST_RX_LOW_DRIVE,
/* Monitor pin using interrupts */
CEC_ST_RX_IRQ,
/* Total number of pin states */
CEC_PIN_STATES
};
/* Error Injection */
/* Error injection modes */
#define CEC_ERROR_INJ_MODE_OFF 0
#define CEC_ERROR_INJ_MODE_ONCE 1
#define CEC_ERROR_INJ_MODE_ALWAYS 2
#define CEC_ERROR_INJ_MODE_TOGGLE 3
#define CEC_ERROR_INJ_MODE_MASK 3ULL
/* Receive error injection options */
#define CEC_ERROR_INJ_RX_NACK_OFFSET 0
#define CEC_ERROR_INJ_RX_LOW_DRIVE_OFFSET 2
#define CEC_ERROR_INJ_RX_ADD_BYTE_OFFSET 4
#define CEC_ERROR_INJ_RX_REMOVE_BYTE_OFFSET 6
#define CEC_ERROR_INJ_RX_ARB_LOST_OFFSET 8
#define CEC_ERROR_INJ_RX_MASK 0xffffULL
/* Transmit error injection options */
#define CEC_ERROR_INJ_TX_NO_EOM_OFFSET 16
#define CEC_ERROR_INJ_TX_EARLY_EOM_OFFSET 18
#define CEC_ERROR_INJ_TX_SHORT_BIT_OFFSET 20
#define CEC_ERROR_INJ_TX_LONG_BIT_OFFSET 22
#define CEC_ERROR_INJ_TX_CUSTOM_BIT_OFFSET 24
#define CEC_ERROR_INJ_TX_SHORT_START_OFFSET 26
#define CEC_ERROR_INJ_TX_LONG_START_OFFSET 28
#define CEC_ERROR_INJ_TX_CUSTOM_START_OFFSET 30
#define CEC_ERROR_INJ_TX_LAST_BIT_OFFSET 32
#define CEC_ERROR_INJ_TX_ADD_BYTES_OFFSET 34
#define CEC_ERROR_INJ_TX_REMOVE_BYTE_OFFSET 36
#define CEC_ERROR_INJ_TX_LOW_DRIVE_OFFSET 38
#define CEC_ERROR_INJ_TX_MASK 0xffffffffffff0000ULL
#define CEC_ERROR_INJ_RX_LOW_DRIVE_ARG_IDX 0
#define CEC_ERROR_INJ_RX_ARB_LOST_ARG_IDX 1
#define CEC_ERROR_INJ_TX_ADD_BYTES_ARG_IDX 2
#define CEC_ERROR_INJ_TX_SHORT_BIT_ARG_IDX 3
#define CEC_ERROR_INJ_TX_LONG_BIT_ARG_IDX 4
#define CEC_ERROR_INJ_TX_CUSTOM_BIT_ARG_IDX 5
#define CEC_ERROR_INJ_TX_LAST_BIT_ARG_IDX 6
#define CEC_ERROR_INJ_TX_LOW_DRIVE_ARG_IDX 7
#define CEC_ERROR_INJ_NUM_ARGS 8
/* Special CEC op values */
#define CEC_ERROR_INJ_OP_ANY 0x00000100
/* The default for the low/high time of the custom pulse */
#define CEC_TIM_CUSTOM_DEFAULT 1000
/* The default for the low/high time of the glitch pulse */
#define CEC_TIM_GLITCH_DEFAULT 1
#define CEC_NUM_PIN_EVENTS 128
#define CEC_PIN_EVENT_FL_IS_HIGH (1 << 0)
#define CEC_PIN_EVENT_FL_DROPPED (1 << 1)
#define CEC_PIN_IRQ_UNCHANGED 0
#define CEC_PIN_IRQ_DISABLE 1
#define CEC_PIN_IRQ_ENABLE 2
struct cec_pin {
struct cec_adapter *adap;
const struct cec_pin_ops *ops;
struct task_struct *kthread;
wait_queue_head_t kthread_waitq;
struct hrtimer timer;
ktime_t ts;
unsigned int wait_usecs;
u16 la_mask;
bool monitor_all;
bool rx_eom;
bool enabled_irq;
bool enable_irq_failed;
enum cec_pin_state state;
struct cec_msg tx_msg;
u32 tx_bit;
bool tx_nacked;
u32 tx_signal_free_time;
bool tx_toggle;
struct cec_msg rx_msg;
u32 rx_bit;
bool rx_toggle;
u32 rx_start_bit_low_too_short_cnt;
u64 rx_start_bit_low_too_short_ts;
u32 rx_start_bit_low_too_short_delta;
u32 rx_start_bit_too_short_cnt;
u64 rx_start_bit_too_short_ts;
u32 rx_start_bit_too_short_delta;
u32 rx_start_bit_too_long_cnt;
u32 rx_data_bit_too_short_cnt;
u64 rx_data_bit_too_short_ts;
u32 rx_data_bit_too_short_delta;
u32 rx_data_bit_too_long_cnt;
u32 rx_low_drive_cnt;
struct cec_msg work_rx_msg;
u8 work_tx_status;
ktime_t work_tx_ts;
atomic_t work_irq_change;
atomic_t work_pin_num_events;
unsigned int work_pin_events_wr;
unsigned int work_pin_events_rd;
ktime_t work_pin_ts[CEC_NUM_PIN_EVENTS];
u8 work_pin_events[CEC_NUM_PIN_EVENTS];
bool work_pin_events_dropped;
u32 work_pin_events_dropped_cnt;
ktime_t timer_ts;
u32 timer_cnt;
u32 timer_100us_overruns;
u32 timer_300us_overruns;
u32 timer_max_overrun;
u32 timer_sum_overrun;
bool rx_no_low_drive;
u32 tx_custom_low_usecs;
u32 tx_custom_high_usecs;
u32 tx_glitch_low_usecs;
u32 tx_glitch_high_usecs;
bool tx_ignore_nack_until_eom;
bool tx_custom_pulse;
bool tx_generated_poll;
bool tx_post_eom;
bool tx_glitch_falling_edge;
bool tx_glitch_rising_edge;
u8 tx_extra_bytes;
u32 tx_low_drive_cnt;
#ifdef CONFIG_CEC_PIN_ERROR_INJ
u64 error_inj[CEC_ERROR_INJ_OP_ANY + 1];
u8 error_inj_args[CEC_ERROR_INJ_OP_ANY + 1][CEC_ERROR_INJ_NUM_ARGS];
#endif
};
void cec_pin_start_timer(struct cec_pin *pin);
#ifdef CONFIG_CEC_PIN_ERROR_INJ
bool cec_pin_error_inj_parse_line(struct cec_adapter *adap, char *line);
int cec_pin_error_inj_show(struct cec_adapter *adap, struct seq_file *sf);
u16 cec_pin_rx_error_inj(struct cec_pin *pin);
u16 cec_pin_tx_error_inj(struct cec_pin *pin);
#endif
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* cec-priv.h - HDMI Consumer Electronics Control internal header
*
* Copyright 2016 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#ifndef _CEC_PRIV_H
#define _CEC_PRIV_H
#include <linux/cec-funcs.h>
#include <media/cec-notifier.h>
#define dprintk(lvl, fmt, arg...) \
do { \
if (lvl <= cec_debug) \
pr_info("cec-%s: " fmt, adap->name, ## arg); \
} while (0)
#define call_op(adap, op, arg...) \
((adap->ops->op && !adap->devnode.unregistered) ? \
adap->ops->op(adap, ## arg) : 0)
#define call_void_op(adap, op, arg...) \
do { \
if (adap->ops->op && !adap->devnode.unregistered) \
adap->ops->op(adap, ## arg); \
} while (0)
/* devnode to cec_adapter */
#define to_cec_adapter(node) container_of(node, struct cec_adapter, devnode)
static inline bool msg_is_raw(const struct cec_msg *msg)
{
return msg->flags & CEC_MSG_FL_RAW;
}
/* cec-core.c */
extern int cec_debug;
/* cec-adap.c */
int cec_monitor_all_cnt_inc(struct cec_adapter *adap);
void cec_monitor_all_cnt_dec(struct cec_adapter *adap);
int cec_monitor_pin_cnt_inc(struct cec_adapter *adap);
void cec_monitor_pin_cnt_dec(struct cec_adapter *adap);
int cec_adap_status(struct seq_file *file, void *priv);
int cec_thread_func(void *_adap);
int cec_adap_enable(struct cec_adapter *adap);
void __cec_s_phys_addr(struct cec_adapter *adap, u16 phys_addr, bool block);
int __cec_s_log_addrs(struct cec_adapter *adap,
struct cec_log_addrs *log_addrs, bool block);
int cec_transmit_msg_fh(struct cec_adapter *adap, struct cec_msg *msg,
struct cec_fh *fh, bool block);
void cec_queue_event_fh(struct cec_fh *fh,
const struct cec_event *new_ev, u64 ts);
/* cec-api.c */
extern const struct file_operations cec_devnode_fops;
#endif
+25
View File
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: GPL-2.0
#
# I2C drivers
config CEC_CH7322
tristate "Chrontel CH7322 CEC controller"
depends on I2C
select REGMAP
select REGMAP_I2C
select CEC_CORE
help
This is a driver for the Chrontel CH7322 CEC controller. It uses the
generic CEC framework interface.
CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_NXP_TDA9950
tristate "NXP Semiconductors TDA9950/TDA998X HDMI CEC"
depends on I2C
select CEC_NOTIFIER
select CEC_CORE
default DRM_I2C_NXP_TDA998X
help
This is a driver for the NXP TDA9950 CEC controller and for the CEC
controller block integrated into several NXP TDA998x HDMI encoders.
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the CEC I2C device drivers.
#
obj-$(CONFIG_CEC_CH7322) += ch7322.o
obj-$(CONFIG_CEC_NXP_TDA9950) += tda9950.o
+602
View File
@@ -0,0 +1,602 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Driver for the Chrontel CH7322 CEC Controller
*
* Copyright 2020 Google LLC.
*/
/*
* Notes
*
* - This device powers on in Auto Mode which has limited functionality. This
* driver disables Auto Mode when it attaches.
*
*/
#include <linux/cec.h>
#include <linux/dmi.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/pci.h>
#include <linux/regmap.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
#define CH7322_WRITE 0x00
#define CH7322_WRITE_MSENT 0x80
#define CH7322_WRITE_BOK 0x40
#define CH7322_WRITE_NMASK 0x0f
/* Write buffer is 0x01-0x10 */
#define CH7322_WRBUF 0x01
#define CH7322_WRBUF_LEN 0x10
#define CH7322_READ 0x40
#define CH7322_READ_NRDT 0x80
#define CH7322_READ_MSENT 0x20
#define CH7322_READ_NMASK 0x0f
/* Read buffer is 0x41-0x50 */
#define CH7322_RDBUF 0x41
#define CH7322_RDBUF_LEN 0x10
#define CH7322_MODE 0x11
#define CH7322_MODE_AUTO 0x78
#define CH7322_MODE_SW 0xb5
#define CH7322_RESET 0x12
#define CH7322_RESET_RST 0x00
#define CH7322_POWER 0x13
#define CH7322_POWER_FPD 0x04
#define CH7322_CFG0 0x17
#define CH7322_CFG0_EOBEN 0x40
#define CH7322_CFG0_PEOB 0x20
#define CH7322_CFG0_CLRSPP 0x10
#define CH7322_CFG0_FLOW 0x08
#define CH7322_CFG1 0x1a
#define CH7322_CFG1_STDBYO 0x04
#define CH7322_CFG1_HPBP 0x02
#define CH7322_CFG1_PIO 0x01
#define CH7322_INTCTL 0x1b
#define CH7322_INTCTL_INTPB 0x80
#define CH7322_INTCTL_STDBY 0x40
#define CH7322_INTCTL_HPDFALL 0x20
#define CH7322_INTCTL_HPDRISE 0x10
#define CH7322_INTCTL_RXMSG 0x08
#define CH7322_INTCTL_TXMSG 0x04
#define CH7322_INTCTL_NEWPHA 0x02
#define CH7322_INTCTL_ERROR 0x01
#define CH7322_DVCLKFNH 0x1d
#define CH7322_DVCLKFNL 0x1e
#define CH7322_CTL 0x31
#define CH7322_CTL_FSTDBY 0x80
#define CH7322_CTL_PLSEN 0x40
#define CH7322_CTL_PLSPB 0x20
#define CH7322_CTL_SPADL 0x10
#define CH7322_CTL_HINIT 0x08
#define CH7322_CTL_WPHYA 0x04
#define CH7322_CTL_H1T 0x02
#define CH7322_CTL_S1T 0x01
#define CH7322_PAWH 0x32
#define CH7322_PAWL 0x33
#define CH7322_ADDLW 0x34
#define CH7322_ADDLW_MASK 0xf0
#define CH7322_ADDLR 0x3d
#define CH7322_ADDLR_HPD 0x80
#define CH7322_ADDLR_MASK 0x0f
#define CH7322_INTDATA 0x3e
#define CH7322_INTDATA_MODE 0x80
#define CH7322_INTDATA_STDBY 0x40
#define CH7322_INTDATA_HPDFALL 0x20
#define CH7322_INTDATA_HPDRISE 0x10
#define CH7322_INTDATA_RXMSG 0x08
#define CH7322_INTDATA_TXMSG 0x04
#define CH7322_INTDATA_NEWPHA 0x02
#define CH7322_INTDATA_ERROR 0x01
#define CH7322_EVENT 0x3f
#define CH7322_EVENT_TXERR 0x80
#define CH7322_EVENT_HRST 0x40
#define CH7322_EVENT_HFST 0x20
#define CH7322_EVENT_PHACHG 0x10
#define CH7322_EVENT_ACTST 0x08
#define CH7322_EVENT_PHARDY 0x04
#define CH7322_EVENT_BSOK 0x02
#define CH7322_EVENT_ERRADCF 0x01
#define CH7322_DID 0x51
#define CH7322_DID_CH7322 0x5b
#define CH7322_DID_CH7323 0x5f
#define CH7322_REVISIONID 0x52
#define CH7322_PARH 0x53
#define CH7322_PARL 0x54
#define CH7322_IOCFG2 0x75
#define CH7322_IOCFG_CIO 0x80
#define CH7322_IOCFG_IOCFGMASK 0x78
#define CH7322_IOCFG_AUDIO 0x04
#define CH7322_IOCFG_SPAMST 0x02
#define CH7322_IOCFG_SPAMSP 0x01
#define CH7322_CTL3 0x7b
#define CH7322_CTL3_SWENA 0x80
#define CH7322_CTL3_FC_INIT 0x40
#define CH7322_CTL3_SML_FL 0x20
#define CH7322_CTL3_SM_RDST 0x10
#define CH7322_CTL3_SPP_CIAH 0x08
#define CH7322_CTL3_SPP_CIAL 0x04
#define CH7322_CTL3_SPP_ACTH 0x02
#define CH7322_CTL3_SPP_ACTL 0x01
/* BOK status means NACK */
#define CH7322_TX_FLAG_NACK BIT(0)
/* Device will retry automatically */
#define CH7322_TX_FLAG_RETRY BIT(1)
struct ch7322 {
struct i2c_client *i2c;
struct regmap *regmap;
struct cec_adapter *cec;
struct mutex mutex; /* device access mutex */
u8 tx_flags;
};
static const struct regmap_config ch7322_regmap = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0x7f,
.disable_locking = true,
};
static int ch7322_send_message(struct ch7322 *ch7322, const struct cec_msg *msg)
{
unsigned int val;
unsigned int len = msg->len;
int ret;
int i;
WARN_ON(!mutex_is_locked(&ch7322->mutex));
if (len > CH7322_WRBUF_LEN || len < 1)
return -EINVAL;
ret = regmap_read(ch7322->regmap, CH7322_WRITE, &val);
if (ret)
return ret;
/* Buffer not ready */
if (!(val & CH7322_WRITE_MSENT))
return -EBUSY;
if (cec_msg_opcode(msg) == -1 &&
cec_msg_initiator(msg) == cec_msg_destination(msg)) {
ch7322->tx_flags = CH7322_TX_FLAG_NACK | CH7322_TX_FLAG_RETRY;
} else if (cec_msg_is_broadcast(msg)) {
ch7322->tx_flags = CH7322_TX_FLAG_NACK;
} else {
ch7322->tx_flags = CH7322_TX_FLAG_RETRY;
}
ret = regmap_write(ch7322->regmap, CH7322_WRITE, len - 1);
if (ret)
return ret;
for (i = 0; i < len; i++) {
ret = regmap_write(ch7322->regmap,
CH7322_WRBUF + i, msg->msg[i]);
if (ret)
return ret;
}
return 0;
}
static int ch7322_receive_message(struct ch7322 *ch7322, struct cec_msg *msg)
{
unsigned int val;
int ret = 0;
int i;
WARN_ON(!mutex_is_locked(&ch7322->mutex));
ret = regmap_read(ch7322->regmap, CH7322_READ, &val);
if (ret)
return ret;
/* Message not ready */
if (!(val & CH7322_READ_NRDT))
return -EIO;
msg->len = (val & CH7322_READ_NMASK) + 1;
/* Read entire RDBUF to clear state */
for (i = 0; i < CH7322_RDBUF_LEN; i++) {
ret = regmap_read(ch7322->regmap, CH7322_RDBUF + i, &val);
if (ret)
return ret;
msg->msg[i] = (u8)val;
}
return 0;
}
static void ch7322_tx_done(struct ch7322 *ch7322)
{
int ret;
unsigned int val;
u8 status, flags;
mutex_lock(&ch7322->mutex);
ret = regmap_read(ch7322->regmap, CH7322_WRITE, &val);
flags = ch7322->tx_flags;
mutex_unlock(&ch7322->mutex);
/*
* The device returns a one-bit OK status which usually means ACK but
* actually means NACK when sending a logical address query or a
* broadcast.
*/
if (ret)
status = CEC_TX_STATUS_ERROR;
else if ((val & CH7322_WRITE_BOK) && (flags & CH7322_TX_FLAG_NACK))
status = CEC_TX_STATUS_NACK;
else if (val & CH7322_WRITE_BOK)
status = CEC_TX_STATUS_OK;
else if (flags & CH7322_TX_FLAG_NACK)
status = CEC_TX_STATUS_OK;
else
status = CEC_TX_STATUS_NACK;
if (status == CEC_TX_STATUS_NACK && (flags & CH7322_TX_FLAG_RETRY))
status |= CEC_TX_STATUS_MAX_RETRIES;
cec_transmit_attempt_done(ch7322->cec, status);
}
static void ch7322_rx_done(struct ch7322 *ch7322)
{
struct cec_msg msg;
int ret;
mutex_lock(&ch7322->mutex);
ret = ch7322_receive_message(ch7322, &msg);
mutex_unlock(&ch7322->mutex);
if (ret)
dev_err(&ch7322->i2c->dev, "cec receive error: %d\n", ret);
else
cec_received_msg(ch7322->cec, &msg);
}
/*
* This device can either monitor the DDC lines to obtain the physical address
* or it can allow the host to program it. This driver lets the device obtain
* it.
*/
static void ch7322_phys_addr(struct ch7322 *ch7322)
{
unsigned int pah, pal;
int ret = 0;
mutex_lock(&ch7322->mutex);
ret |= regmap_read(ch7322->regmap, CH7322_PARH, &pah);
ret |= regmap_read(ch7322->regmap, CH7322_PARL, &pal);
mutex_unlock(&ch7322->mutex);
if (ret)
dev_err(&ch7322->i2c->dev, "phys addr error\n");
else
cec_s_phys_addr(ch7322->cec, pal | (pah << 8), false);
}
static irqreturn_t ch7322_irq(int irq, void *dev)
{
struct ch7322 *ch7322 = dev;
unsigned int data = 0;
mutex_lock(&ch7322->mutex);
regmap_read(ch7322->regmap, CH7322_INTDATA, &data);
regmap_write(ch7322->regmap, CH7322_INTDATA, data);
mutex_unlock(&ch7322->mutex);
if (data & CH7322_INTDATA_HPDFALL)
cec_phys_addr_invalidate(ch7322->cec);
if (data & CH7322_INTDATA_TXMSG)
ch7322_tx_done(ch7322);
if (data & CH7322_INTDATA_RXMSG)
ch7322_rx_done(ch7322);
if (data & CH7322_INTDATA_NEWPHA)
ch7322_phys_addr(ch7322);
if (data & CH7322_INTDATA_ERROR)
dev_dbg(&ch7322->i2c->dev, "unknown error\n");
return IRQ_HANDLED;
}
/* This device is always enabled */
static int ch7322_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
return 0;
}
static int ch7322_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
{
struct ch7322 *ch7322 = cec_get_drvdata(adap);
int ret;
mutex_lock(&ch7322->mutex);
ret = regmap_update_bits(ch7322->regmap, CH7322_ADDLW,
CH7322_ADDLW_MASK, log_addr << 4);
mutex_unlock(&ch7322->mutex);
return ret;
}
static int ch7322_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct ch7322 *ch7322 = cec_get_drvdata(adap);
int ret;
mutex_lock(&ch7322->mutex);
ret = ch7322_send_message(ch7322, msg);
mutex_unlock(&ch7322->mutex);
return ret;
}
static const struct cec_adap_ops ch7322_cec_adap_ops = {
.adap_enable = ch7322_cec_adap_enable,
.adap_log_addr = ch7322_cec_adap_log_addr,
.adap_transmit = ch7322_cec_adap_transmit,
};
#if IS_ENABLED(CONFIG_PCI) && IS_ENABLED(CONFIG_DMI)
struct ch7322_conn_match {
const char *dev_name;
const char *pci_name;
const char *port_name;
};
static struct ch7322_conn_match google_endeavour[] = {
{ "i2c-PRP0001:00", "0000:00:02.0", "Port B" },
{ "i2c-PRP0001:01", "0000:00:02.0", "Port C" },
{ },
};
static const struct dmi_system_id ch7322_dmi_table[] = {
{
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "Google"),
DMI_MATCH(DMI_BOARD_NAME, "Endeavour"),
},
.driver_data = google_endeavour,
},
{ },
};
/* Make a best-effort attempt to locate a matching HDMI port */
static int ch7322_get_port(struct i2c_client *client,
struct device **dev,
const char **port)
{
const struct dmi_system_id *system;
const struct ch7322_conn_match *conn;
*dev = NULL;
*port = NULL;
system = dmi_first_match(ch7322_dmi_table);
if (!system)
return 0;
for (conn = system->driver_data; conn->dev_name; conn++) {
if (!strcmp(dev_name(&client->dev), conn->dev_name)) {
struct device *d;
d = bus_find_device_by_name(&pci_bus_type, NULL,
conn->pci_name);
if (!d)
return -EPROBE_DEFER;
put_device(d);
*dev = d;
*port = conn->port_name;
return 0;
}
}
return 0;
}
#else
static int ch7322_get_port(struct i2c_client *client,
struct device **dev,
const char **port)
{
*dev = NULL;
*port = NULL;
return 0;
}
#endif
static int ch7322_probe(struct i2c_client *client)
{
struct device *hdmi_dev;
const char *port_name;
struct ch7322 *ch7322;
struct cec_notifier *notifier = NULL;
u32 caps = CEC_CAP_DEFAULTS;
int ret;
unsigned int val;
ret = ch7322_get_port(client, &hdmi_dev, &port_name);
if (ret)
return ret;
if (hdmi_dev)
caps |= CEC_CAP_CONNECTOR_INFO;
ch7322 = devm_kzalloc(&client->dev, sizeof(*ch7322), GFP_KERNEL);
if (!ch7322)
return -ENOMEM;
ch7322->regmap = devm_regmap_init_i2c(client, &ch7322_regmap);
if (IS_ERR(ch7322->regmap))
return PTR_ERR(ch7322->regmap);
ret = regmap_read(ch7322->regmap, CH7322_DID, &val);
if (ret)
return ret;
if (val != CH7322_DID_CH7322)
return -EOPNOTSUPP;
mutex_init(&ch7322->mutex);
ch7322->i2c = client;
ch7322->tx_flags = 0;
i2c_set_clientdata(client, ch7322);
/* Disable auto mode */
ret = regmap_write(ch7322->regmap, CH7322_MODE, CH7322_MODE_SW);
if (ret)
goto err_mutex;
/* Enable logical address register */
ret = regmap_update_bits(ch7322->regmap, CH7322_CTL,
CH7322_CTL_SPADL, CH7322_CTL_SPADL);
if (ret)
goto err_mutex;
ch7322->cec = cec_allocate_adapter(&ch7322_cec_adap_ops, ch7322,
dev_name(&client->dev),
caps, 1);
if (IS_ERR(ch7322->cec)) {
ret = PTR_ERR(ch7322->cec);
goto err_mutex;
}
ch7322->cec->adap_controls_phys_addr = true;
if (hdmi_dev) {
notifier = cec_notifier_cec_adap_register(hdmi_dev,
port_name,
ch7322->cec);
if (!notifier) {
ret = -ENOMEM;
goto err_cec;
}
}
/* Configure, mask, and clear interrupt */
ret = regmap_write(ch7322->regmap, CH7322_CFG1, 0);
if (ret)
goto err_notifier;
ret = regmap_write(ch7322->regmap, CH7322_INTCTL, CH7322_INTCTL_INTPB);
if (ret)
goto err_notifier;
ret = regmap_write(ch7322->regmap, CH7322_INTDATA, 0xff);
if (ret)
goto err_notifier;
/* If HPD is up read physical address */
ret = regmap_read(ch7322->regmap, CH7322_ADDLR, &val);
if (ret)
goto err_notifier;
if (val & CH7322_ADDLR_HPD)
ch7322_phys_addr(ch7322);
ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
ch7322_irq,
IRQF_ONESHOT | IRQF_TRIGGER_RISING,
client->name, ch7322);
if (ret)
goto err_notifier;
/* Unmask interrupt */
mutex_lock(&ch7322->mutex);
ret = regmap_write(ch7322->regmap, CH7322_INTCTL, 0xff);
mutex_unlock(&ch7322->mutex);
if (ret)
goto err_notifier;
ret = cec_register_adapter(ch7322->cec, &client->dev);
if (ret)
goto err_notifier;
dev_info(&client->dev, "device registered\n");
return 0;
err_notifier:
if (notifier)
cec_notifier_cec_adap_unregister(notifier, ch7322->cec);
err_cec:
cec_delete_adapter(ch7322->cec);
err_mutex:
mutex_destroy(&ch7322->mutex);
return ret;
}
static void ch7322_remove(struct i2c_client *client)
{
struct ch7322 *ch7322 = i2c_get_clientdata(client);
/* Mask interrupt */
mutex_lock(&ch7322->mutex);
regmap_write(ch7322->regmap, CH7322_INTCTL, CH7322_INTCTL_INTPB);
mutex_unlock(&ch7322->mutex);
cec_unregister_adapter(ch7322->cec);
mutex_destroy(&ch7322->mutex);
dev_info(&client->dev, "device unregistered\n");
}
static const struct of_device_id ch7322_of_match[] = {
{ .compatible = "chrontel,ch7322", },
{},
};
MODULE_DEVICE_TABLE(of, ch7322_of_match);
static struct i2c_driver ch7322_i2c_driver = {
.driver = {
.name = "ch7322",
.of_match_table = ch7322_of_match,
},
.probe = ch7322_probe,
.remove = ch7322_remove,
};
module_i2c_driver(ch7322_i2c_driver);
MODULE_DESCRIPTION("Chrontel CH7322 CEC Controller Driver");
MODULE_AUTHOR("Jeff Chase <jnchase@google.com>");
MODULE_LICENSE("GPL");
+507
View File
@@ -0,0 +1,507 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* TDA9950 Consumer Electronics Control driver
*
* The NXP TDA9950 implements the HDMI Consumer Electronics Control
* interface. The host interface is similar to a mailbox: the data
* registers starting at REG_CDR0 are written to send a command to the
* internal CPU, and replies are read from these registers.
*
* As the data registers represent a mailbox, they must be accessed
* as a single I2C transaction. See the TDA9950 data sheet for details.
*/
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/platform_data/tda9950.h>
#include <linux/slab.h>
#include <drm/drm_edid.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
enum {
REG_CSR = 0x00,
CSR_BUSY = BIT(7),
CSR_INT = BIT(6),
CSR_ERR = BIT(5),
REG_CER = 0x01,
REG_CVR = 0x02,
REG_CCR = 0x03,
CCR_RESET = BIT(7),
CCR_ON = BIT(6),
REG_ACKH = 0x04,
REG_ACKL = 0x05,
REG_CCONR = 0x06,
CCONR_ENABLE_ERROR = BIT(4),
CCONR_RETRY_MASK = 7,
REG_CDR0 = 0x07,
CDR1_REQ = 0x00,
CDR1_CNF = 0x01,
CDR1_IND = 0x81,
CDR1_ERR = 0x82,
CDR1_IER = 0x83,
CDR2_CNF_SUCCESS = 0x00,
CDR2_CNF_OFF_STATE = 0x80,
CDR2_CNF_BAD_REQ = 0x81,
CDR2_CNF_CEC_ACCESS = 0x82,
CDR2_CNF_ARB_ERROR = 0x83,
CDR2_CNF_BAD_TIMING = 0x84,
CDR2_CNF_NACK_ADDR = 0x85,
CDR2_CNF_NACK_DATA = 0x86,
};
struct tda9950_priv {
struct i2c_client *client;
struct device *hdmi;
struct cec_adapter *adap;
struct tda9950_glue *glue;
u16 addresses;
struct cec_msg rx_msg;
struct cec_notifier *notify;
bool open;
};
static int tda9950_write_range(struct i2c_client *client, u8 addr, u8 *p, int cnt)
{
struct i2c_msg msg;
u8 buf[CEC_MAX_MSG_SIZE + 3];
int ret;
if (WARN_ON(cnt > sizeof(buf) - 1))
return -EINVAL;
buf[0] = addr;
memcpy(buf + 1, p, cnt);
msg.addr = client->addr;
msg.flags = 0;
msg.len = cnt + 1;
msg.buf = buf;
dev_dbg(&client->dev, "wr 0x%02x: %*ph\n", addr, cnt, p);
ret = i2c_transfer(client->adapter, &msg, 1);
if (ret < 0)
dev_err(&client->dev, "Error %d writing to cec:0x%x\n", ret, addr);
return ret < 0 ? ret : 0;
}
static void tda9950_write(struct i2c_client *client, u8 addr, u8 val)
{
tda9950_write_range(client, addr, &val, 1);
}
static int tda9950_read_range(struct i2c_client *client, u8 addr, u8 *p, int cnt)
{
struct i2c_msg msg[2];
int ret;
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 1;
msg[0].buf = &addr;
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].len = cnt;
msg[1].buf = p;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret < 0)
dev_err(&client->dev, "Error %d reading from cec:0x%x\n", ret, addr);
dev_dbg(&client->dev, "rd 0x%02x: %*ph\n", addr, cnt, p);
return ret;
}
static u8 tda9950_read(struct i2c_client *client, u8 addr)
{
int ret;
u8 val;
ret = tda9950_read_range(client, addr, &val, 1);
if (ret < 0)
val = 0;
return val;
}
static irqreturn_t tda9950_irq(int irq, void *data)
{
struct tda9950_priv *priv = data;
unsigned int tx_status;
u8 csr, cconr, buf[19];
u8 arb_lost_cnt, nack_cnt, err_cnt;
if (!priv->open)
return IRQ_NONE;
csr = tda9950_read(priv->client, REG_CSR);
if (!(csr & CSR_INT))
return IRQ_NONE;
cconr = tda9950_read(priv->client, REG_CCONR) & CCONR_RETRY_MASK;
tda9950_read_range(priv->client, REG_CDR0, buf, sizeof(buf));
/*
* This should never happen: the data sheet says that there will
* always be a valid message if the interrupt line is asserted.
*/
if (buf[0] == 0) {
dev_warn(&priv->client->dev, "interrupt pending, but no message?\n");
return IRQ_NONE;
}
switch (buf[1]) {
case CDR1_CNF: /* transmit result */
arb_lost_cnt = nack_cnt = err_cnt = 0;
switch (buf[2]) {
case CDR2_CNF_SUCCESS:
tx_status = CEC_TX_STATUS_OK;
break;
case CDR2_CNF_ARB_ERROR:
tx_status = CEC_TX_STATUS_ARB_LOST;
arb_lost_cnt = cconr;
break;
case CDR2_CNF_NACK_ADDR:
tx_status = CEC_TX_STATUS_NACK;
nack_cnt = cconr;
break;
default: /* some other error, refer to TDA9950 docs */
dev_err(&priv->client->dev, "CNF reply error 0x%02x\n",
buf[2]);
tx_status = CEC_TX_STATUS_ERROR;
err_cnt = cconr;
break;
}
/* TDA9950 executes all retries for us */
if (tx_status != CEC_TX_STATUS_OK)
tx_status |= CEC_TX_STATUS_MAX_RETRIES;
cec_transmit_done(priv->adap, tx_status, arb_lost_cnt,
nack_cnt, 0, err_cnt);
break;
case CDR1_IND:
priv->rx_msg.len = buf[0] - 2;
if (priv->rx_msg.len > CEC_MAX_MSG_SIZE)
priv->rx_msg.len = CEC_MAX_MSG_SIZE;
memcpy(priv->rx_msg.msg, buf + 2, priv->rx_msg.len);
cec_received_msg(priv->adap, &priv->rx_msg);
break;
default: /* unknown */
dev_err(&priv->client->dev, "unknown service id 0x%02x\n",
buf[1]);
break;
}
return IRQ_HANDLED;
}
static int tda9950_cec_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct tda9950_priv *priv = adap->priv;
u8 buf[CEC_MAX_MSG_SIZE + 2];
buf[0] = 2 + msg->len;
buf[1] = CDR1_REQ;
memcpy(buf + 2, msg->msg, msg->len);
if (attempts > 5)
attempts = 5;
tda9950_write(priv->client, REG_CCONR, attempts);
return tda9950_write_range(priv->client, REG_CDR0, buf, 2 + msg->len);
}
static int tda9950_cec_adap_log_addr(struct cec_adapter *adap, u8 addr)
{
struct tda9950_priv *priv = adap->priv;
u16 addresses;
u8 buf[2];
if (addr == CEC_LOG_ADDR_INVALID)
addresses = priv->addresses = 0;
else
addresses = priv->addresses |= BIT(addr);
/* TDA9950 doesn't want address 15 set */
addresses &= 0x7fff;
buf[0] = addresses >> 8;
buf[1] = addresses;
return tda9950_write_range(priv->client, REG_ACKH, buf, 2);
}
/*
* When operating as part of the TDA998x, we need additional handling
* to initialise and shut down the TDA9950 part of the device. These
* two hooks are provided to allow the TDA998x code to perform those
* activities.
*/
static int tda9950_glue_open(struct tda9950_priv *priv)
{
int ret = 0;
if (priv->glue && priv->glue->open)
ret = priv->glue->open(priv->glue->data);
priv->open = true;
return ret;
}
static void tda9950_glue_release(struct tda9950_priv *priv)
{
priv->open = false;
if (priv->glue && priv->glue->release)
priv->glue->release(priv->glue->data);
}
static int tda9950_open(struct tda9950_priv *priv)
{
struct i2c_client *client = priv->client;
int ret;
ret = tda9950_glue_open(priv);
if (ret)
return ret;
/* Reset the TDA9950, and wait 250ms for it to recover */
tda9950_write(client, REG_CCR, CCR_RESET);
msleep(250);
tda9950_cec_adap_log_addr(priv->adap, CEC_LOG_ADDR_INVALID);
/* Start the command processor */
tda9950_write(client, REG_CCR, CCR_ON);
return 0;
}
static void tda9950_release(struct tda9950_priv *priv)
{
struct i2c_client *client = priv->client;
int timeout = 50;
u8 csr;
/* Stop the command processor */
tda9950_write(client, REG_CCR, 0);
/* Wait up to .5s for it to signal non-busy */
do {
csr = tda9950_read(client, REG_CSR);
if (!(csr & CSR_BUSY) || !--timeout)
break;
msleep(10);
} while (1);
/* Warn the user that their IRQ may die if it's shared. */
if (csr & CSR_BUSY)
dev_warn(&client->dev, "command processor failed to stop, irq%d may die (csr=0x%02x)\n",
client->irq, csr);
tda9950_glue_release(priv);
}
static int tda9950_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct tda9950_priv *priv = adap->priv;
if (!enable) {
tda9950_release(priv);
return 0;
} else {
return tda9950_open(priv);
}
}
static const struct cec_adap_ops tda9950_cec_ops = {
.adap_enable = tda9950_cec_adap_enable,
.adap_log_addr = tda9950_cec_adap_log_addr,
.adap_transmit = tda9950_cec_transmit,
};
/*
* When operating as part of the TDA998x, we need to claim additional
* resources. These two hooks permit the management of those resources.
*/
static void tda9950_devm_glue_exit(void *data)
{
struct tda9950_glue *glue = data;
if (glue && glue->exit)
glue->exit(glue->data);
}
static int tda9950_devm_glue_init(struct device *dev, struct tda9950_glue *glue)
{
int ret;
if (glue && glue->init) {
ret = glue->init(glue->data);
if (ret)
return ret;
}
ret = devm_add_action(dev, tda9950_devm_glue_exit, glue);
if (ret)
tda9950_devm_glue_exit(glue);
return ret;
}
static void tda9950_cec_del(void *data)
{
struct tda9950_priv *priv = data;
cec_delete_adapter(priv->adap);
}
static int tda9950_probe(struct i2c_client *client)
{
struct tda9950_glue *glue = client->dev.platform_data;
struct device *dev = &client->dev;
struct tda9950_priv *priv;
unsigned long irqflags;
int ret;
u8 cvr;
/*
* We must have I2C functionality: our multi-byte accesses
* must be performed as a single contiguous transaction.
*/
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
dev_err(&client->dev,
"adapter does not support I2C functionality\n");
return -ENXIO;
}
/* We must have an interrupt to be functional. */
if (client->irq <= 0) {
dev_err(&client->dev, "driver requires an interrupt\n");
return -ENXIO;
}
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->client = client;
priv->glue = glue;
i2c_set_clientdata(client, priv);
/*
* If we're part of a TDA998x, we want the class devices to be
* associated with the HDMI Tx so we have a tight relationship
* between the HDMI interface and the CEC interface.
*/
priv->hdmi = dev;
if (glue && glue->parent)
priv->hdmi = glue->parent;
priv->adap = cec_allocate_adapter(&tda9950_cec_ops, priv, "tda9950",
CEC_CAP_DEFAULTS |
CEC_CAP_CONNECTOR_INFO,
CEC_MAX_LOG_ADDRS);
if (IS_ERR(priv->adap))
return PTR_ERR(priv->adap);
ret = devm_add_action(dev, tda9950_cec_del, priv);
if (ret) {
cec_delete_adapter(priv->adap);
return ret;
}
ret = tda9950_devm_glue_init(dev, glue);
if (ret)
return ret;
ret = tda9950_glue_open(priv);
if (ret)
return ret;
cvr = tda9950_read(client, REG_CVR);
dev_info(&client->dev,
"TDA9950 CEC interface, hardware version %u.%u\n",
cvr >> 4, cvr & 15);
tda9950_glue_release(priv);
irqflags = IRQF_TRIGGER_FALLING;
if (glue)
irqflags = glue->irq_flags;
ret = devm_request_threaded_irq(dev, client->irq, NULL, tda9950_irq,
irqflags | IRQF_SHARED | IRQF_ONESHOT,
dev_name(&client->dev), priv);
if (ret < 0)
return ret;
priv->notify = cec_notifier_cec_adap_register(priv->hdmi, NULL,
priv->adap);
if (!priv->notify)
return -ENOMEM;
ret = cec_register_adapter(priv->adap, priv->hdmi);
if (ret < 0) {
cec_notifier_cec_adap_unregister(priv->notify, priv->adap);
return ret;
}
/*
* CEC documentation says we must not call cec_delete_adapter
* after a successful call to cec_register_adapter().
*/
devm_remove_action(dev, tda9950_cec_del, priv);
return 0;
}
static void tda9950_remove(struct i2c_client *client)
{
struct tda9950_priv *priv = i2c_get_clientdata(client);
cec_notifier_cec_adap_unregister(priv->notify, priv->adap);
cec_unregister_adapter(priv->adap);
}
static struct i2c_device_id tda9950_ids[] = {
{ .name = "tda9950" },
{ }
};
MODULE_DEVICE_TABLE(i2c, tda9950_ids);
static struct i2c_driver tda9950_driver = {
.probe = tda9950_probe,
.remove = tda9950_remove,
.driver = {
.name = "tda9950",
},
.id_table = tda9950_ids,
};
module_i2c_driver(tda9950_driver);
MODULE_AUTHOR("Russell King <rmk+kernel@armlinux.org.uk>");
MODULE_DESCRIPTION("TDA9950/TDA998x Consumer Electronics Control Driver");
MODULE_LICENSE("GPL v2");
@@ -0,0 +1,120 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Platform drivers
config CEC_CROS_EC
tristate "ChromeOS EC CEC driver"
depends on CROS_EC
select CEC_CORE
select CEC_NOTIFIER
select CROS_EC_PROTO
help
If you say yes here you will get support for the
ChromeOS Embedded Controller's CEC.
The CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_MESON_AO
tristate "Amlogic Meson AO CEC driver"
depends on ARCH_MESON || COMPILE_TEST
select CEC_CORE
select CEC_NOTIFIER
help
This is a driver for Amlogic Meson SoCs AO CEC interface. It uses the
generic CEC framework interface.
CEC bus is present in the HDMI connector and enables communication
config CEC_MESON_G12A_AO
tristate "Amlogic Meson G12A AO CEC driver"
depends on ARCH_MESON || COMPILE_TEST
depends on COMMON_CLK && OF
select REGMAP
select REGMAP_MMIO
select CEC_CORE
select CEC_NOTIFIER
help
This is a driver for Amlogic Meson G12A SoCs AO CEC interface.
This driver if for the new AO-CEC module found in G12A SoCs,
usually named AO_CEC_B in documentation.
It uses the generic CEC framework interface.
CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_GPIO
tristate "Generic GPIO-based CEC driver"
depends on PREEMPTION || COMPILE_TEST
select CEC_CORE
select CEC_PIN
select CEC_NOTIFIER
select GPIOLIB
help
This is a generic GPIO-based CEC driver.
The CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_SAMSUNG_S5P
tristate "Samsung S5P CEC driver"
depends on ARCH_EXYNOS || COMPILE_TEST
select CEC_CORE
select CEC_NOTIFIER
help
This is a driver for Samsung S5P HDMI CEC interface. It uses the
generic CEC framework interface.
CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_STI
tristate "STMicroelectronics STiH4xx HDMI CEC driver"
depends on ARCH_STI || COMPILE_TEST
select CEC_CORE
select CEC_NOTIFIER
help
This is a driver for STIH4xx HDMI CEC interface. It uses the
generic CEC framework interface.
CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_STM32
tristate "STMicroelectronics STM32 HDMI CEC driver"
depends on ARCH_STM32 || COMPILE_TEST
select REGMAP
select REGMAP_MMIO
select CEC_CORE
help
This is a driver for STM32 interface. It uses the
generic CEC framework interface.
CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_TEGRA
tristate "Tegra HDMI CEC driver"
depends on ARCH_TEGRA || COMPILE_TEST
select CEC_CORE
select CEC_NOTIFIER
help
This is a driver for the Tegra HDMI CEC interface. It uses the
generic CEC framework interface.
The CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_SECO
tristate "SECO Boards HDMI CEC driver"
depends on X86 || (COMPILE_TEST && HAS_IOPORT)
depends on PCI && DMI
select CEC_CORE
select CEC_NOTIFIER
help
This is a driver for SECO Boards integrated CEC interface.
Selecting it will enable support for this device.
CEC bus is present in the HDMI connector and enables communication
between compatible devices.
config CEC_SECO_RC
bool "SECO Boards IR RC5 support"
depends on CEC_SECO
depends on RC_CORE=y || RC_CORE = CEC_SECO
help
If you say yes here you will get support for the
SECO Boards Consumer-IR in seco-cec driver.
The embedded controller supports RC5 protocol only, default mapping
is set to rc-hauppauge.
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the CEC platform device drivers.
#
# Please keep it in alphabetic order
obj-$(CONFIG_CEC_CROS_EC) += cros-ec/
obj-$(CONFIG_CEC_GPIO) += cec-gpio/
obj-y += meson/
obj-$(CONFIG_CEC_SAMSUNG_S5P) += s5p/
obj-$(CONFIG_CEC_SECO) += seco/
obj-$(CONFIG_CEC_STI) += sti/
obj-$(CONFIG_CEC_STM32) += stm32/
obj-$(CONFIG_CEC_TEGRA) += tegra/
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_GPIO) += cec-gpio.o
@@ -0,0 +1,296 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright 2017 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/seq_file.h>
#include <media/cec-notifier.h>
#include <media/cec-pin.h>
struct cec_gpio {
struct cec_adapter *adap;
struct cec_notifier *notifier;
struct device *dev;
struct gpio_desc *cec_gpio;
int cec_irq;
bool cec_is_low;
struct gpio_desc *hpd_gpio;
int hpd_irq;
bool hpd_is_high;
ktime_t hpd_ts;
struct gpio_desc *v5_gpio;
int v5_irq;
bool v5_is_high;
ktime_t v5_ts;
};
static int cec_gpio_read(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
if (cec->cec_is_low)
return 0;
return gpiod_get_value(cec->cec_gpio);
}
static void cec_gpio_high(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
if (!cec->cec_is_low)
return;
cec->cec_is_low = false;
gpiod_set_value(cec->cec_gpio, 1);
}
static void cec_gpio_low(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
if (cec->cec_is_low)
return;
cec->cec_is_low = true;
gpiod_set_value(cec->cec_gpio, 0);
}
static irqreturn_t cec_gpio_5v_irq_handler_thread(int irq, void *priv)
{
struct cec_gpio *cec = priv;
int val = gpiod_get_value_cansleep(cec->v5_gpio);
bool is_high = val > 0;
if (val < 0 || is_high == cec->v5_is_high)
return IRQ_HANDLED;
cec->v5_is_high = is_high;
cec_queue_pin_5v_event(cec->adap, cec->v5_is_high, cec->v5_ts);
return IRQ_HANDLED;
}
static irqreturn_t cec_gpio_5v_irq_handler(int irq, void *priv)
{
struct cec_gpio *cec = priv;
cec->v5_ts = ktime_get();
return IRQ_WAKE_THREAD;
}
static irqreturn_t cec_gpio_hpd_irq_handler_thread(int irq, void *priv)
{
struct cec_gpio *cec = priv;
int val = gpiod_get_value_cansleep(cec->hpd_gpio);
bool is_high = val > 0;
if (val < 0 || is_high == cec->hpd_is_high)
return IRQ_HANDLED;
cec->hpd_is_high = is_high;
cec_queue_pin_hpd_event(cec->adap, cec->hpd_is_high, cec->hpd_ts);
return IRQ_HANDLED;
}
static irqreturn_t cec_gpio_hpd_irq_handler(int irq, void *priv)
{
struct cec_gpio *cec = priv;
cec->hpd_ts = ktime_get();
return IRQ_WAKE_THREAD;
}
static irqreturn_t cec_gpio_cec_irq_handler(int irq, void *priv)
{
struct cec_gpio *cec = priv;
int val = gpiod_get_value(cec->cec_gpio);
if (val >= 0)
cec_pin_changed(cec->adap, val > 0);
return IRQ_HANDLED;
}
static bool cec_gpio_cec_enable_irq(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
enable_irq(cec->cec_irq);
return true;
}
static void cec_gpio_cec_disable_irq(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
disable_irq(cec->cec_irq);
}
static void cec_gpio_status(struct cec_adapter *adap, struct seq_file *file)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
seq_printf(file, "mode: %s\n", cec->cec_is_low ? "low-drive" : "read");
seq_printf(file, "using irq: %d\n", cec->cec_irq);
if (cec->hpd_gpio)
seq_printf(file, "hpd: %s\n",
cec->hpd_is_high ? "high" : "low");
if (cec->v5_gpio)
seq_printf(file, "5V: %s\n",
cec->v5_is_high ? "high" : "low");
}
static int cec_gpio_read_hpd(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
if (!cec->hpd_gpio)
return -ENOTTY;
return gpiod_get_value_cansleep(cec->hpd_gpio);
}
static int cec_gpio_read_5v(struct cec_adapter *adap)
{
struct cec_gpio *cec = cec_get_drvdata(adap);
if (!cec->v5_gpio)
return -ENOTTY;
return gpiod_get_value_cansleep(cec->v5_gpio);
}
static const struct cec_pin_ops cec_gpio_pin_ops = {
.read = cec_gpio_read,
.low = cec_gpio_low,
.high = cec_gpio_high,
.enable_irq = cec_gpio_cec_enable_irq,
.disable_irq = cec_gpio_cec_disable_irq,
.status = cec_gpio_status,
.read_hpd = cec_gpio_read_hpd,
.read_5v = cec_gpio_read_5v,
};
static int cec_gpio_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device *hdmi_dev;
struct cec_gpio *cec;
u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_MONITOR_ALL | CEC_CAP_MONITOR_PIN;
int ret;
hdmi_dev = cec_notifier_parse_hdmi_phandle(dev);
if (PTR_ERR(hdmi_dev) == -EPROBE_DEFER)
return PTR_ERR(hdmi_dev);
if (IS_ERR(hdmi_dev))
caps |= CEC_CAP_PHYS_ADDR;
cec = devm_kzalloc(dev, sizeof(*cec), GFP_KERNEL);
if (!cec)
return -ENOMEM;
cec->dev = dev;
cec->cec_gpio = devm_gpiod_get(dev, "cec", GPIOD_OUT_HIGH_OPEN_DRAIN);
if (IS_ERR(cec->cec_gpio))
return PTR_ERR(cec->cec_gpio);
cec->cec_irq = gpiod_to_irq(cec->cec_gpio);
cec->hpd_gpio = devm_gpiod_get_optional(dev, "hpd", GPIOD_IN);
if (IS_ERR(cec->hpd_gpio))
return PTR_ERR(cec->hpd_gpio);
cec->v5_gpio = devm_gpiod_get_optional(dev, "v5", GPIOD_IN);
if (IS_ERR(cec->v5_gpio))
return PTR_ERR(cec->v5_gpio);
cec->adap = cec_pin_allocate_adapter(&cec_gpio_pin_ops,
cec, pdev->name, caps);
if (IS_ERR(cec->adap))
return PTR_ERR(cec->adap);
ret = devm_request_irq(dev, cec->cec_irq, cec_gpio_cec_irq_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_NO_AUTOEN,
cec->adap->name, cec);
if (ret)
goto del_adap;
if (cec->hpd_gpio) {
cec->hpd_irq = gpiod_to_irq(cec->hpd_gpio);
ret = devm_request_threaded_irq(dev, cec->hpd_irq,
cec_gpio_hpd_irq_handler,
cec_gpio_hpd_irq_handler_thread,
IRQF_ONESHOT |
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"hpd-gpio", cec);
if (ret)
goto del_adap;
}
if (cec->v5_gpio) {
cec->v5_irq = gpiod_to_irq(cec->v5_gpio);
ret = devm_request_threaded_irq(dev, cec->v5_irq,
cec_gpio_5v_irq_handler,
cec_gpio_5v_irq_handler_thread,
IRQF_ONESHOT |
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"v5-gpio", cec);
if (ret)
goto del_adap;
}
if (!IS_ERR(hdmi_dev)) {
cec->notifier = cec_notifier_cec_adap_register(hdmi_dev, NULL,
cec->adap);
if (!cec->notifier) {
ret = -ENOMEM;
goto del_adap;
}
}
ret = cec_register_adapter(cec->adap, &pdev->dev);
if (ret)
goto unreg_notifier;
platform_set_drvdata(pdev, cec);
return 0;
unreg_notifier:
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
del_adap:
cec_delete_adapter(cec->adap);
return ret;
}
static void cec_gpio_remove(struct platform_device *pdev)
{
struct cec_gpio *cec = platform_get_drvdata(pdev);
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
cec_unregister_adapter(cec->adap);
}
static const struct of_device_id cec_gpio_match[] = {
{
.compatible = "cec-gpio",
},
{},
};
MODULE_DEVICE_TABLE(of, cec_gpio_match);
static struct platform_driver cec_gpio_pdrv = {
.probe = cec_gpio_probe,
.remove = cec_gpio_remove,
.driver = {
.name = "cec-gpio",
.of_match_table = cec_gpio_match,
},
};
module_platform_driver(cec_gpio_pdrv);
MODULE_AUTHOR("Hans Verkuil <hverkuil@kernel.org>");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("CEC GPIO driver");
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_CROS_EC) += cros-ec-cec.o
@@ -0,0 +1,607 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* CEC driver for ChromeOS Embedded Controller
*
* Copyright (c) 2018 BayLibre, SAS
* Author: Neil Armstrong <narmstrong@baylibre.com>
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dmi.h>
#include <linux/pci.h>
#include <linux/cec.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/platform_data/cros_ec_commands.h>
#include <linux/platform_data/cros_ec_proto.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
#define DRV_NAME "cros-ec-cec"
/**
* struct cros_ec_cec_port - Driver data for a single EC CEC port
*
* @port_num: port number
* @adap: CEC adapter
* @notify: CEC notifier pointer
* @rx_msg: storage for a received message
* @cros_ec_cec: pointer to the parent struct
*/
struct cros_ec_cec_port {
int port_num;
struct cec_adapter *adap;
struct cec_notifier *notify;
struct cec_msg rx_msg;
struct cros_ec_cec *cros_ec_cec;
};
/**
* struct cros_ec_cec - Driver data for EC CEC
*
* @cros_ec: Pointer to EC device
* @notifier: Notifier info for responding to EC events
* @write_cmd_version: Highest supported version of EC_CMD_CEC_WRITE_MSG.
* @num_ports: Number of CEC ports
* @ports: Array of ports
*/
struct cros_ec_cec {
struct cros_ec_device *cros_ec;
struct notifier_block notifier;
int write_cmd_version;
int num_ports;
struct cros_ec_cec_port *ports[EC_CEC_MAX_PORTS];
};
static void cros_ec_cec_received_message(struct cros_ec_cec_port *port,
uint8_t *msg, uint8_t len)
{
if (len > CEC_MAX_MSG_SIZE)
len = CEC_MAX_MSG_SIZE;
port->rx_msg.len = len;
memcpy(port->rx_msg.msg, msg, len);
cec_received_msg(port->adap, &port->rx_msg);
}
static void handle_cec_message(struct cros_ec_cec *cros_ec_cec)
{
struct cros_ec_device *cros_ec = cros_ec_cec->cros_ec;
uint8_t *cec_message = cros_ec->event_data.data.cec_message;
unsigned int len = cros_ec->event_size;
struct cros_ec_cec_port *port;
/*
* There are two ways of receiving CEC messages:
* 1. Old EC firmware which only supports one port sends the data in a
* cec_message MKBP event.
* 2. New EC firmware which supports multiple ports uses
* EC_MKBP_CEC_HAVE_DATA to notify that data is ready and
* EC_CMD_CEC_READ_MSG to read it.
* Check that the EC only has one CEC port, and then we can assume the
* message is from port 0.
*/
if (cros_ec_cec->num_ports != 1) {
dev_err(cros_ec->dev,
"received cec_message on device with %d ports\n",
cros_ec_cec->num_ports);
return;
}
port = cros_ec_cec->ports[0];
cros_ec_cec_received_message(port, cec_message, len);
}
static void cros_ec_cec_read_message(struct cros_ec_cec_port *port)
{
struct cros_ec_device *cros_ec = port->cros_ec_cec->cros_ec;
struct ec_params_cec_read params = {
.port = port->port_num,
};
struct ec_response_cec_read response;
int ret;
ret = cros_ec_cmd(cros_ec, 0, EC_CMD_CEC_READ_MSG, &params,
sizeof(params), &response, sizeof(response));
if (ret < 0) {
dev_err(cros_ec->dev,
"error reading CEC message on EC: %d\n", ret);
return;
}
cros_ec_cec_received_message(port, response.msg, response.msg_len);
}
static void handle_cec_event(struct cros_ec_cec *cros_ec_cec)
{
struct cros_ec_device *cros_ec = cros_ec_cec->cros_ec;
uint32_t cec_events = cros_ec->event_data.data.cec_events;
uint32_t port_num = EC_MKBP_EVENT_CEC_GET_PORT(cec_events);
uint32_t events = EC_MKBP_EVENT_CEC_GET_EVENTS(cec_events);
struct cros_ec_cec_port *port;
if (port_num >= cros_ec_cec->num_ports) {
dev_err(cros_ec->dev,
"received CEC event for invalid port %d\n", port_num);
return;
}
port = cros_ec_cec->ports[port_num];
if (events & EC_MKBP_CEC_SEND_OK)
cec_transmit_attempt_done(port->adap, CEC_TX_STATUS_OK);
/* FW takes care of all retries, tell core to avoid more retries */
if (events & EC_MKBP_CEC_SEND_FAILED)
cec_transmit_attempt_done(port->adap,
CEC_TX_STATUS_MAX_RETRIES |
CEC_TX_STATUS_NACK);
if (events & EC_MKBP_CEC_HAVE_DATA)
cros_ec_cec_read_message(port);
}
static int cros_ec_cec_event(struct notifier_block *nb,
unsigned long queued_during_suspend,
void *_notify)
{
struct cros_ec_cec *cros_ec_cec;
struct cros_ec_device *cros_ec;
cros_ec_cec = container_of(nb, struct cros_ec_cec, notifier);
cros_ec = cros_ec_cec->cros_ec;
if (cros_ec->event_data.event_type == EC_MKBP_EVENT_CEC_EVENT) {
handle_cec_event(cros_ec_cec);
return NOTIFY_OK;
}
if (cros_ec->event_data.event_type == EC_MKBP_EVENT_CEC_MESSAGE) {
handle_cec_message(cros_ec_cec);
return NOTIFY_OK;
}
return NOTIFY_DONE;
}
static int cros_ec_cec_set_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
struct cros_ec_cec_port *port = adap->priv;
struct cros_ec_cec *cros_ec_cec = port->cros_ec_cec;
struct cros_ec_device *cros_ec = cros_ec_cec->cros_ec;
struct ec_params_cec_set params = {
.cmd = CEC_CMD_LOGICAL_ADDRESS,
.port = port->port_num,
.val = logical_addr,
};
int ret;
ret = cros_ec_cmd(cros_ec, 0, EC_CMD_CEC_SET, &params, sizeof(params),
NULL, 0);
if (ret < 0) {
dev_err(cros_ec->dev,
"error setting CEC logical address on EC: %d\n", ret);
return ret;
}
return 0;
}
static int cros_ec_cec_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *cec_msg)
{
struct cros_ec_cec_port *port = adap->priv;
struct cros_ec_cec *cros_ec_cec = port->cros_ec_cec;
struct cros_ec_device *cros_ec = cros_ec_cec->cros_ec;
struct ec_params_cec_write params;
struct ec_params_cec_write_v1 params_v1;
int ret;
if (cros_ec_cec->write_cmd_version == 0) {
memcpy(params.msg, cec_msg->msg, cec_msg->len);
ret = cros_ec_cmd(cros_ec, 0, EC_CMD_CEC_WRITE_MSG, &params,
cec_msg->len, NULL, 0);
} else {
params_v1.port = port->port_num;
params_v1.msg_len = cec_msg->len;
memcpy(params_v1.msg, cec_msg->msg, cec_msg->len);
ret = cros_ec_cmd(cros_ec, cros_ec_cec->write_cmd_version,
EC_CMD_CEC_WRITE_MSG, &params_v1,
sizeof(params_v1), NULL, 0);
}
if (ret < 0) {
dev_err(cros_ec->dev,
"error writing CEC msg on EC: %d\n", ret);
return ret;
}
return 0;
}
static int cros_ec_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct cros_ec_cec_port *port = adap->priv;
struct cros_ec_cec *cros_ec_cec = port->cros_ec_cec;
struct cros_ec_device *cros_ec = cros_ec_cec->cros_ec;
struct ec_params_cec_set params = {
.cmd = CEC_CMD_ENABLE,
.port = port->port_num,
.val = enable,
};
int ret;
ret = cros_ec_cmd(cros_ec, 0, EC_CMD_CEC_SET, &params, sizeof(params),
NULL, 0);
if (ret < 0) {
dev_err(cros_ec->dev,
"error %sabling CEC on EC: %d\n",
(enable ? "en" : "dis"), ret);
return ret;
}
return 0;
}
static const struct cec_adap_ops cros_ec_cec_ops = {
.adap_enable = cros_ec_cec_adap_enable,
.adap_log_addr = cros_ec_cec_set_log_addr,
.adap_transmit = cros_ec_cec_transmit,
};
#ifdef CONFIG_PM_SLEEP
static int cros_ec_cec_suspend(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct cros_ec_cec *cros_ec_cec = dev_get_drvdata(&pdev->dev);
if (device_may_wakeup(dev))
enable_irq_wake(cros_ec_cec->cros_ec->irq);
return 0;
}
static int cros_ec_cec_resume(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct cros_ec_cec *cros_ec_cec = dev_get_drvdata(&pdev->dev);
if (device_may_wakeup(dev))
disable_irq_wake(cros_ec_cec->cros_ec->irq);
return 0;
}
#endif
static SIMPLE_DEV_PM_OPS(cros_ec_cec_pm_ops,
cros_ec_cec_suspend, cros_ec_cec_resume);
#if IS_ENABLED(CONFIG_PCI) && IS_ENABLED(CONFIG_DMI)
/*
* Specify the DRM device name handling the HDMI output and the HDMI connector
* corresponding to each CEC port. The order of connectors must match the order
* in the EC (first connector is EC port 0, ...), and the number of connectors
* must match the number of ports in the EC (which can be queried using the
* EC_CMD_CEC_PORT_COUNT host command).
*/
struct cec_dmi_match {
const char *sys_vendor;
const char *product_name;
const char *devname;
const char *const *conns;
};
static const char *const port_b_conns[] = { "Port B", NULL };
static const char *const port_db_conns[] = { "Port D", "Port B", NULL };
static const char *const port_ba_conns[] = { "Port B", "Port A", NULL };
static const char *const port_ab_conns[] = { "Port A", "Port B", NULL };
static const char *const port_d_conns[] = { "Port D", NULL };
static const struct cec_dmi_match cec_dmi_match_table[] = {
/* Google Fizz */
{ "Google", "Fizz", "0000:00:02.0", port_b_conns },
/* Google Brask */
{ "Google", "Brask", "0000:00:02.0", port_b_conns },
/* Google Moli */
{ "Google", "Moli", "0000:00:02.0", port_b_conns },
/* Google Kinox */
{ "Google", "Kinox", "0000:00:02.0", port_b_conns },
/* Google Kuldax */
{ "Google", "Kuldax", "0000:00:02.0", port_b_conns },
/* Google Aurash */
{ "Google", "Aurash", "0000:00:02.0", port_b_conns },
/* Google Gladios */
{ "Google", "Gladios", "0000:00:02.0", port_b_conns },
/* Google Lisbon */
{ "Google", "Lisbon", "0000:00:02.0", port_b_conns },
/* Google Dibbi */
{ "Google", "Dibbi", "0000:00:02.0", port_db_conns },
/* Google Constitution */
{ "Google", "Constitution", "0000:00:02.0", port_ba_conns },
/* Google Boxy */
{ "Google", "Boxy", "0000:00:02.0", port_d_conns },
/* Google Taranza */
{ "Google", "Taranza", "0000:00:02.0", port_db_conns },
/* Google Dexi */
{ "Google", "Dexi", "0000:00:02.0", port_db_conns },
/* Google Dita */
{ "Google", "Dita", "0000:00:02.0", port_db_conns },
/* Google Dirks */
{ "Google", "Dirks", "0000:00:02.0", port_ab_conns },
/* Google Moxie */
{ "Google", "Moxie", "0000:00:02.0", port_b_conns },
/* Google Kulnex */
{ "Google", "Kulnex", "0000:00:02.0", port_b_conns },
/* Google Moxoe */
{ "Google", "Moxoe", "0000:00:02.0", port_b_conns },
/* Google Dirkson */
{ "Google", "Dirkson", "0000:00:02.0", port_ab_conns },
};
static struct device *cros_ec_cec_find_hdmi_dev(struct device *dev,
const char * const **conns)
{
int i;
for (i = 0 ; i < ARRAY_SIZE(cec_dmi_match_table) ; ++i) {
const struct cec_dmi_match *m = &cec_dmi_match_table[i];
if (dmi_match(DMI_SYS_VENDOR, m->sys_vendor) &&
dmi_match(DMI_PRODUCT_NAME, m->product_name)) {
struct device *d;
/* Find the device, bail out if not yet registered */
d = bus_find_device_by_name(&pci_bus_type, NULL,
m->devname);
if (!d)
return ERR_PTR(-EPROBE_DEFER);
put_device(d);
*conns = m->conns;
return d;
}
}
/* Hardware support must be added in the cec_dmi_match_table */
dev_warn(dev, "CEC notifier not configured for this hardware\n");
return ERR_PTR(-ENODEV);
}
#else
static struct device *cros_ec_cec_find_hdmi_dev(struct device *dev,
const char * const **conns)
{
return ERR_PTR(-ENODEV);
}
#endif
static int cros_ec_cec_get_num_ports(struct cros_ec_cec *cros_ec_cec)
{
struct ec_response_cec_port_count response;
int ret;
ret = cros_ec_cmd(cros_ec_cec->cros_ec, 0, EC_CMD_CEC_PORT_COUNT, NULL,
0, &response, sizeof(response));
if (ret < 0) {
/*
* Old EC firmware only supports one port and does not support
* the port count command, so fall back to assuming one port.
*/
cros_ec_cec->num_ports = 1;
return 0;
}
if (response.port_count == 0) {
dev_err(cros_ec_cec->cros_ec->dev,
"EC reports 0 CEC ports\n");
return -ENODEV;
}
if (response.port_count > EC_CEC_MAX_PORTS) {
dev_err(cros_ec_cec->cros_ec->dev,
"EC reports too many ports: %d\n", response.port_count);
return -EINVAL;
}
cros_ec_cec->num_ports = response.port_count;
return 0;
}
static int cros_ec_cec_get_write_cmd_version(struct cros_ec_cec *cros_ec_cec)
{
struct cros_ec_device *cros_ec = cros_ec_cec->cros_ec;
struct ec_params_get_cmd_versions_v1 params = {
.cmd = EC_CMD_CEC_WRITE_MSG,
};
struct ec_response_get_cmd_versions response;
int ret;
ret = cros_ec_cmd(cros_ec, 1, EC_CMD_GET_CMD_VERSIONS, &params,
sizeof(params), &response, sizeof(response));
if (ret < 0) {
dev_err(cros_ec->dev,
"error getting CEC write command version: %d\n", ret);
return ret;
}
if (response.version_mask & EC_VER_MASK(1)) {
cros_ec_cec->write_cmd_version = 1;
} else {
if (cros_ec_cec->num_ports != 1) {
dev_err(cros_ec->dev,
"v0 write command only supports 1 port, %d reported\n",
cros_ec_cec->num_ports);
return -EINVAL;
}
cros_ec_cec->write_cmd_version = 0;
}
return 0;
}
static int cros_ec_cec_init_port(struct device *dev,
struct cros_ec_cec *cros_ec_cec,
int port_num, struct device *hdmi_dev,
const char * const *conns)
{
struct cros_ec_cec_port *port;
int ret;
port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
if (!port)
return -ENOMEM;
port->cros_ec_cec = cros_ec_cec;
port->port_num = port_num;
port->adap = cec_allocate_adapter(&cros_ec_cec_ops, port, DRV_NAME,
CEC_CAP_DEFAULTS |
CEC_CAP_CONNECTOR_INFO, 1);
if (IS_ERR(port->adap))
return PTR_ERR(port->adap);
if (!conns[port_num]) {
dev_err(dev, "no conn for port %d\n", port_num);
ret = -ENODEV;
goto out_probe_adapter;
}
port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[port_num],
port->adap);
if (!port->notify) {
ret = -ENOMEM;
goto out_probe_adapter;
}
ret = cec_register_adapter(port->adap, dev);
if (ret < 0)
goto out_probe_notify;
cros_ec_cec->ports[port_num] = port;
return 0;
out_probe_notify:
cec_notifier_cec_adap_unregister(port->notify, port->adap);
out_probe_adapter:
cec_delete_adapter(port->adap);
return ret;
}
static int cros_ec_cec_probe(struct platform_device *pdev)
{
struct cros_ec_dev *ec_dev = dev_get_drvdata(pdev->dev.parent);
struct cros_ec_device *cros_ec = ec_dev->ec_dev;
struct cros_ec_cec *cros_ec_cec;
struct cros_ec_cec_port *port;
struct device *hdmi_dev;
const char * const *conns = NULL;
int ret;
hdmi_dev = cros_ec_cec_find_hdmi_dev(&pdev->dev, &conns);
if (IS_ERR(hdmi_dev))
return PTR_ERR(hdmi_dev);
cros_ec_cec = devm_kzalloc(&pdev->dev, sizeof(*cros_ec_cec),
GFP_KERNEL);
if (!cros_ec_cec)
return -ENOMEM;
platform_set_drvdata(pdev, cros_ec_cec);
cros_ec_cec->cros_ec = cros_ec;
device_init_wakeup(&pdev->dev, 1);
ret = cros_ec_cec_get_num_ports(cros_ec_cec);
if (ret)
return ret;
ret = cros_ec_cec_get_write_cmd_version(cros_ec_cec);
if (ret)
return ret;
for (int i = 0; i < cros_ec_cec->num_ports; i++) {
ret = cros_ec_cec_init_port(&pdev->dev, cros_ec_cec, i,
hdmi_dev, conns);
if (ret)
goto unregister_ports;
}
/* Get CEC events from the EC. */
cros_ec_cec->notifier.notifier_call = cros_ec_cec_event;
ret = blocking_notifier_chain_register(&cros_ec->event_notifier,
&cros_ec_cec->notifier);
if (ret) {
dev_err(&pdev->dev, "failed to register notifier\n");
goto unregister_ports;
}
return 0;
unregister_ports:
/*
* Unregister any adapters which have been registered. We don't add the
* port to the array until the adapter has been registered successfully,
* so any non-NULL ports must have been registered.
*/
for (int i = 0; i < cros_ec_cec->num_ports; i++) {
port = cros_ec_cec->ports[i];
if (!port)
break;
cec_notifier_cec_adap_unregister(port->notify, port->adap);
cec_unregister_adapter(port->adap);
}
return ret;
}
static void cros_ec_cec_remove(struct platform_device *pdev)
{
struct cros_ec_cec *cros_ec_cec = platform_get_drvdata(pdev);
struct device *dev = &pdev->dev;
struct cros_ec_cec_port *port;
int ret;
/*
* blocking_notifier_chain_unregister() only fails if the notifier isn't
* in the list. We know it was added to it by .probe(), so there should
* be no need for error checking. Be cautious and still check.
*/
ret = blocking_notifier_chain_unregister(
&cros_ec_cec->cros_ec->event_notifier,
&cros_ec_cec->notifier);
if (ret)
dev_err(dev, "failed to unregister notifier\n");
for (int i = 0; i < cros_ec_cec->num_ports; i++) {
port = cros_ec_cec->ports[i];
cec_notifier_cec_adap_unregister(port->notify, port->adap);
cec_unregister_adapter(port->adap);
}
}
static const struct platform_device_id cros_ec_cec_id[] = {
{ DRV_NAME, 0 },
{}
};
MODULE_DEVICE_TABLE(platform, cros_ec_cec_id);
static struct platform_driver cros_ec_cec_driver = {
.probe = cros_ec_cec_probe,
.remove = cros_ec_cec_remove,
.driver = {
.name = DRV_NAME,
.pm = &cros_ec_cec_pm_ops,
},
.id_table = cros_ec_cec_id,
};
module_platform_driver(cros_ec_cec_driver);
MODULE_DESCRIPTION("CEC driver for ChromeOS ECs");
MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_MESON_AO) += ao-cec.o
obj-$(CONFIG_CEC_MESON_G12A_AO) += ao-cec-g12a.o
@@ -0,0 +1,792 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Driver for Amlogic Meson AO CEC G12A Controller
*
* Copyright (C) 2017 Amlogic, Inc. All rights reserved
* Copyright (C) 2019 BayLibre, SAS
* Author: Neil Armstrong <narmstrong@baylibre.com>
*/
#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/reset.h>
#include <linux/slab.h>
#include <linux/regmap.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
#include <linux/clk-provider.h>
/* CEC Registers */
#define CECB_CLK_CNTL_REG0 0x00
#define CECB_CLK_CNTL_N1 GENMASK(11, 0)
#define CECB_CLK_CNTL_N2 GENMASK(23, 12)
#define CECB_CLK_CNTL_DUAL_EN BIT(28)
#define CECB_CLK_CNTL_OUTPUT_EN BIT(30)
#define CECB_CLK_CNTL_INPUT_EN BIT(31)
#define CECB_CLK_CNTL_REG1 0x04
#define CECB_CLK_CNTL_M1 GENMASK(11, 0)
#define CECB_CLK_CNTL_M2 GENMASK(23, 12)
#define CECB_CLK_CNTL_BYPASS_EN BIT(24)
/*
* [14:12] Filter_del. For glitch-filtering CEC line, ignore signal
* change pulse width < filter_del * T(filter_tick) * 3.
* [9:8] Filter_tick_sel: Select which periodical pulse for
* glitch-filtering CEC line signal.
* - 0=Use T(xtal)*3 = 125ns;
* - 1=Use once-per-1us pulse;
* - 2=Use once-per-10us pulse;
* - 3=Use once-per-100us pulse.
* [3] Sysclk_en. 0=Disable system clock; 1=Enable system clock.
* [2:1] cntl_clk
* - 0 = Disable clk (Power-off mode)
* - 1 = Enable gated clock (Normal mode)
* - 2 = Enable free-run clk (Debug mode)
* [0] SW_RESET 1=Apply reset; 0=No reset.
*/
#define CECB_GEN_CNTL_REG 0x08
#define CECB_GEN_CNTL_RESET BIT(0)
#define CECB_GEN_CNTL_CLK_DISABLE 0
#define CECB_GEN_CNTL_CLK_ENABLE 1
#define CECB_GEN_CNTL_CLK_ENABLE_DBG 2
#define CECB_GEN_CNTL_CLK_CTRL_MASK GENMASK(2, 1)
#define CECB_GEN_CNTL_SYS_CLK_EN BIT(3)
#define CECB_GEN_CNTL_FILTER_TICK_125NS 0
#define CECB_GEN_CNTL_FILTER_TICK_1US 1
#define CECB_GEN_CNTL_FILTER_TICK_10US 2
#define CECB_GEN_CNTL_FILTER_TICK_100US 3
#define CECB_GEN_CNTL_FILTER_TICK_SEL GENMASK(9, 8)
#define CECB_GEN_CNTL_FILTER_DEL GENMASK(14, 12)
/*
* [7:0] cec_reg_addr
* [15:8] cec_reg_wrdata
* [16] cec_reg_wr
* - 0 = Read
* - 1 = Write
* [31:24] cec_reg_rddata
*/
#define CECB_RW_REG 0x0c
#define CECB_RW_ADDR GENMASK(7, 0)
#define CECB_RW_WR_DATA GENMASK(15, 8)
#define CECB_RW_WRITE_EN BIT(16)
#define CECB_RW_BUS_BUSY BIT(23)
#define CECB_RW_RD_DATA GENMASK(31, 24)
/*
* [0] DONE Interrupt
* [1] End Of Message Interrupt
* [2] Not Acknowlegde Interrupt
* [3] Arbitration Loss Interrupt
* [4] Initiator Error Interrupt
* [5] Follower Error Interrupt
* [6] Wake-Up Interrupt
*/
#define CECB_INTR_MASKN_REG 0x10
#define CECB_INTR_CLR_REG 0x14
#define CECB_INTR_STAT_REG 0x18
#define CECB_INTR_DONE BIT(0)
#define CECB_INTR_EOM BIT(1)
#define CECB_INTR_NACK BIT(2)
#define CECB_INTR_ARB_LOSS BIT(3)
#define CECB_INTR_INITIATOR_ERR BIT(4)
#define CECB_INTR_FOLLOWER_ERR BIT(5)
#define CECB_INTR_WAKE_UP BIT(6)
/* CEC Commands */
#define CECB_CTRL 0x00
#define CECB_CTRL_SEND BIT(0)
#define CECB_CTRL_TYPE GENMASK(2, 1)
#define CECB_CTRL_TYPE_RETRY 0
#define CECB_CTRL_TYPE_NEW 1
#define CECB_CTRL_TYPE_NEXT 2
#define CECB_CTRL2 0x01
#define CECB_CTRL2_RISE_DEL_MAX GENMASK(4, 0)
#define CECB_INTR_MASK 0x02
#define CECB_LADD_LOW 0x05
#define CECB_LADD_HIGH 0x06
#define CECB_TX_CNT 0x07
#define CECB_RX_CNT 0x08
#define CECB_STAT0 0x09
#define CECB_TX_DATA00 0x10
#define CECB_TX_DATA01 0x11
#define CECB_TX_DATA02 0x12
#define CECB_TX_DATA03 0x13
#define CECB_TX_DATA04 0x14
#define CECB_TX_DATA05 0x15
#define CECB_TX_DATA06 0x16
#define CECB_TX_DATA07 0x17
#define CECB_TX_DATA08 0x18
#define CECB_TX_DATA09 0x19
#define CECB_TX_DATA10 0x1A
#define CECB_TX_DATA11 0x1B
#define CECB_TX_DATA12 0x1C
#define CECB_TX_DATA13 0x1D
#define CECB_TX_DATA14 0x1E
#define CECB_TX_DATA15 0x1F
#define CECB_RX_DATA00 0x20
#define CECB_RX_DATA01 0x21
#define CECB_RX_DATA02 0x22
#define CECB_RX_DATA03 0x23
#define CECB_RX_DATA04 0x24
#define CECB_RX_DATA05 0x25
#define CECB_RX_DATA06 0x26
#define CECB_RX_DATA07 0x27
#define CECB_RX_DATA08 0x28
#define CECB_RX_DATA09 0x29
#define CECB_RX_DATA10 0x2A
#define CECB_RX_DATA11 0x2B
#define CECB_RX_DATA12 0x2C
#define CECB_RX_DATA13 0x2D
#define CECB_RX_DATA14 0x2E
#define CECB_RX_DATA15 0x2F
#define CECB_LOCK_BUF 0x30
#define CECB_LOCK_BUF_EN BIT(0)
#define CECB_WAKEUPCTRL 0x31
struct meson_ao_cec_g12a_data {
/* Setup the internal CECB_CTRL2 register */
bool ctrl2_setup;
};
struct meson_ao_cec_g12a_device {
struct platform_device *pdev;
struct regmap *regmap;
struct regmap *regmap_cec;
spinlock_t cec_reg_lock;
struct cec_notifier *notify;
struct cec_adapter *adap;
struct cec_msg rx_msg;
struct clk *oscin;
struct clk *core;
const struct meson_ao_cec_g12a_data *data;
};
static const struct regmap_config meson_ao_cec_g12a_regmap_conf = {
.reg_bits = 8,
.val_bits = 32,
.reg_stride = 4,
.max_register = CECB_INTR_STAT_REG,
};
/*
* The AO-CECB embeds a dual/divider to generate a more precise
* 32,768KHz clock for CEC core clock.
* ______ ______
* | | | |
* ______ | Div1 |-| Cnt1 | ______
* | | /|______| |______|\ | |
* Xtal-->| Gate |---| ______ ______ X-X--| Gate |-->
* |______| | \| | | |/ | |______|
* | | Div2 |-| Cnt2 | |
* | |______| |______| |
* |_______________________|
*
* The dividing can be switched to single or dual, with a counter
* for each divider to set when the switching is done.
* The entire dividing mechanism can be also bypassed.
*/
struct meson_ao_cec_g12a_dualdiv_clk {
struct clk_hw hw;
struct regmap *regmap;
};
#define hw_to_meson_ao_cec_g12a_dualdiv_clk(_hw) \
container_of(_hw, struct meson_ao_cec_g12a_dualdiv_clk, hw) \
static unsigned long
meson_ao_cec_g12a_dualdiv_clk_recalc_rate(struct clk_hw *hw,
unsigned long parent_rate)
{
struct meson_ao_cec_g12a_dualdiv_clk *dualdiv_clk =
hw_to_meson_ao_cec_g12a_dualdiv_clk(hw);
unsigned long n1;
u32 reg0, reg1;
regmap_read(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0, &reg0);
regmap_read(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0, &reg1);
if (reg1 & CECB_CLK_CNTL_BYPASS_EN)
return parent_rate;
if (reg0 & CECB_CLK_CNTL_DUAL_EN) {
unsigned long n2, m1, m2, f1, f2, p1, p2;
n1 = FIELD_GET(CECB_CLK_CNTL_N1, reg0) + 1;
n2 = FIELD_GET(CECB_CLK_CNTL_N2, reg0) + 1;
m1 = FIELD_GET(CECB_CLK_CNTL_M1, reg1) + 1;
m2 = FIELD_GET(CECB_CLK_CNTL_M1, reg1) + 1;
f1 = DIV_ROUND_CLOSEST(parent_rate, n1);
f2 = DIV_ROUND_CLOSEST(parent_rate, n2);
p1 = DIV_ROUND_CLOSEST(100000000 * m1, f1 * (m1 + m2));
p2 = DIV_ROUND_CLOSEST(100000000 * m2, f2 * (m1 + m2));
return DIV_ROUND_UP(100000000, p1 + p2);
}
n1 = FIELD_GET(CECB_CLK_CNTL_N1, reg0) + 1;
return DIV_ROUND_CLOSEST(parent_rate, n1);
}
static int meson_ao_cec_g12a_dualdiv_clk_enable(struct clk_hw *hw)
{
struct meson_ao_cec_g12a_dualdiv_clk *dualdiv_clk =
hw_to_meson_ao_cec_g12a_dualdiv_clk(hw);
/* Disable Input & Output */
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0,
CECB_CLK_CNTL_INPUT_EN | CECB_CLK_CNTL_OUTPUT_EN,
0);
/* Set N1 & N2 */
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0,
CECB_CLK_CNTL_N1,
FIELD_PREP(CECB_CLK_CNTL_N1, 733 - 1));
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0,
CECB_CLK_CNTL_N2,
FIELD_PREP(CECB_CLK_CNTL_N2, 732 - 1));
/* Set M1 & M2 */
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG1,
CECB_CLK_CNTL_M1,
FIELD_PREP(CECB_CLK_CNTL_M1, 8 - 1));
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG1,
CECB_CLK_CNTL_M2,
FIELD_PREP(CECB_CLK_CNTL_M2, 11 - 1));
/* Enable Dual divisor */
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0,
CECB_CLK_CNTL_DUAL_EN, CECB_CLK_CNTL_DUAL_EN);
/* Disable divisor bypass */
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG1,
CECB_CLK_CNTL_BYPASS_EN, 0);
/* Enable Input & Output */
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0,
CECB_CLK_CNTL_INPUT_EN | CECB_CLK_CNTL_OUTPUT_EN,
CECB_CLK_CNTL_INPUT_EN | CECB_CLK_CNTL_OUTPUT_EN);
return 0;
}
static void meson_ao_cec_g12a_dualdiv_clk_disable(struct clk_hw *hw)
{
struct meson_ao_cec_g12a_dualdiv_clk *dualdiv_clk =
hw_to_meson_ao_cec_g12a_dualdiv_clk(hw);
regmap_update_bits(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0,
CECB_CLK_CNTL_INPUT_EN | CECB_CLK_CNTL_OUTPUT_EN,
0);
}
static int meson_ao_cec_g12a_dualdiv_clk_is_enabled(struct clk_hw *hw)
{
struct meson_ao_cec_g12a_dualdiv_clk *dualdiv_clk =
hw_to_meson_ao_cec_g12a_dualdiv_clk(hw);
int val;
regmap_read(dualdiv_clk->regmap, CECB_CLK_CNTL_REG0, &val);
return !!(val & (CECB_CLK_CNTL_INPUT_EN | CECB_CLK_CNTL_OUTPUT_EN));
}
static const struct clk_ops meson_ao_cec_g12a_dualdiv_clk_ops = {
.recalc_rate = meson_ao_cec_g12a_dualdiv_clk_recalc_rate,
.is_enabled = meson_ao_cec_g12a_dualdiv_clk_is_enabled,
.enable = meson_ao_cec_g12a_dualdiv_clk_enable,
.disable = meson_ao_cec_g12a_dualdiv_clk_disable,
};
static int meson_ao_cec_g12a_setup_clk(struct meson_ao_cec_g12a_device *ao_cec)
{
struct meson_ao_cec_g12a_dualdiv_clk *dualdiv_clk;
struct device *dev = &ao_cec->pdev->dev;
struct clk_init_data init;
const char *parent_name;
struct clk *clk;
char *name;
dualdiv_clk = devm_kzalloc(dev, sizeof(*dualdiv_clk), GFP_KERNEL);
if (!dualdiv_clk)
return -ENOMEM;
name = kasprintf(GFP_KERNEL, "%s#dualdiv_clk", dev_name(dev));
if (!name)
return -ENOMEM;
parent_name = __clk_get_name(ao_cec->oscin);
init.name = name;
init.ops = &meson_ao_cec_g12a_dualdiv_clk_ops;
init.flags = 0;
init.parent_names = &parent_name;
init.num_parents = 1;
dualdiv_clk->regmap = ao_cec->regmap;
dualdiv_clk->hw.init = &init;
clk = devm_clk_register(dev, &dualdiv_clk->hw);
kfree(name);
if (IS_ERR(clk)) {
dev_err(dev, "failed to register clock\n");
return PTR_ERR(clk);
}
ao_cec->core = clk;
return 0;
}
static int meson_ao_cec_g12a_read(void *context, unsigned int addr,
unsigned int *data)
{
struct meson_ao_cec_g12a_device *ao_cec = context;
u32 reg = FIELD_PREP(CECB_RW_ADDR, addr);
int ret = 0;
ret = regmap_write(ao_cec->regmap, CECB_RW_REG, reg);
if (ret)
return ret;
ret = regmap_read_poll_timeout(ao_cec->regmap, CECB_RW_REG, reg,
!(reg & CECB_RW_BUS_BUSY),
5, 1000);
if (ret)
return ret;
ret = regmap_read(ao_cec->regmap, CECB_RW_REG, &reg);
*data = FIELD_GET(CECB_RW_RD_DATA, reg);
return ret;
}
static int meson_ao_cec_g12a_write(void *context, unsigned int addr,
unsigned int data)
{
struct meson_ao_cec_g12a_device *ao_cec = context;
u32 reg = FIELD_PREP(CECB_RW_ADDR, addr) |
FIELD_PREP(CECB_RW_WR_DATA, data) |
CECB_RW_WRITE_EN;
return regmap_write(ao_cec->regmap, CECB_RW_REG, reg);
}
static const struct regmap_config meson_ao_cec_g12a_cec_regmap_conf = {
.reg_bits = 8,
.val_bits = 8,
.reg_read = meson_ao_cec_g12a_read,
.reg_write = meson_ao_cec_g12a_write,
.max_register = 0xffff,
};
static inline void
meson_ao_cec_g12a_irq_setup(struct meson_ao_cec_g12a_device *ao_cec,
bool enable)
{
u32 cfg = CECB_INTR_DONE | CECB_INTR_EOM | CECB_INTR_NACK |
CECB_INTR_ARB_LOSS | CECB_INTR_INITIATOR_ERR |
CECB_INTR_FOLLOWER_ERR;
regmap_write(ao_cec->regmap, CECB_INTR_MASKN_REG,
enable ? cfg : 0);
}
static void meson_ao_cec_g12a_irq_rx(struct meson_ao_cec_g12a_device *ao_cec)
{
int i, ret = 0;
u32 val;
ret = regmap_read(ao_cec->regmap_cec, CECB_RX_CNT, &val);
ao_cec->rx_msg.len = val;
if (ao_cec->rx_msg.len > CEC_MAX_MSG_SIZE)
ao_cec->rx_msg.len = CEC_MAX_MSG_SIZE;
for (i = 0; i < ao_cec->rx_msg.len; i++) {
ret |= regmap_read(ao_cec->regmap_cec,
CECB_RX_DATA00 + i, &val);
ao_cec->rx_msg.msg[i] = val & 0xff;
}
ret |= regmap_write(ao_cec->regmap_cec, CECB_LOCK_BUF, 0);
if (ret)
return;
cec_received_msg(ao_cec->adap, &ao_cec->rx_msg);
}
static irqreturn_t meson_ao_cec_g12a_irq(int irq, void *data)
{
struct meson_ao_cec_g12a_device *ao_cec = data;
u32 stat;
regmap_read(ao_cec->regmap, CECB_INTR_STAT_REG, &stat);
if (stat)
return IRQ_WAKE_THREAD;
return IRQ_NONE;
}
static irqreturn_t meson_ao_cec_g12a_irq_thread(int irq, void *data)
{
struct meson_ao_cec_g12a_device *ao_cec = data;
u32 stat;
regmap_read(ao_cec->regmap, CECB_INTR_STAT_REG, &stat);
regmap_write(ao_cec->regmap, CECB_INTR_CLR_REG, stat);
if (stat & CECB_INTR_DONE)
cec_transmit_attempt_done(ao_cec->adap, CEC_TX_STATUS_OK);
if (stat & CECB_INTR_EOM)
meson_ao_cec_g12a_irq_rx(ao_cec);
if (stat & CECB_INTR_NACK)
cec_transmit_attempt_done(ao_cec->adap, CEC_TX_STATUS_NACK);
if (stat & CECB_INTR_ARB_LOSS) {
regmap_write(ao_cec->regmap_cec, CECB_TX_CNT, 0);
regmap_update_bits(ao_cec->regmap_cec, CECB_CTRL,
CECB_CTRL_SEND | CECB_CTRL_TYPE, 0);
cec_transmit_attempt_done(ao_cec->adap, CEC_TX_STATUS_ARB_LOST);
}
/* Initiator reports an error on the CEC bus */
if (stat & CECB_INTR_INITIATOR_ERR)
cec_transmit_attempt_done(ao_cec->adap, CEC_TX_STATUS_ERROR);
/* Follower reports a receive error, just reset RX buffer */
if (stat & CECB_INTR_FOLLOWER_ERR)
regmap_write(ao_cec->regmap_cec, CECB_LOCK_BUF, 0);
return IRQ_HANDLED;
}
static int
meson_ao_cec_g12a_set_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
struct meson_ao_cec_g12a_device *ao_cec = adap->priv;
int ret = 0;
if (logical_addr == CEC_LOG_ADDR_INVALID) {
/* Assume this will allways succeed */
regmap_write(ao_cec->regmap_cec, CECB_LADD_LOW, 0);
regmap_write(ao_cec->regmap_cec, CECB_LADD_HIGH, 0);
return 0;
} else if (logical_addr < 8) {
ret = regmap_update_bits(ao_cec->regmap_cec, CECB_LADD_LOW,
BIT(logical_addr),
BIT(logical_addr));
} else {
ret = regmap_update_bits(ao_cec->regmap_cec, CECB_LADD_HIGH,
BIT(logical_addr - 8),
BIT(logical_addr - 8));
}
/* Always set Broadcast/Unregistered 15 address */
ret |= regmap_update_bits(ao_cec->regmap_cec, CECB_LADD_HIGH,
BIT(CEC_LOG_ADDR_UNREGISTERED - 8),
BIT(CEC_LOG_ADDR_UNREGISTERED - 8));
return ret ? -EIO : 0;
}
static int meson_ao_cec_g12a_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct meson_ao_cec_g12a_device *ao_cec = adap->priv;
unsigned int type;
int ret = 0;
u32 val;
int i;
/* Check if RX is in progress */
ret = regmap_read(ao_cec->regmap_cec, CECB_LOCK_BUF, &val);
if (ret)
return ret;
if (val & CECB_LOCK_BUF_EN)
return -EBUSY;
/* Check if TX Busy */
ret = regmap_read(ao_cec->regmap_cec, CECB_CTRL, &val);
if (ret)
return ret;
if (val & CECB_CTRL_SEND)
return -EBUSY;
switch (signal_free_time) {
case CEC_SIGNAL_FREE_TIME_RETRY:
type = CECB_CTRL_TYPE_RETRY;
break;
case CEC_SIGNAL_FREE_TIME_NEXT_XFER:
type = CECB_CTRL_TYPE_NEXT;
break;
case CEC_SIGNAL_FREE_TIME_NEW_INITIATOR:
default:
type = CECB_CTRL_TYPE_NEW;
break;
}
for (i = 0; i < msg->len; i++)
ret |= regmap_write(ao_cec->regmap_cec, CECB_TX_DATA00 + i,
msg->msg[i]);
ret |= regmap_write(ao_cec->regmap_cec, CECB_TX_CNT, msg->len);
if (ret)
return -EIO;
ret = regmap_update_bits(ao_cec->regmap_cec, CECB_CTRL,
CECB_CTRL_SEND |
CECB_CTRL_TYPE,
CECB_CTRL_SEND |
FIELD_PREP(CECB_CTRL_TYPE, type));
return ret;
}
static int meson_ao_cec_g12a_adap_enable(struct cec_adapter *adap, bool enable)
{
struct meson_ao_cec_g12a_device *ao_cec = adap->priv;
meson_ao_cec_g12a_irq_setup(ao_cec, false);
regmap_update_bits(ao_cec->regmap, CECB_GEN_CNTL_REG,
CECB_GEN_CNTL_RESET, CECB_GEN_CNTL_RESET);
if (!enable)
return 0;
/* Setup Filter */
regmap_update_bits(ao_cec->regmap, CECB_GEN_CNTL_REG,
CECB_GEN_CNTL_FILTER_TICK_SEL |
CECB_GEN_CNTL_FILTER_DEL,
FIELD_PREP(CECB_GEN_CNTL_FILTER_TICK_SEL,
CECB_GEN_CNTL_FILTER_TICK_1US) |
FIELD_PREP(CECB_GEN_CNTL_FILTER_DEL, 7));
/* Enable System Clock */
regmap_update_bits(ao_cec->regmap, CECB_GEN_CNTL_REG,
CECB_GEN_CNTL_SYS_CLK_EN,
CECB_GEN_CNTL_SYS_CLK_EN);
/* Enable gated clock (Normal mode). */
regmap_update_bits(ao_cec->regmap, CECB_GEN_CNTL_REG,
CECB_GEN_CNTL_CLK_CTRL_MASK,
FIELD_PREP(CECB_GEN_CNTL_CLK_CTRL_MASK,
CECB_GEN_CNTL_CLK_ENABLE));
/* Release Reset */
regmap_update_bits(ao_cec->regmap, CECB_GEN_CNTL_REG,
CECB_GEN_CNTL_RESET, 0);
if (ao_cec->data->ctrl2_setup)
regmap_write(ao_cec->regmap_cec, CECB_CTRL2,
FIELD_PREP(CECB_CTRL2_RISE_DEL_MAX, 2));
meson_ao_cec_g12a_irq_setup(ao_cec, true);
return 0;
}
static const struct cec_adap_ops meson_ao_cec_g12a_ops = {
.adap_enable = meson_ao_cec_g12a_adap_enable,
.adap_log_addr = meson_ao_cec_g12a_set_log_addr,
.adap_transmit = meson_ao_cec_g12a_transmit,
};
static int meson_ao_cec_g12a_probe(struct platform_device *pdev)
{
struct meson_ao_cec_g12a_device *ao_cec;
struct device *hdmi_dev;
void __iomem *base;
int ret, irq;
hdmi_dev = cec_notifier_parse_hdmi_phandle(&pdev->dev);
if (IS_ERR(hdmi_dev))
return PTR_ERR(hdmi_dev);
ao_cec = devm_kzalloc(&pdev->dev, sizeof(*ao_cec), GFP_KERNEL);
if (!ao_cec)
return -ENOMEM;
ao_cec->data = of_device_get_match_data(&pdev->dev);
if (!ao_cec->data) {
dev_err(&pdev->dev, "failed to get match data\n");
return -ENODEV;
}
spin_lock_init(&ao_cec->cec_reg_lock);
ao_cec->pdev = pdev;
ao_cec->adap = cec_allocate_adapter(&meson_ao_cec_g12a_ops, ao_cec,
"meson_g12a_ao_cec",
CEC_CAP_DEFAULTS |
CEC_CAP_CONNECTOR_INFO,
CEC_MAX_LOG_ADDRS);
if (IS_ERR(ao_cec->adap))
return PTR_ERR(ao_cec->adap);
ao_cec->adap->owner = THIS_MODULE;
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base)) {
ret = PTR_ERR(base);
goto out_probe_adapter;
}
ao_cec->regmap = devm_regmap_init_mmio(&pdev->dev, base,
&meson_ao_cec_g12a_regmap_conf);
if (IS_ERR(ao_cec->regmap)) {
ret = PTR_ERR(ao_cec->regmap);
goto out_probe_adapter;
}
ao_cec->regmap_cec = devm_regmap_init(&pdev->dev, NULL, ao_cec,
&meson_ao_cec_g12a_cec_regmap_conf);
if (IS_ERR(ao_cec->regmap_cec)) {
ret = PTR_ERR(ao_cec->regmap_cec);
goto out_probe_adapter;
}
irq = platform_get_irq(pdev, 0);
ret = devm_request_threaded_irq(&pdev->dev, irq,
meson_ao_cec_g12a_irq,
meson_ao_cec_g12a_irq_thread,
0, NULL, ao_cec);
if (ret) {
dev_err(&pdev->dev, "irq request failed\n");
goto out_probe_adapter;
}
ao_cec->oscin = devm_clk_get(&pdev->dev, "oscin");
if (IS_ERR(ao_cec->oscin)) {
dev_err(&pdev->dev, "oscin clock request failed\n");
ret = PTR_ERR(ao_cec->oscin);
goto out_probe_adapter;
}
ret = meson_ao_cec_g12a_setup_clk(ao_cec);
if (ret)
goto out_probe_adapter;
ret = clk_prepare_enable(ao_cec->core);
if (ret) {
dev_err(&pdev->dev, "core clock enable failed\n");
goto out_probe_adapter;
}
device_reset_optional(&pdev->dev);
platform_set_drvdata(pdev, ao_cec);
ao_cec->notify = cec_notifier_cec_adap_register(hdmi_dev, NULL,
ao_cec->adap);
if (!ao_cec->notify) {
ret = -ENOMEM;
goto out_probe_core_clk;
}
ret = cec_register_adapter(ao_cec->adap, &pdev->dev);
if (ret < 0)
goto out_probe_notify;
/* Setup Hardware */
regmap_write(ao_cec->regmap, CECB_GEN_CNTL_REG, CECB_GEN_CNTL_RESET);
return 0;
out_probe_notify:
cec_notifier_cec_adap_unregister(ao_cec->notify, ao_cec->adap);
out_probe_core_clk:
clk_disable_unprepare(ao_cec->core);
out_probe_adapter:
cec_delete_adapter(ao_cec->adap);
dev_err(&pdev->dev, "CEC controller registration failed\n");
return ret;
}
static void meson_ao_cec_g12a_remove(struct platform_device *pdev)
{
struct meson_ao_cec_g12a_device *ao_cec = platform_get_drvdata(pdev);
clk_disable_unprepare(ao_cec->core);
cec_notifier_cec_adap_unregister(ao_cec->notify, ao_cec->adap);
cec_unregister_adapter(ao_cec->adap);
}
static const struct meson_ao_cec_g12a_data ao_cec_g12a_data = {
.ctrl2_setup = false,
};
static const struct meson_ao_cec_g12a_data ao_cec_sm1_data = {
.ctrl2_setup = true,
};
static const struct of_device_id meson_ao_cec_g12a_of_match[] = {
{
.compatible = "amlogic,meson-g12a-ao-cec",
.data = &ao_cec_g12a_data,
},
{
.compatible = "amlogic,meson-sm1-ao-cec",
.data = &ao_cec_sm1_data,
},
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, meson_ao_cec_g12a_of_match);
static struct platform_driver meson_ao_cec_g12a_driver = {
.probe = meson_ao_cec_g12a_probe,
.remove = meson_ao_cec_g12a_remove,
.driver = {
.name = "meson-ao-cec-g12a",
.of_match_table = of_match_ptr(meson_ao_cec_g12a_of_match),
},
};
module_platform_driver(meson_ao_cec_g12a_driver);
MODULE_DESCRIPTION("Meson AO CEC G12A Controller driver");
MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,728 @@
/*
* Driver for Amlogic Meson AO CEC Controller
*
* Copyright (C) 2015 Amlogic, Inc. All rights reserved
* Copyright (C) 2017 BayLibre, SAS
* Author: Neil Armstrong <narmstrong@baylibre.com>
*
* SPDX-License-Identifier: GPL-2.0+
*/
#include <linux/bitfield.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/reset.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
/* CEC Registers */
/*
* [2:1] cntl_clk
* - 0 = Disable clk (Power-off mode)
* - 1 = Enable gated clock (Normal mode)
* - 2 = Enable free-run clk (Debug mode)
*/
#define CEC_GEN_CNTL_REG 0x00
#define CEC_GEN_CNTL_RESET BIT(0)
#define CEC_GEN_CNTL_CLK_DISABLE 0
#define CEC_GEN_CNTL_CLK_ENABLE 1
#define CEC_GEN_CNTL_CLK_ENABLE_DBG 2
#define CEC_GEN_CNTL_CLK_CTRL_MASK GENMASK(2, 1)
/*
* [7:0] cec_reg_addr
* [15:8] cec_reg_wrdata
* [16] cec_reg_wr
* - 0 = Read
* - 1 = Write
* [23] bus free
* [31:24] cec_reg_rddata
*/
#define CEC_RW_REG 0x04
#define CEC_RW_ADDR GENMASK(7, 0)
#define CEC_RW_WR_DATA GENMASK(15, 8)
#define CEC_RW_WRITE_EN BIT(16)
#define CEC_RW_BUS_BUSY BIT(23)
#define CEC_RW_RD_DATA GENMASK(31, 24)
/*
* [1] tx intr
* [2] rx intr
*/
#define CEC_INTR_MASKN_REG 0x08
#define CEC_INTR_CLR_REG 0x0c
#define CEC_INTR_STAT_REG 0x10
#define CEC_INTR_TX BIT(1)
#define CEC_INTR_RX BIT(2)
/* CEC Commands */
#define CEC_TX_MSG_0_HEADER 0x00
#define CEC_TX_MSG_1_OPCODE 0x01
#define CEC_TX_MSG_2_OP1 0x02
#define CEC_TX_MSG_3_OP2 0x03
#define CEC_TX_MSG_4_OP3 0x04
#define CEC_TX_MSG_5_OP4 0x05
#define CEC_TX_MSG_6_OP5 0x06
#define CEC_TX_MSG_7_OP6 0x07
#define CEC_TX_MSG_8_OP7 0x08
#define CEC_TX_MSG_9_OP8 0x09
#define CEC_TX_MSG_A_OP9 0x0A
#define CEC_TX_MSG_B_OP10 0x0B
#define CEC_TX_MSG_C_OP11 0x0C
#define CEC_TX_MSG_D_OP12 0x0D
#define CEC_TX_MSG_E_OP13 0x0E
#define CEC_TX_MSG_F_OP14 0x0F
#define CEC_TX_MSG_LENGTH 0x10
#define CEC_TX_MSG_CMD 0x11
#define CEC_TX_WRITE_BUF 0x12
#define CEC_TX_CLEAR_BUF 0x13
#define CEC_RX_MSG_CMD 0x14
#define CEC_RX_CLEAR_BUF 0x15
#define CEC_LOGICAL_ADDR0 0x16
#define CEC_LOGICAL_ADDR1 0x17
#define CEC_LOGICAL_ADDR2 0x18
#define CEC_LOGICAL_ADDR3 0x19
#define CEC_LOGICAL_ADDR4 0x1A
#define CEC_CLOCK_DIV_H 0x1B
#define CEC_CLOCK_DIV_L 0x1C
#define CEC_QUIESCENT_25MS_BIT7_0 0x20
#define CEC_QUIESCENT_25MS_BIT11_8 0x21
#define CEC_STARTBITMINL2H_3MS5_BIT7_0 0x22
#define CEC_STARTBITMINL2H_3MS5_BIT8 0x23
#define CEC_STARTBITMAXL2H_3MS9_BIT7_0 0x24
#define CEC_STARTBITMAXL2H_3MS9_BIT8 0x25
#define CEC_STARTBITMINH_0MS6_BIT7_0 0x26
#define CEC_STARTBITMINH_0MS6_BIT8 0x27
#define CEC_STARTBITMAXH_1MS0_BIT7_0 0x28
#define CEC_STARTBITMAXH_1MS0_BIT8 0x29
#define CEC_STARTBITMINTOT_4MS3_BIT7_0 0x2A
#define CEC_STARTBITMINTOT_4MS3_BIT9_8 0x2B
#define CEC_STARTBITMAXTOT_4MS7_BIT7_0 0x2C
#define CEC_STARTBITMAXTOT_4MS7_BIT9_8 0x2D
#define CEC_LOGIC1MINL2H_0MS4_BIT7_0 0x2E
#define CEC_LOGIC1MINL2H_0MS4_BIT8 0x2F
#define CEC_LOGIC1MAXL2H_0MS8_BIT7_0 0x30
#define CEC_LOGIC1MAXL2H_0MS8_BIT8 0x31
#define CEC_LOGIC0MINL2H_1MS3_BIT7_0 0x32
#define CEC_LOGIC0MINL2H_1MS3_BIT8 0x33
#define CEC_LOGIC0MAXL2H_1MS7_BIT7_0 0x34
#define CEC_LOGIC0MAXL2H_1MS7_BIT8 0x35
#define CEC_LOGICMINTOTAL_2MS05_BIT7_0 0x36
#define CEC_LOGICMINTOTAL_2MS05_BIT9_8 0x37
#define CEC_LOGICMAXHIGH_2MS8_BIT7_0 0x38
#define CEC_LOGICMAXHIGH_2MS8_BIT8 0x39
#define CEC_LOGICERRLOW_3MS4_BIT7_0 0x3A
#define CEC_LOGICERRLOW_3MS4_BIT8 0x3B
#define CEC_NOMSMPPOINT_1MS05 0x3C
#define CEC_DELCNTR_LOGICERR 0x3E
#define CEC_TXTIME_17MS_BIT7_0 0x40
#define CEC_TXTIME_17MS_BIT10_8 0x41
#define CEC_TXTIME_2BIT_BIT7_0 0x42
#define CEC_TXTIME_2BIT_BIT10_8 0x43
#define CEC_TXTIME_4BIT_BIT7_0 0x44
#define CEC_TXTIME_4BIT_BIT10_8 0x45
#define CEC_STARTBITNOML2H_3MS7_BIT7_0 0x46
#define CEC_STARTBITNOML2H_3MS7_BIT8 0x47
#define CEC_STARTBITNOMH_0MS8_BIT7_0 0x48
#define CEC_STARTBITNOMH_0MS8_BIT8 0x49
#define CEC_LOGIC1NOML2H_0MS6_BIT7_0 0x4A
#define CEC_LOGIC1NOML2H_0MS6_BIT8 0x4B
#define CEC_LOGIC0NOML2H_1MS5_BIT7_0 0x4C
#define CEC_LOGIC0NOML2H_1MS5_BIT8 0x4D
#define CEC_LOGIC1NOMH_1MS8_BIT7_0 0x4E
#define CEC_LOGIC1NOMH_1MS8_BIT8 0x4F
#define CEC_LOGIC0NOMH_0MS9_BIT7_0 0x50
#define CEC_LOGIC0NOMH_0MS9_BIT8 0x51
#define CEC_LOGICERRLOW_3MS6_BIT7_0 0x52
#define CEC_LOGICERRLOW_3MS6_BIT8 0x53
#define CEC_CHKCONTENTION_0MS1 0x54
#define CEC_PREPARENXTBIT_0MS05_BIT7_0 0x56
#define CEC_PREPARENXTBIT_0MS05_BIT8 0x57
#define CEC_NOMSMPACKPOINT_0MS45 0x58
#define CEC_ACK0NOML2H_1MS5_BIT7_0 0x5A
#define CEC_ACK0NOML2H_1MS5_BIT8 0x5B
#define CEC_BUGFIX_DISABLE_0 0x60
#define CEC_BUGFIX_DISABLE_1 0x61
#define CEC_RX_MSG_0_HEADER 0x80
#define CEC_RX_MSG_1_OPCODE 0x81
#define CEC_RX_MSG_2_OP1 0x82
#define CEC_RX_MSG_3_OP2 0x83
#define CEC_RX_MSG_4_OP3 0x84
#define CEC_RX_MSG_5_OP4 0x85
#define CEC_RX_MSG_6_OP5 0x86
#define CEC_RX_MSG_7_OP6 0x87
#define CEC_RX_MSG_8_OP7 0x88
#define CEC_RX_MSG_9_OP8 0x89
#define CEC_RX_MSG_A_OP9 0x8A
#define CEC_RX_MSG_B_OP10 0x8B
#define CEC_RX_MSG_C_OP11 0x8C
#define CEC_RX_MSG_D_OP12 0x8D
#define CEC_RX_MSG_E_OP13 0x8E
#define CEC_RX_MSG_F_OP14 0x8F
#define CEC_RX_MSG_LENGTH 0x90
#define CEC_RX_MSG_STATUS 0x91
#define CEC_RX_NUM_MSG 0x92
#define CEC_TX_MSG_STATUS 0x93
#define CEC_TX_NUM_MSG 0x94
/* CEC_TX_MSG_CMD definition */
#define TX_NO_OP 0 /* No transaction */
#define TX_REQ_CURRENT 1 /* Transmit earliest message in buffer */
#define TX_ABORT 2 /* Abort transmitting earliest message */
#define TX_REQ_NEXT 3 /* Overwrite earliest msg, transmit next */
/* tx_msg_status definition */
#define TX_IDLE 0 /* No transaction */
#define TX_BUSY 1 /* Transmitter is busy */
#define TX_DONE 2 /* Message successfully transmitted */
#define TX_ERROR 3 /* Message transmitted with error */
/* rx_msg_cmd */
#define RX_NO_OP 0 /* No transaction */
#define RX_ACK_CURRENT 1 /* Read earliest message in buffer */
#define RX_DISABLE 2 /* Disable receiving latest message */
#define RX_ACK_NEXT 3 /* Clear earliest msg, read next */
/* rx_msg_status */
#define RX_IDLE 0 /* No transaction */
#define RX_BUSY 1 /* Receiver is busy */
#define RX_DONE 2 /* Message has been received successfully */
#define RX_ERROR 3 /* Message has been received with error */
/* RX_CLEAR_BUF options */
#define CLEAR_START 1
#define CLEAR_STOP 0
/* CEC_LOGICAL_ADDRx options */
#define LOGICAL_ADDR_MASK 0xf
#define LOGICAL_ADDR_VALID BIT(4)
#define LOGICAL_ADDR_DISABLE 0
#define CEC_CLK_RATE 32768
struct meson_ao_cec_device {
struct platform_device *pdev;
void __iomem *base;
struct clk *core;
spinlock_t cec_reg_lock;
struct cec_notifier *notify;
struct cec_adapter *adap;
struct cec_msg rx_msg;
};
#define writel_bits_relaxed(mask, val, addr) \
writel_relaxed((readl_relaxed(addr) & ~(mask)) | (val), addr)
static inline int meson_ao_cec_wait_busy(struct meson_ao_cec_device *ao_cec)
{
ktime_t timeout = ktime_add_us(ktime_get(), 5000);
while (readl_relaxed(ao_cec->base + CEC_RW_REG) & CEC_RW_BUS_BUSY) {
if (ktime_compare(ktime_get(), timeout) > 0)
return -ETIMEDOUT;
}
return 0;
}
static void meson_ao_cec_read(struct meson_ao_cec_device *ao_cec,
unsigned long address, u8 *data,
int *res)
{
unsigned long flags;
u32 reg = FIELD_PREP(CEC_RW_ADDR, address);
int ret = 0;
if (res && *res)
return;
spin_lock_irqsave(&ao_cec->cec_reg_lock, flags);
ret = meson_ao_cec_wait_busy(ao_cec);
if (ret)
goto read_out;
writel_relaxed(reg, ao_cec->base + CEC_RW_REG);
ret = meson_ao_cec_wait_busy(ao_cec);
if (ret)
goto read_out;
*data = FIELD_GET(CEC_RW_RD_DATA,
readl_relaxed(ao_cec->base + CEC_RW_REG));
read_out:
spin_unlock_irqrestore(&ao_cec->cec_reg_lock, flags);
if (res)
*res = ret;
}
static void meson_ao_cec_write(struct meson_ao_cec_device *ao_cec,
unsigned long address, u8 data,
int *res)
{
unsigned long flags;
u32 reg = FIELD_PREP(CEC_RW_ADDR, address) |
FIELD_PREP(CEC_RW_WR_DATA, data) |
CEC_RW_WRITE_EN;
int ret = 0;
if (res && *res)
return;
spin_lock_irqsave(&ao_cec->cec_reg_lock, flags);
ret = meson_ao_cec_wait_busy(ao_cec);
if (ret)
goto write_out;
writel_relaxed(reg, ao_cec->base + CEC_RW_REG);
write_out:
spin_unlock_irqrestore(&ao_cec->cec_reg_lock, flags);
if (res)
*res = ret;
}
static inline void meson_ao_cec_irq_setup(struct meson_ao_cec_device *ao_cec,
bool enable)
{
u32 cfg = CEC_INTR_TX | CEC_INTR_RX;
writel_bits_relaxed(cfg, enable ? cfg : 0,
ao_cec->base + CEC_INTR_MASKN_REG);
}
static inline int meson_ao_cec_clear(struct meson_ao_cec_device *ao_cec)
{
int ret = 0;
meson_ao_cec_write(ao_cec, CEC_RX_MSG_CMD, RX_DISABLE, &ret);
meson_ao_cec_write(ao_cec, CEC_TX_MSG_CMD, TX_ABORT, &ret);
meson_ao_cec_write(ao_cec, CEC_RX_CLEAR_BUF, 1, &ret);
meson_ao_cec_write(ao_cec, CEC_TX_CLEAR_BUF, 1, &ret);
if (ret)
return ret;
udelay(100);
meson_ao_cec_write(ao_cec, CEC_RX_CLEAR_BUF, 0, &ret);
meson_ao_cec_write(ao_cec, CEC_TX_CLEAR_BUF, 0, &ret);
if (ret)
return ret;
udelay(100);
meson_ao_cec_write(ao_cec, CEC_RX_MSG_CMD, RX_NO_OP, &ret);
meson_ao_cec_write(ao_cec, CEC_TX_MSG_CMD, TX_NO_OP, &ret);
return ret;
}
static int meson_ao_cec_arbit_bit_time_set(struct meson_ao_cec_device *ao_cec,
unsigned int bit_set,
unsigned int time_set)
{
int ret = 0;
switch (bit_set) {
case CEC_SIGNAL_FREE_TIME_RETRY:
meson_ao_cec_write(ao_cec, CEC_TXTIME_4BIT_BIT7_0,
time_set & 0xff, &ret);
meson_ao_cec_write(ao_cec, CEC_TXTIME_4BIT_BIT10_8,
(time_set >> 8) & 0x7, &ret);
break;
case CEC_SIGNAL_FREE_TIME_NEW_INITIATOR:
meson_ao_cec_write(ao_cec, CEC_TXTIME_2BIT_BIT7_0,
time_set & 0xff, &ret);
meson_ao_cec_write(ao_cec, CEC_TXTIME_2BIT_BIT10_8,
(time_set >> 8) & 0x7, &ret);
break;
case CEC_SIGNAL_FREE_TIME_NEXT_XFER:
meson_ao_cec_write(ao_cec, CEC_TXTIME_17MS_BIT7_0,
time_set & 0xff, &ret);
meson_ao_cec_write(ao_cec, CEC_TXTIME_17MS_BIT10_8,
(time_set >> 8) & 0x7, &ret);
break;
}
return ret;
}
static irqreturn_t meson_ao_cec_irq(int irq, void *data)
{
struct meson_ao_cec_device *ao_cec = data;
u32 stat = readl_relaxed(ao_cec->base + CEC_INTR_STAT_REG);
if (stat)
return IRQ_WAKE_THREAD;
return IRQ_NONE;
}
static void meson_ao_cec_irq_tx(struct meson_ao_cec_device *ao_cec)
{
unsigned long tx_status = 0;
u8 stat;
int ret = 0;
meson_ao_cec_read(ao_cec, CEC_TX_MSG_STATUS, &stat, &ret);
if (ret)
goto tx_reg_err;
switch (stat) {
case TX_DONE:
tx_status = CEC_TX_STATUS_OK;
break;
case TX_BUSY:
tx_status = CEC_TX_STATUS_ARB_LOST;
break;
case TX_IDLE:
tx_status = CEC_TX_STATUS_LOW_DRIVE;
break;
case TX_ERROR:
default:
tx_status = CEC_TX_STATUS_NACK;
break;
}
/* Clear Interruption */
writel_relaxed(CEC_INTR_TX, ao_cec->base + CEC_INTR_CLR_REG);
/* Stop TX */
meson_ao_cec_write(ao_cec, CEC_TX_MSG_CMD, TX_NO_OP, &ret);
if (ret)
goto tx_reg_err;
cec_transmit_attempt_done(ao_cec->adap, tx_status);
return;
tx_reg_err:
cec_transmit_attempt_done(ao_cec->adap, CEC_TX_STATUS_ERROR);
}
static void meson_ao_cec_irq_rx(struct meson_ao_cec_device *ao_cec)
{
int i, ret = 0;
u8 reg;
meson_ao_cec_read(ao_cec, CEC_RX_MSG_STATUS, &reg, &ret);
if (reg != RX_DONE)
goto rx_out;
meson_ao_cec_read(ao_cec, CEC_RX_NUM_MSG, &reg, &ret);
if (reg != 1)
goto rx_out;
meson_ao_cec_read(ao_cec, CEC_RX_MSG_LENGTH, &reg, &ret);
ao_cec->rx_msg.len = reg + 1;
if (ao_cec->rx_msg.len > CEC_MAX_MSG_SIZE)
ao_cec->rx_msg.len = CEC_MAX_MSG_SIZE;
for (i = 0; i < ao_cec->rx_msg.len; i++) {
u8 byte;
meson_ao_cec_read(ao_cec, CEC_RX_MSG_0_HEADER + i, &byte, &ret);
ao_cec->rx_msg.msg[i] = byte;
}
if (ret)
goto rx_out;
cec_received_msg(ao_cec->adap, &ao_cec->rx_msg);
rx_out:
/* Clear Interruption */
writel_relaxed(CEC_INTR_RX, ao_cec->base + CEC_INTR_CLR_REG);
/* Ack RX message */
meson_ao_cec_write(ao_cec, CEC_RX_MSG_CMD, RX_ACK_CURRENT, &ret);
meson_ao_cec_write(ao_cec, CEC_RX_MSG_CMD, RX_NO_OP, &ret);
/* Clear RX buffer */
meson_ao_cec_write(ao_cec, CEC_RX_CLEAR_BUF, CLEAR_START, &ret);
meson_ao_cec_write(ao_cec, CEC_RX_CLEAR_BUF, CLEAR_STOP, &ret);
}
static irqreturn_t meson_ao_cec_irq_thread(int irq, void *data)
{
struct meson_ao_cec_device *ao_cec = data;
u32 stat = readl_relaxed(ao_cec->base + CEC_INTR_STAT_REG);
if (stat & CEC_INTR_TX)
meson_ao_cec_irq_tx(ao_cec);
meson_ao_cec_irq_rx(ao_cec);
return IRQ_HANDLED;
}
static int meson_ao_cec_set_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
struct meson_ao_cec_device *ao_cec = adap->priv;
int ret = 0;
meson_ao_cec_write(ao_cec, CEC_LOGICAL_ADDR0,
LOGICAL_ADDR_DISABLE, &ret);
if (ret)
return ret;
ret = meson_ao_cec_clear(ao_cec);
if (ret)
return ret;
if (logical_addr == CEC_LOG_ADDR_INVALID)
return 0;
meson_ao_cec_write(ao_cec, CEC_LOGICAL_ADDR0,
logical_addr & LOGICAL_ADDR_MASK, &ret);
if (ret)
return ret;
udelay(100);
meson_ao_cec_write(ao_cec, CEC_LOGICAL_ADDR0,
(logical_addr & LOGICAL_ADDR_MASK) |
LOGICAL_ADDR_VALID, &ret);
return ret;
}
static int meson_ao_cec_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct meson_ao_cec_device *ao_cec = adap->priv;
int i, ret = 0;
u8 reg;
meson_ao_cec_read(ao_cec, CEC_TX_MSG_STATUS, &reg, &ret);
if (ret)
return ret;
if (reg == TX_BUSY) {
dev_dbg(&ao_cec->pdev->dev, "%s: busy TX: aborting\n",
__func__);
meson_ao_cec_write(ao_cec, CEC_TX_MSG_CMD, TX_ABORT, &ret);
}
for (i = 0; i < msg->len; i++) {
meson_ao_cec_write(ao_cec, CEC_TX_MSG_0_HEADER + i,
msg->msg[i], &ret);
}
meson_ao_cec_write(ao_cec, CEC_TX_MSG_LENGTH, msg->len - 1, &ret);
meson_ao_cec_write(ao_cec, CEC_TX_MSG_CMD, TX_REQ_CURRENT, &ret);
return ret;
}
static int meson_ao_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct meson_ao_cec_device *ao_cec = adap->priv;
int ret;
meson_ao_cec_irq_setup(ao_cec, false);
writel_bits_relaxed(CEC_GEN_CNTL_RESET, CEC_GEN_CNTL_RESET,
ao_cec->base + CEC_GEN_CNTL_REG);
if (!enable)
return 0;
/* Enable gated clock (Normal mode). */
writel_bits_relaxed(CEC_GEN_CNTL_CLK_CTRL_MASK,
FIELD_PREP(CEC_GEN_CNTL_CLK_CTRL_MASK,
CEC_GEN_CNTL_CLK_ENABLE),
ao_cec->base + CEC_GEN_CNTL_REG);
udelay(100);
/* Release Reset */
writel_bits_relaxed(CEC_GEN_CNTL_RESET, 0,
ao_cec->base + CEC_GEN_CNTL_REG);
/* Clear buffers */
ret = meson_ao_cec_clear(ao_cec);
if (ret)
return ret;
/* CEC arbitration 3/5/7 bit time set. */
ret = meson_ao_cec_arbit_bit_time_set(ao_cec,
CEC_SIGNAL_FREE_TIME_RETRY,
0x118);
if (ret)
return ret;
ret = meson_ao_cec_arbit_bit_time_set(ao_cec,
CEC_SIGNAL_FREE_TIME_NEW_INITIATOR,
0x000);
if (ret)
return ret;
ret = meson_ao_cec_arbit_bit_time_set(ao_cec,
CEC_SIGNAL_FREE_TIME_NEXT_XFER,
0x2aa);
if (ret)
return ret;
meson_ao_cec_irq_setup(ao_cec, true);
return 0;
}
static const struct cec_adap_ops meson_ao_cec_ops = {
.adap_enable = meson_ao_cec_adap_enable,
.adap_log_addr = meson_ao_cec_set_log_addr,
.adap_transmit = meson_ao_cec_transmit,
};
static int meson_ao_cec_probe(struct platform_device *pdev)
{
struct meson_ao_cec_device *ao_cec;
struct device *hdmi_dev;
int ret, irq;
hdmi_dev = cec_notifier_parse_hdmi_phandle(&pdev->dev);
if (IS_ERR(hdmi_dev))
return PTR_ERR(hdmi_dev);
ao_cec = devm_kzalloc(&pdev->dev, sizeof(*ao_cec), GFP_KERNEL);
if (!ao_cec)
return -ENOMEM;
spin_lock_init(&ao_cec->cec_reg_lock);
ao_cec->adap = cec_allocate_adapter(&meson_ao_cec_ops, ao_cec,
"meson_ao_cec",
CEC_CAP_DEFAULTS |
CEC_CAP_CONNECTOR_INFO,
1); /* Use 1 for now */
if (IS_ERR(ao_cec->adap))
return PTR_ERR(ao_cec->adap);
ao_cec->adap->owner = THIS_MODULE;
ao_cec->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(ao_cec->base)) {
ret = PTR_ERR(ao_cec->base);
goto out_probe_adapter;
}
irq = platform_get_irq(pdev, 0);
ret = devm_request_threaded_irq(&pdev->dev, irq,
meson_ao_cec_irq,
meson_ao_cec_irq_thread,
0, NULL, ao_cec);
if (ret) {
dev_err(&pdev->dev, "irq request failed\n");
goto out_probe_adapter;
}
ao_cec->core = devm_clk_get(&pdev->dev, "core");
if (IS_ERR(ao_cec->core)) {
dev_err(&pdev->dev, "core clock request failed\n");
ret = PTR_ERR(ao_cec->core);
goto out_probe_adapter;
}
ret = clk_prepare_enable(ao_cec->core);
if (ret) {
dev_err(&pdev->dev, "core clock enable failed\n");
goto out_probe_adapter;
}
ret = clk_set_rate(ao_cec->core, CEC_CLK_RATE);
if (ret) {
dev_err(&pdev->dev, "core clock set rate failed\n");
goto out_probe_clk;
}
device_reset_optional(&pdev->dev);
ao_cec->pdev = pdev;
platform_set_drvdata(pdev, ao_cec);
ao_cec->notify = cec_notifier_cec_adap_register(hdmi_dev, NULL,
ao_cec->adap);
if (!ao_cec->notify) {
ret = -ENOMEM;
goto out_probe_clk;
}
ret = cec_register_adapter(ao_cec->adap, &pdev->dev);
if (ret < 0)
goto out_probe_notify;
/* Setup Hardware */
writel_relaxed(CEC_GEN_CNTL_RESET,
ao_cec->base + CEC_GEN_CNTL_REG);
return 0;
out_probe_notify:
cec_notifier_cec_adap_unregister(ao_cec->notify, ao_cec->adap);
out_probe_clk:
clk_disable_unprepare(ao_cec->core);
out_probe_adapter:
cec_delete_adapter(ao_cec->adap);
dev_err(&pdev->dev, "CEC controller registration failed\n");
return ret;
}
static void meson_ao_cec_remove(struct platform_device *pdev)
{
struct meson_ao_cec_device *ao_cec = platform_get_drvdata(pdev);
clk_disable_unprepare(ao_cec->core);
cec_notifier_cec_adap_unregister(ao_cec->notify, ao_cec->adap);
cec_unregister_adapter(ao_cec->adap);
}
static const struct of_device_id meson_ao_cec_of_match[] = {
{ .compatible = "amlogic,meson-gx-ao-cec", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, meson_ao_cec_of_match);
static struct platform_driver meson_ao_cec_driver = {
.probe = meson_ao_cec_probe,
.remove = meson_ao_cec_remove,
.driver = {
.name = "meson-ao-cec",
.of_match_table = meson_ao_cec_of_match,
},
};
module_platform_driver(meson_ao_cec_driver);
MODULE_DESCRIPTION("Meson AO CEC Controller driver");
MODULE_AUTHOR("Neil Armstrong <narmstrong@baylibre.com>");
MODULE_LICENSE("GPL");
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_SAMSUNG_S5P) += s5p-cec.o
s5p-cec-y += s5p_cec.o exynos_hdmi_cecctrl.o
@@ -0,0 +1,34 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* drivers/media/platform/s5p-cec/exynos_hdmi_cec.h
*
* Copyright (c) 2010, 2014 Samsung Electronics
* http://www.samsung.com/
*
* Header file for interface of Samsung Exynos hdmi cec hardware
*/
#ifndef _EXYNOS_HDMI_CEC_H_
#define _EXYNOS_HDMI_CEC_H_ __FILE__
#include <linux/regmap.h>
#include "s5p_cec.h"
void s5p_cec_set_divider(struct s5p_cec_dev *cec);
void s5p_cec_enable_rx(struct s5p_cec_dev *cec);
void s5p_cec_mask_rx_interrupts(struct s5p_cec_dev *cec);
void s5p_cec_unmask_rx_interrupts(struct s5p_cec_dev *cec);
void s5p_cec_mask_tx_interrupts(struct s5p_cec_dev *cec);
void s5p_cec_unmask_tx_interrupts(struct s5p_cec_dev *cec);
void s5p_cec_reset(struct s5p_cec_dev *cec);
void s5p_cec_tx_reset(struct s5p_cec_dev *cec);
void s5p_cec_rx_reset(struct s5p_cec_dev *cec);
void s5p_cec_threshold(struct s5p_cec_dev *cec);
void s5p_cec_copy_packet(struct s5p_cec_dev *cec, char *data,
size_t count, u8 retries);
void s5p_cec_set_addr(struct s5p_cec_dev *cec, u32 addr);
u32 s5p_cec_get_status(struct s5p_cec_dev *cec);
void s5p_clr_pending_tx(struct s5p_cec_dev *cec);
void s5p_clr_pending_rx(struct s5p_cec_dev *cec);
void s5p_cec_get_rx_buf(struct s5p_cec_dev *cec, u32 size, u8 *buffer);
#endif /* _EXYNOS_HDMI_CEC_H_ */
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: GPL-2.0-only
/* drivers/media/platform/s5p-cec/exynos_hdmi_cecctrl.c
*
* Copyright (c) 2009, 2014 Samsung Electronics
* http://www.samsung.com/
*
* cec ftn file for Samsung TVOUT driver
*/
#include <linux/io.h>
#include <linux/device.h>
#include "exynos_hdmi_cec.h"
#include "regs-cec.h"
#define S5P_HDMI_FIN 24000000
#define CEC_DIV_RATIO 320000
#define CEC_MESSAGE_BROADCAST_MASK 0x0F
#define CEC_MESSAGE_BROADCAST 0x0F
#define CEC_FILTER_THRESHOLD 0x15
void s5p_cec_set_divider(struct s5p_cec_dev *cec)
{
u32 div_ratio, div_val;
unsigned int reg;
div_ratio = S5P_HDMI_FIN / CEC_DIV_RATIO - 1;
if (regmap_read(cec->pmu, EXYNOS_HDMI_PHY_CONTROL, &reg)) {
dev_err(cec->dev, "failed to read phy control\n");
return;
}
reg = (reg & ~(0x3FF << 16)) | (div_ratio << 16);
if (regmap_write(cec->pmu, EXYNOS_HDMI_PHY_CONTROL, reg)) {
dev_err(cec->dev, "failed to write phy control\n");
return;
}
div_val = CEC_DIV_RATIO * 0.00005 - 1;
writeb(0x0, cec->reg + S5P_CEC_DIVISOR_3);
writeb(0x0, cec->reg + S5P_CEC_DIVISOR_2);
writeb(0x0, cec->reg + S5P_CEC_DIVISOR_1);
writeb(div_val, cec->reg + S5P_CEC_DIVISOR_0);
}
void s5p_cec_enable_rx(struct s5p_cec_dev *cec)
{
u8 reg;
reg = readb(cec->reg + S5P_CEC_RX_CTRL);
reg |= S5P_CEC_RX_CTRL_ENABLE;
writeb(reg, cec->reg + S5P_CEC_RX_CTRL);
}
void s5p_cec_mask_rx_interrupts(struct s5p_cec_dev *cec)
{
u8 reg;
reg = readb(cec->reg + S5P_CEC_IRQ_MASK);
reg |= S5P_CEC_IRQ_RX_DONE;
reg |= S5P_CEC_IRQ_RX_ERROR;
writeb(reg, cec->reg + S5P_CEC_IRQ_MASK);
}
void s5p_cec_unmask_rx_interrupts(struct s5p_cec_dev *cec)
{
u8 reg;
reg = readb(cec->reg + S5P_CEC_IRQ_MASK);
reg &= ~S5P_CEC_IRQ_RX_DONE;
reg &= ~S5P_CEC_IRQ_RX_ERROR;
writeb(reg, cec->reg + S5P_CEC_IRQ_MASK);
}
void s5p_cec_mask_tx_interrupts(struct s5p_cec_dev *cec)
{
u8 reg;
reg = readb(cec->reg + S5P_CEC_IRQ_MASK);
reg |= S5P_CEC_IRQ_TX_DONE;
reg |= S5P_CEC_IRQ_TX_ERROR;
writeb(reg, cec->reg + S5P_CEC_IRQ_MASK);
}
void s5p_cec_unmask_tx_interrupts(struct s5p_cec_dev *cec)
{
u8 reg;
reg = readb(cec->reg + S5P_CEC_IRQ_MASK);
reg &= ~S5P_CEC_IRQ_TX_DONE;
reg &= ~S5P_CEC_IRQ_TX_ERROR;
writeb(reg, cec->reg + S5P_CEC_IRQ_MASK);
}
void s5p_cec_reset(struct s5p_cec_dev *cec)
{
u8 reg;
writeb(S5P_CEC_RX_CTRL_RESET, cec->reg + S5P_CEC_RX_CTRL);
writeb(S5P_CEC_TX_CTRL_RESET, cec->reg + S5P_CEC_TX_CTRL);
reg = readb(cec->reg + 0xc4);
reg &= ~0x1;
writeb(reg, cec->reg + 0xc4);
}
void s5p_cec_tx_reset(struct s5p_cec_dev *cec)
{
writeb(S5P_CEC_TX_CTRL_RESET, cec->reg + S5P_CEC_TX_CTRL);
}
void s5p_cec_rx_reset(struct s5p_cec_dev *cec)
{
u8 reg;
writeb(S5P_CEC_RX_CTRL_RESET, cec->reg + S5P_CEC_RX_CTRL);
reg = readb(cec->reg + 0xc4);
reg &= ~0x1;
writeb(reg, cec->reg + 0xc4);
}
void s5p_cec_threshold(struct s5p_cec_dev *cec)
{
writeb(CEC_FILTER_THRESHOLD, cec->reg + S5P_CEC_RX_FILTER_TH);
writeb(0, cec->reg + S5P_CEC_RX_FILTER_CTRL);
}
void s5p_cec_copy_packet(struct s5p_cec_dev *cec, char *data,
size_t count, u8 retries)
{
int i = 0;
u8 reg;
while (i < count) {
writeb(data[i], cec->reg + (S5P_CEC_TX_BUFF0 + (i * 4)));
i++;
}
writeb(count, cec->reg + S5P_CEC_TX_BYTES);
reg = readb(cec->reg + S5P_CEC_TX_CTRL);
reg |= S5P_CEC_TX_CTRL_START;
reg &= ~0x70;
reg |= retries << 4;
if ((data[0] & CEC_MESSAGE_BROADCAST_MASK) == CEC_MESSAGE_BROADCAST) {
dev_dbg(cec->dev, "Broadcast");
reg |= S5P_CEC_TX_CTRL_BCAST;
} else {
dev_dbg(cec->dev, "No Broadcast");
reg &= ~S5P_CEC_TX_CTRL_BCAST;
}
writeb(reg, cec->reg + S5P_CEC_TX_CTRL);
dev_dbg(cec->dev, "cec-tx: cec count (%zu): %*ph", count,
(int)count, data);
}
void s5p_cec_set_addr(struct s5p_cec_dev *cec, u32 addr)
{
writeb(addr & 0x0F, cec->reg + S5P_CEC_LOGIC_ADDR);
}
u32 s5p_cec_get_status(struct s5p_cec_dev *cec)
{
u32 status = 0;
status = readb(cec->reg + S5P_CEC_STATUS_0) & 0xf;
status |= (readb(cec->reg + S5P_CEC_TX_STAT1) & 0xf) << 4;
status |= readb(cec->reg + S5P_CEC_STATUS_1) << 8;
status |= readb(cec->reg + S5P_CEC_STATUS_2) << 16;
status |= readb(cec->reg + S5P_CEC_STATUS_3) << 24;
dev_dbg(cec->dev, "status = 0x%x!\n", status);
return status;
}
void s5p_clr_pending_tx(struct s5p_cec_dev *cec)
{
writeb(S5P_CEC_IRQ_TX_DONE | S5P_CEC_IRQ_TX_ERROR,
cec->reg + S5P_CEC_IRQ_CLEAR);
}
void s5p_clr_pending_rx(struct s5p_cec_dev *cec)
{
writeb(S5P_CEC_IRQ_RX_DONE | S5P_CEC_IRQ_RX_ERROR,
cec->reg + S5P_CEC_IRQ_CLEAR);
}
void s5p_cec_get_rx_buf(struct s5p_cec_dev *cec, u32 size, u8 *buffer)
{
u32 i = 0;
char debug[40];
while (i < size) {
buffer[i] = readb(cec->reg + S5P_CEC_RX_BUFF0 + (i * 4));
sprintf(debug + i * 2, "%02x ", buffer[i]);
i++;
}
dev_dbg(cec->dev, "cec-rx: cec size(%d): %s", size, debug);
}
@@ -0,0 +1,93 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* drivers/media/platform/s5p-cec/regs-cec.h
*
* Copyright (c) 2010 Samsung Electronics
* http://www.samsung.com/
*
* register header file for Samsung TVOUT driver
*/
#ifndef __EXYNOS_REGS__H
#define __EXYNOS_REGS__H
/*
* Register part
*/
#define S5P_CEC_STATUS_0 (0x0000)
#define S5P_CEC_STATUS_1 (0x0004)
#define S5P_CEC_STATUS_2 (0x0008)
#define S5P_CEC_STATUS_3 (0x000C)
#define S5P_CEC_IRQ_MASK (0x0010)
#define S5P_CEC_IRQ_CLEAR (0x0014)
#define S5P_CEC_LOGIC_ADDR (0x0020)
#define S5P_CEC_DIVISOR_0 (0x0030)
#define S5P_CEC_DIVISOR_1 (0x0034)
#define S5P_CEC_DIVISOR_2 (0x0038)
#define S5P_CEC_DIVISOR_3 (0x003C)
#define S5P_CEC_TX_CTRL (0x0040)
#define S5P_CEC_TX_BYTES (0x0044)
#define S5P_CEC_TX_STAT0 (0x0060)
#define S5P_CEC_TX_STAT1 (0x0064)
#define S5P_CEC_TX_BUFF0 (0x0080)
#define S5P_CEC_TX_BUFF1 (0x0084)
#define S5P_CEC_TX_BUFF2 (0x0088)
#define S5P_CEC_TX_BUFF3 (0x008C)
#define S5P_CEC_TX_BUFF4 (0x0090)
#define S5P_CEC_TX_BUFF5 (0x0094)
#define S5P_CEC_TX_BUFF6 (0x0098)
#define S5P_CEC_TX_BUFF7 (0x009C)
#define S5P_CEC_TX_BUFF8 (0x00A0)
#define S5P_CEC_TX_BUFF9 (0x00A4)
#define S5P_CEC_TX_BUFF10 (0x00A8)
#define S5P_CEC_TX_BUFF11 (0x00AC)
#define S5P_CEC_TX_BUFF12 (0x00B0)
#define S5P_CEC_TX_BUFF13 (0x00B4)
#define S5P_CEC_TX_BUFF14 (0x00B8)
#define S5P_CEC_TX_BUFF15 (0x00BC)
#define S5P_CEC_RX_CTRL (0x00C0)
#define S5P_CEC_RX_STAT0 (0x00E0)
#define S5P_CEC_RX_STAT1 (0x00E4)
#define S5P_CEC_RX_BUFF0 (0x0100)
#define S5P_CEC_RX_BUFF1 (0x0104)
#define S5P_CEC_RX_BUFF2 (0x0108)
#define S5P_CEC_RX_BUFF3 (0x010C)
#define S5P_CEC_RX_BUFF4 (0x0110)
#define S5P_CEC_RX_BUFF5 (0x0114)
#define S5P_CEC_RX_BUFF6 (0x0118)
#define S5P_CEC_RX_BUFF7 (0x011C)
#define S5P_CEC_RX_BUFF8 (0x0120)
#define S5P_CEC_RX_BUFF9 (0x0124)
#define S5P_CEC_RX_BUFF10 (0x0128)
#define S5P_CEC_RX_BUFF11 (0x012C)
#define S5P_CEC_RX_BUFF12 (0x0130)
#define S5P_CEC_RX_BUFF13 (0x0134)
#define S5P_CEC_RX_BUFF14 (0x0138)
#define S5P_CEC_RX_BUFF15 (0x013C)
#define S5P_CEC_RX_FILTER_CTRL (0x0180)
#define S5P_CEC_RX_FILTER_TH (0x0184)
/*
* Bit definition part
*/
#define S5P_CEC_IRQ_TX_DONE (1<<0)
#define S5P_CEC_IRQ_TX_ERROR (1<<1)
#define S5P_CEC_IRQ_RX_DONE (1<<4)
#define S5P_CEC_IRQ_RX_ERROR (1<<5)
#define S5P_CEC_TX_CTRL_START (1<<0)
#define S5P_CEC_TX_CTRL_BCAST (1<<1)
#define S5P_CEC_TX_CTRL_RETRY (0x04<<4)
#define S5P_CEC_TX_CTRL_RESET (1<<7)
#define S5P_CEC_RX_CTRL_ENABLE (1<<0)
#define S5P_CEC_RX_CTRL_RESET (1<<7)
#define S5P_CEC_LOGIC_ADDR_MASK (0xF)
/* PMU Registers for PHY */
#define EXYNOS_HDMI_PHY_CONTROL 0x700
#endif /* __EXYNOS_REGS__H */
@@ -0,0 +1,309 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/* drivers/media/platform/s5p-cec/s5p_cec.c
*
* Samsung S5P CEC driver
*
* Copyright (c) 2014 Samsung Electronics Co., Ltd.
*
* This driver is based on the "cec interface driver for exynos soc" by
* SangPil Moon.
*/
#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/of_platform.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
#include "exynos_hdmi_cec.h"
#include "regs-cec.h"
#include "s5p_cec.h"
#define CEC_NAME "s5p-cec"
static int debug;
module_param(debug, int, 0644);
MODULE_PARM_DESC(debug, "debug level (0-2)");
static int s5p_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
int ret;
struct s5p_cec_dev *cec = cec_get_drvdata(adap);
if (enable) {
ret = pm_runtime_resume_and_get(cec->dev);
if (ret < 0)
return ret;
s5p_cec_reset(cec);
s5p_cec_set_divider(cec);
s5p_cec_threshold(cec);
s5p_cec_unmask_tx_interrupts(cec);
s5p_cec_unmask_rx_interrupts(cec);
s5p_cec_enable_rx(cec);
} else {
s5p_cec_mask_tx_interrupts(cec);
s5p_cec_mask_rx_interrupts(cec);
pm_runtime_put(cec->dev);
}
return 0;
}
static int s5p_cec_adap_log_addr(struct cec_adapter *adap, u8 addr)
{
struct s5p_cec_dev *cec = cec_get_drvdata(adap);
s5p_cec_set_addr(cec, addr);
return 0;
}
static int s5p_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct s5p_cec_dev *cec = cec_get_drvdata(adap);
/*
* Unclear if 0 retries are allowed by the hardware, so have 1 as
* the minimum.
*/
s5p_cec_copy_packet(cec, msg->msg, msg->len, max(1, attempts - 1));
return 0;
}
static irqreturn_t s5p_cec_irq_handler(int irq, void *priv)
{
struct s5p_cec_dev *cec = priv;
u32 status = 0;
status = s5p_cec_get_status(cec);
dev_dbg(cec->dev, "irq received\n");
if (status & CEC_STATUS_TX_DONE) {
if (status & CEC_STATUS_TX_NACK) {
dev_dbg(cec->dev, "CEC_STATUS_TX_NACK set\n");
cec->tx = STATE_NACK;
} else if (status & CEC_STATUS_TX_ERROR) {
dev_dbg(cec->dev, "CEC_STATUS_TX_ERROR set\n");
cec->tx = STATE_ERROR;
} else {
dev_dbg(cec->dev, "CEC_STATUS_TX_DONE\n");
cec->tx = STATE_DONE;
}
s5p_clr_pending_tx(cec);
}
if (status & CEC_STATUS_RX_DONE) {
if (status & CEC_STATUS_RX_ERROR) {
dev_dbg(cec->dev, "CEC_STATUS_RX_ERROR set\n");
s5p_cec_rx_reset(cec);
s5p_cec_enable_rx(cec);
} else {
dev_dbg(cec->dev, "CEC_STATUS_RX_DONE set\n");
if (cec->rx != STATE_IDLE)
dev_dbg(cec->dev, "Buffer overrun (worker did not process previous message)\n");
cec->rx = STATE_BUSY;
cec->msg.len = status >> 24;
if (cec->msg.len > CEC_MAX_MSG_SIZE)
cec->msg.len = CEC_MAX_MSG_SIZE;
cec->msg.rx_status = CEC_RX_STATUS_OK;
s5p_cec_get_rx_buf(cec, cec->msg.len,
cec->msg.msg);
cec->rx = STATE_DONE;
s5p_cec_enable_rx(cec);
}
/* Clear interrupt pending bit */
s5p_clr_pending_rx(cec);
}
return IRQ_WAKE_THREAD;
}
static irqreturn_t s5p_cec_irq_handler_thread(int irq, void *priv)
{
struct s5p_cec_dev *cec = priv;
dev_dbg(cec->dev, "irq processing thread\n");
switch (cec->tx) {
case STATE_DONE:
cec_transmit_done(cec->adap, CEC_TX_STATUS_OK, 0, 0, 0, 0);
cec->tx = STATE_IDLE;
break;
case STATE_NACK:
cec_transmit_done(cec->adap,
CEC_TX_STATUS_MAX_RETRIES | CEC_TX_STATUS_NACK,
0, 1, 0, 0);
cec->tx = STATE_IDLE;
break;
case STATE_ERROR:
cec_transmit_done(cec->adap,
CEC_TX_STATUS_MAX_RETRIES | CEC_TX_STATUS_ERROR,
0, 0, 0, 1);
cec->tx = STATE_IDLE;
break;
case STATE_BUSY:
dev_err(cec->dev, "state set to busy, this should not occur here\n");
break;
default:
break;
}
switch (cec->rx) {
case STATE_DONE:
cec_received_msg(cec->adap, &cec->msg);
cec->rx = STATE_IDLE;
break;
default:
break;
}
return IRQ_HANDLED;
}
static const struct cec_adap_ops s5p_cec_adap_ops = {
.adap_enable = s5p_cec_adap_enable,
.adap_log_addr = s5p_cec_adap_log_addr,
.adap_transmit = s5p_cec_adap_transmit,
};
static int s5p_cec_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device *hdmi_dev;
struct s5p_cec_dev *cec;
bool needs_hpd = of_property_read_bool(pdev->dev.of_node, "needs-hpd");
int ret;
hdmi_dev = cec_notifier_parse_hdmi_phandle(dev);
if (IS_ERR(hdmi_dev))
return PTR_ERR(hdmi_dev);
cec = devm_kzalloc(&pdev->dev, sizeof(*cec), GFP_KERNEL);
if (!cec)
return -ENOMEM;
cec->dev = dev;
cec->irq = platform_get_irq(pdev, 0);
if (cec->irq < 0)
return cec->irq;
ret = devm_request_threaded_irq(dev, cec->irq, s5p_cec_irq_handler,
s5p_cec_irq_handler_thread, 0, pdev->name, cec);
if (ret)
return ret;
cec->clk = devm_clk_get(dev, "hdmicec");
if (IS_ERR(cec->clk))
return PTR_ERR(cec->clk);
cec->pmu = syscon_regmap_lookup_by_phandle(dev->of_node,
"samsung,syscon-phandle");
if (IS_ERR(cec->pmu))
return -EPROBE_DEFER;
cec->reg = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(cec->reg))
return PTR_ERR(cec->reg);
cec->adap = cec_allocate_adapter(&s5p_cec_adap_ops, cec, CEC_NAME,
CEC_CAP_DEFAULTS | (needs_hpd ? CEC_CAP_NEEDS_HPD : 0) |
CEC_CAP_CONNECTOR_INFO, 1);
ret = PTR_ERR_OR_ZERO(cec->adap);
if (ret)
return ret;
cec->notifier = cec_notifier_cec_adap_register(hdmi_dev, NULL,
cec->adap);
if (!cec->notifier) {
ret = -ENOMEM;
goto err_delete_adapter;
}
ret = cec_register_adapter(cec->adap, &pdev->dev);
if (ret)
goto err_notifier;
platform_set_drvdata(pdev, cec);
pm_runtime_enable(dev);
dev_dbg(dev, "successfully probed\n");
return 0;
err_notifier:
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
err_delete_adapter:
cec_delete_adapter(cec->adap);
return ret;
}
static void s5p_cec_remove(struct platform_device *pdev)
{
struct s5p_cec_dev *cec = platform_get_drvdata(pdev);
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
cec_unregister_adapter(cec->adap);
pm_runtime_disable(&pdev->dev);
}
static int __maybe_unused s5p_cec_runtime_suspend(struct device *dev)
{
struct s5p_cec_dev *cec = dev_get_drvdata(dev);
clk_disable_unprepare(cec->clk);
return 0;
}
static int __maybe_unused s5p_cec_runtime_resume(struct device *dev)
{
struct s5p_cec_dev *cec = dev_get_drvdata(dev);
int ret;
ret = clk_prepare_enable(cec->clk);
if (ret < 0)
return ret;
return 0;
}
static const struct dev_pm_ops s5p_cec_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend,
pm_runtime_force_resume)
SET_RUNTIME_PM_OPS(s5p_cec_runtime_suspend, s5p_cec_runtime_resume,
NULL)
};
static const struct of_device_id s5p_cec_match[] = {
{
.compatible = "samsung,s5p-cec",
},
{},
};
MODULE_DEVICE_TABLE(of, s5p_cec_match);
static struct platform_driver s5p_cec_pdrv = {
.probe = s5p_cec_probe,
.remove = s5p_cec_remove,
.driver = {
.name = CEC_NAME,
.of_match_table = s5p_cec_match,
.pm = &s5p_cec_pm_ops,
},
};
module_platform_driver(s5p_cec_pdrv);
MODULE_AUTHOR("Kamil Debski <kamil@wypas.org>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Samsung S5P CEC driver");
@@ -0,0 +1,76 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* drivers/media/platform/s5p-cec/s5p_cec.h
*
* Samsung S5P HDMI CEC driver
*
* Copyright (c) 2014 Samsung Electronics Co., Ltd.
*/
#ifndef _S5P_CEC_H_
#define _S5P_CEC_H_ __FILE__
#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/pm_runtime.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <media/cec.h>
#include "exynos_hdmi_cec.h"
#include "regs-cec.h"
#include "s5p_cec.h"
#define CEC_NAME "s5p-cec"
#define CEC_STATUS_TX_RUNNING (1 << 0)
#define CEC_STATUS_TX_TRANSFERRING (1 << 1)
#define CEC_STATUS_TX_DONE (1 << 2)
#define CEC_STATUS_TX_ERROR (1 << 3)
#define CEC_STATUS_TX_NACK (1 << 4)
#define CEC_STATUS_TX_BYTES (0xFF << 8)
#define CEC_STATUS_RX_RUNNING (1 << 16)
#define CEC_STATUS_RX_RECEIVING (1 << 17)
#define CEC_STATUS_RX_DONE (1 << 18)
#define CEC_STATUS_RX_ERROR (1 << 19)
#define CEC_STATUS_RX_BCAST (1 << 20)
#define CEC_STATUS_RX_BYTES (0xFF << 24)
#define CEC_WORKER_TX_DONE (1 << 0)
#define CEC_WORKER_RX_MSG (1 << 1)
/* CEC Rx buffer size */
#define CEC_RX_BUFF_SIZE 16
/* CEC Tx buffer size */
#define CEC_TX_BUFF_SIZE 16
enum cec_state {
STATE_IDLE,
STATE_BUSY,
STATE_DONE,
STATE_NACK,
STATE_ERROR
};
struct cec_notifier;
struct s5p_cec_dev {
struct cec_adapter *adap;
struct clk *clk;
struct device *dev;
struct mutex lock;
struct regmap *pmu;
struct cec_notifier *notifier;
int irq;
void __iomem *reg;
enum cec_state rx;
enum cec_state tx;
struct cec_msg msg;
};
#endif /* _S5P_CEC_H_ */
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_SECO) += seco-cec.o
@@ -0,0 +1,792 @@
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
* CEC driver for SECO X86 Boards
*
* Author: Ettore Chimenti <ek5.chimenti@gmail.com>
* Copyright (C) 2018, SECO SpA.
* Copyright (C) 2018, Aidilab Srl.
*/
#include <linux/module.h>
#include <linux/acpi.h>
#include <linux/delay.h>
#include <linux/dmi.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/pci.h>
#include <linux/platform_device.h>
/* CEC Framework */
#include <media/cec-notifier.h>
#include "seco-cec.h"
struct secocec_data {
struct device *dev;
struct platform_device *pdev;
struct cec_adapter *cec_adap;
struct cec_notifier *notifier;
struct rc_dev *ir;
char ir_input_phys[32];
int irq;
};
#define smb_wr16(cmd, data) smb_word_op(SECOCEC_MICRO_ADDRESS, \
cmd, data, SMBUS_WRITE, NULL)
#define smb_rd16(cmd, res) smb_word_op(SECOCEC_MICRO_ADDRESS, \
cmd, 0, SMBUS_READ, res)
static int smb_word_op(u16 slave_addr, u8 cmd, u16 data,
u8 operation, u16 *result)
{
unsigned int count;
int status = 0;
/* Active wait until ready */
for (count = 0; count <= SMBTIMEOUT; ++count) {
if (!(inb(HSTS) & BRA_INUSE_STS))
break;
udelay(SMB_POLL_UDELAY);
}
if (count > SMBTIMEOUT)
/* Reset the lock instead of failing */
outb(0xff, HSTS);
outb(0x00, HCNT);
outb((u8)(slave_addr & 0xfe) | operation, XMIT_SLVA);
outb(cmd, HCMD);
inb(HCNT);
if (operation == SMBUS_WRITE) {
outb((u8)data, HDAT0);
outb((u8)(data >> 8), HDAT1);
}
outb(BRA_START + BRA_SMB_CMD_WORD_DATA, HCNT);
for (count = 0; count <= SMBTIMEOUT; count++) {
if (!(inb(HSTS) & BRA_HOST_BUSY))
break;
udelay(SMB_POLL_UDELAY);
}
if (count > SMBTIMEOUT) {
status = -EBUSY;
goto err;
}
if (inb(HSTS) & BRA_HSTS_ERR_MASK) {
status = -EIO;
goto err;
}
if (operation == SMBUS_READ)
*result = ((inb(HDAT0) & 0xff) + ((inb(HDAT1) & 0xff) << 8));
err:
outb(0xff, HSTS);
return status;
}
static int secocec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct secocec_data *cec = cec_get_drvdata(adap);
struct device *dev = cec->dev;
u16 val = 0;
int status;
if (enable) {
/* Clear the status register */
status = smb_rd16(SECOCEC_STATUS_REG_1, &val);
if (status)
goto err;
status = smb_wr16(SECOCEC_STATUS_REG_1, val);
if (status)
goto err;
/* Enable the interrupts */
status = smb_rd16(SECOCEC_ENABLE_REG_1, &val);
if (status)
goto err;
status = smb_wr16(SECOCEC_ENABLE_REG_1,
val | SECOCEC_ENABLE_REG_1_CEC);
if (status)
goto err;
dev_dbg(dev, "Device enabled\n");
} else {
/* Clear the status register */
status = smb_rd16(SECOCEC_STATUS_REG_1, &val);
status = smb_wr16(SECOCEC_STATUS_REG_1, val);
/* Disable the interrupts */
status = smb_rd16(SECOCEC_ENABLE_REG_1, &val);
status = smb_wr16(SECOCEC_ENABLE_REG_1, val &
~SECOCEC_ENABLE_REG_1_CEC &
~SECOCEC_ENABLE_REG_1_IR);
dev_dbg(dev, "Device disabled\n");
}
return 0;
err:
return status;
}
static int secocec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
u16 enable_val = 0;
int status;
/* Disable device */
status = smb_rd16(SECOCEC_ENABLE_REG_1, &enable_val);
if (status)
return status;
status = smb_wr16(SECOCEC_ENABLE_REG_1,
enable_val & ~SECOCEC_ENABLE_REG_1_CEC);
if (status)
return status;
/* Write logical address
* NOTE: CEC_LOG_ADDR_INVALID is mapped to the 'Unregistered' LA
*/
status = smb_wr16(SECOCEC_DEVICE_LA, logical_addr & 0xf);
if (status)
return status;
/* Re-enable device */
status = smb_wr16(SECOCEC_ENABLE_REG_1,
enable_val | SECOCEC_ENABLE_REG_1_CEC);
if (status)
return status;
return 0;
}
static int secocec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
u16 payload_len, payload_id_len, destination, val = 0;
u8 *payload_msg;
int status;
u8 i;
/* Device msg len already accounts for header */
payload_id_len = msg->len - 1;
/* Send data length */
status = smb_wr16(SECOCEC_WRITE_DATA_LENGTH, payload_id_len);
if (status)
goto err;
/* Send Operation ID if present */
if (payload_id_len > 0) {
status = smb_wr16(SECOCEC_WRITE_OPERATION_ID, msg->msg[1]);
if (status)
goto err;
}
/* Send data if present */
if (payload_id_len > 1) {
/* Only data; */
payload_len = msg->len - 2;
payload_msg = &msg->msg[2];
/* Copy message into registers */
for (i = 0; i < payload_len; i += 2) {
/* hi byte */
val = payload_msg[i + 1] << 8;
/* lo byte */
val |= payload_msg[i];
status = smb_wr16(SECOCEC_WRITE_DATA_00 + i / 2, val);
if (status)
goto err;
}
}
/* Send msg source/destination and fire msg */
destination = msg->msg[0];
status = smb_wr16(SECOCEC_WRITE_BYTE0, destination);
if (status)
goto err;
return 0;
err:
return status;
}
static void secocec_tx_done(struct cec_adapter *adap, u16 status_val)
{
if (status_val & SECOCEC_STATUS_TX_ERROR_MASK) {
if (status_val & SECOCEC_STATUS_TX_NACK_ERROR)
cec_transmit_attempt_done(adap, CEC_TX_STATUS_NACK);
else
cec_transmit_attempt_done(adap, CEC_TX_STATUS_ERROR);
} else {
cec_transmit_attempt_done(adap, CEC_TX_STATUS_OK);
}
/* Reset status reg */
status_val = SECOCEC_STATUS_TX_ERROR_MASK |
SECOCEC_STATUS_MSG_SENT_MASK |
SECOCEC_STATUS_TX_NACK_ERROR;
smb_wr16(SECOCEC_STATUS, status_val);
}
static void secocec_rx_done(struct cec_adapter *adap, u16 status_val)
{
struct secocec_data *cec = cec_get_drvdata(adap);
struct device *dev = cec->dev;
struct cec_msg msg = { };
bool flag_overflow = false;
u8 payload_len, i = 0;
u8 *payload_msg;
u16 val = 0;
int status;
if (status_val & SECOCEC_STATUS_RX_OVERFLOW_MASK) {
/* NOTE: Untested, it also might not be necessary */
dev_warn(dev, "Received more than 16 bytes. Discarding\n");
flag_overflow = true;
}
if (status_val & SECOCEC_STATUS_RX_ERROR_MASK) {
dev_warn(dev, "Message received with errors. Discarding\n");
status = -EIO;
goto rxerr;
}
/* Read message length */
status = smb_rd16(SECOCEC_READ_DATA_LENGTH, &val);
if (status)
return;
/* Device msg len already accounts for the header */
msg.len = min(val + 1, CEC_MAX_MSG_SIZE);
/* Read logical address */
status = smb_rd16(SECOCEC_READ_BYTE0, &val);
if (status)
return;
/* device stores source LA and destination */
msg.msg[0] = val;
/* Read operation ID */
status = smb_rd16(SECOCEC_READ_OPERATION_ID, &val);
if (status)
return;
msg.msg[1] = val;
/* Read data if present */
if (msg.len > 1) {
payload_len = msg.len - 2;
payload_msg = &msg.msg[2];
/* device stores 2 bytes in every 16-bit val */
for (i = 0; i < payload_len; i += 2) {
status = smb_rd16(SECOCEC_READ_DATA_00 + i / 2, &val);
if (status)
return;
/* low byte, skipping header */
payload_msg[i] = val & 0x00ff;
/* hi byte */
payload_msg[i + 1] = (val & 0xff00) >> 8;
}
}
cec_received_msg(cec->cec_adap, &msg);
/* Reset status reg */
status_val = SECOCEC_STATUS_MSG_RECEIVED_MASK;
if (flag_overflow)
status_val |= SECOCEC_STATUS_RX_OVERFLOW_MASK;
status = smb_wr16(SECOCEC_STATUS, status_val);
return;
rxerr:
/* Reset error reg */
status_val = SECOCEC_STATUS_MSG_RECEIVED_MASK |
SECOCEC_STATUS_RX_ERROR_MASK;
if (flag_overflow)
status_val |= SECOCEC_STATUS_RX_OVERFLOW_MASK;
smb_wr16(SECOCEC_STATUS, status_val);
}
static const struct cec_adap_ops secocec_cec_adap_ops = {
/* Low-level callbacks */
.adap_enable = secocec_adap_enable,
.adap_log_addr = secocec_adap_log_addr,
.adap_transmit = secocec_adap_transmit,
};
#ifdef CONFIG_CEC_SECO_RC
static int secocec_ir_probe(void *priv)
{
struct secocec_data *cec = priv;
struct device *dev = cec->dev;
int status;
u16 val;
/* Prepare the RC input device */
cec->ir = devm_rc_allocate_device(dev, RC_DRIVER_SCANCODE);
if (!cec->ir)
return -ENOMEM;
snprintf(cec->ir_input_phys, sizeof(cec->ir_input_phys),
"%s/input0", dev_name(dev));
cec->ir->device_name = dev_name(dev);
cec->ir->input_phys = cec->ir_input_phys;
cec->ir->input_id.bustype = BUS_HOST;
cec->ir->input_id.vendor = 0;
cec->ir->input_id.product = 0;
cec->ir->input_id.version = 1;
cec->ir->driver_name = SECOCEC_DEV_NAME;
cec->ir->allowed_protocols = RC_PROTO_BIT_RC5;
cec->ir->priv = cec;
cec->ir->map_name = RC_MAP_HAUPPAUGE;
cec->ir->timeout = MS_TO_US(100);
/* Clear the status register */
status = smb_rd16(SECOCEC_STATUS_REG_1, &val);
if (status != 0)
goto err;
status = smb_wr16(SECOCEC_STATUS_REG_1, val);
if (status != 0)
goto err;
/* Enable the interrupts */
status = smb_rd16(SECOCEC_ENABLE_REG_1, &val);
if (status != 0)
goto err;
status = smb_wr16(SECOCEC_ENABLE_REG_1,
val | SECOCEC_ENABLE_REG_1_IR);
if (status != 0)
goto err;
dev_dbg(dev, "IR enabled\n");
status = devm_rc_register_device(dev, cec->ir);
if (status) {
dev_err(dev, "Failed to prepare input device\n");
cec->ir = NULL;
goto err;
}
return 0;
err:
smb_rd16(SECOCEC_ENABLE_REG_1, &val);
smb_wr16(SECOCEC_ENABLE_REG_1,
val & ~SECOCEC_ENABLE_REG_1_IR);
dev_dbg(dev, "IR disabled\n");
return status;
}
static int secocec_ir_rx(struct secocec_data *priv)
{
struct secocec_data *cec = priv;
struct device *dev = cec->dev;
u16 val, status, key, addr, toggle;
if (!cec->ir)
return -ENODEV;
status = smb_rd16(SECOCEC_IR_READ_DATA, &val);
if (status != 0)
goto err;
key = val & SECOCEC_IR_COMMAND_MASK;
addr = (val & SECOCEC_IR_ADDRESS_MASK) >> SECOCEC_IR_ADDRESS_SHL;
toggle = (val & SECOCEC_IR_TOGGLE_MASK) >> SECOCEC_IR_TOGGLE_SHL;
rc_keydown(cec->ir, RC_PROTO_RC5, RC_SCANCODE_RC5(addr, key), toggle);
dev_dbg(dev, "IR key pressed: 0x%02x addr 0x%02x toggle 0x%02x\n", key,
addr, toggle);
return 0;
err:
dev_err(dev, "IR Receive message failed (%d)\n", status);
return -EIO;
}
#else
static void secocec_ir_rx(struct secocec_data *priv)
{
}
static int secocec_ir_probe(void *priv)
{
return 0;
}
#endif
static irqreturn_t secocec_irq_handler(int irq, void *priv)
{
struct secocec_data *cec = priv;
struct device *dev = cec->dev;
u16 status_val, cec_val, val = 0;
int status;
/* Read status register */
status = smb_rd16(SECOCEC_STATUS_REG_1, &status_val);
if (status)
goto err;
if (status_val & SECOCEC_STATUS_REG_1_CEC) {
/* Read CEC status register */
status = smb_rd16(SECOCEC_STATUS, &cec_val);
if (status)
goto err;
if (cec_val & SECOCEC_STATUS_MSG_RECEIVED_MASK)
secocec_rx_done(cec->cec_adap, cec_val);
if (cec_val & SECOCEC_STATUS_MSG_SENT_MASK)
secocec_tx_done(cec->cec_adap, cec_val);
if ((~cec_val & SECOCEC_STATUS_MSG_SENT_MASK) &&
(~cec_val & SECOCEC_STATUS_MSG_RECEIVED_MASK))
dev_warn_once(dev,
"Message not received or sent, but interrupt fired");
val = SECOCEC_STATUS_REG_1_CEC;
}
if (status_val & SECOCEC_STATUS_REG_1_IR) {
val |= SECOCEC_STATUS_REG_1_IR;
secocec_ir_rx(cec);
}
/* Reset status register */
status = smb_wr16(SECOCEC_STATUS_REG_1, val);
if (status)
goto err;
return IRQ_HANDLED;
err:
dev_err_once(dev, "IRQ: R/W SMBus operation failed %d\n", status);
/* Reset status register */
val = SECOCEC_STATUS_REG_1_CEC | SECOCEC_STATUS_REG_1_IR;
smb_wr16(SECOCEC_STATUS_REG_1, val);
return IRQ_HANDLED;
}
struct cec_dmi_match {
const char *sys_vendor;
const char *product_name;
const char *devname;
const char *conn;
};
static const struct cec_dmi_match secocec_dmi_match_table[] = {
/* UDOO X86 */
{ "SECO", "UDOO x86", "0000:00:02.0", "Port B" },
};
static struct device *secocec_cec_find_hdmi_dev(struct device *dev,
const char **conn)
{
int i;
for (i = 0 ; i < ARRAY_SIZE(secocec_dmi_match_table) ; ++i) {
const struct cec_dmi_match *m = &secocec_dmi_match_table[i];
if (dmi_match(DMI_SYS_VENDOR, m->sys_vendor) &&
dmi_match(DMI_PRODUCT_NAME, m->product_name)) {
struct device *d;
/* Find the device, bail out if not yet registered */
d = bus_find_device_by_name(&pci_bus_type, NULL,
m->devname);
if (!d)
return ERR_PTR(-EPROBE_DEFER);
put_device(d);
*conn = m->conn;
return d;
}
}
return ERR_PTR(-EINVAL);
}
static int secocec_acpi_probe(struct secocec_data *sdev)
{
struct device *dev = sdev->dev;
struct gpio_desc *gpio;
int irq = 0;
gpio = devm_gpiod_get(dev, NULL, GPIOD_IN);
if (IS_ERR(gpio)) {
dev_err(dev, "Cannot request interrupt gpio\n");
return PTR_ERR(gpio);
}
irq = gpiod_to_irq(gpio);
if (irq < 0) {
dev_err(dev, "Cannot find valid irq\n");
return -ENODEV;
}
dev_dbg(dev, "irq-gpio is bound to IRQ %d\n", irq);
sdev->irq = irq;
return 0;
}
static int secocec_probe(struct platform_device *pdev)
{
struct secocec_data *secocec;
struct device *dev = &pdev->dev;
struct device *hdmi_dev;
const char *conn = NULL;
int ret;
u16 val;
hdmi_dev = secocec_cec_find_hdmi_dev(&pdev->dev, &conn);
if (IS_ERR(hdmi_dev))
return PTR_ERR(hdmi_dev);
secocec = devm_kzalloc(dev, sizeof(*secocec), GFP_KERNEL);
if (!secocec)
return -ENOMEM;
dev_set_drvdata(dev, secocec);
/* Request SMBus regions */
if (!request_muxed_region(BRA_SMB_BASE_ADDR, 7, "CEC00001")) {
dev_err(dev, "Request memory region failed\n");
return -ENXIO;
}
secocec->pdev = pdev;
secocec->dev = dev;
if (!has_acpi_companion(dev)) {
dev_dbg(dev, "Cannot find any ACPI companion\n");
ret = -ENODEV;
goto err;
}
ret = secocec_acpi_probe(secocec);
if (ret) {
dev_err(dev, "Cannot assign gpio to IRQ\n");
ret = -ENODEV;
goto err;
}
/* Firmware version check */
ret = smb_rd16(SECOCEC_VERSION, &val);
if (ret) {
dev_err(dev, "Cannot check fw version\n");
goto err;
}
if (val < SECOCEC_LATEST_FW) {
dev_err(dev, "CEC Firmware not supported (v.%04x). Use ver > v.%04x\n",
val, SECOCEC_LATEST_FW);
ret = -EINVAL;
goto err;
}
ret = devm_request_threaded_irq(dev,
secocec->irq,
NULL,
secocec_irq_handler,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
dev_name(&pdev->dev), secocec);
if (ret) {
dev_err(dev, "Cannot request IRQ %d\n", secocec->irq);
ret = -EIO;
goto err;
}
/* Allocate CEC adapter */
secocec->cec_adap = cec_allocate_adapter(&secocec_cec_adap_ops,
secocec,
dev_name(dev),
CEC_CAP_DEFAULTS |
CEC_CAP_CONNECTOR_INFO,
SECOCEC_MAX_ADDRS);
if (IS_ERR(secocec->cec_adap)) {
ret = PTR_ERR(secocec->cec_adap);
goto err;
}
secocec->notifier = cec_notifier_cec_adap_register(hdmi_dev, conn,
secocec->cec_adap);
if (!secocec->notifier) {
ret = -ENOMEM;
goto err_delete_adapter;
}
ret = cec_register_adapter(secocec->cec_adap, dev);
if (ret)
goto err_notifier;
ret = secocec_ir_probe(secocec);
if (ret)
goto err_unregister_adapter;
platform_set_drvdata(pdev, secocec);
dev_dbg(dev, "Device registered\n");
return ret;
err_unregister_adapter:
cec_notifier_cec_adap_unregister(secocec->notifier, secocec->cec_adap);
cec_unregister_adapter(secocec->cec_adap);
goto err;
err_notifier:
cec_notifier_cec_adap_unregister(secocec->notifier, secocec->cec_adap);
err_delete_adapter:
cec_delete_adapter(secocec->cec_adap);
err:
release_region(BRA_SMB_BASE_ADDR, 7);
dev_err(dev, "%s device probe failed\n", dev_name(dev));
return ret;
}
static void secocec_remove(struct platform_device *pdev)
{
struct secocec_data *secocec = platform_get_drvdata(pdev);
u16 val;
if (secocec->ir) {
smb_rd16(SECOCEC_ENABLE_REG_1, &val);
smb_wr16(SECOCEC_ENABLE_REG_1, val & ~SECOCEC_ENABLE_REG_1_IR);
dev_dbg(&pdev->dev, "IR disabled\n");
}
cec_notifier_cec_adap_unregister(secocec->notifier, secocec->cec_adap);
cec_unregister_adapter(secocec->cec_adap);
release_region(BRA_SMB_BASE_ADDR, 7);
dev_dbg(&pdev->dev, "CEC device removed\n");
}
#ifdef CONFIG_PM_SLEEP
static int secocec_suspend(struct device *dev)
{
int status;
u16 val;
dev_dbg(dev, "Device going to suspend, disabling\n");
/* Clear the status register */
status = smb_rd16(SECOCEC_STATUS_REG_1, &val);
if (status)
goto err;
status = smb_wr16(SECOCEC_STATUS_REG_1, val);
if (status)
goto err;
/* Disable the interrupts */
status = smb_rd16(SECOCEC_ENABLE_REG_1, &val);
if (status)
goto err;
status = smb_wr16(SECOCEC_ENABLE_REG_1, val &
~SECOCEC_ENABLE_REG_1_CEC & ~SECOCEC_ENABLE_REG_1_IR);
if (status)
goto err;
return 0;
err:
dev_err(dev, "Suspend failed: %d\n", status);
return status;
}
static int secocec_resume(struct device *dev)
{
int status;
u16 val;
dev_dbg(dev, "Resuming device from suspend\n");
/* Clear the status register */
status = smb_rd16(SECOCEC_STATUS_REG_1, &val);
if (status)
goto err;
status = smb_wr16(SECOCEC_STATUS_REG_1, val);
if (status)
goto err;
/* Enable the interrupts */
status = smb_rd16(SECOCEC_ENABLE_REG_1, &val);
if (status)
goto err;
status = smb_wr16(SECOCEC_ENABLE_REG_1, val | SECOCEC_ENABLE_REG_1_CEC);
if (status)
goto err;
dev_dbg(dev, "Device resumed from suspend\n");
return 0;
err:
dev_err(dev, "Resume failed: %d\n", status);
return status;
}
static SIMPLE_DEV_PM_OPS(secocec_pm_ops, secocec_suspend, secocec_resume);
#define SECOCEC_PM_OPS (&secocec_pm_ops)
#else
#define SECOCEC_PM_OPS NULL
#endif
#ifdef CONFIG_ACPI
static const struct acpi_device_id secocec_acpi_match[] = {
{"CEC00001", 0},
{},
};
MODULE_DEVICE_TABLE(acpi, secocec_acpi_match);
#endif
static struct platform_driver secocec_driver = {
.driver = {
.name = SECOCEC_DEV_NAME,
.acpi_match_table = ACPI_PTR(secocec_acpi_match),
.pm = SECOCEC_PM_OPS,
},
.probe = secocec_probe,
.remove = secocec_remove,
};
module_platform_driver(secocec_driver);
MODULE_DESCRIPTION("SECO CEC X86 Driver");
MODULE_AUTHOR("Ettore Chimenti <ek5.chimenti@gmail.com>");
MODULE_LICENSE("Dual BSD/GPL");
@@ -0,0 +1,141 @@
/* SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause */
/*
* SECO X86 Boards CEC register defines
*
* Author: Ettore Chimenti <ek5.chimenti@gmail.com>
* Copyright (C) 2018, SECO Spa.
* Copyright (C) 2018, Aidilab Srl.
*/
#ifndef __SECO_CEC_H__
#define __SECO_CEC_H__
#define SECOCEC_MAX_ADDRS 1
#define SECOCEC_DEV_NAME "secocec"
#define SECOCEC_LATEST_FW 0x0f0b
#define SMBTIMEOUT 0xfff
#define SMB_POLL_UDELAY 10
#define SMBUS_WRITE 0
#define SMBUS_READ 1
#define CMD_BYTE_DATA 0
#define CMD_WORD_DATA 1
/*
* SMBus definitons for Braswell
*/
#define BRA_DONE_STATUS BIT(7)
#define BRA_INUSE_STS BIT(6)
#define BRA_FAILED_OP BIT(4)
#define BRA_BUS_ERR BIT(3)
#define BRA_DEV_ERR BIT(2)
#define BRA_INTR BIT(1)
#define BRA_HOST_BUSY BIT(0)
#define BRA_HSTS_ERR_MASK (BRA_FAILED_OP | BRA_BUS_ERR | BRA_DEV_ERR)
#define BRA_PEC_EN BIT(7)
#define BRA_START BIT(6)
#define BRA_LAST__BYTE BIT(5)
#define BRA_INTREN BIT(0)
#define BRA_SMB_CMD (7 << 2)
#define BRA_SMB_CMD_QUICK (0 << 2)
#define BRA_SMB_CMD_BYTE (1 << 2)
#define BRA_SMB_CMD_BYTE_DATA (2 << 2)
#define BRA_SMB_CMD_WORD_DATA (3 << 2)
#define BRA_SMB_CMD_PROCESS_CALL (4 << 2)
#define BRA_SMB_CMD_BLOCK (5 << 2)
#define BRA_SMB_CMD_I2CREAD (6 << 2)
#define BRA_SMB_CMD_BLOCK_PROCESS (7 << 2)
#define BRA_SMB_BASE_ADDR 0x2040
#define HSTS (BRA_SMB_BASE_ADDR + 0)
#define HCNT (BRA_SMB_BASE_ADDR + 2)
#define HCMD (BRA_SMB_BASE_ADDR + 3)
#define XMIT_SLVA (BRA_SMB_BASE_ADDR + 4)
#define HDAT0 (BRA_SMB_BASE_ADDR + 5)
#define HDAT1 (BRA_SMB_BASE_ADDR + 6)
/*
* Microcontroller Address
*/
#define SECOCEC_MICRO_ADDRESS 0x40
/*
* STM32 SMBus Registers
*/
#define SECOCEC_VERSION 0x00
#define SECOCEC_ENABLE_REG_1 0x01
#define SECOCEC_ENABLE_REG_2 0x02
#define SECOCEC_STATUS_REG_1 0x03
#define SECOCEC_STATUS_REG_2 0x04
#define SECOCEC_STATUS 0x28
#define SECOCEC_DEVICE_LA 0x29
#define SECOCEC_READ_OPERATION_ID 0x2a
#define SECOCEC_READ_DATA_LENGTH 0x2b
#define SECOCEC_READ_DATA_00 0x2c
#define SECOCEC_READ_DATA_02 0x2d
#define SECOCEC_READ_DATA_04 0x2e
#define SECOCEC_READ_DATA_06 0x2f
#define SECOCEC_READ_DATA_08 0x30
#define SECOCEC_READ_DATA_10 0x31
#define SECOCEC_READ_DATA_12 0x32
#define SECOCEC_READ_BYTE0 0x33
#define SECOCEC_WRITE_OPERATION_ID 0x34
#define SECOCEC_WRITE_DATA_LENGTH 0x35
#define SECOCEC_WRITE_DATA_00 0x36
#define SECOCEC_WRITE_DATA_02 0x37
#define SECOCEC_WRITE_DATA_04 0x38
#define SECOCEC_WRITE_DATA_06 0x39
#define SECOCEC_WRITE_DATA_08 0x3a
#define SECOCEC_WRITE_DATA_10 0x3b
#define SECOCEC_WRITE_DATA_12 0x3c
#define SECOCEC_WRITE_BYTE0 0x3d
#define SECOCEC_IR_READ_DATA 0x3e
/*
* IR
*/
#define SECOCEC_IR_COMMAND_MASK 0x007F
#define SECOCEC_IR_COMMAND_SHL 0
#define SECOCEC_IR_ADDRESS_MASK 0x1F00
#define SECOCEC_IR_ADDRESS_SHL 8
#define SECOCEC_IR_TOGGLE_MASK 0x8000
#define SECOCEC_IR_TOGGLE_SHL 15
/*
* Enabling register
*/
#define SECOCEC_ENABLE_REG_1_CEC 0x1000
#define SECOCEC_ENABLE_REG_1_IR 0x2000
#define SECOCEC_ENABLE_REG_1_IR_PASSTHROUGH 0x4000
/*
* Status register
*/
#define SECOCEC_STATUS_REG_1_CEC SECOCEC_ENABLE_REG_1_CEC
#define SECOCEC_STATUS_REG_1_IR SECOCEC_ENABLE_REG_1_IR
#define SECOCEC_STATUS_REG_1_IR_PASSTHR SECOCEC_ENABLE_REG_1_IR_PASSTHR
/*
* Status data
*/
#define SECOCEC_STATUS_MSG_RECEIVED_MASK BIT(0)
#define SECOCEC_STATUS_RX_ERROR_MASK BIT(1)
#define SECOCEC_STATUS_MSG_SENT_MASK BIT(2)
#define SECOCEC_STATUS_TX_ERROR_MASK BIT(3)
#define SECOCEC_STATUS_TX_NACK_ERROR BIT(4)
#define SECOCEC_STATUS_RX_OVERFLOW_MASK BIT(5)
#endif /* __SECO_CEC_H__ */
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_STI) += stih-cec.o
@@ -0,0 +1,397 @@
// SPDX-License-Identifier: GPL-2.0
/*
* STIH4xx CEC driver
* Copyright (C) STMicroelectronics SA 2016
*
*/
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/mfd/syscon.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <media/cec.h>
#include <media/cec-notifier.h>
#define CEC_NAME "stih-cec"
/* CEC registers */
#define CEC_CLK_DIV 0x0
#define CEC_CTRL 0x4
#define CEC_IRQ_CTRL 0x8
#define CEC_STATUS 0xC
#define CEC_EXT_STATUS 0x10
#define CEC_TX_CTRL 0x14
#define CEC_FREE_TIME_THRESH 0x18
#define CEC_BIT_TOUT_THRESH 0x1C
#define CEC_BIT_PULSE_THRESH 0x20
#define CEC_DATA 0x24
#define CEC_TX_ARRAY_CTRL 0x28
#define CEC_CTRL2 0x2C
#define CEC_TX_ERROR_STS 0x30
#define CEC_ADDR_TABLE 0x34
#define CEC_DATA_ARRAY_CTRL 0x38
#define CEC_DATA_ARRAY_STATUS 0x3C
#define CEC_TX_DATA_BASE 0x40
#define CEC_TX_DATA_TOP 0x50
#define CEC_TX_DATA_SIZE 0x1
#define CEC_RX_DATA_BASE 0x54
#define CEC_RX_DATA_TOP 0x64
#define CEC_RX_DATA_SIZE 0x1
/* CEC_CTRL2 */
#define CEC_LINE_INACTIVE_EN BIT(0)
#define CEC_AUTO_BUS_ERR_EN BIT(1)
#define CEC_STOP_ON_ARB_ERR_EN BIT(2)
#define CEC_TX_REQ_WAIT_EN BIT(3)
/* CEC_DATA_ARRAY_CTRL */
#define CEC_TX_ARRAY_EN BIT(0)
#define CEC_RX_ARRAY_EN BIT(1)
#define CEC_TX_ARRAY_RESET BIT(2)
#define CEC_RX_ARRAY_RESET BIT(3)
#define CEC_TX_N_OF_BYTES_IRQ_EN BIT(4)
#define CEC_TX_STOP_ON_NACK BIT(7)
/* CEC_TX_ARRAY_CTRL */
#define CEC_TX_N_OF_BYTES 0x1F
#define CEC_TX_START BIT(5)
#define CEC_TX_AUTO_SOM_EN BIT(6)
#define CEC_TX_AUTO_EOM_EN BIT(7)
/* CEC_IRQ_CTRL */
#define CEC_TX_DONE_IRQ_EN BIT(0)
#define CEC_ERROR_IRQ_EN BIT(2)
#define CEC_RX_DONE_IRQ_EN BIT(3)
#define CEC_RX_SOM_IRQ_EN BIT(4)
#define CEC_RX_EOM_IRQ_EN BIT(5)
#define CEC_FREE_TIME_IRQ_EN BIT(6)
#define CEC_PIN_STS_IRQ_EN BIT(7)
/* CEC_CTRL */
#define CEC_IN_FILTER_EN BIT(0)
#define CEC_PWR_SAVE_EN BIT(1)
#define CEC_EN BIT(4)
#define CEC_ACK_CTRL BIT(5)
#define CEC_RX_RESET_EN BIT(6)
#define CEC_IGNORE_RX_ERROR BIT(7)
/* CEC_STATUS */
#define CEC_TX_DONE_STS BIT(0)
#define CEC_TX_ACK_GET_STS BIT(1)
#define CEC_ERROR_STS BIT(2)
#define CEC_RX_DONE_STS BIT(3)
#define CEC_RX_SOM_STS BIT(4)
#define CEC_RX_EOM_STS BIT(5)
#define CEC_FREE_TIME_IRQ_STS BIT(6)
#define CEC_PIN_STS BIT(7)
#define CEC_SBIT_TOUT_STS BIT(8)
#define CEC_DBIT_TOUT_STS BIT(9)
#define CEC_LPULSE_ERROR_STS BIT(10)
#define CEC_HPULSE_ERROR_STS BIT(11)
#define CEC_TX_ERROR BIT(12)
#define CEC_TX_ARB_ERROR BIT(13)
#define CEC_RX_ERROR_MIN BIT(14)
#define CEC_RX_ERROR_MAX BIT(15)
/* Signal free time in bit periods (2.4ms) */
#define CEC_PRESENT_INIT_SFT 7
#define CEC_NEW_INIT_SFT 5
#define CEC_RETRANSMIT_SFT 3
/* Constants for CEC_BIT_TOUT_THRESH register */
#define CEC_SBIT_TOUT_47MS BIT(1)
#define CEC_SBIT_TOUT_48MS (BIT(0) | BIT(1))
#define CEC_SBIT_TOUT_50MS BIT(2)
#define CEC_DBIT_TOUT_27MS BIT(0)
#define CEC_DBIT_TOUT_28MS BIT(1)
#define CEC_DBIT_TOUT_29MS (BIT(0) | BIT(1))
/* Constants for CEC_BIT_PULSE_THRESH register */
#define CEC_BIT_LPULSE_03MS BIT(1)
#define CEC_BIT_HPULSE_03MS BIT(3)
/* Constants for CEC_DATA_ARRAY_STATUS register */
#define CEC_RX_N_OF_BYTES 0x1F
#define CEC_TX_N_OF_BYTES_SENT BIT(5)
#define CEC_RX_OVERRUN BIT(6)
struct stih_cec {
struct cec_adapter *adap;
struct device *dev;
struct clk *clk;
void __iomem *regs;
int irq;
u32 irq_status;
struct cec_notifier *notifier;
};
static int stih_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct stih_cec *cec = cec_get_drvdata(adap);
if (enable) {
/* The doc says (input TCLK_PERIOD * CEC_CLK_DIV) = 0.1ms */
unsigned long clk_freq = clk_get_rate(cec->clk);
u32 cec_clk_div = clk_freq / 10000;
writel(cec_clk_div, cec->regs + CEC_CLK_DIV);
/* Configuration of the durations activating a timeout */
writel(CEC_SBIT_TOUT_47MS | (CEC_DBIT_TOUT_28MS << 4),
cec->regs + CEC_BIT_TOUT_THRESH);
/* Configuration of the smallest allowed duration for pulses */
writel(CEC_BIT_LPULSE_03MS | CEC_BIT_HPULSE_03MS,
cec->regs + CEC_BIT_PULSE_THRESH);
/* Minimum received bit period threshold */
writel(BIT(5) | BIT(7), cec->regs + CEC_TX_CTRL);
/* Configuration of transceiver data arrays */
writel(CEC_TX_ARRAY_EN | CEC_RX_ARRAY_EN | CEC_TX_STOP_ON_NACK,
cec->regs + CEC_DATA_ARRAY_CTRL);
/* Configuration of the control bits for CEC Transceiver */
writel(CEC_IN_FILTER_EN | CEC_EN | CEC_RX_RESET_EN,
cec->regs + CEC_CTRL);
/* Clear logical addresses */
writel(0, cec->regs + CEC_ADDR_TABLE);
/* Clear the status register */
writel(0x0, cec->regs + CEC_STATUS);
/* Enable the interrupts */
writel(CEC_TX_DONE_IRQ_EN | CEC_RX_DONE_IRQ_EN |
CEC_RX_SOM_IRQ_EN | CEC_RX_EOM_IRQ_EN |
CEC_ERROR_IRQ_EN,
cec->regs + CEC_IRQ_CTRL);
} else {
/* Clear logical addresses */
writel(0, cec->regs + CEC_ADDR_TABLE);
/* Clear the status register */
writel(0x0, cec->regs + CEC_STATUS);
/* Disable the interrupts */
writel(0, cec->regs + CEC_IRQ_CTRL);
}
return 0;
}
static int stih_cec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
struct stih_cec *cec = cec_get_drvdata(adap);
u32 reg = readl(cec->regs + CEC_ADDR_TABLE);
reg |= 1 << logical_addr;
if (logical_addr == CEC_LOG_ADDR_INVALID)
reg = 0;
writel(reg, cec->regs + CEC_ADDR_TABLE);
return 0;
}
static int stih_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct stih_cec *cec = cec_get_drvdata(adap);
int i;
/* Copy message into registers */
for (i = 0; i < msg->len; i++)
writeb(msg->msg[i], cec->regs + CEC_TX_DATA_BASE + i);
/*
* Start transmission, configure hardware to add start and stop bits
* Signal free time is handled by the hardware
*/
writel(CEC_TX_AUTO_SOM_EN | CEC_TX_AUTO_EOM_EN | CEC_TX_START |
msg->len, cec->regs + CEC_TX_ARRAY_CTRL);
return 0;
}
static void stih_tx_done(struct stih_cec *cec, u32 status)
{
if (status & CEC_TX_ERROR) {
cec_transmit_attempt_done(cec->adap, CEC_TX_STATUS_ERROR);
return;
}
if (status & CEC_TX_ARB_ERROR) {
cec_transmit_attempt_done(cec->adap, CEC_TX_STATUS_ARB_LOST);
return;
}
if (!(status & CEC_TX_ACK_GET_STS)) {
cec_transmit_attempt_done(cec->adap, CEC_TX_STATUS_NACK);
return;
}
cec_transmit_attempt_done(cec->adap, CEC_TX_STATUS_OK);
}
static void stih_rx_done(struct stih_cec *cec, u32 status)
{
struct cec_msg msg = {};
u8 i;
if (status & CEC_RX_ERROR_MIN)
return;
if (status & CEC_RX_ERROR_MAX)
return;
msg.len = readl(cec->regs + CEC_DATA_ARRAY_STATUS) & 0x1f;
if (!msg.len)
return;
if (msg.len > CEC_MAX_MSG_SIZE)
msg.len = CEC_MAX_MSG_SIZE;
for (i = 0; i < msg.len; i++)
msg.msg[i] = readl(cec->regs + CEC_RX_DATA_BASE + i);
cec_received_msg(cec->adap, &msg);
}
static irqreturn_t stih_cec_irq_handler_thread(int irq, void *priv)
{
struct stih_cec *cec = priv;
if (cec->irq_status & CEC_TX_DONE_STS)
stih_tx_done(cec, cec->irq_status);
if (cec->irq_status & CEC_RX_DONE_STS)
stih_rx_done(cec, cec->irq_status);
cec->irq_status = 0;
return IRQ_HANDLED;
}
static irqreturn_t stih_cec_irq_handler(int irq, void *priv)
{
struct stih_cec *cec = priv;
cec->irq_status = readl(cec->regs + CEC_STATUS);
writel(cec->irq_status, cec->regs + CEC_STATUS);
return IRQ_WAKE_THREAD;
}
static const struct cec_adap_ops sti_cec_adap_ops = {
.adap_enable = stih_cec_adap_enable,
.adap_log_addr = stih_cec_adap_log_addr,
.adap_transmit = stih_cec_adap_transmit,
};
static int stih_cec_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct stih_cec *cec;
struct device *hdmi_dev;
int ret;
hdmi_dev = cec_notifier_parse_hdmi_phandle(dev);
if (IS_ERR(hdmi_dev))
return PTR_ERR(hdmi_dev);
cec = devm_kzalloc(dev, sizeof(*cec), GFP_KERNEL);
if (!cec)
return -ENOMEM;
cec->dev = dev;
cec->regs = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(cec->regs))
return PTR_ERR(cec->regs);
cec->irq = platform_get_irq(pdev, 0);
if (cec->irq < 0)
return cec->irq;
ret = devm_request_threaded_irq(dev, cec->irq, stih_cec_irq_handler,
stih_cec_irq_handler_thread, 0,
pdev->name, cec);
if (ret)
return ret;
cec->clk = devm_clk_get(dev, "cec-clk");
if (IS_ERR(cec->clk)) {
dev_err(dev, "Cannot get cec clock\n");
return PTR_ERR(cec->clk);
}
cec->adap = cec_allocate_adapter(&sti_cec_adap_ops, cec, CEC_NAME,
CEC_CAP_DEFAULTS |
CEC_CAP_CONNECTOR_INFO,
CEC_MAX_LOG_ADDRS);
ret = PTR_ERR_OR_ZERO(cec->adap);
if (ret)
return ret;
cec->notifier = cec_notifier_cec_adap_register(hdmi_dev, NULL,
cec->adap);
if (!cec->notifier) {
ret = -ENOMEM;
goto err_delete_adapter;
}
ret = cec_register_adapter(cec->adap, &pdev->dev);
if (ret)
goto err_notifier;
platform_set_drvdata(pdev, cec);
return 0;
err_notifier:
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
err_delete_adapter:
cec_delete_adapter(cec->adap);
return ret;
}
static void stih_cec_remove(struct platform_device *pdev)
{
struct stih_cec *cec = platform_get_drvdata(pdev);
cec_notifier_cec_adap_unregister(cec->notifier, cec->adap);
cec_unregister_adapter(cec->adap);
}
static const struct of_device_id stih_cec_match[] = {
{
.compatible = "st,stih-cec",
},
{},
};
MODULE_DEVICE_TABLE(of, stih_cec_match);
static struct platform_driver stih_cec_pdrv = {
.probe = stih_cec_probe,
.remove = stih_cec_remove,
.driver = {
.name = CEC_NAME,
.of_match_table = stih_cec_match,
},
};
module_platform_driver(stih_cec_pdrv);
MODULE_AUTHOR("Benjamin Gaignard <benjamin.gaignard@linaro.org>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("STIH4xx CEC driver");
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_STM32) += stm32-cec.o
@@ -0,0 +1,375 @@
// SPDX-License-Identifier: GPL-2.0
/*
* STM32 CEC driver
* Copyright (C) STMicroelectronics SA 2017
*
*/
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
#include <media/cec.h>
#define CEC_NAME "stm32-cec"
/* CEC registers */
#define CEC_CR 0x0000 /* Control Register */
#define CEC_CFGR 0x0004 /* ConFiGuration Register */
#define CEC_TXDR 0x0008 /* Rx data Register */
#define CEC_RXDR 0x000C /* Rx data Register */
#define CEC_ISR 0x0010 /* Interrupt and status Register */
#define CEC_IER 0x0014 /* Interrupt enable Register */
#define TXEOM BIT(2)
#define TXSOM BIT(1)
#define CECEN BIT(0)
#define LSTN BIT(31)
#define OAR GENMASK(30, 16)
#define SFTOP BIT(8)
#define BRDNOGEN BIT(7)
#define LBPEGEN BIT(6)
#define BREGEN BIT(5)
#define BRESTP BIT(4)
#define RXTOL BIT(3)
#define SFT GENMASK(2, 0)
#define FULL_CFG (LSTN | SFTOP | BRDNOGEN | LBPEGEN | BREGEN | BRESTP \
| RXTOL)
#define TXACKE BIT(12)
#define TXERR BIT(11)
#define TXUDR BIT(10)
#define TXEND BIT(9)
#define TXBR BIT(8)
#define ARBLST BIT(7)
#define RXACKE BIT(6)
#define RXOVR BIT(2)
#define RXEND BIT(1)
#define RXBR BIT(0)
#define ALL_TX_IT (TXEND | TXBR | TXACKE | TXERR | TXUDR | ARBLST)
#define ALL_RX_IT (RXEND | RXBR | RXACKE | RXOVR)
/*
* 400 ms is the time it takes for one 16 byte message to be
* transferred and 5 is the maximum number of retries. Add
* another 100 ms as a margin.
*/
#define CEC_XFER_TIMEOUT_MS (5 * 400 + 100)
struct stm32_cec {
struct cec_adapter *adap;
struct device *dev;
struct clk *clk_cec;
struct clk *clk_hdmi_cec;
struct reset_control *rstc;
struct regmap *regmap;
int irq;
u32 irq_status;
struct cec_msg rx_msg;
struct cec_msg tx_msg;
int tx_cnt;
};
static void cec_hw_init(struct stm32_cec *cec)
{
regmap_update_bits(cec->regmap, CEC_CR, TXEOM | TXSOM | CECEN, 0);
regmap_update_bits(cec->regmap, CEC_IER, ALL_TX_IT | ALL_RX_IT,
ALL_TX_IT | ALL_RX_IT);
regmap_update_bits(cec->regmap, CEC_CFGR, FULL_CFG, FULL_CFG);
}
static void stm32_tx_done(struct stm32_cec *cec, u32 status)
{
if (status & (TXERR | TXUDR)) {
cec_transmit_done(cec->adap, CEC_TX_STATUS_ERROR,
0, 0, 0, 1);
return;
}
if (status & ARBLST) {
cec_transmit_done(cec->adap, CEC_TX_STATUS_ARB_LOST,
1, 0, 0, 0);
return;
}
if (status & TXACKE) {
cec_transmit_done(cec->adap, CEC_TX_STATUS_NACK,
0, 1, 0, 0);
return;
}
if (cec->irq_status & TXBR) {
/* send next byte */
if (cec->tx_cnt < cec->tx_msg.len)
regmap_write(cec->regmap, CEC_TXDR,
cec->tx_msg.msg[cec->tx_cnt++]);
/* TXEOM is set to command transmission of the last byte */
if (cec->tx_cnt == cec->tx_msg.len)
regmap_update_bits(cec->regmap, CEC_CR, TXEOM, TXEOM);
}
if (cec->irq_status & TXEND)
cec_transmit_done(cec->adap, CEC_TX_STATUS_OK, 0, 0, 0, 0);
}
static void stm32_rx_done(struct stm32_cec *cec, u32 status)
{
if (cec->irq_status & (RXACKE | RXOVR)) {
cec->rx_msg.len = 0;
return;
}
if (cec->irq_status & RXBR) {
u32 val;
regmap_read(cec->regmap, CEC_RXDR, &val);
cec->rx_msg.msg[cec->rx_msg.len++] = val & 0xFF;
}
if (cec->irq_status & RXEND) {
cec_received_msg(cec->adap, &cec->rx_msg);
cec->rx_msg.len = 0;
}
}
static irqreturn_t stm32_cec_irq_thread(int irq, void *arg)
{
struct stm32_cec *cec = arg;
if (cec->irq_status & ALL_TX_IT)
stm32_tx_done(cec, cec->irq_status);
if (cec->irq_status & ALL_RX_IT)
stm32_rx_done(cec, cec->irq_status);
cec->irq_status = 0;
return IRQ_HANDLED;
}
static irqreturn_t stm32_cec_irq_handler(int irq, void *arg)
{
struct stm32_cec *cec = arg;
regmap_read(cec->regmap, CEC_ISR, &cec->irq_status);
regmap_update_bits(cec->regmap, CEC_ISR,
ALL_TX_IT | ALL_RX_IT,
ALL_TX_IT | ALL_RX_IT);
return IRQ_WAKE_THREAD;
}
static int stm32_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct stm32_cec *cec = adap->priv;
int ret = 0;
if (enable) {
ret = clk_enable(cec->clk_cec);
if (ret)
dev_err(cec->dev, "fail to enable cec clock\n");
clk_enable(cec->clk_hdmi_cec);
regmap_update_bits(cec->regmap, CEC_CR, CECEN, CECEN);
} else {
clk_disable(cec->clk_cec);
clk_disable(cec->clk_hdmi_cec);
regmap_update_bits(cec->regmap, CEC_CR, CECEN, 0);
}
return ret;
}
static int stm32_cec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr)
{
struct stm32_cec *cec = adap->priv;
u32 oar = (1 << logical_addr) << 16;
u32 val;
/* Poll every 100µs the register CEC_CR to wait end of transmission */
regmap_read_poll_timeout(cec->regmap, CEC_CR, val, !(val & TXSOM),
100, CEC_XFER_TIMEOUT_MS * 1000);
regmap_update_bits(cec->regmap, CEC_CR, CECEN, 0);
if (logical_addr == CEC_LOG_ADDR_INVALID)
regmap_update_bits(cec->regmap, CEC_CFGR, OAR, 0);
else
regmap_update_bits(cec->regmap, CEC_CFGR, oar, oar);
regmap_update_bits(cec->regmap, CEC_CR, CECEN, CECEN);
return 0;
}
static int stm32_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct stm32_cec *cec = adap->priv;
/* Copy message */
cec->tx_msg = *msg;
cec->tx_cnt = 0;
/*
* If the CEC message consists of only one byte,
* TXEOM must be set before of TXSOM.
*/
if (cec->tx_msg.len == 1)
regmap_update_bits(cec->regmap, CEC_CR, TXEOM, TXEOM);
/* TXSOM is set to command transmission of the first byte */
regmap_update_bits(cec->regmap, CEC_CR, TXSOM, TXSOM);
/* Write the header (first byte of message) */
regmap_write(cec->regmap, CEC_TXDR, cec->tx_msg.msg[0]);
cec->tx_cnt++;
return 0;
}
static const struct cec_adap_ops stm32_cec_adap_ops = {
.adap_enable = stm32_cec_adap_enable,
.adap_log_addr = stm32_cec_adap_log_addr,
.adap_transmit = stm32_cec_adap_transmit,
};
static const struct regmap_config stm32_cec_regmap_cfg = {
.reg_bits = 32,
.val_bits = 32,
.reg_stride = sizeof(u32),
.max_register = 0x14,
};
static int stm32_cec_probe(struct platform_device *pdev)
{
u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_PHYS_ADDR | CEC_MODE_MONITOR_ALL;
struct stm32_cec *cec;
void __iomem *mmio;
int ret;
cec = devm_kzalloc(&pdev->dev, sizeof(*cec), GFP_KERNEL);
if (!cec)
return -ENOMEM;
cec->dev = &pdev->dev;
mmio = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(mmio))
return PTR_ERR(mmio);
cec->regmap = devm_regmap_init_mmio_clk(&pdev->dev, "cec", mmio,
&stm32_cec_regmap_cfg);
if (IS_ERR(cec->regmap))
return PTR_ERR(cec->regmap);
cec->irq = platform_get_irq(pdev, 0);
if (cec->irq < 0)
return cec->irq;
ret = devm_request_threaded_irq(&pdev->dev, cec->irq,
stm32_cec_irq_handler,
stm32_cec_irq_thread,
0,
pdev->name, cec);
if (ret)
return ret;
cec->clk_cec = devm_clk_get(&pdev->dev, "cec");
if (IS_ERR(cec->clk_cec))
return dev_err_probe(&pdev->dev, PTR_ERR(cec->clk_cec),
"Cannot get cec clock\n");
ret = clk_prepare(cec->clk_cec);
if (ret) {
dev_err(&pdev->dev, "Unable to prepare cec clock\n");
return ret;
}
cec->clk_hdmi_cec = devm_clk_get(&pdev->dev, "hdmi-cec");
if (IS_ERR(cec->clk_hdmi_cec) &&
PTR_ERR(cec->clk_hdmi_cec) == -EPROBE_DEFER) {
ret = -EPROBE_DEFER;
goto err_unprepare_cec_clk;
}
if (!IS_ERR(cec->clk_hdmi_cec)) {
ret = clk_prepare(cec->clk_hdmi_cec);
if (ret) {
dev_err(&pdev->dev, "Can't prepare hdmi-cec clock\n");
goto err_unprepare_cec_clk;
}
}
/*
* CEC_CAP_PHYS_ADDR caps should be removed when a cec notifier is
* available for example when a drm driver can provide edid
*/
cec->adap = cec_allocate_adapter(&stm32_cec_adap_ops, cec,
CEC_NAME, caps, CEC_MAX_LOG_ADDRS);
ret = PTR_ERR_OR_ZERO(cec->adap);
if (ret)
goto err_unprepare_hdmi_cec_clk;
ret = cec_register_adapter(cec->adap, &pdev->dev);
if (ret)
goto err_delete_adapter;
cec_hw_init(cec);
platform_set_drvdata(pdev, cec);
return 0;
err_delete_adapter:
cec_delete_adapter(cec->adap);
err_unprepare_hdmi_cec_clk:
clk_unprepare(cec->clk_hdmi_cec);
err_unprepare_cec_clk:
clk_unprepare(cec->clk_cec);
return ret;
}
static void stm32_cec_remove(struct platform_device *pdev)
{
struct stm32_cec *cec = platform_get_drvdata(pdev);
clk_unprepare(cec->clk_cec);
clk_unprepare(cec->clk_hdmi_cec);
cec_unregister_adapter(cec->adap);
}
static const struct of_device_id stm32_cec_of_match[] = {
{ .compatible = "st,stm32-cec" },
{ /* end node */ }
};
MODULE_DEVICE_TABLE(of, stm32_cec_of_match);
static struct platform_driver stm32_cec_driver = {
.probe = stm32_cec_probe,
.remove = stm32_cec_remove,
.driver = {
.name = CEC_NAME,
.of_match_table = stm32_cec_of_match,
},
};
module_platform_driver(stm32_cec_driver);
MODULE_AUTHOR("Benjamin Gaignard <benjamin.gaignard@st.com>");
MODULE_AUTHOR("Yannick Fertre <yannick.fertre@st.com>");
MODULE_DESCRIPTION("STMicroelectronics STM32 Consumer Electronics Control");
MODULE_LICENSE("GPL v2");
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CEC_TEGRA) += tegra_cec.o
@@ -0,0 +1,481 @@
// 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 cec->tegra_cec_irq;
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 void 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);
}
#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 = 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");
@@ -0,0 +1,116 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Tegra CEC register definitions
*
* 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.
*/
#ifndef TEGRA_CEC_H
#define TEGRA_CEC_H
/* CEC registers */
#define TEGRA_CEC_SW_CONTROL 0x000
#define TEGRA_CEC_HW_CONTROL 0x004
#define TEGRA_CEC_INPUT_FILTER 0x008
#define TEGRA_CEC_TX_REGISTER 0x010
#define TEGRA_CEC_RX_REGISTER 0x014
#define TEGRA_CEC_RX_TIMING_0 0x018
#define TEGRA_CEC_RX_TIMING_1 0x01c
#define TEGRA_CEC_RX_TIMING_2 0x020
#define TEGRA_CEC_TX_TIMING_0 0x024
#define TEGRA_CEC_TX_TIMING_1 0x028
#define TEGRA_CEC_TX_TIMING_2 0x02c
#define TEGRA_CEC_INT_STAT 0x030
#define TEGRA_CEC_INT_MASK 0x034
#define TEGRA_CEC_HW_DEBUG_RX 0x038
#define TEGRA_CEC_HW_DEBUG_TX 0x03c
#define TEGRA_CEC_HWCTRL_RX_LADDR_MASK 0x7fff
#define TEGRA_CEC_HWCTRL_RX_LADDR(x) \
((x) & TEGRA_CEC_HWCTRL_RX_LADDR_MASK)
#define TEGRA_CEC_HWCTRL_RX_SNOOP BIT(15)
#define TEGRA_CEC_HWCTRL_RX_NAK_MODE BIT(16)
#define TEGRA_CEC_HWCTRL_TX_NAK_MODE BIT(24)
#define TEGRA_CEC_HWCTRL_FAST_SIM_MODE BIT(30)
#define TEGRA_CEC_HWCTRL_TX_RX_MODE BIT(31)
#define TEGRA_CEC_INPUT_FILTER_MODE BIT(31)
#define TEGRA_CEC_INPUT_FILTER_FIFO_LENGTH_SHIFT 0
#define TEGRA_CEC_TX_REG_DATA_SHIFT 0
#define TEGRA_CEC_TX_REG_EOM BIT(8)
#define TEGRA_CEC_TX_REG_BCAST BIT(12)
#define TEGRA_CEC_TX_REG_START_BIT BIT(16)
#define TEGRA_CEC_TX_REG_RETRY BIT(17)
#define TEGRA_CEC_RX_REGISTER_SHIFT 0
#define TEGRA_CEC_RX_REGISTER_EOM BIT(8)
#define TEGRA_CEC_RX_REGISTER_ACK BIT(9)
#define TEGRA_CEC_RX_TIM0_START_BIT_MAX_LO_TIME_SHIFT 0
#define TEGRA_CEC_RX_TIM0_START_BIT_MIN_LO_TIME_SHIFT 8
#define TEGRA_CEC_RX_TIM0_START_BIT_MAX_DURATION_SHIFT 16
#define TEGRA_CEC_RX_TIM0_START_BIT_MIN_DURATION_SHIFT 24
#define TEGRA_CEC_RX_TIM1_DATA_BIT_MAX_LO_TIME_SHIFT 0
#define TEGRA_CEC_RX_TIM1_DATA_BIT_SAMPLE_TIME_SHIFT 8
#define TEGRA_CEC_RX_TIM1_DATA_BIT_MAX_DURATION_SHIFT 16
#define TEGRA_CEC_RX_TIM1_DATA_BIT_MIN_DURATION_SHIFT 24
#define TEGRA_CEC_RX_TIM2_END_OF_BLOCK_TIME_SHIFT 0
#define TEGRA_CEC_TX_TIM0_START_BIT_LO_TIME_SHIFT 0
#define TEGRA_CEC_TX_TIM0_START_BIT_DURATION_SHIFT 8
#define TEGRA_CEC_TX_TIM0_BUS_XITION_TIME_SHIFT 16
#define TEGRA_CEC_TX_TIM0_BUS_ERROR_LO_TIME_SHIFT 24
#define TEGRA_CEC_TX_TIM1_LO_DATA_BIT_LO_TIME_SHIFT 0
#define TEGRA_CEC_TX_TIM1_HI_DATA_BIT_LO_TIME_SHIFT 8
#define TEGRA_CEC_TX_TIM1_DATA_BIT_DURATION_SHIFT 16
#define TEGRA_CEC_TX_TIM1_ACK_NAK_BIT_SAMPLE_TIME_SHIFT 24
#define TEGRA_CEC_TX_TIM2_BUS_IDLE_TIME_ADDITIONAL_FRAME_SHIFT 0
#define TEGRA_CEC_TX_TIM2_BUS_IDLE_TIME_NEW_FRAME_SHIFT 4
#define TEGRA_CEC_TX_TIM2_BUS_IDLE_TIME_RETRY_FRAME_SHIFT 8
#define TEGRA_CEC_INT_STAT_TX_REGISTER_EMPTY BIT(0)
#define TEGRA_CEC_INT_STAT_TX_REGISTER_UNDERRUN BIT(1)
#define TEGRA_CEC_INT_STAT_TX_FRAME_OR_BLOCK_NAKD BIT(2)
#define TEGRA_CEC_INT_STAT_TX_ARBITRATION_FAILED BIT(3)
#define TEGRA_CEC_INT_STAT_TX_BUS_ANOMALY_DETECTED BIT(4)
#define TEGRA_CEC_INT_STAT_TX_FRAME_TRANSMITTED BIT(5)
#define TEGRA_CEC_INT_STAT_RX_REGISTER_FULL BIT(8)
#define TEGRA_CEC_INT_STAT_RX_REGISTER_OVERRUN BIT(9)
#define TEGRA_CEC_INT_STAT_RX_START_BIT_DETECTED BIT(10)
#define TEGRA_CEC_INT_STAT_RX_BUS_ANOMALY_DETECTED BIT(11)
#define TEGRA_CEC_INT_STAT_RX_BUS_ERROR_DETECTED BIT(12)
#define TEGRA_CEC_INT_STAT_FILTERED_RX_DATA_PIN_TRANSITION_H2L BIT(13)
#define TEGRA_CEC_INT_STAT_FILTERED_RX_DATA_PIN_TRANSITION_L2H BIT(14)
#define TEGRA_CEC_INT_MASK_TX_REGISTER_EMPTY BIT(0)
#define TEGRA_CEC_INT_MASK_TX_REGISTER_UNDERRUN BIT(1)
#define TEGRA_CEC_INT_MASK_TX_FRAME_OR_BLOCK_NAKD BIT(2)
#define TEGRA_CEC_INT_MASK_TX_ARBITRATION_FAILED BIT(3)
#define TEGRA_CEC_INT_MASK_TX_BUS_ANOMALY_DETECTED BIT(4)
#define TEGRA_CEC_INT_MASK_TX_FRAME_TRANSMITTED BIT(5)
#define TEGRA_CEC_INT_MASK_RX_REGISTER_FULL BIT(8)
#define TEGRA_CEC_INT_MASK_RX_REGISTER_OVERRUN BIT(9)
#define TEGRA_CEC_INT_MASK_RX_START_BIT_DETECTED BIT(10)
#define TEGRA_CEC_INT_MASK_RX_BUS_ANOMALY_DETECTED BIT(11)
#define TEGRA_CEC_INT_MASK_RX_BUS_ERROR_DETECTED BIT(12)
#define TEGRA_CEC_INT_MASK_FILTERED_RX_DATA_PIN_TRANSITION_H2L BIT(13)
#define TEGRA_CEC_INT_MASK_FILTERED_RX_DATA_PIN_TRANSITION_L2H BIT(14)
#define TEGRA_CEC_HW_DEBUG_TX_DURATION_COUNT_SHIFT 0
#define TEGRA_CEC_HW_DEBUG_TX_TXBIT_COUNT_SHIFT 17
#define TEGRA_CEC_HW_DEBUG_TX_STATE_SHIFT 21
#define TEGRA_CEC_HW_DEBUG_TX_FORCELOOUT BIT(25)
#define TEGRA_CEC_HW_DEBUG_TX_TXDATABIT_SAMPLE_TIMER BIT(26)
#endif /* TEGRA_CEC_H */
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# USB drivers
if USB_SUPPORT && TTY
source "drivers/media/cec/usb/extron-da-hd-4k-plus/Kconfig"
source "drivers/media/cec/usb/pulse8/Kconfig"
source "drivers/media/cec/usb/rainshadow/Kconfig"
endif
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the CEC USB device drivers.
#
obj-$(CONFIG_USB_EXTRON_DA_HD_4K_PLUS_CEC) += extron-da-hd-4k-plus/
obj-$(CONFIG_USB_PULSE8_CEC) += pulse8/
obj-$(CONFIG_USB_RAINSHADOW_CEC) += rainshadow/
@@ -0,0 +1,14 @@
# SPDX-License-Identifier: GPL-2.0-only
config USB_EXTRON_DA_HD_4K_PLUS_CEC
tristate "Extron DA HD 4K Plus CEC driver"
depends on VIDEO_DEV
depends on USB
depends on USB_ACM
select CEC_CORE
select SERIO
select SERIO_SERPORT
help
This is a CEC driver for the Extron DA HD 4K Plus HDMI Splitter.
To compile this driver as a module, choose M here: the
module will be called extron-da-hd-4k-plus-cec.
@@ -0,0 +1,2 @@
extron-da-hd-4k-plus-cec-objs := extron-da-hd-4k-plus.o cec-splitter.o
obj-$(CONFIG_USB_EXTRON_DA_HD_4K_PLUS_CEC) := extron-da-hd-4k-plus-cec.o
@@ -0,0 +1,657 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#include <media/cec.h>
#include "cec-splitter.h"
/*
* Helper function to reply to a received message with a Feature Abort
* message.
*/
static int cec_feature_abort_reason(struct cec_adapter *adap,
struct cec_msg *msg, u8 reason)
{
struct cec_msg tx_msg = { };
/*
* Don't reply with CEC_MSG_FEATURE_ABORT to a CEC_MSG_FEATURE_ABORT
* message!
*/
if (msg->msg[1] == CEC_MSG_FEATURE_ABORT)
return 0;
/* Don't Feature Abort messages from 'Unregistered' */
if (cec_msg_initiator(msg) == CEC_LOG_ADDR_UNREGISTERED)
return 0;
cec_msg_set_reply_to(&tx_msg, msg);
cec_msg_feature_abort(&tx_msg, msg->msg[1], reason);
return cec_transmit_msg(adap, &tx_msg, false);
}
/* Transmit an Active Source message from this output port to a sink */
static void cec_port_out_active_source(struct cec_splitter_port *p)
{
struct cec_adapter *adap = p->adap;
struct cec_msg msg;
if (!adap->is_configured)
return;
p->is_active_source = true;
cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0);
cec_msg_active_source(&msg, adap->phys_addr);
cec_transmit_msg(adap, &msg, false);
}
/* Transmit Active Source messages from all output ports to the sinks */
static void cec_out_active_source(struct cec_splitter *splitter)
{
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++)
cec_port_out_active_source(splitter->ports[i]);
}
/* Transmit a Standby message from this output port to a sink */
static void cec_port_out_standby(struct cec_splitter_port *p)
{
struct cec_adapter *adap = p->adap;
struct cec_msg msg;
if (!adap->is_configured)
return;
cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0);
cec_msg_standby(&msg);
cec_transmit_msg(adap, &msg, false);
}
/* Transmit Standby messages from all output ports to the sinks */
static void cec_out_standby(struct cec_splitter *splitter)
{
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++)
cec_port_out_standby(splitter->ports[i]);
}
/* Transmit an Image/Text View On message from this output port to a sink */
static void cec_port_out_wakeup(struct cec_splitter_port *p, u8 opcode)
{
struct cec_adapter *adap = p->adap;
u8 la = adap->log_addrs.log_addr[0];
struct cec_msg msg;
if (la == CEC_LOG_ADDR_INVALID)
la = CEC_LOG_ADDR_UNREGISTERED;
cec_msg_init(&msg, la, 0);
msg.len = 2;
msg.msg[1] = opcode;
cec_transmit_msg(adap, &msg, false);
}
/* Transmit Image/Text View On messages from all output ports to the sinks */
static void cec_out_wakeup(struct cec_splitter *splitter, u8 opcode)
{
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++)
cec_port_out_wakeup(splitter->ports[i], opcode);
}
/*
* Update the power state of the unconfigured CEC device to either
* Off or On depending on the current state of the splitter.
* This keeps the outputs in a consistent state.
*/
void cec_splitter_unconfigured_output(struct cec_splitter_port *p)
{
p->video_latency = 1;
p->power_status = p->splitter->is_standby ?
CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON;
/* The adapter was unconfigured, so clear the sequence and ts values */
p->out_give_device_power_status_seq = 0;
p->out_give_device_power_status_ts = ktime_set(0, 0);
p->out_request_current_latency_seq = 0;
p->out_request_current_latency_ts = ktime_set(0, 0);
}
/*
* Update the power state of the newly configured CEC device to either
* Off or On depending on the current state of the splitter.
* This keeps the outputs in a consistent state.
*/
void cec_splitter_configured_output(struct cec_splitter_port *p)
{
p->video_latency = 1;
p->power_status = p->splitter->is_standby ?
CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON;
if (p->splitter->is_standby) {
/*
* Some sinks only obey Standby if it comes from the
* active source.
*/
cec_port_out_active_source(p);
cec_port_out_standby(p);
} else {
cec_port_out_wakeup(p, CEC_MSG_IMAGE_VIEW_ON);
}
}
/* Pass the in_msg on to all output ports */
static void cec_out_passthrough(struct cec_splitter *splitter,
const struct cec_msg *in_msg)
{
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
struct cec_adapter *adap = p->adap;
struct cec_msg msg;
if (!adap->is_configured)
continue;
cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0);
msg.len = in_msg->len;
memcpy(msg.msg + 1, in_msg->msg + 1, msg.len - 1);
cec_transmit_msg(adap, &msg, false);
}
}
/*
* See if all output ports received the Report Current Latency message,
* and if so, transmit the result from the input port to the video source.
*/
static void cec_out_report_current_latency(struct cec_splitter *splitter,
struct cec_adapter *input_adap)
{
struct cec_msg reply = {};
unsigned int reply_lat = 0;
unsigned int cnt = 0;
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
struct cec_adapter *adap = p->adap;
/* Skip unconfigured ports */
if (!adap->is_configured)
continue;
/* Return if a port is still waiting for a reply */
if (p->out_request_current_latency_seq)
return;
reply_lat += p->video_latency - 1;
cnt++;
}
/*
* All ports that can reply, replied, so clear the sequence
* and timestamp values.
*/
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
p->out_request_current_latency_seq = 0;
p->out_request_current_latency_ts = ktime_set(0, 0);
}
/*
* Return if there were no replies or the input port is no longer
* configured.
*/
if (!cnt || !input_adap->is_configured)
return;
/* Reply with the average latency */
reply_lat = 1 + reply_lat / cnt;
cec_msg_init(&reply, input_adap->log_addrs.log_addr[0],
splitter->request_current_latency_dest);
cec_msg_report_current_latency(&reply, input_adap->phys_addr,
reply_lat, 1, 1, 1);
cec_transmit_msg(input_adap, &reply, false);
}
/* Transmit Request Current Latency to all output ports */
static int cec_out_request_current_latency(struct cec_splitter *splitter)
{
ktime_t now = ktime_get();
bool error = true;
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
struct cec_adapter *adap = p->adap;
if (!adap->is_configured) {
/* Clear if not configured */
p->out_request_current_latency_seq = 0;
p->out_request_current_latency_ts = ktime_set(0, 0);
} else if (!p->out_request_current_latency_seq) {
/*
* Keep the old ts if an earlier request is still
* pending. This ensures that the request will
* eventually time out based on the timestamp of
* the first request if the sink is unresponsive.
*/
p->out_request_current_latency_ts = now;
}
}
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
struct cec_adapter *adap = p->adap;
struct cec_msg msg;
if (!adap->is_configured)
continue;
cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0);
cec_msg_request_current_latency(&msg, true, adap->phys_addr);
if (cec_transmit_msg(adap, &msg, false))
continue;
p->out_request_current_latency_seq = msg.sequence | (1U << 31);
error = false;
}
return error ? -ENODEV : 0;
}
/*
* See if all output ports received the Report Power Status message,
* and if so, transmit the result from the input port to the video source.
*/
static void cec_out_report_power_status(struct cec_splitter *splitter,
struct cec_adapter *input_adap)
{
struct cec_msg reply = {};
/* The target power status of the splitter itself */
u8 splitter_pwr = splitter->is_standby ?
CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON;
/*
* The transient power status of the splitter, used if not all
* output report the target power status.
*/
u8 splitter_transient_pwr = splitter->is_standby ?
CEC_OP_POWER_STATUS_TO_STANDBY : CEC_OP_POWER_STATUS_TO_ON;
u8 reply_pwr = splitter_pwr;
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
/* Skip if no sink was found (HPD was low for more than 5s) */
if (!p->found_sink)
continue;
/* Return if a port is still waiting for a reply */
if (p->out_give_device_power_status_seq)
return;
if (p->power_status != splitter_pwr)
reply_pwr = splitter_transient_pwr;
}
/*
* All ports that can reply, replied, so clear the sequence
* and timestamp values.
*/
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
p->out_give_device_power_status_seq = 0;
p->out_give_device_power_status_ts = ktime_set(0, 0);
}
/* Return if the input port is no longer configured. */
if (!input_adap->is_configured)
return;
/* Reply with the new power status */
cec_msg_init(&reply, input_adap->log_addrs.log_addr[0],
splitter->give_device_power_status_dest);
cec_msg_report_power_status(&reply, reply_pwr);
cec_transmit_msg(input_adap, &reply, false);
}
/* Transmit Give Device Power Status to all output ports */
static int cec_out_give_device_power_status(struct cec_splitter *splitter)
{
ktime_t now = ktime_get();
bool error = true;
unsigned int i;
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
struct cec_adapter *adap = p->adap;
/*
* Keep the old ts if an earlier request is still
* pending. This ensures that the request will
* eventually time out based on the timestamp of
* the first request if the sink is unresponsive.
*/
if (adap->is_configured && !p->out_give_device_power_status_seq)
p->out_give_device_power_status_ts = now;
}
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
struct cec_adapter *adap = p->adap;
struct cec_msg msg;
if (!adap->is_configured)
continue;
cec_msg_init(&msg, adap->log_addrs.log_addr[0], 0);
cec_msg_give_device_power_status(&msg, true);
if (cec_transmit_msg(adap, &msg, false))
continue;
p->out_give_device_power_status_seq = msg.sequence | (1U << 31);
error = false;
}
return error ? -ENODEV : 0;
}
/*
* CEC messages received on the HDMI input of the splitter are
* forwarded (if relevant) to the HDMI outputs of the splitter.
*/
int cec_splitter_received_input(struct cec_splitter_port *p, struct cec_msg *msg)
{
if (!cec_msg_status_is_ok(msg))
return 0;
if (msg->len < 2)
return -ENOMSG;
switch (msg->msg[1]) {
case CEC_MSG_DEVICE_VENDOR_ID:
case CEC_MSG_REPORT_POWER_STATUS:
case CEC_MSG_SET_STREAM_PATH:
case CEC_MSG_ROUTING_CHANGE:
case CEC_MSG_REQUEST_ACTIVE_SOURCE:
case CEC_MSG_SYSTEM_AUDIO_MODE_STATUS:
return 0;
case CEC_MSG_STANDBY:
p->splitter->is_standby = true;
cec_out_standby(p->splitter);
return 0;
case CEC_MSG_IMAGE_VIEW_ON:
case CEC_MSG_TEXT_VIEW_ON:
p->splitter->is_standby = false;
cec_out_wakeup(p->splitter, msg->msg[1]);
return 0;
case CEC_MSG_ACTIVE_SOURCE:
cec_out_active_source(p->splitter);
return 0;
case CEC_MSG_SET_SYSTEM_AUDIO_MODE:
cec_out_passthrough(p->splitter, msg);
return 0;
case CEC_MSG_GIVE_DEVICE_POWER_STATUS:
p->splitter->give_device_power_status_dest =
cec_msg_initiator(msg);
if (cec_out_give_device_power_status(p->splitter))
cec_feature_abort_reason(p->adap, msg,
CEC_OP_ABORT_INCORRECT_MODE);
return 0;
case CEC_MSG_REQUEST_CURRENT_LATENCY: {
u16 pa;
p->splitter->request_current_latency_dest =
cec_msg_initiator(msg);
cec_ops_request_current_latency(msg, &pa);
if (pa == p->adap->phys_addr &&
cec_out_request_current_latency(p->splitter))
cec_feature_abort_reason(p->adap, msg,
CEC_OP_ABORT_INCORRECT_MODE);
return 0;
}
default:
return -ENOMSG;
}
return -ENOMSG;
}
void cec_splitter_nb_transmit_canceled_output(struct cec_splitter_port *p,
const struct cec_msg *msg,
struct cec_adapter *input_adap)
{
struct cec_splitter *splitter = p->splitter;
u32 seq = msg->sequence | (1U << 31);
/*
* If this is the result of a failed non-blocking transmit, or it is
* the result of the failed reply to a non-blocking transmit, then
* check if the original transmit was to get the current power status
* or latency and, if so, assume that the remove device is for one
* reason or another unavailable and assume that it is in the same
* power status as the splitter, or has no video latency.
*/
if ((cec_msg_recv_is_tx_result(msg) && !(msg->tx_status & CEC_TX_STATUS_OK)) ||
(cec_msg_recv_is_rx_result(msg) && !(msg->rx_status & CEC_RX_STATUS_OK))) {
u8 tx_op = msg->msg[1];
if (msg->len < 2)
return;
if (cec_msg_recv_is_rx_result(msg) &&
(msg->rx_status & CEC_RX_STATUS_FEATURE_ABORT))
tx_op = msg->msg[2];
switch (tx_op) {
case CEC_MSG_GIVE_DEVICE_POWER_STATUS:
if (p->out_give_device_power_status_seq != seq)
break;
p->out_give_device_power_status_seq = 0;
p->out_give_device_power_status_ts = ktime_set(0, 0);
p->power_status = splitter->is_standby ?
CEC_OP_POWER_STATUS_STANDBY :
CEC_OP_POWER_STATUS_ON;
cec_out_report_power_status(splitter, input_adap);
break;
case CEC_MSG_REQUEST_CURRENT_LATENCY:
if (p->out_request_current_latency_seq != seq)
break;
p->video_latency = 1;
p->out_request_current_latency_seq = 0;
p->out_request_current_latency_ts = ktime_set(0, 0);
cec_out_report_current_latency(splitter, input_adap);
break;
}
return;
}
if (cec_msg_recv_is_tx_result(msg)) {
if (p->out_request_current_latency_seq != seq)
return;
p->out_request_current_latency_ts = ns_to_ktime(msg->tx_ts);
return;
}
}
/*
* CEC messages received on an HDMI output of the splitter
* are processed here.
*/
int cec_splitter_received_output(struct cec_splitter_port *p, struct cec_msg *msg,
struct cec_adapter *input_adap)
{
struct cec_adapter *adap = p->adap;
struct cec_splitter *splitter = p->splitter;
u32 seq = msg->sequence | (1U << 31);
struct cec_msg reply = {};
u16 pa;
if (!adap->is_configured || msg->len < 2)
return -ENOMSG;
switch (msg->msg[1]) {
case CEC_MSG_REPORT_POWER_STATUS: {
u8 pwr;
cec_ops_report_power_status(msg, &pwr);
if (pwr > CEC_OP_POWER_STATUS_TO_STANDBY)
pwr = splitter->is_standby ?
CEC_OP_POWER_STATUS_TO_STANDBY :
CEC_OP_POWER_STATUS_TO_ON;
p->power_status = pwr;
if (p->out_give_device_power_status_seq == seq) {
p->out_give_device_power_status_seq = 0;
p->out_give_device_power_status_ts = ktime_set(0, 0);
}
cec_out_report_power_status(splitter, input_adap);
return 0;
}
case CEC_MSG_REPORT_CURRENT_LATENCY: {
u8 video_lat;
u8 low_lat_mode;
u8 audio_out_comp;
u8 audio_out_delay;
cec_ops_report_current_latency(msg, &pa,
&video_lat, &low_lat_mode,
&audio_out_comp, &audio_out_delay);
if (!video_lat || video_lat >= 252)
video_lat = 1;
p->video_latency = video_lat;
if (p->out_request_current_latency_seq == seq) {
p->out_request_current_latency_seq = 0;
p->out_request_current_latency_ts = ktime_set(0, 0);
}
cec_out_report_current_latency(splitter, input_adap);
return 0;
}
case CEC_MSG_STANDBY:
case CEC_MSG_ROUTING_CHANGE:
case CEC_MSG_GIVE_SYSTEM_AUDIO_MODE_STATUS:
return 0;
case CEC_MSG_ACTIVE_SOURCE:
cec_ops_active_source(msg, &pa);
if (pa == 0)
p->is_active_source = false;
return 0;
case CEC_MSG_REQUEST_ACTIVE_SOURCE:
if (!p->is_active_source)
return 0;
cec_msg_set_reply_to(&reply, msg);
cec_msg_active_source(&reply, adap->phys_addr);
cec_transmit_msg(adap, &reply, false);
return 0;
case CEC_MSG_GIVE_DEVICE_POWER_STATUS:
cec_msg_set_reply_to(&reply, msg);
cec_msg_report_power_status(&reply, splitter->is_standby ?
CEC_OP_POWER_STATUS_STANDBY :
CEC_OP_POWER_STATUS_ON);
cec_transmit_msg(adap, &reply, false);
return 0;
case CEC_MSG_SET_STREAM_PATH:
cec_ops_set_stream_path(msg, &pa);
if (pa == adap->phys_addr) {
cec_msg_set_reply_to(&reply, msg);
cec_msg_active_source(&reply, pa);
cec_transmit_msg(adap, &reply, false);
}
return 0;
default:
return -ENOMSG;
}
return -ENOMSG;
}
/*
* Called every second to check for timed out messages and whether there
* still is a video sink connected or not.
*
* Returns true if sinks were lost.
*/
bool cec_splitter_poll(struct cec_splitter *splitter,
struct cec_adapter *input_adap, bool debug)
{
ktime_t now = ktime_get();
u8 pwr = splitter->is_standby ?
CEC_OP_POWER_STATUS_STANDBY : CEC_OP_POWER_STATUS_ON;
unsigned int max_delay_ms = input_adap->xfer_timeout_ms + 2000;
unsigned int i;
bool res = false;
for (i = 0; i < splitter->num_out_ports; i++) {
struct cec_splitter_port *p = splitter->ports[i];
s64 pwr_delta, lat_delta;
bool pwr_timeout, lat_timeout;
if (!p)
continue;
pwr_delta = ktime_ms_delta(now, p->out_give_device_power_status_ts);
pwr_timeout = p->out_give_device_power_status_seq &&
pwr_delta >= max_delay_ms;
lat_delta = ktime_ms_delta(now, p->out_request_current_latency_ts);
lat_timeout = p->out_request_current_latency_seq &&
lat_delta >= max_delay_ms;
/*
* If the HPD is low for more than 5 seconds, then assume no display
* is connected.
*/
if (p->found_sink && ktime_to_ns(p->lost_sink_ts) &&
ktime_ms_delta(now, p->lost_sink_ts) > 5000) {
if (debug)
dev_info(splitter->dev,
"port %u: HPD low for more than 5s, assume no sink is connected.\n",
p->port);
p->found_sink = false;
p->lost_sink_ts = ktime_set(0, 0);
res = true;
}
/*
* If the power status request timed out, then set the port's
* power status to that of the splitter, ensuring a consistent
* power state.
*/
if (pwr_timeout) {
mutex_lock(&p->adap->lock);
if (debug)
dev_info(splitter->dev,
"port %u: give up on power status for seq %u\n",
p->port,
p->out_give_device_power_status_seq & ~(1 << 31));
p->power_status = pwr;
p->out_give_device_power_status_seq = 0;
p->out_give_device_power_status_ts = ktime_set(0, 0);
mutex_unlock(&p->adap->lock);
cec_out_report_power_status(splitter, input_adap);
}
/*
* If the current latency request timed out, then set the port's
* latency to 1.
*/
if (lat_timeout) {
mutex_lock(&p->adap->lock);
if (debug)
dev_info(splitter->dev,
"port %u: give up on latency for seq %u\n",
p->port,
p->out_request_current_latency_seq & ~(1 << 31));
p->video_latency = 1;
p->out_request_current_latency_seq = 0;
p->out_request_current_latency_ts = ktime_set(0, 0);
mutex_unlock(&p->adap->lock);
cec_out_report_current_latency(splitter, input_adap);
}
}
return res;
}
@@ -0,0 +1,51 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#ifndef _CEC_SPLITTER_H_
#define _CEC_SPLITTER_H_
struct cec_splitter;
#define STATE_CHANGE_MAX_REPEATS 2
struct cec_splitter_port {
struct cec_splitter *splitter;
struct cec_adapter *adap;
unsigned int port;
bool is_active_source;
bool found_sink;
ktime_t lost_sink_ts;
u32 out_request_current_latency_seq;
ktime_t out_request_current_latency_ts;
u8 video_latency;
u32 out_give_device_power_status_seq;
ktime_t out_give_device_power_status_ts;
u8 power_status;
};
struct cec_splitter {
struct device *dev;
unsigned int num_out_ports;
struct cec_splitter_port **ports;
/* High-level splitter state */
u8 request_current_latency_dest;
u8 give_device_power_status_dest;
bool is_standby;
};
void cec_splitter_unconfigured_output(struct cec_splitter_port *port);
void cec_splitter_configured_output(struct cec_splitter_port *port);
int cec_splitter_received_input(struct cec_splitter_port *port, struct cec_msg *msg);
int cec_splitter_received_output(struct cec_splitter_port *port, struct cec_msg *msg,
struct cec_adapter *input_adap);
void cec_splitter_nb_transmit_canceled_output(struct cec_splitter_port *port,
const struct cec_msg *msg,
struct cec_adapter *input_adap);
bool cec_splitter_poll(struct cec_splitter *splitter,
struct cec_adapter *input_adap, bool debug);
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright 2021-2024 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
*/
#ifndef _EXTRON_DA_HD_4K_PLUS_H_
#define _EXTRON_DA_HD_4K_PLUS_H_
#include <linux/kthread.h>
#include <linux/serio.h>
#include <linux/workqueue.h>
#include <media/cec.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-dev.h>
#include <media/v4l2-device.h>
#include <media/v4l2-dv-timings.h>
#include <media/v4l2-event.h>
#include <media/v4l2-fh.h>
#include <media/v4l2-ioctl.h>
#include "cec-splitter.h"
#define DATA_SIZE 256
#define PING_PERIOD (15 * HZ)
#define NUM_MSGS CEC_MAX_MSG_RX_QUEUE_SZ
#define MAX_PORTS (1 + 6)
#define MAX_EDID_BLOCKS 2
struct extron;
struct extron_port {
struct cec_splitter_port port;
struct device *dev;
struct cec_adapter *adap;
struct video_device vdev;
struct v4l2_ctrl_handler hdl;
struct v4l2_ctrl *ctrl_rx_power_present;
struct v4l2_ctrl *ctrl_tx_hotplug;
struct v4l2_ctrl *ctrl_tx_edid_present;
bool is_input;
char direction;
char name[26];
unsigned char edid[MAX_EDID_BLOCKS * 128];
unsigned char edid_tmp[MAX_EDID_BLOCKS * 128];
unsigned int edid_blocks;
bool read_edid;
struct extron *extron;
struct work_struct irq_work;
struct completion cmd_done;
const char *response;
unsigned int cmd_error;
struct cec_msg rx_msg[NUM_MSGS];
unsigned int rx_msg_cur_idx, rx_msg_num;
/* protect rx_msg_cur_idx and rx_msg_num */
spinlock_t msg_lock;
u32 tx_done_status;
bool update_phys_addr;
u16 phys_addr;
bool cec_was_registered;
bool disconnected;
bool update_has_signal;
bool has_signal;
bool update_has_edid;
bool has_edid;
bool has_4kp30;
bool has_4kp60;
bool has_qy;
bool has_qs;
u8 est_i, est_ii;
/* locks access to the video_device */
struct mutex video_lock;
};
struct extron {
struct cec_splitter splitter;
struct device *dev;
struct serio *serio;
/* locks access to serio */
struct mutex serio_lock;
unsigned int num_ports;
unsigned int num_in_ports;
unsigned int num_out_ports;
char unit_name[32];
char unit_type[64];
char unit_fw_version[32];
char unit_cec_engine_version[32];
struct extron_port *ports[MAX_PORTS];
struct cec_splitter_port *splitter_ports[MAX_PORTS];
struct v4l2_device v4l2_dev;
bool hpd_never_low;
struct task_struct *kthread_setup;
struct delayed_work work_update_edid;
/* serializes EDID reading */
struct mutex edid_lock;
unsigned int edid_bytes_read;
struct extron_port *edid_port;
struct completion edid_completion;
bool edid_reading;
bool is_ready;
struct completion cmd_done;
const char *response;
unsigned int cmd_error;
char data[DATA_SIZE];
unsigned int len;
char reply[DATA_SIZE];
char buf[DATA_SIZE];
unsigned int idx;
};
#endif
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: GPL-2.0-only
config USB_PULSE8_CEC
tristate "Pulse Eight HDMI CEC"
select CEC_CORE
select USB
select USB_ACM
select SERIO
select SERIO_SERPORT
help
This is a cec driver for the Pulse Eight HDMI CEC device.
To compile this driver as a module, choose M here: the
module will be called pulse8-cec.
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_USB_PULSE8_CEC) += pulse8-cec.o
@@ -0,0 +1,931 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Pulse Eight HDMI CEC driver
*
* Copyright 2016 Hans Verkuil <hverkuil@kernel.org>
*/
/*
* Notes:
*
* - Devices with firmware version < 2 do not store their configuration in
* EEPROM.
*
* - In autonomous mode, only messages from a TV will be acknowledged, even
* polling messages. Upon receiving a message from a TV, the dongle will
* respond to messages from any logical address.
*
* - In autonomous mode, the dongle will by default reply Feature Abort
* [Unrecognized Opcode] when it receives Give Device Vendor ID. It will
* however observe vendor ID's reported by other devices and possibly
* alter this behavior. When TV's (and TV's only) report that their vendor ID
* is LG (0x00e091), the dongle will itself reply that it has the same vendor
* ID, and it will respond to at least one vendor specific command.
*
* - In autonomous mode, the dongle is known to attempt wakeup if it receives
* <User Control Pressed> ["Power On"], ["Power] or ["Power Toggle"], or if it
* receives <Set Stream Path> with its own physical address. It also does this
* if it receives <Vendor Specific Command> [0x03 0x00] from an LG TV.
*/
#include <linux/completion.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/serio.h>
#include <linux/slab.h>
#include <linux/time.h>
#include <linux/delay.h>
#include <media/cec.h>
MODULE_AUTHOR("Hans Verkuil <hverkuil@kernel.org>");
MODULE_DESCRIPTION("Pulse Eight HDMI CEC driver");
MODULE_LICENSE("GPL");
static int debug;
static int persistent_config;
module_param(debug, int, 0644);
module_param(persistent_config, int, 0644);
MODULE_PARM_DESC(debug, "debug level (0-2)");
MODULE_PARM_DESC(persistent_config, "read config from persistent memory (0-1)");
enum pulse8_msgcodes {
MSGCODE_NOTHING = 0,
MSGCODE_PING,
MSGCODE_TIMEOUT_ERROR,
MSGCODE_HIGH_ERROR,
MSGCODE_LOW_ERROR,
MSGCODE_FRAME_START,
MSGCODE_FRAME_DATA,
MSGCODE_RECEIVE_FAILED,
MSGCODE_COMMAND_ACCEPTED, /* 0x08 */
MSGCODE_COMMAND_REJECTED,
MSGCODE_SET_ACK_MASK,
MSGCODE_TRANSMIT,
MSGCODE_TRANSMIT_EOM,
MSGCODE_TRANSMIT_IDLETIME,
MSGCODE_TRANSMIT_ACK_POLARITY,
MSGCODE_TRANSMIT_LINE_TIMEOUT,
MSGCODE_TRANSMIT_SUCCEEDED, /* 0x10 */
MSGCODE_TRANSMIT_FAILED_LINE,
MSGCODE_TRANSMIT_FAILED_ACK,
MSGCODE_TRANSMIT_FAILED_TIMEOUT_DATA,
MSGCODE_TRANSMIT_FAILED_TIMEOUT_LINE,
MSGCODE_FIRMWARE_VERSION,
MSGCODE_START_BOOTLOADER,
MSGCODE_GET_BUILDDATE,
MSGCODE_SET_CONTROLLED, /* 0x18 */
MSGCODE_GET_AUTO_ENABLED,
MSGCODE_SET_AUTO_ENABLED,
MSGCODE_GET_DEFAULT_LOGICAL_ADDRESS,
MSGCODE_SET_DEFAULT_LOGICAL_ADDRESS,
MSGCODE_GET_LOGICAL_ADDRESS_MASK,
MSGCODE_SET_LOGICAL_ADDRESS_MASK,
MSGCODE_GET_PHYSICAL_ADDRESS,
MSGCODE_SET_PHYSICAL_ADDRESS, /* 0x20 */
MSGCODE_GET_DEVICE_TYPE,
MSGCODE_SET_DEVICE_TYPE,
MSGCODE_GET_HDMI_VERSION, /* Removed in FW >= 10 */
MSGCODE_SET_HDMI_VERSION,
MSGCODE_GET_OSD_NAME,
MSGCODE_SET_OSD_NAME,
MSGCODE_WRITE_EEPROM,
MSGCODE_GET_ADAPTER_TYPE, /* 0x28 */
MSGCODE_SET_ACTIVE_SOURCE,
MSGCODE_GET_AUTO_POWER_ON, /* New for FW >= 10 */
MSGCODE_SET_AUTO_POWER_ON,
MSGCODE_FRAME_EOM = 0x80,
MSGCODE_FRAME_ACK = 0x40,
};
static const char * const pulse8_msgnames[] = {
"NOTHING",
"PING",
"TIMEOUT_ERROR",
"HIGH_ERROR",
"LOW_ERROR",
"FRAME_START",
"FRAME_DATA",
"RECEIVE_FAILED",
"COMMAND_ACCEPTED",
"COMMAND_REJECTED",
"SET_ACK_MASK",
"TRANSMIT",
"TRANSMIT_EOM",
"TRANSMIT_IDLETIME",
"TRANSMIT_ACK_POLARITY",
"TRANSMIT_LINE_TIMEOUT",
"TRANSMIT_SUCCEEDED",
"TRANSMIT_FAILED_LINE",
"TRANSMIT_FAILED_ACK",
"TRANSMIT_FAILED_TIMEOUT_DATA",
"TRANSMIT_FAILED_TIMEOUT_LINE",
"FIRMWARE_VERSION",
"START_BOOTLOADER",
"GET_BUILDDATE",
"SET_CONTROLLED",
"GET_AUTO_ENABLED",
"SET_AUTO_ENABLED",
"GET_DEFAULT_LOGICAL_ADDRESS",
"SET_DEFAULT_LOGICAL_ADDRESS",
"GET_LOGICAL_ADDRESS_MASK",
"SET_LOGICAL_ADDRESS_MASK",
"GET_PHYSICAL_ADDRESS",
"SET_PHYSICAL_ADDRESS",
"GET_DEVICE_TYPE",
"SET_DEVICE_TYPE",
"GET_HDMI_VERSION",
"SET_HDMI_VERSION",
"GET_OSD_NAME",
"SET_OSD_NAME",
"WRITE_EEPROM",
"GET_ADAPTER_TYPE",
"SET_ACTIVE_SOURCE",
"GET_AUTO_POWER_ON",
"SET_AUTO_POWER_ON",
};
static const char *pulse8_msgname(u8 cmd)
{
static char unknown_msg[5];
if ((cmd & 0x3f) < ARRAY_SIZE(pulse8_msgnames))
return pulse8_msgnames[cmd & 0x3f];
snprintf(unknown_msg, sizeof(unknown_msg), "0x%02x", cmd);
return unknown_msg;
}
#define MSGSTART 0xff
#define MSGEND 0xfe
#define MSGESC 0xfd
#define MSGOFFSET 3
#define DATA_SIZE 256
#define PING_PERIOD (15 * HZ)
#define NUM_MSGS 8
struct pulse8 {
struct device *dev;
struct serio *serio;
struct cec_adapter *adap;
unsigned int vers;
struct delayed_work ping_eeprom_work;
struct work_struct irq_work;
struct cec_msg rx_msg[NUM_MSGS];
unsigned int rx_msg_cur_idx, rx_msg_num;
/* protect rx_msg_cur_idx and rx_msg_num */
spinlock_t msg_lock;
u8 new_rx_msg[CEC_MAX_MSG_SIZE];
u8 new_rx_msg_len;
struct work_struct tx_work;
u32 tx_done_status;
u32 tx_signal_free_time;
struct cec_msg tx_msg;
bool tx_msg_is_bcast;
struct completion cmd_done;
u8 data[DATA_SIZE];
unsigned int len;
u8 buf[DATA_SIZE];
unsigned int idx;
bool escape;
bool started;
/* locks access to the adapter */
struct mutex lock;
bool config_pending;
bool restoring_config;
bool autonomous;
};
static int pulse8_send(struct serio *serio, const u8 *command, u8 cmd_len)
{
int err = 0;
err = serio_write(serio, MSGSTART);
if (err)
return err;
for (; !err && cmd_len; command++, cmd_len--) {
if (*command >= MSGESC) {
err = serio_write(serio, MSGESC);
if (!err)
err = serio_write(serio, *command - MSGOFFSET);
} else {
err = serio_write(serio, *command);
}
}
if (!err)
err = serio_write(serio, MSGEND);
return err;
}
static int pulse8_send_and_wait_once(struct pulse8 *pulse8,
const u8 *cmd, u8 cmd_len,
u8 response, u8 size)
{
int err;
if (!pulse8->serio)
return -ENODEV;
if (debug > 1)
dev_info(pulse8->dev, "transmit %s: %*ph\n",
pulse8_msgname(cmd[0]), cmd_len, cmd);
init_completion(&pulse8->cmd_done);
err = pulse8_send(pulse8->serio, cmd, cmd_len);
if (err)
return err;
if (!wait_for_completion_timeout(&pulse8->cmd_done, HZ))
return -ETIMEDOUT;
if ((pulse8->data[0] & 0x3f) == MSGCODE_COMMAND_REJECTED &&
cmd[0] != MSGCODE_SET_CONTROLLED &&
cmd[0] != MSGCODE_SET_AUTO_ENABLED &&
cmd[0] != MSGCODE_GET_BUILDDATE)
return -ENOTTY;
if (response &&
((pulse8->data[0] & 0x3f) != response || pulse8->len < size + 1)) {
dev_info(pulse8->dev, "transmit %s failed with %s\n",
pulse8_msgname(cmd[0]),
pulse8_msgname(pulse8->data[0]));
return -EIO;
}
return 0;
}
static int pulse8_send_and_wait(struct pulse8 *pulse8,
const u8 *cmd, u8 cmd_len, u8 response, u8 size)
{
u8 cmd_sc[2];
int err;
err = pulse8_send_and_wait_once(pulse8, cmd, cmd_len, response, size);
if (err != -ENOTTY)
return err;
cmd_sc[0] = MSGCODE_SET_CONTROLLED;
cmd_sc[1] = 1;
err = pulse8_send_and_wait_once(pulse8, cmd_sc, 2,
MSGCODE_COMMAND_ACCEPTED, 1);
if (!err)
err = pulse8_send_and_wait_once(pulse8, cmd, cmd_len,
response, size);
return err == -ENOTTY ? -EIO : err;
}
static void pulse8_tx_work_handler(struct work_struct *work)
{
struct pulse8 *pulse8 = container_of(work, struct pulse8, tx_work);
struct cec_msg *msg = &pulse8->tx_msg;
unsigned int i;
u8 cmd[2];
int err;
if (msg->len == 0)
return;
mutex_lock(&pulse8->lock);
cmd[0] = MSGCODE_TRANSMIT_IDLETIME;
cmd[1] = pulse8->tx_signal_free_time;
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 1);
cmd[0] = MSGCODE_TRANSMIT_ACK_POLARITY;
cmd[1] = cec_msg_is_broadcast(msg);
pulse8->tx_msg_is_bcast = cec_msg_is_broadcast(msg);
if (!err)
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 1);
cmd[0] = msg->len == 1 ? MSGCODE_TRANSMIT_EOM : MSGCODE_TRANSMIT;
cmd[1] = msg->msg[0];
if (!err)
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 1);
if (!err && msg->len > 1) {
for (i = 1; !err && i < msg->len; i++) {
cmd[0] = ((i == msg->len - 1)) ?
MSGCODE_TRANSMIT_EOM : MSGCODE_TRANSMIT;
cmd[1] = msg->msg[i];
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 1);
}
}
if (err && debug)
dev_info(pulse8->dev, "%s(0x%02x) failed with error %d for msg %*ph\n",
pulse8_msgname(cmd[0]), cmd[1],
err, msg->len, msg->msg);
msg->len = 0;
mutex_unlock(&pulse8->lock);
if (err)
cec_transmit_attempt_done(pulse8->adap, CEC_TX_STATUS_ERROR);
}
static void pulse8_irq_work_handler(struct work_struct *work)
{
struct pulse8 *pulse8 =
container_of(work, struct pulse8, irq_work);
unsigned long flags;
u32 status;
spin_lock_irqsave(&pulse8->msg_lock, flags);
while (pulse8->rx_msg_num) {
spin_unlock_irqrestore(&pulse8->msg_lock, flags);
if (debug)
dev_info(pulse8->dev, "adap received %*ph\n",
pulse8->rx_msg[pulse8->rx_msg_cur_idx].len,
pulse8->rx_msg[pulse8->rx_msg_cur_idx].msg);
cec_received_msg(pulse8->adap,
&pulse8->rx_msg[pulse8->rx_msg_cur_idx]);
spin_lock_irqsave(&pulse8->msg_lock, flags);
if (pulse8->rx_msg_num)
pulse8->rx_msg_num--;
pulse8->rx_msg_cur_idx =
(pulse8->rx_msg_cur_idx + 1) % NUM_MSGS;
}
spin_unlock_irqrestore(&pulse8->msg_lock, flags);
mutex_lock(&pulse8->lock);
status = pulse8->tx_done_status;
pulse8->tx_done_status = 0;
mutex_unlock(&pulse8->lock);
if (status)
cec_transmit_attempt_done(pulse8->adap, status);
}
static irqreturn_t pulse8_interrupt(struct serio *serio, unsigned char data,
unsigned int flags)
{
struct pulse8 *pulse8 = serio_get_drvdata(serio);
unsigned long irq_flags;
unsigned int idx;
if (!pulse8->started && data != MSGSTART)
return IRQ_HANDLED;
if (data == MSGESC) {
pulse8->escape = true;
return IRQ_HANDLED;
}
if (pulse8->escape) {
data += MSGOFFSET;
pulse8->escape = false;
} else if (data == MSGEND) {
u8 msgcode = pulse8->buf[0];
if (debug > 1)
dev_info(pulse8->dev, "received %s: %*ph\n",
pulse8_msgname(msgcode),
pulse8->idx, pulse8->buf);
switch (msgcode & 0x3f) {
case MSGCODE_FRAME_START:
/*
* Test if we are receiving a new msg when a previous
* message is still pending.
*/
if (!(msgcode & MSGCODE_FRAME_EOM)) {
pulse8->new_rx_msg_len = 1;
pulse8->new_rx_msg[0] = pulse8->buf[1];
break;
}
fallthrough;
case MSGCODE_FRAME_DATA:
if (pulse8->new_rx_msg_len < CEC_MAX_MSG_SIZE)
pulse8->new_rx_msg[pulse8->new_rx_msg_len++] =
pulse8->buf[1];
if (!(msgcode & MSGCODE_FRAME_EOM))
break;
spin_lock_irqsave(&pulse8->msg_lock, irq_flags);
idx = (pulse8->rx_msg_cur_idx + pulse8->rx_msg_num) %
NUM_MSGS;
if (pulse8->rx_msg_num == NUM_MSGS) {
dev_warn(pulse8->dev,
"message queue is full, dropping %*ph\n",
pulse8->new_rx_msg_len,
pulse8->new_rx_msg);
spin_unlock_irqrestore(&pulse8->msg_lock,
irq_flags);
pulse8->new_rx_msg_len = 0;
break;
}
pulse8->rx_msg_num++;
memcpy(pulse8->rx_msg[idx].msg, pulse8->new_rx_msg,
pulse8->new_rx_msg_len);
pulse8->rx_msg[idx].len = pulse8->new_rx_msg_len;
spin_unlock_irqrestore(&pulse8->msg_lock, irq_flags);
schedule_work(&pulse8->irq_work);
pulse8->new_rx_msg_len = 0;
break;
case MSGCODE_TRANSMIT_SUCCEEDED:
WARN_ON(pulse8->tx_done_status);
pulse8->tx_done_status = CEC_TX_STATUS_OK;
schedule_work(&pulse8->irq_work);
break;
case MSGCODE_TRANSMIT_FAILED_ACK:
/*
* A NACK for a broadcast message makes no sense, these
* seem to be spurious messages and are skipped.
*/
if (pulse8->tx_msg_is_bcast)
break;
WARN_ON(pulse8->tx_done_status);
pulse8->tx_done_status = CEC_TX_STATUS_NACK;
schedule_work(&pulse8->irq_work);
break;
case MSGCODE_TRANSMIT_FAILED_LINE:
case MSGCODE_TRANSMIT_FAILED_TIMEOUT_DATA:
case MSGCODE_TRANSMIT_FAILED_TIMEOUT_LINE:
WARN_ON(pulse8->tx_done_status);
pulse8->tx_done_status = CEC_TX_STATUS_ERROR;
schedule_work(&pulse8->irq_work);
break;
case MSGCODE_HIGH_ERROR:
case MSGCODE_LOW_ERROR:
case MSGCODE_RECEIVE_FAILED:
case MSGCODE_TIMEOUT_ERROR:
pulse8->new_rx_msg_len = 0;
break;
case MSGCODE_COMMAND_ACCEPTED:
case MSGCODE_COMMAND_REJECTED:
default:
if (pulse8->idx == 0)
break;
memcpy(pulse8->data, pulse8->buf, pulse8->idx);
pulse8->len = pulse8->idx;
complete(&pulse8->cmd_done);
break;
}
pulse8->idx = 0;
pulse8->started = false;
return IRQ_HANDLED;
} else if (data == MSGSTART) {
pulse8->idx = 0;
pulse8->started = true;
return IRQ_HANDLED;
}
if (pulse8->idx >= DATA_SIZE) {
dev_dbg(pulse8->dev,
"throwing away %d bytes of garbage\n", pulse8->idx);
pulse8->idx = 0;
}
pulse8->buf[pulse8->idx++] = data;
return IRQ_HANDLED;
}
static int pulse8_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
struct pulse8 *pulse8 = cec_get_drvdata(adap);
u8 cmd[16];
int err;
mutex_lock(&pulse8->lock);
cmd[0] = MSGCODE_SET_CONTROLLED;
cmd[1] = enable;
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 1);
if (!enable) {
pulse8->rx_msg_num = 0;
pulse8->tx_done_status = 0;
}
mutex_unlock(&pulse8->lock);
return enable ? err : 0;
}
static int pulse8_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
{
struct pulse8 *pulse8 = cec_get_drvdata(adap);
u16 mask = 0;
u16 pa = adap->phys_addr;
u8 cmd[16];
int err = 0;
mutex_lock(&pulse8->lock);
if (log_addr != CEC_LOG_ADDR_INVALID)
mask = 1 << log_addr;
cmd[0] = MSGCODE_SET_ACK_MASK;
cmd[1] = mask >> 8;
cmd[2] = mask & 0xff;
err = pulse8_send_and_wait(pulse8, cmd, 3,
MSGCODE_COMMAND_ACCEPTED, 0);
if ((err && mask != 0) || pulse8->restoring_config)
goto unlock;
cmd[0] = MSGCODE_SET_AUTO_ENABLED;
cmd[1] = log_addr == CEC_LOG_ADDR_INVALID ? 0 : 1;
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
pulse8->autonomous = cmd[1];
if (log_addr == CEC_LOG_ADDR_INVALID)
goto unlock;
cmd[0] = MSGCODE_SET_DEVICE_TYPE;
cmd[1] = adap->log_addrs.primary_device_type[0];
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
switch (adap->log_addrs.primary_device_type[0]) {
case CEC_OP_PRIM_DEVTYPE_TV:
mask = CEC_LOG_ADDR_MASK_TV;
break;
case CEC_OP_PRIM_DEVTYPE_RECORD:
mask = CEC_LOG_ADDR_MASK_RECORD;
break;
case CEC_OP_PRIM_DEVTYPE_TUNER:
mask = CEC_LOG_ADDR_MASK_TUNER;
break;
case CEC_OP_PRIM_DEVTYPE_PLAYBACK:
mask = CEC_LOG_ADDR_MASK_PLAYBACK;
break;
case CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM:
mask = CEC_LOG_ADDR_MASK_AUDIOSYSTEM;
break;
case CEC_OP_PRIM_DEVTYPE_SWITCH:
mask = CEC_LOG_ADDR_MASK_UNREGISTERED;
break;
case CEC_OP_PRIM_DEVTYPE_PROCESSOR:
mask = CEC_LOG_ADDR_MASK_SPECIFIC;
break;
default:
mask = 0;
break;
}
cmd[0] = MSGCODE_SET_LOGICAL_ADDRESS_MASK;
cmd[1] = mask >> 8;
cmd[2] = mask & 0xff;
err = pulse8_send_and_wait(pulse8, cmd, 3,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
cmd[0] = MSGCODE_SET_DEFAULT_LOGICAL_ADDRESS;
cmd[1] = log_addr;
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
cmd[0] = MSGCODE_SET_PHYSICAL_ADDRESS;
cmd[1] = pa >> 8;
cmd[2] = pa & 0xff;
err = pulse8_send_and_wait(pulse8, cmd, 3,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
if (pulse8->vers < 10) {
cmd[0] = MSGCODE_SET_HDMI_VERSION;
cmd[1] = adap->log_addrs.cec_version;
err = pulse8_send_and_wait(pulse8, cmd, 2,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
}
if (adap->log_addrs.osd_name[0]) {
size_t osd_len = strlen(adap->log_addrs.osd_name);
char *osd_str = cmd + 1;
cmd[0] = MSGCODE_SET_OSD_NAME;
strscpy(cmd + 1, adap->log_addrs.osd_name, sizeof(cmd) - 1);
if (osd_len < 4) {
memset(osd_str + osd_len, ' ', 4 - osd_len);
osd_len = 4;
osd_str[osd_len] = '\0';
strscpy(adap->log_addrs.osd_name, osd_str,
sizeof(adap->log_addrs.osd_name));
}
err = pulse8_send_and_wait(pulse8, cmd, 1 + osd_len,
MSGCODE_COMMAND_ACCEPTED, 0);
if (err)
goto unlock;
}
unlock:
if (pulse8->restoring_config)
pulse8->restoring_config = false;
else
pulse8->config_pending = true;
mutex_unlock(&pulse8->lock);
return log_addr == CEC_LOG_ADDR_INVALID ? 0 : err;
}
static int pulse8_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct pulse8 *pulse8 = cec_get_drvdata(adap);
pulse8->tx_msg = *msg;
if (debug)
dev_info(pulse8->dev, "adap transmit %*ph\n",
msg->len, msg->msg);
pulse8->tx_signal_free_time = signal_free_time;
schedule_work(&pulse8->tx_work);
return 0;
}
static void pulse8_cec_adap_free(struct cec_adapter *adap)
{
struct pulse8 *pulse8 = cec_get_drvdata(adap);
cancel_delayed_work_sync(&pulse8->ping_eeprom_work);
cancel_work_sync(&pulse8->irq_work);
cancel_work_sync(&pulse8->tx_work);
kfree(pulse8);
}
static const struct cec_adap_ops pulse8_cec_adap_ops = {
.adap_enable = pulse8_cec_adap_enable,
.adap_log_addr = pulse8_cec_adap_log_addr,
.adap_transmit = pulse8_cec_adap_transmit,
.adap_free = pulse8_cec_adap_free,
};
static void pulse8_disconnect(struct serio *serio)
{
struct pulse8 *pulse8 = serio_get_drvdata(serio);
cancel_delayed_work_sync(&pulse8->ping_eeprom_work);
mutex_lock(&pulse8->lock);
pulse8->serio = NULL;
mutex_unlock(&pulse8->lock);
cec_unregister_adapter(pulse8->adap);
serio_set_drvdata(serio, NULL);
serio_close(serio);
}
static int pulse8_setup(struct pulse8 *pulse8, struct serio *serio,
struct cec_log_addrs *log_addrs, u16 *pa)
{
u8 *data = pulse8->data + 1;
u8 cmd[2];
int err;
time64_t date;
pulse8->vers = 0;
cmd[0] = MSGCODE_FIRMWARE_VERSION;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 2);
if (err)
return err;
pulse8->vers = (data[0] << 8) | data[1];
dev_info(pulse8->dev, "Firmware version %04x\n", pulse8->vers);
if (pulse8->vers < 2) {
*pa = CEC_PHYS_ADDR_INVALID;
return 0;
}
cmd[0] = MSGCODE_GET_BUILDDATE;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 4);
if (err)
return err;
date = ((unsigned)data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
dev_info(pulse8->dev, "Firmware build date %ptT\n", &date);
dev_dbg(pulse8->dev, "Persistent config:\n");
cmd[0] = MSGCODE_GET_AUTO_ENABLED;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 1);
if (err)
return err;
pulse8->autonomous = data[0];
dev_dbg(pulse8->dev, "Autonomous mode: %s",
data[0] ? "on" : "off");
if (pulse8->vers >= 10) {
cmd[0] = MSGCODE_GET_AUTO_POWER_ON;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 1);
if (!err)
dev_dbg(pulse8->dev, "Auto Power On: %s",
data[0] ? "on" : "off");
}
cmd[0] = MSGCODE_GET_DEVICE_TYPE;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 1);
if (err)
return err;
log_addrs->primary_device_type[0] = data[0];
dev_dbg(pulse8->dev, "Primary device type: %d\n", data[0]);
switch (log_addrs->primary_device_type[0]) {
case CEC_OP_PRIM_DEVTYPE_TV:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_TV;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_TV;
break;
case CEC_OP_PRIM_DEVTYPE_RECORD:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_RECORD;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_RECORD;
break;
case CEC_OP_PRIM_DEVTYPE_TUNER:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_TUNER;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_TUNER;
break;
case CEC_OP_PRIM_DEVTYPE_PLAYBACK:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_PLAYBACK;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_PLAYBACK;
break;
case CEC_OP_PRIM_DEVTYPE_AUDIOSYSTEM:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_PLAYBACK;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_AUDIOSYSTEM;
break;
case CEC_OP_PRIM_DEVTYPE_SWITCH:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_UNREGISTERED;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_SWITCH;
break;
case CEC_OP_PRIM_DEVTYPE_PROCESSOR:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_SPECIFIC;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_SWITCH;
break;
default:
log_addrs->log_addr_type[0] = CEC_LOG_ADDR_TYPE_UNREGISTERED;
log_addrs->all_device_types[0] = CEC_OP_ALL_DEVTYPE_SWITCH;
dev_info(pulse8->dev, "Unknown Primary Device Type: %d\n",
log_addrs->primary_device_type[0]);
break;
}
cmd[0] = MSGCODE_GET_LOGICAL_ADDRESS_MASK;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 2);
if (err)
return err;
log_addrs->log_addr_mask = (data[0] << 8) | data[1];
dev_dbg(pulse8->dev, "Logical address ACK mask: %x\n",
log_addrs->log_addr_mask);
if (log_addrs->log_addr_mask)
log_addrs->num_log_addrs = 1;
cmd[0] = MSGCODE_GET_PHYSICAL_ADDRESS;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 1);
if (err)
return err;
*pa = (data[0] << 8) | data[1];
dev_dbg(pulse8->dev, "Physical address: %x.%x.%x.%x\n",
cec_phys_addr_exp(*pa));
log_addrs->cec_version = CEC_OP_CEC_VERSION_1_4;
if (pulse8->vers < 10) {
cmd[0] = MSGCODE_GET_HDMI_VERSION;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 1);
if (err)
return err;
log_addrs->cec_version = data[0];
dev_dbg(pulse8->dev, "CEC version: %d\n", log_addrs->cec_version);
}
cmd[0] = MSGCODE_GET_OSD_NAME;
err = pulse8_send_and_wait(pulse8, cmd, 1, cmd[0], 0);
if (err)
return err;
strscpy(log_addrs->osd_name, data, sizeof(log_addrs->osd_name));
dev_dbg(pulse8->dev, "OSD name: %s\n", log_addrs->osd_name);
return 0;
}
static int pulse8_apply_persistent_config(struct pulse8 *pulse8,
struct cec_log_addrs *log_addrs,
u16 pa)
{
int err;
err = cec_s_log_addrs(pulse8->adap, log_addrs, false);
if (err)
return err;
cec_s_phys_addr(pulse8->adap, pa, false);
return 0;
}
static void pulse8_ping_eeprom_work_handler(struct work_struct *work)
{
struct pulse8 *pulse8 =
container_of(work, struct pulse8, ping_eeprom_work.work);
u8 cmd;
mutex_lock(&pulse8->lock);
cmd = MSGCODE_PING;
if (pulse8_send_and_wait(pulse8, &cmd, 1,
MSGCODE_COMMAND_ACCEPTED, 0)) {
dev_warn(pulse8->dev, "failed to ping EEPROM\n");
goto unlock;
}
if (pulse8->vers < 2)
goto unlock;
if (pulse8->config_pending && persistent_config) {
dev_dbg(pulse8->dev, "writing pending config to EEPROM\n");
cmd = MSGCODE_WRITE_EEPROM;
if (pulse8_send_and_wait(pulse8, &cmd, 1,
MSGCODE_COMMAND_ACCEPTED, 0))
dev_info(pulse8->dev, "failed to write pending config to EEPROM\n");
else
pulse8->config_pending = false;
}
unlock:
schedule_delayed_work(&pulse8->ping_eeprom_work, PING_PERIOD);
mutex_unlock(&pulse8->lock);
}
static int pulse8_connect(struct serio *serio, struct serio_driver *drv)
{
u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_PHYS_ADDR | CEC_CAP_MONITOR_ALL;
struct pulse8 *pulse8;
int err = -ENOMEM;
struct cec_log_addrs log_addrs = {};
u16 pa = CEC_PHYS_ADDR_INVALID;
pulse8 = kzalloc_obj(*pulse8);
if (!pulse8)
return -ENOMEM;
pulse8->serio = serio;
pulse8->adap = cec_allocate_adapter(&pulse8_cec_adap_ops, pulse8,
dev_name(&serio->dev), caps, 1);
err = PTR_ERR_OR_ZERO(pulse8->adap);
if (err < 0) {
kfree(pulse8);
return err;
}
pulse8->dev = &serio->dev;
serio_set_drvdata(serio, pulse8);
INIT_WORK(&pulse8->irq_work, pulse8_irq_work_handler);
INIT_WORK(&pulse8->tx_work, pulse8_tx_work_handler);
INIT_DELAYED_WORK(&pulse8->ping_eeprom_work,
pulse8_ping_eeprom_work_handler);
mutex_init(&pulse8->lock);
spin_lock_init(&pulse8->msg_lock);
pulse8->config_pending = false;
err = serio_open(serio, drv);
if (err)
goto delete_adap;
err = pulse8_setup(pulse8, serio, &log_addrs, &pa);
if (err)
goto close_serio;
err = cec_register_adapter(pulse8->adap, &serio->dev);
if (err < 0)
goto close_serio;
pulse8->dev = &pulse8->adap->devnode.dev;
if (persistent_config && pulse8->autonomous) {
err = pulse8_apply_persistent_config(pulse8, &log_addrs, pa);
if (err)
goto close_serio;
pulse8->restoring_config = true;
}
schedule_delayed_work(&pulse8->ping_eeprom_work, PING_PERIOD);
return 0;
close_serio:
pulse8->serio = NULL;
serio_set_drvdata(serio, NULL);
serio_close(serio);
delete_adap:
cec_delete_adapter(pulse8->adap);
return err;
}
static const struct serio_device_id pulse8_serio_ids[] = {
{
.type = SERIO_RS232,
.proto = SERIO_PULSE8_CEC,
.id = SERIO_ANY,
.extra = SERIO_ANY,
},
{ 0 }
};
MODULE_DEVICE_TABLE(serio, pulse8_serio_ids);
static struct serio_driver pulse8_drv = {
.driver = {
.name = "pulse8-cec",
},
.description = "Pulse Eight HDMI CEC driver",
.id_table = pulse8_serio_ids,
.interrupt = pulse8_interrupt,
.connect = pulse8_connect,
.disconnect = pulse8_disconnect,
};
module_serio_driver(pulse8_drv);
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: GPL-2.0-only
config USB_RAINSHADOW_CEC
tristate "RainShadow Tech HDMI CEC"
select CEC_CORE
select USB
select USB_ACM
select SERIO
select SERIO_SERPORT
help
This is a cec driver for the RainShadow Tech HDMI CEC device.
To compile this driver as a module, choose M here: the
module will be called rainshadow-cec.
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_USB_RAINSHADOW_CEC) += rainshadow-cec.o
@@ -0,0 +1,382 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* RainShadow Tech HDMI CEC driver
*
* Copyright 2016 Hans Verkuil <hverkuil@kernel.org>
*/
/*
* Notes:
*
* The higher level protocols are currently disabled. This can be added
* later, similar to how this is done for the Pulse Eight CEC driver.
*
* Documentation of the protocol is available here:
*
* http://rainshadowtech.com/doc/HDMICECtoUSBandRS232v2.0.pdf
*/
#include <linux/completion.h>
#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/hex.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/serio.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/time.h>
#include <linux/workqueue.h>
#include <media/cec.h>
MODULE_AUTHOR("Hans Verkuil <hverkuil@kernel.org>");
MODULE_DESCRIPTION("RainShadow Tech HDMI CEC driver");
MODULE_LICENSE("GPL");
#define DATA_SIZE 256
struct rain {
struct device *dev;
struct serio *serio;
struct cec_adapter *adap;
struct completion cmd_done;
struct work_struct work;
/* Low-level ringbuffer, collecting incoming characters */
char buf[DATA_SIZE];
unsigned int buf_rd_idx;
unsigned int buf_wr_idx;
unsigned int buf_len;
spinlock_t buf_lock;
/* command buffer */
char cmd[DATA_SIZE];
unsigned int cmd_idx;
bool cmd_started;
/* reply to a command, only used to store the firmware version */
char cmd_reply[DATA_SIZE];
struct mutex write_lock;
};
static void rain_process_msg(struct rain *rain)
{
struct cec_msg msg = {};
const char *cmd = rain->cmd + 3;
int stat = -1;
for (; *cmd; cmd++) {
if (!isxdigit(*cmd))
continue;
if (isxdigit(cmd[0]) && isxdigit(cmd[1])) {
if (msg.len == CEC_MAX_MSG_SIZE)
break;
if (hex2bin(msg.msg + msg.len, cmd, 1))
continue;
msg.len++;
cmd++;
continue;
}
if (!cmd[1])
stat = hex_to_bin(cmd[0]);
break;
}
if (rain->cmd[0] == 'R') {
if (stat == 1 || stat == 2)
cec_received_msg(rain->adap, &msg);
return;
}
switch (stat) {
case 1:
cec_transmit_attempt_done(rain->adap, CEC_TX_STATUS_OK);
break;
case 2:
cec_transmit_attempt_done(rain->adap, CEC_TX_STATUS_NACK);
break;
default:
cec_transmit_attempt_done(rain->adap, CEC_TX_STATUS_LOW_DRIVE);
break;
}
}
static void rain_irq_work_handler(struct work_struct *work)
{
struct rain *rain =
container_of(work, struct rain, work);
while (true) {
unsigned long flags;
char data;
spin_lock_irqsave(&rain->buf_lock, flags);
if (!rain->buf_len) {
spin_unlock_irqrestore(&rain->buf_lock, flags);
break;
}
data = rain->buf[rain->buf_rd_idx];
rain->buf_len--;
rain->buf_rd_idx = (rain->buf_rd_idx + 1) & 0xff;
spin_unlock_irqrestore(&rain->buf_lock, flags);
if (!rain->cmd_started && data != '?')
continue;
switch (data) {
case '\r':
rain->cmd[rain->cmd_idx] = '\0';
dev_dbg(rain->dev, "received: %s\n", rain->cmd);
if (!memcmp(rain->cmd, "REC", 3) ||
!memcmp(rain->cmd, "STA", 3)) {
rain_process_msg(rain);
} else {
strscpy(rain->cmd_reply, rain->cmd,
sizeof(rain->cmd_reply));
complete(&rain->cmd_done);
}
rain->cmd_idx = 0;
rain->cmd_started = false;
break;
case '\n':
rain->cmd_idx = 0;
rain->cmd_started = false;
break;
case '?':
rain->cmd_idx = 0;
rain->cmd_started = true;
break;
default:
if (rain->cmd_idx >= DATA_SIZE - 1) {
dev_dbg(rain->dev,
"throwing away %d bytes of garbage\n", rain->cmd_idx);
rain->cmd_idx = 0;
}
rain->cmd[rain->cmd_idx++] = data;
break;
}
}
}
static irqreturn_t rain_interrupt(struct serio *serio, unsigned char data,
unsigned int flags)
{
struct rain *rain = serio_get_drvdata(serio);
spin_lock(&rain->buf_lock);
if (rain->buf_len == DATA_SIZE) {
spin_unlock(&rain->buf_lock);
dev_warn_once(rain->dev, "buffer overflow\n");
return IRQ_HANDLED;
}
rain->buf_len++;
rain->buf[rain->buf_wr_idx] = data;
rain->buf_wr_idx = (rain->buf_wr_idx + 1) & 0xff;
spin_unlock(&rain->buf_lock);
schedule_work(&rain->work);
return IRQ_HANDLED;
}
static void rain_disconnect(struct serio *serio)
{
struct rain *rain = serio_get_drvdata(serio);
cancel_work_sync(&rain->work);
cec_unregister_adapter(rain->adap);
dev_info(&serio->dev, "disconnected\n");
serio_close(serio);
serio_set_drvdata(serio, NULL);
kfree(rain);
}
static int rain_send(struct rain *rain, const char *command)
{
int err = serio_write(rain->serio, '!');
dev_dbg(rain->dev, "send: %s\n", command);
while (!err && *command)
err = serio_write(rain->serio, *command++);
if (!err)
err = serio_write(rain->serio, '~');
return err;
}
static int rain_send_and_wait(struct rain *rain,
const char *cmd, const char *reply)
{
int err;
init_completion(&rain->cmd_done);
mutex_lock(&rain->write_lock);
err = rain_send(rain, cmd);
if (err)
goto err;
if (!wait_for_completion_timeout(&rain->cmd_done, HZ)) {
err = -ETIMEDOUT;
goto err;
}
if (reply && strncmp(rain->cmd_reply, reply, strlen(reply))) {
dev_dbg(rain->dev,
"transmit of '%s': received '%s' instead of '%s'\n",
cmd, rain->cmd_reply, reply);
err = -EIO;
}
err:
mutex_unlock(&rain->write_lock);
return err;
}
static int rain_setup(struct rain *rain, struct serio *serio,
struct cec_log_addrs *log_addrs, u16 *pa)
{
int err;
err = rain_send_and_wait(rain, "R", "REV");
if (err)
return err;
dev_info(rain->dev, "Firmware version %s\n", rain->cmd_reply + 4);
err = rain_send_and_wait(rain, "Q 1", "QTY");
if (err)
return err;
err = rain_send_and_wait(rain, "c0000", "CFG");
if (err)
return err;
return rain_send_and_wait(rain, "A F 0000", "ADR");
}
static int rain_cec_adap_enable(struct cec_adapter *adap, bool enable)
{
return 0;
}
static int rain_cec_adap_log_addr(struct cec_adapter *adap, u8 log_addr)
{
struct rain *rain = cec_get_drvdata(adap);
u8 cmd[16];
if (log_addr == CEC_LOG_ADDR_INVALID)
log_addr = CEC_LOG_ADDR_UNREGISTERED;
snprintf(cmd, sizeof(cmd), "A %x", log_addr);
return rain_send_and_wait(rain, cmd, "ADR");
}
static int rain_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
u32 signal_free_time, struct cec_msg *msg)
{
struct rain *rain = cec_get_drvdata(adap);
char cmd[2 * CEC_MAX_MSG_SIZE + 16];
unsigned int i;
int err;
if (msg->len == 1) {
snprintf(cmd, sizeof(cmd), "x%x", cec_msg_destination(msg));
} else {
char hex[3];
snprintf(cmd, sizeof(cmd), "x%x %02x ",
cec_msg_destination(msg), msg->msg[1]);
for (i = 2; i < msg->len; i++) {
snprintf(hex, sizeof(hex), "%02x", msg->msg[i]);
strlcat(cmd, hex, sizeof(cmd));
}
}
mutex_lock(&rain->write_lock);
err = rain_send(rain, cmd);
mutex_unlock(&rain->write_lock);
return err;
}
static const struct cec_adap_ops rain_cec_adap_ops = {
.adap_enable = rain_cec_adap_enable,
.adap_log_addr = rain_cec_adap_log_addr,
.adap_transmit = rain_cec_adap_transmit,
};
static int rain_connect(struct serio *serio, struct serio_driver *drv)
{
u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_PHYS_ADDR | CEC_CAP_MONITOR_ALL;
struct rain *rain;
int err = -ENOMEM;
struct cec_log_addrs log_addrs = {};
u16 pa = CEC_PHYS_ADDR_INVALID;
rain = kzalloc_obj(*rain);
if (!rain)
return -ENOMEM;
rain->serio = serio;
rain->adap = cec_allocate_adapter(&rain_cec_adap_ops, rain,
dev_name(&serio->dev), caps, 1);
err = PTR_ERR_OR_ZERO(rain->adap);
if (err < 0)
goto free_device;
rain->dev = &serio->dev;
serio_set_drvdata(serio, rain);
INIT_WORK(&rain->work, rain_irq_work_handler);
mutex_init(&rain->write_lock);
spin_lock_init(&rain->buf_lock);
err = serio_open(serio, drv);
if (err)
goto delete_adap;
err = rain_setup(rain, serio, &log_addrs, &pa);
if (err)
goto close_serio;
err = cec_register_adapter(rain->adap, &serio->dev);
if (err < 0)
goto close_serio;
rain->dev = &rain->adap->devnode.dev;
return 0;
close_serio:
serio_close(serio);
delete_adap:
cec_delete_adapter(rain->adap);
serio_set_drvdata(serio, NULL);
free_device:
kfree(rain);
return err;
}
static const struct serio_device_id rain_serio_ids[] = {
{
.type = SERIO_RS232,
.proto = SERIO_RAINSHADOW_CEC,
.id = SERIO_ANY,
.extra = SERIO_ANY,
},
{ 0 }
};
MODULE_DEVICE_TABLE(serio, rain_serio_ids);
static struct serio_driver rain_drv = {
.driver = {
.name = "rainshadow-cec",
},
.description = "RainShadow Tech HDMI CEC driver",
.id_table = rain_serio_ids,
.interrupt = rain_interrupt,
.connect = rain_connect,
.disconnect = rain_disconnect,
};
module_serio_driver(rain_drv);