Skip to content

File syn_sched.c

File List > sched > syn_sched.c

Go to the documentation of this file

#if __has_include("syn_config.h")
#include "syn_config.h"
#endif

#if !defined(SYN_USE_SCHED) || SYN_USE_SCHED

#include "../port/syn_port_system.h"
#include "../util/syn_assert.h"
#include "../util/syn_event.h"
#include "../util/syn_metrics.h"
#include "syn_sched.h"

#include <limits.h>

#if SYN_USE_METRICS
SYN_METRIC_DECLARE(sched_ticks, "sched_ticks", "Total scheduler run calls",
                   SYN_METRIC_TYPE_COUNTER);
SYN_METRIC_DECLARE(sched_switches, "sched_switches", "Total task executions",
                   SYN_METRIC_TYPE_COUNTER);
#endif

/* ── Initialization ─────────────────────────────────────────────────────── */

void syn_sched_init(SYN_Sched *sched, SYN_Task *tasks, size_t count)
{
    SYN_ASSERT(sched != NULL);
    SYN_ASSERT(tasks != NULL || count == 0);

    if (sched == NULL || (tasks == NULL && count > 0)) {
        return;
    }

    sched->tasks = tasks;
    sched->task_count = count;

    for (size_t i = 0; i < SYN_SCHED_PRIO_LEVELS; i++) {
        sched->rr_per_prio[i] = 0;
    }

    SYN_METRIC_REGISTER(sched_ticks);
    SYN_METRIC_REGISTER(sched_switches);
}

void syn_task_create(SYN_Task *task, const char *name, SYN_TaskFunc func, uint8_t priority,
                     void *user_data)
{
    SYN_ASSERT(task != NULL);
    SYN_ASSERT(func != NULL);
    SYN_ASSERT(priority < SYN_SCHED_PRIO_LEVELS);

    if (task == NULL || func == NULL || priority >= SYN_SCHED_PRIO_LEVELS) {
        return;
    }

    PT_INIT(&task->pt);
    task->func = func;
    task->name = name;
    task->priority = priority;
    task->state = (uint8_t)SYN_TASK_READY;
    task->delay_until = 0;
    task->user_data = user_data;
    task->wait_event = NULL;
    task->wait_mask = 0;
}

static SYN_PT_Status sched_run_task(SYN_Task *task)
{
    SYN_PT_Status status = task->func(&task->pt, task);

    if (status == PT_EXITED || status == PT_ENDED) {
        task->state = (uint8_t)SYN_TASK_DEAD;
    }
    /* PT_YIELDED and PT_WAITING: caller decides next action */
    return status;
}

/* ── Scheduler tick ─────────────────────────────────────────────────────── */

