AVISO
Las imágenes y videos que se van a mostrar a continuación, y el texto que las acompañan, no son adecuados para puristas. Rogamos a todos los usuarios que se consideren a sí mismos "puristas" se abstengan de seguir leyendo este post, dado que su contenido puede herir su sensibilidad.
Bueno, sigo. La cosa es que quería entrenarme un poco más en Verilog (para mi tesis) y de paso, acabar de una vez la descripción de la ULA, con vistas a mis próximos proyectos. Un esquemático como el clon de Superfo viene de perlas para poder estudiar todo el ordenador en un golpe de vista, sin tener que ir pasando páginas adelante y atrás como me pasa con el libro de Chris (que por cierto ha resultado insustituible para estos cacharreos)
Así, la semana pasada hablé sobre unas pruebas que había hecho, de momento en el propio simulador de la plataforma de desarrollo de Xilinx, sin meterme a hacerlo "de verdad". Cuando estuve bastante seguro del resultado, quise probarlo, y para ello, mudé el "target" de desarrollo de una CPLD a una FPGA, concretamente ésta:
La prueba que quería "ver" (esta vez no como cronograma, sino de verdad de la buena) era aquella en la que aparece un borde blanco y un patrón de líneas verticales rojas y celestes. Es la misma prueba que publiqué en ese otro hilo esta semana pasada. Bueno, pues funcionó
Pero precisamente, una de las ventajas de las FPGA's sobre las CPLD's que las primeras tienen bloques de memoria que pueden suarse como ROM o RAM. En mi caso dediqué 6912 bytes de memoria de FPGA para tener un framebuffer, al que le di datos iniciales (una especie de carga de datos cuando se configura la FPGA).
Para esta nuevas prueba estuve buscando algún screen$ chulo, y de los que encontré, me quedé con la pantalla de presentación del Cannon Bubble, que es bastante colorida. Antes de meterla en la FPGA, la retoqué para las letras del logo de CEZ parpadearan. Así podría comprobar que el flash funciona, y con la cadencia correcta.
Mostrado en el nuevo circuito formado únicamente por una ULA y una memoria con datos iniciales (no hay procesador ni nada más), la imagen se ve así:
La salida de video es RGBI digital (igualito que en el Spectrum 128K heatsink), que es lo más fácil de implementar. El cable se encarga de mezclar el brillo con el color para darme todos los tonos que necesito
Obviamente, para llegar a esta pantalla, primero tuve que pasar por integrar un Z80, ROM, y al menos 16K de RAM. Paradójicamente, esta es la parte que más me ha costado, y es que decribir con elementos puramente digitales algo tan aparentemente inocuo como "8 resistencias" pues resulta que da para mucho. Al final he tenido que escribir un módulo arbitrador para decidir quién se queda con la memoria baja en cada ciclo de reloj. Total, que después de un par de días, en los que además estuve muy acatarrado y no pensaba con claridad, obtuve el ansiado...
Bien pero... ¿ahora qué? Tengo que poder teclear cosas, ver si al menos funcionan pequeños programas en código máquina aunque no pueda cargar nada aún desde EAR... Necesito un teclado y tengo dos opciones: usar 13 lineas de la FPGA para que hagan de fila (8) y columna (5), o usar el conector PS/2 que viene con el entrenador, pero eso significa escribir un módulo que convierta de PS/2 a Spectrum. Y no, esto no es un microcontrolador, así que no puedo aprovechar el código que escribí para la interfaz de teclado PS/2 que vende Ben Veersteg.
Por fortuna, dicho módulo lo tenía ya escrito, de mi primer clon de Spectrum en FPGA, así que lo reciclé para éste. El resultado, pues que al fin pude teclear un programa que sacara colores en el borde, paper, ink, etc. El display numérico que vale para depuración: me muestra en tiempo real los scancodes de las teclas que pulso y suelto en el teclado PS/2 (en pantalla se puede leer "5A", que es el scancode de la tecla Intro del teclado PS/2). La verdad es que el módulo me ha quedado muy aparente: puedes teclear los símbolos que hay en el propio teclado, y no tener que acordarte de que, por ejemplo, el signo "=" está en SYMBOL SHIFT + K . También funcionan los cursores, la tecla de borrar, el bloqueo mayúsculas...
Implementar EAR y MIC/SPK es muy sencillo... en teoría. Para MIC/SPK la cosa es que ambas señales están puenteadas en el Spectrum, junto con EAR. Así, lo que entra por EAR se escucha por MIC, y como MIC está unido a SPK, se escucha por la salida de sonido. Para la salida de sonido no uso un jack de 3.5'' separado, como suele ser habitual en los cables de 128K, sino que mis 128K (los españoles) están modificados internamente para sacar el sonido por un pin no usado en el conector DIN.
Esto me da, por lo menos, sonido de beeper, aunque no con el detalle de los distintos niveles de tensión para MIC y SPK (eso, junto con el AY-3-8912, vendrá después).
EAR es un poco más problemática. Por EAR se enchufará la entrada de audio, y ésta desde luego no sigue los mismos voltajes que necesita la FPGA. Es más, la tensión alterna que envía un amplificador de audio podría ser dañina para la etapa de entrada de las FPGA's, así que necesito proteger el chip de alguna forma. Afortunadamente, el entrenador que uso (Spartan 3 Starter Kit de Digilent) dispone de un conector RS232 conectado a la FPGA mediante un MAX3232. Este MAX3232 es un conversor de voltajes: en recepción, toma una señal de hasta +/-25V y los convierte en unos seguros 0 - 3.3V a la FPGA.
Así pues, la idea es usar la entrada de recepción del conector RS232 (pin 3) y usarlo como EAR. En la FPGA, ese pin se llevará directamente hacia la ULA Para poder escuchar el ruido de carga, como no me atrevo a cortocircuitar en el entrenador el pin de EAR con el de SPK, lo hago en lógica, combinando ambas señales. Poder escuchar el ruido de carga y tener realimentación visual y auditiva del proceso de carga es de las mejores cosas que le pudieron poner al Spectrum , en mi opinión
Con 16K de RAM (de momento 16K porque estoy usando unicamente la RAM incluida dentro de la FPGA) ya puedo cargar cosas interesantes. Por ejemplo, Jet Pac (me gusta ver cómo va cargando la pantalla...)
O uno de los juegos que más me han gustado en el Spectrum, el Deathchase, que es también de 16K, y es de lo más frenético que he visto:
Se puede observar que hay como unas rayas estilo "ULA snow" durante la carga. Se debe a que aún no había implementado correctamente la contención, y aunque dicha contención funcionaba para la memoria, no lo hacía para E/S. Resulta que el ciclo de E/S es distinto que el de memoria, y la ULA hace contención del ciclo T2 en lugar de T1, pero en T2 ya se ha pulsado IORQ y eso confundía a mi arbitrador. Una vez más, algo tan "tonto" como unas resistencias en el modelo original seguían dando problemas. La solución final que adopté fue la que hizo Amstrad en el +2/+3: separar el bus de datos "general" del bus de datos de la memoria de pantalla. Una vez hecho eso, no volvió a haber más problemas (que yo sepa) de contención.
El juego por supuesto es perfectamente jugable, aun incluso con la pequeña nieve en pantalla...
Otro clásico: la cinta Horizontes. El sonido de carga del arcoiris lo tengo grabado en la cabeza desde 1984. La foto está tomada en el primer programa, el que te enseña el hardware del Spectrum. ¡Busca las diferencias entre la imagen de la pantalla y el cacharrito que está debajo!
La siguiente parada consistió en dotarle de los 32K de RAM que le faltaban al clon para poder usarse en modo 48K. Tengo otros entrenadores que tienen 4MB de memoria instalada en placa, pero tienen el inconveniente de que es memoria SDRAM, y manejarla tiene guasa. Este entrenador que uso ahora lo compré precisamente por una razón: aparte de tener una FPGA con un millón de puertas lógicas, incorpora 1MB de memoria SRAM normal y corriente. Esto es, memoria RAM estática que no necesita controladores añadidos. Así, fue cuestión de minutos escribir una descripción en Verilog para mapear parte de esa memoria al clon y disfrutar al fin de 48K de RAM
Ahora sí que estaba en condiciones de probar a ver cuán fiel es la ULA que había descrito. Cargué el Aquaplane, y me encontré con esto
Un escalón de unas dos líneas, aproximadamente. Volví a la descripción de la ULA, mirando con el ISim si pasaban o no exactamente 14336 estados de reloj entre la interrupción y el comienzo de la pantalla, y sí, todo seguía ahí.
La Shock Megademo también fue inmisericorde conmigo... (la segunda parte, las demás funcionaron perfectamente)
Llegados a este punto, alguno se preguntará si esto mismo pasará en el clon de verdad cuando se monte y se pruebe. Mi sospecha es que estas incoherencias se deben en su mayor parte a que el clon que he hecho usa un Z80 que también es una descripción en Verilog, no es un chip real. Es lo que se llama un soft-core. Y este soft-core (el TV80 para más señas) no es 100% compatible con el Z80 original: ni soporta todas las instrucciones indocumentadas, ni comportamientos extraños de los flags, ni la temporización es idéntica (dice que similar). Un juego no demasiado exigente con estas cosas se jugará sin problemas, pero todo aquello que necesite el 100% del Z80, fallará.
Precisamente mientras trabajaba buscando el modo de que funcionara bien todo esto, me llamó la atención esta parte del circuito del clon (pero por motivos diferentes a los de Antonio Villena ) :
Los dos chips de la izquierda (U38 y U39) forman un contador que cuenta desde 0 hasta 6143. La salida de este contador, convenientemente recableada, forma la dirección de bitmap y de atributo que ha de leerse en cada momento. De "recablear" la salida del contador se encarga un multiplexor formado por los chips U40, U41 y U42. Bueno, en realidad el multiplexor sólo es U41 y U42, ya que hay 8 bits en común en la dirección de bitmap y de atributo, así que esos 8 bits no hay que cambiarlos al leer bitmap o atributo.
Los bits de la dirección que sí cambian en un caso u otro se eligen en U41 y U42 seleccionando cuál se quiere en la entrada A/B. Si vale 0 se recablea el contador para obtener la dirección de memoria del atributo actual, y si vale 1, la dirección del bitmap actual. Quizás se vea mejor su funcionamiento si digo que en Verilog, esos tres chips se modelan con la siguiente descripción:
Código: Seleccionar todo
if (!AL1)
rva = {1'b0,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]}; // bitmap
else
rva = {4'b0110,ca[12:8],ca[4:0]}; // atributo
Donde: ca es el valor del contador (U38 y U39). ca[n] es el bit n de dicho contador. ca[m:n] es la ristra de bits del contador que va desde el bit m hasta el bit n, ambos inclusive. rva es la dirección de memoria final que "ve" el chip de memoria VRAM (lo que en el esquema es VA1,VA2, etc)
Pues bien, me llamó la atención porque pensé que si se me ocurría duplicar la primera rama del IF en la segunda, estaria leyendo como atributo el mismo valor que el de bitmap.
Pero en el siguiente microsegundo me di cuenta de que si hacía eso, estaría consiguiendo un valor de atributo diferente para cada región de 8x1 píxeles, ¡color en alta resolución!
Claro que usar el mismo dato como bitmap y como atributo no tiene mucho sentido... ¿y si pudiera usar este esquema pero empezando a leer desde otra parte? Sería chulo tener un modo de alta resolución de color en el que en lugar de haber 768 bytes de atributos hubiera 6144 bytes de atributos, y que esos atributos comenzaran justo donde están los "habituales".
Bueno... en Verilog puedo poner aquí un sumador que haga que los atributos sigan el mismo patrón que la lectura de bitmaps, pero comenzando en otro sitio. Algo así...
Código: Seleccionar todo
if (!AL1)
rva = {1'b0,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]}; // bitmap
else
rva = {1'b0,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]} + 14'h1800; // atributo en alta resolución
Los números en Verilog tienen como formato el siguiente: por ejemplo, 14'h1800 significa que es un número de 14 bits, en formato hexadecimal, con el valor 1800. 4'b0110 es un número binario de 4 bits con el valor 0110.
Pero claro, pensando en cómo se implementaría esto "de verdad", se me antojó que un sumador de 14 bits era excesivo. Tenía que haber algo más sencillo... Un momento... todas las direcciones de bitmap tienen el bit más significativo a 0. ¿Y si sencillamente hago que los atributos comiencen en una dirección en la que ese bit está a 1?
Código: Seleccionar todo
if (!AL1)
rva = {1'b0,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]}; // bitmap
else
rva = {1'b1,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]}; // atributo en alta resolución
Un cálculo rápido me indicó que de esta forma, los atributos comenzarían 8192 bytes más adelante. Si el bitmap se almacena en la dirección 16384 (de la CPU, en la ULA esta dirección es la 0), los atributos comenzarían a almacenarse a partir de la dirección 24576. Bueno... no están pegados, hay un hueco en medio, pero por otra parte, al no estar pisando la parte de variables del sistema ni el BASIC, este esquema me permitirá hacer alguna prueba en BASIC.
Así lo implementé, y claro, al arrancar la máquina, no podía ver nada, porque los atributos se estaban leyendo de una zona de memoria que estaba toda a 0. A ciegas teclee una orden tal como ésta:
Código: Seleccionar todo
FOR n=24576 TO 24576+6143: POKE n,56: NEXT n
Este FOR se encargó de inicializar la zona de "atributos extendidos" al valor flash 0, bright 0, paper 7, ink 0. Ahora sí que podía ver la pantalla y teclear cosas.
Tecleé este otro programa:
Código: Seleccionar todo
FOR n=24576 TO 24576+6143: POKE n,PEEK (n-24576): NEXT n
Que basicamente llena el área de atributos extendidos con el contenido de la ROM, es decir, valores pseudoaleatorios. El resultado fue éste
¡Wow! multicolor a pantalla completa, y la CPU tumbada a la bartola sin tener que hacer nada. ¡Mola!
Mi siguiente paso era incorporar al diseño un interruptor de forma que yo pudiera elegir "en caliente" si activaba el modo normal de atributos o este modo extendido. Así podría teclear programas que probasen el multicolor y luego accionar el interruptor y ver el resultado.
Con esa idea en mente, pensé en el programa BMP2SCR, que tiene soporte de multicolor para SAM y Timex. Pero antes, para poder usar las pantallas que produjera con ese programa, necesitaba saber en qué se diferenciaba el modo multicolor, por ejemplo del Timex, con éste que había creado. Así llegué a estos dos programas:
http://freestuff.grok.co.uk/vbspec/timex.html
Uno de ellos (el de abajo) contenía el código en ensamblador de la demo en sí. Al leerlo para ver cómo manejaba los atributos extendidos me topé con esto:
Código: Seleccionar todo
; -------------------------------------------------------------------------------------------
; FILL_ATTRS: Fill the entire hi-colour attribute area with the attribute specified in A
;
; Inputs: A = Attribute to fill
; Output: None
; Destroys: HL,DE,BC
; -------------------------------------------------------------------------------------------
FILL_ATTRS: LD HL,24576 ; Extended Attribute area
LD DE,24577 ; Start of Area + 1
LD BC,6144 ; Length of area
LD (HL),A ; Make the first attr 0 (black on black)
LDIR ; Copy it through to the entire attr area
RET
WTF???!!!!??? ¡Pero si es que resulta que el Timex funciona igual que lo que acabo de hacer! Un poco más arriba, al principio del programa, encontraba la forma de activar este modo (que no es con interruptores, claro):
Código: Seleccionar todo
LD A,2 ; Hi-Colour Mode
OUT (255),A ; Set it
XOR A
IN A,(255) ; Check we're running on a machine that supports hi-colour
CP 2
JP Z,GO_DEMO ; Jump to the start of the demo if we are
¡Ajá! Así que se usa el puerto FFh para esto, concretamente el bit 1. Cambio de planes entonces: incorporé a la ULA del clon este puerto (adios al bus flotante, al menos en el puerto FFh) y la sección en Verilog que multiplexa las direcciones quedó al final así:
Código: Seleccionar todo
if (!AL1)
rva = {1'b0,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]}; // bitmap
else
rva = (rConfHiC[1])?
{1'b1,ca[12],ca[11],ca[7],ca[6],ca[5],ca[10:8],ca[4:0]} : // atributo extendido
{4'b0110,ca[12:8],ca[4:0]}; // atributo
Los que hayais trabajado en C reconocereis el operador ternario ?: Basicamente lo que hago ahora, al formar la dirección del atributo, es preguntar por el valor del bit 1 del registro de configuración de color extendido (el puerto FFh). Si ese bit vale 1, entonces escojo la dirección extendida. En otro caso, escojo la "normal".
Después de comprobar, usando OUT 255,2 y PRINT IN 255, que el puerto 255 estaba funcionando como debe, me atreví con la pequeña demo de vbSpec... y éste fue el resultado
De vuelta al BMP2SCR, probé con tres imágenes: una foto, una imagen generada por computador, y una imagen en colores planos, de un dibujo animado.
La imagen fotográfica quedó así
Y se puede ver el proceso de carga en este video. Se cargan dos bloques de 6144 bytes cada uno. Primero cargo el bloque de atributos, después activo el modo hicolor (OUT 255,2) y a continuación, cargo el bloque de bitmap. Para no hacer tedioso el video, comienzo a grabar justo cuando está para terminar la carga del primer bloque.
Video de carga : http://youtu.be/-frt-5NK9xU
Descargar TAP
Por otra parte, la imagen generada por ordenador quedó tal que así:
Video de carga: http://youtu.be/CPfHuJwCaRU
Descargar TAP
Y por último, la imagen con colores más planos, que además no podía ser otra , quedó así:
Descargar TAP
También probé la otra demo de la página del vbSpec, y funciona todo excepto la pantalla de carga, que usa el modo HiRes (512x192 píxeles, a dos colores). No tengo intención de momento de implementar ese modo, más que nada porque requeriría mucha más lógica. Si me he molestado en explicar todo esto del multicolor, es pura y sencillamente porque estoy seguro de que se puede modificar el clon Harlequin Superfo para que incorpore este modo sin necesidad de hacer cambios gordos en el circuito.
De momento seguiré añadiendo periféricos a este clon. Lo siguiente es un AY-3-8912, pero como este chip saca una señal analógica necesito "algo" que me convierta de digital a analógico en la FPGA. Las FPGA's no son la panacea, tienen algunos puntos débiles, y uno de ellos es que habitualmente no son de señal mixta, y se requieren chips externos (o como mínimo, un array de resistencias) para pasar de digital a analógico.
Sin embargo hay algunas técnicas que pueden usarse para solucionar decentemente este problema, como por ejemplo la modulación PWM, o la que voy a probar: la codificación sigma-delta, que sólo necesita a la salida una resistencia y un condensador para tener sonido de calidad (según lo que he leído). Ya veremos qué tal sale...