Hola! Luego de un tiempo prudencial, volvemos con los tutoriales. La idea a partir de ahora es tomar diferentes aspectos de la programación de nuestra amada C64, para que cada uno pueda aprovecharlo para lo que desee, sea desarrollo de demos, juegos, música, gráficos, etc.
Quería iniciar con un tutorial de interrupciones de raster pero, leyendo diversas fuentes, me gustó mucho uno publicado en http://www.antimon.org/dl/c64/code/raster.txt (que lleva unos cuantos añitos) el cual cuenta con un enfoque básico y claro, así que opté por realizar una traducción casi directa, con agregados míos.
Rasters – Que son y como usarlos.
por Bruce Vrieling – (traducción realizada por Mauro Cifuentes)
Cualquiera que haya jugado con las interrupciones en la C64 escucho en algún momento mencionar el concepto de ‘interrupciones por raster’. Pero… ¿Que es un raster? ¿y como podemos utilizar dichas interrupciones?
Una descripción general
Una interrupción, como su nombre lo indica, es una llamada que se le hace al micro, y lo que sucede es que:
. el micro termina de ejecutar la instrucción que estaba haciendo
. guarda todo el estado (registros, PC…)
. SALTA a la dirección asignada donde esta el código que se ejecuta para dicha interrupción
. ejecuta dicho código
. restaura los registros
. retorna al punto siguiente a la instrucción que interrumpió
. y continua la ejecución del programa como si nada hubiese pasado.
Ejemplo de interrupción son las generadas por la CIA para realizar ciertas tareas, como escanear el teclado, hacer titilar el cursor, actualizar el reloj, etc.
Y a todo esto ¿Como cambiamos el curso normal de acción de la interrupción? ¿Como podemos utilizarlo para nuestros fines? Es mas que sencillo.
La C64 tiene un PUNTERO(*1) en las posiciones de memoria 788/9 ($314/5), el cual apunta a la dirección donde esta el código del Kernel que se ejecuta 60 veces por segundo. Si cambiamos este puntero a NUESTRO CÓDIGO, y al finalizar el mismo saltamos a la posición donde esta el código del Kernal (que es $EA31) y tienes el cuidado de no pisar nada, tendrás tu código ejecutándose 60 veces por segundo.
(*1) PUNTERO: posiciones de memoria que guardan la posición de memoria a la que apuntan. En el texto original dice VECTOR, que es lo mismo.
Ejemplo:
// Compilar con Kickassembler
// BasicUpstart2: esta macro genera codigo
// en BASIC para iniciar el programa
BasicUpstart2(main)
/*
Interrupts - LO BASICO
Este programa cicla los colores del borde, 60 veces
por segundo
*/
* = $1000 "Main Program"
main:
sei // Deshabilita interrupciones
lda #<intcode // Seteo en 788/9
sta 788 // el byte bajo y el alto
lda #>intcode // de nuestra rutina
sta 789
cli // habilito las interrupciones
rts // retorno (en este caso BASIC)
// nuestra super rutina
intcode:
inc $d020 // cicla el color de borde
jmp $ea31 // salto a las rutinas del sistema
Si compilas esto con Kickassembler (si, lo siento…) podrás ver que el borde cicla los colores 60 veces por segundo (en pal: 50).
Fíjense que en el bloque ‘main’, este comienza con SEI y finaliza con CLI, antes del RTS que vuelve al BASIC. No queremos que ocurra una interrupción mientras estamos cambiando el puntero de interrupción, así que las deshabilitamos con SEI y las restauramos con CLI.
Piensen: podríamos estar modificando 788, pero ocurre una interrupción antes de modificar 789, con lo cual el puntero puede quedar apuntando a cualquier parte.
El ejemplo que mostré es muy simple. Hay muchas cosas útiles que se pueden hacer con interrupciones, como tocar música mientras tipeamos un programa en BASIC. GEOS también usa interrupciones para controlar el mouse y disparar eventos. Las interrupciones son una herramienta poderosa y el concepto concerniente a las del raster específicamente es particularmente útil para muchos casos.
El Raster
¿y al final… que es?
Cuando nos referimos a raster estamos hablando de una interrupción que se dispara cuando el rayo del monitor dibuja una linea especifica en la pantalla de video. Hay diferentes fuentes que pueden producir interrupciones, no estamos limitados a las que el chip CIA puede producir. El Raster depende de interrupciones generadas por el VIC2. Podemos hacer que el color del borde cambie a una altura determinada de la pantalla, por ejemplo: cuando la linea de pantalla que especificamos comienza a re-dibujarse ocurre la interrupción, entonces tu código rápidamente debería cambiar el color del borde.
También podemos poner media pantalla en alta resolución, y el resto en texto, o cambiar el set de caracteres, etc…
Algunos hechos de como se re-dibuja la pantalla de video: Esta se actualiza 60 veces por segundo (50 en las PAL). Contiene 200 lineas en el display de 25×40, numeradas de 50 a 250 (noten que hay mas lineas visibles, por ejemplo el borde superior y el borde inferior). El re-dibujado de la pantalla se sincroniza con la frecuencia de la red eléctrica (60 hz en USA, 50 hz en Europa). Por eso hay diferencia de velocidades en juegos, ejemplo el Giana Sisters va mas rápido en NTSC que en PAL, solamente porque el refresco ocurre mas veces por segundo, y el juego se sincroniza con esa frecuencia de refresco.
Pero entonces… ¿las maquinas NTSC van mas rápido que las europeas?
NO… solamente el procesador es interrumpido mas veces por segundo, lo que realmente ocurre es que son ligeramente mas lentas 😦
¿Por que debemos preocuparnos por las interrupciones de video? Si la pantalla se refresca 60 veces por segundo y las interrupciones estandar ocurren 60 veces por segundo, ¿por que directamente no ponemos algo de código dentro del código de la interrupción estandar que haga lo que queremos con la pantalla?
Porque estas 2 interrupciones no están sincronizadas, ninguna de ellas ocurren exactamente 60 veces por segundo, y las diferencias son mas que suficientes para que sea imposible coordinar una actividad de cualquier tipo en la pantalla. Cuando utilizamos una interrupción de video sabemos exactamente a que linea de raster de la pantalla ocurre.
Entonces, hagamos un resumen
Sabemos que las interrupciones regulares ocurren 60 veces por segundo, y que podemos hacer que se genere una interrupción a una determinada linea de la pantalla (la cual se actualiza 60 veces por segundo). Un pequeño inconveniente es que ambas interrupciones (estandar y raster) las atiende el mismo puntero (por lo que tenemos 120 interrupciones por segundo, 60 por las estandar, otras 60 por el video). Nuestro código tendrá que checkear que fuente de interrupción es y actuar en consecuencia.
el sistema necesita una interrupción que ocurra 60 veces por segundo para que haga la gestión interna, y utiliza el reloj interno de la CIA para generarla. También vamos a querer que cada vez que a cierta linea en la pantalla se dispare una interrupción (lo cual ocurrirá 60 veces por segundo). La interrupción estandar deberá enviarse al código en Rom correspondiente, mientras nuestra interrupción de video deberá ir a nuestro código.
Si ambas ocurren a 60 veces por segundo, ¿Por que no ejecutar el código de la Rom del sistema y el nuestro en la MISMA interrupción? Ya sabemos que la CIA no es buena para esto, ya que está fuera de sincronismo con el video. ¿Por que no apagar la interrupción de CIA, habilitar la interrupción de raster, y hacer ambos trabajos en una interrupción? Entonces tendríamos una señal de interrupción que ocurre 60 veces por segundo, y esta en perfecto sincronismo con el video.
Eso es exactamente lo que vamos a hacer
Alguno se habrá percatado de un ligero fallo en la lógica anteriormente expuesta. Con el fin de simplificar no mencione que necesitaras DOS interrupciones por pantalla para realizar algo útil. ¿Por que dos? porque cualquier cambio que realices a una determinada posición de la pantalla, deberás revertir cuando llegues al tope de la misma. Ejemplo, si quisiéramos que 3/4 de la pantalla fuese en alta resolución, y el cuarto restante texto, entonces necesitaremos una interrupción en 3/4 de la pantalla para cambiar a texto, y otra en el tope (scanline 50) para restaurar hires. También podríamos querer ‘trocear’ la pantalla en N modos de video, por lo cual tendremos N + 1 interrupciones.
Bueno, basta de charla, hagamos un pequeño ejemplo para mostrar lo que hablamos. Vamos a hacer lo mismo que en el primer ejemplo, con la diferencia que en vez de utilizar una interrupción de CIA usaremos la del raster, con lo cual no veras aquí grandes diferencias (visibles) con el primer ejemplo.
BasicUpstart2(main)
/*
Interrupts - LO BASICO
Este programa cicla los colores del borde, 60 veces
por segundo. En este ejemplo la fuente de interrupción
es el VIC2
*/
* = $1000 "Main Program"
main:
sei // deshabilito interrupciones
lda #$7f // apago las interrupciones
sta $dc0d // de la CIA
lda $d01a // activo la irq
ora #$01 // por raster
sta $d01a
lda $d011 // borro el MSB de raster
and #$7f
sta $d011
lda #100 // especifico una linea de raster
sta $d012 // LSB (puede ser cualquiera aca)
lda #<intcode // Seteo en 788/9
sta 788 // el byte bajo y el alto
lda #>intcode // de nuestra rutina
sta 789
cli // habilito las interrupciones
rts // retorno (en este caso BASIC)
intcode:
inc $d020 // ciclo el color del borde
lda #$ff // (*2) ver explicación a continuación
sta $d019
//inc $d019 // (*3) forma optimizada
lda #100 // Restauro el punto de interrupción
sta $d012 // para el proximo refresco
jmp $ea31 // salto a las rutinas del sistema
El código de ejemplo esta bastante claro en sus comentarios, si se entendió el anterior no deberías tener problemas con este. El único problema que encontré y que tuve que googlear un poco para entenderlo (y lo vi bien explicado en http://codebase64.org/doku.php?id=base:introduction_to_raster_irqs) fue en la parte en que escribimos nuestro código dentro de la interrupción, y que marqué con un (*2).
¿Para que hacemos esto? $d019 es el registro de estado de interrupciones del VIC-II. Cuando ocurre una interrupción si leemos el registro nos dice de que tipo es, y si escribimos el registro le acusamos recibo que atendimos dicha interrupción. Si no escribimos en este registro (concrétamente el bit 0) la condición de interrupción estará presente todo el tiempo, y el código se ejecutará permanentemente independiente de la linea del raster. Pueden probar comentándolo y verán que queda un patrón de colores muy lindo, pero no vuelve jamás al BASIC.
La linea siguiente que dejé comentada en (*3) hace a efectos prácticos lo mismo, solamente que emplea menos ciclos de reloj, por lo cual es una forma más utilizada. En el siguiente ejemplo se nota la optimización de utilizar una u otra.
Vamos con un ejemplo de código mas complejo, para ya ir finalizando el tutorial. Aquí vamos a dividir el borde en 2 por la mitad, poniendo en blanco la parte superior, y en negro la inferior.
BasicUpstart2(main)
/*
Interrupts - LO BASICO
Este programa divide el borde en 2 colores
*/
* = $1000 "Main Program"
.const COLOUR1 = 0
.const COLOUR2 = 1
.const LINE1 = 20
.const LINE2 = 150
main:
sei // Todo esto es lo mismo que
// en ejemplos anteriores
lda #$7f
sta $dc0d
lda $d01a
ora #$01
sta $d01a
lda $d011
and #$7f
sta $d011
lda #LINE1
sta $d012
lda #<intcode
sta 788
lda #>intcode
sta 789
cli
rts
intcode:
lda modeflag // vemos si estamos en la parte
// superior o inferior
beq mode1 // de la pantalla
jmp mode2
mode1:
lda #$01 // invertimos el modeflag
sta modeflag // para que la proxima vez vaya a
// la otra parte del codigo
lda #COLOUR1 // ponemos color
sta $d020
lda #LINE1 // seteamos nuevamente la
sta $d012 // linea de interrupción
inc $d019 // acusamos recibo de interrupción
jmp $ea31 // esta parte va a las rutinas
// del kernel en ROM
mode2:
lda #$00 // invertimos el modeflag
sta modeflag
lda #COLOUR2 // ponemos el color
sta $d020
lda #LINE2 // seteamos linea de raster
sta $d012
// lda $#ff // interesante en este ejemplo
// sta $d019 // la diferencia de usar
// uno u otro metodo.
inc $d019 // acusamos recibo
// PEEERO:
pla // Aqui salimos completamente
tay // esta es la forma de salir de la
pla // interrupción, restaurando
tax // los registros.
pla // lo explico con mas detalle a continuación
rti
modeflag: .byte 0
Este ejemplo es similar a los anteriores, con la única diferencia que utilizamos un flag para atender la parte superior o inferior de la pantalla. En la parte inferior de la pantalla (mode1) vamos a hacer lo mismo que antes, seteando el color negro, poniendo la siguiente linea de interrupción en 20 y finalizando la interrupción saltando a $ea31.
Pero en el bloque mode2 vamos a atender la parte superior de la pantalla. Hacemos lo mismo, seteamos el color en blanco, seteamos la linea en 150, pero en vez de saltar a las rutinas del sistema finalizamos la interrupción con el bloque pla, tay…. pla, rti.
¿Por que hacemos esto? porque al poner 2 puntos de interrupción vamos a tener 120 llamadas a nuestro código, y las rutinas del sistema solo debemos llamarlas 60 veces, así que en el bloque en negro llamamos a las rutinas, pero en el bloque en blanco salimos de la interrupción…
¿Y por que todo ese código? Bueno, al principio del artículo habíamos dicho que cuando ocurre una interrupción el micro guarda el estado, básicamente los registros y el Program Counter. Con este bloque de código lo que estamos haciendo es restaurar los registros A, X e Y al estado en que estaban al momento en que ocurrió la interrupción.
Un detalle que ya comenté: cuando acusamos el recibo de la interrupción puse comentado la forma lda-sta. Si prueban una forma u otra van a ver que varia mucho la estabilidad del raster.
Actualización: Como la plataforma WordPress me hizo alguna que otra perrada, decidí subir los archivos de ejemplo a Github en esta dirección: https://github.com/moonorongo/c64_tutorial_samples
Y esto es todo por hoy… con esta base ya podemos encarar la próxima entrega, en la cual voy a explicar como hacer un raster ‘estable’. Nos vemos!