From 482cf84d4e60caa402b269bc835ffcc6f50148b0 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Thu, 5 Feb 2026 21:58:15 +0200 Subject: [PATCH 01/13] Update csrs --- port/espressif/esp/src/cpus/esp_riscv.zig | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/port/espressif/esp/src/cpus/esp_riscv.zig b/port/espressif/esp/src/cpus/esp_riscv.zig index dd578576b..89b803641 100644 --- a/port/espressif/esp/src/cpus/esp_riscv.zig +++ b/port/espressif/esp/src/cpus/esp_riscv.zig @@ -615,11 +615,11 @@ pub const csr = struct { pub const tdata1 = riscv32_common.csr.tdata1; pub const tdata2 = riscv32_common.csr.tdata2; pub const tcontrol = Csr(0x7A5, packed struct { - reserved0: u3, - mte: u1, - reserved1: u3, - mpte: u1, - reserved2: u24, + reserved0: u3 = 0, + mte: u1 = 0, + reserved1: u3 = 0, + mpte: u1 = 0, + reserved2: u24 = 0, }); pub const dcsr = riscv32_common.csr.dcsr; @@ -628,23 +628,23 @@ pub const csr = struct { pub const dscratch1 = riscv32_common.csr.dscratch1; pub const mpcer = Csr(0x7E0, packed struct { - cycle: u1, - inst: u1, - ld_hazard: u1, - jmp_hazard: u1, - idle: u1, - load: u1, - store: u1, - jmp_uncond: u1, - branch: u1, - branch_taken: u1, - inst_comp: u1, - reserved0: u21, + cycle: u1 = 0, + inst: u1 = 0, + ld_hazard: u1 = 0, + jmp_hazard: u1 = 0, + idle: u1 = 0, + load: u1 = 0, + store: u1 = 0, + jmp_uncond: u1 = 0, + branch: u1 = 0, + branch_taken: u1 = 0, + inst_comp: u1 = 0, + reserved0: u21 = 0, }); pub const mpcmr = Csr(0x7E1, packed struct { - count_en: u1, - count_sat: u1, - reserved0: u30, + count_en: u1 = 0, + count_sat: u1 = 0, + reserved0: u30 = 0, }); pub const mpccr = Csr(0x7E2, u32); From 93cb4ca918b109da3603527f8a651be739e0a1d4 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Thu, 5 Feb 2026 21:58:54 +0200 Subject: [PATCH 02/13] First pass of RTOS optimizations The priority ordered ready task queue used previously had O(n) complexity for insertions, but when using at most 32 priorities you can optimize this down to O(1) by using one bucket (list) per priority. --- port/espressif/esp/src/hal/rtos.zig | 220 ++++++++++++++++++++++------ 1 file changed, 177 insertions(+), 43 deletions(-) diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 701f42f07..805c7a026 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -41,7 +41,9 @@ const EXTRA_STACK_SIZE = @max(@sizeOf(TrapFrame), 32 * @sizeOf(usize)); pub const Options = struct { enable: bool = false, - Priority: type = enum(u8) { + /// The priority enum to be used by the RTOS. Must define idle, lowest and + /// highest priorities. + Priority: type = enum(u5) { idle = 0, lowest = 1, _, @@ -55,8 +57,15 @@ pub const Options = struct { yield_interrupt: microzig.cpu.Interrupt = .interrupt31, paint_stack_byte: ?u8 = null, + /// Disable the use of buckets (one linked list per priority) for the ready + /// queue. Buckets use a maximum of 260 bytes, but offer a massive speedup. + /// Buckets are disabled automatically if there are more than 32 priorities + /// (the priority enum has a tag type bigger than u5). + ready_queue_force_no_buckets: bool = false, }; +const ready_queue_use_buckets = !rtos_options.ready_queue_force_no_buckets and @bitSizeOf(@typeInfo(Priority).@"enum".tag_type) <= 5; + var main_task: Task = .{ .name = "main", .context = undefined, @@ -78,9 +87,10 @@ var idle_task: Task = .{ var rtos_state: RTOS_State = undefined; pub const RTOS_State = struct { ready_queue: ReadyPriorityQueue = .{}, - timer_queue: std.DoublyLinkedList = .{}, - suspended_list: std.DoublyLinkedList = .{}, - scheduled_for_deletion_list: std.DoublyLinkedList = .{}, + timer_queue: LinkedList(.{ + .use_last = true, + .use_prev = true, + }) = .{}, /// The task in .running state. Safe to access outside of critical section /// as it is always the same for the currently executing task. @@ -93,11 +103,14 @@ pub fn init() void { comptime { if (!microzig.options.cpu.interrupt_stack.enable) @compileError("rtos requires the interrupt stack cpu option to be enabled"); + if (@typeInfo(rtos_options.Priority).@"enum".is_exhaustive) + @compileError("rtos priority enum must be non-exhaustive"); + microzig.cpu.interrupt.expect_handler(rtos_options.general_purpose_interrupt, general_purpose_interrupt_handler); microzig.cpu.interrupt.expect_handler(rtos_options.yield_interrupt, yield_interrupt_handler); } - const cs = microzig.interrupt.enter_critical_section(); + const cs = enter_critical_section(); defer cs.leave(); rtos_state = .{ @@ -218,13 +231,10 @@ pub fn spawn( pub fn make_ready(task: *Task) void { switch (task.state) { .ready, .running, .scheduled_for_deletion => return, - .none => {}, + .none, .suspended => {}, .alarm_set => { rtos_state.timer_queue.remove(&task.node); }, - .suspended => { - rtos_state.suspended_list.remove(&task.node); - }, } task.state = .ready; @@ -263,18 +273,15 @@ fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Ta if (timeout.is_reached_by(.now())) { continue :action .reschedule; } - schedule_wake_at(current_task, timeout); } else { current_task.state = .suspended; - rtos_state.suspended_list.append(¤t_task.node); } }, .delete => { assert(current_task != &idle_task and current_task != &main_task); current_task.state = .scheduled_for_deletion; - rtos_state.scheduled_for_deletion_list.append(¤t_task.node); }, } @@ -351,10 +358,10 @@ pub fn yield_from_isr() void { } pub fn is_a_higher_priority_task_ready() bool { - return if (rtos_state.ready_queue.peek_top()) |top_ready_task| - @intFromEnum(top_ready_task.priority) > @intFromEnum(rtos_state.current_task.priority) - else - false; + const cs = enter_critical_section(); + defer cs.leave(); + + return @intFromEnum(rtos_state.ready_queue.max_ready_priority() orelse .idle) > @intFromEnum(rtos_state.current_task.priority); } pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ @@ -537,7 +544,7 @@ fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) void { while (maybe_node) |node| : (maybe_node = node.next) { const task: *Task = @alignCast(@fieldParentPtr("node", node)); if (ticks.is_reached_by(task.state.alarm_set)) { - rtos_state.timer_queue.insertBefore(&task.node, &sleeping_task.node); + rtos_state.timer_queue.insert_before(&task.node, &sleeping_task.node); break; } } else { @@ -556,13 +563,15 @@ fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) void { } fn sweep_timer_queue() void { - while (rtos_state.timer_queue.popFirst()) |node| { + var now: TimerTicks = .now(); + while (rtos_state.timer_queue.pop()) |node| { const task: *Task = @alignCast(@fieldParentPtr("node", node)); - if (!task.state.alarm_set.is_reached_by(.now())) { + if (!task.state.alarm_set.is_reached_by(now)) { rtos_state.timer_queue.prepend(&task.node); rtos_options.systimer_alarm.set_target(@intFromEnum(task.state.alarm_set)); rtos_options.systimer_alarm.set_enabled(true); - if (task.state.alarm_set.is_reached_by(.now())) + now = .now(); + if (task.state.alarm_set.is_reached_by(now)) continue else break; @@ -574,28 +583,10 @@ fn sweep_timer_queue() void { } } -pub fn log_tasks_info() void { - const cs = microzig.interrupt.enter_critical_section(); +pub fn log_task_info(task: *Task) void { + const cs = enter_critical_section(); defer cs.leave(); - log_task_info(get_current_task()); - - const list: []const ?*std.DoublyLinkedList.Node = &.{ - rtos_state.ready_queue.inner.first, - rtos_state.timer_queue.first, - rtos_state.suspended_list.first, - rtos_state.scheduled_for_deletion_list.first, - }; - for (list) |first| { - var it: ?*std.DoublyLinkedList.Node = first; - while (it) |node| : (it = node.next) { - const task: *Task = @alignCast(@fieldParentPtr("node", node)); - log_task_info(task); - } - } -} - -fn log_task_info(task: *Task) void { if (rtos_options.paint_stack_byte) |paint_byte| { const stack_usage = for (task.stack, 0..) |byte, i| { if (byte != paint_byte) { @@ -630,7 +621,7 @@ pub const Task = struct { state: State = .none, /// Node used for rtos internal lists. - node: std.DoublyLinkedList.Node = .{}, + node: LinkedListNode = .{}, /// Task specific semaphore (required by the wifi driver) semaphore: Semaphore = .init(0, 1), @@ -662,10 +653,50 @@ pub const Context = extern struct { } }; -pub const ReadyPriorityQueue = struct { +pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { + const ReadySet = std.EnumSet(Priority); + + ready: ReadySet = .initEmpty(), + lists: std.EnumArray(Priority, LinkedList(.{ + .use_last = true, + .use_prev = false, + })) = .initFill(.{}), + + pub fn max_ready_priority(pq: *ReadyPriorityQueue) ?Priority { + const raw_prio = pq.ready.bits.findLastSet() orelse return null; + return ReadySet.Indexer.keyForIndex(raw_prio); + } + + pub fn pop(pq: *ReadyPriorityQueue, maybe_more_than_prio: ?Priority) ?*Task { + const prio = pq.max_ready_priority() orelse return null; + if (maybe_more_than_prio) |more_than_prio| { + if (@intFromEnum(prio) <= @intFromEnum(more_than_prio)) { + return null; + } + } + + const bucket = pq.lists.getPtr(prio); + + // We know there is at least one task ready. + const task: *Task = @alignCast(@fieldParentPtr("node", bucket.pop().?)); + + // If there aren't any more tasks inside the current bucket, unset the + // ready bit. + if (bucket.first == null) { + pq.ready.remove(prio); + } + + return task; + } + + pub fn put(pq: *ReadyPriorityQueue, new_task: *Task) void { + pq.lists.getPtr(new_task.priority).append(&new_task.node); + pq.ready.setPresent(new_task.priority, true); + } +} else struct { inner: std.DoublyLinkedList = .{}, - pub fn peek_top(pq: *ReadyPriorityQueue) ?*Task { + fn peek_top(pq: *ReadyPriorityQueue) ?*Task { if (pq.inner.first) |first_node| { return @alignCast(@fieldParentPtr("node", first_node)); } else { @@ -673,6 +704,13 @@ pub const ReadyPriorityQueue = struct { } } + pub fn max_ready_priority(pq: *ReadyPriorityQueue) ?Priority { + return if (pq.peek_top()) |task| + task.priority + else + null; + } + pub fn pop(pq: *ReadyPriorityQueue, maybe_more_than_prio: ?Priority) ?*Task { if (pq.peek_top()) |task| { if (maybe_more_than_prio) |more_than_prio| { @@ -1116,3 +1154,99 @@ pub fn Queue(Elem: type) type { } }; } + +pub const LinkedListNode = struct { + prev: ?*LinkedListNode = null, + next: ?*LinkedListNode = null, +}; + +pub const LinkedListCapabilities = struct { + use_last: bool = true, + use_prev: bool = true, +}; + +pub fn LinkedList(comptime caps: LinkedListCapabilities) type { + return struct { + const Self = @This(); + + first: ?*LinkedListNode = null, + last: if (caps.use_last) ?*LinkedListNode else noreturn = null, + + pub const append = if (caps.use_last) struct { + fn append(ll: *Self, node: *LinkedListNode) void { + if (caps.use_prev) node.prev = ll.last; + node.next = null; + if (ll.last) |last| { + if (caps.use_prev) node.prev = last; + last.next = node; + ll.last = node; + } else { + ll.first = node; + ll.last = node; + } + } + }.append else @compileError("linked list does not support append"); + + pub fn prepend(ll: *Self, node: *LinkedListNode) void { + if (caps.use_prev) { + node.prev = null; + if (ll.first) |first| { + first.prev = node; + } + } + node.next = ll.first; + if (caps.use_last and ll.first == null) { + ll.last = node; + } + ll.first = node; + } + + pub fn pop(ll: *Self) ?*LinkedListNode { + if (ll.first) |first| { + ll.first = first.next; + if (caps.use_last) { + if (ll.last == first) { + ll.last = null; + } + } + return first; + } else return null; + } + + pub const insert_before = if (caps.use_prev) struct { + pub fn insert_before(ll: *Self, existing_node: *LinkedListNode, new_node: *LinkedListNode) void { + new_node.next = existing_node; + if (existing_node.prev) |prev_node| { + // Intermediate node. + new_node.prev = prev_node; + prev_node.next = new_node; + } else { + // First element of the list. + new_node.prev = null; + ll.first = new_node; + } + existing_node.prev = new_node; + } + }.insert_before else @compileError("linked list does not support insert_before"); + + pub const remove = if (caps.use_prev) struct { + pub fn remove(ll: *Self, node: *LinkedListNode) void { + if (node.prev) |prev_node| { + // Intermediate node. + prev_node.next = node.next; + } else { + // First element of the list. + ll.first = node.next; + } + + if (node.next) |next_node| { + // Intermediate node. + next_node.prev = node.prev; + } else { + // Last element of the list. + if (caps.use_last) ll.last = node.prev; + } + } + }.remove else @compileError("linked list does not support remove"); + }; +} From b52a1cbff6d6160f87099a60075e9ca22af31e76 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 09:30:43 +0200 Subject: [PATCH 03/13] Fully switch to custom linked list --- port/espressif/esp/src/hal/rtos.zig | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 805c7a026..d16cc8c17 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -87,10 +87,7 @@ var idle_task: Task = .{ var rtos_state: RTOS_State = undefined; pub const RTOS_State = struct { ready_queue: ReadyPriorityQueue = .{}, - timer_queue: LinkedList(.{ - .use_last = true, - .use_prev = true, - }) = .{}, + timer_queue: DoublyLinkedList = .{}, /// The task in .running state. Safe to access outside of critical section /// as it is always the same for the currently executing task. @@ -228,7 +225,7 @@ pub fn spawn( } /// Must execute inside a critical section. -pub fn make_ready(task: *Task) void { +pub fn make_ready(task: *Task) linksection(".ram_text") void { switch (task.state) { .ready, .running, .scheduled_for_deletion => return, .none, .suspended => {}, @@ -353,11 +350,11 @@ inline fn context_switch(prev_context: *Context, next_context: *Context) void { }); } -pub fn yield_from_isr() void { +pub fn yield_from_isr() linksection(".ram_text") void { rtos_options.cpu_interrupt.set_pending(true); } -pub fn is_a_higher_priority_task_ready() bool { +pub fn is_a_higher_priority_task_ready() linksection(".ram_text") bool { const cs = enter_critical_section(); defer cs.leave(); @@ -537,7 +534,7 @@ pub const general_purpose_interrupt_handler: microzig.cpu.InterruptHandler = .{ }.handler_fn }; /// Must execute inside a critical section. -fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) void { +fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) linksection(".ram_text") void { sleeping_task.state = .{ .alarm_set = ticks }; var maybe_node = rtos_state.timer_queue.first; @@ -562,7 +559,7 @@ fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) void { } } -fn sweep_timer_queue() void { +fn sweep_timer_queue() linksection(".ram_text") void { var now: TimerTicks = .now(); while (rtos_state.timer_queue.pop()) |node| { const task: *Task = @alignCast(@fieldParentPtr("node", node)); @@ -694,7 +691,7 @@ pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { pq.ready.setPresent(new_task.priority, true); } } else struct { - inner: std.DoublyLinkedList = .{}, + inner: DoublyLinkedList = .{}, fn peek_top(pq: *ReadyPriorityQueue) ?*Task { if (pq.inner.first) |first_node| { @@ -729,7 +726,7 @@ pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { while (maybe_node) |node| : (maybe_node = node.next) { const task: *Task = @alignCast(@fieldParentPtr("node", node)); if (@intFromEnum(new_task.priority) > @intFromEnum(task.priority)) { - pq.inner.insertBefore(node, &new_task.node); + pq.inner.insert_before(node, &new_task.node); break; } } else { @@ -763,12 +760,12 @@ pub const TimerTicks = enum(u52) { pub const TimeoutError = error{Timeout}; pub const PriorityWaitQueue = struct { - list: std.DoublyLinkedList = .{}, + list: DoublyLinkedList = .{}, pub const Waiter = struct { task: *Task, priority: Priority, - node: std.DoublyLinkedList.Node = .{}, + node: LinkedListNode = .{}, }; /// Must execute inside a critical section. @@ -1160,6 +1157,11 @@ pub const LinkedListNode = struct { next: ?*LinkedListNode = null, }; +pub const DoublyLinkedList = LinkedList(.{ + .use_last = true, + .use_prev = true, +}); + pub const LinkedListCapabilities = struct { use_last: bool = true, use_prev: bool = true, From 53dc16bfa28bcd59c24128fc5028a6626b8b7a59 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 09:57:35 +0200 Subject: [PATCH 04/13] Add LinkedList tests (and fix esp testing) --- port/espressif/esp/src/hal.zig | 18 +++- port/espressif/esp/src/hal/rtos.zig | 129 ++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/port/espressif/esp/src/hal.zig b/port/espressif/esp/src/hal.zig index da6b6e679..934ede5ac 100644 --- a/port/espressif/esp/src/hal.zig +++ b/port/espressif/esp/src/hal.zig @@ -1,3 +1,4 @@ +const builtin = @import("builtin"); const std = @import("std"); pub const esp_image = @import("esp_image"); @@ -23,8 +24,15 @@ pub const uart = @import("hal/uart.zig"); pub const usb_serial_jtag = @import("hal/usb_serial_jtag.zig"); comptime { - // export atomic intrinsics - _ = @import("hal/atomic.zig"); + if (!builtin.is_test) { + // export atomic intrinsics + _ = @import("hal/atomic.zig"); + + @export(&app_desc, .{ + .name = "esp_app_desc", + .section = ".app_desc", + }); + } } pub const HAL_Options = struct { @@ -89,7 +97,7 @@ fn disable_watchdogs() void { // Don't change the name of this export, it is checked by espflash tool. Only // these fields are populated here. The others will be set by elf2image. -export const esp_app_desc: esp_image.AppDesc linksection(".app_desc") = .{ +const app_desc: esp_image.AppDesc = .{ .secure_version = microzig.options.hal.info.secure_version, .version = str(32, microzig.options.hal.info.version), .project_name = str(32, microzig.options.hal.info.project_name), @@ -105,3 +113,7 @@ fn str(comptime l: usize, comptime s: []const u8) [l]u8 { std.mem.copyForwards(u8, buf[0..s.len], s); return buf; } + +test { + _ = rtos; +} diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index d16cc8c17..209635f54 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -1252,3 +1252,132 @@ pub fn LinkedList(comptime caps: LinkedListCapabilities) type { }.remove else @compileError("linked list does not support remove"); }; } + +test "LinkedList.with_last" { + const expect = std.testing.expect; + const TestNode = struct { + data: i32, + node: LinkedListNode = .{}, + }; + + var list: LinkedList(.{ + .use_prev = false, + .use_last = true, + }) = .{}; + + var n1: TestNode = .{ .data = 1 }; + var n2: TestNode = .{ .data = 2 }; + var n3: TestNode = .{ .data = 3 }; + + // 1. Test Append on empty + list.append(&n1.node); + + // State: [1] + try expect(list.first == &n1.node); + try expect(list.last == &n1.node); + try expect(n1.node.next == null); + + // 2. Test Append on existing + list.append(&n2.node); + + // State: [1, 2] + try expect(list.first == &n1.node); + try expect(list.last == &n2.node); + try expect(n1.node.next == &n2.node); + try expect(n2.node.next == null); + + // 3. Test Prepend + list.prepend(&n3.node); + + // State: [3, 1, 2] + try expect(list.first == &n3.node); + try expect(list.last == &n2.node); + try expect(n3.node.next == &n1.node); + + // 4. Test Pop (FIFO if we pop from head) + const p1 = list.pop(); + + // State: [1, 2] + try expect(p1 == &n3.node); + try expect(list.first == &n1.node); + + if (p1) |node_ptr| { + const parent: *TestNode = @fieldParentPtr("node", node_ptr); + try expect(parent.data == 3); + } + + const p2 = list.pop(); + // State: [2] + try expect(p2 == &n1.node); + try expect(list.first == &n2.node); + try expect(list.last == &n2.node); + + const p3 = list.pop(); + // State: [] + try expect(p3 == &n2.node); + try expect(list.first == null); + try expect(list.last == null); + + // 5. Test Pop on empty + try expect(list.pop() == null); +} + +test "LinkedList.doubly_linked" { + const expect = std.testing.expect; + const TestNode = struct { + data: i32, + node: LinkedListNode = .{}, + }; + + var list: LinkedList(.{ + .use_prev = true, + .use_last = true, + }) = .{}; + + var n1: TestNode = .{ .data = 10 }; + var n2: TestNode = .{ .data = 20 }; + var n3: TestNode = .{ .data = 30 }; + var n4: TestNode = .{ .data = 40 }; + + // 1. Build List + list.append(&n1.node); + list.append(&n2.node); + list.append(&n3.node); + + // State: [10, 20, 30] + try expect(list.first == &n1.node); + try expect(list.last == &n3.node); + try expect(n1.node.next == &n2.node); + try expect(n2.node.prev == &n1.node); // Backward link check + + // 2. Remove Middle Node + list.remove(&n2.node); + + // State: [10, 30] + try expect(n1.node.next == &n3.node); // 10 -> 30 + try expect(n3.node.prev == &n1.node); // 30 <- 10 + try expect(list.first == &n1.node); + try expect(list.last == &n3.node); + + // 3. Insert Before Head + list.insert_before(&n1.node, &n4.node); + + // State: [40, 10, 30] + try expect(list.first == &n4.node); + try expect(n4.node.next == &n1.node); // 40 -> 10 + try expect(n1.node.prev == &n4.node); // 10 <- 40 + + // 4. Remove Tail + list.remove(&n3.node); + + // State: [40, 10] + try expect(list.last == &n1.node); + try expect(n1.node.next == null); + try expect(list.first == &n4.node); + + // 5. Check Parent Pointer + if (list.first) |node_ptr| { + const head_struct: *TestNode = @fieldParentPtr("node", node_ptr); + try expect(head_struct.data == 40); + } +} From abb258122a692532a6800ba97f6ab7cf92c108c7 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 09:59:13 +0200 Subject: [PATCH 05/13] Add esp port unit testing to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f3210d51..1c69a4ee9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,7 +170,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - port_dir: [gigadevice/gd32, raspberrypi/rp2xxx, stmicro/stm32, wch/ch32v] + port_dir: [gigadevice/gd32, espressif/esp, raspberrypi/rp2xxx, stmicro/stm32, wch/ch32v] steps: - name: Checkout uses: actions/checkout@v4 From 17c2a721ca99e207d25b140772ce5812d0dfaaf5 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 10:44:08 +0200 Subject: [PATCH 06/13] Rename pop to pop first --- port/espressif/esp/src/hal/rtos.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 209635f54..659ba123e 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -561,7 +561,7 @@ fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) linksection(".ram_t fn sweep_timer_queue() linksection(".ram_text") void { var now: TimerTicks = .now(); - while (rtos_state.timer_queue.pop()) |node| { + while (rtos_state.timer_queue.pop_first()) |node| { const task: *Task = @alignCast(@fieldParentPtr("node", node)); if (!task.state.alarm_set.is_reached_by(now)) { rtos_state.timer_queue.prepend(&task.node); @@ -675,7 +675,7 @@ pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { const bucket = pq.lists.getPtr(prio); // We know there is at least one task ready. - const task: *Task = @alignCast(@fieldParentPtr("node", bucket.pop().?)); + const task: *Task = @alignCast(@fieldParentPtr("node", bucket.pop_first().?)); // If there aren't any more tasks inside the current bucket, unset the // ready bit. @@ -778,7 +778,7 @@ pub const PriorityWaitQueue = struct { /// Must execute inside a critical section. pub fn wake_all(q: *PriorityWaitQueue) void { - while (q.list.popFirst()) |current_node| { + while (q.list.pop_first()) |current_node| { const current_waiter: *Waiter = @alignCast(@fieldParentPtr("node", current_node)); make_ready(current_waiter.task); } @@ -795,7 +795,7 @@ pub const PriorityWaitQueue = struct { while (it) |current_node| : (it = current_node.next) { const current_waiter: *Waiter = @alignCast(@fieldParentPtr("node", current_node)); if (@intFromEnum(waiter.priority) > @intFromEnum(current_waiter.priority)) { - q.list.insertBefore(¤t_waiter.node, &waiter.node); + q.list.insert_before(¤t_waiter.node, &waiter.node); break; } } else { @@ -1203,7 +1203,7 @@ pub fn LinkedList(comptime caps: LinkedListCapabilities) type { ll.first = node; } - pub fn pop(ll: *Self) ?*LinkedListNode { + pub fn pop_first(ll: *Self) ?*LinkedListNode { if (ll.first) |first| { ll.first = first.next; if (caps.use_last) { @@ -1295,7 +1295,7 @@ test "LinkedList.with_last" { try expect(n3.node.next == &n1.node); // 4. Test Pop (FIFO if we pop from head) - const p1 = list.pop(); + const p1 = list.pop_first(); // State: [1, 2] try expect(p1 == &n3.node); @@ -1306,20 +1306,20 @@ test "LinkedList.with_last" { try expect(parent.data == 3); } - const p2 = list.pop(); + const p2 = list.pop_first(); // State: [2] try expect(p2 == &n1.node); try expect(list.first == &n2.node); try expect(list.last == &n2.node); - const p3 = list.pop(); + const p3 = list.pop_first(); // State: [] try expect(p3 == &n2.node); try expect(list.first == null); try expect(list.last == null); // 5. Test Pop on empty - try expect(list.pop() == null); + try expect(list.pop_first() == null); } test "LinkedList.doubly_linked" { From 3090100157c44085fcaf641887c284f8aeaeec1c Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 11:56:45 +0200 Subject: [PATCH 07/13] More optimizing and refactoring --- port/espressif/esp/src/hal/radio/osi.zig | 2 +- port/espressif/esp/src/hal/rtos.zig | 69 +++++++++++++----------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/port/espressif/esp/src/hal/radio/osi.zig b/port/espressif/esp/src/hal/radio/osi.zig index 0f5649803..da08a564f 100644 --- a/port/espressif/esp/src/hal/radio/osi.zig +++ b/port/espressif/esp/src/hal/radio/osi.zig @@ -673,7 +673,7 @@ pub fn task_delete(handle: ?*anyopaque) callconv(.c) void { if (handle != null) { @panic("task_delete(non-null): not implemented"); } - rtos.yield(.delete); + rtos.yield(.exit); } pub fn task_delay(tick: u32) callconv(.c) void { diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 659ba123e..315df03ae 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -28,9 +28,11 @@ const systimer = @import("systimer.zig"); // yield, tasks are required to have a minimum stack size available at all // times. -// TODO: stack overflow detection // TODO: task joining and deletion // - the idea is that tasks must return before they can be freed +// TODO: trigger context switch if a higher priority is awaken in sync +// primitives +// TODO: stack overflow detection // TODO: direct task signaling // TODO: implement std.Io // TODO: use @stackUpperBound when implemented @@ -180,7 +182,7 @@ pub fn spawn( @ptrFromInt(args_align.forward(@intFromPtr(rtos_state.current_task) + @sizeOf(Task))); @call(.auto, function, context_ptr.*); if (@typeInfo(@TypeOf(function)).@"fn".return_type.? != noreturn) { - yield(.delete); + yield(.exit); unreachable; } } @@ -227,7 +229,7 @@ pub fn spawn( /// Must execute inside a critical section. pub fn make_ready(task: *Task) linksection(".ram_text") void { switch (task.state) { - .ready, .running, .scheduled_for_deletion => return, + .ready, .running, .exited => return, .none, .suspended => {}, .alarm_set => { rtos_state.timer_queue.remove(&task.node); @@ -240,10 +242,9 @@ pub fn make_ready(task: *Task) linksection(".ram_text") void { pub const YieldAction = union(enum) { reschedule, - wait: struct { - timeout: ?TimerTicks = null, - }, - delete, + timeout: TimerTicks, + wait, + exit, }; pub inline fn yield(action: YieldAction) void { @@ -263,26 +264,24 @@ fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Ta current_task.state = .ready; rtos_state.ready_queue.put(current_task); }, - .wait => |wait_action| { + .timeout => |timeout| { assert(current_task != &idle_task); - - if (wait_action.timeout) |timeout| { - if (timeout.is_reached_by(.now())) { - continue :action .reschedule; - } - schedule_wake_at(current_task, timeout); - } else { - current_task.state = .suspended; + if (timeout.is_reached_by(.now())) { + continue :action .reschedule; } + schedule_wake_at(current_task, timeout); }, - .delete => { + .wait => { + assert(current_task != &idle_task); + current_task.state = .suspended; + }, + .exit => { assert(current_task != &idle_task and current_task != &main_task); - - current_task.state = .scheduled_for_deletion; + current_task.state = .exited; }, } - const next_task: *Task = rtos_state.ready_queue.pop(null) orelse @panic("No task ready to run!"); + const next_task: *Task = rtos_state.ready_queue.pop(null).?; next_task.state = .running; rtos_state.current_task = next_task; @@ -293,7 +292,7 @@ fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Ta pub fn sleep(duration: time.Duration) void { const timeout: TimerTicks = .after(duration); while (!timeout.is_reached_by(.now())) - yield(.{ .wait = .{ .timeout = timeout } }); + yield(.{ .timeout = timeout }); } inline fn context_switch(prev_context: *Context, next_context: *Context) void { @@ -535,12 +534,13 @@ pub const general_purpose_interrupt_handler: microzig.cpu.InterruptHandler = .{ /// Must execute inside a critical section. fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) linksection(".ram_text") void { - sleeping_task.state = .{ .alarm_set = ticks }; + sleeping_task.ticks = ticks; + sleeping_task.state = .alarm_set; var maybe_node = rtos_state.timer_queue.first; while (maybe_node) |node| : (maybe_node = node.next) { const task: *Task = @alignCast(@fieldParentPtr("node", node)); - if (ticks.is_reached_by(task.state.alarm_set)) { + if (ticks.is_reached_by(task.ticks)) { rtos_state.timer_queue.insert_before(&task.node, &sleeping_task.node); break; } @@ -563,12 +563,12 @@ fn sweep_timer_queue() linksection(".ram_text") void { var now: TimerTicks = .now(); while (rtos_state.timer_queue.pop_first()) |node| { const task: *Task = @alignCast(@fieldParentPtr("node", node)); - if (!task.state.alarm_set.is_reached_by(now)) { + if (!task.ticks.is_reached_by(now)) { rtos_state.timer_queue.prepend(&task.node); - rtos_options.systimer_alarm.set_target(@intFromEnum(task.state.alarm_set)); + rtos_options.systimer_alarm.set_target(@intFromEnum(task.ticks)); rtos_options.systimer_alarm.set_enabled(true); now = .now(); - if (task.state.alarm_set.is_reached_by(now)) + if (task.ticks.is_reached_by(now)) continue else break; @@ -620,6 +620,11 @@ pub const Task = struct { /// Node used for rtos internal lists. node: LinkedListNode = .{}, + // data: union { + ticks: TimerTicks = undefined, + // awaiter: *Task, + // } = undefined, + /// Task specific semaphore (required by the wifi driver) semaphore: Semaphore = .init(0, 1), @@ -627,9 +632,9 @@ pub const Task = struct { none, ready, running, - alarm_set: TimerTicks, + alarm_set, suspended, - scheduled_for_deletion, + exited, }; }; @@ -802,9 +807,11 @@ pub const PriorityWaitQueue = struct { q.list.append(&waiter.node); } - yield(.{ .wait = .{ - .timeout = maybe_timeout, - } }); + if (maybe_timeout) |timeout| { + yield(.{ .timeout = timeout }); + } else { + yield(.wait); + } q.list.remove(&waiter.node); } From 4dd86cd4b77ad4547a0783f686f83578d2560940 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 12:35:35 +0200 Subject: [PATCH 08/13] Implement task join and free --- port/espressif/esp/src/hal/rtos.zig | 35 +++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 315df03ae..3980d18d9 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -28,8 +28,6 @@ const systimer = @import("systimer.zig"); // yield, tasks are required to have a minimum stack size available at all // times. -// TODO: task joining and deletion -// - the idea is that tasks must return before they can be freed // TODO: trigger context switch if a higher priority is awaken in sync // primitives // TODO: stack overflow detection @@ -226,6 +224,23 @@ pub fn spawn( return task; } +/// Wait for a task to finish and free its memory. The allocator must be the +/// same as the one used for spawning. +pub fn join(allocator: std.mem.Allocator, task: *Task) void { + { + const cs = enter_critical_section(); + defer cs.leave(); + if (task.state != .exited) { + task.awaiter = rtos_state.current_task; + yield(.wait); + } + } + // alloc_size = stack_end - task + const alloc_size = @intFromPtr(task.stack[task.stack.len..].ptr) - @intFromPtr(task); + const alloc: []u8 = @as([*]u8, @ptrCast(task))[0..alloc_size]; + allocator.free(alloc); +} + /// Must execute inside a critical section. pub fn make_ready(task: *Task) linksection(".ram_text") void { switch (task.state) { @@ -278,6 +293,10 @@ fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Ta .exit => { assert(current_task != &idle_task and current_task != &main_task); current_task.state = .exited; + + if (current_task.awaiter) |awaiter| { + make_ready(awaiter); + } }, } @@ -496,7 +515,8 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ }.handler_fn, }; -// Can't be preempted by a higher priority interrupt. +// Can't be preempted by a higher priority interrupt so already in a "critical +// section". fn schedule_in_isr(context: *Context) linksection(".ram_vectors") callconv(.c) void { rtos_options.cpu_interrupt.set_pending(false); @@ -620,15 +640,16 @@ pub const Task = struct { /// Node used for rtos internal lists. node: LinkedListNode = .{}, - // data: union { + /// Ticks for when the task will be awaken. ticks: TimerTicks = undefined, - // awaiter: *Task, - // } = undefined, + + /// Another task waiting for this task to exit. + awaiter: ?*Task = null, /// Task specific semaphore (required by the wifi driver) semaphore: Semaphore = .init(0, 1), - pub const State = union(enum) { + pub const State = enum { none, ready, running, From 25620a018bc81b3774597fb735796ad03a45bd18 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Fri, 6 Feb 2026 17:56:54 +0200 Subject: [PATCH 09/13] Radio timer graceful shutdown --- port/espressif/esp/src/hal/radio.zig | 2 +- port/espressif/esp/src/hal/radio/timer.zig | 26 +++++++++++++++++++--- port/espressif/esp/src/hal/rtos.zig | 4 ++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/port/espressif/esp/src/hal/radio.zig b/port/espressif/esp/src/hal/radio.zig index 6540fbb9f..eed47a990 100644 --- a/port/espressif/esp/src/hal/radio.zig +++ b/port/espressif/esp/src/hal/radio.zig @@ -81,7 +81,7 @@ pub fn deinit() void { return; } - timer.deinit(); + timer.deinit(osi.gpa); } pub fn read_mac(iface: enum { diff --git a/port/espressif/esp/src/hal/radio/timer.zig b/port/espressif/esp/src/hal/radio/timer.zig index 84255f711..f79bf8129 100644 --- a/port/espressif/esp/src/hal/radio/timer.zig +++ b/port/espressif/esp/src/hal/radio/timer.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const c = @import("esp-wifi-driver"); @@ -21,20 +22,36 @@ pub const Timer = struct { node: std.SinglyLinkedList.Node = .{}, }; +var timer_task: ?*rtos.Task = null; +var exit_flag: std.atomic.Value(bool) = .init(false); var reload_semaphore: rtos.Semaphore = .init(0, 1); var mutex: rtos.Mutex = .{}; var timer_list: std.SinglyLinkedList = .{}; pub fn init(gpa: Allocator) Allocator.Error!void { - _ = try rtos.spawn(gpa, task_fn, .{}, .{ + exit_flag.store(false, .monotonic); + + assert(timer_task == null); + timer_task = try rtos.spawn(gpa, task_fn, .{}, .{ .name = "radio_timer", .priority = .lowest, // TODO: what should the priority be? .stack_size = 4096, }); } -pub fn deinit() void { - // TODO: exit mechanism +pub fn deinit(gpa: Allocator) void { + exit_flag.store(true, .monotonic); + reload_semaphore.give(); + + if (timer_task) |task| { + rtos.wait_and_free(gpa, task); + timer_task = null; + } + + while (timer_list.popFirst()) |node| { + const timer: *Timer = @alignCast(@fieldParentPtr("node", node)); + gpa.destroy(timer); + } } pub fn setfn( @@ -112,6 +129,9 @@ pub fn done(gpa: std.mem.Allocator, ets_timer: *c.ets_timer) void { fn task_fn() void { while (true) { + if (exit_flag.load(.monotonic)) + return; + const now = get_time_since_boot(); while (true) { const callback, const arg = blk: { diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 3980d18d9..b4144c420 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -226,7 +226,7 @@ pub fn spawn( /// Wait for a task to finish and free its memory. The allocator must be the /// same as the one used for spawning. -pub fn join(allocator: std.mem.Allocator, task: *Task) void { +pub fn wait_and_free(gpa: std.mem.Allocator, task: *Task) void { { const cs = enter_critical_section(); defer cs.leave(); @@ -238,7 +238,7 @@ pub fn join(allocator: std.mem.Allocator, task: *Task) void { // alloc_size = stack_end - task const alloc_size = @intFromPtr(task.stack[task.stack.len..].ptr) - @intFromPtr(task); const alloc: []u8 = @as([*]u8, @ptrCast(task))[0..alloc_size]; - allocator.free(alloc); + gpa.free(alloc); } /// Must execute inside a critical section. From 7129270d6651bc45822e3c9ee1fb2c61f7a63712 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Sun, 8 Feb 2026 09:40:49 +0200 Subject: [PATCH 10/13] Add tick interrupt and more optimizations --- port/espressif/esp/src/hal/radio.zig | 2 +- port/espressif/esp/src/hal/radio/osi.zig | 18 +- port/espressif/esp/src/hal/radio/timer.zig | 4 +- port/espressif/esp/src/hal/rtos.zig | 445 ++++++++++++--------- 4 files changed, 278 insertions(+), 191 deletions(-) diff --git a/port/espressif/esp/src/hal/radio.zig b/port/espressif/esp/src/hal/radio.zig index eed47a990..7386a2ef1 100644 --- a/port/espressif/esp/src/hal/radio.zig +++ b/port/espressif/esp/src/hal/radio.zig @@ -22,7 +22,7 @@ pub const wifi = @import("radio/wifi.zig"); const log = std.log.scoped(.esp_radio); pub const Options = struct { - interrupt: microzig.cpu.Interrupt = .interrupt29, + interrupt: microzig.cpu.Interrupt = .interrupt30, wifi: wifi.Options = .{}, }; diff --git a/port/espressif/esp/src/hal/radio/osi.zig b/port/espressif/esp/src/hal/radio/osi.zig index da08a564f..24de692a2 100644 --- a/port/espressif/esp/src/hal/radio/osi.zig +++ b/port/espressif/esp/src/hal/radio/osi.zig @@ -148,7 +148,7 @@ pub fn usleep(time_us: u32) callconv(.c) c_int { } pub fn vTaskDelay(ticks: u32) callconv(.c) void { - rtos.sleep(.from_us(ticks)); + rtos.sleep(.from_ticks(ticks)); } pub var WIFI_EVENT: c.esp_event_base_t = "WIFI_EVENT"; @@ -321,8 +321,8 @@ pub fn semphr_take(ptr: ?*anyopaque, tick: u32) callconv(.c) i32 { log.debug("semphr_take {?} {}", .{ ptr, tick }); const sem: *rtos.Semaphore = @ptrCast(@alignCast(ptr)); - const maybe_timeout: ?time.Duration = if (tick == c.OSI_FUNCS_TIME_BLOCKING) - .from_us(tick) + const maybe_timeout: ?rtos.Duration = if (tick == c.OSI_FUNCS_TIME_BLOCKING) + .from_ticks(tick) else null; sem.take_with_timeout(maybe_timeout) catch { @@ -512,8 +512,8 @@ pub fn queue_send(ptr: ?*anyopaque, item_ptr: ?*anyopaque, block_time_tick: u32) else => queue.inner.put( item[0..queue.item_len], 1, - if (block_time_tick == c.OSI_FUNCS_TIME_BLOCKING) - .from_us(block_time_tick) + if (block_time_tick != c.OSI_FUNCS_TIME_BLOCKING) + .from_ticks(block_time_tick) else null, ), @@ -553,8 +553,8 @@ pub fn queue_recv(ptr: ?*anyopaque, item_ptr: ?*anyopaque, block_time_tick: u32) else => queue.inner.get( item[0..queue.item_len], queue.item_len, - if (block_time_tick == c.OSI_FUNCS_TIME_BLOCKING) - .from_us(block_time_tick) + if (block_time_tick != c.OSI_FUNCS_TIME_BLOCKING) + .from_ticks(block_time_tick) else null, ), @@ -679,11 +679,11 @@ pub fn task_delete(handle: ?*anyopaque) callconv(.c) void { pub fn task_delay(tick: u32) callconv(.c) void { log.debug("task_delay {}", .{tick}); - rtos.sleep(.from_us(tick)); + rtos.sleep(.from_ticks(tick)); } pub fn task_ms_to_tick(ms: u32) callconv(.c) i32 { - return @intCast(ms * 1_000); + return @intCast(rtos.Duration.from_ms(ms).to_ticks()); } pub fn task_get_current_task() callconv(.c) ?*anyopaque { diff --git a/port/espressif/esp/src/hal/radio/timer.zig b/port/espressif/esp/src/hal/radio/timer.zig index f79bf8129..3653923af 100644 --- a/port/espressif/esp/src/hal/radio/timer.zig +++ b/port/espressif/esp/src/hal/radio/timer.zig @@ -150,11 +150,11 @@ fn task_fn() void { callback(arg); } - const sleep_duration = blk: { + const sleep_duration: ?rtos.Duration = blk: { mutex.lock(); defer mutex.unlock(); break :blk if (find_next_wake_absolute()) |next_wake_absolute| - next_wake_absolute.diff(now) + .from_us(@truncate(next_wake_absolute.diff(now).to_us())) else null; }; diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index b4144c420..8d0339fb2 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -11,7 +11,6 @@ const TrapFrame = microzig.cpu.TrapFrame; const SYSTEM = microzig.chip.peripherals.SYSTEM; const time = microzig.drivers.time; const rtos_options = microzig.options.hal.rtos; -pub const Priority = rtos_options.Priority; const get_time_since_boot = @import("time.zig").get_time_since_boot; const system = @import("system.zig"); @@ -30,6 +29,8 @@ const systimer = @import("systimer.zig"); // TODO: trigger context switch if a higher priority is awaken in sync // primitives +// TODO: low power mode where tick interrupt is only triggered when necessary +// TODO: investigate tick interrupt assembly and improve generated code // TODO: stack overflow detection // TODO: direct task signaling // TODO: implement std.Io @@ -39,22 +40,38 @@ const systimer = @import("systimer.zig"); const STACK_ALIGN: std.mem.Alignment = .@"16"; const EXTRA_STACK_SIZE = @max(@sizeOf(TrapFrame), 32 * @sizeOf(usize)); +pub const TickFrequency = enum(u32) { + _, + + pub fn from_hz(v: u32) TickFrequency { + assert(v < 100_000); // frequency to high + return @enumFromInt(v); + } + + pub fn from_khz(v: u32) TickFrequency { + return .from_hz(v * 1_000); + } + + fn to_us(comptime freq: TickFrequency) u24 { + return @intFromFloat(1_000_000.0 / @as(f32, @floatFromInt(@intFromEnum(freq)))); + } +}; + pub const Options = struct { enable: bool = false, - /// The priority enum to be used by the RTOS. Must define idle, lowest and - /// highest priorities. - Priority: type = enum(u5) { - idle = 0, - lowest = 1, - _, - - pub const highest: @This() = @enumFromInt(std.math.maxInt(@typeInfo(@This()).@"enum".tag_type)); - }, - general_purpose_interrupt: microzig.cpu.Interrupt = .interrupt30, + + /// How many bits to be used for priority. Highly recommended to be kept + /// less than or equal to 5 to benefit from the use of buckets for the + /// ready task queue. + priority_bits: u5 = 3, + + tick_interrupt: microzig.cpu.Interrupt = .interrupt31, + tick_freq: TickFrequency = .from_khz(1), systimer_unit: systimer.Unit = .unit0, systimer_alarm: systimer.Alarm = .alarm0, cpu_interrupt: system.CPU_Interrupt = .cpu_interrupt_0, - yield_interrupt: microzig.cpu.Interrupt = .interrupt31, + + preempt_same_priority_tasks_on_tick: bool = false, paint_stack_byte: ?u8 = null, /// Disable the use of buckets (one linked list per priority) for the ready @@ -64,6 +81,21 @@ pub const Options = struct { ready_queue_force_no_buckets: bool = false, }; +pub const Priority = enum(@Type(.{ .int = .{ + .bits = rtos_options.priority_bits, + .signedness = .unsigned, +} })) { + idle = 0, + lowest = 1, + _, + + pub const highest: @This() = @enumFromInt(std.math.maxInt(@typeInfo(@This()).@"enum".tag_type)); + + pub fn next_higher(prio: Priority) Priority { + return @enumFromInt(@intFromEnum(prio) +| 1); + } +}; + const ready_queue_use_buckets = !rtos_options.ready_queue_force_no_buckets and @bitSizeOf(@typeInfo(Priority).@"enum".tag_type) <= 5; var main_task: Task = .{ @@ -87,11 +119,19 @@ var idle_task: Task = .{ var rtos_state: RTOS_State = undefined; pub const RTOS_State = struct { ready_queue: ReadyPriorityQueue = .{}, - timer_queue: DoublyLinkedList = .{}, + sleep_queues: [2]DoublyLinkedList = @splat(.{}), + + current_sleep_queue: *DoublyLinkedList, + overflow_sleep_queue: *DoublyLinkedList, /// The task in .running state. Safe to access outside of critical section /// as it is always the same for the currently executing task. current_task: *Task, + + current_ticks: u32 = 0, + overflow_count: u32 = 0, + + just_switched_tasks_cooperatively: bool = false, }; /// Automatically called inside hal startup sequence if it the rtos is enabled @@ -100,11 +140,7 @@ pub fn init() void { comptime { if (!microzig.options.cpu.interrupt_stack.enable) @compileError("rtos requires the interrupt stack cpu option to be enabled"); - if (@typeInfo(rtos_options.Priority).@"enum".is_exhaustive) - @compileError("rtos priority enum must be non-exhaustive"); - - microzig.cpu.interrupt.expect_handler(rtos_options.general_purpose_interrupt, general_purpose_interrupt_handler); - microzig.cpu.interrupt.expect_handler(rtos_options.yield_interrupt, yield_interrupt_handler); + microzig.cpu.interrupt.expect_handler(rtos_options.tick_interrupt, tick_interrupt_handler); } const cs = enter_critical_section(); @@ -112,33 +148,31 @@ pub fn init() void { rtos_state = .{ .current_task = &main_task, + .current_sleep_queue = &rtos_state.sleep_queues[0], + .overflow_sleep_queue = &rtos_state.sleep_queues[1], }; if (rtos_options.paint_stack_byte) |paint_byte| { @memset(&idle_stack, paint_byte); } make_ready(&idle_task); - microzig.cpu.interrupt.map(rtos_options.cpu_interrupt.source(), rtos_options.yield_interrupt); - microzig.cpu.interrupt.set_type(rtos_options.yield_interrupt, .level); - microzig.cpu.interrupt.set_priority(rtos_options.yield_interrupt, .lowest); - microzig.cpu.interrupt.enable(rtos_options.yield_interrupt); - // unit0 is already enabled as it is used by `hal.time`. if (rtos_options.systimer_unit != .unit0) { rtos_options.systimer_unit.apply(.enabled); } rtos_options.systimer_alarm.set_unit(rtos_options.systimer_unit); - rtos_options.systimer_alarm.set_mode(.target); - rtos_options.systimer_alarm.set_enabled(false); + rtos_options.systimer_alarm.set_mode(.period); + rtos_options.systimer_alarm.set_period(comptime @intCast(systimer.ticks_per_us() * rtos_options.tick_freq.to_us())); rtos_options.systimer_alarm.set_interrupt_enabled(true); + rtos_options.systimer_alarm.set_enabled(true); - microzig.cpu.interrupt.map(rtos_options.systimer_alarm.interrupt_source(), rtos_options.general_purpose_interrupt); - microzig.cpu.interrupt.set_type(rtos_options.general_purpose_interrupt, .level); - microzig.cpu.interrupt.set_priority(rtos_options.general_purpose_interrupt, .lowest); - microzig.cpu.interrupt.enable(rtos_options.general_purpose_interrupt); -} + microzig.cpu.interrupt.map(rtos_options.cpu_interrupt.source(), rtos_options.tick_interrupt); + microzig.cpu.interrupt.map(rtos_options.systimer_alarm.interrupt_source(), rtos_options.tick_interrupt); -// TODO: deinit + microzig.cpu.interrupt.set_type(rtos_options.tick_interrupt, .level); + microzig.cpu.interrupt.set_priority(rtos_options.tick_interrupt, .lowest); + microzig.cpu.interrupt.enable(rtos_options.tick_interrupt); +} fn idle() linksection(".ram_text") callconv(.naked) void { // interrupts are initially disabled in new tasks @@ -246,8 +280,11 @@ pub fn make_ready(task: *Task) linksection(".ram_text") void { switch (task.state) { .ready, .running, .exited => return, .none, .suspended => {}, - .alarm_set => { - rtos_state.timer_queue.remove(&task.node); + .sleep_queue_0 => { + rtos_state.sleep_queues[0].remove(&task.node); + }, + .sleep_queue_1 => { + rtos_state.sleep_queues[1].remove(&task.node); }, } @@ -257,7 +294,7 @@ pub fn make_ready(task: *Task) linksection(".ram_text") void { pub const YieldAction = union(enum) { reschedule, - timeout: TimerTicks, + sleep: Duration, wait, exit, }; @@ -279,12 +316,37 @@ fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Ta current_task.state = .ready; rtos_state.ready_queue.put(current_task); }, - .timeout => |timeout| { + .sleep => |sleep_duration| { assert(current_task != &idle_task); - if (timeout.is_reached_by(.now())) { + + const sleep_ticks = sleep_duration.to_ticks(); + if (sleep_ticks == 0) { continue :action .reschedule; } - schedule_wake_at(current_task, timeout); + + const expire_ticks, const overflow = @addWithOverflow(rtos_state.current_ticks, sleep_ticks); + + const sleep_queue = if (overflow == 0) + rtos_state.current_sleep_queue + else + rtos_state.overflow_sleep_queue; + + current_task.ticks = expire_ticks; + current_task.state = if (sleep_queue == &rtos_state.sleep_queues[0]) + .sleep_queue_0 + else + .sleep_queue_1; + + var it = sleep_queue.first; + while (it) |node| : (it = node.next) { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + if (expire_ticks < task.ticks) { + sleep_queue.insert_before(&task.node, ¤t_task.node); + break; + } + } else { + sleep_queue.append(¤t_task.node); + } }, .wait => { assert(current_task != &idle_task); @@ -300,18 +362,22 @@ fn yield_inner(action: YieldAction) linksection(".ram_text") struct { *Task, *Ta }, } - const next_task: *Task = rtos_state.ready_queue.pop(null).?; - + const next_task: *Task = rtos_state.ready_queue.pop(.none).?; next_task.state = .running; + rtos_state.current_task = next_task; + if (rtos_options.preempt_same_priority_tasks_on_tick) { + // Set flag that we already yielded. Don't preempt to an equal priority + // task on the next tick. + rtos_state.just_switched_tasks_cooperatively = true; + } + return .{ current_task, next_task }; } -pub fn sleep(duration: time.Duration) void { - const timeout: TimerTicks = .after(duration); - while (!timeout.is_reached_by(.now())) - yield(.{ .timeout = timeout }); +pub fn sleep(duration: Duration) void { + yield(.{ .sleep = duration }); } inline fn context_switch(prev_context: *Context, next_context: *Context) void { @@ -379,7 +445,7 @@ pub fn is_a_higher_priority_task_ready() linksection(".ram_text") bool { return @intFromEnum(rtos_state.ready_queue.max_ready_priority() orelse .idle) > @intFromEnum(rtos_state.current_task.priority); } -pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ +pub const tick_interrupt_handler: microzig.cpu.InterruptHandler = .{ .naked = struct { pub fn handler_fn() linksection(".ram_vectors") callconv(.naked) void { comptime { @@ -424,8 +490,8 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ \\csrr a0, mepc \\sw a0, 29*4(sp) \\ - \\csrr a0, mstatus - \\sw a0, 30*4(sp) + \\csrr a1, mstatus + \\sw a1, 30*4(sp) \\ // save sp for later \\mv a2, sp @@ -444,7 +510,7 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ \\ // first parameter is a pointer to context \\mv a0, sp - \\jal %[schedule_in_isr] + \\jal %[tick_handler] \\ // load next task context \\lw a1, 0(sp) @@ -460,8 +526,8 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ \\ // ensure interrupts are disabled after mret (when a normal // context switch occured) - \\li a0, 0x80 - \\csrc mstatus, a0 + \\li a2, 0x80 + \\csrc mstatus, a2 \\ // jump to new task \\csrw mepc, a1 @@ -469,8 +535,8 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ \\ \\1: \\ - \\lw a0, 30*4(sp) - \\csrw mstatus, a0 + \\lw a1, 30*4(sp) + \\csrw mstatus, a1 \\ \\lw a0, 29*4(sp) \\csrw mepc, a0 @@ -508,7 +574,7 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ \\addi sp, sp, 32*4 \\mret : - : [schedule_in_isr] "i" (&schedule_in_isr), + : [tick_handler] "i" (&tick_handler), [interrupt_stack_top] "i" (microzig.cpu.interrupt_stack[microzig.cpu.interrupt_stack.len..].ptr), ); } @@ -517,87 +583,61 @@ pub const yield_interrupt_handler: microzig.cpu.InterruptHandler = .{ // Can't be preempted by a higher priority interrupt so already in a "critical // section". -fn schedule_in_isr(context: *Context) linksection(".ram_vectors") callconv(.c) void { - rtos_options.cpu_interrupt.set_pending(false); - - const current_task = rtos_state.current_task; - const ready_task = rtos_state.ready_queue.pop(rtos_state.current_task.priority) orelse return; - - // swap contexts - current_task.context = context.*; - context.* = ready_task.context; - - current_task.state = .ready; - rtos_state.ready_queue.put(current_task); - - ready_task.state = .running; - rtos_state.current_task = ready_task; -} +fn tick_handler(context: *Context) linksection(".ram_vectors") callconv(.c) void { + const status: microzig.cpu.interrupt.Status = .init(); + if (status.is_set(rtos_options.systimer_alarm.interrupt_source())) { + rtos_options.systimer_alarm.clear_interrupt(); -pub const general_purpose_interrupt_handler: microzig.cpu.InterruptHandler = .{ .c = struct { - pub fn handler_fn(_: *TrapFrame) linksection(".ram_text") callconv(.c) void { - var status: microzig.cpu.interrupt.Status = .init(); - if (status.is_set(rtos_options.systimer_alarm.interrupt_source())) { - const cs = enter_critical_section(); - defer cs.leave(); + rtos_state.current_ticks +%= 1; - rtos_options.systimer_alarm.clear_interrupt(); + // if overflow + if (rtos_state.current_ticks == 0) { + @branchHint(.unlikely); - sweep_timer_queue(); + assert(rtos_state.current_sleep_queue.first == null); // the current sleep queue should be empty on an overflow + rtos_state.overflow_count += 1; + std.mem.swap(*DoublyLinkedList, &rtos_state.current_sleep_queue, &rtos_state.overflow_sleep_queue); } - if (is_a_higher_priority_task_ready()) { - yield_from_isr(); + while (rtos_state.current_sleep_queue.first) |node| { + const task: *Task = @alignCast(@fieldParentPtr("node", node)); + if (task.ticks > rtos_state.current_ticks) { + break; + } + _ = rtos_state.current_sleep_queue.pop_first().?; + task.state = .ready; + rtos_state.ready_queue.put(task); } } -}.handler_fn }; -/// Must execute inside a critical section. -fn schedule_wake_at(sleeping_task: *Task, ticks: TimerTicks) linksection(".ram_text") void { - sleeping_task.ticks = ticks; - sleeping_task.state = .alarm_set; - - var maybe_node = rtos_state.timer_queue.first; - while (maybe_node) |node| : (maybe_node = node.next) { - const task: *Task = @alignCast(@fieldParentPtr("node", node)); - if (ticks.is_reached_by(task.ticks)) { - rtos_state.timer_queue.insert_before(&task.node, &sleeping_task.node); - break; - } - } else { - rtos_state.timer_queue.append(&sleeping_task.node); + if (status.is_set(rtos_options.cpu_interrupt.source())) { + rtos_options.cpu_interrupt.set_pending(false); } - // If we updated the first element of the list, it means that we have to - // reschedule the timer - if (rtos_state.timer_queue.first == &sleeping_task.node) { - rtos_options.systimer_alarm.set_target(@intFromEnum(ticks)); - rtos_options.systimer_alarm.set_enabled(true); - if (ticks.is_reached_by(.now())) { - sweep_timer_queue(); - } - } -} + const current_task = rtos_state.current_task; -fn sweep_timer_queue() linksection(".ram_text") void { - var now: TimerTicks = .now(); - while (rtos_state.timer_queue.pop_first()) |node| { - const task: *Task = @alignCast(@fieldParentPtr("node", node)); - if (!task.ticks.is_reached_by(now)) { - rtos_state.timer_queue.prepend(&task.node); - rtos_options.systimer_alarm.set_target(@intFromEnum(task.ticks)); - rtos_options.systimer_alarm.set_enabled(true); - now = .now(); - if (task.ticks.is_reached_by(now)) - continue - else - break; - } - task.state = .ready; - rtos_state.ready_queue.put(task); - } else { - rtos_options.systimer_alarm.set_enabled(false); + // if there is a higher priority task ready switch to it + // if preempt if there is an equal priority task switch to it + const ready_task_constraint: ReadyTaskConstraint = + if (rtos_options.preempt_same_priority_tasks_on_tick and !rtos_state.just_switched_tasks_cooperatively) blk: { + break :blk .{ .at_least_prio = current_task.priority }; + } else .{ .more_than_prio = current_task.priority }; + + if (rtos_options.preempt_same_priority_tasks_on_tick) { + rtos_state.just_switched_tasks_cooperatively = false; } + + const ready_task = rtos_state.ready_queue.pop(ready_task_constraint) orelse return; + + // swap contexts + current_task.context = context.*; + context.* = ready_task.context; + + current_task.state = .ready; + rtos_state.ready_queue.put(current_task); + + ready_task.state = .running; + rtos_state.current_task = ready_task; } pub fn log_task_info(task: *Task) void { @@ -641,7 +681,7 @@ pub const Task = struct { node: LinkedListNode = .{}, /// Ticks for when the task will be awaken. - ticks: TimerTicks = undefined, + ticks: u32 = 0, /// Another task waiting for this task to exit. awaiter: ?*Task = null, @@ -653,7 +693,8 @@ pub const Task = struct { none, ready, running, - alarm_set, + sleep_queue_0, + sleep_queue_1, suspended, exited, }; @@ -676,6 +717,12 @@ pub const Context = extern struct { } }; +pub const ReadyTaskConstraint = union(enum) { + none, + at_least_prio: Priority, + more_than_prio: Priority, +}; + pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { const ReadySet = std.EnumSet(Priority); @@ -690,12 +737,17 @@ pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { return ReadySet.Indexer.keyForIndex(raw_prio); } - pub fn pop(pq: *ReadyPriorityQueue, maybe_more_than_prio: ?Priority) ?*Task { + pub fn pop(pq: *ReadyPriorityQueue, constraint: ReadyTaskConstraint) ?*Task { const prio = pq.max_ready_priority() orelse return null; - if (maybe_more_than_prio) |more_than_prio| { - if (@intFromEnum(prio) <= @intFromEnum(more_than_prio)) { - return null; - } + switch (constraint) { + .none => {}, + inline else => |constraint_priority, tag| { + if ((tag == .at_least_prio and @intFromEnum(prio) < @intFromEnum(constraint_priority)) or + (tag == .more_than_prio and @intFromEnum(prio) <= @intFromEnum(constraint_priority))) + { + return null; + } + }, } const bucket = pq.lists.getPtr(prio); @@ -761,25 +813,45 @@ pub const ReadyPriorityQueue = if (ready_queue_use_buckets) struct { } }; -pub const TimerTicks = enum(u52) { +pub const Duration = enum(u32) { _, - pub fn now() TimerTicks { - return @enumFromInt(rtos_options.systimer_unit.read()); + pub const us_per_tick = rtos_options.tick_freq.to_us(); + pub const ms_per_tick = @max(1, us_per_tick / 1_000); + + pub fn from_us(v: u32) Duration { + return @enumFromInt(v / us_per_tick); + } + + pub fn from_ms(v: u32) Duration { + return @enumFromInt(v / ms_per_tick); + } + + pub fn from_ticks(v: u32) Duration { + return @enumFromInt(v); } - pub fn after(duration: time.Duration) TimerTicks { - return TimerTicks.now().add_duration(duration); + pub fn to_ticks(duration: Duration) u32 { + return @intFromEnum(duration); } +}; - pub fn is_reached_by(a: TimerTicks, b: TimerTicks) bool { - const _a = @intFromEnum(a); - const _b = @intFromEnum(b); - return _b -% _a <= std.math.maxInt(u51); +/// Must be used only from a critical section. +pub const Timeout = struct { + end_ticks: u64, + + pub fn after(duration: Duration) Timeout { + const current_ticks = (@as(u64, rtos_state.overflow_count) << 32) | rtos_state.current_ticks; + return .{ + .end_ticks = current_ticks + duration.to_ticks(), + }; } - pub fn add_duration(ticks: TimerTicks, duration: time.Duration) TimerTicks { - return @enumFromInt(@intFromEnum(ticks) +% @as(u52, @intCast(duration.to_us())) * systimer.ticks_per_us()); + pub fn get_remaining_sleep_duration(timeout: Timeout) ?Duration { + const current_ticks = (@as(u64, rtos_state.overflow_count) << 32) | rtos_state.current_ticks; + const remaining = timeout.end_ticks -| current_ticks; + if (remaining == 0) return null; + return .from_ticks(@truncate(remaining)); } }; @@ -811,7 +883,7 @@ pub const PriorityWaitQueue = struct { } /// Must execute inside a critical section. - pub fn wait(q: *PriorityWaitQueue, task: *Task, maybe_timeout: ?TimerTicks) void { + pub fn wait(q: *PriorityWaitQueue, task: *Task, maybe_timeout_duration: ?Duration) void { var waiter: Waiter = .{ .task = task, .priority = task.priority, @@ -828,8 +900,8 @@ pub const PriorityWaitQueue = struct { q.list.append(&waiter.node); } - if (maybe_timeout) |timeout| { - yield(.{ .timeout = timeout }); + if (maybe_timeout_duration) |duration| { + yield(.{ .sleep = duration }); } else { yield(.wait); } @@ -847,23 +919,24 @@ pub const Mutex = struct { mutex.lock_with_timeout(null) catch unreachable; } - pub fn lock_with_timeout(mutex: *Mutex, maybe_timeout: ?time.Duration) TimeoutError!void { + pub fn lock_with_timeout(mutex: *Mutex, maybe_timeout_after: ?Duration) TimeoutError!void { const cs = enter_critical_section(); defer cs.leave(); const current_task = get_current_task(); - const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| - .after(timeout) + const maybe_timeout: ?Timeout = if (maybe_timeout_after) |duration| + .after(duration) else null; assert(mutex.locked != current_task); while (mutex.locked) |owning_task| { - if (maybe_timeout_ticks) |timeout_ticks| - if (timeout_ticks.is_reached_by(.now())) - return error.Timeout; + const maybe_remaining_duration = if (maybe_timeout) |timeout| + timeout.get_remaining_sleep_duration() orelse return error.Timeout + else + null; // Owning task inherits the priority of the current task if it the // current task has a bigger priority. @@ -874,7 +947,7 @@ pub const Mutex = struct { make_ready(owning_task); } - mutex.wait_queue.wait(current_task, maybe_timeout_ticks); + mutex.wait_queue.wait(current_task, maybe_remaining_duration); } mutex.locked = current_task; @@ -947,21 +1020,21 @@ pub const Semaphore = struct { sem.take_with_timeout(null) catch unreachable; } - pub fn take_with_timeout(sem: *Semaphore, maybe_timeout: ?time.Duration) TimeoutError!void { + pub fn take_with_timeout(sem: *Semaphore, maybe_timeout_after: ?Duration) TimeoutError!void { const cs = enter_critical_section(); defer cs.leave(); - const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| - .after(timeout) + const maybe_timeout: ?Timeout = if (maybe_timeout_after) |duration| + .after(duration) else null; while (sem.current_value <= 0) { - if (maybe_timeout_ticks) |timeout_ticks| - if (timeout_ticks.is_reached_by(.now())) - return error.Timeout; - - sem.wait_queue.wait(rtos_state.current_task, maybe_timeout_ticks); + const maybe_remaining_duration = if (maybe_timeout) |timeout| + timeout.get_remaining_sleep_duration() orelse return error.Timeout + else + null; + sem.wait_queue.wait(rtos_state.current_task, maybe_remaining_duration); } sem.current_value -= 1; @@ -1003,13 +1076,13 @@ pub const TypeErasedQueue = struct { q: *TypeErasedQueue, elements: []const u8, min: usize, - maybe_timeout: ?time.Duration, + maybe_timeout_after: ?Duration, ) usize { assert(elements.len >= min); if (elements.len == 0) return 0; - const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| - .after(timeout) + const maybe_timeout: ?Timeout = if (maybe_timeout_after) |duration| + .after(duration) else null; @@ -1022,11 +1095,11 @@ pub const TypeErasedQueue = struct { n += q.put_non_blocking_from_cs(elements[n..]); if (n >= min) return n; - if (maybe_timeout_ticks) |timeout_ticks| - if (timeout_ticks.is_reached_by(.now())) - return n; - - q.putters.wait(rtos_state.current_task, maybe_timeout_ticks); + const maybe_remaining_duration = if (maybe_timeout) |timeout| + timeout.get_remaining_sleep_duration() orelse return n + else + null; + q.putters.wait(rtos_state.current_task, maybe_remaining_duration); } } @@ -1065,13 +1138,13 @@ pub const TypeErasedQueue = struct { q: *TypeErasedQueue, buffer: []u8, min: usize, - maybe_timeout: ?time.Duration, + maybe_timeout_after: ?Duration, ) usize { assert(buffer.len >= min); if (buffer.len == 0) return 0; - const maybe_timeout_ticks: ?TimerTicks = if (maybe_timeout) |timeout| - .after(timeout) + const maybe_timeout: ?Timeout = if (maybe_timeout_after) |duration| + .after(duration) else null; @@ -1084,11 +1157,11 @@ pub const TypeErasedQueue = struct { n += q.get_non_blocking_from_cs(buffer[n..]); if (n >= min) return n; - if (maybe_timeout_ticks) |timeout_ticks| - if (timeout_ticks.is_reached_by(.now())) - return n; - - q.getters.wait(rtos_state.current_task, maybe_timeout_ticks); + const maybe_remaining_duration = if (maybe_timeout) |timeout| + timeout.get_remaining_sleep_duration() orelse return n + else + null; + q.getters.wait(rtos_state.current_task, maybe_remaining_duration); } } @@ -1132,16 +1205,16 @@ pub fn Queue(Elem: type) type { return .{ .type_erased = .init(@ptrCast(buffer)) }; } - pub fn put(q: *Self, elements: []const Elem, min: usize, timeout: ?time.Duration) usize { + pub fn put(q: *Self, elements: []const Elem, min: usize, timeout: ?Duration) usize { return @divExact(q.type_erased.put(@ptrCast(elements), min * @sizeOf(Elem), timeout), @sizeOf(Elem)); } - pub fn put_all(q: *Self, elements: []const Elem, timeout: ?time.Duration) TimeoutError!void { + pub fn put_all(q: *Self, elements: []const Elem, timeout: ?Duration) TimeoutError!void { if (q.put(elements, elements.len, timeout) != elements.len) return error.Timeout; } - pub fn put_one(q: *Self, item: Elem, timeout: ?time.Duration) TimeoutError!void { + pub fn put_one(q: *Self, item: Elem, timeout: ?Duration) TimeoutError!void { if (q.put(&.{item}, 1, timeout) != 1) return error.Timeout; } @@ -1154,11 +1227,11 @@ pub fn Queue(Elem: type) type { return q.put_non_blocking(@ptrCast(&item)) == 1; } - pub fn get(q: *Self, buffer: []Elem, target: usize, timeout: ?time.Duration) usize { + pub fn get(q: *Self, buffer: []Elem, target: usize, timeout: ?Duration) usize { return @divExact(q.type_erased.get(@ptrCast(buffer), target * @sizeOf(Elem), timeout), @sizeOf(Elem)); } - pub fn get_one(q: *Self, timeout: ?time.Duration) TimeoutError!Elem { + pub fn get_one(q: *Self, timeout: ?Duration) TimeoutError!Elem { var buf: [1]Elem = undefined; if (q.get(&buf, 1, timeout) != 1) return error.Timeout; @@ -1207,7 +1280,6 @@ pub fn LinkedList(comptime caps: LinkedListCapabilities) type { if (caps.use_prev) node.prev = ll.last; node.next = null; if (ll.last) |last| { - if (caps.use_prev) node.prev = last; last.next = node; ll.last = node; } else { @@ -1239,6 +1311,11 @@ pub fn LinkedList(comptime caps: LinkedListCapabilities) type { ll.last = null; } } + if (caps.use_prev) { + if (ll.first) |new_first| { + new_first.prev = null; + } + } return first; } else return null; } @@ -1403,6 +1480,16 @@ test "LinkedList.doubly_linked" { try expect(n1.node.next == null); try expect(list.first == &n4.node); + const n4_popped = list.pop_first().?; + try expect(n1.node.prev == null); + try expect(n1.node.next == null); + try expect(list.first == &n1.node); + try expect(list.last == &n1.node); + + list.insert_before(&n1.node, n4_popped); + + try expect(list.first == &n4.node); + // 5. Check Parent Pointer if (list.first) |node_ptr| { const head_struct: *TestNode = @fieldParentPtr("node", node_ptr); From 291f00fa731aa1f7190486fb6e31678f0dc8e471 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Sun, 8 Feb 2026 09:45:01 +0200 Subject: [PATCH 11/13] Update examples --- examples/espressif/esp/src/rtos.zig | 10 ++++------ examples/espressif/esp/src/tcp_server.zig | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/espressif/esp/src/rtos.zig b/examples/espressif/esp/src/rtos.zig index 73a845ca8..336ef3c55 100644 --- a/examples/espressif/esp/src/rtos.zig +++ b/examples/espressif/esp/src/rtos.zig @@ -12,8 +12,7 @@ const rtos = esp.rtos; pub const microzig_options: microzig.Options = .{ .logFn = usb_serial_jtag.logger.log, .interrupts = .{ - .interrupt30 = rtos.general_purpose_interrupt_handler, - .interrupt31 = rtos.yield_interrupt_handler, + .interrupt31 = rtos.tick_interrupt_handler, }, .log_level = .debug, .cpu = .{ @@ -29,8 +28,6 @@ pub const microzig_options: microzig.Options = .{ }, }; -var heap_buf: [10 * 1024]u8 = undefined; - fn task1(queue: *rtos.Queue(u32)) void { for (0..5) |i| { queue.put_one(i, null) catch unreachable; @@ -39,7 +36,7 @@ fn task1(queue: *rtos.Queue(u32)) void { } pub fn main() !void { - var heap = try microzig.Allocator.init_with_buffer(&heap_buf); + var heap = try microzig.Allocator.init_with_heap(4096); const gpa = heap.allocator(); var buffer: [1]u32 = undefined; @@ -47,7 +44,8 @@ pub fn main() !void { esp.time.sleep_ms(1000); - _ = try rtos.spawn(gpa, task1, .{&queue}, .{}); + const task = try rtos.spawn(gpa, task1, .{&queue}, .{}); + defer rtos.wait_and_free(gpa, task); while (true) { const item = try queue.get_one(.from_ms(1000)); diff --git a/examples/espressif/esp/src/tcp_server.zig b/examples/espressif/esp/src/tcp_server.zig index cba0cfe70..75600ece6 100644 --- a/examples/espressif/esp/src/tcp_server.zig +++ b/examples/espressif/esp/src/tcp_server.zig @@ -25,9 +25,8 @@ pub const microzig_options: microzig.Options = .{ }, .logFn = usb_serial_jtag.logger.log, .interrupts = .{ - .interrupt29 = radio.interrupt_handler, - .interrupt30 = rtos.general_purpose_interrupt_handler, - .interrupt31 = rtos.yield_interrupt_handler, + .interrupt30 = radio.interrupt_handler, + .interrupt31 = rtos.tick_interrupt_handler, }, .cpu = .{ .interrupt_stack = .{ From 6438b1bec82e7da92c556bdc4a7dbc4f7009140a Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Sun, 8 Feb 2026 19:58:07 +0200 Subject: [PATCH 12/13] Address review and error on wrong task return value --- port/espressif/esp/src/hal/rtos.zig | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 8d0339fb2..9e2cabb1e 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -44,7 +44,7 @@ pub const TickFrequency = enum(u32) { _, pub fn from_hz(v: u32) TickFrequency { - assert(v < 100_000); // frequency to high + assert(v < 100_000); // frequency too high return @enumFromInt(v); } @@ -205,6 +205,12 @@ pub fn spawn( const Args = @TypeOf(args); const args_align: std.mem.Alignment = comptime .fromByteUnits(@alignOf(Args)); + const result_type_info = @typeInfo(@typeInfo(@TypeOf(function)).@"fn".return_type.?); + switch (result_type_info) { + .noreturn, .void => {}, + else => @compileError("the return value of an rtos task must be noreturn or void"), + } + const TypeErased = struct { fn call() callconv(.c) void { // interrupts are initially disabled in new tasks @@ -212,8 +218,10 @@ pub fn spawn( const context_ptr: *const Args = @ptrFromInt(args_align.forward(@intFromPtr(rtos_state.current_task) + @sizeOf(Task))); + @call(.auto, function, context_ptr.*); - if (@typeInfo(@TypeOf(function)).@"fn".return_type.? != noreturn) { + + if (result_type_info != .noreturn) { yield(.exit); unreachable; } @@ -680,7 +688,7 @@ pub const Task = struct { /// Node used for rtos internal lists. node: LinkedListNode = .{}, - /// Ticks for when the task will be awaken. + /// Ticks for when the task will wake. ticks: u32 = 0, /// Another task waiting for this task to exit. From 2a5529343ae1997e684407824977eb719ee44447 Mon Sep 17 00:00:00 2001 From: Tudor Andrei Dicu Date: Sun, 8 Feb 2026 20:00:28 +0200 Subject: [PATCH 13/13] Change wording --- port/espressif/esp/src/hal/rtos.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/port/espressif/esp/src/hal/rtos.zig b/port/espressif/esp/src/hal/rtos.zig index 9e2cabb1e..0708f0d5d 100644 --- a/port/espressif/esp/src/hal/rtos.zig +++ b/port/espressif/esp/src/hal/rtos.zig @@ -208,7 +208,7 @@ pub fn spawn( const result_type_info = @typeInfo(@typeInfo(@TypeOf(function)).@"fn".return_type.?); switch (result_type_info) { .noreturn, .void => {}, - else => @compileError("the return value of an rtos task must be noreturn or void"), + else => @compileError("the return type of an rtos task must be noreturn or void"), } const TypeErased = struct {