Buscar en Google

viernes, 4 de marzo de 2011

Capitulo 2 - Variables

CAPITULO 2


Conceptos de programación


En éste capítulo voy a comenzar a describir las nociones básicas sobre programación. Si usted es alguien que nunca ha tenido la oportunidad de programar nada, en éste capítulo encontrará conceptos y ejercicios mentales que ayudan a entender y facilitar la tarea para encarar la programación en general, no sólo de microcontroladores.

Ya en el capítulo anterior habíamos hecho una caracterización del funcionamiento de un sistema programable, como podría ser un microcontrolador. El ejercicio consistía en ponerse en el lugar de un microcontrolador. Imaginarse que uno de nosotros era un microcontrolador y pensaba como tal. Entonces, se nos daba una orden simple, que para la mayoría de los seres humanos es comprensible: limpia tu cuarto.  Pero veíamos que en un microcontrolador (y en cualquier sistema programable), en realidad es una orden muy compleja y que necesita pasos más detallados.

Veíamos con esa interpretación, que un microcontrolador necesita especificar exactamente, punto por punto que es lo que se necesita realizar. Y es eso lo que sucede exactamente en cualquier sistema que lleva una CPU o microcontrolador. No existe hasta el día de hoy, o por lo menos no se conocen, equipos electrónicos que tengan capacidad propia de creación. Esto es, hoy en día es imposible esperar que un microcontrolador o cualquier computadora conocida pueda tomar alguna decisión basada en su propia imaginación. Los microcontroladores no poseen creatividad ni imaginación.

Por lo tanto, los microcontroladores sólo responden a impulsos (eléctricos), y el comportamiento será siempre lo que tenga programado que tenga que hacer. Si lo que tiene programado realizar no es correcto, el microcontrolador ejecutará las acciones en forma incorrecta. Nunca un microcontrolador cuestionará lo que tiene programado para ejecutar.

Variables


En todo sistema informático necesariamente hay lo que se denominan las variables. Es algo que viene innato, y es la esencia en su funcionamiento. Ya hablamos también en la introducción sobre la memoria RAM y para que se utilizaba. La información que se almacena en la memoria RAM es lo que llamamos las variables.

Las variables son casillas o lugares donde un microcontrolador almacena un valor, donde dicho valor es el resultado de haber realizado algún tipo de operación, como puede ser el  resultado de una operación matemática, o el resultado de alguna operación lógica. Como también puede ser el resultado de saber en que estado quedó el microcontrolador al haber terminado de ejecutar alguna orden o instrucción, como por ejemplo, saber cual era el número de paso que estaba ejecutando en el momento previo anterior.

Nunca hay que perder de vista que un microcontrolador es como una persona que posee amnesia inmediata, ésto es, se olvida inmediatamente de lo que acabó de hacer. Por lo tanto, necesita dejar almacenado o guardado en algún lugar las cosas que va a necesitar recordar. Para esto está la memoria RAM, allí es donde se guarda todo ése tipo de información.

Supongamos que queremos realizar un programa cuyo objetivo sea simplemente un contador. Esto es, que arranque la ejecución del programa y cuente consecutivamente números arrancando la primera vez en el valor 1. Y que ésto sea todo el objetivo del programa.

Supongamos que tenemos la memoria RAM, y es un casillero con 8 estantes como el siguiente:

Entonces nuestro microcontrolador tendría una capacidad de 8 posiciones de memoria. Cada casillero del estante puede ser accedido por el microcontrolador en cualquier momento, puede colocar como máximo un número por vez por cada casillero.

Las órdenes que debería tener dicho programa deberían ser:

            1 - Digo que el primer casillero de arriba a la izquierda se va a llamar “A”
             2 - Digo que el segundo casillero, de arriba de izquierda a derecha se va a llamar “B”
            3 - Coloco en el casillero A, el valor 1. O sea hago A=1
             4 - Leo el valor que está en A y le sumo 1. El resultado lo guardo en B. O sea hago B= A+1
             5 - Leo el valor que está en B y lo pongo en A. O sea, hago A = B
             6 - Vuelvo al paso 4

Vamos entonces a ejecutar éste programa como si fuésemos un microcontrolador. Las órdenes 1 y 2 son simplemente declaraciones, adopto un nombre para dos posiciones en el casillero. Esto más adelante cuando entremos de lleno en el lenguaje C veremos que existen éstos pasos de declaraciones. En nuestro caso, lo único que hacen los pasos 1 y 2 es ponerles un nombre a los dos primeros casilleros:

El paso número 3 es lo que se llama inicialización. Es asignarle un valor inicial a una de las variables. Las inicializaciones son también pasos necesarios (y muchas veces obligatorios) en los programas reales. Son un punto de partida. Se necesita saber con que valor debe arrancar un programa. En nuestro caso, dijimos en la consigna que el contador debería contar desde 1 hacia adelante. Entonces, el paso número 3 le asigna a la variable llamada “A” el valor 1.


Ya está la variable A inicializada. Ahora, los pasos 4 al 6 son el núcleo del programa o programa principal. Vamos a realizar la ejecución paso a paso.

El paso 4 solicita leer el valor guardado en A y sumarle 1, el resultado almacenarlo en la variable B. Esto es, hacer B = A + 1.

Como observamos en el dibujo anterior, A tenía cargado el valor 1, por lo tanto 1 + 1  = 2. Este resultado lo guardamos en B

El paso 5, indica que leamos el valor guardado en B y lo copiemos a la variable A. Esto es, hacer A=B

El paso 6 indica volver a ejecutar el paso 4. Por lo tanto, vuelve a hacer B = A + 1. Esto es, ahora A estaba con el valor 2, entonces B = 2 + 1 = 3. Y ese es el nuevo valor de B.

De nuevo el paso 5 pide pasar el valor que está en la variable B a la variable A. Esta vez, copiará el valor 3.
Podríamos continuar indefinidamente, y veríamos que la variable A va a tomar infinitos valores, siempre incrementándose de 1 en 1. Ya veremos más que hay una pequeña limitación en ésto, en realidad el valor no va a incrementarse hasta el infinito. Pero, si el programa se puede continuar ejecutando infinitamente. La única forma de hacer que deje el microcontrolador de ejecutar éste programa será quitando la alimentación.

