Search › A-Z › P › Psion Series 3
Psion Series 3
An in-browser original Psion Series 3 that boots the authentic EPOC16 (SIBO) v1.91F system ROM. No standalone JavaScript NEC V30 executor exists, so PCjs's pure-JavaScript x86 core (CPUx86) is reused as the V30 — the Series 3's 8086/80186-class CPU — and wired to a from-scratch model of the two-chip ASIC1 + ASIC2 gate-array set: the flat memory decode, the 240×80 one-bit mono LCD, the tick/free-running/watchdog timers, the 8-source interrupt controller and the ASIC2 keyboard scanner. The real ROM passes its power-on self-test, boots the EPOC16 kernel, window server and file server, formats a clean internal RAM disk, and paints the System screen with the app suite, which you can navigate with the arrow keys; because the whole machine is plain JavaScript, the shared debugger single-steps the V30 one true instruction at a time, reads and writes the full register file, inspects the flat 1 MB memory map, and sets breakpoints and write watchpoints.
The original Psion Series 3 (1991) was the first SIBO clamshell palmtop: a pocket organiser with a proper QWERTY keyboard and a suite of built-in applications — System, Word, Agenda, Data, Time, World, Calc and Sheet — resident in ROM, sharing the NEC V30 processor and EPOC16 operating system across the whole Series 3 family, but built around the older two-chip ASIC1 + ASIC2 set and a smaller 240×80 display.
The V30 core is provided by PCjs's open-source (MIT) pure-JavaScript x86 executor, reused here and wired to a from-scratch ASIC1+ASIC2 machine written for emulators.org.
Runs on: Web browser
Psion Series 3 Online Emulator
Play Psion Series 3 using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| Psion Series 3 — EPOC16 (SIBO) | Psion Series 3 | Psion Series 3 | grey | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The original Series 3's CPU is a NEC V30 — an 8086-compatible part with the 80186 instruction set, clocked at 3.84 MHz. No standalone JavaScript V30 executor exists, so we reuse the uncompiled PCjs pcx86 bundle purely for its CPUx86 class (a real x86 executor, not a disassembler) and discard the whole IBM-PC machine around it. A bare CPU is built on a 1 MB bus and handed a tiny cmp stand-in and our Psion machine:
var cpu = new CPUx86({ id:'psion.cpu', model:'80186' }); // V30 ~ 8086 pins + 80186 ISA
var bus = new Busx86({ id:'psion.bus', busWidth:20 }, cpu); // 1 MB V30 address space
cpu.initBus(cmp, bus, cpu, null);
var m = new PsionASIC12(romBytes);
m.install(bus, cpu); // flat CTRL memory over 0..0xFFFFF + ASIC1/ASIC2 I/O ports
cpu.chipset = m.chipsetStub(); // so the CPU can pull IRQ vectors 0x78..0x7F
cpu.reset(); // CS:IP = FFFF:0000 -> ROM reset vector JMP C000:0000
Run loop. We drive cpu.stepCPU() ourselves in sub-ticked frames, advancing the ASIC1 timers (4/32 Hz tick, the 512 kHz free-running counter, the 4 Hz watchdog) and delivering interrupts between bursts:
for (var s = 0; s < SUBTICKS; s++) { cpu.stepCPU(SUB); m.tick(SUB); } // timers -> cpu.intFlags |= INTR
m.render(); // scan RAM 0x0400 -> 240x80 canvas
Because the CPU, bus and ASIC1/ASIC2 model are ordinary JavaScript, single-stepping is cpu.stepCPU(0) (exactly one instruction), registers are the core's regEAX… fields and get*/set* accessors, and breakpoints and watchpoints are host-side checks — no change to the CPU core.
Debugger integration
psion-series3-classic-debug.js reads the live machine from window.EMU_BOOT and calls EmuKit.defineMachine with the shared x86 disassembler and the full 8086/V30 register set, reusing the exact CPUx86 hooks that pcjs-debug.js uses (regEAX…regEDI, getCS/getIP/getPS and their setters, regLIP as the linear PC).
Side-effect-free memory over a FLAT map. Unlike the Series 3a (whose ASIC9 pages the V30's 1 MB into a 16 MB space), the classic Series 3's ASIC1 maps memory directly: 0x00000–0x3FFFF is the 256 KB RAM, 0x40000–0x7FFFF is open bus, and 0x80000–0xFFFFF is the 512 KB ROM (holding the reset vector EA 00 00 C0 00 = JMP C000:0000). The hex/disasm view reads through PsionASIC12.peek — the same flat map the CPU uses, with no effects — so what you see is exactly what the V30 fetches. Two more chips expose the raw physical RAM (to watch the LCD framebuffer at 0x0400) and the ROM linearly.
function stepInsn(){ cpu.stepCPU(0); } // TRUE single instruction — the payoff of the JS route
Breakpoints are a Set of linear PCs; when any is set the loop steps one instruction at a time and compares cpu.regLIP. Watchpoints wrap the ASIC1 memory-write path (PsionASIC12.writeByte) and pause when a watched V30 address is written. Interrupt injection mimics ASIC1's 8-source controller: a chipset stub returns the IDT vector 0x78 + lowest-pending-source, and the ASIC1 timers assert cpu.intFlags |= INTR when an unmasked source is pending — the CPU takes the vector through the guest IVT exactly as hardware would.
Architecture
The original Series 3 is the earliest SIBO clamshell, sharing the family processor — a NEC V30 (8086 pins, 80186 instruction set, 3.84 MHz) running EPOC16 / SIBO from ROM — but built around the older two-chip ASIC1 + ASIC2 set rather than the 3a's single ASIC9. Modelled here from MAME's hardware description:
- ASIC1 — system controller. Bus controller, memory decode, LCD controller, the programmable timers and the 8-input interrupt controller. Crucially the memory map is flat (no pager): 256 KB RAM at
0x00000, open bus, then the 512 KB ROM at0x80000–0xFFFFF. ASIC1 also has a write-protection window (A1ProtectionOn/Off/Upper/Lower) that traps out-of-range writes with an address NMI. - ASIC2 — peripheral controller. The system clock, power/standby switching, buzzer, the SIBO eight-channel serial protocol controller (SSD packs), and the keyboard column-scan: the OS writes a column number to
A2Control1and reads the eight rows back onA2External. The Esc/On key sets the on-key status, waking the machine. - LCD. A 240×80 one-bit mono panel (not the 3a's 480×160 three-level grey), scanned from a single-plane framebuffer at RAM
0x0400, drawn only when ASIC1 status bit 5 (LcdEnable) is set; the row stride comes from theA1LcdSizeregister. - Timers & interrupts. A 4/32 Hz tick, a 512 kHz free-running counter and a 4 Hz watchdog feed ASIC1's 8-source controller on IDT vectors
0x78–0x7F; ASIC2 folds into it through EINT3, and door/expansion and watchdog conditions raise the V30 NMI. - V30 CPU. PCjs's 80186 executor supplies the common instruction set, but the NEC V30 adds its own
0x0F-escape instructions (bit-fieldINS/EXT, single-bitTEST1/CLR1/SET1/NOT1, packed-BCDADD4S/SUB4S/CMP4Sand nibbleROL4/ROR4) that Intel never had —psion-v30.jsadds them to the core. EPOC16's window server draws text with theEXT/INSbit-field blitter, so these are essential.
Every part — the V30 executor, the bus, the ASIC1/ASIC2 model — is plain JavaScript, which is what makes the Series 3 a good debugging target: true per-instruction stepping, live registers, and side-effect-free memory over the real flat map.
Sound
Pattern S (authored synthesis). The classic Series 3 has no tone chip — its only sound source is a single piezo buzzer on ASIC2. There is no frequency register: EPOC16 makes every beep by bit-banging one output line in a software delay loop. We confirmed this by tracing the port writes on the live ROM: the buzzer lives in A2Control2 (ASIC2 register 3, I/O port 0x86), where bit 3 (0x08) is the drive-enable (held set for the length of a beep) and bit 2 (0x04) is the piezo diaphragm line the OS toggles — e.g. the power-on beep is a clean 0x08↔0x0C alternation.
How it is turned into sound. PsionASIC12 watches writes to A2Control2 and counts every transition of bit 2 (buzzerFrame() returns and clears the edge count for the frame). Two edges are one square-wave cycle, so edges-per-frame × (fps/2) is the beep's pitch. The run loop (psion-boot.js) renders that as a phase-continuous square wave at EmuAudio.sampleRate — the pitch is derived from the measured toggle rate, so no sample-rate constant needs setting — and pushes exactly Math.round(EmuAudio.sampleRate/60) interleaved-stereo Int16 samples per video frame to the shared sink (debugger/src/audio.js). Idle frames (no toggles) push silence.
var toggles = m.buzzerFrame(); // piezo edges this frame
var freq = toggles * 30; // * (60fps / 2) -> Hz
for (var i = 0; i < N; i++) { // N = round(sampleRate/60)
var s = toggles ? (phase < 0.5 ? AMP : -AMP) : 0;
buf[2*i] = buf[2*i+1] = s; phase += freq / SR;
}
EmuAudio.push(buf);
Mute contract. window.EMU_BOOT.transport exposes isMuted() / setMute(m), delegating to EmuAudio. It starts muted (browsers block audio before a gesture), and the shell's Sound button unmutes from a real click.
A near-silent device. This is a productivity palmtop, not a games machine: EPOC16's key-click is off by default, so at rest the only sounds are system beeps, boundary and error beeps and alarms. It boots to the desktop and synthesizes nothing on load. Sound is fully supported and plays when the software makes it, through the real piezo buzzer path above. The page starts muted; click Sound, and every in-app keypress, boundary and alarm beeps on its own.