Tachibana

Phosphoric › Artículos › Extensiones LOCI

Ampliar los límites del Oric: cinco extensiones experimentales para la tarjeta LOCI

Cómo un emulador cycle-exact sirve de banco de pruebas de hardware — con la ABI, los registros y los esquemas.

Por bmarty · Phosphoric

Acerca de las fotos. Las ilustraciones de la tarjeta LOCI proceden de la documentación de hardware oficial de sodiumlb (loci-hardware) y pertenecen a su autor.

1. El contexto de hardware

La tarjeta LOCI en su carcasa impresa, vista desde arriba: botones Acción (rojo), Reset y actualización de firmware.
La tarjeta LOCI (RP2040/RP2350 + MIA) en su carcasa — botones Acción, Reset y actualización de firmware. Ilustración: sodiumlb / loci-hardware.

El Oric-1 y el Atmos (1983) se basan en un MOS 6502 a 1 MHz, con 64 KB de espacio de direcciones, cuya parte alta ($C000-$FFFF) está ocupada por la ROM BASIC. Sin multiplicación cableada, sin bancos de memoria, sin almacenamiento moderno: todo pasa por el bus de expansión en la parte trasera de la máquina.

La tarjeta LOCI (Lovely Oric Computer Interface) de sodiumlb se conecta a ese bus. Técnicamente, es un derivado del Picocomputer 6502 (RP6502): un microcontrolador RP2040 (una Raspberry Pi Pico) hace las veces de un MIA (Media Interface Adapter) que se expone al Oric como un periférico de entrada/salida. Emula unidades de disquete y cintas, gestiona una tarjeta SD y hace de host USB — es a través de ese puerto USB por donde se conecta un módem Wi-Fi (dongle aparte, el PicoWiFiModemUSB) o un periférico HID (ratón, mando); el Wi-Fi no está integrado en la propia tarjeta LOCI. Dos superficies de direccionamiento nos interesan aquí:

Conector del bus de expansión Oric de la tarjeta LOCI, con el pin Pin 1 señalado.
El conector del bus de expansión Oric de la LOCI (pin Pin 1 señalado) — es por ahí por donde la tarjeta se enchufa en la parte trasera de la máquina. Ilustración: sodiumlb / loci-hardware.
       Espacio de direcciones Oric (64 KB)
   $0000 ┌──────────────────────────────┐
         │ RAM                          │
   $0300 ├──────────────────────────────┤
         │ VIA 6522        $0300-$030F  │
   $0310 │ Microdisc WD1793 $0310-$031F │
         ├──────────────────────────────┤
   $0380 │ ACIA 6551 (LOCI) $0380-$0383 │ ← consola serie / módem
         ├──────────────────────────────┤
   $03A0 │ MIA (LOCI)      $03A0-$03BF  │ ← ventana API 32 bytes
         ├──────────────────────────────┤
   $C000 │ ROM BASIC       $C000-$FFFF  │ ← objetivo del overlay de banco
   $FFFF └──────────────────────────────┘
Mapeo I/O del Oric visto por la tarjeta LOCI.

El emulador Phosphoric (cycle-exact, C11) reproduce fielmente esta tarjeta, lo que permite un enfoque poco común: escribir la especificación de una extensión, programarla y validarla mediante pruebas deterministas — antes de tocar el soldador. Un detalle que cuenta: el autor posee una tarjeta LOCI, pero no un Oric. El banco de pruebas por software no es un lujo, es la única sala de ensayos.

2. La ABI fastcall — cómo el 6502 llama a la tarjeta

Todo se basa en una ventana de 32 registros en $03A0-$03BF (el MIA). He aquí el mapa real de los registros usados por la ABI (nombres fieles al firmware):

 Offset  Dirección Registro          Función
 ------  -------  ----------------  ------------------------------------------
  $00    $03A0    CONS_FLAGS        bit7 = TX libre, bit6 = RX listo
  $01    $03A1    CONS_TX           escritura consola (UART)
  $02    $03A2    CONS_CHAR         lectura consola (consume el byte)
  $0C    $03AC    API_STACK         puntero de xstack (pila de argumentos)
  $0D    $03AD    API_ERRNO_LO      errno bajo
  $0E    $03AE    API_ERRNO_HI      errno alto
  $0F    $03AF    API_OP            ← ESCRIBIR AQUÍ dispara la operación
  $12    $03B2    BUSY              bit7 = tarjeta ocupada
  $14    $03B4    API_A             valor de retorno A
  $16    $03B6    API_X             valor de retorno X
  $18    $03B8    API_SREG          retorno 16 bits (SREG)
