Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion components/zkernel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,14 @@ k_event_wait_all(&evt, 0x07, false, K_MSEC(100)); // wait for ALL
k_event_clear(&evt, BIT(0)); // ISR-safe
```

**BEHAVIORAL DELTA:** FreeRTOS reserves bits 24-31 for internal use. Only bits 0-23 are available (vs Zephyr's 32 bits).
**Notification-backed** (no FreeRTOS event group): the full 32-bit events
word is available (the old event-group backend reserved bits 24-31).
Upstream semantics throughout: `set` replaces (use `post` to merge),
mutators return the previous value, `reset` zeroes the whole tracked set
before waiting, `wait_all` returns 0 on timeout, and the `_safe` wait
variants atomically consume matched bits. Same blocking architecture and
caveats as `k_sem` (reserved notification index; do not abort a blocked
waiter).

## Timer (`k_timer`)

Expand Down
63 changes: 51 additions & 12 deletions components/zkernel/include/boreas/zephyr/kernel.h
Original file line number Diff line number Diff line change
Expand Up @@ -285,29 +285,68 @@ uint32_t k_msgq_num_free_get(struct k_msgq *msgq);

/* ----------------------------------------------------------------
* Event
*
* BEHAVIORAL DELTA: FreeRTOS EventGroup reserves bits 24-31 for
* internal use. Only bits 0-23 (24 bits) are available, vs
* Zephyr's full 32 bits.
* ---------------------------------------------------------------- */

/* Notification-backed (no FreeRTOS event group): the full 32-bit
* events word and the waiter list live here, guarded by the lock --
* the old event-group backend capped usable events at 24 bits. See
* the README design principle: when a wait returns, the kernel holds
* no references into this struct. */
struct k_event {
EventGroupHandle_t handle;
StaticEventGroup_t buffer;
uint32_t events;
sys_dlist_t waiters; /* of z_event_waiter, caller-stack resident */
portMUX_TYPE lock;
};

#define K_EVENT_DEFINE(name) struct k_event name = {0}
/** Statically define a fully-initialized event object (compile-time
* initializer, matching upstream). */
#define K_EVENT_DEFINE(name) \
struct k_event name = { \
.events = 0, \
.waiters = SYS_DLIST_STATIC_INIT(&name.waiters), \
.lock = portMUX_INITIALIZER_UNLOCKED, \
}

int k_event_init(struct k_event *event);
void k_event_init(struct k_event *event);
/** Merge (OR) @p events into the tracked set; all waiters whose
* conditions become met wake. @return the previous value of the
* posted events bits. ISR-safe. */
uint32_t k_event_post(struct k_event *event, uint32_t events);
/** @note In ISR context, returns 0 on failure (timer command queue full)
* rather than the previous event state. FreeRTOS's
* xEventGroupSetBitsFromISR defers to the timer daemon and cannot
* report the prior bits synchronously. */
/** REPLACE the tracked set with @p events (upstream semantics --
* setting differs from posting). @return the previous events value.
* ISR-safe. */
uint32_t k_event_set(struct k_event *event, uint32_t events);
/** Overwrite only the bits selected by @p events_mask. @return the
* previous value of the masked bits. ISR-safe. */
uint32_t k_event_set_masked(struct k_event *event, uint32_t events, uint32_t events_mask);
/** Clear @p events from the tracked set. @return their previous
* value. ISR-safe. */
uint32_t k_event_clear(struct k_event *event, uint32_t events);
/**
* Wait for ANY of @p events. @p reset zeroes the ENTIRE tracked set
* before waiting (upstream semantics). @return the matching events
* (left set -- use the _safe variant to consume them), or 0 on
* timeout.
*
* @note Like k_sem_take: blocking is forbidden in ISRs (K_NO_WAIT is
* fine), and a thread blocked here must not be aborted.
*/
uint32_t k_event_wait(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout);
/** As k_event_wait, but requires ALL of @p events. */
uint32_t k_event_wait_all(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout);
/** As k_event_wait, but atomically CLEARS the matched events before
* returning (upstream parity). */
uint32_t k_event_wait_safe(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout);
/** As k_event_wait_all, but atomically clears the matched events. */
uint32_t k_event_wait_all_safe(struct k_event *event, uint32_t events, bool reset,
k_timeout_t timeout);

/** Non-blocking poll: the currently-set events matching
* @p events_mask (not cleared). ISR-safe. */
static inline uint32_t k_event_test(struct k_event *event, uint32_t events_mask)
{
return k_event_wait(event, events_mask, false, K_NO_WAIT);
}

/* ----------------------------------------------------------------
* Timer (over esp_timer)
Expand Down
264 changes: 232 additions & 32 deletions components/zkernel/src/k_event.c
Original file line number Diff line number Diff line change
@@ -1,67 +1,267 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2026 Intercreate
*
* Notification-backed k_event (same architecture as k_sem): the events
* word and waiter list live in the caller-owned struct under its lock;
* blocking rides direct-to-task notifications on the reserved index.
* Nothing of the caller's memory ever enters a kernel list (the
* object-lifetime principle -- zkernel README, #40). Replaces the
* FreeRTOS event-group backend, which capped usable events at 24 bits
* (EventBits_t reserves the top byte) and could not express upstream's
* set-replaces / reset-before-wait / 0-on-timeout semantics.
*/

#include "zephyr/kernel.h"

#include <errno.h>

#include "esp_attr.h"
#include "esp_log.h"
#include "sdkconfig.h"

static const char *TAG = "k_event";
#include "zkernel_internal.h"

struct z_event_waiter {
sys_dnode_t node;
TaskHandle_t task;
uint32_t mask; /* events the waiter is interested in */
bool all; /* require all bits vs any bit */
bool clear; /* _safe variant: consume matched bits on wake */
uint32_t matched; /* set by the waker under the lock */
bool woken; /* a post/set targeted this waiter */
};

int k_event_init(struct k_event *event)
static uint32_t z_event_match(uint32_t current, uint32_t mask, bool all)
{
event->handle = xEventGroupCreateStatic(&event->buffer);
if (event->handle == NULL) {
ESP_LOGE(TAG, "Failed to create event group");
return -ENOMEM;
uint32_t hit = current & mask;

if (all && hit != mask) {
return 0;
}
return hit;
}

/* Apply `events` under `mask`, wake every waiter whose condition
* becomes met (upstream: all satisfied waiters unpend), and return the
* previous value of the masked events. Satisfied waiters are unlinked
* into a local chain in ONE lock pass and notified after unlock.
*
* Safety of touching the stack-resident nodes post-unlock: a chained
* waiter cannot return from its wait until OUR notification lands.
* Within the protocol, exactly one in-flight notification can exist
* per blocked waiter -- a waiter is targeted by at most one waker
* (unlinking under the lock makes targeting exclusive), and every
* return path drains the notification it was woken by (including the
* timeout-race consume path). So when woken==true, the only
* notification that can release the waiter is ours, sent after we
* finish with its node. This premise is the index reservation itself:
* external code notifying Z_KERNEL_NOTIFY_INDEX is documented-
* forbidden misuse (zkernel_internal.h), under which a stale wake
* could release a chained waiter early -- the same premise
* k_sem_give's post-unlock handle use rests on. */
static uint32_t K_ISR_SAFE z_event_post_internal(struct k_event *event, uint32_t events,
uint32_t mask)
{
sys_dlist_t woken_chain;

sys_dlist_init(&woken_chain);

z_kernel_lock(&event->lock);

uint32_t previous = event->events & mask;

event->events = (event->events & ~mask) | (events & mask);

sys_dnode_t *n = sys_dlist_peek_head(&event->waiters);
uint32_t clear_accum = 0;

while (n != NULL) {
sys_dnode_t *next = sys_dlist_peek_next(&event->waiters, n);
struct z_event_waiter *w = CONTAINER_OF(n, struct z_event_waiter, node);
uint32_t hit = z_event_match(event->events, w->mask, w->all);

if (hit != 0) {
w->matched = hit;
w->woken = true;
if (w->clear) {
/* Accumulate; applied AFTER the walk so every
* waiter is evaluated against the same posted
* state (upstream applies clear_events at the
* end of its wait-queue walk -- clearing
* mid-walk would starve overlapping waiters). */
clear_accum |= hit;
}
sys_dlist_remove(&w->node);
sys_dlist_append(&woken_chain, &w->node);
}
n = next;
}

event->events &= ~clear_accum;

z_kernel_unlock(&event->lock);

bool in_isr = xPortInIsrContext();
BaseType_t isr_yield = pdFALSE;

for (n = sys_dlist_get(&woken_chain); n != NULL; n = sys_dlist_get(&woken_chain)) {
struct z_event_waiter *w = CONTAINER_OF(n, struct z_event_waiter, node);

if (in_isr) {
/* Accumulate across the chain; yield once below. */
vTaskNotifyGiveIndexedFromISR(w->task, Z_KERNEL_NOTIFY_INDEX, &isr_yield);
} else {
xTaskNotifyGiveIndexed(w->task, Z_KERNEL_NOTIFY_INDEX);
}
}
Comment thread
swoisz marked this conversation as resolved.
return 0;

if (in_isr) {
portYIELD_FROM_ISR(isr_yield);
}

return previous;
}

void k_event_init(struct k_event *event)
{
event->events = 0;
sys_dlist_init(&event->waiters);
event->lock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;
}

uint32_t K_ISR_SAFE k_event_post(struct k_event *event, uint32_t events)
{
return k_event_set(event, events);
return z_event_post_internal(event, events, events);
}

uint32_t K_ISR_SAFE k_event_set(struct k_event *event, uint32_t events)
{
if (xPortInIsrContext()) {
BaseType_t wake = pdFALSE;
BaseType_t ret =
xEventGroupSetBitsFromISR(event->handle, (EventBits_t)events, &wake);
if (wake) {
portYIELD_FROM_ISR(wake);
}
return (ret == pdPASS) ? events : 0;
}
return (uint32_t)xEventGroupSetBits(event->handle, (EventBits_t)events);
return z_event_post_internal(event, events, UINT32_MAX);
}

uint32_t K_ISR_SAFE k_event_set_masked(struct k_event *event, uint32_t events, uint32_t events_mask)
{
return z_event_post_internal(event, events, events_mask);
}

uint32_t K_ISR_SAFE k_event_clear(struct k_event *event, uint32_t events)
{
if (xPortInIsrContext()) {
return (uint32_t)xEventGroupClearBitsFromISR(event->handle, (EventBits_t)events);
return z_event_post_internal(event, 0, events);
}

static uint32_t z_event_wait_internal(struct k_event *event, uint32_t events, bool all, bool reset,
bool clear, k_timeout_t timeout)
{
/* The waiter node lives on THIS stack frame; it is unlinked under
* the event lock (by a waker or by our timeout path) before we
* return -- synchronous severance by construction. Identity is
* sampled OUTSIDE the lock and only on the must-block path, via
* the unlock-sample-relock-recheck loop (see k_sem_take: keeps
* the fast paths pre-scheduler-safe and FreeRTOS calls out of
* the critical section). */
struct z_event_waiter w = {0};

for (;;) {
z_kernel_lock(&event->lock);

if (reset) {
/* Upstream: zero the ENTIRE tracked set before
* waiting -- once, not on every recheck pass. */
event->events = 0;
reset = false;
}

uint32_t hit = z_event_match(event->events, events, all);

if (hit != 0) {
if (clear) {
event->events &= ~hit;
}
z_kernel_unlock(&event->lock);
return hit;
}

if (k_timeout_is_no_wait(timeout)) {
z_kernel_unlock(&event->lock);
return 0;
}

if (w.task != NULL) {
break; /* sampled on a previous pass; enqueue under THIS lock */
}

z_kernel_unlock(&event->lock);
w.task = xTaskGetCurrentTaskHandle();
w.mask = events;
w.all = all;
w.clear = clear;
}

sys_dlist_append(&event->waiters, &w.node);
z_kernel_unlock(&event->lock);

bool forever = k_timeout_is_forever(timeout);
TickType_t total = forever ? 0 : k_timeout_to_ticks(timeout);
TickType_t start = xTaskGetTickCount();

for (;;) {
TickType_t wait = portMAX_DELAY;

if (!forever) {
/* Unsigned tick diff is wraparound-safe. */
TickType_t elapsed = xTaskGetTickCount() - start;

wait = (elapsed >= total) ? 0 : (total - elapsed);
}

uint32_t got = ulTaskNotifyTakeIndexed(Z_KERNEL_NOTIFY_INDEX, pdTRUE, wait);

z_kernel_lock(&event->lock);
if (w.woken) {
/* A waker unlinked us. If our wait timed out in the
* same instant (got == 0), its notification is still
* in flight -- consume it before returning so it
* cannot poison a later blocking call on this task. */
uint32_t matched = w.matched;

z_kernel_unlock(&event->lock);
if (got == 0) {
(void)ulTaskNotifyTakeIndexed(Z_KERNEL_NOTIFY_INDEX, pdTRUE,
portMAX_DELAY);
}
return matched;
}

if (got == 0 && !forever) {
/* Timed out, not targeted: unlink ourselves. After
* this unlock no waker can see the node. */
sys_dlist_remove(&w.node);
z_kernel_unlock(&event->lock);
return 0;
}

/* Spurious wake (stale notification from code misusing the
* reserved index): loop; the wait recompute above absorbs
* the elapsed time. */
z_kernel_unlock(&event->lock);
}
return (uint32_t)xEventGroupClearBits(event->handle, (EventBits_t)events);
}

uint32_t k_event_wait(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)
{
EventBits_t bits = xEventGroupWaitBits(event->handle, (EventBits_t)events,
reset ? pdTRUE : pdFALSE, pdFALSE, /* wait for ANY */
k_timeout_to_ticks(timeout));
return (uint32_t)(bits & events);
return z_event_wait_internal(event, events, false, reset, false, timeout);
}

uint32_t k_event_wait_all(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)
{
EventBits_t bits = xEventGroupWaitBits(event->handle, (EventBits_t)events,
reset ? pdTRUE : pdFALSE, pdTRUE, /* wait for ALL */
k_timeout_to_ticks(timeout));
return (uint32_t)(bits & events);
return z_event_wait_internal(event, events, true, reset, false, timeout);
}

uint32_t k_event_wait_safe(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)
{
return z_event_wait_internal(event, events, false, reset, true, timeout);
}

uint32_t k_event_wait_all_safe(struct k_event *event, uint32_t events, bool reset,
k_timeout_t timeout)
{
return z_event_wait_internal(event, events, true, reset, true, timeout);
}
Loading
Loading