Compare commits

...
Author SHA1 Message Date
lyrathorpe 03b4e219bd docs(reference): link scanner API + continuation guide in index 2026-07-06 23:36:53 +01:00
lyrathorpe 88849a4a7f docs(inventory): record oscanner category token + SIBO Debugger DYL breakpoints; link CONTINUATION.md 2026-07-06 23:36:15 +01:00
lyrathorpe 4937811a01 docs(inventory): on-device debugging continuation guide to finish the OO scanner client 2026-07-06 23:36:14 +01:00
lyrathorpe 9b3dc3000b docs(inventory): paradigm correction - scanner is an OO library object (LIBMANAGER/MESSMANAGER), driven via p_getlibh/p_newsend/p_send 2026-07-06 23:25:47 +01:00
lyrathorpe a8a0102a2f docs(mx-re): add Ghidra headless decompilation to the toolchain 2026-07-06 23:20:57 +01:00
lyrathorpe 038c38f029 docs(inventory): decompiled (Ghidra) read path; bottoms out at int 0x84/0xd2 services 2026-07-06 23:20:56 +01:00
lyrathorpe 961526b545 docs(inventory): retrieval is via OS-service handle + OO DYL messaging (per-vector register convention) 2026-07-06 22:49:49 +01:00
lyrathorpe 32d621e769 docs(reference): fix RE toolchain link path in index 2026-07-06 22:36:54 +01:00
lyrathorpe c80e7c8879 docs(reference): 08-re-boot-oscalls — SIBO/Workabout MX programming reference 2026-07-06 22:29:49 +01:00
lyrathorpe 43353d4019 docs(reference): 07-hardware — SIBO/Workabout MX programming reference 2026-07-06 22:29:48 +01:00
lyrathorpe 0ccd38994d docs(reference): 06-ui — SIBO/Workabout MX programming reference 2026-07-06 22:29:48 +01:00
lyrathorpe 0e765bc90c docs(reference): 05-filesystem-dbf — SIBO/Workabout MX programming reference 2026-07-06 22:29:47 +01:00
lyrathorpe 2b6804ab18 docs(reference): 04-plib-core — SIBO/Workabout MX programming reference 2026-07-06 22:29:46 +01:00
lyrathorpe 25078af7fc docs(reference): 03-io-devices — SIBO/Workabout MX programming reference 2026-07-06 22:29:45 +01:00
lyrathorpe 22a92dda5a docs(reference): 02-system — SIBO/Workabout MX programming reference 2026-07-06 22:29:44 +01:00
lyrathorpe 51d864d96d docs(reference): 01-building-apps — SIBO/Workabout MX programming reference 2026-07-06 22:29:44 +01:00
lyrathorpe 1a54086cc2 docs(reference): 00-index — SIBO/Workabout MX programming reference 2026-07-06 22:29:43 +01:00
lyrathorpe 8714b619c6 docs(mx-re): RE toolchain (MAME psionwamx headless + radare2) and documentation plan 2026-07-06 22:19:18 +01:00
lyrathorpe ae4a619172 docs(inventory): confirm WL2:D open + ops 6/7 trigger; record data-retrieval as open gap 2026-07-06 22:08:40 +01:00
lyrathorpe bf5aa1b231 feat(inventory): WL2:D async read (p_ioc + trigger + p_iowait) 2026-07-06 21:56:22 +01:00
lyrathorpe 2731d22266 feat(inventory): WL2:D trigger(6/7)+p_read loop with length + re-trigger 2026-07-06 21:52:26 +01:00
lyrathorpe edda57bdce feat(inventory): RE-based WL2:D init (enable ops 6/7) + read 2026-07-06 21:44:10 +01:00
lyrathorpe 62d277e5d6 docs(inventory): add reverse-engineered WL2 init sequence + default Symbol2 param block 2026-07-06 21:42:36 +01:00
lyrathorpe fc4df77300 docs(inventory): first-ever reverse-engineered Workabout MX scanner API notes 2026-07-06 21:35:08 +01:00
lyrathorpe 1544159afc diag(inventory): try WL2:D (untried unit) then WL2:A; read + dump 2026-07-06 21:23:19 +01:00
lyrathorpe 4bb853cdde build: add the workabout rom 2026-07-06 21:14:55 +01:00
lyrathorpe cdea110e60 diag(inventory): read integral laser via WL2:A (device demman uses) 2026-07-06 20:54:14 +01:00
lyrathorpe 0ec706d658 diag(inventory): probe BAR:A/B/C units (demman uses existing bar device) 2026-07-06 20:39:17 +01:00
lyrathorpe b79b1fa175 diag(inventory): software-trigger scan via Esc-y1J/Esc-y1K over TTY:D 2026-07-06 20:21:03 +01:00
lyrathorpe 724b5a11eb diag(inventory): minimal TTY:D read, no reconfig, one byte at a time 2026-07-06 20:14:54 +01:00
lyrathorpe b86c1b083f diag(inventory): held BAR:A/B open report + read-arms-laser test 2026-07-06 19:04:43 +01:00
lyrathorpe c99927acdf diag(inventory): keyboard/scan-key wedge test (open nothing; echo key codes) 2026-07-06 18:51:31 +01:00
lyrathorpe 397ce274e4 diag(inventory): stream one byte per read (tmask=0 full-count read was blocking) 2026-07-06 18:46:37 +01:00
lyrathorpe ff38d4eb70 diag(inventory): read decoded laser on TTY:D with P_OBEY_DSR cleared + DTR forced 2026-07-06 18:41:30 +01:00
lyrathorpe 4d8917bb02 diag(inventory): correct BAR: read to p_iow(P_FREAD,buf); probe slot letters; dump [count][type][data] 2026-07-06 18:33:34 +01:00
14 changed files with 4842 additions and 42 deletions
+144
View File
@@ -0,0 +1,144 @@
# Finishing the Workabout MX scanner — on-device continuation guide
This document describes exactly how to continue from the current findings to a
fully working scanner client, using on-device debugging. Read `SCANNER-API.md`
first for the findings this builds on.
## Where we are
Established (see `SCANNER-API.md` for detail):
- The integral laser is driven as an **OO library object** in `SCANNER.DYL`
(category token seen in ROM: **`oscanner`**), *not* by raw device I/O.
- The app path is the standard OLIB one: `p_getlibh``p_newsend`/`f_newsend`
(create + init) → `p_send` (configure / trigger / read).
- Internals confirmed: `WL2:D` is the driver's channel; control ops **6** then
**7** (`p_iow(chan,6)`, `p_iow(chan,7)`) fire the laser to a good decode
(green LED) on the physical device.
- The decompiled read (`FUN_858d`) creates the object via `LIBMANAGER`
(`NMLIBCREATE`/`NMLIBCREATEBYHANDLE`), sets up a buffer, then reads; the
decode lands CR/LF-terminated (default Symbol2 postamble).
Missing, and why on-device: the **message ordinals** and **parameter structs**
for the scanner's methods. OLIB assigns ordinals dynamically from the whole
class hierarchy (including base classes in `olib`/`hwim`), so they cannot be
read reliably from a static dump — but they resolve at runtime, where the
debugger can capture them. MAME cannot inject a barcode, so validation must be
on hardware anyway.
## What to capture
For a working client you need five things:
1. The scanner **category** (confirm the name/id for `p_getlibh` — candidate
`oscanner`).
2. The scanner **class** created by the app.
3. The **message ordinals** for: init, set-parameters, enable/trigger, read.
4. The **parameter/result structs** each message takes (esp. the read result
buffer and its length).
5. The exact **call sequence** the working app uses.
## Tooling: the SIBO Debugger
The SIBO Debugger (see manual `2-03`/`2-04`, "The SIBO Debugger") supports:
- **Remote debugging**: development PC connected to the Workabout by a serial
cable; debug up to 8 processes.
- **Breakpoints in dynamic libraries / shared code** — required to break inside
`SCANNER.DYL`.
- Single-step, trace, register and memory display.
Build your own code for source-level debugging with:
```
#pragma debug(vid=>full) /* in the .pr / source */
```
and produce the `.sym`/`.dbd` symbol files with EMAKE.
### Set-up
1. Connect the PC to the Workabout with a serial cable (PC serial ↔ Workabout
RS-232 port — note this is a *different* port from the barcode `TTY:D`).
2. Start the debugger on the PC and connect to the remote (Workabout) target
(Local/Remote CPU menu → Connect to Remote).
3. Have the Workabout ready to run either the ROM `DEMMAN.APP`/`SCANAPP` (for
Procedure A) or your test harness (Procedure B).
## Procedure A — trace the working ROM app (recommended first)
Goal: watch `DEMMAN`/`SCANAPP` drive the real scanner and record the ordinals.
1. On the Workabout, start `DEMMAN` and enter its Scanner (Barcode) demo.
2. From the debugger, attach to that process and set breakpoints on the OLIB
dispatch path so you catch the object creation and messages:
- `LIBMANAGER` (`INT 0x84`) — sub-functions `NMLIBCREATE` (`AH=5`),
`NMLIBCREATEBYHANDLE` (`AH=6`): captures the category/class and the object
handle.
- `MESSMANAGER` (`INT 0x83`) — `NMMESSSEND` and the send/receive variants:
captures each message ordinal and its argument pointer.
- Optionally the `SCANNER.DYL` method handlers we identified (init dispatch,
trigger) as cross-checks.
3. For each captured call record: `AH`, the category/class in registers, the
message **ordinal**, and the pointed-to **argument struct** (dump memory at
the pointer). For the read, note where the decoded bytes land and the
returned length.
4. Trigger a real scan and record the read message and its result buffer.
Result: the exact category, class, ordinals, param structs, and sequence.
## Procedure B — iterate a C test harness
Once Procedure A gives the ordinals (or to trial them), build a small OO client:
```c
#include <plib.h>
#include <p_object.h>
/* Fill these from Procedure A (placeholders until captured): */
#define SCAN_CATEGORY /* category id/handle for "oscanner" via p_getcat/p_getlibh */
#define C_SCANNER /* the scanner class */
#define O_SCAN_INIT /* init message ordinal */
#define O_SCAN_PARAMS /* set-parameters ordinal */
#define O_SCAN_READ /* read-a-barcode ordinal */
GLDEF_C INT main(VOID)
{
VOID *lib, *scanner;
/* result/param structs per Procedure A */
lib = p_getlibh(SCAN_CATEGORY);
scanner = p_newsend(SCAN_CATEGORY, C_SCANNER, O_SCAN_INIT, /*&initargs*/ 0);
/* configure Symbol2 + the default 11-byte param block:
04 3f 01 15 06 04 1e 80 0d 0a 06 (CR/LF postamble) */
p_send3(scanner, O_SCAN_PARAMS, /*&params*/ 0);
for (;;) {
/* send the read message; result is the decoded barcode, CR/LF-terminated */
p_send3(scanner, O_SCAN_READ, /*&result*/ 0);
/* display result ... */
}
}
```
Build with `#pragma debug(vid=>full)`, run under the debugger, single-step the
sends, and inspect return values / the result buffer. Adjust ordinals/structs
until a real scan returns the UPC.
## Turning captures into the shipped reader
- Replace `code/inventory/bcode.c` with the OO client: `bcodeOpen` does
`p_getlibh` + `p_newsend`(init) + params; `bcodeRead` does `p_send`(read) and
returns the decoded string (strip the CR/LF postamble; strip any preamble byte
`0x80` if present).
- `upc.c` (UPC-A check-digit validation) is unchanged and already correct.
- Cross-check the captured ordinals against the decompiled handlers in
`SCANNER-API.md` (`FUN_7ae3` init dispatch — decoder type at `[channel+10]`,
case 4 = Symbol; `FUN_7aa4` trigger = the `6`/`7` ops).
## Validation
- A valid UPC-A scan should return 12 digits (plus any pre/postamble), and
`upcIsValid` should pass. The default postamble is CR LF (`0x0d 0x0a`).
- Confirm repeated scans and clean shutdown (`p_send` a destroy/close message,
then `p_close` any channel).
## Reproduce the RE environment (for further static/dynamic work)
See `../mx-re/toolchain-and-plan.md`: MAME `psionwamx` (live trace/dasm),
radare2 (static), and Ghidra headless (decompilation of the DYLs). These remain
useful for reading further `SCANNER.DYL` methods, but the ordinals themselves
come from the on-device debugger as above.
+263
View File
@@ -0,0 +1,263 @@
# Workabout MX barcode scanner — API notes (reverse-engineered)
There is no official SDK documentation for the Workabout MX integral laser scanner.
The Psion SIBO C SDK and I/O Devices Reference predate the MX and describe only the
older *external* barcode modules (wands / wand-emulation), not the integral laser.
These notes are reverse-engineered from device behaviour and from the Workabout MX
ROM `w2mx_v7.20f_eng.bin` (strings), so treat unconfirmed items as such.
## Hardware
- Integral **laser** scanner, Symbol engine. The demo (`DEMMAN.APP`) reports
`Type: Laser 1223` (some units `1222`).
- It is a **decoded** scanner: it decodes in hardware and lights a green good-read
LED. It does **not** emit an undecoded (HHLC) signal.
- Trigger: the keyboard **scan key**, which the Window Server reports as key
code **368**. The key alone does not fire the laser from an arbitrary app; the
scanner software arms/reads the engine.
## Device driver and ports
`LLDEV` on the device lists a logical driver **`wl2`** (`units=1`). The scanner is
reached through this driver. The demo's "Select Scanner" screen lets you pick both
a **decoder type** and a **port** (device driver + unit letter):
- Port device drivers offered: **`TTY`**, **`WLS`**, **`WL2`**.
The MX integral laser uses **`WL2`**.
- Units seen in the ROM: **`WL2:A`** and **`WL2:D`**.
Decoder types (from the demo, "chosen from those used in standard Workabout
products"):
| Decoder | Meaning |
| --- | --- |
| `Generic` | External non-configurable reader |
| `Symbol1` | Workabout standard scanner |
| `Datalogic` | Workabout CCD |
| `HP` | Workabout Wand |
| **`Symbol2`** | **Workabout MX scanner (the integral laser)** |
## Opening the device
```c
VOID *h;
INT err = p_open(&h, "WL2:D", (UINT)-1);
```
Observed results:
| Call | Result | Meaning |
| --- | --- | --- |
| `p_open("WL2:D", -1)` | `0` | **opens** — this is the integral-laser unit |
| `p_open("WL2:A", -1)` | `-9` (`E_GEN_INUSE`) | in use — held by the resident scanner software |
| `p_open("WL2:B" / ":C")` | (untested) | — |
`WL2` has a single unit, so only one process may hold it. `WL2:A` is permanently
`-9` even with the demo closed, indicating a resident holder; `WL2:D` is the unit
an application opens.
## The 11-byte parameter block
The scanner is configured by an **eleven-byte parameter block**. The application
interprets these bytes and converts them into commands for the selected decoder
(so the same block means different wire commands for `Symbol2` vs `HP`, etc.):
| Byte | Parameter |
| --- | --- |
| Param0 | Decode security |
| Param1 | Code type (symbology select) |
| Param2 | Decode options A |
| Param3 | Decode options B |
| Param4 | General parameters |
| Param5 | ITF length 1 |
| Param6 | ITF length 2 |
| Param7 | Preamble |
| Param8 | Postamble byte 1 |
| Param9 | Postamble byte 2 |
| Param10 | General decode options |
The preamble/postamble bytes mean decoded output may carry configurable leading /
trailing characters — parsing code must account for them.
## Access model
Applications do not drive `WL2` byte-by-byte themselves; they use the ROM library
**`SCANNER.DYL`** (used by `SCANAPP.APP` and `DEMMAN.APP`). That library opens the
port, converts the 11-byte block to decoder commands, configures and enables the
engine, and returns decoded scans.
## Paths that do NOT work for the integral laser
Recorded so they are not retried:
- **`bar*.ldd` decoders (`BAREAN`, `BARC39`, …) + `BAR:` device.** These are
software decoders for the *external* wand modules. Loading `BAREAN.LDD` and
opening `BAR:A`/`BAR:B` returns `-41` (`E_FILE_DEVICE`, "no interface found in
slot") — the integral laser is not a `BAR:` expansion interface. `BAR:D`/`BAR:E`
return `-38` (`E_FILE_NAME`, invalid unit).
- **`TTY:D` serial reads.** Opening `TTY:D` powers the laser (it fires briefly),
but no decoded bytes ever arrive on the serial channel — not with default
config, not after `P_FSET` to 9600/8/1, not with `P_OBEY_DSR` cleared, not with
the intelligent-reader escape commands (`<Esc>-y1J` / `<Esc>-y1K`), not reading
one byte at a time. The integral laser's decoded data is not on `TTY:D`.
## Initialisation sequence (reverse-engineered from the ROM)
Opening `WL2:D` is not enough — the scanner must be configured and enabled first.
The sequence was recovered by disassembling `SCANNER.DYL` in the v7.20f ROM
(around file offset `0xD7AA4`). It uses the SIBO I/O executive **`int 0xCF`**,
whose convention here is: **`CL` = I/O function code, `BX` = channel handle,
`DX` = argument (by value)**, result in `AX`. From C this is the `p_iow(chan,
func, ...)` layer.
Function codes observed on the `WL2` channel (from `p_file.h`, plus WL2-specific
ones above the standard range):
| CL | Meaning |
| --- | --- |
| 6 | (`P_FDETACH` slot) used as a scanner enable/control op |
| 7 | (`P_FSET`) used as a scanner enable/control op |
| 8 | `P_FSENSE` |
| 9 | `P_FFLUSH` |
| 0x0C (12) | **WL2-specific: write a config/command byte** (byte passed in `DX`) |
The driver keeps a per-channel structure; the param block sits at **channel+4**,
with the **decoder type at offset +6** (dispatch values 1..4) and the **11-byte
parameter block at +8..+0x12**. Configuration is pushed to the engine as a series
of `cl=0x0C` writes (one command byte per call in `DX`), then enable ops
(`cl=6`, `cl=7`).
### Default Symbol2 parameter block (from the ROM)
The routine at `0xD7AB3` fills the 11-byte block with these defaults for the
Workabout MX (Symbol2) scanner:
| Byte | Param | Value |
| --- | --- | --- |
| +8 | Param0 Decode security | `0x04` |
| +9 | Param1 Code type | `0x3F` (all symbologies) |
| +0xA| Param2 Decode options A | `0x01` |
| +0xB| Param3 Decode options B | `0x15` |
| +0xC| Param4 General params | `0x06` |
| +0xD| Param5 ITF length 1 | `0x04` |
| +0xE| Param6 ITF length 2 | `0x1E` |
| +0xF| Param7 Preamble | `0x80` |
| +0x10| Param8 Postamble 1 | `0x0D` (CR) |
| +0x11| Param9 Postamble 2 | `0x0A` (LF) |
| +0x12| Param10 General decode | `0x06` |
So **decoded output is terminated by CR LF** (`0x0D 0x0A`), and Code type `0x3F`
enables all symbologies (UPC included). A reader should assemble bytes until
CR/LF and strip the preamble/postamble.
### Confirmed on-device
- `p_open(&h, "WL2:D", -1)` returns `0`.
- Issuing control ops **6 then 7** on the channel (`p_iow(h, 6)`, `p_iow(h, 7)`)
**triggers a scan**: the laser fires and decodes (green good-read LED). This is
reproducible. The trigger is one-shot — it must be re-issued for each scan.
### NOT yet solved: retrieving the decoded data
After a confirmed good read, the decoded barcode could **not** be retrieved from
the `WL2:D` channel by any tried method:
- `p_iow(h, P_FREAD, buf)` (count-in-`buf[0]` style) — no data.
- `p_read(h, buf, len)` (synchronous, with length) — no data.
- `p_ioc(h, P_FREAD, &stat, buf, &len)` + trigger + `p_iowait()` (async) — no data.
The retrieval logic in `SCANNER.DYL`/`SCANAPP` fetches the decoded data on a
**separate handle** (a driver global, e.g. `[0x13b5]`) obtained through
**OS-service stubs** of the form `mov ah,N; int 0x84/0x85/0x86/0x87; ret` — not a
plain `P_FREAD` on the `WL2:D` control channel. This resolves an earlier puzzle:
the SIBO executive uses **different register conventions per interrupt vector**
the `int 0x84` service family passes the function in `AH`, whereas the `int 0xCF`
I/O executive uses `CL`.
`SCANNER.DYL` is an **object-oriented ROM library (a DYL)**. This strongly
suggests the scanner is driven by **creating a scanner object and sending it
messages** (the OLIB/OO model), not by raw device I/O on `WL2:D`. That would
explain why every raw `WL2:D` read returned no data — wrong paradigm.
**Confirmed working on device:** `p_open("WL2:D")` + control ops `6` then `7`
trigger the laser and a good decode (green LED). Only *retrieving* the decoded
bytes is unsolved.
### Decompilation (Ghidra) — the read path
`SCANNER.DYL`/`SCANAPP` decompile cleanly in Ghidra (16-bit real-mode x86). The
app-level read routine (`FUN_858d`) is:
```c
obj->flags |= 8;
obj->[0x190] = 0;
FUN_8ce8(); FUN_8cd3(); FUN_8cd3(); // pre-read setup
obj->[0x194] = FUN_8d8e(); // acquire a read handle (int 0x84)
obj->[0x192] = FUN_8c8b(0,2,1,obj); // set up the read buffer (int 0x84; int 0xd2)
swi(0xcf)(); swi(0xcf)(); // config / enable ops on the channel
if (obj->[0x196] == 0)
obj->[0x196] = swi(0xcf)(); // P_FREAD -> decoded result stored at +0x196
else
swi(0xcf)(); // P_FCANCEL
```
The decoder-side helpers also decompile clearly: `FUN_7ae3` is the per-decoder
init (dispatch on `[channel+10]`; case 4 = the Symbol path, which calls
`FUN_6dc4` to build the `0xff 0xee …` command frame and writes the 11-byte param
block); `FUN_7aa4` is the trigger (two `int 0xCF` ops = the `6`/`7` we use).
### The scanner is an OO library object (paradigm correction)
The System Services vector table names the services: **`int 0x84` = `LIBMANAGER`**
(the Library Manager) and **`int 0x83` = `MESSMANAGER`** (object messaging).
`LIBMANAGER` sub-functions (in `AH`) are `NMLIBLOAD=0, UNLOAD=1, LINK, FIND,
HANDLE, CREATE(=5), CREATEBYHANDLE(=6), …`. So the read's `FUN_8d8e`/`FUN_8c8b`
call **`NMLIBCREATE` / `NMLIBCREATEBYHANDLE`** — they *create a scanner library
object*.
**Therefore the scanner is driven as an object-oriented library object, not by
device I/O.** `WL2:D` is what `SCANNER.DYL` opens and reads *internally*; an
application never `p_open`/`p_read`s it. Every earlier raw-device attempt was the
wrong paradigm — which is why they returned nothing.
The correct C approach uses the standard OO API (the same one the SDK's
`growbar.c` demo uses):
- `p_getlibh(category)` — get the library handle for a category.
- `p_newsend` / `f_newsend(category, class, O_..._INIT, &args)` — create the
scanner object and send its init message.
- `p_send2` / `p_send3(obj, O_...)` — send it messages (configure, trigger, read).
These C wrappers sit over `LIBMANAGER` (create/handle) and `MESSMANAGER` (send).
**Next RE target (well-defined):** extract from `SCANNER.DYL` its **category id**
(candidate token in ROM: **`oscanner`**), **class**, and the **message ordinals**
(`O_...`) for init / set-params / trigger / read, plus their parameter structs.
OLIB assigns ordinals dynamically from the whole class hierarchy (base classes in
`olib`/`hwim`), so they resolve at runtime rather than in a static dump — capture
them with the **SIBO Debugger**, which supports remote debugging (PC↔device over
serial) and **breakpoints inside dynamic libraries**. With those, a C client can
create the scanner object and message it. Validation requires the physical device
(MAME cannot inject a scan).
**See [`CONTINUATION.md`](CONTINUATION.md)** for the full on-device debugging
procedure (capture the category/ordinals/params via `LIBMANAGER`/`MESSMANAGER`
breakpoints), a C test-harness template, and how to turn the captures into the
shipped reader.
To close it, one of:
1. The **Workabout MX C SDK** `SCANNER.DYL` header / OO category definition (what
`SCANAPP` was built against) — the clean answer, and likely the only practical
one, because the interface is object-oriented.
2. A deep RE effort under MAME (`psionwamx`): automate the UI to launch `SCANAPP`,
breakpoint the read routine, and trace it. Caveats: MAME cannot inject a real
barcode (no laser input), so only the read *setup* is observable in emulation;
the reconstructed sequence must still be validated on the physical device.
## Error codes seen (from `epocdefs.h`)
| Value | Name | Meaning |
| --- | --- | --- |
| `-9` | `E_GEN_INUSE` | device already open / in use |
| `-32` | `E_FILE_EXIST` | (LDD) already loaded |
| `-38` | `E_FILE_NAME` | invalid device name / unit |
| `-41` | `E_FILE_DEVICE` | device / interface not present |
+35 -42
View File
@@ -1,66 +1,59 @@
/*
* scan.c - Phase 1 barcode diagnostic for the Workabout MX integral laser.
* scan.c - Phase 1: WL2:D scanner, asynchronous read (reverse-engineered).
*
* The EAN/UPC decoder LDD (BAREAN.LDD) is loaded, then the decoder device is
* opened. The exact device-name form and open mode are not documented in the
* available manuals (they live in the I/O Devices Reference), so this build
* tries several candidates in one run and reports which one opens - most
* likely "BAR:D" (the decoder over port D, like TTY:D), since bare "BAR:"
* returns E_FILE_NAME (-38). Once open, it dumps every byte of each read so
* the decoded UPC format can be seen.
*
* Blocking I/O; exit via the System screen.
* Confirmed: opening WL2:D and issuing control ops 6/7 triggers a scan (laser
* fires, green LED). Neither synchronous read form returned data, and SCANAPP
* in the ROM uses an async pattern (post read -> P_FREAD -> P_FCANCEL). So this
* posts an asynchronous read (p_ioc), triggers the scan (ops 6/7), then waits
* for completion (p_iowait). Decoded output is CR/LF-terminated.
*/
#include <plib.h>
#include "bcode.h"
#define NCAND 5
#define WL2_TRIG1 6
#define WL2_TRIG2 7
GLDEF_C INT main(VOID)
{
VOID *chan;
UBYTE buf[64];
INT loadErr, r, n, i, opened;
UINT k;
UBYTE buf[256];
WORD stat;
UWORD len;
INT err, i;
static TEXT *names[NCAND] = { "BAR:D", "BAR:D", "BAR:", "BAR:E", "BAR:A" };
static UINT modes[NCAND] = { (UINT)-1, 0, 0, (UINT)-1, (UINT)-1 };
loadErr = bcodeLoad();
p_printf("load %s.LDD: %d\n\n", BCODE_LDD, loadErr);
opened = 0;
for (k = 0; k < NCAND; k++) {
r = p_open(&chan, names[k], modes[k]);
p_printf("open \"%s\" mode %u: %d\n", names[k], modes[k], r);
if (r >= 0) {
opened = 1;
break;
}
}
if (opened == 0) {
p_printf("\nNothing opened. Press a key to exit.\n");
err = p_open(&chan, "WL2:D", (UINT)-1);
p_printf("open WL2:D: %d\n", err);
if (err < 0) {
p_printf("Press a key to exit.\n");
p_getch();
return 1;
}
p_printf("\nOpened \"%s\". Scan a barcode. System screen to exit.\n\n",
names[k]);
p_printf("Present a barcode under the laser. Async reads:\n\n");
for (;;) {
n = p_read(chan, buf, sizeof(buf));
if (n < 0) {
p_printf("read err %d\n", n);
len = sizeof(buf);
stat = E_FILE_PENDING;
p_ioc(chan, P_FREAD, &stat, buf, &len); /* post async read */
p_iow(chan, WL2_TRIG1); /* trigger the scan */
p_iow(chan, WL2_TRIG2);
p_iowait(); /* wait for a completion */
if (stat == E_FILE_PENDING) { /* our read didn't finish */
p_iow(chan, P_FCANCEL);
p_waitstat(&stat);
continue;
}
if (n == 0)
if (stat < 0) {
p_printf("stat %d\n", stat);
continue;
p_printf("n=%d:", n);
for (i = 0; i < n; i++)
}
p_printf("len=%d:", len);
for (i = 0; i < 40 && i < 256; i++)
p_printf(" %d", buf[i]);
p_printf(" \"");
for (i = 0; i < n; i++)
for (i = 0; i < 40 && i < 256; i++)
p_putch(buf[i] >= ' ' ? buf[i] : '.');
p_printf("\"\n");
}
+78
View File
@@ -0,0 +1,78 @@
# Workabout MX — reverse-engineering toolchain & documentation plan
Goal: reverse-engineer the Workabout MX ROM (`w2mx_v7.20f_eng.bin`, v7.20f) and
produce complete programming documentation for the device, since none exists for
the MX specifically.
## Toolchain (reproducible, headless)
ROM identifies as MAME machine **`psionwamx`** ("Workabout mx"), CPU NEC V30MX
(`psion_asic9mx`), with `psion_asic5` (barcode/UART) and the SIBO expansion slots.
Setup:
```
mkdir -p roms/psionwamx && cp w2mx_v7.20f_eng.bin roms/psionwamx/w2mx_v7.20f.bin
mame psionwamx -rompath roms -verifyroms # -> "romset psionwamx is good"
```
**Dynamic RE (MAME debugger, headless via xvfb):**
```
xvfb-run -a mame psionwamx -rompath roms -debug \
-debugscript CMDS.txt -sound none -seconds_to_run N
```
Useful `CMDS.txt` debugger commands:
- `trace FILE` then `go` — log every executed instruction (correct bank mapping).
- `dasm FILE,ADDR,LEN` — disassemble a region as currently mapped.
- `dump FILE,ADDR,LEN` — hex dump memory.
- `bpset ADDR,1,{commands}` — breakpoint with actions (e.g. dump then continue).
- `wpset ADDR,LEN,rw` — watchpoint (catch who reads/writes a driver global).
This resolves the segment/relocation mapping that flat static disassembly of the
ROM could not (e.g. `SCANNER.DYL` / `SCANAPP` code).
**Decompilation (Ghidra headless):** extract the DYL region to a slice and load
as 16-bit real-mode x86:
```
ghidra-analyzeHeadless PROJ NAME -import slice.bin -processor "x86:LE:16:Real Mode" \
-scriptPath SCRIPTS -postScript decomp.java -deleteProject
```
`SCANNER.DYL`/`SCANAPP` decompile to readable pseudo-C this way (Ghidra 12 needs
a Java GhidraScript, not Python). Caveat: TopSpeed C uses the register-based
`jpi` calling convention, so argument recovery may need convention hints, but
control flow, struct access and the `int` service calls read clearly.
**Static RE:** `radare2 -e asm.arch=x86 -e asm.bits=16 w2mx_v7.20f_eng.bin`
(good for strings, byte-pattern search, and clean fixed-mapped regions).
## What "complete documentation" covers
1. **Hardware** (largely in the HDK, to be confirmed/extended for the MX):
memory map & banking, ASIC1/2/4/5/9MX register maps, I/O ports, interrupts.
2. **Boot & OS**: reset path, hardware init sequence, the `int 0xCF` OS/executive
call and its function table, the `int 0xD9`/etc. service vectors.
3. **I/O & drivers**: the device model, `p_open`/`p_iow` function codes per driver,
and each driver: `TTY`, `WL2` (scanner), `MCR`, `PAR`, `SND`, IR, etc.
4. **Scanner** (immediate priority, partly done — see SCANNER-API.md): `WL2:D`,
Symbol2 decoder, the 11-byte param block, trigger (ops 6/7), and the
still-open **data-retrieval** path — to be nailed by tracing `SCANAPP` live.
5. **System services / libraries**: file system (DBF), window server, HWIM/FORM.
## RE order
1. Nail the scanner data-retrieval by running `SCANAPP`/`DEMMAN` under MAME,
breakpointing the `WL2` code, and tracing how the decoded bytes are delivered.
2. Map the `int 0xCF` OS call table (breakpoint the vector, log CL/BX/DX per call).
3. Document the boot + ASIC init (already partly traced).
4. Work outward to the drivers and system services.
Findings land in `docs/` as they are confirmed (e.g. `SCANNER-API.md`), each
marked confirmed-on-emulator / confirmed-on-device / inferred.
## Notes
- `int 0xCF` convention (from traced code): `CL` = I/O function code, `BX` =
channel handle, `DX` = argument; result in `AX`. This is the `p_iow` layer.
- MAME cannot inject a real barcode into the laser, so scanner *decode* may not
reproduce in emulation; but the *code path* (open/config/enable/read structure)
traces fully, which is what we need to write correct client code.
+36
View File
@@ -0,0 +1,36 @@
# Workabout MX / SIBO programming reference
A programming reference for the Psion Workabout MX and the SIBO platform,
synthesised from the Psion SDK manuals and the Hardware Development Kit, with
reverse-engineered internals from the v7.20f ROM layered on top. Built as the
foundation for developing applications (e.g. the barcode inventory app in
`code/inventory/`).
Each document cites its source manual and marks claims as documented,
**[RE]** reverse-engineered, or inferred/uncertain.
## Sections
| # | Document | Covers |
| --- | --- | --- |
| 01 | [Building applications](01-building-apps.md) | Toolchain, TopSpeed C, SIBO types, `GLDEF_C`/etc., `.pr` project files, images/libs/LDDs |
| 02 | [System & OS](02-system.md) | Processes, memory/segments, panics + ranges, error codes, async I/O, the executive call |
| 03 | [I/O devices & drivers](03-io-devices.md) | `p_open`/`p_iow`/`p_ioc`, function codes, serial (`P_SRCHAR`), per-device drivers, loading LDDs |
| 04 | [PLIB core API](04-plib-core.md) | Strings, memory, console, conversion, math, date/time, categorised function reference |
| 05 | [File system & DBF database](05-filesystem-dbf.md) | Files, streams, directories; the full DBF record/database API |
| 06 | [User interface](06-ui.md) | Window Server model, events (`WM_*`, `WGetEvent`), the `CON:` console layer |
| 07 | [Hardware](07-hardware.md) | CPU, memory/banking, ASIC1/2/4/5/9 register maps, I/O ports, interrupts, expansion |
| 08 | [Boot & OS-call internals **[RE]**](08-re-boot-oscalls.md) | Reset/ASIC-init path, OS-service `INT` vectors, boot port activity — from the live MAME trace |
Related:
- [Barcode scanner API](../inventory/SCANNER-API.md) — the Symbol2/`WL2` laser scanner (reverse-engineered): device, trigger, decoder params, and the OO-library-object architecture.
- [Scanner continuation guide](../inventory/CONTINUATION.md) — the on-device SIBO-Debugger procedure to capture the OO message ordinals and finish a working scanner client.
- [RE toolchain & plan](../mx-re/toolchain-and-plan.md) — MAME/radare2/Ghidra RE setup and roadmap.
## Status
The manual-derived sections (0107) are broad and citation-backed. The
reverse-engineered material (08, the scanner API, and the executive-call
convention notes) is being extended by tracing the ROM under MAME
(`psionwamx`). Known open items: the full `INT`-vector → service map, and the
scanner data-retrieval path.
+573
View File
@@ -0,0 +1,573 @@
# Building Applications on the Psion SIBO / Workabout in C
A reference for developing C applications for the Psion SIBO family (HC, MC,
Series 3/3a/3c, Siena, Workabout) using the SIBO C SDK and the TopSpeed C
compiler.
Sourced from the *SIBO 'C' Software Development Kit — General Programming Manual*
(v2.30, March 1999) and the *SIBO Hardware Development Kit* (May 1995) LDD
sections. Claims are drawn only from those documents; where a point is not
stated in the source it is marked as such.
> **Workabout MX note.** The manual (SDK v2.30) names the *Workabout* and
> *Siena* but does not mention a distinct "Workabout MX" model. Everything here
> that applies to the Workabout applies to the MX at the SIBO/EPOC-16 level.
> Anything genuinely MX-specific is called out explicitly; unmarked material is
> generic SIBO and holds for the MX unless noted.
---
## 1. Architecture and programming model
A SIBO machine is "a battery-powered portable computer that is based on the SIBO
architecture ... designed to minimise the size, weight and power consumption"
(General, ch. 2). Key hardware components named by the manual:
- A power-management system that "selectively powers subsystems under software control".
- Solid State Disks (SSDs) — "fast low-power silicon-based mass storage with no moving parts".
- Asynchronous serial interface running at "Mega bit rates".
- **"An 8086 class of processor (or any compatible processor such as an 80286)"** (General, ch. 2).
- Hardware protection: "address trapping of out-of-range writes and a watch-dog timer on interrupts being disabled".
- Real-time clock, ROM-resident system software, graphics LCD, and (on some models) a touch-sensitive digitising pad.
The hardware is "primarily implemented in custom ICs called ASICs ... surface-mounted static CMOS ICs throughout."
> **CPU note / uncertainty.** The manual describes the CPU only as "an 8086
> class of processor (or any compatible processor such as an 80286)". The
> commonly cited V30-class part (NEC V30 / V30H, an 8086-compatible) is **not
> named in these source documents**; treat "V30-class" as background, not a
> manual quotation. The programming-relevant fact is what the manual does state:
> it is an 8086-class, 16-bit, real-mode-segmented CPU with no 8087.
The operating system is **EPOC** (here the 16-bit EPOC that runs on SIBO; not
the later 32-bit EPOC32). Features listed (General, ch. 2):
- Preemptive multi-tasking; MS-DOS-compatible and installable file systems.
- Asynchronous services and client-server architecture (file server, window server).
- A comprehensive I/O system with many built-in I/O devices and **dynamically loadable device drivers**.
- A **reentrant function library**; "multiple processes of the same program share a single copy of the code"; code-shared dynamic link libraries.
> The terms are distinguished in the manual: *SIBO* = the hardware
> architecture; *EPOC* = the operating system designed for it. On SIBO
> machines "the system software resides on an in-built ROM."
### Fatal errors (panics)
When the system detects a condition it believes can only be a bug, it
"terminates the process with a 'panic number' in the range 0 to 255 ... There is
no way for applications to avoid being terminated when a panic has been started."
Panic-number ranges (General, ch. 2):
| Range | Source |
|-------|--------|
| 080, and 255 | PLIB library |
| 81129 | Window Server library |
| 130160 | OLIB object library |
| 160254 | non-ROM code (e.g. the ISAM library) |
`panic 80` specifically often means a program "failed to locate SYS$8087.LDD"
(the floating-point emulator LDD) — see §5.
### Why TopSpeed C (small model)
Applications are written, compiled and linked on a PC and then run/debugged on
the SIBO machine. The compiler and linker are the **TopSpeed C** system
(Clarion Software Corporation). The SDK requires the *small code model* of
TopSpeed:
> "`#model small jpi` — The code is to be compiled in small model (code and
> data segments each restricted to 64K), with the jpi (TopSpeed C) convention
> of using registers to pass parameters to subroutines." (General, ch. 3)
You may install other TopSpeed code models, but "they will not be used by the
SIBO SDK." Small model matches the 8086-class segmented architecture: a single
64K code segment and a single 64K data segment, with `CS`/`DS`/`SS` behaving as
a "pure small model" where "`ds=es=ss`" (General, ch. 4). The `jpi` calling
convention (register parameter passing) is mandatory for SIBO builds.
Because there is no 8087, "all floating point is performed by software emulation
of the 8087" (General, ch. 4).
---
## 2. CLIB vs PLIB
Two C libraries are available:
**CLIB** — "a version of the TopSpeed C library for the EPOC operating system
... a version of the standard ANSI C library." Benefits: portability (existing C
ports easily) and less to learn. Documented in the *TopSpeed C Library
Reference*, with SIBO caveats in *Notes on CLIB*.
**PLIB** — Psion's proprietary C library, providing "only very thin shells over
functionality that is present in the ROM." Functions are prefixed `p_`.
The manual "strongly urges" developers to consider PLIB over CLIB:
- "many of the ROM-based EPOC system services are not available from CLIB (eg
asynchronous I/O, inter-process messaging, the window server graphics functions)"
- "executables are larger in CLIB and the process takes a larger data segment."
CLIB is a "much 'thicker' library"; its subsystems "typically require large
static buffers and tables." You can freely mix PLIB and CLIB calls (unless using
the object-oriented UI dynamic libraries). The Window Server library is `WLIB`
(`wlib.lib`, functions `g...`/`w...`, header `wlib.h`), automatically linked
when needed.
CLIB omissions worth knowing (Notes on CLIB): many BIOS/DOS/graphics/far-pointer
functions are not implemented. `getRealHandle(int handle)` converts a CLIB file
handle to a PLIB handle. CLIB startup auto-opens a console channel assigned to
`stdin`/`stdout`/`stderr`; you can suppress it by defining `p_xwind` and setting
the global `_winHandle`.
### Standard SIBO C types
The manual's example code uses a standard set of SIBO C types in place of raw C
types. (These are defined in the SDK headers — `plib.h`/`stddefs`; the General
manual uses them but their formal definitions live in the PLIB Reference.)
Observed usage:
| Type | Meaning (from usage in the manual) |
|---------|-------------------------------------|
| `TEXT` | character / text byte (e.g. `TEXT *pb;`, `TEXT subname[P_FNAMESIZE];`) |
| `UBYTE` | unsigned 8-bit byte |
| `WORD` | signed 16-bit word |
| `UWORD` | unsigned 16-bit word (e.g. `UWORD answer;`) |
| `INT` | signed integer (return type of `main`) |
| `UINT` | unsigned integer (e.g. `UINT lcdtype;`) |
| `VOID` | void (e.g. `main(VOID)`) |
> **Uncertainty.** The exact widths/signedness of each typedef are not spelled
> out in the General manual; they are defined in the SDK headers / PLIB
> Reference. The mappings above reflect how the manual uses them and standard
> SIBO conventions. Use the uppercase SIBO types rather than raw C types in
> SIBO code.
### Storage-class macros
The example code uses uppercase storage-class macros instead of bare C storage
classes. These make the code portable across the segmented model and the SDK's
linkage conventions.
| Macro | Used for | Manual examples |
|-------|----------|-----------------|
| `GLDEF_C` | **global definition** of code (a function you define, visible externally) | `GLDEF_C INT main(VOID)`, `GLDEF_C VOID main(VOID)` |
| `GLREF_C` | **global reference** to code / an external symbol (declared elsewhere) | `GLREF_C TEXT *DatCommandPtr;` |
| `GLDEF_D` | global definition of data | (data counterpart of `GLDEF_C`) |
| `GLREF_D` | global reference to data defined in another module | `GLREF_D TEXT *DatCommandPtr;` |
| `LOCAL_C` | file-local (`static`) function | `LOCAL_C VOID QueueKey(VOID)`, `LOCAL_C VOID CancelTimer(VOID)` |
| `LOCAL_D` | file-local (`static`) data | (data counterpart of `LOCAL_C`) |
> `_C` suffix = code/function, `_D` suffix = data; `GL...` = global (external
> linkage), `LOCAL_...` = module-private. The General manual shows `GLDEF_C`,
> `GLREF_C`, `GLREF_D`, and `LOCAL_C` directly; `GLDEF_D`/`LOCAL_D` are the data
> counterparts (formal definitions in the PLIB Reference).
### Program entry point
A PLIB program's entry point is `main`, written with the SIBO macros:
```c
#include <plib.h>
GLDEF_C INT main(VOID)
{
p_printf("Hello World");
p_getch();
return (0);
}
```
A CLIB program uses ordinary ANSI style with `<stdio.h>`:
```c
#include <stdio.h>
int main(VOID)
{
printf("Hello World");
getchar();
return (0);
}
```
(Both `hello.c` and `p_hello.c` are taken verbatim from General ch. 3.)
---
## 3. Project (`.pr`) files
The compile/link cycle is driven by TopSpeed **project files** (extension
`.pr`). The minimal CLIB project (`hello.pr`):
```
#system epoc img
#model small jpi
#compile hello
#link hello
```
The minimal PLIB project (`p_hello.pr`):
```
#system epoc img
#set epocinit=iplib
#model small jpi
#compile p_hello
#link p_hello
```
### Directives
- **`#system epoc img`** — "The end outcome of the build is a `.img` file, as
defined in the Epoc-customised part of the TopSpeed configuration (alternative
`#system`s include `dos` and `win`)." Required in every SIBO project file.
- **`#model small jpi`** — small code model + jpi register calling convention
(see §1). Required in every SIBO project file.
- **`#set epocinit=iplib`** — selects the startup object and stack size. **Must
precede `#model`.** It "specifies whether you are using the CLIB or PLIB
startup object files, and it specifies the stack size." Allowed values:
| Value | Startup | Stack |
|-------|---------|-------|
| `iclib` | CLIB | 8k (recommended for CLIB) |
| `iclib4` | CLIB | 4k |
| `iclib2` | CLIB | 2k |
| `iplib` | PLIB | 4k (recommended for PLIB) |
| `iplib8` | PLIB | 8k |
| `iplib2` | PLIB | 2k |
If unset, defaults to `iclib`. You **must** use CLIB startup if you use any
CLIB I/O functions; if you use no CLIB functions at all, "you should always
use the PLIB startup." (Non-I/O CLIB functions such as memory allocation can
be used with PLIB startup.)
- **`#compile <name>`** — compile a source module. If ambiguous, add the `.c`
extension (e.g. `#compile query.c`), because the system also treats `.a`
(assembler) and `.rc` files as candidate sources and errors if two candidates
exist.
- **`#link <name>`** — link everything. Its effect: link all `#compile`d files,
plus the startup module and standard libraries, plus anything named by
`#pragma link`, giving the executable the name in the `#link` statement. The
`#link` name need not match the `.pr` filename nor any module name.
- **`#dolink <name.lib>`** — used **instead of** `#link` to build a **library**.
It stops the system linking in a startup object and standard libraries. The
explicit `.lib` extension overrides the default `.img` implied by `#system
epoc img` (see §4).
- **`#pragma link (<file>)`** — link an extra object file or library not
searched automatically. Examples: `#pragma link (hwif.lib)` (HWIF is not
auto-searched), `#pragma link (utils.lib)` (a custom library, found along the
`ts.red` search path).
- **`#pragma debug (vid=>full)`** — insert before any `#compile` to build with
full source-level debug info (equivalent to the `/v2` command-line flag).
### Overriding image attributes
Three project variables (also settable as `/s...` on the command line):
| Variable | Meaning | Default |
|----------|---------|---------|
| `%version` | image version number | `0x100f` |
| `%priority` | initial process priority | `0x80` |
| `%heapsize` | initial and minimum heap, in **paragraphs** (`0x80` = `0x800` bytes = 2 KB) | `0x80` |
Set via a line like `#set heapsize=0x180` or `tsc /m app /sheapsize=0x180`. The
OS refuses to start an instance without `heapsize` free heap, and never shrinks
the heap below it.
### Multi-module and parameterised projects
Multi-file program (`triple.c`, `utils1.c`, `utils2.c`):
```
#system epoc img
#set epocinit=iplib
#model small jpi
#compile triple
#compile utils1
#compile utils2
#link triple
```
Assembler modules are allowed alongside C (`#compile afile` assembles
`afile.a`); assembler modules must follow the rules in the PLIB Reference.
A reusable generic project uses the `%main` macro (`unnamed.pr`):
```
#system epoc img
#set epocinit=iplib
#model small jpi
#compile %main
#link %main
```
invoked as `tsc /m unnamed.pr /smain=%1` (TopSpeed `/s` sets the macro from a
batch parameter). Conditional/parameterised builds are handled in the shipped
batch files (`cc.bat`, `make.bat`, `checkvid.bat`, `vid.bat`), which key
compilation off a `%jpivid%` env var (`v2` = full debug, `v0` = none) and pick
a per-app `.pr` if one exists, else `unnamed.pr`.
### Invoking the compiler: `tsc /m`
Build and link in one step:
```
tsc /m hello
```
"`/m` ... is that the project file is executed in 'make' mode, with files not
being recompiled or relinked needlessly." For source-level debugging add `/v2`:
```
tsc /m hello /v2
```
Other flags seen: `/fp<project>` selects the project file (e.g.
`tsc app.c /fpunnamed`); with no `/m` (and no `/l`) the project runs in
"compile" mode (compile only, always, no link). `%version`, `%priority`,
`%heapsize`, `%main` are passed as `/s<name>=<value>`.
> **`tsc` vs `tscx`.** `tsc` does not use expanded memory "and may cause
> problems, particularly when linking large applications." If linking fails,
> replace `tsc` with **`tscx`** (which uses expanded memory) in the batch files.
### Configuration files behind the build
The SIBO build config lives in two files (do **not** edit their pragmas):
- `tsprj.txt` — "compiled" with `tscfg` before use; the SDK version extends
Clarion's to add the `Epoc img` system type.
- `stdepoc.h` — "always the first include file in any source module."
Header/library search paths come from the redirection file **`ts.red`** (maps
`*.H`, `*.LIB`, `*.PR`, etc. to `.` then the `\sibosdk\...` directories).
---
## 4. Build outputs: images, libraries, device drivers
The image file is produced from an intermediary `.exe` by the Psion tool
**`emake.exe`** (automated by the project system). "Essentially, `.img` files
are to Epoc what `.exe` files are to MS-DOS." `emake` runs (per `tsprj.txt`):
```
emake -b %afl% -o%name% -s -v%version% -p%priority% -h%heapsize% -%epoctype% %name%.exe
```
The `%epoctype` variable selects the output kind:
| `%epoctype` | Output | Extension |
|-------------|--------|-----------|
| `t1` (default) | image file | `.img` |
| `t2` | logical device driver | `.ldd` |
| `t3` | physical device driver | `.pdd` |
| `t4` | dynamic library | `.dyl` |
### Images (`.img` / `.app`)
`.img` and `.app` are "strictly speaking ... no real difference"; both are
image files. By convention a `.app` has one to four embedded **add-files**:
- `.pic` — icon
- `.rsc` or `.rzc` (compressed) — resource file
- `.shd` — shell data (Series 3 only)
Add-files are embedded by `emake` when an **add-file list** (`.afl`, a text file
naming one to four files, e.g. `tele.afl`) with the matching base name exists at
build time. Renaming the result to `.app` is a convention (not automatic). A
`.dfl` file similarly embeds DYL files.
Inspect an image with `edump <name>` (shows version, code/data segment sizes,
stack, heap, priority, checksums, add-file offsets, DYL table). Re-edit an
existing image's add-files/priority/heap/version *without rebuilding* using
`eremake.exe` (e.g. to swap in a French resource file):
```
eremake -afrquery -o..\french\query.app query.app
```
### Libraries (`.lib`)
Build a static library with `#dolink` (see §3):
```
#system epoc img
#set epocinit=iplib
#model small jpi
#compile utils1
#compile utils2
#dolink utils.lib
```
`#dolink` (not `#link`) prevents startup/standard-library linking, and the
explicit `.lib` overrides the default `.img`. Consumers pull it in with
`#pragma link (utils.lib)`.
### Loadable device drivers (`.LDD` / `.PDD`)
At a high level, LDDs and PDDs are just image-type outputs of `emake`
(`%epoctype=t2``.ldd`, `t3``.pdd`). Detail from the *Hardware Development
Kit* (§9):
All SIBO hardware "is controlled by logical and physical device drivers."
Layering:
```
APPLICATION SOFTWARE
Psion C / PLIB call interface
LOGICAL DEVICE DRIVER (LDD)
PHYSICAL DEVICE DRIVER (PDD)
PHYSICAL HARDWARE
```
- A **PDD** "contains the code required for talking directly with the hardware"
— low-level, hardware-specific services.
- An **LDD** "performs the logical processing that transforms these low level
services into the high level services used by an application."
- The same LDD is often paired with a per-hardware PDD; an LDD may also talk to
hardware directly, making a separate PDD unnecessary.
> **Language note.** "Psion device drivers are written in 8086 assembler and
> follow a prescribed structure" (HDK §9). Device drivers are **not** ordinary C
> programs — the C SDK / PLIB is the *client* side (`p_loadldd()`, `p_open()`,
> `p_close()`, `p_iow()`). The `.ldd`/`.pdd` build path exists in the project
> system, but the driver *source* is assembler.
**Naming and channels.** An LDD name is three characters + colon (e.g. `TTY:`
serial). A PDD name is the owning LDD's three chars + `.` + three more + colon
(e.g. `TTY.UAR`, the ASIC5 UART driver). Open a channel via `p_open`:
```c
p_open(&pcb, "LED:", -1); /* LDD; -1 = ignore open mode */
p_open(&pcb, "TTY.UAR:", -1); /* a PDD directly (unusual) */
p_open(&pcb, "PAR:A", -1); /* channel qualifier 'A' */
```
Typically an application opens only the LDD, which opens its PDD during
initialisation. I/O requests reach the driver's **strategy vector**, which maps
to the PLIB `p_iow()` call. Installable drivers are loaded dynamically
(`DevLoadLDD` service; do not call `DevInstall` directly) without resetting the
machine, and can replace resident ROM drivers of the same name (the table is
searched from the most-recently-installed end). EPOC handles "a maximum of 32
device drivers on a Series3 machine and 48 on other machines."
**LDD structure (HDK §9).** Single code segment, no data segments (wrapped by
`CodeSeg`/`EndCodeSeg`); begins with a `LibEnt` structure (2-byte
`LDDSignature`, 8-byte zero-terminated name *without* trailing colon, 2-byte
vector count ≥ 8, then the vector table). The **eight mandatory LDD functions**,
in order:
1. `DevFuncInstall` — on installation
2. `DevFuncRemove` — on removal
3. `DevFuncHold` — temporarily disable
4. `DevFuncResume` — re-enable
5. `DevFuncReset` — application terminated without closing channel
6. `DevFuncUnits` — query number of supported units/channels
7. `DevFuncOpen` — open a channel
8. `DevFuncStrategy` — access functionality via the I/O system
All are called FAR by the OS and must return with a FAR return.
**PDD structure (HDK §9).** Also single code segment, no data segments; starts
with a `LibEnt` (`PDDSignature`). **Two mandatory functions**`DevFuncInstallPDD`
and `DevFuncRemovePDD` — with typically two more (`DevFuncOpenPDD`,
`DevFuncStrategyPDD`). PDD internal variables must live in the driver's own code
segment so hardware can be freed after a client process terminates.
---
## 5. Floating-point emulator (relevant to any build)
Because SIBO has no 8087, floating point is emulated. Under EPOC the emulator is
an LDD, `SYS$8087.LDD` (in `\sibosdk\lib\`), loaded and freed automatically —
saving ~8K per program and allowing sharing between processes. The C startup
(`r_emul.a`) looks for the LDD in the program's directory, then via the `EMS`
environment variable (case-sensitive). A program that dies with **panic 80**
before starting has "almost certainly failed to locate `SYS$8087.LDD`"; fixes:
copy the LDD next to the program, set `EMS`, or remove the floating-point
dependency. Note "floating point instructions can easily be generated
unexpectedly ... if the recommended build configuration pragmas are 'improved'"
— another reason not to touch the pragmas.
---
## 6. Resources / resource compiler
The General manual covers resources only lightly, as **add-files**: a `.rsc`
(or compressed `.rzc`) resource file can be embedded into a `.app` via the
`.afl` add-file list (§4). Multi-lingual applications isolate all text in a
resource file so a translated `.rzc` can be swapped in with `eremake` without
recompiling.
> **Not in this manual.** The resource *compiler* itself and the resource
> source (`.rss`) syntax are **not** described in the General Programming Manual.
> The manual points to the *Resource Files* chapter of the *Additional System
> Information* manual (not provided here) for that detail. Do not assume resource
> compiler behaviour beyond the add-file mechanism described above.
---
## 7. Detecting the machine at runtime
Most SIBO machines are distinguished by screen size via `p_getlcd()` /
`p_geticda()` (General ch. 7). Values include:
| Constant | Value | Display / machine |
|----------|-------|-------------------|
| `E_LCD_640_400` | 0 | 640x400 — MC 400 |
| `E_LCD_640_200_SMALL` | 1 | 640x200 — MC 200 |
| `E_LCD_160_80` | 4 | 160x80 — HC |
| `E_LCD_240_80` | 5 | 240x80 — Series 3 |
| `E_LCD_480_160` | 11 | 480x160 — Series 3a / 3c |
| **`E_LCD_240_100`** | **12** | **240x100 — Workabout** |
| `E_LCD_240_160` | 14 | 240x160 — Siena |
> **Workabout / MX.** The manual lists the Workabout as the 240x100 machine
> (`E_LCD_240_100`, value 12). It does not list a separate MX entry; an MX with
> the same screen reports the same LCD type. Series 3a vs 3c (same screen size)
> are told apart with `p_returnexpansionportinfo()` on EPOC ≥ `0x390F` — the
> pattern generalises if you must disambiguate same-screen models. Do not
> hardcode screen dimensions; query the LCD type.
---
## Quick reference
```
# Minimal PLIB app
#system epoc img # -> .img output (Epoc image)
#set epocinit=iplib # PLIB startup, 4k stack (MUST precede #model)
#model small jpi # 64K code + 64K data, register calling convention
#compile myapp
#link myapp
# Build (make mode) tsc /m myapp
# Build with source debug tsc /m myapp /v2
# Large link needs XMS tscx /m myapp
# Override heap tsc /m myapp /sheapsize=0x180
# Inspect image edump myapp
# Re-embed add-files/version eremake ...
```
| Extension | What it is |
|-----------|-----------|
| `.pr` | TopSpeed project file |
| `.c` / `.a` | C source / assembler source |
| `.img` / `.app` | EPOC image / image with embedded add-files |
| `.lib` | static library (`#dolink`) |
| `.ldd` / `.pdd` | logical / physical device driver (assembler source) |
| `.dyl` | dynamic library |
| `.rsc` / `.rzc` | resource file / compressed resource file |
| `.pic`, `.shd`, `.afl` | icon, shell data, add-file list |
| `ts.red` | header/library redirection (search paths) |
+719
View File
@@ -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 (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.**
+672
View File
@@ -0,0 +1,672 @@
# Psion SIBO / Workabout MX — I/O Device Model and Drivers
Reference for the EPOC (SIBO) I/O system as documented in the Psion SIBO 'C' SDK.
Everything here is drawn from two manuals:
- **PLIB Reference** — I/O System chapter (Ch. 9), Asynchronous Requests and
Semaphores (Ch. 8), Memory Allocation (Ch. 7). Cited as *PLIB*.
- **I/O Devices Reference** v2.30 (March 1999). Cited as *IODEV*.
Where a fact is specific to the Workabout / Workabout MX, or is uncertain from the
manuals, it is flagged. The MX integral laser scanner is **not** covered by these
manuals; see the cross-reference in the Barcode section.
> Notation used by the manuals: an I/O service is identified by a function code of
> the form `P_Fxxx` (defined in `p_file.h`). The manuals write `p_iow(P_FWRITE)`,
> `p_ioc(P_FWRITE)` or just `P_FWRITE` to refer to that service. (*IODEV* Ch.1;
> *PLIB* Ch.9.)
---
## 1. The device model
### 1.1 Device drivers: LDDs and PDDs
The purpose of a device driver is to hide the underlying hardware behind a stable
software interface. EPOC defines two driver types (*PLIB* Ch.9, "LDDs and PDDs"):
- **PDD — physical device driver**: hardware dependent (the lower layer).
- **LDD — logical device driver**: hardware independent (the upper layer).
Applications normally interface to **LDDs only**. An LDD may use one or more PDDs.
The serial driver is the canonical example: an upper hardware-independent LDD over
a hardware-dependent PDD; incoming data is buffered at the LDD level. Some drivers
touch no hardware at all — e.g. the C floating-point library is implemented as a
device driver (the 8087 emulator LDD `sys$8087.ldd`). (*PLIB* Ch.9; Ch.5.)
Some drivers are built into the ROM (e.g. the RS232 LDD/PDD); others are external
and must be loaded (e.g. a bar-code reader). (*PLIB* Ch.9.)
The interface between the OS and an LDD does **not** follow a C calling convention;
an LDD may be written in C but requires some 8086 assembly for the LDD vector
interface. (*PLIB* Ch.9; System Services reference.)
### 1.2 Opening a channel: `p_open`
```c
INT p_open(VOID **ppfcb, TEXT *name, UINT mode);
INT f_open(VOID **ppfcb, TEXT *name, UINT mode); /* p_leave()s on error */
```
`name` is a **3-character device name terminated by a `':'`**, optionally followed
by further text depending on the device. Examples (*PLIB* Ch.9):
| Name | Device |
| ----- | ------ |
| `FIL:` | a file |
| `PAR:` | a parallel port |
| `TTY:` | an RS232 serial port |
| `TIM:` | an asynchronous timer |
Rules (*PLIB* Ch.9, "Opening a channel to a device"):
- The device name of an external LDD bears **no relation** to the file it was
loaded from.
- Where a driver supports more than one **unit**, the `':'` is followed by a unit
letter: `TTY:A` = serial port A, `PAR:B` = parallel port B.
- For `FIL:`, the qualifying text is a file name or full path.
- A loaded device driver **supersedes** any existing device of the same name.
On success the channel control block address is written to `*ppfcb`. On failure
`*ppfcb` is left untouched — pre-set it to zero so `p_close(0)` (a no-op) is safe
in clean-up. For an **attached** driver, the control block is attached to `*ppfcb`
and the value is not changed. (*PLIB* Ch.9.)
Errors: `E_FILE_ALLOC` (no memory for control block), `E_FILE_DEVICE` (device does
not exist), `E_GEN_ARG` (bad `mode`, possibly from falling through to `FIL:`).
### 1.3 The `FIL:` fallback
If `p_open` fails to match a device name, the name is **passed to the `FIL:`
driver** (the process must be connected to the file server). This makes the leading
`FIL:` optional when opening files:
```
p_open(..., "C:\\NOTES\\NEW.TXT", ...) == p_open(..., "FIL:C:\\NOTES\\NEW.TXT", ...)
```
Side effect: opening a **non-existent device** does not give the expected
`E_FILE_DEVICE`, because the name is handed to `FIL:` and the result depends on
`mode` (it might even succeed). Passing `mode = -1` guarantees the `FIL:` open
fails — albeit with the misleading `E_GEN_ARG`. Keeping an explicit `FIL:` prefix
avoids mistaking a file spec for a device name (e.g. a file literally named
`TTY:`). (*PLIB* Ch.9.)
### 1.4 Modes
The interpretation of `mode` **depends on the device**; many devices ignore it.
When a device ignores `mode`, pass `-1`. (*PLIB* Ch.9.) The character/hardware
devices in *IODEV* (`TTY:`, `PAR:`, `SND:`, `FRC:`, `MCR:`, `BAR:`) are all opened
with `mode = -1`. File-open mode flags such as `P_FOPEN`, `P_FREPLACE`,
`P_FUPDATE`, `P_FUNIQUE`, `P_FSTREAM`, `P_FSHARE`, `P_FTEXT` are `FIL:`-specific and
are described in the Files chapter of *PLIB*.
### 1.5 Attached drivers
An **attached driver** is an LDD layered over another LDD, replacing or augmenting
its services. Opening it *attaches* to an already-open underlying channel rather
than allocating a new one. Example: the printer driver `PRO:` is opened over a
print-output device (`PAR:`, `TTY:` or `FIL:`) and thereafter replaces that
device's `P_FWRITE`, `P_FCANCEL` and `P_FCLOSE`. Attached drivers that work
asynchronously install a **device wait handler** at open, called from within the
opening process's `p_iowait`. (*PLIB* Ch.9, "Attached drivers".)
### 1.6 The file server
`FIL:` operations are serviced by the high-priority system process **`SYS$FSRV`**,
which also loads executables and **loads external device drivers**. It serialises
access to shared storage (SSDs) and uses PDDs to reach the many FLASH/RAM/ROM
configurations. Applications are "clients" of the file server; the normal start-up
code connects to it. (*PLIB* Ch.9, "The file server".)
---
## 2. Operations on an open channel
All I/O requests are **asynchronous in principle** — the process I/O semaphore is
always signalled on completion — but many are implemented **synchronously**
(completing before the call returns). A typical device offers zero to three truly
asynchronous functions (commonly `P_FREAD` / `P_FWRITE`); the rest are synchronous.
`P_FCLOSE` is always synchronous. (*PLIB* Ch.9; *IODEV* Ch.1.)
### 2.1 The I/O primitives
```c
/* asynchronous, CDECL + register-calling variants */
INT p_ioa (VOID *pcb, INT func, WORD *pstat, ...);
INT p_ioa3(VOID *pcb, INT func, WORD *pstat);
INT p_ioa4(VOID *pcb, INT func, WORD *pstat, VOID *a1);
INT p_ioa5(VOID *pcb, INT func, WORD *pstat, VOID *a1, VOID *a2);
/* asynchronous, error folded into *pstat (preferred over p_ioa) */
VOID p_ioc (VOID *pcb, INT func, WORD *pstat, ...);
VOID p_ioc3(VOID *pcb, INT func, WORD *pstat);
VOID p_ioc4(VOID *pcb, INT func, WORD *pstat, VOID *a1);
VOID p_ioc5(VOID *pcb, INT func, WORD *pstat, VOID *a1, VOID *a2);
/* synchronous: start then wait (calls p_waitstat) */
INT p_iow (VOID *pcb, INT func, ...);
INT p_iow2(VOID *pcb, INT func);
INT p_iow3(VOID *pcb, INT func, VOID *a1);
INT p_iow4(VOID *pcb, INT func, VOID *a1, VOID *a2);
```
- **`p_ioa`** starts the operation and returns immediately. On success, `*pstat`
holds `E_FILE_PENDING` until the I/O semaphore is signalled, then zero or a
negative error. A cancelled operation completes with `E_FILE_CANCEL`. Returns
`E_FILE_INV` if `func` is invalid for the device. **The status word must outlive
the operation** — never put it on a stack frame you return from. (*PLIB* Ch.9.)
- **`p_ioc`** behaves like `p_ioa` except a failure to *start* is reported exactly
as if the request had started and then completed with that error (only `*pstat`
to check). Preferred over `p_ioa`. (*PLIB* Ch.9.)
- **`p_iow`** starts the request and waits (`p_waitstat`) for completion, returning
the status. Preferred unless you actually need asynchrony. (*PLIB* Ch.9.)
The register-calling variants (`p_iow2/3/4`, `p_ioc3/4/5`, `p_ioa3/4/5`) generate
smaller code and are preferred where applicable; e.g. `p_ioc3` over `p_ioa3`.
(*IODEV* Ch.1; *PLIB* Ch.9.)
Driver rule: normally **one pending request per I/O operation per channel**. A
second `P_FWRITE` while one is pending will `p_panic` the caller; but one read and
one write may be pending simultaneously. (*PLIB* Ch.9; *ADDSYS* driver chapter.)
### 2.2 Convenience functions
| Function | Wraps | Notes |
| -------- | ----- | ----- |
| `INT p_close(VOID *pcb)` | `p_iow(P_FCLOSE)` | `p_close(NULL)` returns 0. Always closes even if it returns an error. |
| `INT p_read(VOID *pcb, VOID *buf, UINT len)` | `p_iow(P_FREAD)` | returns bytes read, or negative error. `f_read` `p_leave`s on error. |
| `INT p_write(VOID *pcb, VOID *buf, UINT len)` | `p_iow(P_FWRITE)` | returns 0 or negative error. `f_write` `p_leave`s on error. |
| `INT p_seek(...)` | `p_iow(P_FSEEK)` | `FIL:` only (Files chapter). |
(*PLIB* Ch.9.)
### 2.3 Waiting and cancelling
```c
VOID p_iowait(VOID); /* wait on the process I/O semaphore */
VOID p_waitstat(WORD *pstat); /* wait until *pstat leaves E_FILE_PENDING */
```
To wait on **one specific** request, use `p_waitstat`; `p_iowait` waits for *any*
completion and is used in the central dispatch loop. Every `p_iosignal` must be
matched by a `p_iowait` (or a function that calls it). (*PLIB* Ch.8.)
Cancelling — `p_iow(pcb, P_FCANCEL)` — cancels outstanding async requests on the
channel and returns zero (harmless if none pending). Principles (*PLIB* Ch.9):
- the cancel **precipitates** completion; it does not stop the request completing;
- it may or may not be effective (the request may complete naturally first);
- **you must still consume the completion signal** — typically an immediate
`p_waitstat` to "use up" the signal.
`p_waitstat` is safer than `p_iowait` for using up a cancelled request's signal.
General-purpose synchronous wrappers must use `p_waitstat`, not `p_iowait`.
---
## 3. I/O function codes (`p_file.h`)
Services are named `P_Fxxx` and defined in `p_file.h`. The generic codes that apply
across many devices are (*PLIB* Ch.9; *IODEV* Ch.1):
| Code | Meaning |
| ---- | ------- |
| `P_FREAD` | read data from a channel |
| `P_FWRITE` | write data to a channel |
| `P_FCLOSE` | close a channel |
| `P_FCANCEL` | cancel outstanding async requests |
| `P_FSENSE` | sense channel characteristics |
| `P_FSET` | set channel characteristics |
| `P_FFLUSH` | flush buffered data |
| `P_FCTRL` | test/set control lines (serial) |
| `P_FINQ` | inquire supported characteristics |
Numeric values are assigned in `p_file.h`. The canonical numbering is:
| Constant | Value |
| -------- | ----- |
| `P_FREAD` | 1 |
| `P_FWRITE` | 2 |
| `P_FCLOSE` | 3 |
| `P_FCANCEL` | 4 |
| `P_FSET` | 7 |
| `P_FSENSE` | 8 |
| `P_FFLUSH` | 9 |
| `P_FCTRL` | 11 |
| `P_FINQ` | 12 |
> Confirmation from the manuals: the AccessIr chapter tabulates its own function
> numbers and gives `P_FREAD = 1`, `P_FWRITE = 2` (*IODEV* Ch.16, "Constants").
> The System Services driver model (*ADDSYS*) documents the OS-defined common set
> `IoFuncRead / IoFuncWrite / IoFuncClose / IoFuncCancel / IoFuncSet /
> IoFuncSense / IoFuncFlush`, which map to `P_FREAD … P_FFLUSH`; `p_read`,
> `p_write` and `p_close` call the driver with `IoFuncRead`, `IoFuncWrite` and
> `IoFuncClose` respectively. Drivers are urged to keep these conventional meanings
> so that attached drivers work.
Device-specific codes also exist, e.g. `P_FSEEK`, `P_FSETEOF` (files); `P_FTEST`
(test for input); `P_FEDIT` (console edit); `P_FRELATIVE` / `P_FABSOLUTE` (timers);
`P_FSTART` (FRC); `P_FCONNECT` / `P_FDISCONNECT` / `P_FRSUPER` (NCP, Xmodem);
`P_FIR*` (IR); `E_FALARM` / `E_FDIAL` / `E_FSSOUNDCHANNELn` (sound). These are
listed with the individual devices below and in *IODEV*.
Error numbers `-32..-63` are reserved for I/O device errors of the form
`E_FILE_xxx` (also in `p_file.h`). (*PLIB* Ch.6.)
---
## 4. Loading external device drivers
### 4.1 Load / delete
```c
INT p_loadldd(TEXT *pName); /* default extension .LDD */
INT p_loadpdd(TEXT *pName); /* default extension .PDD */
INT p_devdel (TEXT *pName, INT devType); /* devType: E_LDD or E_PDD */
```
- `p_loadldd` / `p_loadpdd` load a driver from a file; if `pName` has no extension,
`.LDD` / `.PDD` is assumed, and a relative name uses the current path. After
loading, a channel is opened with `p_open`. Errors: `E_FILE_EXIST` (same file
already loaded), `E_FILE_NXIST` (file missing), `E_GEN_IMAGE` (bad/corrupt
format), `E_GEN_NOMEMORY`, `E_GEN_NOSEGMENTS`. Apps that rely on an external LDD
should call `p_loadldd` and ignore `E_FILE_EXIST`. (*PLIB* Ch.9.)
- `p_devdel` deletes a **RAM-loaded** driver by device name (no trailing `':'`).
Only loaded (not ROM) drivers can be deleted. Errors: `E_FILE_DEVICE` (not
loaded), `E_GEN_NSUP` (ROM driver), `E_GEN_INUSE` (currently open), or a
device-dependent error. **Warning:** a null `pName` deletes the first unloadable
driver of that type. Good practice: attempt `p_devdel` when finished and ignore
the result (harmless if in use elsewhere or in ROM). (*PLIB* Ch.9.)
Related helpers: `p_devqu(pName)` returns the number of units an LDD supports (e.g.
`p_devqu("TTY")` → 2 if two serial boards are fitted; `E_GEN_FAIL` means unlimited,
as `FIL:` returns); `p_devfnd(...)` iterates device names matching a wildcard.
(*PLIB* Ch.9.)
### 4.2 The `.LDD` / `.PDD` model and device memory segments
External drivers are loaded from a driver file into a **device memory segment**,
allocated by the segment allocator. The ability to load and remove drivers **without
a system reset** is a key EPOC feature: you can physically attach a peripheral and
load its driver without exiting any process. The I/O system also notifies drivers
on power off/on so they can save/restore device state. (*PLIB* Ch.9.)
Memory segments (*PLIB* Ch.7, "Memory Allocation"):
- **Device segments** are created when an external device is installed and deleted
when removed; once created a device segment does not normally change size. The
first two device segments are special — the data segments of `SYS$NULL` and the
supervisor `SYS$SMANG`.
- Device segments are allocated at **lower addresses** than the volatile dynamic
segments so that dynamic-segment activity does not move them (a driver normally
has to stop working while its segment moves — risking data loss on, say, a serial
receive).
- Segment name extension conventions: **`.LDD`** = LDD device segment, **`.PDD`** =
PDD device segment; `.$SC` primary shared code, `.DYL` dynamic library, `.$nn`
process data segment.
- The segment index table has fixed sub-capacities — typically **96 total (32
device + 64 dynamic)**. Addresses/sizes are in 16-byte paragraphs.
---
## 5. Per-device reference
### 5.1 Serial port — `TTY:` (*IODEV* Ch.4)
Fully interrupt-driven RS-232. Two cooperating layers: a hardware PDD and a
hardware-independent LDD (which buffers incoming data).
**Device names.** First port `TTY:A`; second `TTY:B`. Availability varies: a Series 3
with a 3-Link recognises only `TTY:A`; an MC with two serial/parallel modules has
`TTY:A` (left) and `TTY:B` (right); HC has a third, cradle port `TTY:C`. HC also
exposes TTL-level ports `TTY:D/E/F` (direct) and `TTY:G/H/I` (inverted) — not
documented in that chapter.
**Parameters — `P_SRCHAR` (`p_serial.h`), all fields:**
```c
typedef struct {
UBYTE tbaud; /* transmit baud rate */
UBYTE rbaud; /* receive baud rate */
UBYTE frame; /* data bits + stop + parity-present */
UBYTE parity; /* parity type */
UBYTE hand; /* handshake flags */
UBYTE xon; /* XON character (default DC1 0x11) */
UBYTE xoff; /* XOFF character (default DC3 0x13) */
UBYTE flags; /* control flags */
ULONG tmask; /* terminator mask */
} P_SRCHAR;
```
**Baud (`tbaud`, `rbaud`)** — one of `P_BAUD_50, _75, _110, _134, _150, _300, _600,
_1200, _1800, _2000, _2400, _3600, _4800, _7200, _9600, _19200, _38400, _56000`.
Default `P_BAUD_9600`. All SIBO machines support `_50`..`_9600`; MC 200/400 and
Series 3a also support `_19200`; HC/Series 3 can be set to `_19200` but their clock
is slightly too slow (frequent overruns). No SIBO hardware supports split
transmit/receive rates.
**Frame (`frame`)** — one of `P_DATA_5 / P_DATA_6 / P_DATA_7 / P_DATA_8`, optionally
OR'd with `P_TWOSTOP` (2 stop bits, else 1) and `P_PARITY` (parity bit present).
Default `P_DATA_8` (8 data, 1 stop, no parity). All settings supported on all SIBO
machines.
**Parity (`parity`)** — used only if `P_PARITY` is set: `P_PAR_EVEN`, `P_PAR_ODD`,
`P_PAR_MARK`, `P_PAR_SPACE`. Default 0. **No SIBO machine supports mark or space
parity.**
**Handshaking (`hand`, plus `xon`/`xoff`)** — combination of:
| Flag | Effect |
| ---- | ------ |
| `P_OBEY_XOFF` | obey received XON/XOFF (input flow control) |
| `P_SEND_XOFF` | transmit XON/XOFF to remote (output flow control) |
| `P_IGN_CTS` | if set, RTS held permanently active and incoming CTS ignored; if clear, RTS/CTS flow control |
| `P_OBEY_DSR` | if set, suspend TX when incoming DSR inactive (DTR/DSR); DTR held active while port is open |
| `P_FAIL_DSR` | (only if `P_OBEY_DSR`) complete outstanding `P_FREAD`/`P_FWRITE` with `E_FILE_LINE` if DSR goes inactive |
| `P_OBEY_DCD` | if set, suspend TX when incoming DCD inactive |
| `P_FAIL_DCD` | (only if `P_OBEY_DCD`) complete requests with `E_FILE_LINE` if DCD goes inactive |
Default `hand = 0` → RTS/CTS handshaking. DCD is an input-only carrier indicator,
not a flow-control line. `xon`/`xoff` default to DC1 (0x11) / DC3 (0x13).
**Control flags (`flags`)** — only `P_IGNORE_PARITY` is defined (discard parity
errors; the errored character is still delivered). Other bits reserved zero. Default 0.
**Terminator mask (`tmask`)** — 32 bit flags, bit *n* (n = 0..31) selecting control
code `0x00`..`0x1F` as a terminating character. E.g. bit 13 = CR, bit 10 = LF (to
read a line at a time); bit 26 = Ctrl-Z. A read completes when a terminator is
received (included in the returned length). Default 0 (none).
**Errors:** `E_FILE_PARITY`, `E_FILE_FRAME`, `E_FILE_OVERRUN`, `E_FILE_LINE`
(inactive required control line), plus `E_GEN_OVER` (driver receive buffer full)
and `E_FILE_RECORD` (buffer filled with no terminator seen).
**Services:**
| Service | Prototype / notes |
| ------- | ----------------- |
| `p_open("TTY:x", -1)` | Powers up port, drives DTR active. Defaults as above; RTS not driven active until first `P_FREAD` or a `P_FSET` (so buffered-modem data is not lost). Errors: `E_GEN_NOMEMORY`, `E_GEN_INUSE`, `E_FILE_DEVICE`, `E_FILE_LOCKED`. |
| `P_FREAD` | `p_iow(pcb, P_FREAD, buf, &len)` — read up to `*len` bytes. Completes on: full length; receive error (partial data + negative status); a `tmask` terminator (length includes it); or cancel (`E_FILE_CANCEL`). After a cancel, use `P_FTEST` — more buffered chars may remain. |
| `P_FWRITE` | `p_iow(pcb, P_FWRITE, buf, &len)` — obeys current handshaking. A **zero-length** write with `P_OBEY_DSR` set completes only when DSR goes active (used to detect a connection). Cancel → `E_FILE_CANCEL`; the driver does not report bytes sent before cancel. |
| `P_FCANCEL` | cancel outstanding read and write. |
| `P_FSENSE` | `p_iow(pcb, P_FSENSE, P_SRCHAR*)` — read current characteristics (cannot fail). |
| `P_FSET` | `p_iow(pcb, P_FSET, P_SRCHAR*)` — set characteristics (sense-modify-set idiom). Errors `E_GEN_ARG`, `E_GEN_NSUP`, `E_FILE_LINE`. Panics if a read/write is outstanding. |
| `P_FFLUSH` | discard the LDD read buffer, clear error status, release paused remote. |
| `P_FTEST` | `p_iow(pcb, P_FTEST, &len)` — bytes currently buffered (at least that many can be read synchronously). |
| `P_FCTRL` | `p_iow(pcb, P_FCTRL, UBYTE *pctrl)` — read CTS/DSR/DCD input line state into `*pctrl` as a bit mask, and optionally set DTR (see below). |
| `P_FINQ` | `p_iow(pcb, P_FINQ, UWORD *pmask)` — write three words of supported-characteristic bit flags. |
**Control-line constants (`p_serial.h`):**
- Input line state returned by `P_FCTRL` in `*pctrl`:
`P_SRCTRL_CTS`, `P_SRCTRL_DSR`, `P_SRCTRL_DCD` (bit set ⇒ line active).
- DTR output: if `*(pctrl+1)` is non-zero it sets DTR to `P_SRDTR_ON` (active) or
`P_SRDTR_OFF` (inactive). `E_GEN_NSUP` if the driver can't set DTR (all current
SIBO machines can).
- `P_FINQ` bit flags: word 0 = `P_SRINQ_50 … P_SRINQ_19200`; word 1 =
`P_SRINQ_38400`, `P_SRINQ_56000` (not supported on SIBO); word 2 =
`P_SRINQ_DATA5/6/7/8`, `P_SRINQ_STOP2`, `P_SRINQ_PAREVEN/PARODD/PARMARK/PARSPACE`,
`P_SRINQ_SETDTR`, `P_SRINQ_SPLIT`. SIBO supports all except `PARMARK`, `PARSPACE`
and `SPLIT`.
`p_close` flushes the receive buffer, waits for any in-flight TX char, cancels
outstanding read/write, drops RTS and DTR, and powers the port down.
### 5.2 Parallel port — `PAR:` (*IODEV* Ch.3)
Standard Centronics, **output-only** (no read service). Available via serial/parallel
or parallel expansion modules; HC cradle provides a third. Names `PAR:A`, `PAR:B`,
`PAR:C` (e.g. Series 3 = `PAR:A` only; MC with two modules = `PAR:A` left, `PAR:B`
right).
- `p_open("PAR:x", -1)` — powers up the port lines (all control lines cleared low);
it draws power until closed. Errors: `E_FILE_ALLOC`, `E_FILE_DEVICE`,
`E_FILE_LOCKED` / `E_GEN_INUSE`.
- `P_FWRITE``p_iow(pcb, P_FWRITE, buf, &len)`. **Never completes if no
functioning receiver is connected** — always write asynchronously with a timer
timeout. Errors `E_FILE_WRITE`, `E_FILE_CANCEL`.
- `P_FCANCEL` — cancel the write (an indeterminate amount will already have been
written).
- `P_FSENSE``p_iow(pcb, P_FSENSE, UWORD *port)` reads input control lines
(`p_par.h`): `S_BUSY` (pin 11), `S_ACK` (10), `S_ERROR` (15), `S_PE` (12). *Only
on HC/MC ranges with EPOC ≥ 2.30; not on Series 3/3a.*
- `P_FSET``p_iow(pcb, P_FSET, UWORD *type, UWORD *port)` sets/clears output lines:
`*type = 1` sets high, `0` clears low. Lines: `S_INIT` (16), `S_AUTOFD` (14),
`S_SELECT` (17), plus `S_SPARE` (custom hardware only — ASIC5 pin 42, no effect on
the standard module). *Same HC/MC ≥ 2.30 restriction.*
### 5.3 Timers — `TIM:` (async) and `FRC:` (free-running counter)
**`TIM:` — asynchronous timer.** Documented in the *PLIB* "Time, Timers and Dates"
chapter (not in *IODEV*). Opened `p_open(&tcb, "TIM:", -1)`. Requests are placed on
a delta queue via `p_ioc(P_FRELATIVE, ..., &ticks)` (relative) or
`p_ioc(P_FABSOLUTE, ...)`; cancel with `P_FCANCEL`. Relative units are 1/10 s (the
examples use `tval = 10L * secs`). `TIM:` is the standard tool for I/O timeouts (see
the `PAR:` and `TTY:` examples in *IODEV*). (*PLIB* Ch.8/9; Time chapter.)
**`FRC:` — free-running counter** (*IODEV* Ch.7). **Specific to Series 3a and
Workabout.** Accuracy ±2 ppm, resolution 1/1024 s. **Supports only one process at a
time.** For resolutions of 1/32 s or coarser, use `TIM:` instead.
- `p_open("FRC:", -1)` — errors `E_FILE_OPEN` (in use), `E_GEN_NOMEMORY`.
- `P_FSTART``p_iow(pcb, P_FSTART, UWORD *pmode, UWORD *pint)`:
- `E_FRC_COUNTING`: counter increments every 1/1024 s from 0; `pint` ignored.
- `E_FRC_REPEATING`: increments every `*pint` × 1/1024 s (`*pint` in 10..65535,
≈0.0164 s). Errors `E_GEN_NSUP` (bad mode), `E_GEN_RANGE` (interval 0..9).
- `P_FREAD` (COUNTING mode): `p_iow(pcb, P_FREAD, ULONG *arg1)` — elapsed 1/1024 s
since `P_FSTART`, not reset by the read.
- `P_FREAD` (REPEATING mode): `p_iow(pcb, P_FREAD)` — completion status = number of
whole intervals elapsed since the last `P_FSTART`/`P_FREAD` (waits for ≥1
interval; time is not lost between reads).
- Read errors: `E_GEN_OVER` (>32767 intervals / too large for `arg1`),
`E_FILE_CANCEL`, `E_FILE_READ` (counter not running, or machine was switched off
mid-count).
### 5.4 Sound — `SND:` (*IODEV* Ch.5)
Speaker driven by the `SND:` driver; a separate piezo **buzzer** (all machines
except Series 3a, which emulates it via the speaker) is driven by the PLIB
`p_sound` routine, not `SND:`. Output is disabled if the `E_SOUND_DEVICE` sound-flag
bit is clear or `E_SOUND_DISABLE` is set (managed via `p_getsnd`/`p_setsnd`). The
speaker is a **two-voice** device (chords / DTMF). Series 3 `SND:` is limited to
DTMF and simple alarms — richer sound needs an extra LDD (e.g. `SVDFRC.LDD` on the
SDK disk). Series 3a can also play/record `.WVE` files.
- `p_open("SND:", -1)` — errors `E_FILE_ALLOC`, `E_GEN_FAIL` (disabled),
`E_FILE_LOCKED`/`E_GEN_INUSE`.
- `P_FCANCEL` — cancel outstanding write.
- `P_FSENSE` / `P_FSET``E_SOUND` struct (`epoc.h`):
```c
typedef struct { UBYTE beatsPerMinute; UBYTE volume; } E_SOUND;
```
`beatsPerMinute` (HC/MC/3a) `E_SOUND_MIN_BPM` 2 .. `E_SOUND_MAX_BPM` 240, default
120 (no effect on Series 3). `volume` 0 (`E_SOUND_MAX_VOLUME`, loudest) .. 5
(`E_SOUND_MIN_VOLUME`); Series 3 range 1..4; Series 3a has four distinct volumes;
smaller = louder; default 1. Always sense-then-set immediately after open, before
playing — even at defaults.
- `E_FALARM` — `p_iow(pcb, E_FALARM, UWORD *palarm)`: `*palarm` 0 = "rings", 1 =
"chimes".
- `E_FSSOUNDCHANNELn` *(HC/MC/3a only)* — `p_ioc(pcb, E_FSSOUNDCHANNELn, pstat,
WORD *pnotes, WORD *plen)`, n = 1 or 2. `*plen ≤ 16384` notes; each note is two
words (frequency Hz — middle A = 440; duration in beats). Output starts only once
**both** channels have been called (for sync); use `*plen = 0` for an unused
voice.
- `E_FDIAL` *(Series 3 / 3a only)* — `p_iow(pcb, E_FDIAL, TEXT *pstr, E_DIAL
*pdial)` emits DTMF for `*pstr`.
```c
typedef struct { UBYTE toneLengthTicks; UBYTE delayLengthTicks;
UWORD pauseLengthTicks; } E_DIAL; /* ticks = 1/32 s */
```
Valid chars 0-9, A-F (`#`→F, `*`→E); space/comma = pause; others ignored; max 26
tone/pause chars. Errors `E_FILE_CANCEL`, `E_GEN_ARG` (too many chars).
### 5.5 Magnetic card reader — `MCR:` (*IODEV* Ch.12)
HC MCR driver, built into the HC OS. Interface fits top (`MCR:A`), bottom (`MCR:B`)
or cradle (`MCR:C`).
- `p_open("MCR:x", -1)` — errors `E_GEN_NOMEMORY`, `E_FILE_DEVICE` (no interface in
slot), `E_FILE_NAME`, `E_FILE_LOCKED`/`E_GEN_INUSE`.
- `P_FREAD` — `p_iow(pcb, P_FREAD, UBYTE *buf1, UBYTE *buf2)`: track 1 → `*buf1`,
track 2 → `*buf2` (each buffer ≥ 256 bytes). Data is **leading-byte-count ASCII**;
count 0 = unsuccessful. Pass either buffer as `NULL` to skip that track. Track 1 =
alphanumeric (name/account), track 2 = numeric (most readers do track 2 only).
Errors `E_FILE_READ` (decode error), `E_GEN_OVER`, `E_FILE_CANCEL`.
- `P_FCANCEL` — cancel the read.
- `P_FSET` *(EPOC ≥ 2.32)* — `p_iow(pcb, P_FSET, UWORD *mask)` programs the 100 kΩ
pull-up/down resistors on the five reader lines (bit set = pull-up, clear =
pull-down; default all pull-down): `M_DATA1PU` 0x01, `M_CLK1PU` 0x02, `M_DATA2PU`
0x04, `M_CLK2PU` 0x08, `M_CLSPU` 0x10 (card-present). Other bits ignored.
### 5.6 Infrared (*IODEV* Ch.1517)
Psion IR is **IrDA-compliant only** (S3a/Siena ports won't talk to non-IrDA
devices). The protocol stack, bottom to top: **SIR** (Serial Infrared physical
layer device driver, drives the hardware) → **IrLAP** (link access) → **IrLMP**
(link management) → the **IrMUX API** (server side) and **AccessIr API** (client
side). Two application-facing device channels are exposed:
- **`AIR:`** — the AccessIr API device. `p_open(&pcb, "AIR:", 0)` (mode 0) powers up
the port; errors `E_GEN_INUSE`, `E_GEN_NOMEMORY`. Services use `P_FIR*` codes
(numeric values from *IODEV* Ch.16): `P_FREAD` 1, `P_FWRITE` 2,
`P_FIRDISCONNECT` 4, `P_FIRDISCOVER` 5, `P_FIRSELECT` 6, `P_FIRAWAITCONNECT` 7,
`P_FIRMAKECONNECT` 8. Flow: `P_FIRDISCOVER` (log in-range machines) →
`P_FIRSELECT` → `P_FIRMAKECONNECT` (primary) / `P_FIRAWAITCONNECT` (secondary),
exchanging up to 56 bytes of connect data → `P_FREAD`/`P_FWRITE` →
`P_FIRDISCONNECT`.
- **`IRP:`** — the IR **printer-port** device driver (the "IRP" API), used for IR
printing. (*IODEV* Ch.15, protocol-layer diagram.)
The **IrMUX API** (*IODEV* Ch.17) is the server-side interface (LM-IAS
registration, connection-oriented and connectionless reads/writes) exposed through
`LM_*` messages rather than `P_Fxxx` codes.
> MX note: these chapters describe the S3a/Siena/S3c-era IrDA stack; the manuals do
> not document Workabout-MX-specific IR behaviour.
---
## 6. Barcode scanning
### 6.1 Workabout MX integral laser — see `SCANNER-API.md`
The **Workabout MX integral laser scanner is not covered by these manuals.** It is
documented separately in **`SCANNER-API.md`** (sibling of this file, at
`/tmp/claude/inv/SCANNER-API.md`). In brief, per that document: the MX uses the
logical driver **`WL2`** (units `WL2:A`, `WL2:D`) with decoder type **`Symbol2`**
(the integral Symbol laser engine); it is a decoded scanner, triggered by the
keyboard scan key (Window Server key code 368). **Do not duplicate that content
here — consult `SCANNER-API.md` for the MX.**
The remainder of this section summarises only the **older external** wand/RS232
barcode devices as documented in *IODEV*.
### 6.2 External wand decoders — `BAR:` (*IODEV* Ch.13)
The HC has **no** built-in barcode driver/decoder; readers are supported by external
combined **decoder + device-driver LDDs** that the app must load (`p_loadldd` or OPL
`DevLoadLdd`). Wand interface module fits the top (`BAR:A`) or bottom (`BAR:B`) of
the HC. Available drivers (device name `BAR` in all cases):
| File | Symbologies |
| ---- | ----------- |
| `BAREAN.LDD` | EAN8, EAN13, UPC, UPCE |
| `BARC39.LDD` | Code 39 |
| `BARITF.LDD` | Interleaved 2 of 5 (ITF) |
| `BAR128.LDD` | Code 128 |
| `BARMPLES.LDD`| Modified Plessey |
| `BARRAW.LDD` | raw |
None auto-discriminates. Load e.g. `p_loadldd("BARC39.LDD")`; remove with
`p_devdel("BAR")` (or OPL `DevDelete`).
- `p_open("BAR:", -1)` — opens via the previously loaded LDD. Errors as for `MCR:`.
- `P_FREAD` — `p_iow(pcb, P_FREAD, UBYTE *buf)` (buf ≥ 256). Leading-byte-count
ASCII; count 0 = failed read. The **first character encodes the symbology** (e.g.
EAN8/13, UPC, Code 39, ITF, Code 128, Modified Plessey, UPCE); the rest is the
decoded data. Errors `E_GEN_OVER`, `E_FILE_CANCEL`.
- `P_FCANCEL` — cancel the read.
### 6.3 Intelligent reader / RS232 module — `TTY:` escape commands (*IODEV* Ch.14)
The **intelligent** RS232/barcode expansion module contains a decoding
micro-controller and shares one serial port between an RS232 interface and the
barcode interface (not usable simultaneously). Port mapping by slot: top slot →
RS232 `TTY:A`, barcode `TTY:D`; bottom slot → RS232 `TTY:B`, barcode `TTY:E`. The
barcode interface talks to the host at the standard HC comms settings (XON/XOFF,
no hardware handshaking). It auto-discriminates EAN/JAN 8/13, UPCA, UPCE, Codabar,
Code 128, Interleaved 2 of 5, and Code 39 (standard/extended), transmitting decoded
data left-to-right as ASCII, terminated by default with a single CR.
Because it is reached through `TTY:`, it uses the normal serial services
(`p_open`, `P_FSENSE`, `P_FSET`, `P_FREAD`, `P_FWRITE`, `p_close`). It is
**programmed by writing escape sequences** to it via the serial channel:
```
<Esc>-y<code><command> general form
<Esc>-y<code><command><text> set-termination-string form
<Esc>E hard reset (special form)
```
`<Esc>` = 0x1B, then `-` (0x2D), then `y`/`Y`, a decimal `<code>` of 13 digits
(0255), and an uppercase command letter; no embedded spaces. Multiple option bits
are combined by **summing** their `<code>` values. Multiple commands may be
concatenated in one sequence — intermediate command letters lowercase, the final
one uppercase (e.g. `<Esc>-y13f2h1D`).
Command letters (each `<Esc>-y<code><letter>`):
| Cmd | Function |
| --- | -------- |
| `D` | serial inter-character delay (0 = none, 1 = 10 ms) |
| `E` | hard reset + self-test (special form `<Esc>E`; reverts all options to default; self-test failures reported as `... SELF TEST FAILED<CR><LF>`) |
| `F` | select symbology bitmask: 1 Code39, 2 ITF, 4 UPC/EAN, 8 Codabar, 16 Code128 (default `31`) |
| `G` | check-character options (verify / transmit check digits; per-symbology) |
| `H` | decoding options (extended Code39, Codabar start/stop, UPC-vs-EAN, 2/5-digit supplements, UPC E→A expansion, auto-discriminate supplements) |
| `J` | single-read mode enable/disable |
| `K` | single-read control (fire one read when in single-read mode) |
| `M` | set Interleaved 2 of 5 length |
| `O` | set termination string (`<Esc>-y<code>O<string>`, up to 4 chars) |
| `Q` | Code ID characters |
| `S` | status request |
| `W` | scanner enable |
Per-symbology output formats (ID chars, check digits, supplement digits for
UPC E/A, EAN 8/13 and their +2/+5 variants; Codabar start/stop; ITF and Code 39
check handling) are tabulated in *IODEV* Ch.14.
---
## 7. Other channel devices (pointers)
*IODEV* also documents the **Console** (`CON:`, Ch.2 — screen/keyboard, `P_SCR_*`
function codes, `P_CON_KBREC`), **Alarm** (Ch.6, `A_FTIMED`/`A_FUNTIMED`), **World
database** (Ch.8), **Xmodem/Ymodem** (Ch.9, `P_FCONNECT`/`P_FREAD`/`P_FWRITE`),
**NCP / Link** (Ch.10, `SYS$NCP`), **Cradle/Docking Station** (Ch.11), and the
**Fast Charger** (Ch.18, `FCHG_*`). The **`FIL:`** file driver and the **`TIM:`**
timer are documented in the *PLIB* Files and Time chapters. These are outside the
scope of this reference but follow the same `p_open` + `P_Fxxx` model described in
Sections 13.
---
### Source map
| Topic | Source |
| ----- | ------ |
| Device model, `p_open`, `FIL:` fallback, modes, attached drivers, file server | *PLIB* Ch.9 |
| I/O primitives (`p_ioa/ioc/iow` + variants), `p_read/write/close`, cancel | *PLIB* Ch.9 |
| `p_iowait` / `p_waitstat`, semaphore rules | *PLIB* Ch.8 |
| Function codes, `IoFunc*` mapping | *PLIB* Ch.9; *IODEV* Ch.1/16; *ADDSYS* driver chapter |
| `p_loadldd`/`p_loadpdd`/`p_devdel`/`p_devqu`/`p_devfnd`; `.LDD`/`.PDD`; device segments | *PLIB* Ch.9, Ch.7 |
| `TTY:`, `P_SRCHAR`, control lines | *IODEV* Ch.4 |
| `PAR:` | *IODEV* Ch.3 |
| `FRC:` | *IODEV* Ch.7 |
| `TIM:` | *PLIB* Time chapter (referenced from *IODEV* Ch.1) |
| `SND:` | *IODEV* Ch.5 |
| `MCR:` | *IODEV* Ch.12 |
| IR (`AIR:`, `IRP:`, IrMUX) | *IODEV* Ch.1517 |
| External `BAR:` wand decoders | *IODEV* Ch.13 |
| Intelligent reader / `TTY:` escape commands | *IODEV* Ch.14 |
| Workabout MX integral laser (`WL2`/`Symbol2`) | **`SCANNER-API.md`** (not these manuals) |
+458
View File
@@ -0,0 +1,458 @@
# PLIB Core API Reference (Psion SIBO / Workabout MX)
Categorised function reference for the core **PLIB** library, drawn from the
*PLIB Reference*, Version 2.10 (3 February 1995), (C) Psion PLC.
## Conventions
- Signatures are transcribed from the manual. The OCR source occasionally
mangles glyphs; signatures below are corrected to the manual's own C types
where the intent is unambiguous. Uncertain items are marked **[?]**.
- Types: `TEXT` = char/byte string, `UBYTE`/`BYTE`, `UWORD`/`WORD` (16-bit),
`UINT`/`INT` (16-bit), `ULONG`/`LONG` (32-bit), `DOUBLE` (64-bit float),
`VOID`, `HANDLE`, `BOOL`.
- Many functions return `0` on success and a negative `E_*` error number
(defined in `p_gen.h` etc.) on failure.
- `f_*` twins of `p_*` allocators/senders behave identically except they call
`p_leave(E_GEN_NOMEMORY)` / `p_leave(err)` instead of returning `NULL`/`err`.
---
## 1. String & Buffer Handling
Declared chiefly in `p_std.h` / `plib.h`. Buffers (`b*`) take an explicit
length; strings (`s*`) are zero-terminated.
### 1.1 Copy, length, concatenate, fill
| Signature | Purpose |
|---|---|
| `UBYTE *p_bcpy(VOID *target, VOID *source, UINT len);` | Copy `len` bytes source→target (overlap-safe); returns `target+len`. |
| `UINT p_slen(TEXT *str);` | Return length of zero-terminated string, excluding the terminator. |
| `TEXT *p_scpy(TEXT *target, TEXT *source);` | Copy string source→target; returns address of target's terminating zero. |
| `TEXT *p_scpym(TEXT *target, ...);` | Copy+concatenate a NULL-terminated list of strings into target. |
| `TEXT *p_scat(TEXT *lstr, TEXT *rstr);` | Append rstr to lstr; returns address of new terminating zero. |
| `TEXT *p_scatm(TEXT *lstr, ...);` | Append a NULL-terminated list of strings to lstr. |
| `UBYTE *p_brep(VOID *buf, INT buf_len, VOID *pattern, INT pat_len);` | Fill buf by replicating a byte pattern; returns `buf+buf_len`. |
| `TEXT *p_srep(TEXT *buf, INT buf_len, TEXT *pattern);` | Fill buf by replicating a string pattern (terminator excluded). |
| `VOID p_bswap(VOID *buf1, VOID *buf2, INT len);` | Swap `len` bytes between two buffers. |
| `UBYTE *p_bfil(VOID *buf, UINT buf_len, INT fill_byte);` | Fill buf with a repeated byte; returns `buf+buf_len`. |
| `TEXT *p_jtob(TEXT *tbuf, INT tlen, TEXT *sbuf, INT slen, INT type, INT fill);` | Left/right/centre-align sbuf in tbuf (`P_JLEFT`/`P_JRIGHT`/`P_JCENTRE`), padding with `fill`. |
### 1.2 Comparison
| Signature | Purpose |
|---|---|
| `INT p_bcmp(VOID *lbuf, INT lbuf_len, VOID *rbuf, INT rbuf_len);` | Compare two buffers byte-wise; returns lbufrbuf (<0 / 0 / >0). |
| `INT p_scmp(TEXT *lstr, TEXT *rstr);` | Compare two strings; returns lstrrstr. |
| `INT p_bcmpi(TEXT *lbuf, INT lbuf_len, TEXT *rbuf, INT rbuf_len);` | Case-independent buffer compare. |
| `INT p_scmpi(TEXT *lstr, TEXT *rstr);` | Case-independent string compare. |
*(OCR renders these as `p_bemp`/`p_bempi`; the intended names are `p_bcmp`/`p_bcmpi`.)*
### 1.3 Searching (character, substring, wildcard)
| Signature | Purpose |
|---|---|
| `INT p_bloc(VOID *buf, INT buf_len, INT ch);` | Index of first `ch` in buffer, or 1. |
| `INT p_sloc(TEXT *str, INT ch);` | Index of first `ch` in string, or 1. |
| `INT p_bloci(TEXT *buf, INT buf_len, INT ch);` | Case-independent forward char search in buffer. |
| `INT p_sloci(TEXT *str, INT ch);` | Case-independent forward char search in string. |
| `INT p_slocr(TEXT *str, INT ch);` | Reverse (last-occurrence) char search in string. |
| `INT p_slocri(TEXT *str, INT ch);` | Case-independent reverse char search in string. |
| `INT p_bsub(VOID *buf, INT buf_len, VOID *sbuf, INT sbuf_len);` | Index of first occurrence of a byte sequence in buffer, or 1. |
| `INT p_ssub(TEXT *str, TEXT *substr);` | Index of first occurrence of substring in string, or 1. |
| `INT p_bsubi(TEXT *buf, INT buf_len, TEXT *sbuf, INT sbuf_len);` | Case-independent buffer subsequence search. |
| `INT p_ssubi(TEXT *str, TEXT *substr);` | Case-independent substring search. |
| `INT p_bmatch(TEXT *buf, INT blen, TEXT *mbuf, INT mlen);` | Match buffer against a wildcard spec (`*`, `?`); returns TRUE/FALSE. |
| `INT p_bmatchi(TEXT *buf, INT blen, TEXT *mbuf, INT mlen);` | Case-independent wildcard buffer match. |
| `INT p_smatch(TEXT *str, TEXT *mstr);` | Match string against wildcard spec (`*`, `?`). |
| `INT p_smatchi(TEXT *str, TEXT *mstr);` | Case-independent wildcard string match. |
### 1.4 Character classification (return TRUE/FALSE for `c` modulo 256)
| Signature | Purpose |
|---|---|
| `INT p_isupper(INT c);` | Uppercase alphabetic (accented or not). |
| `INT p_islower(INT c);` | Lowercase alphabetic. |
| `INT p_isalpha(INT c);` | Alphabetic either case. |
| `INT p_isdigit(INT c);` | Decimal digit 09. |
| `INT p_isalnum(INT c);` | Alphanumeric. |
| `INT p_isxdigit(INT c);` | Hex digit 09, AF, af. |
| `INT p_isspace(INT c);` | Whitespace (space, HT, NL, VT, FF, CR). |
| `INT p_iscntrl(INT c);` | Control character (031, 127). |
| `INT p_ispunct(INT c);` | Printable graphic, not alphanumeric/space. |
| `INT p_isgraph(INT c);` | Printable graphic (alnum or punct). |
| `INT p_isprint(INT c);` | Printable, i.e. `p_isgraph` plus space. |
### 1.5 Folding & case conversion, skipping
| Signature | Purpose |
|---|---|
| `TEXT *p_skipwh(TEXT *str);` | Skip leading whitespace; return first non-white char. |
| `TEXT *p_skipch(TEXT *str);` | Skip non-white chars; return first whitespace/terminator. |
| `INT p_tofold(INT c);` | Fold char via built-in fold table (for case-insensitive matching). |
| `TEXT *p_scpyf(TEXT *target, TEXT *source);` | Copy string folding each char (like `p_scpy`). |
| `VOID p_sconf(TEXT *str);` | Fold the characters of a string in place. |
| `INT p_toupper(INT c);` | Convert char to upper case (display use, not matching). |
| `INT p_tolower(INT c);` | Convert char to lower case (display use, not matching). |
| `VOID p_scap(TEXT *str);` | Capitalise string: first char upper, rest lower. *(EPOC 2.14+)* |
> **Folding vs. case:** `p_tofold`/`p_sconf` are for case-insensitive
> ordering/matching (symbol tables); `p_toupper`/`p_tolower`/`p_scap` are for
> human-visible text and must **not** be used for comparison.
---
## 2. Arrays, Queues & Sorting (misc utilities)
Doubly-linked circular queues use `P_QUE` headers; delta queues use `P_DELTA`.
| Signature | Purpose |
|---|---|
| `INT p_bsrch(INT nrec, INT (*compf)(), INT *pmid, UBYTE *pmatch);` | Binary search over `nrec` records via callback; writes found index to `*pmid`. |
| `INT p_qsort(INT nrec, INT (*ordf)(), VOID (*excf)(), UBYTE *base);` | Quicksort `nrec` records using ordering and exchange callbacks. |
| `VOID p_enque(P_QUE *pNew, P_QUE *pEntry);` | Insert `pNew` before `pEntry` in its doubly-linked queue. |
| `VOID p_deque(P_QUE *pEntry);` | Unlink `pEntry` from its queue. |
*(The manual also documents `p_dequed` for delta queues; its clean signature is
not printed in the source text — **[?]**.)*
---
## 3. CRC & Checksums
| Signature | Purpose |
|---|---|
| `VOID p_crc(UWORD *pcrc, UBYTE *buf, UINT len);` | Incrementally accumulate CCITT CRC-16 (x¹⁶+x¹²+x⁵+1) over `len` bytes; init `*pcrc` to 0 first. |
---
## 4. Console I/O
Primitive console services layered on the `CON:` device / window server. The
console is opened automatically on first use. PLIB offers only line output of
mono-spaced text and simple backspace-edited line input; richer row/column
control lives in the `CON:` device driver, and full UI in the Window Server.
Internal buffer is `P_MAXSYSIO` (258) bytes, limiting output to 256 bytes/call.
| Signature | Purpose |
|---|---|
| `VOID p_putch(UINT c);` | Write one character to the console. |
| `VOID p_puts(TEXT *str);` | Write a string and start a new line. |
| `VOID p_printf(TEXT *fstr, ...);` | Format args (as `p_atob`), write line, advance to next line. |
| `VOID p_print(TEXT *fstr, ...);` | As `p_printf` but no automatic newline (use `\r`,`\n`). |
| `INT p_getch(VOID);` | Wait for a keypress; return its character code. |
| `INT p_gets(TEXT *str);` | Read a line (backspace editing) up to `P_MAXSYSIO-1`; returns length. |
| `INT p_getl(TEXT *pmt, TEXT *str, INT len);` | Write prompt `pmt`, read up to `len` chars into `str`; returns length. |
---
## 5. Number ↔ String Conversion
Declared in the *Integer Conversion and Rectangle Functions* chapter.
### 5.1 Integer/long → text
| Signature | Purpose |
|---|---|
| `UINT p_itob(TEXT *buf, INT value);` | Signed decimal of INT → buf; returns chars written. |
| `INT p_ltob(TEXT *buf, LONG value);` | Signed decimal of LONG → buf; returns chars written. |
| `INT p_gtob(TEXT *buf, UINT value, INT radix);` | Unsigned UINT in any radix → buf. |
| `INT p_gltob(TEXT *buf, ULONG value, INT radix);` | Unsigned ULONG in any radix → buf. |
### 5.2 Formatted multi-argument output
| Signature | Purpose |
|---|---|
| `INT p_atob(TEXT *buf, TEXT *fstr, VOID *parg);` | `printf`-style format of arg list `parg` into buf; returns chars written. |
| `VOID p_atos(TEXT *str, TEXT *fstr, ...);` | Convenience variadic wrapper over `p_atob` producing a zero-terminated string. |
Format `%[<align>][<fill>]<width><type>`; types: `b` binary, `c` char, `d`
signed dec, `f` fill only, `m`/`w` 2-byte MSB/LSB binary *(EPOC 2.17+)*, `o`
octal, `s` string, `u` unsigned dec, `x` hex. Widen to long with `l`/`L` or
upper-case type; `<width>`/`<fill>` may be `*` (taken from args).
### 5.3 Text → integer/long (radix conversions)
All take `TEXT **pstr` (advanced past the parsed field on success), return 0 or
`E_GEN_OVER`/`E_GEN_FAIL`.
| Signature | Purpose |
|---|---|
| `INT p_stoi(TEXT **pstr, WORD *pval);` | Signed decimal string → 16-bit WORD. |
| `INT p_stol(TEXT **pstr, LONG *pval);` | Signed decimal string → 32-bit LONG. |
| `INT p_stog(TEXT **pstr, UWORD *pval, INT radix);` | Unsigned string in any radix → 16-bit UWORD. |
| `INT p_stogl(TEXT **pstr, ULONG *pval, INT radix);` | Unsigned string in any radix → 32-bit ULONG. |
| `INT p_stoa(TEXT **pstr, TEXT *fstr, ...);` | Scan multiple fields from a string into args per format `fstr`. |
### 5.4 Double ↔ text (floating point conversion)
| Signature | Purpose |
|---|---|
| `INT p_dtob(TEXT *pbuf, DOUBLE *pval, P_DTOB *pformat);` | Format a double to text per `P_DTOB` (type/width/decimals/point/triad); returns chars or `E_GEN_*`. |
| `INT p_stod(TEXT **pstr, DOUBLE *pval, INT point);` | Parse a floating-point number from text (`point` = decimal-point char) → double. |
> Note: the manual provides no `p_dtos` — double→string is `p_dtob`.
---
## 6. Floating Point, Scientific & Math
Declared in `p_math.h`. Trig args are in **radians**. `E_CONFIG`/config via
`p_getctd`. Two families exist:
- **Scientific functions** (`p_sin``p_pow`) and `p_rand`/`p_frand` require the
8087 emulator (`sys$8087.ldd`).
- **Arithmetic primitives** (`p_fld``p_longtof`) are emulator-independent —
usable to do FP without loading the emulator.
Macros (also emulator-triggering with FP operands): `ABS(x)`, `MAX(a,b)`, `MIN(a,b)`.
### 6.1 Scientific (require emulator)
All return 0 or `E_GEN_ARG`/`E_GEN_UNDER`/`E_GEN_OVER`.
| Signature | Purpose |
|---|---|
| `INT p_sin(DOUBLE *pret, DOUBLE *parg);` | Sine. |
| `INT p_cos(DOUBLE *pret, DOUBLE *parg);` | Cosine. |
| `INT p_tan(DOUBLE *pret, DOUBLE *parg);` | Tangent (arg magnitude limited). |
| `INT p_asin(DOUBLE *pret, DOUBLE *parg);` | Arc sine (`ABS(*parg)<=1`). |
| `INT p_acos(DOUBLE *pret, DOUBLE *parg);` | Arc cosine (`ABS(*parg)<=1`). |
| `INT p_atan(DOUBLE *pret, DOUBLE *parg);` | Arc tangent. |
| `INT p_ln(DOUBLE *pret, DOUBLE *parg);` | Natural (base-e) log. |
| `INT p_exp(DOUBLE *pret, DOUBLE *parg);` | e raised to `*parg`. |
| `INT p_log(DOUBLE *pret, DOUBLE *parg);` | Base-10 log. |
| `INT p_sqrt(DOUBLE *pret, DOUBLE *parg);` | Square root. |
| `INT p_pow(DOUBLE *pret, DOUBLE *parg1, DOUBLE *parg2);` | `*parg1` raised to `*parg2`. |
### 6.2 Emulator-independent FP arithmetic
For one-arg functions, `pret` and `parg` may alias. Return 0 or `E_GEN_*`.
| Signature | Purpose |
|---|---|
| `INT p_fld(DOUBLE *pret, DOUBLE *parg);` | Load: `*pret = *parg`. |
| `INT p_fadd(DOUBLE *pret, DOUBLE *parg);` | `*pret += *parg`. |
| `INT p_fsub(DOUBLE *pret, DOUBLE *parg);` | `*pret -= *parg`. |
| `INT p_fmul(DOUBLE *pret, DOUBLE *parg);` | `*pret *= *parg`. |
| `INT p_fdiv(DOUBLE *pret, DOUBLE *parg);` | `*pret /= *parg`. |
| `INT p_fcmp(DOUBLE *parg1, DOUBLE *parg2);` | Compare: returns 1 / 0 / 1. *(OCR: `p_femp`.)* |
| `INT p_fneg(DOUBLE *parg);` | Negate `*parg` in place. |
| `INT p_mod(DOUBLE *pret, DOUBLE *parg1, DOUBLE *parg2);` | Remainder of `*parg1 / *parg2`. |
| `INT p_int(DOUBLE *pret, DOUBLE *parg);` | Integer part (toward zero) → double. |
| `INT p_inti(WORD *pret, DOUBLE *parg);` | Integer part → 16-bit WORD (range-checked). |
| `INT p_intl(LONG *pret, DOUBLE *parg);` | Integer part → 32-bit LONG (range-checked). |
| `VOID p_itof(DOUBLE *pret, WORD *parg);` | Convert WORD → double. |
| `VOID p_longtof(DOUBLE *pret, LONG *parg);` | Convert LONG → double. |
### 6.3 Random numbers
| Signature | Purpose |
|---|---|
| `ULONG p_randl(ULONG *pseed);` | Next pseudo-random 32-bit long; updates `*pseed`. (No emulator.) |
| `DOUBLE p_rand(ULONG *pseed);` | Random double in [0,1); updates `*pseed`. (Emulator required.) |
| `VOID p_frand(DOUBLE *pret, ULONG *pseed);` | Random double in [0,1) → `*pret`; updates `*pseed`. |
> **Long integer arithmetic:** PLIB exposes no dedicated `p_lmul`/`p_ldiv`
> helpers; 32-bit `LONG`/`ULONG` work is done with native C, the `l*`
> conversion routines above, and `p_ltob`/`p_stol`/`p_stogl`/`p_randl`.
---
## 7. Rectangle & Geometry Utilities
Operate on `P_RECT` / `P_POINT` (declared with the integer-conversion chapter).
| Signature | Purpose |
|---|---|
| `VOID p_offrec(P_RECT *rect, INT xoffset, INT yoffset);` | Move a rectangle by an offset. |
| `VOID p_insrec(P_RECT *rect, INT xinset, INT yinset);` | Shrink/expand a rectangle about its centre. |
| `VOID p_unirec(P_RECT *rect1, P_RECT *rect2, P_RECT *result);` | Union (smallest enclosing rectangle). |
| `INT p_intrec(P_RECT *rect1, P_RECT *rect2, P_RECT *result);` | Intersection; returns TRUE if they intersect. |
| `INT p_pinrec(P_POINT *point, P_RECT *rect);` | TRUE if point lies inside rectangle. |
| `INT p_emprec(P_RECT *rect);` | TRUE if rectangle is empty. |
| `VOID p_absrec(P_RECT *rect, P_RECT *result);` | Normalise negative sides to positive. |
---
## 8. Memory Allocation & the Heap
Heap cells are allocated from the process data segment (max ~64K). `f_*`
variants leave with `E_GEN_NOMEMORY` instead of returning `NULL`.
### 8.1 Cell allocation
| Signature | Purpose |
|---|---|
| `VOID *p_alloc(UINT size);` | Allocate a heap cell ≥ `size` bytes; returns address or `NULL`. |
| `VOID *f_alloc(UINT size);` | As `p_alloc` but leaves on failure. |
| `VOID p_free(VOID *pcell);` | Free a cell (no-op if `pcell` is 0). |
| `VOID *p_realloc(VOID *pcell, UINT size);` | Resize a cell (preserving contents); `pcell==0``p_alloc`. |
| `VOID *f_realloc(VOID *pcell, UINT size);` | As `p_realloc` but leaves on failure. |
| `VOID *p_adjust(VOID *pcell, UINT offset, INT amount);` | Open (+) or close () a gap mid-cell for insert/delete. |
| `UINT p_alen(VOID *pcell);` | Return actual cell length in bytes (≥ requested). |
### 8.2 Heap tuning & diagnostics
| Signature | Purpose |
|---|---|
| `VOID p_hgran(UINT nparas);` | Set heap growth granularity (paragraphs of 16 bytes; ≤ `E_MAX_GROWBY`). |
| `VOID p_allwalk(VOID (*fptr)(VOID *fpar, INT isalloc, UINT len), VOID *fpar);` | Walk every heap cell, calling `fptr`; panics on inconsistency. |
| `VOID p_allchk(INT num);` | Check allocated cells vs. free list; `p_panic(0xff)` with diagnostics if corrupt. |
| `UINT p_allspc(VOID **pheap);` | Return potential free heap space; write heap start to `*pheap`. |
| `p_altchk(...)` **[?]** | Thorough heap-integrity check (referenced by name; full signature not printed in source). |
### 8.3 System memory info
| Signature | Purpose |
|---|---|
| `UINT p_getram(VOID);` | Addressable system RAM in 16-byte paragraphs (≤ 32768). |
| `UINT p_totalK(VOID);` | Total machine RAM in KB, ignoring bank-switching. *(EPOC 3.50+)* |
| `UINT p_sgfree(VOID);` | Available addressable segmented memory in paragraphs. |
| `UINT p_sgramdisk(VOID);` | Paragraphs of addressable RAM used by the internal RAM disk. |
### 8.4 External data segments (shared memory / IPC)
| Signature | Purpose |
|---|---|
| `INT p_sgdelete(TEXT *pName);` | Delete a named external data segment. |
| `INT p_sgcopyto(HANDLE nHandle, LONG pos, VOID *source, UINT len);` | Write `len` bytes into an open segment at `pos`. |
| `INT p_sgcopyfr(HANDLE nHandle, LONG pos, VOID *target, UINT len);` | Read `len` bytes from an open segment at `pos`. |
| `UINT p_sgsize(HANDLE nHandle);` | Size of an open segment in 16-byte paragraphs. |
| `INT p_sgadjust(HANDLE nHandle, INT nParas);` | Grow/shrink an open segment by `nParas` paragraphs. |
| `INT p_sgclose(HANDLE nHandle);` | Close an open segment. |
| `VOID p_sglock(HANDLE nHandle);` | Lock a segment against relocation. |
| `VOID p_sgunlock(HANDLE nHandle);` | Unlock a previously locked segment. |
*(The chapter also references `p_sgcreate`/`p_sgopen`/`p_sgfind`; their clean
signatures are not isolated in the source text — **[?]**.)*
### 8.5 Environment variables
| Signature | Purpose |
|---|---|
| `INT p_getenv(TEXT *pMatch, TEXT *pValue);` | Get value of matching env var (string). |
| `INT p_getenviron(TEXT *pMatch, INT mLength, VOID *pValue);` | Get env var value (binary, length-specified match). |
| `INT p_setenv(TEXT *pName, TEXT *pValue);` | Set/create a string env var. |
| `INT p_setenviron(TEXT *pName, INT nLength, VOID *pValue, INT vLength);` | Set a binary env var. |
| `INT p_delenv(TEXT *pMatch);` | Delete matching env var. |
| `INT p_delenviron(TEXT *pMatch, INT mLength);` | Delete env var (length-specified match). |
| `INT p_fndenv(TEXT *pMatch, TEXT *pName, TEXT *pValue, HANDLE *pHandle);` | Iterate/find env vars by name pattern. |
| `INT p_findenviron(TEXT *pMatch, INT mLength, UBYTE *pBuf, HANDLE *pHandle);` | Iterate/find env vars (binary). |
---
## 9. Date, Time & Timers
System time = seconds since 00:00:00 1 Jan 1970 (`ULONG`). Day-based math uses
`P_DAYSEC` (days since 1900 + seconds in day); human form uses `P_DATE`
(year-1900, month 011, day 030, h/m/s, yrday). Declared in `p_date.h`.
### 9.1 System clock
| Signature | Purpose |
|---|---|
| `ULONG p_date(VOID);` | Return current system time (seconds since 1970). |
| `VOID p_sdate(ULONG newTime);` | Set system time (may fire due absolute timers). |
### 9.2 Sleeping / synchronous timers
| Signature | Purpose |
|---|---|
| `INT p_sleep(ULONG n);` | Sleep `n` tenths of a second (relative timer). |
| `INT p_sleept(LONG nTicks);` | Sleep `nTicks` system ticks (32/s SIBO). |
| `INT p_sleepa(ULONG time);` | Sleep until absolute system `time` (wakes machine). |
*(Asynchronous timers are driven through an open `TIM:` channel via
`p_ioc(P_FRELATIVE)` / `p_ioc(P_FABSOLUTE)` and cancelled with `p_iow(...,P_FCANCEL)`.)*
### 9.3 Date/time conversions
| Signature | Purpose |
|---|---|
| `VOID p_sttods(ULONG *pstim, P_DAYSEC *pds);` | System time → days-since-1900 + seconds-in-day. |
| `INT p_dstost(P_DAYSEC *pds, ULONG *pstim);` | `P_DAYSEC` → system time. |
| `INT p_dstodt(P_DAYSEC *pds, P_DATE *pdt);` | `P_DAYSEC``P_DATE` (fills yrday). |
| `INT p_dttods(P_DATE *pdt, P_DAYSEC *pds);` | Validate `P_DATE``P_DAYSEC`. |
| `INT p_dayinm(INT year, INT month);` | Days in given month (leap-aware); year since 1900. |
| `INT p_wkday(ULONG nDay);` | Weekday (0=Mon … 6=Sun) for day-since-1900. |
| `INT p_weekno(ULONG nDay);` | Week number 153 for day-since-1900. |
### 9.4 Localised name/text lookups
| Signature | Purpose |
|---|---|
| `INT p_nmday(TEXT *buf, INT daynum);` | Language day name (0=Mon … 6). |
| `INT p_nmdaya(TEXT *buf, INT daynum);` | Abbreviated day name. *(EPOC 3.18+)* |
| `INT p_nmmon(TEXT *buf, INT monthnum);` | Language month name (0=Jan … 11). |
| `INT p_nmmona(TEXT *buf, INT monthnum);` | Abbreviated month name. *(EPOC 3.18+; OCR prints this as a second `p_nmmon`.)* |
| `VOID p_getsuffixes(TEXT *buf);` | Fill array of 31 day-of-month suffixes (3-byte elements). |
| `VOID p_getampmtext(TEXT *buf, INT n);` | am (`n=0`) / pm (`n=1`) suffix text. |
| `VOID p_getctd(E_CONFIG *pcfg);` | Copy system locale/config (`E_CONFIG`) struct. |
---
## 10. Object-Oriented Programming Primitives (brief)
PLIB provides run-time OOP: categories (load modules of classes) identified by
category number or `HANDLE`; objects are heap instances with a class header.
`f_*` twins leave with `p_leave` on failure. Declared with the OOP chapter.
### 10.1 Category / object creation & class management
| Signature | Purpose |
|---|---|
| `HANDLE p_getlibh(INT catNum);` | Convert a category number to a category handle (0 = local). |
| `VOID *p_new(INT catNum, INT classNum);` | Create an instance of `classNum` in category `catNum`; property zeroed. |
| `VOID *f_new(INT catNum, INT classNum);` | As `p_new`, leaves on OOM. |
| `VOID *p_newlibh(HANDLE catHandle, INT classNum);` | Create instance by category handle. |
| `VOID *f_newlibh(HANDLE catHandle, INT classNum);` | As `p_newlibh`, leaves on OOM. |
| `VOID *f_newsend(INT catNum, INT classNum, INT methodNum, ...);` | Create then send init `methodNum`; cleans up (destroy) on leave. |
| `VOID *f_newlibhsend(HANDLE catHandle, INT classNum, INT methodNum, ...);` | As `f_newsend` but by category handle. |
| `VOID p_reclass(INT catNum, INT classNum, VOID *pObject);` | Change an object's class (same property length). |
| `VOID p_reclassbyhandle(HANDLE catHandle, INT classNum, VOID *pObject);` | Reclass by category handle. |
| `INT p_loadfilelib(VOID *fcb, UINT n, HANDLE *pCatHandle, INT link);` | Load an external category (load module) and get its handle. |
| `VOID p_linklib(HANDLE catHandle);` | Link a loaded category to the local process. |
| `VOID p_ccpy(VOID *pTarget, VOID *pSource, UINT count);` | Copy `count` bytes (category/object helper copy). |
### 10.2 Message sending
`p_send*` use the stack (`CDECL`) convention; the `p_send2..5` / `f_send2..5`
numbered variants use the faster register convention. `f_*` leaves with
`p_leave(err)` if the method returns negative.
| Signature | Purpose |
|---|---|
| `INT p_send(VOID *pObject, INT methodNum, ...);` | Dispatch `methodNum` to object, searching class then superclasses. |
| `INT f_send(VOID *pObject, INT methodNum, ...);` | As `p_send`, leaves on negative return. |
| `INT p_send2(VOID *pObject, INT methodNum);``p_send5(VOID *pObject, INT methodNum, VOID *p1, VOID *p2, VOID *p3);` | Register-convention sends with 03 extra args (`f_send2..5` twins). |
| `INT p_supersend(VOID *pObject, INT methodNum, ...);` | Dispatch starting at the superclass of the calling method (`p_supersend2..5`). |
| `INT p_entersend(VOID *pObject, INT methodNum, ...);` | Dispatch as if via `p_enter` (unwinds on `p_leave`) (`p_entersend2..5`). |
| `INT p_exactsend(HANDLE catHandle, INT classNum, VOID *pObject, INT methodNum, ...);` | Dispatch starting at an explicitly named class. |
---
## 11. Related Error/Process Primitives (referenced above)
These appear alongside the categories above and are commonly used with them:
| Signature | Purpose |
|---|---|
| `INT p_enter(VOID *pfunc, ...);` | Enter a function under structured error trapping. |
| `VOID p_panic(INT nPanic);` | Abort the process with a panic number (unrecoverable). |
| `VOID p_exit(INT nReason);` | Graceful process termination. |
*(Full error-handling and process/IPC APIs are outside this core-PLIB scope but
`p_enter`/`p_leave`/`p_panic` underpin the `f_*` allocator and sender twins.)*
---
### Uncertainty notes
- OCR corrected: `p_bcmp`/`p_bcmpi` (src `p_bemp`), `p_fcmp` (src `p_femp`),
`p_pow` (src fragment `p (…)1_POw`), `p_nmmona` (src duplicate `p_nmmon`).
- `p_altchk`, `p_dequed`, `p_sgcreate`, `p_sgopen`, `p_sgfind` are named in the
manual but their clean C signatures are not isolated in the source text; marked **[?]**.
- Version-gated functions annotated where the manual states a minimum EPOC version.
+725
View File
@@ -0,0 +1,725 @@
# Psion SIBO / Workabout MX — File System and DBF Database API Reference
Source: *PLIB Reference*, chapters **Files** (ch. 11) and **Database Files** (ch. 14), with
corroborating structural detail from *EPOC O/S System Services*, chapter 20 (*Database File
Management*). Everything below is drawn from those manuals; page-anchored citations are given as
`[PLIB p.NNN]` / `[SysSvc §20]`. OCR-garbled items are flagged explicitly.
> **OCR caveat.** The source is scanned OCR text. Identifiers written `p_open`, function-name
> casing, and hex constants have been normalised to their obvious intended form. Where a
> *signature* itself is corrupted or truncated in the source, this is called out inline. Do not
> treat a normalised name as a guarantee of exact header spelling — verify against `p_file.h` /
> `p_dbf.h` before compiling.
---
## Part 1 — The File System
### 1.1 The file server and file systems (nodes)
- All file operations are performed by a high-priority system process, the **file server**
(process name `SYS$FSRV`). A process must connect to it before use; the C startup module
normally does this automatically. Sending a message to the file server without connecting
panics with panic number 41. [PLIB p.117]
- The file server supports multiple **file systems**, also called **nodes**. `p_open` can list
them. Three were implemented at the time of writing [PLIB p.117]:
- `LOC::` — the local filing system, with the RAM drive `M:` and SSD drives `A:`, `B:`, …
(count depends on hardware).
- `REM::` — the remote filing system (present only while connected to a remote file server).
- `ROM::` — the ROM filing system for ROM-based files; normally invisible to the user; does
**not** support devices or directories.
- Within `LOC::` (and `REM::` when the remote end is a PC or another SIBO machine), the device
and directory structure is MSDOS-compatible. [PLIB p.117]
**Media types (SSDs).** RAM SSDs (battery-backed static RAM, block-structured, not buffered on
SIBO) and Flash SSDs (linked variable-length records). Overwriting or deleting on Flash consumes
space that is only reclaimed by reformatting; a byte can be physically overwritten with **no**
storage penalty only if the new value is derived from the old by clearing bits to zero. This is
the property the DBF layer exploits. [PLIB pp.118120, 195]
### 1.2 File specification structure
A full file specification has the form [PLIB p.120]:
```
<node><device><dir><name><ext>
```
Example: `LOC::B:\NOTES\OLD\PLANS.TPD`, where:
| Component | Meaning | Example |
|-----------|----------------------|-----------------|
| `<node>` | file system node | `LOC::` |
| `<device>`| device name | `B:` |
| `<dir>` | directory name | `\NOTES\OLD\` |
| `<name>` | file name | `PLANS` |
| `<ext>` | extension name | `.TPD` |
Rules [PLIB p.120]:
- A file specification never exceeds `P_FNAMESIZE` (**128**) bytes including the zero terminator.
- The `<node>` component is always `P_FSYSNAMESIZE` bytes long (excluding any zero terminator).
- Beyond `<node>` and the `P_FNAMESIZE` total, make **no** assumptions about component sizes; the
syntax of `<device>`/`<dir>`/`<name>`/`<ext>` is owned by the node's file-system code (foreign
remote systems can map onto this model, e.g. VMS or Mac paths). Use `p_fparse`/`p_chdir` rather
than manipulating specs by hand.
**Default path.** The file server stores a per-client default path (`<node><device><dir>`) plus a
system-wide default assigned to new clients. Manipulate with:
`p_setpth` / `p_setpthasync` (set this process's default), `p_getpth` (get it),
`p_getpthbyid` (get another process's), `p_setdefaultpath` (set the system-wide default).
[PLIB pp.121, 126]
**Parsing / directory manipulation.**
- `INT p_fparse(TEXT *name, TEXT *related, TEXT *full, P_FPARSE *perk);` — builds a full spec into
`full` (reserve `P_FNAMESIZE` bytes; `P_FNAMESIZE` bytes are always written). Components are
taken from `name`, then `related` (may be `NULL`), then the default path, in that order of
precedence. Output is upper-cased. `perk` (may be `NULL`) receives a `P_FPARSE` struct with the
lengths of each component and a wildcard-flags byte (`P_PWILD_ANY`, `P_PWILD_NAME`,
`P_PWILD_EXT`). `f_fparse` is identical but calls `p_leave` on error instead of returning it;
`p_fparseasync` adds a trailing `WORD *stat`. [PLIB pp.123124]
- `INT p_chdir(TEXT *src, TEXT *outp, INT mode, TEXT *subdir);` — parse `src` and change its
directory per `mode`: `P_CD_ROOT`, `P_CD_PARENT`, or `P_CD_SUBDIR` (append zero-terminated
`subdir`). Reserve `P_FNAMESIZE` at `outp`. Async form `p_chdirasync`. [PLIB p.125]
### 1.3 `p_open` — modes, formats, and access flags
Channel-based file services all go through `p_open`:
```
INT p_open(VOID **ppfcb, TEXT *name, UINT mode);
```
On success returns 0 and writes the channel handle to `*ppfcb` (not written on failure). `name` is
parsed with a `NULL` related name. [PLIB p.142]
`mode` is a bitwise-OR of exactly **one open mode**, exactly **one format**, and any combination of
**access flags**.
#### Open modes (choose exactly one) [PLIB pp.142143]
| Mode | Behaviour |
|--------------|-----------|
| `P_FOPEN` | Open an existing file. `E_FILE_NXIST` if it does not exist. Normally used for read access. |
| `P_FCREATE` | Create a file that must **not** already exist. `E_FILE_EXIST` if it does. Requires `P_FUPDATE` for write. |
| `P_FREPLACE` | If the file exists, open and truncate to zero length; otherwise create it. Requires `P_FUPDATE` for write. |
| `P_FAPPEND` | Same as `P_FOPEN` but initial position is at end of file so the next write appends. `P_FRANDOM` not needed; `P_FUPDATE` needed for write. |
| `P_FUNIQUE` | Create a unique file, using the passed path as the related path; the unique name is written back to `name` (reserve `P_FNAMESIZE` bytes). `P_FUPDATE` not needed. |
#### Formats (choose exactly one) [PLIB pp.142, 146147; §list p.121]
| Format | Meaning |
|-------------------|---------|
| `P_FSTREAM` | Flat binary file. |
| `P_FSTREAM_TEXT` | Flat binary file, but declares the file is text; on `REM::` it makes the remote side present/parse CRLF-terminated records. Locally usually identical to `P_FSTREAM`; no penalty, potential gain on remote. Prefer this for self-processed text files. |
| `P_FTEXT` | Record-oriented text file. Implemented as a client-side layer (the `TXT:` device) over `P_FSTREAM_TEXT`; data is buffered in that layer, so flushing is needed. |
| `P_FDIR` | Open a directory-listing channel (list files/subdirectories). |
| `P_FDEVICE` | Open a device-listing channel (list devices of a node). |
| `P_FNODE` | Open a node-listing channel (list file systems). |
| `P_FFORMAT` | Open a device-format channel (`LOC::` only at time of writing; OR in `P_FLOWDENSITY` for dual-density low-density format). |
#### Access flags (combine as needed) [PLIB p.143]
| Flag | Meaning |
|-------------|---------|
| `P_FUPDATE` | Write access as well as read. Writing without it → `E_FILE_RDONLY`. |
| `P_FRANDOM` | Random access required; needed to use `p_seek`. Without it, `p_seek``E_FILE_INV`. Do not specify unless you will seek (the FS may optimise sequential-only access). |
| `P_FSHARE` | Do not block the file from being re-opened for **read** access. Without it, a later open → `E_FILE_LOCKED`. **Cannot** be combined with `P_FUPDATE` (shared *write* is unsupported). |
**Sharing rules:** any number of processes may open the same file for reading only, provided all
readers specify `P_FSHARE`. Multiple writers are never allowed. Once open for reading it may be
re-opened for reading but not writing; once open for writing it may not be re-opened at all.
[PLIB p.140]
**Examples** [PLIB p.140]:
```c
p_open(&fcb, "fred.dat", P_FSTREAM | P_FSHARE); /* read only */
p_open(&fcb, "fred.dat", P_FSTREAM | P_FUPDATE | P_FREPLACE | P_FRANDOM);/* writable */
```
Selected `p_open` errors: `E_GEN_NOMEMORY`, `E_GEN_ARG` (illegal flag combination),
`E_FILE_DEVICE`, `E_FILE_NOTREADY`, `E_FILE_EXIST`, `E_FILE_NXIST`, `E_FILE_ACCESS`,
`E_FILE_DIRFULL`, `E_FILE_PROTECT`, `E_FILE_FULL`, `E_FILE_LOCKED`, `E_FILE_DIR`, plus any
`p_fparse` error. [PLIB pp.143144]
### 1.4 Reading and writing streams
```
INT p_read (VOID *pfcb, VOID *buf, UINT len);
INT p_write(VOID *pfcb, VOID *buf, UINT len);
INT p_close(VOID *pfcb);
```
- `p_read` reads `len` bytes (or the bytes remaining before EOF, whichever is smaller) from the
current position; returns the number of bytes read. At EOF, reads 0 bytes and returns
`E_FILE_EOF`. `len` must not exceed `P_FMAXSSIZE` (**16K**). Most efficient in multiples of
`P_FBLKSIZE` (**512**) on 512-byte boundaries. Other errors: `E_FILE_ABORT`, `E_FILE_READ`.
[PLIB p.144]
- `p_write` writes `len` bytes at the current position; increments position by bytes written;
returns 0 on success. `len``P_FMAXSSIZE`. On Flash you may overwrite a single byte (`len==1`)
if the new byte only clears bits (then the modification date is not changed). Errors:
`E_FILE_FULL`, `E_FILE_RDONLY`, `E_FILE_ABORT`, `E_FILE_WRITE`. [PLIB p.144]
- `p_close(NULL)` is harmless (returns 0). Close may perform a final buffered write and a date
update, so it can return `p_write`/`p_fdate` errors — but it always closes the channel. To
handle failures cleanly, `p_iow(pfcb, P_FFLUSH)` first. `LOC::` on SIBO does not buffer written
data; EPOC-on-PC does. [PLIB p.143]
**`p_iow` control operations** on a binary/text channel [PLIB pp.145146]:
- `INT p_iow(VOID *pfcb, P_FFLUSH);` — flush buffered data and write the modification date.
- `INT p_iow(VOID *pfcb, P_FSETEOF, ULONG *peof);` — set the logical EOF to `*peof` (requires
`P_FUPDATE`). Extending pre-allocates storage on block devices; truncating also reduces the
current position if needed.
- `INT p_iow(VOID *pfcb, P_FCANCEL);` — cancel pending async requests (not actively supported; a
no-op even when a request is pending — simulate a cancel instead).
### 1.5 Seeking
**Binary / stream:**
```
INT p_seek(VOID *pfcb, INT sense, LONG *ppos);
```
`sense` is one of [PLIB p.144]:
| `sense` | Meaning |
|-----------|---------|
| `P_FABS` | set position to `*ppos` |
| `P_FEND` | set position to `*ppos` relative to end-of-file |
| `P_FCUR` | set position to `*ppos` relative to current position |
On success the new position is written back to `*ppos` and 0 returned. `E_FILE_INV` if the channel
was not opened with `P_FRANDOM`. A negative resulting position clamps to the start; beyond EOF
clamps to EOF (use `P_FSETEOF` to extend). Setting `*ppos=0` with `P_FCUR` senses the current
position; with `P_FEND` senses the file length. `f_seek` is the `p_leave`-on-error variant. On
`P_FSTREAM_TEXT` remote channels, only "seek to 0 (`P_FABS`)" and "seek to end (`P_FEND`, rel 0)"
are guaranteed. [PLIB pp.144145]
**Record-oriented text (`P_FTEXT`):** `p_seek` sets the **read** position only (writes always go to
EOF), and `sense` is different [PLIB pp.149]:
| `sense` | Meaning |
|--------------|---------|
| `P_FREWIND` | position to the first record (`ppos` ignored) |
| `P_FRSENSE` | get the position of the last record read/written |
| `P_FRSET` | set the record position previously got with `P_FRSENSE` |
Requires `P_FRANDOM` (else `E_FILE_INV`). Some remote systems may not support `P_FRSET`/`P_FRSENSE`.
### 1.6 Text records (`P_FTEXT`)
Convention: records are terminated by CRLF (CR = 13, LF = 10); a file may optionally end with SUB
(26). On read, the parser also accepts CR, LF, or LFCR as a terminator and treats a SUB as EOF; on
write it appends CRLF only (no SUB). Record content must not exceed `P_FMAXRSIZE` (**256**) bytes
and must not contain CR, LF, or SUB. [PLIB pp.146148]
- `p_read` returns the record length (bytes written to `buf`), positioning to the next record. If
`len` < record length, the first `len` bytes are read and `E_FILE_RECORD` returned (still
advances). Returns 0 on a zero-length record; `E_FILE_EOF` past the last record. [PLIB p.148]
- `p_write` writes a record of `len` bytes (0 … `P_FMAXRSIZE`); always appended at EOF; must not
contain delimiters. Errors incl. `E_FILE_RECORD` if oversize. [PLIB p.149]
### 1.7 Directory, device, and node listing
All three use the open → repeated `p_iow(..., P_FREAD, ...)` until `E_FILE_EOF` → close pattern.
- **Nodes:** `p_open(&ncb, "FIL:" or NULL, P_FNODE)`; each `p_iow(ncb, P_FREAD, buf, pinfo)`
writes the next node name (`buf` capacity `P_FSYSNAMESIZE+1`, i.e. 6). Optional `pinfo`
(`P_NINFO*`) gets the same info as `p_ninfo`. [PLIB p.127]
- **Devices:** `p_open(&dcb, node-name, P_FDEVICE)`; `E_GEN_FSYS` if the node is bad,
`E_GEN_NSUP` if the node has no devices (e.g. `ROM::`). Each `p_iow(P_FREAD)` writes the next
device name (`buf` capacity `P_FNAMESIZE`); trailing arg `NULL`. [PLIB p.128]
- **Files:** `p_open(&dcb, name, P_FDIR)`; `name` is parsed with a wildcard related name (`*.*`),
so a name of `""` lists the current directory. Each `p_iow(P_FREAD, buf, pinfo)` writes the next
matching file name (excluding node/device/dir; `buf` capacity `P_FNAMESIZE`). Optional
`pinfo` (`P_INFO*`) carries per-file info. A root directory of a PC-based device may return a
volume-name entry with `P_FAVOLUME` set. [PLIB pp.133134]
`P_INFO` (from `p_file.h`) [PLIB p.134]:
```c
typedef struct {
UWORD version;
UWORD status; /* status bits */
ULONG size; /* size of the file in bytes (end-of-file position) */
ULONG modst; /* system time of last modification (secs since 1970-01-01) */
UBYTE spare[4];
} P_INFO;
```
`status` bit fields: `P_FAWRITE` (not read-only), `P_FAMOD` (modified), `P_FAHIDDEN`,
`P_FASYSTEM`, `P_FADIR` (directory file), `P_FAVOLUME` (volume-name directory), `P_FATEXT`
(recognised text file; `LOC::` cannot recognise text files). [PLIB p.134]
### 1.8 File / device / node information and non-channel operations
- `INT p_finfo(TEXT *name, P_INFO *pinfo);` — info on one file/directory (same fields as a `P_FDIR`
read). Good atomic existence check (`E_FILE_NXIST` if absent). Async `p_finfoasync`. [PLIB p.135]
- `INT p_testpth(TEXT *dname);` — returns 0 if the directory component exists. [PLIB p.135]
- `INT p_ninfo(TEXT *node, P_NINFO *pninfo);` — node info. `P_NINFO { UWORD version; UWORD type;
UWORD formattable; UBYTE spare[26]; }`. `type` is `P_FSYSTYPE_FLAT` or `P_FSYSTYPE_HIER`.
[PLIB pp.127128]
- `INT p_dinfo(TEXT *dname, P_DINFO *pdinfo);` — device + mounted-medium info.
`P_DINFO { UWORD version; UWORD mediatype; UWORD removable; ULONG size; ULONG free;
UBYTE name[P_VOLUMENAME]; WORD batterystate; UBYTE spare[16]; }`. Low byte of `mediatype`:
`P_FMEDIA_UNKNOWN|FLOPPY|HARDDISK|RAM|FLASH|ROM|WRITEPROTECTED`; high byte flags incl.
`P_FMEDIA_COMPRESSIBLE` (worth compressing out deleted records — **not** set for Flash),
`P_FMEDIA_DYNAMIC`, `P_FMEDIA_INTERNAL`, `P_FMEDIA_DUAL_DENSITY`, `P_FMEDIA_FORMATTABLE`.
[PLIB pp.129130]
- Other non-channel calls (each with an `…async` twin): `p_rename`, `p_delete` (directory must be
empty), `p_mkdir` (creates intermediate dirs), `p_sfstat` (set attributes; with `P_FAVOLUME` set
in the mask, sets/deletes the volume label), `p_fdate` (set modification date, ≥ 1980-01-01).
[PLIB pp.136139]
- Formatting: open with `P_FFORMAT` then repeatedly `p_read` (first read yields a `UWORD` total
count, subsequent reads step the format, `E_FILE_EOF` when done). Aborting early corrupts the
medium. [PLIB pp.131132]
---
## Part 2 — The DBF Database API
### 2.1 The model
A **database file (DBF)** is a binary file of **typed, variable-length records**. Used by MC Diary,
Series 3 Database, and OPL data files. DBFs are **Flash-friendly**: records can be appended,
deleted, or replaced in place on a Flash SSD without rewriting the whole file. [PLIB p.195]
Key model properties [PLIB pp.195197, 204207]:
- **Record type 0 = deleted.** Deleting a record only overwrites its 4-bit type field with zero
(which merely clears bits — legal on Flash). Therefore **deleting does not shrink the file.**
- **Append-only; updates = erase + append.** Updating a record deletes the original and appends
the modified version, so an update **always moves the record to the end of the file**.
- **Reclaiming space:** `DbfCompress` (only on a *compressible* medium — not Flash) or copy the
file record-by-record with `DbfCopyFile` (deleted records are not copied).
- **Sparse index** (optional): one 4-byte address per **sixteenth** record, held in a *separate
segment* (`DBF$nnnn.INX`, `nnnn` a 4-hex-digit number from the channel) so it does not consume
the app's data space. Enables fast random access and fast backward scans; adjusted on
append/delete so it always points to every 16th record.
- **Read-ahead buffer:** each file-server read fills a caller-supplied buffer (typically 4K),
usually pulling in many records; a read for a record already in the buffer just locates it.
**Most DBF services may overwrite the buffer.** The next read after the buffer is disturbed
re-reads the whole buffer (a performance hit), so any direct modification of the buffer that is
*not* done via a DBF service must be followed by `DbfTrash`. Services guaranteed **not** to
alter the buffer: `DbfFlush`, `DbfVersion`, `DbfAppend`, `DbfSense`, `DbfCount`. [PLIB p.196]
**Record limit:** max **65534** records *of any one visible type*, numbered 0…65533. Since only one
type is visible per open, a file may hold more in aggregate. A file with more than the max (of the
visible type) is logically truncated to the max on open. [PLIB p.197]
**End-of-file record.** Reading past the last record returns `E_FILE_EOF`; the current record
number (per `DbfSense`) then becomes *last record + 1* — the fictitious **end-of-file record**.
Reading before the first record (`DbfBackRead`/`DbfFindRead` backwards) also gives `E_FILE_EOF`
with current record number 0. If the file has no records, `DbfSense` always returns 0. Services
that operate on the current record (`DbfEraseRead`, `DbfUpdate`, …) do nothing and return
`E_FILE_EOF` when positioned on the EOF record. [PLIB p.197]
### 2.2 The file header
DBFs begin with a **22-byte** standard header [PLIB p.196; SysSvc §20]:
| Byte offset | Contents |
|-------------|----------|
| 015 | Zero-terminated file signature (all 16 bytes are verified — pad with trailing zeros). |
| 16, 17 | Version of DBF software used to produce the file. |
| 18, 19 | Offset from start of file to the first record. |
| 20, 21 | Minimum version of DBF software required. |
The first-record offset allows an **extended header** (application-specific data after the standard
header). If none, the value is **22**. The first record is always the type-2 field information
record. Version-number format: see `DbfVersion`. [PLIB p.196; SysSvc §20]
### 2.3 Records and the record header word
In memory, a record is a `DbfRecord` (`p_dbf.h`) [PLIB p.196]:
```c
typedef struct {
UWORD header; /* record header word */
UBYTE data[2]; /* data to be written... (variable length in practice) */
} DbfRecord;
```
The **header word**: top **4 bits** = record **type** (015); low **12 bits** = record **length**.
Maximum record length is **4094** bytes (one less than the theoretical 0xFFF = 4095, so a 4094-byte
record plus its 2-byte header fits a 4096-byte buffer). [PLIB p.196; SysSvc §20]
**Record types** [PLIB pp.196197; SysSvc §20]:
| Type | Meaning |
|-------|---------|
| 0 | Deleted record — ignored by all DBF services; never copied by `DbfCopyFile`. |
| 1 | Standard data record (fields per the field information record). Usually the only visible type. |
| 2 | **Field information record (FIR)** — must exist and be the first record; later type-2 records ignored. |
| 3 | Descriptive record — optional, file-wide app data (see `DbfDescRecordRead/Write`). |
| 47 | App-specific: copied to a **new** file, **not** appended to an existing file, by `DbfCopyFile`. |
| 813 | App-specific: **both** copied to a new file and appended to an existing file (merged) by `DbfCopyFile`. |
| 14 | Reserved for voice records. |
| 15 | Reserved for internal use — do not use. |
### 2.4 The Field Information Record (FIR) and field types
The FIR (type 2) holds up to **32 bytes**, one per field, giving each field's type [PLIB p.197;
SysSvc §20]:
| Byte value | Field type |
|------------|------------|
| 0 | Word |
| 1 | Long |
| 2 | Double |
| 3 | String |
| 4255 | Reserved |
So a data record has at most 32 fields — **with one exception:** a record that contains **only
string fields** is not bound by the 32-field limit and may hold any number of fields, subject to
the 4094-byte record cap. Records may contain **fewer** fields than the FIR lists, provided only
*trailing* fields are omitted; records with **more** fields than the FIR are assumed to have the
extra ones as string fields. Only `DbfFindRead`/`DbfFindReadField` assume records match the FIR.
[PLIB p.197]
### 2.5 String fields (leading byte count) and continuation sub-fields
A string field is **leading byte-counted text**, so a normal string field holds at most **255**
characters. Longer strings use **continuation sub-fields**: the first 254 characters plus a
terminating byte `0x14`, stored in a string field whose count byte is **255**; the `0x14` +
count-255 signals that the immediately following string field continues the text. This chains
indefinitely, subject only to the 4094-byte record limit. [PLIB p.197]
### 2.6 The structs
`DbfHeader` (`p_dbf.h`) — passed to open [PLIB p.199]:
```c
typedef struct {
UBYTE fileType[DbfHeaderNameSize]; /* 16-byte file signature */
UWORD createVersion; /* software version used to create the file */
UWORD dataStart; /* offset in file of first record (22 if no ext header) */
UWORD needVersion; /* minimum software version needed to handle this file */
UWORD firHeader; /* header word for the field information record */
UBYTE fir[DbfMaxFirLength]; /* the field information record bytes */
} DbfHeader;
```
- When **creating/replacing**, pre-fill *all* elements (header + FIR). There is no gap between the
header and FIR in the struct even when an extended header is used; the *file* leaves a gap of
`dataStart 22` bytes for it.
- When **opening existing**, pre-fill only `fileType` (the signature); the rest is filled from the
file. All 16 signature bytes are verified — mismatch → `E_FILE_INVALID`.
- `DbfHeaderNameSize` = 16 (implied by "16-byte file signature"). `DbfMaxFirLength` is the FIR
capacity; the FIR maximum length is 32. *(Exact numeric value of `DbfMaxFirLength` not stated
verbatim in the source beyond the max-32 rule — verify in `p_dbf.h`.)*
`DbfOpenArgs` (`p_dbf.h`) — for `DbfQuickOpen` [PLIB p.200]:
```c
typedef struct {
VOID **pFcb;
UBYTE *fName;
UINT mode;
DbfHeader *pHead;
} DbfOpenArgs;
```
`DbfRecord` — see §2.3.
### 2.7 The DBF functions
Every DBF function calls `p_panic` if `pFcb` is not a valid DBF channel from `DbfOpen`/
`DbfQuickOpen`; that clause is omitted per-entry below. Unless noted "may be used on a DBF opened
without an index", the *without-index* behaviour is called out where the manual specifies it.
#### Opening / closing / flushing
- `INT DbfOpen(INT *pstate, VOID **pFcb, TEXT *fName, UINT mode, DbfHeader *pHead, UBYTE *pbuffer, UINT len, UINT type);`
Open a DBF channel. `mode` = exactly one of `P_FOPEN`/`P_FCREATE`/`P_FREPLACE`/`P_FAPPEND`/
`P_FUNIQUE`, optionally OR'd with `P_FUPDATE` and/or `P_FSHARE` (other required stream flags are
supplied automatically; `P_FOPEN` and `P_FAPPEND` are treated identically). `pbuffer`/`len` is
the caller's read-ahead buffer: **len must be 512…16384** (else `E_FILE_RECORD`), and must be at
least as large as the largest record (4096 is guaranteed sufficient). Only records of type `type`
are visible (normally 1; may be 414; results undefined for 0/2/3/>14). `*pstate` selects the
open strategy [PLIB pp.198199]:
- `DbfStateDisabled` — open with a full sparse index; returns only when the index is fully built
(may take a while).
- `DbfStateOpenNoIndex` — open **without** an index; returns fast, but some services are then
disallowed (see each function).
- `DbfStateStart` — open with a sparse index incrementally: call `DbfOpen` repeatedly, feeding
back `*pstate` each time, until `*pstate` becomes `DbfStateStart` again *(as printed — the
source states the loop terminates when the written-back value is `DbfStateStart`; this reads
like an OCR/spec inconsistency, likely intended to be a distinct "finished" state such as
`DbfStateDisabled`/`DbfStateEnd`. **Flag: verify the terminating state in `p_dbf.h`.**)*.
After a successful open the current record is **0**, so `DbfNextRead` reads record 1 and
`DbfEraseRead` erases record 0; use `DbfFirstRead` to read record 0. Errors: those of
`p_open(P_FSTREAM)`, `p_seek`, `p_read`, plus `E_FILE_INVALID` (bad signature / bad FIR),
`E_FILE_RECORD` (bad buffer length). [PLIB p.199]
- `INT DbfQuickOpen(INT *pstate, DbfOpenArgs *pargs, UBYTE *pbuffer, UINT len, UINT type);`
As `DbfOpen` but `pFcb`/`fName`/`mode`/`pHead` are bundled in `DbfOpenArgs`. **Preferred** over
`DbfOpen` (more efficient, shorter code); `DbfOpen` retained for compatibility. [PLIB p.200]
- `INT DbfClose(VOID *pFcb);` — close the file (returns as `p_close`); channel is closed even on
error. May be used without an index. [PLIB p.200]
- `INT DbfFlush(VOID *pFcb);` — flush all buffers so modified data is written (returns as
`p_write`). Does not alter the buffer. May be used without an index. [PLIB p.200]
- `VOID DbfTrash(VOID *pFcb);` — tell the DBF layer the read-ahead buffer was overwritten by the
caller and can no longer be relied on. May be used without an index. [PLIB p.200]
- `INT DbfCopyDown(VOID *pFcb, UINT offset);` — copy the record at `offset` in the buffer to the
start of the buffer (and flag the buffer invalid, so no `DbfTrash` needed); returns the record
length. `offset` must be one previously returned by a read service and the caller must not have
written to the buffer since. May be used without an index. [PLIB p.201]
#### Whole-file operations
- `INT DbfCompress(UINT *pstate, VOID *pFcb);` — reclaim space from deleted records **if** the
medium is compressible (otherwise a no-op that still returns 0). After a real compress the
current record is the EOF record. `*pstate` = `DbfStateDisabled` (blocking) or `DbfStateStart`
(incremental loop, terminating as written back). **Should not** be used without an index.
[PLIB p.201]
- `INT DbfCopyFile(UINT *pstate, VOID *pFcb, TEXT *pTargetName, UINT targetMode, UINT type, INT dir);`
Copy non-deleted records between the current file and `pTargetName`. `targetMode` = same options
as open (target always opened without an index; `P_FUNIQUE` writes back the unique name).
`type` selects a single type (usually 1) or `DbfRecordTypeAll` for all types. `dir` is
`DbfCopyFromHandle` (current → target; target new file with `P_FCREATE`/`P_FREPLACE`/`P_FUNIQUE`,
or append with `P_FOPEN`/`P_FAPPEND`) or `DbfCopyToHandle` (target → current; target sensibly
`P_FOPEN`). Copying to a **new** file always copies the FIR (type 2) and the file header
(incl. extended header) regardless of `type`; **appending** never copies types 27. Appending
requires matching signatures and compatible FIRs (identical, or both string-only) else
`E_FILE_INVALID`. `*pstate` supports `DbfStateDisabled`, `DbfStateStart` (est. calls =
file size / buffer size + 2), and `DbfStateCopyAbort` (abort an in-progress `DbfStateStart`
copy). May be used without an index. **Warning:** appending can exceed 65534 records with no
error. [PLIB pp.201202]
- `INT DbfFileSize(VOID *pFcb, ULONG *pSize);` — write the open file's size to `*pSize`. May be
used without an index. [PLIB p.202]
- `UINT DbfVersion(VOID);` — DBF software version as hex `xyyF`: `x` = major (4 bits), `yy` = minor
(8 bits), `F` = release type A/B/F (Alpha/Beta/Final, 4 bits). E.g. `0x110F` → 1.10F. Only the
major number gates whether a file can be handled. May be used without an index. [PLIB p.203]
#### Extended header and descriptive record
- `INT DbfExtHeaderRead(UINT cont, VOID *pFcb, VOID *buf, UINT len);` — read up to `len` bytes of
the extended header; returns bytes read. `cont`: 0 = initial read (resets to start of ext
header); 1 = continue; use 0 if reading the whole thing in one call. `E_FILE_EOF` at end. May be
used without an index. [PLIB p.202]
- `INT DbfExtHeaderWrite(UINT cont, VOID *pFcb, VOID *buf, UINT len);` — symmetric writer. May be
used without an index. [PLIB pp.202203]
- `INT DbfDescRecordRead(VOID *pFcb);` — read the descriptive record to offset 0 of the read-ahead
buffer; returns its length, or `E_FILE_EOF` if none, or `E_FILE_INVALID` if the file was opened
without an index. Sub-records use the same word-header format as main records; ignore (do not
delete) unrecognised sub-record types. [PLIB p.203]
- `INT DbfDescRecordWrite(VOID *pFcb, UINT len);` — write a descriptive record from a `DbfRecord`
at buffer offset 0 (content starts at offset 2; the leading 2 bytes form the header and are not
counted in `len`). Any existing descriptive record is erased first (max one per file); `len==0`
just erases it. `E_FILE_INVALID` if opened without an index. [PLIB p.203]
#### Reading records
All read services return the **record data length** (excluding the 2-byte header) and write the
buffer **offset of the DbfRecord** (including its header) to `*pOffset`. The read record becomes
the current record. On `E_FILE_EOF`, `*pOffset` is invalid.
- `INT DbfAbsRead(VOID *pFcb, UINT recnum, UWORD *pOffset);` — seek to and read record `recnum`.
`E_FILE_EOF` if `recnum` > last record (current → EOF record). May be used without an index.
[PLIB p.204]
- `INT DbfAbsReadSense(VOID *pFcb, UINT recnum, UWORD *pOffset, ULONG *pPos);` — as `DbfAbsRead`
but also writes the file position of the record header to `*pPos`. May be used without an index.
[PLIB p.204]
- `INT DbfNextRead(VOID *pFcb, UWORD *pOffset);` — read the next record. `E_FILE_EOF` if already on
the last record / no records (current → EOF record). May be used without an index. [PLIB p.204]
- `INT DbfBackRead(VOID *pFcb, UWORD *pOffset);` — read the previous record. `E_FILE_EOF` if on the
first record / no records (current → record 0). May be used without an index. [PLIB p.204]
- `INT DbfFirstRead(VOID *pFcb, UWORD *pOffset);` — read the first record. `E_FILE_EOF` if no
records of the current type (current → 0 = EOF record). May be used without an index. [PLIB p.205]
- `INT DbfLastRead(VOID *pFcb, UWORD *pOffset);` — read the last record. `E_FILE_EOF` if none.
**Should not** be used without an index. [PLIB p.205]
#### Appending / erasing / updating
- `INT DbfAppend(VOID *pFcb, UINT len);` — append a record of the current type and length `len`
(data only) to EOF and make it current; returns 0. The record must be at the **start** of the
read-ahead buffer as a `DbfRecord` including its 2-byte header; `DbfAppend` builds the type/length
header from those two bytes (not counted in `len`). `E_GEN_OVER` if already 65534 records of the
type; `E_FILE_RECORD` if total (data + 2) exceeds the buffer. Does not alter the buffer.
**Should not** be used without an index. [PLIB p.205]
- `INT DbfEraseRead(INT *pstate, VOID *pFcb, UWORD *pOffset);` — erase the current record and read
the following one (becomes current); returns its length or negative. Two `E_FILE_EOF` cases:
(a) already on the EOF record / no records → does nothing; (b) current was the last record → it
is erased and current becomes the EOF record. Distinguish via `DbfSense`+`DbfCount`, or by
comparing `DbfCount` before/after. `*pstate` = `DbfStateDisabled` / `DbfStateStart` (incremental).
**Should not** be used without an index. [PLIB p.206]
- `INT DbfUpdate(INT *pstate, VOID *pFcb, UINT len);` — erase the current record and append a new
one of length `len` from the read-ahead buffer, making it current. The new record must be a
`DbfRecord` at the start of the buffer (leading 2 bytes = header, not counted in `len`). The old
record is **not** erased until the new one is successfully appended. `E_FILE_EOF` (no-op) if no
records of the current type or if on the EOF record. `*pstate` = `DbfStateDisabled` /
`DbfStateStart`. **Should not** be used without an index. [PLIB p.206]
*(Note: the manual's prose here says "DbfAppend uses these two bytes" — clearly referring to the
same header-construction mechanism.)*
#### Sensing / counting
- `UINT DbfSense(VOID *pFcb);` — return the current record number (the EOF record number — 0 if
empty, else count+1 — if a preceding call returned `E_FILE_EOF`). Does not alter the buffer. May
be used without an index. [PLIB p.209]
- `UINT DbfCount(VOID *pFcb);` — return the number of records of the currently visible type without
changing the current record. Does not alter the buffer. **Should not** be used without an index.
[PLIB p.209]
#### Finding by content
- `INT DbfFindRead(UINT *pstate, VOID *pFcb, VOID *pBuffer, UINT len, UINT findMode, UINT nStrings, UWORD *pOffset);`
Match wildcard text at `pBuffer` of length `len` (≤ 255) against the first `nStrings` **string
fields** of each record, starting at the current record; returns the matching record's data
length or a negative error. Equivalent to `DbfFindReadField` with `startStr = 0`. `nStrings ==
DbfFindAllStrings` searches all string fields to end-of-record. The FIR is used to type the first
32 fields; fields beyond that are assumed strings. On match, that record becomes current and its
buffer offset is written to `*pOffset`; on no match, `E_FILE_EOF` and current becomes the first
record (backward search) or the EOF record (forward search). May be used without an index
**except** `DbfFindLast` (unpredictable). Panics if `findMode` is malformed. [PLIB pp.208209]
- `DbfFindReadField(...)` — same as `DbfFindRead` but with an extra `startStr` argument: match
starts at string field number `startStr` (0 = first string field). **Available only in EPOC
≥ 3.18.**
**OCR FLAG:** the signature is **garbled/truncated** in the source. The printed fragment reads:
`… UINT nStrings, UWORD *pOffset, UINT startStr);` (line lacks the return type, `pstate`, `pFcb`,
`pBuffer`, `len`, `findMode`). By analogy with `DbfFindRead` the full signature is almost
certainly:
```c
/* RECONSTRUCTED — verify against p_dbf.h before use */
INT DbfFindReadField(UINT *pstate, VOID *pFcb, VOID *pBuffer, UINT len,
UINT findMode, UINT nStrings, UWORD *pOffset, UINT startStr);
```
[PLIB pp.207208]
**`findMode`** is an OR of three parts [PLIB pp.207208]:
1. **Max match length** in any one string field (0…255; 255 = no truncation) occupies the low part
of `findMode`.
2. **Direction / start**: `DbfFindForwards`, `DbfFindBackwards`, `DbfFindFirst`, `DbfFindLast`.
3. **Case**: `DbfFindCaseIndependent` or `DbfFindCaseDependent`.
**Finding across continuation sub-fields** (only needed if strings may exceed 255 chars): OR
`0x1400` into `len` (whose base value cannot exceed 255); set the length component of `findMode` to
255 and OR in `0x4000`; only case-independent matching is allowed (OR `DbfFindCaseIndependent`);
then OR in the direction flag as usual. [PLIB p.208]
> **No per-field getter/setter exists.** The DBF API deals in whole records only. There is no
> function to read or write an individual Word/Long/Double/String field. The application must
> **pack and unpack field bytes by hand** into/out of the `DbfRecord.data` area, following the FIR
> field order and the string leading-byte-count convention. The find services are the only ones
> that interpret field structure, and only for string matching.
### 2.8 Error names (DBF)
`E_FILE_EOF` (past end / before start), `E_FILE_INVALID` (bad signature / FIR / no-index misuse),
`E_FILE_RECORD` (bad buffer length or record too big for buffer), `E_GEN_OVER` (65534-record limit
hit on append). `DbfClose`/`DbfFlush` propagate `p_close`/`p_write` errors; most read services
propagate `p_seek`/`p_read` errors. [PLIB pp.199, 205206] The System Services manual names the
same conditions `EofErr`, `InvalidFileErr`, and the panics `PanicDbf1` (bad handle) / `PanicDbf2`
(bad offset). [SysSvc §20]
---
## Part 3 — Worked recipe: create schema → append → read back
Assembled strictly from the manual's prose (PLIB ch. 14). Field packing is done by hand because
there is no per-field API. Illustrative — variable names/details are the author's; the API calls,
struct layout, and offsets are per the manual.
**Schema for the example:** two fields — field 0 = Long (FIR byte `1`), field 1 = String (FIR byte
`3`).
### Step 1 — Create the file with header + FIR
Pre-fill a `DbfHeader` completely (creating/replacing requires the full header **and** FIR;
`p.199`). The FIR is itself a type-2 record, so `firHeader` is a record header word: top 4 bits =
type 2, low 12 bits = FIR length (here 2). Provide a read-ahead buffer of 4096 (guaranteed
sufficient; `p.199`) and open with visible `type = 1`.
```c
#include <p_dbf.h>
UBYTE buf[4096]; /* read-ahead buffer, 512..16384; >= largest record */
VOID *fcb;
INT state;
DbfHeader h;
DbfOpenArgs args;
/* --- header --- */
p_bfill(&h.fileType[0], DbfHeaderNameSize, 0); /* zero the 16-byte signature */
p_scpy(&h.fileType[0], "MYAPPDatabase"); /* pad remainder stays 0 (p.196) */
h.createVersion = DbfVersion(); /* p.203 */
h.dataStart = 22; /* no extended header (p.196) */
h.needVersion = DbfVersion();
/* --- FIR (type 2, length 2): field0=Long(1), field1=String(3) --- */
h.firHeader = (2 << 12) | 2; /* top 4 bits type, low 12 len */
h.fir[0] = 1; /* Long (p.197) */
h.fir[1] = 3; /* String (p.197) */
args.pFcb = &fcb;
args.fName = (UBYTE *)"MYDATA.DBF";
args.mode = P_FCREATE | P_FUPDATE; /* create new, writable (Files ch)*/
args.pHead = &h;
state = DbfStateDisabled; /* build index, blocking (p.199) */
if (DbfQuickOpen(&state, &args, &buf[0], sizeof(buf), 1) < 0)
/* handle error (p_fparse / p_open / E_FILE_* errors) */;
```
### Step 2 — Pack a record by hand and append it
A record to be appended must sit at the **start of the buffer** as a `DbfRecord`: 2-byte header
then data; `DbfAppend` builds the header from those two bytes and `len` is the **data** length only
(`p.205`). Pack the Long field (its byte layout is the app's responsibility), then the String field
as a leading count byte followed by its characters (`p.197`).
```c
DbfRecord *rec = (DbfRecord *)&buf[0];
UBYTE *d = &rec->data[0]; /* data starts at buffer offset 2 */
UINT len = 0;
LONG idNum = 42;
TEXT *nameStr = "Widget";
UINT nameLen = p_slen(nameStr); /* <= 255 for a plain string field */
/* field 0: Long (4 bytes, app-defined byte order) */
p_bcpy(&d[len], &idNum, sizeof(LONG)); len += sizeof(LONG);
/* field 1: String = leading byte count + chars (p.197) */
d[len++] = (UBYTE)nameLen;
p_bcpy(&d[len], nameStr, nameLen); len += nameLen;
/* header bytes are set by DbfAppend from rec->header; type is the current type (1) */
if (DbfAppend(fcb, len) < 0) /* appends, becomes current record (p.205) */
/* handle E_GEN_OVER / E_FILE_RECORD / p_write errors */;
```
### Step 3 — Read the record back and unpack
After open the current record is 0 (`p.199`). Reads return the **data length** and write the
`DbfRecord` **offset** (header included) into `*pOffset`. Read the first record, then unpack in FIR
order. (`DbfCopyDown` first if you intend to edit in the buffer — `p.201`/`SysSvc §20`.)
```c
UWORD off;
INT rlen = DbfFirstRead(fcb, &off); /* read record 0 (p.205) */
if (rlen >= 0) {
DbfRecord *r = (DbfRecord *)&buf[off];
UBYTE *p = &r->data[0];
LONG gotId;
UBYTE slen;
TEXT gotName[256];
p_bcpy(&gotId, &p[0], sizeof(LONG)); /* field 0: Long */
p += sizeof(LONG);
slen = *p++; /* field 1: count byte, then chars (p.197) */
p_bcpy(&gotName[0], p, slen);
gotName[slen] = 0;
/* gotId == 42, gotName == "Widget" */
}
DbfFlush(fcb); /* ensure written data is on the medium (p.200) */
DbfClose(fcb); /* p.200 */
```
Notes tied to the model:
- To **update** the record, pack a new `DbfRecord` at buffer offset 0 and call
`DbfUpdate(&state, fcb, len)`; the record is deleted and re-appended at EOF (`p.206`).
- To **delete**, position on it (e.g. `DbfAbsRead`) and call `DbfEraseRead(&state, fcb, &off)`
(`p.206`); the file does not shrink until `DbfCompress` (non-Flash) or a `DbfCopyFile` rebuild
(`p.195`).
- `DbfFindRead` / `DbfFindReadField` can locate a record by matching a wildcard against its string
fields (`p.207`209); numeric fields are not searchable by these services.
+480
View File
@@ -0,0 +1,480 @@
# Psion SIBO / Workabout MX — User-Interface Programming
**The Window Server and the Console**
Reference notes drawn from the SIBO 'C' SDK *Window Server Reference* (v2.30,
March 1999) and the *PLIB Reference* (console functions). Everything below is
sourced from those two manuals; section/line references are given as
`[wserv]` and `[plib]`. Anything not backed by the manuals is explicitly
flagged as **reverse-engineered** or **uncertain**.
> **Machine-type note.** The manuals cover the HC, MC, S3, S3a and Workabout.
> The **Workabout** is reported by the window server as machine type
> `WS_TYPE_S3C` (i.e. it shares the "Series 3c" identity), and in
> compatibility mode its `version_id` is `WS_TYPE_S3C | WS_VERSION_4`
> `[wserv 7288-7291]`. The manuals do **not** name a "Workabout MX"
> variant; MX-specific behaviour (notably the barcode scan key, below) is
> **not documented** and is marked as reverse-engineered where it appears.
---
## 1. The Window Server model
The window server is a **system process** that provides shared access to the
screen and the keyboard (and a pointing device, where one exists) `[plib
1295-1296]`. Application processes talk to it as **clients** through the
**WLIB** library — a thin C shell over ROM-based code reached by software
interrupts `[plib 1338-1339]`.
### 1.1 Clients, foreground and background
- Of all the clients, exactly **one is the foreground client**; all others are
background clients. The **foreground client is the one that receives
keyboard input** `[wserv 1906-1910]`.
- On the small-screen machines (HC, S3, S3a, Workabout) the windows of
background clients are **not visible at all**; only the foreground client's
windows are shown, in front `[wserv 1918-1921]`.
- On the **MC** (large screen) windows of several clients can be visible at
once, and a client can *attach* to another `[wserv 1330-1333]`.
- The server keeps clients in a **front-to-back task order**: position 0 is the
foreground; position 1 is the frontmost background task, and so on `[wserv
1968-1971]`.
- A client can move itself or another client between foreground and background
with `wClientPosition` `[wserv 1983-1984]`.
### 1.2 The shared screen and keyboard
Drawing goes into a **hierarchical system of overlapping windows**; all drawing
is clipped to visible areas, and the server issues **redraw events** telling a
client which areas need repainting `[plib 1318-1323]`. Keyboard input is routed
to the foreground client, except for keys the server processes itself (task
switch, pause) or keys a client has **captured** with `wCaptureKey` so that it
receives them whether foreground or not `[wserv 1911-1915, 6283-6293]`.
### 1.3 Connecting
A process must connect before using the server, via `wConnect` (or a wrapper
such as `wStartup`) `[wserv 1371-1372]`.
```c
VOID wConnect(WSERV_SPEC *pwserv_spec, VOID *pnws_handle, UINT flags);
```
`[wserv 7182-7183]`
- `flags` combines: `W_CONNECT_AT_BACK` (connect as background; default is
foreground), `W_CONNECT_USER_FLAG`, `W_CONNECT_SYSTEM_MODAL`,
`W_CONNECT_PRIORITY` (enable the server's process-priority handling),
`W_CONNECT_DISABLE_LEAVES` (return negative error numbers instead of calling
`p_leave`) `[wserv 7187-7203]`.
- `pnws_handle` is the handle the server puts into events **not** directed at a
window (e.g. key events) `[wserv 7206-7207]`.
- `pwserv_spec` points to a `WSERV_SPEC` the client must keep for the life of
the connection. `wConnect` fills its `CONNECT_INFO conn` sub-struct with
useful data — screen size (`pixels`), pixel geometry, `set_is_dark`,
`version_id` (machine type + server version), and the default
`system_font_handle` `[wserv 7210-7251]`.
**Connection tests / helpers.** After connecting, the reserved static
`wClientData` is non-zero (zero if not connected) `[wserv 1882-1894]`. The
`WSERV_SPEC` address is also recorded in the reserved static `wserv_channel`,
which general-purpose code can read for screen size etc. `[wserv 7294-7303]`.
**Two startup paths (see also §5 on the console):**
- **PLIB startup module:** call `wStartup`, which connects, creates and
initialises a backed-up window covering the whole screen, and creates a
permanent graphics context on it — ready to draw `[wserv 1496-1519]`. The
created window's ID is in the global `wMainWid` `[wserv 1609]`.
- **CLIB startup module:** the startup automatically opens the console device
`con:`, which on the HC/S3/S3a/Workabout **is itself a window-server
connection** — so the process is already a client. Connecting again with
`wConnect`/`wStartup` panics the process (panic 100) `[wserv 1387-1396,
1757]`. The console channel is in the static `winHandle` `[wserv 1399-1403]`.
### 1.4 Windows (briefly)
- Create with `wCreateWindow` (returns a window ID); the window is dormant and
invisible until **initialised** with `wInitialiseWindowTree`, which activates
the window and all descendants `[wserv 3141-3167]`.
- Destroy a tree with `wCloseWindowTree`; destroying a window more than once is
**not** treated as an error `[wserv 3185-3202]`.
- Windows can be **backed-up** by bitmaps: drawing is mirrored to a backup
bitmap and the server redraws automatically, so the client largely avoids
redraw handling `[wserv 1270-1273, plib 1334-1335]`.
- Redraw/mouse events are directed at a window by the **handle** the client
supplied at `wCreateWindow` (commonly the address of a client structure)
`[wserv 1944-1946]`.
---
## 2. The event system
For each client the server maintains a **queue of events** reporting user input
and other state changes: key presses, foreground/background changes, redraw
events, and (on pointer machines) mouse events `[wserv 1926-1937]`. Key and
mouse events are time-stamped in system ticks (1 tick = 1/32 s) for e.g.
double-click detection `[wserv 1939-1941, 14331-14333]`.
### 2.1 Fetching events
```c
VOID wGetEventWait (WS_EV *event); /* block until an event */
VOID wGetEvent (WS_EV *event); /* async, no wait */
VOID wGetEventSpecial(WS_EV *event, UINT flags); /* async + event filter */
VOID wGetEventUpdate (UINT flags); /* change the filter */
```
`[wserv 14302-14432]`
- **`wGetEventWait`** returns only when an event is available; on an empty queue
it waits indefinitely `[wserv 1949-1951]`.
- **`wGetEvent`** is the asynchronous version, for clients that also service
non-server sources (serial input, timers). It sets `event->type =
E_FILE_PENDING` immediately; when an event arrives the server fills `event`
and signals the client's I/O semaphore. **Only one may be outstanding** — a
second pending `wGetEvent` panics the process `[wserv 14363-14378]`.
- **`wGetEventSpecial`** (window server **version 4** only) is `wGetEvent` plus
a `flags` filter selecting which events to deliver. Only one call to
`wGetEvent`/`wGetEventSpecial` may be outstanding at a time `[wserv
14381-14393]`. Filter flags (OR-able) `[wserv 14396-14416]`:
- `WE_KEY` — key and task-key events
- `WE_REDRAW``WM_REDRAW`
- `WE_STATUS``WM_FOREGROUND`, `WM_BACKGROUND`, `WM_ON`
- `WE_MOUSE` — mouse and rubber-band events
- `WE_OTHERS` — everything else
- `WE_NORMAL` — all of the above (`wGetEventSpecial(..., WE_NORMAL)` ==
`wGetEvent`)
- `WE_ESC` — only meaningful when `WE_KEY` is *not* set: on ESC the keyboard
buffer is discarded and a `WM_ESCAPE` event is delivered.
- **`wGetEventUpdate`** (v4) replaces the filter of an outstanding
`wGetEvent`/`wGetEventSpecial`; a no-op if none is outstanding `[wserv
14419-14432]`.
> **Async discipline.** Fully process one server event before requesting the
> next. Because the server runs at higher priority than clients, an async
> request can complete while you are handling some *other* source. Never
> destroy a window directly in response to a non-server event — instead call
> `wCancelGetEvent`, which delivers a **`WM_CANCELLED`** at the **highest
> priority** (overtaking all queued but not-yet-delivered events), and do the
> destruction when that arrives `[wserv 3227-3252]`.
### 2.2 The event structure
```c
typedef struct {
WORD type; /* positive event type, WM_xxxx */
UWORD handle; /* target window, or the wConnect handle */
UWORD time; /* low word of tick count (key/mouse) */
WS_EVENT_UNION p; /* type-dependent payload */
} WS_EV;
typedef union {
UWORD uword;
UBYTE *dpoint;
P_RECT rect; /* WM_REDRAW rectangle */
WMSG_KEY key; /* WM_KEY */
WMSG_MOUSE mouse; /* WM_MOUSE / rubber band */
WMSG_RUBBER rubber;
WMSG_CAPS caps; /* WM_KEYBOARD_STATE_CHANGE */
} WS_EVENT_UNION;
```
`[wserv 14311-14350]`
- `handle`: for window-directed events (`WM_REDRAW`, `WM_MOUSE`) it is the
handle given to `wCreateWindow`; for non-window events (`WM_KEY`,
`WM_FOREGROUND`) it is the handle given to `wConnect` `[wserv 14326-14329]`.
The key payload:
```c
typedef struct {
UWORD keycode; /* code of the key pressed */
UBYTE modifiers; /* shift/ctrl/psion/caps/num-lock */
UBYTE count; /* auto-repeat accumulation */
} WMSG_KEY;
```
`[wserv 14456-14465]`
- `modifiers` bit flags: `W_SHIFT_MODIFIER` (0x02), `W_CTRL_MODIFIER` (0x04),
`W_PSION_MODIFIER` (0x08), `W_CAPS_MODIFIER` (0x10),
`W_NUM_LOCK_MODIFIER` (0x20, MC only) `[wserv 14481-14499]`.
- `count` is 1 for a single press; it exceeds 1 only when the client cannot
keep up with auto-repeat. The manual's advice is to **ignore the repeat
count** `[wserv 14466-14476, 6263-6280]`.
- `keycode`: printable SIBO characters (code page 850-like) fall in
`0x20``0xFF` excluding `0x7F`. The **PSION** shift typically adds `0x200`
(`W_SPECIAL_KEY`). "Special" keys carry codes `< 0x20`, `0x7F`, or `> 0xFF`
`[wserv 14501-14541]`. Named special codes include `W_KEY_TAB` (0x09),
`W_KEY_DELETE_LEFT` (0x08), `W_KEY_DELETE_RIGHT` (0x7F), `W_KEY_RETURN`
(0x0D), `W_KEY_ESCAPE` (0x1B), `W_KEY_UP/DOWN/RIGHT/LEFT` (0x1000x103),
`W_KEY_PAGE_UP/DOWN` (0x104/0x105), `W_KEY_HOME/END` (0x106/0x107),
`W_KEY_TASK` (0x108), `W_KEY_MENU` (0x122), `W_KEY_HELP` (0x123),
`W_KEY_ON` (0x2002), `W_KEY_OFF` (0x2003), etc. `[wserv 14547-14852]`.
### 2.3 Event types
Common event types (`WS_EV event;` assumed) `[wserv 14441-15048]`:
| Event | Meaning | Payload |
|-------|---------|---------|
| **`WM_KEY`** | A key was pressed. The full description is in `event.p.key` (keycode, modifiers, count). Delivered to the foreground client (or a capturer). `[wserv 14449-14476, 6249-6250]` | `p.key` |
| **`WM_REDRAW`** | Sent when the client's queue is empty and one or more windows has an update region. `event.p.rect` is a rectangular block of pixels needing redraw. Lowest priority except for `WM_USER_MSG`. `[wserv 14855-14865]` | `p.rect` |
| **`WM_BACKGROUND`** | The foreground client has just gone to background. Usually nothing to do, but suspend real-time/animated activity until foreground returns. Only `type` set. `[wserv 14868-14878]` | — |
| **`WM_FOREGROUND`** | A background client has just become foreground. Only `type` set. `[wserv 14881-14887]` | — |
| **`WM_CANCELLED`** | Sent in response to `wCancelGetEvent`; delivered at highest priority. Only `type` set. `[wserv 14890-14897, 3250-3252]` | — |
| **`WM_USER_MSG`** | Sent in response to `wUserMsg`. **Lowest priority of all** — useful as an "the server has nothing more to send" marker. Only `type` set. `[wserv 14909-14916]` | — |
| **`WM_ON`** | Machine switched on (server v3.5+). Delivered to the foreground client if it called `wInformOn`; in v4, to any client (fore or back) that called `wInformOnAll(TRUE)`. Prompts a display refresh. Only `type` set. `[wserv 14919-14937]` | — |
| **`WM_COMMAND`** | Another client sent a command via `wSendCommand` (server v3.5). Prompts the receiver to call `wGetCommand` for the data (up to 127 bytes). Only `type` set. `[wserv 14940-14947, 1128-1129]` | — |
| **`WM_TASK_KEY`** | Sent to the **application-key handler** (the shell) when an application key is pressed with no process of that application present, or a PSION-shifted application key is pressed. S3/S3a/Workabout only. The app-key index (015) is in `event.p.key.keycode`. `[wserv 14963-14975]` | `p.key.keycode` |
| **`WM_ESCAPE`** | (v4) Delivered when ESC is pressed *and* the client selected events with `wGetEventSpecial` including `WE_ESC`/`WM_ESCAPE` but **excluding** key events; the keyboard buffer is discarded. `[wserv 14999-15015]` | — |
| **`WM_TASK_UPDATE`** | Sent to the shell (when foreground) whenever *any* process terminates. S3/S3a/Workabout and HC v3.5+. Only `type` set. `[wserv 14950-14960]` | — |
| **`WM_DATE_CHANGED`** | (v4) Date changed (reset, or past midnight). Sent to a foreground S3a/Workabout app not in S3 compatibility mode; background apps get it on next foreground; delivered at next power-on if off. `[wserv 14978-14993]` | — |
| **`WM_KEYBOARD_STATE_CHANGE`** | Sent only to the shell when numlock/capslock changes; new state in `event.p.caps.modifiers`. (MC generates it for the caps-lock key.) `[wserv 15065-15069, 14734-14735]` | `p.caps` |
**Mouse / pointer events** (only on machines with a pointing device, e.g. the
MC) `[wserv 15076-15187]`:
| Event | Meaning |
|-------|---------|
| **`WM_MOUSE`** | State change on the digitiser. `event.p.mouse` is a `WMSG_MOUSE { UBYTE event; UBYTE state; P_POINT pos; }`. `event.p.mouse.event` is `WM_MOUSE_MOVE` (filtered out by default), `WM_MOUSE_PRESS`, or `WM_MOUSE_RELEASE`; `state` carries `W_MOUSE_DOWN`, `W_MOUSE_OUTSIDE`, and the shift modifiers. `[wserv 15083-15129]` |
| **`WM_RUBBER_BAND_INIT`** | Special `WM_MOUSE` variant sent when a press occurs in a window flagged `W_WIN_RUBBER_BAND_CAPTURE`. `[wserv 15146-15154]` |
| **`WM_RUBBER`** | Sent on completion of a rubber-band interaction. `[wserv 15160-15166]` |
| **`WM_ACTIVE`** | Sent to a window that set `W_WIN_INACTIVE` when a `WM_MOUSE_PRESS` lands on it or a descendant (in place of the `WM_MOUSE`); the client typically re-activates the tree by clearing `W_WIN_INACTIVE`. Only `type` set. `[wserv 15169-15186]` |
Large-screen (MC) events also include `WM_DEICONISE`, `WM_ATTACHED`,
`WM_DETACHED` for the multi-task attach/iconise model `[wserv 15017-15062]`.
### 2.4 Event priority
Event delivery is **prioritised**, not strictly FIFO:
- `WM_CANCELLED` is delivered at the **highest** priority, overtaking anything
queued but not yet delivered `[wserv 3250-3252]`.
- `WM_REDRAW` is **lower** than user input and foreground/background events;
the only type lower than `WM_REDRAW` is `WM_USER_MSG` `[wserv 14865,
3126-3127]`.
- `WM_USER_MSG` has the **lowest** priority of all `[wserv 14912-14913]`.
- Between windows there is a further **two-level redraw priority**: windows are
low by default; `W_WIN_PRIORITY` (on `wCreateWindow`/`wSetWindow`) promotes a
window's redraws ahead of the rest `[wserv 3130-3131]`.
### 2.5 The barcode scan key — **reverse-engineered (Workabout MX)**
> **Not in the manuals.** The *Window Server Reference* documents no barcode /
> scanner / laser trigger key, and defines no keycode near 368. On barcode-
> equipped Workabout / Workabout MX hardware the **scan (trigger) key is
> observed to arrive as an ordinary `WM_KEY` event with keycode 368**
> (`0x170`). This is **reverse-engineered field knowledge, not documented
> behaviour** — treat the exact value and delivery path as version/hardware
> dependent and confirm on the target unit. Because it surfaces as a normal
> key event, it can be handled in the ordinary `WM_KEY` path (and, like other
> keys, potentially captured with `wCaptureKey`).
---
## 3. Drawing and text output primitives
All drawing targets a **drawable** selected by the **current graphics context
(GC)** `[wserv 5504, 5526]`.
### 3.1 Graphics contexts
- Create a **permanent** GC on a window/bitmap with `gCreateGC` /
`gCreateGC0` (the latter with default values) `[wserv 304-305, 1438]`. Select
the current GC with `gSetGC` / `gSetGC0` `[wserv 311-312, 2993]`.
- **Temporary** GCs are used during redraws (see below) and are freed
automatically by `wEndRedraw` `[wserv 3037-3050]`.
- `wStartup` conveniently makes a permanent GC on its full-screen window
`[wserv 1502-1505]`.
### 3.2 Graphics primitives
- Lines / shapes / borders / fills: `gDrawLine`, `gBorderRect`, `gBorder`,
`gBorder2Rect`/`gBorder2` (shadowed, v4), `gFillPattern`, `gInvObloid`
`[wserv 315-326, 1296, 919-957]`.
- Bitmaps: `gCopyBit` (copy a bitmap to a window) and the bitmap-file family
(`gInitBit`, `gGetBit`, `gLoadBit`, `gPeekBit`, `gSaveBit`) `[wserv 354,
829-1021]`.
- The server also draws buttons (`wDrawButton`, `wDrawButton2`), sprites
(`wCreateSprite`/`wSetSprite`) and scaled objects (`gDrawObject`) `[wserv
926-969, 1168, 1203]`.
### 3.3 Text output
Text is drawn through the current GC (font ID, style, transfer mode) `[wserv
5526-5536]`:
- `gPrintText` — draw text from a pixel position `[wserv 5508, 1439]`
- `gPrintClipText` — clipped to a given width `[wserv 5510]`
- `gPrintBoxText` — left/right/centred in a box; commonly used for
**flicker-free** redraw `[wserv 5512-5513, 2961]`
- `gXPrintText` — with embellishment `[wserv 5515]`
- `gShadowText` — shadowed (v4) `[wserv 5517]`
Font styles combine `G_STY_NORMAL`, `G_STY_BOLD`, `G_STY_UNDERLINE`,
`G_STY_INVERSE`, `G_STY_DOUBLE`, `G_STY_MONO`, `G_STY_ITALIC` `[wserv
5539-5557]`. Layout helpers: `gTextWidth`, `gTextCount`, `wGetWidthTable`
`[wserv 5492-5497]`.
### 3.4 Redraw handling
On a `WM_REDRAW`, the normal response is to **validate** the update region and
draw it `[wserv 10248-10256]`. Bracket the drawing with a `wBeginRedraw`
variant and `wEndRedraw`; `wBeginRedraw` both signals "this is a redraw" and
**validates** the given rectangle `[wserv 3005-3007]`. There are six
`wBeginRedraw*` variants covering partial vs whole window and
independent/temporary GC combinations `[wserv 3020-3050]`. Related: validate
without redrawing (`wValidateRect`, `wValidateWin`) and force redraws by
invalidating (`wInvalidateRect`, `wInvalidateWin`) `[wserv 270-274, 2901]`.
Backed-up windows largely avoid this cycle because the server repaints from the
backup bitmap `[wserv 1270-1273]`.
Minimal MC (non-backed-up) redraw loop `[wserv 1589-1608]`:
```c
wStartup();
do {
wGetEventWait(&event);
if (event.type == WM_REDRAW) {
wBeginRedrawWin(wMainWid);
gPrintText(10, 20, "Hello world!", 12);
wEndRedraw();
}
} while (event.type != WM_KEY);
```
---
## 4. Error handling (server calls)
By default a failing server function calls `p_leave` with the negative error
number; `wDisableLeaves(TRUE)` (or `W_CONNECT_DISABLE_LEAVES` at connect time)
makes it **return** the error instead `[wserv 1629-1641]`. Many drawing calls
are **blind** (queued client-side, no acknowledgement); on failure the server
discards further blind ops until a non-blind call reports the error. Force the
issue with `wCheckPoint` (flush + signal) or `wFlush` (flush only); after an
error, `wCleanUp` releases dangling resources `[wserv 1653-1703]`. Client-side
buffering means a panic can lag the offending call — insert `wFlush`/
`wCheckPoint` when hunting one down `[wserv 1791-1793]`.
---
## 5. The console layer — `CON:` and the PLIB console functions
The console is the **lightweight text-UI path**. PLIB contains only *primitive*
console functions — typed-line input (with simple backspace editing) and
mono-spaced line output — while the `con:` device driver adds row/column
positioning and mono-spaced character printing `[plib 1299-1301]`. They are
explicitly "for quick-and-dirty applications... not suitable for constructing
quality user interfaces" `[plib 8580-8581]`.
### 5.1 Relationship to the window server
Both the PLIB console functions and `con:` **ultimately call the window
server** for input and drawing — but expose only a fraction of its capability
`[plib 1310-1315]`. How `con:` is implemented differs by machine `[wserv
1319-1323]`:
- **HC / S3 / S3a / Workabout:** `con:` is implemented by **connecting
directly to the window server** — opening `con:` makes the process a window-
server client. Opening it also creates and initialises a **backed-up**
console window (no redraw handling needed) `[wserv 1319-1320, 1387-1407]`.
The console window's ID can be fetched with the `P_FINQ` I/O function
(`CONSOLE_INFO.window_handle`), letting a CLIB program create its own GC on
that window and draw with WLIB `[wserv 1410-1453]`.
- **MC:** `con:` is implemented by a **separate `SYS$CONS` display process**
that keeps a character map and redraws its own window (with title/menu bars).
Here it is `SYS$CONS`, not the application, that is the window-server client;
the application cannot use the console window, and `P_FINQ` is **not**
supported. On the MC the recommendation is the **PLIB startup module +
`wStartup`** `[wserv 1305-1322, 1539-1615]`.
The two startup modules therefore differ: the **CLIB** startup auto-opens
`con:` (backing `printf`, `gets`, `cprintf`, `cgets`) `[wserv 1387-1389]`; the
**PLIB** startup does not, and you call `wStartup` yourself `[wserv 1496-1497]`.
Auto-opening can be suppressed in CLIB by defining `p_xwind` `[wserv
1457-1490]`.
### 5.2 The PLIB console functions
Output (open `con:` automatically on first use) `[plib 8584-8595, 8700-8737]`:
```c
VOID p_putch(UINT c); /* write one character */
VOID p_puts (TEXT *str); /* write string + newline */
VOID p_printf(TEXT *fstr, ...); /* formatted line + newline */
VOID p_print (TEXT *fstr, ...); /* formatted, no trailing newline */
```
- `p_printf` uses an internal buffer of `P_MAXSYSIO` (258) bytes; output is
limited to `P_MAXSYSIO - 2` (256) bytes per call. Format is that of `p_atob`
`[plib 8720-8726]`.
- `p_print` moves no line; embed `\r` (start of line) and `\n` (down a line)
yourself `[plib 8732-8737]`.
Input `[plib 8597-8599, 8740-8770]`:
```c
INT p_getch(VOID); /* wait for a key, return its code */
INT p_gets (TEXT *str); /* read a line (backspace edit) */
INT p_getl (TEXT *pmt, TEXT *str, INT len); /* prompt, then read a line */
```
- `p_getch` waits for a key and returns its character code (no echo) `[plib
8740-8743]`.
- `p_gets` reads up to `P_MAXSYSIO-1` characters, Enter-terminated, and NUL-
terminates at `str` (needs `P_MAXSYSIO` bytes there); returns the length
`[plib 8746-8756]`.
- `p_getl` writes the prompt `pmt`, then reads up to `len` characters
(Enter-terminated, needs `len+1` bytes); returns the length `[plib
8759-8770]`.
Canonical smallest program `[plib 1020-1021]`:
```c
p_printf("Hello world");
p_getch();
```
### 5.3 Console configuration statics
Set these **before the first console call** `[plib 8612-8694]`:
- `winHandle` (`GLREF_D VOID *`) holds the open console channel; open a file or
`TTY:` into it first to **redirect** `p_putch` / `p_print` / `p_printf`. Do
**not** use `p_getch` / `p_gets` / `p_getl` when redirected `[plib
8615-8628]`.
- `_DefScreenRect` (`P_RECT`, top-left `(0,0)`, bottom-right = columns × rows)
changes the console window size `[plib 8629-8665]`.
- `_DefScreenMode` (`INT`) selects the mode: `0` native (default, currently
single-pixel non-compatibility on all SIBO machines), `1` compatibility, `2`
non-compatibility with grey, `3` compatibility with grey. Irrelevant values
are ignored `[plib 8668-8694]`.
---
## 6. Higher UI libraries (pointer only)
For full applications — menus, dialogs, forms — SIBO provides **higher-level UI
libraries layered on the window server** (commonly referred to as **HWIM**,
**FORM** and **OLIB**). **These are separate libraries and are not documented
in the *Window Server Reference* or *PLIB Reference*** covered here; the two
manuals mention menus and dialog boxes only as *windows the server draws/clips*
(e.g. `[wserv 2395, 10646-10647]`), not as an API. Use those libraries' own
references for real application UIs; the material above is the **foundation**
they sit on. *(Library names are noted as a pointer only and are not sourced
from these two manuals.)*
---
### Source key
- `[wserv N]` — *Window Server Reference* v2.30, line N of the supplied text.
- `[plib N]` — *PLIB Reference* (console functions), line N of the supplied
text.
- Items marked **reverse-engineered** / **uncertain** are **not** in either
manual (notably the code-368 barcode scan key and the higher UI library
names).
+581
View File
@@ -0,0 +1,581 @@
# Psion SIBO / Workabout — Hardware Reference for Programmers
Scope and sources. This reference is compiled **only** from two Psion manuals:
- **HDK** — *The Psion SIBO Hardware Development Kit*, Psion PLC, Rev 1.00, May 1995.
- **WPG** — *Workabout Programming Guide* (Workabout Programmers Reference), hardware
appendices / introduction.
Citations are given as `[HDK p.N]` or `[WPG §/p]`. Where a fact is asserted by the manuals it is
cited; nothing here is invented. The manuals themselves note that both refer onward to a separate
*Hardware Reference manual* and *SIBO Computers Programmers Reference* for the full physical
memory map — those documents are **not** in scope, so the memory-map section below is limited to
what the two supplied manuals actually state, and gaps are marked explicitly.
> **MX note.** The two supplied manuals describe the **V30H**-based Workabout (NEC V30H at
> 7.68 MHz) and the ASIC1/ASIC2/ASIC4/ASIC5/ASIC9 family. **They do not mention the V30MX or
> ASIC9MX.** Every statement about the **V30MX / ASIC9MX / Workabout MX** in this document is
> therefore flagged as **[MX — not in supplied manuals]** and is presented only as an orientation
> note, not as a sourced fact. Treat all MX specifics as *uncertain / to be confirmed against the
> MX hardware reference.*
---
## 1. SIBO architecture overview
All Psion machines in this family are built on the proprietary **SIBO** ("SIxteen Bit Organiser")
architecture: a battery-powered, 8086-class computer system designed for size, weight and power
[HDK p.2; WPG §basic hardware].
Key components of the architecture [HDK p.2; WPG p.1-x]:
- An **8086-class processor**.
- A **power-management system** that selectively powers subsystems under software control.
- An **asynchronous high-speed serial protocol** (the *Psion SIBO serial interface*) between the
machine and its peripherals.
- **Solid State Disks (SSDs)** — fast, low-power, silicon mass storage, no moving parts.
- **Hardware protection** from aberrant software: trapping of out-of-range addressing, plus a
**watch-dog timer** that fires if interrupts are left disabled too long.
- **Real-time clock**, **ROM-resident system software**, **graphics LCD**.
- On some models: touch digitiser pad; ISDN-8-bit combo sound.
The digital logic of a SIBO machine is implemented in custom **ASICs** (Application Specific
Integrated Circuits). At the time of the HDK there were **ten** SIBO ASICs; all are static-CMOS
surface-mount parts [HDK p.2].
### 1.1 Processor (NEC V-series)
- The MC/HC/Series 3 and the Workabout use the **NEC V30H**, "an enhanced 16-bit CMOS version of
the 8088 found in the original IBM PC … software compatible with the 8088" [HDK p.2].
- **Workabout:** NEC V30H, clocked at **7.68 MHz** [WPG p.1-x, Tech Spec: "NEC V30 running at
7.68 MHz"].
- The V30H is a **fully static** design: all internal storage (registers) is static, so there is
**no minimum clock speed** and the clock can be stopped at any instant with **no loss of state**.
SIBO exploits this to stop the clock while the CPU is idle, saving power [HDK p.2].
> **[MX — not in supplied manuals]** Later Workabout variants are marketed as the **Workabout MX**,
> based on the **NEC V30MX** core integrated in the **ASIC9MX**. Neither the V30MX nor ASIC9MX
> appears in the HDK or WPG supplied here; their clock rate, memory reach and register differences
> are **not documented in these sources** and must be confirmed elsewhere. Software written to the
> V30H/8086 programming model and the ASIC register interfaces below is the documented baseline.
### 1.2 Chip integration levels
- **Discrete generation** (MC, HC, Series 3): three principal chips — **V30H + ASIC1 + ASIC2**
[HDK p.2].
- **Integrated generation** (Series 3a, Workabout): V30H, ASIC1 and ASIC2 collapsed into a single
custom chip, **ASIC9** (plus general I/O and PSU-control logic). ASIC9 "integrates all the
digital logic required to produce a SIBO architecture computer less the memory onto one chip",
and adds a free-running clock (FRC) and a codec interface for sound [HDK p.4].
So on the Workabout, the functions attributed below to "ASIC1" (system/interrupt/LCD) and "ASIC2"
(peripheral/serial-protocol/power) are physically inside **ASIC9** [HDK p.4].
> **[MX — not in supplied manuals]** On the MX, the equivalent single-chip part is **ASIC9MX** with
> the **V30MX** core. Same architectural role as ASIC9 is assumed; specifics unconfirmed here.
---
## 2. The ASICs and their roles
| ASIC | Role (from HDK p.4) |
|------|---------------------|
| **ASIC1** | Main **system controller**. Connects directly to the V30H, controlling all CPU bus cycles (forming a micro-controller-like unit executing 8086 code). Blocks: bus controller, programmable timer, **eight-input interrupt controller**, **LCD controller**, memory-decode circuitry. |
| **ASIC2** | **Peripheral controller**. Contains the system clock oscillator; controls standby↔operating switching; interfaces to PSU, keyboard, buzzer and SSDs; contains the **eight-channel SIBO serial-protocol controller**; drives the reduced-external and extended-internal expansion ports. |
| **ASIC4** | Serial-protocol **slave** for addressing memory / memory-mapped peripherals. Used in SSDs. A cut-down ASIC5. See §5. |
| **ASIC5** | General-purpose I/O **slave** with an on-board **UART**; several modes (RS232 / parallel / barcode / card-reader / memory pack). See §6. |
| **ASIC9** | Composite: **V30H + ASIC1 + ASIC2** + general I/O + PSU control on one die; adds FRC and codec interface. Used in S3a and Workabout. |
On the **host** side of the SIBO serial link the controller (SPC) is in **ASIC2** (HC/MC/S3) or
**ASIC9** (S3a/Workabout). On the **peripheral** side the slave (SPS) is **ASIC4** or **ASIC5**
[HDK p.4, p.6].
> **[MX — not in supplied manuals]** **ASIC9MX** — the composite CPU/LCD/system chip on the MX — is
> not described in these sources. Assume it plays the ASIC9 role (V30MX core + ASIC1/ASIC2
> functions). No register-level MX detail is available here.
---
## 3. Memory: ROM / RAM / SSD and segments
What the supplied manuals state:
- **ROM:** all Workabout models have **1 MB internal masked ROM** holding the OS (EPOC), the
MS-DOS-like Command Processor, OPL editor and other utilities [WPG p.1-9, Tech Spec].
- **RAM:** Workabout ships with either **256 KB** or **1 MB** internal RAM (Tech Spec lists memory
build codes A=256 KB, B=512 KB, C=1 MB, D=2 MB) [WPG p.1-x, Tech Spec, serial-number table].
- **SSD:** two SSD drives, exposed as **drive A:** (top) and **drive B:** (bottom); RAM or Flash
SSDs, "up to 8 MB" of extra storage [WPG p.1-3, Tech Spec]. SSDs retain data independently of the
main power source [WPG p.1-4]. Contents of the internal RAM are preserved across power-off (the
machine resumes previous state on power-on) [WPG p.1-x].
- **Internal drive M:** internal RAM is also exposed as a filing volume **M:** (with
`M:RAMDRIVE`), formattable like an SSD volume [WPG §FORMAT, §MEM].
### 3.1 The programmer's memory model (segments, protection)
- The CPU is 8086-class, so software sees the standard **16-bit segmented** address model
(segment:offset, 16-byte "paragraphs") [HDK, WPG passim]. `LSEG` reports each segment's **segment
address**, **size in paragraphs** (1 paragraph = 16 bytes) and **access count** [WPG §LSEG].
- **Memory protection:** any attempt by an application to write **outside its own data segment**
causes the OS to terminate it (a **"panic"**); likewise leaving interrupts disabled too long
(watch-dog) [WPG §Resetting; HDK p.2].
- **`MEM`** reports free RAM in KB; this exceeds free bytes on `M:` because "some parts of internal
memory are reserved for code and data segments" [WPG §MEM].
- The OS **moves memory segments** at runtime (e.g. when a driver is installed/removed). Drivers
must cope: FAR return addresses into the OS stay valid, but a driver's own absolute segment
address may change, so ISR/PDD addresses must be re-established on **resume** (see §7.4)
[HDK p.4042].
### 3.2 Banking / paging — status in these sources
The physical **memory map and paging/banking scheme of the host CPU are not laid out in the
supplied manuals**; both defer to the separate *SIBO Computers Programmers Reference* / *Hardware
Reference* [WPG p.1-3: "See the SIBO Computers Programmers Reference manual for further details
regarding SSDs"; HDK p.2]. What the HDK *does* document is the **address decode inside ASIC4/ASIC5
slaves**, i.e. how a peripheral or SSD's own address space is banked via **chip-select blocks**:
- **ASIC4:** 8 chip-selects forming selectable **addressing blocks**, size software-defined,
**default 32 KB/block**; the filing system re-sizes the block when reaching the top of the
address space [HDK p.24]. Up to **28 address bits (256 MB)** in Extended mode; **21 bits + 4
chip-selects (4 × 2 MB)** in SSD/compatibility mode [HDK p.25]. See §5.
- **ASIC5 (pack mode):** address lines A0A20 generated across ports B/D/C, with **CS0CS3**
selected by SEL0/SEL1; **counter mode** on port B auto-increments the low address so 256
consecutive locations can be read without re-addressing [HDK p.30, p.57].
> **Gap flagged:** the host-side ROM/RAM/SSD address ranges, and any V30-level bank registers, are
> **not in these two manuals**. Do not infer them from the peripheral-side decode above.
---
## 4. SIBO serial protocol (host ↔ peripheral bus)
Everything a program does to an ASIC4/ASIC5 peripheral goes over the **two-wire synchronous SIBO
serial link** [HDK p.6]:
- **CLK** — clock, always **output from the controller** (ASIC2/ASIC9). Nominal **3.84 MHz** for
memory interfaces; **1.536 MHz continuous** for peripherals. Tri-stated low when idle
[HDK p.6].
- **DATA** — bi-directional, synchronous. Direction is set by the transport layer, not the
physical layer. Changed on falling CLK edge by the transmitter, latched on rising edge by the
receiver; pulled low when idle [HDK p.6].
**Framing (physical layer).** 12-bit frames, 8 data bits each → theoretical max ≈ **312 KB/s**
[HDK p.6]. Frame = `ST` (start, goes high) · `CTL` (0 = control frame, 1 = data frame) · `I1`
(idle/turn-around) · `D0D7` · `I2` (idle) [HDK p.7]. Four frame types: **Null** (sync all slaves —
12 clocks with DATA held low), **Control**, **Data-out** (controller→slave), **Data-in**
(slave→controller; controller drives cycles 12 then tri-states, slave drives D0D7) [HDK p.7].
**Transport layer.** The controller has two registers: a write-only **Control register** and a
read/write **Data register** (byte or word) [HDK p.8]. Control bytes are classified by **bit 7,
the Select (S) bit**:
- **S = 0 — Slave-select mode.** Byte = `R` (reset bit) + **6-bit slave ID**. ID 0 is illegal, so
up to **63 slaves** per controller. `R=0` resets, `R=1` selects. Selecting a slave with `R=1`
makes it return a **non-zero 8-bit info byte**; a reply of **0 means no slave of that ID present**
[HDK p.89].
- **S = 1 — Slave-control mode.** Byte = `R/W` (0=write,1=read) · `B/W` (0=byte,1=word) · `S/M`
(0=single,1=multi) · 4 slave-specific data bits. Byte-pair (word) transfers are **listed as not
implemented** [HDK p.9].
**Timing rules (clock cycles, ≈260 ns each at 3.84 MHz)** [HDK p.1011]:
control-byte process = 12 cycles; byte transfer = 12 cycles; byte-pair = 24 cycles. After writing
the control register, wait ≥12 cycles before touching the data register or writing another control
word.
Assembler helper macros the HDK uses for all of this [HDK p.52]:
| Macro | Action |
|-------|--------|
| `SCONTOUT` | Output control byte in AL |
| `SDATAOUT` | Output data byte in AL |
| `SDATAIN` | Input a data byte into AL |
| `SBUSY` | Wait while the link is busy |
| `XNOP` | Short wait |
| `HwNullFrame` | Emit a null frame to sync slaves |
Serial-protocol control defines (`SerialSelect`, `SerialReadSingle`, `SerialWriteSingle`, slave
IDs) live in `ospack.inc` / `ossibo.inc` [HDK p.59].
---
## 5. ASIC4 — memory / peripheral slave
Purpose: convert SIBO serial frames into the address/data/control signals needed to drive memory
and memory-mapped peripherals; used in SSDs and in ASIC4-based expansion boards [HDK p.24].
**Bus:** 8-bit data, **28-bit address**, **8 chip-selects** (CS0CS7) [HDK p.24].
### 5.1 Modes (selected by slave ID)
Select ASIC4 with the appropriate ID, then read the info byte [HDK p.25]:
- **SSD / ASIC5-compatibility mode — ID 2.** Mimics an ASIC5 in pack mode; compatible with all
existing SSD software. Max **21 address bits + 4 CS (4 × 2 MB)**.
- **ASIC4 Extended mode — ID 6.** Up to **28 address bits (256 MB)**. On reset, four extra config
bits come from A27A24; **A27 = M** selects standard-SSD (M=0) or **mixed mode (M=1)**
memory + peripherals. Mixed mode is the one of interest for peripheral developers.
**Mixed mode split:** address space halves — lower half = memory-mapped peripherals (any use),
upper half = pure memory (typically a control ROM the filing system can mount). Chip-selects split:
**CS0CS3 = peripheral blocks, CS4CS7 = memory devices 14** [HDK p.2526].
**Reset config** is read from the data bus (**Info Byte**, D0D7) and A24A27 (extended nibble),
set with pull-up/pull-down resistors (~100 k so the bus can still drive the lines in normal
operation) [HDK p.26].
### 5.2 ASIC4 registers (mixed/peripheral mode) [HDK p.5355]
| Reg | Name | R/W | Notes |
|-----|------|-----|-------|
| **0** | **Data Register** | R/W | Drives/reads D0D7 (tri-state when idle). **On reset holds the Info Byte** (see below). |
| **1** | **Input Register** (read) / **Device-Size Register** (write) | R/W | Read: extended-info bits + live state of general inputs **In0In2** and X2. Write: bits S3S0 set the address-decoder block size (32 KB … 256 MB); defaults to `0x0F` (unused) on reset. |
| **2** | **Address-Increment Register** | W | A write increments address lines A0A3. |
| **3** | **Address Register** | W (multi-byte, LSB first) | Loads all 28 address lines. Bytes: **ATO = A0A7, AT1 = A8A15, AT2 = A16A23, AT3 = A24A27**. Writing the first byte resets A8A27 to 0. AT3 bits 46 can steer CS0CS3. Cleared on reset. |
| **4,5,6** | — | — | **Not implemented.** |
| **7** | **Control Register** | W | Bits: `7 LBO · 6 TSTA · 5 LTM · 4 VPS · 3 EDA · 2 CSS · 1 WRS · 0 OES`. Setting **LBO** / **VPS** drives those pins high (usable as GP outputs). **OES/WRS** control read/write accesses. |
**Info Byte** (Reg 0 at reset) [HDK p.53]: bits encode **device type** (RAM / Intel Flash type 1 /
type 2 / read-only SSD / hardware write-protected SSD), **number of devices** (14), and **device
size** (000 = no SSD present, then 32 KB → 2 MB). In Extended mode a further **Extended Info Byte**
(M, De, Ne, Se) identifies the peripheral type — e.g. `1 0 0 1` = Turbo RS232 (16550), `1 0 1 0` =
3Fax, `1 1 1 1` = extended info held in ROM [HDK p.54].
**Access sequence** (bottom 256 addresses) [HDK p.5859]: control-write to the **Address** register
→ data-write the address → control-frame read/write to the **Data** register → transfer the byte.
The HDK gives `Input`/`Output` assembler routines doing exactly this; **interrupts must be off**
during them so the host does not multitask mid-transfer.
**Other pins** [HDK p.27, p.55]: `In0In2` GP inputs; `LBO` (open-drain low-battery driver), `VPS`
(VPP control) GP outputs; `OE`/`WR` bus control; `SCLK`/`SDAT` the serial link; `SDIR` protocol
direction bit; `POR` reset; `ATST` test (pull high = address test mode); `MCSD`/`X2D2` tie to GND
via 100 k. A PSRAM mode reuses some pins for refresh/oscillator.
---
## 6. ASIC5 — UART / parallel / barcode / card-reader slave
Three primary functions: a **UART** (up to **48000 baud**), **general-purpose I/O**, and
**address/data lines** for memory / memory-mapped peripherals [HDK p.29].
### 6.1 Modes
- **Pack mode:** generates full address/data/control to access memory; **no peripheral functions**.
- **Peripheral mode:** limited memory addressing (lines reused for I/O). Entered by setting the
**peripheral bit (bit 0) in the Port-B mode register** [HDK p.29].
**Port re-use** [HDK p.30]:
- **PA0PA7** — 8-bit non-latched GP I/O; in peripheral mode **PA0PA3 = UART RX, CTS, DSR, DCD**;
**PA4** is a change-of-state interrupt source (e.g. Centronics **BUSY**, or **RI** on the VIC —
see below). In pack mode PA0PA7 is the memory data bus.
- **PB0PB7** — latched output / **counter** mode; in pack mode = address **A0A7**.
- **PD0PD7** — GP outputs; in pack mode = address **A8A15**; in peripheral mode **PD0/PD1/PD2 =
UART TX, RTS, DTR**.
- **PC0PC4** — in pack mode = address **A16A20**; in peripheral mode **PC4/PC7** = inverted
edge-triggered interrupt inputs, **PC5** = interrupt output, **PC6** = GP latched output +
pack/peripheral select at reset, **PC0PC3** = a **dual synchronous serial port** for magnetic
card readers.
**UART details** [HDK p.2930, p.61]: needs the host link in **continuous-clocking** mode (baud
clocks are derived from the 1.536 MHz link clock). No internal buffering — the **Transmitter
Holding Register must be empty** before writing. One shared **active-high interrupt line**; software
reads the status/interrupt registers to find the cause. Reading the UART Status register clears the
Tx interrupt.
**Barcode:** the UART receives data from a dedicated barcode-scanner IC [HDK p.30].
**Card readers:** two synchronous serial ports (PC0PC3) take clocked serial data [HDK p.30].
**Reset config** read from **PA0PA7** (device type/size, like ASIC4); **PC6** selects pack vs
peripheral; in peripheral mode PA0PA7 indicate the peripheral type (RS232 port / Centronics port,
combinable) [HDK p.31].
### 6.2 ASIC5 register set — the sixteen registers
Reading/writing a register is two steps: send a control byte selecting the register, then
read/write the data [HDK p.56]. Register list [HDK p.5657, cross-checked against the
`SYS$AS5.ASM` defines at HDK p.102]:
| Reg | Name | R/W | Function |
|-----|------|-----|----------|
| **0** | **Port A data** | R/W | Read/write Port A; a read/write triggers a memory access cycle (pack mode) or one-cycle CS0 strobe (peripheral mode). |
| **~2** | **Port B data** | R/W | Latches PB0PB7 (= A0A7 in pack mode); read returns last value. |
| **~4** | **Port B mode / Inc** | R/W | **bit 0 = 0 memory / 1 peripheral (enables UART)**; also selects **counter** vs **latch** mode and "baud rate out on Port B". Counter increments on Port A access. |
| **~5** | **Port C/D write** | W | First write latches PD0PD7; subsequent (multiwrite) writes latch Port C, incl. **SEL0/SEL1** choosing CS0CS3. In peripheral mode PD0 = UART TX (bit 0 has no effect). |
| **6** | **Interrupt Mask** | R/W | 1 = enable that event's interrupt; 0 = disable. All disabled on reset. |
| **~7** | **Interrupt Status / Type-Ctrl** | R | Reading indicates interrupt source; bit meanings mirror the mask. |
| **8** | **UART Status / UART Control** | R/W | Format (stop/data bits, parity), break, and status (Tx empty, Rx/parity/framing/overrun errors). |
| **9** | **UART Receive / Transmit holding register** | R/W | Read = received char; write = char to transmit. |
| **10** | **UART Baud rate LSB** | W | Low byte of baud divisor. |
| **11** | **UART Baud rate MSB** | W | High byte of baud divisor. |
| **12** | **MCR shift register** | — | Magnetic-card-reader shift register. |
| **13** | **Barcode read data** | R | Returns states of the barcode lines / general interrupt bits. |
| **14** | **Synchronous Port 2 read** | R | Second synchronous serial port (card reader). |
| **(15)** | (sync port 1 / reserved) | — | Synchronous-port-1 data; documented in prose (sync port 1 & 2 char-received are distinct interrupt sources). |
> The HDK's printed register table is OCR-garbled for some indices; the register **names, R/W
> attributes and functions** above are taken verbatim from the HDK prose (p.5657) and the
> assembler comment block in `SYS$AS5.ASM` (p.102): *Port A R/W · Port B R/W · Inc/Mode · Port CD
> write-only · Interrupt mask R/W · IntType/Ctrl · UART Status/Ctrl · Receive/Transmit · Baud Rate
> (×2, write-only) · MCR shift register · Barcode data & ints.* Exact numeric slots for the
> mid-range registers should be confirmed against the include files (`ospack.inc`, `ossibo.inc`).
**Interrupt-mask bits** [HDK p.56, and `S_*` defines p.102]: UART character received; UART
transmitter awaiting character; modem/handshake change of state; **synchronous port 1 char
received**; **synchronous port 2 char received**; **barcode data / general interrupt**. Reading the
**Interrupt Status** register (same bit layout, high = active) identifies the source [HDK p.56].
**UART Status/Control bits** (`SYS$AS5.ASM`, HDK p.102):
| Define | Value | Meaning |
|--------|-------|---------|
| `S_RXENB` | `0000_0001b` | Receive interrupt enable |
| `S_TXENB` | `0000_0010b` | Transmit interrupt enable |
| `S_TXEMPTY` | `0001_0000b` | Transmit buffer empty (status) |
| `S_RXINT` | `0000_0001b` | Receive interrupt pending |
| `S_TXINT` | `0000_0010b` | Transmit interrupt pending |
| `S_MDINT` | `0000_0100b` | Modem-status interrupt |
| `S_CTS`/`S_RTS`/`S_DCD`/`S_DSR`/`S_DTR` | `01b`/`02b`/`04b`/`02b`/`04b` | Handshake-line states |
| `OVERRUN_ERROR` | `0100_0000b` | Character overrun |
| `PARITY_ERROR` | `1000_0000b` | Parity error |
| `S_PERIPHERALMODE` | `0000_0011b` | ASIC5 RS232 (peripheral) mode |
| `S_UART_OFF` | `0000_0010b` | ASIC5 peripheral mode, UART off |
**Baud rate.** `Divisor = 1 (96000 / desired_baud)`, written as a 16-bit word: LSB → reg 10,
MSB → reg 11 [HDK p.61]. The driver's `BaudRateTable` (HDK p.102) holds the two's-complement
divisors, e.g. `-0x077F, -0x04FF, -0x0368, -0x02CC, -0x027F, -0x013F, -0x009F, -0x004F, -0x0035,
-0x0030, -0x0027, -0x0019, -0x0013, -0x000C, -0x0009, -0x0004` for the standard rates.
### 6.3 Selecting / configuring ASIC5
Before use, select the chip (and re-select after every hold/resume from power-down, pack-door, or
insert/remove) [HDK p.56]. Selection differs by mode:
- Peripheral: `mov al,(SerialSelect or Asic5NormalId)``SCONTOUT``SDATAIN`; a **zero reply
means no ASIC5 present** [HDK p.56].
- Pack: same with `Asic5PackId` [HDK p.56]. An **ASIC4 will answer a pack-mode select** (it
impersonates an ASIC5 in pack mode) [HDK p.56].
---
## 7. I/O ports, interrupts and the channel model
### 7.1 Eight hardware interrupts
SIBO supports **8 level-triggered hardware interrupts**, IRQ0 (highest) … IRQ7 (lowest), handled by
the interrupt controller in **ASIC1 (or ASIC9)**. Expansion ports carry an interrupt line into this
controller [HDK p.3]:
- **Extended internal** expansion ports: **active-low** interrupt input.
- **Reduced external** expansion ports (S3a 6-pin, Workabout LIF): **active-high** interrupt input.
Servicing order [HDK p.3]:
1. Device asserts its IRQ line.
2. The controller (ASIC2/ASIC9) places the **vector of the highest-priority pending device** on the
data bus; the CPU jumps to that ISR.
3. The ISR clears the interrupt at the device (device-specific action).
4. The ISR writes the **non-specific end-of-interrupt (NSEOI/NSEOD)** location to tell the
controller the interrupt is cleared.
5. Repeat if another is pending.
**Nested interrupts are not possible** on these 8086-class processors [HDK p.3].
### 7.2 The four channel variables
To be portable across host machines, a driver parameterises four values per expansion channel
[HDK p.52]:
- **Channel interrupt mask** — an 8-bit value OR'd into the mask register (`A1InterruptMask` or
`A9BInterruptMask`, depending on whether the controller is ASIC1 or ASIC9) to enable interrupts on
that channel; also passed to `HwGetChannel` / `HwFreeChannel`.
- **Channel interrupt number** — 16-bit, used with `GenSetRevector` / `GenResetRevector` to say
which default ISR the driver replaces.
- **Channel interrupt vector** — 16-bit pointer to the driver's ISR.
- **Hardware SIBO channel** — used by `HwSelectChannel` to route serial frames to that channel.
### 7.3 Per-platform channel map [HDK p.52]
| Host | Controller | Ports and channels |
|------|-----------|--------------------|
| **Series 3** | ASIC2 | Expansion port C = serial **channel 7**, interrupt `Asic2Int` (IRQ4), select `SelectChannel7`. |
| **Series 3a** | ASIC9 | Expansion port C = serial **channel 5**, mask `A9MSlave`, revector `IHwIrq2Revector` (IRQ2), select `SelectChannel5`. |
| **Workabout / HC (3-channel)** | ASIC9 / ASIC2 | Port **A** (top) and port **B** (bottom) internal + port **C** (side/cradle); masks `ExpIntLeftA` / `A9MExpIntA`, `ExpIntRightB` / `A9MExpIntB`, plus the slave/`Asic2Int` channel; selects `ExpChannelLeftA`, `ExpChannelRightB`, `SelectChannel5/7`. |
Build flags select the machine so one driver source compiles for all: **Consumer (S3a)** = 1 SIBO
channel; **HC/S3C** = 3 channels; S3 = single-channel [HDK p.5152; `SYS$AS5.ASM` p.102].
### 7.4 Owning and selecting a channel
The S3/S3a/HC have **three** Psion serial links: two are the SSD slots, the third is the expansion
port [HDK p.60]. Only **one link is selected at a time**.
- **`HwGetChannel`** (AL = channel interrupt mask): reserve a channel; **carry clear = success**.
Normally called from the driver's **open** vector; open should fail if the channel is unavailable
[HDK p.60].
- **`HwFreeChannel`** (AL = mask): release it (on close) [HDK p.60].
- **`HwSelectChannel`** (AL = select code): make a channel current; **returns the previously
selected channel in AL** so it can be restored on exit [HDK p.60].
Discipline [HDK p.60]: select the correct channel on entry to any vector/ISR that talks down it, and
restore the previous one on exit. **Disable interrupts (multitasking) between select and restore**
but only briefly, because the **watch-dog** forbids leaving interrupts off indefinitely; hence
comms happen in short bursts. Idiom:
```asm
pushf
cli
mov al, SelectChannel5
HwSelectChannel ; old channel returned in AL
push ax
; ... Input / Output to the peripheral ...
pop ax
HwSelectChannel ; restore previous channel
popf
```
### 7.5 Channel unit letters (open qualifiers)
A driver can support several channels distinguished by a **single-letter qualifier** appended to
the device name, allocated from `'A'` [HDK p.38]. Convention: **A = top port, B = bottom port,
C = side/cradle port** [HDK p.38]. Example: `p_open(&pcb,"PAR:A",-1)`. On the S3a only **one** SIBO
channel (port C) can be opened; on Workabout/HC up to **three** (A, B, C) [HDK p.38]. The VIC's
extra serial connectors appear to the Workabout as standard serial devices **ports I and F**, and
the extended 15-way port is addressed as **port C** [WPG Appendix A, VIC].
### 7.6 Direct I/O (HC extended internal ports)
On the HC's 25-way extended internal ports, expansion devices also get **direct processor I/O**:
`AD0AD7` (multiplexed addr/data, low half only → up to 128 I/O addresses, even addresses only),
`ALE`, `IOWR`, `IORD`, `EES` (External Expansion Select — high during I/O to that device), `INTR`
(active-high IRQ to ASIC1). Port 1 decodes I/O `1001FF`, port 2 `2002FF`; A0 is always low for
valid writes so A1 is the lowest usable address line [HDK p.19]. Each HC port also carries one SIBO
serial channel (port 1 = channel 5, port 2 = channel 6) [HDK p.20]. (The Workabout uses a 26-way
Torson connector for its internal expansion — see §8.)
---
## 8. Expansion slots, ports and connectors
### 8.1 Overview by machine [HDK p.2]
| Machine | SSD ports | Expansion |
|---------|-----------|-----------|
| MC / HC | 2 | Two independent single-row **25-way extended internal** ports (SIBO channel + direct parallel I/O + 7.2 V battery). |
| Series 3 / 3a | 2 | One **6-pin reduced external** port (port C): SIBO serial channel + limited power (**< 25 mA**). |
| **Workabout** | 2 (A: top, B: bottom) | Two internal expansion points + one **11-pin LIF** external port. Each internal port = single-row **26-way** carrying two high-speed serial ports. |
### 8.2 Workabout internal 26-way (Torson) connector [HDK p.13]
Signals: **Vcc1** = 3.0 V nominal, **≤ 100 mA**; **Vcc2** = 5 V nominal, **≤ 200 mA**; **RUN**
(low = powered down, high = powered up — used to power-down/reset the module); **SCK2/SCK3** serial
clocks (run continuously at **1.536 MHz** when the port is in use; clock the ASIC5 UART in the
RS232-AT/TTL and AT/Barcode modules); **SD2/SD3** bi-directional serial data; **EINT1/EINT2**
active-low interrupt inputs; **EXON** active-high turn-on. All logic at **3.0/3.3 V** (per Vcc1).
Unused lines are reserved for a codec interface.
The Workabout expansion module (RS232/TTL or RS232/Barcode) connects the main board to the module
via this 26-way Torson connector + flexi cable; modules are **factory-fitted** [WPG p.1-3,
Appendix A]. The RS232/TTL module runs up to **19,200 baud**; the RS232/Barcode module uses an HP
HBCR-1612 micro to decode and transmit barcode data at up to **9,600 baud** [WPG Appendix A].
### 8.3 Workabout external 11-pin LIF connector [HDK p.1416]
The reduced external port for connecting to the Cradle system. Standard pin functions (Polarisation
Type B, cradle perspective) [HDK p.16]:
| Pin | Name | Function |
|-----|------|----------|
| 1 | LCA | Local Computer Active — high when the computer is on. Workabout can **source 100 mA** from this pin (HC/HCDOS: 5 mA). |
| 2 | EXON | External switch-ON, active-high (+5 V) — asserted by a remote/cradle device to switch the computer on. |
| 3 | THERM | Battery thermistor terminal. |
| 4 | DLA | Disconnect Local ASIC (does not apply to Workabout). |
| 7 | VIN | Power supply to computer (+10 V). |
| 8 | SCLK | Serial-channel clock. |
| 9 | GND | Power / signal ground / ve battery (1 A). |
| 10 | SDATA | Serial-channel data. |
| 11 | STATUS | Cradle status; pulled-up for open-collector sensing (low = remote device present). |
Type-A polarisation repurposes several pins for RS232 signals. The LIF cover can be moulded with a
polarising pin in up to four positions to differentiate variants [HDK p.14].
### 8.4 S3/S3a 6-pin reduced external port (port C) [HDK p.12]
Six wires: **MSD/MCLK** (a master SIBO serial channel — **channel 7 on S3, channel 5 on S3a**),
**SDS/INT** and **SCK/EXON** (dual-function: form a slave serial channel, or SDS/INT is an
active-high interrupt, and a rising edge on SCK/EXON wakes the machine from standby), **VCC** (+5 V,
switched off in standby, **max 25 mA**). Opening the pack doors on an S3a or Workabout **cuts power
to external peripherals** [HDK p.12].
---
## 9. Power
- **Workabout power sources** [WPG p.1-4, Appendix A]: NiCd rechargeable pack **or** 2 × AA
alkaline (main), plus a **3 V lithium CR1620 backup** battery that preserves internal RAM when the
main source is absent (recommend yearly replacement; alone it preserves memory for a limited
period). External: a Series 3 mains adaptor via the **LIF Converter** (also trickle-charges), and
(from Jan/Feb 1997) a **vehicle power adaptor** / Docking Station.
- Power management is **software-controlled** and selective; the machine auto-switches-off after
~5 minutes idle by default [WPG p.1-4, §basic hardware].
- Under **low battery** the Workabout may run the screen/keyboard but refuse to write Flash SSD or
access expansion devices; it turns off if such an operation is attempted [WPG p.1-4].
- **States:** standby vs operating/idle. In standby, switched supplies (VCC/Vcc2) are **off**;
peripheral designs must not drive lines high in standby (pull-down inputs cause excessive standby
current) [HDK p.12, p.19].
- **Peripheral power budgets** (from the connector tables): S3a port C **< 25 mA**; HC 25-way Vcc2
**50 mA** (or use unregulated **Vsup** 5.512 V with a low-dropout regulator for higher loads);
Workabout Torson **Vcc1 100 mA / Vcc2 200 mA**; LIF **LCA 100 mA** [HDK p.12, p.1920, p.13,
p.16].
- **Power-fail handling for drivers** [HDK p.41]: on power loss there is ~**2 ms** to power
everything down; the hold vector must be fast or the RAM-hold voltage drops and the machine
cold-reboots, losing internal RAM (including any loaded driver). On a power-fail hold the OS has
already reset the SIBO channels, so a channel driver need only record the reason for its resume
vector. Hold reasons passed in AH: `DevHoldNormal` (memory move), `DevHoldPowerDown` (standby),
`DevHoldPowerFail` (supply lost) [HDK p.42].
---
## 10. The SIBO serial link (host ↔ PC / peripheral)
- The **3-Link** is an **ASIC5-based** cable that converts the S3/S3a port-C SIBO serial channel to
**RS232** (host ↔ PC/printer), using the ASIC5 on-board UART + line drivers [HDK p.5, p.35].
- Workabout communications: the OS provides serial comms at up to **19,200 baud**, with **XMODEM /
YMODEM** (and ZMODEM except in early models) file transfer, plus a scripting language for modem
control [WPG §basic hardware, Tech Spec].
- The high-speed inter-machine link is the same continuously-clocked SIBO serial channel described
in §4; the physical media are the S3a 6-pin port C, the HC 8-pin side port C (used by the HC
cradle), or the Workabout 11-pin LIF via the LIF Converter [HDK p.12, p.20; WPG Appendix A].
---
## 11. Programmer's cheat-sheet
- Talking to a peripheral = **own a channel** (`HwGetChannel`) → **select it** (`HwSelectChannel`,
save/restore the old one, interrupts off, short bursts) → drive the slave with control/data frames
via `SCONTOUT`/`SDATAOUT`/`SDATAIN`**free it** (`HwFreeChannel`) on close [HDK p.52, p.60].
- **ASIC4 access** = set Address register (multi-byte, LSB first) → read/write Data register; only
even/low-256 addresses in the simple routines; interrupts off during a transfer [HDK p.5359].
- **ASIC5 UART** = enable continuous clocking, set peripheral bit (Port-B mode bit 0), write baud
divisor `1 96000/baud` (LSB reg 10, MSB reg 11), set format in UART Status/Control (reg 8),
Tx via reg 9 only when `S_TXEMPTY`, Rx via reg 9, one shared active-high IRQ; read status/interrupt
registers to find the cause [HDK p.5661].
- **Interrupts** = 8 levels, IRQ0 highest, level-triggered, **no nesting**; clear at the device then
write NSEOI [HDK p.3].
- **Segments** = 8086 model; writing outside your data segment or hogging interrupts → **panic**
[WPG §Resetting; HDK p.2].
---
## 12. Known gaps / uncertainties in these sources
- **V30MX / ASIC9MX / Workabout MX:** absent from both supplied manuals. All MX statements above
are flagged **[MX — not in supplied manuals]** and are orientation only. Confirm against the MX
hardware reference.
- **Host CPU physical memory map and any bank/page registers:** not in these manuals (deferred to
*SIBO Computers Programmers Reference* / *Hardware Reference*). Only the peripheral-side
(ASIC4/ASIC5) chip-select block decode is documented here.
- **Exact ASIC5 register numeric slots (regs ~2, 4, 5, 7):** the printed table is OCR-damaged; names
and functions are reliable (prose + `SYS$AS5.ASM`), but verify the numeric indices against
`ospack.inc` / `ossibo.inc`.
- Several connector pin tables in the WPG appendix are OCR-garbled (RS232 module pinouts, VIC 15-way
pinout); signal names are recoverable but pin numbers should be checked against a clean copy.
+78
View File
@@ -0,0 +1,78 @@
# Boot & OS-call internals (reverse-engineered from the ROM under MAME)
Source: dynamic trace of `psionwamx` (v7.20f) under MAME — 170,160 instructions
of boot captured with correct bank mapping. Everything here is **reverse-
engineered / observed**, not from a manual, and describes internal mechanisms
below the documented C API.
## Reset & early boot
```
FFFF0: jmp A000:0 ; reset vector -> ROM
A0000: jmp A14A4
A14A4: di ; disable interrupts
in/out 30h,... ; ASIC hardware init via I/O ports
mov ds0,190h ; segment setup
out 2h / 2Ch / 22h / 26h / 24h / 3Ch ; ASIC control-register init
jmp AE69C ; signature check ([6896h]==0F0A5h) then continue
```
Early boot disables interrupts, programs the ASIC control registers through the
I/O ports, sets up segments, and validates a ROM signature word.
## OS service calls are software interrupts (not a single gate)
SIBO dispatches operating-system services through a **range of `INT` vectors**
(NEC V-series `brk` mnemonic), each vector a service group with a function
selector in a register. Vectors observed executing during boot, by frequency:
| Vector | Count (boot) |
| --- | --- |
| `0xA6` | 414 |
| `0x97` | 81 |
| `0x85` | 49 |
| `0x96` | 41 |
| `0xBA` | 41 |
| `0x8B` | 17 |
| `0xB9` | 15 |
| `0xAC` | 12 |
| others `0x81``0xB5` | few each |
Additional service vectors seen in driver code (not exercised at boot):
- **`INT 0xCF`** — the I/O executive: `CL` = function code, `BX` = channel
handle, `DX` = argument, result in `AX`. This is the layer beneath `p_iow`.
- **`INT 0xD9`**, **`INT 0xD3`** — further service vectors (exact roles TBD).
The specific service behind each vector is not yet mapped; doing so means
breakpointing each ISR and correlating with the documented C calls. This table
is the starting point.
## ASIC I/O ports touched during boot
Distinct `out` port targets and write counts during boot (ASIC register access —
cross-reference the ASIC register maps in the hardware reference):
| Port | Writes | Likely role |
| --- | --- | --- |
| `0x28` | 1580 | display/LCD controller (dominant) |
| `0x15` | 88 | |
| `0x21` | 54 | |
| `0x10` | 17 | |
| `0x02` | 14 | ASIC control |
| `0x0A` | 13 | |
| `0x08` | 12 | |
| `0x2C` `0x24` `0x22` `0x26` `0x30` `0x3C` | few | ASIC control-register setup (from early boot) |
## Method (reproduce)
```
xvfb-run -a mame psionwamx -rompath roms -debug \
-debugscript trace.txt -sound none -seconds_to_run 1
# trace.txt: trace boot_trace.asm / go
```
Then post-process `boot_trace.asm` for instruction/port/vector frequencies.
## Next RE steps
1. Map each `brk` vector to its service (breakpoint ISR, correlate to C API).
2. Trace `SCANAPP`/`DEMMAN` to resolve the `WL2` scanner data-retrieval path.
3. Correlate the ASIC ports here with the HDK register maps.
Binary file not shown.