83cde38357
Based on 1 normalized pattern(s): this program is free software you can redistribute it and or modify it under the terms of the gnu general public license as published by the free software foundation either version of 2 of the license or at your option any later version extracted by the scancode license scanner the SPDX license identifier GPL-2.0-or-later has been chosen to replace the boilerplate/reference in 4 file(s). Signed-off-by: Thomas Gleixner <tglx@linutronix.de> Reviewed-by: Richard Fontana <rfontana@redhat.com> Reviewed-by: Kate Stewart <kstewart@linuxfoundation.org> Reviewed-by: Allison Randal <allison@lohutok.net> Cc: linux-spdx@vger.kernel.org Link: https://lkml.kernel.org/r/20190520071858.207758043@linutronix.de Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
381 lines
8.3 KiB
C
381 lines
8.3 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* RainShadow Tech HDMI CEC driver
|
|
*
|
|
* Copyright 2016 Hans Verkuil <hverkuil@xs4all.nl
|
|
*/
|
|
|
|
/*
|
|
* 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/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@xs4all.nl>");
|
|
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);
|
|
|
|
if (rain->buf_len == DATA_SIZE) {
|
|
dev_warn_once(rain->dev, "buffer overflow\n");
|
|
return IRQ_HANDLED;
|
|
}
|
|
spin_lock(&rain->buf_lock);
|
|
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(sizeof(*rain), GFP_KERNEL);
|
|
|
|
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);
|