Lo que hemos visto, es un ejemplo muy sencillo, y que vemos que solo hacer lo que el microcontrolador hace, tratando de ponernos en el lugar que tiene que tomar parecen ser pasos tediosos. ¿Cuanto tiempo nos tomó en contar de ésta manera de 1 hasta 3 que es lo que muestro en el ejemplo? (y no se imaginan cuanto tiempo me tomó escribirlo). Sin embargo, esta tarea la puede realizar sin ningún inconveniente cualquier microcontrolador, y la puede realizar millones de veces por segundo. Si éste fuera el programa real, y nuestro microcontrolador trabaja a 5 millones de instrucciones por segundos (5 MIPS), nuestro contador se incrementaría una vez cada 3 instrucciones, el incremento se produce sólo en el paso número 4. El pasos 5 es la copia del valor entre la variable A y B. Y el paso 6 es la orden de volver al paso 4. Entonces, nuestro contador se incrementará 1,66 millones de veces por segundo.

A manera de ir acercándonos un poco más al lenguaje de programación. Reescribo el programa anterior en un lenguaje más cercano al lenguaje del microcontrolador. El programa reescrito sería:

1 - Definir A en posición 1
2 - Definir B en posición 2
3 - A = 1
4 - B = A + 1
5 - A = B
6 - Ir a paso 4

Todavía estamos lejos del lenguaje C, pero nos estamos acercando un poco más.

Tipos de variables


Ya habíamos dicho que el programa anterior podía dejárselo ejecutando en un microcontrolador infinitamente, pero el valor al que va a llegar la variable no va a ser infinito, va a estar limitado. Esta limitación lo define el tipo de variable que hayamos elegido para declarar las variables A o B.

Volviendo al ejemplo de ponernos en el lugar del microcontrolador, supongamos que los valores que almacenamos en las estanterías cada vez que ejecutamos un paso, es un papelito que escribimos a mano antes de guardar en la estantería. ¿Que sucede con el tamaño del papel? Supongamos que nuestro papelito mide 2x2 cm. ¿Cuantos dígitos podríamos escribir en el papel?, seguramente, 1, 2, 3 o incluso 4 dígitos podrían ingresar en nuestro papelito. Por ejemplo podríamos escribir cada vez números en un mismo tamaño:

Pero, ¿que sucede cuando superamos el valor 9999? Suponiendo que nuestro tipo de letra no nos permite achicar más la letra, no podemos escribir un número más grande. Entonces, si seguimos poniéndonos en el lugar del microcontrolador, lo que va a suceder es que como humanos vamos a comenzar a escribir el número, y nos vamos a topar con el fin del papel. Entonces, sólo vamos a haber escrito 4 dígitos en el papelito. Supongamos que la variable A estaba en el valor 9999 y al sumar 1 tenemos que escribir el valor 10000. Como seres humanos puestos desde el punto de vista del microcontrolador, vamos a comenzar a escribir en el papelito, pero se nos va a acabar en el cuarto dígito. Entonces el papelito va a quedar cargado como:

Vemos que al no haber espacio en el papelito, el resultado ya no es el esperado. En realidad, el microcontrolador tampoco escribe los números como seres humanos, la escritura del número debe hacerse de derecha a izquierda. Comenzando por las unidades para seguir por las decenas, y centenas. Por lo tanto, para el microcontrolador, cuando se acaba la capacidad del papelito, el número resultante sería:

Esto último es lo que se parece más a lo que sucede dentro de un microcontrolador cuando se sobre pasa la capacidad de almacenamiento de cada variable. Siempre se pierde lo que se llama los dígitos más significativos, ésto es en éste caso, se descarta la decena de miles y se mantienen todos los dígitos restantes.

Si nuestro programa corriera con las condiciones dadas en los ejemplos, el programa contaría desde 1 hasta 9999 y cuando pase de 9999 volvería a 0. Si nuestro programa corre a una velocidad de 1,66 millones de veces por segundo, se puede observar que 166 veces por segundo pasará de 0 a 9999.

En los microcontroladores reales, las variables se agrupan en bytes, words, doble words. Lo que define entonces la capacidad de almacenamiento está dada por el lenguaje de programación. A nosotros nos interesa el lenguaje C. Tenemos en éste lenguaje la posibilidad de elegir entre diferentes tipos de variables. Los tipos de variables definirían el tamaño del papelito que vamos a usar. Sólo que en un microcontrolador, el tipo de variable define la cantidad de bits que ésta puede almacenar.

Los tipos de variables usados en C son:

char

 Este es el tipo de variable más chico que es posible utilizar en C nativo. Este tipo de variable ocupa 1 byte (8 bits) en la memoria del microcontrolador. En éste tipo de variables se pueden almacenar valores desde  0 hasta 255.

signed char

 Es lo mismo que el tipo char, o sólo que se indica además que se considere como que los valores desde 0 hasta 127 son tomados como valores positivos. En cambio, los valores 128 hasta 255 son tomados como -128 hasta -1 respectivamente. En la memoria del microcontrolador, los valores son agrupados también en 8 bits. Pero cambia la forma de interpretación.

unsigned char

 Es exactamente lo mismo que declarar una variable como char, solo que se especifica en forma explícita que se quiere interpretar una variable como del tipo char, y que se considera siempre que todos los valores que pueda tomar sean positivos. El rango de valores va desde 0 a 255, y ocupa 1 byte u 8 bits.

int

 Es una variable que representa una palabra en el microcontrolador. El largo que ocupa este tipo de variable depende de que compilador se esté utilizando. Por ejemplo, para el compilador PICC por defecto ésta variable ocupa 8 bits. Sin embargo, para la mayoría de los compiladores ésta variable ocupa 16 bits. Para compiladores que utilizan microcontroladores de 32 bits, éste tipo de variable ocupa 32 bits de largo. Dependiendo de que compilador se esté utilizando será el rango de valores que podrá tener. Por defecto, para la mayoría de los compiladores, ésta variable se considera con signo (hay compiladores donde ésta variable por defecto sólo permite valores positivos). Es decir, acepta valores positivos  y negativos. Para el caso del compilador PICC este tipo de variables puede tomar valores entre -128 y +127. Para otros compiladores (HiTech, IAR), este tipo de variables es tomado como de 16 bits, entonces los valores que podrá tomar son desde -32768 hasta +32767. En cambio, si el microcontrolador es de 32 bits, este tipo de variables podrá tomar valores desde - 2147483648 hasta +2147483647

