cursos:ensamblador:habituales

¡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.


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


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



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


  • cursos/ensamblador/habituales.1706113190.txt.gz
  • Última modificación: 24-01-2024 16:19
  • por sromero