commit 363a1214e1e469b8b0ab96444f60c507bbceb303 Author: Scott Duensing Date: Mon Feb 23 20:28:38 2026 -0600 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c91406 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.claude/ +*~ +*.exe +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d77a4f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +# Makefile for the cooperative task switching library (DJGPP) +# +# Native DJGPP: make +# Cross-compile: make CC=i586-pc-msdosdjgpp-gcc + +CC = gcc +CFLAGS = -Wall -Wextra -std=gnu99 -O2 +LDFLAGS = + +.PHONY: all clean debug + +all: demo.exe + +debug: CFLAGS = -Wall -Wextra -std=gnu99 -g -O0 +debug: demo.exe + +demo.exe: demo.o taskswitch.o + $(CC) $(LDFLAGS) -o $@ $^ + +%.o: %.c taskswitch.h + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + -rm -f *.o demo.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..878fe29 --- /dev/null +++ b/README.md @@ -0,0 +1,261 @@ +# taskswitch -- Cooperative Task Switching Library for DJGPP + +A lightweight cooperative multitasking library targeting DJGPP (i386 protected +mode DOS). Tasks voluntarily yield the CPU with `tsYield()`. A credit-based +weighted round-robin scheduler ensures every task runs while giving +higher-priority tasks proportionally more CPU time. + +## Files + +| File | Description | +|----------------|------------------------------------------| +| `taskswitch.h` | Public API -- types, constants, functions | +| `taskswitch.c` | Implementation | +| `demo.c` | Example program exercising every feature | +| `Makefile` | Build rules for native DJGPP or cross | + +## Building + +Native DJGPP (from a DOS prompt with DJGPP in the PATH): + +``` +make +``` + +Cross-compiling from Linux (adjust the prefix to match your toolchain): + +``` +make CC=i586-pc-msdosdjgpp-gcc +``` + +Debug build with no optimisation and symbols: + +``` +make debug +``` + +Clean: + +``` +make clean +``` + +## Quick Start + +```c +#include +#include "taskswitch.h" + +void myTask(void *arg) { + const char *name = (const char *)arg; + for (int i = 0; i < 3; i++) { + printf("[%s] working...\n", name); + tsYield(); + } +} + +int main(void) { + tsInit(4); + tsCreate("alpha", myTask, "alpha", 0, TS_PRIORITY_NORMAL); + tsCreate("beta", myTask, "beta", 0, TS_PRIORITY_HIGH); + + for (int i = 0; i < 10; i++) { + tsYield(); + } + + tsShutdown(); + return 0; +} +``` + +## Lifecycle + +1. **`tsInit(maxTasks)`** -- Allocate the task table. The calling context + (typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. `maxTasks` + is the total capacity including this main task slot. + +2. **`tsCreate(...)`** -- Create tasks. Each gets a name, entry function, + argument pointer, stack size (0 for the 8 KB default), and a priority. + +3. **`tsYield()`** -- Call from any task (including main) to hand the CPU to + the next eligible task. + +4. **`tsShutdown()`** -- Free all task stacks and the task table. + +Tasks terminate by returning from their entry function or by calling +`tsExit()`. The main task (id 0) must never call `tsExit()`. + +## API Reference + +### Initialisation and Teardown + +| Function | Signature | Description | +|----------------|------------------------------|--------------------------------------------------------------------------------------| +| `tsInit` | `int32_t tsInit(uint32_t)` | Initialise the library. Returns `TS_OK` or a negative error code. | +| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. | + +### Task Creation and Termination + +| Function | Signature | Description | +|------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE` (8 KB). | +| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. | + +### Scheduling + +| Function | Signature | Description | +|-----------|-----------------------|--------------------------------------------------------------------------| +| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. | + +### Pausing and Resuming + +| Function | Signature | Description | +|------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------| +| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. The main task (id 0) cannot be paused. If a task pauses itself, an implicit yield occurs. | +| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Its credits are refilled to `priority + 1` so it is not penalised for having been paused. | + +### Priority + +| Function | Signature | Description | +|-----------------|--------------------------------------------------|---------------------------------------------------------------------------------------------------| +| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)`| Change a task's priority. Credits are reset to `pri + 1` so the change takes effect immediately. | +| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. | + +### Query + +| Function | Signature | Description | +|-----------------|-----------------------------------------|---------------------------------------------------------------| +| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. | +| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. | +| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. | +| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. | + +## Constants + +### Error Codes + +| Name | Value | Meaning | +|----------------|-------|--------------------------------------| +| `TS_OK` | 0 | Success | +| `TS_ERR_INIT` | -1 | Library not initialised | +| `TS_ERR_PARAM` | -2 | Invalid parameter | +| `TS_ERR_FULL` | -3 | Task table full | +| `TS_ERR_NOMEM` | -4 | Memory allocation failed | +| `TS_ERR_STATE` | -5 | Invalid state transition | + +### Priority Presets + +| Name | Value | Credits per Round | +|----------------------|-------|-------------------| +| `TS_PRIORITY_LOW` | 0 | 1 | +| `TS_PRIORITY_NORMAL` | 5 | 6 | +| `TS_PRIORITY_HIGH` | 10 | 11 | + +Any non-negative `int32_t` may be used as a priority. The presets are +provided for convenience. + +### Defaults + +| Name | Value | Description | +|-------------------------|-------|-------------------------| +| `TS_DEFAULT_STACK_SIZE` | 8192 | Default stack per task | +| `TS_NAME_MAX` | 32 | Max task name length | + +## Types + +### `TaskStateE` + +```c +typedef enum { + TaskStateReady = 0, // Eligible for scheduling + TaskStateRunning = 1, // Currently executing + TaskStatePaused = 2, // Suspended until tsResume() + TaskStateTerminated = 3 // Finished; slot cannot be reused +} TaskStateE; +``` + +### `TaskEntryT` + +```c +typedef void (*TaskEntryT)(void *arg); +``` + +The signature every task entry function must follow. `arg` is the pointer +passed to `tsCreate`. + +## Scheduler Details + +The scheduler is a **credit-based weighted round-robin**. + +1. Every ready task holds a credit counter initialised to `priority + 1`. +2. When `tsYield()` is called, the scheduler scans tasks starting one past + the current task (wrapping around) looking for a ready task with + credits > 0. When found, that task's credits are decremented and it + becomes the running task. +3. When **no** ready task has credits remaining, every ready task is + refilled to `priority + 1` and the scan repeats. + +This means a priority-10 task receives 11 turns for every 1 turn a +priority-0 task receives, but the low-priority task still runs -- it is +never starved. + +Credits are also refilled when: + +- A task is **created** (`tsCreate`) -- starts with `priority + 1`. +- A task is **resumed** (`tsResume`) -- refilled so it is not penalised. +- A task's **priority changes** (`tsSetPriority`) -- reset to `new + 1`. + +## Context Switch Internals + +Context switching is performed entirely in i386 inline assembly. Six values +are saved and restored per switch: + +| Register | Offset | Purpose | +|----------|--------|-----------------------------------------| +| EBX | 0 | Callee-saved general purpose | +| ESI | 4 | Callee-saved general purpose | +| EDI | 8 | Callee-saved general purpose | +| EBP | 12 | Frame pointer | +| ESP | 16 | Stack pointer | +| EIP | 20 | Resume address (captured as local label)| + +The save and restore pointers are passed into the assembly block via GCC +register constraints (`%eax` and `%edx`). Segment registers are not +saved because DJGPP runs in a flat 32-bit protected-mode environment where +CS, DS, ES, and SS share the same base. + +New tasks have their initial ESP pointed at a 16-byte-aligned region at the +top of a `malloc`'d stack, with EIP set to an internal trampoline that calls +the user's entry function and then `tsExit()`. + +## Limitations + +- **Cooperative only** -- tasks must call `tsYield()` (or `tsPause`/`tsExit`) + to allow other tasks to run. A task that never yields blocks everything. +- **No task slot reuse** -- once a task terminates, its slot in the task table + cannot be reclaimed. The `maxTasks` value passed to `tsInit` is a lifetime + limit. +- **Not interrupt-safe** -- the library uses no locking or `volatile` module + state. Do not call library functions from interrupt handlers. +- **Single-threaded** -- designed for one CPU under DOS protected mode. +- **Stack overflow is not detected** -- size the stack appropriately for each + task's needs. + +## Demo + +`demo.c` exercises four phases: + +1. **Priority scheduling** -- creates tasks at low, normal, and high priority. + All tasks run, but the high-priority task gets significantly more turns. +2. **Pause** -- pauses one task mid-run and shows it stops being scheduled. +3. **Resume** -- resumes the paused task and shows it picks up where it + left off. +4. **Priority boost** -- raises the low-priority task above all others and + shows it immediately gets more turns. + +Build and run: + +``` +make +demo +``` diff --git a/RUNDEMO.BAT b/RUNDEMO.BAT new file mode 100644 index 0000000..14250fd --- /dev/null +++ b/RUNDEMO.BAT @@ -0,0 +1,2 @@ +@ECHO OFF +DEMO.EXE > OUTPUT.TXT diff --git a/demo.c b/demo.c new file mode 100644 index 0000000..9fe3afb --- /dev/null +++ b/demo.c @@ -0,0 +1,95 @@ +// demo.c -- Demonstration of the cooperative task switching library +// +// Shows priority scheduling, round-robin, pausing/resuming, and +// dynamic priority changes. + +#include +#include "taskswitch.h" + +// Forward declarations +static void pauseTarget(void *arg); +static void worker(void *arg); + + +static void pauseTarget(void *arg) { + (void)arg; + for (int32_t i = 0; i < 10; i++) { + printf(" [pauser] iteration %d\n", (int)i); + tsYield(); + } + printf(" [pauser] finished\n"); +} + + +static void worker(void *arg) { + const char *name = (const char *)arg; + int32_t pri = tsGetPriority(tsCurrentId()); + + for (int32_t i = 0; i < 5; i++) { + printf(" [%s] step %d (priority %d)\n", name, (int)i, (int)pri); + tsYield(); + } + printf(" [%s] finished\n", name); +} + + +int main(void) { + printf("=== Cooperative Task Switching Demo ===\n\n"); + + if (tsInit(8) != TS_OK) { + printf("ERROR: failed to initialize task system\n"); + return 1; + } + + int32_t tHigh = tsCreate("high", worker, "high", 0, TS_PRIORITY_HIGH); + int32_t tNorm = tsCreate("normal", worker, "normal", 0, TS_PRIORITY_NORMAL); + int32_t tLow = tsCreate("low", worker, "low", 0, TS_PRIORITY_LOW); + int32_t tPauser = tsCreate("pauser", pauseTarget, NULL, 0, TS_PRIORITY_NORMAL); + + if (tHigh < 0 || tNorm < 0 || tLow < 0 || tPauser < 0) { + printf("ERROR: failed to create tasks\n"); + tsShutdown(); + return 1; + } + + printf("Created: high=%d normal=%d low=%d pauser=%d\n\n", (int)tHigh, (int)tNorm, (int)tLow, (int)tPauser); + + // Phase 1: all tasks run, but high-priority tasks get more turns per round. + // Low (priority 0) gets 1 turn, normal (5) gets 6, high (10) gets 11. + printf("--- Phase 1: Priority scheduling ---\n"); + for (int32_t i = 0; i < 6; i++) { + printf("[main] yield %d\n", (int)i); + tsYield(); + } + + // Phase 2: pause the pauser task so it stops being scheduled + printf("\n--- Phase 2: Pause 'pauser' ---\n"); + tsPause((uint32_t)tPauser); + printf("[main] pauser state: %d (expect %d=paused)\n", tsGetState((uint32_t)tPauser), TaskStatePaused); + for (int32_t i = 0; i < 4; i++) { + printf("[main] yield %d (pauser is paused)\n", (int)i); + tsYield(); + } + + // Phase 3: resume pauser + printf("\n--- Phase 3: Resume 'pauser' ---\n"); + tsResume((uint32_t)tPauser); + for (int32_t i = 0; i < 4; i++) { + printf("[main] yield %d\n", (int)i); + tsYield(); + } + + // Phase 4: boost low to the highest priority so it gets more turns + printf("\n--- Phase 4: Boost 'low' to highest ---\n"); + tsSetPriority((uint32_t)tLow, TS_PRIORITY_HIGH + 5); + for (int32_t i = 0; i < 6; i++) { + printf("[main] yield %d\n", (int)i); + tsYield(); + } + + printf("\nActive tasks: %u\n", (unsigned)tsActiveCount()); + printf("=== Demo complete ===\n"); + + tsShutdown(); + return 0; +} diff --git a/dosbox-x.conf b/dosbox-x.conf new file mode 100644 index 0000000..d279be1 --- /dev/null +++ b/dosbox-x.conf @@ -0,0 +1,13 @@ +[dosbox] +memsize = 16 + +[cpu] +cycles = max + +[autoexec] +@ECHO OFF +MOUNT C . +C: +DEMO.EXE +PAUSE +EXIT diff --git a/taskswitch.c b/taskswitch.c new file mode 100644 index 0000000..70cd8a3 --- /dev/null +++ b/taskswitch.c @@ -0,0 +1,457 @@ +// taskswitch.c -- Cooperative task switching library for DJGPP +// +// Uses inline assembly for context switching (i386 and x86_64). The +// scheduler uses credit-based weighted round-robin so all tasks run, +// but higher-priority tasks run proportionally more often. + +#include "taskswitch.h" +#include +#include + +// ============================================================================ +// Internal types +// ============================================================================ + +#if defined(__x86_64__) +// Saved CPU context for x86_64 (field order matches asm byte offsets) +typedef struct { + uintptr_t rbx; // offset 0 + uintptr_t r12; // offset 8 + uintptr_t r13; // offset 16 + uintptr_t r14; // offset 24 + uintptr_t r15; // offset 32 + uintptr_t rbp; // offset 40 + uintptr_t rsp; // offset 48 + uintptr_t rip; // offset 56 +} TaskContextT; +#else +// Saved CPU context for i386 (field order matches asm byte offsets) +typedef struct { + uintptr_t ebx; // offset 0 + uintptr_t esi; // offset 4 + uintptr_t edi; // offset 8 + uintptr_t ebp; // offset 12 + uintptr_t esp; // offset 16 + uintptr_t eip; // offset 20 +} TaskContextT; +#endif + +// Task control block +typedef struct { + char name[TS_NAME_MAX]; + TaskContextT context; + uint8_t *stack; + uint32_t stackSize; + TaskStateE state; + int32_t priority; + int32_t credits; + TaskEntryT entry; + void *arg; + bool isMain; +} TaskBlockT; + +// ============================================================================ +// Module state +// ============================================================================ + +static TaskBlockT *tasks = NULL; +static uint32_t taskCapacity = 0; +static uint32_t taskCount = 0; +static uint32_t currentIdx = 0; +static bool initialized = false; + +// ============================================================================ +// Forward declarations +// ============================================================================ + +// Static helpers +static void contextSwitch(TaskContextT *save, TaskContextT *restore); +static uint32_t scheduleNext(void); +static void taskTrampoline(void); + +// Public API prototypes are provided by taskswitch.h via #include. +// Explicit prototypes repeated here per project convention: +uint32_t tsActiveCount(void); +int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority); +uint32_t tsCurrentId(void); +void tsExit(void); +const char *tsGetName(uint32_t taskId); +int32_t tsGetPriority(uint32_t taskId); +TaskStateE tsGetState(uint32_t taskId); +int32_t tsInit(uint32_t maxTasks); +int32_t tsPause(uint32_t taskId); +int32_t tsResume(uint32_t taskId); +int32_t tsSetPriority(uint32_t taskId, int32_t priority); +void tsShutdown(void); +void tsYield(void); + +// ============================================================================ +// Static functions (alphabetical) +// ============================================================================ + +// Switch execution from the current task to another by saving and restoring +// callee-saved registers and the stack pointer. The return address is +// captured as a local label so that when another task switches back to us, +// execution resumes right after the save point. +#if defined(__x86_64__) +// x86_64: save rbx, r12-r15, rbp, rsp, rip. +// Inputs via GCC constraints: %rdi = save ptr, %rsi = restore ptr. +static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskContextT *restore) { + __asm__ __volatile__( + // Save current context + "movq %%rbx, 0(%%rdi)\n\t" + "movq %%r12, 8(%%rdi)\n\t" + "movq %%r13, 16(%%rdi)\n\t" + "movq %%r14, 24(%%rdi)\n\t" + "movq %%r15, 32(%%rdi)\n\t" + "movq %%rbp, 40(%%rdi)\n\t" + "movq %%rsp, 48(%%rdi)\n\t" + "leaq 1f(%%rip), %%rax\n\t" + "movq %%rax, 56(%%rdi)\n\t" + // Restore new context + "movq 0(%%rsi), %%rbx\n\t" + "movq 8(%%rsi), %%r12\n\t" + "movq 16(%%rsi), %%r13\n\t" + "movq 24(%%rsi), %%r14\n\t" + "movq 32(%%rsi), %%r15\n\t" + "movq 40(%%rsi), %%rbp\n\t" + "movq 48(%%rsi), %%rsp\n\t" + "movq 56(%%rsi), %%rax\n\t" + "jmp *%%rax\n\t" + "1:\n\t" + : + : "D" (save), "S" (restore) + : "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "memory", "cc" + ); +} +#else +// i386: save ebx, esi, edi, ebp, esp, eip. +// Inputs via GCC constraints: %eax = save ptr, %edx = restore ptr. +static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskContextT *restore) { + __asm__ __volatile__( + // Save current context + "movl %%ebx, 0(%%eax)\n\t" + "movl %%esi, 4(%%eax)\n\t" + "movl %%edi, 8(%%eax)\n\t" + "movl %%ebp, 12(%%eax)\n\t" + "movl %%esp, 16(%%eax)\n\t" + "movl $1f, 20(%%eax)\n\t" + // Restore new context + "movl 0(%%edx), %%ebx\n\t" + "movl 4(%%edx), %%esi\n\t" + "movl 8(%%edx), %%edi\n\t" + "movl 12(%%edx), %%ebp\n\t" + "movl 16(%%edx), %%esp\n\t" + "movl 20(%%edx), %%eax\n\t" + "jmp *%%eax\n\t" + "1:\n\t" + : + : "a" (save), "d" (restore) + : "ecx", "memory", "cc" + ); +} +#endif + + +// Find the next task to run using credit-based weighted round-robin. +// Each ready task holds (priority + 1) credits. One credit is consumed +// per scheduling turn. When no ready task has credits left, every +// ready task is refilled. This guarantees all tasks run while giving +// higher-priority tasks proportionally more turns. +static uint32_t scheduleNext(void) { + // First pass: look for a ready task with remaining credits + for (uint32_t i = 1; i <= taskCount; i++) { + uint32_t idx = (currentIdx + i) % taskCount; + if (tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) { + tasks[idx].credits--; + return idx; + } + } + + // All credits exhausted -- refill every ready task + bool anyReady = false; + for (uint32_t i = 0; i < taskCount; i++) { + if (tasks[i].state == TaskStateReady) { + tasks[i].credits = tasks[i].priority + 1; + anyReady = true; + } + } + + if (!anyReady) { + return currentIdx; + } + + // Pick the first ready task after refill + for (uint32_t i = 1; i <= taskCount; i++) { + uint32_t idx = (currentIdx + i) % taskCount; + if (tasks[idx].state == TaskStateReady && tasks[idx].credits > 0) { + tasks[idx].credits--; + return idx; + } + } + + return currentIdx; +} + + +// Entry point for every new task. Calls the user-supplied function and +// then terminates the task when it returns. +static void taskTrampoline(void) { + TaskBlockT *task = &tasks[currentIdx]; + task->entry(task->arg); + tsExit(); +} + +// ============================================================================ +// Public API (alphabetical, main-equivalent functions last if applicable) +// ============================================================================ + +uint32_t tsActiveCount(void) { + if (!initialized) { + return 0; + } + + uint32_t count = 0; + for (uint32_t i = 0; i < taskCount; i++) { + if (tasks[i].state != TaskStateTerminated) { + count++; + } + } + return count; +} + + +int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority) { + if (!initialized || !entry) { + return TS_ERR_PARAM; + } + if (taskCount >= taskCapacity) { + return TS_ERR_FULL; + } + if (stackSize == 0) { + stackSize = TS_DEFAULT_STACK_SIZE; + } + + uint32_t id = taskCount; + TaskBlockT *task = &tasks[id]; + + task->stack = (uint8_t *)malloc(stackSize); + if (!task->stack) { + return TS_ERR_NOMEM; + } + + if (name) { + strncpy(task->name, name, TS_NAME_MAX - 1); + task->name[TS_NAME_MAX - 1] = '\0'; + } else { + task->name[0] = '\0'; + } + + task->stackSize = stackSize; + task->state = TaskStateReady; + task->priority = priority; + task->credits = priority + 1; + task->entry = entry; + task->arg = arg; + task->isMain = false; + + // Set up initial stack (grows downward, 16-byte aligned) + uintptr_t top = (uintptr_t)(task->stack + stackSize); + top &= ~(uintptr_t)0xF; + top -= sizeof(uintptr_t); + *(uintptr_t *)top = 0; // dummy return address; trampoline never returns + +#if defined(__x86_64__) + task->context.rsp = top; + task->context.rbp = 0; + task->context.rbx = 0; + task->context.r12 = 0; + task->context.r13 = 0; + task->context.r14 = 0; + task->context.r15 = 0; + task->context.rip = (uintptr_t)taskTrampoline; +#else + task->context.esp = top; + task->context.ebp = 0; + task->context.ebx = 0; + task->context.esi = 0; + task->context.edi = 0; + task->context.eip = (uintptr_t)taskTrampoline; +#endif + + taskCount++; + return (int32_t)id; +} + + +uint32_t tsCurrentId(void) { + return currentIdx; +} + + +void tsExit(void) { + if (!initialized || tasks[currentIdx].isMain) { + return; + } + + tasks[currentIdx].state = TaskStateTerminated; + + uint32_t next = scheduleNext(); + uint32_t prev = currentIdx; + + currentIdx = next; + tasks[next].state = TaskStateRunning; + + contextSwitch(&tasks[prev].context, &tasks[next].context); + // Terminated task never resumes here +} + + +const char *tsGetName(uint32_t taskId) { + if (!initialized || taskId >= taskCount) { + return NULL; + } + return tasks[taskId].name; +} + + +int32_t tsGetPriority(uint32_t taskId) { + if (!initialized || taskId >= taskCount) { + return TS_ERR_PARAM; + } + return tasks[taskId].priority; +} + + +TaskStateE tsGetState(uint32_t taskId) { + if (!initialized || taskId >= taskCount) { + return TaskStateTerminated; + } + return tasks[taskId].state; +} + + +int32_t tsInit(uint32_t maxTasks) { + if (initialized || maxTasks < 1) { + return TS_ERR_PARAM; + } + + tasks = (TaskBlockT *)calloc(maxTasks, sizeof(TaskBlockT)); + if (!tasks) { + return TS_ERR_NOMEM; + } + + taskCapacity = maxTasks; + taskCount = 1; + currentIdx = 0; + + strncpy(tasks[0].name, "main", TS_NAME_MAX - 1); + tasks[0].state = TaskStateRunning; + tasks[0].priority = TS_PRIORITY_NORMAL; + tasks[0].credits = TS_PRIORITY_NORMAL + 1; + tasks[0].isMain = true; + tasks[0].stack = NULL; + + initialized = true; + return TS_OK; +} + + +int32_t tsPause(uint32_t taskId) { + if (!initialized || taskId >= taskCount) { + return TS_ERR_PARAM; + } + if (tasks[taskId].isMain) { + return TS_ERR_STATE; + } + if (tasks[taskId].state != TaskStateRunning && tasks[taskId].state != TaskStateReady) { + return TS_ERR_STATE; + } + + tasks[taskId].state = TaskStatePaused; + + // If we paused ourselves, yield immediately + if (taskId == currentIdx) { + uint32_t next = scheduleNext(); + if (next != currentIdx) { + uint32_t prev = currentIdx; + + currentIdx = next; + tasks[next].state = TaskStateRunning; + + contextSwitch(&tasks[prev].context, &tasks[next].context); + } + } + + return TS_OK; +} + + +int32_t tsResume(uint32_t taskId) { + if (!initialized || taskId >= taskCount) { + return TS_ERR_PARAM; + } + if (tasks[taskId].state != TaskStatePaused) { + return TS_ERR_STATE; + } + + tasks[taskId].state = TaskStateReady; + tasks[taskId].credits = tasks[taskId].priority + 1; + return TS_OK; +} + + +int32_t tsSetPriority(uint32_t taskId, int32_t priority) { + if (!initialized || taskId >= taskCount) { + return TS_ERR_PARAM; + } + if (tasks[taskId].state == TaskStateTerminated) { + return TS_ERR_STATE; + } + + tasks[taskId].priority = priority; + tasks[taskId].credits = priority + 1; + return TS_OK; +} + + +void tsShutdown(void) { + if (!initialized) { + return; + } + + for (uint32_t i = 0; i < taskCount; i++) { + free(tasks[i].stack); + } + + free(tasks); + + tasks = NULL; + taskCapacity = 0; + taskCount = 0; + currentIdx = 0; + initialized = false; +} + + +void tsYield(void) { + if (!initialized) { + return; + } + + uint32_t next = scheduleNext(); + if (next == currentIdx) { + return; + } + + uint32_t prev = currentIdx; + + if (tasks[prev].state == TaskStateRunning) { + tasks[prev].state = TaskStateReady; + } + + currentIdx = next; + tasks[next].state = TaskStateRunning; + + contextSwitch(&tasks[prev].context, &tasks[next].context); +} diff --git a/taskswitch.h b/taskswitch.h new file mode 100644 index 0000000..96e4cbb --- /dev/null +++ b/taskswitch.h @@ -0,0 +1,85 @@ +// taskswitch.h -- Cooperative task switching library for DJGPP +// +// Credit-based cooperative multitasking with task pausing. +// Each task receives (priority + 1) credits per scheduling round. +// Tasks run round-robin, consuming one credit per turn. When all +// credits are spent, every ready task is refilled. Higher-priority +// tasks run proportionally more often but never starve lower ones. + +#ifndef TASKSWITCH_H +#define TASKSWITCH_H + +#include +#include + +// Error codes +#define TS_OK 0 +#define TS_ERR_INIT (-1) +#define TS_ERR_PARAM (-2) +#define TS_ERR_FULL (-3) +#define TS_ERR_NOMEM (-4) +#define TS_ERR_STATE (-5) + +// Defaults +#define TS_DEFAULT_STACK_SIZE 8192 +#define TS_NAME_MAX 32 + +// Priority levels +#define TS_PRIORITY_LOW 0 +#define TS_PRIORITY_NORMAL 5 +#define TS_PRIORITY_HIGH 10 + +// Task states +typedef enum { + TaskStateReady = 0, + TaskStateRunning = 1, + TaskStatePaused = 2, + TaskStateTerminated = 3 +} TaskStateE; + +// Task entry function signature +typedef void (*TaskEntryT)(void *arg); + +// Initialize the task system. The calling context becomes task 0 (main). +// maxTasks includes the main task slot. +int32_t tsInit(uint32_t maxTasks); + +// Shut down the task system and free all resources. +void tsShutdown(void); + +// Create a new task. Pass 0 for stackSize to use TS_DEFAULT_STACK_SIZE. +// Returns the task ID (>= 0) on success, or a negative error code. +int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority); + +// Yield CPU to the next eligible ready task (credit-based round-robin). +void tsYield(void); + +// Pause a task. Cannot pause the main task (id 0). +// If a task pauses itself, an implicit yield occurs. +int32_t tsPause(uint32_t taskId); + +// Resume a paused task. +int32_t tsResume(uint32_t taskId); + +// Set a task's scheduling priority. +int32_t tsSetPriority(uint32_t taskId, int32_t priority); + +// Get a task's priority, or TS_ERR_PARAM on invalid id. +int32_t tsGetPriority(uint32_t taskId); + +// Get a task's current state. +TaskStateE tsGetState(uint32_t taskId); + +// Get the currently running task's ID. +uint32_t tsCurrentId(void); + +// Get a task's name, or NULL on invalid id. +const char *tsGetName(uint32_t taskId); + +// Terminate the calling task. Must not be called from the main task. +void tsExit(void); + +// Get the number of non-terminated tasks. +uint32_t tsActiveCount(void); + +#endif // TASKSWITCH_H