signed int

 Es la forma explícita de indicarle al compilador que el tipo de variable es int con interpretación de que se trata de una variable con signo. O sea, que puede tomar valores positivos como negativos.

unsigned int

 Esta es la forma explícita de indicarle al compilador que el tipo de variable es int pero sin signo. Se indica o fuerza al compilador que éste tipo de variables debe aceptar o ser tomada siempre como con signo positivo. Para el caso de que el compilador que estén utilizando tome ésta variable como de 16 bits, entonces, este tipo de variables podrá tomar valores entre 0 y 65536.

long

 Este tipo de variable indica que los datos están armados por 4 bytes, o lo que es lo mismo 32 bits. Entonces, los valores que pueden tomar éste tipo de variables pueden ir desde 0 hasta 4294967297 para el caso de que sean valores sólo positivos. O desde  - 2147483648 hasta +2147483647 si la variable está declarada que es una variable con signo, o sea que permite valores positivos o negativos. De igual manera que los tipos int y char, se puede declarar las variables tipo long como unsigned long donde sólo aceptará valores positivos. O como el tipo signed long, en cuyo caso se especifica que la variable respetará signos y aceptará valores negativos.

short

 Este tipo de variable, por definición en el estándar de C es una variable de 16 bits. Pero, para el caso de los compiladores para microcontroladores, no está respetado en todos los casos. Conviene revisar el compilador que se vaya a utilizar, normalmente ingresando en la ayuda del programa y averiguar que longitud le asigna a éste tipo de variables.

Hasta aquí, los todos tipos de variables que les he mostrado, son para representar siempre valores enteros, ya sean positivos o negativos. Lo que significa que sólo se pueden almacenar números sin decimales. Por ejemplo, si una variable está declarada como tipo int y se quiere guardar un valor como 25,2309. El valor que se podrá guardar será 25 o 26, pero nunca el número con la exactitud requerida.

Existen dos tipos de variables que sirven para representar valores con punto decimal.

float

 Este tipo de datos es para representar valores con punto flotante. Este tipo de variables se almacena en la memoria RAM como un dato de 32 bits, o sea ocupa en la memoria RAM 4 bytes por cada variable declarada como float. Pero, el valor representado está compuesto por una mantisa y un exponente. De manera que se pueden representar números mucho más grandes en magnitud. El tipo de datos float admite números desde 1.175494351x10-38 hasta 3,402823466x1038. Esto significa que se pueden representar hasta un número 3 seguido de 38 ceros. Este tipo de datos es excelente cuando se necesitan realizar operaciones matemáticas que sus resultados dan en valores con decimales. Tiene el inconveniente, que sólo puede representar hasta 7 dígitos significativos. Esto es, el número 1234567 cuando se almacene en una variable, ésta quedará cargada con el valor 1,234567x106. Pero, no se puede representar un número que requiera de mayor precisión que los 7 dígitos. Por ejemplo, si queremos representar el número 12345672, el valor que quedará almacenado en la memoria será 1,234567x106. Se observa que ambos números se representan igual. El segundo inconveniente que trae el tipo de datos float, en el mundo de los microcontroladores, es que cuesta mucho trabajo para el compilador interpretar éste tipo de números. Por lo tanto, si lo que se desea es implementar un software que tenga que realizar operaciones a la máxima velocidad, usar el tipo de datos float puede penalizar el resultado, limitando la velocidad.

double

 Este tipo de datos, también sirve para representar números decimales. Al igual que con el tipo de datos float, es excelente el uso para cuando se requiera realizar operaciones matemáticas donde los resultados necesariamente van a estar expresados en valores con punto decimal. El tipo de datos double, se almacena en la memoria RAM como una variable de 64 bits, esto significa que ocupa 8 bytes de memoria por cada variable declarada de ésta forma. Este tipo de datos es mucho más preciso que el tipo de datos float, ya que permite representar números con 15 dígitos significativos. Los valores límites del tipo de datos double son desde 2,2550738585072014x10-308 hasta 1,7976931348623158x10+308.  Observamos que los números son bastante más grandes y poseen mayor precisión que los float. Este tipo de datos, NO está disponible en todos los compiladores C, hay que verificar en cada compilador si lo soporta o no. Igual que los float, indicarle al compilador que interprete las variables utilizando el tipo de datos double, hace que tenga que realizar muchas operaciones el microcontrolador para poder interpretarlo. Por lo tanto, también el tipo de datos double puede penalizar la velocidad de procesamiento resultante del microcontrolador.

Hay otros tipos de variables que son específicos de algunos compiladores, pero no es propio del lenguaje C sino que son características de cada empresa que proveedora de compiladores en C. Conviene revisar en  el compilador que se vaya a utilizar cuales son los tipos de datos permitidos y cuales son sus tamaños y valores límites.

Volviendo al ejercicio anterior, vamos a mejorar la sintaxis del programa ejemplo que habíamos dado. Vamos a declarar las variables A y B nuevamente para que quede bien específico como queremos que cuente nuestro programita sencillo.

Por ejemplo, supongamos que elegimos que para nuestro contador, las variables sean del tipo unsigned char.  Entonces, reescribimos el programita pero ahora declarando mejor las variables:

1 - unsigned char A;
2 - unsigned char B;
3 - A = 1
4 - B = A + 1
5 - A = B
6 - Volver al paso 4

Ya tenemos en los pasos 1 y 2 declaraciones que son específicas del C. Escribiendo las sentencias de ésa forma, se le indica al compilador que los casilleros llamados A y B, van a ser variables tipo char, lo cual significa que van a ser variables de 8 bits de longitud cada una.

