lunes, 18 de julio de 2016

¿Par o impar? ¿Cero o no cero? ¿Multiplo de algo? ¿Bit o no bit? (parte 3)

Hasta este momento hemos visto la mecánica de los flags del CCR y los branch. Verificamos que no cambian su estado ante los branch, permitiéndonos incluir varios consecutivos en un programa. Hemos considerado cómo determinar si un operando o resultado de operación es cero o negativo. Ahora veamos en detalle cómo discernir si un operando es par o impar.

La representación de magnitudes en binario sigue las reglas de un sistema numérico posicional. Por tanto podemos descomponer el número segun el peso de la posición de cada dígito. Sin entrar en mayores detalles recordamos los pesos de un número entero sin signo de 8 bits:
128 64 32 16 8 4 2 1
y si se tratase de un número negativo los pesos son:
-128 64 32 16 8 4 2 1
Para mayor información recomiendo leer Como expresar numeros negativos sin que se te vuele el peluquin

Observe que solo hay un peso de magnitud impar: el LSB (less significant bit, o bit menos significativo). Se trata del bit más a la derecha (si es que puede asignarse izquierda y derecha a los bits!). A diferencia del MSB, que se copia en el flag N, el LSB no se copia en ningun flag. Evidentemente para la gente que diseñó el HC11 no es un bit tan glamoroso.  ¿Cómo podríamos evaluar su estado? Queda claro que sin importar el resto de los bits que compongan una palabra binaria, alcanzaría con saber el estado del LSB para determinar si es par o impar. Empecemos por resolverlo en 8 bits.

¿Cómo podemos determinar el valor del LSB de un byte? Hay varias formas:
  • desplazamiento/rotación
  • AND lógica
  • dividir por dos y evaluar el resto
Veremos que las dos primeras formas son mucho más elegantes y sencillas que la tercera. ¿Raro no? Porque para un programador de alto nivel saber si un número es par se reduce a dividirlo por dos y observar el resto... pero aquí esa es la peor solución. Vayamos por partes, como dijo Jack el Destripador...

Desplazamiento/rotación

La primer forma de evaluar el LSB que veremos es haciendo que vaya a Carry. Luego podremos usar algun branch que use Carry como criterio (BCS branch if carry set; BCC branch if carry clear). Tenemos a nuestro alcance tres familias de instrucciones:
LSR: logical shift right
ASR: arithmetic shift right
ROR: rotate right
Las tres familias permiten trabajar directamente con operandos en memoria o hacerlo con los acumuladores A y B. Incluso LSR permite operar con el acumulador D (en 16 bits). Todas ellas copian el valor del LSB a Carry. Lo que las diferencia es la forma en que completan el MSB, pero para el caso de determinar si un número es par o impar, convengamos en que no nos interesa.
Supongamos que la consigna es recorrer un vector y contar los números pares, podríamos entonces hacer algo así (extracto el código):

        CLR CuentaP
        ...
        LDAA 0,x
        LSRA
        BCS impar
        INC CuentaP
impar

Si lo que necesitásemos fuese la cantidad de impares, bastaría con cambiar BCS por BCC. En este ejemplo puntual daría lo mismo usar ASRA y RORA. Tal vez la diferencia más significativa sea con esta última, ya que la rotación nos permitiría volver al número original con solo ejecutar una rotación en el sentido inverso (ROLA).

AND lógica

Una segunda forma de resolver el problema de examinar el LSB sería empleando la operación de disyunción o Y (and) lógica. En bajo nivel podemos realizar operaciones lógicas con mucha facilidad, y la verdad es que muchos lenguajes alto nivel también lo permiten... pero no solemos estar al tanto de ello. 
Sabemos que la disyunción solo genera un valor verdadero cuando ambas proposiciones son verdaderas. Si lo aplicamos a palabras de 8 bits, cada par de bits del mismo peso de las dos palabras se evaluaría simultáneamente. Llamando a los operandos A y B y a sus bits a7, a6, a5..., a0 y b7, b6, b5... b0, el resultado de la operación AND (Y) se compondría bit a bit de esta forma:
y7 = a7 AND b7
y6 = a6 AND b6
y5 = a5 AND b5
y4 = a4 AND b4
y3 = a3 AND b3 
y2 = a2 AND b2
y1 = a1 AND b1
y0 = a0 AND b0


Si lo que deseamos es "despejar" el valor del LSB, podríamos realizar un AND contra una palabra binaria que tenga todos sus bits en cero, salvo el LSB: 0000 0001. De esta forma cualquier operación AND contra 0000 0001 daría por resultado cero si el LSB del operando era cero y uno si el LSB del operando era uno (puse un espacio entre nibbles para claridad).
Veámoslo en assembler:

        CLR CuentaP
        ...
        LDAA 0,x
        ANDA #%00000001 ; podría usar tambien #$01 o #01
        BNE impar       ; si no es cero es porque era impar
        INC CuentaP
impar 

 
¿Fácil, verdad? Dado que el número 1 es igual en binario, hexadecimal, decimal u octal, en este caso lo importante es recordar que se usa modo inmediato y no tanto el sistema de numeración. Me gusta indicarlo en binario para mayor claridad, pero es solo cuestión de gustos. (Gracias Martin Greco por avisarme que tenía un error. Eso pasa por copiar-pegar, menos mal que hay amigos que avisan.)

Dividir por dos y evaluar el resto

