]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
gpio: shared: allow sharing a reset-gpios pin between reset-gpio and gpiolib
authorBartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Mon, 22 Dec 2025 10:01:28 +0000 (11:01 +0100)
committerBartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
Fri, 2 Jan 2026 09:34:32 +0000 (10:34 +0100)
We currently support sharing GPIOs between multiple devices whose drivers
use either the GPIOLIB API *OR* the reset control API but not both at
the same time.

There's an unlikely corner-case where a reset-gpios pin can be shared by
one driver using the GPIOLIB API and a second using the reset API. This
will currently not work as the reset-gpio consumers are not described in
firmware at the time of scanning so the shared GPIO just chooses one of
the proxies created for the consumers when the reset-gpio driver performs
the lookup. This can of course conflict in the case described above.

In order to fix it: if we deal with the "reset-gpios" pin that's shared
acconding to the firmware node setup, create a proxy for each described
consumer as well as another one for the potential reset-gpio device. To
that end: rework the matching to take this into account.

Fixes: 7b78b26757e0 ("gpio: shared: handle the reset-gpios corner case")
Link: https://lore.kernel.org/r/20251222-gpio-shared-reset-gpio-proxy-v1-3-8d4bba7d8c14@oss.qualcomm.com
Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
drivers/gpio/gpiolib-shared.c

index f589109590c7c6bc9c0c1828ea15ab9003846523..baf7e07a3bb887dab8155078666a15779e304409 100644 (file)
@@ -76,6 +76,56 @@ gpio_shared_find_entry(struct fwnode_handle *controller_node,
        return NULL;
 }
 
+static struct gpio_shared_ref *gpio_shared_make_ref(struct fwnode_handle *fwnode,
+                                                   const char *con_id,
+                                                   enum gpiod_flags flags)
+{
+       char *con_id_cpy __free(kfree) = NULL;
+
+       struct gpio_shared_ref *ref __free(kfree) = kzalloc(sizeof(*ref), GFP_KERNEL);
+       if (!ref)
+               return NULL;
+
+       if (con_id) {
+               con_id_cpy = kstrdup(con_id, GFP_KERNEL);
+               if (!con_id_cpy)
+                       return NULL;
+       }
+
+       ref->dev_id = ida_alloc(&gpio_shared_ida, GFP_KERNEL);
+       if (ref->dev_id < 0)
+               return NULL;
+
+       ref->flags = flags;
+       ref->con_id = no_free_ptr(con_id_cpy);
+       ref->fwnode = fwnode;
+       mutex_init(&ref->lock);
+
+       return no_free_ptr(ref);
+}
+
+static int gpio_shared_setup_reset_proxy(struct gpio_shared_entry *entry,
+                                        enum gpiod_flags flags)
+{
+       struct gpio_shared_ref *ref;
+
+       list_for_each_entry(ref, &entry->refs, list) {
+               if (!ref->fwnode && ref->con_id && strcmp(ref->con_id, "reset") == 0)
+                       return 0;
+       }
+
+       ref = gpio_shared_make_ref(NULL, "reset", flags);
+       if (!ref)
+               return -ENOMEM;
+
+       list_add_tail(&ref->list, &entry->refs);
+
+       pr_debug("Created a secondary shared GPIO reference for potential reset-gpio device for GPIO %u at %s\n",
+                entry->offset, fwnode_get_name(entry->fwnode));
+
+       return 0;
+}
+
 /* Handle all special nodes that we should ignore. */
 static bool gpio_shared_of_node_ignore(struct device_node *node)
 {
@@ -106,6 +156,7 @@ static int gpio_shared_of_traverse(struct device_node *curr)
        size_t con_id_len, suffix_len;
        struct fwnode_handle *fwnode;
        struct of_phandle_args args;
+       struct gpio_shared_ref *ref;
        struct property *prop;
        unsigned int offset;
        const char *suffix;
@@ -138,6 +189,7 @@ static int gpio_shared_of_traverse(struct device_node *curr)
 
                for (i = 0; i < count; i++) {
                        struct device_node *np __free(device_node) = NULL;
+                       char *con_id __free(kfree) = NULL;
 
                        ret = of_parse_phandle_with_args(curr, prop->name,
                                                         "#gpio-cells", i,
@@ -182,15 +234,6 @@ static int gpio_shared_of_traverse(struct device_node *curr)
                                list_add_tail(&entry->list, &gpio_shared_list);
                        }
 
-                       struct gpio_shared_ref *ref __free(kfree) =
-                                       kzalloc(sizeof(*ref), GFP_KERNEL);
-                       if (!ref)
-                               return -ENOMEM;
-
-                       ref->fwnode = fwnode_handle_get(of_fwnode_handle(curr));
-                       ref->flags = args.args[1];
-                       mutex_init(&ref->lock);
-
                        if (strends(prop->name, "gpios"))
                                suffix = "-gpios";
                        else if (strends(prop->name, "gpio"))
@@ -202,27 +245,32 @@ static int gpio_shared_of_traverse(struct device_node *curr)
 
                        /* We only set con_id if there's actually one. */
                        if (strcmp(prop->name, "gpios") && strcmp(prop->name, "gpio")) {
-                               ref->con_id = kstrdup(prop->name, GFP_KERNEL);
-                               if (!ref->con_id)
+                               con_id = kstrdup(prop->name, GFP_KERNEL);
+                               if (!con_id)
                                        return -ENOMEM;
 
-                               con_id_len = strlen(ref->con_id);
+                               con_id_len = strlen(con_id);
                                suffix_len = strlen(suffix);
 
-                               ref->con_id[con_id_len - suffix_len] = '\0';
+                               con_id[con_id_len - suffix_len] = '\0';
                        }
 
-                       ref->dev_id = ida_alloc(&gpio_shared_ida, GFP_KERNEL);
-                       if (ref->dev_id < 0) {
-                               kfree(ref->con_id);
+                       ref = gpio_shared_make_ref(fwnode_handle_get(of_fwnode_handle(curr)),
+                                                  con_id, args.args[1]);
+                       if (!ref)
                                return -ENOMEM;
-                       }
 
                        if (!list_empty(&entry->refs))
                                pr_debug("GPIO %u at %s is shared by multiple firmware nodes\n",
                                         entry->offset, fwnode_get_name(entry->fwnode));
 
-                       list_add_tail(&no_free_ptr(ref)->list, &entry->refs);
+                       list_add_tail(&ref->list, &entry->refs);
+
+                       if (strcmp(prop->name, "reset-gpios") == 0) {
+                               ret = gpio_shared_setup_reset_proxy(entry, args.args[1]);
+                               if (ret)
+                                       return ret;
+                       }
                }
        }
 
