Cómo funciona un computador (parte 1)
En esta sección se escribe un resumen de la primera parte del resumen del libro CODE "The Hidden Language of Computer Hardware and Software" del autor Charles Petzold en español, donde explica cómo funciona un computador a nivel de circuito.
En esta primera parte se hablará desde formas de comunicación hasta la construcción de un computador en su formato más simplista.
Mejores amigos
La comunicación es una de las necesidades más intrínsecas del ser humano. Imagina que eres un niño de 9 años. Tu mejor amigo es tu vecino donde la ventana de su pieza da con la tuya. Es de noche y tus padres te ordenaron ir a dormir, sin embargo, desobedeces esa orden, porque quieres hablar con tu amigo. El problema es que si no quieres que tus padres se den cuenta que estás despierto. En ese caso, las palabras no son favorables para la comunicación.
Una linterna puede ser una buena herramienta de comunicación en este contexto y más encima cuando se está inmerso en la oscuridad. Sólo tienes que saber de qué manera puedes utilizarla. ¿Debo agitar la linterna para que haga formas de letras en el aire o debe parpadear un número de veces según el alfabeto de las letras? Si lo hago así, tengo que parpadear la linterna 131 veces para decir simplemente "¿Cómo estás?", ¡sin añadir el signo de interrogación! Eres un niño inteligente, así que se te ocurre utilizar un método más eficiente llamado código Morse, simplificando el parpadeo a 31 veces para decir "¿Cómo estás?" incluyendo un código para el signo de interrogación. Hay en definitiva formas que son más eficientes para comunicarse con alguien que otras, dependiendo del contexto.
Cuando se habla de cómo funciona el código Morse, la gente no habla de parpadeo corto y parpadeo largo. En su lugar, se habla de "puntos" y "rayas". En el código Morse, cada letra del alfabeto corresponde a una serie corta de puntos y rayas.
Aunque el código Morse no tiene nada que ver con los computadores, familiarizarse con la naturaleza de los códigos es un paso previo esencial para lograr una comprensión profunda de los lenguajes ocultos y las estructuras internas del hardware y el software de los computadores. Los códigos permiten comunicarse y la mayoría de ellos deben entenderse bien porque son la base de la comunicación humana (podría decirse que el vocabulario inglés o español es un tipo de código).
Usamos una variedad de códigos diferentes para comunicarnos entre nosotros porque algunos códigos son más convenientes que otros (si no puedes oír usarías el lenguaje de signos o si no puedes hablar preferirías comunicarse por texto). El código es útil si sirve para un propósito que ningún otro puede, por ejemplo, si quieres grabar algo, las palabras habladas no pueden almacenarse en un papel, así que en su lugar se utiliza el código de la palabra escrita o si se intercambia información en silencio a través de una distancia en la oscuridad no es posible con el habla o el papel. De ahí que el código Morse sea una alternativa conveniente.
Como veremos, en los computadores también se utilizan varios tipos de códigos para almacenar y comunicar números, sonido, música, imágenes y películas. Los computadores no pueden tratar directamente con los códigos humanos porque no pueden duplicar la forma en que los seres humanos utilizan sus ojos, oídos, boca y dedos.
El código también está adaptado para la eficacia de nuestra comunicación: En el código Morse las letras más comunes, como la "e" y la "t", utilizan códigos más cortos que la "q" o la "z", que son las menos comunes.
Un inconveniente del Código Morse es que no distingue entre letras mayúsculas y minúsculas. Pero además de representar las letras, el código Morse también incluye códigos para los números mediante una serie de cinco puntos y guiones. Además, los signos de puntuación utilizan cinco, seis o siete puntos y guiones:
La palabra clave aquí es dos. Dos tipos de parpadeos, dos sonidos vocales, dos cosas diferentes. Con las combinaciones adecuadas, solo dos códigos bastan para que podamos transmitir todo tipo de información.
Códigos y combinaciones
Para la mayoría de la gente, el código Morse es más fácil de enviar que de recibir. Recibir el código Morse y volver a traducirlo en palabras es considerablemente más difícil y lleva más tiempo que enviarlo, porque hay que trabajar hacia atrás para averiguar la letra que corresponde a una determinada secuencia codificada de puntos y rayas.
Si bien disponemos de una tabla que proporciona una traducción (imagen de arriba),no tenemos una tabla que nos permita ir hacia atrás. Tal vez una mejor manera de organizar los códigos sea agruparlos según el número de puntos y rayas que tengan y podamos averiguar la letra fácilmente contando el número de puntos y rayas que recibimos. Con un punto o una raya podemos representar sólo dos letras (e y t). Con la combinación de dos puntos y dos rayas tenemos cuatro letras (i, a, n y m). Un patrón de tres puntos y tres rayas nos da ocho letras más y con una secuencia de cuatro puntos y cuatro rayas tenemos 16 caracteres más. Esto hace que exista un patrón entre el número de puntos y guiones y el número de códigos.
|
Número de puntos y rayas |
Número de códigos |
|
1 |
2^1 = 2 |
|
2 |
2^2 = 4 |
|
3 |
2^3 = 8 |
|
4 |
2^4 = 16 |
** número de códigos: 2 ^número de puntos y guiones.
Para facilitar aún más el proceso de descodificación del código, podríamos dibujar algo parecido a la gran tabla en forma de árbol
Esta tabla muestra las letras que resultan de cada secuencia particular consecutiva de puntos y guiones siguiendo las flechas de izquierda a derecha.
Se dice que el código Morse es un código binario (que significa literalmente dos por dos) porque los componentes del código están formados por sólo dos cosas: un punto y una raya. Los objetos binarios (como las monedas) y los códigos binarios (como el código Morse) se describen siempre mediante potencias de dos.
Lo que hacemos al analizar los códigos binarios es un simple ejercicio de la rama de las matemáticas conocida como combinatoria o análisis combinatorio que se utiliza más a menudo en probabilidad y estadística porque consiste en determinar el número de formas en que se pueden combinar cosas, como monedas y dados. Pero también nos ayuda a entender cómo se pueden juntar y desarmar los códigos.
(esta parte es del tercer capítulo: Códigos braille y binarios)
Puede haber algún código que cambie el significado del código que le sigue. Por ejemplo en Braille tienes 64 combinaciones porque funciona como un sistema de código binario que se expresa con una superficie elevada y otra plana y entre esas combinaciones tienes códigos especiales como "ble" .
El código de "ble es realmente importante porque cuando no forma parte de una palabra, significa que el código que sigue debe interpretarse como números. Este tipo de códigos suelen denominarse códigos de precedencia o de desplazamiento. Alteran el significado de todos los códigos posteriores hasta que se deshace el desplazamiento.
El código indicador de mayúsculas significa que la letra siguiente (y sólo la siguiente) debe ser mayúscula y no minúscula. Un código como éste se conoce como código de escape. Los códigos de escape le permiten "escapar" de la interpretación rutinaria de una secuencia de códigos y pasar a una nueva interpretación. Como verás en capítulos posteriores, los códigos de desplazamiento y los códigos de escape son comunes cuando los lenguajes escritos se representan mediante códigos binarios.
Anatomía de una linterna
La linterna es sin duda uno de los aparatos eléctricos más sencillos en la mayoría de los hogares. Sólo se necesita un par de pilas, una bombilla, un interruptor, algunas piezas metálicas y plástico para mantenerlo todo unido.
Lo que hemos construido aquí es un simple circuito eléctrico, y lo primero que hay que notar es que un circuito es un círculo. Cualquier interrupción en el circuito hará que la bombilla se apague. El propósito del interruptor es controlar este proceso.
La naturaleza circular del circuito eléctrico sugiere que algo se mueve alrededor del circuito, como el agua que fluye por las tuberías.
La electricidad lo hace posible y la electricidad deriva del movimiento de los electrones. Como sabemos, toda la materia está formada por átomos. Cada átomo está compuesto por tres tipos de partículas: neutrones, protones y electrones.
El número de electrones de un átomo suele ser el mismo que el de protones, pero en determinadas circunstancias, los electrones pueden desprenderse de los átomos. Así es como se produce la electricidad.
Los protones y los electrones tienen una característica llamada carga. Se dice que los protones son positivos (+) y que los electrones son negativos (-). Los neutrones son neutros y no tienen carga. Los protones y los electrones son opuestos de alguna manera. Esta característica opuesta se manifiesta en la forma en que los protones y los electrones se relacionan entre sí.
Los protones y los electrones son más felices y estables cuando existen juntos en igual número. Un desequilibrio de protones y electrones intentará corregirse a sí mismo. Cuando la alfombra recoge los electrones de tus zapatos, al final se equilibra cuando tocas algo y sientes una chispa. Esa chispa de electricidad estática es el movimiento de electrones por una ruta bastante tortuosa desde la alfombra a través de tu cuerpo hasta tus zapatos.
Cuando un átomo del circuito pierde un electrón en favor de otro átomo cercano, toma otro electrón de un átomo adyacente, y así sucesivamente. La electricidad en el circuito es el paso de electrones de átomo a átomo. Esto no ocurre por sí mismo; necesitamos algo que precipite el movimiento de los electrones por un circuito: Una batería.
Las pilas producen electricidad mediante reacciones químicas (las moléculas se descomponen o se combinan en otras moléculas). Las sustancias químicas de las pilas se eligen de forma que las reacciones entre ellas generan electrones de reserva en el lado del signo menos y demandan electrones extra en el otro lado de la pila (+). De este modo, la energía química se convierte en energía eléctrica. Las reacciones sólo tienen lugar si hay un circuito eléctrico para que los electrones viajen por este circuito en sentido contrario a las agujas del reloj (del terminal negativo al positivo).
El extremo positivo de la pila inferior toma los electrones del extremo negativo de la pila superior. Es como si las dos pilas se combinarán en una sola más grande. Si cambiará el lado de una pila no funciona porque los dos extremos positivos de la pila necesitan electrones para las reacciones químicas.
Algunas sustancias son mucho mejores que otras para transportar electricidad. La capacidad de un elemento para transportar electricidad está relacionada con su estructura subatómica. Los electrones orbitan en torno al núcleo en varios niveles, denominados cáscaras. Un átomo que sólo tiene un electrón en esta capa exterior puede ceder fácilmente ese electrón, que es lo que se necesita para transportar la electricidad. Estas sustancias son conductoras de la electricidad y por eso se dice que el cobre, la plata y el oro son elementos conductores. Lo contrario de la conductancia es la resistencia (también llamado aislante) como el caucho y el plástico.
Cuanto más largo es un cable, mayor es su resistencia. Cuanto más grueso es un cable, menor es su resistencia.
Las baterías tienen voltaje y eso es el potencial de hacer el trabajo. El voltaje existe tanto si algo está conectado a una batería como si no. Otro concepto en electricidad es la noción de corriente. La corriente está relacionada con el número de electrones que circulan por el circuito. La corriente se mide en amperios (amps). Para obtener un amperio se necesitan 6.240.000.000.000.000 de electrones que pasen por un punto concreto por segundo. La corriente es como la cantidad de agua que fluye por una tubería, el voltaje es similar a la presión del agua y la resistencia es como el ancho de una tubería. La corriente (cantidad de agua) es directamente proporcional al voltaje (presión del agua) e inversamente proporcional a la resistencia (grosor de la tubería). La tendencia de una sustancia a retrasar el flujo de electrones se mide en ohmios, creada por Georg Simon Ohm, quien también propuso la famosa Ley de Ohm: El estado de la ley.
I = E / R
I es corriente, E es voltaje y R es resistencia.
Si un cable tiene una resistencia baja, puede calentarse y empezar a brillar. Así es como funcionan las bombillas incandescentes. En el interior de una bombilla hay un fino cable llamado filamento, que suele ser de tungsteno. Uno de los filamentos está conectado a la punta en la parte inferior de la base; el otro extremo del filamento está conectado al lado de la base metálica, separado de la punta por un aislante. La resistencia del hilo hace que se dirija hacia arriba. Al aire libre, el tungsteno se calentará lo suficiente como para quemarse, pero en el vacío de la bombilla, el tungsteno brilla y nos da luz.
Algunas bombillas están etiquetadas con una determinada potencia. El vatio es una medida de la potencia (P) y se puede calcular como
P = E x I
La última parte de una linterna, el interruptor, controla si la electricidad fluye en el circuito o no. Se dice que está encendida o cerrada, y apagada o abierta (es la idea opuesta a la de una puerta, porque una puerta cerrada impide que pase nada; un interruptor cerrado permite que fluya la electricidad).
Al igual que los códigos binarios inventados por Morse y Braille, una simple linterna está encendida o apagada, por lo que puede utilizarse como binario.
Ver por las esquinas
Tienes 12 años y tu mejor amigo se muda a otra casa, así que al final tienes un nuevo amigo que está en el mismo lugar que tu antiguo amigo.
Le enseñas a tu nuevo amigo a comunicarse con el código Morse y preparas una linterna para ello. El problema es que tu ventana no da a la de tu nuevo mejor amigo pero se te ocurre una brillante idea que es hacer un gran circuito eléctrico para instalar tu bombilla en su casa y su bombilla en la tuya y con ello ambos son capaces de ver cada uno de los mensajes.
La imagen representa un sistema telegráfico bidireccional y tiene dos circuitos idénticos que son independientes y no están conectados entre sí. Eso puede ser un problema porque tu amigo y tú podrían estar enviando mensajes al mismo tiempo. Eres tan brillante que te das cuenta de que puedes reducir la necesidad de cables en un 25% (3 cables en lugar de 4) cableando el sistema de esta manera:
Esta conexión se llama conexión simple. Cuando pulsas el interruptor de tu lado, la bombilla de la casa de tu amigo se enciende
No fluye electricidad en la otra parte del circuito, porque no hay lugar para que los electrones vayan a completar el circuito. La electricidad fluye en las otras partes del circuito cuando tu amigo controla la bombilla de tu casa.
Cuando ambos interruptores están cerrados, el flujo de electricidad tiene este aspecto:
Nótese que no fluye electricidad por el circuito común. Una vez que hayas establecido la parte común del circuito, no tienes que utilizar un cable para ello. Puedes sustituir el cable por otra cosa... digamos... la tierra, un muy buen conductor de electricidad. Para extraer energía de la tierra necesitas un conductor con una gran superficie, como un poste de cobre de al menos 2 metros de largo y 1/2 pulgada de diámetro. La conexión física con la tierra se llama tierra y se simboliza con esto:
Si usaras baterías y bombillas de alto voltaje, sólo necesitarías un cable entre tu casa y la de tu amigo porque puedes usar la tierra como uno de los conectores:
Los electrones vienen de la tierra. La tierra es para los electrones lo que un océano es para las gotas de agua. Las siguientes imágenes van a utilizar la letra V mayúscula en lugar del símbolo de la tierra. La V representa el voltaje o el vacío de electrones que atrae a los electrones de la tierra a través del circuito. Ahora podemos comunicarnos a cientos y miles de kilómetros simplemente tendiendo cables cada vez más largos, pero recuerda que las distancias más largas significan menos resistencia, así que tenemos que hacer algo si queremos conectar dos lugares que están a miles de kilómetros de distancia.
Telégrafos y relés
Samuel F.B Morse es conocido por su invención del telégrafo y el código que lleva su nombre. Samuel comenzó a experimentar con un fenómeno llamado electromagnetismo.
Si se toma una barra de hierro y luego se envuelve esta barra con un par de cientos de vueltas de alambres finos y finalmente pasamos una corriente por el alambre, la barra de hierro se convierte en un imán. Si se quita la corriente, la barra de hierro pierde su magnetismo.
El electroimán es la base del telégrafo. Encender y apagar el interruptor en un extremo hace que el electroimán haga algo en el otro. Este trozo de barra de hierro se utilizaba en el telégrafo para recibir el código Morse por sonido. Si se coloca una barra móvil encima de la barra de hierro, se podría mover esa barra móvil y hacer ruido cambiando el estado de un interruptor de un lado a otro.
Con esto en mente, Morse pensó en un dispositivo que pudiera aprovechar esta ventaja del vaivén de un electroimán para escribir código Morse en un papel. Si fueran palabras, claramente sería muy complejo, por lo que el código Morse pareciese ser el código más adecuado para este contexto.
Para escribir mensajes y alterar el vaivén del electroimán se utilizó un aparato llamado manipulador, que se veía así:
Esto es simplemente un interruptor, donde si se presionaba por poco tiempo producía un punto y si se presionaba por más tiempo, una raya.
Por el otro lado, hay un receptor que es un electroimán que tira una palanca metálica para producir electromagnetismo.
Por otro lado, este vaivén hace escribir en un papel puntos y rayas. Una persona traducía luego estos puntos y rayas en palabras.
Como nosotros somos una especie floja, se descubrió que bastaba con escuchar el sonido del vaivén arriba y abajo. El lápiz para escribir Morse fue reemplazado por una sonda de telégrafo que se ve más o menos así:
El electroimán al atraer la barra hacia sí mismo con el electromagnetismo, se genera un sonido de "clic". Cuando se corta la corriente, la barra vuelve a su posición normal haciendo un "clack". Un "click-clack" rápido era un punto; un "click-clack" más lento es una raya.
El manipulador, la sonda de telégrafo, unas pilas y algunos cables se pueden conectar tal como la linterna:
La invención del telégrafo marca realmente el comienzo de la comunicación moderna. Por primera vez, la gente pudo comunicarse más allá de lo que el ojo podía ver o el oído podía oír y más rápido de lo que un caballo podía galopar. Este invento también utilizaba el código binario. En las formas posteriores de comunicación eléctrica e inalámbrica, como el teléfono, la radio y la televisión, se abandonaron los códigos binarios, para luego hacer su aparición en el ordenador, los discos compactos, los videodiscos digitales, la emisión de televisión digital por satélite y la televisión de alta definición.
Un gran problema del telégrafo era la resistencia de los cables de gran longitud (entre más longitud, menos resistencia). Una solución es tener un sistema de retransmisión: Una persona podría recibir un mensaje y recentrar, vale decir, recibirlo y volver a enviarlo a una ruta específica, volviendo a escribir el mensaje en un manipulador, así como una central eléctrica distribuye la electricidad generada a distintos puntos. El tema es que esa distribución de mensajes a otros puntos podemos automatizarlo (no necesitaríamos a una persona), poniendo un trozo de madera, haciendo que el vaivén sea inmediato, entre el telégrafo (salida) y el manipulador (entrada). Esto se vería más o menos así:
De esta manera, una débil corriente entrante es "amplificada" para hacer una corriente saliente más fuerte. Este dispositivo se llama repetidor o relé. Un relé es como una sonda de telégrafo, donde la corriente que recibe es utilizada para generar electromagnetismo y así bajar una palanca metálica. Esta palanca metálica puede ser conectada a otro circuito, lo que es utilizado para amplificar una corriente, como se mostró en el ejemplo anterior.
Los relés se ven así:
Al llegar corriente, el relay "atrae" la palanca metálica y cuando no la hay, la palanca vuelve a subir. Esto es un avance gigante, porque puedes prender y apagar un interruptor sin la interferencia humana. Este nivel de automatización permite incluso, con las conexiones correctas, armar un computador.
Finalmente, un manipulador, un telégrafo y un relé se pueden conectar más o menos así:
Los relés son tremendamente útiles, pero antes de usarlos, debemos aprender a contar.
Nuestro sistema decimal
La idea del lenguaje es simplemente un código aceptado y cuando estudiamos otro idioma tendremos que amoldar lo que ya sabemos para adaptarnos a las reglas del otro idioma. Para los números sin embargo, esto es mucho menos moldeable, dado que todas las personas en el planeta escribimos los números de la misma manera: 1, 2, 3, 4, 5, 6, 7, 8, 9.
Los números son los códigos más abstractos que existen. Si vemos por ejemplo el código "3" inmediatamente pensaremos que hay 3 de algo, pero al no estar marcado en un contexto ese "3" puede ser cualquier cosa. La clave para entender la lógica de un computador es entender y normalizar que por ejemplo el número 3 puede ser representado también con el número 11 (sé que es confuso, ya veremos el porqué).
Todas las matemáticas comenzaron a contar. No es raro pensar que tengamos un sistema decimal, dado que tenemos 10 dedos y nosotros le damos al 10 un valor especial, por ejemplo, 10 años es un siglo y 1000 años un milenio.
Muchos historiadores concuerdan que los números fueron inventados para contar cosas o tener registros, como por ejemplo, poner tres rayas en una roca | | | para representar 3 de algo. El problema con este sistema es que si quiero representar el número 1000 son muchas rayas lo que es un sinsentido. He ahí cómo se crearon los sistemas numéricos.
Uno de los primeros sistemas eficientes que pueden representar incluso números muy grandes fueron creados por los romanos. Este sistema puede representar largos números con el uso de letras e incluso podemos ver este sistema en algunos relojes, monumentos o páginas de libros. Sin embargo, las necesidades del ser humano van más allá de contar y es necesario que el sistema numérico nos permite realizar operaciones matemáticas. Los números romanos son fáciles para sumar y restar, pero no lo son para multiplicar o dividir. Hay otros sistemas numéricos, como el sistema indo-arábico que son más adecuados para realizar este tipo de operaciones y no por nada sigue siendo el sistema numérico que utilizamos hasta el día de hoy. También es conocido como el sistema decimal.
Se dice que nuestro sistema decimal es posicional, lo que significa que un dígito particular representa una cantidad diferente dependiendo de dónde se encuentre el número. También el sistema indo-arábico no tiene un símbolo especial para el número diez, pero tiene un número especial llamado cero, uno de los inventos más importantes en el mundo de las matemáticas, porque permite diferenciar por ejemplo el 25 del 205 o del 250, permitiendo a su vez que el cálculo de distintas operaciones sea mucho más simple.
Toda la estructura de los números indo-arábigos es por como lo pronunciamos: el número 4825 se dice cuatro mil ochocientos veinticinco, vale decir, 4000 + 800 + 20 + 5. También podemos representarlo así: 4x1000 + 8x100 + 2x10 + 5x1.
Cada posición de un número tiene su propio significado, donde cada posición corresponde a una potencia de 10. No necesitamos un símbolo especial para el 10, porque podemos poner el 1 en una posición diferente y usar el 0 como un marcador de posición.
No solo podremos representar enteros con este sistema, si no que también números que se encuentren entre un número entero y otro (números decimales). Por ejemplo, el número 42.705,684 es 4x1000 + 2x1000 + 7x100 + 0x10 + 5x1 + 6/10 + 8/100 + 4/1000. Como vemos, podemos romper el número en pequeñas partes lo que nos permite realizar operaciones con otros números de manera sencilla siguiendo ciertos pasos sin importar que tan grande sean estos números.
Lo mejor de que el sistema sea posicional no es lo bien que funciona para contar números, si no lo bien que funciona como sistema de conteo para cualquier sistema que no sea base de 10. Nuestro sistema de 10 no necesariamente es apropiado para todos, como por ejemplo, los personajes animados que tienen cuatro dedos probablemente prefieran un sistema de base 8 y lo más interesante es que todo lo que sabemos del sistema decimal puede ser aplicado de la misma manera para nuestros amigos animados.
Alternativas a 10
El número 10 es muy importante para nosotros, dado que son los dedos que tenemos en la mano que nos han permitido contar y en eso hemos adaptado todo nuestro sistema numérico que es en base de 10. Es un sistema tan natural que es difícil concebir algo distinto.
Si los humanos hubiésemos tenido 8 dedos en vez de 10, probablemente el número 10 sería un 8 y los símbolos 8 y 9 no existirían, dado que, como en nuestro sistema decimal no existe un símbolo especial para el 10, en un sistema octal o en base de 8 no hay un símbolo especial para el 8. Para contar sería 1, 2, 3, 4, 5, 6, 7, 10, siendo 10 el número que tienen los personajes animados y no los humanos. Para evitar confusiones podemos decir diez en base ocho en vez de simplemente diez. Al escribirlo, podemos usar un subíndice para referirnos al sistema numérico que estamos usando, por ejemplo, 10eight para referirnos a un número en base de ocho.
Los números redondos son aquellos que tienen algunos ceros al final, por ejemplo, el 100 o el 1000. Podemos también tener números redondos en el sistema octal. Por ejemplo el 100eight, 200eight y el 400eight son números redondos en octal que cuando lo pasamos a decimal equivale a 64, 128 y 256 que son potencias de dos. Esto tiene sentido pensando que el número 400eight, por ejemplo, equivale a 4eight x 10eight x 10eight, siendo todos ellos potencia de dos. Cada vez que multiplicamos una potencia de dos por otra obtendremos una potencia de dos.
El sistema octal no es distinto al decimal en su estructura y solo se diferencia en que su posición, en vez de multiplicarse por una potencia de 10 se multiplica por una potencia de 8. Por ejemplo el número 3725eight equivale a 3000eight + 700eight + 20eight + 5eight o 3x1000eight + 7x100eight + 2x10eight + 5x1eight. Esto sugiere que podemos multiplicar números octales de la misma manera que los decimales:
Al sumar los últimos dígitos 5 y 3 nos da 10 (en sistema octal) llevando el 1 a la otra posición del número (carry en inglés). Al sumar ahora 3 + 4 + 1 nos da nuevamente 10, llevando el 1 a la siguiente posición. Finalmente, 1 + 6 + 1 también es 10, por lo que el resultado es 1000. El procedimiento es exactamente el mismo que en un sistema decimal.
Así como el sistema octal es apropiado para realizar operaciones matemáticas, también lo es cualquier sistema numérico, como el sistema cuaternario (base 4). Nosotros no lo usamos solo porque tenemos 10 dedos en lugar de 4.
Ahora haremos un salto importante: imaginemos que somos delfines y solo tenemos dos aletas para contar. Este es un sistema conocido como base de dos o binario. Se ve como si tuviéramos solo dos números: el 0 y el 1. El problema es que nos quedaremos sin dígitos muy rápidamente, dado que del 1 pasamos al 10 y luego al 100, cambiando rápidamente de dos dígitos a tres.
Los números de la derecha se pueden ver muy grandes, pero en realidad no lo son. Es más acertado decir que los números binarios se vuelven largos muy rápidamente en vez de grandes.
En el sistema binario la posición de los dígitos corresponden a la potencia de dos, por lo que cada vez que tengamos un número redondo en binario este número será una potencia de dos. A continuación se muestra una tabla con distintos sistemas numéricos y sus equivalentes en potencias de dos:
|
Potencia de |
Decimal |
Octal |
Cuaternario |
Binario |
|
2^0 |
1 |
1 |
1 |
1 |
|
2^1 |
2 |
2 |
2 |
10 |
|
2^2 |
4 |
4 |
10 |
100 |
|
2^3 |
8 |
10 |
20 |
1000 |
|
2^4 |
16 |
20 |
100 |
10000 |
|
2^5 |
32 |
40 |
200 |
100000 |
|
2^6 |
64 |
100 |
1000 |
1000000 |
|
2^7 |
128 |
200 |
2000 |
10000000 |
|
2^8 |
256 |
400 |
10000 |
100000000 |
|
2^9 |
512 |
1000 |
20000 |
1000000000 |
|
2^10 |
1024 |
2000 |
100000 |
10000000000 |
|
2^11 |
2048 |
4000 |
200000 |
100000000000 |
|
2^12 |
4096 |
10000 |
1000000 |
1000000000000 |
Notar como los dígitos de más a la derecha se van intercalando entre 1, 2 y 4 en el octal y 1, 2 en el cuaternario. En el sistema decimal no hay cambios, dado que el 1 es el dígito más alto. También notar que en potencia de dos en decimal, el sistema octal, cuaternario y binario son números redondos.
Imaginemos que tenemos el número binario 10010110. Este número puede ser representado en decimales si multiplicamos el número por la potencia de dos correspondiente a su posición:
Multiplicando cada dígito de unos y ceros por su potencia de dos correspondiente a su posición y luego sumar estos resultados nos da el mismo número, pero en decimal. Se ve fácil, pero hacerlo al revés no es tan simple. Si por ejemplo queremos transformar el 150 decimal a binario primero debemos dividir ese número en 128 y el resto pasarlo al siguiente cuadrado y dividir esta vez por 64 y así sucesivamente. Si el dividendo es menor al divisor el cociente es cero y el resto es el dividendo. Si el dividendo es mayor o igual al divisor el cociente es uno y el resto es el dividendo menos el divisor.
Sumar y multiplicar números binarios es más simple que hacerlo con decimales. Utilizamos el mismo algoritmo que los números decimales, pero solo con unos y ceros.
Con esto en mente podemos tener una tabla de adición y de multiplicación.
|
+ |
0 |
1 |
x |
0 |
1 |
|
|
0 |
0 |
1 |
0 |
0 |
0 |
|
|
1 |
1 |
10 |
1 |
0 |
1 |
Podemos anotar los números binarios con ceros antes del primer uno solo por temas de orden. Por ejemplo, el número 00000011 es lo mismo que el número 11 Los ceros de la izquierda no hacen cambiar el valor del número.
|
Binario |
Decimal |
|
0000 |
0 |
|
0001 |
1 |
|
0010 |
2 |
|
0011 |
3 |
|
0100 |
4 |
|
0101 |
5 |
|
0110 |
6 |
|
0111 |
7 |
|
1000 |
8 |
Hay algo interesante en los números binarios y es como se van alternando de 0 a 1: El dígito de más a la derecha se va alternando de 0 a 1 cada vez que aumentemos su valor en uno, el penúltimo dígito se va alternando cada dos incrementos, el antepenúltimo cada cuatro y el cuarto de derecha a izquierda cada 8. Esto tiene un patrón de potencia de dos: los dígitos se van alternando de 0 a 1 cada 2^(n-1) veces donde n es su posición de derecha a izquierda (el dígito de más a la derecha se alterna por cada incremento o 2^0, el segundo de derecha a izquierda cada dos incrementos o 2^1, etc.)
Reduciendo el sistema numérico a dos dígitos ya llegamos al punto máximo de reducción; no podemos hacerlo más simple. Además, los números binarios unen la separación entre la aritmética y la electricidad, dado que varios componentes pueden ser representados por números binarios: los cables pueden ser uno si la corriente fluye en ellos o cero si no fluye, el interruptor puede ser uno si esta prendido o cerrado y cero si está apagado o abierto, la ampolleta puede ser uno si está prendida y cero si está apagada o también un relé si está cerrado representa un uno y si está abierto, un cero. Los números binarios tienen mucho que ver con los computadores.
Bit a bit
Alrededor de 1948 John Wilder Tukey acuñó el término bit para referirse a una unidad de información que puede ser 0 o 1. Esta unidad de información puede estar en muchas formas, como si o no, prendido o apagado, la presencia o ausencia de algo, etc.
El binario es un sistema especial, porque no puede ser más pequeño: si eliminamos el uno y nos quedamos solo con el cero no podríamos hacer mucho. Es por esto que un bit es considerado como un bloque básico de información o la unidad mínima de información. Algo más pequeño que un bit no es información en absoluto, pero como un bit es el más pequeño monto de información posible, información más compleja puede ser armada con mayor número de bits. No confundir información pequeña con información de poca importancia; un solo bit puede ser la separación entre la vida y la muerte. Veamos un ejemplo de esto:
Las colonias tenían un sistema de comunicación para informar sobre la condición de los británicos. Ellos usaban dos linternas para este fin donde tenían las siguientes posibilidades:
-
Si los británicos invaden por tierra prendían una linterna en la torre de la iglesia.
-
Si los británicos invaden por mar prendían dos linternas en la torre de la iglesia.
-
Si los británicos no estaban invadiendo aún las linternas permanecen apagadas.
Cada linterna es un bit donde puede ser 1 para una linterna prendida o 0 para una linterna apagada. Esto quiere decir que un bit representa dos posibilidades. Si quisiéramos solo informar si los británicos invaden o no solo una linterna habría sido suficiente, pero como son tres posibilidades necesitamos una linterna extra:
-
00: Los británicos no están invadiendo.
-
01: Los británicos invaden por tierra.
-
10: Los británicos invaden por tierra.
-
11: Los británicos invaden por mar.
Aquí lo esencial es que la información representa una elección entre dos o más posibilidades. Por ejemplo, cuando hablamos con otra persona podríamos enumerar todas las palabras del diccionario desde el 1 al 351.482 y conversar con números en vez de palabras y los números pueden ser representados por bits, pero los interlocutores deben conocer el código.
El significado de un bit siempre tiene que estar dentro de un contexto, como la invasión británica, porque de lo contrario no sabríamos qué significa que estén las linternas prendidas. Si dijéramos que un bit apagado o cero bit significa 1 es contraintuitivo, porque pensamos en un 1 bit como algo afirmativo y 0 bit como algo negativo, pero eso es arbitrario; lo importante es que las partes conozcan que representa un bit que es cero y un bit que es uno.
Los colonos utilizaron 4 bits de información para representar si los británicos estaban invadiendo a los colonos y por donde. Si agregaramos una linterna más podríamos abarcar 8 posibilidades lo que significa más información para los colonos en caso de necesitarla (dos posibilidades en cada linterna combinada con la posibilidad de las otras linternas es 2 * 2 * 2, vale decir 2^3). Si agregamos otra linterna serían 16 posibilidades y así. En binario el número de posibilidades es siempre dos elevado al número de bits
|
Número de bits |
Número de posibilidades |
|
1 |
2^1 = 2 |
|
2 |
2^2 = 4 |
|
3 |
2^3 = 8 |
|
4 |
2^4 = 16 |
|
5 |
2^5 = 32 |
Cada bit adicional dobla el número de posibilidades. Si quisiéramos hacerlo al revés, es decir, del número de posibilidades calcular cuántos bits necesitamos, necesitamos calcular el logaritmo en base de dos, dado que es al revés de la potencia de dos. Por ejemplo, si tenemos 200 posibilidades vamos a necesitar 8 bits, dado que log 2(200) = 7,64.
Muchas veces los bits están escondidos de la observación humana en muchos aparatos electrónicos, pero otras veces los podemos ver claramente. Veamos un par de ejemplos en donde esto ocurre:
Esto es una cámara de 35 milímetros de filmación que usa un sistema llamado DX-Encoding que trata de 12 cuadrados que expresan 12 bits. Un cuadrado plateado (conductor) expresa 1 bit y un cuadrado negro expresa 0 bit (aislante). La corriente será captada o no por los cuadrados. Los primeros 6 cuadrados nos dice que tan sensible es la cámara a la luz, donde eso va a garantizar que tan rápida es la filmación (la velocidad de filmación es indicada por el ASA o American Standards Association). El circuito eléctrico pasará por el cuadrado 1 que es siempre plateado y la corriente será luego tomada o no tomada por los cuadrados 2 a 6. Según qué cuadrado tome la corriente eléctrica se define la velocidad de la cámara, por ejemplo, si la corriente toma solo los cuadrados 4 y 5 la película tiene una velocidad de 400 ASA. La cámara luego se ajustará a esa velocidad.
Los cuadrados 8, 9 y 10 indican el número de exposición del rollo de la película y el 11 y el 12 indican la latitud de exposición que depende si la película es blanco-negro o en color.
Una de las muestras más visuales de dígitos binarios en el Universal Product Code (UPC). Este es un código de barras que aparece en prácticamente todos los artículos envasados que compramos hoy en día. El UPC viene a simbolizar una de las formas en que los computadores se han colado en nuestras vidas. Se inventó con el fin de automatizar los checkout y registrar el inventario.
Las barras negras del UPC pueden tener cuatro anchos diferentes. El escáner sólo ve las barras así:
Como podemos ver, el computador escanea las barras y asigna 1 a las barras negras y 0 a las blancas.
El UPC completo es simplemente una serie de 95 bits. En este ejemplo concreto, los bits pueden agruparse de la siguiente manera:
Los primeros 3 bits son siempre 101 (patrón de protección a la izquierda) y permite que el computador se oriente y pueda determinar el ancho de un bit, dado que cada código de barra puede tener anchos distintos. A continuación sigue un grupo de seis bits de 7 y luego un grupo de 5 que vendría siendo el centro. Este siempre es 01010 y es una forma de precaución ante un código malintencionado o mal impreso. Después viene otro grupo de seis donde cada uno tiene 7 bits y finalmente viene otro patrón de protección, esta vez a la derecha que es siempre 101.
El UPC completo codifica 12 dígitos numéricos. Podemos usar la siguiente tabla para decodificar estos bits:
|
0001101 = 0 |
0110001 = 5 |
|
0011001 = 1 |
0101111 = 6 |
|
0010011 = 2 |
0111011 = 7 |
|
0111101 = 3 |
0110111 = 8 |
Observe que en los códigos del lado izquierdo de cada grupo de códigos comienza con un 0 y termina con un 1 y también cada código tiene un número impar de bits de 1. Esta es otra forma de comprobación de errores y consistencia conocida como paridad (en este caso una paridad impar). En los códigos del lado derecho los códigos son el complemento de los códigos del lado izquierdo: lo que era un 0 ahora es un 1 y viceversa.
Así que ahora que hemos analizado el UPC podemos averiguar que el equivalente numérico a los números decimales de esos códigos son 0 51000 01251 7 (los mismos números que aparecen debajo de las barras). Esto tiene mucho sentido porque si el escáner no puede leer el código por alguna razón, la persona que está en la caja registradora puede introducir los números manualmente.
El primer dígito (0) se conoce como el carácter del sistema numérico. Un 0 significa que se trata de un código UPC normal. Los cinco dígitos siguientes (51000) constituyen el trabajo del fabricante y todos los productos fabricados por un determinado fabricante tienen el mismo código de cinco dígitos. Los cinco dígitos que siguen (01251) son el código de un producto concreto de esa empresa. Como puede ver, el UPC no incluye el precio del artículo. El último dígito (7) se denomina carácter de comprobación del módulo, que es otra forma de comprobación de errores. Si el ordenador que controla el escáner no calcula el mismo carácter de comprobación de módulo que el codificado en el UPC, el ordenador no aceptará el UPC como válido. De los 95 bits de un UPC solo 11 son utilizados para generar los números del código mientras que el resto son por razones de seguridad. Parte de los bits adicionales que utiliza el UPC son necesarios para la comprobación de errores. Un código de producto como éste no sería muy útil si pudiera ser fácilmente alterado por un cliente con un rotulador. El UPC también se beneficia de ser leído en ambas direcciones. Lo importante aquí es mantener la paridad o, que en otras palabras, el código pueda ser leído de izquierda a derecha y viceversa.
Recordemos que el Morse y el Braille también funcionan con código binario. Como estamos trabajando con el mismo sistema o lenguaje podemos convertir esos dos sistemas de comunicación a 1 y 0s.
Podemos decir que un punto es un uno y una raya son dos unos, que equivale a dos puntos o dos bits de uno. El espacio puede ser simbolizado con un 0. YA con esto podemos traducir un código Morse a uno binario:
Observe que todos los códigos comienzan con un 1 y terminan con un par de bits 0. El par de bits 0 representa la pausa entre las letras de una misma palabra.
En términos de bits, el Braille es mucho más sencillo que el código Morse. El braille es un código de 6 bits y por cada punto en relieve hay un 1 y por cada punto en plano hay un 0. En el siguiente ejemplo hay una conversión entre braille y números binarios para la palabra "código":
Como veremos, los bits pueden representar palabras, imágenes, sonidos, música y películas, así como códigos de productos, velocidades de películas, clasificación de películas, etc. Pero, fundamentalmente, los bits son números. Lo único que hay que hacer cuando los bits representan otra información es contar el número de posibilidades. Esto determina el número de bits que son necesarios para que cada posibilidad pueda ser asignada a un número.
Los bits también desempeñan un papel en la lógica, cuyo objetivo principal es determinar si ciertas afirmaciones son verdaderas o falsas. Verdadero y falso también puede ser 1 y 0.
Lógica e interruptores
¿Qué es la verdad? Aristóteles pensaba que la lógica tenía algo que ver con ella, por ejemplo en el famoso silogismo "todos los hombres son mortales, Sócrates es un hombre, por tanto Socrates es mortal", dos premisas son asumidas como correctas y una conclusión es deducida con respecto a eso. Para los antiguos griegos, la lógica era un medio para analizar el lenguaje en busca de la verdad.
George Boole (1815 - 1864) dio un gran paso adelante al crear un tipo de álgebra que funciona de forma muy parecida al álgebra convencional y que sirve para ver si una afirmación es verdadera o falsa utilizando la lógica. Ciertamente, Boole utilizó en su Álgebra de Boole todas las reglas que tiene el álgebra convencional como:
|
Conmutativo |
A + B = B + A
|
|
Asociativo |
A + (B + C) = (A + B) + C A x (B x C) = (A x B) x C |
|
Distributivo |
A x (B + C) = (A x B) + (A x C) |
Otra característica del álgebra convencional es que siempre trata con números. El álgebra de Boole es más abstracta porque no contiene números y los operandos no se refieren a números sino a clases (un grupo de cosas) que se conoce como conjunto. Por ejemplo, imaginemos que estamos en una tienda de mascotas y queremos comprar un gato o gata. Podemos expresar las gatas con una "F" y los gatos con una "M". También podemos representar el color de los gatos. Por ejemplo, la T puede referirse a los gatos morenos, la B a los negros, la W a los blancos y la O a los de otros colores. Por último, los gatos pueden estar castrados ("N") o sin castrar ("U").
El símbolo + significa la unión de dos clases (combinación) en el álgebra booleana. El símbolo x significa una intersección de dos clases; una intersección es todo lo que está en la primera clase y en la segunda. Por ejemplo, F x T o FT representa todos los gatos que son a la vez hembras y bronceados.
Para evitar la confusión entre el álgebra convencional y el álgebra booleana, a veces se utilizan los símbolos ⋃ y ⋂ para la unión y la intersección en lugar de + y x. En el álgebra booleana el símbolo + es distributivo sobre el operador x. Esto no es cierto en el álgebra convencional:
W + (B x F) = (W + B) x (W + F)
La unión de los gatos blancos y los gatos negros es lo mismo que la intersección de dos uniones: la unión de los gatos blancos y los gatos negros y la unión de los gatos blancos y las gatas.
Hay otros dos símbolos en el Álgebra de Boole: 1 que significa "el universo" (todas las clases)
F + M = 1
El símbolo 1 se puede utilizar con un signo menos para indicar que el universo excluye algo, por ejemplo
1 - M = F
El símbolo 0 significa lo contrario: Una clase vacía. La clase vacía resulta cuando tomamos una intersección de dos clases mutuamente excluyentes, por ejemplo
F x M = 0
Podemos usar el 0 y el 1 igual que usamos el álgebra convencional como 1 x F = F o 0 + F = F pero donde el álgebra booleana se ve diferente del álgebra convencional es en una afirmación como esta: F x F = F. Esta afirmación tiene sentido en el álgebra booleana como también F + 1 = F o F + F = F, pero no tiene sentido alguno en el álgebra convencional. Así que F^2 o 2F sigue siendo una F.
El álgebra de Boole no sólo se puede utilizar para demostrar hechos obvios (Si todos los humanos son mortales y Sócrates es un humano, entonces Sócrates es mortal), sino que también se puede utilizar para determinar si algo satisface un determinado conjunto de criterios. Por ejemplo, nosotros seguimos en la tienda de mascotas pensando en qué gato escoger y finalmente le decimos al vendedor "Quiero un gato macho, castrado, de color negro o fuego; o una gata castrada, de cualquier color menos blanco; o me llevo cualquier gato que tengan siempre que sea negro". La expresión puede escribirse en álgebra booleana así
(M x N x (W + T)) + (F x N x (1 - W)) + B
Notar que:
El + (unión) ahora significa OR
La x (intersección) ahora significa AND
El 1 - (sin algo) ahora significa NO
Cuando se forma una unión de dos clases, en realidad se está aceptando una cosa de la primera clase O de la segunda clase y cuando se forma una intersección se está aceptando sólo aquellas cosas que están tanto de la primera clase Y en la segunda clase.
Con esta fórmula, el vendedor puede realizar algo llamado prueba booleana. Las letras ahora se pueden asignar a los números 1 o 0. El número 1 significa verdadero (este gato satisface estos criterios) y el 0 significa falso (este gato en particular no satisface estos criterios).
Así, por ejemplo, si el vendedor trae un gato macho de color canela, tenemos que ver la expresión de los gatos aceptables y poner 1 cuando ese gato en particular satisfaga los criterios.
(M x N x (W + T)) + (F x N x (1 - W)) + B = (1 x 0 x (0 + 1) + (0 x 0 x 0 (1 - 0)) + 0
Observa que los únicos símbolos a los que se les ha asignado un 1 son M y T porque el gato es macho y está bronceado. Ahora tenemos que simplificar esta expresión. Aquí se aplican la mayoría de las mismas reglas de las matemáticas convencionales:
Tabla de OR (a la izquierda) y tabla de AND (a la derecha)
|
+ |
0 |
1 |
x |
0 |
1 |
|
|
0 |
0 |
1 |
0 |
0 |
0 |
|
|
1 |
1 |
10 |
1 |
0 |
1 |
Estamos listos para usar estas tablas para calcular el resultado de la expresión:
(1 x 0 x (0 + 1) + (0 x 0 x 0 (1 - 0)) + 0 = 0 + 0 + 0 = 0
El resultado 0 significa No, Falso, este gatito no sirve.
Así podemos seguir hasta conseguir el gato que nosotros deseamos, por ejemplo, ahora queremos un gato hembra que sea esterilizado y que sea gris (cae en la categoría otros colores). La expresión sería:
(0 x 1 x (0 + 0) + (1 x 1 x (1 - 0)) + 0 = 1
El resultado final significa Si, Verdadero, este gatito ha encontrado un hogar.
Mientras estás con tu gato descansando en tu pierna, empiezas a pensar que podemos utilizar cables, interruptores y ampolletas para determinar si un gato puede satisfacer un criterio (si, eres un niño muy raro). Aquí entramos en un quiebre fundamental: estamos por hacer experimentos que unen el álgebra de Boole con los circuitos eléctricos que hacen posible diseñar y construir un computador que trabaja con números binarios.
Para comenzar con nuestro experimento conectamos una lámpara con una batería, pero en vez de tener un interruptor tendremos dos interruptores, dónde, si ambos están cerrados la corriente fluye, pero si uno está abierto la corriente se interrumpe y la ampolleta no se podría prender.
Aquí la palabra clave es AND. La ampolleta estará prendida si y sólo si ambos interruptores están cerrados. Este circuito hace un trabajo en lógica, donde responde a la pregunta ¿están ambos interruptores cerrados?. No nos importa qué interruptor está abierto o está cerrado; lo importante es que estamos generando un bit de información o dos posibilidades. En este sentido, estamos haciendo lo mismo que las linternas que usaron los colonos para informar si los británicos invaden o no. Además, la misma lógica se aplica en este circuito que la tabla AND que vimos anteriormente (la salida es uno solo si ambas entradas son 1).
También podemos hacer un circuito donde la palabra clave es OR y lo logramos conectando los interruptores en paralelo. La diferencia con el anterior es que si uno de los interruptores está cerrado se prenderá la lámpara.
Nuevamente el circuito está realizando un ejercicio lógico. La ampolleta responde a la pregunta ¿Hay algún interruptor cerrado?. El comportamiento de este circuito es el mismo que una tabla OR (la salida es uno si solo una de las entradas es uno).
Cuando originalmente entramos a la tienda le dijimos al vendedor: "Quiero un gato macho, castrado, de color negro o fuego; o una gata castrada, de cualquier color menos blanco; o me llevo cualquier gato que tengan siempre que sea negro"
(M x N x (W + T)) + (F x N x (1 - W)) + B
Podemos crear nuestro propio circuito que pueda responder a esa pregunta poniendo interruptores en el mismo cable para los AND y en cables paralelos para los OR.
Cada interruptor es etiquetado con una letra igual a la expresión booleana. El W barra significa que no es W o en otras palabras 1 - W. En estas tres imágenes tenemos distintas opciones para el gatito de nuestros sueños. Cuando la ampolleta se prenda mientras jugamos con los interruptores podemos llevarnos un gatito a casa.
George Boole nunca realizó un circuito así. Un obstáculo claro es que las ampolletas se inventaron 15 años después de su muerte. De hecho, ninguna persona en el siglo XIX relacionó el álgebra de Boole y las conexiones eléctricas, ni siquiera Charles Babbage (1792-1871) que estuvo toda su vida intentando diseñar primero una máquina diferencial y luego una máquina analítica que un siglo después serían consideradas las precursoras de los computadores modernos. Lo que podría haber ayudado a Babbage, lo sabemos ahora, fue la comprensión de que tal vez en lugar de engranajes y palancas para realizar los cálculos, un ordenador podría construirse mejor con relés telegráficos.
Puertas lógicas
Las puertas lógicas realizan tareas simples en lógica, bloqueando o permitiendo el paso de la electricidad.
Recordemos nuevamente como era la expresión booleana que representa el criterio de nuestro gato favorito.
(M x N x (W + T)) + (F x N x (1 - W)) + B
Este circuito se llama network o red, aunque hoy en día la red se refiere más a la conexión de computadores en vez del ensamblaje de interruptores.
Aunque este circuito no contiene nada que no haya sido inventado en el siglo XIX, nadie en ese siglo se dio cuenta de la conexión entre las expresiones booleanas y los circuitos eléctricos. Fue hasta 1930 por el trabajo de Claude Elwood Shannon que hizo esa conexión en su trabajo de tesis. Es tanta la conexión que existe que si puedes simplificar una expresión booleana que describe la red, puedes simplificar la red en consecuencia. Eso es justamente lo que haremos ahora.
La expresión booleana anterior la podemos simplificar utilizando la ley asociativa y distributiva, reordenando las variables que están combinadas con AND:
Paso 1: Ordenamos las variables que son combinadas con el AND: (N x M (W + T)) + (N x F x (1 - W)) + B
Paso 2: Para clarificar lo que haremos a continuación, crearemos dos variables nuevas: X = M x (W + T) e Y = F x (1 - W)
Paso 3: Ahora podemos escribir la expresión así: (N x X) + (N x Y) + B
Paso 4: El valor N aparece dos veces en la expresión, por lo que, utilizando la ley de distribución, podemos reescribir la expresión así: (N x (X + Y)) + B
Paso 5: Reemplazamos los valores de X e Y a sus valores correspondientes quedando la expresión así: (N x ((M x (W + T) + (F x (1 - W)))) + B
Ahora con esta simplificación tenemos una variable menos en nuestra expresión, es decir, un interruptor menos en nuestra red.
En realidad, todavía hay tres interruptores de más en esta red. En teoría, sólo se necesitan cuatro interruptores porque esa es toda la información que se necesita para elegir al gato perfecto (1 bit para el sexo, 1 bit para un gato castrado o sin castrar y 2 bits para el color).
Hagamos un panel de control para elegir a nuestro micifuz. El panel de control está compuesto por cuatro interruptores (4 bits) y una ampolleta.
Los interruptores están encendidos (cerrados) si están arriba y apagados (abiertos) si están abajo. Si los dos interruptores del lado derecho están arriba es de otro color, si ambos están abajo es blanco, si el primero está arriba y el segundo abajo es negro y finalmente si el primero está abajo y el segundo arriba es bronceado. Los interruptores de la izquierda indican si el gato es hembra o macho y si está esterilizado o no. Ahora solo nos falta crear un circuito adecuado para que este panel de control de 4 bits funcione.
En la terminología informática, los interruptores son un dispositivo de entrada. La entrada es la información que controla el comportamiento de un circuito. El dispositivo de salida es la bombilla. Esta bombilla se enciende si los interruptores describen un gato satisfactorio.
Al igual que los interruptores, los relés pueden conectarse en serie y en paralelo para realizar tareas sencillas en lógica. Estas combinaciones de relés se denominan puertas lógicas. Los relés tienen la ventaja sobre los interruptores de que pueden ser encendidos y apagados por otros relés en lugar de por los dedos. Esto significa que las puertas lógicas pueden combinarse para realizar tareas más complejas, cómo resolver funciones en aritmética.
Como vimos anteriormente, los relés estuvieron presentes exclusivamente en la industria de la comunicación con los telégrafos y fueron utilizados para amplificar una señal recibida. Para nuestro propósito, a nosotros no nos interesa amplificar una señal, sino que nos interesa usar los relés como interruptores para que sean controlados por la electricidad en vez de por los dedos. Podemos conectar un relé con un interruptor manual, una ampolleta y un par de baterías de la siguiente manera:
Cuando cerramos el interruptor manual, la corriente comienza a correr sobre los cables alrededor de la barra de hierro, haciendo que esta sea magnética. Esto genera que el interruptor interno se vea atraído por el magnetismo, haciendo que se cierre, generando corriente eléctrica y prendiendo la ampolleta.
Cuando el electroimán tira del contacto metálico, se dice que el relé está activado. Cuando se apaga el interruptor, la barra de hierro deja de ser magnética y el contacto metálico vuelve a su posición normal. Podemos volver a dibujar este diagrama utilizando sólo una pila:
Podemos simplificar aún más el diagrama, conectando la batería a tierra y así eliminar algunos cables. La batería a su vez la podemos reemplazar por la letra "V" de voltaje como lo hicimos anteriormente. Ahora nuestro relé quedaría así:
La entrada de un relé no necesariamente debe ser un interruptor y la salida no necesariamente una ampolleta. Por ejemplo, la salida de un relé puede estar conectada a la entrada de otro relé. Conectar los relés de esta forma permite construir puertas lógicas.
En la imagen anterior vemos que los relés están conectados en serie. La única manera de cerrar un círculo en el circuito es que ambos interruptores estén cerrados. Estos relés conectados en serie se le conoce como puerta AND. Para evitar dibujos en exceso los ingenieros eléctricos tienen un símbolo especial para referirse a las puertas AND:
Este es el primero de cuatro puertas lógicas. Notar que acá lo dibujamos de izquierda a derecha desde la entrada, pero puede tener cualquier orientación.
Las entradas de la puerta AND no tienen porqué estar conectadas a interruptores, y la salida no tiene por qué estar conectada a una ampolleta. Todo se trata de los voltajes de la entrada y del voltaje de salida. Por ejemplo, la salida de una puerta AND puede ser la entrada de una segunda puerta AND:
Notemos que la ampolleta se prenderá sólo si están todos los interruptores cerrados. Esto significa que una puerta AND puede tener más de dos inputs. Este se conoce como puerta 3-input AND y se simboliza de la siguiente manera:
La próxima puerta lógica involucra dos relés que están conectados en paralelo de la siguiente manera:
Note que las salidas de los dos relés están conectadas entre sí. Esto genera que la ampolleta se prenda si solo una de estas dos salidas tiene voltaje. Esto quiere decir que si cerramos uno o dos interruptores la luz se prenderá y sólo estará apagada si ambos interruptores están abiertos. Este comportamiento hace referencia a la lógica OR y por consecuencia un circuito conectado de esta manera se le conoce como puerta OR. La puerta OR se simboliza de la siguiente manera:
El símbolo es similar a la puerta AND solo que el lado del input está redondeado como una O. Esto nos puede servir para recordar cómo luce el símbolo de OR. Las puertas OR también pueden tener más de dos inputs.
Hay un tipo de relé llamado double-throw que tiene dos salidas eléctricamente opuestas, vale decir, cuando una tiene voltaje, la otra no. En este caso, la ampolleta está encendida cuando el interruptor está abierto o apagado.
Como vemos, cuando la entrada es uno, la salida es cero y viceversa. Un simple relé conectado de esta manera se llama relé inversor. Esta no es una puerta lógica (las puertas lógicas siempre tienen dos o más entradas), pero es muy útil. Este relé es representado por el siguiente símbolo:
Con el relé inversor, la puerta AND y la puerta OR ya podemos comenzar a construir el panel de control para automatizar la decisión de qué gato llevarnos a casa.
Para seleccionar el gatito de nuestros sueños, podemos usar el inversor cuando necesite que mi gato soñado sea de cualquier color excepto blanco (1 - W) o cuando no podemos elegir entre dos posibilidades que son opuestas entre sí (como un gato macho y una hembra)
Cuando F es uno el gato es hembra y viceversa. De igual manera, si el interruptor está cerrado es que el gato es esterilizado y viceversa.
Ahora vamos por el color donde necesitamos una combinación mucho más complicada de puertas lógicas. Necesitamos seleccionar entre cuatro colores (negro, blanco, bronceado, otro color). Aquí como estamos expresando 4 bits de información necesitamos dos entradas o inputs, dado que 2^2 = 4.
Para poder utilizar solo dos entradas, debemos asegurarnos que cada combinación de interruptores prenda una ampolleta distinta. Para ello nos valdremos de la puerta lógica AND y de los inversores.
|
Color |
Entrada |
Red |
|
Blanco |
0 y 0 |
|
|
Negro |
1 y 0 |
|
|
Bronceado |
0 y 1 |
|
|
Otro |
1 y 1 |
|
La entrada cero significa que el interruptor está abierto y uno es que está cerrado. La combinación 1 y 0, por ejemplo, significa que el interruptor de arriba está cerrado y el interruptor de abajo está abierto. La combinación de entradas de cada uno de los colores son únicas para que una ampolleta en específico se prenda, por ejemplo, para el color bronceado si el primer interruptor lo abrimos y el segundo lo cerramos se prenderá la ampolleta, pero ninguna otra combinación hará prender la ampolleta, es decir, para las demás combinaciones la ampolleta permanecerá apagada. Lo mismo ocurre con los demás colores.
Si combinamos todas estas conexiones en un gran circuito tendrá este aspecto (los puntos negros indican las conexiones entre los cables del circuito):
Este circuito puede que se vea intimidante al principio, pero si rastreamos el camino por el que corre la electricidad veremos que funciona, vale decir, cualquier combinación de entradas (son 4 combinaciones) hará que sólo una ampolleta se prenda.
Este circuito con cuatro puertas AND y dos inversores se le conoce como 2-Line-to-4-Line Decoder y es llamado así porque la entrada tiene dos bits y la salida 4 bits y también porque solo habrá una ampolleta prendida en todo momento sin importar qué interruptores movamos. Podemos hacer por ejemplo un circuito 3-Line-to-8-Line Decoder o 4-Line-to-16-Line Decoder bajo los mismos principios.
Ahora estamos listos para terminar el trabajo de crear una red para seleccionar a nuestro gato ideal. Recordemos que nuestra expresión simplificada de selección felina es (N x ((M x (W + T) + (F x (1 - W)))) + B. Por cada signo + debe ir una puerta OR y por cada signo x debe ir una puerta AND.
Los símbolos que están en la entrada del circuito aparecen en el mismo orden que la expresión. Los símbolos de los colores como W o B reciben el output del 2-Line-to-4-Line Decoder construido recientemente. Notemos el uso de 1 - W para expresar los gatos que son de otros colores.
Nos falta ver dos puertas lógicas más donde ambas tienen la misma configuración que las puertas AND y OR, pero utilizan relés de tipo double-throw.
Como vemos la ampolleta solo se prenderá si todos los interruptores están abiertos. Este comportamiento es el opuesto a una puerta OR. Por esto, esta puerta se llama NOT OR o más precisamente NOR. El símbolo de esta puerta es el siguiente:
Esto es lo mismo a que si tuviéramos una puerta OR con un inversor en la salida, dado que un inversor genera una salida opuesta. El círculo en la salida justamente simboliza a un inversor.
De igual manera, podemos tener una puerta lógica AND, pero utilizando relés de tipo double-throw:
En esta puerta las salidas están conectadas lo que significa que si un relé tiene voltaje y el otro no de igual manera la ampolleta se prenderá. Esto es similar a una puerta OR, pero utilizando otros contactos.
La ampolleta permanecerá apagada si y sólo si ambos interruptores están cerrados. Esto es lo opuesto a una puerta AND y por lo mismo a esta puerta se le conoce como NOT AND o NAND. El símbolo NAND se dibuja de la misma manera que la puerta AND salvo por un círculo en la salida que simboliza un inversor.
Ahora que ya vimos las cuatro puertas lógicas o en otras palabras, cuatro formas de de conectar relés con dos entradas y una salida, podemos resumir su comportamiento en las siguientes tablas:
|
AND |
0 |
1 |
OR |
0 |
1 |
NAND |
0 |
1 |
NOR |
0 |
1 |
|||
|
0 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
0 |
|||
|
1 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
1 |
0 |
0 |
Complementamos las conexiones que hemos hecho con un relé más:
Este relé se llama buffer y su símbolo es así:
Es el mismo símbolo que el inversor, pero sin el círculo. Si nos fijamos en el flujo de la electricidad, el buffer no hace nada más que fortalecer una señal al igual como se utilizaba con los telégrafos, dado que la entrada es la misma que la salida. El buffer en computación puede ser utilizado para retrasar un poco la señal.
Anteriormente cuando construimos nuestro 2-Line-to-4-Line Decoder pudimos observar un pequeño circuito que se veía así:
Las dos entradas se invierten y se llegan a la puerta AND como input. Este comportamiento tiene el siguiente símbolo:
Como tenemos dos inversores en la entrada, podemos tener el mismo comportamiento si tenemos un inversor en la salida y con una puerta OR en vez de una puerta AND. De la misma manera, tenemos el mismo comportamiento con una puerta OR con dos inversores en su entrada y una puerta AND con un inversor en la salida.
Estas equivalencias representan una implementación de la Ley de De Morgan que es expresada de la siguiente manera:
Estas son dos expresiones booleanas en donde la primera significa que al invertir A y B y luego combinarlas es equivalente a la unión de A y B y luego invertir su resultado (equivalente a un NOR). En la segunda expresión es al revés, vale decir, invertir A y B y luego unirlas es lo mismo que combinar A y B y luego invertir su resultado (equivalente a un NAND).
La ley de Morgan es importante para simplificar las expresiones Booleanas y por lo tanto los circuitos. Aquí no nos enfocaremos tanto en reducir circuitos, si no en ver como funcionan que es más importante.
Una máquina que suma números binarios
La suma es la más básica de las operaciones aritméticas, por lo que si queremos construir un computador debemos saber primero cómo construir algo que sume dos números. A fin de cuentas, la suma es casi lo único que hacen los computadores. Si podemos construir algo que sume, estaremos en camino de construir algo que utilice la suma para también restar, multiplicar, dividir, calcular los pagos de la hipoteca, guiar cohetes a Marte, jugar al ajedrez y un sinfín de otras tareas. Esta máquina de sumar funcionará completamente con números binarios, interruptores y ampolletas tal como lo hemos estado haciendo hasta ahora (tecnología de hace más de 120 años), pero lo hará de la misma forma que los computadores modernos suman números. Para no complicarnos, esta máquina sólo será capaz de sumar dos números de un máximo de 8 bits.
Recordemos que sumar números binarios funciona de la misma forma que sumar números decimales. La ventaja es que es muchísimo más simple:
|
+ |
0 |
1 |
|
0 |
0 |
1 |
|
1 |
1 |
10 |
El resultado de sumar dos números en binario genera dos bits, donde uno es llamado el bit de suma y el otro el bit de carga (1 + 1 es 0 con la carga del 1). Con esto podemos dividir la suma de números binarios en dos tablas: bit de suma y bit de carga. Veamos cómo funciona una carga en números binarios:
Como en una suma en base de 10, sumamos los números de columna en columna comenzando con la derecha. Si tenemos un número de dos dígitos como resultado, como se muestra en la tercera columna, el resultado de esa columna es cero y pasamos el uno a la columna de la izquierda y repetimos el proceso. Es por este procedimiento que necesitamos separar las sumas y cargas en dos tablas distintas, porque es así como construiremos nuestra máquina.
|
Bit de suma |
0 |
1 |
Bit de carga |
0 |
1 |
|
|
0 |
0 |
1 |
0 |
0 |
0 |
|
|
1 |
1 |
0 |
1 |
0 |
1 |
Podemos ver que una de estas tablas, específicamente la de carga, es igual al comportamiento de una puerta AND. Para poder crear una máquina fabricada de relés que pueda sumar debemos relacionar el comportamiento de la suma al comportamiento de una puerta lógica. La tabla de suma, sin embargo, no cuadra con ninguna puerta lógica, pero vemos que es igual a la puerta OR a excepción de la esquina inferior derecha y a una puerta NAND a excepción de la esquina superior derecha. Conectemos entonces ambas puertas lógicas a la misma entrada para ver que nos resulta:
Veamos cómo se comporta esta combinación de puertas lógicas. En la siguiente tabla podemos ver su comportamiento:
|
Entrada A |
Entrada B |
Salida OR |
Salida AND |
Lo que queremos |
|
0 |
0 |
0 |
1 |
0 |
|
0 |
1 |
1 |
1 |
1 |
|
1 |
0 |
1 |
1 |
1 |
|
1 |
1 |
1 |
0 |
0 |
Notar que queremos un 1 solo si el output de la puerta OR y la puerta NAND son 1. Esto sugiere que estas dos salidas deben estar conectadas a una puerta AND.
Y con eso ya está. Este circuito es conocido como puerta OR exclusivo o XOR. Es llamado OR exclusivo, porque la salida es uno si la entrada de A es uno o la entrada de B es uno, pero no ambos. El símbolo para este circuito es el siguiente:
La puerta XOR es la última que veremos (Hay una sexta puerta llamada puerta coincidente o equivalente, porque su salida es uno solo si dos de las entradas son la misma, generando una salida opuesta al XOR). Ahora que tenemos las puertas lógicas que necesitamos podemos combinarlas para acercarnos más a la máquina que queremos construir.
La imagen izquierda es la combinación de ambas puertas lógicas y la imagen derecha es la representación de esta combinación en forma de caja llamada "Half Adder". Esta caja tiene dos números que sumaremos llamados A y B. Esta caja también tiene dos salidas que corresponden al bit de suma y al bit de carga. En lo que falla el Half Adder es añadir un bit de carga de una suma previa (de ahí su nombre). Veamos un ejemplo:
Como vemos en la segunda columna, no se genera un bit de carga a la siguiente columna. Es por eso que podríamos ocupar un Half Adder en la columna más a la derecha, pero para la siguiente columna necesitamos añadir 3 bits y así consecutivamente para las columnas que siguen. Es por ello que debemos incluir otro Half Adder que esté conectado al primero para generar un bit adicional y conectar ambos bit de carga a una puerta OR.
Para comprender esto veamos primero las entradas A y B. La salida es un bit de suma y un bit de carga y estas a su vez son entradas para el segundo Half Adder. La suma del segundo Half Adder es la suma final. Los bit de carga de ambos Half Adder están conectados a una puerta OR de tal manera que si uno de ellos es uno, el resultado del bit de carga es uno. Ahora imaginemos que este Carry Out pasa a ser la entrada del Carry In. En definitiva el Carry In es el bit de carga que viene de la suma anterior. A estas conexiones la llamaremos "Full Adder" y se simboliza de la siguiente manera:
El comportamiento del Full Adder es el siguiente:
|
Entrada A |
Entrada B |
Bit de carga anterior |
Bit de suma |
Bit de carga |
|
0 |
0 |
0 |
0 |
0 |
|
0 |
1 |
0 |
1 |
0 |
|
1 |
0 |
0 |
1 |
0 |
|
1 |
1 |
0 |
0 |
1 |
|
0 |
0 |
1 |
1 |
0 |
|
0 |
1 |
1 |
0 |
1 |
|
1 |
0 |
1 |
0 |
1 |
|
1 |
1 |
1 |
1 |
1 |
Como vemos, el comportamiento de un Full Adder es igual a sumar números binarios. Entonces ahora estamos listos para comenzar a conectar los interruptores y las ampolletas a un Full Adder y así ver los resultados de la suma.
Esta conexión corresponde a la suma de la columna más a la derecha o la primera columna en donde realizamos la suma. Como no tenemos un carry bit que llegue de una columna precedente (al ser la primera suma), el CI o bit de carga anterior lo conectamos a tierra (a esto se le llama 0 bit). Por otro lado, el Carry Out lo conectamos al siguiente Full Adder siendo para este el bit de carga anterior o Carry In. Esto quiere decir que el bit de carga pasaría a ser un input para el siguiente Full Adder.
Esta conexión corresponde desde la penúltima columna a la segunda. Observemos que el Carry In viene del bit de carga del Full Adder anterior.
Finalmente, veamos cómo está conectada la última columna de la suma:
Tenemos una ampolleta extra porque la suma de dos números de 8 bits puede resultar en un número de 9 bits. La segunda ampolleta corresponde al bit de carga de la primera columna.
Si juntamos todos los Full Adder para completar la maquina de suma tendremos algo así:
Esta máquina puede ser representada por una caja donde sus entradas están etiquetadas de A0 a A7 y de B0 a B7. Las salidas están etiquetadas de S0 a S7.
Todo esto lo podríamos conectar a un panel con interruptores para que el usuario pueda ingresar los números que desea sumar.
Los bits A0, B0 y S0 son los bits menos significativos o los ubicados más a la derecha mientras que los bits A7, B7 y S7 son los bits más significativos o los ubicados más a la izquierda. Se le dice que son más significativos, porque inciden más en el tamaño del número.
Ya tenemos listo nuestra máquina de suma. Recordemos que esta máquina funciona como cascada, dado que, utiliza los bits de carga de un Full Adder a otro. A este funcionamiento se le llama Ripple Carry Adder (RCA) (complemento: https://es.jf-parede.pt/ripple-carry-adder-working ).
Si nos quedamos cortos con 8 bits nada impide que conectemos otra máquina de 8 bits para tener una máquina de suma de 16 bits.
Como vemos en la imagen basta con conectar el bit de carga del dígito más significativo de la primera máquina al bit de carga anterior o Carry Bit del dígito menos significativo de la segunda máquina. Con solo una conexión podemos agregar más bits en la suma.
Quizás nos estemos preguntando si esta es la forma en que los computadores suman. Bueno, si, pero no exactamente por dos razones. Primero, los sumadores modernos o "adders" son más rápidos, dado que tienen un circuito adicional llamado look-ahead carry que acelera el proceso. Segundo, los computadores modernos no usan relés, sino transistores, aunque en los años 30 los primeros computadores si usaban relés para luego pasar a las válvulas termoiónicas o tubos de vacío utilizados hoy en día en amplificadores.
Los transistores son mucho más pequeños, rápidos, silenciosos, baratos y usan mucha menos energía que los relés. Esta máquina que construimos llamada sumador de 8 bits utiliza 144 relés (más si reemplazamos la conexión ripple carry por look-ahead carry) lo que sería bastante grande, pero si reemplazamos todos esos relés por transistores, el circuito sería microscópico (hoy en día los transistores tienen un tamaño aproximado de 70 átomos de silicio).
Pero, ¿qué hay de la resta?
La suma y la resta se complementan en algunos aspectos, pero la mecánica de ambos operadores es diferente. Sin embargo, en la resta no llevamos un número a la siguiente columna, sino que lo tomamos prestado, y eso implica un mecanismo intrínsecamente diferente. El préstamo funciona tomando un número de la siguiente columna de la derecha sólo si el número que queremos restar es menor que el otro número. Veamos un ejemplo:
Primero, vemos que el 3 es mayor que el 6, por lo que debemos pedirle prestado al 5 un 1, por lo que el 5 quedaría en 4. El 3 a su vez toma este uno y se transforma en 13 y de ahí se resta con el 6, dando 7. En la segunda columna, el 4 también es menor al 7, por lo que debemos pedir prestado un uno al dos de la siguiente columna, quedando este en un uno y el 5 en un 15 y restamos, quedando nuevamente 7 como resultado.
El problema es cómo convertimos todo este comportamiento a un montón de relés. La verdad es que ni lo intentaremos, sino que utilizaremos un truco que nos permite restar sin pedir prestado. Para ello, debemos restar 999, que sería nuestro minuendo, con 176 que sería nuestro sustraendo.
Restar una cadena de nueves con un número se llama complemento de 9. En este caso, el complemento de 9 de 176 es 823 y funciona al revés: el complemento de 823 es 176. Cuando calculamos el complemento de 9 nunca vamos a necesitar pedir prestado.
Luego que hayamos calculado el complemento de 9 sumamos el minuendo original, que sería 823. Al resultado le sumamos 1 y luego le restamos 1000. El proceso completo sería el siguiente:
Llegamos al mismo resultado sin pedir prestado ningún número, dado que 253 - 176 = 77 es lo mismo que 253 + (999 - 176) + 1 - 1000 = 77.
¿Qué ocurre si el sustraendo es mayor que el minuendo? Normalmente, haríamos la resta, dando vuelta el sustraendo con el dividendo y el resultado lo pasamos a negativo.
Hacer este cálculo es un poco diferente al ejemplo anterior. Primero restamos 999 con el sustraendo, luego sumamos el resultado con el minuendo y finalmente (aquí está la diferencia) restamos por 999, porque si lo hiciéramos como el ejercicio anterior (sumar uno y restar 1000) necesitamos pedir prestado el número a la columna siguiente.
Cuando pasamos esta técnica a binario, no podremos trabajar con el complemento de 9 en decimales, dado que 999 en binario es 1111100111 lo que habría que pedir prestado un número por los dos ceros. Lo que hay que hacer es utilizar solamente números unos llamados complemento de uno. Veamos un ejemplo de 8 bits tomando el ejemplo original que es 253 - 176.
Nótese que el resultado es complemento del sustraendo, es decir, los números están invertidos de uno a cero y de cero a uno. Por esta razón el complemento de uno también se le conoce como la negación o la inversa. El siguiente paso sería sumar el resultado con el minuendo, añadir uno y luego restar 100000000 que en decimal equivale a 256.
El resultado es equivalente a 77.
Ahora bien, intentemos restar dos números en binario, pero ahora el sustraendo es mayor que el minuendo siguiendo los pasos anteriores.
Con esta estrategia nuevamente estamos invirtiendo los bits para obtener el resultado, la respuesta es nuevamente 77, pero más bien -77.
En este punto, ya tenemos los conocimientos necesarios para modificar nuestra máquina sumadora para que pueda realizar sumas y restas. Para evitar complejidades esta máquina realizará restas solo si el resultado es positivo (el minuendo es mayor al sustraendo).
El nuevo panel tendrá un interruptor adicional para definir si la operación es suma o resta (está prendido cuando es resta y apagado cuando es suma). La novena ampolleta es ahora etiquetada como "Overflow/Underflow" por si una suma supera los 8 bits de resultado (overflow) o si una resta genera un resultado negativo (underflow).
La mayor añadidura a este circuito sería algo que calcule el complemento de uno de un número. Recordemos que los complementos de uno equivalen a bits invertidos, por lo que lo lógico sería utilizar inversores. El único problema es que los inversores siempre me van a invertir los bits y eso, aunque me sirve para la resta, no me va a servir para la suma. Los inversores sólo se deben activar cuando el interruptor del panel esté prendido, por lo que un mejor circuito para este caso sería el siguiente:
Recordemos que XOR da como resultado 1 si una de las entradas es también 1. Si las entradas son del mismo voltaje el resultado es cero. Si por ejemplo el interruptor del panel está apagado (suma), el input es igual que el output (1 con 0 da 1 y 0 con 0 da 0). Si por el contrario, prendemos el interruptor del panel (resta) tendremos que uno de las dos entradas de cada puerta lógica será 1, por lo que, si la otra entrada, que son los inputs que nosotros demos, es 1, el resultado será cero y si es cero, el resultado será uno por el efecto que tiene la puerta XOR.
Ahora podemos ensamblar estos XOR en una caja llamada complemento de uno y unirla a la caja del sumador y una puerta XOR adicional para que quede lo siguiente:
Notar que hay tres entradas llamadas "SUB" que representan el interruptor de si sumamos (0) o si restamos (1). Si el interruptor está prendido, los inputs B serán invertidos al complemento de uno antes de entrar al sumador (en caso contrario, el sumador recibe el mismo input en B). Además, agregamos un uno al resultado configurando el CI (bit de carga) a uno. En el caso del Overflow/Underflow si estamos sumando (SUB no tiene corriente) la ampolleta se prenderá si CO o el bit de carga de salida es uno (el resultado es mayor a 255). En caso contrario, si estamos restando, el SUB será 1. La ampolleta Overflow/Underflow se prenderá en caso que el minuendo (inputs A) sea menor al sustraendo (inputs B), dado que CO será cero en este caso. Recordar que esta máquina no está diseñada para mostrar números negativos.
Uno podría pensar a estas alturas cómo representar un número negativo en binario. Quizás podamos hacer lo mismo que en los números decimales anteponiendo un menos en el número, como 1001101 y -1001101. Aunque podemos hacer eso, la idea es que podamos representar todo con números binarios. Podríamos también usar un bit adicional para representar los números negativos y 0 para los positivos, pero aunque funcione no servirá para hacer aritmética.
La idea es poder representar números negativos de tal forma que cuando lo sumemos no restemos con un número negativo o positivo lleguemos al resultado correcto. Además, como representamos todo con binario debemos decidir de antemano si solo trabajaremos con números positivos o negativos y positivos. Veamos un ejemplo para que quede más claro.
Asumamos que tengo una cuenta corriente en un banco dónde nunca tengo más de $500 en la cuenta. El Banco nos da también una línea de crédito con un límite de $500. Esto quiere decir que en nuestra cuenta corriente podemos tener entre -$500 y $499 dólares. Asumamos también que nunca depositamos más allá de $500 y que siempre tratamos con dólares y nunca con centavos.
En el ejemplo tenemos 1000 números entre -500 y 499. Esta restricción implica que podemos utilizar números de tres dígitos y sin signo negativo para representar todos los números que necesitamos. Los números que van desde 500 a 999 pueden representar los números negativos mientras que del 0 al 499 los positivos. De esta manera para referirse al -500 utilizamos el 500, para el -499 utilizamos el 501, para el -498 utilizamos el 502 y así consecutivamente hasta llegar al 999 que representa el -1. Para referirnos a los números positivos tenemos que el 0 es 000, el 1 es 001, el 2 es 002 y así consecutivamente hasta llegar al 499 que representa el 499. En resumen, la secuencia de -500, -499, -498, -4, -3, -2, -1, 0, 1, 2, 3, 4, 497, 498, 499 se puede también escribir así 500, 501, 502, 996, 997, 998, 999, 000, 001, 002, 003, 004, …, 497, 498, 499.
Nótese que este orden es una especie de círculo. El número más pequeño (500) es la continuación del más grande (499) y el número 999 (o -1) es uno menor que el cero. Con esto ya podemos hacer aritmética, por ejemplo, si añadimos 1 a 999 normalmente obtendremos 1000, pero como estamos trabajando con tres dígitos el resultado es cero. De la misma manera, si restamos 500 en uno, llegamos al número más alto que es 499, formando un círculo.
Este tipo de notación es llamada complemento de 10. Para convertir un número de 3 dígitos a números negativos restamos 999 y sumamos uno. Por ejemplo el 501 representa el -498, entonces 500 - 999 + 1 = -498. Con el complemento de 10 ya no necesitaremos restar, dado que todo sería suma. Por ejemplo, si tenemos 143 en nuestra cuenta corriente y tenemos un gasto de 78, debemos realizar la operación 143 - 78 para llegar al resultado. El -78 se puede representar como positivo con el complemento de 10 lo que equivale a 999 - 78 + 1 = 922. Por lo que nuestro balance en la cuenta sería de 143 + 922 = 1.065 que equivale a, ignorando el overflow, a 65, dado que solo estamos trabajando con números de tres dígitos.
El sistema equivalente en binario se llama complemento de dos. Imaginemos que estamos trabajando con números que van desde los 00000000 a 11111111. Si queremos representar con este rango números negativos, en vez de ir de 0 a 255 este rango iría de -128 a 127.
|
Binario |
Decimal |
|
10000000 |
-128 |
|
10000001 |
-127 |
|
10000010 |
-126 |
|
… |
|
|
11111110 |
-2 |
|
11111111 |
-1 |
|
00000000 |
0 |
|
00000001 |
1 |
|
00000010 |
2 |
|
… |
|
|
01111100 |
125 |
|
01111110 |
126 |
|
01111111 |
127 |
Notemos que el primer dígito de los negativos es un uno y el de los positivos es un cero. A esto se le conoce como el signo bit. Para calcular el complemento de dos seguimos los mismos pasos que para los números decimales, vale decir, calculamos el complemento de uno y sumamos uno. Esto es equivalente a invertir todos los números y añadir 1. Por ejemplo, el número decimal 126 equivale a 01111110 y para representar el -126 primero debemos calcular el complemento de dos invirtiendo el número (01111110 a 10000001 ) y añadiendo un uno dando como resultado el número 10000010 que equivale a -126. Podemos verificar esto en la tabla. Si queremos hacer el efecto inverso invertimos los bits y sumamos uno al resultado.
Este sistema nos permite utilizar números positivos y negativos sin utilizar el signo negativo para los números negativos. También nos permite sumar números positivos y negativos utilizando las reglas de la adición. Por ejemplo, si sumamos -127 con 124 el resultado es -3. Esto en binario sería 10000001 + 011111100 que resulta en 11111101 o -3. Fijémonos que el resultado 11111101 tiene "Overflow/Underflow" por el signo bit.
En general, el resultado de una adición que involucra números positivos y negativos es invalido si los signos bits de los dos operandos son el mismo, pero el signo bit resultante es diferente. Por ejemplo:
-
Si sumamos el número 125 por sí mismo, vale decir, 01111101 obtendremos el resultado 11111010 que es -6.
-
Si sumamos el número -125 por sí mismo, vale decir, 10000011 obtendremos 100000110. En este caso, hay 9 bits y como estamos trabajando con 8, el dígito 1 que está en la posición más significativa se ignora, por lo que el resultado es 6.
En estos dos ejemplos sumamos operandos que tienen un signo bit, pero el resultado es otro signo bit. Por tanto, el resultado sería invalido.
Ahora conocemos dos formas de usar números binarios: para números solamente positivos de 0 a 255 y para números positivos y negativos de -128 a 127. El número persé no nos indicará si el número está expresado solo en rango positivo o como rango positivo y negativo. Por ejemplo, el número 10110110 puede ser tanto un -74, como un 182. Ese es el problema fundamental de los bits: son solo unos y ceros, pero no nos dicen lo que expresan.
Feedback y Flip-Flops
La electricidad hace que las cosas estén en movimiento, como un parlante que comprime y descomprime el aire gracias a la electricidad. Estas oscilaciones eléctricas pueden generar funcionamiento en varios artículos del hogar. El concepto de oscilamiento se puede apreciar también en los timbres o en las campanas.
Este circuito no es algo que hayamos visto antes, dado que no separa el input con el output, sino que forma un círculo. Si cerramos el interruptor, se genera electromagnetismo atrayendo el contacto flexible hacia la barra de hierro. Con esto, sin embargo, hace que el circuito se abra, por lo que el flujo eléctrico es interrumpido y el contacto flexible vuelve a su posición original, cerrando el circuito y nuevamente haciendo pasar la electricidad.
El contacto flexible sube y baja todo el tiempo mientras el interruptor esté cerrado. Esto funciona como un timbre, donde su vibración constante hace que genere un sonido.
Este circuito se parece mucho a un inversor, salvo que el inversor separa los input y output mientras que este nuevo circuito los une.
Es por esta similitud que el símbolo de este circuito incluye un inversor.
La gracia de este circuito es que la salida rápidamente se alterna entre 0 y 1 y es por esto que a este circuito se le conoce como oscilador. Este circuito no requiere intervención humana para abrir y cerrar interruptores lo que lo transforma en la piedra angular para la automatización. A pesar de que ahora no lo vemos tan útil, veremos más adelante como el oscilador es tan importante para la automatización.
Una forma de representar la oscilación entre ceros y unos que realiza este circuito es con un gráfico donde el eje X es el tiempo y el eje Y es la salida que puede ser cero o uno.
En la representación gráfica podemos ver lo que se entiende por un ciclo. Formalmente un ciclo es definido como un intervalo de tiempo que abarca el cambio de un estado a otro hasta que el siguiente cambio sea el mismo que el estado del punto inicial, vale decir, un ciclo en este caso es el cambio entre cero y uno. El siguiente cambio comienza con cero, por lo que ya sería parte del siguiente ciclo. El tiempo requerido para completar un ciclo se le conoce como el periodo de un oscilador.
La frecuencia de un oscilador es uno dividido por su periodo. Supongamos que el periodo de un oscilador es de 0,05 segundos, vale decir, en 0,05 segundos el oscilador completa un ciclo. La frecuencia por este oscilador estaría dada por 1 / 0,05 = 20 ciclos por segundo o más apropiadamente 20 hertz (el nombre hertz fue gracias al trabajo de Heinrich Rudolph Hertz).
Ahora nos moveremos a otro circuito que utiliza dos puertas NOR, dos interruptores y una ampolleta. Recordar que NOR solo da 1 si ambas entradas son ceros.
Notar la forma peculiar en cómo están conectadas las puertas lógicas: la salida del primer NOR es una entrada al segundo NOR y a su vez la salida del segundo NOR es una de las entradas al primer NOR. Esto es como si fuese un feedback y al igual que el oscilador, la salida se conecta con la entrada.
La única corriente que fluye de este circuito corresponde a la salida del primer NOR dado que ambos interruptores están abiertos. La magia ocurre cuando cerramos el primer interruptor y lo volvemos a abrir.
A pesar de que volvimos a abrir el interruptor dejándolo igual a su estado original, la ampolleta está prendida en vez de apagada. Todos los circuitos que hemos visto hasta ahora el estado de la salida depende plenamente de las entradas, pero no parece ser el caso con este circuito. En este punto, podemos cerrar y abrir el interruptor de arriba cuántas veces queramos, pero la ampolleta seguirá prendida.
Ahora, cerremos el interruptor de abajo. La entrada del segundo NOR pasa de 0 a 1, haciendo que la salida sea 0. Las entradas del primer NOR son ceros haciendo que su salida sea uno. Volvemos al estado original.
Podemos abrir y cerrar el circuito de abajo varias veces y seguiremos viendo la ampolleta apagada. Entonces, en resumen:
-
Cerrando el interruptor de arriba hace que la ampolleta se prenda y seguirá prendida aunque se vuelva a abrir.
-
Cerrando el interruptor de abajo hace que la ampolleta se apague y seguirá apagada aunque se vuelva a abrir.
Este circuito se llama flip-flop y es obra del trabajo del físico William Henry Eccles y F.W. Jordan.
Un circuito flip-flop retiene información, pero ¿qué información? Simplemente recuerda cuál fue el último interruptor que se cerró. Si por casualidad ves un flip-flop que esté prendido puedes estar seguro que el último interruptor en cerrarse fue el de arriba y por el contrario, si lo ves apagado, el último interruptor en cerrarse fue el de abajo. Es como si fuera un balancín donde, dependiendo de su posición, sabrás qué lado fue el último en ser presionado.
Los flip-flops son vitales, dado que agregan memoria a un circuito. La memoria la necesitamos, por ejemplo, para contar, porque necesitamos recordar el número que viene antes para saber el número que le sigue. De hecho, para hacer un circuito que pueda contar necesitamos flip-flops.
Hay diferentes tipos de flip-flops, como el que vimos recién que es el más simple y se llama flip-flop R-S (Reset-Set). Este circuito es comúnmente dibujado en forma de circuito (imagen de la izquierda) o en forma de caja (imagen de la derecha):
Las salidas están etiquetadas como Q y Q barra. Estas son opuestas, es decir, cuando Q es 1, Q barra es 0 y viceversa. Las entradas están etiquetadas como R y S (reset y set). Podemos pensar en estos verbos como "activar (set) Q a 1" o "restablecer (reset) Q a cero" (solo pensar que con S prendemos la ampolleta y con R la apagamos). Cuando ambos interruptores están abiertos, la salida indica cuál Q fue últimamente activado o restablecido. Podemos resumir la información de entradas y salidas de este circuito en la siguiente tabla:
|
Entradas |
Salidas |
||
|
S |
R |
Q |
Q barra |
|
1 |
0 |
1 |
0 |
|
0 |
1 |
0 |
1 |
|
0 |
0 |
Q |
Q barra |
|
1 |
1 |
Ilegal |
Ilegal |
Esta tabla se llama tabla lógica y muestra las salidas que resultan de una combinación particular de entradas. Las salidas que están indicadas como Q y Q barra significa que el voltaje sigue siendo el mismo antes que las entradas S y R fueran cero. La última fila es cuando tanto R y S son unos. El resultado es "ilegal" lo que no quiere decir que vas a ir a la cárcel si cierras los dos interruptores, sino que cuando ambas entradas son uno, ambas salidas son ceros, lo que viola la noción que Q barra debe ser lo opuesto a Q, por lo que es mejor evitar que ambos interruptores están cerrados.
Los flip-flop R-S son interesantes, porque pueden recordar cuál fue el último input en dar voltaje. Lo que sería más útil, sin embargo, es crear un circuito que recuerde si una señal en particular fue cero o uno en un punto del tiempo. Para construir algo así primero pensemos en dos entradas llamadas Data y Hold That Bit que sería el equivalente a "guarda ese pensamiento". Normalmente, Hold That Bit es cero y Data puede ir variando entre 0 y 1. Cuando Hold That Bit sea uno, la salida refleja el valor de Data en ese momento y Data ya no tendrá efecto en el circuito. Cuando Hold That Bit sea cero nuevamente, el circuito recordará el último valor de Data. En resumen, Hold That Bit cuando pasa de cero a uno recordaremos el valor de Data en ese momento de transición.
|
Entradas |
Salidas |
|
|
Data |
Hold That Bit |
Q |
|
0 |
1 |
0 |
|
1 |
1 |
1 |
|
0 |
0 |
Q |
|
1 |
0 |
Q |
En los primeros dos casos cuando Hold That Bit es uno, la salida es la misma que Data, dado que recordamos ese valor. Cuando Hold That Bit es cero, la salida es la misma que era anteriormente, por lo que no importa si Data es 0 o 1, dado que este circuito sólo recordará el último valor de Data cuando hicimos el cambio de 0 a 1 en Hold That Bit.
Implementar un Hold That Bit basado en el flip-flop R-S requiere que agreguemos dos puertas AND en las entradas del circuito.
La salida de una puerta AND es uno si ambas entradas son uno, por lo que la única forma que la corriente pase al flip-flop R-S es que Hold That Bit sea uno, dado que controla las dos entradas AND. Mientras Hold That Bit sea cero, la señal Set y Reset no tienen control sobre la salida. Cuando Hold That Bit es uno, el comportamiento de este circuito es el mismo que el de un flip-flop R-S.
A pesar del avance aún no hemos cumplido nuestro objetivo, dado que necesitamos tener dos entradas y no tres. Recordemos que es ilegal que set y reset sean uno y también que no tiene sentido que set y reset sean cero, porque la salida no cambiaría. Con eso en mente, set y reset deben ser opuestos. Podríamos poner una entrada llamada Data que controle a set y reset. Cuando Data esté apagado, reset es uno y si está prendido set es uno. Esto lo podemos lograr con un inversor.
En el diagrama anterior, ambas entradas son cero y Q barra es uno. Data no tiene efecto en la salida, porque Hold That Bit es cero, pero cuando sea uno y Data esté prendida, la salida Q se va a iluminar y Q barra se apagará.
Ahora el circuito recuerda el valor de Data cuando Hold That Bit fue uno por última vez. Ahora que Hold That Bit es cero no importa cuál es el valor de Data en el circuito. Imagine que usted ahora apaga el interruptor de Data. La entrada de Reset sería uno, pero faltaría la otra entrada que la controla Hold That Bit.
Este circuito se llama level-triggered D-type flip-flop. El D es por Data y level-triggered significa que el flip-flop guarda la información de Data cuando la entrada de Hold That Bit está en un estado en particular, en este caso uno. Normalmente a Hold That Bit no se le conoce por ese nombre si no como Clock, dado que tiene atributos de un reloj al oscilar entre ceros y unos.
A este circuito también se le conoce como level-triggered D-type latch (o latch para abreviar), porque el circuito encierra ese bit y lo mantiene para uso futuro. También el circuito puede hacer referencia a 1-bit de memoria. Podemos ensamblar varios de estos circuitos para guardar varios bits.
Guardar varios bits puede ser muy útil, por ejemplo, si queremos sumar tres números con la máquina que construimos anteriormente. Para ello, debemos primero escribir con los interruptores los números que queremos sumar en las entradas A y B respectivamente y guardar el resultado en un latch para luego escribir ese resultado en uno de esos dos interruptores (A o B). Finalmente, escribimos en la entrada que falta el tercer número y lo sumamos con el número que nos dió como resultado de la suma de los dos primeros. Es hora de construir algo que pueda sumar tres o más números a la vez.
Comencemos con ensamblar 8 latches en una caja. La entrada Clock (abreviado Clk) está conectada a cada uno de los 8 latches.
Aquí podemos guardar 8 bits de una. Cuando el reloj sea uno, los 8 bits de la entrada D son transferidos a Q y cuando el reloj vuelve a cero los valores de cada Q se mantienen hasta que volvamos a cerrar el interruptor del reloj. La caja del 8-Bit Latch puede ser también dibujada como una caja más compacta como la imagen de la derecha.
En la máquina sumadora, ignorando el circuito para restar números, tenemos que las 8 entradas A y B están conectadas a interruptores, el CI (Carry In o bit de carga inicial) está conectada a la tierra y las 8 salidas y el CO (Carry Out o bit de carga final) está conectada a las ampolletas. Ahora conectamos las salidas de la máquina sumadora a las ampolletas y a las entradas D del 8-Bit Latch. El interruptor llamado "Save" es el reloj.
Hay un circuito que no hemos visto aún llamado 2-to-1 Selector. Este circuito nos permite elegir qué entrada queremos para generar la salida (por eso el nombre 2-to-1). En este caso, la salida de este circuito será la entrada que viene desde los interruptores que nosotros configuremos manualmente o de la salida del Latch. Si cerramos el interruptor, el sumador recibe el número del Latch a la entrada B y si lo abrimos, recibe el número que nosotros configuramos manualmente con los interruptores.
Si Select es uno, el resultado será lo mismo que la entrada B. Esto es porque la salida de A será cero independiente de cuánto sea la entrada A y por tanto la salida ahora solo dependería de si B tiene o no voltaje. Si el Select es cero, la salida será igual a la entrada de A, porque ahora la salida de B será cero. Para que nuestra máquina funcione debemos tener ocho de estos 1-bit selectors y todos deben ir conectados entre sí.
El problema con el circuito que suma tres números es que el Carry Out funciona para la primera suma si se produce un número mayor a 8 bits, pero cuando agregamos el tercer número podríamos tener un número mayor a 9 bits, por lo que necesitamos más ampolletas. Una solución es hacer que el sumador, el latch y el selector soporten 16 bits o al menos el número máximo que puede soportar la suma de 3 números de 8 bits, pero ya solucionaremos ese problema más adelante.
Podríamos prescindir del selector para esta nueva máquina, pero primero modifiquemos el level-triggered D-type flip-flop añadiendo una puerta OR y una entrada llamada "Clear" que apaga todas las salidas Q (pasan a ser cero) cuando Clear es uno.
Quizás te estés preguntando porque necesitamos algo como Clear si podemos poner las entradas Data como cero y poner el reloj como uno para que los Q sean todos ceros. Esto es básicamente porque no podremos controlar lo que pasará con las entradas Data. La idea es llegar a algo que pueda funcionar por sí misma y por eso conectaremos la salida de los latches (Q) a la entrada B del sumador.
Ahora "Add" controla el reloj para guardar la información que viene del sumador.
Con esta máquina ya somos capaces de poder sumar varios números. Los pasos para usar esta máquina son los siguientes:
-
Cerramos el interruptor Clear para apagar todas las ampolletas.
-
Configuramos los interruptores en la entrada A anotando el número que queremos sumar.
-
Cerramos Add para guardar la información en el Latch y luego lo abrimos. Las ampolletas reflejarán el número que introdujimos en los interruptores.
-
Anotamos el segundo número que queremos sumar con los interruptores y cerramos "Add" y abrimos. El resultado se verá reflejado en las ampolletas.
Este ciclo lo podemos repetir una y otra vez hasta que hayamos sumado todos los números que queramos. Mencionamos antes que este circuito es level-triggered lo que significa que el Add o reloj va cambiando su entrada alternándose entre 0 y 1 para guardar información en el Latch. Cualquier cambio de Data mientras el reloj sea uno se verá reflejado en los valores de Q y Q barra.
Hay aplicaciones en que un reloj level-triggered no nos servirá y en vez de eso preferimos un reloj edge-triggered. La diferencia entre ambos está en que edge-triggered es que causa que la salida cambie cuando el reloj hace una transición entre 0 y 1. Bajo este concepto podemos construir un edge-triggered D-type flip-flop que es construido con dos R-S flip-flops conectados entre sí.
La idea central es que el reloj pueda controlar el primer R-S flip-flop y el segundo (observe que el reloj tiene dos cables, cada uno conectado a un flip-flop), pero notemos que el reloj está invertido en el primer flip-flop. Esto significa que Data guardará información cuando el reloj sea cero en el primer estado (lo llamaremos así de aquí en adelante) y uno en el segundo, dado que en el segundo no hay un inversor. Esto genera que la entrada Data sea guardada cuando el reloj cambia de cero a uno. Veamos como funciona este circuito cuando juguemos con las entradas.
Podemos ver el proceso de cambio de las entradas desde la esquina izquierda de arriba a la esquina derecha de abajo. Cuando cambiamos Data a uno con el reloj en cero cambiamos el primer estado por el reloj invertido, pero no el segundo. Si cambiamos ahora el reloj a uno cambiamos el segundo estado, pero no el primero. La diferencia es que Data puede cambiar su entrada, por ejemplo a cero, sin afectar las salidas Q. Las salidas Q y Q barra solo pueden cambiar en el instante en que el reloj cambie de cero a uno.
Podemos resumir este comportamiento en la siguiente tabla, pero con dos nuevos símbolos: una flecha hacia arriba (↑) que indica que la señal está haciendo una transición entre 0 y 1 y una X que indica que no importa cuál sea la entrada. Las salidas Q y Q barra
|
Entradas |
Salidas |
||
|
Data |
Reloj |
Q |
Q barra |
|
0 |
↑ |
0 |
1 |
|
1 |
↑ |
1 |
0 |
|
X |
0 |
Q |
Q barra |
La flecha también indica que la salida Q se convierte en lo mismo que la entrada Data cuando el reloj hace una transición entre 0 y 1. A esto se le llama una transición positiva de la señal del reloj. Una transición negativa sería cuando el reloj realiza una transición de uno a cero. El símbolo de este circuito es el siguiente:
El pequeño triángulo en la entrada del reloj indica que este circuito es edge-triggered. Este circuito lo podemos conectar a un oscilador en la entrada del reloj y conectar el Q barra a la entrada Data obteniendo algo así:
El hecho que esté conectado de esta forma, donde la salida de Q barra es la entrada de Data puede suponer un problema, porque los demás relés pueden no seguirle el ritmo a la velocidad del oscilador. Para evitar este problema asumimos que los relés usados para el oscilador son mucho más lentos que los relés usados para el resto del circuito.
El comportamiento de este circuito puede llegar a ser difícil de entender, por lo que, en resumidas cuentas, cada vez que el reloj cambia de cero a uno, la entrada Q cambia de cero a uno o de uno a cero debido a que Q barra es el input de la entrada Data. Esta situación se puede ver más claramente si miramos un diagrama de tiempo.
Cuando el reloj cambia de cero a uno, el valor de D (que es el mismo que Q barra) es transferido a Q y esto hará que Q barra y D cambien para la siguiente transición del reloj de cero a uno.
Si la frecuencia del oscilador es de 20 Hz (es decir, 20 ciclos por segundo), la frecuencia de la salida Q es la mitad, es decir, 10 Hz. Por esta razón, un circuito de este tipo, en el que la salida Q se encamina de nuevo a la entrada Data de un flip-flop, también se conoce como divisor de frecuencia.
Podemos conectar distintos divisores de frecuencia entre sí haciendo que la salida de un divisor de frecuencia sea la entrada del reloj de otro divisor de frecuencia. En este diagrama conectamos tres de ellos:
Recordemos que este circuito divide las frecuencias en las salidas, vale decir, el reloj tiene el doble de ciclos que la salida Q. Si conectamos un circuito con otro de esta manera Q3 tiene la mitad de hertz que Q2 y a su vez Q2 tiene la mitad de hertz que Q1. Veamos cómo se ve esto en un diagrama de tiempo.
Si este diagrama de tiempo lo llenamos con unos y ceros vamos a notar algo interesante. Si miramos la imagen de la derecha, la giramos en 90° al sentido del reloj y leemos los 4-bits de cada fila notaremos que este circuito no hace nada menos que contar números en binario y entre más flip-flops agreguemos, más grande será el conteo. A este circuito que conecta varios edge-triggered D-type flip-flops se le conoce como ripple counter, porque la salida de cada flip-flop se convierte en el reloj del siguiente. Algunos contadores más sofisticados son sincronizados, es decir, todas las salidas cambian a la vez. Conectemos 8 de estos flip-flops para hacer un ripple counter de 8-bits que cuenta hasta 8-bits.
El Q7 es el dígito más significativo y Q0 es el menos significativo, por lo que será este último el primero en cambiar. El siguiente diagrama de tiempo muestra las 8 salidas juntas y como van cambiando con las transiciones del reloj:
En cada transición del reloj algunas salidas Q cambian mientras otras no, pero en conjunto reflejan números binarios crecientes. Podemos medir la frecuencia del oscilador contando el tiempo que demora el contador en darse la vuelta, es decir, partiendo de cero hasta que llegue a cero nuevamente (después del 11111111 el contador pasa a ser 00000000). Luego, dividimos la cantidad de ciclos por ese tiempo, por ejemplo, si el tiempo en darse la vuelta fue de 10 segundos, la frecuencia del oscilador sería de 256 / 10= 25,6 Hertz. Los ciclos del contador son de 256, porque de 00000000 a 11111111 hay 256 números.
A medida que un flip-flop va adquiriendo características, también va ganando mayor complejidad. Podemos por ejemplo incluir un interruptor Preset y Clear en el edge-triggered D-type flip-flop para obtener el siguiente circuito:
Los interruptores Preset y Clear reemplazan al reloj y a la entrada Data, respectivamente. Normalmente las dos entradas son cero. Cuando Preset es uno, Q se convierte en uno y Q barra en cero. Cuando Clear es uno, Q se convierte en cero y Q barra en uno. Este comportamiento es como el Set y el Reset del R-S flip-flop por lo que Preset y Clear no deberían ser uno al mismo tiempo.
El comportamiento se puede resumir en la siguiente tabla lógica:
|
Entradas |
Salidas |
||||
|
Preset |
Reloj |
D |
Reloj |
Q |
Q barra |
|
1 |
0 |
X |
X |
1 |
0 |
|
0 |
1 |
X |
X |
0 |
1 |
|
0 |
0 |
0 |
↑ |
0 |
1 |
|
0 |
0 |
1 |
↑ |
1 |
0 |
|
0 |
0 |
X |
0 |
Q |
Q barra |
Ya hemos visto relés que construidos de cierta manera pueden sumar, restar y contar. Tenemos aún mucho por descubrir, pero antes tomaremos un descanso en construir cosas para ver otra base numérica sumamente importante.
Bytes y Hex
Las dos máquinas sumadoras construidas anteriormente se mueven con datos de 8-bits de largo, dónde los interruptores son de 8-bits, los latches son de 8-bits, las ampolletas son 8 y así con los demás componentes del circuito, pero ¿por qué 8 y no 6, 9 o 10?
La verdad es que no hay razón para haber construido la máquina sumadora en 8-bits, pero es un buen tamaño de información. IBM comenzó a utilizar circuitos de 8-bits debido a la facilidad para almacenar números en un formato conocido como BCD (ya veremos lo que es más adelante) y ellos acuñaron el término byte alrededor de 1956 para referirse a 8-bits de información. La mitad de un byte se llama nibble (4-bits), pero no está ni cerca de ser un término tan común como el byte.
Los bytes son ideales para almacenar texto para la gran mayoría de los lenguajes del mundo que pueden ser representados por 256 caracteres. También es ideal para representar sombra de grises en fotos en blanco y negro, porque el ojo humano puede diferenciar aproximadamente 256 formas de grises.
Dado que los bytes aparecen mucho en los componentes de un computador es conveniente referirse a ellos de la manera más breve posible. Si nos referimos a ellos con números binarios es explícito, pero muy largos a medida que abarcamos más información. Podríamos entonces representar la información con números decimales, pero sería molesto tener que convertir de binario a decimal y viceversa. Si probamos con octales quizás sea mejor idea por que transformar un binario a octal es mucho más intuitivo, lo único que tenemos que hacer es tener a mano una tabla de 3-bits con su equivalente en octales y realizar una pequeña técnica.
|
Binario |
Octal |
|
000 |
0 |
|
001 |
1 |
|
010 |
2 |
|
011 |
3 |
|
100 |
4 |
|
101 |
5 |
|
110 |
6 |
|
111 |
7 |
Si tenemos un número binario cómo 10110110, separamos en grupos de 3 comenzando con el dígito de más a la derecha.
Recordando la tabla y reemplazando cada grupo por su equivalente en octal obtendremos el valor octal del número binario 10110110. La representación octal es más breve que la representación decimal, pero tiene un pequeño problema: el grupo de más a la derecha en el ejemplo tiene dos bits en vez de tres como los otros dos grupos. Si por ejemplo tenemos 16-bits de información, podemos dividirla en dos para tener dos grupos de un byte de información cada uno. Si representamos en octales los 16-bits y también los dos grupos de un byte cada uno, no nos dará el mismo resultado.
Para que las representaciones de los valores de muchos bytes sean coherentes con las representaciones de los bytes individuales, tenemos que utilizar un sistema en el que cada byte se divide en un número igual de bits. Eso significa que tenemos que dividir cada byte en cuatro grupos de dos bits cada uno (base de 4) o dos grupos de cuatro bits cada uno (base de 16).
Un sistema en base a 16 es llamado sistema hexadecimal y no es nada similar a los sistemas que hemos visto hasta ahora, dado que, todos los sistemas que hemos visto son menores a un sistema base de 10, pero el sistema hexadecimal es mayor. Esto significa que necesitaremos más dígitos de los que tiene el sistema decimal. Recordemos que el sistema decimal tiene 10 dígitos (del 0 al 9), por lo que en el sistema hexadecimal necesitaríamos 16 dígitos, es decir, 6 dígitos más. Estos 6 digitos son las primeras 6 letras del abecedario, es decir, ABCDEF.
Cada byte puede ser expresado por dos dígitos hexadecimales (16 / 8 = 2). En otras palabras cada dígito hexadecimal es equivalente a 4 bits o un nibble. En la siguiente tabla podremos ver las equivalencias que existen entre el sistema binario, decimal y hexadecimal en 4-bits.
|
Binario |
Hexadecimal |
Decimal |
Binario |
Hexadecimal |
Decimal |
|
|
0000 |
0 |
0 |
1000 |
8 |
8 |
|
|
0001 |
1 |
1 |
1001 |
9 |
9 |
|
|
0010 |
2 |
2 |
1010 |
A |
10 |
|
|
0011 |
3 |
3 |
1011 |
B |
11 |
|
|
0100 |
4 |
4 |
1100 |
C |
12 |
|
|
0101 |
5 |
5 |
1101 |
D |
13 |
|
|
0110 |
6 |
6 |
1110 |
E |
14 |
|
|
0111 |
7 |
7 |
1111 |
F |
15 |
La palabra hexadecimal se puede abreviar a hex y cuando queramos expresar un número en hexadecimal podemos utilizar esta abreviatura al lado derecho del número. Por ejemplo, si queremos expresar el número decimal 182, lo podemos escribir también en hexadecimal como B6hex o simplemente B6h.
Al igual que los otros sistemas que hemos visto, cada posición de un número que es hexadecimal corresponde a una potencia de 16. Por tanto, para convertir un número hexadecimal, por ejemplo, el 9A48Ch a decimal, debemos multiplicar cada dígito por su potencia de 16 correspondiente dependiendo de su posición.
9A48Ch = 9x16^4 + Ax16^3 + 4 x 16^2 + 8 x 16^1 + Cx16^0
Convertimos los dígitos A y C a su equivalente decimal, obteniendo lo siguiente: 9x16^4 + 10x16^3 + 4x16^2 + 8x16^1 + 12x16^0 = 631.948.
También podríamos usar una plantilla estándar para poder convertir números hexadecimales de 4 dígitos a números decimales. Por ejemplo, convirtamos el número 79AC a decimal:
Para hacer el trabajo inverso, es decir, convertir un número decimal a hexadecimal debemos dividir tal y como lo hemos estado haciendo con los demás sistemas numéricos. Si tenemos por ejemplo el número 182 en decimal, debemos dividirlo en 16 para obtener 11 (que es equivalente a B en hexadecimal) con un resto de 6, por lo que el número en hexadecimal es B6h.
También podemos utilizar una plantilla para dividir y obtener la conversión de un número decimal a hexadecimal. Convirtamos por ejemplo el número 31.148.
Reemplazamos el 10 por A y el 12 por C para obtener el número 79AC. El procedimiento es que el resultado va en el cuadro de abajo como entero y el resto va en el cuadrado de la derecha.
Otra aproximación de convertir números decimales es separar el número en dos bytes dividiendo por 256 y luego, por cada byte agrupado dividimos por 16. Por ejemplo, aquí está la conversión para el 51.966.
Los números 12, 10, 15 y 14 representan el número CAFE que parece más una palabra que un número.
Al igual que los demás sistemas numéricos, podemos ayudarnos en una tabla de suma hexadecimal para realizar sumas ( https://www.ecured.cu/Sistema_hexadecimal ).
Aquí también utilizamos cargas de una posición a otra igual que cualquier otra suma. También podemos representar números negativos con números hexadecimales. Si el primer dígito del número comienza con 8, 9, A, B, C, D, E o F es un número negativo, porque en su expresión binaria, todos estos números comienzan con uno. Por ejemplo, el número 99h puede representar el 153 (si estamos trabajando solo con positivos) o el -103 (si trabajamos con positivos y negativos).
Los números hexadecimales son útiles porque son utilizados para expresar la ubicación de los datos en la memoria.
Un ensamblaje de memoria
Nuestra memoria es frágil y probablemente la escritura se inventó para compensar el fallo de la memoria humana. Escribimos y más tarde leemos. Guardamos y luego recuperamos. Almacenamos y más tarde accedemos. Cada vez que almacenamos información, utilizamos diferentes tipos de memoria, como el papel para almacenar información textual y la cinta magnética para almacenar música y películas.
Los relés también son capaces de almacenar información y un flip-flop, como vimos anteriormente, es capaz de guardar 1 bit. Tomamos prestado el level-triggered D-type flip-flop que hicimos anteriormente y renombramos las entradas y las salidas. Ahora el reloj se llamará "Write" y la salida Q se llamará "Data Out".
Llamaremos al reloj Write porque va más acorde a nuestro propósito haciendo como si estuviésemos escribiendo información al circuito. Recordar que cuando Write se cierra o es uno, Q toma la información de Data In y si cambiamos Write a cero, no importa cuántas veces cambiamos Data In, dado que el Data Out va a reflejar la información de Data In cuando Write fue uno por última vez. Recordar que este circuito se llama latch y fácilmente podremos unir 8 latch para guardar 1 byte de información.
Hay otra manera de ensamblar 8 latches que no es tan intuitiva como esta. Supongamos que tenemos una entrada Data y una salida Data igual que el circuito que ya tenemos. Sin embargo, queremos tener la opción de guardar la información en el Data In de 8 maneras distintas durante el día o quizás de 8 maneras distintas en el próximo minuto y queremos también tener la opción de revisar esos ocho valores con solo mirar la salida Data o Data Out. En otras palabras, en vez de guardar valores de 8-bits en un latch de 8-bits, queremos guardar ocho bits por separado.
Pensemos por un momento en una cajonera o cómoda, donde cada cajón contiene 1-bit de información. Nosotros seleccionamos qué cajón abrir para ver cuál es su contenido. Ahora pasemos esta cajonera al circuito: cada cajón es un latch y para decidir ver que contiene un latch debo usar los interruptores de entrada para seleccionar un cajón y ver su contenido. Esa es la razón por la que necesitamos una ampolleta, porque, como cada cajón contiene un bit, la ampolleta nos muestra la información de ese cajón en particular, seleccionado por los interruptores.
Si queremos seleccionar un latch entre ocho (o un cajón de una cómoda de 8 cajones) necesitaremos 3 interruptores, porque tres interruptores son suficientes para representar 8 valores (000, 001, 010, 011, 100, 101, 110, 111).
En el diagrama tenemos la ampolleta, los ocho latch y los tres interruptores para decidir qué latch queremos ver. El "What Is This?" es un circuito que debe seleccionar cuál latch llevar al output a través de cómo estén configurados los interruptores de entrada. Este tipo de comportamiento de seleccionar una entrada entre varias ya lo habíamos visto anteriormente, aunque no con tantas entradas como esta. Cuando creamos una máquina que sumará más de dos números creamos el circuito 2-Line-to-1-Line Selector para seleccionar la entrada que viene del latch o la entrada que viene de los interruptores de entrada que nosotros mismos debemos configurar. En este caso, lo que necesitamos es un 8-Line-to-1-Line Selector.
Este circuito tiene 8 entradas Data mostradas arriba y 3 entradas Select a la izquierda. Los interruptores de la izquierda eligen cuál de las entradas Data aparecerá en la salida. Por ejemplo, si la entrada Select es 000, la salida es la misma que D0, si es 101, la salida es D5. Veamos una tabla lógica para saber qué Data seleccionamos con Select.
|
Entradas |
Salidas |
||
|
S2 |
S1 |
S0 |
Q |
|
0 |
0 |
0 |
D0 |
|
0 |
0 |
1 |
D1 |
|
0 |
1 |
0 |
D2 |
|
0 |
1 |
1 |
D3 |
|
1 |
0 |
0 |
D4 |
|
1 |
0 |
1 |
D5 |
|
1 |
1 |
0 |
D6 |
|
1 |
1 |
1 |
D7 |
Un 8-to-1 Selector es construido con tres inversores, ocho puertas AND con cuatro entradas cada una y una puerta OR que recibe 8 entradas.
Si seguimos la corriente configurando los interruptores de entrada veremos que el circuito si funciona. Ahora solo basta con conectar el 8-to-1 Selector con los latches para que, con los interruptores podamos elegir qué latch queremos ver en la ampolleta. Sin embargo, aún no hemos terminado. Hemos decidido que hacer del lado de la salida, pero ahora nos faltaría desarrollar el lado de la entrada.
El lado de la entrada contiene los Data In y el Write para guardar la información en la salida Q, es decir, son los latches que vimos anteriormente. En estos latches del lado de la entrada podemos conectar las entradas Data conjuntamente, pero no podemos conectar ocho entradas Write, porque no podremos escribir en cada latch individualmente. Debemos tener entonces una entrada Write que sea encaminado a solo un latch dentro de los ocho.
Para lograr esto necesitamos otro circuito que se parece al 8-to-Selector, pero que haga lo contrario: en vez de seleccionar un cajón para ver la información dentro de él, necesitamos incluir información en un cajón a nuestra elección. Este tipo de comportamiento es lo que hace el circuito 3-to-8-Decoder. Es 3 porque tenemos tres interruptores para seleccionar en qué cajón queremos guardar información y 8 porque son 8 cajones en total. Anteriormente hemos visto un decodificador de datos cuando construimos el circuito para elegir el color de nuestro gato ideal. Es así como se ve un 3-to-8 Decoder:
Solo una de las salidas llamadas "O" va a recibir la información desde Data In. Notar que cada puerta AND tiene cuatro entradas donde una de ellas es la que viene del Data In y las otras tres vienen de S0, S1, S2 o de sus inversas. Si la compuerta AND recibe las tres corrientes de S0, S1 y S2, entonces en un O determinado se guardará la información de Data In.
En la siguiente tabla lógica podemos ver todas las combinaciones de entrada y qué ocurre con sus salidas:
|
Entrada |
Salidas |
||||||||
|
S(2) |
S(1) |
S(0) |
O(6) |
O(5) |
O(4) |
O(3) |
O(2) |
O(1) |
O(0) |
|
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Data |
|
0 |
0 |
1 |
0 |
0 |
0 |
0 |
0 |
Data |
0 |
|
0 |
1 |
0 |
0 |
0 |
0 |
0 |
Data |
0 |
0 |
|
0 |
1 |
1 |
0 |
0 |
0 |
Data |
0 |
0 |
0 |
|
1 |
0 |
0 |
0 |
0 |
Data |
0 |
0 |
0 |
0 |
|
1 |
0 |
1 |
0 |
Data |
0 |
0 |
0 |
0 |
0 |
|
1 |
1 |
1 |
Data |
0 |
0 |
0 |
0 |
0 |
0 |
Y el circuito completo con los 8 latches se ve así:
Notar que las entradas para el 3-to-8 Decoder y del 8-to-1 Selector son las mismas llamadas "Address" que es la dirección en la que vamos a guardar la información. Al ser las mismas entradas estamos guardando y accediendo a la información en una dirección específica.
Esta configuración de latches es conocida como memoria de lectura/escritura o más comúnmente como memoria de acceso aleatorio o RAM. La RAM que acabamos de construir guarda ocho valores de 1-bit de manera separada. La podemos simplificar en el siguiente diagrama:
Es llamada memoria de lectura/escritura, porque puede guardar nuevos valores en cada latch con el interruptor "Write" (escritura) y elegir en qué latch guardar la información con "Address", lo que significa que después puedo leer ese valor (lectura). Se le llama también memoria de acceso aleatorio, porque puedo escribir o leer desde uno de los ocho latches simplemente cambiando la entrada de Address. En contraste, otros tipos de memoria leen la información secuencialmente.
Al igual que los demás circuitos que hemos visto, podemos combinar varias RAM para que funcionen como una. Por ejemplo, podemos combinar dos RAM 8x1 en paralelo para transformarla en una RAM de 8 x 2, es decir, que pueda guardar 16 bits (8x2=16) o, en otras palabras, ocho entradas y dos salidas. A esta configuración también puede ser referida como un RAM array.
Podemos también combinar ambos RAM para que tengan una sola salida en vez de dos, utilizando un 1-to-2 Decoder para decidir en qué RAM guardaremos la información y un 2-to-1 Selector para decidir en qué RAM queremos tener acceso a la información.
El número de valores que una RAM puede guardar está directamente relacionado con el número de entradas Address. El RAM de 16 x 1 es en realidad de 4 Address y no 3, dado que si tuviéramos 3 podríamos acceder a máximo 2^3 = 8 latches y necesitamos uno más para acceder a 2^4 = 16 latches. Esto nos da cuenta que el número de valores guardados en una RAM = 2^número de entradas Address.
De esta manera, podemos crear RAMs mucho más grandes. Por ejemplo, si queremos guardar distintas sumas de nuestra máquina sumadora necesitaremos una RAM con ocho datos de entrada y de salida.
Esta RAM puede guardar 8196 bits organizados por 1024 valores de 8 bits cada uno. Hay 10 Address, porque 2^10 = 1024. En otras palabras, esta RAM es capaz de guardar 1024 bits de memoria, que es como si tuviéramos una cajonera con 1024 cajones, donde cada cajón guarda un byte de memoria.
Cuando empezamos a hablar de cantidades de bits muy grandes, podemos hacer la conversión para hablar de su equivalente en otra medida con cantidades más pequeñas. Cuando hablamos de 1024 bytes, estamos hablando de 1 kilobyte. El prefijo "kilo" es utilizado más en el sistema métrico, por ejemplo, 1 kilo son 1000 gramos, pero los kilobytes son 1024 bytes y no 1000 bytes. Esto es porque el sistema métrico está basado en el sistema de 10, por tanto 10^3 gramos = 1 kilo, mientras que los números binarios son en base de dos, por tanto 2^10 bytes = 1 kilobyte. Al igual que con las distancias, áreas, volumen o masa, la información también tiene su tabla de conversión.
|
Nombre |
Símbolo |
Potencia |
|
Byte |
b |
2^0 |
|
Kilobyte |
Kb |
2^10 |
|
Megabyte |
MB |
2^20 |
|
Gigabyte |
GB |
2^30 |
|
Terabyte |
TB |
2^40 |
|
Petabyte |
PB |
2^50 |
|
Exabyte |
EB |
2^60 |
|
Zettabyte |
ZB |
2^70 |
|
Yottabyte |
YB |
2^80 |
|
Brontobyte |
BB |
2^90 |
|
Geopbyte |
GB |
2^100 |
Cada medida de información es equivalente a 1024 veces la anterior, por ejemplo, un terabyte es equivalente a 1024 gigabytes y un petabyte es equivalente a 1024 terabytes.
A veces las personas hablan de los términos kilobits o megabits (notar el uso de bits en vez de bytes), pero esto no es común. Cuando estos términos salen a colación normalmente se habla de datos que son transmitidos a través de un cable, por lo que podrías escuchar frases como "kilobits por segundo" o "megabits por segundo". Por ejemplo, un módem de 56K se refiere a 56 kilobits por segundo y no kilobytes. Si queremos convertir de kilobytes a kilobits debemos multiplicar los kilobytes por ocho.
Ahora que sabemos cómo construir una RAM en el tamaño que queramos, construyamos por ahora una RAM con 65.536 bytes de memoria.
Elegimos 64KB en particular porque, porque esto genera que el Address sea de 16 interruptores, lo que podríamos interpretar como hexadecimal desde el 0000h hasta el FFFFh. De hecho, 64KB era una cantidad común para los computadores personales alrededor de 1980.
Para poder gestionar tal cantidad de memoria podríamos contar con un panel de control para escribir valores en la RAM para luego examinarlos. Este panel de control tiene 16 interruptores para indicar la dirección en la memoria, 8 interruptores para definir el valor de 8-bits que queremos guardar en la memoria, otro interruptor para ordenar el almacenamiento de un valor en una dirección en particular (interruptor Write) y ocho ampolletas que nos mostrará el resultado del valor que queremos examinar.
Hemos incluido también un interruptor llamado Takeover que permite que otros circuitos usen la misma memoria que está conectada al panel de control. Cuando Takeover es cero, el resto de los interruptores no hacen nada, pero cuando Takeover es uno, el panel de control tiene control exclusivo sobre la memoria. Este es un trabajo para varios 2-to-1 Selectors. De hecho, necesitamos 25 de ellos: 16 para los interruptores del Address, 8 para el Data Input y otro más para el Write.
Cuando el Takeover es cero, el Address, el Data Input y el Write vienen de señales externas (arriba a la izquierda). Si es uno, las entradas del RAM vienen del panel de control. De todas formas, la salida de la RAM va a las ampolletas o quizás a alguna otra parte.
El panel de control y el RAM de 64Kx8 nos permiten seguir el rastro de 65.536 bits de información y es una buena manera de facilitar la gestión de información. Con estas facilidades hemos dejado la puerta abierta para algo más, como que otros circuitos se aprovechen de las bondades del panel de control y el RAM para poder usar los valores guardados allí y escribir otros valores también.
Tenemos que tener en cuenta que si apagamos la energía que alimenta este circuito perderemos toda la información que está alojada en la RAM, porque los relés perderían su electromagnetismo y el contacto metálico volvería a su posición original, perdiendo la información contenida allí. Es por esto que el RAM es considerado un tipo de memoria volátil: necesita un constante flujo eléctrico para retener su contenido.
Automatización
La especie humana suele ser asombrosamente inventiva y laboriosa, pero al mismo tiempo profundamente perezosa. Está claro que a los humanos no nos gusta trabajar. Esta aversión al trabajo es tan extrema -y nuestro ingenio tan agudo- que estamos dispuestos a dedicar innumerables horas a diseñar y construir dispositivos que puedan reducir unos minutos nuestra jornada laboral. En este caso en particular, construiremos una máquina que pueda automatizar el proceso de sumar y restar números. Esto puede sonar muy limitado, pero da solución a una cantidad asombrosa de problemas que ni siquiera hubiésemos sospechado. Al final de esta sección inventaremos una máquina que legítimamente podemos llamar un computador.
Comencemos con algo que ya vimos como punto de partida para nuestra automatización. Esta máquina de contar incluye un latch de 8-bits (asumamos que son edge-triggered) que acumula las sumas y restas que se han generado en total. Muestra el resultado de las operaciones hechas hasta el momento.
Como recordarás, primero cerramos el Clear para generar nuevas operaciones (el equivalente al botón "C" en una calculadora). Luego, introducimos el primer número en los interruptores A, siendo este la salida del sumador. Para que este número se guarde en el latch, cerramos el interruptor "Add" y lo abrimos. El número que escribimos en A ahora pasa a ser parte de la segunda entrada en B desde el latch. Ahora, introducimos otro número en la entrada A y repetimos el proceso abriendo y cerrando el interruptor "Add". Esto hará que A y B se sumen generando un resultado que se guarda en el latch y pasa nuevamente a formar parte de los interruptores B.
El latch va acumulando sumas, mostrando el resultado de todas las sumas anteriores. Por eso se le conoce como acumulador, dado que va acumulando los resultados realizados por el sumador.
El problema con el funcionamiento de esta máquina es que sigue siendo muy manual, vale decir, debemos ingresar los números en los interruptores y abrir y cerrar el Add, en ese orden, y repetir. Imaginemos que queremos sumar 100 números y realizamos este proceso. Luego, nos damos cuenta que nos equivocamos al escribir uno de esos números y tenemos que volver a empezar. Un verdadero dolor de cabeza.
Anteriormente mencionamos que dejamos una puerta abierta para aprovechar las bondades que la RAM nos ofrece. Bueno, llegó el momento: en vez de escribir estos 100 números uno a uno en esta máquina sumadora, podríamos hacerlos en la RAM y si nos equivocamos en escribir un número, podemos corregirlo en la dirección correspondiente anotando otro valor.
Esta RAM debemos conectarla como entrada al sumador. Al final nosotros queremos sumar números que guardamos anteriormente en el RAM por lo que la salida del RAM (o el número que queremos sumar) debe ser la entrada al sumador. Asimismo, como entrada del RAM tenemos el reloj que puede estar conectado a un oscilador. De esta manera, por cada ciclo que pase voy guardando números en la RAM para eventualmente usarlos para sumar y guardarlos en el acumulador. Hablando del acumulador, el reloj también debiese estar conectado aquí. Recordemos que en nuestra máquina sumadora anterior pasamos manualmente el "Add" de 0 a 1 para guardar un número y después lo pasamos a cero para cambiar la entrada del sumador y así consecutivamente. Con el oscilador estaríamos automatizando esta subida y bajada del interruptor "Add" que ahora se llama "Clk" por Clock (reloj).
Para usarlo primero debemos limpiar la información con "Clear" como lo hicimos anteriormente y dejamos el "Takeover" del panel de control en uno (cerramos el interruptor). Ahora podemos ingresar los números de 8-bits que queremos sumar comenzando en el Address 0000h. Si quisiéramos ingresar 100 números, estos estarán guardados desde la dirección 0000h al 0063h. Una vez ingresados todos los números que queremos sumar abrimos el interruptor "Takeover" para que el panel de control ya no tenga control sobre el RAM y abrimos el interruptor "Clear". Ahora a esperar y ver como se prenden y apagan las ampolletas.
Cuando abrimos "Clear" y el reloj pasa de cero a uno, el número que está guardado en la dirección 0000h es incorporado en la entrada A del sumador. Como la entrada B es cero (limpiamos el circuito completo) el acumulador guarda ese número y lo envía a la entrada B del sumador. En el siguiente ciclo, cuando el oscilador pase de cero a uno nuevamente, el número que está en la entrada A del sumador cambia al número que está guardado en la dirección 0001h y se suma con la entrada B, que es el número guardado en la dirección 0000h. Ese resultado, como el oscilador está en uno, se guarda y pasa a ser la entrada B del sumador. Luego, el oscilador pasa de cero a uno nuevamente, sumando el número que está en 0002h con el resultado de la suma entre los números que están en 0001h y 0000h. Este ciclo se repite constantemente sumando y sumando los números guardados en la RAM.
Notar que cuando el reloj pasa de cero a uno dos cosas pasan a la vez: el acumulador guarda la suma desde el sumador y el contador se incrementa en uno, tomando el siguiente número guardado en la RAM. Por supuesto, estamos asumiendo que el oscilador es lo suficientemente lento para que todo el ciclo (desde extraer el número de la RAM hasta guardar el resultado en el acumulador) se cumpla, porque son muchos relés los que tienen que activarse antes que el oscilador pase a cero nuevamente.
Este circuito, a pesar de que funciona, tiene un par de problemas: No hay forma de detener este circuito. Cuando el contador llega a FFFFh, pasa a 000h a continuación, sumando todo nuevamente. Además lo único que hace este circuito es sumar y no restar y el límite al ser tan pequeño (255) puede llegar a su límite rápidamente entre suma y suma. Una manera obvia de solucionar esto es doblar el tamaño de todos los componentes, como el RAM, latch, sumador y ampolletas, pero tomaremos otra aproximación.
Por el momento, nos enfocaremos en otro problema: ¿qué tal si no necesitáramos añadir 100 números en una gran suma, si no que añadimos 50 pares de números para obtener 50 sumas diferentes?. La idea es que nuestra máquina puede añadir distintos pares de números y tener los resultados disponibles para nuestra conveniencia. La máquina que hemos construido no sirve para ese propósito, porque arroja inmediatamente el resultado sin poder utilizarlo en el futuro. La solución está en el RAM, porque podemos guardar el resultado en la memoria para su futura revisión en vez de conectarla en las ampolletas. Recordemos que el panel de control tiene sus propias ampolletas para revisar los resultados.
Eliminamos algunas otras partes del sumador automático en este diagrama, específicamente el oscilador y el interruptor Clear, porque ya no es obvio de dónde vendrán las entradas de Clear y Clock al contador y al latch. Además, ahora que ya tenemos el Data In utilizado por el acumulador, necesitamos una forma de controlar el Write en la RAM. Por ende, no nos preocupemos por ahora del circuito, si no del problema que intentamos resolver que es tener completa libertad en cuántos números añadir y cuántas sumas diferentes guardaremos en el RAM para una examinación futura.
Supongamos que sumamos tres números, luego sumamos otros dos números y finalmente sumamos otros tres números. La memoria en este caso se vería así:
Este diagrama es la representación de la memoria. Los cuadrados representan el contenido de la memoria que toma un byte máximo y la dirección de esos contenidos está a la izquierda. No están todas las direcciones anotadas, porque al ser secuencial podemos darnos cuenta nosotros de las direcciones, por ejemplo, el número 18h está en la dirección 0002h. Los comentarios a la derecha nos muestran donde queremos guardar los resultados de estas sumas (aunque se muestran vacíos, la memoria siempre contiene algo, aunque sea un dato aleatorio).
En vez de que esté sumador automático haga una cosa como nuestro primer prototipo de sumador, queremos que haga cuatro cosas:
-
Para comenzar una suma, transferimos un byte desde la memoria al acumulador (Load o carga)
-
Sumar un byte de memoria con el valor que está en el acumulador (Add o añadir)
-
Tomar la suma resultante del acumulador y lo guardamos en la memoria (Store o guardado)
-
Necesitamos una manera de detener el circuito (Halt o detener)
Los nombres puestos en paréntesis es el nombre de cada una de estas operaciones que, más que nada, es el movimiento de corriente entre un componente y otro. Insisto, no nos preocupemos en cómo va el circuito por ahora, porque si lo hiciéramos, sumariamos todos los números y los iremos acumulando uno a uno guardandolos en la RAM sin poder guardarlos en pares o en tripletas como queremos.
Las operaciones que tenemos que hacer para lograr sumar estos números y guardarlos en los grupos descritos anteriormente son:
-
Cargamos el valor de la dirección 0000h al acumulador (operación Load).
-
Sumamos el valor de la dirección 0001h al contenido del acumulador (operación Add).
-
Sumamos el valor de la dirección 0002h al contenido del acumulador (operación Add).
-
Guardamos el contenido del acumulador (suma resultante de la tripleta) a la dirección 0003h (operación Store).
-
Cargamos el valor de la dirección 0004h al acumulador (operación Load).
-
Sumamos el valor de la dirección 0005h al contenido del acumulador (operación Add).
-
Guardamos el contenido del acumulador (suma resultante del par) a la dirección 0006h (operación Store).
-
Cargamos el valor de la dirección 0007h al acumulador (operación Load).
-
Sumamos el valor de la dirección 0008h al contenido del acumulador (operación Add).
-
Sumamos el valor de la dirección 0009h al contenido del acumulador (operación Add).
-
Guardamos el contenido del acumulador (suma resultante de la tripleta) a la dirección 000Ah (operación Store).
-
Detenemos el trabajo del sumador automático (operación Halt).
En la versión de sumador automático que tenemos los datos, los valores se suman y se guardan solamente. Lo que queremos es que el sumador automático pueda hacer distintas operaciones como nosotros le indiquemos, pero para ello necesitamos algún tipo de código que le indique al sumador automático qué hacer. Quizás, la forma más fácil, pero no la más barata, es tener dos RAM dónde una guarde los números que se van a sumar (la RAM original) y la otra guarde las operaciones o instrucciones de lo que el sumador automático debería hacer con las direcciones correspondientes al RAM original. Estas dos RAM pueden ser llamadas como Data (la RAM original) y Code (la nueva RAM).
Este nuevo RAM será alimentado únicamente desde el panel de control. Para las cuatro operaciones necesitamos cuatro códigos para representarlas. Puede ser cualquier código y para este ejercicio utilizaremos los siguientes:
|
Operación |
Código |
|
Load |
10h |
|
Store |
11h |
|
Add |
20h |
|
Halt |
FFh |
Para el ejercicio que estamos haciendo, necesitamos escribir desde el panel de control el código de las instrucciones para el sumador automático. Estas se guardarán de manera consecutiva desde el 0000h hasta el 000Bh (son 12 instrucciones en total). Notar cómo estas instrucciones calzan con los números que están en la RAM original para que puedan realizar una acción.
|
Dirección |
RAM "Data" |
RAM "Code" |
Operación |
|
0000h |
27h |
10h |
Load |
|
0001h |
A2h |
20h |
Add |
|
0002h |
18h |
20h |
Add |
|
0003h |
11h |
Store |
|
|
0004h |
1Fh |
10h |
Load |
|
0005h |
89h |
20h |
Add |
|
0006h |
11h |
Store |
|
|
0007h |
33h |
10h |
Load |
|
0008h |
2Ah |
20h |
Add |
|
0008h |
55h |
20h |
Add |
|
000Ah |
11h |
Store |
|
|
000Bh |
Ffh |
Halt |
Hay otro cambio que debemos hacer: originalmente la salida del sumador era la entrada del acumulador, pero ahora, para realizar la instrucción Load es necesario que la salida de la RAM sea la entrada al acumulador. Este es un trabajo de un 2-to-Line Data Selector.
Los datos que el acumulador reciba depende de si estamos ante una instrucción Add (la información proviene del sumador) o una instrucción Load (la información proviene del RAM). Esto se controla por el interruptor "S" que está en el 2-to-1 Selector.
Este diagrama está incompleto y lo que falta son señales que controlan estos componentes, como el 2-to-1 Selector, conocidos como señales de control y esto es emitido por la unidad de control. Estos componentes incluyen el reloj y la entrada Clear para el contador y el acumulador, la entrada Write y la RAM "Data". Estas señales de control, como vimos, vienen del RAM "Code". Por ejemplo, la entrada de un 2-to-1 Selector es cero (se selecciona la salida del RAM "Data") si la salida del RAM "Code" indica una instrucción Load. La entrada Write conectada al RAM "Data" es uno solo si aparece la instrucción Store. Estas señales de control pueden ser generadas utilizando varias combinaciones de puentes lógicos.
Ahora bien, incluyamos en el sumador la habilidad de poder restar. Esto implica que tengamos un nuevo código:
|
Operación |
Código |
|
Subtract |
21h |
El código 21h genera la acción de cerrar el interruptor C0 que se puede ver en el siguiente diagrama. Cuando aplicamos la instrucción Substract, el circuito debería de hacer lo mismo que Add, excepto que los datos que vienen del RAM se invierten antes de llegar al sumador y el bit de carga (Carry Input o CI) es configurado a uno.
Anteriormente mencionamos que un problema con esta máquina es que está limitada sólo a 8 bits. Podemos aumentar los componentes a 16-bit, pero hay una solución más barata que esa. Supongamos que queremos sumar dos números de 16-bits: el 76ABh con el 232Ch. Estos números los podemos separar en dos, donde tenemos el byte de mayor orden (los números 76h y 23h) y los de menor orden (Abh y 2Ch). Podemos sumar estos distintos órdenes de byte por separado y al final concatenarlos. Este sería el proceso completo:
El resultado es 99D7h. Esta propiedad de los números de 16-bits que nos permiten separar, sumar y concatenar números, nos permiten guardar números de 16-bits sin problemas en la RAM, dado que separamos la información en 8-bits.
El resultado D7h será guardado en 0002h mientras que el resultado 99h será guardado en 0005h.
Esto no funcionará en todos los casos. Veamos por ejemplo los números 76ABh t 236Ch. En este caso, si sumamos los bytes de menor orden (Abh y 6Ch) vamos a tener una carga que debe ser añadida a los bytes de mayor orden.
Para lograr que nuestra máquina pueda captar esa carga de bit y de esta manera sumar dos números de 16-bits es guardar el Carry Out que proviene del sumador (en el caso del ejemplo, el Carry Out o bit de carga del dígito más significativo es 1h) cuando la primera suma sea ejecutada, es decir, los bytes de menor orden. Luego, se utiliza el Carry Out como Carry Input (el bit de carga para el dígito menos significativo) en la segunda suma, vale decir, en los bytes de mayor orden. Para guardar este bit de carga para la suma de bytes de mayor orden necesitamos un latch conocido como Carry Latch.
Para utilizar el Carry Latch necesitamos otra operación llamada Add with Carry. Si queremos sumar dos números de 16-bits utilizamos la vieja instrucción Add para sumar los bytes de menor orden. El Carry Input sería cero al igual que el Carry Latch y por tanto el Carry Out. Para sumar los bytes de mayor orden tenemos que usar la nueva instrucción Add with Carry, porque debemos considerar el contenido del Carry Latch. Si la primera suma resulta en un bit de carga, este bit se considera para la segunda suma. Si el Carry Latch es cero, no hay carga, por lo que la suma se realiza como si fuese una instrucción Add normal.
Cuando restamos sucede algo similar, pero en vez de añadir un número al siguiente dígito más significativo, como es el caso de una suma, pedimos prestado un dígito. Esta instrucción se llama Subtract with Borrow. Normalmente, como ya vimos, una instrucción Subtract requiere que invirtamos el sustraendo y establecer el Carry Input (bit de carga del dígito menos significante) del sumador a uno. El Carry Out normalmente es uno y debe de ser ignorado. Sin embargo, si estamos restando un número de 16-bits con otro, el Carry Output es guardado en el Carry Latch. En la segunda resta, el Carry Input del sumador toma el valor del Carry Latch y de esta forma se estaría llegando al resultado correcto.
Con las instrucciones Add with Carry y Subtract with Borrow ya tenemos 7 instrucciones:
|
Operación |
Código |
|
Load |
10h |
|
Store |
11h |
|
Add |
20h |
|
Subtract |
21h |
|
Add with Carry |
22h |
|
Subtract with Borrow |
23h |
|
Halt |
FFh |
En resumen, el número enviado al sumador se invierte para una operación Subtract o de Subtract with Borrow. El Carry Output del sumador es la entrada de datos al Carry Latch. El latch se sincroniza cada vez que se realiza una operación Add, Subtract, Add with Carry o Subtract with Borrow. El Carry Input del sumador de 8 bits es uno cuando se realiza una operación Subtract o cuando la salida del Carry Latch es 1 y se realiza una operación Add with Carry o Subtract with Borrow.
Recordemos que cuando tenemos una operación de números que contienen más de un byte de información, utilizaremos Add with Carry o Subtract with Borrow en los bytes siguientes al de menor orden, independiente de qué números se están sumando, vale decir, siempre utilizaremos esta instrucción en los bytes de mayor orden. Por lo tanto, tomando nuestro ejemplo anterior de suma, las instrucciones del Data RAM y del Code RAM serían así:
Esto funciona correctamente independiente de los números que pongamos. Si tenemos números mayores a 16-bits simplemente debemos usar repetidamente Add with Carry para todos los bytes a excepción del byte de menor orden. Por ejemplo, si queremos sumar dos números de 32-bits que son el 7A892BCDh y el 65A872FFh, necesitaríamos una instrucción Add y tres instrucciones Add with Carry.
Esta implementación tiene algunos inconvenientes. Uno de ellos es que los números no son guardados en direcciones consecutivas lo que podría ser un problema para obtener el resultado final, por ejemplo el resultado se encuentra en 0002h, 0005h, 0008h y 000Bh, teniendo que examinar cada una de esas direcciones y concatenarlas para obtener un resultado. Sería un trabajo molesto tener que ir rastreando en qué direcciones estará el resultado para examinar cada una por separado y así obtener el resultado final.
Además, el diseño de nuestro sumador automático no nos permite reutilizar los resultados en operaciones futuras. Por ejemplo, si queremos sumar tres números de 8-bit cada uno y luego a ese resultado restarle un número de 8-bit y guardar el resultado necesitaremos una instrucción Load, dos instrucciones Add, un Subtract y un Store, pero ¿qué tal si además queremos realizar otra resta a la suma original? Esa suma no es accesible, por lo que tendremos que recalcular cada vez que necesitemos acceder a esa suma.
El problema es que hemos construido un sumador automático que direcciona la memoria "Code" y la memoria "Data" simultáneamente y secuencialmente comenzando en la dirección 0000h. Cada instrucción en la memoria "Code" corresponde a una ubicación en la memoria "Data" en la misma dirección. Una vez que una instrucción Store hace que se almacene algo en la memoria "Data", ese valor no puede ser cargado de nuevo en el acumulador.
Para solucionar este problema haremos algunos cambios en nuestro sumador automático que se verán insanamente complicados al principio, pero verás que con el tiempo se abre la puerta para que nuestro circuito pueda ser mucho más flexible de lo que es ahora.
El primer cambio que haremos es en los códigos de operación. Sabemos que estos tienen un tamaño de un byte de memoria cada uno a excepción de Halt. Ahora, cambiaremos la estructura de estos códigos a tres bytes, donde el primero es el código en sí y los otros dos serán una ubicación en memoria de 16-bits. Para la instrucción Load, esta dirección contiene el byte que tiene que ser cargado al acumulador. Para las instrucciones Add, Subtract, Add with Carry y Subtract with Borrow la dirección indica la ubicación del byte que será sumado o restado desde el acumulador. Para la instrucción Store la dirección indica dónde se guardará el contenido del acumulador.
Hagamos el ejemplo más simple posible: sumar dos números. Para eso, tomando en cuenta nuestra antigua estructura de los códigos de operación el RAM "Code" y el RAM "Data" se vería así:
Para la nueva configuración de códigos de operación con 3 bytes cada instrucción sería así:
Donde cada instrucción, a excepción de Halt, es seguido de dos bytes que indican la dirección de 16-bits en el RAM "Data". Estas tres direcciones son el 0000h, 0001h y el 0002h que es donde se encuentran los números que queremos sumar en el ejemplo, pero puede ser cualquier dirección.
Para ver los beneficios del cambio de estructura en los códigos de operación veamos otro ejemplo: anteriormente vimos cómo sumar dos números de 16-bits, específicamente el 76ABh y el 232Ch, utilizando las instrucciones Add y Add with Carry. Los dos bytes de menor orden se guardarán en las direcciones 0000h y 0001h, respectivamente, dejando el resultado en 0003h, mientras que los bytes de mayor orden se guardarán en 0003h y 0004h dejando el resultado de la suma en 0005h. Ahora bien, con los cambios que hemos hecho en los códigos de operación, podemos guardar los bytes de mayor y menor orden junto con los resultados en un área del RAM que no hemos usado antes.
Estas seis ubicaciones no tienen porqué estar juntas entre sí, si no que pueden estar distribuidas a lo largo de 64 KB de espacio. Para agregar estos valores a la ubicación de memoria del ejemplo (o en realidad a la ubicación que queramos) debemos configurarlo en las instrucciones del RAM "Code" de la siguiente manera:
Los 2 bytes de menor orden ubicados en las direcciones 4001h (Abh) y 4003h (2Ch) se suman primero dejando el resultado en la dirección 4005h. Los 2 bytes de mayor orden (almacenados en 4000h y 4002h) se suman con la instrucción Add with Carry dejando el resultado en la dirección 4004h. Notar que si queremos realizar más instrucciones de suma o de resta, podemos remover la instrucción Halt y añadir más instrucciones al RAM "Code" pudiendo tomar los resultados que ya están guardados en el RAM. Esto soluciona el problema que nos habíamos planteado anteriormente, dado que ahora sí podemos utilizar antiguas operaciones con otras nuevas sin tener que correr todo de nuevo.
Lo esencial para poder implementar esta nueva configuración en las instrucciones del RAM "Code" es tener tres latches de 8-bits cada una. Cada una de estos latch guarda uno de los tres bytes que contienen los códigos de operación. El primer latch guarda la instrucción persé, el segundo guarda el byte de mayor orden de la dirección del RAM "Data" y el tercero guarda el byte de menor orden. La salida de estos dos últimos latches corresponden a la dirección de 16-bit de la RAM dónde estará almacenada la información que manejaremos.
El proceso de tomar una instrucción de la memoria es llamado como operación Fetch o captación (complemento: https://tinyurl.com/2p8f2ucb). En nuestra máquina, cada instrucción es de 3 bytes y podemos tomar un byte por cada ciclo. La instrucción completa es de cuatro ciclos, porque tenemos tres para extraer la instrucción y una cuarta en ejecutarla. Esta ejecución no se realiza como si la máquina mirara una instrucción y dijera "Ah ahora tengo que sumar este número ubicado en x dirección con el que está en el acumulador", si no que cada código activa varios interruptores en una forma única que causa que la máquina realice varias cosas.
Al tener cuatro ciclos para realizar una acción en vez de uno estamos haciendo la máquina cuatro veces más lenta, pero también la estamos haciendo más versátil. Esto es el resultado de un concepto ingenieril llamado no hay tal cosa como un almuerzo gratis (TANSTAAFL), es decir, si mejoramos una máquina en un sentido, va a sufrir en otro.
Originalmente introducimos dos RAM (una para los datos y otra para el código) con el fin de que la arquitectura fuera lo más simple y clara posible. Ahora que hemos decidido utilizar 3 bytes para las instrucciones ya no hay necesidad de tener dos RAM, dado que tanto el código como los datos los podemos utilizar en un mismo RAM. Sin embargo, para lograr esto necesitamos un 2-to-1 Selector para determinar como el RAM va a direccionar los datos. Normalmente, la dirección o, en otras palabras, la entrada del RAM, proviene del contador de 16-bit como lo teníamos antes. Sin embargo, necesitamos otra entrada para el 2-to-1 Selector además del contador que corresponde a la dirección de la memoria que está guardada en los dos latch de 8-bits cada uno. De esta forma, tenemos la dirección del contador que es secuencial y tenemos la dirección guardada en los dos latch que es la que configuramos nosotros, ambas de 16-bits de tamaño.
Con esto hemos hecho un gran progreso, dado que ya somos capaces de introducir instrucciones y datos en una sola RAM. Veamos a continuación un diagrama de cómo sumar dos números de 8-bits y luego restar un tercer número en una única RAM.
La instrucción comienza en la dirección 0000h como es usual, dado que el contador es quién comienza accediendo a la RAM después de que la máquina haya sido reseteada. Las primeras 13 casillas del RAM están las instrucciones que vamos a realizar y la dirección 010h hasta el 013h, equivalente en decimal a la casilla 16 a la 19 están los números con sus resultados. Partimos ingresando el número que está en 10h en el acumulador (instrucción Load) y luego sumamos ese número por el que está en la dirección 0011h, vale decir, sumamos el número 45h con el A9h. Después, guardamos ese resultado en la RAM con la instrucción Store y restamos el número ubicado en la dirección 0012h que corresponde al número 8Eh. Finalmente, guardamos el resultado en la dirección 0013h desde el acumulador. Terminamos con la instrucción Halt.
Imaginemos ahora que queremos añadir más instrucciones sumando dos números más. Una solución es borrar todas las instrucciones que tenemos y volverlas a escribir, pero sería poco eficiente. Otra aproximación es continuar con las nuevas instrucciones reemplazando la instrucción Halt con una nueva instrucción Load en 000Ch. Sin embargo, y aquí comienzan los problemas, debemos añadir dos instrucciones Add y una nueva instrucción Halt, pero ¿dónde? si ya estamos teniendo un número en la dirección 0010h. Habría que mover ese dato en alguna parte del RAM en una dirección más alta, pero también tenemos que cambiar las instrucciones para dirigirnos a esa nueva dirección. La idea es que no tengamos que cambiar las instrucciones iniciales, pero eso sería casi como volver a hacer todo de nuevo.
Podrías pensar que separar la RAM en Code y Data no era tan mala idea después de todo, pero este problema hubiese ocurrido tarde o temprano. Dado que no podemos esperar mover la instrucción Halt y esperar que todo salga bien, porque los datos de números con códigos corren peligro de superponerse, debemos pensar en otra cosa. ¿Qué tal si tan solo pudiéramos saltar a la instrucción o dato que queramos en vez de que todo vaya en secuencia? Bueno, en realidad podemos.
La instrucción jump rompe con el esquema de secuencialidad de una RAM y permite alterar el patrón de funcionamiento como nosotros convengamos, vale decir, podemos direccionar la RAM a diferentes ubicaciones especificadas. A esta instrucción se le conoce también como branch o goto (go to another place). Si queremos, en nuestro ejemplo anterior podemos reemplazar la instrucción Halt por una instrucción Jump que tomará el valor de 30h.
Recordemos que la instrucción jump, como las demás, tiene 3 bytes de tamaño, uno para la instrucción y dos para la dirección al cuál saltar que en este caso sería 20h. Aquí se encuentra una instrucción Load y continua con dos instrucciones Add, un store y finalmente la instrucción Halt.
La instrucción jump afecta directamente al contador forzandolo de alguna manera a generar un nuevo número fuera de la secuencia. El contador toma el número que está dentro de la instrucción jump de 16-bits y por tanto va a otra casilla en la RAM. Este tipo de comportamiento se puede hacer utilizando las entradas de Preset y Clear de un edge-triggered D-type flip-flop que ya vimos anteriormente.
Recordemos que Preset y Clear deberían de ser cero para operaciones normales. Si Preset es uno, Q se convierte en uno y si Clear es uno, Q se convierte en cero. Si queremos entregarle un nuevo valor a este circuito (que llamamos A por Address) podemos armar esto de la siguiente manera:
Normalmente el Set It es cero, lo que hace que Preset también sea cero gracias a la puerta AND. Clear también es cero por la misma razón a menos que Reset sea uno. El Reset resetea el circuito independiente del valor que tome Set it gracias a la puerta OR. Cuando el input Preset es uno, Clear será cero si A es uno. Esto significa que Q tomará el valor de A. Para que Preset sea uno en primer lugar tanto A como Set It deben de ser uno. Set It se activa cuando la instrucción Jump aparece y A toma el valor de los 16-bit que viene de la misma instrucción jump, procedente de los dos latch de 8-bit . Sin embargo, para que A tome 16-bits necesitamos 16 As y por tanto, 16 de estos circuitos conectados entre sí. Una vez que los 16 As tomen un nuevo valor, el contador partirá contando a partir de ese valor. Conectamos los dos 8-latch tanto al contador como al 2-to-1 Selector.
La conexión de los dos latch de 8-bits al contador y al 2-to-1 Selector permite que la dirección que proviene de ambos latch sea una entrada para el RAM y para el contador de 16-bit. Esto genera dos cosas: una que el RAM pueda tomar la dirección que nosotros queramos y dos que el contador siga contando a partir de esa dirección.
Sin duda alguna la instrucción Jump es útil, pero sería mucho más útil si pudiéramos saltar a una dirección del RAM si cierta condición se cumple. Para ver la utilidad de esto supongamos que queremos multiplicar dos números de 8-bits, digamos A7h y 1Ch. El resultado de multiplicar dos números de 8-bits da un número de 16-bits, pero por conveniencia expresaremos todos los números en hexadecimal. Nuestro primer trabajo es decidir en qué parte de la memoria pondremos estos números y su producto.
Multiplicar A7h y 1Ch (que es 28 en decimal) es lo mismo que sumar A7h 28 veces, por lo que en realidad las direcciones 1004h y 1005h irán acumulando estas sumas. Los resultados se guardarán en dos direcciones distintas, porque el resultado es de 16-bits y cada dirección es de 8-bits. A continuación están las instrucciones para sumar A7h por sí mismo una sola vez.
Estas instrucciones se tendrán que repetir 27 veces, porque ya llevamos una suma y por tanto nos faltan 27 para llegar a las 28 sumas. Por lo tanto, para terminar la multiplicación necesitamos escribir en total 168 instrucciones. Otra solución para llegar al resultado deseado sería poner una instrucción Halt en 0012h y apretar el botón Reset 28 veces para llegar al resultado final. Estas opciones, sin embargo, no son ideales, porque implican que tu hagas un esfuerzo (escribir un montón de instrucciones o presionar el botón Reset muchas veces). Esto se complica demasiado cuando multipliquemos números muy grandes, entonces ¿Qué hacemos?
Una solución a esa repetición es poner una instrucción jump en 0012h causando que el contador comienza en 000h
El problema con esto, como podrás notar, es que no hay forma de detener la instrucción jump, vale decir, siempre se están multiplicando los números y nunca se llegará a la instrucción Halt para terminar con las instrucciones. Lo que necesitamos es un jump que repita una serie de instrucciones en una cantidad determinada y en este caso, necesitamos generar la instrucción jump que se repita 28 veces.
En informática, además de la instrucción jump que ya vimos, hay otras instrucciones jump llamadas saltos condicionales. Estas instrucciones saltan de una dirección a otra siempre y cuando se cumpla cierta condición. Hay muchas instrucciones de saltos condicionales, pero la primera que veremos es una de las más simples llamada Jump if Zero. Este salto sólo se activa cuando la suma o resta de una operación da como resultado cero. (complemento: http://unixwiz.net/techtips/x86-jumps.html )
Primero, vamos a utilizar un latch de 1-bit igual que el Carry Latch que llamaremos Zero latch, porque guardará el valor de uno solo si las salidas del sumador de 8-bits son todas cero.
La salida del puente NOR es uno solo si todas las entradas son ceros. Al igual que el reloj del Carry latch, el reloj de Zero latch es uno solo si una operación de Add, Subtract, Add with Carry o Subtract with Borrow se realiza. El valor de salida de este latch es conocido como Zero latch que solo es uno si la suma o resta de dos números es cero.
La pregunta es, ¿de qué nos sirve el Jump if Zero para multiplicar? Bueno, eso lo veremos en breve, pero primero debemos saber que al usar en conjunto el Carry Latch con el Zero Latch podremos crear tres instrucciones más de saltos condicionales. Así es como se vería la tabla de códigos de operaciones con cuatro instrucciones más.
|
Operación |
Código |
|
Load |
10h |
|
Store |
11h |
|
Add |
20h |
|
Subtract |
21h |
|
Add with Carry |
22h |
|
Subtract with Borrow |
23h |
|
Jump |
30h |
|
Jump if Zero |
31h |
|
Jump if Carry |
32h |
|
Jump if Not Zero |
33h |
|
Jump if Not Carry |
34h |
|
Halt |
FFh |
Lo que hace cada salto condicional queda claro con solo leer su nombre, por ejemplo, la instrucción Jump if Not Zero, salta a una dirección específica sólo si la salida del Zero latch es cero, o en otras palabras, no habrá salto si las instrucciones Add, Subtract, Add with Carry o Subtract with Borrow resulten en cero. La implementación de este diseño es sólo un añadido a las señales de control que implementa la instrucción Jump: Si la instrucción es Jump If Not Zero, la señal Set It en el contador de 16 bits se activa sólo si la bandera Zero es 0.
Ahora ya tenemos lo que necesitamos para poder multiplicar los números A7h y 1Ch. Veamos cómo se ven las instrucciones primero y después las explicamos:
Anteriormente, guardamos en 0004h y 0005h la primera suma entre A7h y 1Ch. Ahora debemos cargar el número que se encuentra en la dirección 1003h al acumulador, que es el número 1Ch. Este valor es añadido al valor en la dirección 001Eh con la instrucción Add. El número guardado en la dirección 001Eh es en dónde está la instrucción Halt, pero también es un número válido. Añadir FFh a 1Ch es lo mismo que restar 1Ch en una unidad (aquí está la magia), por lo que el resultado es 1Bh. Este número está guardado en el lugar que antes ocupaba 1Ch, vale decir, la dirección 1003h. La siguiente instrucción es un Jump if Not Zero y como 1Bh no es cero, ocurre el salto a la dirección 0000h volviendo a empezar todo de nuevo. En la segunda ronda A7h y 1Ch están sumados dos veces. El valor 1Bh es añadido a FFh y resulta en 1Ah. Como no es cero, se repite nuevamente el proceso.
Este proceso se repite en total 28 veces, dado que cuando se llegue a ese número, el 1Ch, que es 28 en decimal, será sumado 28 veces por el número FFh, equivalente a restar ese número 28 veces por sí mismo, lo que da cero. Cuando el resultado sea cero, el Zero Flag será uno, por lo que no ocurrirá el salto nuevamente. En vez de ir a 0000h, la siguiente instrucción es el Halt y el programa termina. Estamos listos.
El hardware que hemos creado aquí lo podemos llamar sin tapujos como un computador. Lo que hace la diferencia es el salto condicional, porque una repetición controlada o bucle (looping en inglés) es lo que separa un computador de una calculadora. En una forma similar podemos también ser capaces de dividir números y más aún, no nos limitados solamente a 8-bits, si no que podemos realizar operaciones con números de 16-bits, 32-bits o incluso más grandes, separándolos en la memoria en 8-bits. Si podemos hacer esto, también podremos resolver raíces cuadradas, logaritmos, funciones trigonométricas y un sinfín de otros problemas.
Este computador que ensamblamos se llama computador digital, porque trabaja con números discretos. Los computadores análogos, por otro lado, trabajan con números continuos. Los computadores análogos, sin embargo, ya no se utilizan en el mundo moderno.
Los computadores digitales tienen cuatro partes principales: el procesador, la memoria, un dispositivo de entrada y otro dispositivo de salida. En nuestra máquina la memoria es el RAM de 64-KB, el dispositivo de entrada son los interruptores del panel de control, el dispositivo de salida son las ampolletas y el procesador es todo lo demás. Un procesador también es llamado como unidad central de procesamiento o CPU y hoy en día también se le conoce como microprocesador, por el pequeño tamaño que ha ido tomando a lo largo de los años.
Un procesador tiene varios componentes de los que ya especificamos como el sumador y el acumulador de 8-bits. En nuestro computador, el inversor y el sumador en conjunto se le conoce como unidad aritmética lógica o ALU. Esta unidad realiza sólo aritmética, específicamente sumas y restas. En computadores más sofisticados, como ya veremos, el ALU puede realizar funciones lógicas como AND, OR y XOR. El contador de 16-bits es llamado como contador de programa.
El computador que realizamos es construido por relés, cables, ampolletas e interruptores. Estos componentes son lo que llamamos hardware mientras que las instrucciones o números que hemos ingresado a la memoria se conoce como software. Es "soft" o suave, porque puede cambiar mucho más fácilmente que el hardware. El software también es conocido como un programa computacional. Escribir un software es la acción conocida como programador de computadores. Anteriormente realizamos las instrucciones para que dos números puedan multiplicarse entre sí. Eso es programar.
En un programa podemos distinguir entre el código que son las instrucciones y los datos que son los números que el código manipula. Hay veces que la distinción entre código y datos no es muy obvia. Ya vimos anteriormente cómo utilizamos el código Halt como dato en vez de instrucción y es por esto que Halt tiene un doble propósito que es detener el programa y como un número que representa el -1. Los códigos de operación, como 10h y 11h para Load y Store, respectivamente, son conocidos como código máquina o lenguaje máquina. El término lenguaje se utiliza porque es similar a un lenguaje hablado o escrito por humanos para que el computador pueda "entender" y responder a eso.
Los códigos de las máquinas generalmente son acortados en escrito por mnemotécnicos. Estos están escritos en mayúscula de dos o tres letras. A continuación se muestran los mnemotécnicos que el computador reconoce:
|
Operación |
Código |
Mnemotécnico |
|
Load |
10h |
LOD |
|
Store |
11h |
STO |
|
Add |
20h |
ADD |
|
Subtract |
21h |
SUB |
|
Add with Carry |
22h |
ADC |
|
Subtract with Borrow |
23h |
SBB |
|
Jump |
30h |
JPM |
|
Jump if Zero |
31h |
JZ |
|
Jump if Carry |
32h |
JC |
|
Jump if Not Zero |
33h |
JNZ |
|
Jump if Not Carry |
34h |
JNC |
|
Halt |
FFh |
HLT |
Estos mnemotécnicos se pueden combinar con otros atajos, por ejemplo, si dijéramos "Carga un byte desde la dirección 1003h al acumulador" eso lo podríamos manifestar de una manera mucho más corta: LOD A, [1003h]. El A (de acumulador) y el [1003] que aparecen a la derecha son llamados argumentos que indican lo que sucede con la instrucción Load, donde a la izquierda se escribe el destino (el acumulador) y a la derecha el origen o la fuente (la dirección 1003h). Los corchetes indican que el valor es la dirección 1003h y no el número 1003h. Otros ejemplos que podemos ver son:
|
Instrucción |
Código |
|
Suma el número en la dirección 001Eh con el del acumulador |
ADD A, [001Eh] |
|
Guarda el contenido del acumulador a la dirección 1003h |
STO [1003h], A |
|
Salta a la dirección 0000h si el Zero Flag no es uno |
JNZ 0000H |
Notar que en la instrucción Store la dirección está a la izquierda y el acumulador a la derecha, dado que la dirección en este caso es el destino de los datos y el origen está en el acumulador. En la instrucción de Jump if Not Zero, en cambio, no tiene corchetes, porque la instrucción salta a la dirección 0000h y no al valor que puede contener la dirección 0000h. Esto quiere decir que cuando tenemos corchetes, se toma el valor ubicado en esa dirección y si es sin corchetes, la instrucción no toma ese valor para realizar alguna acción.
Estas instrucciones las podemos listar secuencialmente y de una manera legible de tal forma que ya no necesitaremos las casillas de las direcciones de memoria.
Podemos también indicar si los datos fueron guardados en una dirección en particular de la siguiente manera:
1000h: 00h, A7C
1002h: 00h, 1Ch
1004h: 00h, 00h
Los dos bytes separados por comas indican que el primer byte es guardado en la dirección de la izquierda y el segundo en la derecha. Con esto en mente ya podemos escribir las instrucciones en la multiplicación de los números A7h y 1Ch y así tener listo nuestro programa computacional.
Las últimas tres líneas equivalen a lo que habíamos hecho anteriormente con la memoria:
Para que no sea más fácil escribir estas instrucciones es mejor no utilizar direcciones de memoria numéricas, dado que estas pueden cambiar. Si cambian las direcciones tendremos que cambiar también muchas de las instrucciones lo que se puede volver engorroso. Es mejor utilizar etiquetas que se vinculen con una dirección de memoria. Estas etiquetas son palabras, lo que transformaría a este lenguaje a algo más parecido a como nosotros nos comunicamos.
Las palabras NUM1, NUM2 y RESULT están vinculadas a una dirección de memoria donde dos bytes son almacenados. Las etiquetas NUM1 + 1, NUM2 + 1 y RESULT + 1 se refieren al segundo byte después de una etiqueta en particular. La etiqueta NEG1 se vincula con la instrucción Halt y se llama así porque también tiene la funcionalidad de restar a un byte una unidad.
También, para mejorar la legibilidad humana, se pueden incluir comentarios que no son leídos por el programa, si no que le dan una ayuda al programador para que pueda recordar ciertas cosas de su código. Los comentarios los haremos con un punto coma.
Este código es conocido como lenguaje ensamblasador y tiene una mezcla entre el código máquina y el lenguaje inglés. Algunas personas confunden las diferencias entre el lenguaje máquina y el lenguaje ensamblasador, porque son dos formas diferentes de llegar a lo mismo. Cada sentencia en el lenguaje ensamblasador corresponde a bytes específicos en el código máquina.
Si queremos construir un programa en el computador que construimos lo más sensato sería escribir las instrucciones en lenguaje ensamblasador en un papel. Luego de que estés satisfecho con las instrucciones ya podemos ensamblarlas a mano, es decir, convertir manualmente estas instrucciones en lenguaje ensamblasador a lenguaje máquina, también en papel. En este punto, podemos utilizar los interruptores para ingresar el código máquina en la RAM y correr el programa para que la máquina pueda ejecutar las instrucciones que nosotros le entregamos.
La gran parte del tiempo que utliza un programador es en corregir errores, en especial cuando programamos en lenguaje ensamblasador que es muy fácil cometerlos. Estos errores se conocen como bugs y muchos de ellos son difíciles de encontrar. Por ejemplo, si ingresamos una instrucción incorrecta digamos 11h (Store) en vez de 10h, no solo la máquina no cargará en el acumulador el número que queremos, si no que va a sobreescribir una instrucción o número en la RAM. La máquina seguirá funcionando, pero los resultados serán impredecibles.
Incluso hay un error en el programa de multiplicación. Si lo ejecutas dos veces, la segunda vez multiplicará A7h por 256 y añadirá ese resultado al resultado ya calculado. Esto se debe a que después de ejecutar el programa una vez, el número en la dirección 1003h será 0. Cuando lo ejecutes la segunda vez, se añadirá FFh a ese valor. El resultado no será 0, por lo que el programa seguirá ejecutándose hasta que lo sea.
Es hora de poner las cosas en perspectiva y revisar la historia de la computación para darnos cuenta que no necesitamos construir de cero este elaborado computador, porque ya está maquetado. Revisemos cómo llegamos a eso.
La parte dos del libro está en desarrollo.
Deja un comentario