Este método se hace complejo en primer lugar porque el HC11 solo permite división de números de 16 bits. Por tanto debemos llevar el operando de 8 a 16 bits. Además utiliza el registro IX para guardar el resto de la división, así que si estamos recorriendo un vector debemos usar el índice IY a fin de dejar libre IX (o guardar su valor en otro lado y luego recuperarlo). Una vez hecha la división todavía debemos hacer una comparación contra el resto, expresado en 16 bits. El CCR se actualiza de acuerdo al resultado de la división y no del resto, por ello debemos hacer una comparación adicional.
Quedaría algo así (es un extracto del código como los anteriores):

        CLR CuentaP
        ...
        LDAB  0,y     ; cargo en la parte baja de D
        CLRA          ; y limpio la parte alta
        LDX   #02     ; en IX guardo el divisor
        IDIV          ; divide D/IX, resultado -> IX resto -> D
        CPD   #0001
        BEQ  impar
        INC  CuentaP impar
Más allá de que es un código más extenso, nos vemos obligados a emplear más registros que en los anteriores y la eficiencia es más baja (observe la cantidad de ciclos de reloj que insume ejecutar IDIV). De ahí que las dos primeras alternativas sean preferibles.

En la próxima y última entrega de esta serie veremos cómo determinar si un número es múltiplo de otros (con restricciones). También está en la cocina dorándose un post sobre shift y rotate...

¿Par o impar? ¿Cero o no cero? ¿Multiplo de algo? ¿Bit o no bit? (parte 2)

En la primera parte de este tema vimos que luego de una operación aritmética, de carga, etc, que actualice el flag Z de la palabra de estado podemos verificar si el resultado fue cero (o si se cargó o guardó un cero) y actuar conforme a ello. Las dos instrucciones de salto que solo actúan por la condición Zero son BNE (branch not equal) y BEQ (branch equal). ¿Por qué tienen ese nombre?
Entender bien esto le ayudará a emplear bien las instrucciones de la familia Branch. Consideremos por un momento lo siguiente:
¿Cómo se determina si dos operandos son iguales? En alto nivel seguramente alcanzará con hacer algo como:
if (a == b)  { 
       algunas otras instrucciones 
}
¿Qué es lo que hace CPU para saber si a y b son iguales? Seguramente ya lo dedujo: los resta. Si la operación da por resultado cero, son iguales. Caso contrario, son distintos. Aun cuando estemos comparando dos cadenas de texto, estructuras o construcciones más complejas, determinar si son iguales se reduce a restar cada byte de su contraparte. ¿Cómo se comparan dos valores en bajo nivel? De la misma manera: restando. Veamos en el set de instrucciones el detalle de las instrucciones de comparación:


Note la diferencia con las instrucciones de resta (recorté una parte del set):

¿En qué se diferencian? La comparación es una resta de la que no nos interesa el resultado. Por lo demás, se comportan igual. Por lo tanto, ¿cómo sabemos en bajo nivel si dos operandos son iguales? Comparándolos y luego realizando un branch en función del flag Z. 
Vayamos entonces a un dilema existencial de muchos newbies: si hay que determinar si un valor es cero... ¿tengo que compararlo con cero? En otras palabras, supongamos que estamos recorriendo un vector usando el índice IX y estamos llevando la cuenta de los ceros en la posición de memoria CUENTAZ, ¿estaría bien hacer esto...?

        CLR CuentaZ
        ....
        LDAA 0,x
        CMPA #0
        BNE noCero
        INC CuentaZ
noCero  ....

Si leyó el post anterior ya debería saber la respuesta (sugiero releerlo para llegar a la conclusión por usted mismo).
El caso es que como hemos visto la instrucción LDAA también actualiza el flag Z en función del operando. Por lo tanto aunque no realicemos ninguna comparación, también podemos usar el flag Z para decidir un salto si el resultado de la última operación (o el operando cargado) es cero.

El flag N (Negative) es copia del MSB (most significant bit, bit más significativo) del operando cargado o del resultado de la última operación. Cuando representamos negativos en complemento a la base (Cb), complemento a la base menos uno (Cb-1) o módulo y signo (MyS) el flag N nos indica si se trata de un número negativo. 
Obviamente, si no manejamos números signados, si estamos manejando números negativos representados en exceso (como ocurre con el exponente de las representaciones de punto flotante), o si es otro dato como ser un caracter ascii, por solo citar dos casos, el flag N se actualizará pero no nos indicará el signo del número. Es el programador el que sabe en qué formato representa los operandos y por tanto puede determinar si aplica el uso del flag N como indicador de números negativos.
Por lo tanto, si el formato de representación empleado implica que el MSB coincide con el bit de signo, podremos usar los branch BMI (salta si N=1) y BPL (salta si N=0) para tomar decisiones en función del bit negative.

Supongamos entonces que necesitamos contar los números negativos de un vector, sabiendo que se almacenan en Cb. Siguiendo una pauta similar al resto de los ejemplos del post podríamos hacer algo así:
        CLR CuentaNeg
        ....
        LDAA 0,x

        BPL positi
        INC CuentaNeg
positi  ....

¿Y si necesitásemos contar tanto los ceros como los negativos? Dado que las instrucciones de la familia branch no modifican el estado de los flags, podríamos incluir varios branch consecutivos:

        CLR CuentaNeg
        CLR CuentaZ
        ....
        LDAA 0,x
        BEQ esCero     ; puedo hacer dos BRANCH seguidos, o mas!

        BPL positi     ; porque el estado de los flags no cambia
        INC CuentaNeg
positi  ...
        BRA noZ
esCero  INC CuentaZ
noZ     ...

El ejemplo no busca ser la solución óptima al problema, solo ilustrar el uso de dos branch consecutivos.
En la próxima parte veremos cómo verificar fácilmente si un número es par o impar.