Tachibana

Phosphoric › Artikel › LOCI-Erweiterungen

Die Grenzen des Oric verschieben: fünf experimentelle Erweiterungen für die LOCI-Karte

Wie ein zyklusgenauer Emulator als Hardware-Prüfstand dient — mit der ABI, den Registern und den Schemata.

Von bmarty · Phosphoric

Über die Fotos. Die Abbildungen der LOCI-Karte stammen aus der offiziellen Hardware-Dokumentation von sodiumlb (loci-hardware) und gehören ihrem Autor.

1. Der Hardware-Kontext

Die LOCI-Karte in ihrem gedruckten Gehäuse, Ansicht von oben: Tasten Action (rot), Reset und Firmware-Update.
Die LOCI-Karte (RP2040/RP2350 + MIA) in ihrem Gehäuse — Tasten Action, Reset und Firmware-Update. Abbildung: sodiumlb / loci-hardware.

Der Oric-1 und der Atmos (1983) basieren auf einem MOS 6502 mit 1 MHz, 64 KB Adressraum, dessen oberer Teil ($C000-$FFFF) vom BASIC-ROM belegt ist. Keine verdrahtete Multiplikation, keine Speicherbänke, kein moderner Speicher: alles läuft über den Erweiterungsbus an der Rückseite der Maschine.

Die LOCI-Karte (Lovely Oric Computer Interface) von sodiumlb wird an diesen Bus angeschlossen. Technisch ist sie ein Ableger des Picocomputer 6502 (RP6502): ein Mikrocontroller RP2040 (ein Raspberry Pi Pico) übernimmt die Rolle eines MIA (Media Interface Adapter), der sich dem Oric als Ein-/Ausgabe-Peripherie präsentiert. Er emuliert Diskettenlaufwerke und Kassetten, verwaltet eine SD-Karte und spielt die Rolle eines USB-Hosts — über diesen USB-Port wird ein Wi-Fi-Modem (separater Dongle, der PicoWiFiModemUSB) oder ein HID-Gerät (Maus, Gamepad) angeschlossen; das Wi-Fi ist nicht auf der LOCI-Karte selbst integriert. Zwei Adressierungs- flächen interessieren uns hier:

Steckverbinder des Oric-Erweiterungsbusses der LOCI-Karte, mit markiertem Pin 1.
Der Steckverbinder des Oric-Erweiterungsbusses der LOCI (Pin 1 markiert) — hier wird die Karte an der Rückseite der Maschine eingesteckt. Abbildung: sodiumlb / loci-hardware.
         Oric-Adressraum (64 KB)
   $0000 ┌──────────────────────────────┐
         │ RAM                          │
   $0300 ├──────────────────────────────┤
         │ VIA 6522        $0300-$030F  │
   $0310 │ Microdisc WD1793 $0310-$031F │
         ├──────────────────────────────┤
   $0380 │ ACIA 6551 (LOCI) $0380-$0383 │ ← serielle Konsole / Modem
         ├──────────────────────────────┤
   $03A0 │ MIA (LOCI)      $03A0-$03BF  │ ← API-Fenster 32 Bytes
         ├──────────────────────────────┤
   $C000 │ ROM BASIC       $C000-$FFFF  │ ← Ziel des Bank-Overlays
   $FFFF └──────────────────────────────┘
I/O-Mapping des Oric, wie es die LOCI-Karte sieht.

Der Emulator Phosphoric (zyklusgenau, C11) bildet diese Karte originalgetreu nach, was ein seltenes Vorgehen ermöglicht: die Spezifikation einer Erweiterung schreiben, sie codieren und durch deterministische Tests validieren — bevor man den Lötkolben in die Hand nimmt. Ein wichtiges Detail: Der Autor besitzt eine LOCI-Karte, aber keinen Oric. Der Software-Prüfstand ist kein Luxus, er ist der einzige Testraum.

2. Die fastcall-ABI — wie der 6502 die Karte aufruft

Alles beruht auf einem Fenster von 32 Registern in $03A0-$03BF (dem MIA). Hier die tatsächliche Belegung der von der ABI genutzten Register (Namen originalgetreu zur Firmware):

 Offset  Adresse  Register          Rolle
 ------  -------  ----------------  ------------------------------------------
  $00    $03A0    CONS_FLAGS        Bit7 = TX frei, Bit6 = RX bereit
  $01    $03A1    CONS_TX           Konsolen-Schreiben (UART)
  $02    $03A2    CONS_CHAR         Konsolen-Lesen (konsumiert das Byte)
  $0C    $03AC    API_STACK         xstack-Zeiger (Argument-Stack)
  $0D    $03AD    API_ERRNO_LO      errno niedrig
  $0E    $03AE    API_ERRNO_HI      errno hoch
  $0F    $03AF    API_OP            ← HIER SCHREIBEN löst die Operation aus
  $12    $03B2    BUSY              Bit7 = Karte belegt
  $14    $03B4    API_A             Rückgabewert A
  $16    $03B6    API_X             Rückgabewert X
  $18    $03B8    API_SREG          16-Bit-Rückgabe (SREG)