Por lo tanto, ya sabemos entonces, que nuestro programita va a correr, comenzará contando desde el valor 1 puesto que en la línea número 3 le indicamos que tome la variable llamada “A” el valor 1. Luego comenzará a incrementarse, hasta llegar al valor 255. Cuando haga el paso B=A+1, como 255 + 1 = 256, éste valor no puede ser representado por una variable tipo char, ya que el valor máximo es 255. Todavía no hablamos de sistemas de numeración binarios, decimales y hexadecimales. Pero, créanme por ahora, que el valor 256 representado en binario es 100000000. Notemos que los 8 bits que están a la derecha del 1 están todos en 0. Casualmente en un char entran 8 bits. El microcontrolador, tomará siempre los 8 bits que están más a la derecha, o lo que es lo mismo, los 8 bits menos significativos y ese valor será el que asigne a la variable “A”. Por lo tanto, el resultado de hacer B = A + 1 donde A vale 255. Entonces B = 255 + 1 = 0.

Esto quiere decir, que nuestro contador, de manera similar al ejemplo que habíamos dado para el caso “humano” donde contábamos desde 1 hasta 9999 y luego pasamos a 0 hasta 9999. Se repite nuevamente, pero el valor que van a tomar las variables, van a pasar desde 1 hasta 255, y luego 0 hasta 255.

Algo similar sucedería, si en vez de haber elegido para las variables A y B tipos de variable char, hubiéramos elegido por ejemplo el tipo int. Suponiendo que nuestro compilador interpreta el tipo int como de 16 bits. Esto es, que utiliza 2 bytes en la memoria para cada variable declarada como int.

El código de nuestro programita, ahora sería:

1 - unsigned int A;
2 - unsigned int B;
3 - A = 1
4 - B = A + 1
5 - A = B
6 - Volver al paso 4

Si ponemos a correr el programa, la variable “A” comenzaría con el valor 1. Y comenzaría a contar a medida que avanza entre los pasos 4 al 6. Cuando llega al valor de A igual a 255, no hay problema, puesto que cuando haga B = A + 1. Hará B = 255 + 1 = 256, de nuevo, el valor 256 representado en binario es 100000000.  Este valor ocupa 9 bits, y como las variables A y B son de 16 bits, entra sin inconvenientes.

Ahora bien, cuando la variable A vale 65535 y entra en el paso 4. Intentará hacer B = A + 1. Pero ahora será  B = 65535 + 1 = 65535. De nuevo, créanme cuando les digo que 65536 es 10000000000000000, o sea, un 1 seguido de 16 ceros. La variable B es sólo de 16 bits, por lo tanto el 1 a la izquierda no entra. El microcontrolador, almacenará como resultado el valor 0 en la variable B.

Para este caso, entonces, la variable A contará desde 1  hasta 65535, y luego pasará a valer 0 para seguir contando hasta 65535.

Todo esto que estoy contando, y tratando de hacerles notar, es algo que sólo puede hacerse en el lenguaje C. Y aquí estamos en presencia de algo peligroso, pero muy poderoso que sólo tiene este y ningún otro lenguaje en forma tan directa. Esto, cuando seamos más expertos en la programación lo vamos a sufrir como querer mucho, ya que en muchos casos puede ser un problema, pero en muchísimos otros ayuda en la programación y hace que el lenguaje C sea uno de los mas poderosos, especialmente cuando se trabaja con microcontroladores, donde se busca la optimización de recursos. En otros lenguajes, al intentar realizar la operación B = A + 1, si el resultado produce un sobre-pasamiento, que es lo que estamos viendo, hace que el software dé un error y bloquea o detiene la ejecución del programa.

Fíjense que pasaría, si por ejemplo hacemos que la variable “A” sea del tipo char  pero la variable “B” del tipo int. O sea, que pasaría si nuestro programita está escrito así:

1 - unsigned char A;
2 - unsigned int B;
3 - A = 1
4 - B = A + 1
5 - A = B
6 - Volver al paso 4

De nuevo, el programa arrancaría, y la variable “A” tomaría el valor 1. Luego comienza la ejecución y va incrementando de a uno a medida que va circulando por los pasos 4 al 6. Cuando la variable A llega al valor 255, y entra en el paso 4, hace B=A+1. Como A vale 255, entonces el paso 4 será B=255+1=256. De nuevo 256 es 100000000, éste valor tiene 9 bits de longitud. La variable “B” tiene 16 bits de longitud, por lo tanto B toma el valor 256 sin inconvenientes. Pero, cuando pasa al paso 5, donde hace A = B, intenta hacer A=256, o expresado en binario A=100000000. Pero la variable A sólo tiene 8 bits. Puesto que está declarada como char. Entonces, sólo toma el microcontrolador los bits menos significativos. O sea, la variable A va a tomar el valor 0. Cuando vuelva a pasar por el paso 4, hará B=A+1, pero como A ahora vale 0, entonces B pasará a valer el valor 1. Resumiendo, en éste caso, A valdrá 1 en el arranque. Luego se irá incrementando hasta 255, pasará a 0 y volverá a incrementarse hasta 255. Pero, notemos que la variable B siempre toma el valor de A+1, por lo tanto tomará valores que van a ser desde 1 hasta 256.

Esto último, que parece un poco enredado y trivial, es un error muy común que suele suceder cuando tenemos programas extensos en C. Confundir dos variables de distintos tipos, puede hacer que nuestro programa no responda como lo esperamos. Otras veces, esto se hace apropósito y el C lo permite (ya lo vamos a ver más adelante como hacerlo para que sea controladamente), lo que lo hace un lenguaje muy poderoso.


Sistemas de numeración: Binario, Decimal y Hexadecimal


Antes de continuar, creo que es muy conveniente y necesario dar un repaso a lo que es el sistema binario, decimal y hexadecimal. Para aquellos que dominan el tema de cambios de base, pueden pasar al próximo capítulo. Pero sólo recomiendo que saltee éste capítulo si usted sabe de memoria el concepto de cambio de base, especialmente entre estas 3 bases. En la programación en C, y especialmente cuando se trabaja con microcontroladores, es fundamental dominar éste tema. Me resultaría imposible entender que alguien entienda y domine la programación de microcontroladores sin conocer y manejar correctamente estos cambios de base. Toda la bibliografía existente en el mercado, y todos los microcontroladores existentes se basan en tablas las cuales vienen codificadas a niveles de bits, y eso lleva indefectiblemente a una representación directa en hexadecimal o en binario. Muy raras veces nos vamos a encontrar con programas hechos para microcontroladores que se hable en decimal, tal cual lo hablamos nosotros los seres humanos. Por lo tanto, recomiendo repasar para los que ya conocen, y aprender muy bien para los demás, lo que significa el cambio de base y la forma de representar los números.

