kernel::dynobj
Runtime-created kernel objects, for the radio blobs.
A second object model, beside the first
Section titled “A second object model, beside the first”FlintOS’s primitives are compile-time sized. Queue<T, N> fixes length and
type at build time, which is exactly right for a static RTOS and is what
gives it bounded latency and no fragmentation.
Espressif’s blobs do not work that way. They create and destroy objects at
runtime, in sizes they choose, through wifi_osi_funcs_t:
queue_create(len, item_size) copies bytes and does not know a type.
This is not an adapter over the static primitives — it is a second model standing beside them. The static ones keep their semantics and their tests unchanged, and nothing here is meant for kernel or application use.
What is reused
Section titled “What is reused”The blocking machinery, all of it. [crate::queue]’s waiter lists are keyed
by an arbitrary usize, not by a queue type, so a dynamic object passes its
own address and gets the scheduler integration, the timeout handling, the
refusal to block in interrupt context and — most valuable — the existing
race tests, which were hard won.
What is not reused is storage: these live in [crate::heap], which exists
for the radio and is confined to it.
Why this is P1 and not routine
Section titled “Why this is P1 and not routine”Dynamic objects are where an RTOS loses its timing guarantees. Every operation here is O(1) apart from waking waiters, which is the same cost the static queues already pay. Nothing allocates on the send or receive path; allocation happens at create and free at delete, and both are documented as unsuitable for a hot path.
Structs
Section titled “Structs”DynQueue
Section titled “DynQueue”struct DynQueue { /* private fields */ }A byte-copying queue whose length and item size are chosen at runtime.
The blob’s queue_create(len, item_size). Items are opaque bytes: the
queue never knows a type, so it cannot drop one, which is why
[DynQueue::delete] is safe to call with items still in it.
Implements: Send
EventGroup
Section titled “EventGroup”struct EventGroup { /* private fields */ }Bits that tasks can wait on, any or all.
Twenty-four bits, matching what the blob expects; the top eight of a u32
are reserved and rejected rather than silently masked off.
Implements: Default
RecursiveMutex
Section titled “RecursiveMutex”struct RecursiveMutex { /* private fields */ }A mutex that the same task may lock more than once.
A second type, deliberately. [crate::mutex] refuses re-entry and logs
an error, for a stated reason: nothing in its guard and unlock bookkeeping
supports nested ownership. That decision stands. The blob needs recursion,
so it gets its own type rather than the existing one being weakened.
No priority inheritance here, which the static mutex does have. Adding it would mean this type reaching into the scheduler’s boost bookkeeping, and the blob’s own locks are short and uncontended between its tasks. Worth revisiting if a priority inversion is ever actually observed.
Implements: Default
Semaphore
Section titled “Semaphore”struct Semaphore { /* private fields */ }A counting semaphore. Nothing like it exists in the static model.
A binary semaphore is this with a maximum of one, which is what the blob asks for most often.
SpinlockHandle
Section titled “SpinlockHandle”struct SpinlockHandle { /* private fields */ }An opaque spinlock, created and destroyed at runtime.
Wraps [crate::smp::Spinlock] so the blob gets a handle it can pass around.
The wrapped type’s interrupt-masking order is load-bearing and is preserved
exactly: it masks on the calling core for the whole critical section. The
other order — take the lock, then mask — deadlocks when a handler on the
same core wants the lock, which is precisely the case the radio hits.
Reentrancy still panics rather than hanging, as it does for every other
user of Spinlock. That is deliberate: a hang reports nothing.
Implements: Default
DeleteError
Section titled “DeleteError”enum DeleteError { NoSuchTask, StillRunning, NotDeletable,}Why a delete was refused.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
Functions
Section titled “Functions”current_task
Section titled “current_task”fn current_task() -> u32The id of the task that is running now.
delete_self
Section titled “delete_self”fn delete_self() -> !End the calling task. Never returns.
A task cannot free the stack it is executing on: the moment the heap
reissued those bytes, the next interrupt on this core would write its frame
into somebody else’s allocation. So this does the half that is safe from
here — leave the ready set, stop being a queue waiter — and hands the rest
to [reap_deleted], which runs on the idle task’s stack.
The task stays in TaskState::Deleting with its stack intact until then.
Nothing schedules it: every transition back into the ready set matches on
the blocked states by name, and this is not one of them.
Why the loop at the end
Section titled “Why the loop at the end”request_switch marks a switch pending; it does not perform one. Until the
scheduler acts, this core is still executing here, on a stack that must
stay valid. Parking is what keeps that true — returning would run the
caller’s epilogue on a task the scheduler has already written off.
delete_task
Section titled “delete_task”fn delete_task(id: u32) -> Result<(), DeleteError>Delete a task and return its stack to the heap.
Deleting the calling task never returns — see [delete_self].
What this refuses
Section titled “What this refuses”A task running on the other core. It cannot be made to switch away
synchronously, and freeing the stack under it would corrupt it
mid-instruction. [DeleteError::StillRunning].
Pool-backed tasks. Nothing to reclaim; see [DeleteError::NotDeletable].
What it handles
Section titled “What it handles”Removing the task from every queue waiter list. A deleted task left listed
gets unblocked by a later wake_one_*, by which point the slot may belong
to a different task — which then returns from a wait it never entered, far
from the delete that caused it.
References: DeleteError
reap_deleted
Section titled “reap_deleted”fn reap_deleted() -> usizeFree the stacks and slots of tasks that have deleted themselves.
Called from the idle loop, which is the one context guaranteed not to be running on a dying task’s stack: idle runs on the boot stack and is only reached when nothing else is ready.
Returns how many were reaped, which is what the tests assert on.
The check that makes this safe
Section titled “The check that makes this safe”A task is only freed once it is TaskState::Deleting and is not any
core’s current task. The second condition is what covers the other core:
while it is still executing the dying task — or saving its context on the
way out — current_per_core still names it, and this skips it for now.
Idle runs often; there is no hurry.
spawn_task
Section titled “spawn_task”fn spawn_task(name: &'static str, entry: fn(..), priority: hal::types::Priority, stack_size: usize) -> Option<u32>Create a task whose stack is returned when it is deleted.
The static [crate::spawn::sys_spawn] bump-allocates from the linker’s
task_stacks region and never reclaims it — correct for a static RTOS,
which creates its tasks once. The blobs create and delete tasks throughout
a session, so these take heap stacks instead and the pool is left alone.
Returns the task id, or None if there is no TCB slot or no heap.
References: Priority
spawn_task_on
Section titled “spawn_task_on”fn spawn_task_on(name: &'static str, entry: fn(..), priority: hal::types::Priority, stack_size: usize, affinity: crate::scheduler::Affinity) -> Option<u32>Create a heap-stacked task pinned to a core.
[spawn_task] with the affinity spelled out. It exists because the blobs
ask for it: _task_create_pinned_to_core is a separate entry from
_task_create precisely because the radio’s own task must run on the core
whose interrupt matrix its handlers were routed through, and answering that
request with Affinity::Any would be a wrong answer rather than a missing
one — the task would work, until the tick migrated it and its interrupts
stopped arriving.
References: Affinity, Priority
yield_from_isr
Section titled “yield_from_isr”fn yield_from_isr()Ask for a reschedule from an interrupt handler.
Does not switch here — the switch happens on the way out of the handler, which is the only point at which it is safe.
Constants
Section titled “Constants”EVENT_BITS_MASK
Section titled “EVENT_BITS_MASK”const EVENT_BITS_MASK: u32;The usable bits. The blob’s own API reserves the top byte for control.
FOREVER
Section titled “FOREVER”const FOREVER: u32;Wait forever. Matches the convention the static queues already use.
HIGHEST_PRIORITY
Section titled “HIGHEST_PRIORITY”const HIGHEST_PRIORITY: u8;The lowest-numbered — that is, highest — priority a task may be given.
FlintOS numbers priorities so that smaller is more urgent. The blob assumes
the opposite in places, so the adapter has to invert; this constant and
[LOWEST_PRIORITY] are what it inverts between.
LOWEST_PRIORITY
Section titled “LOWEST_PRIORITY”const LOWEST_PRIORITY: u8;The highest-numbered — least urgent — priority. Background(15).