JESVS

BMAmiga: una Amiga A500 emulada desde cero, directo sobre el metal de una Raspberry Pi

Enciende una Raspberry Pi con nuestra imagen y esto es todo lo que pasa: el firmware de VideoCore carga kernel8.img —un archivo de ~2 MB— y lo siguiente que se ejecuta es un intérprete de 68000. Sin Linux. Sin gestor de arranque. Sin init. Sin compositor, sin planificador, sin una pila USB con polling de milisegundos. La primera cosa en la que se convierte esa Raspberry Pi, antes de que acabes de pestañear, es una Amiga A500.

Se llama BMAmiga y está escrito desde cero: el CPU, Agnus, Denise, Paula, los dos CIA, el copper, el blitter, la disquetera con MFM crudo, el IDE de Gayle. Todo el chipset. Lo único que no escribimos nosotros es la capa de plataforma (Circle, el mismo entorno que usa BMC64) y el Kickstart, que es material con copyright y jamás va a vivir en este repositorio — tú pones el tuyo.

Por qué sin sistema operativo: la latencia

Un emulador hospedado en Linux arrastra un planificador, un compositor, una cadena de video que no controlas y una pila USB de escala milisegunda: entre que presionas una tecla y ves el píxel pasan típicamente 3 a 5 frames. Sobre el metal, tú eres el dueño del flip del framebuffer y del camino de entrada, y puedes bajar de un frame, consistente. Ese es el punto entero del proyecto. Si no te importa, corre Amiberry sobre Raspberry Pi OS y esta misma tarde tienes un emulador mejor — en serio. Esto existe para la gente a la que le importa el frame.

Qué hay sobre la mesa

  • Un 68000 completo, interpretado. Todos los modos de direccionamiento, BCD, MOVEM, MOVEP, stacks de supervisor, traps, interrupciones autovectorizadas. Hace 40–60 millones de instrucciones por segundo en un solo núcleo Cortex-A53; una 68000 a 7 MHz necesita alrededor de un millón. Es 40× de margen para gastarlo en lo que de verdad decide si el software arranca: corrección.
  • Agnus con arbitraje de bus slot-exacto. Un slot de bus por color clock, concedido en el orden de prioridad del hardware: bitplanes > refresh > disco > audio > sprites > copper > blitter > CPU, con las posiciones fijas de DMAL que marca el manual. El blitter no “toma N ciclos”: su tiempo de bus se replaya slot por slot, así que un blit bajo una pantalla de 6 bitplanes se estira al doble, exactamente como en el bus compartido real — y el CPU pelea por el bus chip y pierde, mediblemente, igual que en la máquina de carne y hueso.
  • Denise completa: 1 a 6 bitplanes, lores y hires, dual playfield, EHB, HAM, los 8 sprites con pares attached de 16 colores, prioridad sprite/playfield. Las barras raster y los sprites partidos a media línea se ven, porque cada escritura de registro se registra con el color clock en el que aterrizó y cada línea se pinta en segmentos.
  • El copper a nivel de ciclo y el blitter con sus 256 minterms, barrel shifts, máscaras, modos de relleno y el modo línea con su lógica de octantes saliendo de SUD/SUL/AUL como lo hace el silicio.
  • Paula: los cuatro canales de audio con la cadencia real de bytes, la regla de bloque, los clamps de periodo y volumen, la modulación cruzada de ADKCON — un porte semántico de la máquina de estados de WinUAE, que usamos como documentación ejecutable de solo lectura.
  • Disquetera en MFM crudo. trackdisk.device no pide sectores: lee el flujo de bits de la cabeza y lo decodifica en software. Por eso sintetizamos marcas de sincronía, splits odd/even y checksums exactamente como los lleva un disco físico. Servir “sectores de 512 bytes” simplemente no funcionaría. El camino de escritura corre lo mismo al revés.
  • Gayle IDE: un hardfile en la tarjeta SD se conecta como disco duro IDE, y la Kickstart 3.1 arranca desde él directo al escritorio de Workbench. El IRQ de la unidad es un cable al nivel 2 del 68000 — modelarlo como un bit de Paula te compra un interruptor perdido o una tormenta de interrupciones, tú eliges.
  • USB desde el controlador: un porte fiel a registros del DWC2, HID en boot protocol, hubs, dongles combo teclado+mouse. Nada de drivers del sistema — no hay sistema.
  • Menú F12: pausa la máquina, lista ROMs y ADFs de la tarjeta, cambia discos en caliente con /DSKCHG como un cambio de disquete físico, crea hardfiles nuevos, reinicia en frío.

