Tachibana
Language: English

Technical architecture — OricTel

Overview

OricTel is made of two independent subsystems:

  1. Oric program (6502 C/ASM) — the Minitel terminal.
  2. Python bridge — a WebSocket ↔ TCP gateway (secondary path).

The reference setup is now LOCI + PicoWiFiModemUSB: the AT modem opens the TCP sockets to the Minitel server itself, the bridge is no longer in the path. It remains useful for servers exposed only over WebSocket.

Data flow

Minitel server (ws://3617.fr/ws)
        |
        | WebSocket (binary frames, raw Videotex bytes)
        v
+-------------------+
| orictel_bridge.py |  Python asyncio
| TCP port 3615     |  Transparent bidirectional relay
+-------------------+
        |
        | TCP socket (raw bytes, no framing)
        v
+-------------------+
| Phosphoric        |  Oric emulator
| ACIA backend      |  --serial picowifi / com: / tcp:
| AT modem 8N1      |  1200 baud (Control $18, Command $0B)
+-------------------+
        |
        | ACIA 6551 registers @ $0380-$0383 (LOCI base)
        | Cycle-accurate timing
        v
+-------------------+
| OricTel program   |  6502 code (cc65)
|                   |
| serial_asm.s      |  <-- Low-level ACIA driver (polling, SEI/PLP)
| serial.c/tx.c     |  <-- Thin layer + non-blocking transmit queue
| at_modem.c        |  <-- AT commands, hang-up, carrier watch
| videotex.c        |  <-- Protocol state machine
| display.c         |  <-- HIRES rendering (1 row/pass budget)
| display_asm.s     |  <-- Assembly blits (cell, run, double height)
| fonts.c           |  <-- G0 / G1 / G2 tables
| keyboard.c        |  <-- Keyboard scan + Minitel mapping
| ui.c              |  <-- Menu helpers (host-testable)
| main.c            |  <-- Main loop, menus, session
+-------------------+

main.c is a menus → connection → session cycle: the session is only left through ESC (confirmed in the status bar, without clearing the page), which hangs up (at_hangup) and goes back to the Connection mode menu with vtx_init. The connection failure and carrier loss screens return on ESC. ESC on the main menu exits OricTel through jmp ($FFFC) (reset vector, independent of ROM 1.0/1.1).

Reference setup (hardware)

Oric --> ACIA 6551 $0380 (LOCI cartridge)
             |
             | RP2040 firmware: 32-BYTE RX ring in front of the register
             v
        USB-CDC --> PicoWiFiModemUSB --> WiFi --> Minitel server (TCP)

The 32-byte ring is an important design element: the bare 6551 buffers a single byte, and OricTel uses no IRQ. This buffer is what gives the software a tolerance of about 250,000 cycles (30 bytes at 1200 baud) between two reads. Emulating this setup without --serial-buffer is therefore more pessimistic than the real hardware.

Oric memory map

$0000-$00FB  Zero Page (cc65: $00E2-$00FB)
$0100-$01FF  6502 stack
$0200-$02FF  System variables
$0300-$030F  VIA 6522 (mirror $0300-$03FF)
$0380-$0383  ACIA 6551 (serial, LOCI base)
$0400-$0500  Oric system area
$0501-$97FF  OricTel CODE + DATA + BSS (~37 KB; BSS bounded at $9800)
$9800-$9BFF  Text-row character set (copy of font_g0)
$9C00-$9FFF  cc65 stack (1 KB; measured < 32 bytes in session)
$A000-$BF3F  HIRES framebuffer (8000 bytes)
$BF68-$BFDF  3 text rows below HIRES = status bar
$C000-$FFFF  ROM (16 KB)

ACIA 6551 registers (Oric side)

AddressReadWrite
$0380Received dataData to send
$0381Status registerProgram reset
$0382Command registerCommand register
$0383Control registerControl register

Effective configuration (LOCI base $0380)

6551 datasheet trap: TIC=01 (values $05/$07) enables the TX IRQ; TDRE being permanently set, the ACIA kept reasserting /IRQ and the machine froze (v0.3.3/v0.3.4 regressions). The old V23 7E1 configuration ($28/$69) no longer matches any supported setup: the PicoWiFiModemUSB talks 8N1.

Status register bits

Status bar (3 text rows)

In HIRES, the Oric still displays 3 text rows below the 200 graphic lines ($BF68-$BFDF). They were hidden because their character set, which the ROM places at $9800-$9FFF, was overwritten by the C stack. The stack is reduced to 1 KB at $9C00-$9FFF (the alternate set's area, never selected) and display_init copies font_g0 to $9900-$9BFF. Row 0 (cyan): C/F indicator, server, mm:ss timer, rendering mode, ESC reminder; row 1 (yellow): transient messages; row 2 (white): key help. The link indicator thus leaves row 0 of the Videotex page, which belongs to the server.

Time base: VIA 6522 Timer 2

The session loop has no ISR and its iteration has no fixed duration. Delays (carrier-loss confirmation 4 s, indicator back to F after 30 s) are therefore counted on T2 ($0308/$0309), read on every iteration: the ROM only uses it for tape, it counts down continuously at 1 MHz, and elapsed cycles are accumulated in 10 ms ticks. OricTel arms it itself and disables its IRQ (IER = $20). Verified by make test-carrier: 4.0 s ± 0.1 on ROM 1.0 and 1.1.

Videotex protocol — state machine

NORMAL ──ESC($1B)──> ESC_SEQ
       ──US($1F)───> US_ROW
       ──SO($0E)───> (switch to G1)
       ──SI($0F)───> (switch to G0)
       ──$20-$7F──> (display character)
       ──$08-$0B──> (cursor move)
       ──$0C─────> (clear screen)
       ──$0D─────> (carriage return)

ESC_SEQ ──$40-$47──> (ink colour) -> NORMAL
        ──$50-$57──> (background colour) -> NORMAL
        ──$48/$49──> (flash on/off) -> NORMAL
        ──$4C-$4F──> (size) -> NORMAL
        ──$5B─────> CSI
        ──$39─────> PRO

US_ROW ──$40-$57──> US_COL (stores row)

US_COL ──$41-$68──> NORMAL (positions cursor)

CSI ──params+letter──> NORMAL (ANSI-like command)

HIRES display

Per-cell attributes

Optimised rendering (“dirty” spans, measured budget)

Each modified row is marked “dirty” with the touched column range. Rendering is not paced on the video frame: it is budgeted at one row per pass from the main loop, which calls display_render() again as long as dirty rows remain and no byte is waiting in reception. During a pass reception is not re-read: if the pass exceeds the RX buffer tolerance, bytes are lost.

Measured costs (make bench-render, real 6502 cycles)

RegionCostIn byte-times at 1200 baud
display_render() pass (1 row)~151,00018.8
realistic hybrid row~79,00010.1
double-height row~134,00016.7
single-cell update~30,0003.6
vtx_process() (40 characters)~105,00013.2

Budget of one byte at 1200 baud: 8,333 cycles. Real setup tolerance: ~250,000 cycles (32-byte ring of the LOCI firmware). The benchmark also prints the SHA-256 fingerprint of the framebuffer after a canonical page: any optimisation meant to be visually neutral must leave it unchanged.

Assembly fast paths (display_asm.s)

RoutineRole
blit_cell8one normal-size cell (~190 cycles vs ~2,000 in C)
blit_runrun of eligible cells; stops on any non-normal size
blit_cell4x2double height: 4 source lines stretched over 8 pixel lines
display_clearpage-wise framebuffer fill (~5 ms)

cc65 trap: ctx->screen[row][col] generates a 16-bit multiplication by 240 on every access, and a parameter pointer is reloaded from the software stack on every dereference. Hot loops therefore hoist a row pointer, and videotex.c goes through a file-scope context pointer plus offset tables: 4.0× on rendering, 1.50× on the decoder.

Reproduced from docs/ARCHITECTURE.md (authoritative, up-to-date version on GitHub).

← Documentation · User manual →