diff mbox

reset: Add generic GPIO reset driver.

Message ID 20140210125432.10683.86571.stgit@localhost (mailing list archive)
State New, archived
Headers show

Commit Message

Martin Fuzzey Feb. 10, 2014, 12:54 p.m. UTC
This driver allows GPIO lines to be used as reset signals.
It has two main use cases:

1) Allow drivers to reset their hardware via a GPIO line in a standard fashion
as supplied by the reset framework.
This allows adhoc driver code requesting GPIOs etc to be replaced with a
single call to device_reset().

2) Allow hardware on discoverable busses to be rest via a GPIO line
without driver modifications.

Examples of the second use case include:
* SDIO wifi modules
* USB hub chips with a reset line

In this second use case the reset has to be done externally to the driver
managing the hardware since resetting the device from the driver's probe()
method will either do nothing (if the device needs to be reset before
ennumeration will work) or cause racy beahviour (when the device disappears
from the bus during probe()).

So, in addition to providing a gpio based  reset controller implementation
it is also possible to reset devices at boot via a DT property or from
userspace on request via sysfs attributes.

Signed-off-by: Martin Fuzzey <mfuzzey@parkeon.com>
---
 Documentation/ABI/testing/sysfs-driver-gpio-reset |   18 +
 Documentation/devicetree/bindings/reset/gpio.txt  |   44 ++
 drivers/reset/Kconfig                             |   13 +
 drivers/reset/Makefile                            |    1 
 drivers/reset/gpio-reset.c                        |  460 +++++++++++++++++++++
 5 files changed, 536 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-gpio-reset
 create mode 100644 Documentation/devicetree/bindings/reset/gpio.txt
 create mode 100644 drivers/reset/gpio-reset.c

Comments

Philipp Zabel Feb. 10, 2014, 1:16 p.m. UTC | #1
Hi Martin,

Am Montag, den 10.02.2014, 13:54 +0100 schrieb Martin Fuzzey:
> This driver allows GPIO lines to be used as reset signals.
> It has two main use cases:
> 
> 1) Allow drivers to reset their hardware via a GPIO line in a standard fashion
> as supplied by the reset framework.
> This allows adhoc driver code requesting GPIOs etc to be replaced with a
> single call to device_reset().

have you seen the patch at https://lkml.org/lkml/2014/1/8/190:
"reset: Add GPIO support to reset controller framework" ?

Adding a GPIO reset controller device node to the device tree is the
wrong approach for devices enumerated in the device tree. Those should
just declare their reset-gpios directly.

> 2) Allow hardware on discoverable busses to be rest via a GPIO line
> without driver modifications.
>
> Examples of the second use case include:
> * SDIO wifi modules
> * USB hub chips with a reset line

Now this is interesting. But if you export it to userspace anyway, why
not use the existing gpio sysfs API?

I think a proper solution should handle this in the kernel. For SDIO
wifi modules you usually have a powerdown line that can be implemented
as an rfkill switch.

regards
Philipp
Martin Fuzzey Feb. 11, 2014, 9:34 a.m. UTC | #2
Hi Philipp,

On 10 February 2014 14:16, Philipp Zabel <p.zabel@pengutronix.de> wrote:
> Hi Martin,
>
> Am Montag, den 10.02.2014, 13:54 +0100 schrieb Martin Fuzzey:
>> This driver allows GPIO lines to be used as reset signals.
>> It has two main use cases:
>>
>> 1) Allow drivers to reset their hardware via a GPIO line in a standard fashion
>> as supplied by the reset framework.
>> This allows adhoc driver code requesting GPIOs etc to be replaced with a
>> single call to device_reset().
>
> have you seen the patch at https://lkml.org/lkml/2014/1/8/190:
> "reset: Add GPIO support to reset controller framework" ?
>

Ah no missed that.

> Adding a GPIO reset controller device node to the device tree is the
> wrong approach for devices enumerated in the device tree. Those should
> just declare their reset-gpios directly.
>

Oh well if that was the conclusion.
My use case is 2) anyway - just thought it would be sensible to
implement a reset controller too.
As that simplifies the driver code and makes gpio / vs more complex
reset controller (FPGA, ..) a pure DT change.

I do get the point about having to continue to support the old way
anyway though.


