From 6f2311e85974fd9676d4ee9a68093a25b90d1224 Mon Sep 17 00:00:00 2001
From: Phil Elwell <phil@raspberrypi.com>
Date: Wed, 30 Sep 2020 12:00:54 +0100
Subject: [PATCH] gpio: Add gpio-fsm driver

The gpio-fsm driver implements simple state machines that allow GPIOs
to be controlled in response to inputs from other GPIOs - real and
soft/virtual - and time delays. It can:
+ create dummy GPIOs for drivers that demand them,
+ drive multiple GPIOs from a single input, with optional delays,
+ add a debounce circuit to an input,
+ drive pattern sequences onto LEDs
etc.

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio-fsm: Fix a build warning

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio-fsm: Rename 'num-soft-gpios' to avoid warning

As of 5.10, the Device Tree parser warns about properties that look
like references to "suppliers" of various services. "num-soft-gpios"
resembles a declaration of a GPIO called "num-soft", causing the value
to be interpreted as a phandle, the owner of which is checked for a
"#gpio-cells" property.

To avoid this warning, rename the gpio-fsm property to "num-swgpios".

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio-fsm: Show state info in /sys/class/gpio-fsm

Add gpio-fsm sysfs entries under /sys/class/gpio-fsm. For each state
machine show the current state, which state (if any) will be entered
after a delay, and the current value of that delay.

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio-fsm: Fix shutdown timeout handling

The driver is intended to jump directly to a shutdown state in the
event of a timeout during shutdown, but the sense of the test was
inverted.

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio-fsm: Clamp the delay time to zero

The sysfs delay_ms value is calculated live, and it is possible for
the time left to appear to be negative briefly if the timer handling
hasn't completed. Ensure the displayed value never goes below zero,
for the sake of appearances.

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio-fsm: Sort functions into a more logical order

Move some functions into a more logical ordering. This change causes
no functional change and is essentially cosmetic.

Signed-off-by: Phil Elwell <phil@raspberrypi.com>

gpio_fsm: Rework the atomic-vs-non-atomic split

Partition the code to separate atomic and non-atomic methods so that
none of them have to handle both cases. The result avoids using deferred
work unless necessary, and should be easier to understand.

Signed-off-by: Phil Elwell <phil@raspberrypi.com>
---
 drivers/gpio/Kconfig    |    9 +
 drivers/gpio/Makefile   |    1 +
 drivers/gpio/gpio-fsm.c | 1210 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 1220 insertions(+)
 create mode 100644 drivers/gpio/gpio-fsm.c

--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1375,6 +1375,15 @@ config GPIO_ELKHARTLAKE
 	  To compile this driver as a module, choose M here: the module will
 	  be called gpio-elkhartlake.
 
+config GPIO_FSM
+	tristate "GPIO FSM support"
+	help
+	  The GPIO FSM driver allows the creation of state machines for
+	  manipulating GPIOs (both real and virtual), with state transitions
+	  triggered by GPIO edges or delays.
+
+	  If unsure, say N.
+
 config GPIO_JANZ_TTL
 	tristate "Janz VMOD-TTL Digital IO Module"
 	depends on MFD_JANZ_CMODIO
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -66,6 +66,7 @@ obj-$(CONFIG_GPIO_EN7523)		+= gpio-en752
 obj-$(CONFIG_GPIO_EP93XX)		+= gpio-ep93xx.o
 obj-$(CONFIG_GPIO_EXAR)			+= gpio-exar.o
 obj-$(CONFIG_GPIO_F7188X)		+= gpio-f7188x.o
+obj-$(CONFIG_GPIO_FSM)			+= gpio-fsm.o
 obj-$(CONFIG_GPIO_FTGPIO010)		+= gpio-ftgpio010.o
 obj-$(CONFIG_GPIO_FXL6408)		+= gpio-fxl6408.o
 obj-$(CONFIG_GPIO_GE_FPGA)		+= gpio-ge.o
