La serie “El computador mágico” está disponible también en forma de libro. |
En el último capítulo de la serie vimos la arquitectura de von Neumann y mostramos el diseño a alto nivel de un ordenador basado en dicha arquitectura. En este capítulo veremos su funcionamiento.
Este es probablemente el artículo más importante de la serie, el que te contará cómo funciona un ordenador. Todo lo que hemos ido haciendo hasta ahora ha sido para poder llegar aquí con soltura. Y todo lo que veamos a partir de ahora serán mejoras (en la máquina, inicialmente, y en el software más adelante). Pero si entiendes este artículo, tendrás realizada la mitad del camino al entendimiento de cómo funciona el ordenador.
Empecemos recordando la arquitectura que habíamos planteado para el C16A:
El problema para entender este capítulo es que requiere entender muchas cosas a la vez (por eso hemos tratado de explicar todo lo posible antes de llegar aquí). Es posible que cada uno de los párrafos te resulte un poco “extraño”, pero cuando acabes el artículo espero que cierres el círculo y todo te encaje.
Recuerda también, antes de seguir, la estructura general:
- Tanto el programa (el software, el algoritmo) como los datos “viven” en la memoria principal (MM, Main Memory).
- La ALU (Arithmetic-Logic Unit, unidad aritmético-lógica) hace operaciones básicas: sumar A+B (SUM), decrementar A (DEC), traspasar B (TRANS) y poner a 0 (CERO). Su salida se manda al acumulador (AC, Accumulator). La entrada A es la de la izquierda (recuérdalo, porque es importante; cada una viene de sitios distintos).
- La unidad de control (CU, Control Unit) hace de director de orquesta. Lo que hace es ir tomando las instruciones de la MM y las va interpretando. Dependiendo de la instrucción que sea, puede que tenga que ir a por más cosas a la memoria, que tenga que hacerlas pasar por la ALU o que tenga que hacer llegar resultados hacia la memoria.
- Recuerda que la entrada (el teclado) y la salida (la pantalla), desde este punto de vista, son simplemente parte de la memoria: podemos leer o escribir en ellos.
¿Profundizamos un poco más?
Vamos a empezar por el PC (Program Counter, contador de programa). El registro PC, de 12 bits, contiene la dirección en memoria de la próxima instrucción a ejecutar. Al empezar vale 0×000. Así que la CU pone en el bus A el contenido del PC, que es 0×000, y lee de la MM esa posición. Por lo tanto, el contenido de esa instrucción acabará en el bus D, y lo podrá leer la CU. Entonces la CU “ejecutará” esa instrucción. Dependiendo de la instrucción que sea, “ejecutar” significará unas cosas u otras; luego veremos las instrucciones que hay y cómo se “ejecutan”.
Después de “ejecutar” la instrucción, incrementa PC en 1 (aunque veremos que algunas instrucciones pueden cambiar esto). Y vuelve a empezar el ciclo. Es decir: como ahora PC vale 0×001, cogemos la segunda instrucción, la ejecutamos e incrementamos PC: ahora vale 0×002. Y vuelta a empezar…
Vamos a verlo otra vez, con otras palabras. Ya te digo que esto es probablemente lo más importante de toda la serie.
- Ponemos el valor de PC en el bus A. Ponemos el bit R/W’ a 1. Leemos la instrucción del bus D.
- Dependiendo de la instrucción que hemos leído, haremos cosas distintas (luego veremos todas las instrucciones). Esas “cosas distintas” implicarán dar microórdenes al resto de componentes del sistema: leer una nueva posición de memoria, escribir en una posición de memoria, darle “capones” al acumulador… incluso es posible (ocurre muy a menudo) que estas “cosas” impliquen más de un tick de reloj. No es problema.
- Actualizamos el valor de PC, bien incrementándolo en 1, bien usando lo que corresponda consecuencia del paso 2 (ya que hay instrucciones que lo que hacen precisamente es modificar PC).[1]
- Volvemos al paso 1.
Relee esos párrafos hasta que los hayas interiorizado, porque toda la grandeza de la arquitectura de von Neumann está resumida en ellos (a veces los grandes avances de la ciencia se pueden resumir en un par de frases).
Vamos a ver entonces qué instrucciones tenemos disponibles y cómo se codifican. Tenemos 8 instrucciones posibles:
Número |
Mnemotécnico |
¿Qué hace? |
¿Cómo lo hace? |
0000 (0) |
NOP |
No Operation. Operación que no hace nada. Parece un poco estúpida… pues no; luego veremos para qué sirve. |
En fin… no hace nada, simplemente deja pasar un tick de reloj. |
0001 (1) |
ST <addr> |
Almacena (del inglés store) el contenido del acumulador en la posición de memoria <addr>. addr es del inglés address, dirección. |
Pone en el bus A lo que viene en <addr>, pone R/W’ a 0, activa la salida del acumulador hacia el bus D. |
0010 (2) |
LD <addr> |
Carga (del inglés load) en el acumulador el contenido de la posición de memoria <addr>. |
Pone en el bus A lo que viene en <addr>, pone R/W’ a 1, ordena a la ALU la operación TRANS. |
0011 (3) |
ADD <addr> |
Suma (del inglés add) el contenido del acumulador con el contenido de la posición de memoria <addr>, y deja el resultado en el acumulador. |
Pone en el bus A lo que viene en <addr>, pone R/W’ a 1, ordena a la ALU la operacion SUM. |
0100 (4) |
BR <addr> |
Salta (del inglés branch) la ejecución a la posición <addr>. Es decir, en vez de continuar ejecutando la instrucción de PC+1, continúa ejecutando en la posición de memoria <addr> |
Pone en el bus A lo que viene en <addr>, pone R/W’ a 1, y traspasa a PC el contenido del bus D. |
0101 (5) |
BZ <addr> |
Del inglés branch if zero, salta si es cero. Es decir, lo mismo que BR, pero solo si el contenido del acumulador es un 0. |
Lo mismo que BR, pero solo si el contenido del acumulador es un 0. |
0110 (6) |
CLR |
Limpia (del inglés clear) el acumulador. Es decir, pone un 0 en él. |
Ordena a la ALU la operación CERO. |
0111 (7) |
DEC |
Decrementa (del inglés decrease) el acumulador y deja el resultado en el propio acumulador. |
Ordena a la ALU la operación DEC. |
¿Cómo codificamos las operaciones? Dado que nuestras palabras son de 16 bits, vamos a hacerlo de la siguiente forma:
Es decir, <addr> mide 12 bits, que es justo lo que mide nuestro bus A.[2] Fíjate en el detalle de que, aunque hay solo 8 operaciones, codificamos la operación con 4 bits. O bien podríamos tener hasta 16 operaciones o bien nos valdría con 3 bits… supongo que te imaginarás que lo hacemos así porque en el futuro añadiremos más operaciones. Date cuenta también de que hay tres operaciones en las que <addr> no se usa para nada… bueno, pues nada, simplemente se ignora lo que ponga ahí y listo.
Por lo tanto, si queremos que el ordenador ejecute la instrucción “salta a la posición de memoria 0xabc” lo que haremos es escribir: 0100 1010 1011 1100. Si queremos que ejecute una instrucción, y luego otra y otra, pues las ponemos todas en la memoria y listo. Fácil, ¿verdad?[3]
Pero claro, si tenemos un software que tenga más de dos o tres instrucciones, es imposible entenderlo, con tantos 0s y 1s en fila. Ahí es donde entra el mnemotécnico. No solemos describir el programa poniendo sus chorros de 1s y 0s, sino en una cosa que llamamos “lenguaje ensamblador” (assembler, en inglés). El lenguaje ensamblador no es más que usar el mnemotécnico para las las instrucciones en lugar del chorro de 0s y 1s.[4]
Así, si queremos poner la instrucción de antes “salta a la posición de memoria 0xabc”, lo que escribiremos es “BR 0xabc”. Por ejemplo, podemos decir que en la memoria a partir de la posición 0×000 tenemos el siguiente programa:
LD 0x010 ST 0x800 LD 0x011 ST 0x801 LD 0x012 ST 0x802 LD 0x013 ST 0x803 NOP BR 0x008 NOP NOP NOP NOP NOP NOP 0x0048 0x006f 0x006c 0x0061
No vamos a ver el programa ahora, eso lo veremos en el próximo capítulo, simplemente quédate con la idea de que ese chorro de mnemotécnicos, ese ensamblador, podemos convertirlo automáticamente a un montón de 0s y 1s y esos 0s y 1s son los que están de verdad en la memoria y sabe interpretar el ordenador. ¿Entendido hasta aquí?
Entonces solo nos queda saber cómo ejecutar esas instrucciones. Porque en la tabla hemos dicho en román paladino cómo se ejecutan esas instrucciones, pero de ahí a lo que realmente hace la unidad de control… hay un mundo… ¿Sí?
¿O no?
…
Pues no. Aunque no te lo creas, ya sabes hacerlo. Fíjate en la unidad de control, que hasta ahora hemos dejado un poco de lado. Ella es la encargada de dar esas microórdenes a los demás componentes. Pero fíjate en lo siguiente:
- La CU tiene un estado. El estado es el contenido del PC junto con algunos biestables adicionales para indicar en cuál de los pasos del prodecimiento estamos (en nuestro ordenador son muy pocos pasos, pero en una CPU real podrían ser muchos más).
- Podemos definir el estado siguiente en base al estado actual más las entradas (lo que viene por el bus D, es decir, la instrucción y su <addr> si aplica).
- Podemos definir la salida (las microórdenes y lo que escribimos en el bus A) en base al estado y las entradas.
- Por lo tanto, ¡podemos diseñar un circuito secuencial que lo haga!
Así que, aunque no lo diseñemos, podríamos, si quisiéramos, diseñar completamente la CU que hace esto.[5]
¡Ya está! ¡Ya hemos conseguido que funcione nuestro ordenador! Va ejecutando las instrucciones que se encuentra en la memoria principal, empezando por la 0×000 y a partir de ahí lo que le digan. Es un invento maravilloso.
Bueno, nos queda la operación NOP, que hemos visto, pero que nos parece un poco estúpida. En realidad en un ordenador como el que hemos diseñado sí que es estúpida. Pero en un ordenador moderno, la operación NOP tiene un papel fundamental.
Fíjate en que el ordenador no deja nunca de ejecutar cosas. Siempre está ejecutando algo. Si no queremos hacer nada, por ejemplo porque estamos esperando, él sigue ejecutando cosas… ¿qué ejecutará en ese caso? En esos casos, lo que le ordenamos es que ejecute la instrucción NOP… una y otra vez, y otra y otra… hasta que el usuario o alguno de los periféricos o alguien haga algo. En un ordenador antiguo, ejecutar NOP o simplemente seguir sumando números arbitrarios es lo mismo. Pero en ordenadores modernos, cuando la CPU ejecuta NOPs, sabe que no tiene nada que hacer. Cuando lleva muchos NOPs, sabe que está perdiendo el tiempo (es decir, gastando energía) a lo tonto, así que puede tomar medidas de ahorro: puede, por ejemplo, ralentizar la señal de reloj. Ya que lo que está haciendo es “nada”, puede hacer “nada” muy despacio, que el usuario no se va a enfadar.
En el próximo capítulo veremos cómo hacer algunos programitas sencillitos… bueno, quizá no tan sencillos. Ah, y también veremos qué es lo que hace el programa que hemos puesto más arriba.
- Por razones de eficiencia, lo de “incrementar PC en 1″ se hace ya en el paso 1, y como parte del paso 2 se modifica PC si toca (es decir, que a lo mejor incrementamos PC en 1 para nada… no importa). Es decir, este paso 3 en realidad no existe. Pero insisto: esto es solo por cuestiones de eficiencia; conceptualmente el proceso tiene esos tres pasos. [↩]
- Obviamente, no es casualidad. [↩]
- Queda la duda aún de cómo rellenamos la memoria inicialmente con esas instrucciones. Eso lo veremos más adelante, mucho más adelante. De momento supón que ha llegado allí manualmente o algo así, y listo. [↩]
- A menudo en el ensamblador real se usan más cosas, casi tantas que parece un lenguaje de programación sencillito… lo veremos. [↩]
- En realidad, en ordenadores reales modernos las operaciones son tan complejas que la CU no es simplemente un circuito secuencial, sino que es, en sí misma, un nuevo microordenador. Entonces se dice que la CU es “microprogramada”. Por contraposición se dice que esta CU está cableada. [↩]
The Computador mágico XXII – Ordenador C16A II: funcionamiento by , unless otherwise expressly stated, is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 2.5 Spain License.
{ 2 } Comentarios
Bien explicado. Que tiempos aquellos del código máquina.
“Vamos a ver entonces ahora” creo que sobra algo.
Corregido, gracias.
Escribe un comentario