3. Porting guide - a new CPU architecture

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

This is for bringing TUWA-RTOS to a processor it does not yet support. Bringing it to a new board on a supported processor is a smaller and much more common job - see 04-bsp-guide.md first, and be sure that is not what you need.

A CPU port needs the kernel sources. It is not something the binary release supports, because the port layer is compiled into the kernel library rather than linked alongside it. Talk to the copyright holder before starting. What follows describes the contract a port must satisfy, so you can judge the size of the job and so a port done under licence has something to be measured against.

What a port is

Everything that knows about a CPU lives in two places:

processor/<arch>/
    cpu.c               CPU setup, register access
    intrpt.c            interrupt controller glue
    regs.c              register formatting for diagnostics
    <arch>_context.S    THE CONTEXT SWITCH
    <arch>_vectors.S    trap and interrupt entry
include/CPU/<arch>/
    <arch>.h            the register-context layout

Nothing else changes. The scheduler, the memory manager and every IPC primitive are the same C source on every target, and a port that finds itself editing them has misunderstood the boundary - or found a real portability bug, which is worth reporting rather than working around locally.

The contract

A port has to provide four guarantees. They are stated as guarantees rather than as a recipe because how you meet them is CPU-specific, and the failures below come from meeting three of them and assuming the fourth.

1. A saved context is complete. Every register the compiler may use across a function call, plus the program counter, the stack pointer and the status register, has to survive being saved and restored. Anything omitted becomes a task that works until the moment it is preempted at the wrong instruction.

2. Interrupt entry saves the interrupted task, and interrupt exit resumes whatever the kernel selected. Those need not be the same task - that is what makes preemption possible, and it is the whole of what the port contributes to scheduling. The kernel decides which task; the port only has to make the save and the resume real.

3. The tick calls the kernel. One periodic interrupt, routed to the kernel's tick entry.

4. Interrupts can be masked and unmasked. The kernel's critical sections depend on it.

Register save sets - where ports go wrong

Save what the ABI does not. The context switch runs from an interrupt, so it cannot rely on the caller-saved / callee-saved split: the interrupted code did not call anything. Every register that holds live state has to be saved, including the ones a function-call ABI would let a callee destroy.

Vector and floating-point registers are the usual omission. They are wide, saving them is expensive, and most kernels do not use them - so a port reasonably leaves them out. That is a defensible choice, but then the kernel must be built so the compiler never uses them:

-mgeneral-regs-only

Without that flag GCC will happily vectorise a memcpy in kernel code, and the next context switch corrupts it. The symptom is memory corruption under load with no obvious cause, which is an expensive way to learn this.

A platform register, if the ABI reserves one, is the right home for the module base register. On AArch64 that is x18, and the port saves and restores it unconditionally for every task, so a module's base survives preemption with no extra state. If your CPU has no such register, a module format that needs one cannot be entered on it until you choose a register the context switch already preserves - see 06-modules.md.

Order of work

Each step is provable before the next, so a failure is always in the part you just wrote:

  1. Boot to a printed character. Entry, a stack, a UART. Everything after this is debuggable; nothing before it is.
  2. Save and restore a context, to the same task. If a task can be interrupted and resumed exactly where it was, the save set is right. This is testable without a scheduler and it is where save-set bugs are cheap.
  3. Take the tick. Count it, print the count.
  4. Switch between two tasks. Two tasks that never yield, two counters. If both move, preemption works.
  5. Run the module loader self-test. It needs no toolchain support and no filesystem, and it exercises a lot of the port.

Steps 2 and 4 are the port. Steps 1, 3 and 5 mostly exercise the board.

Endianness and width

TUWA-RTOS runs on both byte orders and on 32- and 64-bit targets. Two things need saying explicitly:

What you get for free

Once the four guarantees hold, the whole kernel works: scheduling, memory management, timers, message queues, pipes, mailboxes, events, mutexes, semaphores, and the module loader. None of that is per-CPU.

The filesystem and shell are per-board rather than per-CPU, and need nothing from a port beyond a working console.

A realistic estimate

The context switch and trap entry are a few hundred lines of assembly. The hard part is not writing them, it is the guarantees above - specifically save set completeness, which is invisible until something is preempted in exactly the wrong place. Budget most of the time for step 2, and do not move on from it early.