@@ -306,20 +354,16 @@ static bool gpio_shared_dev_is_reset_gpio(struct device *consumer,
        struct fwnode_handle *reset_fwnode = dev_fwnode(consumer);
        struct fwnode_reference_args ref_args, aux_args;
        struct device *parent = consumer->parent;
+       struct gpio_shared_ref *real_ref;
        bool match;
        int ret;
 
+       lockdep_assert_held(&ref->lock);
+
        /* The reset-gpio device must have a parent AND a firmware node. */
        if (!parent || !reset_fwnode)
                return false;
 
-       /*
-        * FIXME: use device_is_compatible() once the reset-gpio drivers gains
-        * a compatible string which it currently does not have.
-        */
-       if (!strstarts(dev_name(consumer), "reset.gpio."))
-               return false;
-
        /*
         * Parent of the reset-gpio auxiliary device is the GPIO chip whose
         * fwnode we stored in the entry structure.
@@ -328,33 +372,56 @@ static bool gpio_shared_dev_is_reset_gpio(struct device *consumer,
                return false;
 
        /*
-        * The device associated with the shared reference's firmware node is
-        * the consumer of the reset control exposed by the reset-gpio device.
-        * It must have a "reset-gpios" property that's referencing the entry's
-        * firmware node.
-        *
-        * The reference args must agree between the real consumer and the
-        * auxiliary reset-gpio device.
+        * Now we need to find the actual pin we want to assign to this
+        * reset-gpio device. To that end: iterate over the list of references
+        * of this entry and see if there's one, whose reset-gpios property's
+        * arguments match the ones from this consumer's node.
         */
-       ret = fwnode_property_get_reference_args(ref->fwnode, "reset-gpios",
-                                                NULL, 2, 0, &ref_args);
-       if (ret)
-               return false;
+       list_for_each_entry(real_ref, &entry->refs, list) {
+               if (!real_ref->fwnode)
+                       continue;
+
+               /*
+                * The device associated with the shared reference's firmware
+                * node is the consumer of the reset control exposed by the
+                * reset-gpio device. It must have a "reset-gpios" property
+                * that's referencing the entry's firmware node.
+                *
+                * The reference args must agree between the real consumer and
+                * the auxiliary reset-gpio device.
+                */
+               ret = fwnode_property_get_reference_args(real_ref->fwnode,
+                                                        "reset-gpios",
+                                                        NULL, 2, 0, &ref_args);
+               if (ret)
+                       continue;
+
+               ret = fwnode_property_get_reference_args(reset_fwnode, "reset-gpios",
+                                                        NULL, 2, 0, &aux_args);
+               if (ret) {
+                       fwnode_handle_put(ref_args.fwnode);
+                       continue;
+               }
+
+               match = ((ref_args.fwnode == entry->fwnode) &&
+                        (aux_args.fwnode == entry->fwnode) &&
+                        (ref_args.args[0] == aux_args.args[0]));
 
-       ret = fwnode_property_get_reference_args(reset_fwnode, "reset-gpios",
-                                                NULL, 2, 0, &aux_args);
-       if (ret) {
                fwnode_handle_put(ref_args.fwnode);
-               return false;
-       }
+               fwnode_handle_put(aux_args.fwnode);
+
+               if (!match)
+                       continue;
 
-       match = ((ref_args.fwnode == entry->fwnode) &&
-                (aux_args.fwnode == entry->fwnode) &&
-                (ref_args.args[0] == aux_args.args[0]));
+               /*
+                * Reuse the fwnode of the real device, next time we'll use it
+                * in the normal path.
+                */
+               ref->fwnode = fwnode_handle_get(real_ref->fwnode);
+               return true;
+       }
 
-       fwnode_handle_put(ref_args.fwnode);
-       fwnode_handle_put(aux_args.fwnode);
-       return match;
+       return false;
 }
 #else
 static bool gpio_shared_dev_is_reset_gpio(struct device *consumer,
@@ -379,12 +446,20 @@ int gpio_shared_add_proxy_lookup(struct device *consumer, const char *con_id,
 
        list_for_each_entry(entry, &gpio_shared_list, list) {
                list_for_each_entry(ref, &entry->refs, list) {
-                       if (!device_match_fwnode(consumer, ref->fwnode) &&
-                           !gpio_shared_dev_is_reset_gpio(consumer, entry, ref))
-                               continue;
-
                        guard(mutex)(&ref->lock);
 
+                       /*
+                        * FIXME: use device_is_compatible() once the reset-gpio
+                        * drivers gains a compatible string which it currently
+                        * does not have.
+                        */
+                       if (!ref->fwnode && strstarts(dev_name(consumer), "reset.gpio.")) {
+                               if (!gpio_shared_dev_is_reset_gpio(consumer, entry, ref))
+                                       continue;
+                       } else if (!device_match_fwnode(consumer, ref->fwnode)) {
+                               continue;
+                       }
+
                        if ((!con_id && ref->con_id) || (con_id && !ref->con_id) ||
                            (con_id && ref->con_id && strcmp(con_id, ref->con_id) != 0))
                                continue;
@@ -471,8 +546,9 @@ int gpio_device_setup_shared(struct gpio_device *gdev)
                         entry->offset, gpio_device_get_label(gdev));
 
                list_for_each_entry(ref, &entry->refs, list) {
-                       pr_debug("Setting up a shared GPIO entry for %s\n",
-                                fwnode_get_name(ref->fwnode));
+                       pr_debug("Setting up a shared GPIO entry for %s (con_id: '%s')\n",
+                                fwnode_get_name(ref->fwnode) ?: "(no fwnode)",
+                                ref->con_id ?: "(none)");
 
                        ret = gpio_shared_make_adev(gdev, entry, ref);
                        if (ret)