Copyright (c) 2026 Muhammad Anisur Rahman. All rights reserved.
This describes the subsystems most applications use, with the signatures as they actually are. api-index.md is the complete checkable list of public declarations, generated from the headers; anything not in it is internal to the kernel and may change between releases.
Handles are long, and negative means failure. Every Create* returns an identifier to pass back to the rest of that subsystem, or a negative value.
Timeouts. Calls that can block take a wait argument. WAIT_FOREVER blocks indefinitely; 0 returns immediately if the operation would block; anything else is a tick count. A tick is whatever the board's timer was configured for.
FromISR variants exist where blocking would be fatal. In an interrupt there is no task to suspend, so the ordinary call has nothing to block. Use TuwaSendMessageFromISR, SendMailBoxFromISR, SetEventFromISR and WritePipeFromISR from interrupt context and the ordinary forms from tasks.
Widths. Use TUWA_ADDR for anything holding an address. long is not address-width everywhere - on LLP64 hosts it is 32 bits while a pointer is 64.
long LoadTask(char *name, void *func, void *param_ptr, long priority,
long TaskState, long stacksize, long parentid,
char MultipleInstance);
void TuwaSleep(unsigned long tick, long wakeupflg);
void TaskDelay(unsigned long tick);
void TuwaExitTask(long id);
long GetCurrentTaskId(void);
long ChangeTaskPriority(long id, unsigned char newpriority);
void Blinker(void)
{
for (;;)
{
TuwaPrintConsole("tick\r\n");
TaskDelay(100);
}
}
LoadTask("Blinker", (void *) Blinker, NULL, 2, READYTASK,
default_task_stack_size, -1, NO_MULTIPLE_INSTANCE);
A task function does not return. Use TuwaExitTask() to finish one.
Preemption is off until the BSP enables it - see 04-bsp-guide.md. Until it does, a task that never blocks never yields the CPU.
void *TuwaFsMalloc(void *heap_ptr, long requested_size, long id, int persistent);
void TuwaFsFree(void *heap_ptr, void *inptr);
long TuwaAllocMem(long id, long Type, long size);
long FreeMem(long memid);
TuwaFsMalloc is the general allocator; pass &GlobalHeap and the current task id. It is what modules use, and it is exported to them.
char *buf = (char *) TuwaFsMalloc(&GlobalHeap, 512, GetCurrentTaskId(), 0);
if (buf == NULL) { /* out of memory - always check */ }
TuwaFsFree(&GlobalHeap, buf);
long CreateMutex(long tskid, unsigned long waitflag);
long WaitForMutex(long tskid, long MutexId, unsigned long wait);
long ReleaseMutex(long Tskid, long MutexId);
long DeleteMutex(long MutexId);
long m = CreateMutex(GetCurrentTaskId(), WAIT_FOREVER);
WaitForMutex(GetCurrentTaskId(), m, WAIT_FOREVER);
/* ... the protected section ... */
ReleaseMutex(GetCurrentTaskId(), m);
Both take the task id, so ownership is explicit rather than implied by who happens to be running.
Semaphores follow the same shape with CreateSemaphore, WaitForSemaphore and ReleaseSemaphore. Use a mutex for mutual exclusion and a semaphore for counting; the difference matters when you come to reason about who may release.
long CreateEvent(char *event_name, unsigned long wait);
long SetEvent(long id, unsigned long event);
long SetEventFromISR(long id, unsigned long event);
long ClearEvent(long id, unsigned long event);
long WaitForEvent(long id, unsigned long event_to_wait,
unsigned long *event_long, unsigned long wait);
Events are a bit set, so one event object carries up to 32 independent conditions and a waiter can be woken by any of them.
long ev = CreateEvent("io", WAIT_FOREVER);
/* in a driver ISR */
SetEventFromISR(ev, 0x01);
/* in a task */
unsigned long got;
WaitForEvent(ev, 0x01 | 0x02, &got, WAIT_FOREVER);
/* 'got' says WHICH arrived - check it, do not assume */
long CreateMessageQueue(long msgsize, long elementcount);
long TuwaSendMessage(long qid, char *msg, long msglen, unsigned long timeout);
long TuwaSendMessageFromISR(long qid, char *msg, long msglen);
long TuwaReceiveMessage(long qid, char *msg, long msgbufsize, unsigned long timeout);
long MessageCount(long qid);
long FlushMessageQueue(long qid);
Fixed-size elements, copied in and out. The queue owns its storage, so the sender may reuse its buffer as soon as the call returns.
long q = CreateMessageQueue(sizeof(MY_MSG), 8);
MY_MSG m;
TuwaSendMessage(q, (char *) &m, sizeof(m), WAIT_FOREVER);
MY_MSG got;
TuwaReceiveMessage(q, (char *) &got, sizeof(got), WAIT_FOREVER);
GetTransmitFailureCount(q) counts sends that failed rather than blocked - worth logging, because a queue that is quietly dropping messages under load looks exactly like a producer that is not running.
Pipes are byte streams: CreatePipe, ReadPipe, WritePipe, WritePipeFromISR, PipeMessageCount, FlushPipe.
Mailboxes pass a buffer by reference rather than copying, which suits large or variable-sized messages: CreateMailBox, CreateMailBuffer, SendMail, ReceiveMail, FreeMailBuffer.
With a mailbox the buffer's ownership moves with the message. The sender must not touch it after SendMail, and the receiver frees it with FreeMailBuffer. A message queue copies and has no such rule; that is the trade between them.
long CreateTimer(char *timer_name, void (*func)(void),
unsigned long ticks, long wait);
long StartTimer(long timer_id);
long StopTimer(long timer_id);
long DeleteTimer(long timer_id);
long t = CreateTimer("heartbeat", OnHeartbeat, 500, WAIT_FOREVER);
StartTimer(t);
The callback runs in timer context, not in a task of its own. Keep it short and hand real work to a task through a queue or an event.
int TuwaPrintConsole(char *buf);
long TuwaReadConsoleChar(void);
long TuwaReadConsoleBuffer(char *buf, long size);
Supplied by the BSP. TuwaPrintConsole takes a NUL-terminated string, so printing part of a buffer means terminating it first.
FILE_DESCRIPTOR *tuwa_fopen(char *filepath, long mode);
long tuwa_fread (void *buffer, long count, int size, FILE_DESCRIPTOR *fp);
long tuwa_fwrite(void *buffer, long count, int size, FILE_DESCRIPTOR *fp);
long tuwa_fclose(FILE_DESCRIPTOR *fp);
long tuwa_ls(char *path);
long tuwa_mkdir(char *path);
long tuwa_rmdir(char *path);
long tuwa_remove(char *path);
long tuwa_exists(char *path);
Note the argument order: (buffer, count, size, fp), and the return is bytes, not elements. That is not C's fread and mixing them up reads the right data and reports the wrong length.
mode is CMD_TUWA_FILE_READ to read and CMD_TUWA_FILE_CREATE or CMD_TUWA_FILE_WRITE to write. Only the write modes create a missing file; opening a name that is not there for reading fails rather than bringing an empty file into existence.
tuwa_exists is three-valued: 1 yes, 0 no, -1 this device cannot answer. A device that has not implemented the test has not said "no".
Names are 8.3. The FAT12 driver refuses a name it cannot express rather than truncating it, so a four-character extension fails at open with nothing pointing at the name as the cause. This is why module files are .twa.
See 06-modules.md. In brief:
long TuwaLoadModule(void *image, long imagesize, char *name);
long TuwaLoadModuleFromFile(char *path, char *name);
long TuwaCallModuleFunc(long handle, char *name, TUWA_ADDR *result);
void *TuwaModuleExportAddr(long handle, char *name);
long TuwaUnloadModule(long handle);
char *ModLoadErrorText(long err);
Always report ModLoadErrorText() on failure. The loader distinguishes about thirty causes - wrong architecture, an unresolved import, a relocation off the end of the image - and they send you to entirely different places.