MIA-Register der ABI (Auszug aus include/io/loci.h).

Das Aufrufprotokoll (fastcall) umfasst vier Schritte:

   6502 (Oric)                         MIA (LOCI, µC)
   ───────────                         ──────────────
   1. push args ──► xstack ($03AC)
   2. set A/X (direkte Parameter)
   3. write op ──► API_OP ($03AF) ───► löst den Handler aus
                                       ├─ BUSY=1
      poll BUSY ($03B2) ◄──────────────┤  führt aus
                                       └─ BUSY=0, füllt API_A/X/SREG, ERRNO
   4. read API_A/API_X ($03B4/$03B6) ◄─ Ergebnis
Ablauf eines LOCI-fastcall-Aufrufs.

Jeder noch freie Opcode-Wert ist ein Einsprungpunkt für eine neue Funktion. Die Standardoperationen reichen von $01 bis $98 (Uhr, open/read/lseek, Verzeichnisse, Image- Mounten, TAP…). In den ungenutzten Opcodes finden unsere fünf Erweiterungen ihren Platz:

  $A7  SET_BANK        umschaltbare Bank 16 KB       (--loci-bank)
  $A8  STREAM_BANK     Asset-Streamer               (--loci-bank)
  $A9  MATH            Arithmetik-Coprozessor       (--loci-coproc)
  $AA  ACIA_RELIABLE   zuverlässiger ACIA (seqlock) (opt-in-Modus)
       + acia_stat_checked : verlustfreies RX-Handshake  (--loci-acia-rx-nag)

Schutz by design. Ohne das entsprechende Aktivierungs-Flag gibt der Opcode ENOSYS (errno 13) zurück — genau wie eine ungepatchte Firmware. Die Oric-Software kann also das Vorhandensein der Erweiterung erkennen und auf ihre eigenen Routinen zurückfallen. Das Standardverhalten der LOCI bleibt strikt das der Original-Hardware.

3. Arithmetik-Coprozessor — $A9

Das Problem. Der 6502 hat weder Multiplikation, noch Division, noch verdrahtete Gleitkommazahlen. Jede Operation ist eine Software-Routine: langsam, umfangreich, zyklenintensiv. Auf einer 1-MHz-Maschine zählt eine 16×16-Multiplikation Hunderte von Zyklen.

Die Idee. Die Berechnung an den Mikrocontroller der Karte delegieren, der weitaus schneller ist, über die bestehende fastcall-ABI — ein einziger Opcode $A9, der Operations-Subcode in API_A, die Operanden auf dem xstack, das Ergebnis in API_A/SREG.

   ; konzeptionelles Beispiel: A×B über den Coprozessor
   LDA #<op_mul  : STA API_A     ; Operations-Subcode
   ... push A, B auf den xstack ($03AC)
   LDA #$A9      : STA API_OP     ; löst MATH aus
   ; poll BUSY, dann das 32-Bit-Ergebnis in SREG lesen

Die Implementierung. Eine isolierte Datei, src/io/loci_math.c (op_math), an den Dispatch angebunden. Ganzzahlen, Gleitkommazahlen, vektorielle Operationen. Gated durch --loci-coproc. Abdeckung: 23 deterministische Tests (Ganzzahl-Vektoren, Gleitkomma, Grenz- fälle). Kein Zufall — gleiche Eingaben, gleiche Ausgaben, Voraussetzung eines reproduzierbaren Prüfstands.

4. Zuverlässiger ACIA-Modus — $AA (seqlock + ACK)

USB-C-Host-Port der LOCI-Karte, umrandet, an der Kante des Gehäuses.
Der USB-C-Host-Port der LOCI — hier wird der Wi-Fi-Modem-Dongle (PicoWiFiModemUSB) angeschlossen, jener, der den ACIA bei hoher Datenrate sättigt. Abbildung: sodiumlb / loci-hardware.

Das Problem. Der reale ACIA 6551 hat einen bekannten Nachteil: Liest der 6502 das Datenregister nicht rechtzeitig, wird das empfangene Byte vom nächsten überschrieben. Bei hoher Datenrate — etwa einem Wi-Fi-Modem — gehen Bytes verloren, und die Verbindung wird unbrauchbar. Das ist siliziumgetreu, aber hinderlich.

