791 lines
17 KiB
C
791 lines
17 KiB
C
|
// 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_notifier;
|
||
|
|
||
|
platform_set_drvdata(pdev, secocec);
|
||
|
|
||
|
dev_dbg(dev, "Device registered\n");
|
||
|
|
||
|
return ret;
|
||
|
|
||
|
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 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\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");
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
#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");
|