From 22a92dda5aca69c68e685fcde3aeacc8a519fb53 Mon Sep 17 00:00:00 2001 From: lyrathorpe Date: Mon, 6 Jul 2026 22:29:44 +0100 Subject: [PATCH] =?UTF-8?q?docs(reference):=2002-system=20=E2=80=94=20SIBO?= =?UTF-8?q?/Workabout=20MX=20programming=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/02-system.md | 719 ++++++++++++++++++++++++++++++++++++ 1 file changed, 719 insertions(+) create mode 100644 docs/reference/02-system.md diff --git a/docs/reference/02-system.md b/docs/reference/02-system.md new file mode 100644 index 0000000..4c4588c --- /dev/null +++ b/docs/reference/02-system.md @@ -0,0 +1,719 @@ +# Psion SIBO / EPOC — Operating-System Programming Model + +Reference notes for the SIBO family (HC, MC, Series 3/3a/3c, Siena, Workabout / +Workabout MX) running the ROM-resident **EPOC** operating system, programmed +through the **PLIB** library. + +Sources, cited inline as: + +- **[GEN]** — *SIBO 'C' SDK, General Programming Manual*, v2.30 +- **[PLIB]** — *PLIB Reference* +- **[SYS]** — *EPOC OS System Services* (assembly-level service interface) + +Everything below is drawn from those manuals. Items derived from prior +reverse-engineering, rather than the manuals, are explicitly flagged +**[RE — reverse-engineered]**. Workabout-MX-specific points are flagged +**[MX]**; where the manuals do not distinguish the MX, that is noted as an +uncertainty rather than invented. + +> **Terminology.** *SIBO* is the hardware architecture (8086-class CPU, ASICs, +> SSD storage). *EPOC* is the operating system that runs on it. This is the +> 16-bit SIBO-era EPOC, not the later 32-bit EPOC32/Symbian. [GEN §2] + +--- + +## 1. The memory model + +### 1.1 Small model, segments, and memory moving + +EPOC programs are compiled in the 8086 **small model**: the code segment and the +data segment are each limited to **64 KB**. The restriction is deliberate — it +lets EPOC **move memory segments** (including a process's own code and data +segments) around physical RAM without any cooperation from the application, which +is essential for efficient RAM use in a multitasking system. [GEN §1; PLIB +"Introduction"] + +Physical memory (address 0 to 0xFFFFF, up to 1 MB addressable; banked above +512 KB) is laid out roughly as: interrupt vectors (1 KB) → screen bitmap (small +displays) → OS data space → **allocated memory segments** → unallocated memory → +internal RAM drive (`LOC::M:`) → environment variables (≤4 KB) → screen bitmap +(large displays) → ROM (~256 KB). [PLIB §7 "Overview of system memory usage"] + +All unallocated RAM is kept in a **single contiguous chunk**. Consequently +creating, deleting, or resizing one segment forces others to move. Segment +addresses and sizes are always expressed in **16-byte paragraphs**; segments +start on 16-byte boundaries. The segment allocator's maximum segment size is +**512 KB**. [PLIB §7] + +**How moving is safe.** The supervisor process (`SYS$MANG`) does the moving. It +runs at higher priority than any other process (so it is not interrupted mid-move) +and its own data/code segments never move. After a move it walks each process +context and **adjusts the 8086 segment registers** (CS, DS, SS, ES) by the amount +the relevant segment moved. A register is adjusted only if it holds a value inside +a moved segment's range. [GEN §1; PLIB §7] + +Two rules the application must obey so the supervisor's fix-up works: + +- Never store a segment register to memory and later reload it (the memory may + move in between). In assembler, protect any unavoidable save/restore by + disabling interrupts across it (which blocks the context switch). +- Never load a non-segment value into a segment register (it might fall inside a + moved segment's range and get "adjusted"). [GEN §1] + +### 1.2 Segment kinds and naming + +Allocated segments are described by a **name**, a **handle**, a **size**, and an +**address**. [PLIB §7] + +- **Device segments** — created when an external device is installed, deleted on + removal; normally do not resize. Placed at **lower addresses** than dynamic + segments so device drivers (e.g. serial) are not disturbed by volatile dynamic + activity. The first two are special: the data segments of `SYS$NULL` and + `SYS$MANG`, created at startup and never resized. Extensions `.LDD` / `.PDD` + denote Logical / Physical Device Driver segments. +- **Dynamic segments** — code and data segments created/deleted as processes come + and go; process data segments grow/shrink to fit heap demand. Extensions: + `.$SC` = shared primary code segment, `.DYL` = dynamic library code, `.$nn` = + process data segment (nn = process slot number). + +The **segment handle** is the relative address of the segment's 16-byte +index-table entry in OS data space; that entry holds the segment address, a +**usage count**, and the name. Usage count = number of processes using the +segment; when it drops to zero the segment is (normally) deleted. The index table +has fixed capacity — typically **96 total = 32 device + 64 dynamic**. [PLIB §7] + +Segment API (names follow file-name rules, ≤8 chars + optional `.ext`): +`p_sgcreate`, `p_sgdelete`, `p_sgopen`, `p_sgclose`, `p_sgcopyto`, `p_sgcopyfr`, +`p_sgsize`, `p_sgadjust`, `p_sgfind`, `p_sglock` / `p_sgunlock` (usage-count +inc/dec). Access to data in an **external segment** is less convenient than to the +process data segment, but a single external segment can grow to the 512 KB limit — +the standard way to hold data structures larger than 64 KB (e.g. a document). +[GEN §1; PLIB §7] + +### 1.3 The process data segment and heap + +A process data segment is at most **0xFFE0** bytes (32 bytes short of 64 KB, so a +stack underflow always trips an address trap). Layout, low → high: + +1. reserved static variables (0x40 bytes; "magic statics") +2. floating-point emulator data space (0x200 bytes from offset 0x100) +3. processor stack +4. initialised static variables +5. uninitialised static variables (also zero-initialised) +6. **process heap** — at the top so it can grow by growing the data segment + +The word at offset 0 is initialised to **0xDEAD**; many OS calls check it and +panic (`PanicDead0`, panic 68) if a stray NULL-pointer write has changed it. Stack +bytes are pre-filled with 0xFF (the count of 0xFF from 0x40 measures free stack, +unless the FP emulator is in use, in which case the stack floor is 0x300). [PLIB +§7 "Process data segments"; §12 "Reserved statics"] + +### 1.4 Heap cell allocation + +Cells are referenced **directly by address** and **do not move** to compact freed +space. Each cell is prefixed by a hidden 16-bit length word (skipped by the +address returned to you; not counted by `p_alen`). Writing past a cell's bounds +corrupts the length word (or a free-list pointer) — a "time bomb" that a later +allocator call detects and turns into a panic. + +The heap is **not fixed size**: EPOC grows it (up to the 0xFFE0 limit) to satisfy +requests and shrinks it to hand memory back, but never below the **initial / +minimum heap** value stored in the image (default 2 KB). Allocation is +**first-fit** over the free list; if nothing fits, the data segment is grown by +the requested amount **plus the heap granularity** (default `E_GROWBY_DEFAULT` +2 KB; max `E_MAX_GROWBY` 16 KB, set via `p_hgran`). [PLIB §7 "The heap allocator"] + +| Function | Purpose | +|---|---| +| `void *p_alloc(UINT size)` | Allocate ≥`size` bytes; returns address or **NULL** on failure. Always test for NULL. | +| `void *f_alloc(UINT size)` | As `p_alloc` but calls `p_leave(E_GEN_NOMEMORY)` instead of returning NULL. | +| `void p_free(void *pcell)` | Free a cell. No-op if `pcell` is 0 (handy in cleanup). Double-free corrupts the heap. | +| `void *p_realloc(void *pcell, UINT size)` | Resize; returns new address or NULL (original untouched). `pcell==0` ⇒ acts like `p_alloc`. | +| `void *f_realloc(...)` | As `p_realloc` but leaves on failure. | +| `void *p_adjust(void *pcell, UINT offset, INT amount)` | Open (`amount`>0) or close (`amount`<0) a gap inside a cell — insert/delete records in place. | +| `UINT p_alen(void *pcell)` | Cell length (may be a few bytes > requested). | +| `void p_hgran(UINT nparas)` | Set heap growth granularity (paragraphs). | +| `void p_allwalk(fn, fpar)` | Walk every cell (diagnostic); panics on inconsistency. | +| `void p_allchk(INT num)` | Force a heap-integrity check; `p_panic(0xFF)` if corrupt. | +| `UINT p_allspc(void **pheap)` | Potential free space + heap start address. | + +**Design cautions.** *Alloc heaven* — if a multi-cell structure is built and a +later allocation fails, free the already-allocated cells or they leak. *Internal +fragmentation* — avoid churning tiny transient cells (use the stack), and +"granularise" many-cell variable-length structures. Do not let an unbounded +heap structure grow until allocation fails at the 0xFFE0 limit: leave enough heap +for the user's expected actions (e.g. saving to file). Note EPOC does **not** +distinguish "no system RAM" from "hit the 64 KB data-segment limit" — both surface +as an allocation failure. [PLIB §7] + +System-memory queries: `p_getram` (RAM in paragraphs, capped at 32768 = 512 KB +due to banking), `p_totalK` (total KB ignoring banking; **EPOC ≥ 3.50 only**), +`p_sgfree` (free segmented memory), `p_sgramdisk` (RAM-disk usage). [PLIB §7] + +--- + +## 2. Processes + +### 2.1 What a process is + +A process is a running program, normally created by loading an image (`.img` / +`.app`) file. EPOC is a **single-user, preemptive multitasking** OS. Max +concurrent processes `E_MAX_PROCESSES` = **24**. A process consists of at least: a +**process control block** (`E_proc`), a **data segment**, a (possibly shared) +**primary code segment**, and an **I/O semaphore**. [PLIB §12] + +**Process ID** — a positive 16-bit value: low 12 bits = PCB offset in OS data +space (the "slot"); high 4 bits = a generation counter (0–7, incremented mod 8 on +slot reuse) so a stale PID from a terminated process is rejected. A bad PID panics +the caller with **panic 7**. [PLIB §12] + +**Process names** — `.$nn` where `` is 1–8 chars and `nn` is the +2-digit slot index. For a **task** (a subsidiary process sharing its owner's data +segment — a lightweight thread) the `$` becomes `a`, e.g. `SYS$WSRV.a05`. Max name +`E_MAX_NAME` = 12. Loading the same image twice yields distinct names (e.g. +`SORT.$07`, `SORT.$11`); `p_pidfind("SORT.*")` / `p_pfind` resolve names→PIDs with +wildcards. [PLIB §12] + +The standard system processes created at startup: + +| Name | Role | Priority | +|---|---|---| +| `SYS$NULL` (`.$01`) | Null process; runs when nothing else is ready; powers the machine off after inactivity | 0 | +| `SYS$MANG` (`.$02`) | Supervisor: memory moving, resource cleanup, critical functions | 248 | +| `SYS$FSRV` (`.$03`) | File server; also loads images to create processes | 240 | +| `SYS$WSRV` (`.$04`) | Window server: screen/keyboard/digitiser | — | +| `SYS$SHLL` (`.$05`) | Shell / launcher | — | + +Name/priority/suspend operations **fail** on `SYS$NULL`, `SYS$MANG`, `SYS$FSRV`. +[PLIB §12] + +### 2.2 Creating a process + +```c +HANDLE p_execc(TEXT *pName, VOID *pCommand, INT length); /* usual route */ +``` + +Loads the image `pName` (parsed with default extension `.IMG`) and returns a +**positive PID** — but the new process is created **SUSPENDED**, so the creator +can initialise it (e.g. set priority) before starting it with `p_presume`. A +command cell is allocated in the *new* process's heap containing the full parsed +path, a length byte, and `length` bytes copied from `pCommand` +(`length` ≤ `E_MAX_COMMAND_BUFFER` = 127); its address lands in the reserved +static `DatCommandPtr`. Loading is done by the file server, so **the caller must +already be connected to the file server** (the standard PLIB startup does this +before `main`; otherwise panic 41). [PLIB §12] + +Error returns from `p_execc` (illustrative of the E_FILE_*/E_GEN_* families): +`E_GEN_NOMEMORY`, `E_FILE_DEVICE`, `E_FILE_NOTREADY`, `E_FILE_DIR`, +`E_FILE_NXIST`, `E_FILE_EXIST` (a different program of the same name — checksum +mismatch — already runs), `E_GEN_IMAGE` (not a valid/corrupt image), +`E_GEN_NOPROC` (no free slots), `E_GEN_ARG` (data+stack+heap > 0xFFE0, or command +too long). [PLIB §12] + +- `p_execcasync(pName, pCommand, length, pStatus, pPid)` — asynchronous load + (completion via status word; see §5). +- `p_pcreate(E_CPB *pBlock)` — the **primitive** creator that does *not* involve + the file server; `p_execc` eventually calls it. The `E_CPB` block carries code/ + stack/data/heap paragraph counts, initial IP, checksum, `minHeap`, initial + `priority`, RAM/ROM flag, and name. [PLIB §12] + +**Shared code.** A second instance of the same program shares the existing +`.$SC` code segment (only the initialised statics are reloaded). The load +verifies the code segment checksum against the image header — so you cannot run +two different programs (or versions) of the same name simultaneously. [PLIB §12] + +### 2.3 Priorities and scheduling + +Priority is an **unsigned byte, 1–255**. At each reschedule the highest-priority +**ready** process runs; if several share the top priority they run **round-robin**, +each for a **4-tick** slice (the system ticks **32×/second**). A lower-priority +process is otherwise blocked indefinitely. Preemption is real: a process need not +call the OS to be switched out. [PLIB §12; SYS §10] + +A process leaves the running state by moving to: `SEMAPHORE` (via `p_iowait` / +`p_wait`), `DELTA` (via `p_sleep`/`p_sleept`/`p_sleepa`), or `SUSPENDED` (via +`p_psuspend` on itself). Process states: `E_PROC_READY`, `E_PROC_SEMAPHORE`, +`E_PROC_DELTA`, `E_PROC_SUSPENDED` (plus `E_PROC_FREE` for a vacated slot). The +READY queue is priority-ordered; SEMAPHORE queues are FIFO; the DELTA queue stores +inter-timer tick deltas. [PLIB §12] + +Priority guidance and constants: + +- Applications should stay within **`E_MIN_PRIORITY` (64)** … **`E_MAX_PRIORITY` + (192)** inclusive; values outside are OS-reserved. [PLIB §12] +- Window-server clients are typically created at **`E_PRIORITY_FORE` (128 = + 0x80)** and then let the window server raise/lower them as they gain/lose focus. + A default image priority of **0x80** is the norm. [PLIB §12; GEN §3] +- Supervisor 248, file server 240; interrupts run at the priority of the process + they interrupt. [PLIB §12; SYS §10] + +> **Uncertainty / cross-source discrepancy.** [PLIB] states `E_MIN_PRIORITY` = 64 +> and `E_MAX_PRIORITY` = 192. [SYS §10] speaks only of reserved bands +> `cPBMinPriority` / `cPBMaxPriority` without giving numbers, and the [SYS] +> service-number appendix lists a symbol `E_MAX_PRIORITY 154`. Treat the PLIB +> pair (64/192) as the documented application range; the 154 appendix value is +> unexplained in prose and should not be relied on. + +Priority API: `INT p_getpri(HANDLE pid)`, `INT p_setpri(HANDLE pid, INT nPriority)` +(reschedules; `E_GEN_RANGE` out of range, `E_FILE_NXIST` no such process, +`E_GEN_FAIL` on null/supervisor/file-server), `p_presume`, `p_psuspend`, +`p_marka`/`p_unmarka` (mark this process (in)active — servers mark themselves +non-active), `p_getpid`, `p_pname`/`p_prename`, `p_getowner`. Cross-process data +access: `p_pcpyfr`, `p_pcpyto`, `p_piscpyfr`. [PLIB §12] + +### 2.4 Termination + +Self-termination: + +- `void p_exit(INT nReason)` — normal, graceful. `nReason` in −127..128; **0** = + clean exit; negative = an init-failure error code; positive = an exit status to + a parent. Falling off the end of `main` calls `p_exit` with `main`'s return + value (falling off a `void main` is poor practice — it exits with garbage). +- `void p_panic(INT nPanic)` — abnormal (see §3). Application panic numbers should + count **down from 254** to avoid the system ranges. + +Terminating another process: + +- `INT p_pterminate(HANDLE pid, INT nReason)` — **preferred**. If the target + called `p_onterminate(nMessage)`, it is merely *sent* that message and gets to + run cleanup and exit itself; otherwise it behaves like `p_pkill`. +- `INT p_pkill(HANDLE pid, INT nReason)` — summary kill, no cleanup chance. +- `INT p_ppanic(HANDLE pid, INT nPanic)` — panic another process (e.g. a server + panicking a client that sent garbage). + +All three return `E_FILE_NXIST` (no such process) or `E_GEN_FAIL` (target is the +null/supervisor/file-server process). [PLIB §6] + +**Termination notification.** `p_logona(pid,&status)` (async, signals the +caller's I/O semaphore), `p_logon(pid,mType)` (delivers an IPC message — for +servers tracking clients), `p_watchall` (message on *any* process termination — +used by the shell). Each delivers a 16-bit **process termination word** whose high +byte is one of: `E_NORMAL_EXIT` (low byte = the `nReason`), `E_PANIC_EXIT` (low +byte = the panic number), or `E_TASK_PANIC_EXIT` (a task it owned panicked). +`p_logoffa`/`p_logoff` cancel; after `p_logoffa` the status word gets +`E_FILE_CANCEL` and you must still `p_waitstat`. [PLIB §6] + +### 2.5 Inter-process communication (IPC) + +Choices, roughly in order of coupling: + +1. **Inter-process messaging** — the primary client/server mechanism (below). +2. **Shared memory segments** — a named external segment opened by several + processes, guarded by a semaphore. +3. **Cross-process copy** — `p_pcpyfr` / `p_pcpyto` (a server pulling/pushing a + client's data by reference passed in a message). +4. **Termination logon** and the **notifier** service as lightweight signalling. + +**Messaging model** [PLIB §12; SYS §5]. Designed for client/server, one server / +many clients. A receiver first calls `p_minit(nMess, lMess)` to allocate `nMess` +**message slots** from its heap, each an `E_MESSAGE` header +(`next`, `status`, `type`, `pid`) + `lMess` data bytes. The message length is +fixed by the **server**; the file server and supervisor both use `lMess` = 8. When +data exceeds the slot, the message carries an **address+length by reference** and +the server uses `p_pcpyfr`/`p_pcpyto`. + +Lifecycle: sender `p_msend*` copies the message into a free slot and enqueues it → +server `p_mreceivew` (or async `p_mreceive`) dequeues from the **front** and gets +the slot address → server processes it → `p_mfree` frees the slot **and**, if the +client used a "receive" variant, writes back a completion status and signals the +client's I/O semaphore. + +- Client: `p_msend` (blind), `p_msendreceivew` (send + wait for reply), + `p_msendreceivea` (send + set up an async reply). `p_mcancel` cancels a pending + `p_mreceive`. +- If the server has no free slot, the send **blocks on a mutual-exclusion + semaphore** — even the "async" variant can block indefinitely. Multi-client + servers therefore pre-allocate a slot per potential client (≈ `E_MAX_PROCESSES` + minus known processes). +- **Priority queue-jumping:** a message from a client of priority **≥ 0x80** is + inserted at the **front** of the server queue (overtaking others), so a + foreground task's requests are served first. [PLIB §12; SYS §5] +- For relative priorities to work, a **multi-client server should run at a higher + priority than any client**; a client waiting on a low-priority server is + effectively lowered to the server's priority. [PLIB §12] + +The message and I/O systems share status-word + I/O-semaphore mechanics, so async +messaging and async I/O can be mixed freely. [SYS §5] + +--- + +## 3. Panics + +A **panic** is a fatal exception. When EPOC detects a condition that "could only +arise from a bug", it **terminates the process immediately** with a panic number +in **0–255**. Applications cannot trap or survive a panic (contrast `p_leave`, +which is recoverable). Panicking is cheaper than an error return and enforces +discipline by killing the defect at the point of detection. The OS still uses +**error returns** where a condition can arise from legitimate user action — e.g. +`p_alloc` returns NULL on genuine memory exhaustion but **panics** if it finds the +heap *corrupted*. [GEN §2; PLIB §6] + +Panics are raised by `p_panic` (self) or `p_ppanic` (another process — used by a +server against a misbehaving client). Applications may call `p_panic` themselves +to catch "impossible" cases (e.g. a `switch` default). [PLIB §6] + +### 3.1 Panic-number ranges — how to interpret one + +| Range | Origin | Where documented | +|---|---|---| +| **0–80, and 255** | PLIB library | PLIB Reference (§6) | +| **81–129** | Window Server library | Window Server Reference | +| **130–160** | OLIB object library | OLIB Reference | +| **160–254** | Non-ROM code (e.g. ISAM and other loadable libraries) | the relevant library's manual | + +[GEN §2; PLIB §6] + +Notes on reading a panic: + +- A given number in the **160–254** band may be used by **more than one** + non-ROM component, so it is ambiguous — the only definitive way to locate the + source is to catch it under the **SIBO Debugger** and trace back. [GEN §2] +- The manual overview says PLIB owns 0–80 & 255; the detailed PLIB list also + documents specific panics at **60, 65–69, 77, 78** etc., and warns that a panic + **79** or anything **81–254** may belong to another component. Window-server + panics are cited as 81–110 in one place and OLIB as 130–158 in another (both + inside the summary bands above). [PLIB §6] +- Panics described as *"Invalid function number for …"* rarely indicate a + specific coding bug; they usually mean a **trashed return address** sent the IP + wandering into arbitrary code. [PLIB §6] + +### 3.2 Selected PLIB system panic numbers (0–80, 255) + +| # | Meaning | +|---|---| +| 0 | Test-code failure | +| 1–5 | Semaphore manager: bad function no. / handle / not allocated / negative initial count / negative signal count | +| 6–8 | Process manager: bad function no. / invalid PID / task tried to create a task | +| 9 | Time manager: bad function number | +| 10–14 | Segment manager: bad fn / negative size / bad type / bad handle / copy out of range | +| 15–19 | Heap manager: bad fn / heap not initialised / cell reduced by more than its size / granularity > `E_MAX_GROWBY` / **cell address outside heap (heap corrupted — try `p_allchk`)** | +| 20–23 | IPC message manager (bad fn; already init'd (double `p_minit`); not init'd; zero-length queue) | +| 24–26 | I/O manager: bad fn / **invalid I/O channel** (unchecked `p_open`, closed/overwritten handle) / device requested panic | +| 27 | Invalid wait-handler handle (often really a low-address overwrite at addr 2 via an uninitialised pointer) | +| 28–29 | Key/pointer already hooked / requester not a task | +| 30–31 | Device manager: bad fn / bad device handle | +| 32–34 | File manager: bad fn / already connected to file server / reserved | +| 35–41 | Library manager & file-server connection (bad handle/fn, invalid LIB channel, bad DYL index, invalid message to file server, **not connected to file server** = 41) | +| 42–47 | Conversion / general manager fns; unhook-notify-when-not-hooked; invalid revector address | +| 48 | `p_leave` called before `p_enter` | +| 49–56 | OOP errors (no method, invalid reclass, unknown category/class, supersend outside method, handle before link, missing external categories, object not a valid class) | +| 57 | Invalid link-layer completion code | +| 58–59 | Invalid fn for window server / hardware manager | +| 60 | **Write outside process data segment** (uninitialised pointer / corrupt structure) | +| 61 | Interrupts disabled too long | +| 63–64 | Divide-by-zero / overflow interrupt | +| 65–67 | Dbf manager: bad fn / bad DBF I/O channel / bad DBF parameter | +| 68 | **Address-zero overwrite** (the `0xDEAD` word changed — uninitialised pointer) | +| 69 | Less than 0x100 bytes of stack remain (large automatics — make them static or heap) | +| 70 | Environment name > `EnvMaxNameSize` | +| 71 / 72 | Single-step (INT 1) / breakpoint (INT 3) | +| 73 | **A request was made while an async request of the same type on the same channel was still pending** | +| 74–76 | Serial-I/O bad fn / ASIC1 call on an ASIC9 machine / DYL not in a visible bank | +| 77 | Floating-point emulator exception | +| 78 | Semaphore count exceeds 0x7FFF | +| 80 | Library fatal error (preceded by a specific-error notification) | +| 255 | `p_allchk` detected a corrupted heap | + +[PLIB §6 "System panic numbers"] + +--- + +## 4. Error codes + +System functions signal failure by convention: + +- Functions returning an **address** return **NULL (0)** on failure. +- Otherwise: **zero or positive = success, −1 = failure** (`E_GEN_FAIL` = −1; you + may just test the sign). +- When the error is elaborated, a **negative system error number in −1..−128** is + returned, allocated in bands: [PLIB §6 "Error returns"] + +| Band | Family | Header | +|---|---|---| +| −1 … −31 | General errors `E_GEN_xxx` | `p_gen.h` | +| −32 … −63 | I/O / device / file errors `E_FILE_xxx` | `p_file.h` | +| −64 … −95 | Reserved | — | +| −96 … −128 | OPL run-time errors | — | + +`p_errs(TEXT *str, INT errno)` converts a system error number to a +language-dependent string (buffer ≥ `E_MAX_ERROR_TEXT_SIZE` = 64; unknown numbers +give `"Unknown error [xx]"`). The notifier services `p_notify` / `p_notifyerr` +present errors to the user; `p_setnotify(FALSE)` suppresses automatic notification +for unattended/server processes. [PLIB §6] + +### 4.1 `E_GEN_*` (general) family + +The named generics referenced across the manuals (all defined in `p_gen.h`, +values in −1..−31): + +| Symbol | Value | Meaning | +|---|---|---| +| `E_GEN_NONE` | 0 | No error / success | +| `E_GEN_FAIL` | **−1** | General failure (also the generic "just test the sign") | +| `E_GEN_ARG` | *−n* | Bad argument (e.g. invalid `double`, buffer too small, out-of-range command length) | +| `E_GEN_OVER` | *−n* | Overflow — value too large for the target type/representation | +| `E_GEN_UNDER` | *−n* | Underflow — magnitude too small to represent | +| `E_GEN_RANGE` | *−n* | Value outside a permitted range (e.g. priority) | +| `E_GEN_INUSE` | **−9** | Resource in use (e.g. segment usage count > 0) | +| `E_GEN_NOMEMORY` | *−n* | Insufficient (system) memory; `f_alloc`/`f_realloc` `p_leave` with this | +| `E_GEN_NOPROC` | *−n* | No free process slots | +| `E_GEN_NOSEM` | *−n* | No semaphores available (`p_semcrt`) | +| `E_GEN_IMAGE` | *−n* | File is not a valid image, or is corrupt | + +> **Uncertainty.** Of the `E_GEN_*` family only two exact numeric values are +> **fixed by the task's cross-check set and consistent with the manuals**: +> `E_GEN_FAIL = −1` and `E_GEN_INUSE = −9`. The manuals reference the other +> `E_GEN_*` symbols by name and behaviour but the two source files do **not** +> tabulate their individual numeric values (they live in `p_gen.h`). Values shown +> as *−n* above are known only to fall in the −1..−31 band; do not assume specific +> numbers without the header. [PLIB §6, and usage throughout] + +### 4.2 `E_FILE_*` (I/O / file / device) family + +Defined in `p_file.h`, values in −32..−63. Anchor values confirmed against the +task's cross-check set: + +| Symbol | Value | Meaning | +|---|---|---| +| `E_FILE_EXIST` | **−32** | Already exists (file, or a differently-checksummed process/segment of the same name) | +| `E_FILE_NAME` | **−38** | Invalid name (file / memory-segment name) | +| `E_FILE_DEVICE` | **−41** | Device does not exist / device driver not found | +| `E_FILE_PENDING` | *−n* | Async request still outstanding (see §5) — the status-word "in progress" marker | +| `E_FILE_CANCEL` | *−n* | Request was cancelled (written to the status word by a cancel) | +| `E_FILE_NXIST` | *−n* | Does not exist (file, directory entry, or **process**) | +| `E_FILE_DIR` | *−n* | Directory does not exist | +| `E_FILE_NOTREADY` | *−n* | Device present but no medium (e.g. no SSD in the pack) | + +> **Uncertainty.** Three exact values are pinned by the cross-check and match the +> manuals' usage: **`E_FILE_EXIST = −32`, `E_FILE_NAME = −38`, +> `E_FILE_DEVICE = −41`.** The remaining `E_FILE_*` symbols appear by name in the +> manuals (e.g. as `p_execc`, `p_setpri`, segment, and async return values) but +> the two source files do **not** enumerate their numeric values — those are in +> `p_file.h`. Serial-port variants such as `E_FILE_PARITY` are mentioned in prose +> without numbers. [PLIB §6/§7/§12] + +Observed usage tying symbols to operations: `p_execc` → `E_FILE_DEVICE`, +`E_FILE_NOTREADY`, `E_FILE_DIR`, `E_FILE_NXIST`, `E_FILE_EXIST`; `p_setpri` / +`p_presume` / termination calls → `E_FILE_NXIST`; segment create/open → +`E_FILE_EXIST`, `E_FILE_NAME`; segment delete → `E_GEN_INUSE`. [PLIB §7, §12, §6] + +--- + +## 5. Asynchronous I/O model + +### 5.1 Two-step services, status words, the I/O semaphore + +Most services exist in two halves: **make the request**, then **wait for +completion**. A *synchronous* function does both; the internal *asynchronous* +function issues the request and returns at once. Async request functions include +`p_ioa`/`p_ioc` (on an open I/O channel), `p_mreceive` (IPC), `p_execcasync` +(image load), and `p_logona` (termination). [PLIB §8] + +Every process gets an **I/O semaphore** at creation ("asynchronous-request +semaphore" would have been the accurate name). Each async request is tied to a +caller-supplied **status word** (a signed 16-bit `WORD`). The invariant for +*every* async request: [GEN §5; PLIB §8] + +1. On issue, the status word is set to the negative **`E_FILE_PENDING`**. +2. On completion, a value **other than** `E_FILE_PENDING` is written — **≥ 0** for + success, a **negative error number** for failure. +3. The write to the status word happens **before** the caller's I/O semaphore is + signalled. Both steps are integral to the mechanism. + +**`p_iowait`** decrements the I/O semaphore and blocks until it is non-negative, +i.e. until *something* has been signalled. It does **not** tell you *which* +request completed — after it returns you **poll the status words** for the first +that is no longer `E_FILE_PENDING`. The semaphore starts at 0; because `p_iowait` +decrements first, it returns only when a signal has arrived. [GEN §5; PLIB §8] + +```c +p_ioc(SerialChannel, P_FWRITE, &SerialStatus, str, &len); /* async write */ +p_ioc(TimerChannel, P_FRELATIVE, &TimerStatus, &timeout); /* async 5s timer */ +p_iowait(); /* block on one */ +if (SerialStatus == E_FILE_PENDING) { /* timed out: cancel the write */ + p_iow(SerialChannel, P_FCANCEL); + p_waitstat(&SerialStatus); + p_leave(SERIAL_TIMEOUT); +} +p_iow(TimerChannel, P_FCANCEL); +p_waitstat(&TimerStatus); +``` +[PLIB §8, adapted] + +**Prioritisation** is implicit in **poll order**: if two requests have completed, +whichever status word you poll first is serviced first. A low-priority source can +be "locked out" by a rapidly firing higher one. [GEN §5] + +**Where to declare status words** — in static data or in a heap control block, +**never** on a stack frame that will return before the request completes. A stale +status word is later written to random memory. [GEN §5] + +### 5.2 The `p_io?` calls and channels + +I/O devices share a uniform interface: `p_open` (by name, returns a channel +handle) → request services → `p_close`. Synchronous requests use **`p_iow`** +(convenience wrappers `p_iowN`); asynchronous use **`p_ioa`** / **`p_ioc`** +(wrappers `p_ioaN`, `p_iocN`). The async form always takes **one extra parameter** +— the status-word address. `p_ioa` vs `p_ioc` are the async requestors used in the +examples ("a" = asynchronous). [GEN §5; PLIB §8/§9] + +### 5.3 The semaphore & I/O-semaphore primitives + +EPOC semaphores are **counting** semaphores (signed value): `p_signal` +increments, `p_wait` decrements; a negative value means processes are queued +(released FIFO). [PLIB §8] + +| Function | Effect | +|---|---| +| `HANDLE p_semcrt(INT nCount)` | Create a semaphore, initial count `nCount` (≥0); returns handle or `E_GEN_NOSEM`. Auto-deleted on process exit. | +| `void p_semdel(HANDLE s)` | Delete (waiters are released). Rarely needed pre-v3. | +| `void p_wait(HANDLE s)` | Decrement; block if it goes negative. | +| `void p_signal(HANDLE s)` | Increment; release the first waiter and reschedule. | +| `void p_signaln(HANDLE s, INT n)` | `p_signal` × n. | +| `void p_signalnr(HANDLE s)` | Signal **without** rescheduling (reschedule waits for the next tick, or force one with `p_sleept(0)`). | +| `void p_iosignal(void)` | Increment **this** process's I/O semaphore (used by wait handlers, or to raise an "internal event"). | +| `void p_iosignalbypid(HANDLE pid)` | Signal another process's I/O semaphore (after setting its status word via `p_pcpyto`). | +| `void p_iowait(void)` | Wait on the I/O semaphore; runs active wait handlers. | +| `void p_waitstat(WORD *pstat)` | Wait until `*pstat != E_FILE_PENDING`, correctly re-signalling any other completions seen meanwhile. | +| `void p_ioyield(void)` | `p_iosignal` + `p_iowait` — give installed wait handlers a chance to run. | + +[PLIB §8] + +**Discipline.** Every `p_iosignal` must be matched by a `p_iowait`, and its status +word must already be complete when signalled. An unmatched signal is a **"stray +signal"** at Psion — the next `p_iowait` returns spuriously with nothing ready. +Guard against it (e.g. `p_panic` if the post-`p_iowait` poll finds no completed +word); the **Spy** app helps spot accumulated stray signals. Making a second +request while one of the same type is pending on the same channel → **panic 73**. +[PLIB §8; GEN §5] + +**Waiting for one specific request:** use **`p_waitstat`**, not `p_iowait` — it +returns only when *that* status word completes and re-issues signals for other +completions it absorbs. General-purpose synchronous wrappers **must** use +`p_waitstat` (they cannot assume no other requests are pending). [PLIB §8] + +### 5.4 Cancelling a request + +General principles for all cancels (`p_iow(P_FCANCEL)` for channels, `p_mcancel` +for IPC, `p_logoffa` for termination): [GEN §5; PLIB §8] + +- A cancel **precipitates completion**; it does **not** un-issue the request. +- It **may or may not** be effective — the operation may finish naturally first. +- **You must still consume the completion**, normally with an immediate + `p_waitstat`. If the request was still outstanding, `E_FILE_CANCEL` is written + to the status word and the process is signalled. + +Canonical timer cancel: + +```c +p_iow2(timH, P_FCANCEL); /* synchronous; completes at once */ +p_waitstat(&timstat); /* use up the resulting signal */ +``` + +Forgetting the `p_waitstat` after `P_FCANCEL` is a **common bug**: the next +`p_iowait` then returns immediately although nothing is actually ready. [GEN §5] + +### 5.5 Wait handlers and interrupt-driven completion + +**Wait handlers** run *inside* `p_iowait`/`p_waitstat`, just before it would +return. Install with `p_svecadd`, remove with `p_svecrem`, (de)activate with +`p_sveccall`. Each returns `P_SIGNAL_DISABLE` (completion handled, deactivate), +`P_SIGNAL_ENABLE` (handled, keep active — more pending), or `P_SIGNAL_UNUSED` +(nothing of mine — the signal is external). `p_iowait` returns to the caller only +when all active handlers report `P_SIGNAL_UNUSED`. The handler queue hangs off a +4-byte header in the reserved static at **address 2**; corrupting it usually +yields **panic 27**. [PLIB §8] + +This matters because a **hardware interrupt handler cannot write to the requesting +process's data segment** (it may be mid-move). Instead the driver writes to a +fixed location and signals the I/O semaphore; the driver's **wait handler** later +copies the data into the process data segment when the process next enters +`p_iowait`. Therefore, if you **poll** a status word (e.g. between chunks of a long +computation) instead of blocking, call **`p_ioyield` before each poll** so those +handlers can run — and still consume the signal with `p_iowait` once the poll sees +completion, or you get a stray signal. Tight-loop polling without yielding is +"anti-social" (it hogs the CPU). Server-implemented drivers (e.g. the window +server) need no wait handler. [PLIB §8] + +--- + +## 6. The SIBO OS executive call (assembly-level interface) + +At the C level, `p_iow`/`p_ioa`/`p_ioc` are thin shells over EPOC's ROM services. +Those services are reached by an **80C86 software interrupt** (`INT XXH`). Two +flavours: **single-service** (`INT XXH` — one dedicated interrupt per common +service, minimal overhead) and **multi-service** (`MOV AH, ZZH` then `INT XXH`, +where `ZZ` selects the function). [SYS §"System services"] + +**General calling convention** [SYS §"Calling conventions"]: + +- All registers except **AX** are preserved unless they carry a return value; AX + is a scratch/return register. +- **Error** ⇒ **carry flag set**, error number in **AL** (always negative); other + result registers are then indeterminate. +- Handles are 16-bit, guaranteed positive, non-zero and **even**; passed in **BX** + where possible; results returned in **AX**. +- A programmatically invalid argument ⇒ the process is **terminated immediately** + (a panic), not an error return. + +**The I/O executive specifically.** The manual documents the underlying I/O +services at register level: + +- **`IoAsynchronous`** — `AL` = I/O function number, `BX` = I/O handle, + `DS:CX` → arg1, `DS:DX` → arg2, `DS:DI` → status word; returns `AX` = device + driver result (carry set ⇒ error). This is exactly the layer beneath `p_ioa`/ + `p_ioc`: on success the driver later signals completion via + `IoSignal`/`IoSignalByPid`, and you wait with `IoWaitForSignal` / + `IoWaitForStatus` (the `p_iowait`/`p_waitstat` primitives). Panics `PanicIo1` + (bad channel), `PanicLib1`/`PanicLib2` (bad library handle/function). +- **`IoWithWait`** — same inputs minus the status word (`AL`, `BX`, `DS:CX`, + `DS:DX`), returns `AX` = result; the **synchronous** service beneath `p_iow`. +- `IoAsynchronousNoError` — as `IoAsynchronous` but start-up errors are reported + through the status word + signal rather than the carry flag. [SYS §8 "Input + Output Management"] + +**[RE — reverse-engineered]** Prior reverse-engineering of a SIBO ROM identifies +the I/O executive concretely as **`INT 0xCF`**, with **`CL` = function code, +`BX` = channel handle, `DX` = argument, result in `AX`** — i.e. the actual +interrupt vector and register wiring under `p_iow`. This is *consistent in shape* +with the documented `IoWithWait`/`IoAsynchronous` services above (handle in BX, +result in AX, args passed by register), but note two things the manuals do **not** +corroborate and that should be treated as RE findings only: + +- The manuals **never name the interrupt number** for any service (`INT XXH` is + left abstract throughout `[SYS]`); **`0xCF` comes from RE, not the SDK docs.** +- The documented multi-service convention puts the **function code in `AH`**, and + the I/O services put it in **`AL`** with args in `CX`/`DX` and the status word in + `DI`. The RE note's **`CL` = function / `DX` = argument** differs from the + published register layout. The discrepancy may reflect a particular wrapper, + ROM version, or single-service entry point observed during RE; **do not treat + the `INT 0xCF` / `CL`/`BX`/`DX`/`AX` mapping as documented** — it is an + RE observation to be verified against the specific ROM. + +--- + +## 7. Workabout / Workabout MX notes + +The manuals cover the SIBO family collectively (HC, MC, Series 3/3a, Workabout); +the programming model above — small model, segment moving, panics, error +families, async I/O, the executive interrupt — applies to the Workabout as it does +to the rest of the range. **[MX]** The Workabout MX is a later, faster +(higher-clock, more-RAM) member of the same SIBO/EPOC-16 line; **the SDK source +manuals used here (v2.30, 1999) do not carry MX-specific programming differences**, +so any MX deltas (RAM sizing, ROM version, clock/timer specifics) are **not +established from these sources** and are left as an explicit uncertainty rather +than guessed. Version-gated APIs seen above (`p_totalK` needs EPOC ≥ 3.50; +`p_semdel` recommended only from v3) are the kind of thing to check against the +actual MX ROM version via `p_getver` / the version services. [GEN §7; PLIB §7] + +--- + +### Source map + +- Memory model & allocation: **[PLIB §1, §7]**, **[GEN §1]** +- Processes, priorities, IPC, termination: **[PLIB §6, §12]**, **[SYS §5, §10]** +- Panics & panic ranges: **[GEN §2]**, **[PLIB §6]** +- Error families: **[PLIB §6]** (+ usage in §7/§12) +- Async I/O, semaphores, wait handlers, cancel: **[GEN §5]**, **[PLIB §8, §9]** +- Executive interrupt / register conventions: **[SYS]** "System services", + "Calling conventions", §8; **`INT 0xCF` mapping = [RE], not in the manuals.**