El sistema decimal


El sistema decimal es el tipo de representación  de los números que utilizamos normalmente como seres humanos. En el sistema decimal tenemos 10 signos, que sirven para representar todas las combinaciones de números posibles. Los signos son 0,1,2,3,4,5,6,7,8, y 9.

Cuando decimos por ejemplo que tenemos 123 naranjas, estamos queriendo decir que tenemos 100 naranjas junto con 20 naranjas mas junto con 3 naranjas mas.  O sea, si decimos que tenemos 123 naranjas, es lo mismo que decir que tenemos:

                                         100 + 20 + 3 = 123 naranjas

Dicho en una forma normalizada, el valor 123 equivale a decir 1 centena 2 decenas y 3 unidades.

Expresado en forma matemática, lo mismo dicho anteriormente se puede representar como:

                                               123 = 100 + 20 + 3

También se representa como

                                   123 = 1 x 100 + 2 x 10 + 3

Entonces, cualquier número que armemos en el sistema decimal, se representa como una suma de potencias de 10. Las potencias de 10 son 1, 10, 100, 100, etc.

Una regla práctica que siempre usé y me parece muy visual, es armar los números utilizando casilleros, donde pongo casilleros con las potencias de la base que se está manejando.

Por ejemplo, en base decimal, en el casillero coloco en la fila superior las potencias de 10, ordenadas desde la derecha hacia la izquierda, comenzando con la potencia más baja (1) , y aumentando a medida que se avanza hacia la izquierda con las otras potencias 10, 100 , 1000.

Y en la fila inferior, coloco el número que quiero representar, ordenando siempre las cifras menos significativas de manera que quede colocada en el casillero que está más a la derecha.

En nuestro ejemplo, el número era 123 en base decimal.

Potencias de 10
100
10
1
Número a representar
1
2
3

Luego, para obtener el número representado en la base, multiplico columna por columna el valor del casillero superior con el valor del casillero inferior.

Potencias de 10
100
10
1
Número a representar
1
2
3

100 x 1 = 100
10 x 2 = 20
1 x 3 = 3

Finalmente sumo los resultados de las multiplicaciones:

                                   100 + 20 + 3 = 123

Todo esto, que parece muy trivial y que no tiene mucho sentido, si lo va a tener cuando comencemos a ver los otros sistemas de representación. Lo que intento explicitar, es como funciona la representación decimal. Vamos a ver en las otras representaciones que es exactamente lo mismo pero cambiando las potencias que se utilizan.

El sistema binario



Este sistema, es el que se utiliza en los circuitos electrónicos, ya que es una de las mejores maneras que hay para poder transmitir o almacenar información. Ya que se asigna el valor cero lógico cuando en un pedazo de pista o chip, mas bien en un cable que comunica dos partes de un circuito, se tiene que no hay tensión, o su valor está en el rango entre 0 y 0,7 volts.

Por otro lado, el valor uno lógico, se asigna cuando en ese mismo pedazo de cable, la tensión que lleva está entre los 2 y 5 volts.

De ésta manera, con ésos límites es mucho mas sencillo, electrónicamente hablando de discriminar si ese pedazo de circuito está en un valor lógico 0 o 1, ya que hay un salto relativamente importante de tensión. Así, todas las computadoras y microcontroladores, cuando almacenan información o datos en la memoria, y cuando realizan cualquier tarea, siempre trabajan a nivel de bits. Los bits sólo pueden tener dos valores: 0 o 1, y no hay otra posibilidad. Por ello, es necesario conocer el funcionamiento del sistema binario, ya que programar microcontroladores, indefectiblemente nos inducirá a pensar en algún momento en bits y por lo tanto, entender como tratarlos.

En el sistema binario se tiene una codificación, o forma de expresar los valores que es similar a la forma de representarlos que en la forma decimal. La diferencia es que sólo se poseen 2 dígitos diferentes, son el 0 y el 1. Con ceros y unos se arman todas las combinaciones de números.
¿Cómo es posible esto? Bien, veamos entonces un ejemplo práctico.

Supongamos que tenemos que contar desde 0 hasta 15 en el sistema decimal conocido por nosotros, la cuenta que hacemos normalmente es: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ¿que pasa cuando llegamos al 9 y queremos pasar al dígito siguiente? Automáticamente incrementamos la decena y ponemos en cero la unidad, así los dígitos continúan 10, 11, 12, 13, 14, 15.

Llevando la misma forma de pensar, pero al sistema binario. Supongamos que queremos contar desde 0 hasta 7.

Comenzamos    0,
            1

Y acá se nos acabaron los dígitos. Pero, razonando igual que para el sistema decimal, incrementamos entonces ahora un dígito a la izquierda del número. Entonces ponemos en 1 un dígito más

10
11

Y ahora, cuando queramos sumar uno a éste último valor. Pensemos que pasa cuando tenemos por ejemplo que incrementar en 1 el número 99, se incrementa la unidad, pero el incremento  hace que se incremente también la decena. Entonces, al incrementar la decena, también se terminan los dígitos, por lo tanto hace que se incremente la centena.

Llevado de nuevo al caso binario, incrementamos la unidad, esto hace que tengamos que incrementar el dígito siguiente a su izquierda, que a su vez, al incrementar ése dígito se acaban los números posibles, entonces, tenemos que agregar o incrementar el dígito siguiente a su izquierda:

11 + 1 = 100
100
101
110
111















Puesto en forma de tabla, les muestro los números 0 al 16 representado en sistema binario


Decimal




0



0
1



1
2


1
0
3


1
1
4

1
0
0
5

1
0
1
6

1
1
0
7

