¡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 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 < 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.
- 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, mientras que el resto de comparaciones requiere utilizar también el flag de Carry:
Comparaciones de valores sin signo: ----------------------------------- = => Z=1 != => Z=0 < => 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, 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, de nuevo comprobamos ZF:
cp 50 jr z, distinto_de_50 ; igual a 50 ; (... codigo para caso a == 50 ...) jr fin_comparacion distinto_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:
Otra opción para las comparaciones de >=
y ⇐
es comparar sin igualdad con el número anterior o el siguiente, en este ejemplo, con 49 o con 51:
; comparamos con valor +1 y entonces sí que podemos hacer NC ; y comprobar "mayor_que_50" mediante "mayor_o_igual_que_51". ; saltaría con >=51 es decir, con >50 cp 51 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:
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 compara 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 ó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, podemos usar las siguientes construcciones:
or a ; ZF=1 si A es 0, y además limpia el CF and a ; ZF=1 si A es 0 xor a ; Siempre establece ZF=1 y además hace A=0 cp a ; Siempre establece ZF=1 sub a ; Siempre establece ZF=1 y además hace A=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
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
Realizar un NEG
de un registro de 16 bits
xor a sub Parte_baja_Registro ld Parte_baja_Registro, a sbc a, a sub Parte_alta_Registro ld Parte_alta_Registro, a
Por ejemplo, para simular NEG HL
:
xor a sub l ld l, a sbc a, a sub h ld h, a
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