// 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/gpio.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(CMD_WORD_DATA, SECOCEC_MICRO_ADDRESS, \
cmd, data, SMBUS_WRITE, NULL)
#define smb_rd16(cmd, res) smb_word_op(CMD_WORD_DATA, SECOCEC_MICRO_ADDRESS, \
cmd, 0, SMBUS_READ, res)
static int smb_word_op(short data_format, u16 slave_addr, u8 cmd, u16 data,
u8 operation, u16 *result)
{
unsigned int count;
short _data_format;
int status = 0;
switch (data_format) {
case CMD_BYTE_DATA:
_data_format = BRA_SMB_CMD_BYTE_DATA;
break;
case CMD_WORD_DATA:
_data_format = BRA_SMB_CMD_WORD_DATA;
break;
default:
return -EINVAL;
}
/* 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 + _data_format, 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");
} 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");
}
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");
flag_overflow = true;
}
if (status_val & SECOCEC_STATUS_RX_ERROR_MASK) {
dev_warn(dev, "Message received with errors. Discarding");
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 [31mCONFIG_VIDEO_SECO_RC[0m
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_NS(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");
status = devm_rc_register_device(dev, cec->ir);
if (status) {
dev_err(dev, "Failed to prepare input device");
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");
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", key,
addr, toggle);
return 0;
err:
dev_err(dev, "IR Receive message failed (%d)", 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)", 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, GPIOF_IN);
if (IS_ERR(gpio)) {
dev_err(dev, "Cannot request interrupt gpio");
return PTR_ERR(gpio);
}
irq = gpiod_to_irq(gpio);
if (irq < 0) {
dev_err(dev, "Cannot find valid irq");
return -ENODEV;
}
dev_dbg(dev, "irq-gpio is bound to IRQ %d", 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");
return -ENXIO;
}
secocec->pdev = pdev;
secocec->dev = dev;
if (!has_acpi_companion(dev)) {
dev_dbg(dev, "Cannot find any ACPI companion");
ret = -ENODEV;
goto err;
}
ret = secocec_acpi_probe(secocec);
if (ret) {
dev_err(dev, "Cannot assign gpio to IRQ");
ret = -ENODEV;
goto err;
}
/* Firmware version check */
ret = smb_rd16(SECOCEC_VERSION, &val);
if (ret) {
dev_err(dev, "Cannot check fw version");
goto err;
}
if (val < SECOCEC_LATEST_FW) {
dev_err(dev, "CEC Firmware not supported (v.%04x). Use ver > v.%04x",
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", 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_notifier;
platform_set_drvdata(pdev, secocec);
dev_dbg(dev, "Device registered");
return ret;
err_notifier:
cec_notifier_cec_adap_unregister(secocec->notifier);
err_delete_adapter:
cec_delete_adapter(secocec->cec_adap);
err:
dev_err(dev, "%s device probe failed\n", dev_name(dev));
return ret;
}
static int 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");
}
cec_notifier_cec_adap_unregister(secocec->notifier);
cec_unregister_adapter(secocec->cec_adap);
release_region(BRA_SMB_BASE_ADDR, 7);
dev_dbg(&pdev->dev, "CEC device removed");
return 0;
}
#ifdef [31mCONFIG_PM_SLEEP[0m
static int secocec_suspend(struct device *dev)
{
int status;
u16 val;
dev_dbg(dev, "Device going to suspend, disabling");
/* 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 (err: %d)", status);
return status;
}
static int secocec_resume(struct device *dev)
{
int status;
u16 val;
dev_dbg(dev, "Resuming device from suspend");
/* 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");
return 0;
err:
dev_err(dev, "Resume failed (err: %d)", 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 [31mCONFIG_ACPI[0m
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");