forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1210 lines
28 KiB
1210 lines
28 KiB
// 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/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 int 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 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_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_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>"); |
|
|
|
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; |
|
gf->delay_target_state = NULL; |
|
|
|
if (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); |
|
|
|
if (new_state) |
|
schedule_work(&gf->work); |
|
} |
|
|
|
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_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_DIR_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_DIR_OUT; |
|
gpio_fsm_set_soft(gf, off, value); |
|
|
|
return 0; |
|
} |
|
|
|
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 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; |
|
|
|
// 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_go_to_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(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_go_to_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_work(struct work_struct *work) |
|
{ |
|
struct input_gpio_state *inp_state; |
|
struct fsm_state *new_state; |
|
struct fsm_state *state; |
|
struct gpio_event *gp_ev; |
|
struct gpio_fsm *gf; |
|
int i; |
|
|
|
gf = container_of(work, struct gpio_fsm, work); |
|
spin_lock(&gf->spinlock); |
|
state = gf->current_state; |
|
new_state = gf->next_state; |
|
if (!new_state) |
|
new_state = gf->delay_target_state; |
|
gf->next_state = NULL; |
|
gf->delay_target_state = NULL; |
|
spin_unlock(&gf->spinlock); |
|
|
|
if (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); |
|
} |
|
} |
|
} |
|
|
|
if (new_state) |
|
gpio_fsm_enter_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(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(gf, target); |
|
} |
|
|
|
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; |
|
} |
|
|
|
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; |
|
} |
|
|
|
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; |
|
} |
|
|
|
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_DIR_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; |
|
} |
|
|
|
|
|
/* |
|
* /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, |
|
.owner = THIS_MODULE, |
|
|
|
.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; |
|
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_DIR_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.of_node = np; |
|
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_go_to_state(gf, gf->start_state); |
|
|
|
return devm_gpiochip_add_data(dev, &gf->gc, gf); |
|
} |
|
|
|
static int 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"); |
|
|
|
return 0; |
|
} |
|
|
|
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 <[email protected]>"); |
|
MODULE_DESCRIPTION("GPIO FSM driver"); |
|
MODULE_ALIAS("platform:gpio-fsm");
|
|
|