diff --git a/examples/raspberrypi/rp2xxx/build.zig b/examples/raspberrypi/rp2xxx/build.zig index ea3b8c876..674b878b5 100644 --- a/examples/raspberrypi/rp2xxx/build.zig +++ b/examples/raspberrypi/rp2xxx/build.zig @@ -25,10 +25,14 @@ pub fn build(b: *std.Build) void { .{ .target = raspberrypi.pico, .name = "pico_pcf8574", .file = "src/rp2040_only/pcf8574.zig" }, .{ .target = raspberrypi.pico, .name = "pico_i2c_slave", .file = "src/rp2040_only/i2c_slave.zig" }, .{ .target = raspberrypi.pico, .name = "pico_freertos-hello-task", .file = "src/freertos/hello_task.zig" }, + .{ .target = raspberrypi.pico, .name = "pico_freertos-queue-demo", .file = "src/freertos/queue_demo.zig" }, + .{ .target = raspberrypi.pico, .name = "pico_freertos-multitask-demo", .file = "src/freertos/multitask_demo.zig" }, .{ .target = raspberrypi.pico2_arm, .name = "pico2_arm_multicore", .file = "src/blinky_core1.zig" }, .{ .target = raspberrypi.pico2_arm, .name = "pico2_arm_board_blinky", .file = "src/board_blinky.zig" }, .{ .target = raspberrypi.pico2_arm, .name = "pico2_arm_freertos-hello-task", .file = "src/freertos/hello_task.zig" }, + .{ .target = raspberrypi.pico2_arm, .name = "pico2_arm_freertos-queue-demo", .file = "src/freertos/queue_demo.zig" }, + .{ .target = raspberrypi.pico2_arm, .name = "pico2_arm_freertos-multitask-demo", .file = "src/freertos/multitask_demo.zig" }, .{ .target = raspberrypi.pico_flashless, .name = "pico_flashless_blinky", .file = "src/blinky.zig" }, .{ .target = raspberrypi.pico_flashless, .name = "pico_flashless_flash-program", .file = "src/rp2040_only/flash_program.zig" }, diff --git a/examples/raspberrypi/rp2xxx/src/freertos/hello_task.zig b/examples/raspberrypi/rp2xxx/src/freertos/hello_task.zig index 5de13af5b..614eb0dcd 100644 --- a/examples/raspberrypi/rp2xxx/src/freertos/hello_task.zig +++ b/examples/raspberrypi/rp2xxx/src/freertos/hello_task.zig @@ -1,11 +1,23 @@ +//! # FreeRTOS Hello Task Example +//! +//! The simplest FreeRTOS example — creates a single task that prints +//! "Hello from FreeRTOS!" every 500ms via UART. Demonstrates the basic +//! pattern: create a task, start the scheduler, let FreeRTOS manage execution. +//! +//! ## Primitives Used +//! - **Task**: `freertos.task.create` / `freertos.task.delay` +//! - **Scheduler**: `freertos.task.start_scheduler` +//! +//! ## Hardware Tested +//! - XIAO RP2350 (RP2350 ARM Cortex-M33) +//! - Raspberry Pi Pico (RP2040) + const std = @import("std"); const microzig = @import("microzig"); const freertos = @import("freertos"); -const freertos_os = freertos.OS; const rp2xxx = microzig.hal; -const time = rp2xxx.time; const gpio = rp2xxx.gpio; const uart = rp2xxx.uart.instance.num(0); @@ -19,6 +31,8 @@ pub const microzig_options = microzig.Options{ }, }; +/// Initialize UART logging, create a single FreeRTOS task, and start the scheduler. +/// The scheduler never returns — FreeRTOS takes over execution from this point. pub fn main() !void { uart_tx_pin.set_function(.uart); @@ -28,59 +42,25 @@ pub fn main() !void { rp2xxx.uart.init_logger(uart); - // Give it large stack because printing is demanding - _ = freertos_os.xTaskCreate(hello_task, "hello_task", freertos_os.MINIMAL_STACK_SIZE * 8, null, freertos_os.MAX_PRIORITIES - 1, null); - - freertos_os.vTaskStartScheduler(); + // Create a task using the new idiomatic API + _ = try freertos.task.create( + hello_task, + "hello_task", + freertos.config.minimal_stack_size * 8, + null, + freertos.config.max_priorities - 1, + ); + + // Start the scheduler (never returns) + freertos.task.start_scheduler(); } +/// FreeRTOS task entry point — logs a greeting with an incrementing counter +/// every 500ms. Runs forever; FreeRTOS preempts it as needed. pub fn hello_task(_: ?*anyopaque) callconv(.c) void { var i: u32 = 0; while (true) : (i += 1) { std.log.info("Hello from FreeRTOS task {}", .{i}); - freertos_os.vTaskDelay(500); + freertos.task.delay(500); } } - -export fn __unhandled_user_irq() callconv(.c) void { - std.log.err("Unhandled IRQ called!", .{}); - @panic("Unhandled IRQ"); -} - -/// -/// Some ugly glue code to implement required functions from FreeRTOS and Pico SDK -/// - This can be improved later -/// - Some or even all of these could be implemented in freertos module directly? -/// - Multicore not supported yet - multicore_reset_core1 have to be implemented -export fn panic_unsupported() callconv(.c) noreturn { - @panic("not supported"); -} - -export fn multicore_launch_core1(entry: *const fn () callconv(.c) void) callconv(.c) void { - microzig.hal.multicore.launch_core1(@ptrCast(entry)); -} - -export fn multicore_reset_core1() callconv(.c) void { - // TODO: please implement this in microzig.hal.multicore and call it here -} - -export fn multicore_doorbell_claim_unused(_: c_uint, _: bool) callconv(.c) c_int { - // TODO: please implement this in microzig.hal.multicore and call it here - return 0; -} - -export fn clock_get_hz(_: u32) callconv(.c) u32 { - std.log.info("clock_get_hz called", .{}); - // FIXME: this seems to return null - // return microzig.hal.clock_config.sys_freq.?; - return switch (microzig.hal.compatibility.chip) { - .RP2040 => 125_000_000, - .RP2350 => 150_000_000, - }; -} - -export fn spin_lock_claim(_: c_uint) callconv(.c) void {} - -export fn next_striped_spin_lock_num() callconv(.c) c_uint { - return 16; -} diff --git a/examples/raspberrypi/rp2xxx/src/freertos/multitask_demo.zig b/examples/raspberrypi/rp2xxx/src/freertos/multitask_demo.zig new file mode 100644 index 000000000..99130411b --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/freertos/multitask_demo.zig @@ -0,0 +1,202 @@ +//! FreeRTOS Multitask Demo — Sensor Dashboard +//! +//! Showcases queues, mutexes, timers, event groups, notifications, and +//! semaphores working together across 4 cooperating tasks: +//! +//! sensor_task — produces fake readings → queue + notification +//! logger_task — consumes the queue, prints data under mutex +//! watchdog_task — waits on event-group bit set by a periodic timer +//! stats_task — periodically reports task count and tick count +//! +//! A binary semaphore acts as a startup gate so every task begins in sync. + +const std = @import("std"); +const microzig = @import("microzig"); +const freertos = @import("freertos"); + +const rp2xxx = microzig.hal; +const gpio = rp2xxx.gpio; + +const uart = rp2xxx.uart.instance.num(0); +const uart_tx_pin = gpio.num(0); + +pub const microzig_options = microzig.Options{ + .log_level = .debug, + .logFn = rp2xxx.uart.log, + .cpu = .{ + .ram_vector_table = true, + }, +}; + +// ── Shared Primitives ─────────────────────────────────────────────────── + +/// Queue carrying sensor readings from sensor_task → logger_task. +var sensor_queue: freertos.queue.Queue(u32) = undefined; + +/// Mutex protecting serial output (shared by logger_task and stats_task). +var uart_mutex: freertos.mutex.Mutex = undefined; + +/// Event group: timer callback sets WATCHDOG_BIT, watchdog_task waits on it. +var watchdog_events: freertos.event_group.EventGroup = undefined; + +/// Binary semaphore used as a startup gate — all tasks block until released. +var startup_gate: freertos.semaphore.Semaphore = undefined; + +/// Handle to the logger task, used for direct-to-task notifications. +var logger_handle: freertos.config.TaskHandle = undefined; + +/// Periodic 3-second heartbeat timer. +var heartbeat_timer: freertos.timer.Timer = undefined; + +const WATCHDOG_BIT: freertos.config.EventBits = 0x01; + +// ── Main ──────────────────────────────────────────────────────────────── + +/// Initialize hardware, create all shared FreeRTOS primitives (queue, mutex, +/// event group, semaphore, timer), spawn the four tasks, release the startup +/// gate, and hand control to the FreeRTOS scheduler (which never returns). +pub fn main() !void { + // Hardware setup + uart_tx_pin.set_function(.uart); + uart.apply(.{ .clock_config = rp2xxx.clock_config }); + rp2xxx.uart.init_logger(uart); + + std.log.info("[main] Starting FreeRTOS multitask demo...", .{}); + + // Create shared primitives + sensor_queue = try freertos.queue.create(u32, 10); + uart_mutex = try freertos.mutex.create(); + watchdog_events = try freertos.event_group.create(); + startup_gate = try freertos.semaphore.create_binary(); + + // 3-second auto-reload timer that fires the heartbeat callback + heartbeat_timer = try freertos.timer.create( + "heartbeat", + 3000, + true, + null, + heartbeat_timer_callback, + ); + + // Create the four tasks (highest priority first) + _ = try freertos.task.create(sensor_task, "sensor", 2048, null, 3); + logger_handle = try freertos.task.create(logger_task, "logger", 2048, null, 2); + _ = try freertos.task.create(watchdog_task, "watchdog", 2048, null, 1); + _ = try freertos.task.create(stats_task, "stats", 2048, null, 1); + + std.log.info("[main] All tasks created, releasing startup gate", .{}); + + // Release the startup gate — tasks cascade through take/give + startup_gate.give() catch {}; + + // Hand control to FreeRTOS (never returns) + freertos.task.start_scheduler(); +} + +// ── Sensor Task ───────────────────────────────────────────────────────── +/// Simulates reading a sensor every 500 ms. Sends the reading to a queue +/// and pings logger_task via a lightweight notification. +fn sensor_task(_: ?*anyopaque) callconv(.c) void { + wait_for_startup_gate(); + + var reading: u32 = 0; + while (true) : (reading +%= 1) { + // Enqueue the reading for the logger + sensor_queue.send(&reading, freertos.config.max_delay) catch continue; + + // Lightweight signal — wake the logger + freertos.notification.give(logger_handle) catch {}; + + // Log the reading under mutex + uart_mutex.acquire(freertos.config.max_delay) catch {}; + std.log.info("[sensor] reading: {}", .{reading}); + uart_mutex.release() catch {}; + + freertos.task.delay(500); + } +} + +// ── Logger Task ───────────────────────────────────────────────────────── +/// Waits for a notification, then drains the sensor queue and prints each +/// reading under the UART mutex. +fn logger_task(_: ?*anyopaque) callconv(.c) void { + wait_for_startup_gate(); + + while (true) { + // Block until sensor_task sends a notification + _ = freertos.notification.take(true, freertos.config.max_delay) catch continue; + + // Drain every pending reading from the queue + while (true) { + const value = sensor_queue.receive(0) catch break; + uart_mutex.acquire(freertos.config.max_delay) catch {}; + std.log.info("[logger] logged sensor data: {}", .{value}); + uart_mutex.release() catch {}; + } + } +} + +// ── Watchdog Task ─────────────────────────────────────────────────────── +/// Blocks on an event-group bit that the heartbeat timer callback sets +/// every 3 seconds, then logs a heartbeat message. +fn watchdog_task(_: ?*anyopaque) callconv(.c) void { + wait_for_startup_gate(); + + // Arm the heartbeat timer now that the scheduler (and its timer queue) is running. + heartbeat_timer.start(0) catch |err| { + std.log.err("[watchdog] timer start failed: {}", .{err}); + }; + + var beat: u32 = 0; + while (true) { + // Wait for the timer to signal + _ = watchdog_events.wait_bits(WATCHDOG_BIT, .{ + .clear_on_exit = true, + .wait_for_all = false, + .timeout = freertos.config.max_delay, + }) catch continue; + + beat += 1; + + uart_mutex.acquire(freertos.config.max_delay) catch {}; + std.log.info("[watchdog] heartbeat #{} (tick: {})", .{ + beat, + freertos.task.get_tick_count(), + }); + uart_mutex.release() catch {}; + } +} + +// ── Stats Task ────────────────────────────────────────────────────────── +/// Every 5 seconds, reports the number of active tasks, uptime, and +/// current queue depth. +fn stats_task(_: ?*anyopaque) callconv(.c) void { + wait_for_startup_gate(); + + while (true) { + freertos.task.delay(5000); + + uart_mutex.acquire(freertos.config.max_delay) catch {}; + std.log.info("[stats] tasks: {}, uptime: {} ticks, queue depth: {}/10", .{ + freertos.task.get_count(), + freertos.task.get_tick_count(), + sensor_queue.messages_waiting(), + }); + uart_mutex.release() catch {}; + } +} + +// ── Timer Callback ────────────────────────────────────────────────────── +/// Runs in the timer-daemon context; sets the watchdog event bit so +/// watchdog_task unblocks. +fn heartbeat_timer_callback(_: freertos.config.TimerHandle) callconv(.c) void { + _ = watchdog_events.set_bits(WATCHDOG_BIT); +} + +// ── Startup Gate Helper ───────────────────────────────────────────────── +/// Every task calls this once at entry. The binary semaphore cascades: +/// each task takes → gives back, so the next one can proceed. +fn wait_for_startup_gate() void { + startup_gate.take(freertos.config.max_delay) catch return; + startup_gate.give() catch {}; +} diff --git a/examples/raspberrypi/rp2xxx/src/freertos/queue_demo.zig b/examples/raspberrypi/rp2xxx/src/freertos/queue_demo.zig new file mode 100644 index 000000000..1b5dae39d --- /dev/null +++ b/examples/raspberrypi/rp2xxx/src/freertos/queue_demo.zig @@ -0,0 +1,84 @@ +//! FreeRTOS Queue Demo — Producer/Consumer pattern. +//! +//! Demonstrates type-safe queue communication between two tasks. +//! A producer task sends incrementing numbers to a queue, +//! and a consumer task receives and logs them. + +const std = @import("std"); +const microzig = @import("microzig"); + +const freertos = @import("freertos"); + +const rp2xxx = microzig.hal; +const gpio = rp2xxx.gpio; + +const uart = rp2xxx.uart.instance.num(0); +const uart_tx_pin = gpio.num(0); + +pub const microzig_options = microzig.Options{ + .log_level = .debug, + .logFn = rp2xxx.uart.log, + .cpu = .{ + .ram_vector_table = true, + }, +}; + +/// The queue shared between producer and consumer. +var message_queue: freertos.queue.Queue(u32) = undefined; + +pub fn main() !void { + uart_tx_pin.set_function(.uart); + + uart.apply(.{ + .clock_config = rp2xxx.clock_config, + }); + + rp2xxx.uart.init_logger(uart); + + // Create a queue that holds up to 5 u32 values + message_queue = try freertos.queue.create(u32, 5); + + // Create producer and consumer tasks + _ = try freertos.task.create( + producer_task, + "producer", + freertos.config.minimal_stack_size * 4, + null, + 2, + ); + _ = try freertos.task.create( + consumer_task, + "consumer", + freertos.config.minimal_stack_size * 4, + null, + 1, + ); + + freertos.task.start_scheduler(); +} + +/// Sends an incrementing counter to the shared queue every 1000ms. +/// Blocks indefinitely if the queue is full (`max_delay`). +fn producer_task(_: ?*anyopaque) callconv(.c) void { + var counter: u32 = 0; + while (true) : (counter +%= 1) { + message_queue.send(&counter, freertos.config.max_delay) catch { + std.log.err("Failed to send to queue", .{}); + continue; + }; + std.log.info("[producer] sent: {}", .{counter}); + freertos.task.delay(1000); + } +} + +/// Blocks waiting for items on the shared queue and logs each received value. +/// Runs at lower priority than the producer, so it processes items after they are sent. +fn consumer_task(_: ?*anyopaque) callconv(.c) void { + while (true) { + const value = message_queue.receive(freertos.config.max_delay) catch { + std.log.err("Failed to receive from queue", .{}); + continue; + }; + std.log.info("[consumer] received: {}", .{value}); + } +} diff --git a/modules/freertos/README.md b/modules/freertos/README.md new file mode 100644 index 000000000..f94ddb436 --- /dev/null +++ b/modules/freertos/README.md @@ -0,0 +1,328 @@ +# FreeRTOS Module for MicroZig + +Idiomatic Zig wrappers for the [FreeRTOS](https://www.freertos.org/) real-time operating system kernel, integrated into the [MicroZig](https://github.com/ZigEmbeddedGroup/microzig) embedded framework. Provides type-safe tasks, queues, semaphores, mutexes, timers, event groups, and task notifications — all with Zig error handling instead of raw C return codes. + +Hardware-tested on **Seeed XIAO RP2350** (ARM Cortex-M33). + +## Features + +- ✅ **Tasks** — create, delete, suspend, resume, delay, priority management +- ✅ **Queues** — generic `Queue(T)` with compile-time type safety +- ✅ **Semaphores** — binary and counting +- ✅ **Mutexes** — standard (with priority inheritance) and recursive +- ✅ **Software Timers** — auto-reload and one-shot with callback support +- ✅ **Event Groups** — multi-bit synchronization and rendezvous/barrier patterns +- ✅ **Task Notifications** — lightweight direct-to-task signaling +- ✅ **ISR variants** — all primitives provide `*FromIsr` functions with wake flags +- ✅ **Raw C escape hatch** — `freertos.c.*` for anything not yet wrapped + +## Quick Start + +```zig +const std = @import("std"); +const freertos = @import("freertos"); + +pub fn main() !void { + // ... hardware init ... + + _ = try freertos.task.create( + hello_task, + "hello", + freertos.config.minimal_stack_size * 8, + null, + freertos.config.max_priorities - 1, + ); + + // Start the scheduler — never returns + freertos.task.startScheduler(); +} + +fn hello_task(_: ?*anyopaque) callconv(.c) void { + var i: u32 = 0; + while (true) : (i += 1) { + std.log.info("Hello from FreeRTOS task {}", .{i}); + freertos.task.delay(500); // 500 ms at default 1 kHz tick rate + } +} +``` + +## API Reference + +### Tasks (`freertos.task`) + +Task creation, deletion, scheduling, and runtime queries. + +| Function | Description | +|---|---| +| `create(function, name, stack_depth, params, priority)` | Create a task → `Error!TaskHandle` | +| `destroy(handle)` | Delete a task (`null` = self) | +| `delay(ticks)` | Block for `ticks` ticks | +| `delayUntil(prev_wake_time, increment)` | Periodic delay → `bool` | +| `suspend(handle)` | Suspend a task (`null` = self) | +| `resume(handle)` | Resume a suspended task | +| `resumeFromIsr(handle)` | Resume from ISR → `bool` (context switch needed) | +| `abortDelay(handle)` | Force-unblock a task → `bool` | +| `setPriority(handle, priority)` | Change a task's priority | +| `getPriority(handle)` | Get current priority | +| `getCurrentHandle()` | Handle of the running task | +| `getHandle(name)` | Look up task by name → `?TaskHandle` | +| `getName(handle)` | Task name string | +| `getState(handle)` | → `.running`, `.ready`, `.blocked`, `.suspended`, `.deleted` | +| `getStackHighWaterMark(handle)` | Minimum free stack (words) since creation | +| `getTickCount()` | Current system tick | +| `getCount()` | Total number of tasks | +| `startScheduler()` | Start FreeRTOS — **never returns** | +| `endScheduler()` | Stop FreeRTOS | +| `getSchedulerState()` | → `.not_started`, `.running`, `.suspended` | +| `suspendAll()` / `resumeAll()` | Disable/enable task switching | + +### Queues (`freertos.queue`) + +Type-safe FIFO queues for inter-task communication. `Queue(T)` is a generic — the item type is checked at compile time and `receive()` returns `T` directly. + +```zig +var q = try freertos.queue.create(u32, 10); // Queue(u32), capacity 10 +defer q.destroy(); + +try q.send(&@as(u32, 42), freertos.config.max_delay); +const value = try q.receive(freertos.config.max_delay); // value: u32 +``` + +| Function | Description | +|---|---| +| `create(T, length)` | Create a `Queue(T)` with given capacity → `Error!Queue(T)` | +| `q.send(item, timeout)` | Send to back → `Error!void` (`QueueFull` on timeout) | +| `q.sendToFront(item, timeout)` | Send to front | +| `q.overwrite(item)` | Overwrite single-item queue (never blocks) | +| `q.receive(timeout)` | Receive and remove → `Error!T` (`QueueEmpty` on timeout) | +| `q.peek(timeout)` | Read front item without removing | +| `q.sendFromIsr(item)` | ISR send → `IsrResult` | +| `q.receiveFromIsr()` | ISR receive → `?IsrReceiveResult(T)` | +| `q.messagesWaiting()` | Number of items in queue | +| `q.spacesAvailable()` | Free slots remaining | +| `q.reset()` | Flush the queue | + +### Semaphores (`freertos.semaphore`) + +Binary and counting semaphores for synchronization and signaling between tasks/ISRs. + +| Function | Description | +|---|---| +| `createBinary()` | Create binary semaphore (starts empty) → `Error!Semaphore` | +| `createCounting(max, initial)` | Create counting semaphore → `Error!Semaphore` | +| `s.take(timeout)` | Acquire → `Error!void` (`Timeout`) | +| `s.give()` | Release → `Error!void` (`Failure` if not taken) | +| `s.giveFromIsr()` | Release from ISR → `IsrResult` | +| `s.takeFromIsr()` | Acquire from ISR → `IsrResult` | +| `s.getCount()` | Current count (or 1/0 for binary) | +| `s.destroy()` | Free the semaphore | + +### Mutexes (`freertos.mutex`) + +Standard and recursive mutexes with **priority inheritance** to prevent priority inversion. Use mutexes to protect shared resources; use semaphores for signaling. + +```zig +var mtx = try freertos.mutex.create(); +defer mtx.destroy(); + +try mtx.acquire(freertos.config.max_delay); +defer mtx.release() catch {}; + +// ... access shared resource ... +``` + +| Function | Description | +|---|---| +| `create()` | Create a standard mutex → `Error!Mutex` | +| `createRecursive()` | Create a recursive mutex → `Error!Recursive` | +| `m.acquire(timeout)` | Lock → `Error!void` (`Timeout`) | +| `m.release()` | Unlock → `Error!void` (`Failure` if not held) | +| `m.getHolder()` | Task holding the mutex → `?TaskHandle` | +| `m.destroy()` | Free the mutex | + +Recursive mutexes can be acquired multiple times by the same task — each `acquire` must be paired with a `release`. + +### Event Groups (`freertos.event_group`) + +Multi-bit synchronization primitives. Tasks can wait on any combination of event bits, enabling rendezvous-style coordination. + +```zig +var events = try freertos.event_group.create(); +defer events.destroy(); + +// Task A sets bit 0 +_ = events.setBits(0x01); + +// Task B waits for bits 0 AND 1 +const bits = try events.waitBits(0x03, .{ .wait_for_all = true, .timeout = 1000 }); +``` + +| Function | Description | +|---|---| +| `create()` | Create an event group → `Error!EventGroup` | +| `e.setBits(bits)` | Set bits → returns new value | +| `e.clearBits(bits)` | Clear bits → returns previous value | +| `e.getBits()` | Read current bits | +| `e.waitBits(bits, opts)` | Wait for bit pattern → `Error!EventBits` (`Timeout`) | +| `e.sync(set, wait, timeout)` | Barrier: set bits then wait for others → `Error!EventBits` | +| `e.setBitsFromIsr(bits)` | Set bits from ISR → `IsrResult` | +| `e.destroy()` | Free the event group | + +`WaitOptions` fields: `clear_on_exit` (default `true`), `wait_for_all` (default `false`), `timeout` (default `max_delay`). + +### Software Timers (`freertos.timer`) + +Periodic or one-shot timers that execute a callback in the timer daemon task context. + +```zig +var tmr = try freertos.timer.create("heartbeat", 1000, true, null, my_callback); +try tmr.start(0); +defer tmr.destroyBlocking(); +``` + +| Function | Description | +|---|---| +| `create(name, period, auto_reload, id, callback)` | Create a timer → `Error!Timer` | +| `t.start(cmd_timeout)` | Start/restart the timer | +| `t.stop(cmd_timeout)` | Stop the timer | +| `t.reset(cmd_timeout)` | Reset countdown from now | +| `t.changePeriod(new_period, cmd_timeout)` | Change period and restart | +| `t.destroy(cmd_timeout)` | Delete timer (can fail if queue full) | +| `t.destroyBlocking()` | Delete timer, wait forever (safe for `defer`) | +| `t.isActive()` | Check if running → `bool` | +| `t.getName()` / `t.getPeriod()` / `t.getExpiryTime()` | Timer properties | +| `t.getId()` / `t.setId(ptr)` | User-defined ID pointer | +| `t.getAutoReload()` / `t.setAutoReload(bool)` | Auto-reload mode | +| `t.startFromIsr()` / `t.stopFromIsr()` / `t.resetFromIsr()` | ISR variants → `IsrResult` | +| `pendFunctionCall(fn, p1, p2, timeout)` | Defer a function call to the timer daemon | + +> ⚠️ **Implementation note:** The C macros `xTimerStart`, `xTimerStop`, etc. pass untyped `NULL` that Zig's `@cImport` can't coerce. The wrapper calls `xTimerGenericCommandFromTask` / `FromISR` directly, which is functionally identical. + +### Task Notifications (`freertos.notification`) + +Lightweight direct-to-task notifications — faster and smaller than semaphores or event groups. Each task has a 32-bit notification value per index. + +```zig +// Lightweight binary semaphore pattern: +freertos.notification.give(task_handle) catch {}; // sender +_ = freertos.notification.take(true, freertos.config.max_delay) catch {}; // receiver +``` + +| Function | Description | +|---|---| +| `notify(handle, value, action)` | Send notification at index 0 | +| `notifyIndexed(handle, index, value, action)` | Send at specific index | +| `notifyAndQuery(handle, value, action)` | Send and get previous value → `Error!u32` | +| `give(handle)` | Increment notification (lightweight semaphore give) | +| `notifyFromIsr(handle, value, action)` | Send from ISR → `IsrResult` | +| `giveFromIsr(handle)` | Increment from ISR → `IsrResult` | +| `wait(clear_entry, clear_exit, timeout)` | Wait for notification → `Error!u32` | +| `waitIndexed(index, clear_entry, clear_exit, timeout)` | Wait at specific index | +| `take(clear_on_exit, timeout)` | Binary/counting semaphore pattern → `Error!u32` | +| `clearState(handle)` | Clear pending state → `bool` | +| `clearBits(handle, bits)` | Clear bits in notification value → previous value | + +`Action` enum: `.none`, `.set_bits`, `.increment`, `.set_value_overwrite`, `.set_value_no_overwrite` + +### Error Handling + +All fallible operations return `freertos.config.Error`: + +```zig +pub const Error = error{ + OutOfMemory, // Heap exhausted (create functions) + Timeout, // Operation timed out + QueueFull, // Queue send failed + QueueEmpty, // Queue receive failed + Failure, // Generic pdFAIL +}; +``` + +Two helper functions convert C return codes to Zig errors: + +```zig +// Check pdPASS/pdFAIL return codes +try freertos.config.checkBaseType(rc); + +// Convert nullable C handle → Zig error (null → error.OutOfMemory) +const handle = try freertos.config.checkHandle(c.TaskHandle_t, raw_handle); +``` + +### Raw C Access + +For anything not wrapped, use `freertos.c` to access the full FreeRTOS C API directly: + +```zig +const freertos = @import("freertos"); + +// Direct C call +freertos.c.vTaskDelay(100); + +// Access C constants +const pass = freertos.c.pdPASS; +``` + +## Examples + +| Example | Description | +|---|---| +| [`hello_task.zig`](../../examples/raspberrypi/rp2xxx/src/freertos/hello_task.zig) | Minimal — one task, UART logging, periodic delay | +| [`queue_demo.zig`](../../examples/raspberrypi/rp2xxx/src/freertos/queue_demo.zig) | Producer/consumer pattern with type-safe `Queue(u32)` | +| [`multitask_demo.zig`](../../examples/raspberrypi/rp2xxx/src/freertos/multitask_demo.zig) | 4 cooperating tasks using queues, mutexes, timers, event groups, notifications, and semaphores | + +## Platform Support + +| Platform | Chip | Status | Notes | +|---|---|---|---| +| RP2040 (Pico) | ARM Cortex-M0+ | ✅ Builds | Not yet hardware-tested | +| RP2350 ARM (Pico 2) | ARM Cortex-M33 | ✅ Tested on hardware | XIAO RP2350 | +| RP2350 RISC-V | Hazard3 | ❌ Blocked | Linker symbol issues with RISC-V port | +| ESP32 | Xtensa/RISC-V | 🔲 Planned | — | +| STM32 | ARM Cortex-M | 🔲 Planned | — | + +## Configuration + +Each platform has its own `FreeRTOSConfig.h` under `config//`: + +``` +modules/freertos/config/ +├── RP2040/FreeRTOSConfig.h +└── RP2350_ARM/FreeRTOSConfig.h +``` + +### Key settings (defaults) + +| Setting | Value | Notes | +|---|---|---| +| `configTICK_RATE_HZ` | 1000 | 1 ms tick resolution | +| `configMAX_PRIORITIES` | 32 | Priority levels 0–31 | +| `configMINIMAL_STACK_SIZE` | 256 | Words (1 KB on 32-bit) | +| `configTOTAL_HEAP_SIZE` | 128 KB | FreeRTOS heap (heap_4) | +| `configCHECK_FOR_STACK_OVERFLOW` | 2 | Full stack painting check — traps via `@trap()` | +| `configSUPPORT_DYNAMIC_ALLOCATION` | 1 | Dynamic only (static not yet supported) | +| `configNUMBER_OF_CORES` | 1 | Single-core only | + +### Pico SDK interop (disabled) + +`configSUPPORT_PICO_SYNC_INTEROP` and `configSUPPORT_PICO_TIME_INTEROP` are **disabled**. The FreeRTOS RP2xxx port relies on `.init_array` constructors to initialize spin-locks and event groups before `main()`. MicroZig does not process `.init_array`, so enabling these causes a NULL dereference and Usage Fault on scheduler start. Re-enable only after MicroZig adds `.init_array` support. + +## Known Limitations + +- **Static allocation** — `configSUPPORT_STATIC_ALLOCATION` is off; `xTaskCreateStatic` etc. are not wrapped +- **Stream/message buffers** — not yet wrapped (use `freertos.c` as a workaround) +- **Multicore SMP** — `configNUMBER_OF_CORES` is 1; FreeRTOS SMP support is not yet integrated +- **`.init_array` constructors** — not processed by MicroZig startup, blocking Pico SDK interop +- **RISC-V** — RP2350 RISC-V port has unresolved linker symbols + +## Contributing + +FreeRTOS integration is tracked in [**issue #880**](https://github.com/ZigEmbeddedGroup/microzig/issues/880). + +Areas that need work: + +- Static allocation variants (`*CreateStatic`) +- Stream and message buffer wrappers +- Multicore SMP support +- `.init_array` processing in MicroZig startup +- Additional platform ports (ESP32, STM32) +- RISC-V port linker fixes diff --git a/modules/freertos/build.zig b/modules/freertos/build.zig index 959e95722..acf03f589 100644 --- a/modules/freertos/build.zig +++ b/modules/freertos/build.zig @@ -69,12 +69,11 @@ pub fn build(b: *std.Build) void { freertos_lib.addIncludePath(freertos_kernel_community_dep.path("./GCC/RP2350_ARM_NTZ/non_secure/")); } - // Add some Pico SDK glue code - // TODO: maybe we should use Pico source files directly? - // but current way - we know what is internaly used by FreeRTOS port + // Pico SDK glue code: partial re-implementations of SDK functions that + // the FreeRTOS port references but MicroZig does not provide. freertos_lib.addCSourceFiles(.{ .root = b.path("."), - .files = &[_][]const u8{ "src/picosdk_irq.c", "src/picosdk_exception.c" }, + .files = &[_][]const u8{ "src/picosdk_irq.c", "src/picosdk_exception.c", "src/picosdk_stubs.c" }, .flags = &flags, }); diff --git a/modules/freertos/config/RP2040/FreeRTOSConfig.h b/modules/freertos/config/RP2040/FreeRTOSConfig.h index a9a2ae710..4bc05c76a 100644 --- a/modules/freertos/config/RP2040/FreeRTOSConfig.h +++ b/modules/freertos/config/RP2040/FreeRTOSConfig.h @@ -75,7 +75,7 @@ #define configAPPLICATION_ALLOCATED_HEAP 0 /* Hook function related definitions. */ - #define configCHECK_FOR_STACK_OVERFLOW 0 + #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 0 #define configUSE_DAEMON_TASK_STARTUP_HOOK 0 @@ -108,9 +108,15 @@ #define configUSE_CORE_AFFINITY 0 #define configUSE_PASSIVE_IDLE_HOOK 0 - /* RP2040 specific */ - #define configSUPPORT_PICO_SYNC_INTEROP 1 - #define configSUPPORT_PICO_TIME_INTEROP 1 + /* PicoSDK interop — DISABLED because MicroZig does not run .init_array + * constructors. The FreeRTOS RP2040 port relies on a + * __attribute__((constructor)) function (prvRuntimeInitializer in port.c) to + * initialise pxCrossCoreSpinLock and an internal xEventGroup *before* main(). + * Without that, the doorbell ISR dereferences a NULL spin-lock pointer and + * triggers a Usage Fault immediately after the scheduler starts. + * Re-enable these only after adding .init_array support to MicroZig startup. */ + #define configSUPPORT_PICO_SYNC_INTEROP 0 + #define configSUPPORT_PICO_TIME_INTEROP 0 #include /* Define to trap errors during development. */ diff --git a/modules/freertos/config/RP2350_ARM/FreeRTOSConfig.h b/modules/freertos/config/RP2350_ARM/FreeRTOSConfig.h index 2d8b4563e..2ffdd90fd 100644 --- a/modules/freertos/config/RP2350_ARM/FreeRTOSConfig.h +++ b/modules/freertos/config/RP2350_ARM/FreeRTOSConfig.h @@ -75,7 +75,7 @@ #define configAPPLICATION_ALLOCATED_HEAP 0 /* Hook function related definitions. */ - #define configCHECK_FOR_STACK_OVERFLOW 0 + #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 0 #define configUSE_DAEMON_TASK_STARTUP_HOOK 0 @@ -108,9 +108,15 @@ #define configUSE_CORE_AFFINITY 0 #define configUSE_PASSIVE_IDLE_HOOK 0 - /* PicoSDK specific */ - #define configSUPPORT_PICO_SYNC_INTEROP 1 - #define configSUPPORT_PICO_TIME_INTEROP 1 + /* PicoSDK interop — DISABLED because MicroZig does not run .init_array + * constructors. The FreeRTOS RP2350 (and RP2040) port relies on a + * __attribute__((constructor)) function (prvRuntimeInitializer in port.c) to + * initialise pxCrossCoreSpinLock and an internal xEventGroup *before* main(). + * Without that, the doorbell ISR dereferences a NULL spin-lock pointer and + * triggers a Usage Fault immediately after the scheduler starts. + * Re-enable these only after adding .init_array support to MicroZig startup. */ + #define configSUPPORT_PICO_SYNC_INTEROP 0 + #define configSUPPORT_PICO_TIME_INTEROP 0 /* RP2350 required */ #define configENABLE_FPU 1 diff --git a/modules/freertos/src/config.zig b/modules/freertos/src/config.zig new file mode 100644 index 000000000..a14d916d8 --- /dev/null +++ b/modules/freertos/src/config.zig @@ -0,0 +1,91 @@ +//! FreeRTOS configuration types, error handling, and shared constants. +//! +//! This module provides idiomatic Zig types that map to FreeRTOS C types, +//! following the error-handling pattern established by `modules/network/`. + +const c = @import("root.zig").c; + +// ── Type Aliases ──────────────────────────────────────────────────────── + +/// Tick count type used for delays and timeouts. +pub const TickType = c.TickType_t; + +/// Base signed integer type (used for pdTRUE/pdFALSE). +pub const BaseType = c.BaseType_t; + +/// Base unsigned integer type. +pub const U_BaseType = c.UBaseType_t; + +/// Opaque task handle. +pub const TaskHandle = c.TaskHandle_t; + +/// Opaque queue handle. +pub const QueueHandle = c.QueueHandle_t; + +/// Opaque semaphore handle (alias for QueueHandle in FreeRTOS). +pub const SemaphoreHandle = c.SemaphoreHandle_t; + +/// Opaque event group handle. +pub const EventGroupHandle = c.EventGroupHandle_t; + +/// Event bits type. +pub const EventBits = c.EventBits_t; + +/// Opaque timer handle. +pub const TimerHandle = c.TimerHandle_t; + +/// Task function signature: `fn(?*anyopaque) callconv(.c) void`. +pub const TaskFunction = c.TaskFunction_t; + +/// Timer callback signature. +pub const TimerCallbackFunction = c.TimerCallbackFunction_t; + +/// Stack depth type. +pub const StackDepthType = c.configSTACK_DEPTH_TYPE; + +// ── Constants ─────────────────────────────────────────────────────────── + +/// Maximum delay value (wait forever). +pub const max_delay: TickType = c.portMAX_DELAY; + +/// Minimum stack size for a task (in words, from FreeRTOSConfig.h). +pub const minimal_stack_size: u32 = c.configMINIMAL_STACK_SIZE; + +/// Maximum number of task priorities. +pub const max_priorities: u32 = c.configMAX_PRIORITIES; + +/// Tick rate in Hz. +pub const tick_rate_hz: u32 = c.configTICK_RATE_HZ; + +// ── Error Handling ────────────────────────────────────────────────────── + +/// Errors that FreeRTOS operations can return. +pub const Error = error{ + /// Memory allocation failed (heap exhausted). + OutOfMemory, + /// Operation timed out before completing. + Timeout, + /// Queue/semaphore is full — cannot send. + QueueFull, + /// Queue is empty — cannot receive. + QueueEmpty, + /// Generic FreeRTOS failure (pdFAIL). + Failure, +}; + +/// Convert a FreeRTOS BaseType_t return code to a Zig error. +/// pdPASS (1) returns void; pdFAIL (0) returns `error.Failure`. +pub fn check_base_type(rc: BaseType) Error!void { + if (rc != c.pdPASS) return error.Failure; +} + +/// Check a boolean-style return where pdTRUE means success. +pub fn check_true(rc: BaseType) bool { + return rc == c.pdTRUE; +} + +/// Convert a nullable C handle to a Zig optional, returning OutOfMemory if null. +pub fn check_handle(comptime T: type, handle: T) Error!T { + if (handle == null) return error.OutOfMemory; + return handle; +} diff --git a/modules/freertos/src/event_group.zig b/modules/freertos/src/event_group.zig new file mode 100644 index 000000000..eecd597e8 --- /dev/null +++ b/modules/freertos/src/event_group.zig @@ -0,0 +1,131 @@ +//! FreeRTOS event groups — multi-bit synchronization primitives. +//! +//! Event groups allow tasks to wait on combinations of event bits, +//! enabling rendezvous-style synchronization between multiple tasks. +//! +//! ## Example +//! ```zig +//! var events = try freertos.event_group.create(); +//! defer events.destroy(); +//! +//! // Task A sets bit 0: +//! _ = events.set_bits(0x01); +//! +//! // Task B waits for bits 0 and 1: +//! const bits = try events.wait_bits(0x03, .{ .wait_for_all = true, .timeout = 1000 }); +//! ``` + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const EventBits = config.EventBits; +const Error = config.Error; + +/// Options for `wait_bits`. +pub const WaitOptions = struct { + /// Clear the waited-for bits on exit (before returning). + clear_on_exit: bool = true, + /// Wait for ALL bits (true) or ANY bit (false). + wait_for_all: bool = false, + /// Maximum ticks to wait. Defaults to wait forever. + timeout: TickType = config.max_delay, +}; + +/// Result from an ISR set-bits operation. +pub const ISR_Result = struct { + /// Whether the operation succeeded (command posted to timer daemon queue). + success: bool, + /// Whether a higher-priority task was woken and a context switch is needed. + higher_priority_task_woken: bool, +}; + +/// A FreeRTOS event group. +pub const EventGroup = struct { + /// Underlying FreeRTOS C handle. Use `freertos.c` for direct C API access. + handle: c.EventGroupHandle_t, + + /// Delete the event group and free its memory. + pub fn destroy(self: EventGroup) void { + c.vEventGroupDelete(self.handle); + } + + /// Set one or more event bits. + /// Returns the event group value after the bits were set. + pub fn set_bits(self: EventGroup, bits: EventBits) EventBits { + return c.xEventGroupSetBits(self.handle, bits); + } + + /// Clear one or more event bits. + /// Returns the event group value *before* the bits were cleared. + pub fn clear_bits(self: EventGroup, bits: EventBits) EventBits { + return c.xEventGroupClearBits(self.handle, bits); + } + + /// Get the current event bit values. + pub fn get_bits(self: EventGroup) EventBits { + // xEventGroupGetBits is a macro for xEventGroupClearBits(h, 0) + return c.xEventGroupClearBits(self.handle, 0); + } + + /// Block until the specified bits are set, with configurable options. + /// + /// Returns the event group value at the time the bits were set (or + /// at timeout). Returns `error.Timeout` if the bits were not set + /// within the timeout period. + pub fn wait_bits(self: EventGroup, bits_to_wait: EventBits, opts: WaitOptions) Error!EventBits { + const result = c.xEventGroupWaitBits( + self.handle, + bits_to_wait, + if (opts.clear_on_exit) c.pdTRUE else c.pdFALSE, + if (opts.wait_for_all) c.pdTRUE else c.pdFALSE, + opts.timeout, + ); + // If the function returned due to timeout, the awaited bits won't all be set. + if (opts.wait_for_all) { + if (result & bits_to_wait != bits_to_wait) return error.Timeout; + } else { + if (result & bits_to_wait == 0) return error.Timeout; + } + return result; + } + + /// Atomically set bits and then wait for a different set of bits. + /// Used for rendezvous / barrier synchronization. + /// + /// Returns `error.Timeout` if the wait-for bits were not set in time. + pub fn sync( + self: EventGroup, + bits_to_set: EventBits, + bits_to_wait: EventBits, + timeout: TickType, + ) Error!EventBits { + const result = c.xEventGroupSync(self.handle, bits_to_set, bits_to_wait, timeout); + if (result & bits_to_wait != bits_to_wait) return error.Timeout; + return result; + } + + /// Set bits from an ISR context (deferred to the timer daemon task). + pub fn set_bits_from_isr(self: EventGroup, bits: EventBits) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xEventGroupSetBitsFromISR(self.handle, bits, &woken); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Get current event bits from an ISR context. + pub fn get_bits_from_isr(self: EventGroup) EventBits { + return c.xEventGroupGetBitsFromISR(self.handle); + } +}; + +// ── Creation ──────────────────────────────────────────────────────────── + +/// Create a new event group. +/// Returns `error.OutOfMemory` if the kernel heap is exhausted. +pub fn create() Error!EventGroup { + const handle = c.xEventGroupCreate(); + return .{ .handle = try config.check_handle(c.EventGroupHandle_t, handle) }; +} diff --git a/modules/freertos/src/mutex.zig b/modules/freertos/src/mutex.zig new file mode 100644 index 000000000..2ff8b99d7 --- /dev/null +++ b/modules/freertos/src/mutex.zig @@ -0,0 +1,100 @@ +//! FreeRTOS mutex wrappers — standard and recursive mutexes. +//! +//! Mutexes provide mutual exclusion with priority inheritance, preventing +//! priority inversion. Use mutexes to protect shared resources; use +//! semaphores for signaling/synchronization. +//! +//! ## Example +//! ```zig +//! var mtx = try freertos.mutex.create(); +//! defer mtx.destroy(); +//! +//! try mtx.acquire(freertos.config.max_delay); +//! defer mtx.release() catch {}; +//! +//! // ... access shared resource ... +//! ``` + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const Error = config.Error; +const TaskHandle = config.TaskHandle; + +/// A FreeRTOS mutex with priority inheritance. +pub const Mutex = struct { + /// Underlying FreeRTOS C handle. Use `freertos.c` for direct C API access. + handle: c.SemaphoreHandle_t, + + /// Delete the mutex and free its memory. + pub fn destroy(self: Mutex) void { + c.vSemaphoreDelete(self.handle); + } + + /// Acquire (lock) the mutex, blocking up to `timeout` ticks. + /// Returns `error.Timeout` if the mutex could not be acquired. + pub fn acquire(self: Mutex, timeout: TickType) Error!void { + const rc = c.xSemaphoreTake(self.handle, timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Release (unlock) the mutex. + /// Returns `error.Failure` if the calling task does not hold the mutex. + pub fn release(self: Mutex) Error!void { + const rc = c.xSemaphoreGive(self.handle); + if (rc != c.pdPASS) return error.Failure; + } + + /// Get the handle of the task that currently holds this mutex. + /// Returns `null` if the mutex is not held. + pub fn get_holder(self: Mutex) ?TaskHandle { + return c.xSemaphoreGetMutexHolder(self.handle); + } + + /// Get the mutex holder from an ISR context. + pub fn get_holder_from_isr(self: Mutex) ?TaskHandle { + return c.xSemaphoreGetMutexHolderFromISR(self.handle); + } +}; + +/// A FreeRTOS recursive mutex — can be acquired multiple times by the same task. +pub const Recursive = struct { + /// Underlying FreeRTOS C handle. Use `freertos.c` for direct C API access. + handle: c.SemaphoreHandle_t, + + /// Delete the recursive mutex and free its memory. + pub fn destroy(self: Recursive) void { + c.vSemaphoreDelete(self.handle); + } + + /// Acquire the recursive mutex, blocking up to `timeout` ticks. + /// The same task can acquire it multiple times; each acquire must be + /// matched by a corresponding release. + pub fn acquire(self: Recursive, timeout: TickType) Error!void { + const rc = c.xSemaphoreTakeRecursive(self.handle, timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Release the recursive mutex (one level). + pub fn release(self: Recursive) Error!void { + const rc = c.xSemaphoreGiveRecursive(self.handle); + if (rc != c.pdPASS) return error.Failure; + } +}; + +// ── Creation Functions ────────────────────────────────────────────────── + +/// Create a standard mutex with priority inheritance. +/// Returns `error.OutOfMemory` if the kernel heap is exhausted. +pub fn create() Error!Mutex { + const handle = c.xSemaphoreCreateMutex(); + return .{ .handle = try config.check_handle(c.SemaphoreHandle_t, handle) }; +} + +/// Create a recursive mutex. +/// Returns `error.OutOfMemory` if the kernel heap is exhausted. +pub fn create_recursive() Error!Recursive { + const handle = c.xSemaphoreCreateRecursiveMutex(); + return .{ .handle = try config.check_handle(c.SemaphoreHandle_t, handle) }; +} diff --git a/modules/freertos/src/notification.zig b/modules/freertos/src/notification.zig new file mode 100644 index 000000000..0315db84d --- /dev/null +++ b/modules/freertos/src/notification.zig @@ -0,0 +1,191 @@ +//! FreeRTOS direct-to-task notification wrappers. +//! +//! Task notifications are a lightweight, high-performance alternative to +//! binary semaphores, counting semaphores, event groups, and mailboxes. +//! Each task has a small array of 32-bit notification values (one per index). +//! +//! ## Example +//! ```zig +//! // Lightweight binary semaphore pattern (index 0): +//! try freertos.notification.give(task_handle); +//! +//! // In the receiving task: +//! _ = try freertos.notification.take(true, freertos.config.max_delay); +//! ``` + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const TaskHandle = config.TaskHandle; +const Error = config.Error; + +/// Notification action — determines how the notification value is updated. +pub const Action = enum(u32) { + /// Don't update the value, just send the notification. + none = c.eNoAction, + /// Bitwise-OR `value` into the notification value. + set_bits = c.eSetBits, + /// Increment the notification value (ignores `value` parameter). + increment = c.eIncrement, + /// Set the notification value, overwriting any unread value. + set_value_overwrite = c.eSetValueWithOverwrite, + /// Set the notification value only if the previous was read. + set_value_no_overwrite = c.eSetValueWithoutOverwrite, +}; + +/// Result from an ISR notification operation. +pub const ISR_Result = struct { + /// Whether the notification was successfully sent. + success: bool, + /// Whether a higher-priority task was woken and a context switch is needed. + higher_priority_task_woken: bool, +}; + +// ── Send Notifications ────────────────────────────────────────────────── + +/// Send a notification to a task at the default index (0). +/// +/// Returns `error.Failure` only when `action` is `.set_value_no_overwrite` +/// and the target task had an unread notification. +pub fn notify(task_handle: TaskHandle, value: u32, action: Action) Error!void { + const rc = c.xTaskGenericNotify( + task_handle, + 0, // index + value, + @intFromEnum(action), + null, // don't care about previous value + ); + if (rc != c.pdPASS) return error.Failure; +} + +/// Send a notification at a specific index. +pub fn notify_indexed(task_handle: TaskHandle, index: u32, value: u32, action: Action) Error!void { + const rc = c.xTaskGenericNotify( + task_handle, + @intCast(index), + value, + @intFromEnum(action), + null, + ); + if (rc != c.pdPASS) return error.Failure; +} + +/// Send a notification and retrieve the previous notification value. +pub fn notify_and_query(task_handle: TaskHandle, value: u32, action: Action) Error!u32 { + var previous: u32 = 0; + const rc = c.xTaskGenericNotify( + task_handle, + 0, + value, + @intFromEnum(action), + &previous, + ); + if (rc != c.pdPASS) return error.Failure; + return previous; +} + +/// Increment the notification value at index 0 (lightweight "give" semaphore pattern). +pub fn give(task_handle: TaskHandle) Error!void { + return notify(task_handle, 0, .increment); +} + +// ── ISR Send ──────────────────────────────────────────────────────────── + +/// Send a notification from an ISR at the default index (0). +pub fn notify_from_isr(task_handle: TaskHandle, value: u32, action: Action) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xTaskGenericNotifyFromISR( + task_handle, + 0, + value, + @intFromEnum(action), + null, + &woken, + ); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; +} + +/// "Give" from ISR — increment the notification value at index 0. +pub fn give_from_isr(task_handle: TaskHandle) ISR_Result { + return notify_from_isr(task_handle, 0, .increment); +} + +// ── Wait / Receive ────────────────────────────────────────────────────── + +/// Wait for a notification at the default index (0). +/// +/// - `bits_clear_on_entry`: Bits to clear in the notification value on entry. +/// - `bits_clear_on_exit`: Bits to clear after reading the value. +/// - `timeout`: Maximum ticks to wait. +/// +/// Returns the notification value, or `error.Timeout` if timed out. +pub fn wait(bits_clear_on_entry: u32, bits_clear_on_exit: u32, timeout: TickType) Error!u32 { + var value: u32 = 0; + const rc = c.xTaskGenericNotifyWait( + 0, // index + bits_clear_on_entry, + bits_clear_on_exit, + &value, + timeout, + ); + if (rc != c.pdPASS) return error.Timeout; + return value; +} + +/// Wait for a notification at a specific index. +pub fn wait_indexed(index: u32, bits_clear_on_entry: u32, bits_clear_on_exit: u32, timeout: TickType) Error!u32 { + var value: u32 = 0; + const rc = c.xTaskGenericNotifyWait( + @intCast(index), + bits_clear_on_entry, + bits_clear_on_exit, + &value, + timeout, + ); + if (rc != c.pdPASS) return error.Timeout; + return value; +} + +/// Lightweight binary/counting semaphore using task notifications. +/// +/// If `clear_on_exit` is true, the notification value is cleared to 0 on exit. +/// If `clear_on_exit` is false, the notification value is decremented. +/// Returns the notification value *before* modification, or `Timeout` if the +/// value was zero throughout the wait. +pub fn take(clear_on_exit: bool, timeout: TickType) Error!u32 { + const value = c.ulTaskGenericNotifyTake( + 0, // notification index + if (clear_on_exit) c.pdTRUE else c.pdFALSE, + timeout, + ); + if (value == 0) return error.Timeout; + return value; +} + +// ── State Management ──────────────────────────────────────────────────── + +/// Clear the notification pending state at the default index (0). +/// Returns `true` if a notification was pending. +pub fn clear_state(task_handle: ?TaskHandle) bool { + return config.check_true(c.xTaskGenericNotifyStateClear(task_handle, 0)); +} + +/// Clear the notification pending state at a specific index. +pub fn clear_state_indexed(task_handle: ?TaskHandle, index: u32) bool { + return config.check_true(c.xTaskGenericNotifyStateClear(task_handle, @intCast(index))); +} + +/// Clear specific bits in the notification value at the default index (0). +/// Returns the notification value *before* the bits were cleared. +pub fn clear_bits(task_handle: ?TaskHandle, bits_to_clear: u32) u32 { + return c.ulTaskGenericNotifyValueClear(task_handle, 0, bits_to_clear); +} + +/// Clear specific bits at a specific index. +pub fn clear_bits_indexed(task_handle: ?TaskHandle, index: u32, bits_to_clear: u32) u32 { + return c.ulTaskGenericNotifyValueClear(task_handle, @intCast(index), bits_to_clear); +} diff --git a/modules/freertos/src/picosdk_stubs.c b/modules/freertos/src/picosdk_stubs.c new file mode 100644 index 000000000..676bbd1da --- /dev/null +++ b/modules/freertos/src/picosdk_stubs.c @@ -0,0 +1,78 @@ +// picosdk_stubs.c — Pico SDK function stubs for the FreeRTOS port. +// +// The FreeRTOS RP2040/RP2350 port references these Pico SDK functions, +// which are not provided by MicroZig. These minimal stubs satisfy the +// linker for single-core FreeRTOS usage. +// +// See: https://github.com/ZigEmbeddedGroup/microzig/issues/880 + +#include +#include + +// ── IRQ / Panic ───────────────────────────────────────────────────────── +// Default handlers that halt immediately. A debugger will stop here. + +// Fallback for unhandled user IRQs — traps immediately so a debugger +// catches the fault rather than silently continuing. +void __unhandled_user_irq(void) { + __builtin_trap(); +} + +// Called by the Pico SDK when an unsupported operation is attempted. +// Traps immediately to surface the error during development. +void panic_unsupported(void) { + __builtin_trap(); +} + +// ── Multicore (no-ops for single-core FreeRTOS) ───────────────────────── +// The RP2350 port references multicore symbols even in single-core mode +// because LIB_PICO_MULTICORE=1 is required for compilation. + +// No-op stub — core 1 is never launched in single-core FreeRTOS mode. +void multicore_launch_core1(void (*entry)(void)) { + (void)entry; +} + +// No-op stub — core 1 is never used, so nothing to reset. +void multicore_reset_core1(void) { +} + +// Always returns false — doorbells are not available without the full Pico SDK +// multicore runtime. The `required` flag is ignored; callers must handle failure. +int multicore_doorbell_claim_unused(uint32_t *doorbell, bool required) { + (void)doorbell; + (void)required; + return 0; +} + +// ── Clocks ────────────────────────────────────────────────────────────── +// Returns the system clock frequency for the target chip. +// The FreeRTOS port uses this to configure the SysTick timer. + +// Returns the default system clock: 125 MHz for RP2040, 150 MHz for RP2350. +// The clock_id parameter is ignored — we always return the system clock. +uint32_t clock_get_hz(uint32_t clock_id) { + (void)clock_id; +#if defined(PICO_RP2040) && PICO_RP2040 + return 125000000u; +#else + return 150000000u; +#endif +} + +// ── Spinlocks ─────────────────────────────────────────────────────────── +// The port claims spinlocks for internal synchronization. +// These stubs are safe because hardware spinlocks are managed by MicroZig. + +// No-op — spin locks are managed by hardware and don't need software claiming +// in single-core mode. The lock_num is ignored. +void spin_lock_claim(unsigned int lock_num) { + (void)lock_num; +} + +// Returns sequential lock numbers for the FreeRTOS port, which expects unique +// values per lock. The counter starts at 16 to avoid conflicting with +// low-numbered locks (0–15) reserved by the Pico SDK and hardware. +unsigned int next_striped_spin_lock_num(void) { + return 16; +} diff --git a/modules/freertos/src/queue.zig b/modules/freertos/src/queue.zig new file mode 100644 index 000000000..9288cbc1b --- /dev/null +++ b/modules/freertos/src/queue.zig @@ -0,0 +1,171 @@ +//! Type-safe FreeRTOS queue wrapper for inter-task communication. +//! +//! FreeRTOS queues are FIFO buffers that allow tasks (and ISRs) to send +//! and receive fixed-size items. This wrapper adds compile-time type safety. +//! +//! ## Example +//! ```zig +//! var q = try freertos.queue.create(u32, 10); +//! defer q.destroy(); +//! +//! try q.send(&@as(u32, 42), freertos.config.max_delay); +//! const value = try q.receive(freertos.config.max_delay); +//! ``` + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const Error = config.Error; + +// ── ISR Result Types ──────────────────────────────────────────────────── + +/// Result from an ISR send operation. +pub const ISR_Result = struct { + /// Whether the operation succeeded (queue was not full/empty). + success: bool, + /// Whether a higher-priority task was woken and a context switch is needed. + higher_priority_task_woken: bool, +}; + +/// Result from an ISR receive operation. +pub fn IsrReceiveResult(comptime T: type) type { + return struct { + /// The received item. + item: T, + /// Whether a higher-priority task was woken. + higher_priority_task_woken: bool, + }; +} + +// ── Generic Queue ─────────────────────────────────────────────────────── + +/// A type-safe FreeRTOS queue holding items of type `T`. +pub fn Queue(comptime T: type) type { + return struct { + const Self = @This(); + /// Underlying FreeRTOS C handle. Use `freertos.c` for direct C API access. + handle: c.QueueHandle_t, + + /// Create a new queue that can hold `length` items of type `T`. + /// Returns `error.OutOfMemory` if the kernel heap is exhausted. + pub fn create(length: u32) Error!Self { + const handle = c.xQueueCreate( + @as(config.U_BaseType, @intCast(length)), + @as(config.U_BaseType, @intCast(@sizeOf(T))), + ); + return .{ .handle = try config.check_handle(c.QueueHandle_t, handle) }; + } + + /// Delete the queue and free its memory. + pub fn destroy(self: Self) void { + c.vQueueDelete(self.handle); + } + + // ── Send ──────────────────────────────────────────────────── + + /// Send an item to the back of the queue. + /// Blocks up to `timeout` ticks if the queue is full. + pub fn send(self: Self, item: *const T, timeout: TickType) Error!void { + const rc = c.xQueueSendToBack(self.handle, item, timeout); + if (rc != c.pdPASS) return error.QueueFull; + } + + /// Send an item to the front of the queue. + pub fn send_to_front(self: Self, item: *const T, timeout: TickType) Error!void { + const rc = c.xQueueSendToFront(self.handle, item, timeout); + if (rc != c.pdPASS) return error.QueueFull; + } + + /// Overwrite the item in a queue of length 1. + /// Always succeeds (never blocks). + pub fn overwrite(self: Self, item: *const T) void { + _ = c.xQueueOverwrite(self.handle, item); + } + + /// Send an item from an ISR context. + pub fn send_from_isr(self: Self, item: *const T) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xQueueSendToBackFromISR(self.handle, item, &woken); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Send an item to the front from an ISR context. + pub fn send_to_front_from_isr(self: Self, item: *const T) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xQueueSendToFrontFromISR(self.handle, item, &woken); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + // ── Receive ───────────────────────────────────────────────── + + /// Receive an item from the queue, removing it. + /// Blocks up to `timeout` ticks if the queue is empty. + pub fn receive(self: Self, timeout: TickType) Error!T { + var item: T = undefined; + const rc = c.xQueueReceive(self.handle, &item, timeout); + if (rc != c.pdPASS) return error.QueueEmpty; + return item; + } + + /// Peek at the front item without removing it. + pub fn peek(self: Self, timeout: TickType) Error!T { + var item: T = undefined; + const rc = c.xQueuePeek(self.handle, &item, timeout); + if (rc != c.pdPASS) return error.QueueEmpty; + return item; + } + + /// Receive an item from ISR context. + pub fn receive_from_isr(self: Self) ?IsrReceiveResult(T) { + var item: T = undefined; + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xQueueReceiveFromISR(self.handle, &item, &woken); + if (rc != c.pdPASS) return null; + return .{ + .item = item, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + // ── Status ────────────────────────────────────────────────── + + /// Get the number of items currently in the queue. + pub fn messages_waiting(self: Self) u32 { + return @intCast(c.uxQueueMessagesWaiting(self.handle)); + } + + /// Get the number of free spaces remaining in the queue. + pub fn spaces_available(self: Self) u32 { + return @intCast(c.uxQueueSpacesAvailable(self.handle)); + } + + /// Reset the queue to its empty state. + pub fn reset(self: Self) void { + _ = c.xQueueReset(self.handle); + } + + /// Check if the queue is empty (ISR-safe). + pub fn is_empty_from_isr(self: Self) bool { + return c.xQueueIsQueueEmptyFromISR(self.handle) != c.pdFALSE; + } + + /// Check if the queue is full (ISR-safe). + pub fn is_full_from_isr(self: Self) bool { + return c.xQueueIsQueueFullFromISR(self.handle) != c.pdFALSE; + } + }; +} + +// ── Top-Level Convenience ─────────────────────────────────────────────── + +/// Create a new queue holding items of type `T` with the given capacity. +pub fn create(comptime T: type, length: u32) Error!Queue(T) { + return Queue(T).create(length); +} diff --git a/modules/freertos/src/root.zig b/modules/freertos/src/root.zig index 8162c43c7..cd8322a92 100644 --- a/modules/freertos/src/root.zig +++ b/modules/freertos/src/root.zig @@ -1,23 +1,106 @@ -const std = @import("std"); +//! # FreeRTOS for MicroZig +//! +//! Idiomatic Zig wrappers for the FreeRTOS real-time operating system kernel. +//! +//! ## Quick Start +//! ```zig +//! const freertos = @import("freertos"); +//! +//! // Create a task +//! const handle = try freertos.task.create(my_task, "demo", 512, null, 1); +//! +//! // Start the scheduler (never returns) +//! freertos.task.start_scheduler(); +//! ``` +//! +//! For direct access to the underlying C API, use `freertos.c.*`. +// ── Raw C Bindings (escape hatch) ─────────────────────────────────────── + +/// Direct access to all FreeRTOS C functions and types via `@cImport`. +/// Use this for advanced use-cases not covered by the Zig wrappers. pub const c = @cImport({ @cInclude("FreeRTOS.h"); @cInclude("task.h"); + @cInclude("queue.h"); + @cInclude("semphr.h"); + @cInclude("event_groups.h"); + @cInclude("timers.h"); }); +// ── Zig API Modules ───────────────────────────────────────────────────── + +/// Shared types, constants, and error handling. +pub const config = @import("config.zig"); + +/// Task creation, deletion, control, and scheduler management. +pub const task = @import("task.zig"); + +/// Type-safe FIFO queues for inter-task communication. +pub const queue = @import("queue.zig"); + +/// Binary and counting semaphores. +pub const semaphore = @import("semaphore.zig"); + +/// Standard and recursive mutexes with priority inheritance. +pub const mutex = @import("mutex.zig"); + +/// Event groups for multi-bit synchronization. +pub const event_group = @import("event_group.zig"); + +/// Software timers with callback support. +pub const timer = @import("timer.zig"); + +/// Lightweight direct-to-task notification mechanism. +pub const notification = @import("notification.zig"); + +// ── ISR Exports ───────────────────────────────────────────────────────── +// These interrupt service routines are provided by the FreeRTOS port layer. +// They are exported so the vector table (RAM or flash) can reference them. + +/// PendSV interrupt handler — used by the FreeRTOS port for context switching +/// between tasks. Exported so the vector table can reference it. +pub extern fn isr_pendsv() callconv(.naked) void; +/// SVCall interrupt handler — used by FreeRTOS to start the first task when +/// the scheduler begins. Exported so the vector table can reference it. +pub extern fn isr_svcall() callconv(.c) void; +/// SysTick interrupt handler — used by FreeRTOS for tick counting and +/// time-slicing. Fires at `configTICK_RATE_HZ`. Exported so the vector +/// table can reference it. +pub extern fn isr_systick() callconv(.c) void; + +// ── Pico SDK Stubs ────────────────────────────────────────────────────── +// Platform-specific stubs (IRQ handler, multicore, clocks, spinlocks) are +// provided by picosdk_stubs.c, compiled as part of the freertos_lib module. +// See: src/picosdk_stubs.c + +// ── FreeRTOS Hooks ────────────────────────────────────────────────────── + +/// FreeRTOS stack overflow hook — called when a task exceeds its stack. +/// Traps immediately; a debugger will stop here with a meaningful backtrace. +export fn vApplicationStackOverflowHook(_: config.TaskHandle, _: [*c]u8) void { + @trap(); +} + +// ── Backward Compatibility ────────────────────────────────────────────── + +/// Deprecated: use `freertos.task` and `freertos.config` instead. +/// Preserved for backward compatibility with existing code. pub const OS = struct { - pub const MINIMAL_STACK_SIZE: usize = c.configMINIMAL_STACK_SIZE; - pub const MAX_PRIORITIES: usize = c.configMAX_PRIORITIES; + pub const MINIMAL_STACK_SIZE: usize = config.minimal_stack_size; + pub const MAX_PRIORITIES: usize = config.max_priorities; + /// Deprecated: use `freertos.task.start_scheduler` instead. pub fn vTaskStartScheduler() noreturn { - c.vTaskStartScheduler(); - unreachable; + task.start_scheduler(); } + /// Deprecated: use `freertos.task.delay` instead. pub fn vTaskDelay(ticks: c.TickType_t) void { - c.vTaskDelay(ticks); + task.delay(ticks); } + /// Deprecated: use `freertos.task.create` instead. pub fn xTaskCreate( task_function: c.TaskFunction_t, name: [*:0]const u8, @@ -36,23 +119,3 @@ pub const OS = struct { ); } }; - -// Those 3 exported interrupt functions are used when PICO_NO_RAM_VECTOR_TABLE is 1 -// This doesn't work because Microzig but also Pico SDK? dont set VTOR to 0x10000100 for RP2040 at boot -// even if proper configuration is set: -// pub const microzig_options = microzig.Options{ -// .overwrite_hal_interrupts = true, -// .interrupts = .{ -// .PendSV = .{ .naked = freertos.isr_pendsv }, -// .SysTick = .{ .c = freertos.isr_systick }, -// .SVCall = .{ .c = freertos.isr_svcall } -// }, -// .cpu = .{ -// .ram_vector_table = false, -// }, -//}; -// probably related to: https://github.com/raspberrypi/pico-sdk/issues/6 -// In most scenarious we are not interested in no ram vector case anyway -pub extern fn isr_pendsv() callconv(.naked) void; -pub extern fn isr_svcall() callconv(.c) void; -pub extern fn isr_systick() callconv(.c) void; diff --git a/modules/freertos/src/semaphore.zig b/modules/freertos/src/semaphore.zig new file mode 100644 index 000000000..1900f9081 --- /dev/null +++ b/modules/freertos/src/semaphore.zig @@ -0,0 +1,102 @@ +//! FreeRTOS semaphore wrappers — binary and counting semaphores. +//! +//! Semaphores are signaling primitives used for synchronization between +//! tasks and between tasks and ISRs. They are distinct from mutexes: +//! semaphores signal events, mutexes protect resources. +//! +//! ## Example +//! ```zig +//! var sem = try freertos.semaphore.create_binary(); +//! defer sem.destroy(); +//! +//! // In producer task or ISR: +//! try sem.give(); +//! +//! // In consumer task: +//! try sem.take(freertos.config.max_delay); +//! ``` + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const Error = config.Error; + +/// Result from an ISR give/take operation. +pub const ISR_Result = struct { + /// Whether the operation succeeded. + success: bool, + /// Whether a higher-priority task was woken and a context switch is needed. + higher_priority_task_woken: bool, +}; + +/// A FreeRTOS semaphore (binary or counting). +pub const Semaphore = struct { + /// Underlying FreeRTOS C handle. Use `freertos.c` for direct C API access. + handle: c.SemaphoreHandle_t, + + /// Delete the semaphore and free its memory. + pub fn destroy(self: Semaphore) void { + c.vSemaphoreDelete(self.handle); + } + + /// Acquire (take) the semaphore, blocking up to `timeout` ticks. + /// Returns `error.Timeout` if the semaphore could not be acquired. + pub fn take(self: Semaphore, timeout: TickType) Error!void { + const rc = c.xSemaphoreTake(self.handle, timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Release (give) the semaphore. + /// Returns `error.Failure` if the semaphore could not be given + /// (e.g., it was not previously taken, for a binary semaphore). + pub fn give(self: Semaphore) Error!void { + const rc = c.xSemaphoreGive(self.handle); + if (rc != c.pdPASS) return error.Failure; + } + + /// Release the semaphore from an ISR context. + pub fn give_from_isr(self: Semaphore) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xSemaphoreGiveFromISR(self.handle, &woken); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Take the semaphore from an ISR context (binary and counting only). + pub fn take_from_isr(self: Semaphore) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xSemaphoreTakeFromISR(self.handle, &woken); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Get the current count of a counting semaphore, or 1/0 for binary. + pub fn get_count(self: Semaphore) u32 { + return @intCast(c.uxSemaphoreGetCount(self.handle)); + } +}; + +// ── Creation Functions ────────────────────────────────────────────────── + +/// Create a binary semaphore (starts in the "empty" / not-given state). +/// Returns `error.OutOfMemory` if the kernel heap is exhausted. +pub fn create_binary() Error!Semaphore { + const handle = c.xSemaphoreCreateBinary(); + return .{ .handle = try config.check_handle(c.SemaphoreHandle_t, handle) }; +} + +/// Create a counting semaphore. +/// - `max_count`: Maximum count value. +/// - `initial_count`: Starting count value. +pub fn create_counting(max_count: u32, initial_count: u32) Error!Semaphore { + const handle = c.xSemaphoreCreateCounting( + @intCast(max_count), + @intCast(initial_count), + ); + return .{ .handle = try config.check_handle(c.SemaphoreHandle_t, handle) }; +} diff --git a/modules/freertos/src/task.zig b/modules/freertos/src/task.zig new file mode 100644 index 000000000..629954a21 --- /dev/null +++ b/modules/freertos/src/task.zig @@ -0,0 +1,235 @@ +//! FreeRTOS task management — creation, deletion, control, and scheduler operations. +//! +//! Tasks are the fundamental execution unit in FreeRTOS. This module provides +//! idiomatic Zig wrappers for creating tasks, controlling execution (delay, +//! suspend, resume), querying state, and managing the scheduler lifecycle. +//! +//! ## Key Functions +//! - `create` / `destroy` — task lifecycle +//! - `delay` / `delay_until` — blocking delays +//! - `start_scheduler` — hand control to FreeRTOS (never returns) +//! - `get_tick_count` / `get_count` — runtime queries + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const TaskHandle = config.TaskHandle; +const TaskFunction = config.TaskFunction; +const Error = config.Error; + +// ── Task State ────────────────────────────────────────────────────────── + +/// Runtime state of a FreeRTOS task. +pub const State = enum { + running, + ready, + blocked, + suspended, + deleted, + invalid, + + /// Convert from the C `eTaskState` enum. + fn from_c(state: c.eTaskState) State { + return switch (state) { + c.eRunning => .running, + c.eReady => .ready, + c.eBlocked => .blocked, + c.eSuspended => .suspended, + c.eDeleted => .deleted, + else => .invalid, + }; + } +}; + +// ── Task Creation & Deletion ──────────────────────────────────────────── + +/// Create a new dynamically-allocated FreeRTOS task. +/// +/// Returns a handle to the created task, or `error.OutOfMemory` if the +/// kernel heap is exhausted. +/// +/// ## Example +/// ```zig +/// const handle = try freertos.task.create(my_func, "worker", 512, null, 1); +/// ``` +pub fn create( + function: TaskFunction, + name: [*:0]const u8, + stack_depth: config.StackDepthType, + parameters: ?*anyopaque, + priority: config.U_BaseType, +) Error!TaskHandle { + var handle: TaskHandle = null; + const rc = c.xTaskCreate( + function, + name, + stack_depth, + parameters, + priority, + &handle, + ); + try config.check_base_type(rc); + return handle; +} + +/// Delete a task. Pass `null` to delete the calling task. +pub fn destroy(task_handle: ?TaskHandle) void { + c.vTaskDelete(task_handle); +} + +// ── Task Control ──────────────────────────────────────────────────────── + +/// Block the calling task for the given number of ticks. +pub fn delay(ticks: TickType) void { + c.vTaskDelay(ticks); +} + +/// Block the calling task until an absolute tick count, for periodic execution. +/// Returns `true` if the task was actually delayed (not already past the target time). +pub fn delay_until(previous_wake_time: *TickType, time_increment: TickType) bool { + return config.check_true(c.xTaskDelayUntil(previous_wake_time, time_increment)); +} + +/// Suspend a task. Pass `null` to suspend the calling task. +pub fn @"suspend"(task_handle: ?TaskHandle) void { + c.vTaskSuspend(task_handle); +} + +/// Resume a previously suspended task. +pub fn @"resume"(task_handle: TaskHandle) void { + c.vTaskResume(task_handle); +} + +/// Resume a task from an ISR context. +/// Returns `true` if a context switch should be requested. +pub fn resume_from_isr(task_handle: TaskHandle) bool { + return config.check_true(c.xTaskResumeFromISR(task_handle)); +} + +/// Force a task out of the Blocked state. +/// Returns `true` if the task was actually unblocked. +pub fn abort_delay(task_handle: TaskHandle) bool { + return config.check_true(c.xTaskAbortDelay(task_handle)); +} + +/// Set a task's priority. +pub fn set_priority(task_handle: ?TaskHandle, new_priority: config.U_BaseType) void { + c.vTaskPrioritySet(task_handle, new_priority); +} + +/// Get a task's current priority. +pub fn get_priority(task_handle: ?TaskHandle) config.U_BaseType { + return c.uxTaskPriorityGet(task_handle); +} + +// ── Task Utilities ────────────────────────────────────────────────────── + +/// Get the handle of the currently running task. +pub fn get_current_handle() TaskHandle { + return c.xTaskGetCurrentTaskHandle(); +} + +/// Look up a task handle by its name string. +/// Returns `null` if no task with that name exists. +pub fn get_handle(name: [*:0]const u8) ?TaskHandle { + return c.xTaskGetHandle(name); +} + +/// Get the idle task's handle. +pub fn get_idle_task_handle() TaskHandle { + return c.xTaskGetIdleTaskHandle(); +} + +/// Get the human-readable name of a task. +pub fn get_name(task_handle: ?TaskHandle) [*:0]const u8 { + return c.pcTaskGetName(task_handle); +} + +/// Query the runtime state of a task. +pub fn get_state(task_handle: TaskHandle) State { + return State.from_c(c.eTaskGetState(task_handle)); +} + +/// Get the minimum amount of free stack space (in words) that has existed +/// since the task was created — the "high water mark". +pub fn get_stack_high_water_mark(task_handle: ?TaskHandle) u32 { + return @intCast(c.uxTaskGetStackHighWaterMark(task_handle)); +} + +/// Get the current system tick count. +pub fn get_tick_count() TickType { + return c.xTaskGetTickCount(); +} + +/// Get the current system tick count (safe to call from ISR). +pub fn get_tick_count_from_isr() TickType { + return c.xTaskGetTickCountFromISR(); +} + +/// Get the total number of tasks currently managed by the kernel. +pub fn get_count() u32 { + return @intCast(c.uxTaskGetNumberOfTasks()); +} + +// ── Scheduler Control ─────────────────────────────────────────────────── + +/// Start the FreeRTOS scheduler. This function never returns on success. +pub fn start_scheduler() noreturn { + c.vTaskStartScheduler(); + unreachable; +} + +/// Stop the FreeRTOS scheduler. +pub fn end_scheduler() void { + c.vTaskEndScheduler(); +} + +/// Scheduler state. +pub const SchedulerState = enum { + /// The scheduler has not been started yet (`start_scheduler` not called). + not_started, + /// The scheduler is running and dispatching tasks normally. + running, + /// The scheduler is suspended via `suspend_all`; no context switches occur. + suspended, +}; + +/// Get the current scheduler state. +pub fn get_scheduler_state() SchedulerState { + return switch (c.xTaskGetSchedulerState()) { + c.taskSCHEDULER_NOT_STARTED => .not_started, + c.taskSCHEDULER_RUNNING => .running, + c.taskSCHEDULER_SUSPENDED => .suspended, + else => .not_started, + }; +} + +/// Suspend the scheduler. Tasks will not be switched, but interrupts +/// are still serviced. Call `resume_all()` to resume. +pub fn suspend_all() void { + c.vTaskSuspendAll(); +} + +/// Resume the scheduler after a `suspend_all()` call. +/// Returns `true` if a context switch occurred as a result. +pub fn resume_all() bool { + return config.check_true(c.xTaskResumeAll()); +} + +/// Advance the tick count (used in tickless idle implementations). +pub fn step_tick(ticks_to_jump: TickType) void { + c.vTaskStepTick(ticks_to_jump); +} + +// ── Thread Local Storage ──────────────────────────────────────────────── + +/// Set a thread-local storage pointer for a task. +pub fn set_thread_local_storage_pointer(task_handle: ?TaskHandle, index: config.BaseType, value: ?*anyopaque) void { + c.vTaskSetThreadLocalStoragePointer(task_handle, index, value); +} + +/// Get a thread-local storage pointer for a task. +pub fn get_thread_local_storage_pointer(task_handle: ?TaskHandle, index: config.BaseType) ?*anyopaque { + return c.pvTaskGetThreadLocalStoragePointer(task_handle, index); +} diff --git a/modules/freertos/src/timer.zig b/modules/freertos/src/timer.zig new file mode 100644 index 000000000..7c2d6372a --- /dev/null +++ b/modules/freertos/src/timer.zig @@ -0,0 +1,229 @@ +//! FreeRTOS software timer wrappers. +//! +//! Software timers execute a callback function at a configured interval. +//! Timer callbacks run in the context of the timer service (daemon) task. +//! +//! ## Example +//! ```zig +//! var tmr = try freertos.timer.create("heartbeat", 1000, true, null, heartbeat_cb); +//! try tmr.start(0); +//! ``` + +const config = @import("config.zig"); +const c = @import("root.zig").c; + +const TickType = config.TickType; +const Error = config.Error; +const TaskHandle = config.TaskHandle; + +/// Timer callback function type. +pub const CallbackFn = config.TimerCallbackFunction; + +/// Result from an ISR timer operation. +pub const ISR_Result = struct { + /// Whether the command was successfully posted to the timer queue. + success: bool, + higher_priority_task_woken: bool, +}; + +/// A FreeRTOS software timer. +pub const Timer = struct { + /// Underlying FreeRTOS C handle. Use `freertos.c` for direct C API access. + handle: c.TimerHandle_t, + + // ── Control ───────────────────────────────────────────────────── + // + // NOTE: The C macros (xTimerStart, xTimerStop, etc.) pass `NULL` for + // `pxHigherPriorityTaskWoken`. Zig's cImport cannot coerce the + // untyped `NULL` (`?*anyopaque`) to `[*c]BaseType_t`, so we call + // the underlying `xTimerGenericCommandFromTask` / `FromISR` directly. + + /// Start the timer. If already running, this resets it. + /// `command_timeout` is how long to wait to post the command to the + /// timer command queue. + pub fn start(self: Timer, command_timeout: TickType) Error!void { + const rc = c.xTimerGenericCommandFromTask(self.handle, c.tmrCOMMAND_START, c.xTaskGetTickCount(), null, command_timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Stop the timer. + pub fn stop(self: Timer, command_timeout: TickType) Error!void { + const rc = c.xTimerGenericCommandFromTask(self.handle, c.tmrCOMMAND_STOP, 0, null, command_timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Reset the timer — restarts counting from now. + /// If the timer was not running, this starts it. + pub fn reset(self: Timer, command_timeout: TickType) Error!void { + const rc = c.xTimerGenericCommandFromTask(self.handle, c.tmrCOMMAND_RESET, c.xTaskGetTickCount(), null, command_timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Change the timer's period and restart it. + pub fn change_period(self: Timer, new_period: TickType, command_timeout: TickType) Error!void { + const rc = c.xTimerGenericCommandFromTask(self.handle, c.tmrCOMMAND_CHANGE_PERIOD, new_period, null, command_timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Delete the timer. Unlike other primitives' `destroy()`, timer deletion sends a + /// command to the timer daemon queue, which can fail if the queue is full. + /// Use `destroy_blocking()` for a fire-and-forget variant that waits indefinitely. + pub fn destroy(self: Timer, command_timeout: TickType) Error!void { + const rc = c.xTimerGenericCommandFromTask(self.handle, c.tmrCOMMAND_DELETE, 0, null, command_timeout); + if (rc != c.pdPASS) return error.Timeout; + } + + /// Delete the timer, waiting indefinitely for the command to be accepted. + /// This cannot fail and is safe to use in `defer`. + pub fn destroy_blocking(self: Timer) void { + _ = c.xTimerGenericCommandFromTask( + self.handle, + c.tmrCOMMAND_DELETE, + 0, + null, + config.max_delay, + ); + } + + // ── ISR Variants ──────────────────────────────────────────────── + + /// Start the timer from an ISR context. + pub fn start_from_isr(self: Timer) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xTimerGenericCommandFromISR(self.handle, c.tmrCOMMAND_START_FROM_ISR, c.xTaskGetTickCountFromISR(), &woken, 0); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Stop the timer from an ISR context. + pub fn stop_from_isr(self: Timer) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xTimerGenericCommandFromISR(self.handle, c.tmrCOMMAND_STOP_FROM_ISR, 0, &woken, 0); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Reset the timer from an ISR context. + pub fn reset_from_isr(self: Timer) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xTimerGenericCommandFromISR(self.handle, c.tmrCOMMAND_RESET_FROM_ISR, c.xTaskGetTickCountFromISR(), &woken, 0); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + /// Change the timer period from an ISR context. + pub fn change_period_from_isr(self: Timer, new_period: TickType) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xTimerGenericCommandFromISR(self.handle, c.tmrCOMMAND_CHANGE_PERIOD_FROM_ISR, new_period, &woken, 0); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; + } + + // ── Queries ───────────────────────────────────────────────────── + + /// Check if the timer is currently active (running). + pub fn is_active(self: Timer) bool { + return c.xTimerIsTimerActive(self.handle) != c.pdFALSE; + } + + /// Get the human-readable timer name. + pub fn get_name(self: Timer) [*:0]const u8 { + return c.pcTimerGetName(self.handle); + } + + /// Get the timer's period in ticks. + pub fn get_period(self: Timer) TickType { + return c.xTimerGetPeriod(self.handle); + } + + /// Get the tick time at which the timer will expire. + pub fn get_expiry_time(self: Timer) TickType { + return c.xTimerGetExpiryTime(self.handle); + } + + /// Get the timer's user-defined ID pointer. + pub fn get_id(self: Timer) ?*anyopaque { + return c.pvTimerGetTimerID(self.handle); + } + + /// Set the timer's user-defined ID pointer. + pub fn set_id(self: Timer, id: ?*anyopaque) void { + c.vTimerSetTimerID(self.handle, id); + } + + /// Check whether the timer auto-reloads. + pub fn get_auto_reload(self: Timer) bool { + return c.uxTimerGetReloadMode(self.handle) != 0; + } + + /// Set whether the timer auto-reloads. + pub fn set_auto_reload(self: Timer, auto_reload: bool) void { + c.vTimerSetReloadMode(self.handle, if (auto_reload) @as(c.UBaseType_t, 1) else @as(c.UBaseType_t, 0)); + } +}; + +// ── Creation ──────────────────────────────────────────────────────────── + +/// Create a new software timer. +/// +/// - `name`: Human-readable name for debugging. +/// - `period`: Timer period in ticks. +/// - `auto_reload`: If true, the timer restarts automatically after expiry. +/// - `id`: User-defined pointer stored with the timer (accessible via `get_id`). +/// - `callback`: Function called when the timer expires. +/// +/// Returns `error.OutOfMemory` if the kernel heap is exhausted. +pub fn create( + name: [*:0]const u8, + period: TickType, + auto_reload: bool, + id: ?*anyopaque, + callback: CallbackFn, +) Error!Timer { + const handle = c.xTimerCreate( + name, + period, + if (auto_reload) c.pdTRUE else c.pdFALSE, + id, + callback, + ); + return .{ .handle = try config.check_handle(c.TimerHandle_t, handle) }; +} + +/// Get the handle of the timer service (daemon) task. +pub fn get_daemon_task_handle() TaskHandle { + return c.xTimerGetTimerDaemonTaskHandle(); +} + +/// Defer a function call to the timer daemon task from a normal task context. +pub fn pend_function_call( + function: c.PendedFunction_t, + param1: ?*anyopaque, + param2: u32, + timeout: TickType, +) Error!void { + const rc = c.xTimerPendFunctionCall(function, param1, param2, timeout); + if (rc != c.pdPASS) return error.Timeout; +} + +/// Defer a function call to the timer daemon task from an ISR context. +pub fn pend_function_call_from_isr( + function: c.PendedFunction_t, + param1: ?*anyopaque, + param2: u32, +) ISR_Result { + var woken: c.BaseType_t = c.pdFALSE; + const rc = c.xTimerPendFunctionCallFromISR(function, param1, param2, &woken); + return .{ + .success = rc == c.pdPASS, + .higher_priority_task_woken = woken != c.pdFALSE, + }; +}