Tachibana

Phosphoric › Artikelen › LOCI-uitbreidingen

De grenzen van de Oric verleggen: vijf experimentele uitbreidingen voor de LOCI-kaart

Hoe een cycle-exacte emulator dienstdoet als hardware-testbank — met de ABI, de registers en de schema's.

Door bmarty · Phosphoric

Over de foto's. De illustraties van de LOCI-kaart zijn afkomstig uit de officiële hardwaredocumentatie van sodiumlb (loci-hardware) en behoren hun auteur toe.

1. De hardwarecontext

De LOCI-kaart in haar geprinte behuizing, bovenaanzicht: Action-knop (rood), Reset en firmware-update.
De LOCI-kaart (RP2040/RP2350 + MIA) in haar behuizing — knoppen Action, Reset en firmware-update. Illustratie: sodiumlb / loci-hardware.

De Oric-1 en de Atmos (1983) draaien op een MOS 6502 op 1 MHz, met 64 KB adresruimte waarvan het bovenste deel ($C000-$FFFF) wordt ingenomen door de BASIC-ROM. Geen bedrade vermenigvuldiging, geen geheugenbanken, geen moderne opslag: alles loopt via de uitbreidingsbus achter in de machine.

De LOCI-kaart (Lovely Oric Computer Interface) van sodiumlb wordt op deze bus aangesloten. Technisch gezien is het een afgeleide van de Picocomputer 6502 (RP6502): een microcontroller RP2040 (een Raspberry Pi Pico) vervult de rol van een MIA (Media Interface Adapter) die zich aan de Oric presenteert als een in-/uitvoerapparaat. Hij emuleert diskettedrives en cassettes, beheert een SD-kaart en fungeert als USB-host — via deze USB-poort wordt een Wi-Fi-modem (aparte dongle, de PicoWiFiModemUSB) of een HID-apparaat (muis, controller) aangesloten; Wi-Fi zit niet ingebouwd op de LOCI-kaart zelf. Twee adresseringsvlakken zijn hier voor ons van belang:

Connector van de Oric-uitbreidingsbus van de LOCI-kaart, met Pin 1 gemarkeerd.
De connector van de Oric-uitbreidingsbus van de LOCI (Pin 1 gemarkeerd) — hier steekt de kaart achter in de machine. Illustratie: sodiumlb / loci-hardware.
         Oric-adresruimte (64 KB)
   $0000 ┌──────────────────────────────┐
         │ RAM                          │
   $0300 ├──────────────────────────────┤
         │ VIA 6522        $0300-$030F  │
   $0310 │ Microdisc WD1793 $0310-$031F │
         ├──────────────────────────────┤
   $0380 │ ACIA 6551 (LOCI) $0380-$0383 │ ← seriële console / modem
         ├──────────────────────────────┤
   $03A0 │ MIA (LOCI)      $03A0-$03BF  │ ← API-venster 32 bytes
         ├──────────────────────────────┤
   $C000 │ ROM BASIC       $C000-$FFFF  │ ← doel van de bank-overlay
   $FFFF └──────────────────────────────┘
I/O-mapping van de Oric zoals gezien door de LOCI-kaart.

De emulator Phosphoric (cycle-exact, C11) reproduceert deze kaart getrouw, wat een zeldzame aanpak mogelijk maakt: de spec van een uitbreiding schrijven, ze coderen en ze valideren met deterministische tests — voordat je de soldeerbout aanraakt. Een detail dat telt: de auteur bezit een LOCI-kaart, maar geen Oric. De softwaretestbank is geen luxe, het is de enige testruimte.

2. De fastcall-ABI — hoe de 6502 de kaart aanroept

Alles berust op een venster van 32 registers op $03A0-$03BF (de MIA). Hier is de werkelijke kaart van de registers die door de ABI worden gebruikt (namen getrouw aan de firmware):

 Offset  Adres    Register          Rol
 ------  -------  ----------------  ------------------------------------------
  $00    $03A0    CONS_FLAGS        bit7 = TX vrij, bit6 = RX gereed
  $01    $03A1    CONS_TX           console schrijven (UART)
  $02    $03A2    CONS_CHAR         console lezen (verbruikt de byte)
  $0C    $03AC    API_STACK         xstack-pointer (argumentenstack)
  $0D    $03AD    API_ERRNO_LO      errno laag
  $0E    $03AE    API_ERRNO_HI      errno hoog
  $0F    $03AF    API_OP            ← HIER SCHRIJVEN start de operatie
  $12    $03B2    BUSY              bit7 = kaart bezig
  $14    $03B4    API_A             retourwaarde A
  $16    $03B6    API_X             retourwaarde X
  $18    $03B8    API_SREG          16-bits retour (SREG)
