{"id":2703,"date":"2021-01-01T22:32:03","date_gmt":"2021-01-01T22:32:03","guid":{"rendered":"http:\/\/blog.rastersoft.com\/?p=2703"},"modified":"2023-12-25T19:48:34","modified_gmt":"2023-12-25T19:48:34","slug":"pintando-en-el-spectrum","status":"publish","type":"post","link":"https:\/\/blog.rastersoft.com\/?p=2703","title":{"rendered":"Pintando en el Spectrum (1)"},"content":{"rendered":"\n<p>Cuando ten\u00eda 12 a\u00f1os hered\u00e9 el <a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/ZX_Spectrum\" target=\"_blank\">Sinclair ZX Spectrum<\/a> de mi hermano. Era un ordenador que me fascinaba, y con \u00e9l aprend\u00ed a programar, primero en BASIC, y luego directamente en Ensamblador. Tambi\u00e9n aprend\u00ed rudimentos de electr\u00f3nica digital, y gracias a ello constru\u00ed varios circuitos que le acopl\u00e9, como un teclado nuevo, un puerto de E\/S de 16 bits, y m\u00e1s.<\/p>\n\n\n\n<p>Es una m\u00e1quina a la que siempre le tuve mucho cari\u00f1o, y por eso me lanc\u00e9 hace unos a\u00f1os a escribir mi propio emulador, <a rel=\"noreferrer noopener\" href=\"https:\/\/rastersoft.com\/programas\/fbzx.html\" target=\"_blank\">FBZX<\/a>, cuando los que hab\u00eda en aquel entonces no me acababan de convencer. Y recientemente, a ra\u00edz de varios canales de youtube de \u00abnostalgia de 8 bits\u00bb, me he puesto un poquito as\u00ed y he decidido intentar programar algo. Al principio prob\u00e9 a usar el compilador de <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/z88dk\/z88dk\" target=\"_blank\">Z88dk<\/a> para poder utilizar lenguaje C, hasta que vi la chapuza de c\u00f3digo que genera (algo que no es culpa de los desarrolladores, sino de la propia arquitectura del Z80, que no es nada adecuada para C y, sencillamente, es imposible generar mejor c\u00f3digo). Ante esto, decid\u00ed pasarme a <a rel=\"noreferrer noopener\" href=\"https:\/\/www.nongnu.org\/z80asm\/\" target=\"_blank\">Z80ASM<\/a> y trabajar desde cero en ensamblador. A fin de cuentas, en una m\u00e1quina de 8 bits cada bit cuenta, y el poder optimizar cada rutina hasta la \u00faltima instrucci\u00f3n puede ser la diferencia entre conseguir o no conseguir algo concreto.<\/p>\n\n\n\n<p>Y precisamente una de las cosas en donde la velocidad es cr\u00edtica es a la hora de pintar en la pantalla, pues, por desgracia, el Spectrum original no tiene ninguna ayuda para esa tarea, si siquiera una doble p\u00e1gina (a pesar de que s\u00f3lo habr\u00eda requerido a\u00f1adir un \u00fanico flip-flop a la ULA). El Spectrum 128K s\u00ed tiene dos p\u00e1ginas de v\u00eddeo, lo que permite que la ULA muestre una de ellas mientras el c\u00f3digo genera el siguiente fotograma en la otra p\u00e1gina, y cuando haya terminado, s\u00f3lo tiene que esperar a que la ULA empiece a pintar un nuevo cuadro para cambiar la p\u00e1gina activa, de manera que ahora se mostrar\u00e1 lo que haya en la segunda p\u00e1gina y el programa podr\u00e1 pintar el siguiente fotograma en la primera p\u00e1gina.<\/p>\n\n\n\n<p>Cuando se usa el sistema de doble p\u00e1gina, las animaciones van fluidas y sin parpadeos ni deformaciones. Por desgracia, los modelos de 48K, al no tener esta capacidad, obligan al programador a sincronizarse con el haz de electrones de la pantalla para evitar que \u00able pille el rayo\u00bb en mitad de un acceso a la pantalla. Hay mucha literatura al respecto, as\u00ed que no voy a entrar en explicar en qu\u00e9 consiste lo de \u00ab<a rel=\"noreferrer noopener\" href=\"https:\/\/en.wikipedia.org\/wiki\/Racing_the_Beam\" target=\"_blank\">competir con el haz<\/a>\u00ab. S\u00ed voy a dar, sin embargo, algunas notas sobre c\u00f3mo funciona la pantalla en el Spectrum. Para ello, veamos este dibujo de una televisi\u00f3n con la imagen generada por la circuiter\u00eda del ordenador (la famosa ULA):<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/barrido_haz_spectrum.png\" rel=\"lightbox-0\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"762\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/barrido_haz_spectrum.png\" alt=\"\" class=\"wp-image-2704\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/barrido_haz_spectrum.png 800w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/barrido_haz_spectrum-300x286.png 300w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/barrido_haz_spectrum-768x732.png 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><figcaption>Una televisi\u00f3n con la imagen generada por un Spectrum.<\/figcaption><\/figure>\n\n\n\n<p>En el dibujo vemos una televisi\u00f3n, y en la pantalla podemos ver las dos zonas en las que se divide la imagen generada por la ULA en el Spectrum: el <em>border<\/em> (en color verde en el dibujo) y el <em>paper<\/em>, o la zona de trabajo, en color blanco. Tambi\u00e9n vemos esquematizado el recorrido del haz de electrones, de derecha a izquierda y de arriba a abajo, aunque exagerado. El <em>border<\/em> es una zona en la que s\u00f3lo podemos definir qu\u00e9 color global queremos, pero no podemos pintar en ella. S\u00f3lo en el <em>paper<\/em> podemos pintar p\u00edxeles escribiendo en una zona de memoria, a partir de 0x4000 hasta 0x5800, en donde cada bit se corresponde con un pixel. Esta zona se divide en 256&#215;192 pixels. A mayores, justo a continuaci\u00f3n se encuentra la zona que almacena los <em>atributos<\/em> de color, que mide 768 bytes. Esta zona ocupa un byte para cada grupo de 8&#215;8 p\u00edxels, y especifica qu\u00e9 color se usar\u00e1 para cuando el bit asociado a cada pixel est\u00e1 a 0 o a 1.Para m\u00e1s detalles, recomiendo leer la informaci\u00f3n de <a rel=\"noreferrer noopener\" href=\"https:\/\/worldofspectrum.org\/faq\/reference\/48kreference.htm\" target=\"_blank\">World of Spectrum<\/a> sobre la memoria de v\u00eddeo. <em>Edito: o bien la <a rel=\"noreferrer noopener\" href=\"https:\/\/blog.rastersoft.com\/?p=2737\" target=\"_blank\">entrada n\u00famero 4<\/a> de esta misma serie, donde entro en m\u00e1s profundidad en c\u00f3mo es la distribuci\u00f3n de la pantalla en el Spectrum. Siento no haber hecho las cosas en orden.<\/em><\/p>\n\n\n\n<p>La ULA refresca la pantalla a una tasa de 50 veces por segundo. Adem\u00e1s, para simplificar la circuiter\u00eda, no utiliza entrelazado, sino que siempre pinta \u00fanicamente las l\u00edneas impares. Y dado que el reloj de la CPU va a 3,5 MHz, eso significa que desde que comienza a pintar la imagen, en la esquina superior izquierda del borde, hasta que ha terminado y empieza a pintar la siguiente, tenemos, en teor\u00eda, 3 500 000 \/ 50 = 70 000 ciclos de reloj o Tstados. Y como tenemos 312,5 l\u00edneas (recordemos que s\u00f3lo utilizamos medio campo, por lo que es la mitad de 625), cada l\u00ednea dura 224 Tstados. En la pr\u00e1ctica no podemos tener media l\u00ednea, por lo que, en realidad, cada frame dura 69 888 Tstados y tenemos un total de 312 l\u00edneas. Adem\u00e1s, sabemos que cada vez que se empieza a pintar un <em>frame<\/em>, la ULA genera una interrupci\u00f3n, la cual podemos utilizar para sincronizarnos con la generaci\u00f3n de la imagen. Por \u00faltimo, hay 64 l\u00edneas de <em>border<\/em> antes de que empiece a pintarse el <em>paper<\/em>.<\/p>\n\n\n\n<p>Con todo esto ya podemos hacer un primer c\u00e1lculo, que nos dice que si queremos estar seguros de que hemos pintado todo en pantalla antes de que el haz llegue al <em>paper<\/em>, tenemos que hacerlo en menos de 64 * 224 = 14 336 Tstados. Por desgracia esto es muy poco tiempo.<\/p>\n\n\n\n<p>Una soluci\u00f3n consiste en esperar a que la ULA haya terminado de pintar el <em>paper<\/em> y, entonces, pintar lo que necesitemos. En este caso tendremos 69 888 &#8211; (224 * 192) = 26 880 Tstados, que es el tiempo que tarda en pintar la parte inferior y la superior del borde. Ya es un poco m\u00e1s, pero todav\u00eda no llega para demasiado.<\/p>\n\n\n\n<p>La soluci\u00f3n que usan muchos juegos consiste en implementar una doble p\u00e1gina <em>por software<\/em>. Para ello, pintan toda la imagen en una zona diferente de memoria, y cuando est\u00e1 lista, la copian de golpe en la memoria de pantalla. De esta manera la imagen que aparece siempre es definitiva y no hay parpadeos. El inconveniente es que consume casi siete Kbytes extra.<\/p>\n\n\n\n<p>El problema es que, como de costumbre, la cosa no es tan sencilla. Veamos por qu\u00e9. La primera idea, la m\u00e1s <em>naive<\/em>, ser\u00eda utilizar la instrucci\u00f3n LDIR del Z80. Esta instrucci\u00f3n permite copiar un bloque de memoria a otra posici\u00f3n, y recibe tres par\u00e1metros: la direcci\u00f3n inicial del bloque (en el registro <em>HL<\/em>), la direcci\u00f3n de destino (en el registro <em>DE<\/em>), y el tama\u00f1o del bloque (en el registro <em>BC<\/em>), y hace todo el trabajo por nosotros: lee el byte contenido en la direcci\u00f3n de memoria apuntada por HL, lo escribe en la direcci\u00f3n de memoria apuntada por DE, incrementa en uno ambos registros, decrementa en uno el registro BC, y repite la operaci\u00f3n hasta que este \u00faltimo valga cero. \u00a1Una <a rel=\"noreferrer noopener\" href=\"https:\/\/blogs.20minutos.es\/yaestaellistoquetodolosabe\/cual-es-el-origen-de-la-expresion-ser-una-bicoca\/\" target=\"_blank\">bicoca<\/a>! As\u00ed que lo \u00fanico que tendr\u00edamos que hacer es esperar a la interrupci\u00f3n, cargar los valores en los registros, y ejecutar LDIR. \u00bfO no?<\/p>\n\n\n\n<p>El problema es que cada iteraci\u00f3n de LDIR consume 21 Tstados. Eso significa que una pantalla completa, que ocupa 6 912 bytes, tardar\u00e1 145 152 Tstados en copiarse&#8230; \u00a1que es m\u00e1s del doble de lo que se tarda en generarse! Eso significa que el haz nos alcanzar\u00e1 como m\u00ednimo dos veces en cada volcado. Y s\u00ed, digo \u00abcomo m\u00ednimo\u00bb, porque por cuestiones de dise\u00f1o, la pantalla no se almacena de manera secuencial en memoria, sino que primero viene la fila 0 de pixels, ocupando 32 bytes, luego la fila 8, luego la 16, 24, 32, 40, 48 y la 56, momento en que vuelve atr\u00e1s hasta la fila 1, luego la 9, la 17&#8230; Eso hace que podamos cruzar el haz varias veces, y complica a\u00fan m\u00e1s la gesti\u00f3n de la pantalla.<\/p>\n\n\n\n<p>Por si fuera poco, hay un segundo problema, y es que la ULA interfiere en el acceso a la RAM cuando est\u00e1 generando la imagen. Esto es debido a que en chips de RAM normales no es posible que dos sistemas lean o escriban a la vez, sino que se tienen que turnar. La ULA necesita leer dos bytes (uno con los pixels, y otro con los atributos de color) cada cuatro Tstados mientras est\u00e1 pintando el <em>paper<\/em>, y s\u00f3lo deja la memoria libre cuando est\u00e1 pintando el borde. Para conseguir esto, lo que hace es monitorizar constantemente a la CPU, y si intenta acceder a la zona de v\u00eddeo mientras la ULA est\u00e1 pintando el <em>paper<\/em>, le detiene el reloj a la CPU hasta que termine.<\/p>\n\n\n\n<p>Por suerte, gracias a un ingenioso dise\u00f1o, la ULA s\u00f3lo necesita tres Tstados para leer esos dos bytes, as\u00ed que lo que hace es agrupar dos lecturas seguidas y dejar los dos Tstados restantes para la CPU. Esto significa que, si queremos que la escritura en la zona de v\u00eddeo no se vea penalizada por la ULA, tenemos que hacer accesos con un periodo que sea <strong>m\u00faltiplo de ocho Tstados<\/strong>. De esta manera, el primer acceso se ver\u00e1 retrasado entre uno y seis Tstados seg\u00fan donde caiga, pero el resto coincidir\u00e1n exactamente en el siguiente hueco. Y este es otro de los problemas de LDIR: como cada ciclo dura 21 Tstados, que no es m\u00faltiplo de ocho, significa que, en la pr\u00e1ctica, ser\u00e1 como si la instrucci\u00f3n durase 24 Tstados (la primera har\u00e1 la escritura en el primer hueco libre; la segunda lo har\u00e1 en el segundo hueco, tres periodos despu\u00e9s, con lo que ambas durar\u00e1n 21 Tstados; la tercera, en cambio, se retrasar\u00e1 seis Tstados, la cuarta no sufrir\u00e1 retraso, la quinta se retrasar\u00e1 seis Tstados&#8230; y as\u00ed sucesivamente).<\/p>\n\n\n\n<p>Sin embargo, si vemos el juego de instrucciones, comprobaremos que existe tambi\u00e9n una instrucci\u00f3n similar, LDI, que hace lo mismo excepto repetir la operaci\u00f3n, y s\u00f3lo consume 16 Tstados, que, adem\u00e1s, es m\u00faltiplo de 8 y, por tanto, no sufrir\u00eda penalizaci\u00f3n por parte de la ULA. \u00bfQu\u00e9 pasar\u00eda si pusi\u00e9semos una laaaaaarga ristra de ellas, una detr\u00e1s de otra? Pues que necesitar\u00edamos 6 912 * 16 = 110 592 Tstados. A\u00fan es m\u00e1s que el tiempo que se necesita para refrescar una pantalla, pero a\u00fan nos queda un as en la manga. \u00bfQu\u00e9 ocurrir\u00eda si esperamos a que el haz llegue hasta el principio del <em>paper<\/em>, y justo entonces empezamos a pintar? En ese caso, el haz ir\u00eda por delante nuestra, y nunca lo adelantar\u00edamos porque ya hemos visto que somos m\u00e1s lentos que \u00e9l, y eso significa que cuando el haz haya pintado toda la pantalla y vuelto al punto de partida, ya llevaremos m\u00e1s de la mitad de la imagen pintada y, adem\u00e1s, tendremos a\u00fan casi otro <em>frame<\/em> de tiempo antes de que nos alcance.<\/p>\n\n\n\n<p>Veamos exactamente de cuanto tiempo disponemos. Tenemos los 69 888 Tstados que tarda en recorrer un <em>frame<\/em>, y ahora tenemos que sumarle el tiempo que tarda en llegar hasta el borde inferior derecho del <em>paper<\/em>. Esto ser\u00e1 224 Tstados_por_linea * 191 lineas_completas + 128 Tstados_de_la_ultima_linea = 42 912 Tstados. Esto significa que si utilizamos este truco, tendremos disponibles un total de 112 800 Tstados para copiar la pantalla antes de que nos alcance el haz, que es m\u00e1s de lo que tarda la ristra de LDIs. \u00a1Buena cosa!<\/p>\n\n\n\n<p>Por desgracia, esta idea tiene dos problemas:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Para que funcione, tenemos que escribir los datos de manera lineal en la pantalla, de manera que nos mantengamos siempre por detr\u00e1s del haz. Pero si usamos LDI directamente, se har\u00e1 de manera escalonada por c\u00f3mo est\u00e1 organizada la pantalla del Spectrum, y adelantaremos y retrasaremos al haz constantemente.<\/li><li>Cada instrucci\u00f3n LDI ocupa dos bytes, lo que significa que necesitar\u00edamos una ristra de instrucciones que ocupar\u00eda el doble que el tama\u00f1o del bloque a copiar, lo que supone un desperdicio exagerado de memoria.<\/li><\/ul>\n\n\n\n<p>As\u00ed, dado que la \u00fanica secuencia consecutiva que tenemos son los 32 bytes de cada l\u00ednea, la soluci\u00f3n es utilizar una secuencia de 32 instrucciones LDI, y modificar entre medias los registros para ir l\u00ednea a l\u00ednea. El problema es que c\u00f3mo hacerlo con el m\u00ednimo de instrucciones posible, pues de nada sirve si perdemos por un lado lo que ahorramos por otro.<\/p>\n\n\n\n<p>La primera soluci\u00f3n consiste en tener una tabla con las direcciones de cada <em>scanline<\/em>, de manera que s\u00f3lo tenemos que coger la direcci\u00f3n actual, ejecutar 32 instrucciones LDI, y repetir el bucle hasta que el <em>flag<\/em> de desbordamiento (<em>overflow<\/em>) se active despu\u00e9s de la \u00faltima instrucci\u00f3n LDI. El problema es que LDI utiliza los tres pares de registros principales, con lo que no nos queda nada para mantener un puntero a la lista de direcciones&#8230;<\/p>\n\n\n\n<p>\u00bfO s\u00ed que lo tenemos? Porque podemos simplemente cargar el puntero de pila <em>SP<\/em> con la direcci\u00f3n de la tabla, y leer los valores directamente con <em>POP DE<\/em>. Esta instrucci\u00f3n tiene la ventaja de que con s\u00f3lo 11 Tstados nos carga un valor de 16 bits e incrementa el puntero de la tabla.<\/p>\n\n\n\n<p>El resultado ser\u00eda este c\u00f3digo:<\/p>\n\n\n\n<p class=\"mycode\">    LD SP, tabla_direcciones<br>    LD HL, buffer<br>    LD BC, 6144 ; tama\u00f1o del buffer. De momento prescindimos del color<br>loop:<br>    POP DE<br>    LDI<br>    LDI<br>    &#8230; ; 32 instrucciones LDI en total<br>    LDI<br>    JP PE, loop<br>&#8230;<br>tabla_direcciones:<br>    DEFW 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700<br>    DEFW 0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720<br>    &#8230;. ; completar hasta las 192 l\u00edneas<\/p>\n\n\n\n<p>Este c\u00f3digo es muy r\u00e1pido: dado que la instrucci\u00f3n JP consume 10 Tstados, tenemos que copiar cada fila consume un total de 533 Tstados; pero como tenemos que redondear a un m\u00faltiplo de 8 para tener en cuenta la contienda de memoria, se nos quedan en 536 Tstados; y como una pantalla completa son 192 l\u00edneas, al final gastamos entre 102 336 y 102 912 Tstados (no olvidemos que s\u00f3lo hay contienda mientras se pinta el <em>paper<\/em>, nunca mientras se pinta el <em>border<\/em>, por lo que el valor final estar\u00e1 entre ambos), lo que est\u00e1 muy por debajo del l\u00edmite de 112 800 Tstados. \u00a1Buena cosa! Por desgracia, nos falta por copiar los atributos de color, y el problema es que tenemos que copiar una fila de atributos por cada ocho de p\u00edxels, pues tenemos que mantenernos por detr\u00e1s del haz cat\u00f3dico; no podemos copiar primero todos los p\u00edxels y luego todos los atributos. El problema es que ya estamos usando absolutamente todos los registros con la instrucci\u00f3n LDI, y ni siquiera tenemos la pila disponible para almacenar los valores entre uno y otro grupo.<\/p>\n\n\n\n<p>\u00a1\u00a1\u00a1Pero tenemos el juego de registros alternativo!!! El Z80 tiene su conjunto principal de registros, AF, BC, DE y HL, pero tambi\u00e9n tiene un segundo juego, AF&#8217;, BC&#8217;, DE&#8217; y HL&#8217;, que podemos intercambiar con s\u00f3lo dos instrucciones: EX AF, AF&#8217; (que intercambia los valores de AF y AF&#8217;) y EXX (que intercambia los valores de BC, DE y HL con los de sus hom\u00f3logos). As\u00ed que, dado que los atributos de color s\u00ed siguen un formato <em>normal<\/em>, podemos copiar ocho filas primero usando la tabla, cambiar al juego alternativo de registros, copiar una fila de atributos de color, volver a cambiar el juego, copiar otras ocho filas de pixeles, etc. El c\u00f3digo quedar\u00eda as\u00ed:<\/p>\n\n\n\n<p class=\"mycode\">    LD SP, tabla_direcciones<br>    LD HL, buffer<br>    EXX ; cambiamos al juego de registros alternativo<br>    LD HL, buffer + 6144 ; apunta a los atributos de color del buffer<br>    LD DE, 22528 ;  zona de atributos de la pantalla<br>    LD BC, 768 ; tama\u00f1o de los atributos<br>loop1:<br>    EXX ; volvemos al juego original con los datos de p\u00edxeles<br>    LD B, 9 ; las ocho filas necesitan 256 LDIs. Cada uno resta<br>             ; uno a BC; luego al final de ellos, C valdr\u00e1 lo mismo<br>             ; pero B se habr\u00e1 decrementado en una unidad. As\u00ed<br>             ; que tenemos que tenerlo en cuenta para DJNZ <br>loop2:<br>    POP DE<br>    LDI<br>    &#8230; ; 32 LDIs en total<br>    LDI<br>    DJNZ loop2<br>    EXX<br>    LDI<br>    &#8230; ; 32 LDIs en total<br>    LDI<br>    JP PE, loop1 ; no podemos usar DJNZ porque el salto es<br>                  ; de m\u00e1s de 128 bytes<br>    &#8230;<br>tabla_direcciones:<br>    DEFW 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700<br>    DEFW 0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720<br>    ; completar hasta las 192 l\u00edneas<\/p>\n\n\n\n<p>El bucle interno consume un total de 512 + 11 + 13 = 536 Tstados cuando B es distinto de cero (que, adem\u00e1s, es m\u00faltiplo de 8), y 512 + 11 + 8 = 531 Tstados cuando B es cero. Por tanto, para ocho filas tenemos un total de 536 * 7 + 531 = 4 283 Tstados. Pero a esto hay que sumar la inicializaci\u00f3n previa y la copia de los atributos. Antes tenemos, del EXX y el LD B, 9, un total de 4 + 7 = 13 Tstados, y despu\u00e9s 4 + 512 + 10 = 526 Tstados; luego cada fila completa de 8 p\u00edxels de altura y con atributos necesita un total de 4 283 + 13 + 526 = 4 822 Tstados. Pero para que sea m\u00faltiplo de 8 hay que subir a 4 824 Tstados. Y como tenemos 24 filas, el total estar\u00e1 entre 115 728 y 115 776 Tstados.<\/p>\n\n\n\n<p>Vaya, ahora nos hemos pasado. Una pena, porque eso significa que no podemos animar algo a pantalla completa sin que haya <em>artifacts<\/em>. Pero si copiamos s\u00f3lo 23 filas s\u00ed nos da tiempo, pues ah\u00ed no necesitar\u00edamos m\u00e1s de  110 952 Tstados. Por tanto, una soluci\u00f3n consiste en obviar la \u00faltima fila, lo cual no tiene por qu\u00e9 ser un problema, pues normalmente se suele dejar la parte baja de la pantalla para marcadores y otros elementos relativamente est\u00e1ticos. Adem\u00e1s, tenemos la ventaja de que el buffer de trabajo est\u00e1 en un formato secuencial, lo que simplifica pintar en \u00e9l.<\/p>\n\n\n\n<p>Esta rutina ya ser\u00eda utilizable, pero tiene el inconveniente de necesitar una tabla de 384 bytes para las direcciones, que es mucho mayor que los 157 bytes de la rutina en s\u00ed. Sin embargo, si renunciamos a las dos \u00faltimas filas en lugar de s\u00f3lo a la \u00faltima, podemos reducir much\u00edsimo la memoria consumida. Pero eso ser\u00e1 en el pr\u00f3ximo art\u00edculo.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cuando ten\u00eda 12 a\u00f1os hered\u00e9 el Sinclair ZX Spectrum de mi hermano. Era un ordenador que me fascinaba, y con \u00e9l aprend\u00ed a programar, primero en BASIC, y luego directamente en Ensamblador. Tambi\u00e9n aprend\u00ed rudimentos de electr\u00f3nica digital, y gracias a ello constru\u00ed varios circuitos que le acopl\u00e9, como un teclado nuevo, un puerto de &hellip; <a href=\"https:\/\/blog.rastersoft.com\/?p=2703\" class=\"more-link\">Seguir leyendo <span class=\"screen-reader-text\">Pintando en el Spectrum (1)<\/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-2703","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\/2703","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=2703"}],"version-history":[{"count":16,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2703\/revisions"}],"predecessor-version":[{"id":2762,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2703\/revisions\/2762"}],"wp:attachment":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2703"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2703"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2703"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}