Buscar en Google

lunes, 21 de marzo de 2011

Operaciones lógicas bit a bit

Operaciones lógicas a bit a bit: bitwise


Ya vimos como funcionan las operaciones relacionales AND, OR, y NOT.  Estas operaciones se usan principalmente cuando lo que se desea realizar son comparaciones de valores. El resultado comparado puede servir para bifurcar la ejecución del programa hacia otro lado. También se usan para generar funciones matemáticas o aritméticas que cambien según que valores vayan tomando sus variables.

Sin embargo, existe otro uso para las operaciones lógicas, AND, OR, NOT y una nueva que es la XOR. Y es que éstas se pueden aplicar sobre las variables, pero afectando bit por bit en cada comparación, y a su vez, el resultado también afectado a nivel de bit.

Estas operaciones lógicas, aplicadas a nivel bit a bit, son muy útiles en el mundo de los microcontroladores. Estos chips, tienen registros que son los manejan los diferentes periféricos. Dichos periféricos colocan sus resultados, y necesitan ser configurados, encendiendo y apagando bits específicos dentro de uno o varios bytes que componen los registros de configuración.

Entonces, podemos con la ayuda de estas funciones lógicas, modificar, comparar, y realizar muchas combinaciones de tareas que afectan los bits de un valor en forma particular.

Cuando se realizan operaciones del tipo bitwise, o bit a bit. El microcontrolador toma siempre dos valores para ejecutar. Estos valores pueden ser de cualquier tipo (char, int, long) y o longitud. Y va evaluando uno a uno bit por bit. Siempre toma para la operación los bits que están en la misma posición en cada uno de los valores utilizados como datos o entradas. Esto es, ejecuta la operación entre el bit 0 de un valor con el bit 0 del otro valor. Luego continúa con el bit 1 de ambos valores, y así sucesivamente hasta llegar al último bit. La ventaja es que el microcontrolador ejecuta la acción en sólo un ciclo de trabajo. No ocupa tantos ciclos de trabajo como bits tengan las variables. Por lo tanto, la acción es muy rápida.

Es importante que cuando se ejecuten operaciones lógicas a nivel de bit, los dos valores que se utilizan sean por lo menos del mismo tamaño en bits. De ser de diferentes tamaños, el compilador realiza un “casting” automático y lleva toda la operación de manera que los valores utilizados sean de la longitud del dato con mayor cantidad de bits.

 Repasemos como son las funciones lógicas que conocemos hasta ahora, y veamos la función lógica XOR.

Función AND: Su salida vale uno, cuando en ambas entradas en forma simultanea hay un uno colocado.  De otra manera el resultado es cero. El símbolo en C para realizar una operación lógica AND bit a bit es el &. Se diferencia de la operación lógica AND que vimos antes, que cuando se quiere aplicar bit a bit sólo lleva un símbolo & y no dos como era el otro caso.

C = A & B;

Función OR: Su salida vale uno, cuando en cualquiera de sus entradas hay un uno colocado. Sólo cuando en ambas entradas hay un cero, a su salida pone un cero. El símbolo de la operación lógica OR bitwise o bit a bit es el |. De la misma manera que la función AND se diferencia de la operación OR que vimos antes, puesto que lleva sólo un caracter | y no dos.

C = A | B ;

Función NOT: Su salida es el inverso al valor que haya en la entrada. Si hay un uno a la salida coloca un cero, y viceversa. El símbolo para indicar la operación inversión que trabaja a nivel bit a bit es ~. Acá es diferente al usado en el caso anterior que era el símbolo de exclamación ¨!¨

C = ~ A;

Función XOR: Su salida vale uno, cuando en sus entradas sólo una está en 1. Si ambas entradas están en uno, o ambas entradas están en cero, su salida es cero.  El símbolo utilizado para indicar la operación XOR bit a bit es el ^. No existe la operación XOR a nivel relacional.

La tabla de verdad de la función XOR es como la siguiente:

A
B
Salida
0
0
0
1
0
1
0
1
1
1
1
0

Veremos ahora como se aplican las funciones lógicas bitwise en algunos ejemplos.




Ejemplos de uso de la función And bitwise


