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.deviceno 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 cuandovpos & 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
0xFE9C3Epara 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=1000clava 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
-O3porque a-O2el handshake de disco de la Kickstart se comporta mal (nunca root-causado).-mstrict-alignen 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
freesolo 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