diff options
author | Luca Ceresoli <luca@lucaceresoli.net> | 2023-06-19 14:22:06 +0200 |
---|---|---|
committer | Mauro Carvalho Chehab <mchehab@kernel.org> | 2023-07-14 13:11:44 +0200 |
commit | a076a860acae77bbdcbd316541e5552e81fb1772 (patch) | |
tree | d89211b36c516e4547da8dcbee44bc092b798a20 /drivers/i2c/i2c-atr.c | |
parent | media: dt-bindings: i2c: Add I2C Address Translator (ATR) (diff) | |
download | linux-a076a860acae77bbdcbd316541e5552e81fb1772.tar.xz linux-a076a860acae77bbdcbd316541e5552e81fb1772.zip |
media: i2c: add I2C Address Translator (ATR) support
An ATR is a device that looks similar to an i2c-mux: it has an I2C
slave "upstream" port and N master "downstream" ports, and forwards
transactions from upstream to the appropriate downstream port. But it
is different in that the forwarded transaction has a different slave
address. The address used on the upstream bus is called the "alias"
and is (potentially) different from the physical slave address of the
downstream chip.
Add a helper file (just like i2c-mux.c for a mux or switch) to allow
implementing ATR features in a device driver. The helper takes care of
adapter creation/destruction and translates addresses at each transaction.
Signed-off-by: Luca Ceresoli <luca@lucaceresoli.net>
Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
Reviewed-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com>
Acked-by: Wolfram Sang <wsa@kernel.org>
Signed-off-by: Sakari Ailus <sakari.ailus@linux.intel.com>
Signed-off-by: Mauro Carvalho Chehab <mchehab@kernel.org>
Diffstat (limited to 'drivers/i2c/i2c-atr.c')
-rw-r--r-- | drivers/i2c/i2c-atr.c | 710 |
1 files changed, 710 insertions, 0 deletions
diff --git a/drivers/i2c/i2c-atr.c b/drivers/i2c/i2c-atr.c new file mode 100644 index 000000000000..8ca1daadec93 --- /dev/null +++ b/drivers/i2c/i2c-atr.c @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * I2C Address Translator + * + * Copyright (c) 2019,2022 Luca Ceresoli <luca@lucaceresoli.net> + * Copyright (c) 2022,2023 Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> + * + * Originally based on i2c-mux.c + */ + +#include <linux/fwnode.h> +#include <linux/i2c-atr.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/spinlock.h> + +#define ATR_MAX_ADAPTERS 100 /* Just a sanity limit */ +#define ATR_MAX_SYMLINK_LEN 11 /* Longest name is 10 chars: "channel-99" */ + +/** + * struct i2c_atr_alias_pair - Holds the alias assigned to a client. + * @node: List node + * @client: Pointer to the client on the child bus + * @alias: I2C alias address assigned by the driver. + * This is the address that will be used to issue I2C transactions + * on the parent (physical) bus. + */ +struct i2c_atr_alias_pair { + struct list_head node; + const struct i2c_client *client; + u16 alias; +}; + +/** + * struct i2c_atr_chan - Data for a channel. + * @adap: The &struct i2c_adapter for the channel + * @atr: The parent I2C ATR + * @chan_id: The ID of this channel + * @alias_list: List of @struct i2c_atr_alias_pair containing the + * assigned aliases + * @orig_addrs_lock: Mutex protecting @orig_addrs + * @orig_addrs: Buffer used to store the original addresses during transmit + * @orig_addrs_size: Size of @orig_addrs + */ +struct i2c_atr_chan { + struct i2c_adapter adap; + struct i2c_atr *atr; + u32 chan_id; + + struct list_head alias_list; + + /* Lock orig_addrs during xfer */ + struct mutex orig_addrs_lock; + u16 *orig_addrs; + unsigned int orig_addrs_size; +}; + +/** + * struct i2c_atr - The I2C ATR instance + * @parent: The parent &struct i2c_adapter + * @dev: The device that owns the I2C ATR instance + * @ops: &struct i2c_atr_ops + * @priv: Private driver data, set with i2c_atr_set_driver_data() + * @algo: The &struct i2c_algorithm for adapters + * @lock: Lock for the I2C bus segment (see &struct i2c_lock_operations) + * @max_adapters: Maximum number of adapters this I2C ATR can have + * @num_aliases: Number of aliases in the aliases array + * @aliases: The aliases array + * @alias_mask_lock: Lock protecting alias_use_mask + * @alias_use_mask: Bitmask for used aliases in aliases array + * @i2c_nb: Notifier for remote client add & del events + * @adapter: Array of adapters + */ +struct i2c_atr { + struct i2c_adapter *parent; + struct device *dev; + const struct i2c_atr_ops *ops; + + void *priv; + + struct i2c_algorithm algo; + /* lock for the I2C bus segment (see struct i2c_lock_operations) */ + struct mutex lock; + int max_adapters; + + size_t num_aliases; + const u16 *aliases; + /* Protects alias_use_mask */ + spinlock_t alias_mask_lock; + unsigned long *alias_use_mask; + + struct notifier_block i2c_nb; + + struct i2c_adapter *adapter[]; +}; + +static struct i2c_atr_alias_pair * +i2c_atr_find_mapping_by_client(const struct list_head *list, + const struct i2c_client *client) +{ + struct i2c_atr_alias_pair *c2a; + + list_for_each_entry(c2a, list, node) { + if (c2a->client == client) + return c2a; + } + + return NULL; +} + +static struct i2c_atr_alias_pair * +i2c_atr_find_mapping_by_addr(const struct list_head *list, u16 phys_addr) +{ + struct i2c_atr_alias_pair *c2a; + + list_for_each_entry(c2a, list, node) { + if (c2a->client->addr == phys_addr) + return c2a; + } + + return NULL; +} + +/* + * Replace all message addresses with their aliases, saving the original + * addresses. + * + * This function is internal for use in i2c_atr_master_xfer(). It must be + * followed by i2c_atr_unmap_msgs() to restore the original addresses. + */ +static int i2c_atr_map_msgs(struct i2c_atr_chan *chan, struct i2c_msg *msgs, + int num) +{ + struct i2c_atr *atr = chan->atr; + static struct i2c_atr_alias_pair *c2a; + int i; + + /* Ensure we have enough room to save the original addresses */ + if (unlikely(chan->orig_addrs_size < num)) { + u16 *new_buf; + + /* We don't care about old data, hence no realloc() */ + new_buf = kmalloc_array(num, sizeof(*new_buf), GFP_KERNEL); + if (!new_buf) + return -ENOMEM; + + kfree(chan->orig_addrs); + chan->orig_addrs = new_buf; + chan->orig_addrs_size = num; + } + + for (i = 0; i < num; i++) { + chan->orig_addrs[i] = msgs[i].addr; + + c2a = i2c_atr_find_mapping_by_addr(&chan->alias_list, + msgs[i].addr); + if (!c2a) { + dev_err(atr->dev, "client 0x%02x not mapped!\n", + msgs[i].addr); + + while (i--) + msgs[i].addr = chan->orig_addrs[i]; + + return -ENXIO; + } + + msgs[i].addr = c2a->alias; + } + + return 0; +} + +/* + * Restore all message address aliases with the original addresses. This + * function is internal for use in i2c_atr_master_xfer() and for this reason it + * needs no null and size checks on orig_addr. + * + * @see i2c_atr_map_msgs() + */ +static void i2c_atr_unmap_msgs(struct i2c_atr_chan *chan, struct i2c_msg *msgs, + int num) +{ + int i; + + for (i = 0; i < num; i++) + msgs[i].addr = chan->orig_addrs[i]; +} + +static int i2c_atr_master_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, + int num) +{ + struct i2c_atr_chan *chan = adap->algo_data; + struct i2c_atr *atr = chan->atr; + struct i2c_adapter *parent = atr->parent; + int ret; + + /* Translate addresses */ + mutex_lock(&chan->orig_addrs_lock); + + ret = i2c_atr_map_msgs(chan, msgs, num); + if (ret < 0) + goto err_unlock; + + /* Perform the transfer */ + ret = i2c_transfer(parent, msgs, num); + + /* Restore addresses */ + i2c_atr_unmap_msgs(chan, msgs, num); + +err_unlock: + mutex_unlock(&chan->orig_addrs_lock); + + return ret; +} + +static int i2c_atr_smbus_xfer(struct i2c_adapter *adap, u16 addr, + unsigned short flags, char read_write, u8 command, + int size, union i2c_smbus_data *data) +{ + struct i2c_atr_chan *chan = adap->algo_data; + struct i2c_atr *atr = chan->atr; + struct i2c_adapter *parent = atr->parent; + struct i2c_atr_alias_pair *c2a; + + c2a = i2c_atr_find_mapping_by_addr(&chan->alias_list, addr); + if (!c2a) { + dev_err(atr->dev, "client 0x%02x not mapped!\n", addr); + return -ENXIO; + } + + return i2c_smbus_xfer(parent, c2a->alias, flags, read_write, command, + size, data); +} + +static u32 i2c_atr_functionality(struct i2c_adapter *adap) +{ + struct i2c_atr_chan *chan = adap->algo_data; + struct i2c_adapter *parent = chan->atr->parent; + + return parent->algo->functionality(parent); +} + +static void i2c_atr_lock_bus(struct i2c_adapter *adapter, unsigned int flags) +{ + struct i2c_atr_chan *chan = adapter->algo_data; + struct i2c_atr *atr = chan->atr; + + mutex_lock(&atr->lock); +} + +static int i2c_atr_trylock_bus(struct i2c_adapter *adapter, unsigned int flags) +{ + struct i2c_atr_chan *chan = adapter->algo_data; + struct i2c_atr *atr = chan->atr; + + return mutex_trylock(&atr->lock); +} + +static void i2c_atr_unlock_bus(struct i2c_adapter *adapter, unsigned int flags) +{ + struct i2c_atr_chan *chan = adapter->algo_data; + struct i2c_atr *atr = chan->atr; + + mutex_unlock(&atr->lock); +} + +static const struct i2c_lock_operations i2c_atr_lock_ops = { + .lock_bus = i2c_atr_lock_bus, + .trylock_bus = i2c_atr_trylock_bus, + .unlock_bus = i2c_atr_unlock_bus, +}; + +static int i2c_atr_reserve_alias(struct i2c_atr *atr) +{ + unsigned long idx; + + spin_lock(&atr->alias_mask_lock); + + idx = find_first_zero_bit(atr->alias_use_mask, atr->num_aliases); + if (idx >= atr->num_aliases) { + spin_unlock(&atr->alias_mask_lock); + dev_err(atr->dev, "failed to find a free alias\n"); + return -EBUSY; + } + + set_bit(idx, atr->alias_use_mask); + + spin_unlock(&atr->alias_mask_lock); + + return atr->aliases[idx]; +} + +static void i2c_atr_release_alias(struct i2c_atr *atr, u16 alias) +{ + unsigned int idx; + + spin_lock(&atr->alias_mask_lock); + + for (idx = 0; idx < atr->num_aliases; ++idx) { + if (atr->aliases[idx] == alias) { + clear_bit(idx, atr->alias_use_mask); + spin_unlock(&atr->alias_mask_lock); + return; + } + } + + spin_unlock(&atr->alias_mask_lock); + + /* This should never happen */ + dev_warn(atr->dev, "Unable to find mapped alias\n"); +} + +static int i2c_atr_attach_client(struct i2c_adapter *adapter, + const struct i2c_client *client) +{ + struct i2c_atr_chan *chan = adapter->algo_data; + struct i2c_atr *atr = chan->atr; + struct i2c_atr_alias_pair *c2a; + u16 alias; + int ret; + + ret = i2c_atr_reserve_alias(atr); + if (ret < 0) + return ret; + + alias = ret; + + c2a = kzalloc(sizeof(*c2a), GFP_KERNEL); + if (!c2a) { + ret = -ENOMEM; + goto err_release_alias; + } + + ret = atr->ops->attach_client(atr, chan->chan_id, client, alias); + if (ret) + goto err_free; + + dev_dbg(atr->dev, "chan%u: client 0x%02x mapped at alias 0x%02x (%s)\n", + chan->chan_id, client->addr, alias, client->name); + + c2a->client = client; + c2a->alias = alias; + list_add(&c2a->node, &chan->alias_list); + + return 0; + +err_free: + kfree(c2a); +err_release_alias: + i2c_atr_release_alias(atr, alias); + + return ret; +} + +static void i2c_atr_detach_client(struct i2c_adapter *adapter, + const struct i2c_client *client) +{ + struct i2c_atr_chan *chan = adapter->algo_data; + struct i2c_atr *atr = chan->atr; + struct i2c_atr_alias_pair *c2a; + + atr->ops->detach_client(atr, chan->chan_id, client); + + c2a = i2c_atr_find_mapping_by_client(&chan->alias_list, client); + if (!c2a) { + /* This should never happen */ + dev_warn(atr->dev, "Unable to find address mapping\n"); + return; + } + + i2c_atr_release_alias(atr, c2a->alias); + + dev_dbg(atr->dev, + "chan%u: client 0x%02x unmapped from alias 0x%02x (%s)\n", + chan->chan_id, client->addr, c2a->alias, client->name); + + list_del(&c2a->node); + kfree(c2a); +} + +static int i2c_atr_bus_notifier_call(struct notifier_block *nb, + unsigned long event, void *device) +{ + struct i2c_atr *atr = container_of(nb, struct i2c_atr, i2c_nb); + struct device *dev = device; + struct i2c_client *client; + u32 chan_id; + int ret; + + client = i2c_verify_client(dev); + if (!client) + return NOTIFY_DONE; + + /* Is the client in one of our adapters? */ + for (chan_id = 0; chan_id < atr->max_adapters; ++chan_id) { + if (client->adapter == atr->adapter[chan_id]) + break; + } + + if (chan_id == atr->max_adapters) + return NOTIFY_DONE; + + switch (event) { + case BUS_NOTIFY_ADD_DEVICE: + ret = i2c_atr_attach_client(client->adapter, client); + if (ret) + dev_err(atr->dev, + "Failed to attach remote client '%s': %d\n", + dev_name(dev), ret); + break; + + case BUS_NOTIFY_DEL_DEVICE: + i2c_atr_detach_client(client->adapter, client); + break; + + default: + break; + } + + return NOTIFY_DONE; +} + +static int i2c_atr_parse_alias_pool(struct i2c_atr *atr) +{ + struct device *dev = atr->dev; + unsigned long *alias_use_mask; + size_t num_aliases; + unsigned int i; + u32 *aliases32; + u16 *aliases16; + int ret; + + ret = fwnode_property_count_u32(dev_fwnode(dev), "i2c-alias-pool"); + if (ret < 0) { + dev_err(dev, "Failed to count 'i2c-alias-pool' property: %d\n", + ret); + return ret; + } + + num_aliases = ret; + + if (!num_aliases) + return 0; + + aliases32 = kcalloc(num_aliases, sizeof(*aliases32), GFP_KERNEL); + if (!aliases32) + return -ENOMEM; + + ret = fwnode_property_read_u32_array(dev_fwnode(dev), "i2c-alias-pool", + aliases32, num_aliases); + if (ret < 0) { + dev_err(dev, "Failed to read 'i2c-alias-pool' property: %d\n", + ret); + goto err_free_aliases32; + } + + aliases16 = kcalloc(num_aliases, sizeof(*aliases16), GFP_KERNEL); + if (!aliases16) { + ret = -ENOMEM; + goto err_free_aliases32; + } + + for (i = 0; i < num_aliases; i++) { + if (!(aliases32[i] & 0xffff0000)) { + aliases16[i] = aliases32[i]; + continue; + } + + dev_err(dev, "Failed to parse 'i2c-alias-pool' property: I2C flags are not supported\n"); + ret = -EINVAL; + goto err_free_aliases16; + } + + alias_use_mask = bitmap_zalloc(num_aliases, GFP_KERNEL); + if (!alias_use_mask) { + ret = -ENOMEM; + goto err_free_aliases16; + } + + kfree(aliases32); + + atr->num_aliases = num_aliases; + atr->aliases = aliases16; + atr->alias_use_mask = alias_use_mask; + + dev_dbg(dev, "i2c-alias-pool has %zu aliases", atr->num_aliases); + + return 0; + +err_free_aliases16: + kfree(aliases16); +err_free_aliases32: + kfree(aliases32); + return ret; +} + +struct i2c_atr *i2c_atr_new(struct i2c_adapter *parent, struct device *dev, + const struct i2c_atr_ops *ops, int max_adapters) +{ + struct i2c_atr *atr; + int ret; + + if (max_adapters > ATR_MAX_ADAPTERS) + return ERR_PTR(-EINVAL); + + if (!ops || !ops->attach_client || !ops->detach_client) + return ERR_PTR(-EINVAL); + + atr = kzalloc(struct_size(atr, adapter, max_adapters), GFP_KERNEL); + if (!atr) + return ERR_PTR(-ENOMEM); + + mutex_init(&atr->lock); + spin_lock_init(&atr->alias_mask_lock); + + atr->parent = parent; + atr->dev = dev; + atr->ops = ops; + atr->max_adapters = max_adapters; + + if (parent->algo->master_xfer) + atr->algo.master_xfer = i2c_atr_master_xfer; + if (parent->algo->smbus_xfer) + atr->algo.smbus_xfer = i2c_atr_smbus_xfer; + atr->algo.functionality = i2c_atr_functionality; + + ret = i2c_atr_parse_alias_pool(atr); + if (ret) + goto err_destroy_mutex; + + atr->i2c_nb.notifier_call = i2c_atr_bus_notifier_call; + ret = bus_register_notifier(&i2c_bus_type, &atr->i2c_nb); + if (ret) + goto err_free_aliases; + + return atr; + +err_free_aliases: + bitmap_free(atr->alias_use_mask); + kfree(atr->aliases); +err_destroy_mutex: + mutex_destroy(&atr->lock); + kfree(atr); + + return ERR_PTR(ret); +} +EXPORT_SYMBOL_NS_GPL(i2c_atr_new, I2C_ATR); + +void i2c_atr_delete(struct i2c_atr *atr) +{ + unsigned int i; + + for (i = 0; i < atr->max_adapters; ++i) + WARN_ON(atr->adapter[i]); + + bus_unregister_notifier(&i2c_bus_type, &atr->i2c_nb); + bitmap_free(atr->alias_use_mask); + kfree(atr->aliases); + mutex_destroy(&atr->lock); + kfree(atr); +} +EXPORT_SYMBOL_NS_GPL(i2c_atr_delete, I2C_ATR); + +int i2c_atr_add_adapter(struct i2c_atr *atr, u32 chan_id, + struct device *adapter_parent, + struct fwnode_handle *bus_handle) +{ + struct i2c_adapter *parent = atr->parent; + struct device *dev = atr->dev; + struct i2c_atr_chan *chan; + char symlink_name[ATR_MAX_SYMLINK_LEN]; + int ret; + + if (chan_id >= atr->max_adapters) { + dev_err(dev, "No room for more i2c-atr adapters\n"); + return -EINVAL; + } + + if (atr->adapter[chan_id]) { + dev_err(dev, "Adapter %d already present\n", chan_id); + return -EEXIST; + } + + chan = kzalloc(sizeof(*chan), GFP_KERNEL); + if (!chan) + return -ENOMEM; + + if (!adapter_parent) + adapter_parent = dev; + + chan->atr = atr; + chan->chan_id = chan_id; + INIT_LIST_HEAD(&chan->alias_list); + mutex_init(&chan->orig_addrs_lock); + + snprintf(chan->adap.name, sizeof(chan->adap.name), "i2c-%d-atr-%d", + i2c_adapter_id(parent), chan_id); + chan->adap.owner = THIS_MODULE; + chan->adap.algo = &atr->algo; + chan->adap.algo_data = chan; + chan->adap.dev.parent = adapter_parent; + chan->adap.retries = parent->retries; + chan->adap.timeout = parent->timeout; + chan->adap.quirks = parent->quirks; + chan->adap.lock_ops = &i2c_atr_lock_ops; + + if (bus_handle) { + device_set_node(&chan->adap.dev, fwnode_handle_get(bus_handle)); + } else { + struct fwnode_handle *atr_node; + struct fwnode_handle *child; + u32 reg; + + atr_node = device_get_named_child_node(dev, "i2c-atr"); + + fwnode_for_each_child_node(atr_node, child) { + ret = fwnode_property_read_u32(child, "reg", ®); + if (ret) + continue; + if (chan_id == reg) + break; + } + + device_set_node(&chan->adap.dev, child); + fwnode_handle_put(atr_node); + } + + atr->adapter[chan_id] = &chan->adap; + + ret = i2c_add_adapter(&chan->adap); + if (ret) { + dev_err(dev, "failed to add atr-adapter %u (error=%d)\n", + chan_id, ret); + goto err_fwnode_put; + } + + snprintf(symlink_name, sizeof(symlink_name), "channel-%u", + chan->chan_id); + + ret = sysfs_create_link(&chan->adap.dev.kobj, &dev->kobj, "atr_device"); + if (ret) + dev_warn(dev, "can't create symlink to atr device\n"); + ret = sysfs_create_link(&dev->kobj, &chan->adap.dev.kobj, symlink_name); + if (ret) + dev_warn(dev, "can't create symlink for channel %u\n", chan_id); + + dev_dbg(dev, "Added ATR child bus %d\n", i2c_adapter_id(&chan->adap)); + + return 0; + +err_fwnode_put: + fwnode_handle_put(dev_fwnode(&chan->adap.dev)); + mutex_destroy(&chan->orig_addrs_lock); + kfree(chan); + return ret; +} +EXPORT_SYMBOL_NS_GPL(i2c_atr_add_adapter, I2C_ATR); + +void i2c_atr_del_adapter(struct i2c_atr *atr, u32 chan_id) +{ + char symlink_name[ATR_MAX_SYMLINK_LEN]; + struct i2c_adapter *adap; + struct i2c_atr_chan *chan; + struct fwnode_handle *fwnode; + struct device *dev = atr->dev; + + adap = atr->adapter[chan_id]; + if (!adap) + return; + + chan = adap->algo_data; + fwnode = dev_fwnode(&adap->dev); + + dev_dbg(dev, "Removing ATR child bus %d\n", i2c_adapter_id(adap)); + + snprintf(symlink_name, sizeof(symlink_name), "channel-%u", + chan->chan_id); + sysfs_remove_link(&dev->kobj, symlink_name); + sysfs_remove_link(&chan->adap.dev.kobj, "atr_device"); + + i2c_del_adapter(adap); + + atr->adapter[chan_id] = NULL; + + fwnode_handle_put(fwnode); + mutex_destroy(&chan->orig_addrs_lock); + kfree(chan->orig_addrs); + kfree(chan); +} +EXPORT_SYMBOL_NS_GPL(i2c_atr_del_adapter, I2C_ATR); + +void i2c_atr_set_driver_data(struct i2c_atr *atr, void *data) +{ + atr->priv = data; +} +EXPORT_SYMBOL_NS_GPL(i2c_atr_set_driver_data, I2C_ATR); + +void *i2c_atr_get_driver_data(struct i2c_atr *atr) +{ + return atr->priv; +} +EXPORT_SYMBOL_NS_GPL(i2c_atr_get_driver_data, I2C_ATR); + +MODULE_AUTHOR("Luca Ceresoli <luca.ceresoli@bootlin.com>"); +MODULE_AUTHOR("Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>"); +MODULE_DESCRIPTION("I2C Address Translator"); +MODULE_LICENSE("GPL"); |