Y para que nada de esto sea una afirmación vacía: el disco de benchmark es un disquete de arranque escrito en ensamblador 68000 a mano, que se auto-cronometra con el reloj de raster de la propia máquina (vblcount × 313

  • TOD de la CIA-B) e imprime su puntuación como porcentaje de una A500 real de 7.09 MHz. El total ronda el 89%, y donde no llegamos a 100% medimos que una A500 real tampoco llega: nuestro CPU-MULDIV da 115% — el mismo número exacto que da FS-UAE en su modo de máxima exactitud — y el MOVEM da 39%, el mismo 39% que mide FS-UAE. Cuando tu benchmark y el referente coinciden hasta en el número raro, sabes que el modelo de timing es el correcto. La cadencia de audio y del TOD, clavada al 99%.

Lo mejor del método: el 90% de la depuración ocurre en la laptop. El core es C portable sin una sola dependencia de la Pi — todo lo de plataforma pasa por una docena de funciones — así que make test corre el emulador completo de forma nativa, con suites de CPU, chipset, USB simulado a nivel de registro (un simulador DWC2 que juega el bus con data toggles y split-transactions estrictos) y hasta un tracer que arranca ROMs reales y vuelca el estado de exec. La suite de USB cazó en segundos que los registros de punteros del blitter están ordenados C, B, A, D en el chip — encontrar eso reflasheando tarjetas SD habría costado una tarde.

Lo que el Amiga nos cobró

Cada balín de esta lista costó días, y cada uno enseña algo sobre por qué emular bien esta máquina es un deporte de precisión:

  • El reloj de sistema de KS 1.3 es el contador TOD de la CIA-A. Con un TOD avanzando a ritmo de E-clock, el reloj corría 14,188 veces más rápido y la detección NTSC/PAL se rompía. La ROM no usa “un RTC”: lee un contador específico con una aritmética específica.
  • El terminador de la copper list (WAIT $FFDF..$FFFE/$FFFE) parquea el copper hasta el vblank. Un decodificador ingenuo despierta cuando vpos & 0xFF == 0xFF — y como PAL tiene 313 líneas, pasa por 255 a media pantalla y ejecuta chip RAM como instrucciones de copper. Ese fue un wedge de arranque de los que te hacen dudar de todo.
  • KS 1.3 carga los bootblocks en un buffer dinámico sobre un stack casi agotado. Un bootblock de verdad debe cambiarse a su propio stack antes de hacer nada pesado. Nada te lo dice; hay que desensamblar el dispatcher a 0xFE9C3E para descubrirlo.
  • La palabra serie del teclado lleva el key-up en el bit 7, empaquetada con rotación e inversión. Y la ventana del handshake de power-up tiene que cerrarse a tiempo, o KS 1.3 trata las teclas nuevas como retenidas desde el encendido y el input.device descarrila — la infame “pantalla verde”.
  • El IDE responde LSB-first en IDENTIFY: si el driver parsea cero cabezas y sectores, es porque tus palabras están en el orden equivocado. Y el slot esclavo vacío tiene que contestar — un esclavo mudo cuelga el driver de la ROM en un sleep sin timeout.

