/* * Copyright (c) 2019 Peter Bigot Consulting, LLC * Copyright (c) 2020 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #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; }