drivers/i2c/i2c-slave-mqueue.c (158 lines of code) (raw):

// SPDX-License-Identifier: GPL-2.0 // Copyright (c) 2017 - 2018, Intel Corporation. #include <linux/i2c.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/of.h> #include <linux/slab.h> #include <linux/spinlock.h> #include <linux/sysfs.h> #define MQ_MSGBUF_SIZE CONFIG_I2C_SLAVE_MQUEUE_MESSAGE_SIZE #define MQ_QUEUE_SIZE CONFIG_I2C_SLAVE_MQUEUE_QUEUE_SIZE #define MQ_QUEUE_NEXT(x) (((x) + 1) & (MQ_QUEUE_SIZE - 1)) struct mq_msg { int len; u8 *buf; }; struct mq_queue { struct bin_attribute bin; struct kernfs_node *kn; spinlock_t lock; /* spinlock for queue index handling */ int in; int out; struct mq_msg *curr; int truncated; /* drop current if truncated */ struct mq_msg queue[MQ_QUEUE_SIZE]; }; static int i2c_slave_mqueue_callback(struct i2c_client *client, enum i2c_slave_event event, u8 *val) { struct mq_queue *mq = i2c_get_clientdata(client); struct mq_msg *msg = mq->curr; int ret = 0; switch (event) { case I2C_SLAVE_WRITE_REQUESTED: mq->truncated = 0; msg->len = 1; msg->buf[0] = client->addr << 1; break; case I2C_SLAVE_WRITE_RECEIVED: if (msg->len < MQ_MSGBUF_SIZE) { msg->buf[msg->len++] = *val; } else { dev_err(&client->dev, "message is truncated!\n"); mq->truncated = 1; ret = -EINVAL; } break; case I2C_SLAVE_STOP: if (unlikely(mq->truncated || msg->len < 2)) break; spin_lock(&mq->lock); mq->in = MQ_QUEUE_NEXT(mq->in); mq->curr = &mq->queue[mq->in]; mq->curr->len = 0; /* Flush the oldest message */ if (mq->out == mq->in) mq->out = MQ_QUEUE_NEXT(mq->out); spin_unlock(&mq->lock); kernfs_notify(mq->kn); break; default: *val = 0xFF; break; } return ret; } static ssize_t i2c_slave_mqueue_bin_read(struct file *filp, struct kobject *kobj, struct bin_attribute *attr, char *buf, loff_t pos, size_t count) { struct mq_queue *mq; struct mq_msg *msg; unsigned long flags; bool more = false; ssize_t ret = 0; mq = dev_get_drvdata(container_of(kobj, struct device, kobj)); spin_lock_irqsave(&mq->lock, flags); if (mq->out != mq->in) { msg = &mq->queue[mq->out]; if (msg->len <= count) { ret = msg->len; memcpy(buf, msg->buf, ret); } else { ret = -EOVERFLOW; /* Drop this HUGE one. */ } mq->out = MQ_QUEUE_NEXT(mq->out); if (mq->out != mq->in) more = true; } spin_unlock_irqrestore(&mq->lock, flags); if (more) kernfs_notify(mq->kn); return ret; } static int i2c_slave_mqueue_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct device *dev = &client->dev; struct mq_queue *mq; int ret, i; void *buf; mq = devm_kzalloc(dev, sizeof(*mq), GFP_KERNEL); if (!mq) return -ENOMEM; BUILD_BUG_ON(!is_power_of_2(MQ_QUEUE_SIZE)); buf = devm_kmalloc_array(dev, MQ_QUEUE_SIZE, MQ_MSGBUF_SIZE, GFP_KERNEL); if (!buf) return -ENOMEM; for (i = 0; i < MQ_QUEUE_SIZE; i++) mq->queue[i].buf = buf + i * MQ_MSGBUF_SIZE; i2c_set_clientdata(client, mq); spin_lock_init(&mq->lock); mq->curr = &mq->queue[0]; sysfs_bin_attr_init(&mq->bin); mq->bin.attr.name = "slave-mqueue"; mq->bin.attr.mode = 0400; mq->bin.read = i2c_slave_mqueue_bin_read; mq->bin.size = MQ_MSGBUF_SIZE * MQ_QUEUE_SIZE; ret = sysfs_create_bin_file(&dev->kobj, &mq->bin); if (ret) return ret; mq->kn = kernfs_find_and_get(dev->kobj.sd, mq->bin.attr.name); if (!mq->kn) { sysfs_remove_bin_file(&dev->kobj, &mq->bin); return -EFAULT; } ret = i2c_slave_register(client, i2c_slave_mqueue_callback); if (ret) { kernfs_put(mq->kn); sysfs_remove_bin_file(&dev->kobj, &mq->bin); return ret; } return 0; } static int i2c_slave_mqueue_remove(struct i2c_client *client) { struct mq_queue *mq = i2c_get_clientdata(client); i2c_slave_unregister(client); kernfs_put(mq->kn); sysfs_remove_bin_file(&client->dev.kobj, &mq->bin); return 0; } static const struct i2c_device_id i2c_slave_mqueue_id[] = { { "slave-mqueue", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, i2c_slave_mqueue_id); static struct i2c_driver i2c_slave_mqueue_driver = { .driver = { .name = "i2c-slave-mqueue", }, .probe = i2c_slave_mqueue_probe, .remove = i2c_slave_mqueue_remove, .id_table = i2c_slave_mqueue_id, }; module_i2c_driver(i2c_slave_mqueue_driver); MODULE_LICENSE("GPL v2"); MODULE_AUTHOR("Haiyue Wang <haiyue.wang@linux.intel.com>"); MODULE_DESCRIPTION("I2C slave mode for receiving and queuing messages");