Registros MIA de la ABI (extraídos de include/io/loci.h).

El protocolo de llamada (fastcall) consta de cuatro pasos:

   6502 (Oric)                         MIA (LOCI, µC)
   ───────────                         ──────────────
   1. push args ──► xstack ($03AC)
   2. set A/X (parámetros directos)
   3. write op ──► API_OP ($03AF) ───► dispara el handler
                                       ├─ BUSY=1
      poll BUSY ($03B2) ◄──────────────┤  ejecuta
                                       └─ BUSY=0, rellena API_A/X/SREG, ERRNO
   4. read API_A/API_X ($03B4/$03B6) ◄─ resultado
Secuencia de una llamada fastcall LOCI.

Cada valor de opcode aún libre es un punto de entrada para una nueva función. Las operaciones estándar van de $01 a $98 (reloj, open/read/lseek, directorios, montaje de imágenes, TAP…). Es en los opcodes sin usar donde se alojan nuestras cinco extensiones:

  $A7  SET_BANK        banco conmutable 16 KB       (--loci-bank)
  $A8  STREAM_BANK     streamer de assets           (--loci-bank)
  $A9  MATH            coprocesador aritmético      (--loci-coproc)
  $AA  ACIA_RELIABLE   modo ACIA fiable (seqlock)   (modo opt-in)
       + acia_stat_checked : handshake RX lossless  (--loci-acia-rx-nag)

Salvaguarda by design. Sin el indicador de activación correspondiente, el opcode devuelve ENOSYS (errno 13) — exactamente como un firmware sin parchear. El software Oric puede, por tanto, detectar la presencia de la extensión y recurrir a sus propias rutinas. El comportamiento por defecto de la LOCI sigue siendo estrictamente el del hardware original.

3. Coprocesador aritmético — $A9

El problema. El 6502 no tiene multiplicación, ni división, ni coma flotante cableadas. Cada operación es una rutina de software: lenta, voluminosa, costosa en ciclos. En una máquina a 1 MHz, una multiplicación 16×16 se cuenta en cientos de ciclos.

La idea. Delegar el cálculo en el microcontrolador de la tarjeta, mucho más rápido, a través de la ABI fastcall existente — un único opcode $A9, el subcódigo de operación en API_A, los operandos en la xstack, el resultado en API_A/SREG.

   ; ejemplo conceptual : A×B mediante el coprocesador
   LDA #<op_mul  : STA API_A     ; subcódigo de operación
   ... push A, B en la xstack ($03AC)
   LDA #$A9      : STA API_OP     ; dispara MATH
   ; poll BUSY, luego leer el resultado 32 bits en SREG

La implementación. Un archivo aislado, src/io/loci_math.c (op_math), conectado al dispatch. Enteros, coma flotante, operaciones vectoriales. Gestionado por --loci-coproc. Cobertura: 23 pruebas deterministas (vectores enteros, coma flotante, casos límite). Cero aleatoriedad — mismas entradas, mismas salidas, condición de un banco reproducible.

4. Modo ACIA fiable — $AA (seqlock + ACK)

Puerto USB-C host de la tarjeta LOCI, resaltado, en el canto de la carcasa.
El puerto USB-C host de la LOCI — es ahí donde se conecta el dongle módem Wi-Fi (PicoWiFiModemUSB), el que satura la ACIA a alta velocidad. Ilustración: sodiumlb / loci-hardware.

El problema. La ACIA 6551 real tiene una pega conocida: si el 6502 no lee el registro de datos a tiempo, el byte recibido queda sobrescrito por el siguiente. A alta velocidad — un módem Wi-Fi, por ejemplo — se pierden bytes, y el enlace se vuelve inutilizable. Es fiel al silicio, pero limitante.

