¡Esta es una revisión vieja del documento!
Operaciones habituales
En este capítulo vamos a ver operaciones, optimizaciones y “construcciones del lenguaje” habituales que utilizaremos en nuestros programas repetidamente, y que por tanto debemos conocer y dominar.
Por ejemplo, las comparaciones de 8 y 16 bits son básicas y tendremos que usarlas decenas (sino cientos) de veces a lo largo de nuestro programa para implementar la lógica no sólo de bucles sino también de condiciones como “¿El jugador ha colisionado con un objeto, comparando las coordenadas de ambos?”.
También veremos construcciones u optimizaciones habituales en ensamblador, como utilizar XOR A
en lugar de LD A, 0
para poner a 0 el valor del registro A. Estas “construcciones” o “trucos” se utilizan mucho en los programas en ensamblador porque ahorran memoria y además se suelen ejecutar más rápido. Se utilizan mucho hasta el punto en que son casi la “forma estándar” de realizar ese tipo de operaciones, y no es habitual ver un LD A, 0
en ningún programa, ya que no tiene sentido utilizar 2 bytes y 7 ciclos de reloj para hacer algo que puedes hacer con XOR
usando 1 sólo byte y 4 ciclos. Ya no es sólo una cuestión de que el programa sea más rápido, sino de que ocupe mucho menos y por tanto podamos meter más código en la limitada memoria del Spectrum.
Comparaciones de 8 bits
Las comparaciones de valores de 8 bits son situaciones extremadamente habituales en nuestros programas. En múltiples ocasiones tendremos que verificar si el valor de un determinado registro es igual, distingo, menor, mayor, menor igual o mayor igual que el valor de otro registro o que un valor inmediato.
Como ya vimos en el capítulo dedicado a las instrucciones básicas, las comparaciones se basan en restas (SUB
y SBC
) y en cómo estas afectan a los flags (registro F) del procesador. Las comparaciones son tan importantes, que hay una instrucción dedicada a realizar una resta descartando el resultado (evitando así alterar un registro innecesariamente), pero afectando a los flags. Esa instrucción es CP
de “ComPare”.
cp registro ; 1 byte, 4 t-estados cp valor_inmediato ; 2 bytes, 7 t-estados cp (hl) ; 1 byte, 7 t-estados cp ixh ; 2 bytes, 8 t-estados cp ixl ; 2 bytes, 8 t-estados cp (ix+d) ; 3 bytes, 19 t-estados cp (iy+d) ; 3 bytes, 19 t-estados
La instrucción CP n
resta n de A (realiza un A-n
), pero sin almacenar el resultado en ningún sitio, simplemente alterando los flags de acuerdo al resultado:
Flags Instrucción |S Z H P N C| ---------------------------------- |cp n |* * * V 1 *|
Así, tras un CP n
podemos utilizar los flas de la siguiente forma:
- A == N (A igual n): Si se activa ZF (flag de Zero), es porque el resultado de la resta es 0, es decir, A y N son iguales.
- A != n (A distinto de n): Si no se activa ZF (flag de Zero), es porque el resultado de la resta no es 0, es decir A y n son diferentes.
- A < n (A menor que n): Si no era igual, y se activa CF (flag de Carry), es porque el resultado de la resta es negativo, es decir, A es menor que B. No es necesario comprobar primero el valor del ZF, si CF está activo, entonces A es menor que N, siempre.
- A > n (mayor que): Si no era igual, y no se activa CF (flag de Carry), es porque el resultado de la resta es positivo, es decir, A es mayor que B.
- A ⇐ n y A >= n (menor igual que y mayor igual que): Implica comprobar primero si está activo el flag de Zero para el igual, y después utilizar el flag de Carry para el menor y mayor que.
La igualdad requiere sólo verificar el flag de Zero, la comparación “menor que” requiere sólo verificar el flag de Carry, y el resto de comparaciones requiere verificar el estado de ambos:
Comparaciones de valores sin signo: ----------------------------------- = => Z=1 != => Z=0 < => C=1 > => Z=0, C=0 <= => Z=1, C=1 >= => Z=1, C=0
Las comparaciones de valores con signo son algo más complejas:
Comparaciones de valores CON signo: ----------------------------------- = => Z=1 != => Z=0 < => Flags S y P/V son diferentes >= => Flags S y P/V son iguales
Veamos ejemplos de código, comparando A con 50 (podemos usar para los saltos tanto JP
como JR
):
Para comparar si A == 50 o si A != 50, simplemente comprobamos ZF:
cp 50 jp z, igual_a_50 ; distinto de 50 ; (... codigo para caso a != 50 ...) jr fin_comparacion igual_a_50: ; igual a 50 ; (... codigo para caso a == 50 ...) fin_comparacion:
Para comparar si A < 50 o si A >= 50, comprobamos simplemente si CF está activo:
cp 50 jp c, menor_que_50 ; mayor o igual que 50 ; (... codigo para caso a >= 50 ...) jr fin_comparacion menor_que_50: ; (... codigo para caso a < 50 ...) fin_comparacion:
Para comparar si A > 50, no podemos simplemente verificar si CF es cero, porque esto también ocurre cuando A == 50.
; No podemos usar jp nc, porque "==" también es NC = 1 ; MAL, saltaria tambien si es = cp 50 jp nc, mayor_que_50 ; aqui A < 50 mayor_que_50: ; MAL: aqui salta para >= 50, no solo > 50
Deberemos pues también comprobar el estado del ZF para asegurarnos de que no es mayor o igual. Esto nos permite comprobar tanto A > 50 como A ⇐ 50:
cp 50 jp nc, menor_o_igual_que_50 jp nz, menor_o_igual_que_50 ; => Si no salto, es que C = 1 ; Ahora o es '=' o es '<' segun Z ; mayor o igual que 50 ; (... codigo para caso a > 50 ...) jr fin_comparacion menor_o_igual_que_50: ; (... codigo para caso a <= 50 ...) fin_comparacion:
Como se puede ver en el código, tenemos que añadir un jp nz
o jp z
tras el chequeo del carry para comprobar si es “menor o igual”. Si queremos ahorrar varios t-estados, podemos hacer las comparaciones de >=
y ⇐
sin igualdad, pero utilizando el número anterior o el siguiente. Repitamos el ejemplo anterior, pero con 51 (>=
) o con 49 (⇐
):
; comparamos con valor +1 y entonces sí que podemos hacer JP NC ; y comprobar "mayor_que_50" mediante "mayor_o_igual_que_51". ; saltaría con >=51 es decir, con >50 cp 50+1 jp nc, mayor_O_IGUAL_que_51 ; = mayor_que_50 ; aqui A < 50 jr fin_comparacion mayor_que_50: M aqui A >= 51 y por tanto A > 50 fin_comparacion:
; comparamos con valor -1 y entonces sí que podemos hacer JP C ; y comprobar "mayor_que_50" mediante "mayor_o_igual_que_49". ; saltaría con <=49 es decir, con <50 cp 50-1 jp c, menor_igual_que_49 ; = menor_que_50 ; aqui A >= 50 jr fin_comparacion mayor_o_igual_que_49: ; aqui A <= 49 y por tanto A < 50 fin_comparacion:
Recordamos que CP
, y en general el uso de los flags (con DEC
, INC
, AND/OR/XOR
, etc) no nos limita a usar después JP
o JR
, sino que también podemos usarlo para salir de las rutinas con RET
o hacer llamadas condicionales con CALL
;
; Salida condicional cuando encontramos en memoria un 255 Rutina: ld a, (hl) inc hl cp $ff ret z jr Rutina
; LLamadas condicionales ld a, (vidas) dec a ; descrementamos A call z, FinDelJuego ; Si A es cero, fin del juego
Comparaciones de 16 bits
En muchas ocasiones necesitaremos comparar valores de 16 bits. Como ya vimos en el capítulo dedicado a las instrucciones condicionales, aunque la instrucción CP
sólo permite comparar un valor de 8 bits con el valor contenido en el registro A, siempre podemos realizar 2 comparaciones CP
con la parte alta y baja de los registros para verificar si son iguales, diferentes o menores usando los mismos flags.
La situación más típica es comparar HL con DE:
;;; Comparacion 16 bits de HL y DE ld a, h cp d jr nz, no_iguales ld a, l cp e jr nz, no_iguales iguales: ;;; (...) no_iguales: ;;; (...)
Para comparar si el valor de un registro es igual a un valor numérico inmediato de 16 bits, tendremos que comparar la parte alta y la parta baja de dicho valor de la siguiente forma:
;;; Comparacion 16 bits de HL y VALOR_NUMERICO (inmediato) ;;; VALOR_NUMERICO puede ser cualquier valor de 0 a 65535 ld a, h cp VALOR_NUMERICO / 256 ; Parte alta (VALOR/256) jr nz, no_iguales ld a, l cp VALOR_NUMERICO % 256 ; Parte baja (Resto de VALOR/256) jr nz, no_iguales iguales: ;;; (...) no_iguales: ;;; (...)
La comparación HL vs DE es tan habitual que mucha gente opta por crearse una rutina de comparación a la cual llamar y que ésta vuelva con los flags modificados. Otra opción es crear una macro en nuestro ensamblador para que en lugar de ser una rutina, la macro nos inserte (cada vez) el código de la comparación, evitando el CALL
y el RET
.
En el caso de las comparaciones de 16 bits, podemos directamente realizar la siguiente operación, la cual nos dejará los flags ZF y CF listos para saltar con los mismos criterios que hemos usado en 8 bits con CP
:
scf ; Ponemos CF = 1 ccf ; Invertimos CF => Ponemos CF = 0 sbc hl, registro_16_bits ; ZF y CF permiten saltar ahora
Dado que no existe CP
de 16 bits, hemos tenido que usar SBC
para realizar la misma función (una resta y alterar los flags), pero como consecuencia de ello, perdemos el valor original de HL.
Veamos con el código anterior un ejemplo de rutina que compara HL y DE, y que preserva el valor de HL. Podemos llamar a esta rutina con un CALL cpHLDE
y después del CALL utilizar los flags para saltar igual que con CP
:
; Compara HL y DE alterando los flags para posterior ; uso de ZF y CF tras la vuelta de la subrutina: CpHLDE: push hl scf ; Ponemos CF = 1 ccf ; Invertimos CF => Ponemos CF = 0 sbc hl, de ; HL = HL + DE (seteamos flags) pop hl ; Recuperamos HL ret
Esta rutina permite 2 optimizaciones.
Por un lado, como veremos más adelante en el apartado dedicado a “Optimizaciones habituales”, lo normal es reemplazar la pareja SCF
+ CCF
(para poner a 0 el Carry Flag) con un OR A
(o AND A
) ya que tiene el mismo efecto (limpiar el CF, aunque también tenemos que tener en cuenta que cambia el ZF) sin modificar A, y en vez de usar 2 bytes y 8 ciclos, usa sólo 1 byte y tarda 4 ciclos.
Por otro, podemos realizar la comparación de forma que no perdamos el valor de HL y por tanto no sea necesario el PUSH
y el POP
que suman 21 ciclos de reloj:
; Compara HL y DE alterando los flags para posterior ; uso de ZF y CF tras la vuelta de la subrutina: ; De: http://wikiti.brandonw.net/index.php?title=Z80_Routines:Optimized:CpHLDE CpHLDE: or a ; limpiar carry flag (scf+ccf) sbc hl, de add hl, de ; Flags CF y ZF alterados ret
Esta rutina vuelve con los mismos flags que la rutina CP
sobre un valor de 8 bits:
- Si HL == DE ⇒ Z=1 y C=0.
- Si HL < DE ⇒ Z=0 y C=1.
- Si HL > DE ⇒ Z=0 y C=0.
De la misma forma, podemos realizar rutinas específicas para comparar condiciones como por ejemplo, “mayor o igual” (>=):
;### CMPGTE -> test if A>=B ;### Input HL=A, DE=B ;### Output CF=0 -> true ;### CF=1 -> false CMPGTE: ld a,h xor d jp m, cmpgte2 sbc hl, de jr nc, cmpgte3 cmpgte1: scf ; false ret cmpgte2: bit 7, d jr z, cmpgte1 cmpgte3: or a ; true
Optimizaciones habituales
Como hemos visto en el apartado dedicado a las comparaciones de 8 y 16 bits, existen una serie de operaciones básicas las cuales se pueden realizar de forma “alternativa” siendo esta forma normalmente mucho más óptima o rápida.
Lo hemos visto con el ejemplo de “resetear el carry flag”, operación que haríamos con la pareja de instrucciones SCF
(ponerlo a 1) y CCF
(complementarlo, es decir, ponerlo a 0 después de haberlo puesto a 1, ya que no hay operación para ponerlo a 0 directamente) cuyos 2 opcodes ocupan 2 bytes en nuestro programa y tardaría en ejecutarse 4+4 = 8 ciclos de reloj o t-estados.
La alternativa es hacer un simple OR A
el cual no modifica A pero pone el Carry Flag a 0, con 1 byte y 4 t-estados, aunque tenemos que tener en cuenta que modificaría otros flags como el de Zero, por ejemplo, pero en la mayoría de las ocasiones puede no importarnos.
Igual que existe OR A
como alternativa a SCF
+CCF
, tenemos otras alternativas “optimizadas” para operaciones habituales. Las operaciones que vamos a ver a continuación están detalladas, entre otras, en la página Z80 Heaven: Optimizations.
Poner a 0 el registro A
; No óptimo: ld a, 0 ; 2 bytes, 7 t-estados ; Opción óptima 1: xor a ; 1 byte, 4 t-estados ; Opción óptima 2: sub a ; 1 byte, 4 t-estados
Verificar si A vale 0
; No óptimo: cp 0 ; 2 bytes, 7 t-estados ; Óptimo: or a ; 1 byte, 4 t-estados y resetea CF
Resetear el Carry Flag
No existe una instrucción para poner el Carry Flag a 0, aunque sí una para ponerlo a 1, y una para complementarlo. Esto permite ponerlo a 0 usando 2 instrucciones (2 bytes) y 8 t-estados. Sin embargo, es más fácil hacerlo usando cualquier operación lógica que lo resetee, como or
:
; No óptimo: scf ccf ; 2 bytes, 8 t-estados ; Óptimo: or a ; 1 byte, 4 t-estados
Utilizando instrucciones lógicas, de resta o de comparación, así como instrucciones específicas del Z80, podemos usar las siguientes construcciones:
scf ; Pone CF=1 or a ; Pone CF=0 y hace ZF=1 si A es 0 and a ; Pone CF=0 y hace ZF=1 si A es 0 xor a ; Hace ZF=1 y además hace A=0 y resetea el flag de signo S cp a ; Hace ZF=1 y pone CF=0 sub a ; Hace ZF=1 y además hace A=0 or 1 ; Modifica A, pone CF=0 y altera ZF y S como corresponda. or %10000000 ; Pone el flag de Signo a 1: S=1, CF=0 y Z=0 xor a ; Pone el flag P/O=1 sub a ; Pone el flag P/O=0
Existen una serie de instrucciones que debemos evitar porque ocupan 2 bytes y 7 t-estados y podemos simularlas con alguna de las anteriores:
sub 0 add a, 0 cp 0
Complementar A
; No óptimo: xor %11111111 ; 2 bytes, 7 t-estados ; Óptimo: cpl ; 1 byte, 4 t-estados
Verificar si un registro de 8 bits vale 1, si no nos importa modificarlo
; No óptimo: cp 1 ; 2 bytes, 7 t-estados ; Sólo permite verificar el valor de A ; Óptimo: dec a ; 1 byte, 4 t-estados, pone ZF=1 si A==1 ; Se puede hacer con cualquier registro de 8 bits
Verificar si un registro de 8 bits vale $ff, si no nos importa modificarlo
; No óptimo: cp $ff ; 2 bytes, 7 t-estados ; Sólo permite verificar el valor de A ; Óptimo: inc a ; 1 byte, 4 t-estados, pone ZF=1 si A==$FF ; Se puede hacer con cualquier registro de 8 bits
Poner el registro A a 0 o $ff según el valor de un flag (CF)
sbc a, a ; 1 byte, 4 t-estados, y preserva CF ; Si CF era 1 hace A=$ff ; Si CF era 0 hace A=0
Asignar valores directos a memoria
; No óptimo: ld a, $40 ld (hl), a ; 3 bytes y 14 t-estados ; Óptimo: ld (hl), $40 ; 2 bytes y 10 t-estados
Asignar dos valores a la parte alta y baja de un registro
; No óptimo: ld b, $20 ld c, $30 ; 4 bytes, 14 t-estados ; Opción óptima 1: ld bc, $2030 ; 3 bytes, 10 t-estados ; Opción óptima 2: ld bc,(NUMERO_B*256) + NUMERO_C ; 3 bytes, 10 t-estados
Dividir con SLR
cuando podemos usar RRCA
Para dividir A por potencias de dos, como veremos en el capítulo de operaciones aritméticas, se puede utilizar slr a
. Esta operación utiliza 2 bytes y 8 ciclos de reloj.
En su lugar podemos usar RRCA
(1 sólo byte y 4 ciclos de reloj) si después eliminamos los posibles bits insertados por la izquierda con un AND
(1 byte y otros 4 ciclos):
; No óptimo: srl a srl a srl a ; 6 bytes y 24 t-estados ; Óptimo: rrca rrca rrca and %00011111 ; Eliminamos los 3 posibles bits ; insertados por los 3 rrca. ; 4 bytes y 15 t-estados
Establecer varias variables consecutivas a un valor
Supongamos que tenemos varias variables consecutivas en memoria y queremos asignarles un mismo valor a todas ellas, por ejemplo 0. Para eso, lo mejor establecer la primera de las variables y usar HL, DE y LDIR
con BC =
variable1 DB 1 variable2 DB 1 variable3 DB 1 variable4 DB 1 variable5 DB 1 ; No óptimo: xor a ld (variable1), a ld (variable2), a ld (variable3), a ld (variable4), a ld (variable5), a ; Óptimo: ld hl, variable1 ld de, variable1+1 xor a ld (hl), a ; Establecemos el valor del primero ld bc, 4 ; Numero de elementos a asignar - 1 ldir ; Hacemos el resto copiando BC veces ; el valor del primero, en adelante.
Con este código, ahorramos 3 bytes por cada ld (variableN), a
que nos evitemos.
Incrementar el valor de una variable y dejarla en A
; No óptimo ld a, (variable) inc a ld (variable), a ; Óptimo: ld hl, variable inc (hl) ld a, (hl) ; Ahorramos 2 bytes y 2 t-estados
Copiar (HL) ⇒ (DE) sin modificar BC
Como ya sabemos, LDI copia el valor de la memoria apuntado por HL en la posición de memoria apuntada por DE, y decrementa BC.
Cuando no queremos que BC sea modificado, tendemos a hacer lo siguiente:
; No óptimo ld a, (hl) ld (de), A inc hl inc de
Sin embargo, como LDI
sólo va a decrementar BC en una unidad, podemos ahorrar 2 bytes y 8 t-estados así:
; Óptimo: ldi ; (HL) => (DE) y se decrementa BC inc bc ; recuperamos el BC decrementado
Testear el estado del bit 0 o el 7 de A para un salto
Si queremos comprobar el estado del bit 0 o el bit 7 de A para un salto, lo normal es hacer lo siguiente:
; Con bit 0: bit 0, a ; 2 bytes, 8 t-estados jr z, destino ; Con bit 7: bit 7, a ; 2 bytes, 8 t-estados call z, destino
En su lugar podemos utilizar operaciones de desplazamiento de dichos bits hacia el Carry Flag (1 byte y 4 t-estados) y saltar según el valor de C:
; Con bit 0: rra ; 1 byte, 4 t-estados jr c, destino ; Con bit 7: rla ; 1 byte, 4 t-estados call c, destino