Comienzo con la función AND bit a bit. La forma más común de pensar en la función AND cuando se hacen las operaciones bitwise, es que tengamos en la cabeza que esta función trabaja como si fuera un filtro. Imaginemos que tenemos una especie de  filtro o colador al cual solo dejamos pasar por sus huecos parte de lo que pongamos del otro lado. Por ejemplo, tengo una variable A con un valor cualquiera. Esta variable A es de 8 bits. Y quiero que en una variable B se almacenen sólo los bits 1,2 y 3 de la variable A. El resto de los bits quiero que siempre permanezcan en cero. Esta operación se resuelve muy fácilmente con la operación AND &:

char A;
char B;

A = cualquier valor

B = A & 0x0E;

Veamos ahora como funciona este programita ejemplo que les acabo de pasar. Para poder entenderlo bien, debemos convertir las variables a binario. Cosa que debemos hacer siempre cuando trabajemos con operaciones lógicas bit a bit. Que si a ésta altura ya están bien familiarizados con el sistema hexadecimal y se conocen de memoria la tabla de conversión deberían ya haber entendido lo que acabo de hacer.

Si convertimos el valor 0x0E a binario, tenemos

Numero de bit
7
6
5
4
3
2
1
0
0x0E
0
0
0
0
1
1
1
0

Ahora veamos, cualquier valor que tenga la variable A. Por ejemplo el valor 0x55 que en binario es 01010101. Coloco en la misma tabla la variable A

Numero de bit
7
6
5
4
3
2
1
0
0x0E
0
0
0
0
1
1
1
0
A
0
1
0
1
0
1
0
1

Calculemos el resultado de hacer A & 0x0E, que es el valor que se carga en la variable B. Notemos que la operación And tiene como resultado 1 sólo cuando ambos bits a comparar están en 1. El resultado de la operación va a ser:

Numero de bit
7
6
5
4
3
2
1
0
0x0E
0
0
0
0
1
1
1
0
A
0
1
0
1
0
1
0
1
B = A & 0x0E
0
0
0
0
0
1
0
0

Notemos, que en el resultado los bits 1 a 3 de la variable A se reflejan en el resultado. Los bits restantes quedan en cero.

Si coloco en forma genérica valores en A, podemos tener una operación como se ve:

Numero de bit
7
6
5
4
3
2
1
0
0x0E
0
0
0
0
1
1
1
0
A
b7
b6
b5
b4
b3
b2
b1
b0
B = A & 0x0E
0
0
0
0
b3
b2
b1
0

Seguramente, alguno de ustedes se preguntará: ¿Para que sirve hacer algo como ésto?

Cuando se trabaja con microcontroladores, muchas veces hay que configurar diferentes periféricos dentro del mismo. También, tenemos los puertos que son las entradas y salidas. Estos puertos son bits dentro de algún registro que significan que alguna de las entradas o salidas están con una señal en 0v o 5v. O que tienen una de sus patitas con un 1 lógico o con un cero lógico. Supongamos que tenemos un microcontrolador, que tiene conectado un botón en una de sus patitas. Esa patita será un bit dentro de un registro que se pone en cero o uno, según si está apretado o soltado el botón. Supongamos que además, en las otras patitas hay otras cosas,  y que esas otras cosas van conmutando entre diferentes valores que no tiene sentido conocer ahora. Si nuestro programa tiene que saber si está apretado o nó el botoncito, puede hacer uso de la función And para descubrir si el botón está pulsado o nó.

Por ejemplo, si nuestro botoncito está conectado al bit 5 de uno de los puertos del microcontrolador. Y si tenemos que tomar alguna decisión cuando se pulsa el botón, nuestro programita sería algo como lo siguiente:

char A;
char B;

A = lee el puerto;

B = A & 0x20;

si B es diferente de cero el boton estaba apretado.

Notemos que la operación B = A & 0x20 es lo mismo que hacer B = A & 00100000. Eso es dejar pasar sólo el bit 5. No importa que valores tengan los otros bits en la variable A, si el bit 5 de A está en uno, el resultado siempre será 0x20, si el bit 5 de A está en cero, el resultado será siempre 0x00.

Otra operación donde muy útil la función AND, es cuando tenemos que hacer lo que se llaman bufferes rotativos. No importa todavía que es lo que significa. Pero si es interesante lo que podemos hacer con la función And para limitar un contador.  Paso a explicarles.

