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.
655 lines
16 KiB
655 lines
16 KiB
/* |
|
* Copyright (c) 2019 Peter Bigot Consulting, LLC |
|
* Copyright (c) 2020 Nordic Semiconductor ASA |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#include <zephyr/kernel.h> |
|
#include <zephyr/sys/onoff.h> |
|
#include <stdio.h> |
|
|
|
#define SERVICE_REFS_MAX UINT16_MAX |
|
|
|
/* Confirm consistency of public flags with private flags */ |
|
BUILD_ASSERT((ONOFF_FLAG_ERROR | ONOFF_FLAG_ONOFF | ONOFF_FLAG_TRANSITION) |
|
< BIT(3)); |
|
|
|
#define ONOFF_FLAG_PROCESSING BIT(3) |
|
#define ONOFF_FLAG_COMPLETE BIT(4) |
|
#define ONOFF_FLAG_RECHECK BIT(5) |
|
|
|
/* These symbols in the ONOFF_FLAGS namespace identify bits in |
|
* onoff_manager::flags that indicate the state of the machine. The |
|
* bits are manipulated by process_event() under lock, and actions |
|
* cued by bit values are executed outside of lock within |
|
* process_event(). |
|
* |
|
* * ERROR indicates that the machine is in an error state. When |
|
* this bit is set ONOFF will be cleared. |
|
* * ONOFF indicates whether the target/current state is off (clear) |
|
* or on (set). |
|
* * TRANSITION indicates whether a service transition function is in |
|
* progress. It combines with ONOFF to identify start and stop |
|
* transitions, and with ERROR to identify a reset transition. |
|
* * PROCESSING indicates that the process_event() loop is active. It |
|
* is used to defer initiation of transitions and other complex |
|
* state changes while invoking notifications associated with a |
|
* state transition. This bounds the depth by limiting |
|
* active process_event() call stacks to two instances. State changes |
|
* initiated by a nested call will be executed when control returns |
|
* to the parent call. |
|
* * COMPLETE indicates that a transition completion notification has |
|
* been received. This flag is set in the notification, and cleared |
|
* by process_events() which is invoked from the notification. In |
|
* the case of nested process_events() the processing is deferred to |
|
* the top invocation. |
|
* * RECHECK indicates that a state transition has completed but |
|
* process_events() must re-check the overall state to confirm no |
|
* additional transitions are required. This is used to simplify the |
|
* logic when, for example, a request is received during a |
|
* transition to off, which means that when the transition completes |
|
* a transition to on must be initiated if the request is still |
|
* present. Transition to ON with no remaining requests similarly |
|
* triggers a recheck. |
|
*/ |
|
|
|
/* Identify the events that can trigger state changes, as well as an |
|
* internal state used when processing deferred actions. |
|
*/ |
|
enum event_type { |
|
/* No-op event: used to process deferred changes. |
|
* |
|
* This event is local to the process loop. |
|
*/ |
|
EVT_NOP, |
|
|
|
/* Completion of a service transition. |
|
* |
|
* This event is triggered by the transition notify callback. |
|
* It can be received only when the machine is in a transition |
|
* state (TO-ON, TO-OFF, or RESETTING). |
|
*/ |
|
EVT_COMPLETE, |
|
|
|
/* Reassess whether a transition from a stable state is needed. |
|
* |
|
* This event causes: |
|
* * a start from OFF when there are clients; |
|
* * a stop from ON when there are no clients; |
|
* * a reset from ERROR when there are clients. |
|
* |
|
* The client list can change while the manager lock is |
|
* released (e.g. during client and monitor notifications and |
|
* transition initiations), so this event records the |
|
* potential for these state changes, and process_event() ... |
|
* |
|
*/ |
|
EVT_RECHECK, |
|
|
|
/* Transition to on. |
|
* |
|
* This is synthesized from EVT_RECHECK in a non-nested |
|
* process_event() when state OFF is confirmed with a |
|
* non-empty client (request) list. |
|
*/ |
|
EVT_START, |
|
|
|
/* Transition to off. |
|
* |
|
* This is synthesized from EVT_RECHECK in a non-nested |
|
* process_event() when state ON is confirmed with a |
|
* zero reference count. |
|
*/ |
|
EVT_STOP, |
|
|
|
/* Transition to resetting. |
|
* |
|
* This is synthesized from EVT_RECHECK in a non-nested |
|
* process_event() when state ERROR is confirmed with a |
|
* non-empty client (reset) list. |
|
*/ |
|
EVT_RESET, |
|
}; |
|
|
|
static void set_state(struct onoff_manager *mgr, |
|
uint32_t state) |
|
{ |
|
mgr->flags = (state & ONOFF_STATE_MASK) |
|
| (mgr->flags & ~ONOFF_STATE_MASK); |
|
} |
|
|
|
static int validate_args(const struct onoff_manager *mgr, |
|
struct onoff_client *cli) |
|
{ |
|
if ((mgr == NULL) || (cli == NULL)) { |
|
return -EINVAL; |
|
} |
|
|
|
int rv = sys_notify_validate(&cli->notify); |
|
|
|
if ((rv == 0) |
|
&& ((cli->notify.flags |
|
& ~BIT_MASK(ONOFF_CLIENT_EXTENSION_POS)) != 0)) { |
|
rv = -EINVAL; |
|
} |
|
|
|
return rv; |
|
} |
|
|
|
int onoff_manager_init(struct onoff_manager *mgr, |
|
const struct onoff_transitions *transitions) |
|
{ |
|
if ((mgr == NULL) |
|
|| (transitions == NULL) |
|
|| (transitions->start == NULL) |
|
|| (transitions->stop == NULL)) { |
|
return -EINVAL; |
|
} |
|
|
|
*mgr = (struct onoff_manager)ONOFF_MANAGER_INITIALIZER(transitions); |
|
|
|
return 0; |
|
} |
|
|
|
static void notify_monitors(struct onoff_manager *mgr, |
|
uint32_t state, |
|
int res) |
|
{ |
|
sys_slist_t *mlist = &mgr->monitors; |
|
struct onoff_monitor *mon; |
|
struct onoff_monitor *tmp; |
|
|
|
SYS_SLIST_FOR_EACH_CONTAINER_SAFE(mlist, mon, tmp, node) { |
|
mon->callback(mgr, mon, state, res); |
|
} |
|
} |
|
|
|
static void notify_one(struct onoff_manager *mgr, |
|
struct onoff_client *cli, |
|
uint32_t state, |
|
int res) |
|
{ |
|
onoff_client_callback cb = |
|
(onoff_client_callback)sys_notify_finalize(&cli->notify, res); |
|
|
|
if (cb != NULL) { |
|
cb(mgr, cli, state, res); |
|
} |
|
} |
|
|
|
static void notify_all(struct onoff_manager *mgr, |
|
sys_slist_t *list, |
|
uint32_t state, |
|
int res) |
|
{ |
|
while (!sys_slist_is_empty(list)) { |
|
sys_snode_t *node = sys_slist_get_not_empty(list); |
|
struct onoff_client *cli = |
|
CONTAINER_OF(node, |
|
struct onoff_client, |
|
node); |
|
|
|
notify_one(mgr, cli, state, res); |
|
} |
|
} |
|
|
|
static void process_event(struct onoff_manager *mgr, |
|
int evt, |
|
k_spinlock_key_t key); |
|
|
|
static void transition_complete(struct onoff_manager *mgr, |
|
int res) |
|
{ |
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
|
|
mgr->last_res = res; |
|
process_event(mgr, EVT_COMPLETE, key); |
|
} |
|
|
|
/* Detect whether static state requires a transition. */ |
|
static int process_recheck(struct onoff_manager *mgr) |
|
{ |
|
int evt = EVT_NOP; |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
|
|
if ((state == ONOFF_STATE_OFF) |
|
&& !sys_slist_is_empty(&mgr->clients)) { |
|
evt = EVT_START; |
|
} else if ((state == ONOFF_STATE_ON) |
|
&& (mgr->refs == 0U)) { |
|
evt = EVT_STOP; |
|
} else if ((state == ONOFF_STATE_ERROR) |
|
&& !sys_slist_is_empty(&mgr->clients)) { |
|
evt = EVT_RESET; |
|
} else { |
|
; |
|
} |
|
|
|
return evt; |
|
} |
|
|
|
/* Process a transition completion. |
|
* |
|
* If the completion requires notifying clients, the clients are moved |
|
* from the manager to the output list for notification. |
|
*/ |
|
static void process_complete(struct onoff_manager *mgr, |
|
sys_slist_t *clients, |
|
int res) |
|
{ |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
|
|
if (res < 0) { |
|
/* Enter ERROR state and notify all clients. */ |
|
*clients = mgr->clients; |
|
sys_slist_init(&mgr->clients); |
|
set_state(mgr, ONOFF_STATE_ERROR); |
|
} else if ((state == ONOFF_STATE_TO_ON) |
|
|| (state == ONOFF_STATE_RESETTING)) { |
|
*clients = mgr->clients; |
|
sys_slist_init(&mgr->clients); |
|
|
|
if (state == ONOFF_STATE_TO_ON) { |
|
struct onoff_client *cp; |
|
|
|
/* Increment reference count for all remaining |
|
* clients and enter ON state. |
|
*/ |
|
SYS_SLIST_FOR_EACH_CONTAINER(clients, cp, node) { |
|
mgr->refs += 1U; |
|
} |
|
|
|
set_state(mgr, ONOFF_STATE_ON); |
|
} else { |
|
__ASSERT_NO_MSG(state == ONOFF_STATE_RESETTING); |
|
|
|
set_state(mgr, ONOFF_STATE_OFF); |
|
} |
|
if (process_recheck(mgr) != EVT_NOP) { |
|
mgr->flags |= ONOFF_FLAG_RECHECK; |
|
} |
|
} else if (state == ONOFF_STATE_TO_OFF) { |
|
/* Any active clients are requests waiting for this |
|
* transition to complete. Queue a RECHECK event to |
|
* ensure we don't miss them if we don't unlock to |
|
* tell anybody about the completion. |
|
*/ |
|
set_state(mgr, ONOFF_STATE_OFF); |
|
if (process_recheck(mgr) != EVT_NOP) { |
|
mgr->flags |= ONOFF_FLAG_RECHECK; |
|
} |
|
} else { |
|
__ASSERT_NO_MSG(false); |
|
} |
|
} |
|
|
|
/* There are two points in the state machine where the machine is |
|
* unlocked to perform some external action: |
|
* * Initiation of an transition due to some event; |
|
* * Invocation of the user-specified callback when a stable state is |
|
* reached or an error detected. |
|
* |
|
* Events received during these unlocked periods are recorded in the |
|
* state, but processing is deferred to the top-level invocation which |
|
* will loop to handle any events that occurred during the unlocked |
|
* regions. |
|
*/ |
|
static void process_event(struct onoff_manager *mgr, |
|
int evt, |
|
k_spinlock_key_t key) |
|
{ |
|
sys_slist_t clients; |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
int res = 0; |
|
bool processing = ((mgr->flags & ONOFF_FLAG_PROCESSING) != 0); |
|
|
|
__ASSERT_NO_MSG(evt != EVT_NOP); |
|
|
|
/* If this is a nested call record the event for processing in |
|
* the top invocation. |
|
*/ |
|
if (processing) { |
|
if (evt == EVT_COMPLETE) { |
|
mgr->flags |= ONOFF_FLAG_COMPLETE; |
|
} else { |
|
__ASSERT_NO_MSG(evt == EVT_RECHECK); |
|
|
|
mgr->flags |= ONOFF_FLAG_RECHECK; |
|
} |
|
|
|
goto out; |
|
} |
|
|
|
sys_slist_init(&clients); |
|
do { |
|
onoff_transition_fn transit = NULL; |
|
|
|
if (evt == EVT_RECHECK) { |
|
evt = process_recheck(mgr); |
|
} |
|
|
|
if (evt == EVT_NOP) { |
|
break; |
|
} |
|
|
|
res = 0; |
|
if (evt == EVT_COMPLETE) { |
|
res = mgr->last_res; |
|
process_complete(mgr, &clients, res); |
|
/* NB: This can trigger a RECHECK */ |
|
} else if (evt == EVT_START) { |
|
__ASSERT_NO_MSG(state == ONOFF_STATE_OFF); |
|
__ASSERT_NO_MSG(!sys_slist_is_empty(&mgr->clients)); |
|
|
|
transit = mgr->transitions->start; |
|
__ASSERT_NO_MSG(transit != NULL); |
|
set_state(mgr, ONOFF_STATE_TO_ON); |
|
} else if (evt == EVT_STOP) { |
|
__ASSERT_NO_MSG(state == ONOFF_STATE_ON); |
|
__ASSERT_NO_MSG(mgr->refs == 0); |
|
|
|
transit = mgr->transitions->stop; |
|
__ASSERT_NO_MSG(transit != NULL); |
|
set_state(mgr, ONOFF_STATE_TO_OFF); |
|
} else if (evt == EVT_RESET) { |
|
__ASSERT_NO_MSG(state == ONOFF_STATE_ERROR); |
|
__ASSERT_NO_MSG(!sys_slist_is_empty(&mgr->clients)); |
|
|
|
transit = mgr->transitions->reset; |
|
__ASSERT_NO_MSG(transit != NULL); |
|
set_state(mgr, ONOFF_STATE_RESETTING); |
|
} else { |
|
__ASSERT_NO_MSG(false); |
|
} |
|
|
|
/* Have to unlock and do something if any of: |
|
* * We changed state and there are monitors; |
|
* * We completed a transition and there are clients to notify; |
|
* * We need to initiate a transition. |
|
*/ |
|
bool do_monitors = (state != (mgr->flags & ONOFF_STATE_MASK)) |
|
&& !sys_slist_is_empty(&mgr->monitors); |
|
|
|
evt = EVT_NOP; |
|
if (do_monitors |
|
|| !sys_slist_is_empty(&clients) |
|
|| (transit != NULL)) { |
|
uint32_t flags = mgr->flags | ONOFF_FLAG_PROCESSING; |
|
|
|
mgr->flags = flags; |
|
state = flags & ONOFF_STATE_MASK; |
|
|
|
k_spin_unlock(&mgr->lock, key); |
|
|
|
if (do_monitors) { |
|
notify_monitors(mgr, state, res); |
|
} |
|
|
|
if (!sys_slist_is_empty(&clients)) { |
|
notify_all(mgr, &clients, state, res); |
|
} |
|
|
|
if (transit != NULL) { |
|
transit(mgr, transition_complete); |
|
} |
|
|
|
key = k_spin_lock(&mgr->lock); |
|
mgr->flags &= ~ONOFF_FLAG_PROCESSING; |
|
} |
|
|
|
/* Process deferred events. Completion takes priority |
|
* over recheck. |
|
*/ |
|
if ((mgr->flags & ONOFF_FLAG_COMPLETE) != 0) { |
|
mgr->flags &= ~ONOFF_FLAG_COMPLETE; |
|
evt = EVT_COMPLETE; |
|
} else if ((mgr->flags & ONOFF_FLAG_RECHECK) != 0) { |
|
mgr->flags &= ~ONOFF_FLAG_RECHECK; |
|
evt = EVT_RECHECK; |
|
} else { |
|
; |
|
} |
|
|
|
state = mgr->flags & ONOFF_STATE_MASK; |
|
} while (evt != EVT_NOP); |
|
|
|
out: |
|
k_spin_unlock(&mgr->lock, key); |
|
} |
|
|
|
int onoff_request(struct onoff_manager *mgr, |
|
struct onoff_client *cli) |
|
{ |
|
bool add_client = false; /* add client to pending list */ |
|
bool start = false; /* trigger a start transition */ |
|
bool notify = false; /* do client notification */ |
|
int rv = validate_args(mgr, cli); |
|
|
|
if (rv < 0) { |
|
return rv; |
|
} |
|
|
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
|
|
/* Reject if this would overflow the reference count. */ |
|
if (mgr->refs == SERVICE_REFS_MAX) { |
|
rv = -EAGAIN; |
|
goto out; |
|
} |
|
|
|
rv = state; |
|
if (state == ONOFF_STATE_ON) { |
|
/* Increment reference count, notify in exit */ |
|
notify = true; |
|
mgr->refs += 1U; |
|
} else if ((state == ONOFF_STATE_OFF) |
|
|| (state == ONOFF_STATE_TO_OFF) |
|
|| (state == ONOFF_STATE_TO_ON)) { |
|
/* Start if OFF, queue client */ |
|
start = (state == ONOFF_STATE_OFF); |
|
add_client = true; |
|
} else if (state == ONOFF_STATE_RESETTING) { |
|
rv = -ENOTSUP; |
|
} else { |
|
__ASSERT_NO_MSG(state == ONOFF_STATE_ERROR); |
|
rv = -EIO; |
|
} |
|
|
|
out: |
|
if (add_client) { |
|
sys_slist_append(&mgr->clients, &cli->node); |
|
} |
|
|
|
if (start) { |
|
process_event(mgr, EVT_RECHECK, key); |
|
} else { |
|
k_spin_unlock(&mgr->lock, key); |
|
|
|
if (notify) { |
|
notify_one(mgr, cli, state, 0); |
|
} |
|
} |
|
|
|
return rv; |
|
} |
|
|
|
int onoff_release(struct onoff_manager *mgr) |
|
{ |
|
bool stop = false; /* trigger a stop transition */ |
|
|
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
int rv = state; |
|
|
|
if (state != ONOFF_STATE_ON) { |
|
if (state == ONOFF_STATE_ERROR) { |
|
rv = -EIO; |
|
} else { |
|
rv = -ENOTSUP; |
|
} |
|
goto out; |
|
} |
|
|
|
__ASSERT_NO_MSG(mgr->refs > 0); |
|
mgr->refs -= 1U; |
|
stop = (mgr->refs == 0); |
|
|
|
out: |
|
if (stop) { |
|
process_event(mgr, EVT_RECHECK, key); |
|
} else { |
|
k_spin_unlock(&mgr->lock, key); |
|
} |
|
|
|
return rv; |
|
} |
|
|
|
int onoff_reset(struct onoff_manager *mgr, |
|
struct onoff_client *cli) |
|
{ |
|
bool reset = false; |
|
int rv = validate_args(mgr, cli); |
|
|
|
if ((rv >= 0) |
|
&& (mgr->transitions->reset == NULL)) { |
|
rv = -ENOTSUP; |
|
} |
|
|
|
if (rv < 0) { |
|
return rv; |
|
} |
|
|
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
|
|
rv = state; |
|
|
|
if ((state & ONOFF_FLAG_ERROR) == 0) { |
|
rv = -EALREADY; |
|
} else { |
|
reset = (state != ONOFF_STATE_RESETTING); |
|
sys_slist_append(&mgr->clients, &cli->node); |
|
} |
|
|
|
if (reset) { |
|
process_event(mgr, EVT_RECHECK, key); |
|
} else { |
|
k_spin_unlock(&mgr->lock, key); |
|
} |
|
|
|
return rv; |
|
} |
|
|
|
int onoff_cancel(struct onoff_manager *mgr, |
|
struct onoff_client *cli) |
|
{ |
|
if ((mgr == NULL) || (cli == NULL)) { |
|
return -EINVAL; |
|
} |
|
|
|
int rv = -EALREADY; |
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
uint32_t state = mgr->flags & ONOFF_STATE_MASK; |
|
|
|
if (sys_slist_find_and_remove(&mgr->clients, &cli->node)) { |
|
__ASSERT_NO_MSG((state == ONOFF_STATE_TO_ON) |
|
|| (state == ONOFF_STATE_TO_OFF) |
|
|| (state == ONOFF_STATE_RESETTING)); |
|
rv = state; |
|
} |
|
|
|
k_spin_unlock(&mgr->lock, key); |
|
|
|
return rv; |
|
} |
|
|
|
int onoff_monitor_register(struct onoff_manager *mgr, |
|
struct onoff_monitor *mon) |
|
{ |
|
if ((mgr == NULL) |
|
|| (mon == NULL) |
|
|| (mon->callback == NULL)) { |
|
return -EINVAL; |
|
} |
|
|
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
|
|
sys_slist_append(&mgr->monitors, &mon->node); |
|
|
|
k_spin_unlock(&mgr->lock, key); |
|
|
|
return 0; |
|
} |
|
|
|
int onoff_monitor_unregister(struct onoff_manager *mgr, |
|
struct onoff_monitor *mon) |
|
{ |
|
int rv = -EINVAL; |
|
|
|
if ((mgr == NULL) |
|
|| (mon == NULL)) { |
|
return rv; |
|
} |
|
|
|
k_spinlock_key_t key = k_spin_lock(&mgr->lock); |
|
|
|
if (sys_slist_find_and_remove(&mgr->monitors, &mon->node)) { |
|
rv = 0; |
|
} |
|
|
|
k_spin_unlock(&mgr->lock, key); |
|
|
|
return rv; |
|
} |
|
|
|
int onoff_sync_lock(struct onoff_sync_service *srv, |
|
k_spinlock_key_t *keyp) |
|
{ |
|
*keyp = k_spin_lock(&srv->lock); |
|
return srv->count; |
|
} |
|
|
|
int onoff_sync_finalize(struct onoff_sync_service *srv, |
|
k_spinlock_key_t key, |
|
struct onoff_client *cli, |
|
int res, |
|
bool on) |
|
{ |
|
uint32_t state = ONOFF_STATE_ON; |
|
|
|
/* Clear errors visible when locked. If they are to be |
|
* preserved the caller must finalize with the previous |
|
* error code. |
|
*/ |
|
if (srv->count < 0) { |
|
srv->count = 0; |
|
} |
|
if (res < 0) { |
|
srv->count = res; |
|
state = ONOFF_STATE_ERROR; |
|
} else if (on) { |
|
srv->count += 1; |
|
} else { |
|
srv->count -= 1; |
|
/* state would be either off or on, but since |
|
* callbacks are used only when turning on don't |
|
* bother changing it. |
|
*/ |
|
} |
|
|
|
int rv = srv->count; |
|
|
|
k_spin_unlock(&srv->lock, key); |
|
|
|
if (cli != NULL) { |
|
/* Detect service mis-use: onoff does not callback on transition |
|
* to off, so no client should have been passed. |
|
*/ |
|
__ASSERT_NO_MSG(on); |
|
notify_one(NULL, cli, state, res); |
|
} |
|
|
|
return rv; |
|
}
|
|
|