Develop/nn#17
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a cooperative, event-driven task scheduler for non-RTOS builds (ek_evoke) and adds supporting infrastructure in utils (a new SPSC ring buffer variant and list insertion helpers), along with configuration and build-guard updates.
Changes:
- Added
ek_evokecooperative event scheduler (task/event creation, publish/broadcast, defer, ISR request FIFO, sleep hooks). - Added SPSC ring buffer (
ek_ringbuf_*_spsc) and updated ring buffer module guards to allow building either variant. - Extended doubly-linked list utilities with
ek_list_insert_before/afterand enabled SPSC ringbuf inek_conf.h.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| L2_Core/utils/src/ek_ringbuf.c | Adds SPSC ring buffer implementation and adjusts compile-time guards/includes. |
| L2_Core/utils/inc/ek_ringbuf.h | Declares SPSC ring buffer types/APIs and updates feature gating. |
| L2_Core/utils/src/ek_evoke.c | New non-RTOS cooperative event loop implementation using SPSC ISR FIFO + deferred event list. |
| L2_Core/utils/inc/ek_evoke.h | New public API for the cooperative scheduler (tasks/events, ISR helpers, weak hooks). |
| L2_Core/utils/inc/ek_list.h | Adds ek_list_insert_before/after helpers used by deferred scheduling. |
| ek_conf.h | Adds/updates module enable macros (notably EK_RINGBUF_SPSC_ENABLE). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| struct ek_ringbuf_spsc_t | ||
| { | ||
| uint8_t *buffer; /**< 缓冲区指针 */ | ||
| uint32_t write_idx; /**< 写入位置索引 */ | ||
| uint32_t read_idx; /**< 读取位置索引 */ | ||
| size_t cap; /**< 底层槽位数量,实际最大可存元素数为 cap - 1 */ | ||
| size_t item_size; /**< 单个元素大小(字节) */ | ||
| }; |
There was a problem hiding this comment.
ek_ringbuf_spsc_t is used as an ISR->main SPSC queue (see ek_evoke.c), but read_idx/write_idx are plain uint32_t. Without volatile/atomics (and appropriate memory ordering), the compiler is free to cache these fields, which can cause missed updates (e.g., ek_ringbuf_empty_spsc() not observing a new write) or non-terminating loops. Consider making the indices atomic (C11 _Atomic uint32_t) with acquire/release semantics, or at least volatile plus a compiler barrier tailored to the supported toolchains.
| bool ek_ringbuf_full_spsc(const ek_ringbuf_spsc_t *rb) | ||
| { | ||
| ek_assert_param(rb != NULL); | ||
| return _ek_ringbuf_spsc_next_idx(rb, rb->write_idx) == rb->read_idx; | ||
| } | ||
|
|
||
| bool ek_ringbuf_empty_spsc(const ek_ringbuf_spsc_t *rb) | ||
| { | ||
| ek_assert_param(rb != NULL); | ||
| return rb->read_idx == rb->write_idx; | ||
| } |
There was a problem hiding this comment.
SPSC ring buffer operations are used across ISR and main contexts, but these functions read/write read_idx/write_idx without any atomic/volatile semantics. Even on MCUs where 32-bit loads/stores are atomic, the lack of compiler-visible ordering can break the SPSC contract. Please add atomic/volatile handling in the implementation consistent with the chosen approach in the header.
| ek_list_remove(&tsk->node); | ||
| tsk->state = EK_EVOKE_STATE_RUNNING; | ||
| tsk->cb(tsk->wait_event, tsk->arg); | ||
| ek_list_insert_tail(&tsk->wait_event->wait_list, &tsk->node); | ||
| tsk->state = EK_EVOKE_STATE_WAITTING; |
There was a problem hiding this comment.
ek_evoke_event_loop() reinserts the task back into tsk->wait_event->wait_list after executing the callback. If the callback destroys the event (or otherwise invalidates tsk->wait_event) this becomes a use-after-free; similarly, if the callback causes the task to be destroyed, tsk becomes invalid. This needs an explicit safety rule (document and enforce with assertions), or a redesign where the scheduler does not touch tsk/evt after calling the user callback unless they are guaranteed to remain alive.
| ek_list_remove(&tsk->node); | |
| tsk->state = EK_EVOKE_STATE_RUNNING; | |
| tsk->cb(tsk->wait_event, tsk->arg); | |
| ek_list_insert_tail(&tsk->wait_event->wait_list, &tsk->node); | |
| tsk->state = EK_EVOKE_STATE_WAITTING; | |
| ek_evoke_event_t *evt = tsk->wait_event; | |
| void *arg = tsk->arg; | |
| EK_ASSERT(evt != NULL); | |
| EK_ASSERT(tsk->cb != NULL); | |
| ek_list_remove(&tsk->node); | |
| tsk->state = EK_EVOKE_STATE_RUNNING; | |
| /* | |
| * 回调可能销毁任务或事件,因此调度器不能在回调返回后继续访问 tsk/evt。 | |
| * 在调用用户回调前先恢复等待链表和等待状态,避免回调后的悬空访问。 | |
| */ | |
| ek_list_insert_tail(&evt->wait_list, &tsk->node); | |
| tsk->state = EK_EVOKE_STATE_WAITTING; | |
| tsk->cb(evt, arg); |
| uint32_t wakeup_tick = delay + _event_tick_base; | ||
|
|
||
| req->evt = evt; | ||
| req->broadcast = broadcast; | ||
| req->payload = payload; | ||
| req->wakeup_tick = wakeup_tick; | ||
|
|
||
| ek_list_node_t *pos; | ||
| ek_list_foreach(pos, &_defer_evt_list) | ||
| { | ||
| _defer_req_t *pos_req = ek_list_container(pos, _defer_req_t, node); | ||
| if (pos_req->wakeup_tick >= wakeup_tick) break; | ||
| } | ||
| ek_list_insert_before(pos, &req->node); |
There was a problem hiding this comment.
Deferred-event scheduling compares absolute uint32_t ticks (wakeup_tick vs other nodes, and later vs _defer_earilest_tick) using plain >= and subtraction. This will break when _event_tick_base overflows (wrap-around), causing misordering and potentially very large computed delays. Consider using a wrap-around-safe time comparison (signed-difference style, e.g., (int32_t)(a-b) >= 0) and computing deltas via subtraction only after such comparisons, or move to a 64-bit tick accumulator.
| _isr_req_t req = { | ||
| .type = ISR_REQ_BROADCAST, | ||
| .evt = evt, | ||
| .payload = payload, | ||
| }; | ||
| ek_ringbuf_write_spsc(_isr_fifo, &req); | ||
|
|
||
| ek_evoke_exit_critical(); | ||
| } | ||
|
|
||
| void ek_evoke_event_publish_from_isr(ek_evoke_event_handle_t evt, void *payload) | ||
| { | ||
| ek_assert_param(evt != NULL); | ||
|
|
||
| ek_evoke_enter_critical(); | ||
|
|
||
| _isr_req_t req = { | ||
| .type = ISR_REQ_PUBLISH, | ||
| .evt = evt, | ||
| .payload = payload, | ||
| }; | ||
| ek_ringbuf_write_spsc(_isr_fifo, &req); | ||
|
|
||
| ek_evoke_exit_critical(); | ||
| } | ||
|
|
||
| void ek_evoke_event_defer_from_isr(ek_evoke_event_handle_t evt, void *payload, uint32_t delay, bool broadcast) | ||
| { | ||
| ek_assert_param(evt != NULL); | ||
|
|
||
| ek_evoke_enter_critical(); | ||
|
|
||
| _isr_req_t req = { | ||
| .type = (broadcast == true) ? (ISR_REQ_PUBLISH_DEALY | ISR_REQ_BROADCAST) | ||
| : (ISR_REQ_PUBLISH_DEALY | ISR_REQ_PUBLISH), | ||
| .evt = evt, | ||
| .payload = payload, | ||
| .delay = delay, | ||
| }; | ||
| ek_ringbuf_write_spsc(_isr_fifo, &req); | ||
|
|
There was a problem hiding this comment.
The ISR enqueue helpers (*_from_isr) ignore the return value of ek_ringbuf_write_spsc(). When the ISR FIFO is full, requests will be silently dropped, potentially losing events with no visibility. Please handle the false case (e.g., increment a dropped counter, set an overflow flag processed in the main loop, or assert in debug builds).
| static volatile uint32_t _sleep_lock; | ||
| static volatile uint32_t _event_tick_base; | ||
| static volatile uint32_t _event_tick_diff; | ||
| static volatile uint32_t _defer_earilest_tick; |
There was a problem hiding this comment.
Typo in identifier: _defer_earilest_tick should be _defer_earliest_tick (and update related uses).
| static volatile uint32_t _defer_earilest_tick; | |
| static volatile uint32_t _defer_earliest_tick; |
| typedef enum | ||
| { | ||
| EK_EVOKE_STATE_IDLE = 0, /**< 空闲状态 */ | ||
| EK_EVOKE_STATE_DELAY, /**< 延迟状态 */ | ||
| EK_EVOKE_STATE_WAITTING, /**< 等待状态 */ | ||
| EK_EVOKE_STATE_READY, /**< 就绪状态 */ | ||
| EK_EVOKE_STATE_RUNNING, /**< 运行状态 */ | ||
|
|
||
| EK_EVOKE_STATE_MAX, /**< 状态最大值 */ | ||
| } ek_evoke_state_t; |
There was a problem hiding this comment.
Typo in enum value: EK_EVOKE_STATE_WAITTING should be EK_EVOKE_STATE_WAITING. Since this is a public header, it’s best to correct before release; if you need compatibility, you can keep the old name as a deprecated alias to the corrected one.
| ek_ringbuf_spsc_t *ek_ringbuf_create_spsc(size_t item_size, uint32_t item_amount) | ||
| { | ||
| ek_assert_param(item_amount > 1U); | ||
| ek_assert_param(item_size != 0U); | ||
|
|
||
| ek_ringbuf_spsc_t *rb = (ek_ringbuf_spsc_t *)ek_malloc(sizeof(ek_ringbuf_spsc_t)); | ||
| if (rb == NULL) | ||
| { | ||
| return NULL; | ||
| } | ||
|
|
||
| rb->buffer = (uint8_t *)ek_malloc(item_amount * item_size); | ||
| if (rb->buffer == NULL) | ||
| { | ||
| ek_free(rb); | ||
| return NULL; | ||
| } | ||
|
|
||
| rb->cap = item_amount; | ||
| rb->item_size = item_size; | ||
| rb->read_idx = 0U; | ||
| rb->write_idx = 0U; | ||
|
|
||
| return rb; | ||
| } | ||
|
|
||
| void ek_ringbuf_destroy_spsc(ek_ringbuf_spsc_t *rb) | ||
| { | ||
| ek_assert_param(rb != NULL); | ||
|
|
||
| ek_free(rb->buffer); | ||
| ek_free(rb); | ||
| } | ||
|
|
||
| bool ek_ringbuf_write_spsc(ek_ringbuf_spsc_t *rb, const void *item) | ||
| { | ||
| ek_assert_param(rb != NULL); | ||
| ek_assert_param(item != NULL); | ||
|
|
||
| uint32_t next_idx = _ek_ringbuf_spsc_next_idx(rb, rb->write_idx); | ||
| if (next_idx == rb->read_idx) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| uint8_t *target = rb->buffer + (rb->write_idx * rb->item_size); | ||
| memcpy(target, item, rb->item_size); | ||
| rb->write_idx = next_idx; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool ek_ringbuf_read_spsc(ek_ringbuf_spsc_t *rb, void *item) | ||
| { | ||
| ek_assert_param(rb != NULL); | ||
|
|
||
| if (rb->read_idx == rb->write_idx) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| if (item != NULL) | ||
| { | ||
| const uint8_t *source = rb->buffer + (rb->read_idx * rb->item_size); | ||
| memcpy(item, source, rb->item_size); | ||
| } | ||
|
|
||
| rb->read_idx = _ek_ringbuf_spsc_next_idx(rb, rb->read_idx); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool ek_ringbuf_peek_spsc(ek_ringbuf_spsc_t *rb, void *item) | ||
| { | ||
| ek_assert_param(rb != NULL); | ||
| ek_assert_param(item != NULL); | ||
|
|
||
| if (rb->read_idx == rb->write_idx) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| const uint8_t *source = rb->buffer + (rb->read_idx * rb->item_size); | ||
| memcpy(item, source, rb->item_size); | ||
|
|
||
| return true; | ||
| } |
There was a problem hiding this comment.
New SPSC ring buffer API has no corresponding tests, while the existing generic ring buffer has coverage in Test/ringbuf_test.c. Please add unit/functional tests for SPSC behavior (empty/full boundaries, wrap-around, overwrite prevention, and basic ISR-producer/main-consumer simulation where possible) to prevent regressions.
| # define ek_ringbuf_destroy_safely(rb_ptr) \ | ||
| do \ | ||
| { \ | ||
| ek_ringbuf_destroy(rb_ptr); \ |
There was a problem hiding this comment.
Macro arg isn’t parenthesized in the destroy call: ek_ringbuf_destroy(rb_ptr) vs the project’s existing pattern ek_stack_destroy((sk_ptr)) (ek_stack.h:105). Please wrap rb_ptr in parentheses in both ek_ringbuf_destroy_safely and ek_ringbuf_destroy_safely_spsc to avoid surprises if a non-trivial expression is ever passed.
| ek_ringbuf_destroy(rb_ptr); \ | |
| ek_ringbuf_destroy((rb_ptr)); \ |
| # define ek_ringbuf_destroy_safely_spsc(rb_ptr) \ | ||
| do \ | ||
| { \ | ||
| ek_ringbuf_destroy_spsc(rb_ptr); \ |
There was a problem hiding this comment.
Same macro-parenthesization issue exists for ek_ringbuf_destroy_safely_spsc: wrap rb_ptr in parentheses when passing to ek_ringbuf_destroy_spsc() to match existing macros and avoid surprises if an expression is passed.
| ek_ringbuf_destroy_spsc(rb_ptr); \ | |
| ek_ringbuf_destroy_spsc((rb_ptr)); \ |
No description provided.