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:
@@ -75,3 +75,4 @@ emulation.h
|
||||
/examples/mmu_multitasking
|
||||
/examples/mmu_pae
|
||||
/examples/mmu_mpu
|
||||
/examples/tinyos
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 task’s 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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user