summaryrefslogtreecommitdiff
path: root/drivers/gpio
diff options
context:
space:
mode:
authorBartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>2025-12-22 11:01:28 +0100
committerBartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>2026-01-02 10:34:32 +0100
commit49416483a953662aa53c6d9bef651757d4a95ba5 (patch)
tree8affde32dbf3c5a6146b9c1a95848eea22c5ca57 /drivers/gpio
parentcb0451e33be047fff7137f58d9996370e11fb344 (diff)
gpio: shared: allow sharing a reset-gpios pin between reset-gpio and gpiolib
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>
Diffstat (limited to 'drivers/gpio')
-rw-r--r--drivers/gpio/gpiolib-shared.c182
1 files changed, 129 insertions, 53 deletions
diff --git a/drivers/gpio/gpiolib-shared.c b/drivers/gpio/gpiolib-shared.c
index f589109590c7..baf7e07a3bb8 100644
--- a/drivers/gpio/gpiolib-shared.c
+++ b/drivers/gpio/gpiolib-shared.c
@@ -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,21 +354,17 @@ 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)