--- /dev/null
+++ b/drivers/gpio/gpio-fsm.c
@@ -0,0 +1,1210 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ *  GPIO FSM driver
+ *
+ *  This driver implements simple state machines that allow real GPIOs to be
+ *  controlled in response to inputs from other GPIOs - real and soft/virtual -
+ *  and time delays. It can:
+ *  + create dummy GPIOs for drivers that demand them
+ *  + drive multiple GPIOs from a single input,  with optional delays
+ *  + add a debounce circuit to an input
+ *  + drive pattern sequences onto LEDs
+ *  etc.
+ *
+ *  Copyright (C) 2020 Raspberry Pi (Trading) Ltd.
+ */
+
+#include <linux/err.h>
+#include <linux/gpio.h>
+#include <linux/gpio/driver.h>
+#include <linux/interrupt.h>
+#include <linux/kdev_t.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/sysfs.h>
+
+#include <dt-bindings/gpio/gpio-fsm.h>
+
+#define MODULE_NAME "gpio-fsm"
+
+#define GF_IO_TYPE(x) ((u32)(x) & 0xffff)
+#define GF_IO_INDEX(x) ((u32)(x) >> 16)
+
+enum {
+	SIGNAL_GPIO,
+	SIGNAL_SOFT
+};
+
+enum {
+	INPUT_GPIO,
+	INPUT_SOFT
+};
+
+enum {
+	SYM_UNDEFINED,
+	SYM_NAME,
+	SYM_SET,
+	SYM_START,
+	SYM_SHUTDOWN,
+
+	SYM_MAX
+};
+
+struct soft_gpio {
+	int dir;
+	int value;
+};
+
+struct input_gpio_state {
+	struct gpio_fsm *gf;
+	struct gpio_desc  *desc;
+	struct fsm_state *target;
+	int index;
+	int value;
+	int irq;
+	bool enabled;
+	bool active_low;
+};
+
+struct gpio_event {
+	int index;
+	int value;
+	struct fsm_state *target;
+};
+
+struct symtab_entry {
+	const char *name;
+	void *value;
+	struct symtab_entry *next;
+};
+
+struct output_signal {
+	u8 type;
+	u8 value;
+	u16 index;
+};
+
+struct fsm_state {
+	const char *name;
+	struct output_signal *signals;
+	struct gpio_event *gpio_events;
+	struct gpio_event *soft_events;
+	struct fsm_state *delay_target;
+	struct fsm_state *shutdown_target;
+	unsigned int num_signals;
+	unsigned int num_gpio_events;
+	unsigned int num_soft_events;
+	unsigned int delay_ms;
+	unsigned int shutdown_ms;
+};
+
+struct gpio_fsm {
+	struct gpio_chip gc;
+	struct device *dev;
+	spinlock_t spinlock;
+	struct work_struct work;
+	struct timer_list timer;
+	wait_queue_head_t shutdown_event;
+	struct fsm_state *states;
+	struct input_gpio_state *input_gpio_states;
+	struct gpio_descs *input_gpios;
+	struct gpio_descs *output_gpios;
+	struct soft_gpio *soft_gpios;
+	struct fsm_state *start_state;
+	struct fsm_state *shutdown_state;
+	unsigned int num_states;
+	unsigned int num_output_gpios;
+	unsigned int num_input_gpios;
+	unsigned int num_soft_gpios;
+	unsigned int shutdown_timeout_ms;
+	unsigned int shutdown_jiffies;
+
+	struct fsm_state *current_state;
+	struct fsm_state *next_state;
+	struct fsm_state *delay_target_state;
+	unsigned long delay_jiffies;
+	int delay_ms;
+	unsigned int debug;
+	bool shutting_down;
+	struct symtab_entry *symtab;
+};
+
+static struct symtab_entry *do_add_symbol(struct symtab_entry **symtab,
+					  const char *name, void *value)
+{
+	struct symtab_entry **p = symtab;
+
+	while (*p && strcmp((*p)->name, name))
+		p = &(*p)->next;
+
+	if (*p) {
+		/* This is an existing symbol */
+		if ((*p)->value) {
+			/* Already defined */
+			if (value) {
+				if ((uintptr_t)value < SYM_MAX)
+					return ERR_PTR(-EINVAL);
+				else
+					return ERR_PTR(-EEXIST);
+			}
+		} else {
+			/* Undefined */
+			(*p)->value = value;
+		}
+	} else {
+		/* This is a new symbol */
+		*p = kmalloc(sizeof(struct symtab_entry), GFP_KERNEL);
+		if (*p) {
+			(*p)->name = name;
+			(*p)->value = value;
+			(*p)->next = NULL;
+		}
+	}
+	return *p;
+}
+
+static int add_symbol(struct symtab_entry **symtab,
+		      const char *name, void *value)
+{
+	struct symtab_entry *sym = do_add_symbol(symtab, name, value);
+
+	return PTR_ERR_OR_ZERO(sym);
+}
+
+static struct symtab_entry *get_symbol(struct symtab_entry **symtab,
+				       const char *name)
+{
+	struct symtab_entry *sym = do_add_symbol(symtab, name, NULL);
+
+	if (IS_ERR(sym))
+		return NULL;
+	return sym;
+}
+
+static void free_symbols(struct symtab_entry **symtab)
+{
+	struct symtab_entry *sym = *symtab;
+	void *p;
+
+	*symtab = NULL;
+	while (sym) {
+		p = sym;
+		sym = sym->next;
+		kfree(p);
+	}
+}
+
+static void gpio_fsm_set_soft(struct gpio_fsm *gf,
+			      unsigned int off, int val);
+
+static void gpio_fsm_enter_state(struct gpio_fsm *gf,
+				 struct fsm_state *state)
+{
+	struct input_gpio_state *inp_state;
+	struct output_signal *signal;
+	struct gpio_event *event;
+	struct gpio_desc *gpiod;
+	struct soft_gpio *soft;
+	int value;
+	int i;
+
+	dev_dbg(gf->dev, "enter_state(%s)\n", state->name);
+
+	gf->current_state = state;
+	gf->delay_target_state = NULL;
+
+	// 1. Apply any listed signals
+	for (i = 0; i < state->num_signals; i++) {
+		signal = &state->signals[i];
+
+		if (gf->debug)
+			dev_info(gf->dev, "  set %s %d->%d\n",
+				 (signal->type == SIGNAL_GPIO) ? "GF_OUT" :
+				 "GF_SOFT",
+				 signal->index, signal->value);
+		switch (signal->type) {
+		case SIGNAL_GPIO:
+			gpiod = gf->output_gpios->desc[signal->index];
+			gpiod_set_value_cansleep(gpiod, signal->value);
+			break;
+		case SIGNAL_SOFT:
+			soft = &gf->soft_gpios[signal->index];
+			gpio_fsm_set_soft(gf, signal->index, signal->value);
+			break;
+		}
+	}
+
+	// 2. Exit if successfully reached shutdown state
+	if (gf->shutting_down && state == state->shutdown_target) {
+		wake_up(&gf->shutdown_event);
+		return;
+	}
+
+	// 3. Schedule a timer callback if shutting down
+	if (state->shutdown_target) {
+		// Remember the absolute shutdown time in case remove is called
+		// at a later time.
+		gf->shutdown_jiffies =
+			jiffies + msecs_to_jiffies(state->shutdown_ms);
+
+		if (gf->shutting_down) {
+			gf->delay_jiffies = gf->shutdown_jiffies;
+			gf->delay_target_state = state->shutdown_target;
+			gf->delay_ms = state->shutdown_ms;
+			mod_timer(&gf->timer, gf->delay_jiffies);
+		}
+	}
+
+	// During shutdown, skip everything else
+	if (gf->shutting_down)
+		return;
+
+	// Otherwise record what the shutdown time would be
+	gf->shutdown_jiffies = jiffies + msecs_to_jiffies(state->shutdown_ms);
+
+	// 4. Check soft inputs for transitions to take
+	for (i = 0; i < state->num_soft_events; i++) {
+		event = &state->soft_events[i];
+		if (gf->soft_gpios[event->index].value == event->value) {
+			if (gf->debug)
+				dev_info(gf->dev,
+					 "GF_SOFT %d=%d -> %s\n", event->index,
+					 event->value, event->target->name);
+			gpio_fsm_enter_state(gf, event->target);
+			return;
+		}
+	}
+
+	// 5. Check GPIOs for transitions to take, enabling the IRQs
+	for (i = 0; i < state->num_gpio_events; i++) {
+		event = &state->gpio_events[i];
+		inp_state = &gf->input_gpio_states[event->index];
+		inp_state->target = event->target;
+		inp_state->value = event->value;
+		inp_state->enabled = true;
+
+		value = gpiod_get_value_cansleep(gf->input_gpios->desc[event->index]);
+
+		// Clear stale event state
+		disable_irq(inp_state->irq);
+
+		irq_set_irq_type(inp_state->irq,
+				 (inp_state->value ^ inp_state->active_low) ?
+				 IRQF_TRIGGER_RISING : IRQF_TRIGGER_FALLING);
+		enable_irq(inp_state->irq);
+
+		if (value == event->value && inp_state->target) {
+			if (gf->debug)
+				dev_info(gf->dev,
+					 "GF_IN %d=%d -> %s\n", event->index,
+					 event->value, event->target->name);
+			gpio_fsm_enter_state(gf, event->target);
+			return;
+		}
+	}
+
+	// 6. Schedule a timer callback if delay_target
+	if (state->delay_target) {
+		gf->delay_target_state = state->delay_target;
+		gf->delay_jiffies = jiffies +
+			msecs_to_jiffies(state->delay_ms);
+		gf->delay_ms = state->delay_ms;
+		mod_timer(&gf->timer, gf->delay_jiffies);
+	}
+}
+
+static void gpio_fsm_go_to_state(struct gpio_fsm *gf,
+				 struct fsm_state *new_state)
+{
+	struct input_gpio_state *inp_state;
+	struct gpio_event *gp_ev;
+	struct fsm_state *state;
+	int i;
+
+	dev_dbg(gf->dev, "go_to_state(%s)\n",
+		  new_state ? new_state->name : "<unset>");
+
+	state = gf->current_state;
+
+	/* Disable any enabled GPIO IRQs */
+	for (i = 0; i < state->num_gpio_events; i++) {
+		gp_ev = &state->gpio_events[i];
+		inp_state = &gf->input_gpio_states[gp_ev->index];
+		if (inp_state->enabled) {
+			inp_state->enabled = false;
+			irq_set_irq_type(inp_state->irq,
+					 IRQF_TRIGGER_NONE);
+		}
+	}
+
+	gpio_fsm_enter_state(gf, new_state);
+}
+
+static void gpio_fsm_go_to_state_deferred(struct gpio_fsm *gf,
+					  struct fsm_state *new_state)
+{
+	struct input_gpio_state *inp_state;
+	struct gpio_event *gp_ev;
+	struct fsm_state *state;
+	int i;
+
+	dev_dbg(gf->dev, "go_to_state_deferred(%s)\n",
+		  new_state ? new_state->name : "<unset>");
+
+	spin_lock(&gf->spinlock);
+
+	if (gf->next_state) {
+		/* Something else has already requested a transition */
+		spin_unlock(&gf->spinlock);
+		return;
+	}
+
+	gf->next_state = new_state;
+	state = gf->current_state;
+
+	/* Disarm any GPIO IRQs */
+	for (i = 0; i < state->num_gpio_events; i++) {
+		gp_ev = &state->gpio_events[i];
+		inp_state = &gf->input_gpio_states[gp_ev->index];
+		inp_state->target = NULL;
+	}
+
+	spin_unlock(&gf->spinlock);
+
+	schedule_work(&gf->work);
+}
+
+static void gpio_fsm_work(struct work_struct *work)
+{
+	struct fsm_state *new_state;
+	struct gpio_fsm *gf;
+
+	gf = container_of(work, struct gpio_fsm, work);
+	spin_lock(&gf->spinlock);
+	new_state = gf->next_state;
+	gf->next_state = NULL;
+	spin_unlock(&gf->spinlock);
+
+	gpio_fsm_go_to_state(gf, new_state);
+}
+
+static irqreturn_t gpio_fsm_gpio_irq_handler(int irq, void *dev_id)
+{
+	struct input_gpio_state *inp_state = dev_id;
+	struct gpio_fsm *gf = inp_state->gf;
+	struct fsm_state *target;
+
+	target = inp_state->target;
+	if (!target)
+		return IRQ_NONE;
+
+	/* If the IRQ has fired then the desired state _must_ have occurred */
+	inp_state->enabled = false;
+	irq_set_irq_type(inp_state->irq, IRQF_TRIGGER_NONE);
+	if (gf->debug)
+		dev_info(gf->dev, "GF_IN %d->%d -> %s\n",
+			 inp_state->index, inp_state->value, target->name);
+	gpio_fsm_go_to_state_deferred(gf, target);
+	return IRQ_HANDLED;
+}
+
+static void gpio_fsm_timer(struct timer_list *timer)
+{
+	struct gpio_fsm *gf = container_of(timer, struct gpio_fsm, timer);
+	struct fsm_state *target;
+
+	target = gf->delay_target_state;
+	if (!target)
+		return;
+	if (gf->debug)
+		dev_info(gf->dev, "GF_DELAY %d -> %s\n", gf->delay_ms,
+			 target->name);
+
+	gpio_fsm_go_to_state_deferred(gf, target);
+}
+
+static int gpio_fsm_parse_signals(struct gpio_fsm *gf, struct fsm_state *state,
+				  struct property *prop)
+{
+	const __be32 *cells = prop->value;
+	struct output_signal *signal;
+	u32 io;
+	u32 type;
+	u32 index;
+	u32 value;
+	int ret = 0;
+	int i;
+
+	if (prop->length % 8) {
+		dev_err(gf->dev, "malformed set in state %s\n",
+			state->name);
+		return -EINVAL;
+	}
+
+	state->num_signals = prop->length/8;
+	state->signals = devm_kcalloc(gf->dev, state->num_signals,
+				      sizeof(struct output_signal),
+				      GFP_KERNEL);
+	for (i = 0; i < state->num_signals; i++) {
+		signal = &state->signals[i];
+		io = be32_to_cpu(cells[0]);
+		type = GF_IO_TYPE(io);
+		index = GF_IO_INDEX(io);
+		value = be32_to_cpu(cells[1]);
+
+		if (type != GF_OUT && type != GF_SOFT) {
+			dev_err(gf->dev,
+				"invalid set type %d in state %s\n",
+				type, state->name);
+			ret = -EINVAL;
+			break;
+		}
+		if (type == GF_OUT && index >= gf->num_output_gpios) {
+			dev_err(gf->dev,
+				"invalid GF_OUT number %d in state %s\n",
+				index, state->name);
+			ret = -EINVAL;
+			break;
+		}
+		if (type == GF_SOFT && index >= gf->num_soft_gpios) {
+			dev_err(gf->dev,
+				"invalid GF_SOFT number %d in state %s\n",
+				index, state->name);
+			ret = -EINVAL;
+			break;
+		}
+		if (value != 0 && value != 1) {
+			dev_err(gf->dev,
+				"invalid set value %d in state %s\n",
+				value, state->name);
+			ret = -EINVAL;
+			break;
+		}
+		signal->type = (type == GF_OUT) ? SIGNAL_GPIO : SIGNAL_SOFT;
+		signal->index = index;
+		signal->value = value;
+		cells += 2;
+	}
+
+	return ret;
+}
+
+static struct gpio_event *new_event(struct gpio_event **events, int *num_events)
+{
+	int num = ++(*num_events);
+	*events = krealloc(*events, num * sizeof(struct gpio_event),
+			   GFP_KERNEL);
+	return *events ? *events + (num - 1) : NULL;
+}
+
+static int gpio_fsm_parse_events(struct gpio_fsm *gf, struct fsm_state *state,
+			    struct property *prop)
+{
+	const __be32 *cells = prop->value;
+	struct symtab_entry *sym;
+	int num_cells;
+	int ret = 0;
+	int i;
+
+	if (prop->length % 8) {
+		dev_err(gf->dev,
+			"malformed transitions from state %s to state %s\n",
+			state->name, prop->name);
+		return -EINVAL;
+	}
+
+	sym = get_symbol(&gf->symtab, prop->name);
+	num_cells = prop->length / 4;
+	i = 0;
+	while (i < num_cells) {
+		struct gpio_event *gp_ev;
+		u32 event, param;
+		u32 index;
+
+		event = be32_to_cpu(cells[i++]);
+		param = be32_to_cpu(cells[i++]);
+		index = GF_IO_INDEX(event);
+
+		switch (GF_IO_TYPE(event)) {
+		case GF_IN:
+			if (index >= gf->num_input_gpios) {
+				dev_err(gf->dev,
+					"invalid GF_IN %d in transitions from state %s to state %s\n",
+					index, state->name, prop->name);
+				return -EINVAL;
+			}
+			if (param > 1) {
+				dev_err(gf->dev,
+					"invalid GF_IN value %d in transitions from state %s to state %s\n",
+					param, state->name, prop->name);
+				return -EINVAL;
+			}
+			gp_ev = new_event(&state->gpio_events,
+					  &state->num_gpio_events);
+			if (!gp_ev)
+				return -ENOMEM;
+			gp_ev->index = index;
+			gp_ev->value = param;
+			gp_ev->target = (struct fsm_state *)sym;
+			break;
+
+		case GF_SOFT:
+			if (index >= gf->num_soft_gpios) {
+				dev_err(gf->dev,
+					"invalid GF_SOFT %d in transitions from state %s to state %s\n",
+					index, state->name, prop->name);
+				return -EINVAL;
+			}
+			if (param > 1) {
+				dev_err(gf->dev,
+					"invalid GF_SOFT value %d in transitions from state %s to state %s\n",
+					param, state->name, prop->name);
+				return -EINVAL;
+			}
+			gp_ev = new_event(&state->soft_events,
+					  &state->num_soft_events);
+			if (!gp_ev)
+				return -ENOMEM;
+			gp_ev->index = index;
+			gp_ev->value = param;
+			gp_ev->target = (struct fsm_state *)sym;
+			break;
+
+		case GF_DELAY:
+			if (state->delay_target) {
+				dev_err(gf->dev,
+					"state %s has multiple GF_DELAYs\n",
+					state->name);
+				return -EINVAL;
+			}
+			state->delay_target = (struct fsm_state *)sym;
+			state->delay_ms = param;
+			break;
+
+		case GF_SHUTDOWN:
+			if (state->shutdown_target == state) {
+				dev_err(gf->dev,
+					"shutdown state %s has GF_SHUTDOWN\n",
+					state->name);
+				return -EINVAL;
+			} else if (state->shutdown_target) {
+				dev_err(gf->dev,
+					"state %s has multiple GF_SHUTDOWNs\n",
+					state->name);
+				return -EINVAL;
+			}
+			state->shutdown_target =
+				(struct fsm_state *)sym;
+			state->shutdown_ms = param;
+			break;
+
+		default:
+			dev_err(gf->dev,
+				"invalid event %08x in transitions from state %s to state %s\n",
+				event, state->name, prop->name);
+			return -EINVAL;
+		}
+	}
+	if (i != num_cells) {
+		dev_err(gf->dev,
+			"malformed transitions from state %s to state %s\n",
+			state->name, prop->name);
+		return -EINVAL;
+	}
+
+	return ret;
+}
+
+static int gpio_fsm_parse_state(struct gpio_fsm *gf,
+			   struct fsm_state *state,
+			   struct device_node *np)
+{
+	struct symtab_entry *sym;
+	struct property *prop;
+	int ret;
+
+	state->name = np->name;
+	ret = add_symbol(&gf->symtab, np->name, state);
+	if (ret) {
+		switch (ret) {
+		case -EINVAL:
+			dev_err(gf->dev, "'%s' is not a valid state name\n",
+				np->name);
+			break;
+		case -EEXIST:
+			dev_err(gf->dev, "state %s already defined\n",
+				np->name);
+			break;
+		default:
+			dev_err(gf->dev, "error %d adding state %s symbol\n",
+				ret, np->name);
+			break;
+		}
+		return ret;
+	}
+
+	for_each_property_of_node(np, prop) {
+		sym = get_symbol(&gf->symtab, prop->name);
+		if (!sym) {
+			ret = -ENOMEM;
+			break;
+		}
+
+		switch ((uintptr_t)sym->value) {
+		case SYM_SET:
+			ret = gpio_fsm_parse_signals(gf, state, prop);
+			break;
+		case SYM_START:
+			if (gf->start_state) {
+				dev_err(gf->dev, "multiple start states\n");
+				ret = -EINVAL;
+			} else {
+				gf->start_state = state;
+			}
+			break;
+		case SYM_SHUTDOWN:
+			state->shutdown_target = state;
+			gf->shutdown_state = state;
+			break;
+		case SYM_NAME:
+			/* Ignore */
+			break;
+		default:
+			/* A set of transition events to this state */
+			ret = gpio_fsm_parse_events(gf, state, prop);
+			break;
+		}
+	}
+
+	return ret;
+}
+
+static void dump_all(struct gpio_fsm *gf)
+{
+	int i, j;
+
+	dev_info(gf->dev, "Input GPIOs:\n");
+	for (i = 0; i < gf->num_input_gpios; i++)
+		dev_info(gf->dev, "  %d: %p\n", i,
+			 gf->input_gpios->desc[i]);
+
+	dev_info(gf->dev, "Output GPIOs:\n");
+	for (i = 0; i < gf->num_output_gpios; i++)
+		dev_info(gf->dev, "  %d: %p\n", i,
+			 gf->output_gpios->desc[i]);
+
+	dev_info(gf->dev, "Soft GPIOs:\n");
+	for (i = 0; i < gf->num_soft_gpios; i++)
+		dev_info(gf->dev, "  %d: %s %d\n", i,
+			 (gf->soft_gpios[i].dir == GPIOF_IN) ? "IN" : "OUT",
+			 gf->soft_gpios[i].value);
+
+	dev_info(gf->dev, "Start state: %s\n",
+		 gf->start_state ? gf->start_state->name : "-");
+
+	dev_info(gf->dev, "Shutdown timeout: %d ms\n",
+		 gf->shutdown_timeout_ms);
+
+	for (i = 0; i < gf->num_states; i++) {
+		struct fsm_state *state = &gf->states[i];
+
+		dev_info(gf->dev, "State %s:\n", state->name);
+
+		if (state->shutdown_target == state)
+			dev_info(gf->dev, "  Shutdown state\n");
+
+		dev_info(gf->dev, "  Signals:\n");
+		for (j = 0; j < state->num_signals; j++) {
+			struct output_signal *signal = &state->signals[j];
+
+			dev_info(gf->dev, "    %d: %s %d=%d\n", j,
+				 (signal->type == SIGNAL_GPIO) ? "GPIO" :
+								 "SOFT",
+				 signal->index, signal->value);
+		}
+
+		dev_info(gf->dev, "  GPIO events:\n");
+		for (j = 0; j < state->num_gpio_events; j++) {
+			struct gpio_event *event = &state->gpio_events[j];
+
+			dev_info(gf->dev, "    %d: %d=%d -> %s\n", j,
+				 event->index, event->value,
+				 event->target->name);
+		}
+
+		dev_info(gf->dev, "  Soft events:\n");
+		for (j = 0; j < state->num_soft_events; j++) {
+			struct gpio_event *event = &state->soft_events[j];
+
+			dev_info(gf->dev, "    %d: %d=%d -> %s\n", j,
+				 event->index, event->value,
+				 event->target->name);
+		}
+
+		if (state->delay_target)
+			dev_info(gf->dev, "  Delay: %d ms -> %s\n",
+				 state->delay_ms, state->delay_target->name);
+
+		if (state->shutdown_target && state->shutdown_target != state)
+			dev_info(gf->dev, "  Shutdown: %d ms -> %s\n",
+				 state->shutdown_ms,
+				 state->shutdown_target->name);
+	}
+	dev_info(gf->dev, "\n");
+}
+
+static int resolve_sym_to_state(struct gpio_fsm *gf, struct fsm_state **pstate)
+{
+	struct symtab_entry *sym = (struct symtab_entry *)*pstate;
+
+	if (!sym)
+		return -ENOMEM;
+
+	*pstate = sym->value;
+
+	if (!*pstate) {
+		dev_err(gf->dev, "state %s not defined\n",
+			sym->name);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static void gpio_fsm_set_soft(struct gpio_fsm *gf,
+			      unsigned int off, int val)
+{
+	struct soft_gpio *sg = &gf->soft_gpios[off];
+	struct gpio_event *gp_ev;
+	struct fsm_state *state;
+	int i;
+
+	dev_dbg(gf->dev, "set(%d,%d)\n", off, val);
+	state = gf->current_state;
+	sg->value = val;
+	for (i = 0; i < state->num_soft_events; i++) {
+		gp_ev = &state->soft_events[i];
+		if (gp_ev->index == off && gp_ev->value == val) {
+			if (gf->debug)
+				dev_info(gf->dev,
+					 "GF_SOFT %d->%d -> %s\n", gp_ev->index,
+					 gp_ev->value, gp_ev->target->name);
+			gpio_fsm_go_to_state(gf, gp_ev->target);
+			break;
+		}
+	}
+}
+
+static int gpio_fsm_get(struct gpio_chip *gc, unsigned int off)
+{
+	struct gpio_fsm *gf = gpiochip_get_data(gc);
+	struct soft_gpio *sg;
+
+	if (off >= gf->num_soft_gpios)
+		return -EINVAL;
+	sg = &gf->soft_gpios[off];
+
+	return sg->value;
+}
+
+static void gpio_fsm_set(struct gpio_chip *gc, unsigned int off, int val)
+{
+	struct gpio_fsm *gf;
+
+	gf = gpiochip_get_data(gc);
+	if (off < gf->num_soft_gpios)
+		gpio_fsm_set_soft(gf, off, val);
+}
+
+static int gpio_fsm_get_direction(struct gpio_chip *gc, unsigned int off)
+{
+	struct gpio_fsm *gf = gpiochip_get_data(gc);
+	struct soft_gpio *sg;
+
+	if (off >= gf->num_soft_gpios)
+		return -EINVAL;
+	sg = &gf->soft_gpios[off];
+
+	return sg->dir;
+}
+
+static int gpio_fsm_direction_input(struct gpio_chip *gc, unsigned int off)
+{
+	struct gpio_fsm *gf = gpiochip_get_data(gc);
+	struct soft_gpio *sg;
+
+	if (off >= gf->num_soft_gpios)
+		return -EINVAL;
+	sg = &gf->soft_gpios[off];
+	sg->dir = GPIOF_IN;
+
+	return 0;
+}
+
+static int gpio_fsm_direction_output(struct gpio_chip *gc, unsigned int off,
+				       int value)
+{
+	struct gpio_fsm *gf = gpiochip_get_data(gc);
+	struct soft_gpio *sg;
+
+	if (off >= gf->num_soft_gpios)
+		return -EINVAL;
+	sg = &gf->soft_gpios[off];
+	sg->dir = GPIOF_OUT_INIT_LOW;
+	gpio_fsm_set_soft(gf, off, value);
+
+	return 0;
+}
+
+/*
+ * /sys/class/gpio-fsm/<fsm-name>/
+ *   /state ... the current state
+ */
+
+static ssize_t state_show(struct device *dev,
+			  struct device_attribute *attr, char *buf)
+{
+	const struct gpio_fsm *gf = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%s\n", gf->current_state->name);
+}
+static DEVICE_ATTR_RO(state);
+
+static ssize_t delay_state_show(struct device *dev,
+			  struct device_attribute *attr, char *buf)
+{
+	const struct gpio_fsm *gf = dev_get_drvdata(dev);
+
+	return sprintf(buf, "%s\n",
+		       gf->delay_target_state ? gf->delay_target_state->name :
+		       "-");
+}
+
+static DEVICE_ATTR_RO(delay_state);
+
+static ssize_t delay_ms_show(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	const struct gpio_fsm *gf = dev_get_drvdata(dev);
+	int jiffies_left;
+
+	jiffies_left = max((int)(gf->delay_jiffies - jiffies), 0);
+	return sprintf(buf,
+		       gf->delay_target_state ? "%u\n" : "-\n",
+		       jiffies_to_msecs(jiffies_left));
+}
+static DEVICE_ATTR_RO(delay_ms);
+
+static struct attribute *gpio_fsm_attrs[] = {
+	&dev_attr_state.attr,
+	&dev_attr_delay_state.attr,
+	&dev_attr_delay_ms.attr,
+	NULL,
+};
+
+static const struct attribute_group gpio_fsm_group = {
+	.attrs = gpio_fsm_attrs,
+	//.is_visible = gpio_is_visible,
+};
+
+static const struct attribute_group *gpio_fsm_groups[] = {
+	&gpio_fsm_group,
+	NULL
+};
+
+static struct attribute *gpio_fsm_class_attrs[] = {
+	// There are no top-level attributes
+	NULL,
+};
+ATTRIBUTE_GROUPS(gpio_fsm_class);
+
+static struct class gpio_fsm_class = {
+	.name =		MODULE_NAME,
+
+	.class_groups = gpio_fsm_class_groups,
+};
+
+static int gpio_fsm_probe(struct platform_device *pdev)
+{
+	struct input_gpio_state *inp_state;
+	struct device *dev = &pdev->dev;
+	struct device *sysfs_dev;
+	struct device_node *np = dev_of_node(dev);
+	struct device_node *cp;
+	struct gpio_fsm *gf;
+	u32 debug = 0;
+	int num_states;
+	u32 num_soft_gpios;
+	int ret;
+	int i;
+	static const char *const reserved_symbols[] = {
+		[SYM_NAME] = "name",
+		[SYM_SET] = "set",
+		[SYM_START] = "start_state",
+		[SYM_SHUTDOWN] = "shutdown_state",
+	};
+
+	if (of_property_read_u32(np, "num-swgpios", &num_soft_gpios) &&
+	    of_property_read_u32(np, "num-soft-gpios", &num_soft_gpios)) {
+		dev_err(dev, "missing 'num-swgpios' property\n");
+		return -EINVAL;
+	}
+
+	of_property_read_u32(np, "debug", &debug);
+
+	gf = devm_kzalloc(dev, sizeof(*gf), GFP_KERNEL);
+	if (!gf)
+		return -ENOMEM;
+
+	gf->dev = dev;
+	gf->debug = debug;
+
+	if (of_property_read_u32(np, "shutdown-timeout-ms",
+				 &gf->shutdown_timeout_ms))
+		gf->shutdown_timeout_ms = 5000;
+
+	gf->num_soft_gpios = num_soft_gpios;
+	gf->soft_gpios = devm_kcalloc(dev, num_soft_gpios,
+				      sizeof(struct soft_gpio), GFP_KERNEL);
+	if (!gf->soft_gpios)
+		return -ENOMEM;
+	for (i = 0; i < num_soft_gpios; i++) {
+		struct soft_gpio *sg = &gf->soft_gpios[i];
+
+		sg->dir = GPIOF_IN;
+		sg->value = 0;
+	}
+
+	gf->input_gpios = devm_gpiod_get_array_optional(dev, "input", GPIOD_IN);
+	if (IS_ERR(gf->input_gpios)) {
+		ret = PTR_ERR(gf->input_gpios);
+		dev_err(dev, "failed to get input gpios from DT - %d\n", ret);
+		return ret;
+	}
+	gf->num_input_gpios = (gf->input_gpios ? gf->input_gpios->ndescs : 0);
+
+	gf->input_gpio_states = devm_kcalloc(dev, gf->num_input_gpios,
+					     sizeof(struct input_gpio_state),
+					     GFP_KERNEL);
+	if (!gf->input_gpio_states)
+		return -ENOMEM;
+	for (i = 0; i < gf->num_input_gpios; i++) {
+		inp_state = &gf->input_gpio_states[i];
+		inp_state->desc = gf->input_gpios->desc[i];
+		inp_state->gf = gf;
+		inp_state->index = i;
+		inp_state->irq = gpiod_to_irq(inp_state->desc);
+		inp_state->active_low = gpiod_is_active_low(inp_state->desc);
+		if (inp_state->irq >= 0)
+			ret = devm_request_irq(gf->dev, inp_state->irq,
+					       gpio_fsm_gpio_irq_handler,
+					       IRQF_TRIGGER_NONE,
+					       dev_name(dev),
+					       inp_state);
+		else
+			ret = inp_state->irq;
+
+		if (ret) {
+			dev_err(dev,
+				"failed to get IRQ for input gpio - %d\n",
+				ret);
+			return ret;
+		}
+	}
+
+	gf->output_gpios = devm_gpiod_get_array_optional(dev, "output",
+							 GPIOD_OUT_LOW);
+	if (IS_ERR(gf->output_gpios)) {
+		ret = PTR_ERR(gf->output_gpios);
+		dev_err(dev, "failed to get output gpios from DT - %d\n", ret);
+		return ret;
+	}
+	gf->num_output_gpios = (gf->output_gpios ? gf->output_gpios->ndescs :
+				0);
+
+	num_states = of_get_child_count(np);
+	if (!num_states) {
+		dev_err(dev, "no states declared\n");
+		return -EINVAL;
+	}
+	gf->states = devm_kcalloc(dev, num_states,
+				  sizeof(struct fsm_state), GFP_KERNEL);
+	if (!gf->states)
+		return -ENOMEM;
+
+	// add reserved words to the symbol table
+	for (i = 0; i < ARRAY_SIZE(reserved_symbols); i++) {
+		if (reserved_symbols[i])
+			add_symbol(&gf->symtab, reserved_symbols[i],
+				   (void *)(uintptr_t)i);
+	}
+
+	// parse the state
+	for_each_child_of_node(np, cp) {
+		struct fsm_state *state = &gf->states[gf->num_states];
+
+		ret = gpio_fsm_parse_state(gf, state, cp);
+		if (ret)
+			return ret;
+		gf->num_states++;
+	}
+
+	if (!gf->start_state) {
+		dev_err(gf->dev, "no start state defined\n");
+		return -EINVAL;
+	}
+
+	// resolve symbol pointers into state pointers
+	for (i = 0; !ret && i < gf->num_states; i++) {
+		struct fsm_state *state = &gf->states[i];
+		int j;
+
+		for (j = 0; !ret && j < state->num_gpio_events; j++) {
+			struct gpio_event *ev = &state->gpio_events[j];
+
+			ret = resolve_sym_to_state(gf, &ev->target);
+		}
+
+		for (j = 0; !ret && j < state->num_soft_events; j++) {
+			struct gpio_event *ev = &state->soft_events[j];
+
+			ret = resolve_sym_to_state(gf, &ev->target);
+		}
+
+		if (!ret) {
+			resolve_sym_to_state(gf, &state->delay_target);
+			if (state->shutdown_target != state)
+				resolve_sym_to_state(gf,
+						     &state->shutdown_target);
+		}
+	}
+
+	if (!ret && gf->debug > 1)
+		dump_all(gf);
+
+	free_symbols(&gf->symtab);
+
+	if (ret)
+		return ret;
+
+	gf->gc.parent = dev;
+	gf->gc.label = np->name;
+	gf->gc.owner = THIS_MODULE;
+	gf->gc.base = -1;
+	gf->gc.ngpio = num_soft_gpios;
+
+	gf->gc.get_direction = gpio_fsm_get_direction;
+	gf->gc.direction_input = gpio_fsm_direction_input;
+	gf->gc.direction_output = gpio_fsm_direction_output;
+	gf->gc.get = gpio_fsm_get;
+	gf->gc.set = gpio_fsm_set;
+	gf->gc.can_sleep = true;
+	spin_lock_init(&gf->spinlock);
+	INIT_WORK(&gf->work, gpio_fsm_work);
+	timer_setup(&gf->timer, gpio_fsm_timer, 0);
+	init_waitqueue_head(&gf->shutdown_event);
+
+	platform_set_drvdata(pdev, gf);
+
+	sysfs_dev = device_create_with_groups(&gpio_fsm_class, dev,
+					      MKDEV(0, 0), gf,
+					      gpio_fsm_groups,
+					      "%s", np->name);
+	if (IS_ERR(sysfs_dev))
+		dev_err(gf->dev, "Error creating sysfs entry\n");
+
+	if (gf->debug)
+		dev_info(gf->dev, "Start -> %s\n", gf->start_state->name);
+
+	gpio_fsm_enter_state(gf, gf->start_state);
+
+	return devm_gpiochip_add_data(dev, &gf->gc, gf);
+}
+
+static void gpio_fsm_remove(struct platform_device *pdev)
+{
+	struct gpio_fsm *gf = platform_get_drvdata(pdev);
+	int i;
+
+	if (gf->shutdown_state) {
+		if (gf->debug)
+			dev_info(gf->dev, "Shutting down...\n");
+
+		spin_lock(&gf->spinlock);
+		gf->shutting_down = true;
+		if (gf->current_state->shutdown_target &&
+		    gf->current_state->shutdown_target != gf->current_state) {
+			gf->delay_target_state =
+				gf->current_state->shutdown_target;
+			mod_timer(&gf->timer, gf->shutdown_jiffies);
+		}
+		spin_unlock(&gf->spinlock);
+
+		wait_event_timeout(gf->shutdown_event,
+				   gf->current_state->shutdown_target ==
+				   gf->current_state,
+				   msecs_to_jiffies(gf->shutdown_timeout_ms));
+		/* On failure to reach a shutdown state, jump to one */
+		if (gf->current_state->shutdown_target != gf->current_state)
+			gpio_fsm_enter_state(gf, gf->shutdown_state);
+	}
+	cancel_work_sync(&gf->work);
+	del_timer_sync(&gf->timer);
+
+	/* Events aren't allocated from managed storage */
+	for (i = 0; i < gf->num_states; i++) {
+		kfree(gf->states[i].gpio_events);
+		kfree(gf->states[i].soft_events);
+	}
+	if (gf->debug)
+		dev_info(gf->dev, "Exiting\n");
+}
+
+static void gpio_fsm_shutdown(struct platform_device *pdev)
+{
+	gpio_fsm_remove(pdev);
+}
+
+static const struct of_device_id gpio_fsm_ids[] = {
+	{ .compatible = "rpi,gpio-fsm" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, gpio_fsm_ids);
+
+static struct platform_driver gpio_fsm_driver = {
+	.driver	= {
+		.name		= MODULE_NAME,
+		.of_match_table	= of_match_ptr(gpio_fsm_ids),
+	},
+	.probe = gpio_fsm_probe,
+	.remove = gpio_fsm_remove,
+	.shutdown = gpio_fsm_shutdown,
+};
+
+static int gpio_fsm_init(void)
+{
+	int ret;
+
+	ret = class_register(&gpio_fsm_class);
+	if (ret)
+		return ret;
+
+	ret = platform_driver_register(&gpio_fsm_driver);
+	if (ret)
+		class_unregister(&gpio_fsm_class);
+
+	return ret;
+}
+module_init(gpio_fsm_init);
+
+static void gpio_fsm_exit(void)
+{
+	platform_driver_unregister(&gpio_fsm_driver);
+	class_unregister(&gpio_fsm_class);
+}
+module_exit(gpio_fsm_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Phil Elwell <phil@raspberrypi.com>");
+MODULE_DESCRIPTION("GPIO FSM driver");
+MODULE_ALIAS("platform:gpio-fsm");
