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.
Über die Fotos. Die Abbildungen der LOCI-Karte stammen aus der offiziellen Hardware-Dokumentation von sodiumlb (loci-hardware) und gehören ihrem Autor.
- LOCI-Karte, Produktansichten: raxiss.com
- Händlerseite: Tindie — 8BitClub
- Entwicklungs-Thread: forum.defence-force.org t=2593
1. Der Hardware-Kontext
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:
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 └──────────────────────────────┘
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)
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
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)
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
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
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)
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)
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)
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
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
- LOCI-Firmware / -Hardware (sodiumlb): loci-firmware, loci-hardware, loci-rom
- Benutzerhandbuch (FR): LOCI-Wiki, ceo.oric.org
- Händler & Fotos: RAXISS, Tindie
- Entwicklungs-Thread: forum.defence-force.org
- Emulator Phosphoric: github.com/benedictemarty/Phosphoric, LOCI-Dokumentation auf dieser Website