Chapter 14: Operating System (OS/816)¶
Note
This chapter is still under construction.
OS/816 is the operating system being built for the X65. It is a minimal, 65C816-assembly kernel designed to stay out of the way — small enough that a single developer can hold it in their head, opinionated enough that task isolation and cooperative I/O feel natural, and explicit enough that an application can always drop to bare-metal behaviour when it needs to.
OS/816 as the X65’s Default Machine Personality¶
OS/816 is itself shipped as a boot ROM image — a .xex file (format: Appendix F) installed in the NORTH firmware’s internal LittleFS catalogue. When the X65 powers on, the firmware initializes the hardware and then loads and jumps into whatever image is installed as the boot ROM; on a stock machine, that image is OS/816, and the first thing the user sees after the brief firmware banner is the OS shell.
Because the machine personality is a .xex like any other, OS/816 can in principle be replaced — a developer who wants the X65 to boot into a C64-style BASIC, a games-cartridge-style front end, or a bare demo can build such a boot ROM and install it side by side to OS/816 (see Chapter 9 for the deployment commands). In practice this is rare; most development targets the user-application layer instead — programs that run under OS/816 rather than in its place.
User applications are also .xex files, built with the same toolchain and the same linker config. They differ only in their runtime environment: instead of owning the machine, they run as a task inside OS/816’s 32-slot scheduler. Each task is effectively a small virtual machine — a dedicated 64 KB memory bank for its code and data, plus a 256-byte page in bank 0 holding its co-located direct page and hardware stack (see Task Model below) — with hardware access typically mediated by syscalls. Programs that genuinely need the full machine — a demo, a graphics experiment, a timing-critical routine — are better written as boot ROMs instead; the default application posture is “well-behaved task”.
Shape¶
OS/816 is written entirely in 65C816 assembly using the K816 assembler (see Chapter 7). The kernel lives in bank $FF; kernel data and the main CLI run in bank $00. The first 32 × 256 bytes of bank 0 (i.e. $0000–$1FFF) are split into one 256-byte page per task, each of which holds that task’s direct page at the bottom (growing up) and its hardware stack at the top (growing down).
A second bank-0 region, starting at $4000, holds the Task Control Block (TCB) table: 32 × 64-byte TCBs, one per task slot.
Bank 0:
$0000 ─┐
│ Task 0 DP+stack page (256 B)
$0100 ─┤
│ Task 1 DP+stack page (256 B)
... │
│ Task 31 DP+stack page (256 B)
$2000 ─┘
...
$4000 ─┐
│ 32 × 64-byte TCBs
$4800 ─┘
Each task additionally gets a dedicated bank of its own — task N’s primary data bank is $N. The kernel task that runs the main CLI uses bank 0 alongside OS data; application tasks (1..31) are free to use their assigned banks for code and data. Banks $20–$FE are available as general-purpose banks that any task can request from the OS.
Task Model¶
TCB layout¶
Each TCB is 64 bytes. The parts relevant to application code:
Offset |
Field |
Size |
Purpose |
|---|---|---|---|
0 |
|
2 |
Saved 16-bit stack pointer |
2 |
|
1 |
Task state (see below) |
3 |
|
1 |
Pending signal bitmap |
4 |
|
1 |
Signals the task is currently blocking |
5 |
|
1 |
Kernel events the task subscribes to |
6 |
|
2 |
Signal-handler entry address |
8 |
|
5 |
Inter-task message slot (from, type, + 3 data bytes) |
… |
|
23 |
Reserved for future kernel use |
… |
|
12 |
Null-terminated task name (shown by the |
… |
|
16 |
Open file descriptors |
TCBs are mostly managed by the kernel. Application code touches them only indirectly through syscalls (TASK_SET_NAME, future IPC calls).
Task states¶
State |
Symbol |
Meaning |
|---|---|---|
|
|
Slot is empty; no task allocated |
|
|
Task is being created but not yet runnable |
|
|
Normal scheduled state |
|
|
Does not receive CPU; still processes signals |
|
|
Blocked on I/O; can still receive signals |
|
|
Currently running a signal handler; cannot be preempted |
|
|
Non-preemptible; must yield or syscall to give up CPU |
The tasks shell command prints the single-character symbol in its “state” column for each slot.
Scheduling¶
Scheduling is hybrid:
Preemption happens at VBL — every 1/60 of a second, the NMI handler runs the scheduler and may pick a different task.
Yielding happens explicitly — a task can give up its timeslice via a syscall (or by calling an equivalent library routine) without waiting for the next VBL.
The scheduler today is a simple round-robin that rotates between eligible TASK_RUNNING slots. Non-preemptible states (SIGNAL, SINGLE) make the scheduler skip the slot until the task returns to RUNNING.
During a context switch the NMI handler saves the current task’s 16-bit S into its TCB (TCB::sp), loads the next task’s saved S, and returns. Because each task’s DP + stack page is co-located and because D is re-pointed as part of the switch, per-task state is naturally isolated without the kernel needing to walk any other memory.
Syscall ABI¶
Entry¶
System calls are issued with the COP instruction. The kernel installs its COP handler at the native COP vector ($FFE4); the 65C816 pushes PBR, PC, and P on entry, the kernel saves the rest of the register state, and dispatches to the requested call.
The syscall number is passed in the low byte of A:
; Example: write a character via the console-write syscall (2)
lda #'H' ; parameter for CON_WRITE
pha ; argument on task stack
lda #2 ; CON_WRITE
cop #0 ; COP operand is ignored by current kernel
The saved A at syscall entry carries both the syscall id and the parameter (with A interpreted in 16-bit mode when appropriate). Register / stack conventions:
A— parameter (and return value, on success).X,Y— preserved across the call.Additional arguments — pushed onto the task stack before the
cop. The kernel unwinds the stack on return, so the caller does not need to balance it.Status —
C(carry) clear on success, set on error;Acarries the error code whenCis set.
Mid-syscall task switches¶
A subtle but important property: if a VBL NMI fires while a syscall is in progress and the scheduler decides to switch tasks, the kernel completes the syscall but returns into the newly scheduled task rather than the original caller. The SYSCALL_LOCK mechanism defers the NMI-side switch until the COP handler finishes, then the normal switch-at-exit logic uses NEXT_TASK rather than CURRENT_TASK.
The upshot for callers: a syscall cannot assume “when I return, I am still the task that called”. In a hybrid-scheduled kernel with cooperative I/O, this is the correct behavior — long-running calls become yield points for free. But it means a task that must run with no interruptions (graphics critical section, time-sensitive hardware interaction) should either avoid syscalls during that window or enter TASK_SINGLE state first.
IPC — Signals and Messages¶
Each TCB carries a 5-byte message slot and a signal bitmap. Today the infrastructure is present (storage in the TCB) but the public send/receive API has not landed as a syscall yet. Design-level behaviour:
Signals: every task can send any signal (0..7 bits) to any other task; sending to task-id 255 fans out to all tasks except the sender. Stopping other tasks en masse in this way would, in principle, let an application claim sole CPU time within OS/816, though today this is design intent rather than working infrastructure — and for full machine ownership the intended pattern is to write a boot ROM instead (see Chapter 9).
Messages: a (
from,type,data) triple in each TCB, withdatabeing either an immediate value or a pointer.
These are covered here because the field reservations exist in the implemented TCB layout and because tasks-shell reports reflect the signal state; the send/receive mechanics will be documented here once the API stabilises.
Virtual Terminals¶
The S device is responsible for per-task screens. Each of the 32 tasks may register video memory, attribute memory, and a display-list pointer; only the “currently active” VT’s registration drives CGIA at any given moment.
VT names:
S0–S31(shell can reachS0–S9).Switching: press the 🐾 (Paw) modifier plus a digit
0–9to jump to that VT, or 🐾+ / 🐾- to cycle next / previous. On the X65 keyboard this is the machine’s dedicated command-modifier key; on a standard PC keyboard it sits in the position of the Windows / Meta key. The VBL handler detects the change, saves the outgoing task’s CGIA state into its TCB slot, and loads the incoming task’s.A task can have zero or one screen.
This gives the X65 classic multi-terminal behaviour (per-task screen with instant switching) without the overhead of a framebuffer copy — all that moves is a handful of CGIA register pointers.
Memory and Banks¶
Bank ownership:
Bank
$00— shared by OS data, per-task DP + stack pages ($0000–$1FFF), and TCB table ($4000+).Banks
$01–$1F— per-task primary banks (task N’s data/code bank is$N).Banks
$20–$FE— free pool, request from OS. 223 general-purpose banks available.Bank
$FF— the OS itself.
A task requests a free bank via a syscall (future API); the OS returns a bank number for the task to own exclusively. The task is responsible for tracking which banks it owns — the OS does not keep a per-task bookkeeping list. The idiomatic pattern is to store owned bank numbers in the task’s direct page alongside other per-task globals.
Devices¶
Device names use one-letter codes with optional numeric subcodes. Currently planned / reserved:
Code |
Device |
Subcode meaning |
|---|---|---|
|
Disk drive |
Drive number (0, 1, …) |
|
Editor |
Virtual terminal number |
|
Keyboard |
— |
|
Printer |
Printer number |
|
Screen |
Virtual terminal number |
|
RAM disk / random |
— |
|
Null device |
— |
Default assigns: A: → D0:, B: → D1:. Letters not otherwise used can be assign-ed to any device / subdevice pair.
The Shell¶
OS/816 ships with a minimal built-in CLI. Command dispatch is a small table of name-to-handler pairs; today it holds three commands plus two aliases:
Command |
Purpose |
|---|---|
|
List available commands |
|
Print arguments |
|
List task slots: id, state ( |
The dispatcher does a linear name-match, so new commands are added by appending a name and a handler address to the two parallel tables and bumping CMDS_NO. The shell uses plain CON_READ / CON_WRITE for input and output — it is a straightforward example of a syscall-using application task.
Relationship to RIA¶
OS/816 reaches hardware directly through the MMIO windows ($FFE0–$FFE1 for the UART, $FFB0–$FFBF for HID, etc.) rather than going through the RIA fastcall API at $FFF0–$FFF3. The design reason is simple: fastcall invocations are potentially long-running, and running them from kernel-scheduled contexts would either force single-caller-at-a-time locking or require the kernel to understand how to suspend a fastcall mid-flight. Direct register access is bounded and understandable.
The fastcall path is therefore currently an application-level tool — a user program can reach it directly when it wants modern services like FatFS file I/O — and the OS kernel itself stays below it.
System-Wide Locks¶
Hardware resources shared between tasks will eventually be coordinated through a syscall-based locks registry. A task that needs exclusive access to a resource — the SGU-1 sound chip, an expansion slot etc. — requests its lock via syscall. If the lock is free the OS grants it and returns success; if it is held by another task, the caller blocks until the lock is released.
The design intent is that the registry itself stays small and generic — a named-lock table the kernel owns — so applications can coordinate access to shared hardware without needing to build a per-device kernel abstraction for each resource.
What’s Implemented, What’s Not¶
A quick summary to set expectations:
Working today:
32-task model, TCB table, per-task DP + stack pages.
Round-robin scheduler with VBL preemption (60 Hz) and explicit yield.
Context switch saving/restoring
S, with task-page co-located DP.COP-based syscall trap with
SYSCALL_LOCKhandling of mid-syscall VBL.Syscalls 1
CON_READ, 2CON_WRITE, 3IO_OPEN, 8TASK_SET_NAME.Shell with
help,echo,tasksand their aliases.Virtual terminal infrastructure (per-task CGIA state in TCB; the S device to register them).
Stubbed / aspirational:
Syscalls 4–7 (
IO_CLOSE/IO_READ/IO_WRITE/IO_DUP) returnEINVAL; device-handler dispatch not implemented.Signals and messages: TCB fields reserved, send/receive syscalls not yet exposed.
Bank-request syscall: banks
$20–$FEdesign is specified; the allocator is not yet coded.Device name → handler table (
D,E,K,P,S,R,N): names reserved; implementations pending.System-wide locks registry: designed; no syscalls yet.
Fastcall bridge from OS to RIA: intentionally not implemented today.
Because the OS is actively evolving, treat this chapter as a snapshot — current as of the X65 Book revision but subject to change as the kernel grows. The source of truth is always the OS/816 repository.
Summary¶
OS/816 gives the X65 a small but real multitasking environment: 32 tasks, each with its own dedicated 64 KB memory bank, a 256-byte direct-page-plus-stack page in bank 0, and a TCB slot; a round-robin VBL-preempted scheduler; a COP-based syscall trap; a three-command shell; and a device / virtual-terminal model that the rest of the kernel is growing into. Each task is, in effect, a small 64 KB virtual machine of its own, using syscalls for console I/O (or talking to hardware directly) and able to become non-preemptible for critical sections when the moment calls for it. The OS’s relationship to the RIA fastcall API is deliberately hands-off today — applications reach fastcalls themselves while the kernel stays below.