bool syn_sched_run(SYN_Sched *sched)
{
    SYN_ASSERT(sched != NULL);

    const size_t n = sched->task_count;

    if (n == 0) {
        return false;
    }

    uint32_t now = syn_port_get_tick_ms();
    bool any_alive = false;
    SYN_METRIC_INC(sched_ticks);

    /*
     * Priority scan with retry on PT_WAITING.
     *
     * When a task returns PT_WAITING its condition was false — it couldn't
     * execute.  We set its state to SYN_TASK_WAITING (skipped for the
     * remainder of this tick only) and immediately re-scan for the next
     * best candidate.  This prevents high-priority waiting tasks from
     * starving lower-priority tasks that CAN do useful work.
     *
     * SYN_TASK_WAITING is separate from SYN_TASK_DEFERRED (used by
     * PT_DEFER) — DEFERRED persists across ticks, WAITING is tick-local.
     *
     * Bounded by task_count so we stop if every task is waiting.
     */
    const SYN_Task *executed_task = NULL;
    size_t executed_idx = 0;
    size_t attempts = 0;

    while (attempts < n) {
        SYN_Task *best_task = NULL;
        size_t best_idx = 0;
        uint8_t best_prio = 255;
        size_t best_dist = n; /* Larger than any valid distance */

        for (size_t i = 0; i < n; i++) {
            SYN_Task *task = &sched->tasks[i];

            if (task->state == (uint8_t)SYN_TASK_DEAD) {
                continue;
            }

            any_alive = true;

            if (task->state == (uint8_t)SYN_TASK_SUSPENDED ||
                task->state == (uint8_t)SYN_TASK_DEFERRED ||
                task->state == (uint8_t)SYN_TASK_WAITING) {
                continue;
            }

            /* Blocked on event — check if the event has fired */
            if (task->state == (uint8_t)SYN_TASK_BLOCKED) {
                if (task->wait_event != NULL &&
                    (syn_event_flags_get(task->wait_event) & task->wait_mask)) {
                    /* Event fired — transition to READY */
                    task->wait_event = NULL;
                    task->state = (uint8_t)SYN_TASK_READY;
                    /* Fall through to normal priority evaluation */
                } else {
                    continue; /* Still blocked */
                }
            }

            /* Delay check — signed arithmetic for wraparound safety */
            if (task->delay_until != 0 && (int32_t)(now - task->delay_until) < 0) {
                continue;
            }

            /* Task is ready — compute rotation distance for its priority */
            const uint8_t prio = task->priority;
            size_t rr_start = sched->rr_per_prio[prio];
            if (rr_start >= n) {
                rr_start = 0;
            } /* Defensive clamp */

            const size_t dist = (i >= rr_start) ? (i - rr_start) : (n - rr_start + i);

            if (prio < best_prio || (prio == best_prio && dist < best_dist)) {
                best_prio = prio;
                best_dist = dist;
                best_task = task;
                best_idx = i;
            }
        }

        if (best_task == NULL) {
            break; /* No eligible tasks this tick */
        }

        SYN_PT_Status status = sched_run_task(best_task);

        if (status == PT_WAITING) {
            /* Task's condition was false — it didn't do useful work.
             * Mark WAITING so it's skipped on re-scan, then try the
             * next candidate immediately within this same tick. */
            best_task->state = (uint8_t)SYN_TASK_WAITING;
            attempts++;
            continue;
        }

        /* Task did real work (or deferred itself via PT_DEFER). */
        executed_task = best_task;
        executed_idx = best_idx;
        SYN_METRIC_INC(sched_switches);

        /* Advance round-robin unless the task deferred (PT_DEFER sets
         * state to DEFERRED before yielding — no RR advance). */
        if (best_task->state != (uint8_t)SYN_TASK_DEFERRED) {
            const size_t next = executed_idx + 1;
            sched->rr_per_prio[best_prio] = (next >= n) ? 0 : next;
        }
        break; /* One useful execution per tick */
    }

    /* Clean up task states for the next tick:
     *
     * WAITING  → always cleared (tick-local, used for PT_WAITING retry).
     * DEFERRED → cleared UNLESS it's the task that just ran and deferred
     *            itself (PT_DEFER).  That task must stay DEFERRED so it's
     *            skipped on the NEXT tick — one-tick skip contract. */
    for (size_t i = 0; i < n; i++) {
        const uint8_t st = sched->tasks[i].state;
        if (st == (uint8_t)SYN_TASK_WAITING) {
            sched->tasks[i].state = (uint8_t)SYN_TASK_READY;
        } else if (st == (uint8_t)SYN_TASK_DEFERRED && &sched->tasks[i] != executed_task) {
            sched->tasks[i].state = (uint8_t)SYN_TASK_READY;
        }
    }

    return any_alive;
}

SYN_NORETURN void syn_sched_run_forever(SYN_Sched *sched)
{
    for (;;) {
        syn_sched_run(sched);
    }
}

#if defined(SYN_USE_TICKLESS) && SYN_USE_TICKLESS

uint32_t syn_sched_next_wakeup(const SYN_Sched *sched)
{
    SYN_ASSERT(sched != NULL);

    uint32_t now = syn_port_get_tick_ms();
    uint32_t earliest = UINT32_MAX;
    bool any_ready_now = false;

    for (size_t i = 0; i < sched->task_count; i++) {
        const SYN_Task *task = &sched->tasks[i];

        if (task->state == (uint8_t)SYN_TASK_DEAD || task->state == (uint8_t)SYN_TASK_SUSPENDED) {
            continue;
        }

        /* Blocked on event — check if the event has fired since the
         * last syn_sched_run() (e.g. from an ISR or timer service). */
        if (task->state == (uint8_t)SYN_TASK_BLOCKED) {
            if (task->wait_event != NULL &&
                (syn_event_flags_get(task->wait_event) & task->wait_mask)) {
                any_ready_now = true; /* Event fired — don't sleep */
            }
            continue;
        }

        if (task->delay_until == 0) {
            /* Task is ready immediately (no delay) */
            any_ready_now = true;
            continue;
        }

        /* Check if delay has already passed */
        if ((int32_t)(now - task->delay_until) >= 0) {
            any_ready_now = true;
            continue;
        }

        /* This task is in the future — track the earliest.
         * Cap at UINT32_MAX-1 so a real deadline at counter wrap
         * is never confused with the "no deadlines" sentinel. */
        uint32_t target = task->delay_until;
        if (target == UINT32_MAX)
            target = UINT32_MAX - 1;

        if ((int32_t)(target - earliest) < 0 || earliest == UINT32_MAX) {
            earliest = target;
        }
    }

    /* If any task is ready right now, return 'now' to indicate no sleep */
    if (any_ready_now) {
        return now;
    }

    return earliest;
}