MIA-registers van de ABI (uittreksel uit include/io/loci.h).

Het aanroepprotocol (fastcall) verloopt in vier stappen:

   6502 (Oric)                         MIA (LOCI, µC)
   ───────────                         ──────────────
   1. push args ──► xstack ($03AC)
   2. set A/X (directe parameters)
   3. write op ──► API_OP ($03AF) ───► start de handler
                                       ├─ BUSY=1
      poll BUSY ($03B2) ◄──────────────┤  voert uit
                                       └─ BUSY=0, vult API_A/X/SREG, ERRNO
   4. read API_A/API_X ($03B4/$03B6) ◄─ resultaat
Volgorde van een LOCI fastcall-aanroep.

Elke nog vrije opcodewaarde is een toegangspunt voor een nieuwe functie. De standaardoperaties lopen van $01 tot $98 (klok, open/read/lseek, mappen, image mounten, TAP…). In de ongebruikte opcodes nestelen zich onze vijf uitbreidingen:

  $A7  SET_BANK        schakelbare bank 16 KB       (--loci-bank)
  $A8  STREAM_BANK     asset-streamer               (--loci-bank)
  $A9  MATH            rekencoprocessor             (--loci-coproc)
  $AA  ACIA_RELIABLE   betrouwbare ACIA-modus (seqlock)  (opt-in-modus)
       + acia_stat_checked : lossless RX-handshake   (--loci-acia-rx-nag)

Vangrail by design. Zonder de bijbehorende activeringsvlag geeft de opcode ENOSYS terug (errno 13) — precies zoals een niet-gepatchte firmware. De Oric-software kan dus de aanwezigheid van de uitbreiding detecteren en terugvallen op haar eigen routines. Het standaardgedrag van de LOCI blijft strikt dat van de oorspronkelijke hardware.

3. Rekencoprocessor — $A9

Het probleem. De 6502 heeft geen bedrade vermenigvuldiging, deling of zwevende komma. Elke operatie is een softwareroutine: traag, omvangrijk, kostbaar in cycli. Op een machine van 1 MHz telt een vermenigvuldiging 16×16 honderden cycli.

Het idee. De berekening delegeren aan de microcontroller van de kaart, die veel sneller is, via de bestaande fastcall-ABI — één enkele opcode $A9, de operatie-subcode in API_A, de operanden op de xstack, het resultaat in API_A/SREG.

   ; conceptueel voorbeeld: A×B via de coprocessor
   LDA #<op_mul  : STA API_A     ; operatie-subcode
   ... push A, B op de xstack ($03AC)
   LDA #$A9      : STA API_OP     ; start MATH
   ; poll BUSY, lees dan het 32-bits resultaat in SREG

De implementatie. Een geïsoleerd bestand, src/io/loci_math.c (op_math), aangesloten op de dispatch. Integers, zwevende komma, vectoroperaties. Gepoort door --loci-coproc. Dekking: 23 deterministische tests (integer-vectoren, zwevende komma, rand- gevallen). Nul willekeur — dezelfde invoer, dezelfde uitvoer, voorwaarde voor een reproduceerbare testbank.

4. Betrouwbare ACIA-modus — $AA (seqlock + ACK)

USB-C-hostpoort van de LOCI-kaart, omcirkeld, op de rand van de behuizing.
De USB-C-hostpoort van de LOCI — hier wordt de Wi-Fi-modemdongle (PicoWiFiModemUSB) aangesloten, die de ACIA op hoge snelheid verzadigt. Illustratie: sodiumlb / loci-hardware.

Het probleem. De echte ACIA 6551 heeft een bekend gebrek: als de 6502 het dataregister niet op tijd leest, wordt de ontvangen byte overschreven door de volgende. Op hoge snelheid — een Wi-Fi-modem, bijvoorbeeld — gaan bytes verloren, en wordt de verbinding onbruikbaar. Dat is getrouw aan het silicium, maar hinderlijk.

