39 KiB
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$NULLandSYS$MANG, created at startup and never resized. Extensions.LDD/.PDDdenote 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:
- reserved static variables (0x40 bytes; "magic statics")
- floating-point emulator data space (0x200 bytes from offset 0x100)
- processor stack
- initialised static variables
- uninitialised static variables (also zero-initialised)
- 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 — <name>.$nn where <name> 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
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_execceventually calls it. TheE_CPBblock carries code/ stack/data/heap paragraph counts, initial IP, checksum,minHeap, initialpriority, 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, 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 andE_MAX_PRIORITY= 192. [SYS §10] speaks only of reserved bandscPBMinPriority/cPBMaxPrioritywithout giving numbers, and the [SYS] service-number appendix lists a symbolE_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.nReasonin −127..128; 0 = clean exit; negative = an init-failure error code; positive = an exit status to a parent. Falling off the end ofmaincallsp_exitwithmain's return value (falling off avoid mainis 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 calledp_onterminate(nMessage), it is merely sent that message and gets to run cleanup and exit itself; otherwise it behaves likep_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:
- Inter-process messaging — the primary client/server mechanism (below).
- Shared memory segments — a named external segment opened by several processes, guarded by a semaphore.
- Cross-process copy —
p_pcpyfr/p_pcpyto(a server pulling/pushing a client's data by reference passed in a message). - 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_mcancelcancels a pendingp_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_PROCESSESminus 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 = −1andE_GEN_INUSE = −9. The manuals reference the otherE_GEN_*symbols by name and behaviour but the two source files do not tabulate their individual numeric values (they live inp_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 remainingE_FILE_*symbols appear by name in the manuals (e.g. asp_execc,p_setpri, segment, and async return values) but the two source files do not enumerate their numeric values — those are inp_file.h. Serial-port variants such asE_FILE_PARITYare 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]
- On issue, the status word is set to the negative
E_FILE_PENDING. - On completion, a value other than
E_FILE_PENDINGis written — ≥ 0 for success, a negative error number for failure. - 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]
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_CANCELis written to the status word and the process is signalled.
Canonical timer cancel:
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; returnsAX= device driver result (carry set ⇒ error). This is exactly the layer beneathp_ioa/p_ioc: on success the driver later signals completion viaIoSignal/IoSignalByPid, and you wait withIoWaitForSignal/IoWaitForStatus(thep_iowait/p_waitstatprimitives). PanicsPanicIo1(bad channel),PanicLib1/PanicLib2(bad library handle/function).IoWithWait— same inputs minus the status word (AL,BX,DS:CX,DS:DX), returnsAX= result; the synchronous service beneathp_iow.IoAsynchronousNoError— asIoAsynchronousbut 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 XXHis left abstract throughout[SYS]);0xCFcomes from RE, not the SDK docs. - The documented multi-service convention puts the function code in
AH, and the I/O services put it inALwith args inCX/DXand the status word inDI. The RE note'sCL= 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 theINT 0xCF/CL/BX/DX/AXmapping 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 0xCFmapping = [RE], not in the manuals.