SYN_NORETURN void syn_sched_run_tickless(SYN_Sched *sched, SYN_Sleep *sleep)
{
    SYN_ASSERT(sched != NULL);
    SYN_ASSERT(sleep != NULL);

    for (;;) {
        /* Run the scheduler — returns true if any tasks alive */
        syn_sched_run(sched);

        /* Check if we can sleep */
        uint32_t now = syn_port_get_tick_ms();
        uint32_t wake = syn_sched_next_wakeup(sched);

        /* Only sleep if no tasks are immediately ready */
        if (wake != now && !syn_sleep_any_locked(sleep)) {
            if (wake == UINT32_MAX) {
                /* No deadlines — light sleep until interrupt */
                syn_sleep_enter(sleep);
            } else {
                /* Sleep until the next deadline */
                syn_port_sleep_until(wake);
            }
        }
    }
}

#if defined(SYN_USE_TIMER) && SYN_USE_TIMER

SYN_NORETURN void syn_sched_run_tickless_ex(SYN_Sched *sched, SYN_Sleep *sleep, SYN_Timer *timers,
                                            size_t timer_count)
{
    SYN_ASSERT(sched != NULL);
    SYN_ASSERT(sleep != NULL);

    for (;;) {
        /* Run the scheduler */
        syn_sched_run(sched);

        /* Service software timers */
        syn_timer_service(timers, timer_count);

        /* Compute sleep duration: min of task deadlines and timer expiries */
        uint32_t now = syn_port_get_tick_ms();
        uint32_t task_wake = syn_sched_next_wakeup(sched);
        uint32_t timer_wake = syn_timer_next_expiry(timers, timer_count);

        /* Pick the earlier deadline */
        uint32_t wake = task_wake;
        if (timer_wake != UINT32_MAX && (wake == UINT32_MAX || (int32_t)(timer_wake - wake) < 0)) {
            wake = timer_wake;
        }

        /* Only sleep if nothing is immediately ready */
        if (wake != now && !syn_sleep_any_locked(sleep)) {
            if (wake == UINT32_MAX) {
                /* No deadlines — light sleep until interrupt */
                syn_sleep_enter(sleep);
            } else {
                /* Sleep until the next deadline */
                syn_port_sleep_until(wake);
            }
        }
    }
}

#endif /* SYN_USE_TIMER */

#endif /* SYN_USE_TICKLESS */

/* ── Task control ───────────────────────────────────────────────────────── */

void syn_task_suspend(SYN_Task *task)
{
    SYN_ASSERT(task != NULL);
    if (task->state != (uint8_t)SYN_TASK_DEAD) {
        task->state = (uint8_t)SYN_TASK_SUSPENDED;
    }
}

void syn_task_resume(SYN_Task *task)
{
    SYN_ASSERT(task != NULL);
    if (task->state == (uint8_t)SYN_TASK_SUSPENDED) {
        task->state = (uint8_t)SYN_TASK_READY;
    }
}

void syn_task_restart(SYN_Task *task)
{
    SYN_ASSERT(task != NULL);
    PT_INIT(&task->pt);
    task->delay_until = 0;
    task->wait_event = NULL;
    task->wait_mask = 0;
    task->state = (uint8_t)SYN_TASK_READY;
}

size_t syn_sched_alive_count(const SYN_Sched *sched)
{
    SYN_ASSERT(sched != NULL);

    size_t count = 0;
    for (size_t i = 0; i < sched->task_count; i++) {
        if (sched->tasks[i].state != (uint8_t)SYN_TASK_DEAD) {
            count++;
        }
    }
    return count;
}

#endif /* SYN_USE_SCHED */