Die Lösung: ein Seqlock. Ein Empfangs-Sequenzzähler und ein Bestätigungssignal seitens des 6502. Das Byte wird erst dann konsumiert, wenn es bestätigt wurde — nie verloren, selbst wenn das Lesen sich verzögert oder fehlschlägt.

     Klassischer 6551-Empfang (destruktiv)
     ─────────────────────────────────────
     RX Byte1 ──► RDR   (6502 hat nicht gelesen…)
     RX Byte2 ──► RDR   ✗ Byte1 ÜBERSCHRIEBEN, verloren

     Zuverlässiger Modus $AA (seqlock + ACK)
     ───────────────────────────────────────
        RXSEQ $0384  (Zähler, bei jedem präsentierten Byte inkrementiert)
        RXACK $0385  (vom 6502 geschriebenes Bestätigungssignal)

     RX Byte1 ─► präsentiert, RXSEQ++        konsumiert := (RXACK == RXSEQ)
     6502 liest Byte1, schreibt RXACK = RXSEQ ─► Byte1 bestätigt → weiter
     RX Byte2 ─► präsentiert nur wenn bestätigt  ✓ kein Byte verloren
Destruktiver 6551-Empfang vs. zuverlässiger Modus per Seqlock.

Der Sendekanal bleibt unverändert (weiterhin zuverlässig). Zustand getragen von loci_t (acia_reliable, acia_rx_seq, acia_rx_presented), Opcode $AA, Aktivierung über API_A Bit0. Abdeckung: +7 Tests (test-loci-acia-miss, 13 → 20) — DATA nicht destruktiv, Konsum ACK-gated, Multi-Byte-Seqlock in der Reihenfolge, und vor allem Überleben eines verpassten Lesevorgangs.

4bis. Verlustfreies RX-Handshake — acia_stat_checked (--loci-acia-rx-nag)

Angrenzende Verfeinerung, der realen Firmware nachempfunden (feature/acia-rx-lossless). Auf dem echten LOCI ist das /IRQ des ACIA ein Pegelsignal, kein Impuls. Wir modellieren diesen Pegel durch ein „Nag": solange das Byte nicht gelesen wurde (stat_checked falsch), wird der Interrupt periodisch erneut ausgelöst (Standard: alle 1000 Zyklen), und verstummt dann, sobald der 6502 das Statusregister abgefragt hat.

   RDRF=1 (Byte verfügbar) ┐
                            │  Nag: deassert+assert /IRQ alle 1000 Zyk
   /IRQ  ▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔   │  solange  RDRF && !stat_checked && RX-IRQ aktiv
                            │
   6502 liest STATUS ───────┘  stat_checked = true  ──►  /IRQ still
Das „Nag" modelliert den Pegel-IRQ des realen 6551.

Ohne --loci-acia-rx-nag bleibt der ACIA strikt ein 6551 (kein Nag). Abdeckung in test-loci-acia-miss: Nag vor der Bestätigung beobachtet, Stille danach, leerer Puffer ⇒ kein IRQ.

5. Umschaltbare 16-KB-Bank — $A7 (--loci-bank)

Das Problem. Wie gibt man einer Maschine mehr Speicher, deren Adressraum vom ROM gesättigt ist?

Die Lösung. Temporär 16 KB des RAMs der Karte (xram) im Fenster $C000-$FFFF überlagern, dort wo das ROM sitzt.

        $A7 deaktiviert               $A7 EN | SEL=n
   $C000 ┌───────────┐          $C000 ┌───────────────┐
         │ ROM BASIC │   ─────►        │ xram[n*0x4000]│  Overlay (Lesen)
   $FFFF └───────────┘          $FFFF  └───────────────┘
                                       └─ ROM intakt DARUNTER (nicht überschrieben)
   Basis xram = SEL * 0x4000 ; SEL geklemmt auf 0..3 (wie mia_set_bank)
Nicht destruktives Bank-Overlay auf dem ROM-Fenster.

Der Kernpunkt: nicht destruktives Overlay. Die Bank hat beim Lesen (und bei der Speicherinspektion) Priorität, ohne jemals das ROM-Array zu überschreiben. Eine Deaktivierung stellt die Maschine Byte für Byte wieder her. Der alte Prototyp-Ansatz per memcpy + Sicherung wurde zugunsten dieser sauberen Überlagerung aufgegeben (memory_set_loci_bank() in memory.c).

Doppelmodus über reset. Die Aktivierung durch $A7 EN löst einen Reset aus: der 6502 liest seinen Vektor $FFFC aus der Bank erneut (man kann also Bank-Code booten). Der Hot-Swap (vom Streamer $A8 genutzt) schaltet dagegen ohne CPU-Reset um — absolute Voraussetzung des Double-Bufferings. Abdeckung: test-loci 170/170 (+4: enable/Zustand, disable, gated OFF → ENOSYS, Klemmung SEL 15→3) und ein test-loci-bank-e2e Ende-zu-Ende.

6. Asset-Streamer — $A8

