Initial commit.

This commit is contained in:
Scott Duensing 2026-02-23 20:28:38 -06:00
commit 363a1214e1
8 changed files with 941 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.claude/
*~
*.exe
*.o

24
Makefile Normal file
View file

@ -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

261
README.md Normal file
View file

@ -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 <stdio.h>
#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
```

2
RUNDEMO.BAT Normal file
View file

@ -0,0 +1,2 @@
@ECHO OFF
DEMO.EXE > OUTPUT.TXT

95
demo.c Normal file
View file

@ -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 <stdio.h>
#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;
}

13
dosbox-x.conf Normal file
View file

@ -0,0 +1,13 @@
[dosbox]
memsize = 16
[cpu]
cycles = max
[autoexec]
@ECHO OFF
MOUNT C .
C:
DEMO.EXE
PAUSE
EXIT

457
taskswitch.c Normal file
View file

@ -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 <stdlib.h>
#include <string.h>
// ============================================================================
// 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);
}

85
taskswitch.h Normal file
View file

@ -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 <stdint.h>
#include <stdbool.h>
// 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