La solución: un seqlock. Un contador de secuencia de recepción y un acuse por parte del 6502. El byte solo se consume una vez confirmado — nunca se pierde, aunque la lectura se retrase o falle.

     Recepción clásica 6551 (destructiva)
     ─────────────────────────────────────
     RX byte1 ──► RDR   (el 6502 no ha leído…)
     RX byte2 ──► RDR   ✗ byte1 SOBRESCRITO, perdido

     Modo fiable $AA (seqlock + ACK)
     ───────────────────────────────
        RXSEQ $0384  (contador, incrementado en cada byte presentado)
        RXACK $0385  (acuse escrito por el 6502)

     RX byte1 ─► presenta, RXSEQ++          consumido := (RXACK == RXSEQ)
     6502 lee byte1, escribe RXACK = RXSEQ ─► byte1 confirmado → avanza
     RX byte2 ─► presenta solo si confirmado  ✓ ningún byte perdido
Recepción 6551 destructiva vs modo fiable por seqlock.

El canal de emisión permanece inalterado (siempre fiable). Estado gestionado por loci_t (acia_reliable, acia_rx_seq, acia_rx_presented), opcode $AA, activación mediante API_A bit0. Cobertura: +7 pruebas (test-loci-acia-miss, 13 → 20) — DATA no destructivo, consumo ACK-gated, seqlock multi-byte en orden, y sobre todo supervivencia a un fallo de lectura.

4bis. Handshake RX losslessacia_stat_checked (--loci-acia-rx-nag)

Refinamiento adyacente, calcado del firmware real (feature/acia-rx-lossless). En la LOCI real, la /IRQ de la ACIA es una señal de nivel, no un pulso. Modelamos ese nivel mediante un «nag»: mientras el byte no se haya leído (stat_checked falso), la interrupción se reemite periódicamente (por defecto: cada 1000 ciclos), y luego se calla en cuanto el 6502 ha consultado el registro de estado.

   RDRF=1 (byte disponible) ─┐
                             │  nag: deassert+assert /IRQ cada 1000 cic
   /IRQ  ▁▔▁▔▁▔▁▔▁▔▁▔▁▔▁▔    │  mientras RDRF && !stat_checked && RX-IRQ activada
                             │
   6502 lee STATUS ──────────┘  stat_checked = true  ──►  /IRQ en silencio
El «nag» modela la IRQ de nivel del 6551 real.

Sin --loci-acia-rx-nag, la ACIA sigue siendo estrictamente una 6551 (sin nag). Cobertura en test-loci-acia-miss: nag observado antes del acuse, silencio después, búfer vacío ⇒ ninguna IRQ.

5. Banco conmutable de 16 KB — $A7 (--loci-bank)

El problema. ¿Cómo dar más memoria a una máquina cuyo espacio está saturado por la ROM?

La solución. Superponer temporalmente 16 KB de la RAM de la tarjeta (xram) en la ventana $C000-$FFFF, allí donde reside la ROM.

        $A7 desactivado               $A7 EN | SEL=n
   $C000 ┌───────────┐          $C000 ┌───────────────┐
         │ ROM BASIC │   ─────►        │ xram[n*0x4000]│  overlay (lectura)
   $FFFF └───────────┘          $FFFF  └───────────────┘
                                       └─ ROM intacta DEBAJO (no sobrescrita)
   base xram = SEL * 0x4000 ; SEL acotado a 0..3 (como mia_set_bank)
Overlay de banco no destructivo sobre la ventana ROM.

El punto clave: overlay no destructivo. El banco toma prioridad en lectura (y para la inspección de memoria) sin sobrescribir jamás la tabla ROM. Una desactivación restaura la máquina byte a byte. El antiguo enfoque prototipo por memcpy + copia de seguridad se abandonó en favor de esta superposición limpia (memory_set_loci_bank() en memory.c).

Doble modo mediante reset. La activación por $A7 EN dispara un reset: el 6502 relee su vector $FFFC desde el banco (se puede, por tanto, arrancar código de banco). El hot-swap (usado por el streamer $A8) conmuta, por el contrario, sin reset de la CPU — requisito absoluto del doble búfer. Cobertura: test-loci 170/170 (+4: enable/estado, disable, gestionado OFF → ENOSYS, clamp SEL 15→3) y un test-loci-bank-e2e de extremo a extremo.

