blob: d279233fe2bfe9dce420ab1aec72937f804d6cb0 [file] [edit]
/*
* SPDX-FileCopyrightText: <text>Copyright (c) 2026 Infineon Technologies AG,
* or an affiliate of Infineon Technologies AG. All rights reserved.</text>
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief RTIO timeout iodev backing the RTIO_OP_DELAY operation.
*
* Delay submissions are handled like any other iodev operation rather than as
* an executor special case. The "device" here is the system clock: pending
* delays are kept in a per-instance list sorted by absolute expiration and
* driven by a single kernel timeout re-armed to the nearest deadline. This
* keeps the per-SQE footprint to a single k_timepoint_t (no embedded
* struct _timeout) and reduces the kernel timeout queue to one node per
* timeout iodev instance.
*/
#include <zephyr/kernel.h>
#include <zephyr/rtio/rtio.h>
/** Required to access the Timeout Queue APIs, which are used instead of the
* Timer APIs because of concerns on size: a shared struct _timeout is cheaper
* than a k_timer, and RTIO applications are sensitive to memory footprint.
*/
#include <../kernel/include/timeout_q.h>
/**
* @brief Per-instance state for a timeout iodev
*
* The sorted list link reuses each iodev_sqe's embedded mpsc_node (@ref
* rtio_iodev_sqe.q), which is free while the submission is owned by this iodev
* (it is out of the context submission queue and not yet recycled to the pool).
*/
struct rtio_timeout_iodev_data {
struct k_spinlock lock;
struct _timeout timeout; /* single kernel timeout shared by all pending delays */
struct rtio_iodev_sqe *head; /* soonest-first sorted list of pending delays */
};
/* Reuse the iodev_sqe's mpsc_node storage as the sorted-list link. */
static inline struct rtio_iodev_sqe *rtio_tq_next(struct rtio_iodev_sqe *iodev_sqe)
{
struct mpsc_node *node = mpsc_ptr_get(iodev_sqe->q.next);
return node == NULL ? NULL : CONTAINER_OF(node, struct rtio_iodev_sqe, q);
}
static inline void rtio_tq_set_next(struct rtio_iodev_sqe *iodev_sqe, struct rtio_iodev_sqe *next)
{
mpsc_ptr_set(iodev_sqe->q.next, next == NULL ? NULL : &next->q);
}
/**
* @brief Insert a submission into the sorted list
*
* @retval true if the submission became the new head (nearest deadline)
*/
static bool rtio_tq_insert(struct rtio_timeout_iodev_data *data, struct rtio_iodev_sqe *iodev_sqe)
{
struct rtio_iodev_sqe *prev = NULL;
struct rtio_iodev_sqe *curr = data->head;
while (curr != NULL &&
sys_timepoint_cmp(curr->sqe.delay.expiry, iodev_sqe->sqe.delay.expiry) <= 0) {
prev = curr;
curr = rtio_tq_next(curr);
}
rtio_tq_set_next(iodev_sqe, curr);
if (prev == NULL) {
data->head = iodev_sqe;
return true;
}
rtio_tq_set_next(prev, iodev_sqe);
return false;
}
/**
* @brief Detach all expired submissions from the head into a NULL-terminated batch
*/
static struct rtio_iodev_sqe *rtio_tq_pop_expired(struct rtio_timeout_iodev_data *data)
{
struct rtio_iodev_sqe *batch = NULL;
struct rtio_iodev_sqe *last = NULL;
while (data->head != NULL && sys_timepoint_expired(data->head->sqe.delay.expiry)) {
struct rtio_iodev_sqe *iodev_sqe = data->head;
data->head = rtio_tq_next(iodev_sqe);
rtio_tq_set_next(iodev_sqe, NULL);
if (last == NULL) {
batch = iodev_sqe;
} else {
rtio_tq_set_next(last, iodev_sqe);
}
last = iodev_sqe;
}
return batch;
}
static void rtio_tq_expired(struct _timeout *timeout);
/**
* @brief Arm the shared kernel timeout to the current head, or disarm if empty
*
* Must be called with @p data->lock held. On SMP, aborting the timeout may fail
* with -EAGAIN if the handler is in flight on another CPU; the only correct
* response is to drop the lock and retry, which is done by the caller.
*
* @retval true if the arm completed
* @retval false if the timeout abort needs the caller to drop the lock and retry
*/
static bool rtio_tq_try_arm(struct rtio_timeout_iodev_data *data)
{
if (z_try_abort_timeout(&data->timeout) == -EAGAIN) {
return false;
}
if (data->head != NULL) {
z_add_timeout(&data->timeout, rtio_tq_expired,
sys_timepoint_timeout(data->head->sqe.delay.expiry));
}
return true;
}
/**
* @brief Kernel timeout handler for the nearest pending delay
*
* Runs in the system clock announce context, exactly as the previous per-SQE
* alarm did. Expired submissions are detached and the timeout re-armed while
* holding the lock; completions run with the lock released because completing a
* submission may re-enter rtio_timeout_iodev_submit() (e.g. a chained delay).
*/
static void rtio_tq_expired(struct _timeout *timeout)
{
struct rtio_timeout_iodev_data *data =
CONTAINER_OF(timeout, struct rtio_timeout_iodev_data, timeout);
struct rtio_iodev_sqe *batch;
k_spinlock_key_t key = k_spin_lock(&data->lock);
batch = rtio_tq_pop_expired(data);
if (data->head != NULL) {
z_add_timeout(&data->timeout, rtio_tq_expired,
sys_timepoint_timeout(data->head->sqe.delay.expiry));
}
k_spin_unlock(&data->lock, key);
while (batch != NULL) {
struct rtio_iodev_sqe *iodev_sqe = batch;
batch = rtio_tq_next(iodev_sqe);
rtio_tq_set_next(iodev_sqe, NULL);
rtio_iodev_sqe_ok(iodev_sqe, 0);
}
}
static void rtio_timeout_iodev_submit(struct rtio_iodev_sqe *iodev_sqe)
{
struct rtio_timeout_iodev_data *data = iodev_sqe->sqe.iodev->data;
k_spinlock_key_t key;
iodev_sqe->sqe.delay.expiry = sys_timepoint_calc(iodev_sqe->sqe.delay.timeout);
key = k_spin_lock(&data->lock);
if (rtio_tq_insert(data, iodev_sqe)) {
/* New nearest deadline: re-arm, dropping the lock to retry if the
* handler is in flight on another CPU (SMP).
*/
while (!rtio_tq_try_arm(data)) {
k_spin_unlock(&data->lock, key);
key = k_spin_lock(&data->lock);
}
}
k_spin_unlock(&data->lock, key);
}
static const struct rtio_iodev_api rtio_timeout_iodev_api = {
.submit = rtio_timeout_iodev_submit,
};
static struct rtio_timeout_iodev_data rtio_timeout_iodev_data_inst;
RTIO_IODEV_DEFINE(rtio_timeout_iodev, &rtio_timeout_iodev_api, &rtio_timeout_iodev_data_inst);