|
| 1 | +# FreeRTOS Module for MicroZig |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +Hardware-tested on **Seeed XIAO RP2350** (ARM Cortex-M33). |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +- ✅ **Tasks** — create, delete, suspend, resume, delay, priority management |
| 10 | +- ✅ **Queues** — generic `Queue(T)` with compile-time type safety |
| 11 | +- ✅ **Semaphores** — binary and counting |
| 12 | +- ✅ **Mutexes** — standard (with priority inheritance) and recursive |
| 13 | +- ✅ **Software Timers** — auto-reload and one-shot with callback support |
| 14 | +- ✅ **Event Groups** — multi-bit synchronization and rendezvous/barrier patterns |
| 15 | +- ✅ **Task Notifications** — lightweight direct-to-task signaling |
| 16 | +- ✅ **ISR variants** — all primitives provide `*FromIsr` functions with wake flags |
| 17 | +- ✅ **Raw C escape hatch** — `freertos.c.*` for anything not yet wrapped |
| 18 | + |
| 19 | +## Quick Start |
| 20 | + |
| 21 | +```zig |
| 22 | +const std = @import("std"); |
| 23 | +const freertos = @import("freertos"); |
| 24 | +
|
| 25 | +pub fn main() !void { |
| 26 | + // ... hardware init ... |
| 27 | +
|
| 28 | + _ = try freertos.task.create( |
| 29 | + hello_task, |
| 30 | + "hello", |
| 31 | + freertos.config.minimal_stack_size * 8, |
| 32 | + null, |
| 33 | + freertos.config.max_priorities - 1, |
| 34 | + ); |
| 35 | +
|
| 36 | + // Start the scheduler — never returns |
| 37 | + freertos.task.startScheduler(); |
| 38 | +} |
| 39 | +
|
| 40 | +fn hello_task(_: ?*anyopaque) callconv(.c) void { |
| 41 | + var i: u32 = 0; |
| 42 | + while (true) : (i += 1) { |
| 43 | + std.log.info("Hello from FreeRTOS task {}", .{i}); |
| 44 | + freertos.task.delay(500); // 500 ms at default 1 kHz tick rate |
| 45 | + } |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +## API Reference |
| 50 | + |
| 51 | +### Tasks (`freertos.task`) |
| 52 | + |
| 53 | +Task creation, deletion, scheduling, and runtime queries. |
| 54 | + |
| 55 | +| Function | Description | |
| 56 | +|---|---| |
| 57 | +| `create(function, name, stack_depth, params, priority)` | Create a task → `Error!TaskHandle` | |
| 58 | +| `destroy(handle)` | Delete a task (`null` = self) | |
| 59 | +| `delay(ticks)` | Block for `ticks` ticks | |
| 60 | +| `delayUntil(prev_wake_time, increment)` | Periodic delay → `bool` | |
| 61 | +| `suspend(handle)` | Suspend a task (`null` = self) | |
| 62 | +| `resume(handle)` | Resume a suspended task | |
| 63 | +| `resumeFromIsr(handle)` | Resume from ISR → `bool` (context switch needed) | |
| 64 | +| `abortDelay(handle)` | Force-unblock a task → `bool` | |
| 65 | +| `setPriority(handle, priority)` | Change a task's priority | |
| 66 | +| `getPriority(handle)` | Get current priority | |
| 67 | +| `getCurrentHandle()` | Handle of the running task | |
| 68 | +| `getHandle(name)` | Look up task by name → `?TaskHandle` | |
| 69 | +| `getName(handle)` | Task name string | |
| 70 | +| `getState(handle)` | → `.running`, `.ready`, `.blocked`, `.suspended`, `.deleted` | |
| 71 | +| `getStackHighWaterMark(handle)` | Minimum free stack (words) since creation | |
| 72 | +| `getTickCount()` | Current system tick | |
| 73 | +| `getCount()` | Total number of tasks | |
| 74 | +| `startScheduler()` | Start FreeRTOS — **never returns** | |
| 75 | +| `endScheduler()` | Stop FreeRTOS | |
| 76 | +| `getSchedulerState()` | → `.not_started`, `.running`, `.suspended` | |
| 77 | +| `suspendAll()` / `resumeAll()` | Disable/enable task switching | |
| 78 | + |
| 79 | +### Queues (`freertos.queue`) |
| 80 | + |
| 81 | +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. |
| 82 | + |
| 83 | +```zig |
| 84 | +var q = try freertos.queue.create(u32, 10); // Queue(u32), capacity 10 |
| 85 | +defer q.destroy(); |
| 86 | +
|
| 87 | +try q.send(&@as(u32, 42), freertos.config.max_delay); |
| 88 | +const value = try q.receive(freertos.config.max_delay); // value: u32 |
| 89 | +``` |
| 90 | + |
| 91 | +| Function | Description | |
| 92 | +|---|---| |
| 93 | +| `create(T, length)` | Create a `Queue(T)` with given capacity → `Error!Queue(T)` | |
| 94 | +| `q.send(item, timeout)` | Send to back → `Error!void` (`QueueFull` on timeout) | |
| 95 | +| `q.sendToFront(item, timeout)` | Send to front | |
| 96 | +| `q.overwrite(item)` | Overwrite single-item queue (never blocks) | |
| 97 | +| `q.receive(timeout)` | Receive and remove → `Error!T` (`QueueEmpty` on timeout) | |
| 98 | +| `q.peek(timeout)` | Read front item without removing | |
| 99 | +| `q.sendFromIsr(item)` | ISR send → `IsrResult` | |
| 100 | +| `q.receiveFromIsr()` | ISR receive → `?IsrReceiveResult(T)` | |
| 101 | +| `q.messagesWaiting()` | Number of items in queue | |
| 102 | +| `q.spacesAvailable()` | Free slots remaining | |
| 103 | +| `q.reset()` | Flush the queue | |
| 104 | + |
| 105 | +### Semaphores (`freertos.semaphore`) |
| 106 | + |
| 107 | +Binary and counting semaphores for synchronization and signaling between tasks/ISRs. |
| 108 | + |
| 109 | +| Function | Description | |
| 110 | +|---|---| |
| 111 | +| `createBinary()` | Create binary semaphore (starts empty) → `Error!Semaphore` | |
| 112 | +| `createCounting(max, initial)` | Create counting semaphore → `Error!Semaphore` | |
| 113 | +| `s.take(timeout)` | Acquire → `Error!void` (`Timeout`) | |
| 114 | +| `s.give()` | Release → `Error!void` (`Failure` if not taken) | |
| 115 | +| `s.giveFromIsr()` | Release from ISR → `IsrResult` | |
| 116 | +| `s.takeFromIsr()` | Acquire from ISR → `IsrResult` | |
| 117 | +| `s.getCount()` | Current count (or 1/0 for binary) | |
| 118 | +| `s.destroy()` | Free the semaphore | |
| 119 | + |
| 120 | +### Mutexes (`freertos.mutex`) |
| 121 | + |
| 122 | +Standard and recursive mutexes with **priority inheritance** to prevent priority inversion. Use mutexes to protect shared resources; use semaphores for signaling. |
| 123 | + |
| 124 | +```zig |
| 125 | +var mtx = try freertos.mutex.create(); |
| 126 | +defer mtx.destroy(); |
| 127 | +
|
| 128 | +try mtx.acquire(freertos.config.max_delay); |
| 129 | +defer mtx.release() catch {}; |
| 130 | +
|
| 131 | +// ... access shared resource ... |
| 132 | +``` |
| 133 | + |
| 134 | +| Function | Description | |
| 135 | +|---|---| |
| 136 | +| `create()` | Create a standard mutex → `Error!Mutex` | |
| 137 | +| `createRecursive()` | Create a recursive mutex → `Error!Recursive` | |
| 138 | +| `m.acquire(timeout)` | Lock → `Error!void` (`Timeout`) | |
| 139 | +| `m.release()` | Unlock → `Error!void` (`Failure` if not held) | |
| 140 | +| `m.getHolder()` | Task holding the mutex → `?TaskHandle` | |
| 141 | +| `m.destroy()` | Free the mutex | |
| 142 | + |
| 143 | +Recursive mutexes can be acquired multiple times by the same task — each `acquire` must be paired with a `release`. |
| 144 | + |
| 145 | +### Event Groups (`freertos.event_group`) |
| 146 | + |
| 147 | +Multi-bit synchronization primitives. Tasks can wait on any combination of event bits, enabling rendezvous-style coordination. |
| 148 | + |
| 149 | +```zig |
| 150 | +var events = try freertos.event_group.create(); |
| 151 | +defer events.destroy(); |
| 152 | +
|
| 153 | +// Task A sets bit 0 |
| 154 | +_ = events.setBits(0x01); |
| 155 | +
|
| 156 | +// Task B waits for bits 0 AND 1 |
| 157 | +const bits = try events.waitBits(0x03, .{ .wait_for_all = true, .timeout = 1000 }); |
| 158 | +``` |
| 159 | + |
| 160 | +| Function | Description | |
| 161 | +|---|---| |
| 162 | +| `create()` | Create an event group → `Error!EventGroup` | |
| 163 | +| `e.setBits(bits)` | Set bits → returns new value | |
| 164 | +| `e.clearBits(bits)` | Clear bits → returns previous value | |
| 165 | +| `e.getBits()` | Read current bits | |
| 166 | +| `e.waitBits(bits, opts)` | Wait for bit pattern → `Error!EventBits` (`Timeout`) | |
| 167 | +| `e.sync(set, wait, timeout)` | Barrier: set bits then wait for others → `Error!EventBits` | |
| 168 | +| `e.setBitsFromIsr(bits)` | Set bits from ISR → `IsrResult` | |
| 169 | +| `e.destroy()` | Free the event group | |
| 170 | + |
| 171 | +`WaitOptions` fields: `clear_on_exit` (default `true`), `wait_for_all` (default `false`), `timeout` (default `max_delay`). |
| 172 | + |
| 173 | +### Software Timers (`freertos.timer`) |
| 174 | + |
| 175 | +Periodic or one-shot timers that execute a callback in the timer daemon task context. |
| 176 | + |
| 177 | +```zig |
| 178 | +var tmr = try freertos.timer.create("heartbeat", 1000, true, null, my_callback); |
| 179 | +try tmr.start(0); |
| 180 | +defer tmr.destroyBlocking(); |
| 181 | +``` |
| 182 | + |
| 183 | +| Function | Description | |
| 184 | +|---|---| |
| 185 | +| `create(name, period, auto_reload, id, callback)` | Create a timer → `Error!Timer` | |
| 186 | +| `t.start(cmd_timeout)` | Start/restart the timer | |
| 187 | +| `t.stop(cmd_timeout)` | Stop the timer | |
| 188 | +| `t.reset(cmd_timeout)` | Reset countdown from now | |
| 189 | +| `t.changePeriod(new_period, cmd_timeout)` | Change period and restart | |
| 190 | +| `t.destroy(cmd_timeout)` | Delete timer (can fail if queue full) | |
| 191 | +| `t.destroyBlocking()` | Delete timer, wait forever (safe for `defer`) | |
| 192 | +| `t.isActive()` | Check if running → `bool` | |
| 193 | +| `t.getName()` / `t.getPeriod()` / `t.getExpiryTime()` | Timer properties | |
| 194 | +| `t.getId()` / `t.setId(ptr)` | User-defined ID pointer | |
| 195 | +| `t.getAutoReload()` / `t.setAutoReload(bool)` | Auto-reload mode | |
| 196 | +| `t.startFromIsr()` / `t.stopFromIsr()` / `t.resetFromIsr()` | ISR variants → `IsrResult` | |
| 197 | +| `pendFunctionCall(fn, p1, p2, timeout)` | Defer a function call to the timer daemon | |
| 198 | + |
| 199 | +> ⚠️ **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. |
| 200 | +
|
| 201 | +### Task Notifications (`freertos.notification`) |
| 202 | + |
| 203 | +Lightweight direct-to-task notifications — faster and smaller than semaphores or event groups. Each task has a 32-bit notification value per index. |
| 204 | + |
| 205 | +```zig |
| 206 | +// Lightweight binary semaphore pattern: |
| 207 | +freertos.notification.give(task_handle) catch {}; // sender |
| 208 | +_ = freertos.notification.take(true, freertos.config.max_delay) catch {}; // receiver |
| 209 | +``` |
| 210 | + |
| 211 | +| Function | Description | |
| 212 | +|---|---| |
| 213 | +| `notify(handle, value, action)` | Send notification at index 0 | |
| 214 | +| `notifyIndexed(handle, index, value, action)` | Send at specific index | |
| 215 | +| `notifyAndQuery(handle, value, action)` | Send and get previous value → `Error!u32` | |
| 216 | +| `give(handle)` | Increment notification (lightweight semaphore give) | |
| 217 | +| `notifyFromIsr(handle, value, action)` | Send from ISR → `IsrResult` | |
| 218 | +| `giveFromIsr(handle)` | Increment from ISR → `IsrResult` | |
| 219 | +| `wait(clear_entry, clear_exit, timeout)` | Wait for notification → `Error!u32` | |
| 220 | +| `waitIndexed(index, clear_entry, clear_exit, timeout)` | Wait at specific index | |
| 221 | +| `take(clear_on_exit, timeout)` | Binary/counting semaphore pattern → `Error!u32` | |
| 222 | +| `clearState(handle)` | Clear pending state → `bool` | |
| 223 | +| `clearBits(handle, bits)` | Clear bits in notification value → previous value | |
| 224 | + |
| 225 | +`Action` enum: `.none`, `.set_bits`, `.increment`, `.set_value_overwrite`, `.set_value_no_overwrite` |
| 226 | + |
| 227 | +### Error Handling |
| 228 | + |
| 229 | +All fallible operations return `freertos.config.Error`: |
| 230 | + |
| 231 | +```zig |
| 232 | +pub const Error = error{ |
| 233 | + OutOfMemory, // Heap exhausted (create functions) |
| 234 | + Timeout, // Operation timed out |
| 235 | + QueueFull, // Queue send failed |
| 236 | + QueueEmpty, // Queue receive failed |
| 237 | + Failure, // Generic pdFAIL |
| 238 | +}; |
| 239 | +``` |
| 240 | + |
| 241 | +Two helper functions convert C return codes to Zig errors: |
| 242 | + |
| 243 | +```zig |
| 244 | +// Check pdPASS/pdFAIL return codes |
| 245 | +try freertos.config.checkBaseType(rc); |
| 246 | +
|
| 247 | +// Convert nullable C handle → Zig error (null → error.OutOfMemory) |
| 248 | +const handle = try freertos.config.checkHandle(c.TaskHandle_t, raw_handle); |
| 249 | +``` |
| 250 | + |
| 251 | +### Raw C Access |
| 252 | + |
| 253 | +For anything not wrapped, use `freertos.c` to access the full FreeRTOS C API directly: |
| 254 | + |
| 255 | +```zig |
| 256 | +const freertos = @import("freertos"); |
| 257 | +
|
| 258 | +// Direct C call |
| 259 | +freertos.c.vTaskDelay(100); |
| 260 | +
|
| 261 | +// Access C constants |
| 262 | +const pass = freertos.c.pdPASS; |
| 263 | +``` |
| 264 | + |
| 265 | +## Examples |
| 266 | + |
| 267 | +| Example | Description | |
| 268 | +|---|---| |
| 269 | +| [`hello_task.zig`](../../examples/raspberrypi/rp2xxx/src/freertos/hello_task.zig) | Minimal — one task, UART logging, periodic delay | |
| 270 | +| [`queue_demo.zig`](../../examples/raspberrypi/rp2xxx/src/freertos/queue_demo.zig) | Producer/consumer pattern with type-safe `Queue(u32)` | |
| 271 | +| [`multitask_demo.zig`](../../examples/raspberrypi/rp2xxx/src/freertos/multitask_demo.zig) | 4 cooperating tasks using queues, mutexes, timers, event groups, notifications, and semaphores | |
| 272 | + |
| 273 | +## Platform Support |
| 274 | + |
| 275 | +| Platform | Chip | Status | Notes | |
| 276 | +|---|---|---|---| |
| 277 | +| RP2040 (Pico) | ARM Cortex-M0+ | ✅ Builds | Not yet hardware-tested | |
| 278 | +| RP2350 ARM (Pico 2) | ARM Cortex-M33 | ✅ Tested on hardware | XIAO RP2350 | |
| 279 | +| RP2350 RISC-V | Hazard3 | ❌ Blocked | Linker symbol issues with RISC-V port | |
| 280 | +| ESP32 | Xtensa/RISC-V | 🔲 Planned | — | |
| 281 | +| STM32 | ARM Cortex-M | 🔲 Planned | — | |
| 282 | + |
| 283 | +## Configuration |
| 284 | + |
| 285 | +Each platform has its own `FreeRTOSConfig.h` under `config/<platform>/`: |
| 286 | + |
| 287 | +``` |
| 288 | +modules/freertos/config/ |
| 289 | +├── RP2040/FreeRTOSConfig.h |
| 290 | +└── RP2350_ARM/FreeRTOSConfig.h |
| 291 | +``` |
| 292 | + |
| 293 | +### Key settings (defaults) |
| 294 | + |
| 295 | +| Setting | Value | Notes | |
| 296 | +|---|---|---| |
| 297 | +| `configTICK_RATE_HZ` | 1000 | 1 ms tick resolution | |
| 298 | +| `configMAX_PRIORITIES` | 32 | Priority levels 0–31 | |
| 299 | +| `configMINIMAL_STACK_SIZE` | 256 | Words (1 KB on 32-bit) | |
| 300 | +| `configTOTAL_HEAP_SIZE` | 128 KB | FreeRTOS heap (heap_4) | |
| 301 | +| `configCHECK_FOR_STACK_OVERFLOW` | 2 | Full stack painting check — traps via `@trap()` | |
| 302 | +| `configSUPPORT_DYNAMIC_ALLOCATION` | 1 | Dynamic only (static not yet supported) | |
| 303 | +| `configNUMBER_OF_CORES` | 1 | Single-core only | |
| 304 | + |
| 305 | +### Pico SDK interop (disabled) |
| 306 | + |
| 307 | +`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. |
| 308 | + |
| 309 | +## Known Limitations |
| 310 | + |
| 311 | +- **Static allocation** — `configSUPPORT_STATIC_ALLOCATION` is off; `xTaskCreateStatic` etc. are not wrapped |
| 312 | +- **Stream/message buffers** — not yet wrapped (use `freertos.c` as a workaround) |
| 313 | +- **Multicore SMP** — `configNUMBER_OF_CORES` is 1; FreeRTOS SMP support is not yet integrated |
| 314 | +- **`.init_array` constructors** — not processed by MicroZig startup, blocking Pico SDK interop |
| 315 | +- **RISC-V** — RP2350 RISC-V port has unresolved linker symbols |
| 316 | + |
| 317 | +## Contributing |
| 318 | + |
| 319 | +FreeRTOS integration is tracked in [**issue #880**](https://github.com/ZigEmbeddedGroup/microzig/issues/880). |
| 320 | + |
| 321 | +Areas that need work: |
| 322 | + |
| 323 | +- Static allocation variants (`*CreateStatic`) |
| 324 | +- Stream and message buffer wrappers |
| 325 | +- Multicore SMP support |
| 326 | +- `.init_array` processing in MicroZig startup |
| 327 | +- Additional platform ports (ESP32, STM32) |
| 328 | +- RISC-V port linker fixes |
0 commit comments