En el tutorial de hoy veremos como hacer un raster estable que, si bien no es complicado, requiere tener un poco mas de conocimiento del VIC2 y de la duración de las instrucciones del 6502. Voy a poner 4 ejemplos de código, el primero de los cuales es una interrupción simple (en la que cambiamos el color como lo hicimos en anteriores posts), en el segundo ejemplo muestro ya una primera aproximación a la técnica, en la cual pongo con color blanco la parte sin estabilizar, y en color rojo la parte estabilizada.
En el tercer ejemplo muestro la corrección final, en la que se ve que el cáracter que parpadeaba ya no parpadea más, y finalmente en el cuarto ejemplo esta el código completo y comentado para que se entienda, con las explicaciones y la cantidad de clocks utilizados por cada instrucción.
Técnicas para estabilizar/sincronizar el raster hay varias, yo me decidí por esta debido a que es bastante común y en teoría simple. Digo en teoría porque estuve un par de semanas para entender los detalles a fondo, pero al final pude comprender bastante como funciona.
Cuando intentamos hacer un rasterbar nos encontramos con algo bastante molesto: por más que nosotros seteemos una interrupción y hagamos todos los deberes en el medio de la pantalla nos queda una vibración (a la que llamaremos ‘jitter’ a partir de ahora) bastante molesta, como la que pueden ver a continuación:
Este es el código correspondiente
BasicUpstart2(main)
.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0
.const LINE = 134
.const LINE2 = 160
main:
{
sei
lda #$7f
sta $dc0d
sta $dd0d
lda #$35
sta $01
lda $d01a
ora #$01
sta $d01a
lda $d011
and #$7f
sta $d011
lda #LINE
sta $d012
lda #intcode
sta $ffff
asl $d019
bit $dc0d
bit $dd0d
cli
dummy_loop:
ldx #$00
nop
inx
nop
ldx $d020
cpx #33
nop
ldy #$00
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
ldx #$00
nop
ldx $d020
inx
nop
cpx #33
nop
ldy #$00
ldx $d020
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
jmp dummy_loop
}
intcode:
{
pha
txa
pha
tya
pha
setColor(RASTERUNSTABLE)
lda #intcode_restore
sta $ffff
ldx #LINE2
stx $d012
asl $d019
pla
tay
pla
tax
pla
rti
}
intcode_restore:
{
pha
txa
pha
tya
pha
setColor(NORMALCOLOR)
lda #intcode
sta $ffff
ldx #LINE
stx $d012
asl $d019
pla
tay
pla
tax
pla
rti
}
.macro setColor(color) {
lda #color
sta $d020
sta $d021
}
¿Y esto por que esta ocurriendo? ¿No se supone que el VIC avisa al procesador cuando llegamos a esa linea?
Pues si… y no. El VIC2 avisa al procesador mediante una interrupción que el raster llegó a dicha linea… y sigue su tarea, que es seguir dibujando la pantalla (y allí es donde se va todo a la m…). En el camino el procesador recibió la interrupción, finalizó la instrucción que estaba ejecutando, guardo el estado en el stack, hizo el salto a nuestra rutina… para este punto el raster ya esta por la mitad de la pantalla, y encima, como la instrucción que estaba ejecutando puede tener una longitud entre 2 y 7 ciclos de clock, nos aparece ese condenado jitter.
El problema es que no podemos saber de antemano que instrucción estaba ejecutando el procesador. Si tuviesemos una forma de saber que se estaba ejecutando o MEJOR AUN podemos especificar una instrucción de antemano, entonces el jitter desaparece (la mayoría, después veremos que queda un chiquito por eliminar).
A alguien muy creativo se le ocurrió lo siguiente:
“Si pongo un punto de interrupción en una linea, inmediatamente cuando sucede la interrupción SETEO LA SIGUIENTE LINEA, y luego completo el resto de los ciclos de clocks que faltan para terminar la misma con una instrucción conocida (8 NOP seguidos), entonces cuando ocurra la próxima interrupción SABRE QUE SE ESTA EJECUTANDO UN NOP”
Efectivamente, como podemos ver en la siguiente imagen:
BasicUpstart2(main)
.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0
.const LINE = 132
.const LINE2 = 160
main:
{
sei
lda #$7f
sta $dc0d
sta $dd0d
lda #$35
sta $01
lda $d01a
ora #$01
sta $d01a
lda $d011
and #$7f
sta $d011
lda #LINE
sta $d012
lda #intcode
sta $ffff
asl $d019
bit $dc0d
bit $dd0d
cli
dummy_loop:
ldx #$00
nop
inx
nop
ldx $d020
cpx #33
nop
ldy #$00
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
ldx #$00
nop
ldx $d020
inx
nop
cpx #33
nop
ldy #$00
ldx $d020
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
jmp dummy_loop
}
intcode:
{
pha
tya
pha
txa
pha
lda #intcode_stable
sta $ffff
setColor(RASTERUNSTABLE)
inc $d012
asl $d019
tsx
cli
nop
nop
nop
nop
nop
nop
nop
nop
nop
}
intcode_stable:
{
txs
waitLine(8)
noEstabilizaJitter()
setColor(0)
nop
nop
nop
setColor(RASTERCOLOR1)
lda #intcode_restore
sta $ffff
ldx #LINE2
stx $d012
asl $d019
pla
tax
pla
tay
pla
rti
}
intcode_restore:
{
pha
tya
pha
txa
pha
setColor(NORMALCOLOR)
lda #intcode
sta $ffff
ldx #LINE
stx $d012
asl $d019
pla
tax
pla
tay
pla
rti
}
.macro waitLine(cant_loops) {
ldx #cant_loops
dex
bne *-1
bit $00
}
.macro estabilizaJitter() {
lda $d012
cmp $d012
beq *+2
}
.macro noEstabilizaJitter() {
nop
nop
nop
bit $00
nop
}
.macro setColor(color) {
lda #color
sta $d020
sta $d021
}
Peeero… está CASI estable. Si bien el jitter disminuye considerablemente, todavía queda una linea de un cáracter que esta parpadeando. Esto ocurre porque, si bien nuestra interrupción ocurrió durante una instrucción NOP, esta puede haber estado ejecutándose o haber finalizado, lo cual nos da esa diferencia de un clock.
Para ello tenemos que introducir una pausa (puede ser un bucle) MUY precisa, para que nos lleve CASI al final de la linea (una linea completa lleva 63 ciclos de procesador, en una c64 pal, y en cada ciclo el VIC2 dibuja una linea de un cáracter de ancho). Allí cargamos en A el valor de $d012, inmediatamente lo comparamos con el valor de $d012 (que loco no?) y si es igual saltamos… a la siguiente linea de código.
Esto que estoy explicando parece un sinsentido total, es mas, les muestro el código para que vean:
lda $d012 // 4 clocks
cmp $d012 // 5 clocks
beq siguiente // 2 clocks si distinto o 3 clocks si igual
// si es igual va a siguiente y si no tambien… pfff
siguiente:
// el resto
// del codigo
// aqui…
O sea… si es distinto va a siguiente y si es igual tambien… CUAL ES LA LOGICA?
A lo mejor ustedes ya lo entendieron, esta fue la parte que mas me costó comprender. En primer lugar, este bloque de código tiene sentido cuando el raster ESTA CERCA DE FINALIZAR LA LINEA. El truco pasa por comparar el valor de $D012 (la linea de raster) con el valor que toma unos pocos clocks después. Si es el mismo valor quiere decir que la interrupción ocurrió justo cuando finalizó la ejecución del NOP, entonces SALTA a la siguiente instrucción, lo cual le insume 3 ciclos de clock.
Ahora bien, si el valor da diferente es porque hubo un clock de más (producido porque se estaba ejecutando todavía el NOP). El salto no se ejecuta,, lo cual le insume 2 ciclos de clock… a la siguiente instrucción (a la misma que hubiera hecho el salto) ¿Ven la magia? si hay un clock de mas suma 2 clocks, si no hay clock suma 3… y se nos estabiliza el raster.
BasicUpstart2(main)
.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0
.const LINE = 52
.const LINE2 = 132
main:
{
sei
lda #$7f
sta $dc0d
sta $dd0d
lda #$35
sta $01
lda $d01a
ora #$01
sta $d01a
lda $d011
and #$7f
sta $d011
lda #LINE
sta $d012
lda #intcode
sta $ffff
asl $d019
bit $dc0d
bit $dd0d
cli
dummy_loop:
ldx #$00
nop
inx
nop
ldx $d020
cpx #33
nop
ldy #$00
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
ldx #$00
nop
ldx $d020
inx
nop
cpx #33
nop
ldy #$00
ldx $d020
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
jmp dummy_loop
}
intcode:
{
pha
tya
pha
txa
pha
lda #intcode_stable
sta $ffff
setColor(RASTERUNSTABLE)
inc $d012
asl $d019
tsx
cli
nop
nop
nop
nop
nop
nop
nop
nop
nop
}
intcode_stable:
{
txs
waitLine(8)
estabilizaJitter()
setColor(0)
nop
nop
nop
setColor(RASTERCOLOR1)
lda #intcode_restore
sta $ffff
ldx #LINE2
stx $d012
asl $d019
pla
tax
pla
tay
pla
rti
}
intcode_restore:
{
pha
tya
pha
txa
pha
setColor(NORMALCOLOR)
dec $d020
lda #intcode
sta $ffff
ldx #LINE
stx $d012
asl $d019
pla
tax
pla
tay
pla
rti
}
.macro waitLine(cant_loops) {
ldx #cant_loops
dex
bne *-1
bit $00
}
.macro estabilizaJitter() {
lda $d012
cmp $d012
beq *+2
}
.macro noEstabilizaJitter() {
nop
nop
nop
bit $00
nop
}
.macro setColor(color) {
lda #color
sta $d020
sta $d021
}
En estos ejemplos previos puse una linea blanca, que indica el punto de la primera interrupción y la parte sin estabilizar, luego en negro para mostrar el resto de jitter, y finalmente en rojo la parte estabilizada. Pueden ver que la linea final de la rasterbar sigue con jitter, esto es porque no me tomé el trabajo de estabilizarla.
El código final está a continuación, en el mismo estan cada uno de los pasos a realizar comentados y ademas los ciclos de clock que lleva cada instrucción o macro utilizada.
BasicUpstart2(main)
.const RASTERUNSTABLE = $1
.const RASTERCOLOR1 = $2
.const NORMALCOLOR = 0
// no podemos poner LINE = 131, 139, 147, 155... porque son badlines y rompen
// hay que recalcular waitLine() para las bad lines
.const LINE = 132
.const LINE2 = 160
main:
{
sei
// disable CIA
lda #$7f
sta $dc0d
sta $dd0d
// Bank out kernal and basic
// ponemos disponibles
// $A000-$BFFF (BASIC)
// y $E000-$FFFF (KERNAL)
lda #$35
sta $01
/*
Basicamente: apagamos las roms del basic y el kernel
(realmente, nos interesa apagar el kernel porque
necesitamos escribir un valor alli en $fffe y $ffff
y el sistema pueda leer lo que escribimos)
mas info leer: https://dustlayer.com/c64-architecture/2013/4/13/ram-under-rom
*/
// aca: todo lo de antes...
lda $d01a // enable VIC IRQ
ora #$01
sta $d01a
lda $d011 // clear MSB raster
and #$7f
sta $d011
lda #LINE
sta $d012
lda #intcode
sta $ffff
// esto lo hacemos para que no haga
// cosas raras cuando arranca...
asl $d019 // Ack any previous raster interrupt
bit $dc0d // reading the interrupt control registers
bit $dd0d // clears them
cli
// este dummy loop es para que la interrupcion ocurra
// en cualquier instruccion, asi tenemos un caso lo mas
// real posible... en los ejemplos que encontre
// normalmente hacian un jmp *, lo cual no era real, ya que
// la interrupcion ocurria en una instruccion que conocemos (JMP)
dummy_loop:
ldx #$00
nop
inx
nop
ldx $d020
cpx #33
nop
ldy #$00
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
ldx #$00
nop
ldx $d020
inx
nop
cpx #33
nop
ldy #$00
ldx $d020
iny
bit $00
nop
ldx $d020
nop
ldx #$00
nop
inx
ldx $d020
nop
cpx #33
nop
ldy #$00
iny
ldx $d020
bit $00
nop
ldx $d020
nop
jmp dummy_loop
}
intcode:
{
// GUARDAMOS REGISTROS *1
/*
En los ejemplos que vi lo hace mejor
escribiendo codigo automodificable
el cual ocupa menos clocks del procesador
pero a fines didacticos esto es mas claro
*/
pha
tya
pha
txa
pha
lda #intcode_stable
sta $ffff
// pongo la interrupcion en la proxima linea
inc $d012
asl $d019
// Almacena el actual puntero del stack
// porque no queremos volver aca cuando se produzca el RTI
// sino que queremos que vaya al dummy_loop
// (o la parte de nuestro codigo)
tsx
cli
// en algun punto en los siguientes nop's
// se ejecutara la siguiente interrupcion
nop
nop
nop
nop
nop
nop
nop
nop
nop
}
intcode_stable:
{
// en este punto, se ejecuto la interrupcion en un comando NOP,
// por lo tanto, ya tenemos 1 o 2 clocks corridos a lo sumo
// (dependiendo si estaba ejecutando la instruccion cuando llamó
// la interrupcion o si la habÃa finalizado)
// nop 1 o 2 clocks
// salto interrupcion 7 clocks
// 8 o 9 clocks
// Restaura el puntero del stack al punto de retorno
// donde se llamo por primera vez (y que guardamos en X)
// nos interesa que esté en el punto donde guardamos (*1)
// si no el RTI va a volver acá, y no es la idea
txs
// 10 u 11 clocks
// espero que pase CASI toda la linea
// el calculo:
// 2 + (7 * (2 + 3)) + 2 + 3 = 42
waitLine(8) // (43 + (10 u 11)) = 52/53)
// corrige el jitter de 1 clock del nop
// para q esto funcione tenemos q estar casi al final
// de la linea: cargamos en A el valor de $d012
// luego lo comparamos
// si es igual hay un ciclo de menos, entonces salta (3 clocks)
// si es distinto pasa, hay un ciclo de mas, no salta (2 clocks)
// y con eso se estabiliza
estabilizaJitter()
setColor(RASTERCOLOR1)
// restauro interrupcion a 2da linea
lda #intcode_restore
sta $ffff
ldx #LINE2
stx $d012
asl $d019
pla
tax
pla
tay
pla
rti
}
intcode_restore:
{
pha
tya
pha
txa
pha
// en este punto esta sin estabilizar
// necesitamos hacer nuevamente todo lo que hicimos
// anteriormente...
setColor(NORMALCOLOR)
// restauro interrupcion
lda #intcode
sta $ffff
ldx #LINE
stx $d012
asl $d019
pla
tax
pla
tay
pla
rti
}
.macro waitLine(cant_loops) {
ldx #cant_loops // 2
dex // 2
bne *-1 // 3
bit $00 // 3
}
.macro estabilizaJitter() {
lda $d012 // 4 (56/57)
cmp $d012 // 5 (62/63)
beq *+2 // 2 distinto o 3 igual
}
.macro noEstabilizaJitter() {
nop
nop
nop
bit $00
nop
// 11 clocks
}
.macro setColor(color) {
lda #color // 2
sta $d020 // 4
sta $d021 // 4
}
Para el final me he dejado los detalles tediosos del VIC.
Este código funciona en tanto y cuando no usemos las lineas 51, 59, 67 … y asi de 8 en 8, las cuales se conocen como “bad lines”. ¿Que esta pasando? Bueno, en dichas lineas (que coinciden con la primera linea de una fila de caracteres) el VIC2 interrumpe durante 40 CICLOS al procesador para obtener los códigos de caracteres de una linea desde la matriz de vídeo, haciendo que se nos de-sincronice el raster y se vaya todo al demonio. Lamentablemente no encontré todavía información para evitar esto, en cuanto encuentre algo actualizo el artículo.
Por último, dejo una lista de enlaces para profundizar mas en el tema:
Double IRQ explained: http://codebase64.org/doku.php?id=base:double_irq_explained
STABLE RASTER ROUTINE: http://codebase64.org/doku.php?id=base:stable_raster_routine
VIC-II FOR BEGINNERS PART 3 – BEYOND THE SCREEN: RASTERS AND CYCLES https://dustlayer.com/vic-ii/2013/4/25/vic-ii-for-beginners-beyond-the-screen-rasters-cycle
The MOS 6567/6569 video controller (VIC-II) http://www.zimmers.net/cbmpics/cbm/c64/vic-ii.txt
Y esto es todo por hoy. Pueden descargar el código desde Github, visitar mi página en Facebook y comentar cualquier duda que tengan.
Hasta la próxima!