Die Idee. Sobald die Bank eingerichtet ist, dort Daten aus einer Datei (Flash oder SD-Karte) in einem einzigen fastcall hineinschütten: lseek(SEEK_SET) + read → 16-KB-Bank, mit optionalem Mapping in $C000-$FFFF. Eine Oric-Software kann dann die 48 nutzbaren KB überschreiten: Overlays, Kulissen, Level auf Abruf.

   Double-Buffering (48 KB ohne Reset überschreiten)
   ─────────────────────────────────────────────────
   $A8 MAP=0 SEL=1 ─► lädt Bank 1 vor (unsichtbar)       ┐ währenddessen
   (der 6502 führt Bank 0 weiter aus/zeigt sie an)        ┘
   $A8 MAP=1 SEL=1 ─► schaltet Bank 1 auf $C000 (hot-swap, PC intakt)
Streaming + Double-Buffering von Assets.

Details: API_A Bit7 = MAP, Bits3:0 = SEL (0..3 gültig; >3 = EINVAL, keine Klemmung im Gegensatz zu $A7). Argumente auf dem xstack LIFO (len, dst, off, fd), Schreiben begrenzt auf die Bankgröße, Rückgabe AX = gelesene Bytes. Zwei Lesepfade: Host-Datei und SD-Image. Nutzt das opt-in --loci-bank wieder.

7. Das Tearing-Modell — die Frage der zwei Kerne

Die subtilste Erweiterung, und die intellektuell ehrlichste.

Die Frage. Wenn man eine Bank umschaltet, während ein Buszyklus läuft, was latcht der 6502? Auf dem Single-Thread-Emulator ist der Swap atomar: unsichtbar, die Frage stellt sich nicht. Aber die reale Hardware hat zwei Kerne; ein zu einem Speicherzugriff nebenläufiger Swap kann ein Tearing erzeugen.

Die Antwort: den schlimmsten Fall explizit modellieren. Mit --loci-bank-tearing lässt ein Hot-Swap $A8 MAP, nebenläufig zu einem verlorenen PHI2-Bus-Rennen, den 6502 beim ersten Lesen des Fensters den Open-Bus latchen (One-Shot-Verhalten), danach übernimmt die Bank.

   PHI2-Buszyklus ─┬─ Rennen gewonnen ─► Bank sauber bedient (atomar)
                    │
                    └─ Rennen VERLOREN ─► 1. Lesen = OPEN-BUS (gelatchter Wert)
                                        dann ─► Bank   (One-Shot konsumiert)
   (nutzt loci_mia_serve_lost_sampled + geseedeter Jitter wieder, deterministisch)
Tearing-Modell: verlorenes PHI2-Rennen → One-Shot-Open-Bus.

Der zugehörige Test ist selbstdiagnostizierend: er prüft zunächst die Vorbedingung (das Rennen ist tatsächlich verloren), dann dass das Modell das Tearing-Flag scharfstellt, dann dass der One-Shot konsumiert wird. Ein unvollständiger Build scheitert nunmehr an einer klaren Assertion statt an einem kryptischen torn != pat[0]. Deterministisch (Jitter 0): drei saubere Builds, identische Ergebnisse. test-loci-bank-e2e 8/8.

8. Was die Übung lehrt

Terminal-Ausgabe der test-loci-Suite von Phosphoric: 166 Tests bestanden, 0 Fehler.
Phosphoric bei der Ausführung der Suite test-loci — der headless Prototyping-Prüfstand. Reale Ausgabe des stabilen Zweigs (main, 166/166); die fünf Erweiterungen bringen die Summe auf 170 im Zweig experiment.

Fünf Erweiterungen, fünf minimale Ergänzungen zu einer bestehenden ABI, null Regression beim Standardverhalten. Jede ist reversibel (per Flag), gated auf ENOSYS ohne ihr opt-in, und durch deterministische Tests abgesichert.

Die wahre Lehre liegt vielleicht darin: auf einer Maschine von 1983 besteht die Schwierigkeit nicht darin, moderne Funktionen zu ersinnen, sondern sie aufzupfropfen, ohne das Originalverhalten zu verraten — und es zu beweisen, bevor man den Lötkolben hervorholt.

Der zyklusgenaue Emulator hört auf, ein bloßes spielbares Museum zu sein: er wird zu einem Hardware-Prototyping-Prüfstand. Man schreibt dort die Spezifikation, codiert die Erweiterung, lässt die Tests durchlaufen… und das Silizium kommt erst zuletzt, mit bereits ausgefülltem Abnahmeheft.


Die fünf Erweiterungen leben im Zweig experiment/loci-coproc-acia-reliable von Phosphoric — opt-in, reversibel, außerhalb der stabilen Version. Zum Testen, Kritisieren, Verbessern.

Quellen & Links

← Phosphoric · LOCI-Dokumentation →