{"id":2835,"date":"2021-01-23T21:06:49","date_gmt":"2021-01-23T21:06:49","guid":{"rendered":"http:\/\/blog.rastersoft.com\/?p=2835"},"modified":"2023-12-25T19:48:01","modified_gmt":"2023-12-25T19:48:01","slug":"pintando-en-el-spectrum-8","status":"publish","type":"post","link":"https:\/\/blog.rastersoft.com\/?p=2835","title":{"rendered":"Pintando en el Spectrum (8)"},"content":{"rendered":"\n<p>En esta entrada vamos a hacer un cambio peque\u00f1o en el c\u00f3digo para que pinte mediante interrupciones. Aunque ya antes utiliz\u00e1bamos la instrucci\u00f3n<em> HALT<\/em> para sincronizarnos con las interrupciones y saber cuando empezar a pintar en pantalla, tiene el inconveniente de que desperdiciamos tiempo, tiempo que podr\u00edamos aprovechar para ir adelantando trabajo. Aunque ahora mismo no hay nada que hacer entre que hemos pintado un <em>frame<\/em> y esperamos a que el barrido de pantalla vuelva al principio, en el futuro, cuando implemente la l\u00f3gica del juego, tiene sentido que una vez que hemos terminado de pintar los <em>sprites<\/em> en el <em>buffer<\/em> intermedio, y esperamos a que termine de trazarse en pantalla en <em>frame<\/em> actual, podamos ir adelantando trabajo ejecutando la l\u00f3gica del juego en s\u00ed, en lugar de desperdiciar un tiempo precioso simplemente <em>no haciendo nada<\/em>.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"104\" style=\"aspect-ratio: 186 \/ 104;\" width=\"186\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/waiting.mp4\"><\/video><\/figure>\n\n\n\n<p>El Z80 tiene dos tipos de interrupciones: <em>enmascarables<\/em> (INT) y <em>no-enmascarables<\/em> (NMI). La interrupci\u00f3n no enmascarable siempre se atiende: cada vez que un perif\u00e9rico ponga a cero el pin 17, en cuanto el procesador termine de ejecutar la instrucci\u00f3n actual, guardar\u00e1 la direcci\u00f3n actual del contador de programa y saltar\u00e1 a la direcci\u00f3n 0x66. Para retornar de una NMI se utiliza la instrucci\u00f3n RETN. En el Spectrum, sin embargo, esta interrupci\u00f3n est\u00e1 bloqueada por software, pues en la direcci\u00f3n 0x66 est\u00e1 la ROM con una rutina que comprueba dos bytes de la RAM: si valen 0, saltar\u00e1 a la direcci\u00f3n que contengan dichos bytes (o sea, a la direcci\u00f3n 0x0000, con lo que el ordenador se reiniciar\u00e1), mientras que si tienen un valor diferente, simplemente retornar\u00e1 sin hacer nada, con lo que el resultado es que, sencillamente, no se puede utilizar para nada. Lo <em>\u00abdivertido\u00bb<\/em> es que s\u00f3lo cambiando un bit en esa rutina el comportamiento ser\u00eda exactamente el opuesto: si la direcci\u00f3n almacenada es cero, retornar\u00eda; y si es diferente, saltar\u00eda a dicha direcci\u00f3n, lo que ser\u00eda un comportamiento much\u00edsimo m\u00e1s \u00fatil. Lo m\u00e1s probable es que fuese un cambio de \u00faltima hora para evitar que se pueda utilizar de manera sencilla para copiar programas.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"192\" style=\"aspect-ratio: 320 \/ 192;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/working.mp4\"><\/video><\/figure>\n\n\n\n<p>La interrupci\u00f3n enmascarable, por su parte, se llama as\u00ed porque es posible desactivarla (en el Z80 mediante la instrucci\u00f3n DI; tras ejecutarla, no se responder\u00e1 a ninguna INT; para activarlas de nuevo se utiliza la instrucci\u00f3n EI). Como ya dijimos, en el Spectrum se genera una cada vez que la <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Gate_array\" target=\"_blank\">ULA<\/a> comienza a pintar un nuevo <em>frame<\/em> en la pantalla, en el primer <em>scanline<\/em> del BORDER (salvo que estemos en un <a rel=\"noreferrer noopener\" href=\"https:\/\/es.wikipedia.org\/wiki\/Investr%C3%B3nica_Inves_Spectrum_%2B\" target=\"_blank\">Inves Specrum+<\/a>, en cuyo caso la interrupci\u00f3n se genera justo al comenzar a pintar la zona de PAPER). El Z80 tiene tres modos de trabajo para estas interrupciones, seleccionables mediante las instrucciones IM0, IM1 e IM2.<\/p>\n\n\n\n<p>En el modo IM0 (que es el modo por defecto cuando se resetea el procesador, y compatible con el 8080 de Intel), cuando un perif\u00e9rico pone a cero el pin 16 para solicitar una interrupci\u00f3n, tiene que poner, adem\u00e1s, en el bus de datos el c\u00f3digo de una instrucci\u00f3n para que el procesador la ejecute. Normalmente esa instrucci\u00f3n ser\u00e1 una de las ocho instrucciones RST (RST #00, RST #08, RST #10, RST #18, RST #20, RST #28, RST #30 o RST #38) para saltar a una direcci\u00f3n de memoria concreta y ejecutar ah\u00ed una rutina de interrupci\u00f3n, aunque en teor\u00eda podr\u00eda ser cualquier instrucci\u00f3n, incluso instrucciones de varios bytes.<\/p>\n\n\n\n<p>En el modo IM1, que es el modo que utiliza la ROM del Spectrum por su simplicidad, cada vez que se produce una INT se saltar\u00e1 a la direcci\u00f3n 0x38, por lo que tiene un comportamiento similar a la NMI, aunque a una direcci\u00f3n diferente. Cabe se\u00f1alar que el <em>opcode<\/em> de la instrucci\u00f3n <em>RST #38<\/em>, que hace una llamada a subrutina a la direcci\u00f3n 0x38, es 255, lo que significa que el Spectrum se comportar\u00e1 igual tanto en modo 0 como en modo 1. En efecto, si recordamos, cuando leemos el bus y nadie est\u00e1 poniendo un dato, leeremos 255, por lo que si el procesador est\u00e1 en modo 0, ser\u00e1 eso lo que lea y ejecutar\u00e1 una instrucci\u00f3n <em>RST #38<\/em>, que saltar\u00e1 a la misma direcci\u00f3n que si estuvi\u00e9semos en modo 1.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"144\" style=\"aspect-ratio: 258 \/ 144;\" width=\"258\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/thesame.mp4\"><\/video><\/figure>\n\n\n\n<p>Finalmente, el modo IM2 es el m\u00e1s vers\u00e1til, aunque tambi\u00e9n el m\u00e1s complejo de utilizar. Cuando estamos en este modo y se produce una INT, el procesador lee el bus de datos y lo utiliza como la parte baja de una direcci\u00f3n de memoria. La parte alta la tomar\u00e1 del contenido del registro I. Una vez hecho esto, leer\u00e1 el contenido de esa posici\u00f3n de memoria y de la siguiente y saltar\u00e1 a la direcci\u00f3n de memoria indicada por esos dos bytes le\u00eddos. La idea es que si cargamos el registro I con el valor <em>0xYY<\/em>, debemos almacenar una tabla de 256 bytes a partir de la direcci\u00f3n <em>0xYY00<\/em> conteniendo direcciones de memoria de las rutinas de interrupci\u00f3n de cada perif\u00e9rico que pueda generar una interrupci\u00f3n. La idea original es que cuando un perif\u00e9rico genere la interrupci\u00f3n, pondr\u00e1 en el bus de datos el <em>offset<\/em> o desplazamiento en la tabla en que se encuentra la direcci\u00f3n de memoria de su rutina de gesti\u00f3n. As\u00ed, supongamos que I vale 0xF0, y que tenemos esta tabla a partir de 0xF000:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">DEFW 0x4012\nDEFW 0x5200\nDEFW 0x2103<\/pre>\n\n\n\n<p>Si tuvi\u00e9semos un perif\u00e9rico que genera una interrupci\u00f3n y pone el valor 0 en el bus, el Z80 compondr\u00e1 la direcci\u00f3n 0xF000 (usando el registro I y el valor presente en el bus), y leer\u00e1 los dos bytes que haya en dicha direcci\u00f3n. En este caso leer\u00e1 0x4012, as\u00ed que comenzar\u00e1 a ejecutar c\u00f3digo a partir de esa direcci\u00f3n, que se supone que ser\u00e1 el c\u00f3digo para gestionar ese perif\u00e9rico concreto. En cambio, si otro perif\u00e9rico genera una interrupci\u00f3n y pone el valor 4 en el bus, el Z80 compondr\u00e1 la direcci\u00f3n 0xF004 y leer\u00e1 en dicha zona de la tabla el valor 0x2103, y ser\u00e1 esa la direcci\u00f3n de memoria en la que comenzar\u00e1 a ejecutar c\u00f3digo. Este sistema permite que cada perif\u00e9rico indique quien es a la vez que genera una interrupci\u00f3n, de manera que el Z80 puede saltar directamente a la rutina que lo gestiona. Por supuesto, la \u00fanica manera de aprovechar al 100% las tablas es si todos los valores que pueden poner los perif\u00e9ricos son pares, o impares, pues si se mezclan, algunos perif\u00e9ricos podr\u00edan llamar a una rutina compuesta por parte de una direcci\u00f3n y parte de la siguiente.<\/p>\n\n\n\n<p>Sin embargo, en el Spectrum no tenemos ning\u00fan perif\u00e9rico que trabaje as\u00ed: el \u00fanico perif\u00e9rico que genera interrupciones es la ULA, y \u00e9sta no pone ning\u00fan valor en el bus, por lo que \u00e9ste siempre valdr\u00e1 255. Eso significa que, en teor\u00eda, s\u00f3lo necesitamos una \u00fanica entrada en nuestra tabla; pero siendo un valor impar, vemos que se \u00absaldr\u00eda por arriba\u00bb, en el sentido de que se utilizar\u00edan los bytes de las direcciones 0xF0FF y 0xF100.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"232\" style=\"aspect-ratio: 232 \/ 232;\" width=\"232\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/subirescalera.mp4\"><\/video><\/figure>\n\n\n\n<p>Veamos un ejemplo de c\u00f3digo para inicializar las interrupciones, y c\u00f3mo lo integramos con nuestro c\u00f3digo actual:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">ORG 32768\n\n; buffer intermedio de pantalla\nBUFFER:\n    DEFS 6912\n\n; espacio vac\u00edo para tener INTM2 alineado en el punto que necesitamos\n    DEFS 255\n\nINTM2:\n    DEFW IM2FUNC\n\n[...]\n\nINIT_IM2:\n    di\n    ld a, 0x9B\n    ld i, a\n    im 2\n    ei\n    ret\n\n[...]\nIM2FUNC:\n    ; c\u00f3digo a ejecutar por la interrupci\u00f3n\n    [...]\n    RETI<\/pre>\n\n\n\n<p>Vemos que el c\u00f3digo que ya ten\u00edamos empezaba en la direcci\u00f3n de memoria 0x8000 (o sea, 32768, tal y como especifica el ORG del principio). A continuaci\u00f3n tenemos el <em>buffer intermedio<\/em>, donde pintamos los <em>sprites<\/em> para que m\u00e1s tarde, cuando hayamos terminado, se vuelque de manera sincronizada en el <em>buffer de pantalla<\/em>. Este bloque termina en la direcci\u00f3n 0x9AFF, lo que significa que el primer byte libre est\u00e1 en 0x9B00, y como es una <em>direcci\u00f3n redonda<\/em> (tiene la parte baja a cero) nos sirve para meter la tabla de interrupciones. Sin embargo, recordemos que el bus del Spectrum siempre valdr\u00e1 255 (aunque de esto hablar\u00e9 m\u00e1s un poco m\u00e1s adelante), as\u00ed que no necesitamos poner la tabla entera, sino s\u00f3lo la \u00faltima entrada, y eso es lo que hacemos: dejamos 255 bytes libres, y justo a continuaci\u00f3n reservamos dos bytes. Esos 255 bytes los podemos aprovechar para meter otras variables (por ejemplo, la tabla de direcciones de la rutina de volcado del <em>buffer intermedio<\/em>, u otras), pues no se utilizan para nada.<\/p>\n\n\n\n<p>Vamos ahora a la funci\u00f3n <em>INIT_IM2<\/em>: lo primero que tenemos que hacer es desactivar las interrupciones, pues no queremos que nos llegue una justo en mitad de la configuraci\u00f3n y que el procesador salte a <em>vete-t\u00fa-a-saber-donde<\/em>. Una vez hecho esto, cargamos el registro I con el valor 0x9B, de manera que apunte a la tabla que hemos definido antes (que, como recordamos, empieza precisamente en la direcci\u00f3n de memoria 0x9B00). A continuaci\u00f3n cambiamos a modo 2, activamos las interrupciones, y salimos.<\/p>\n\n\n\n<p>Ahora, cada vez que la ULA genere una interrupci\u00f3n, al estar el procesador en modo 2 proceder\u00e1 a leer el bus de datos, que valdr\u00e1 255, y lo combinar\u00e1 con el registro I para componer la direcci\u00f3n de memoria 0x9BFF. Ahora leer\u00e1 el valor de 16 bits de dicha direcci\u00f3n, que es justo el que est\u00e1 almacenado en INTM2 gracias a los espacios que hemos dejado en el c\u00f3digo, y este valor ser\u00e1 el de la direcci\u00f3n de la rutina IM2FUNC, por lo que pasar\u00e1 a ejecutar su c\u00f3digo. Cuando termine, tenemos que retornar con RETI para que restaure el estado de las interrupciones (en lugar de RETI tambi\u00e9n podr\u00edamos utilizar EI + RET, pues EI tiene la particularidad de que activa las interrupciones despu\u00e9s de la siguiente instrucci\u00f3n, por lo que no hay peligro de que llegue justo una interrupci\u00f3n entre el EI y el RET y que la pila se llene sin control).<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"172\" style=\"aspect-ratio: 220 \/ 172;\" width=\"220\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/07\/ok.mp4\"><\/video><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Integrando las interrupciones<\/h2>\n\n\n\n<p>Todo esto est\u00e1 muy bien, pero \u00bfy qu\u00e9 se supone que hacemos ahora? Pues la idea consiste en reservar una direcci\u00f3n de memoria para indicar a la rutina de interrupciones que ya hemos terminado de pintar en el <em>buffer intermedio<\/em>, y que ya puede copiarlo en la pantalla cuando pueda. Y en la rutina de interrupciones lo que pondremos ser\u00e1 el c\u00f3digo de nuestra vieja amiga <em>paint_screen<\/em>, la rutina que vimos en las tres primeras entradas para volcar el <em>buffer intermedio<\/em> en la pantalla real. La idea ser\u00eda algo as\u00ed:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">DO_REPAINT:\n    ; esta variable nos servira para notificar que el buffer\n    ; intermedio est\u00e1 listo para ser volcado a la pantalla (si vale 1)\n    DEFB 0\n\nIM2_FUNC:\n    ; Esta es la ISR para la interrupci\u00f3n. Se ejecutar\u00e1 cada vez\n    ; que la ULA genere una interrupci\u00f3n\n    push AF\n    ld A, (DO_REPAINT)\n    and A\n    jp Z, IM2_EXIT ; si DO_REPAINT vale cero significa que el buffer\n                   ; intermedio a\u00fan no est\u00e1 listo, as\u00ed que no hacemos\n                   ; nada\n\n    push BC\n    push DE\n    push HL\n    exx\n    push BC\n    push DE\n    push HL ; Tenemos que preservar todos los registros que\n    push IX ; utilicemos dentro de paint_screen\n    ; c\u00f3digo de paint_screen\n    pop IX\n    pop HL\n    pop DE\n    pop BC\n    exx\n    pop HL\n    pop DE ; y restaurarlos al terminar, antes de retornar\n    pop BC\n    xor A\n    ld (DO_REPAINT), A ; notificamos que hemos terminado\nIM2_EXIT:\n    pop AF\n    reti ; y devolvemos la ejecuci\u00f3n al c\u00f3digo principal\n\n\n\n\n    [...]\n    ; c\u00f3digo principal. Pintamos la pantalla en el buffer intermedio\n\n    [...]\n    ; ya terminamos de pintar la pantalla, as\u00ed que notificamos\n    ; que hemos terminado poniendo DO_REPAINT a 1\n\n    ld a, 1\n    ld (DO_REPAINT), a\n\n    ; hacemos otras cosas, como l\u00f3gica del juego o dem\u00e1s.\n    ; Cuando llegue la siguiente interrupci\u00f3n, se copiar\u00e1\n    ; el buffer a la pantalla autom\u00e1ticamente\n\n    [...]\n\n    ; si hemos terminado de actualizar la l\u00f3gica del juego y\n    ; queremos pintar el siguiente frame, tenemos que esperar\n    ; a que la rutina de interrupci\u00f3n nos notifique que ha acabado.\n    ; Si no lo hacemos, podr\u00edamos estar sobreescribiendo el frame\n    ; anterior.\n\nWAIT_PAINTED:\n    ld A, (REPAINT)\n    and A\n    jr nz, WAIT_PAINTED\n\n    ; ahora ya estamos seguros de que el frame ha sido copiado a\n    ; la pantalla, as\u00ed que podemos empezar a pintar el siguiente\n    ; en el buffer intermedio\n\n    [...]\n<\/pre>\n\n\n\n<p>As\u00ed, vemos que en la rutina de interrupciones guardamos en la pila el contenido del registro AF, y luego leemos el valor que hay en la posici\u00f3n de memoria DO_REPAINT. Si es cero, significa que el buffer intermedio a\u00fan est\u00e1 a medio pintar y a\u00fan no debemos hacer nada, por lo que simplemente saltamos al final de la funci\u00f3n, donde restauramos el valor de AF y retornamos.<\/p>\n\n\n\n<p>En cambio, si DO_REPAINT es diferente de cero significa que el buffer intermedio est\u00e1 listo para ser transferido a la pantalla, as\u00ed que guardamos en la pila el resto de registros que vamos a utilizar (sin olvidarnos de los registros alternativos), copiamos los datos del buffer a la pantalla, restauramos los registros, y retornamos.<\/p>\n\n\n\n<p>Esto es muy \u00fatil pues nos permite aprovechar un tiempo que, antes, sencillamente perd\u00edamos. As\u00ed, cuando ejecut\u00e1bamos HALT para esperar al retrazado vertical y empezar a volcar el <em>buffer intermedio<\/em> a la pantalla, el procesador se quedaba literalmente congelado esperando a que se produjese una interrupci\u00f3n. Ahora, en cambio, podemos empezar a procesar otros datos en cuanto hemos terminado de pintar el frame, y el propio procesador saltar\u00e1 a la rutina de volcado autom\u00e1ticamente en el momento preciso, lo que nos permitir\u00e1 exprimir al m\u00e1ximo el procesador.<\/p>\n\n\n\n<p>Es fundamental guardar el valor de los registros, pues no hay que olvidar que la rutina de interrupciones se puede llamar en cualquier momento de la ejecuci\u00f3n del c\u00f3digo principal, y que lo \u00fanico que se guarda es la direcci\u00f3n de retorno; es nuestra responsabilidad guardar absolutamente el resto de cosas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Una tabla llena de 255<\/h2>\n\n\n\n<p>En mucha literatura sobre el uso de interrupciones en el Spectrum se recomienda crear una tabla de 257 elementos, todos ellos con el mismo valor, y utilizarla como tabla de vectores de interrupci\u00f3n. Es m\u00e1s: recomiendan aprovechar para ello un bloque de algo m\u00e1s de 1 Kbyte de la ROM que est\u00e1 todo \u00e9l a 255, y as\u00ed no desperdiciar memoria RAM. El motivo, explican, es que el bus no siempre tiene el valor 255, por lo que si cuando se produce una interrupci\u00f3n el valor no es el esperado, podr\u00edamos saltar a <em>vete-tu-a-saber-donde<\/em> y que se nos cuelgue el ordenador. Esto, como veremos luego, no es cierto (o, al menos, no es cierto en condiciones normales), por lo que no es necesario hacerlo. Pero, de todas formas, voy a describir el proceso porque nunca est\u00e1 de m\u00e1s entenderlo, en caso de que queramos, por ejemplo, desensamblar un programa que lo utilice, entender de qu\u00e9 va.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"220\" style=\"aspect-ratio: 320 \/ 220;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/07\/fixing.mp4\"><\/video><\/figure>\n\n\n\n<p>Veamos con detalle la idea: primero creamos una tabla de 257 bytes, todos ellos con el valor 255, y que empiece en una direcci\u00f3n de memoria cuyo byte bajo sea cero (por ejemplo 0xFE00 hasta 0xFF00 ambos inclu\u00eddos), o bien aprovechamos que en la ROM del Spectrum 48K, a partir de la direcci\u00f3n 0x386D hay 1179 bytes sin usar, todos ellos a 255, tomando como tabla el bloque que va desde 0x3900 hasta 0x3A00. A continuaci\u00f3n, cargamos en el registro I el valor necesario para utilizar ese bloque como tabla de interrupciones (0xFE en el primer caso, 0x3A en el segundo). Esto har\u00e1 que cuando se produzca una interrupci\u00f3n no importe qu\u00e9 valor hay en el bus: siempre resultar\u00e1 en que comenzar\u00e1 a ejecutar a partir de la direcci\u00f3n 0xFFFF (o sea, el \u00faltimo byte de memoria). Aparentemente esto nos deja muy limitados, pues s\u00f3lo podr\u00edamos ejecutar un \u00fanico byte antes de dar la vuelta al contador de programa. Pero dado que el primer byte de la ROM del Spectrum es 0xF3 (instrucci\u00f3n DI), si ponemos el valor 0x18 en la direcci\u00f3n 0xFFFF lo que tendremos es que se ejecutar\u00e1 la instrucci\u00f3n <em>JR -13<\/em>. Por tanto, esa instrucci\u00f3n saltar\u00e1 a la direcci\u00f3n 65524 (no hay que olvidar que despu\u00e9s de leer los dos bytes del <em>JR -13<\/em>, el contador de programa estar\u00e1 apuntando a la direcci\u00f3n 1). Es en esa direcci\u00f3n donde podemos poner una instrucci\u00f3n JP que salte a donde quiera que est\u00e9 nuestra rutina real.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"180\" style=\"aspect-ratio: 302 \/ 180;\" width=\"302\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/07\/lista.mp4\"><\/video><\/figure>\n\n\n\n<p>Alambicado, s\u00ed, pero nos da una gran seguridad a la hora de trabajar con interrupciones&#8230; \u00bfo no?<\/p>\n\n\n\n<p>Para empezar, est\u00e1 la cuesti\u00f3n de que ese bloque de m\u00e1s de 1 kbyte a 255 s\u00f3lo est\u00e1 disponible en el Spectrum 48K. En el modelo de 128K y posteriores ese bloque tiene una serie de rutinas extra, por lo que no est\u00e1 disponible. Esto significa que si utilizamos esto, s\u00f3lo funcionar\u00e1 en equipos de 48K, pero no en modelos de 128K, ni siquiera aunque cambiemos a modo 48K.<\/p>\n\n\n\n<p>Pero la verdadera cuesti\u00f3n es que, en condiciones normales, el bus de datos del Spectrum siempre estar\u00e1 a 255. Obviamente si nos dedicamos a leer el bus directamente, como ya coment\u00e9 en entradas anteriores, veremos que a veces muestra otros valores porque estamos viendo el dato que la ULA est\u00e1 leyendo en ese momento para generar la pantalla. Pero la cuesti\u00f3n es que el \u00fanico valor que nos interesa es el que encontraremos cuando se produzca una interrupci\u00f3n, y dado que \u00e9stas se generan cuando la ULA va a comenzar a pintar la parte superior del borde, en ese momento no leer\u00e1 nada de la RAM, por lo que siempre, siempre, se leer\u00e1 un 255.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter meme\"><video height=\"180\" style=\"aspect-ratio: 320 \/ 180;\" width=\"320\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2020\/06\/think.mp4\"><\/video><\/figure>\n\n\n\n<p>Existe, sin embargo, un caso en el que esto puede no cumplirse, y es si tenemos un perif\u00e9rico conectado a un ordenador real, y este perif\u00e9rico, por dise\u00f1o, fuerza un valor diferente en el bus. Un ejemplo es el infame <a rel=\"noreferrer noopener\" href=\"http:\/\/www.robertp.net\/SD1.htm\" target=\"_blank\">sistema de protecci\u00f3n SD1 de Dinamic<\/a>, utilizado en el Camelot Warriors. Se trata de un peque\u00f1o conector que incluye una resistencia de 1000 ohmios entre el bit D5 del bus de datos y masa, lo que hace que cuando se lee el bus, si ning\u00fan perif\u00e9rico fuerza un valor, se leer\u00e1 223 en lugar de 255. Seg\u00fan el art\u00edculo, esto tambi\u00e9n ocurr\u00eda al leer del puerto 254, el de la ULA, lo que me hace sospechar que, para ahorrar celdas, la ULA s\u00f3lo fuerza los bits 0-4 (teclado) y 6 (cassete), pero no hace nada con los bits 5 y 7.<\/p>\n\n\n\n<p>As\u00ed que, por esto, realmente considero que no es necesario utilizar una tabla. Pero si te empe\u00f1as, cuidado en los modelos Plus 2A y Plus 3, pues dos de sus cuatro ROMs no empiezan con 0xF3, por lo que lo mejor es asegurarse de paginar la correcta antes. Eso s\u00ed, ya puestos, puede ser una mejor soluci\u00f3n hacer una tabla de 256 elementos situada a partir 0xFF00 y con el valor 0xF3, lo que har\u00e1 que la interrupci\u00f3n salte a la direcci\u00f3n 0xF3F3 (62451). Esto permite compactar algo m\u00e1s el c\u00f3digo de las interrupciones.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>En esta entrada vamos a hacer un cambio peque\u00f1o en el c\u00f3digo para que pinte mediante interrupciones. Aunque ya antes utiliz\u00e1bamos la instrucci\u00f3n HALT para sincronizarnos con las interrupciones y saber cuando empezar a pintar en pantalla, tiene el inconveniente de que desperdiciamos tiempo, tiempo que podr\u00edamos aprovechar para ir adelantando trabajo. Aunque ahora mismo &hellip; <a href=\"https:\/\/blog.rastersoft.com\/?p=2835\" class=\"more-link\">Seguir leyendo <span class=\"screen-reader-text\">Pintando en el Spectrum (8)<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,17,7],"tags":[20],"class_list":["post-2835","post","type-post","status-publish","format-standard","hentry","category-programacion","category-retrocomputacion","category-tutoriales","tag-pintando-en-el-spectrum"],"_links":{"self":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2835","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2835"}],"version-history":[{"count":12,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2835\/revisions"}],"predecessor-version":[{"id":2860,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2835\/revisions\/2860"}],"wp:attachment":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2835"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2835"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2835"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}