De oplossing: een seqlock. Een sequentieteller voor ontvangst en een bevestiging aan de 6502-zijde. De byte wordt pas verbruikt wanneer hij is bevestigd — nooit verloren, zelfs als het lezen te laat komt of mislukt.

     Klassieke 6551-ontvangst (destructief)
     ─────────────────────────────────────
     RX byte1 ──► RDR   (6502 heeft niet gelezen…)
     RX byte2 ──► RDR   ✗ byte1 OVERSCHREVEN, verloren

     Betrouwbare modus $AA (seqlock + ACK)
     ───────────────────────────────
        RXSEQ $0384  (teller, verhoogd bij elke aangeboden byte)
        RXACK $0385  (bevestiging geschreven door de 6502)

     RX byte1 ─► aanbieden, RXSEQ++          verbruikt := (RXACK == RXSEQ)
     6502 leest byte1, schrijft RXACK = RXSEQ ─► byte1 bevestigd → verder
     RX byte2 ─► aanbieden alleen indien bevestigd  ✓ geen byte verloren
Destructieve 6551-ontvangst vs betrouwbare modus met seqlock.

Het zendkanaal blijft ongewijzigd (altijd betrouwbaar). Toestand gedragen door loci_t (acia_reliable, acia_rx_seq, acia_rx_presented), opcode $AA, activering via API_A bit0. Dekking: +7 tests (test-loci-acia-miss, 13 → 20) — DATA niet-destructief, verbruik ACK-gated, seqlock voor meerdere bytes in volgorde, en vooral overleving van een leesfout.

4bis. Lossless RX-handshake — acia_stat_checked (--loci-acia-rx-nag)

Aangrenzende verfijning, gemodelleerd naar de echte firmware (feature/acia-rx-lossless). Op de echte LOCI is de /IRQ van de ACIA een niveausignaal, geen puls. We modelleren dit niveau met een «nag»: zolang de byte niet is gelezen (stat_checked onwaar), wordt de interrupt periodiek opnieuw uitgezonden (standaard: elke 1000 cycli), en zwijgt zodra de 6502 het statusregister heeft geraadpleegd.

   RDRF=1 (byte klaar) ───┐
                          │  nag: deassert+assert /IRQ elke 1000 cyc
   /IRQ  ▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔ │  zolang  RDRF && !stat_checked && RX-IRQ actief
                          │
   6502 leest STATUS ─────┘  stat_checked = true  ──►  /IRQ stil
De «nag» modelleert de niveau-IRQ van de echte 6551.

Zonder --loci-acia-rx-nag blijft de ACIA strikt een 6551 (geen nag). Dekking in test-loci-acia-miss: nag waargenomen vóór bevestiging, stilte erna, lege buffer ⇒ geen IRQ.

5. Schakelbare bank van 16 KB — $A7 (--loci-bank)

Het probleem. Hoe geef je meer geheugen aan een machine waarvan de adresruimte is verzadigd door de ROM?

De oplossing. Tijdelijk 16 KB van de RAM van de kaart (xram) overlappen in het venster $C000-$FFFF, waar de ROM zetelt.

        $A7 uitgeschakeld             $A7 EN | SEL=n
   $C000 ┌───────────┐          $C000 ┌───────────────┐
         │ ROM BASIC │   ─────►        │ xram[n*0x4000]│  overlay (lezen)
   $FFFF └───────────┘          $FFFF  └───────────────┘
                                       └─ ROM intact ERONDER (niet overschreven)
   basis xram = SEL * 0x4000 ; SEL geklemd op 0..3 (zoals mia_set_bank)
Niet-destructieve bank-overlay op het ROM-venster.

Het kernpunt: niet-destructieve overlay. De bank krijgt voorrang bij het lezen (en voor geheugeninspectie) zonder ooit de ROM-tabel te overschrijven. Een deactivering herstelt de machine byte voor byte. De oude prototype-aanpak met memcpy + back-up is verlaten ten gunste van deze schone overlay (memory_set_loci_bank() in memory.c).

Dubbele modus via reset. De activering door $A7 EN triggert een reset: de 6502 herleest zijn vector $FFFC uit de bank (men kan dus bankcode booten). De hot-swap (gebruikt door de streamer $A8) schakelt daarentegen zonder CPU-reset — een absolute vereiste voor double-buffering. Dekking: test-loci 170/170 (+4: enable/toestand, disable, gepoort OFF → ENOSYS, clamp SEL 15→3) en een test-loci-bank-e2e end-to-end.