Supongamos que tenemos una variable A. Esta variable A significa un valor o algo dentro de otra parte del programa que no interesa por ahora. Lo importante, es que esa variable A nunca puede salirse del rango. Por ejemplo, necesitamos que nuestra variable A siempre esté dentro del rango 0 a 7. Independientemente de cualquier operación que se  haga con la variable A, debe estar en el rango 0 a 7. Cuando los rangos de valores que tenemos que limitar, son siempre potencias de 2. Recordemos que las potencias de 2 son 1, 2, 4, 8, 16, 32, 64, 128, etcétera. Veamos que dije que queríamos que la variable A esté en el rango 0 a 7, o sea que tome 8 valores. Por lo tanto es un rango que es potencia de 2.

Si realizo al final de cualquier operación aritmética sobre la variable A, la operación A = A & 0x7, veamos que lo que hacemos es limitar el rango de la variable A en los valores 0 a 7.

Numero de bit
7
6
5
4
3
2
1
0
0x07
0
0
0
0
0
1
1
1
A
b7
b6
b5
b4
b3
b2
b1
b0
A & 0x07
0
0
0
0
0
b2
b1
b0

Notemos que siempre quedan almacenados en A los bits 0, 1 y 2. El resto de los bits siempre quedarán en cero. Notemos que si tenemos los 3 bits menos significativos, sólo podrá tomar la variable A los valores 0 a 7. Lo que acabamos de hacer, es como si fuera crear un tipo de datos nuevo, donde los valores de este tipo de datos, pueden estar entre 0 y 7.

Recordemos que si queremos utilizar este truco, siempre debemos utilizar rangos que sean potencia de 2. Esto nos permite en un sólo ciclo de microcontrolador crear o limitar los valores de una variable. Si pensamos realizarlo de otra manera, por ejemplo, preguntando si el valor de la variable A superó el límite máximo para después descontarle el sobre-pasamiento seguramente vamos a ocupar mucho mayor cantidad de ciclos de trabajo, cuando utilizando la operación & (and) se toma sólo uno. Es muy importante recordar que los microcontroladores, especialmente los más pequeños, son limitados en su velocidad. Por lo tanto necesitamos optimizar al máximo las operaciones para que ocupen el menor tiempo posible.

Por supuesto, que a esta altura, si me está siguiendo alguien que tiene conocimientos en C ya se habrá dado cuenta que los programas que les he ido pasando no están para nada optimizados en la sintaxis. Por ejemplo, el último programita que les pasé se podría haber resumido todo en una sola línea. Más adelante vamos a ver como mejorar aun más las líneas de código. Por ahora, prefiero seguir escribiendo los programas de esta manera ya que es mas clara su lectura. Sepamos que a medida que voy avanzando, vamos a ir adentrándonos más y más en lo profundo del C.

Ejemplos de uso de la función OR bitwise


Veamos ahora algunos casos donde tenemos que actuar sobre bits, y que se resuelven fácilmente con la función OR.

La función OR podemos pensarla como que es el opuesto de la función And. En la función Or, el resultado será cero, sólo cuando ambos bits a su entrada son ceros. ¿Notamos la inversión?, en la función And el resultado era uno cuando ambos bits estaban en uno. Para la función Or, en los demás casos el resultado es siempre uno.

La función Or se puede utilizar cuando necesitamos forzar un valor de un bit a 1. Por ejemplo, si queremos colocar en uno el bit 4 de una variable A, la operación sería:

char A;
char B;

B = A | 0x10;

De nuevo, para entender el programita, pensemos en binario. Pongamos la tablita para entenderlo:

Numero de bit
7
6
5
4
3
2
1
0
0x10
0
0
0
1
0
0
0
0
A
b7
b6
b5
b4
b3
b2
b1
b0
B = A | 0x10
b7
b6
b5
1
b3
b2
b1
b0

Notemos que la función Or dejó pasar todos los bits de la variable A, excepto el bit 4 que lo dejó forzado en 1. Observemos comparando con la tabla de la operación And, vemos que es exactamente la operación inversa. Mientras la operación And dejaba pasar todos los bits que se colocaban en la operación con su valor en 1, y los restantes los forzaba a cero. En la operación Or, deja pasar todos los bits que se comparan con el valor 0, y los restantes bits los fuerza a uno.

Supongamos que tenemos un puerto, donde tenemos conectado un LED. Un led es un diodo emisor de luz, pensémoslo como si fuera una lamparita. Suponiendo que dicha luz está conectada en el bit 4 del puerto. Entonces, si queremos encender y apagar el led, tenemos dos programitas que sirven para realizar dicha tarea.