6. Streamer de assets — $A8

La idea. Una vez el banco en su sitio, volcar en él datos desde un archivo (flash o tarjeta SD) en un solo fastcall: lseek(SEEK_SET) + read → banco 16 KB, con mapeo opcional en $C000-$FFFF. Un software Oric puede entonces superar los 48 KB útiles: overlays, decorados, niveles bajo demanda.

   Doble búfer (superar 48 KB sin reset)
   ─────────────────────────────────────────────
   $A8 MAP=0 SEL=1 ─► precarga el banco 1 (invisible)     ┐ durante
   (el 6502 sigue ejecutando/mostrando el banco 0)         ┘ ese tiempo
   $A8 MAP=1 SEL=1 ─► conmuta banco 1 en $C000 (hot-swap, PC intacto)
Streaming + doble búfer de assets.

Detalles: API_A bit7 = MAP, bits3:0 = SEL (0..3 válidos ; >3 = EINVAL, sin clamp a diferencia de $A7). Argumentos en la xstack LIFO (len, dst, off, fd), escritura acotada al tamaño de banco, retorno AX = bytes leídos. Dos vías de lectura: archivo host e imagen SD. Reutiliza el opt-in --loci-bank.

7. El modelo de tearing — la cuestión de los dos núcleos

La extensión más sutil, y la más honesta intelectualmente.

La cuestión. Cuando se conmuta un banco mientras un ciclo de bus está en curso, ¿qué latchea el 6502? En el emulador mono-hilo, el swap es atómico: invisible, la cuestión no se plantea. Pero el hardware real tiene dos núcleos; un swap simultáneo a un acceso a memoria puede producir un tearing.

La respuesta: modelar explícitamente el peor caso. Con --loci-bank-tearing, un hot-swap $A8 MAP simultáneo a una carrera de bus PHI2 perdida hace latchear el open-bus al 6502 en la primera lectura de la ventana (comportamiento one-shot), y luego el banco toma el relevo.

   Ciclo bus PHI2  ─┬─ carrera ganada ─► banco servido limpiamente (atómico)
                    │
                    └─ carrera PERDIDA ─► 1ª lectura = OPEN-BUS (valor latcheado)
                                         luego ─► banco   (one-shot consumido)
   (reutiliza loci_mia_serve_lost_sampled + jitter con semilla, determinista)
Modelo de tearing: carrera PHI2 perdida → open-bus one-shot.

La prueba asociada es autodiagnóstica: verifica primero la precondición (que la carrera se pierde efectivamente), luego que el modelo arma el indicador de tearing, y luego que el one-shot se consume. Un build incompleto ahora falla con una aserción clara en lugar de un críptico torn != pat[0]. Determinista (jitter 0): tres builds limpios, resultados idénticos. test-loci-bank-e2e 8/8.

8. Lo que enseña el ejercicio

Salida de terminal de la suite test-loci de Phosphoric: 166 pruebas superadas, 0 fallos.
Phosphoric ejecutando la suite test-loci — el banco de prototipado headless. Salida real de la rama estable (main, 166/166); las cinco extensiones llevan el total a 170 en la rama experiment.

Cinco extensiones, cinco adiciones mínimas a una ABI existente, cero regresión en el comportamiento por defecto. Cada una es reversible (indicador mediante), gestionada ENOSYS sin su opt-in, y respaldada por pruebas deterministas.

La verdadera lección quizá esté ahí: en una máquina de 1983, la dificultad no es imaginar funciones modernas, sino injertarlas sin traicionar el comportamiento original — y probarlo antes de sacar el soldador.

El emulador cycle-exact deja de ser un simple museo jugable: se convierte en un banco de prototipado de hardware. En él se escribe la especificación, se programa la extensión, se pasan las pruebas… y el silicio solo llega en último lugar, con el cuaderno de recepción ya cumplimentado.


Las cinco extensiones viven en la rama experiment/loci-coproc-acia-reliable de Phosphoric — opt-in, reversibles, fuera de la versión estable. Para probar, criticar y mejorar.

Fuentes y enlaces

← Phosphoric · Documentación LOCI →