diff --git a/CODEOWNERS b/CODEOWNERS index 2d011032433..b8dc7941317 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -105,6 +105,7 @@ /doc/scripts/ @carlescufi /doc/guides/bluetooth/ @joerchan @jhedberg @Vudentz /doc/reference/bluetooth/ @joerchan @jhedberg @Vudentz +/doc/reference/kernel/other/resource_mgmt.rst @pabigot /drivers/*/*cc13xx_cc26xx* @bwitherspoon /drivers/*/*mcux* @MaureenHelm /drivers/*/*stm32* @erwango diff --git a/doc/reference/kernel/index.rst b/doc/reference/kernel/index.rst index b4f0ef27b43..21504fcc065 100644 --- a/doc/reference/kernel/index.rst +++ b/doc/reference/kernel/index.rst @@ -116,6 +116,7 @@ These pages cover other kernel services. other/atomic.rst other/float.rst other/ring_buffers.rst + other/resource_mgmt.rst other/cxx_support.rst other/version.rst other/fatal.rst diff --git a/doc/reference/kernel/other/resource_mgmt.rst b/doc/reference/kernel/other/resource_mgmt.rst new file mode 100644 index 00000000000..3cf284e0be5 --- /dev/null +++ b/doc/reference/kernel/other/resource_mgmt.rst @@ -0,0 +1,83 @@ +.. _resource_mgmt: + +Resource Management +################### + +There are various situations where it's necessary to coordinate resource +use at runtime among multiple clients. These include power rails, +clocks, other peripherals, and binary device power management. The +complexity of properly managing multiple consumers of a device in a +multithreaded system, especially when transitions may be asynchronous, +suggests that a shared implementation is desirable. + +.. contents:: + :local: + :depth: 2 + + +On-Off Services +*************** + +An on-off service supports an arbitrary number of clients of a service +which has a binary state. Example applications are power rails, clocks, +and binary device power management. + +The service has the following properties: + +* The stable states are off, on, and error. The service always begins + in the off state. The service may also be in a transition to a given + state. +* The core operations are request (add a dependency) and release (remove + a dependency). The service manages the state based on calls to + functions that initiate these operations. +* The service transitions from off to on when first client request is + received. +* The service transitions from on to off when last client release is + received. +* Each service configuration provides functions that implement the + transition from off to on, from on to off, and optionally from an + error state to off. Transitions that may put a calling thread to + sleep must be flagged in the configuration to support safe invocation + from non-thread context. +* All operations are asynchronous, and are initiated by a function call + that references a specific service and is given client notification + data. The function call will succeed or fail. On success, the + operation is guaranteed to be initiated, but whether the operation + itself succeeds or fails is indicated through client notification. + The initiation functions can be invoked from pre-kernel, thread, or + ISR context. In contexts and states where the operation cannot + be started the function will result in an error. +* Requests to turn on may be queued while a transition to off is in + progress: when the service has turned off successfully it will be + immediately turned on again (where context allows) and waiting clients + notified when the start completes. + +Requests are reference counted, but not tracked. That means clients are +responsible for recording whether their requests were accepted, and for +initiating a release only if they have previously successfully completed +a request. Improper use of the API can cause an active client to be +shut out, and the service does not maintain a record of specific clients +that have been granted a request. + +Failures in executing a transition are recorded and inhibit further +requests or releases until the service is reset. Pending requests are +notified (and cancelled) when errors are discovered. + +Transition operation completion notifications are provided through any +of the following mechanisms: + +* Signal: A pointer to a :c:type:`struct k_poll_signal` is provided, and + the signal is raised when the transition completes. The operation + completion code is stored as the signal value. +* Callback: a function pointer is provided by the client along with an + opaque pointer, and on completion of the operation the function is + invoked with the pointer and the operation completion code. +* Spin-wait: the client is required to check for operation completion + using the :cpp:func:`onoff_client_fetch_result()` function. + +Synchronous transition may be implemented by a caller based on its +context, for example by using :cpp:func:`k_poll()` to wait until the +completion is signalled. + +.. doxygengroup:: resource_mgmt_apis + :project: Zephyr diff --git a/include/sys/onoff.h b/include/sys/onoff.h new file mode 100644 index 00000000000..363e62d08d4 --- /dev/null +++ b/include/sys/onoff.h @@ -0,0 +1,603 @@ +/* + * Copyright (c) 2019 Peter Bigot Consulting, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_SYS_ONOFF_H_ +#define ZEPHYR_INCLUDE_SYS_ONOFF_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @defgroup resource_mgmt_apis Resource Management APIs + * @ingroup kernel_apis + * @{ + */ + +/** + * @brief Flag fields used to specify on-off service behavior. + */ +enum onoff_service_flags { + /** + * @brief Flag passed to onoff_service_init(). + * + * When provided this indicates the start transition function + * may cause the calling thread to wait. This blocks attempts + * to initiate a transition from a non-thread context. + */ + ONOFF_SERVICE_START_SLEEPS = BIT(0), + + /** + * @brief Flag passed to onoff_service_init(). + * + * As with @ref ONOFF_SERVICE_START_SLEEPS but describing the + * stop transition function. + */ + ONOFF_SERVICE_STOP_SLEEPS = BIT(1), + + /** + * @brief Flag passed to onoff_service_init(). + * + * As with @ref ONOFF_SERVICE_START_SLEEPS but describing the + * reset transition function. + */ + ONOFF_SERVICE_RESET_SLEEPS = BIT(2), + + /* Internal use. */ + ONOFF_SERVICE_HAS_ERROR = BIT(3), + + /* This and higher bits reserved for internal use. */ + ONOFF_SERVICE_INTERNAL_BASE = BIT(4), +}; + +/* Forward declaration */ +struct onoff_service; + +/** + * @brief Signature used to notify an on-off service that a transition + * has completed. + * + * Functions of this type are passed to service-specific transition + * functions to be used to report the completion of the operation. + * The functions may be invoked from any context. + * + * @param srv the service for which transition was requested. + * + * @param res the result of the transition. This shall be + * non-negative on success, or a negative error code. If an error is + * indicated the service shall enter an error state. + */ +typedef void (*onoff_service_notify_fn)(struct onoff_service *srv, + int res); + +/** + * @brief Signature used by service implementations to effect a + * transition. + * + * Service definitions use two function pointers of this type to be + * notified that a transition is required, and a third optional one to + * reset service state. + * + * The start function will be called only from the off state. + * + * The stop function will be called only from the on state. + * + * The reset function may be called only when + * onoff_service_has_error() returns true. + * + * @param srv the service for which transition was requested. + * + * @param notify the function to be invoked when the transition has + * completed. The callee shall capture this parameter to notify on + * completion of asynchronous transitions. If the transition is not + * asynchronous, notify shall be invoked before the transition + * function returns. + */ +typedef void (*onoff_service_transition_fn)(struct onoff_service *srv, + onoff_service_notify_fn notify); + +/** + * @brief State associated with an on-off service. + * + * No fields in this structure are intended for use by service + * providers or clients. The state is to be initialized once, using + * onoff_service_init(), when the service provider is initialized. + * In case of error it may be reset through the + * onoff_service_reset() API. + */ +struct onoff_service { + /* List of clients waiting for completion of reset or + * transition to on. + */ + sys_slist_t clients; + + /* Function to invoke to transition the service to on. */ + onoff_service_transition_fn start; + + /* Function to invoke to transition the service to off. */ + onoff_service_transition_fn stop; + + /* Function to force the service state to reset, where + * supported. + */ + onoff_service_transition_fn reset; + + /* Mutex protection for flags, clients, releaser, and refs. */ + struct k_spinlock lock; + + /* Client to be informed when transition to off completes. */ + struct onoff_client *releaser; + + /* Flags identifying the service state. */ + u16_t flags; + + /* Number of active clients for the service. */ + u16_t refs; +}; + +/** @internal */ +#define ONOFF_SERVICE_INITIALIZER(_start, _stop, _reset, _flags) { \ + .start = _start, \ + .stop = _stop, \ + .reset = _reset, \ + .flags = _flags, \ +} + +/** + * @brief Initialize an on-off service to off state. + * + * This function must be invoked exactly once per service instance, by + * the infrastructure that provides the service, and before any other + * on-off service API is invoked on the service. + * + * This function should never be invoked by clients of an on-off service. + * + * @param srv the service definition object to be initialized. + * + * @param start the function used to (initiate a) transition from off + * to on. This must not be null. Include @ref ONOFF_SERVICE_START_SLEEPS as + * appropriate in flags. + * + * @param stop the function used to (initiate a) transition from on to + * off. This must not be null. Include @ref ONOFF_SERVICE_STOP_SLEEPS + * as appropriate in flags. + * + * @param reset the function used to clear errors and force the + * service to an off state. Pass null if the service cannot or need + * not be reset. (Services where a transition operation can complete + * with an error notification should support the reset operation.) + * Include @ref ONOFF_SERVICE_RESET_SLEEPS as appropriate in flags. + * + * @param flags any or all of the flags mentioned above, + * e.g. @ref ONOFF_SERVICE_START_SLEEPS. Use of other flags produces an + * error. + * + * @retval 0 on success + * @retval -EINVAL if start, stop, or flags are invalid + */ +int onoff_service_init(struct onoff_service *srv, + onoff_service_transition_fn start, + onoff_service_transition_fn stop, + onoff_service_transition_fn reset, + u32_t flags); + +/** @internal + * + * Flag fields used to specify on-off client behavior. + * + * These flags control whether calls to onoff_service_request() and + * onoff_service_release() are synchronous or asynchronous, and for + * asynchronous operations how the operation result is communicated to + * the client. + */ +enum onoff_client_flags { + /* Known-invalid field, used in validation */ + ONOFF_CLIENT_NOTIFY_INVALID = 0, + + /* + * Indicates that no notification will be provided. + * + * Callers must check for completions using + * onoff_client_fetch_result(). + * + * See onoff_client_init_spinwait(). + */ + ONOFF_CLIENT_NOTIFY_SPINWAIT = 1, + + /* + * Select notification through @ref k_poll signal + * + * See onoff_client_init_signal(). + */ + ONOFF_CLIENT_NOTIFY_SIGNAL = 2, + + /** + * Select notification through a user-provided callback. + * + * See onoff_client_init_callback(). + */ + ONOFF_CLIENT_NOTIFY_CALLBACK = 3, +}; + +/* Forward declaration */ +struct onoff_client; + +/** + * @brief Signature used to notify an on-off service client of the + * completion of an operation. + * + * These functions may be invoked from any context including + * pre-kernel, ISR, or cooperative or pre-emptible threads. + * Compatible functions must be isr-callable and non-suspendable. + * + * @param srv the service for which the operation was initiated. + * + * @param cli the client structure passed to the function that + * initiated the operation. + * + * @param user_data user data provided when the client structure was + * initialized with onoff_client_init_callback(). + * + * @param res the result of the operation. Expected values are + * service-specific, but the value shall be non-negative if the + * operation succeeded, and negative if the operation failed. + */ +typedef void (*onoff_client_callback)(struct onoff_service *srv, + struct onoff_client *cli, + void *user_data, + int res); + +/** + * @brief State associated with a client of an on-off service. + * + * Objects of this type are allocated by a client, which must use an + * initialization function (e.g. onoff_client_init_signal()) to + * configure them. + * + * Control of the object content transfers to the service provider + * when a pointer to the object is passed to any on-off service + * function. While the service provider controls the object the + * client must not change any object fields. Control reverts to the + * client: + * * if the call to the service API returns an error; + * * if the call to the service API succeeds for a no-wait operation; + * * when operation completion is posted (signalled or callback + * invoked). + * + * Only the result field is intended for direct use by clients, and it + * is available for inspection only after control reverts to the + * client. + */ +struct onoff_client { + /* Links the client into the set of waiting service users. */ + sys_snode_t node; + + union async { + /* Pointer to signal used to notify client. + * + * The signal value corresponds to the res parameter + * of onoff_client_callback. + */ + struct k_poll_signal *signal; + + /* Handler and argument for callback notification. */ + struct callback { + onoff_client_callback handler; + void *user_data; + } callback; + } async; + + /* + * The result of the operation. + * + * This is the value that was (or would be) passed to the + * async infrastructure. This field is the sole record of + * success or failure for no-wait synchronous operations. + */ + int volatile result; + + /* Flags recording client state. */ + u32_t volatile flags; +}; + +/** + * @brief Check for and read the result of an asynchronous operation. + * + * @param op pointer to the object used to specify asynchronous + * function behavior and store completion information. + * + * @param result pointer to storage for the result of the operation. + * The result is stored only if the operation has completed. + * + * @retval 0 if the operation has completed. + * @retval -EAGAIN if the operation has not completed. + */ +static inline int onoff_client_fetch_result(const struct onoff_client *op, + int *result) +{ + __ASSERT_NO_MSG(op != NULL); + __ASSERT_NO_MSG(result != NULL); + + int rv = -EAGAIN; + + if (op->flags == 0U) { + rv = 0; + *result = op->result; + } + return rv; +} + +/** + * @brief Initialize an on-off client to be used for a spin-wait + * operation notification. + * + * Clients that use this initialization receive no asynchronous + * notification, and instead must periodically check for completion + * using onoff_client_fetch_result(). + * + * @param cli pointer to the client state object. + */ +static inline void onoff_client_init_spinwait(struct onoff_client *cli) +{ + __ASSERT_NO_MSG(cli != NULL); + + *cli = (struct onoff_client){ + .flags = ONOFF_CLIENT_NOTIFY_SPINWAIT, + }; +} + +/** + * @brief Initialize an on-off client to be used for a signal + * operation notification. + * + * Clients that use this initialization will be notified of the + * completion of operations submitted through onoff_request() and + * onoff_release() through the provided signal. + * + * @note + * @rst + * This capability is available only when :option:`CONFIG_POLL` is + * selected. + * @endrst + * + * @param cli pointer to the client state object. + * + * @param sigp pointer to the signal to use for notification. The + * value must not be null. The signal must be reset before the client + * object is passed to the on-off service API. + */ +static inline void onoff_client_init_signal(struct onoff_client *cli, + struct k_poll_signal *sigp) +{ + __ASSERT_NO_MSG(cli != NULL); + __ASSERT_NO_MSG(sigp != NULL); + + *cli = (struct onoff_client){ +#ifdef CONFIG_POLL + .async = { + .signal = sigp, + }, +#endif /* CONFIG_POLL */ + .flags = ONOFF_CLIENT_NOTIFY_SIGNAL, + }; +} + +/** + * @brief Initialize an on-off client to be used for a callback + * operation notification. + * + * Clients that use this initialization will be notified of the + * completion of operations submitted through on-off service API + * through the provided callback. Note that callbacks may be invoked + * from various contexts depending on the specific service; see + * @ref onoff_client_callback. + * + * @param cli pointer to the client state object. + * + * @param handler a function pointer to use for notification. + * + * @param user_data an opaque pointer passed to the handler to provide + * additional context. + */ +static inline void onoff_client_init_callback(struct onoff_client *cli, + onoff_client_callback handler, + void *user_data) +{ + __ASSERT_NO_MSG(cli != NULL); + __ASSERT_NO_MSG(handler != NULL); + + *cli = (struct onoff_client){ + .async = { + .callback = { + .handler = handler, + .user_data = user_data, + }, + }, + .flags = ONOFF_CLIENT_NOTIFY_CALLBACK, + }; +} + +/** + * @brief Request a reservation to use an on-off service. + * + * The return value indicates the success or failure of an attempt to + * initiate an operation to request the resource be made available. + * If initiation of the operation succeeds the result of the request + * operation is provided through the configured client notification + * method, possibly before this call returns. + * + * Note that the call to this function may succeed in a case where the + * actual request fails. Always check the operation completion + * result. + * + * As a specific example: A call to this function may succeed at a + * point while the service is still transitioning to off due to a + * previous call to onoff_release(). When the transition completes + * the service would normally start a transition to on. However, if + * the transition to off completed in a non-thread context, and the + * transition to on can sleep, the transition cannot be started and + * the request will fail with `-EWOULDBLOCK`. + * + * @param srv the service that will be used. + * + * @param cli a non-null pointer to client state providing + * instructions on synchronous expectations and how to notify the + * client when the request completes. Behavior is undefined if client + * passes a pointer object associated with an incomplete service + * operation. + * + * @retval Non-negative on successful (initiation of) request + * @retval -EIO if service has recorded an an error + * @retval -EINVAL if the parameters are invalid + * @retval -EAGAIN if the reference count would overflow + * @retval -EWOULDBLOCK if the function was invoked from non-thread + * context and successful initiation could result in an attempt to + * make the calling thread sleep. + */ +int onoff_request(struct onoff_service *srv, + struct onoff_client *cli); + +/** + * @brief Release a reserved use of an on-off service. + * + * The return value indicates the success or failure of an attempt to + * initiate an operation to release the resource. If initiation of + * the operation succeeds the result of the release operation itself + * is provided through the configured client notification method, + * possibly before this call returns. + * + * Note that the call to this function may succeed in a case where the + * actual release fails. Always check the operation completion + * result. + * + * @param srv the service that will be used. + * + * @param cli a non-null pointer to client state providing + * instructions on how to notify the client when release completes. + * Behavior is undefined if cli references an object associated with + * an incomplete service operation. + * + * @retval Non-negative on successful (initiation of) release + * @retval -EINVAL if the parameters are invalid + * @retval -EIO if service has recorded an an error + * @retval -EWOULDBLOCK if a non-blocking request was made and + * could not be satisfied without potentially blocking. + * @retval -EALREADY if the service is already off or transitioning + * to off + * @retval -EBUSY if the service is transitioning to on + */ +int onoff_release(struct onoff_service *srv, + struct onoff_client *cli); + +/** + * @brief Test whether an on-off service has recorded an error. + * + * This function can be used to determine whether the service has + * recorded an error. Errors may be cleared by invoking + * onoff_service_reset(). + * + * @return true if and only if the service has an uncleared error. + */ +static inline bool onoff_service_has_error(const struct onoff_service *srv) +{ + return (srv->flags & ONOFF_SERVICE_HAS_ERROR) != 0; +} + +/** + * @brief Clear errors on an on-off service and reset it to its off + * state. + * + * A service can only be reset when it is in an error state as + * indicated by onoff_service_has_error(). + * + * The return value indicates the success or failure of an attempt to + * initiate an operation to reset the resource. If initiation of the + * operation succeeds the result of the reset operation itself is + * provided through the configured client notification method, + * possibly before this call returns. Multiple clients may request a + * reset; all are notified when it is complete. + * + * Note that the call to this function may succeed in a case where the + * actual reset fails. Always check the operation completion result. + * + * This function is blocking if the reset transition is blocking, + * unless client notification specifies no-wait. + * + * @note Due to the conditions on state transition all incomplete + * asynchronous operations will have been informed of the error when + * it occurred. There need be no concern about dangling requests left + * after a reset completes. + * + * @param srv the service to be reset. + * + * @param cli pointer to client state, including instructions on how + * to notify the client when reset completes. Behavior is undefined + * if cli references an object associated with an incomplete service + * operation. + * + * @retval 0 on success + * @retval -ENOTSUP if reset is not supported + * @retval -EINVAL if the parameters are invalid, or if the service + * @retval -EALREADY if the service does not have a recorded error + */ +int onoff_service_reset(struct onoff_service *srv, + struct onoff_client *cli); + +/** + * @brief Attempt to cancel an in-progress client operation. + * + * It may be that a client has initiated an operation but needs to + * shut down before the operation has completed. For example, when a + * request was made and the need is no longer present. + * + * There is limited support for cancelling an in-progress operation: + * * If a start or reset is in progress, all but one clients + * requesting the start can cancel their request. + * * If a stop is in progress, all clients requesting a restart can + * cancel their request; + * * A client requesting a release cannot cancel the release. + * + * Be aware that any transition that was initiated on behalf of the + * client will continue to progress to completion. The restricted + * support for cancellation ensures that for any in-progress + * transition there will always be at least one client that will be + * notified when the operation completes. + * + * If the cancellation fails the service retains control of the client + * object, and the client must wait for operation completion. + * + * @param srv the service for which an operation is to be cancelled. + * + * @param cli a pointer to the same client state that was provided + * when the operation to be cancelled was issued. If the cancellation + * is successful the client will be notified of operation completion + * with a result of `-ECANCELED`. + * + * @retval 0 if the cancellation was completed before the client could + * be notified. The client will be notified through cli with an + * operation completion of `-ECANCELED`. + * @retval -EINVAL if the parameters are invalid. + * @retval -EWOULDBLOCK if cancellation was rejected because the + * client is the only waiter for an in-progress transition. The + * service retains control of the client structure. + * @retval -EALREADY if cli was not a record of an uncompleted + * notification at the time the cancellation was processed. This + * likely indicates that the operation and client notification had + * already completed. + */ +int onoff_cancel(struct onoff_service *srv, + struct onoff_client *cli); + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_SYS_ONOFF_H_ */ diff --git a/lib/os/CMakeLists.txt b/lib/os/CMakeLists.txt index dcf78354675..7918f78dc6d 100644 --- a/lib/os/CMakeLists.txt +++ b/lib/os/CMakeLists.txt @@ -12,6 +12,7 @@ zephyr_sources( hex.c mempool.c printk.c + onoff.c rb.c sem.c thread_entry.c diff --git a/lib/os/onoff.c b/lib/os/onoff.c new file mode 100644 index 00000000000..2a999b6ef8b --- /dev/null +++ b/lib/os/onoff.c @@ -0,0 +1,518 @@ +/* + * Copyright (c) 2019 Peter Bigot Consulting, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#define CLIENT_NOTIFY_METHOD_MASK 0x03 +#define CLIENT_VALID_FLAGS_MASK 0x07 + +#define SERVICE_CONFIG_FLAGS \ + (ONOFF_SERVICE_START_SLEEPS \ + | ONOFF_SERVICE_STOP_SLEEPS \ + | ONOFF_SERVICE_RESET_SLEEPS) + +#define SERVICE_REFS_MAX UINT16_MAX + +#define SERVICE_STATE_OFF 0 +#define SERVICE_STATE_ON ONOFF_SERVICE_INTERNAL_BASE +#define SERVICE_STATE_TRANSITION (ONOFF_SERVICE_INTERNAL_BASE << 1) +#define SERVICE_STATE_TO_ON (SERVICE_STATE_TRANSITION | SERVICE_STATE_ON) +#define SERVICE_STATE_TO_OFF (SERVICE_STATE_TRANSITION | SERVICE_STATE_OFF) + +#define SERVICE_STATE_MASK (SERVICE_STATE_ON | SERVICE_STATE_TRANSITION) + +static void set_service_state(struct onoff_service *srv, + u32_t state) +{ + srv->flags &= ~SERVICE_STATE_MASK; + srv->flags |= (state & SERVICE_STATE_MASK); +} + +static int validate_args(const struct onoff_service *srv, + struct onoff_client *cli) +{ + if ((srv == NULL) || (cli == NULL)) { + return -EINVAL; + } + + int rv = 0; + u32_t mode = cli->flags; + + /* Reject unexpected flags. */ + if (mode != (cli->flags & CLIENT_VALID_FLAGS_MASK)) { + return -EINVAL; + } + + /* Validate configuration based on mode */ + switch (mode & CLIENT_NOTIFY_METHOD_MASK) { + case ONOFF_CLIENT_NOTIFY_SPINWAIT: + break; + case ONOFF_CLIENT_NOTIFY_CALLBACK: + if (cli->async.callback.handler == NULL) { + rv = -EINVAL; + } + break; + case ONOFF_CLIENT_NOTIFY_SIGNAL: + if (cli->async.signal == NULL) { + rv = -EINVAL; + } + break; + default: + rv = -EINVAL; + break; + } + + /* Clear the result here instead of in all callers. */ + if (rv == 0) { + cli->result = 0; + } + + return rv; +} + +int onoff_service_init(struct onoff_service *srv, + onoff_service_transition_fn start, + onoff_service_transition_fn stop, + onoff_service_transition_fn reset, + u32_t flags) +{ + if ((flags & SERVICE_CONFIG_FLAGS) != flags) { + return -EINVAL; + } + + if ((start == NULL) || (stop == NULL)) { + return -EINVAL; + } + + *srv = (struct onoff_service)ONOFF_SERVICE_INITIALIZER(start, stop, + reset, flags); + + return 0; +} + +static void notify_one(struct onoff_service *srv, + struct onoff_client *cli, + int res) +{ + unsigned int flags = cli->flags; + + /* Store the result, and notify if requested. */ + cli->result = res; + cli->flags = 0; + switch (flags & CLIENT_NOTIFY_METHOD_MASK) { + case ONOFF_CLIENT_NOTIFY_SPINWAIT: + break; + case ONOFF_CLIENT_NOTIFY_CALLBACK: + cli->async.callback.handler(srv, cli, + cli->async.callback.user_data, res); + break; +#ifdef CONFIG_POLL + case ONOFF_CLIENT_NOTIFY_SIGNAL: + k_poll_signal_raise(cli->async.signal, res); + break; +#endif /* CONFIG_POLL */ + default: + __ASSERT_NO_MSG(false); + } +} + +static void notify_all(struct onoff_service *srv, + sys_slist_t *list, + 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(srv, cli, res); + } +} + +static void onoff_start_notify(struct onoff_service *srv, + int res) +{ + k_spinlock_key_t key = k_spin_lock(&srv->lock); + sys_slist_t clients = srv->clients; + + /* Can't have a queued releaser during start */ + __ASSERT_NO_MSG(srv->releaser == NULL); + + /* If the start failed log an error and leave the rest of the + * state in place for diagnostics. + * + * If the start succeeded record a reference for all clients + * and set the state to ON. There must be at least one client + * left to receive the result. + * + * In either case reset the client queue and notify all + * clients of operation completion. + */ + if (res < 0) { + srv->flags &= ~SERVICE_STATE_TRANSITION; + srv->flags |= ONOFF_SERVICE_HAS_ERROR; + } else { + sys_snode_t *node; + unsigned int refs = 0U; + + set_service_state(srv, SERVICE_STATE_ON); + + SYS_SLIST_FOR_EACH_NODE(&clients, node) { + refs += 1U; + } + + /* Update the reference count, or fail if the count + * would overflow. + */ + if (srv->refs > (SERVICE_REFS_MAX - refs)) { + srv->flags |= ONOFF_SERVICE_HAS_ERROR; + } else { + srv->refs += refs; + } + __ASSERT_NO_MSG(srv->refs > 0U); + } + + sys_slist_init(&srv->clients); + + k_spin_unlock(&srv->lock, key); + + notify_all(srv, &clients, res); +} + +int onoff_request(struct onoff_service *srv, + struct onoff_client *cli) +{ + bool add_client = false; /* add client to pending list */ + bool start = false; /* invoke start transition */ + bool notify = false; /* do client notification */ + int rv = validate_args(srv, cli); + + if (rv < 0) { + return rv; + } + + k_spinlock_key_t key = k_spin_lock(&srv->lock); + + if ((srv->flags & ONOFF_SERVICE_HAS_ERROR) != 0) { + rv = -EIO; + goto out; + } + + /* Reject if this would overflow the reference count. */ + if (srv->refs == SERVICE_REFS_MAX) { + rv = -EAGAIN; + goto out; + } + + u32_t state = srv->flags & SERVICE_STATE_MASK; + + switch (state) { + case SERVICE_STATE_TO_OFF: + /* Queue to start after release */ + __ASSERT_NO_MSG(srv->releaser != NULL); + add_client = true; + rv = 3; + break; + case SERVICE_STATE_OFF: + /* Reject if in a non-thread context and start could + * wait. + */ + if ((k_is_in_isr() || k_is_pre_kernel()) + && ((srv->flags & ONOFF_SERVICE_START_SLEEPS) != 0U)) { + rv = -EWOULDBLOCK; + break; + } + + /* Start with first request while off */ + __ASSERT_NO_MSG(srv->refs == 0); + set_service_state(srv, SERVICE_STATE_TO_ON); + start = true; + add_client = true; + rv = 2; + break; + case SERVICE_STATE_TO_ON: + /* Already starting, just queue it */ + add_client = true; + rv = 1; + break; + case SERVICE_STATE_ON: + /* Just increment the reference count */ + notify = true; + break; + default: + rv = -EINVAL; + break; + } + +out: + if (add_client) { + sys_slist_append(&srv->clients, &cli->node); + } else if (notify) { + srv->refs += 1; + } + + k_spin_unlock(&srv->lock, key); + + if (start) { + __ASSERT_NO_MSG(srv->start != NULL); + srv->start(srv, onoff_start_notify); + } else if (notify) { + notify_one(srv, cli, 0); + } + + return rv; +} + +static void onoff_stop_notify(struct onoff_service *srv, + int res) +{ + bool notify_clients = false; + int client_res = res; + bool start = false; + k_spinlock_key_t key = k_spin_lock(&srv->lock); + sys_slist_t clients = srv->clients; + struct onoff_client *releaser = srv->releaser; + + /* If the stop operation failed log an error and leave the + * rest of the state in place. + * + * If it succeeded remove the last reference and transition to + * off. + * + * In either case remove the last reference, and notify all + * waiting clients of operation completion. + */ + if (res < 0) { + srv->flags &= ~SERVICE_STATE_TRANSITION; + srv->flags |= ONOFF_SERVICE_HAS_ERROR; + notify_clients = true; + } else if (sys_slist_is_empty(&clients)) { + set_service_state(srv, SERVICE_STATE_OFF); + } else if ((k_is_in_isr() || k_is_pre_kernel()) + && ((srv->flags & ONOFF_SERVICE_START_SLEEPS) != 0U)) { + set_service_state(srv, SERVICE_STATE_OFF); + notify_clients = true; + client_res = -EWOULDBLOCK; + } else { + set_service_state(srv, SERVICE_STATE_TO_ON); + start = true; + } + + __ASSERT_NO_MSG(releaser); + srv->refs -= 1U; + srv->releaser = NULL; + __ASSERT_NO_MSG(srv->refs == 0); + + /* Remove the clients if there was an error or a delayed start + * couldn't be initiated, because we're resolving their + * operation with an error. + */ + if (notify_clients) { + sys_slist_init(&srv->clients); + } + + k_spin_unlock(&srv->lock, key); + + /* Notify the releaser. If there was an error, notify any + * pending requests; otherwise if there are pending requests + * start the transition to ON. + */ + notify_one(srv, releaser, res); + if (notify_clients) { + notify_all(srv, &clients, client_res); + } else if (start) { + srv->start(srv, onoff_start_notify); + } +} + +int onoff_release(struct onoff_service *srv, + struct onoff_client *cli) +{ + bool stop = false; /* invoke stop transition */ + bool notify = false; /* do client notification */ + int rv = validate_args(srv, cli); + + if (rv < 0) { + return rv; + } + + k_spinlock_key_t key = k_spin_lock(&srv->lock); + + if ((srv->flags & ONOFF_SERVICE_HAS_ERROR) != 0) { + rv = -EIO; + goto out; + } + + u32_t state = srv->flags & SERVICE_STATE_MASK; + + switch (state) { + case SERVICE_STATE_ON: + /* Stay on if release leaves a client. */ + if (srv->refs > 1U) { + notify = true; + rv = 1; + break; + } + + /* Reject if in non-thread context but stop could + * wait + */ + if ((k_is_in_isr() || k_is_pre_kernel()) + && ((srv->flags & ONOFF_SERVICE_STOP_SLEEPS) != 0)) { + rv = -EWOULDBLOCK; + break; + } + + stop = true; + + set_service_state(srv, SERVICE_STATE_TO_OFF); + srv->releaser = cli; + rv = 2; + + break; + case SERVICE_STATE_TO_ON: + rv = -EBUSY; + break; + case SERVICE_STATE_OFF: + case SERVICE_STATE_TO_OFF: + rv = -EALREADY; + break; + default: + rv = -EINVAL; + } + +out: + if (notify) { + srv->refs -= 1U; + } + + k_spin_unlock(&srv->lock, key); + + if (stop) { + __ASSERT_NO_MSG(srv->stop != NULL); + srv->stop(srv, onoff_stop_notify); + } else if (notify) { + notify_one(srv, cli, 0); + } + + return rv; +} + +static void onoff_reset_notify(struct onoff_service *srv, + int res) +{ + k_spinlock_key_t key = k_spin_lock(&srv->lock); + sys_slist_t clients = srv->clients; + + /* If the reset failed clear the transition flag but otherwise + * leave the state unchanged. + * + * If it was successful clear the reference count and all + * flags except capability flags (sets to SERVICE_STATE_OFF). + */ + if (res < 0) { + srv->flags &= ~SERVICE_STATE_TRANSITION; + } else { + __ASSERT_NO_MSG(srv->refs == 0U); + srv->refs = 0U; + srv->flags &= SERVICE_CONFIG_FLAGS; + } + + sys_slist_init(&srv->clients); + + k_spin_unlock(&srv->lock, key); + + notify_all(srv, &clients, res); +} + +int onoff_service_reset(struct onoff_service *srv, + struct onoff_client *cli) +{ + if (srv->reset == NULL) { + return -ENOTSUP; + } + + bool reset = false; + int rv = validate_args(srv, cli); + + if (rv < 0) { + return rv; + } + + /* Reject if in a non-thread context and reset could wait. */ + if ((k_is_in_isr() || k_is_pre_kernel()) + && ((srv->flags & ONOFF_SERVICE_RESET_SLEEPS) != 0U)) { + return -EWOULDBLOCK; + } + + k_spinlock_key_t key = k_spin_lock(&srv->lock); + + if ((srv->flags & ONOFF_SERVICE_HAS_ERROR) == 0) { + rv = -EALREADY; + goto out; + } + + if ((srv->flags & SERVICE_STATE_TRANSITION) == 0) { + reset = true; + srv->flags |= SERVICE_STATE_TRANSITION; + } + +out: + if (rv >= 0) { + sys_slist_append(&srv->clients, &cli->node); + } + + k_spin_unlock(&srv->lock, key); + + if (reset) { + srv->reset(srv, onoff_reset_notify); + } + + return rv; +} + +int onoff_cancel(struct onoff_service *srv, + struct onoff_client *cli) +{ + int rv = validate_args(srv, cli); + + if (rv < 0) { + return rv; + } + + rv = -EALREADY; + k_spinlock_key_t key = k_spin_lock(&srv->lock); + u32_t state = srv->flags & SERVICE_STATE_MASK; + + /* Can't remove the last client waiting for the in-progress + * transition, as there would be nobody to receive the + * completion notification, which might indicate a service + * error. + */ + if (sys_slist_find_and_remove(&srv->clients, &cli->node)) { + rv = 0; + if (sys_slist_is_empty(&srv->clients) + && (state != SERVICE_STATE_TO_OFF)) { + rv = -EWOULDBLOCK; + sys_slist_append(&srv->clients, &cli->node); + } + } else if (srv->releaser == cli) { + /* must be waiting for TO_OFF to complete */ + rv = -EWOULDBLOCK; + } + + k_spin_unlock(&srv->lock, key); + + if (rv == 0) { + notify_one(srv, cli, -ECANCELED); + } + + return rv; +} diff --git a/tests/lib/onoff/CMakeLists.txt b/tests/lib/onoff/CMakeLists.txt new file mode 100644 index 00000000000..0987b751825 --- /dev/null +++ b/tests/lib/onoff/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(onoff) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/lib/onoff/README b/tests/lib/onoff/README new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/lib/onoff/prj.conf b/tests/lib/onoff/prj.conf new file mode 100644 index 00000000000..1948aaa649a --- /dev/null +++ b/tests/lib/onoff/prj.conf @@ -0,0 +1,2 @@ +CONFIG_POLL=y +CONFIG_ZTEST=y diff --git a/tests/lib/onoff/src/main.c b/tests/lib/onoff/src/main.c new file mode 100644 index 00000000000..d7bb902425e --- /dev/null +++ b/tests/lib/onoff/src/main.c @@ -0,0 +1,1164 @@ +/* + * Copyright (c) 2019 Peter Bigot Consulting, LLC + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +static struct onoff_client spinwait_cli; + +static int callback_res; +static void *callback_ud; +static void callback(struct onoff_service *srv, + struct onoff_client *cli, + void *ud, + int res) +{ + callback_ud = ud; + callback_res = res; +} + +static inline void init_notify_sig(struct onoff_client *cli, + struct k_poll_signal *sig) +{ + k_poll_signal_init(sig); + onoff_client_init_signal(cli, sig); +} + +static inline void init_notify_cb(struct onoff_client *cli) +{ + onoff_client_init_callback(cli, callback, NULL); +} + +static inline void init_spinwait(struct onoff_client *cli) +{ + onoff_client_init_spinwait(cli); +} + +static inline int cli_result(const struct onoff_client *cli) +{ + int result; + int rc = onoff_client_fetch_result(cli, &result); + + if (rc == 0) { + rc = result; + } + return rc; +} + +struct transit_state { + const char *tag; + bool async; + int retval; + onoff_service_notify_fn notify; + struct onoff_service *srv; +}; + +static void reset_transit_state(struct transit_state *tsp) +{ + tsp->async = false; + tsp->retval = 0; + tsp->notify = NULL; + tsp->srv = NULL; +} + +static void run_transit(struct onoff_service *srv, + onoff_service_notify_fn notify, + struct transit_state *tsp) +{ + if (tsp->async) { + TC_PRINT("%s async\n", tsp->tag); + tsp->notify = notify; + tsp->srv = srv; + } else { + TC_PRINT("%s notify %d\n", tsp->tag, tsp->retval); + notify(srv, tsp->retval); + } +} + +static void notify(struct transit_state *tsp) +{ + TC_PRINT("%s settle %d\n", tsp->tag, tsp->retval); + tsp->notify(tsp->srv, tsp->retval); + tsp->notify = NULL; + tsp->srv = NULL; +} + +static struct k_sem isr_sync; +static struct k_timer isr_timer; + +static void isr_notify(struct k_timer *timer) +{ + struct transit_state *tsp = k_timer_user_data_get(timer); + + TC_PRINT("ISR NOTIFY %s %d\n", tsp->tag, k_is_in_isr()); + notify(tsp); + k_sem_give(&isr_sync); +} + +struct isr_call_state { + struct onoff_service *srv; + struct onoff_client *cli; + int result; +}; + +static void isr_request(struct k_timer *timer) +{ + struct isr_call_state *rsp = k_timer_user_data_get(timer); + + rsp->result = onoff_request(rsp->srv, rsp->cli); + k_sem_give(&isr_sync); +} + +static void isr_release(struct k_timer *timer) +{ + struct isr_call_state *rsp = k_timer_user_data_get(timer); + + rsp->result = onoff_release(rsp->srv, rsp->cli); + k_sem_give(&isr_sync); +} + +static void isr_reset(struct k_timer *timer) +{ + struct isr_call_state *rsp = k_timer_user_data_get(timer); + + rsp->result = onoff_service_reset(rsp->srv, rsp->cli); + k_sem_give(&isr_sync); +} + +static struct transit_state start_state = { + .tag = "start", +}; +static void start(struct onoff_service *srv, + onoff_service_notify_fn notify) +{ + run_transit(srv, notify, &start_state); +} + +static struct transit_state stop_state = { + .tag = "stop", +}; +static void stop(struct onoff_service *srv, + onoff_service_notify_fn notify) +{ + run_transit(srv, notify, &stop_state); +} + +static struct transit_state reset_state = { + .tag = "reset", +}; +static void reset(struct onoff_service *srv, + onoff_service_notify_fn notify) +{ + run_transit(srv, notify, &reset_state); +} + +static void clear_transit(void) +{ + callback_res = 0; + reset_transit_state(&start_state); + reset_transit_state(&stop_state); + reset_transit_state(&reset_state); +} + +static void test_service_init_validation(void) +{ + int rc; + struct onoff_service srv; + + clear_transit(); + + rc = onoff_service_init(NULL, NULL, NULL, NULL, 0); + zassert_equal(rc, -EINVAL, + "init null srv %d", rc); + + rc = onoff_service_init(&srv, NULL, NULL, NULL, 0); + zassert_equal(rc, -EINVAL, + "init null transit %d", rc); + + rc = onoff_service_init(&srv, start, NULL, NULL, 0); + zassert_equal(rc, -EINVAL, + "init null stop %d", rc); + + rc = onoff_service_init(&srv, NULL, stop, NULL, 0); + zassert_equal(rc, -EINVAL, + "init null start %d", rc); + + rc = onoff_service_init(&srv, start, stop, NULL, + ONOFF_SERVICE_INTERNAL_BASE); + zassert_equal(rc, -EINVAL, + "init bad flags %d", rc); + + u32_t flags = ONOFF_SERVICE_START_SLEEPS; + + memset(&srv, 0xA5, sizeof(srv)); + zassert_false(sys_slist_is_empty(&srv.clients), + "slist empty"); + + rc = onoff_service_init(&srv, start, stop, reset, flags); + zassert_equal(rc, 0, + "init good %d", rc); + zassert_equal(srv.start, start, + "init start mismatch"); + zassert_equal(srv.stop, stop, + "init stop mismatch"); + zassert_equal(srv.reset, reset, + "init reset mismatch"); + zassert_equal(srv.flags, ONOFF_SERVICE_START_SLEEPS, + "init flags mismatch"); + zassert_equal(srv.refs, 0, + "init refs mismatch"); + zassert_true(sys_slist_is_empty(&srv.clients), + "init slist empty"); +} + +static void test_client_init_validation(void) +{ + struct onoff_client cli; + + clear_transit(); + + memset(&cli, 0xA5, sizeof(cli)); + onoff_client_init_spinwait(&cli); + zassert_equal(z_snode_next_peek(&cli.node), NULL, + "cli node mismatch"); + zassert_equal(cli.flags, ONOFF_CLIENT_NOTIFY_SPINWAIT, + "cli spinwait flags"); + + struct k_poll_signal sig; + + memset(&cli, 0xA5, sizeof(cli)); + onoff_client_init_signal(&cli, &sig); + zassert_equal(z_snode_next_peek(&cli.node), NULL, + "cli signal node"); + zassert_equal(cli.flags, ONOFF_CLIENT_NOTIFY_SIGNAL, + "cli signal flags"); + zassert_equal(cli.async.signal, &sig, + "cli signal async"); + + memset(&cli, 0xA5, sizeof(cli)); + onoff_client_init_callback(&cli, callback, &sig); + zassert_equal(z_snode_next_peek(&cli.node), NULL, + "cli callback node"); + zassert_equal(cli.flags, ONOFF_CLIENT_NOTIFY_CALLBACK, + "cli callback flags"); + zassert_equal(cli.async.callback.handler, callback, + "cli callback handler"); + zassert_equal(cli.async.callback.user_data, &sig, + "cli callback user_data"); +} + +static void test_validate_args(void) +{ + int rc; + struct onoff_service srv; + struct k_poll_signal sig; + struct onoff_client cli; + + clear_transit(); + + /* The internal validate_args is invoked from request, + * release, and reset; test it through the request API. + */ + + rc = onoff_service_init(&srv, start, stop, NULL, 0); + zassert_equal(rc, 0, + "service init"); + + rc = onoff_request(NULL, NULL); + zassert_equal(rc, -EINVAL, + "validate req null srv"); + + rc = onoff_release(NULL, NULL); + zassert_equal(rc, -EINVAL, + "validate rel null srv"); + + rc = onoff_release(&srv, NULL); + zassert_equal(rc, -EINVAL, + "validate rel null cli"); + + rc = onoff_request(&srv, NULL); + zassert_equal(rc, -EINVAL, + "validate req null cli"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_true(rc > 0, + "trigger to on"); + + memset(&cli, 0xA3, sizeof(cli)); + rc = onoff_request(&srv, &cli); + zassert_equal(rc, -EINVAL, + "validate req cli flags"); + + init_spinwait(&cli); + cli.flags = ONOFF_CLIENT_NOTIFY_INVALID; + rc = onoff_request(&srv, &cli); + zassert_equal(rc, -EINVAL, + "validate req cli mode"); + + init_notify_sig(&cli, &sig); + rc = onoff_request(&srv, &cli); + zassert_equal(rc, 0, + "validate req cli signal: %d", rc); + init_notify_sig(&cli, &sig); + cli.async.signal = NULL; + rc = onoff_request(&srv, &cli); + zassert_equal(rc, -EINVAL, + "validate req cli signal null"); + + init_notify_cb(&cli); + rc = onoff_request(&srv, &cli); + zassert_equal(rc, 0, + "validate req cli callback"); + + init_notify_cb(&cli); + cli.async.callback.handler = NULL; + rc = onoff_request(&srv, &cli); + zassert_equal(rc, -EINVAL, + "validate req cli callback null"); + + memset(&cli, 0x3C, sizeof(cli)); /* makes flags invalid */ + rc = onoff_request(&srv, &cli); + zassert_equal(rc, -EINVAL, + "validate req cli notify mode"); +} + +static void test_reset(void) +{ + int rc; + struct onoff_service srv; + struct k_poll_signal sig; + struct onoff_client cli; + unsigned int signalled = 0; + int result = 0; + + clear_transit(); + + rc = onoff_service_init(&srv, start, stop, NULL, 0); + zassert_equal(rc, 0, + "service init"); + rc = onoff_service_reset(&srv, &cli); + zassert_equal(rc, -ENOTSUP, + "reset: %d", rc); + + rc = onoff_service_init(&srv, start, stop, reset, 0); + zassert_equal(rc, 0, + "service init"); + + rc = onoff_service_reset(&srv, NULL); + zassert_equal(rc, -EINVAL, + "rst no cli"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_true(rc > 0, + "req ok"); + zassert_equal(srv.refs, 1U, + "reset req refs: %u", srv.refs); + + + zassert_false(onoff_service_has_error(&srv), + "has error"); + reset_state.retval = 57; + init_notify_sig(&cli, &sig); + rc = onoff_service_reset(&srv, &cli); + zassert_equal(rc, -EALREADY, + "reset: %d", rc); + + stop_state.retval = -23; + init_notify_sig(&cli, &sig); + rc = onoff_release(&srv, &cli); + zassert_equal(rc, 2, + "rel trigger: %d", rc); + zassert_equal(srv.refs, 0U, + "reset req refs: %u", srv.refs); + zassert_true(onoff_service_has_error(&srv), + "has error"); + zassert_equal(cli_result(&cli), stop_state.retval, + "cli result"); + signalled = 0; + result = -1; + k_poll_signal_check(&sig, &signalled, &result); + zassert_true(signalled != 0, + "signalled"); + zassert_equal(result, stop_state.retval, + "result"); + k_poll_signal_reset(&sig); + + reset_state.retval = -59; + init_notify_sig(&cli, &sig); + rc = onoff_service_reset(&srv, &cli); + zassert_equal(rc, 0U, + "reset: %d", rc); + zassert_equal(cli_result(&cli), reset_state.retval, + "reset result"); + zassert_equal(srv.refs, 0U, + "reset req refs: %u", srv.refs); + zassert_true(onoff_service_has_error(&srv), + "has error"); + + reset_state.retval = 62; + init_notify_sig(&cli, &sig); + rc = onoff_service_reset(&srv, &cli); + zassert_equal(rc, 0U, + "reset: %d", rc); + zassert_equal(cli_result(&cli), reset_state.retval, + "reset result"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + signalled = 0; + result = -1; + k_poll_signal_check(&sig, &signalled, &result); + zassert_true(signalled != 0, + "signalled"); + zassert_equal(result, reset_state.retval, + "result"); + + zassert_equal(srv.refs, 0U, + "reset req refs: %u", srv.refs); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + rc = onoff_service_init(&srv, start, stop, reset, + ONOFF_SERVICE_RESET_SLEEPS); + zassert_equal(rc, 0, + "service init"); + start_state.retval = -23; + zassert_false(onoff_service_has_error(&srv), + "has error"); + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_true(onoff_service_has_error(&srv), + "has error"); + + struct isr_call_state isr_state = { + .srv = &srv, + .cli = &spinwait_cli, + }; + struct k_timer timer; + + init_spinwait(&spinwait_cli); + k_timer_init(&timer, isr_reset, NULL); + k_timer_user_data_set(&timer, &isr_state); + + k_timer_start(&timer, K_MSEC(1), K_NO_WAIT); + rc = k_sem_take(&isr_sync, K_MSEC(10)); + zassert_equal(rc, 0, + "isr sync"); + + zassert_equal(isr_state.result, -EWOULDBLOCK, + "isr reset"); + zassert_equal(cli_result(&spinwait_cli), -EAGAIN, + "is reset result"); +} + +static void test_request(void) +{ + int rc; + struct onoff_service srv; + + clear_transit(); + + rc = onoff_service_init(&srv, start, stop, reset, 0); + zassert_equal(rc, 0, + "service init"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_true(rc >= 0, + "reset req: %d", rc); + zassert_equal(srv.refs, 1U, + "reset req refs: %u", srv.refs); + zassert_equal(cli_result(&spinwait_cli), 0, + "reset req result: %d", cli_result(&spinwait_cli)); + + /* Can't reset when no error present. */ + init_spinwait(&spinwait_cli); + rc = onoff_service_reset(&srv, &spinwait_cli); + zassert_equal(rc, -EALREADY, + "reset spin client"); + + /* Reference overflow produces -EAGAIN */ + u32_t refs = srv.refs; + + srv.refs = UINT16_MAX; + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, -EAGAIN, + "reset req overflow: %d", rc); + srv.refs = refs; + + /* Force an error. */ + stop_state.retval = -32; + init_spinwait(&spinwait_cli); + rc = onoff_release(&srv, &spinwait_cli); + zassert_equal(rc, 2, + "error release"); + zassert_equal(cli_result(&spinwait_cli), stop_state.retval, + "error retval"); + zassert_true(onoff_service_has_error(&srv), + "has error"); + + /* Can't request when error present. */ + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, -EIO, + "req with error"); + + /* Can't release when error present. */ + init_spinwait(&spinwait_cli); + rc = onoff_release(&srv, &spinwait_cli); + zassert_equal(rc, -EIO, + "rel with error"); + + struct k_poll_signal sig; + struct onoff_client cli; + + /* Clear the error */ + init_notify_sig(&cli, &sig); + rc = onoff_service_reset(&srv, &cli); + zassert_equal(rc, 0, + "reset"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + /* Error on start */ + start_state.retval = -12; + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 2, + "req with error"); + zassert_equal(cli_result(&spinwait_cli), start_state.retval, + "req with error"); + zassert_true(onoff_service_has_error(&srv), + "has error"); + + /* Clear the error */ + init_spinwait(&spinwait_cli); + rc = onoff_service_reset(&srv, &spinwait_cli); + zassert_equal(rc, 0, + "reset"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + /* Diagnose a no-wait delayed start */ + rc = onoff_service_init(&srv, start, stop, reset, + ONOFF_SERVICE_START_SLEEPS); + zassert_equal(rc, 0, + "service init"); + start_state.async = true; + start_state.retval = 12; + + struct isr_call_state isr_state = { + .srv = &srv, + .cli = &spinwait_cli, + }; + struct k_timer timer; + + init_spinwait(&spinwait_cli); + k_timer_init(&timer, isr_request, NULL); + k_timer_user_data_set(&timer, &isr_state); + + k_timer_start(&timer, K_MSEC(1), K_NO_WAIT); + rc = k_sem_take(&isr_sync, K_MSEC(10)); + zassert_equal(rc, 0, + "isr sync"); + + zassert_equal(isr_state.result, -EWOULDBLOCK, + "isr request"); + zassert_equal(cli_result(&spinwait_cli), -EAGAIN, + "isr request result"); +} + +static void test_sync(void) +{ + int rc; + struct onoff_service srv; + + clear_transit(); + + rc = onoff_service_init(&srv, start, stop, reset, 0); + zassert_equal(rc, 0, + "service init"); + + /* WHITEBOX: request that triggers on returns positive */ + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 2, /* WHITEBOX starting request */ + "req ok"); + zassert_equal(srv.refs, 1U, + "reset req refs: %u", srv.refs); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 0, /* WHITEBOX on request */ + "req ok"); + zassert_equal(srv.refs, 2U, + "reset req refs: %u", srv.refs); + + init_spinwait(&spinwait_cli); + rc = onoff_release(&srv, &spinwait_cli); + zassert_equal(rc, 1, /* WHITEBOX non-stopping release */ + "rel ok"); + zassert_equal(srv.refs, 1U, + "reset rel refs: %u", srv.refs); + + init_spinwait(&spinwait_cli); + rc = onoff_release(&srv, &spinwait_cli); + zassert_equal(rc, 2, /* WHITEBOX stopping release*/ + "rel ok: %d", rc); + zassert_equal(srv.refs, 0U, + "reset rel refs: %u", srv.refs); + + init_spinwait(&spinwait_cli); + rc = onoff_release(&srv, &spinwait_cli); + zassert_equal(rc, -EALREADY, + "rel noent"); +} + +static void test_async(void) +{ + int rc; + struct onoff_service srv; + struct k_poll_signal sig[2]; + struct onoff_client cli[2]; + unsigned int signalled = 0; + int result = 0; + + clear_transit(); + start_state.async = true; + start_state.retval = 23; + stop_state.async = true; + stop_state.retval = 17; + + rc = onoff_service_init(&srv, start, stop, reset, + ONOFF_SERVICE_START_SLEEPS + | ONOFF_SERVICE_STOP_SLEEPS); + zassert_equal(rc, 0, + "service init"); + + /* WHITEBOX: request that triggers on returns positive */ + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_request(&srv, &cli[0]); + zassert_equal(rc, 2, /* WHITEBOX starting request */ + "req ok"); + k_poll_signal_check(&sig[0], &signalled, &result); + zassert_equal((bool)signalled, false, + "cli signalled"); + zassert_equal(srv.refs, 0U, + "reset req refs: %u", srv.refs); + + + /* Non-initial request from ISR is OK */ + struct onoff_client isrcli; + struct isr_call_state isr_state = { + .srv = &srv, + .cli = &isrcli, + }; + struct k_timer timer; + + init_spinwait(&isrcli); + k_timer_init(&timer, isr_request, NULL); + k_timer_user_data_set(&timer, &isr_state); + + k_timer_start(&timer, K_MSEC(1), K_NO_WAIT); + rc = k_sem_take(&isr_sync, K_MSEC(10)); + zassert_equal(rc, 0, + "isr sync"); + + zassert_equal(isr_state.result, 1, /* WHITEBOX pending request */ + "isr request: %d", isr_state.result); + zassert_equal(cli_result(&isrcli), -EAGAIN, + "isr request result"); + + /* Off while on pending is not supported */ + init_notify_sig(&cli[1], &sig[1]); + rc = onoff_release(&srv, &cli[1]); + zassert_equal(rc, -EBUSY, + "rel in to-on"); + + /* Second request is delayed for first. */ + init_notify_sig(&cli[1], &sig[1]); + rc = onoff_request(&srv, &cli[1]); + zassert_equal(rc, 1, /* WHITEBOX pending request */ + "req ok"); + k_poll_signal_check(&sig[1], &signalled, &result); + zassert_equal((bool)signalled, false, + "cli signalled"); + zassert_equal(srv.refs, 0U, + "reset req refs: %u", srv.refs); + + /* Complete the transition. */ + notify(&start_state); + k_poll_signal_check(&sig[0], &signalled, &result); + k_poll_signal_reset(&sig[0]); + zassert_equal((bool)signalled, true, + "cli signalled"); + zassert_equal(result, start_state.retval, + "cli result"); + zassert_equal(cli_result(&isrcli), start_state.retval, + "isrcli result"); + k_poll_signal_check(&sig[1], &signalled, &result); + k_poll_signal_reset(&sig[1]); + zassert_equal((bool)signalled, true, + "cli2 signalled"); + zassert_equal(result, start_state.retval, + "cli2 result"); + zassert_equal(srv.refs, 3U, + "reset req refs: %u", srv.refs); + + /* Non-final release decrements refs and completes. */ + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_release(&srv, &cli[0]); + zassert_equal(rc, 1, /* WHITEBOX non-stopping release */ + "rel ok"); + zassert_equal(srv.refs, 2U, + "reset rel refs: %u", srv.refs); + k_poll_signal_check(&sig[0], &signalled, &result); + k_poll_signal_reset(&sig[0]); + zassert_equal((bool)signalled, true, + "cli signalled"); + zassert_equal(result, 0, + "cli result"); + + /* Non-final release from ISR is OK */ + init_spinwait(&isrcli); + k_timer_init(&timer, isr_release, NULL); + k_timer_user_data_set(&timer, &isr_state); + + k_timer_start(&timer, K_MSEC(1), K_NO_WAIT); + rc = k_sem_take(&isr_sync, K_MSEC(10)); + zassert_equal(rc, 0, + "isr sync"); + + zassert_equal(isr_state.result, 1, /* WHITEBOX pending request */ + "isr release: %d", isr_state.result); + zassert_equal(cli_result(&isrcli), 0, + "isr release result"); + zassert_equal(srv.refs, 1U, + "reset rel refs: %u", srv.refs); + + /* Final release cannot be from ISR */ + + init_spinwait(&isrcli); + k_timer_start(&timer, K_MSEC(1), K_NO_WAIT); + rc = k_sem_take(&isr_sync, K_MSEC(10)); + zassert_equal(rc, 0, + "isr sync"); + + zassert_equal(isr_state.result, -EWOULDBLOCK, + "isr release"); + zassert_equal(cli_result(&isrcli), -EAGAIN, + "is release result"); + + /* Final async release holds until notify */ + init_notify_sig(&cli[1], &sig[1]); + rc = onoff_release(&srv, &cli[1]); + zassert_equal(rc, 2, /* WHITEBOX stopping release */ + "rel ok: %d", rc); + zassert_equal(srv.refs, 1U, + "reset rel refs: %u", srv.refs); + + /* Redundant release in to-off */ + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_release(&srv, &cli[0]); + zassert_equal(rc, -EALREADY, + "rel to-off: %d", rc); + zassert_equal(srv.refs, 1U, + "reset rel refs: %u", srv.refs); + k_poll_signal_check(&sig[0], &signalled, &result); + zassert_equal((bool)signalled, false, + "cli signalled"); + + /* Request when turning off is queued */ + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_request(&srv, &cli[0]); + zassert_equal(rc, 3, /* WHITEBOX stopping request */ + "req in to-off"); + + /* Finalize release, queues start */ + zassert_true(start_state.notify == NULL, + "start not invoked"); + notify(&stop_state); + zassert_false(start_state.notify == NULL, + "start invoked"); + zassert_equal(srv.refs, 0U, + "reset rel refs: %u", srv.refs); + k_poll_signal_check(&sig[1], &signalled, &result); + k_poll_signal_reset(&sig[1]); + zassert_equal((bool)signalled, true, + "cli signalled"); + zassert_equal(result, stop_state.retval, + "cli result"); + + /* Release when starting is an error */ + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_release(&srv, &cli[0]); + zassert_equal(rc, -EBUSY, + "rel to-off: %d", rc); + + /* Finalize queued start, gets us to on */ + cli[0].result = 1 + start_state.retval; + zassert_equal(cli_result(&cli[0]), -EAGAIN, + "fetch failed"); + notify(&start_state); + zassert_equal(cli_result(&cli[0]), start_state.retval, + "start notified"); + zassert_equal(srv.refs, 1U, + "reset rel refs: %u", srv.refs); +} + +static void test_half_sync(void) +{ + int rc; + struct onoff_service srv; + struct k_poll_signal sig; + struct onoff_client cli; + + clear_transit(); + start_state.retval = 23; + stop_state.async = true; + stop_state.retval = 17; + + rc = onoff_service_init(&srv, start, stop, NULL, + ONOFF_SERVICE_STOP_SLEEPS); + zassert_equal(rc, 0, + "service init"); + + /* Test that a synchronous start delayed by a pending + * asynchronous stop is accepted. + */ + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 2, + "req0"); + zassert_equal(srv.refs, 1U, + "active"); + zassert_equal(cli_result(&spinwait_cli), start_state.retval, + "request"); + + zassert_true(stop_state.notify == NULL, + "not stopping"); + init_notify_sig(&cli, &sig); + rc = onoff_release(&srv, &cli); + zassert_equal(rc, 2, + "rel0"); + zassert_equal(srv.refs, 1U, + "active"); + zassert_false(stop_state.notify == NULL, + "stop pending"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 3, /* WHITEBOX start delayed for stop */ + "restart"); + + zassert_equal(cli_result(&cli), -EAGAIN, + "stop incomplete"); + zassert_equal(cli_result(&spinwait_cli), -EAGAIN, + "restart incomplete"); + notify(&stop_state); + zassert_equal(cli_result(&cli), stop_state.retval, + "stop complete"); + zassert_equal(cli_result(&spinwait_cli), start_state.retval, + "restart complete"); +} + +static void test_cancel_request_waits(void) +{ + int rc; + struct onoff_service srv; + struct k_poll_signal sig; + struct onoff_client cli; + + clear_transit(); + start_state.async = true; + start_state.retval = 14; + stop_state.async = true; + stop_state.retval = 31; + + rc = onoff_service_init(&srv, start, stop, NULL, + ONOFF_SERVICE_START_SLEEPS + | ONOFF_SERVICE_STOP_SLEEPS); + zassert_equal(rc, 0, + "service init"); + + init_notify_sig(&cli, &sig); + rc = onoff_request(&srv, &cli); + zassert_true(rc > 0, + "request pending"); + zassert_false(start_state.notify == NULL, + "start pending"); + zassert_equal(cli_result(&cli), -EAGAIN, + "start pending"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 1, /* WHITEBOX secondary request */ + "start2 pending"); + zassert_equal(cli_result(&spinwait_cli), -EAGAIN, + "start2 pending"); + + /* Allowed to cancel in-progress start if doing so leaves + * something to receive the start completion. + */ + rc = onoff_cancel(&srv, &cli); + zassert_equal(rc, 0, + "cancel failed: %d", rc); + zassert_equal(cli_result(&cli), -ECANCELED, + "cancel notified"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + /* Not allowed to cancel the last pending start. + */ + rc = onoff_cancel(&srv, &spinwait_cli); + zassert_equal(rc, -EWOULDBLOCK, + "last cancel", rc); + zassert_false(onoff_service_has_error(&srv), + "has error"); + zassert_equal(cli_result(&spinwait_cli), -EAGAIN, + "last request"); + + notify(&start_state); + zassert_equal(cli_result(&spinwait_cli), start_state.retval, + "last request"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + + /* Issue a stop, then confirm that you can request and cancel + * a restart. + */ + init_spinwait(&cli); + rc = onoff_release(&srv, &cli); + zassert_equal(rc, 2, /* WHITEBOX stop pending */ + "stop pending, %d", rc); + zassert_equal(cli_result(&cli), -EAGAIN, + "stop pending"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_equal(rc, 3, /* WHITEBOX restart pending */ + "restart pending"); + + rc = onoff_cancel(&srv, &spinwait_cli); + zassert_equal(rc, 0, + "restart cancel"); + zassert_equal(cli_result(&spinwait_cli), -ECANCELED, + "restart cancel"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + zassert_equal(cli_result(&cli), -EAGAIN, + "stop pending"); + + notify(&stop_state); + zassert_equal(cli_result(&cli), stop_state.retval, + "released"); + zassert_false(onoff_service_has_error(&srv), + "has error"); +} + +static void test_cancel_request_ok(void) +{ + int rc; + struct onoff_service srv; + struct k_poll_signal sig; + struct onoff_client cli; + + clear_transit(); + start_state.async = true; + start_state.retval = 14; + stop_state.retval = 31; + + rc = onoff_service_init(&srv, start, stop, NULL, + ONOFF_SERVICE_START_SLEEPS); + zassert_equal(rc, 0, + "service init"); + + init_notify_sig(&cli, &sig); + rc = onoff_request(&srv, &cli); + zassert_true(rc > 0, + "request pending"); + zassert_false(start_state.notify == NULL, + "start pending"); + + /* You can't cancel the last start request */ + rc = onoff_cancel(&srv, &cli); + zassert_equal(rc, -EWOULDBLOCK, + "cancel"); + zassert_equal(srv.refs, 0, + "refs empty"); + + notify(&start_state); + zassert_equal(srv.refs, 1, + "refs"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + zassert_equal(cli_result(&cli), start_state.retval, + "cancel notified"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + /* You can "cancel" an request that isn't active */ + init_spinwait(&cli); + rc = onoff_cancel(&srv, &cli); + zassert_equal(rc, -EALREADY, + "unregistered"); + + /* Error if cancel params invalid */ + rc = onoff_cancel(&srv, NULL); + zassert_equal(rc, -EINVAL, + "invalid"); +} + +static void test_blocked_restart(void) +{ + int rc; + struct onoff_service srv; + unsigned int signalled = 0; + int result; + struct k_poll_signal sig[2]; + struct onoff_client cli[2]; + + clear_transit(); + start_state.async = true; + start_state.retval = 14; + stop_state.async = true; + stop_state.retval = 31; + + rc = onoff_service_init(&srv, start, stop, NULL, + ONOFF_SERVICE_START_SLEEPS + | ONOFF_SERVICE_STOP_SLEEPS); + zassert_equal(rc, 0, + "service init"); + + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_request(&srv, &cli[0]); + zassert_true(rc > 0, + "started"); + zassert_false(start_state.notify == NULL, + "start pending"); + notify(&start_state); + + result = -start_state.retval; + k_poll_signal_check(&sig[0], &signalled, &result); + zassert_true(signalled != 0, + "signalled"); + zassert_equal(result, start_state.retval, + "result"); + k_poll_signal_reset(&sig[0]); + + start_state.async = true; + init_notify_sig(&cli[0], &sig[0]); + rc = onoff_release(&srv, &cli[0]); + zassert_true(rc > 0, + "stop initiated"); + zassert_false(stop_state.notify == NULL, + "stop pending"); + init_notify_sig(&cli[1], &sig[1]); + rc = onoff_request(&srv, &cli[1]); + zassert_true(rc > 0, + "start pending"); + + result = start_state.retval + stop_state.retval; + k_poll_signal_check(&sig[0], &signalled, &result); + zassert_true(signalled == 0, + "stop signalled"); + k_poll_signal_check(&sig[1], &signalled, &result); + zassert_true(signalled == 0, + "restart signalled"); + + k_timer_user_data_set(&isr_timer, &stop_state); + k_timer_start(&isr_timer, K_MSEC(1), K_NO_WAIT); + rc = k_sem_take(&isr_sync, K_MSEC(10)); + zassert_equal(rc, 0, + "isr sync"); + + /* Fail-to-restart is not an error */ + zassert_false(onoff_service_has_error(&srv), + "has error"); + + k_poll_signal_check(&sig[0], &signalled, &result); + zassert_false(signalled == 0, + "stop pending"); + zassert_equal(result, stop_state.retval, + "stop succeeded"); + + k_poll_signal_check(&sig[1], &signalled, &result); + zassert_false(signalled == 0, + "restart pending"); + zassert_equal(result, -EWOULDBLOCK, + "restart failed"); +} + +static void test_cancel_release(void) +{ + int rc; + struct onoff_service srv; + + clear_transit(); + start_state.retval = 16; + stop_state.async = true; + stop_state.retval = 94; + + rc = onoff_service_init(&srv, start, stop, NULL, + ONOFF_SERVICE_STOP_SLEEPS); + zassert_equal(rc, 0, + "service init"); + + init_spinwait(&spinwait_cli); + rc = onoff_request(&srv, &spinwait_cli); + zassert_true(rc > 0, + "request done"); + zassert_equal(cli_result(&spinwait_cli), start_state.retval, + "started"); + + init_spinwait(&spinwait_cli); + rc = onoff_release(&srv, &spinwait_cli); + zassert_true(rc > 0, + "release pending"); + zassert_false(stop_state.notify == NULL, + "release pending"); + zassert_equal(cli_result(&spinwait_cli), -EAGAIN, + "release pending"); + + /* You can't cancel a stop request. */ + rc = onoff_cancel(&srv, &spinwait_cli); + zassert_equal(rc, -EWOULDBLOCK, + "cancel succeeded"); + zassert_false(onoff_service_has_error(&srv), + "has error"); + + notify(&stop_state); + zassert_equal(cli_result(&spinwait_cli), stop_state.retval, + "release pending"); + zassert_false(onoff_service_has_error(&srv), + "has error"); +} + +void test_main(void) +{ + k_sem_init(&isr_sync, 0, 1); + k_timer_init(&isr_timer, isr_notify, NULL); + + ztest_test_suite(onoff_api, + ztest_unit_test(test_service_init_validation), + ztest_unit_test(test_client_init_validation), + ztest_unit_test(test_validate_args), + ztest_unit_test(test_reset), + ztest_unit_test(test_request), + ztest_unit_test(test_sync), + ztest_unit_test(test_async), + ztest_unit_test(test_half_sync), + ztest_unit_test(test_cancel_request_waits), + ztest_unit_test(test_cancel_request_ok), + ztest_unit_test(test_blocked_restart), + ztest_unit_test(test_cancel_release)); + ztest_run_test_suite(onoff_api); +} diff --git a/tests/lib/onoff/testcase.yaml b/tests/lib/onoff/testcase.yaml new file mode 100644 index 00000000000..29301aee84a --- /dev/null +++ b/tests/lib/onoff/testcase.yaml @@ -0,0 +1,3 @@ +tests: + libraries.onoff: + tags: onoff timer