1
0
mirror of https://github.com/rkujawa/rk65c02.git synced 2026-04-26 17:17:56 +00:00

Add Tiny OS example: scheduler with extended physical RAM

- examples/tinyos.c: host with 64K system RAM, 3x32KB task regions above 64K
  (bus_device_add_phys), MMU translate (virtual 32KB per task), console at
  , cooperative yield ( + JMP cd /home/rkujawa/repos/rk65c02 && git commit --trailer "Made-with: Cursor" -m "Add Tiny OS example: scheduler with extended physical RAM

- examples/tinyos.c: host with 64K system RAM, 3x32KB task regions above 64K
  (bus_device_add_phys), MMU translate (virtual 32KB per task), console at
  $DE00, cooperative yield ($FF00 + JMP $1000) and WAI + idle_wait + IRQ
  yield, vectors at $FFFC-$FFFF
- examples/tinyos_kernel.s: entry at $8000, IRQ handler at $8010 (JMP $1000)
- examples/tinyos_task.s: per-task loop printing task id, round-robin yield,
  optional WAI, STP after 3 runs
- Makefile: tinyos target, run-tinyos with timeout
- README.md, doc/MMU.md: document Tiny OS (64K virtual + expanded physical)
- .gitignore: /examples/tinyos"000) and WAI + idle_wait + IRQ
  yield, vectors at -
- examples/tinyos_kernel.s: entry at 000, IRQ handler at 010 (JMP cd /home/rkujawa/repos/rk65c02 && git commit --trailer "Made-with: Cursor" -m "Add Tiny OS example: scheduler with extended physical RAM

- examples/tinyos.c: host with 64K system RAM, 3x32KB task regions above 64K
  (bus_device_add_phys), MMU translate (virtual 32KB per task), console at
  $DE00, cooperative yield ($FF00 + JMP $1000) and WAI + idle_wait + IRQ
  yield, vectors at $FFFC-$FFFF
- examples/tinyos_kernel.s: entry at $8000, IRQ handler at $8010 (JMP $1000)
- examples/tinyos_task.s: per-task loop printing task id, round-robin yield,
  optional WAI, STP after 3 runs
- Makefile: tinyos target, run-tinyos with timeout
- README.md, doc/MMU.md: document Tiny OS (64K virtual + expanded physical)
- .gitignore: /examples/tinyos"000)
- examples/tinyos_task.s: per-task loop printing task id, round-robin yield,
  optional WAI, STP after 3 runs
- Makefile: tinyos target, run-tinyos with timeout
- README.md, doc/MMU.md: document Tiny OS (64K virtual + expanded physical)
- .gitignore: /examples/tinyos

Made-with: Cursor
This commit is contained in:
Radosław Kujawa
2026-03-09 22:11:41 +01:00
parent 981bcb0ec2
commit d3bafd5892
7 changed files with 300 additions and 1 deletions
+1
View File
@@ -75,3 +75,4 @@ emulation.h
/examples/mmu_multitasking
/examples/mmu_pae
/examples/mmu_mpu
/examples/tinyos
+3
View File
@@ -95,6 +95,9 @@ Notes:
host polls in the tick callback and remaps the cart window via the MMU API.
- `mmu_multitasking` - minimal task switching: two tasks with private low memory,
guest yields by writing next task id to 0xFF00, host remaps and continues.
- `tinyos` - Tiny OS scheduler: three tasks in extended physical RAM (above 64K),
first 32KB virtual per task, cooperative yield and WAI+IRQ-driven switch,
console at $DE00. See [doc/MMU.md](doc/MMU.md) §Tiny OS.
- `mmu_pae` - PAE-like one-level page table, extended physical addresses, and
demand paging (fault → install mapping → restart); runs with JIT enabled.
- `mmu_mpu` - simple MPU: flat 64K with programmable protection regions, MMIO
+2
View File
@@ -154,6 +154,8 @@ The library does **not** define what the guest sees (bank registers, page tables
**Minimal multitasking** — Low addresses are per-task, high addresses shared. The guest yields by writing the next task id to a “yield” address; the host sees it (e.g. in the tick callback), sets the current task, calls `begin_update` / `mark_changed_vpage` / `end_update`, and the translate callback maps low addresses to the current tasks region. See `examples/mmu_multitasking`.
**Tiny OS (64K virtual + expanded physical)** — First 64KB of physical is system RAM (kernel, vectors, MMIO); tasks live in extended physical space above 64K (e.g. 0x10000, 0x18000, 0x20000), each with 32KB. Virtual $0000$7FFF is per-task (mapped to the current task's 32KB region via 32-bit `paddr`); $8000$FFFF is shared (identity). Cooperative yield: guest writes next task id to $FF00 and JMPs to task entry; translate callback at entry reads $FF00 and maps the low 32KB to that task's physical region. IRQ-driven yield: guest executes WAI; host `idle_wait` callback picks next task, writes $FF00/$FF01, calls `begin_update` / `mark_changed_vpage` / `end_update`, then `rk65c02_assert_irq(e)`; guest IRQ handler JMPs to task entry so the new task runs. Uses `bus_device_add_phys` and `bus_load_file_phys` for task RAM. See **`examples/tinyos`**.
Other possibilities (all host-defined): MMIO “MMU” device with registers for page table base and fault status; syscall-based mapping changes; multiple bank registers per region. The library API stays the same.
**PAE-like (Physical Address Extension)** — The same API supports a design similar to x86 PAE: guest keeps a 64K virtual space; physical addresses can be much larger (up to `RK65C02_PHYS_MAX`). The host implements a translate callback that (1) reads a "page directory base" from a fixed physical address or host state, (2) walks one or more levels of page tables via `bus_read_1(e->bus, addr)` or `bus_read_1_phys(e->bus, paddr)` (direct bus reads; no recursion), (3) returns the resolved 32-bit `paddr` and permissions, or `ok = false` with a `fault_code` for page fault or protection fault. Page tables can live in the first 64K or in extended physical RAM. For demand paging: on fault, the host installs the mapping, calls `rk65c02_mmu_begin_update` / `mark_changed_vpage` (or vrange) / `rk65c02_mmu_end_update`, then calls `rk65c02_start()` again so the same instruction re-executes. When the guest (or host) changes page table contents, the host must call the same update API so the library's TLB and JIT stay coherent. See **`examples/mmu_pae`** for a minimal one-level table walk, extended-physical mapping, and demand paging with JIT enabled (the library leaves PC at the faulting instruction on MMU fault so the host can fix the mapping and re-run).
+6 -1
View File
@@ -16,7 +16,7 @@ RK6502LIB=../src/librk65c02.a
VASM=vasm6502_std
VASMFLAGS=-Fbin -wdc02
EXAMPLES=min3 mul_8bit_to_8bits host_control idle_wait interrupts hello_serial stepper jit_bench breakpoints mmu_cart mmu_multitasking mmu_pae mmu_mpu
EXAMPLES=min3 mul_8bit_to_8bits host_control idle_wait interrupts hello_serial stepper jit_bench breakpoints mmu_cart mmu_multitasking mmu_pae mmu_mpu tinyos
EXAMPLES_ROMS:=$(addsuffix .rom,$(basename $(wildcard *.s)))
all : $(EXAMPLES) $(EXAMPLES_ROMS)
@@ -63,6 +63,9 @@ mmu_pae : mmu_pae.o $(RK6502LIB)
mmu_mpu : mmu_mpu.o $(RK6502LIB)
$(CC) -o $@ $(LDFLAGS) $< $(RK6502LIB)
tinyos : tinyos.o $(RK6502LIB)
$(CC) -o $@ $(LDFLAGS) $< $(RK6502LIB)
%.rom : %.s
$(VASM) $(VASMFLAGS) -o $@ $<
@@ -74,6 +77,8 @@ run-mmu_pae: mmu_pae mmu_pae.rom
timeout $(TIMEOUT) ./mmu_pae
run-mmu_multitasking: mmu_multitasking mmu_multitasking_kernel.rom mmu_multitasking_task.rom
timeout $(TIMEOUT) ./mmu_multitasking
run-tinyos: tinyos tinyos_kernel.rom tinyos_task.rom
timeout $(TIMEOUT) ./tinyos
run-mmu_cart: mmu_cart mmu_cart_bank0.rom mmu_cart_bank1.rom
timeout $(TIMEOUT) ./mmu_cart
run-min3: min3 min3.rom
+225
View File
@@ -0,0 +1,225 @@
/*
* Tiny OS Example — Host program (task scheduler with extended physical RAM)
*
* Demonstrates 64K virtual address space with 512KB physical: first 64KB is
* system RAM (kernel, vectors, MMIO); three tasks each have 32KB in extended
* physical space (0x10000, 0x18000, 0x20000). Virtual $0000$7FFF is
* per-task (ZP, stack, code); $8000$FFFF is shared. Tasks yield cooperatively
* (write next id to $FF00, JMP $1000) or via WAI (host idle_wait picks next
* task, updates MMU, asserts IRQ; handler JMP $1000).
*
* Build: make tinyos tinyos_kernel.rom tinyos_task.rom
* Run: ./tinyos (or: make run-tinyos)
*
* Expected: tasks 0, 1, 2 print to console in round-robin; cooperative and
* IRQ-driven switches; eventual STP and PASS.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "bus.h"
#include "device.h"
#include "device_ram.h"
#include "rk65c02.h"
/* -------------------------------------------------------------------------
* Physical layout (512KB conceptual; we use 64K system + 3×32K task regions)
* 0x00000 0x0FFFF System RAM (kernel at $8000, vectors $FFFC$FFFF,
* yield $FF00, current task $FF01, console MMIO $DE00)
* 0x10000 0x17FFF Task 0 private (32KB)
* 0x18000 0x1FFFF Task 1 private (32KB)
* 0x20000 0x27FFF Task 2 private (32KB)
*
* Virtual layout
* $0000 $7FFF Per-task → physical 0x10000 + (current_task * 0x8000)
* $8000 $FFFF Shared (identity to physical $8000$FFFF)
* ------------------------------------------------------------------------- */
#define TASK_SIZE 0x8000u
#define TASK_ENTRY 0x1000u
#define PHYS_TASK0_BASE 0x10000u
#define PHYS_TASK1_BASE 0x18000u
#define PHYS_TASK2_BASE 0x20000u
#define YIELD_REG 0xFF00u
#define CURRENT_TASK_REG 0xFF01u
#define KERNEL_START 0x8000u
#define IRQ_HANDLER 0x8010u
#define CONSOLE_BASE 0xDE00u
#define NUM_TASKS 3u
struct task_state {
uint8_t current_task; /* 0, 1, or 2 */
};
static uint8_t
console_read_1(void *dev, uint16_t doff)
{
(void)dev;
(void)doff;
return 0;
}
static void
console_write_1(void *dev, uint16_t doff, uint8_t val)
{
struct task_state *ts;
(void)doff;
ts = (struct task_state *)((device_t *)dev)->config;
if (ts != NULL)
printf("[%u] %c", (unsigned)ts->current_task, (char)val);
else
putchar((char)val);
}
static device_t console_device = {
.name = "console",
.size = 16,
.read_1 = console_read_1,
.write_1 = console_write_1,
.finish = NULL,
.config = NULL,
.aux = NULL
};
static rk65c02_mmu_result_t
tinyos_translate(rk65c02emu_t *e, uint16_t vaddr, rk65c02_mmu_access_t access,
void *ctx)
{
struct task_state *ts = (struct task_state *)ctx;
rk65c02_mmu_result_t r = {
.ok = true,
.paddr = (uint32_t)vaddr,
.perms = RK65C02_MMU_PERM_R | RK65C02_MMU_PERM_W | RK65C02_MMU_PERM_X,
.fault_code = 0,
.no_fill_tlb = false,
};
(void)access;
if (vaddr < TASK_SIZE) {
if (vaddr == TASK_ENTRY) {
ts->current_task = bus_read_1(e->bus, YIELD_REG) % NUM_TASKS;
bus_write_1(e->bus, CURRENT_TASK_REG, ts->current_task);
}
switch (ts->current_task) {
case 0:
r.paddr = PHYS_TASK0_BASE + vaddr;
break;
case 1:
r.paddr = PHYS_TASK1_BASE + vaddr;
break;
case 2:
r.paddr = PHYS_TASK2_BASE + vaddr;
break;
default:
r.paddr = PHYS_TASK0_BASE + vaddr;
break;
}
r.no_fill_tlb = true;
}
return r;
}
static void
on_idle_wait(rk65c02emu_t *e, void *ctx)
{
struct task_state *ts = (struct task_state *)ctx;
uint8_t next;
unsigned int p;
next = (ts->current_task + 1) % NUM_TASKS;
bus_write_1(e->bus, YIELD_REG, next);
bus_write_1(e->bus, CURRENT_TASK_REG, next);
ts->current_task = next;
rk65c02_mmu_begin_update(e);
for (p = 0; p < 0x80; p++)
rk65c02_mmu_mark_changed_vpage(e, (uint8_t)p);
rk65c02_mmu_end_update(e);
rk65c02_assert_irq(e);
}
int
main(void)
{
struct task_state ts = { .current_task = 0 };
rk65c02emu_t e;
bus_t b;
device_t *sys_ram;
device_t *task0_ram;
device_t *task1_ram;
device_t *task2_ram;
b = bus_init();
/* System RAM: 0-$FFFE (0xFFFF bytes) + $FF00-$FFFF (256 bytes). */
sys_ram = device_ram_init(0xFFFF);
bus_device_add(&b, sys_ram, 0);
bus_device_add(&b, device_ram_init(0x100), 0xFF00);
console_device.config = &ts;
bus_device_add(&b, &console_device, CONSOLE_BASE);
task0_ram = device_ram_init(TASK_SIZE);
task1_ram = device_ram_init(TASK_SIZE);
task2_ram = device_ram_init(TASK_SIZE);
bus_device_add_phys(&b, task0_ram, PHYS_TASK0_BASE);
bus_device_add_phys(&b, task1_ram, PHYS_TASK1_BASE);
bus_device_add_phys(&b, task2_ram, PHYS_TASK2_BASE);
if (!bus_load_file(&b, KERNEL_START, "tinyos_kernel.rom")) {
fprintf(stderr, "tinyos: cannot load tinyos_kernel.rom\n");
bus_finish(&b);
return 1;
}
if (!bus_load_file_phys(&b, PHYS_TASK0_BASE + TASK_ENTRY,
"tinyos_task.rom")) {
fprintf(stderr, "tinyos: cannot load tinyos_task.rom at task 0\n");
bus_finish(&b);
return 1;
}
if (!bus_load_file_phys(&b, PHYS_TASK1_BASE + TASK_ENTRY,
"tinyos_task.rom")) {
fprintf(stderr, "tinyos: cannot load tinyos_task.rom at task 1\n");
bus_finish(&b);
return 1;
}
if (!bus_load_file_phys(&b, PHYS_TASK2_BASE + TASK_ENTRY,
"tinyos_task.rom")) {
fprintf(stderr, "tinyos: cannot load tinyos_task.rom at task 2\n");
bus_finish(&b);
return 1;
}
bus_write_1(&b, 0xFFFC, (uint8_t)(KERNEL_START & 0xFF));
bus_write_1(&b, 0xFFFD, (uint8_t)(KERNEL_START >> 8));
bus_write_1(&b, 0xFFFE, (uint8_t)(IRQ_HANDLER & 0xFF));
bus_write_1(&b, 0xFFFF, (uint8_t)(IRQ_HANDLER >> 8));
e = rk65c02_init(&b);
e.regs.SP = 0xFF;
e.regs.PC = KERNEL_START;
assert(rk65c02_mmu_set(&e, tinyos_translate, &ts, NULL, NULL, true, false));
bus_write_1(&b, YIELD_REG, 0);
bus_write_1(&b, CURRENT_TASK_REG, 0);
rk65c02_idle_wait_set(&e, on_idle_wait, &ts);
rk65c02_start(&e);
if (e.stopreason != STP) {
fprintf(stderr, "FAIL: stop reason is %s (expected STP)\n",
rk65c02_stop_reason_string(e.stopreason));
bus_finish(&b);
return 1;
}
printf("\nPASS: Tiny OS tasks ran (cooperative + IRQ yield), stopped with STP.\n");
bus_finish(&b);
return 0;
}
+20
View File
@@ -0,0 +1,20 @@
; =============================================================================
; Tiny OS — Shared kernel (loaded at physical $8000, system RAM)
; =============================================================================
; Virtual $8000$FFFF is identity-mapped (shared). Kernel sets initial task 0,
; then JMPs to task entry at $1000 (MMU maps to current task's physical region).
; IRQ handler at $8010: JMP $1000 so that after WAI the host-selected task runs.
; =============================================================================
.org 0x8000
kernel:
lda #0
sta 0xFF00 ; yield reg: request task 0
sta 0xFF01 ; current task id (host mirror)
jmp 0x1000 ; run task 0 at virtual $1000
.org 0x8010
irq_handler:
jmp 0x1000 ; switch to task in $FF00 (set by host in idle_wait)
+43
View File
@@ -0,0 +1,43 @@
; =============================================================================
; Tiny OS — Per-task code (loaded at physical $11000, $18100, $20100)
; =============================================================================
; Assemble to tinyos_task.rom. Host loads at 0x10000+$1000 for each task.
; Virtual $1000 is mapped to the current task's physical 32KB region.
;
; Guest contract: $FF00 = yield (write next task id 0/1/2), $FF01 = current
; task id (read-only). Console at $DE00. Each task prints its id, yields
; round-robin; every other yield does WAI so host can switch via IRQ.
; =============================================================================
.org 0x1000
task_entry:
lda 0xFF01 ; current task id
clc
adc #48 ; ASCII '0'
sta 0xDE00 ; print to console
inc 0x02 ; yield parity (per-task ZP)
lda 0x02
and #1
bne coop_yield
wai ; every other yield: host switches, asserts IRQ
coop_yield:
inc 0x0200 ; run count (per-task)
lda 0x0200
cmp #3
bcs halt
lda 0xFF01 ; next task = (current + 1) % 3
clc
adc #1
cmp #3
bcc ok
lda #0
ok:
sta 0xFF00 ; yield to next task
jmp task_entry
halt:
stp