Files
sibo-playground/docs/reference/02-system.md
T

720 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (07, 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**`<name>.$nn` where `<name>` is 18 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
`<name>.$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, 1255**. 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 **0255**. 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 |
|---|---|---|
| **080, and 255** | PLIB library | PLIB Reference (§6) |
| **81129** | Window Server library | Window Server Reference |
| **130160** | OLIB object library | OLIB Reference |
| **160254** | 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 **160254** 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 080 & 255; the detailed PLIB list also
documents specific panics at **60, 6569, 77, 78** etc., and warns that a panic
**79** or anything **81254** may belong to another component. Window-server
panics are cited as 81110 in one place and OLIB as 130158 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 (080, 255)
| # | Meaning |
|---|---|
| 0 | Test-code failure |
| 15 | Semaphore manager: bad function no. / handle / not allocated / negative initial count / negative signal count |
| 68 | Process manager: bad function no. / invalid PID / task tried to create a task |
| 9 | Time manager: bad function number |
| 1014 | Segment manager: bad fn / negative size / bad type / bad handle / copy out of range |
| 1519 | 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`)** |
| 2023 | IPC message manager (bad fn; already init'd (double `p_minit`); not init'd; zero-length queue) |
| 2426 | 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) |
| 2829 | Key/pointer already hooked / requester not a task |
| 3031 | Device manager: bad fn / bad device handle |
| 3234 | File manager: bad fn / already connected to file server / reserved |
| 3541 | 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) |
| 4247 | Conversion / general manager fns; unhook-notify-when-not-hooked; invalid revector address |
| 48 | `p_leave` called before `p_enter` |
| 4956 | 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 |
| 5859 | Invalid fn for window server / hardware manager |
| 60 | **Write outside process data segment** (uninitialised pointer / corrupt structure) |
| 61 | Interrupts disabled too long |
| 6364 | Divide-by-zero / overflow interrupt |
| 6567 | 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** |
| 7476 | 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.**