Every foreign binary execution in PPAP is a subsystem: an eCPU
emulator core (docs/ecpu/overview.md) paired with a binary loader and an
OS personality layer that gives meaning to the emulated traps/syscalls.
eCPU provides the raw CPU emulation — fetch, decode, execute. When the emulated program hits a trap or syscall instruction, eCPU fires a callback. The subsystem personality handles that callback: it decides what the trap means and translates it into PPAP syscalls.
This applies to all foreign binary execution, including running PPAP binaries cross-architecture. Each subsystem combines three parts:
- Binary loader — recognises the executable format and loads it into emulated memory (ELF loader, .COM loader, X-format loader, etc.)
- CPU emulator — eCPU core that interprets the foreign ISA
- OS personality — intercepts traps and translates them to PPAP syscalls
+----------------------------------------------------------+
| Foreign binary |
| (PPAP ELF, CP/M .COM, DOS .EXE, Human68k .X, etc.) |
+---------------------------+------------------------------+
|
+---------------------------v------------------------------+
| Subsystem personality layer |
| (PPAP ABI remap / CP/M BDOS / DOS INT 21h / etc.) |
| Intercepts traps → translates → PPAP syscalls |
+---------------------------+------------------------------+
|
+---------------------------v------------------------------+
| eCPU emulator core (docs/ecpu/overview.md) |
| Interprets foreign ISA instructions |
+---------------------------+------------------------------+
|
+---------------------------v------------------------------+
| PPAP kernel (native) |
| Handles syscalls normally |
+----------------------------------------------------------+
All subsystems use the same architecture. They differ only in the complexity of the personality layer:
| Subsystem | Personality complexity | What it translates |
|---|---|---|
| PPAP cross-arch | Trivial — register remap only | Same syscall numbers, different register ABI |
| CP/M | Simple — ~40 BDOS functions | BDOS calls → open/read/write/exit |
| S-OS SWORD | Simple — ~40 monitor functions | CALL/JP to monitor entries → PPAP syscalls |
| DOS | Moderate — INT 21h + BIOS stubs | INT 21h + INT 10h/16h → PPAP syscalls |
| Human68k | Moderate — F-line + IOCS | F-line exceptions → PPAP syscalls |
| Win32 | Complex — DLL function stubs | Win32 API → PPAP syscalls (stretch) |
Analogy: This is PPAP's equivalent of WSL (Windows Subsystem for Linux), Wine, or FreeBSD's Linux binary compatibility — but much simpler, targeting retro OS binaries on microcontrollers.
The exec() path tries native ELF first, then walks a chain of
registered subsystem detectors. Foreign-arch ELF is handled as
a subsystem (the PPAP cross-arch personality), not as a special case:
int execve(pcb_t *p, const char *path, const char *const *argv) {
const uint8_t *file = romfs_lookup(path);
/* Try native ELF (matching host e_machine) */
if (is_native_elf(file))
return exec_elf_native(p, file, argv);
/* Try registered subsystems — including foreign-arch ELF */
for (int i = 0; i < n_subsystems; i++) {
if (subsystems[i].detect(file, file_size))
return exec_subsystem(p, &subsystems[i], file, argv);
}
return -ENOEXEC;
}Foreign-arch ELF detection: valid ELF magic but e_machine doesn't
match the host → dispatches to the PPAP cross-arch subsystem.
Each subsystem registers a descriptor:
typedef struct {
const char *name; /* "ppap-arm", "cpm", "human68k", etc. */
int (*detect)(const uint8_t *file, uint32_t size);
int (*exec)(pcb_t *p, const uint8_t *file, uint32_t size,
const char *const *argv);
} subsystem_t;Detection heuristics vary by format — see per-subsystem sections below.
All subsystem components — eCPU emulator cores, binary loaders, and
personality layers — are kernel-embedded. When exec() detects a
foreign binary, the kernel loads it directly and either:
- Native architecture match (e.g., Human68k on m68k): loads the binary into user pages and runs it natively. The personality layer intercepts foreign OS traps (F-line, BDOS calls) via exception handlers in the kernel.
- Foreign architecture (e.g., CP/M on ARM): loads the binary into kernel-allocated emulated memory and enters the eCPU interpreter loop. The interpreter runs in kernel context as the process's execution, preempted by the scheduler like any other process.
int exec_subsystem(pcb_t *p, const subsystem_t *ss,
const uint8_t *file, uint32_t size,
const char *const *argv) {
/* Loader + personality + eCPU (if needed) are all kernel code */
return ss->exec(p, file, size, argv);
}Why kernel-embedded, not user-space emulators?
An earlier design placed emulators as user-space binaries under
/subsys/. The kernel-embedded approach is preferable for PPAP:
- No re-exec overhead. User-space emulators require
exec()of a separate native binary, argv passing, and process image setup. - Direct syscall dispatch. The personality bridge calls
sys_open(),sys_read(), etc. internally — no trap instruction per translated call. - Simpler pointer handling. The kernel accesses emulated memory directly (it allocated the pages). No guest-to-host address translation through user-space indirection.
- Consistent with PPAP's design. Monolithic kernel, no MMU on most targets. The kernel/user boundary is thin. Subsystem code is small (~4 KB bridge + ~8–16 KB eCPU per emulator core).
- No romfs bloat. No separate emulator ELF binaries in romfs.
- Single code path. Native-arch subsystems (e.g., Human68k on m68k) already require kernel-side exception handlers. Keeping foreign-arch subsystems in the kernel too avoids maintaining two execution models.
Foreign binaries (the applications, not the emulation infrastructure)
are stored under /subsys/<name>/ in romfs:
/subsys/
├── human68k/ # Human68k .x and .r executables
│ ├── bin/
│ └── ...
├── cpm/ # CP/M .COM files
│ ├── MBASIC.COM
│ └── ...
├── sos/ # S-OS SWORD .obj files
│ └── ...
├── dos/ # DOS .COM and .EXE files
│ ├── EDIT.COM
│ └── ...
└── ppap-arm/ # PPAP ARM ELFs (for cross-arch on m68k)
└── hello
This is a top-level directory — subsystems are a first-class PPAP
feature. The /subsys/ prefix also serves as a disambiguation hint
for formats without unique magic bytes (e.g., .com files under
/subsys/cpm/ use CP/M, under /subsys/dos/ use DOS — see §8.2).
Each subsystem is guarded by an ENABLE_SUBSYS_<NAME> compile-time
flag (e.g., ENABLE_SUBSYS_HUMAN68K, ENABLE_SUBSYS_CPM). Targets
enable only the subsystems they need:
- A target that does not enable a subsystem pays zero code size — the loader, bridge, and eCPU core are all compiled out.
- The subsystem registration table is built at compile time from the enabled flags; no runtime probing.
- The romfs image for each target includes only the
/subsys/directories matching its enabled subsystems.
This keeps the kernel small on constrained targets (e.g., RP2040 with 2 MB flash) while allowing full subsystem support on targets with more resources (e.g., m68k QEMU with unlimited RAM).
The simplest subsystem: run PPAP ELF binaries built for a different architecture. Same OS, same syscall numbers — only the register ABI differs.
Standard ELF with valid magic but non-native e_machine:
- On ARM host: detects
EM_68K(4) → dispatches toppap-m68k - On m68k host: detects
EM_ARM(40) → dispatches toppap-arm
The thinnest possible personality — mechanical register remapping:
/* PPAP uses the same syscall numbers on all architectures.
* Only the register positions differ. */
/* ARM registers → native syscall */
void ppap_arm_personality(ecpu_state_t *cpu) {
long ret = syscall6(
cpu->regs[7], /* r7 = syscall number */
cpu->regs[0], /* r0 = arg1 */
cpu->regs[1], /* r1 = arg2 */
cpu->regs[2], /* r2 = arg3 */
cpu->regs[3], /* r3 = arg4 */
cpu->regs[4], /* r4 = arg5 */
cpu->regs[5] /* r5 = arg6 */
);
cpu->regs[0] = ret;
}
/* m68k registers → native syscall */
void ppap_m68k_personality(ecpu_state_t *cpu) {
long ret = syscall6(
cpu->dregs[0], /* d0 = syscall number */
cpu->dregs[1], /* d1 = arg1 */
cpu->dregs[2], /* d2 = arg2 */
cpu->dregs[3], /* d3 = arg3 */
cpu->dregs[4], /* d4 = arg4 */
cpu->dregs[5], /* d5 = arg5 */
0 /* m68k ABI has 5 args in d1-d5 (a0 for 6th) */
);
cpu->dregs[0] = ret;
}No file format translation, no path mapping, no API bridging.
Pointer translation (guest→host address) is still needed for
syscalls that take pointer arguments — this is handled by the
eCPU memory model (see docs/ecpu/overview.md §3.3).
| Subsystem name | eCPU core | Host → Guest |
|---|---|---|
| ppap-arm | ecpu-arm | m68k host runs ARM ELF |
| ppap-m68k | ecpu-m68k | ARM host runs m68k ELF |
| ppap-armv6 | ecpu-armv6 | Any host runs ARMv6 ELF |
Run CP/M 2.2 .COM files — the simplest and most historically
significant target. Thousands of CP/M programs exist: MBASIC, Turbo
Pascal, WordStar, dBASE II, etc.
CP/M .COM is the simplest executable format:
- Raw binary, no header
- Loaded at address 0x0100
- Execution starts at 0x0100
- Max size: 0xFE00 bytes (to 0xFF00)
Detection: any file with .com extension and size <= 0xFE00 is treated
as CP/M .COM. (Ambiguity with DOS .COM is resolved by user
configuration or directory convention — see §8.)
0x0000 BIOS warm-boot entry (JP to warm boot handler)
0x0005 BDOS entry point (JP to BDOS dispatcher)
0x005C Default FCB 1 (parsed from command line)
0x006C Default FCB 2
0x0080 Default DMA buffer / command-line tail
0x0100 Program load area (.COM binary)
...
0xFE00 End of TPA (Transient Program Area)
Zilog Z80 interpreter (~150 instructions + CB/DD/ED/FD prefix groups).
typedef struct {
uint8_t a, f, b, c, d, e, h, l; /* main registers */
uint8_t a2, f2, b2, c2, d2, e2, h2, l2; /* shadow set */
uint16_t ix, iy, sp, pc;
uint8_t iff1, iff2, im; /* interrupt state */
uint8_t memory[65536]; /* 64 KB flat address space */
} z80_state_t;Size estimate: ~3000 lines of C, ~12 KB binary.
The Z80 is a superset of Intel 8080. Programs written for 8080/CP/M work unmodified.
CP/M programs invoke BDOS via CALL 5 with C=function number, DE=parameter.
The emulator intercepts execution at address 0x0005 and translates:
| BDOS fn | Name | PPAP translation |
|---|---|---|
| 0 | System Reset | _exit(0) |
| 1 | Console Input | read(0, &ch, 1) |
| 2 | Console Output | write(1, &ch, 1) |
| 6 | Direct Console I/O | read/write with non-blocking flag |
| 9 | Print String | write(1, buf, len) (scan for $ terminator) |
| 10 | Read Console Buffer | read(0, buf, max) with line editing |
| 15 | Open File | FCB → pathname, open() |
| 16 | Close File | close() |
| 19 | Delete File | unlink() |
| 20 | Read Sequential | read() 128 bytes at current position |
| 21 | Write Sequential | write() 128 bytes |
| 22 | Make File | open(O_CREAT) |
| 26 | Set DMA Address | Update internal DMA pointer |
| 33 | Read Random | lseek() + read() |
| 34 | Write Random | lseek() + write() |
| 35 | Compute File Size | fstat() or lseek(SEEK_END) |
| 36 | Set Random Record | Compute from FCB sequential position |
FCB (File Control Block) translation: CP/M uses FCBs with 8.3 filenames in a flat directory. The bridge maps FCB drive/name to PPAP paths:
FCB { drive=A, name="HELLO ", ext="COM" }
→ "/a/HELLO.COM" (or configurable mount point)
- Phase 1: Z80 interpreter core + BDOS 0/1/2/9 (console I/O) ✅
- Test: run a "Hello World" CP/M .COM
- Phase 2: FCB file operations (BDOS 15-22, 33-36) ✅
- Test: run MBASIC, load/save files
- Phase 3: Full BDOS compatibility, user area support ✅
- Test: run Turbo Pascal, WordStar
- Kernel integration + userland tests ✅
- 42 host tests + 13 userland tests (see
docs/subsystems/cpm.md)
- 42 host tests + 13 userland tests (see
Run S-OS SWORD programs — a Japanese hobbyist OS from the 1980s that ran on Sharp MZ-series, X1, and other Z80 machines. S-OS provided a unified API (monitor calls) across different hardware, making programs portable between machines.
S-OS .obj binary format:
- 18-byte ASCII header:
_SOSmagic, file mode, load address, exec address - Raw Z80 code follows the header
- Detection:
_SOSmagic at offset 0 + valid hex fields
Same Z80 interpreter as CP/M (shared core). S-OS programs use a subset of Z80 instructions.
S-OS programs invoke monitor calls via CALL or JP to fixed
addresses in the monitor entry table (0x1F80–0x1FFD). CALL is
trapped directly by ecpu-z80; JP hits an RST 0 stub at each
entry, which triggers the trap indirectly. The bridge translates:
| Monitor fn | Name | PPAP translation |
|---|---|---|
| 03 | #GETL | Line input via read(0, buf, len) |
| 06 | #1CHR | read(0, &ch, 1) (single char) |
| 09 | #MSG | write(1, str, len) (print string) |
| 0C | #MSX | write(1, str, len) (alternate) |
| 15 | #LOPEN | open() file for loading |
| 18 | #LREAD | read() file data |
| 1B | #SOPEN | open() file for saving |
| 1E | #SWRITE | write() file data |
| 21 | #FCLOSE | close() file |
| 24 | #FSAME | File attribute query |
| 27 | #DEVNM | Device name query |
| 56 | #WIDCH | Screen mode (no-op, preserves user's mode) |
| 59 | #SCRN | Read screen character at position |
The bridge also provides an in-memory screen buffer for #SCRN support and warns on exit if unsupported APIs were called.
Fully implemented and tested:
- Z80 interpreter (shared with CP/M)
- S-OS binary loader (
sos_loader.c) - Monitor bridge with console I/O, file operations, screen APIs
/subsys/sos/directory in PATH for transparent execution
Run MS-DOS .COM and .EXE files. Focus on simple DOS programs
(text utilities, games, educational software).
DOS .COM:
- Raw binary, loaded at offset 0x0100 within a segment
- CS=DS=ES=SS=PSP segment
- Execution starts at CS:0100
- Max size: ~64 KB
- Detection:
.comextension (disambiguated from CP/M by configuration)
DOS .EXE (MZ format):
- Header starts with
MZ(0x4D5A) - Relocation table for segment fixups
- Separate code/data/stack segments
- Detection: first two bytes =
MZ
Intel 8086/8088 real-mode interpreter.
typedef struct {
uint16_t ax, bx, cx, dx; /* general registers */
uint16_t si, di, bp, sp; /* index/pointer registers */
uint16_t cs, ds, es, ss; /* segment registers */
uint16_t ip; /* instruction pointer */
uint16_t flags; /* FLAGS register */
uint8_t memory[1048576]; /* 1 MB address space */
} x86_state_t;Real-mode addressing: physical = segment × 16 + offset.
Size estimate: ~5000 lines of C, ~20 KB binary. The 8086 ISA has variable-length instructions with complex ModR/M addressing — more effort than Z80 but well-documented.
DOS programs invoke services via INT 21h with AH=function number.
The emulator installs a handler in the emulated IVT at vector 0x21:
| INT 21h AH | Name | PPAP translation |
|---|---|---|
| 01h | Char Input with Echo | read(0, &ch, 1) + write(1, &ch, 1) |
| 02h | Char Output | write(1, &ch, 1) |
| 09h | Print String | write(1, buf, len) (scan for $) |
| 0Ah | Buffered Input | read(0, buf, max) |
| 3Ch | Create File | `open(O_CREAT |
| 3Dh | Open File | open() |
| 3Eh | Close File | close() |
| 3Fh | Read File | read() |
| 40h | Write File | write() |
| 41h | Delete File | unlink() |
| 42h | Seek (LSEEK) | lseek() |
| 4Ch | Terminate | _exit() |
| 4Eh | Find First | opendir() + readdir() |
| 4Fh | Find Next | readdir() |
Additional INT handlers needed:
- INT 20h — Program terminate (older .COM exit method)
- INT 10h — BIOS video (stub: text-mode output via UART)
- INT 16h — BIOS keyboard (stub: input from UART)
- Phase 1: 8086 interpreter + INT 21h 01/02/09/4Ch (console I/O)
- Test: "Hello World" DOS .COM
- Phase 2: File handle operations (3Ch-42h) + MZ .EXE loader
- Test: simple DOS utilities
- Phase 3: INT 10h/16h stubs, Find First/Next, more complete DOS
Run X68000 Human68k binaries on PPAP-m68k (and via eCPU on other
architectures). See docs/targets/68000.md §7 for X68000 hardware details.
Human68k X-format executable:
- Header: magic
HU(0x4855), then base/text/data/bss/reloc sizes - Detection: first two bytes =
HU
Human68k R-format (relocatable .R):
- For device drivers and TSRs
- Not prioritised for initial implementation
On PPAP-m68k: runs natively (same CPU), no emulator needed — only the personality layer is used (F-line exception handler). On PPAP-ARM: uses ecpu-m68k from eCPU.
Human68k uses the F-line exception (opcodes $FFxx) for DOS calls. The emulator/trap handler intercepts these and translates:
| DOS call | Number | PPAP translation |
|---|---|---|
_EXIT |
$FF00 | _exit() |
_GETCHAR |
$FF01 | read(0, &ch, 1) |
_PUTCHAR |
$FF02 | write(1, &ch, 1) |
_PRINT |
$FF09 | write(1, str, len) |
_CREATE |
$FF39 | open(O_CREAT) |
_OPEN |
$FF3D | open() |
_CLOSE |
$FF3E | close() |
_READ |
$FF3F | read() |
_WRITE |
$FF40 | write() |
_SEEK |
$FF42 | lseek() |
See docs/targets/68000.md §7 for X68000 hardware details including IOCS
(TRAP #15) passthrough.
Run simple Win32 console applications (.EXE) on PPAP. This is aspirational — a minimal proof-of-concept rather than full compatibility.
PE (Portable Executable):
- DOS stub +
PE\0\0signature - COFF header + optional header + section table
- Detection: MZ header with PE offset at e_lfanew
32-bit x86 protected-mode interpreter. Significantly more complex than 8086 real mode — 32-bit operands, paging concepts (ignored in emulation), many more instructions.
Size estimate: ~8000+ lines of C. This is the most complex emulator.
Instead of syscall-level translation, Win32 programs call DLL functions. The emulator provides stub DLLs:
- kernel32.dll —
CreateFileA,ReadFile,WriteFile,CloseHandle,ExitProcess,GetStdHandle,GetCommandLineA,HeapAlloc,HeapFree - msvcrt.dll —
printf,fopen,fread,fwrite,malloc,free(C runtime mapped to PPAP libc)
This is conceptually similar to Wine's approach but dramatically smaller in scope — only enough to run trivial console programs.
Deferred until CP/M, DOS, and Human68k subsystems are proven. The PE loader and x86 emulator are the most complex components in this feature.
When exec() encounters a non-native binary:
1. ELF magic (0x7f "ELF"):
- e_machine matches host → native exec (no subsystem)
- e_machine is foreign → PPAP cross-arch subsystem (§3)
2. Other magic bytes:
- "MZ" → check for PE ("PE\0\0" at e_lfanew)
yes → Windows PE subsystem (§7)
no → DOS .EXE subsystem (§5)
- "HU" → Human68k X-format subsystem (§6)
- No magic → raw binary (step 3)
3. Raw binary — check file extension:
- .com → CP/M or DOS .COM (see 8.2)
- .x → Human68k (alternate extension)
- .r → Human68k R-format
- other → -ENOEXEC
4. If still ambiguous → -ENOEXEC
Both CP/M and DOS use .com for raw binaries. Resolution:
- Directory convention: files under
/subsys/cpm/use CP/M subsystem, files under/subsys/dos/use DOS subsystem (see §2.4) - Configuration:
/etc/subsys.confmaps paths to subsystems - Default: CP/M (simpler, more likely to work on constrained targets)
Every subsystem uses an eCPU core. The same core serves multiple personalities:
| eCPU core | Subsystem personalities |
|---|---|
| ecpu-arm | PPAP cross-arch (ARM ELF on m68k) |
| ecpu-m68k | PPAP cross-arch (m68k ELF on ARM), Human68k |
| ecpu-z80 | CP/M, S-OS SWORD |
| ecpu-8086 | DOS |
| ecpu-armv6 | PPAP cross-arch (ARMv6 ELF on other hosts) |
| ecpu-x86 | Windows PE (stretch) |
All eCPU cores expose a common interface with a trap callback hook.
The subsystem's exec function initialises the eCPU, loads the binary,
and enters the interpreter loop — all in kernel context:
/* Common interface — see docs/ecpu/overview.md §3.2 */
typedef void (*ecpu_trap_handler_t)(ecpu_state_t *cpu, uint32_t trap_id);
/* Example: CP/M subsystem exec (kernel-side) */
int cpm_exec(pcb_t *p, const uint8_t *file, uint32_t size,
const char *const *argv) {
ecpu_state_t *cpu = &p->ecpu;
ecpu_init(cpu, arch_z80);
ecpu_set_trap_handler(cpu, cpm_bdos_handler); /* personality */
cpm_load_com(cpu, file, size); /* loader */
return ecpu_run(cpu); /* interpreter loop, preempted by scheduler */
}All subsystems need to map foreign filenames to PPAP paths:
| Foreign OS | Filename style | Mapping |
|---|---|---|
| CP/M | A:HELLO.COM |
/a/HELLO.COM |
| DOS | C:\DIR\FILE.TXT |
/c/DIR/FILE.TXT |
| Human68k | A:\DIR\FILE.X |
/a/DIR/FILE.X |
| Win32 | C:\Users\file.txt |
/c/Users/file.txt |
Drive letters map to directories under root. Path separators (\) are
converted to /. Case handling follows the foreign OS convention
(CP/M and DOS: case-insensitive matching).
| Priority | Subsystem | Status | Rationale |
|---|---|---|---|
| 1 | PPAP cross-arch | — | Simplest personality. Proves the eCPU+subsystem framework. |
| 2 | CP/M (Z80) | Done | Simplest foreign OS. Small CPU + small API. |
| 3 | S-OS SWORD (Z80) | Done | Shares ecpu-z80 with CP/M. Japanese retro software. |
| 4 | Human68k (m68k) | — | Natural fit for X68000 target. Shares ecpu-m68k. |
| 5 | DOS (8086) | — | Larger software library. More complex emulator. |
| 6 | Windows PE (x86) | — | Stretch goal. Very complex. Low priority. |
Phase A — Framework + PPAP cross-arch
- eCPU common interface and trap hook API
- Subsystem detection chain in
exec()(ELF + non-ELF) - Subsystem registration and dispatch
- First eCPU core (ecpu-arm) + PPAP personality
- Test: run ARM
helloELF on m68k PPAP
Phase B — CP/M Subsystem ✅
- ✅ Z80 interpreter (85 host tests, Steps 1–7)
- ✅ CP/M memory map setup + .COM loader (
com_loader.c) - ✅ BDOS console I/O bridge (functions 0–12)
- ✅ BDOS file operations (functions 15–36)
- ✅ Kernel integration + 13 userland tests
- Future: test with MBASIC, Turbo Pascal
Phase B2 — S-OS SWORD Subsystem ✅
- ✅ S-OS binary loader (18-byte
_SOSheader format) - ✅ Monitor bridge (console I/O, file operations, screen APIs)
- ✅ Screen buffer for #SCRN + unsupported API warnings
- ✅ Integration with
/subsys/sos/directory
Phase C — Human68k Subsystem
- X-format loader
- F-line exception handler
- DOS call bridge (console + file I/O)
- Test with X68000 utilities
Phase D — DOS Subsystem
- 8086 real-mode interpreter
- .COM loader + PSP setup
- INT 21h console I/O bridge
- MZ .EXE loader with relocation
- INT 21h file operations
- Test with simple DOS programs
Phase E — Win32 Subsystem (if pursued)
- x86 protected-mode interpreter extensions
- PE loader
- kernel32.dll / msvcrt.dll stubs
- Test with trivial console .EXE
Subsystem overhead has two components:
- CPU emulation — same as eCPU (10-100x slower than native)
- OS call translation — negligible (a few comparisons and register copies per call)
Programs that spend most time in I/O (which is most CP/M and DOS software) will run at near-native I/O speed since syscalls execute natively after translation.
On RP2040 (133 MHz), running a CP/M program through Z80 emulation:
- Z80 effective speed: ~5-10 MHz equivalent
- Original CP/M machines ran Z80 at 2-4 MHz
- Result: faster than original hardware
-
Memory limits: Z80 (64 KB) and 8086 real mode (1 MB) fit easily in RP2040's 264 KB SRAM. x86 protected mode programs may expect more. Limit TPA/conventional memory to what fits.
-
Terminal emulation: CP/M programs expect VT52/VT100 or ADM-3A terminals. DOS programs expect ANSI.SYS. The PPAP terminal should handle these escape sequences (or the personality layer translates them).
-
Block I/O vs stream I/O: CP/M uses 128-byte records, DOS uses byte-level I/O. The BDOS bridge must handle record-oriented read/write mapped to PPAP's byte-stream
read()/write(). -
Multiple subsystems simultaneously: can a CP/M program pipe output to a DOS program? In principle yes — the kernel sees both as regular PPAP processes with normal file descriptors.
-
Self-contained binaries— resolved: all subsystem components (eCPU, loader, personality) are kernel-embedded (see §2.3).