Skip to content

radio_esp32::tasks

_task_create, _task_create_pinned_to_core and _task_delete — the blob’s own threads.

Three entries, and the reason esp_wifi_init_internal could not return. The Wi-Fi driver creates wifiT before it does anything else, so with these null the first call into the blob jumped to address zero — which is what EXCCAUSE=0x14 epc=0x00000000 was saying all along.

Same shape as [crate::interrupts], for the same reason. FreeRTOS’s task entry is void (*)(void *param); kernel::dynobj::spawn_task takes a plain fn(), which carries no environment, and a closure cannot bridge that. What can is a distinct function per slot, each knowing its own slot number at compile time — TRAMPOLINES is [SLOTS] different fn() values, each of which looks up its slot and calls what the blob asked for with the argument the blob supplied.

[SLOTS] is the ceiling on how many blob tasks may exist at once. It is a fixed array rather than a growable one because the trampolines have to be written out by hand — there is no way to generate fn() items at runtime — and because the number of tasks the driver creates is a property of the driver, not of the workload.

FreeRTOS’s TaskHandle_t is a TCB pointer, and a null handle means “the calling task” — vTaskDelete(NULL) deletes yourself. FlintOS’s task ids start at zero, so handing an id out raw would make task 0 indistinguishable from “myself”, and the blob would delete the wrong thread exactly once, at a point far from here.

So a handle is id + 1. [handle_from_id] and [id_from_handle] are the only two places that know it, and _task_get_current_task uses them too — the blob compares the handle it was given against the one it stored, and two encodings would never match.

Vanilla FreeRTOS counts usStackDepth in words. The ESP-IDF port counts it in bytes, and the blob is built against the ESP-IDF port: NuttX passes stack_depth straight to kthread_create, which is bytes. Treating it as words would give the Wi-Fi task four times the stack it asked for, and kernel::spawn would reject it against MAX_STACK_SIZE — a failure that reads like “out of memory” rather than like a units bug.

struct Created {
pub core_id: u32,
pub prio: u32,
pub stack: u32,
pub pinned_to: Option<u8>,
pub slot: u8,
pub spawned: bool,
}

One _task_create request, as it arrived and as it was resolved.

Recorded rather than logged: this runs on the blob’s init path, and logging from inside the blob has hung the board before. The application reads it back afterwards.

The question it exists to answer: does the driver ever ask for the second core? That core is not started in this application (soc_esp32::appcpu::is_running() is false, measured), and [create] maps a request for it onto Affinity::Core(1) because MAX_CORES is 2 — a task pinned to a core that never runs is created successfully, reports success to the blob, and then sits there. That would explain both the init hang and the receiver never being switched on, and it is entirely unconfirmed until one of these records shows core_id of 1.

Implements: Clone, Copy

unsafe fn create(entry: *mut core::ffi::c_void, name: *const core::ffi::c_char, stack_depth: u32, param: *mut core::ffi::c_void, prio: u32, task_handle: *mut core::ffi::c_void, core_id: u32) -> i32

_task_create_pinned_to_core(entry, name, stack_depth, param, prio, task_handle, core_id).

task_handle is an out-parameter, and may be null when the caller does not want one back. core_id is FreeRTOS’s tskNO_AFFINITY (0xFFFFFFFF) or any out-of-range value when the task may float, which is how [create_anywhere] is written in terms of this.

Returns pdTRUE/pdFALSE, not an id — the id goes through task_handle.

entry is called on a new task with param. name is a C string. task_handle, if non-null, is written with one TaskHandle_t.

unsafe fn create_anywhere(entry: *mut core::ffi::c_void, name: *const core::ffi::c_char, stack_depth: u32, param: *mut core::ffi::c_void, prio: u32, task_handle: *mut core::ffi::c_void) -> i32

_task_create(entry, name, stack_depth, param, prio, task_handle).

esp-idf’s is xTaskCreate, which is xTaskCreatePinnedToCore with tskNO_AFFINITY. Written the same way here rather than duplicated.

As [create].

unsafe fn delete(handle: *mut core::ffi::c_void)

_task_delete(handle). A null handle deletes the calling task.

The slot is released before the task is, because after delete_task the id belongs to the allocator again and a later create could be handed it — at which point a slot still claiming that id would be freed out from under a live task.

handle is one this module handed out, or null. Called by the blob.

fn for_each_create<impl FnMut(&Created)>(f: impl FnMut)

Every task the blob has asked for, in order.

fn handle_from_id(id: u32) -> *mut core::ffi::c_void

A FlintOS task id as the blob’s TaskHandle_t.

fn id_from_handle(handle: *mut core::ffi::c_void) -> Option<u32>

The blob’s TaskHandle_t as a FlintOS task id, or None for null — which FreeRTOS defines as “the calling task”.

fn set_start_delay_ms(ms: u32)

Set the injected delay. See START_DELAY_MS.

fn slot_counts(n: usize) -> (u32, u32)

Entered and exited counts for slot n.

const SLOTS: usize;

How many blob tasks may exist at once.

The Wi-Fi driver creates one (wifiT); the timer and event machinery adds a couple more, and BLE will want its own when #66 lands. Eight is room for all of that with the array still small enough to write out below.