Primero quiero mostraros un gráfico para que entendáis en qué punto estamos y hacia donde queremos llegar. Es una representación en la que cada columna equivale a una lección. Ojo, los tiempos no están a escala.
La primera columna es la carga original del juego que mostramos en la lección 1, es decir son 6 bloques físicos (3 lógicos) con carga estándar.
La segunda columna es cuando en la lección 2 conseguimos reducir las cabeceras de los 2 últimos bloques lógicos (pantalla y juego), pero manteniendo la carga estándar.
En la tercera columna (tercera lección) quitamos las pausas entre bloques, recortamos los tiempos de tono guía y ligeramente los pulsos que codifican los ceros y los unos, pero manteniéndonos en carga estándar.
En esta cuarta lección lo que pretendemos es modificar el cargador de la ROM (LD-BYTES) para acelerar la carga (y de paso mostrar otros colores de borde), evidentemente sólo en los 2 últimos bloques que es cuando podemos tener disponible el nuevo cargador.
Como no podemos modificar la ROM, lo que hacemos es ubicar el nuevo cargador (LD-BYTES) en RAM. Desgraciadamente no nos vale cualquier parte de RAM, tiene que ser memoria no contenida o memoria alta (rango $8000-$FFFF), porque de lo contrario se producirían retardos en las instrucciones que producirían errores de carga.
Antes de nada os voy a mostrar el cargador de la ROM:
Código: Seleccionar todo
;; LD-BYTES
L0556: INC D ; reset the zero flag without disturbing carry.
EX AF,AF' ; preserve entry flags.
DEC D ; restore high byte of length.
DI ; disable interrupts
LD A,$0F ; make the border white and mic off.
OUT ($FE),A ; output to port.
LD HL,L053F ; Address: SA/LD-RET
PUSH HL ; is saved on stack as terminating routine.
; the reading of the EAR bit (D6) will always be preceded by a test of the
; space key (D0), so store the initial post-test state.
IN A,($FE) ; read the ear state - bit 6.
RRA ; rotate to bit 5.
AND $20 ; isolate this bit.
OR $02 ; combine with red border colour.
LD C,A ; and store initial state long-term in C.
CP A ; set the zero flag.
;
;; LD-BREAK
L056B: RET NZ ; return if at any time space is pressed.
;; LD-START
L056C: CALL L05E7 ; routine LD-EDGE-1
JR NC,L056B ; back to LD-BREAK with time out and no
; edge present on tape.
; but continue when a transition is found on tape.
LD HL,$0415 ; set up 16-bit outer loop counter for
; approx 1 second delay.
;; LD-WAIT
L0574: DJNZ L0574 ; self loop to LD-WAIT (for 256 times)
DEC HL ; decrease outer loop counter.
LD A,H ; test for
OR L ; zero.
JR NZ,L0574 ; back to LD-WAIT, if not zero, with zero in B.
; continue after delay with H holding zero and B also.
; sample 256 edges to check that we are in the middle of a lead-in section.
CALL L05E3 ; routine LD-EDGE-2
JR NC,L056B ; back to LD-BREAK
; if no edges at all.
;; LD-LEADER
L0580: LD B,$9C ; set timing value.
CALL L05E3 ; routine LD-EDGE-2
JR NC,L056B ; back to LD-BREAK if time-out
LD A,$C6 ; two edges must be spaced apart.
CP B ; compare
JR NC,L056C ; back to LD-START if too close together for a
; lead-in.
INC H ; proceed to test 256 edged sample.
JR NZ,L0580 ; back to LD-LEADER while more to do.
; sample indicates we are in the middle of a two or five second lead-in.
; Now test every edge looking for the terminal sync signal.
;; LD-SYNC
L058F: LD B,$C9 ; initial timing value in B.
CALL L05E7 ; routine LD-EDGE-1
JR NC,L056B ; back to LD-BREAK with time-out.
LD A,B ; fetch augmented timing value from B.
CP $D4 ; compare
JR NC,L058F ; back to LD-SYNC if gap too big, that is,
; a normal lead-in edge gap.
; but a short gap will be the sync pulse.
; in which case another edge should appear before B rises to $FF
CALL L05E7 ; routine LD-EDGE-1
RET NC ; return with time-out.
; proceed when the sync at the end of the lead-in is found.
; We are about to load data so change the border colours.
LD A,C ; fetch long-term mask from C
XOR $03 ; and make blue/yellow.
LD C,A ; store the new long-term byte.
LD H,$00 ; set up parity byte as zero.
LD B,$B0 ; timing.
JR L05C8 ; forward to LD-MARKER
; the loop mid entry point with the alternate
; zero flag reset to indicate first byte
; is discarded.
; --------------
; the loading loop loads each byte and is entered at the mid point.
;; LD-LOOP
L05A9: EX AF,AF' ; restore entry flags and type in A.
JR NZ,L05B3 ; forward to LD-FLAG if awaiting initial flag
; which is to be discarded.
JR NC,L05BD ; forward to LD-VERIFY if not to be loaded.
LD (IX+$00),L ; place loaded byte at memory location.
JR L05C2 ; forward to LD-NEXT
; ---
;; LD-FLAG
L05B3: RL C ; preserve carry (verify) flag in long-term
; state byte. Bit 7 can be lost.
XOR L ; compare type in A with first byte in L.
RET NZ ; return if no match e.g. CODE vs. DATA.
; continue when data type matches.
LD A,C ; fetch byte with stored carry
RRA ; rotate it to carry flag again
LD C,A ; restore long-term port state.
INC DE ; increment length ??
JR L05C4 ; forward to LD-DEC.
; but why not to location after ?
; ---
; for verification the byte read from tape is compared with that in memory.
;; LD-VERIFY
L05BD: LD A,(IX+$00) ; fetch byte from memory.
XOR L ; compare with that on tape
RET NZ ; return if not zero.
;; LD-NEXT
L05C2: INC IX ; increment byte pointer.
;; LD-DEC
L05C4: DEC DE ; decrement length.
EX AF,AF' ; store the flags.
LD B,$B2 ; timing.
; when starting to read 8 bits the receiving byte is marked with bit at right.
; when this is rotated out again then 8 bits have been read.
;; LD-MARKER
L05C8: LD L,$01 ; initialize as %00000001
;; LD-8-BITS
L05CA: CALL L05E3 ; routine LD-EDGE-2 increments B relative to
; gap between 2 edges.
RET NC ; return with time-out.
LD A,$CB ; the comparison byte.
CP B ; compare to incremented value of B.
; if B is higher then bit on tape was set.
; if <= then bit on tape is reset.
RL L ; rotate the carry bit into L.
LD B,$B0 ; reset the B timer byte.
JP NC,L05CA ; JUMP back to LD-8-BITS
; when carry set then marker bit has been passed out and byte is complete.
LD A,H ; fetch the running parity byte.
XOR L ; include the new byte.
LD H,A ; and store back in parity register.
LD A,D ; check length of
OR E ; expected bytes.
JR NZ,L05A9 ; back to LD-LOOP
; while there are more.
; when all bytes loaded then parity byte should be zero.
LD A,H ; fetch parity byte.
CP $01 ; set carry if zero.
RET ; return
; in no carry then error as checksum disagrees.
; -------------------------
; Check signal being loaded
; -------------------------
; An edge is a transition from one mic state to another.
; More specifically a change in bit 6 of value input from port $FE.
; Graphically it is a change of border colour, say, blue to yellow.
; The first entry point looks for two adjacent edges. The second entry point
; is used to find a single edge.
; The B register holds a count, up to 256, within which the edge (or edges)
; must be found. The gap between two edges will be more for a '1' than a '0'
; so the value of B denotes the state of the bit (two edges) read from tape.
; ->
;; LD-EDGE-2
L05E3: CALL L05E7 ; call routine LD-EDGE-1 below.
RET NC ; return if space pressed or time-out.
; else continue and look for another adjacent
; edge which together represent a bit on the
; tape.
; ->
; this entry point is used to find a single edge from above but also
; when detecting a read-in signal on the tape.
;; LD-EDGE-1
L05E7: LD A,$16 ; a delay value of twenty two.
;; LD-DELAY
L05E9: DEC A ; decrement counter
JR NZ,L05E9 ; loop back to LD-DELAY 22 times.
AND A ; clear carry.
;; LD-SAMPLE
L05ED: INC B ; increment the time-out counter.
RET Z ; return with failure when $FF passed.
LD A,$7F ; prepare to read keyboard and EAR port
IN A,($FE) ; row $7FFE. bit 6 is EAR, bit 0 is SPACE key.
RRA ; test outer key the space. (bit 6 moves to 5)
RET NC ; return if space pressed. >>>
XOR C ; compare with initial long-term state.
AND $20 ; isolate bit 5
JR Z,L05ED ; back to LD-SAMPLE if no edge.
; but an edge, a transition of the EAR bit, has been found so switch the
; long-term comparison byte containing both border colour and EAR bit.
LD A,C ; fetch comparison value.
CPL ; switch the bits
LD C,A ; and put back in C for long-term.
AND $07 ; isolate new colour bits.
OR $08 ; set bit 3 - MIC off.
OUT ($FE),A ; send to port to effect the change of colour.
SCF ; set carry flag signaling edge found within
; time allowed.
RET ; return.
En lugar de ir a la solución exacta como en otras lecciones, voy a hacerlo equivocándome varias veces a conciencia para que sea más instructivo. Pues bien, lo primero que se me ocurre es copiar el código del cargador en loader.asm a ver qué pasa:
Código: Seleccionar todo
org xxxx
ini ld sp, $7530
di
db $de, $c0, $37, $0e, $8f, $39, $96
ld hl, yyyy
ld de, zzzz
ld bc, fin-L0556
ldir
ld hl, $5800
ld de, $5801
ld bc, $2ff
ld (hl), l
ldir
scf
sbc a, a
ld ix, $4000
ld de, $1b00
call L0556
scf
sbc a, a
ld ix, $8000
ld de, $8000
call L0556
jp $8400
;; LD-BYTES
L0556: INC D ; reset the zero flag without disturbing carry.
EX AF,AF' ; preserve entry flags.
DEC D ; restore high byte of length.
...
fin
Código: Seleccionar todo
ld hl, yyyy
ld de, zzzz
ld bc, fin-L0556
ldir
Siempre es mala idea poner valores absolutos, ya que si modifico la longitud del cargador tengo que recalcularlo todo de nuevo. Por eso hay que pensarse muy bien estos valores. Como en el código que hay desde ini hasta L0556 no hay ninguna etiqueta, prefiero fijar el ORG a un valor que favorezca lo que hay a continuación de L0556. Los valores adecuados son:
Código: Seleccionar todo
org $10000-fin+ini
ini ld sp, $7530
di
db $de, $c0, $37, $0e, $8f, $39, $96
ld hl, $5ccb+L0556-ini
ld de, L0556
ld bc, fin-L0556
ldir
ld hl, $5800
ld de, $5801
...
Código: Seleccionar todo
FF57 D3 FE OUT ($FE),A
FF59 21 00 00 LD HL,$0000
FF5C E5 PUSH HL
Código: Seleccionar todo
LD HL,$053F ; Address: SA/LD-RET
Comprobemos qué ocurre en Spectaculator. Primero ponemos un punto de ruptura en $5CCB, luego cargamos el TZX y escribimos LOAD"" pulsando Enter. Tras la carga del bloque Basic salta el depurador en $5CCB. Nos vamos un poco más abajo y ponemos otro punto de ruptura donde está el asterisco:
Código: Seleccionar todo
5CF9 37 SCF
5CFA 9F SBC A,A
5CFB DD 21 00 80 LD IX,$8000
5CFF 11 00 80 LD DE,$8000
5D02 CD 51 FF CALL $FF51
* 5D05 C3 00 84 JP $8400
Código: Seleccionar todo
FFA2 07 RLCA
FFA3 80 ADD A,B
FFA4 07 RLCA
FFA5 20 07 JR NZ,$FFAE
FFA7 30 0F JR NC,$FFB8
El problema es que el juego ocupa precisamente toda la memoria alta, que es donde obligatoriamente tiene que ir el cargador. La solución es cargar el juego un poco más abajo y moverlo a posteriori. Nada que no podamos hacer con la socorrida instrucción LDIR.
Código: Seleccionar todo
scf
sbc a, a
ld ix, $8000-fin+L0556
ld de, $8000-fin+L0556
call L0556
ld hl, $8000-fin+L0556
ld de, $8000
ld bc, $8000
ldir
jp $8400
Corregimos el error. Mucho cuidado que los límites de LDDR suelen confundir bastante.
Código: Seleccionar todo
scf
sbc a, a
ld ix, $8000-fin+L0556
ld de, $8000-fin+L0556
call L0556
ld hl, $ffff-fin+L0556
ld de, $ffff
ld bc, $8000
lddr
jp $8400
Código: Seleccionar todo
IN A,($FE) ; read the ear state - bit 6.
RRA ; rotate to bit 5.
AND $20 ; isolate this bit.
; OR $02 ; combine with red border colour.
LD C,A ; and store initial state long-term in C.
CP A ; set the zero flag.
¿Os acordáis lo que os conté hace poco de que era mala idea poner valores absolutos? Pues echadle un vistazo al código máquina: ahora el cargador comienza en $FF53 y tiene longitud $AD. Como hemos hecho las cosas "bien", no tenemos que hacer ningún cambio de valores. Repasamos lo que hemos hecho hasta ahora, copiamos el make.bat en make2.bat (el loader.asm en loader2.asm) y procedemos con la segunda parte de la lección.
En esta segunda parte trataremos de acelerar la carga de los 2 últimos bloques. Para ello tengo que explicar un poco la parte de ensamblador que convierte cambios del puerto EAR en bits que luego se convierten en bytes y se guardan en memoria. La parte clave está en este trozo de código:
Código: Seleccionar todo
;; LD-MARKER
L05C8: LD L,$01 ; initialize as %00000001
;; LD-8-BITS
L05CA: CALL L05E3 ; routine LD-EDGE-2 increments B relative to
; gap between 2 edges.
RET NC ; return with time-out.
LD A,$CB ; the comparison byte.
CP B ; compare to incremented value of B.
; if B is higher then bit on tape was set.
; if <= then bit on tape is reset.
RL L ; rotate the carry bit into L.
LD B,$B0 ; reset the B timer byte.
JP NC,L05CA ; JUMP back to LD-8-BITS
¿Cómo se lee un bit del puerto EAR? Pues la primera instrucción es un salto a L05E3, también llamada rutina LD-EDGE-2 que muestro a continuación. Observa que el valor de B antes de llamar a esta rutina es $B0.
Código: Seleccionar todo
;; LD-EDGE-2
L05E3: CALL L05E7 ; call routine LD-EDGE-1 below.
RET NC ; return if space pressed or time-out.
; else continue and look for another adjacent
; edge which together represent a bit on the
; tape.
; ->
; this entry point is used to find a single edge from above but also
; when detecting a read-in signal on the tape.
;; LD-EDGE-1
L05E7: LD A,$16 ; a delay value of twenty two.
;; LD-DELAY
L05E9: DEC A ; decrement counter
JR NZ,L05E9 ; loop back to LD-DELAY 22 times.
AND A ; clear carry.
;; LD-SAMPLE
L05ED: INC B ; increment the time-out counter.
RET Z ; return with failure when $FF passed.
LD A,$7F ; prepare to read keyboard and EAR port
IN A,($FE) ; row $7FFE. bit 6 is EAR, bit 0 is SPACE key.
RRA ; test outer key the space. (bit 6 moves to 5)
RET NC ; return if space pressed. >>>
XOR C ; compare with initial long-term state.
AND $20 ; isolate bit 5
JR Z,L05ED ; back to LD-SAMPLE if no edge.
; but an edge, a transition of the EAR bit, has been found so switch the
; long-term comparison byte containing both border colour and EAR bit.
LD A,C ; fetch comparison value.
CPL ; switch the bits
LD C,A ; and put back in C for long-term.
AND $07 ; isolate new colour bits.
OR $08 ; set bit 3 - MIC off.
OUT ($FE),A ; send to port to effect the change of colour.
SCF ; set carry flag signaling edge found within
; time allowed.
RET ; return.
En resumen, B tendrá un valor entre $B2 y $FF (si todo ha ido bien) tras la llamada a LD-EDGE-2 dependiendo de si los dos pulsos codifican un "0" o un "1". Por eso lo que hay después es una comparación con un valor umbral ($CB), y el resultado de dicha comparación es lo que va a parar al flag carry, que se meterá por la derecha del registro L (en RL L).
Ahora vamos a hacer una serie de cálculos. Primero vamos a ver cuántos ciclos son necesarios para que se desborde el registro B (llegue a $00) en la llamada LD-EDGE-2. El conteo es tedioso, aunque no muy complicado, si quieres seguirlo aquí tienes el desglose, siendo un total de 5598 ciclos. Recordemos que el "1" se codificaba con dos pulsos de 1710 ciclos cada uno, en total 3420 ciclos. Si leemos un pulso de más de 5598 ciclos (el 64% más del tiempo esperado) se produce un error.
Por otro lado y debido al pequeño retardo que hay al comienzo, el valor mínimo de B (que es $B2) no se corresponde con 0 ciclos, sino que lo hace con un conteo de 996 ciclos. Teniendo en cuenta que un "0" dura 855*2= 1710 ciclos, esto sería el 58% del tiempo esperado, aunque en este caso no se produce error directamente si hay menos de 996 ciclos, simplemente se añaden los ciclos que falten para completar los 996 al siguiente pulso.
Pues bien, si tenemos que con $B2 son 996 ciclos y con $00 son 5598, la fórmula que me devuelve el número de ciclos que duran los 2 pulsos leídos es:
Código: Seleccionar todo
Ciclos= 996+(regB-178)*59
Código: Seleccionar todo
Ciclos= 292+32*regA+(regB-178)*59
Código: Seleccionar todo
Ciclos= 292+32*22+(203-178)*59= 2471.
Bueno pues ahora queremos acelerar la carga a un valor de entre 1500bps y 3000bps (más sería arriesgado). Cojamos números sencillos y así agilizamos las cuentas: 1000 ciclos para el doble pulso del cero y 2000 ciclos para el del uno, que serían 2565bps. Para hacerlo todo simétrico y no pillarnos los dedos mantenemos el 58% del "0" como retardo mínimo. En la teoría basta con comprobar que 996<1000 sin cambiar el retardo; en la práctica los pulsos pueden ser muy asimétricos y tendríamos problemas. Retardo mínimo= 0.58*1000= 580 ciclos. En esta ecuación:
Código: Seleccionar todo
580= 292+32*regA
Código: Seleccionar todo
0.692= 1000/Umbral
Código: Seleccionar todo
292+32*9+(regB-178)*59= 1445
Para el retardo:
Código: Seleccionar todo
;; LD-EDGE-1
L05E7: LD A,$09 ; a delay value of nine.
Código: Seleccionar todo
RET NC ; return with time-out.
LD A,$C1 ; the comparison byte.
CP B ; compare to incremented value of B.
Código: Seleccionar todo
SjAsmPlus loader2.asm
rem SjAsmPlus manic.asm
FlagCheck header2.bin 0
FlagCheck loader2.bin
FlagCheck manic.scr
FlagCheck manic.bin
GenTape manic2.tzx ^
turbo 2168 667 735 ^
600 1600 1500 0 header2.bin.fck ^
turbo 2168 667 735 ^
600 1600 1500 0 loader2.bin.fck ^
turbo 2168 667 735 ^
500 1000 1500 0 manic.scr.fck ^
turbo 2168 667 735 ^
500 1000 1500 0 manic.bin.fck
¿Os acordáis cuando movíamos todo el bloque del juego (32768 bytes) a su sitio porque era necesario ubicar el cargador en memoria alta? Pues bien, mover tal cantidad de bytes con un LDDR es muy lento, son 32768*21= 688128 ciclos, casi 200ms. Pensaréis que 200ms no es nada pero creedme, es un montón. Tal vez en esta situación sea plausible dejarlo, pero en cualquier otra circustancia puede ralentizar el desarrollo del juego, así que os diré como reducirlo a casi cero.
Tenemos 32768 bytes que parten de un archivo binario, que hay que desplazar 173 bytes hacia adelante. ¿Por qué no transformamos el binario de tal forma que haya que mover la mínima cantidad de bytes? Os dejo que penséis un poco y en breve muestro la solución.
...
...
...
Pues sí, se trata de poner los 173 últimos bytes del fichero al principio (o los 32595 bytes primeros al final). De esta forma bastaría con machacar el cargador con los primeros 173 bytes del archivo transformado para tener el juego correctamente cargado en memoria. Pues bien, el fichero se puede "transformar" haciendo un sencillo copy/paste con el editor hexadecimal. Pero como hemos dicho muchas veces, este tipo de soluciones a la larga no nos gustan porque suponen un trabajo extra cada vez que cambiemos la longitud del cargador. Por suerte tengo la utilidad hecha desde antes, no la he tenido que hacer ad-hoc como FlagCheck.c. Se llama fcut e incluyo sólo el ejecutable, el código fuente y el funcionamiento lo podéis ver aquí.
Vayamos al lío. Primero renombramos make2.bat y loader2.asm a make3.bat y loader3.asm respectivamente y hacemos los cambios allí.
En loader3.asm:
Código: Seleccionar todo
scf
sbc a, a
ld ix, $8000-fin+L0556
ld de, $8000-fin+L0556
call L0556
ld hl, $8000-fin+L0556
ld de, $10000-fin+L0556
ld bc, fin-L0556
ldir
jp $8400
Código: Seleccionar todo
SjAsmPlus loader3.asm
rem SjAsmPlus manic.asm
fcut manic.bin -AD AD manic.cut1
fcut manic.bin 00 -AD manic.cut2
copy /b manic.cut1 ^
+ manic.cut2 ^
manic.new
FlagCheck header3.bin 0
FlagCheck loader3.bin
FlagCheck manic.scr
FlagCheck manic.new
GenTape manic3.tzx ^
turbo 2168 667 735 ^
600 1600 1500 0 header3.bin.fck ^
turbo 2168 667 735 ^
600 1600 1500 0 loader3.bin.fck ^
turbo 2168 667 735 ^
500 1000 1500 0 manic.scr.fck ^
turbo 2168 667 735 ^
500 1000 1500 0 manic.new.fck
Pincha aquí para bajar el archivo de la lección