Tuwa API — a user's manual

Copyright (c) 2026 Muhammad Anisur Rahman. All rights reserved.

05-api-reference.md and api-index.md list what exists. This manual is the other half: how to actually get something done with it, in the order a person meets the problems.

The public API is 167 functions across 17 areas. You will use perhaps fifteen of them to write a working application.

The shape of a Tuwa program

There is no main() that owns the machine. The board boots, the kernel starts, and your code is one or more tasks that the scheduler runs. A task is a C function that does not return.


#include "tuwa.h"

void blinker(void)
{
    while (1)
    {
        TuwaPrintConsole("tick\n");
        TuwaSleep(100, WAKEUPONIPC);
    }
}

void UserAppInit(void)
{
    LoadTask("blinker",
             (void *) blinker,
             NULL,                    /* parameter                */
             5,                       /* priority - see below     */
             READYTASK | WAKEUPONIPC, /* initial state            */
             default_task_stack_size,
             -1,                      /* parent - none            */
             NO_MULTIPLE_INSTANCE);
}

UserAppInit() is called by the board's boot code. That is your entry point.

Tasks

To… Call
Create a task LoadTask(name, func, param, prio, state, stack, parent, multi)
Sleep this task TuwaSleep(ticks, wakeupflag)
Delay without sleeping TaskDelay(ticks)
Find out who you are GetCurrentTaskId()
Change priority ChangeTaskPriority(task, prio)
End a task TuwaExitTask()
Stop another task KillTask(task) / UnKillTask(task)
Give up the CPU now TuwaSwitchTask()

Priority is a queue number and lower is more urgent. Queue 0 is scanned first. The scheduler walks queues from 0 upward and takes the first task that can run, so a priority-0 task that never sleeps will starve everything below it. System tasks that must respond promptly — the interrupt job task, the debug notification task — sit at 0 and sleep, which is the pattern to copy: high priority is for latency, not for hogging.

Within one queue, tasks take turns. There is also a periodic boost for the lowest queue, so background work cannot be starved forever by a busy higher queue.

Sleeping properly

TuwaSleep(ticks, wakeupflag) is not just a delay — the wakeup flag says what else can wake you early:


TuwaSleep(1000, WAKEUPONIPC);   /* wake on a message, or after 1000 ticks */

A signal arriving also makes a sleeping task schedulable. So the common shape for a service task is sleep long, get woken by work:


while (1)
{
    while (work_available())
        do_one_piece();
    TuwaSleep(LONG, WAKEUPONIPC);   /* the timeout is the fallback */
}

The sleep duration in that pattern is a safety net, not the mechanism.

Talking between tasks

Four mechanisms, and picking the right one saves more trouble than any amount of tuning.

Use When Cost
Message queue Structured, queued, sender and receiver decoupled Allocates
Mailbox One buffer, latest value wins Cheap
Pipe Byte stream Buffered
Signal "Something happened", 64 of them per task A bit. No allocation

Signals coalesce. SendSignal sets a bit, so two signals of the same number delivered before the task runs arrive as one. That makes them perfect for "there is work" and wrong for "here are two items". If you need to count, put the items in a queue and use the signal only as the doorbell.

Register a handler before you can receive one:


SignalSetVector(GetCurrentTaskId(), MY_SIGNAL, my_handler);

and end it when done, or that signal stays in service and further ones for that task are blocked:


void my_handler(unsigned char signo, long mytask)
{
    while (drain_one_item())      /* loop - signals coalesce */
        ;
    EndOfSignal(mytask, signo);   /* promptly */
}

EndOfSignal promptly is not politeness. A handler that holds the signal open across a long operation blocks every later one.

Mutual exclusion

To… Call
Protect a resource mutex — take, use, give
Count a resource semaphore
Wait for a condition event
Protect a short critical section DisableSysInterrupt() / EnableSysInterrupt()

Turning interrupts off is the cheapest and the most dangerous: it delays every interrupt on the machine, including the timer. Use it for a handful of instructions, never across anything that can block, and never across a console write.

There is also a preemption switch for the case where you need the scheduler to leave you alone but interrupts must keep working.

Memory

TuwaAllocMem / free, and for images that need a page-aligned region:


unsigned long long base;
int got = request_virt_memory_from_os(size, &base);
...
release_virt_memory_to_os(base);

That pair exists for the module loader — PIC images need alignment and a base address, which ordinary allocation does not promise.

Do not allocate from an interrupt handler. The allocator can deadlock against whatever task you interrupted mid-allocation, or corrupt the free list. If a handler needs to hand something to a task, use a preallocated buffer or a fixed ring and let the task do the allocating.

Console


TuwaPrintConsole("text\n");

bootMessage() is the lower-level one: a polled loop over one character, no interrupts, no allocation, no locks — which is why it is the only thing safe to call from a panic or an exception handler.

Filesystem, devices, modules

The filesystem API is POSIX-shaped (open/read/write/close/seek) over FILE_DESCRIPTOR, which is opaque — you hold the handle, you do not look inside it.

Devices are registered and reached by name. Run-time modules load with the module API and resolve against the kernel symbol table, which is what lets a loaded module call kernel functions by name.

See 06-modules.md for the module story and 05-api-reference.md for signatures.

Things that will catch you

Priority 0 is not "important", it is "runs first, always." If it does not sleep, nothing else runs.

A signal is a bit, not a count. Drain in a loop.

EndOfSignal promptly. Holding it blocks later signals for that task.

No allocation in interrupt context. Ever.

long is not pointer-sized on every port. If you store an address in an integer, use the pointer-sized type — code that works on four ports can fail on x86-64 for exactly this.

A task function does not return. If it has finished, call TuwaExitTask().