Para encender el led:

char A;
char B;

A = leer puerto;

B = A  | 0x10;

sacar por el puerto el valor de B


Para apagar el led:

A = leer puerto;

B = A & 0xEF;

sacar por el puerto el valor de B

Notemos que para encender el led utilicé la operación Or, mientras que para apagarlo utilicé la operación And. Notemos que utilicé primero la variable A para leer el valor del puerto y luego le apliqué una operación lógica. Esto lo hice de ésta manera, puesto que no quiero modificar los valores que hayan previamente cargados en los restantes bits del puerto que manejan nuestro led.

Ejemplo de uso de la función Not bitwise


Me tomo del último ejemplo que acabo de pasarles en el programita anterior, ya que me interesa demostrar el potencial de la función Not bit a bit.

Veamos en el programita anterior que utilicé la operación A | 0x10 para encender el led. Y que para apagarlo utilicé la operación A & 0xEF. Así puesto de esa manera, parece un poco complicado. Ya que hay que pensar como es el inverso de 0x10 para que cuando se ejecute la operación And queden todos los bits en 1 excepto el bit 4 que es el que quiero colocar en cero.

Sin embargo, si utilizo la función Not bitwise, la lectura del código para colocar en cero el bit 4 se hace más evidente:

char A;
char B;

A = leer el puerto;

B = A & ~0x10;

Vemos que utilicé el operador ~ y luego el número 0x10. Esto significa que antes de aplicar la operación And, tome el inverso de 0x10 y le aplique la función. ¿Vemos que es más simple leer que la operación and se hace con el inverso de 0x10, que tener que leer en forma directa el valor 0xEF?

Numero de bit
7
6
5
4
3
2
1
0
0x10
0
0
0
1
0
0
0
0
~ 0x10 = 0xEF
1
1
1
0
1
1
1
1




La función lógica Xor significa O Exclusivo. Quiere decir que su salida se coloca en uno, cuando uno de sus bits de entrada esta colocado en uno, pero no los dos. Esta operación lógica tiene una cualidad importante. Veamos eso aplicado en un ejemplo.

Supongamos que queremos hacer un programa que cambie constantemente el valor de 2 bits en una variable o registro. Por ejemplo, que con cada pasada invierta los valores de sus bits. Supongamos que tenemos conectados en un puerto dos leds o lucecitas. Arrancamos el programa colocando una de las luces encendida y la otra apagada. Y queremos que vayan moviéndose las luces como si fuera un pequeño secuenciador. Supongamos que tenemos los leds conectados en los bits 3 y 7 del registro de un puerto:

char A;

 Paso 1: A = 0x80;

Paso 2: A = A ^ 0x88;

Paso 3: sacar por el puerto el valor de A

Paso 4: Volver al paso 2

Analicemos el programita. En el paso 2, se cargó la variable A con el valor 0x80 que en binario es 1000000. En el segundo paso se hace la operación Xor con el valor 0x88 que es 10001000

Numero de bit
7
6
5
4
3
2
1
0
A
1
0
0
0
0
0
0
0
0x88
1
0
0
0
1
0
0
0
A = A ^ 0x88
0
0
0
0
1
0
0
0

Observemos que en el paso 2, el bit 7 que originalmente estaba en uno, pasa a valer 0, puesto que cuando ejecuta la operación Xor los dos bits que se comparan están en uno, por lo tanto el resultado es cero. En cambio, el bit 3 en la variable A originalmente está en cero, pero el valor en el número 0x88 está en 1. El resultado para el valor final de la variable A será que el bit 3 pasó al estado lógico uno.

En la próxima pasada del programa, la variable A arranca con el valor 0x08 (00001000). Entonces la operación Xor, A = A ^ 0x88 quedará:

Numero de bit
7
6
5
4
3
2
1
0
A
0
0
0
0
1
0
0
0
0x88
1
0
0
0
1
0
0
0
A = A ^ 0x88
1
0
0
0
0
0
0
0


Como podemos ver, de nuevo se invirtieron los bits 7 y 3. De ésta manera podemos seguir indefinidamente, y en cada pasada se invertirán los bits 7 y 3.





No hay comentarios:

Publicar un comentario