6. Asset-streamer — $A8

Het idee. Zodra de bank aanwezig is, er gegevens uit een bestand in storten (flash of SD-kaart) in één enkele fastcall: lseek(SEEK_SET) + read → bank 16 KB, met optionele mapping in $C000-$FFFF. Een Oric-programma kan dan de bruikbare 48 KB overschrijden: overlays, decors, levels op verzoek.

   Double-buffering (48 KB overschrijden zonder reset)
   ─────────────────────────────────────────────
   $A8 MAP=0 SEL=1 ─► laadt bank 1 vooraf (onzichtbaar)   ┐ gedurende
   (de 6502 blijft bank 0 uitvoeren/tonen)                 ┘ deze tijd
   $A8 MAP=1 SEL=1 ─► schakelt bank 1 in $C000 (hot-swap, PC intact)
Streaming + double-buffering van assets.

Details: API_A bit7 = MAP, bits3:0 = SEL (0..3 geldig; >3 = EINVAL, geen clamp in tegenstelling tot $A7). Argumenten op de xstack LIFO (len, dst, off, fd), schrijven begrensd tot de bankgrootte, retour AX = gelezen bytes. Twee leespaden: hostbestand en SD-image. Hergebruikt de opt-in --loci-bank.

7. Het tearing-model — de kwestie van de twee cores

De subtielste uitbreiding, en de intellectueel eerlijkste.

De vraag. Wanneer men een bank schakelt terwijl een buscyclus bezig is, wat latcht de 6502 dan? Op de single-thread-emulator is de swap atomisch: onzichtbaar, de vraag rijst niet. Maar de echte hardware heeft twee cores; een swap gelijktijdig met een geheugentoegang kan tearing veroorzaken.

Het antwoord: het slechtste geval expliciet modelleren. Met --loci-bank-tearing laat een hot-swap $A8 MAP gelijktijdig met een verloren PHI2-busrace de open-bus latchen bij de 6502 op de eerste lezing van het venster (one-shot-gedrag), waarna de bank het overneemt.

   PHI2-buscyclus ─┬─ race gewonnen ─► bank netjes geserveerd (atomisch)
                    │
                    └─ race VERLOREN ─► 1e lezing = OPEN-BUS (gelatchte waarde)
                                        dan   ─► bank   (one-shot verbruikt)
   (hergebruikt loci_mia_serve_lost_sampled + geseede jitter, deterministisch)
Tearing-model: verloren PHI2-race → one-shot open-bus.

De bijbehorende test is zelfdiagnosticerend: hij verifieert eerst de preconditie (de race is inderdaad verloren), dan dat het model de tearing-vlag armt, dan dat de one-shot wordt verbruikt. Een onvolledige build faalt voortaan op een duidelijke assertie in plaats van een cryptische torn != pat[0]. Deterministisch (jitter 0): drie schone builds, identieke resultaten. test-loci-bank-e2e 8/8.

8. Wat de oefening leert

Terminaluitvoer van de test-loci-suite van Phosphoric: 166 tests geslaagd, 0 mislukt.
Phosphoric voert de test-loci-suite uit — de headless prototypeerbank. Werkelijke uitvoer van de stabiele branch (main, 166/166); de vijf uitbreidingen brengen het totaal op 170 op de branch experiment.

Vijf uitbreidingen, vijf minimale toevoegingen aan een bestaande ABI, nul regressie op het standaardgedrag. Elk is omkeerbaar (met vlag), gepoort op ENOSYS zonder zijn opt-in, en onderbouwd met deterministische tests.

De echte les schuilt misschien hierin: op een machine uit 1983 is de moeilijkheid niet het bedenken van moderne functies, maar het enten ervan zonder het oorspronkelijke gedrag te verraden — en het te bewijzen voordat je de soldeerbout tevoorschijn haalt.

De cycle-exacte emulator houdt op een louter speelbaar museum te zijn: hij wordt een hardware-prototypeerbank. Je schrijft er de spec, je codeert er de uitbreiding, je doorstaat er de tests… en het silicium komt pas als laatste, met het acceptatieboek al ingevuld.


De vijf uitbreidingen leven op de branch experiment/loci-coproc-acia-reliable van Phosphoric — opt-in, omkeerbaar, buiten de stabiele versie. Te testen, te bekritiseren, te verbeteren.

Bronnen & links

← Phosphoric · LOCI-documentatie →