1
1
1
8
1
0
0
0
9
1
0
0
1
10
1
0
1
0
11
1
0
1
1
12
1
1
0
0
13
1
1
0
1
14
1
1
1
0
15
1
1
1
1

Y les aconsejo encarecidamente que esa tablita anterior la tengan siempre presente. Esa tablita tiene que ser nativa y ley máxima para todo programador que se digne experto en programación de microcontroladores. Con el tiempo me van a dar la razón, se van a dar cuenta que si se dedican a esto que esa tablita la van a considerar fundamental. Sacando un poco de lado la filosofía fundamentalista de la programación de microcontroladores, volvamos a revisar la tabla anterior. Y también recordemos la tabla que les había armado para el caso del sistema decimal.

Recordemos que en el sistema decimal, 123 significaba 1 centena + 2 decenas + 3 unidades, o lo que es lo mismo 1x100 + 2x10 + 3 = 123.

Ahora veamos, que 100 = 10 x 10. Y que una unidad de mil, o sea 1000 = 10 x 10 x 10.

Si, replanteamos el tema de las unidades, decenas y centenas pero para el sistema binario. Podemos armar otra tablita como

2x2x2
2x2
2
1

O también, armar siempre desde la derecha hacia la izquierda, comenzando por el primer dígito de la derecha siempre en 1. E ir multiplicando en el dígito consecutivo a la izquierda como el doble del dígito de la derecha. De ésta forma

512
256
128
64
32
16
8
4
2
1

De nuevo, estos valores son fundamentales para entender el sistema binario. Esos valores mostrados son lo que se llaman potencias de 2, y equivalen a hacer 2n. O elevar el número 2 a la potencia que se desee. Esta segunda tablita es importante dominarla y conocerla de memoria.

Ahora, si tenemos un número binario cualquiera, por ejemplo: 0100100110, lo cargamos en ésta segunda tablita y obtenemos

512
256
128
64
32
16
8
4
2
1
0
1
0
0
1
0
0
1
1
0

De igual manera a lo que habíamos hecho para el sistema decimal, el número binario aquí representado, se puede obtener multiplicando el valor de cada casilla superior de cada columna, por el valor en cada casilla inferior, y luego sumar todos los valores. Ese es el valor que representa en decimal, el mismo número en binario.

Del ejemplo, el número 0100100110 según la tablita se representa como

512 x 0 + 256 x 1 + 0 x 128 + 0 x 64 + 1 x 32 + 0 x 16 + 0 x 8 + 1 x 4 + 1 x 2 + 0 x 1

O lo que es lo mismo: 256 + 32 + 4 + 2 = 294

O lo que es más fácil, sumar sólo las casillas que tienen puesto un 1 en el valor

Fíjense, ahora de nuevo en la tabla que les había dado al principio sobre los valores 0 a 15, que les recomiendo que la sepan de memoria. Como se clarifica si les completo las casillas que había dejado apropósito en blanco. Sólo sumen en forma horizontal los valores que tienen cada casilla en 1 y van a obtener los números representados.

Decimal
8
4
2
1
0



0
1



1
2


1
0
3


1
1
4

1
0
0
5

1
0
1
6

1
1
0
7

1
1
1
8
1
0
0
0
9
1
0
0
1
10
1
0
1
0
11
1
0
1
1
12
1
1
0
0
13
1
1
0
1
14
1
1
1
0
15
1
1
1
1

Por ejemplo, el número 7, se representa por 4 + 2 + 1 = 7

Además, fíjense que en ésta tablita he usado 4 bits. Y que no es posible representar mas valores. El máximo valor posible de representar en un nibble, o en 4 bits entonces es 8 + 4 + 2 + 1 =15.

Corroboremos cual es el máximo valor posible de representar en un char, o en un byte, que tiene 8 bits. La tabla con las potencias de 2 sería:

128
64
32
16
8
4
2
1
1
1
1
1
1
1
1
1

El valor máximo posible de representar en un byte, o en 8 bits es 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 que es igual a 255.

Notemos algo interesante. Un nibble o 4 bits permiten representar como número máximo al número 15. Pero, ¿cual es la potencia de 2 inmediata siguiente que corresponde al bit en la posición 5? Fíjense en la segunda tablita, que la potencia de 2 es 16. O sea, que el máximo valor del nibble es la potencia de 2 inmediata siguiente menos 1.

Lo mismo sucede con el tipo de datos char, cuando tenemos 8 bits, el máximo número que se puede representar es 255. Y la potencia de 2 que corresponde a hacer 28, o el valor inmediato que se debería colocar a la izquierda de la tablita es 256. Por lo tanto el valor del numero máximo que se puede almacenar en un char es 256-1.

Fijémonos otra cosa, el valor más grande para un nibble es 15, y el valor más chico es 0. Pero entre 0 y 15 hay 16 números posibles. Por lo tanto la cantidad de combinaciones o números que se pueden representar con 4 bits, es igual a la potencia de 2. Y en éste caso es 24=16. Que es también el número inmediato que se debe colocar a la izquierda de la tablita conversora.

Hago estos remarcamientos, para machacar la idea de la forma de pensar en el sistema binario. Es importante tratar de amigarse con ésta representación, tratar de entenderla, y encontrar todas éstas relaciones. Son obvias para muchos, pero al momento de aplicarlas cuando se hagan programas reales, estas “trampas” o formas de ver los números ayudan a comprender y encontrar mejores y más formas de solucionar problemas, para implementar nuestros algoritmos. Lo que he hecho miles de veces, es escribir cosas similares, en una hoja cuadriculada para así poder ver el número en binario y realizar la conversión en forma automática, les aseguro que después el programa que vayan a implementar fluye desde sus cerebros como por un tubo y sale sin más problemas.

El sistema Hexadecimal


¿Ya dominan el sistema binario?, ¿Están seguros? Les hago estas preguntas, puesto que recién ahora les conviene entrar en el sistema hexadecimal.

El sistema hexadecimal, es más usado todavía que el sistema binario. Especialmente cuando se programa con microcontroladores. En C, estimo que el 80% y tal vez más de los programas hechos para microcontroladores tienen en sus valores expresados en sistema hexadecimal.

