cursos:ensamblador:habituales

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.


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 flags 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 c, menor_que_50
    jp nz, mayor_que_50             ; => Si no salto, es que C = 0
                                    ;    Ahora o es '=' o es '<' segun Z
 
    ; igual que 50
    ; (... codigo para caso a == 50 ...)
 
    jr fin_comparacion
 
mayor_que_50:
    ; (... codigo para caso a > 50 ...)
    jr fin_comparacion
 
menor_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_que_50           ; => mayor O IGUAL que 51 = mayor que 50
 
    ; aqui A <= 50
    jr fin_comparacion
 
mayor_que_50:
    ; aqui A >= 51 y por tanto A > 50
 
fin_comparacion:
    ; comparamos con valor -1 y entonces sí que podemos hacer JP C
    ; y comprobar "menor_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
 
menor_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


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



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


  • cursos/ensamblador/habituales.txt
  • Última modificación: 02-02-2024 18:39
  • por sromero