blob: cdb418aadd99eabf2e73953c7aad8660401de4d9 [file] [log] [blame]
/*
* Copyright 2018 NXP
*
* Peng Fan <peng.fan@nxp.com>
*
* 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 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/slab.h>
#include <xen/xen.h>
#include <xen/events.h>
#include <xen/xenbus.h>
#include <xen/grant_table.h>
#include <xen/page.h>
#include <xen/interface/grant_table.h>
#include <xen/interface/io/i2cif.h>
struct i2cback_info {
domid_t domid;
u32 irq;
u64 handle;
struct xenbus_device *i2cdev;
spinlock_t i2c_ring_lock;
struct i2cif_back_ring i2c_ring;
int is_connected;
int ring_error;
struct i2c_adapter *adapter;
u32 num_slaves;
u32 *allowed_slaves;
};
static bool i2cback_access_allowed(struct i2cback_info *info,
struct i2cif_request *req)
{
int i;
if (req->is_smbus) {/*check for smbus access permission*/
for (i = 0; i < info->num_slaves; i++)
if (req->addr == info->allowed_slaves[i])
return true;
return false;
}
/*check for master_xfer access permission*/
if (req->num_msg == I2CIF_MAX_MSG) {
if (req->msg[0].addr != req->msg[1].addr)
return false;
}
for (i = 0; i < info->num_slaves; i++) {
if (req->msg[0].addr == info->allowed_slaves[i])
return true;
}
return false;
}
static bool i2cback_handle_int(struct i2cback_info *info)
{
struct i2cif_back_ring *i2c_ring = &info->i2c_ring;
struct i2cif_request req;
struct i2cif_response *res;
RING_IDX rc, rp;
int more_to_do, notify, num_msg = 0, ret;
struct i2c_msg msg[I2CIF_MAX_MSG];
union i2c_smbus_data smbus_data;
char tmp_buf[I2CIF_BUF_LEN];
unsigned long flags;
bool allow_access;
int i;
rc = i2c_ring->req_cons;
rp = i2c_ring->sring->req_prod;
rmb(); /* req_cons is written by frontend. */
if (RING_REQUEST_PROD_OVERFLOW(i2c_ring, rp)) {
rc = i2c_ring->rsp_prod_pvt;
dev_err(&info->i2cdev->dev, "ring overflow\n");
info->ring_error = 1;
return 0;
}
while (rc != rp) {
if (RING_REQUEST_CONS_OVERFLOW(i2c_ring, rc)) {
dev_err(&info->i2cdev->dev, "%s overflow\n", __func__);
break;
}
req = *RING_GET_REQUEST(i2c_ring, rc);
allow_access = i2cback_access_allowed(info, &req);
if (allow_access && !req.is_smbus) {
/* Write/Read sequence */
num_msg = req.num_msg;
if (num_msg > I2CIF_MAX_MSG)
num_msg = I2CIF_MAX_MSG;
for (i = 0; i < num_msg; i++) {
msg[i].addr = req.msg[i].addr;
msg[i].len = req.msg[i].len;
msg[i].flags = 0;
if (req.msg[i].flags & I2CIF_M_RD)
msg[i].flags |= I2C_M_RD;
if (req.msg[i].flags & I2CIF_M_TEN)
msg[i].flags |= I2C_M_TEN;
if (req.msg[i].flags & I2CIF_M_RECV_LEN)
msg[i].flags |= I2C_M_RECV_LEN;
if (req.msg[i].flags & I2CIF_M_NO_RD_ACK)
msg[i].flags |= I2C_M_NO_RD_ACK;
if (req.msg[i].flags & I2CIF_M_IGNORE_NAK)
msg[i].flags |= I2C_M_IGNORE_NAK;
if (req.msg[i].flags & I2CIF_M_REV_DIR_ADDR)
msg[i].flags |= I2C_M_REV_DIR_ADDR;
if (req.msg[i].flags & I2CIF_M_NOSTART)
msg[i].flags |= I2C_M_NOSTART;
if (req.msg[i].flags & I2CIF_M_STOP)
msg[i].flags |= I2C_M_STOP;
}
if ((num_msg == 2) &&
(!(msg[0].flags & I2C_M_RD)) &&
(msg[1].flags & I2C_M_RD)) {
/* overwrite the remote buf with local buf */
msg[0].buf = tmp_buf;
msg[1].buf = tmp_buf;
/* msg[0] write buf */
memcpy(tmp_buf, req.write_buf, I2CIF_BUF_LEN);
ret = i2c_transfer(info->adapter, msg,
num_msg);
} else if (num_msg == 1) {
msg[0].buf = tmp_buf;
if (!(msg[0].flags & I2C_M_RD))
memcpy(tmp_buf, req.write_buf,
I2CIF_BUF_LEN);
ret = i2c_transfer(info->adapter, msg,
req.num_msg);
} else {
dev_dbg(&info->i2cdev->dev, "too many msgs\n");
ret = -EIO;
}
} else if (allow_access && req.is_smbus) {
memcpy(&smbus_data, &req.write_buf, sizeof(smbus_data));
ret = i2c_smbus_xfer(info->adapter,
req.addr,
req.flags,
req.read_write,
req.command,
req.protocol,
&smbus_data);
}
spin_lock_irqsave(&info->i2c_ring_lock, flags);
res = RING_GET_RESPONSE(&info->i2c_ring,
info->i2c_ring.rsp_prod_pvt);
if (allow_access && !req.is_smbus) {
res->result = ret;
if ((req.num_msg == 2) &&
(!(msg[0].flags & I2C_M_RD)) &&
(msg[1].flags & I2C_M_RD) && (ret >= 0)) {
memcpy(res->read_buf, tmp_buf, I2CIF_BUF_LEN);
} else if (req.num_msg == 1) {
if ((msg[0].flags & I2C_M_RD) && (ret >= 0))
memcpy(res->read_buf, tmp_buf,
I2CIF_BUF_LEN);
}
} else if (allow_access && req.is_smbus) {
if (req.read_write == I2C_SMBUS_READ)
memcpy(&res->read_buf, &smbus_data, sizeof(smbus_data));
res->result = ret;
} else
res->result = -EPERM;
info->i2c_ring.rsp_prod_pvt++;
barrier();
RING_PUSH_RESPONSES_AND_CHECK_NOTIFY(&info->i2c_ring,
notify);
spin_unlock_irqrestore(&info->i2c_ring_lock, flags);
if (notify)
notify_remote_via_irq(info->irq);
i2c_ring->req_cons = ++rc;
cond_resched();
}
RING_FINAL_CHECK_FOR_REQUESTS(i2c_ring, more_to_do);
return !!more_to_do;
}
static irqreturn_t i2cback_be_int(int irq, void *dev_id)
{
struct i2cback_info *info = dev_id;
if (info->ring_error)
return IRQ_HANDLED;
while (i2cback_handle_int(info))
cond_resched();
return IRQ_HANDLED;
}
static int i2cback_map(struct i2cback_info *info, grant_ref_t *i2c_ring_ref,
evtchn_port_t evtchn)
{
int err;
void *addr;
struct i2cif_sring *i2c_sring;
if (info->irq)
return 0;
err = xenbus_map_ring_valloc(info->i2cdev, i2c_ring_ref, 1, &addr);
if (err)
return err;
i2c_sring = addr;
BACK_RING_INIT(&info->i2c_ring, i2c_sring, PAGE_SIZE);
err = bind_interdomain_evtchn_to_irq(info->domid, evtchn);
if (err < 0)
goto fail_evtchn;
info->irq = err;
err = request_threaded_irq(info->irq, NULL, i2cback_be_int,
IRQF_ONESHOT, "xen-i2cback", info);
if (err) {
dev_err(&info->i2cdev->dev, "bind evtchn to irq failure!\n");
goto free_irq;
}
return 0;
free_irq:
unbind_from_irqhandler(info->irq, info);
info->irq = 0;
info->i2c_ring.sring = NULL;
fail_evtchn:
xenbus_unmap_ring_vfree(info->i2cdev, i2c_sring);
return err;
}
static int i2cback_connect_rings(struct i2cback_info *info)
{
struct xenbus_device *dev = info->i2cdev;
unsigned int i2c_ring_ref, evtchn;
int i, err;
char *buf;
u32 adapter_id;
err = xenbus_scanf(XBT_NIL, dev->nodename,
"adapter", "%u", &adapter_id);
if (err != 1) {
xenbus_dev_fatal(dev, err, "%s reading adapter", dev->nodename);
return err;
}
info->adapter = i2c_get_adapter(adapter_id);
if (!info->adapter)
return -ENODEV;
err = xenbus_scanf(XBT_NIL, dev->nodename,
"num-slaves", "%u", &info->num_slaves);
if (err != 1) {
xenbus_dev_fatal(dev, err, "%s reading num-slaves",
dev->nodename);
return err;
}
info->allowed_slaves = devm_kmalloc(&dev->dev,
info->num_slaves * sizeof(u32),
GFP_KERNEL);
if (!info->allowed_slaves)
return -ENOMEM;
/* 128 bytes is enough */
buf = kmalloc(128, GFP_KERNEL);
for (i = 0; i < info->num_slaves; i++) {
snprintf(buf, 128, "%s/%d", dev->nodename, i);
err = xenbus_scanf(XBT_NIL, buf, "addr", "%x",
&info->allowed_slaves[i]);
if (err != 1) {
kfree(buf);
return err;
}
}
kfree(buf);
err = xenbus_gather(XBT_NIL, dev->otherend,
"ring-ref", "%u", &i2c_ring_ref,
"event-channel", "%u", &evtchn, NULL);
if (err) {
xenbus_dev_fatal(dev, err,
"reading %s/ring-ref and event-channel",
dev->otherend);
return err;
}
dev_info(&info->i2cdev->dev,
"xen-pvi2c: ring-ref %u, event-channel %u\n",
i2c_ring_ref, evtchn);
err = i2cback_map(info, &i2c_ring_ref, evtchn);
if (err)
xenbus_dev_fatal(dev, err, "mapping ring-ref %u evtchn %u",
i2c_ring_ref, evtchn);
return err;
}
static void i2cback_disconnect(struct i2cback_info *info)
{
if (info->irq) {
unbind_from_irqhandler(info->irq, info);
info->irq = 0;
}
if (info->i2c_ring.sring) {
xenbus_unmap_ring_vfree(info->i2cdev, info->i2c_ring.sring);
info->i2c_ring.sring = NULL;
}
}
static void i2cback_frontend_changed(struct xenbus_device *dev,
enum xenbus_state frontend_state)
{
struct i2cback_info *info = dev_get_drvdata(&dev->dev);
int ret;
switch (frontend_state) {
case XenbusStateInitialised:
case XenbusStateReconfiguring:
case XenbusStateReconfigured:
break;
case XenbusStateInitialising:
if (dev->state == XenbusStateClosed) {
dev_info(&dev->dev,
"xen-pvi2c: %s: prepare for reconnect\n",
dev->nodename);
xenbus_switch_state(dev, XenbusStateInitWait);
}
break;
case XenbusStateConnected:
if (dev->state == XenbusStateConnected)
break;
xenbus_switch_state(dev, XenbusStateConnected);
ret = i2cback_connect_rings(info);
if (ret) {
xenbus_dev_fatal(dev, ret, "connect ring fail");
}
break;
case XenbusStateClosing:
i2cback_disconnect(info);
xenbus_switch_state(dev, XenbusStateClosing);
break;
case XenbusStateClosed:
xenbus_switch_state(dev, XenbusStateClosed);
if (xenbus_dev_is_online(dev))
break;
/* fall through if not online */
case XenbusStateUnknown:
device_unregister(&dev->dev);
break;
default:
xenbus_dev_fatal(dev, -EINVAL, "saw state %d at frontend",
frontend_state);
break;
}
}
static struct i2cback_info *i2cback_alloc(domid_t domid, u64 handle)
{
struct i2cback_info *info;
info = kzalloc(sizeof(struct i2cback_info), GFP_KERNEL);
if (!info)
return NULL;
info->domid = domid;
info->handle = handle;
spin_lock_init(&info->i2c_ring_lock);
info->ring_error = 0;
return info;
}
static int i2cback_probe(struct xenbus_device *dev,
const struct xenbus_device_id *id)
{
struct i2cback_info *info;
unsigned long handle;
int err;
if (kstrtoul(strrchr(dev->otherend, '/') + 1, 0, &handle))
return -EINVAL;
info = i2cback_alloc(dev->otherend_id, handle);
if (!info) {
xenbus_dev_fatal(dev, -ENOMEM, "Allocating backend interface");
return -ENOMEM;
}
info->i2cdev = dev;
dev_set_drvdata(&dev->dev, info);
err = xenbus_switch_state(dev, XenbusStateInitWait);
if (err)
return err;
return 0;
}
static int i2cback_remove(struct xenbus_device *dev)
{
struct i2cback_info *info = dev_get_drvdata(&dev->dev);
if (!info)
return 0;
i2cback_disconnect(info);
kfree(info);
dev_set_drvdata(&dev->dev, NULL);
dev_info(&dev->dev, "%s\n", __func__);
return 0;
}
static const struct xenbus_device_id i2cback_ids[] = {
{ "vi2c" },
{ "" },
};
static struct xenbus_driver i2cback_driver = {
.ids = i2cback_ids,
.probe = i2cback_probe,
.otherend_changed = i2cback_frontend_changed,
.remove = i2cback_remove,
};
static int __init i2cback_init(void)
{
int err;
if (!xen_domain())
return -ENODEV;
err = xenbus_register_backend(&i2cback_driver);
if (err)
return err;
return 0;
}
module_init(i2cback_init);
static void __exit i2cback_exit(void)
{
xenbus_unregister_driver(&i2cback_driver);
}
module_exit(i2cback_exit);
MODULE_ALIAS("xen-i2cback:vi2c");
MODULE_AUTHOR("Peng Fan <peng.fan@nxp.com>");
MODULE_DESCRIPTION("Xen I2C backend driver (i2cback)");
MODULE_LICENSE("GPL");