¿Por qué es tan usado el sistema hexadecimal? Por una razón muy sencilla, convertir de hexadecimal a binario es directo, no hay que hacer mayores cálculos. Realizar pasajes de números desde el sistema hexadecimal para expresarlos en binario, también es muy simple. ¿Cual es la clave?, conocer de memoria la tablita que les pasé en el ejemplo anterior con la representación en 4 bits de los números 0 al 15.

El sistema hexadecimal está compuesto por 16 dígitos diferentes. Se utilizan los números 0,1,2,3,4,5,6,7,8,9 y para completar los 6 dígitos restantes. Se utilizan las letras A,B,C,D,E, y F.

La letra A vale 10, y la letra F vale 15. Entonces, les presento la tabla de conversión entre los sistemas decimal, hexadecimal y binario de los números 0 al 15.





Decimal
Hexa
Decimal
Binario
8
4
2
1
0
0
0
0
0
0
1
1
0
0
0
1
2
2
0
0
1
0
3
3
0
0
1
1
4
4
0
1
0
0
5
5
0
1
0
1
6
6
0
1
1
0
7
7
0
1
1
1
8
8
1
0
0
0
9
9
1
0
0
1
10
A
1
0
1
0
11
B
1
0
1
1
12
C
1
1
0
0
13
D
1
1
0
1
14
E
1
1
1
0
15
F
1
1
1
1

Insisto, esta tablita que les estoy presentando arriba, deben aprendérsela de memoria, va a ser muy práctica cuando hagan sus programas en microcontroladores.

Ahora, la base del sistema hexadecimal, es 16. Por lo tanto, podemos volver a generar la tabla de conversión entre esta base para llevarla al sistema decimal, dicha tabla queda como sigue:
4096
256
16
1

Quiero que observen algo interesante. ¿Se acuerdan de la misma tabla para el sistema binario? La copio nuevamente aquí para el caso de un número con 16 bits:

32768
16384
8192
4096
2048
1024
512
256
128
64
32
16
8
4
2
1

Observemos más detenidamente que pasa cada 4 bits.  Recordemos que 4 bits en el sistema binario representan números del 0 al 15. Que casualmente, en el sistema hexadecimal también utilizamos 16 dígitos, del 0 al 9, y de la A a la F.

Pongo juntas, las dos tablas, la del sistema hexadecimal con la del sistema binario

4096
256
16
1

32768
16384
8192
4096
2048
1024
512
256
128
64
32
16
8
4
2
1



















¿Notan las coincidencias? El bit para el sistema binario, que están en la posición 5 desde la izquierda a la derecha, está apuntando al mismo valor en las potencias que el caso del mismo dígito en hexadecimal. Lo mismo sucede con el bit en la posición 9 y el bit en la posición 13.

Con ésto quiero hacerles notar, que si tienen un número cualquiera en binario, y lo agrupan en nibbles, o lo agrupan de a 4 bits. Pueden buscar en la tabla de conversión hexadecimal a binario y representar el mismo número en forma directa.

Por ejemplo, el número 011101100100011111 en binario, le hacemos la conversión directa al sistema hexadecimal de la siguiente manera.

Primero, reescribimos el mismo número, pero agrupándolo de a 4 bits. Comenzamos la agrupación, siempre desde derecha hacia izquierda (no olvidar nunca esto).

Entonces, el mismo número queda:

01
1101
1001
0001
1111

Ahora, buscamos en la tablita de conversión que les pasé, y ponemos los valores equivalentes en hexadecimal:

01
1101
1001
0001
1111
1
D
9
1
F

Y listo, el número que antes estaba expresado en binario como 011101100100011111, queda expresado en hexadecimal como 1D91F. Se ve que la conversión en muy sencilla.

La sintaxis para indicar que un número está escrito en hexadecimal, es anteponer al número un cero  seguido por una equis y después el valor en hexadecimal (0xnnnn). Esta es la forma que tiene definido el estandard de C para declarar números en base hexadecimal. Los siguientes son números expresados en hexadecimal siguiendo la codificación del lenguaje C. A partir de ahora, voy a comenzar a indicar los números de ésta manera cuando estén escritos en hexadecimal:

            0x00  -> Significa el valor 0 expresado en hexadecimal
             0x12  -> Significa el valor 18 expresado  en hexadecimal
             0xFF -> Significa el valor 255 expresado en hexadecimal


Veamos que la conversión inversa, desde hexadecimal a binario también es directa. Por ejemplo, si tenemos el valor 0xC5 para  pasarlo a binario, de nuevo recurrimos a la tablita de conversión:

C
5
1100
0101

El valor en binario 0xC5 será 11000101

Para realizar la conversión de hexadecimal a decimal, hay que usar la tabla con las potencias de 16. Por ejemplo, para pasar el número 0x3D2 a decimal se debe hacer

256
16
1
3
D
2

Entonces el valor convertido a decimal, será:

            256 x 3 + 16 x D + 1 x 2

Pero, ¿cómo se multiplica 16 x D? Acá lo puse así para que caigan  en una pequeña trampa. En realidad el valor D significa 13 en decimal, por lo tanto lo que debimos haber puesto en la tabla es:

256
16
1
3
D en hexadecimal es 13
en decimal
2

Entonces, el valor convertido a decimal es 256 x 3 + 16 x 13 + 1 x 2 = 978

¿Se nota que es mucho más difícil convertir de decimal a hexadecimal, o de decimal a binario que convertir entre binario y hexadecimal? Es por ello, que el sistema hexadecimal es muy utilizado en los programas en C y en la programación de microcontroladores. Ya lo vamos a ver más adelante, pero les cuento que todos los microcontroladores poseen registros que tienen un montón de bits, y cada bit tiene un significado especial. Entonces, en las hojas de datos vienen indicados los significados de éstos registros, pero expresados en forma de byte y dentro de ese byte, indicando que significa cada bit. Es mucho más fácil pensar directamente con números en binario o hexadecimales para poner en cero o uno cada bit en forma individual, que intentar pensar en decimal para ver como hacer para poner en cero o uno el mismo bit.

Por ejemplo, supongamos que tenemos una variable “A” del tipo char. Es decir, es una variable de 8 bits.

