| /* |
| * 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"); |