>> 2) Allow hardware on discoverable busses to be rest via a GPIO line
>> without driver modifications.
>>
>> Examples of the second use case include:
>> * SDIO wifi modules
>> * USB hub chips with a reset line
>
> Now this is interesting. But if you export it to userspace anyway, why
> not use the existing gpio sysfs API?
>

In the normal case of reset on boot the userspace interface isn't needed.
Setting the "auto" dt property will make the kernel do the reset by
itself during
early boot. This is the standard use case.

The userspace interface is to let applications deal with special cases.
It is also simpler for userspace than manlually toggling the GPIO line
and keeps the configuration (active high / low, delay) centralised in
the DT and consistent between the automatic on boot reset and the
manually triggered reset.


> I think a proper solution should handle this in the kernel. For SDIO
> wifi modules you usually have a powerdown line that can be implemented
> as an rfkill switch.
>

I think this is too specific. It's not just for SDIO wifi. We also
have the problem
of a USB hub needing to be reset.

Also even for the SDIO wifi case rfkill doesn't ssem the right
abstraction to say "reset me"
(particularly when firmware fails to load on warm boot if you don't).

cheers,

Martin
Philipp Zabel Feb. 11, 2014, 5:36 p.m. UTC | #3
Hi Martin,

Am Dienstag, den 11.02.2014, 10:34 +0100 schrieb Fuzzey, Martin:
[...]
> >> 2) Allow hardware on discoverable busses to be rest via a GPIO line
> >> without driver modifications.
> >>
> >> Examples of the second use case include:
> >> * SDIO wifi modules
> >> * USB hub chips with a reset line
> >
> > Now this is interesting. But if you export it to userspace anyway, why
> > not use the existing gpio sysfs API?
> >
> 
> In the normal case of reset on boot the userspace interface isn't needed.
> Setting the "auto" dt property will make the kernel do the reset by
> itself during
> early boot. This is the standard use case.
> 
> The userspace interface is to let applications deal with special cases.
> It is also simpler for userspace than manlually toggling the GPIO line
> and keeps the configuration (active high / low, delay) centralised in
> the DT and consistent between the automatic on boot reset and the
> manually triggered reset.
>
> > I think a proper solution should handle this in the kernel. For SDIO
> > wifi modules you usually have a powerdown line that can be implemented
> > as an rfkill switch.
> >
> 
> I think this is too specific. It's not just for SDIO wifi. We also
> have the problem
> of a USB hub needing to be reset.
> 
> Also even for the SDIO wifi case rfkill doesn't ssem the right
> abstraction to say "reset me"
> (particularly when firmware fails to load on warm boot if you don't).

so long as you need to take devices out of reset before they can be
discovered, maybe the corresponding host controller would be the right
place to put the reset.

regards
Philipp
diff mbox

Patch

diff --git a/Documentation/ABI/testing/sysfs-driver-gpio-reset b/Documentation/ABI/testing/sysfs-driver-gpio-reset
new file mode 100644
index 0000000..6c14144
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-gpio-reset
@@ -0,0 +1,18 @@ 
+In the descriptions below <line-name> is the name of the device tree node
+describing the reset line.
+
+What:		/sys/bus/platform/devices/gpio-reset.<ID>/<line-name>/duration_ms
+Date:		February 2014
+Contact:	Martin Fuzzey <mfuzzey@parkeon.com>
+Description:	Allows reset duration to be configured
+		Read: returns reset duration in ms (decimal)
+		Write: sets new resst duration in ms (decimal)
+
+What:		/sys/bus/platform/devices/gpio-reset.<ID>/<line-name>/control
+Date:		February 2014
+Contact:	Martin Fuzzey <mfuzzey@parkeon.com>
+Description:	Allows reset line state to be controlled
+		Write only accepting the following strings:
+			"reset" : Line will be asserted during the reset duration then deasserted
+			"assert" : Line will be asserted until next write operation
+			"deassert" : Line will be deasserted until the next write operation
diff --git a/Documentation/devicetree/bindings/reset/gpio.txt b/Documentation/devicetree/bindings/reset/gpio.txt
new file mode 100644
index 0000000..1a36821
--- /dev/null
+++ b/Documentation/devicetree/bindings/reset/gpio.txt
@@ -0,0 +1,44 @@ 
+Generic GPIO based Reset Controller
+======================================
+
+Please also refer to reset.txt in this directory for common reset
+controller binding usage.
+
+The parent node only needs a compatible property "linux,gpio-reset".
+
+Eaach reset line is described by a child node with the following properties:
+Required properties:
+- gpios : phandle of the GPIO line to use
+
+Optional properties:
+- asserted-state:  0 => line low to reset, 1  => line high to reset. Defalut 0
+- duration-ms : Number of ms the line should be asserted while resetting (default 1)
+- auto : boolean property - if present a reset will be done on boot
+- #reset-cells: 0 if drivers can trigger reset via phandle
+
+example:
+
+gpio-reset {
+	compatible = "linux,gpio-reset";
+
+	wifi {
+		gpios = <&gpio2 1 0>;
+		asserted-state = <0>;
+		duration-ms = <100>;
+		auto;
+	};
+
+	ethernet_phy_reset:ethernet_phy {
+		#reset-cells = <0>;
+		gpios = <&gpio7 6 0>;
+		asserted-state = <0>;
+		duration-ms = <100>;
+	};
+};
+
+Note that, although this controller may be used as part of the reset
+framework, meaning that a device driver may request the reset using the
+child node's phandle it (ethernet example above) it can also be used
+in a standalone mode where the reset is performed automatically at boot
+or from userspace by sysfs. This is particularly for devices that require
+reset but are on discoverable busses (eg SDIO, USB).
diff --git a/drivers/reset/Kconfig b/drivers/reset/Kconfig
index c9d04f7..edf867c 100644
--- a/drivers/reset/Kconfig
+++ b/drivers/reset/Kconfig
@@ -11,3 +11,16 @@  menuconfig RESET_CONTROLLER
 	  via GPIOs or SoC-internal reset controller modules.
 
 	  If unsure, say no.
+
+config GPIO_RESET_CONTROLLER
+	tristate "GPIO reset controller"
+	help
+	  Reset controller using GPIO lines
+
+	  There are two main methods of using this controller:
+	  * As a reset controller within the reset framework allowing device
+	  drivers to request a hardware reset of their devices.
+	  * As a standalone driver performing reset at boot or upon userspace
+	  request (via sysfs)
+	  The second method is suitable for devices on discoverable busses
+	  (SDIO, USB)
diff --git a/drivers/reset/Makefile b/drivers/reset/Makefile
index 1e2d83f..57c2751 100644
--- a/drivers/reset/Makefile
+++ b/drivers/reset/Makefile
@@ -1 +1,2 @@ 
 obj-$(CONFIG_RESET_CONTROLLER) += core.o
+obj-$(CONFIG_GPIO_RESET_CONTROLLER) += gpio-reset.o
diff --git a/drivers/reset/gpio-reset.c b/drivers/reset/gpio-reset.c
new file mode 100644
index 0000000..17f8a45
--- /dev/null
+++ b/drivers/reset/gpio-reset.c
@@ -0,0 +1,460 @@ 
+/*
+ * Copyright 2014 Parkeon
+ * Martin Fuzzey <mfuzzey@parkeon.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License version 2 as published by the
+ * Free Software Foundation.
+ *
+ * Driver allowing arbitary hardware to be reset by GPIO signals.
+ * The reset may be triggered in several ways:
+ *	At boot time (if configured in DT)
+ *	On userspace request via sysfs
+ *	By a driver using the reset controller framework
+ *
+ * The first two methods are supplied for devices on discoverable busses
+ * needing an external reset (eg some SDIO modules, USB hub chips)
+ */
+
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/of_gpio.h>
+#include <linux/platform_device.h>
+#include <linux/reset-controller.h>
+#include <linux/slab.h>
+
+
+struct gpio_reset_priv;
+
+struct gpio_reset_line {
+	struct kobject kobj;
+	struct gpio_reset_priv *priv;
+	const char *name;
+	int gpio;
+	uint32_t asserted_value;
+	uint32_t duration_ms;
+#ifdef CONFIG_RESET_CONTROLLER
+	struct reset_controller_dev rcdev;
+#endif
+};
+#define kobj_to_gpio_reset_line(x) container_of(x, struct gpio_reset_line, kobj)
+
+
+struct gpio_reset_priv {
+	struct kref refcount;
+	struct device *dev;
+	int num_lines;
+	struct gpio_reset_line lines[];
+};
+
+struct gpio_reset_attribute {
+	struct attribute attr;
+	ssize_t (*show)(struct gpio_reset_line *line,
+				struct gpio_reset_attribute *attr, char *buf);
+	ssize_t (*store)(struct gpio_reset_line *line,
+				struct gpio_reset_attribute *attr,
+				const char *buf, size_t count);
+};
+#define to_gpio_reset_attr(x) container_of(x, struct gpio_reset_attribute, attr)
+
+
+static void gpio_reset_assert(struct gpio_reset_line *line)
+{
+	gpio_set_value(line->gpio, line->asserted_value);
+}
+
+static void gpio_reset_deassert(struct gpio_reset_line *line)
+{
+	gpio_set_value(line->gpio, !line->asserted_value);
+}
+
+static void gpio_reset_reset(struct gpio_reset_line *line)
+{
+	dev_info(line->priv->dev, "Resetting '%s' (%dms)",
+				line->name, line->duration_ms);
+	gpio_reset_assert(line);
+	msleep(line->duration_ms);
+	gpio_reset_deassert(line);
+}
+
+
+static void gpio_reset_free_priv(struct kref *ref)
+{
+	struct gpio_reset_priv *priv = container_of(ref,
+					struct gpio_reset_priv, refcount);
+
+	kfree(priv);
+}
+
+static void gpio_reset_release_kobj(struct kobject *kobj)
+{
+	struct gpio_reset_line *line;
+
+	line = kobj_to_gpio_reset_line(kobj);
+
+	kref_put(&line->priv->refcount, gpio_reset_free_priv);
+}
+
+
+#ifdef CONFIG_SYSFS
+
+static ssize_t gpio_reset_attr_show(struct kobject *kobj,
+			     struct attribute *attr,
+			     char *buf)
+{
+	struct gpio_reset_attribute *attribute;
+	struct gpio_reset_line *line;
+
+	attribute = to_gpio_reset_attr(attr);
+	line = kobj_to_gpio_reset_line(kobj);
+
+	if (!attribute->show)
+		return -EIO;
+
+	return attribute->show(line, attribute, buf);
+}
+
+static ssize_t gpio_reset_attr_store(struct kobject *kobj,
+			      struct attribute *attr,
+			      const char *buf, size_t len)
+{
+	struct gpio_reset_attribute *attribute;
+	struct gpio_reset_line *line;
+
+	attribute = to_gpio_reset_attr(attr);
+	line = kobj_to_gpio_reset_line(kobj);
+
+	if (!attribute->store)
+		return -EIO;
+
+	return attribute->store(line, attribute, buf, len);
+}
+
+static ssize_t control_store(struct gpio_reset_line *line,
+	struct gpio_reset_attribute *attr, const char *buf, size_t count)
+{
+	char action[10];
+	char *eol;
+
+	strncpy(action, buf, min(count, sizeof(action)));
+	action[sizeof(action) - 1] = '\0';
+	eol = strrchr(action, '\n');
+	if (eol)
+		*eol = '\0';
+
+	if (!strcmp("reset", action))
+		gpio_reset_reset(line);
+	else if (!strcmp("assert", action))
+		gpio_reset_assert(line);
+	else if (!strcmp("deassert", action))
+		gpio_reset_deassert(line);
+	else
+		return -EINVAL;
+
+	return count;
+}
+
+static ssize_t duration_ms_show(struct gpio_reset_line *line,
+	struct gpio_reset_attribute *attr, char *buf)
+{
+	return sprintf(buf, "%d\n", line->duration_ms);
+}
+
+static ssize_t duration_ms_store(struct gpio_reset_line *line,
+	struct gpio_reset_attribute *attr, const char *buf, size_t count)
+{
+	if (sscanf(buf, "%u", &line->duration_ms) != 1)
+		return -EINVAL;
+
+	return count;
+}
+
+static struct gpio_reset_attribute control_attribute = __ATTR_WO(control);
+static struct gpio_reset_attribute duration_attribute = __ATTR_RW(duration_ms);
+
+static struct attribute *gpio_reset_attrs[] = {
+	&control_attribute.attr,
+	&duration_attribute.attr,
+	NULL
+};
+
+
+static const struct sysfs_ops gpio_reset_sysfs_ops = {
+	.show = gpio_reset_attr_show,
+	.store = gpio_reset_attr_store,
+};
+
+
+static struct kobj_type gpio_reset_ktype = {
+	.release = gpio_reset_release_kobj,
+	.sysfs_ops = &gpio_reset_sysfs_ops,
+	.default_attrs = gpio_reset_attrs,
+};
+
+static int gpio_reset_create_sysfs(struct gpio_reset_line *line)
+{
+	int ret;
+
+	ret = kobject_init_and_add(&line->kobj, &gpio_reset_ktype,
+		&line->priv->dev->kobj, "reset-%s", line->name);
+
+	kref_get(&line->priv->refcount); /* kobject part of private structure */
+
+	if (ret) {
+		kobject_put(&line->kobj);
+		return ret;
+	}
+
+	kobject_uevent(&line->kobj, KOBJ_ADD);
+
+	return 0;
+}
+
+static void gpio_reset_destroy_sysfs(struct gpio_reset_line *line)
+{
+	kobject_put(&line->kobj);
+}
+
+
+#else
+
+static int gpio_reset_create_sysfs(struct gpio_reset_line *line)
+{
+	return 0;
+}
+
+static void gpio_reset_destroy_sysfs(struct gpio_reset_line *line)
+{
+}
+
+#endif  /* CONFIG_SYSFS */
+
+
+#ifdef CONFIG_RESET_CONTROLLER
+#define rcdev_to_gpio_reset_line(x) \
+		container_of(x, struct gpio_reset_line, rcdev)
+
+static int gpio_reset_controller_assert(struct reset_controller_dev *rcdev,
+		unsigned long sw_reset_idx)
+{
+	struct gpio_reset_line *line =  rcdev_to_gpio_reset_line(rcdev);
+
+	gpio_reset_assert(line);
+
+	return 0;
+}
+
+static int gpio_reset_controller_deassert(struct reset_controller_dev *rcdev,
+		unsigned long sw_reset_idx)
+{
+	struct gpio_reset_line *line =  rcdev_to_gpio_reset_line(rcdev);
+
+	gpio_reset_deassert(line);
+
+	return 0;
+}
+
+static int gpio_reset_controller_reset(struct reset_controller_dev *rcdev,
+		unsigned long sw_reset_idx)
+{
+	struct gpio_reset_line *line =  rcdev_to_gpio_reset_line(rcdev);
+
+	gpio_reset_reset(line);
+
+	return 0;
+}
+
+static struct reset_control_ops gpio_reset_ops = {
+	.assert = gpio_reset_controller_assert,
+	.deassert = gpio_reset_controller_deassert,
+	.reset = gpio_reset_controller_reset,
+};
+
+
+int gpio_reset_nooarg_xlate(struct reset_controller_dev *rcdev,
+			  const struct of_phandle_args *reset_spec)
+{
+	if (WARN_ON(reset_spec->args_count != 0))
+		return -EINVAL;
+
+	return 0;
+}
+
+/* We register one controller per line rather than a single
+ * global controller so that drivers my directly reference the
+ * phandle of the gpio_reset subnode rather than having to know
+ * the index.
+ */
+static int gpio_reset_register_controller(
+	struct device_node *np,
+	struct gpio_reset_line *line)
+{
+	line->rcdev.of_node = np;
+	line->rcdev.nr_resets = 1;
+	line->rcdev.ops = &gpio_reset_ops;
+	line->rcdev.of_xlate = gpio_reset_nooarg_xlate;
+
+	return reset_controller_register(&line->rcdev);
+}
+
+static void gpio_reset_unregister_controller(struct gpio_reset_line *line)
+{
+	if (line->rcdev.nr_resets)
+		reset_controller_unregister(&line->rcdev);
+}
+
+#else
+
+static int gpio_reset_register_controller(
+	struct device_node *np,
+	struct gpio_reset_line *line)
+{
+	return 0;
+}
+
+static void gpio_reset_unregister_controller(struct gpio_reset_line *line)
+{
+}
+#endif  /* CONFIG_RESET_CONTROLLER */
+
+
+static int gpio_reset_init_line(
+	struct device_node *np,
+	struct gpio_reset_line *line)
+{
+	int ret;
+	struct device *dev = line->priv->dev;
+
+	line->name = np->name;
+
+	line->gpio = of_get_gpio(np, 0);
+	if (!gpio_is_valid(line->gpio)) {
+		dev_warn(dev, "Invalid reset gpio for '%s'", np->name);
+		return 0;
+	}
+
+	line->duration_ms = 1;
+	of_property_read_u32(np, "asserted-state", &line->asserted_value);
+	of_property_read_u32(np, "duration-ms", &line->duration_ms);
+
+	ret = devm_gpio_request_one(dev, line->gpio,
+		line->asserted_value ? GPIOF_OUT_INIT_LOW : GPIOF_OUT_INIT_HIGH,
+		line->name);
+	if (ret)
+		return ret;
+
+	ret = gpio_reset_create_sysfs(line);
+	if (ret)
+		return ret;
+
+	ret = gpio_reset_register_controller(np, line);
+	if (ret)
+		return ret;
+
+
+	if (of_property_read_bool(np, "auto"))
+		gpio_reset_reset(line);
+
+	return 0;
+}
+
+static void gpio_reset_free_line(struct gpio_reset_line *line)
+{
+	gpio_reset_destroy_sysfs(line);
+	gpio_reset_unregister_controller(line);
+}
+
+static int gpio_reset_probe(struct platform_device *pdev)
+{
+	struct device_node *np = pdev->dev.of_node, *child;
+	struct gpio_reset_priv *priv;
+	struct gpio_reset_line *line;
+	int num_lines;
+	int ret;
+
+	num_lines = of_get_available_child_count(np);
+	if (!num_lines)
+		return -ENODEV;
+
+	for_each_available_child_of_node(np, child) {
+		ret = of_get_gpio(child, 0);
+		if (ret == -EPROBE_DEFER)
+			return ret;
+	}
+
+	priv = kzalloc(sizeof(*priv) + sizeof(*line) * num_lines, GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	kref_init(&priv->refcount);
+	priv->dev = &pdev->dev;
+	priv->num_lines = num_lines;
+
+	line = priv->lines;
+	for_each_available_child_of_node(np, child) {
+		line->priv = priv;
+		ret = gpio_reset_init_line(child, line);
+		if (ret)
+			goto rollback;
+		line++;
+	}
+
+	platform_set_drvdata(pdev, priv);
+
+	return 0;
+
+rollback:
+	while (line >= priv->lines)
+		gpio_reset_free_line(line--);
+
+	kref_put(&priv->refcount, gpio_reset_free_priv);
+
+	return ret;
+}
+
+static int gpio_reset_remove(struct platform_device *pdev)
+{
+	struct gpio_reset_priv *priv = platform_get_drvdata(pdev);
+	int i;
+
+	for (i = 0; i < priv->num_lines; i++)
+		gpio_reset_free_line(&priv->lines[i]);
+
+	kref_put(&priv->refcount, gpio_reset_free_priv);
+
+	return 0;
+}
+
+static const struct of_device_id gpio_reset_dt_ids[] = {
+	{ .compatible = "linux,gpio-reset" },
+	{}
+};
+
+static struct platform_driver gpio_reset_driver = {
+	.probe		= gpio_reset_probe,
+	.remove		= gpio_reset_remove,
+	.driver		= {
+		.name	= "gpio_reset",
+		.owner	= THIS_MODULE,
+		.of_match_table = gpio_reset_dt_ids,
+	},
+};
+
+static int __init gpio_reset_init(void)
+{
+	return platform_driver_register(&gpio_reset_driver);
+}
+subsys_initcall(gpio_reset_init);
+
+static void __exit gpio_reset_exit(void)
+{
+	platform_driver_unregister(&gpio_reset_driver);
+}
+module_exit(gpio_reset_exit);
+
+MODULE_AUTHOR("Martin Fuzzey <mfuzzey@parkeon.com>");
+MODULE_DESCRIPTION("GPIO reset controller");
+MODULE_LICENSE("GPL");