4. Board support package guide

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

A BSP brings TUWA-RTOS up on one board, on a CPU that is already supported. It is the layer you write and the layer you own. Bringing up a new CPU is a different job - see 03-porting-guide.md.

Layout

Every reference BSP has the same shape, and copying the closest one is the fastest way to start:

hardware/CPU/<arch>/<board>/
    start.S             reset entry: set up a stack, call into C
    startup/start.c     early C, before the kernel exists
    config/bootcfg.c    the hooks below - the bulk of a BSP
    config/syscfg.c     resource limits, tables the kernel indexes into
    h/bootcfg.h         sizes and counts for this board
    h/platform.h        memory map, device base addresses
    drivers/            UART, timer, interrupt controller
    tuwa_kernel.ld      link script

The seam

TUWA-RTOS calls the following. Every one must exist or the link fails at the last step - which is the honest failure, but a late one.

Include tuwa_bsp.h and the compiler checks all of this for you.

#include "tuwa_bsp.h"

It declares every function below. A missing one is still a link error, but a WRONG SIGNATURE becomes a compile error at the point of definition instead of a clean link and a board that misbehaves - C will otherwise let you define void InterruptRoutine(void) where the kernel calls InterruptRoutine(cause), and the callee then reads a parameter that was never passed.

All four reference BSPs include it.

Configuration

FunctionDoes
InitConfig()Fill in the resource limits: table sizes, stack sizes, max_file_slot. Called before anything allocates.
ShowConfig()Print them. May be empty.
UserOsConfig()Kernel policy for this board. This is where preemption is enabled.
UserFreeMemAreaSet()Declare the heap: fill MemAllocAreas[] and call InitTuwaMemManager().
UserAppInit()Create the board's own tasks with LoadTask().

Hardware

FunctionDoes
PreKernelHwInit()The minimum needed to print: usually just the UART.
InitializeHardware()Interrupt controller, timers, devices.
InitializeTickTimer() / StartTickTimer()The scheduler's tick.
InstallInterrupt()Route an IRQ to a handler.
InterruptRoutine(long cause)The board's dispatcher.
RunSystemTest()Board self-test during boot.
Reboot(), Reboot_PowerOff()Optional.

ReportAndHalt is not in this list, though every reference board has one. It is a local helper each of them happens to give the same name; the kernel never calls it. Writing it into tuwa_bsp.h made all four boards fail to compile, which is how the mistake was found.

Console

FunctionDoes
TuwaPrintConsole(char *)Write a string. The one function everything else depends on - bring it up first.
TuwaReadConsoleChar()Read one character.
TuwaReadConsoleBuffer()Read a line.

Shell hooks

Only if you link the shell. All four are hardware inspection, so there is no generic version:

void ShowAsicReg(void);
void TuwaReadPort (char *portname, unsigned long fromaddr);
void TuwaWritePort(char *portname, unsigned long val_addr, char *extra);
void UserShellCmd (char *CommandBuf);      /* unrecognised command */

On a board with no I/O port space, say so rather than returning quietly:

void TuwaReadPort(char *portname, unsigned long fromaddr)
{
    (void) portname; (void) fromaddr;
    TuwaPrintConsole("inp: this target is memory-mapped and has no port space\n");
}

A silent return makes inp 0x60 look as though it worked and read zero, which is worse than a refusal - it produces a number that a reader will believe.

Enable preemption, or nothing preempts

int UserOsConfig(void)
{
    EnablePreemption();
    return 1;
}

The kernel starts cooperative and never turns this on by itself. Leave it out and the system boots, tasks start, the tick fires, and nothing ever switches - which presents as "the scheduler is broken". It is the BSP's decision because only the board knows whether its timer and interrupt controller are ready to be preempted on.

This is the single most common bring-up mistake, and it costs an afternoon every time.

Declaring memory

void UserFreeMemAreaSet(void)
{
    unsigned long heapbegin = (unsigned long)&tuwa_heap_begin;
    unsigned long heapsize;

    heapbegin = (heapbegin + 15) & ~15UL;          /* align */
    heapsize  = (RAM_BASE + RAM_SIZE) - heapbegin;

    MemAllocAreas[0].Start = (char *) heapbegin;
    MemAllocAreas[0].Size  = heapsize;
    InitTuwaMemManager(heapbegin, heapsize);
}

tuwa_heap_begin comes from the link script and marks the end of the image. Deriving the heap from it rather than writing an address down means the heap cannot overlap the kernel after the image grows - a class of bug that appears only once a build crosses some size and is then very hard to attribute.

Creating tasks

void UserAppInit(void)
{
    LoadTask("MyTask", (void *) MyTaskFunc, NULL,
             2,                       /* priority   */
             READYTASK,               /* start state */
             default_task_stack_size,
             -1,                      /* no parent  */
             NO_MULTIPLE_INSTANCE);
}

Bring-up order

Each step depends on the one before, so working in this order means a failure is always in the thing you just wrote:

  1. start.S and a stack. Nothing else can run.
  2. The UART and TuwaPrintConsole. Until you can print, every later failure looks the same.
  3. UserFreeMemAreaSet. The kernel allocates during startup.
  4. The tick timer and interrupt controller. Prove the tick arrives by counting it and printing the count.
  5. EnablePreemption() and two spinning tasks. Both counters must move. Neither task yields, so if both advanced, preemption works.
  6. Optional: filesystem, shell, module loader - see 02-build-guide.md.

Do not skip step 5. It is the only step that proves the port rather than the board, and it is a dozen lines - two counters and a comparison.

Checking it

ALL-SCHEDULABLE-TASKS-RAN
TUWA-KERNEL-RUNNING

Add the filesystem and the shell and you should also see RAMFS-MOUNTED, MODULE-LOADED-FROM-FILE and SHELL-FS: ALL PASSED.