653 lines
16 KiB
C
653 lines
16 KiB
C
/*
|
|
* Copyright (c) 2019 Peter Bigot Consulting, LLC
|
|
* Copyright (c) 2020 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <kernel.h>
|
|
#include <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 simplfy 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) {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
state = mgr->flags & ONOFF_STATE_MASK;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
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) {
|
|
/* 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;
|
|
}
|