1
0
mirror of https://github.com/rkujawa/rk65c02.git synced 2026-03-11 03:16:11 +00:00
Files
rk65c02/examples/mmu_multitasking.c
Radosław Kujawa 1b4e7fe598 MMU/JIT: extended bus, examples (mmu_mpu, mmu_pae), tests, doc
- src: bus extended phys API, MMU/JIT hooks, rk65c02 MMU/tick/stop APIs
- examples: mmu_mpu and mmu_pae, Makefile targets; idle_wait, mmu_cart,
  mmu_multitasking updates
- test: test_mmu, test_bus, test_debug, Makefile
- doc: jit-design.md

Made-with: Cursor
2026-03-07 13:49:52 +01:00

176 lines
5.6 KiB
C

/*
* MMU Multitasking Example — Host program (minimal task switching)
*
* This example shows how a host uses the rk65c02 MMU API to implement
* two tasks with private low memory ($0000-$3FFF) and shared high memory
* ($8000-$FFFF). The guest "yields" by writing the next task id to $FF00;
* the host polls that address and remaps virtual $0000-$3FFF to the
* selected task's physical region. The contract (yield register at $FF00,
* current-task id at $FF01) is defined by this host — the library only
* provides the translation callback and begin/mark/end_update.
*
* Build: make mmu_multitasking mmu_multitasking_kernel.rom mmu_multitasking_task.rom
* Run: ./mmu_multitasking
*
* Expected: kernel at $8000 starts task 0; task 0 and task 1 alternate
* (each inc $0200, yield); after a few switches both tasks hit 3 and stop.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "bus.h"
#include "device_ram.h"
#include "rk65c02.h"
/* -------------------------------------------------------------------------
* Physical layout
* 0x0000 - 0x3FFF Task 0 private
* 0x4000 - 0x7FFF Task 1 private
* 0x8000 - 0xFFFF Shared (kernel; identity in translate)
*
* Virtual layout (what the CPU sees)
* 0x0000 - 0x3FFF Mapped to current task's physical region (0-$3FFF or $4000-$7FFF)
* 0x8000 - 0xFFFF Identity (shared)
*
* Guest contract: write next task id (0 or 1) to $FF00 to yield; host
* writes current task id to $FF01 when switching so guest can yield to "the other".
* ------------------------------------------------------------------------- */
#define TASK_SIZE 0x4000
#define TASK_ENTRY 0x1000 /* Guest JMPs here to run a task; switch only at entry */
#define PHYS_TASK0_START 0x0000
#define PHYS_TASK1_START 0x4000
#define YIELD_REG 0xFF00
#define CURRENT_TASK_REG 0xFF01
#define KERNEL_START 0x8000
struct task_state {
uint8_t current_task; /* 0 or 1 */
uint8_t last_yield; /* last value seen at $FF00 */
};
static rk65c02_mmu_result_t
task_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 = vaddr,
.perms = RK65C02_MMU_PERM_R | RK65C02_MMU_PERM_W | RK65C02_MMU_PERM_X,
.fault_code = 0,
.no_fill_tlb = false,
};
(void)access;
/*
* Low 16KB is per-task; high 32KB is shared (identity).
* Apply the task selection only when translating the task entry (0x1000),
* so we switch when the guest actually JMPs there, not in the middle of
* the yielding task's code. See MMU.md §3.2 (no_fill_tlb at entry).
*/
if (vaddr < TASK_SIZE) {
if (vaddr == TASK_ENTRY)
ts->current_task = bus_read_1(e->bus, YIELD_REG) & 1;
if (ts->current_task == 0)
r.paddr = vaddr;
else
r.paddr = (uint32_t)(vaddr + PHYS_TASK1_START);
/* Do not cache so that a fetch from 0x1000 always calls us and sees latest yield. */
r.no_fill_tlb = true;
}
return r;
}
static void
task_tick(rk65c02emu_t *e, void *ctx)
{
struct task_state *ts = (struct task_state *)ctx;
uint8_t next;
next = bus_read_1(e->bus, YIELD_REG);
if (next == ts->last_yield)
return;
ts->last_yield = next;
/*
* Notify library: low 16KB mapping may change when guest next enters
* the task region (translate callback reads YIELD_REG and applies it).
*/
rk65c02_mmu_begin_update(e);
for (uint16_t p = 0; p < 0x40; p++)
rk65c02_mmu_mark_changed_vpage(e, (uint8_t)p);
rk65c02_mmu_end_update(e);
/* Guest-visible "current task" so the task can yield to the other. */
bus_write_1(e->bus, CURRENT_TASK_REG, next & 1);
}
int
main(void)
{
struct task_state ts = { .current_task = 0, .last_yield = 0xFF };
rk65c02emu_t e;
bus_t b;
uint8_t c0, c1;
b = bus_init_with_default_devs();
/* Default RAM ends at 0xDFFE; add top page so YIELD_REG and CURRENT_TASK_REG are in bus. */
bus_device_add(&b, device_ram_init(0x100), 0xFF00);
if (!bus_load_file(&b, KERNEL_START, "mmu_multitasking_kernel.rom")) {
fprintf(stderr, "mmu_multitasking: cannot load mmu_multitasking_kernel.rom\n");
return 1;
}
if (!bus_load_file(&b, PHYS_TASK0_START + 0x1000, "mmu_multitasking_task.rom")) {
fprintf(stderr, "mmu_multitasking: cannot load mmu_multitasking_task.rom\n");
return 1;
}
/* Load same task code into task 1's region at physical $5000. */
if (!bus_load_file(&b, PHYS_TASK1_START + 0x1000, "mmu_multitasking_task.rom")) {
fprintf(stderr, "mmu_multitasking: cannot load task ROM into task 1 region\n");
return 1;
}
e = rk65c02_init(&b);
e.regs.SP = 0xFF;
e.regs.PC = KERNEL_START;
assert(rk65c02_mmu_set(&e, task_translate, &ts, NULL, NULL, true, false));
/* Initial yield and current task so kernel's JMP 0x1000 runs task 0. */
bus_write_1(&b, YIELD_REG, 0);
bus_write_1(&b, CURRENT_TASK_REG, 0);
/*
* Optional: tick flushes TLB when yield changes; switch is applied in
* translate when vaddr==TASK_ENTRY, so we can run without tick.
*/
rk65c02_tick_set(&e, task_tick, 1, &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;
}
c0 = bus_read_1(&b, 0x0200);
c1 = bus_read_1(&b, 0x4200);
/* Both tasks run; the one that does the final STP may be 3, the other at least 2. */
if (c0 < 2 || c1 < 2 || (c0 != 3 && c1 != 3)) {
fprintf(stderr, "FAIL: task 0=$%02x task 1=$%02x (expected both >=2, one STP at 3)\n",
c0, c1);
bus_finish(&b);
return 1;
}
printf("PASS: both tasks ran and stopped with STP (task0=%u task1=%u).\n",
(unsigned)c0, (unsigned)c1);
bus_finish(&b);
return 0;
}