Lo que la Raspberry Pi nos cobró

  • El piso de 600 MHz. Sin governor de cpufreq, el firmware parquea el ARM a los 600 MHz de reposo un minuto después del arranque — sin bandera de throttling ninguna, porque no es throttling: es política. Lo cazamos con instrumentación propia: un panel térmico en pantalla graficando el reloj real derivado del contador de PMU contra el timer de 1 MHz, con la línea roja de los 80 °C. arm_freq_min=1000 clava el piso, y la Zero 2 W sostiene 1 GHz a ~51 °C bajo carga completa — PAL a 50 fps reales, sostenidos.
  • El audio HDMI fue una novela de detectives. Un tono entrecortado, y fuimos matando sospechosos por medición: el modo de video (probado con la blanking normal y reducida), el CTS (externo y luego interno, como el driver vc4 de Linux), la pila completa (flasheamos el sample de audio de Circle sin modificar como control A/B: su tono sonaba limpio). El culpable: el pump sondeado por CPU no podía mantener cebado el FIFO MAI de poca profundidad contra la demanda por muestra del packetizer. La solución fue DMA + DREQ. De paso: el FIFO “Full” que casi nunca se activaba era ficción — los writes de más se botaban en silencio. Y un bug de regalo: escribíamos pend[0] para ambos subframes — el canal derecho jamás se transmitió y nadie lo notó porque era dual-mono.
  • El toolchain muerde. El core está clavado a -O3 porque a -O2 el handshake de disco de la Kickstart se comporta mal (nunca root-causado). -mstrict-align en CFLAGS y CPPFLAGS, porque clang -O3 vectoriza fills en parejas STP Q que fallan en direcciones AArch64 sin alinear. El LTO whole-program borró en silencio un store a un global — el constructor asignaba el handle, el binario linkeado nunca lo hacía, y cada callee se veía correcto en el disassembly. Regla de la casa: verifica el output con nm y objdump, jamás confíes en la forma del fuente.
  • El heap de Circle se traga los bloques grandes: su free solo recicla tamaños que caen en un bucket, y todo lo que se pasa se esfuma. Un browser mandando 50 POSTs de mouse por segundo le prendió fuego al heap de un gigabyte en menos de un minuto. Y los sockets TCP de Circle copian la IP local en el constructor — un socket creado antes del DHCP valida cada segmento contra 0.0.0.0 y responde RST a todo. DHCP fino, ping fino, TCP muerto. Sin documentación que te advierta ninguna de las dos.

La Zero 2 W que dio su vida

La bench original fue una Raspberry Pi Zero 2 W: un solo núcleo, sin Ethernet, sin monitor serial — toda la telemetría dibujada en pantalla sobre el video emulado, y el feedback llegaba en fotografías del TV. Esa placa aguantó meses de flashes y cargas a fondo… hasta que se quemó. Literal. Un final honesto para una computadora que vivió siendo otra computadora.

La bench ahora es una Pi 4B, y el flujo de trabajo dio un salto generacional: la Pi es el servidor. Arranca, toma DHCP, y publica su propia IP en pantalla. Desde el navegador flasheas una imagen nueva (tools/httpflash.sh) y la Pi se instala y se reinicia sola. Mientras emula, sirve una consola web en /emu: capturas de frames en vivo, mouse, teclado, los botones que el navegador secuestra — el menú F12 completo, manejable desde la otra punta de la casa. La misma página sube AFDs y ROMs directo a la tarjeta, con la máquina aplicándolos al vuelo: el disquete cae en DF0, la ROM power-cyclea la máquina. Y cuando no hay red, el mismo lead serial que responde la IP también reflashea el kernel con un protocolo stop-and-wait propio, verificado por md5, a ~3 minutos por imagen — sin TV, sin tarjeta, sin piedad. Un watchdog de hardware reincia la bench colgada en 15 segundos. Nada de esto necesita Linux: es un emulador de Amiga que de paso implementa HTTP, FAT32 a mano y su propio protocolo de flashing.

Dónde estamos y hacia dónde vamos

El estado hoy: una A500 funciona. Kickstart 1.3 y 3.1 arrancan Workbench desde disquete y desde disco duro; teclado y mouse USB responden; el menú cambia todo en caliente; la consola web deja manejar la máquina remota. No es un juguete de demo — es una máquina en la que se puede trabajar (y jugar, que también cuenta).

Seamos honestos también con lo que falta: el blitter deposita su resultado funcional de golpe mientras replaya su ocupancia de bus slot por slot; los gamepads USB están pendientes; hay un vector del benchmark que todavía quiere pelearse. Lo importante es que el esqueleto de timing — el arbitraje slot-exacto, el bus compartido, la cadencia real de cada canal — ya está puesto. Sobre esa base, lo demás es construir habitaciones.

Y esa es exactamente la dirección:

Un emulador bare-bones — sin nada debajo, sin grasa — que emule todas las Amigas: OCS, ECS y AGA, de la A500 a la A4000, con performance de sobra y timing frame-perfect.

Frame-perfect no es marketing: es la consecuencia natural de haber modelado el bus slot por slot desde el día uno, en vez de parchear ciclos hasta que el software deje de quejarse. Cuando el modelo es el hardware, la exactitud no es una feature — es el estado base. Para llegar a las máquinas de 68020 en adelante hay margen de sobra en el intérprete, y si un día el JIT llama (estilo Emu68, que ya probó que este hardware se lo merece), el chipset no tendrá que cambiar ni una línea para acompañarlo.

Nos vemos en el siguiente frame. A 50 Hz, puntuales.

← volver a posts