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.
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.
- Tarjeta LOCI, vistas de producto: raxiss.com
- Página del distribuidor: Tindie — 8BitClub
- Hilo de desarrollo: forum.defence-force.org t=2593
1. El contexto de 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í:
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 └──────────────────────────────┘
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)
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
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)
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
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 lossless — acia_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
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)
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)
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)
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
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
- Firmware / hardware LOCI (sodiumlb): loci-firmware, loci-hardware, loci-rom
- Manual de usuario (FR): wiki LOCI, ceo.oric.org
- Distribuidor y fotos: RAXISS, Tindie
- Hilo de desarrollo: forum.defence-force.org
- Emulador Phosphoric: github.com/benedictemarty/Phosphoric, documentación LOCI en este sitio