| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * General Purpose I2C multiplexer |
| * |
| * Copyright (C) 2017 Axentia Technologies AB |
| * |
| * Author: Peter Rosin <peda@axentia.se> |
| */ |
| |
| #include <linux/i2c.h> |
| #include <linux/i2c-mux.h> |
| #include <linux/module.h> |
| #include <linux/mux/consumer.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| |
| struct mux { |
| struct mux_control *control; |
| |
| bool do_not_deselect; |
| }; |
| |
| static int i2c_mux_select(struct i2c_mux_core *muxc, u32 chan) |
| { |
| struct mux *mux = i2c_mux_priv(muxc); |
| int ret; |
| |
| ret = mux_control_select(mux->control, chan); |
| mux->do_not_deselect = ret < 0; |
| |
| return ret; |
| } |
| |
| static int i2c_mux_deselect(struct i2c_mux_core *muxc, u32 chan) |
| { |
| struct mux *mux = i2c_mux_priv(muxc); |
| |
| if (mux->do_not_deselect) |
| return 0; |
| |
| return mux_control_deselect(mux->control); |
| } |
| |
| static struct i2c_adapter *mux_parent_adapter(struct device *dev) |
| { |
| struct device_node *np = dev->of_node; |
| struct device_node *parent_np; |
| struct i2c_adapter *parent; |
| |
| parent_np = of_parse_phandle(np, "i2c-parent", 0); |
| if (!parent_np) { |
| dev_err(dev, "Cannot parse i2c-parent\n"); |
| return ERR_PTR(-ENODEV); |
| } |
| parent = of_get_i2c_adapter_by_node(parent_np); |
| of_node_put(parent_np); |
| if (!parent) |
| return ERR_PTR(-EPROBE_DEFER); |
| |
| return parent; |
| } |
| |
| static const struct of_device_id i2c_mux_of_match[] = { |
| { .compatible = "i2c-mux", }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, i2c_mux_of_match); |
| |
| static int i2c_mux_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct device_node *np = dev->of_node; |
| struct device_node *child; |
| struct i2c_mux_core *muxc; |
| struct mux *mux; |
| struct i2c_adapter *parent; |
| int children; |
| int ret; |
| |
| if (!np) |
| return -ENODEV; |
| |
| mux = devm_kzalloc(dev, sizeof(*mux), GFP_KERNEL); |
| if (!mux) |
| return -ENOMEM; |
| |
| mux->control = devm_mux_control_get(dev, NULL); |
| if (IS_ERR(mux->control)) |
| return dev_err_probe(dev, PTR_ERR(mux->control), |
| "failed to get control-mux\n"); |
| |
| parent = mux_parent_adapter(dev); |
| if (IS_ERR(parent)) |
| return dev_err_probe(dev, PTR_ERR(parent), |
| "failed to get i2c-parent adapter\n"); |
| |
| children = of_get_child_count(np); |
| |
| muxc = i2c_mux_alloc(parent, dev, children, 0, 0, |
| i2c_mux_select, i2c_mux_deselect); |
| if (!muxc) { |
| ret = -ENOMEM; |
| goto err_parent; |
| } |
| muxc->priv = mux; |
| |
| platform_set_drvdata(pdev, muxc); |
| |
| muxc->mux_locked = of_property_read_bool(np, "mux-locked"); |
| |
| for_each_child_of_node(np, child) { |
| u32 chan; |
| |
| ret = of_property_read_u32(child, "reg", &chan); |
| if (ret < 0) { |
| dev_err(dev, "no reg property for node '%pOFn'\n", |
| child); |
| goto err_children; |
| } |
| |
| if (chan >= mux_control_states(mux->control)) { |
| dev_err(dev, "invalid reg %u\n", chan); |
| ret = -EINVAL; |
| goto err_children; |
| } |
| |
| ret = i2c_mux_add_adapter(muxc, 0, chan, 0); |
| if (ret) |
| goto err_children; |
| } |
| |
| dev_info(dev, "%d-port mux on %s adapter\n", children, parent->name); |
| |
| return 0; |
| |
| err_children: |
| of_node_put(child); |
| i2c_mux_del_adapters(muxc); |
| err_parent: |
| i2c_put_adapter(parent); |
| |
| return ret; |
| } |
| |
| static void i2c_mux_remove(struct platform_device *pdev) |
| { |
| struct i2c_mux_core *muxc = platform_get_drvdata(pdev); |
| |
| i2c_mux_del_adapters(muxc); |
| i2c_put_adapter(muxc->parent); |
| } |
| |
| static struct platform_driver i2c_mux_driver = { |
| .probe = i2c_mux_probe, |
| .remove_new = i2c_mux_remove, |
| .driver = { |
| .name = "i2c-mux-gpmux", |
| .of_match_table = i2c_mux_of_match, |
| }, |
| }; |
| module_platform_driver(i2c_mux_driver); |
| |
| MODULE_DESCRIPTION("General Purpose I2C multiplexer driver"); |
| MODULE_AUTHOR("Peter Rosin <peda@axentia.se>"); |
| MODULE_LICENSE("GPL v2"); |