Supongamos que por la razón que sea, necesitamos poner en 1 el bit 4 de ésta variable. Y todos los demás bits en 0.  Entonces, veamos la variable “A” en forma de tabla.

7
6
5
4
3
2
1
0
128
64
32
16
8
4
2
1
0
0
0
1
0
0
0
0

En la fila de arriba, coloqué los números de bit. Por convención, siempre los números de bits en cualquier campo, ya sea un byte, word, nibble, etcétera, se comienza numerando desde la derecha hacia la izquierda, y se comienza con el valor 0. Esto tiene una justificación muy simple, y es que corresponde con las potencias de la base. No importa si no conoce nada de potencias, les cuento que 20=1, luego 21=2, 22=4 y así sucesivamente. Estas potencias están escritas en la segunda fila de la tabla. En la tercer fila, coloqué en cero todos los bits que no quiero encender, y en 1 el bit 4.

Volviendo al ejemplo, nos solicitaron que coloquemos sólo el bit 4 de la variable “A” en el valor 1.

Podemos resolverlo en forma decimal, para ello, tenemos que ver cual es el valor de la potencia que corresponde al bit 4. En nuestro caso es 16. El programa resuelto en forma “decimal” queda:

char A;
A=16;

Esto que les acabo de pasar, de nuevo ya es código C puro. Notemos que todas las instrucciones finalizan SIEMPRE con un punto y coma. Es cosa de la sintaxis en C.

Volvamos al ejemplo, veamos que para colocar el bit 4 en 1, tuvimos que pensar o hacer la tablita de conversión, para encontrar cual es el valor de la potencia de 24 que hacer que en binario la variable resultante quede con el bit 4 en 1.

Supongamos que hubiéramos pedido encender simultáneamente el bit 4 y el bit 3. Ya se complica, volvemos a poner la misma tabla, pero con 2 bits encendidos:


7
6
5
4
3
2
1
0
128
64
32
16
8
4
2
1
0
0
0
1
1
0
0
0

Entonces, el valor a cargar en la variable A, será 16 + 8 = 24. Quedando nuestro programa así:

char A;
A=24;

Ahora volvamos a resolver el mismo problema, pero pensando sólo en hexadecimal. Vamos a ver que es mucho más sencillo.

Para el primer caso, que necesitábamos colocar sólo el bit 4 en 1. La tablita ahora sería sólo:

7
6
5
4
3
2
1
0
0
0
0
1
0
0
0
0

Si reagrupo el valor en binario, de a 4 bits. El valor en binario agrupado queda como 0001 0000.
Si busco en la tablita de conversión entre binario y hexadecimal, tengo un 0x1 para la primera agrupación y un 0x0 para la segunda. Entonces el valor en hexadecimal es 0x10.

Para el caso que necesitábamos encender o poner en 1 los bits 4 y 3 la tabla queda:


7
6
5
4
3
2
1
0
0
0
0
1
1
0
0
0

Que reagrupados de a 4 bits el número en binario es: 0001 1000. Busco nuevamente en la tablita de conversión entre hexadecimal a binario (ya a ésta altura tienen que saberla de memoria) y tenemos que el número en hexadecimal es 0x18

Entonces, el primer programita, que encendía sólo el bit 4 de la variable A quedaba:

char A;
A=0x10;

Y el segundo programita, que enciende los bits 3 y 4 es como el siguiente:

char A;
A=0x18;

Si ahora hacemos el pasaje inverso, es decir, tenemos el programa y no sabemos que es lo que hace, podemos hacer la conversión directa desde el hexadecimal al binario de los valores que se cargan en la variable A.

Para el primer caso, convertimos el valor 0x10 a binario. Para ello, buscamos primero la conversión directa entre el valor 1 en hexadecimal a binario. De la tablita, sacamos que 0x1 es 0001. Luego, convertimos 0 en hexadecimal a binario. De la tablita sacamos que 0x0 es 0000. Agrupamos los dos resultados y obtenemos 00010000. Podemos observar que lo que se hace con éste programa es encender el bit 4 de la variable A.

Si realizamos lo mismo con el segundo caso, tenemos que convertir 0x18 a binario. Primero buscamos el valor 1 en hexadecimal, y encontramos que es 0001 en binario. Luego buscamos el valor 8 en hexadecimal y es 1000 representado en binario. Finalmente el número en binario son los dos resultados agrupados: 00011000. Y vemos que el segundo código de programa, está encendiendo los bits 3 y 4 de la variable A.

Cuando tomemos experiencia con éstas conversiones, vamos a encontrar la facilidad que significa trabajar de ésta manera. Incluso entender programas implementados por otras personas, son más sencillos de interpretar cuando están codificados con números hexadecimales que cuando los programadores han realizado operaciones con bits y/o lógicas usando números en notaciones decimales.

La notación decimal es útil cuando los resultados esperados deben ser mostrados ante seres humanos. Tal es el caso cuando las variables que se deben utilizar en un programa a su salida generan algún tipo de reporte, como puede ser una pantalla o display. O cuando se trata de conversiones de valores por ejemplo de algún tipo de sensor, que hay que aplicar matemática común. En ésos casos, sin dudas que la notación decimal es mucho más conveniente.

También es más conveniente la notación decimal cuando nos referimos a cosas lineales, por ejemplo, si tenemos que hacer un retardo o demora en nuestro programa de 1000 milisegundos, es mucho más sencillo llamar a una función retardo(1000) que indicarlo como retardo(0x3E8). 0x3E8 es el número 1000 en hexadecimal.

Sin embargo, cuando se trata de bits, la notación hexadecimal se vuelve más práctica. En todo caso, la opción existe, y queda a criterio del programador cual notación utilizar.







2 comentarios:

  1. Hola Pablo , bárbaro el curso seguí adelante, te pregunto si lo puedo bajar en pdf para imprimirlo.
    saludos!

    ResponderEliminar
  2. Hola Anónimo. Muchas gracias. Estoy escribiendolo en otro lado, en un archivo. Todavía no lo tengo terminado, pero la idea va a ser que se pueda tener en PDF.

    ResponderEliminar