El nivel de
microarquitectura
El nivel que esta
arriba de lógica digital es el de la microarquitectura. Su tarea es implementar
el nivel ISA (arquitectura de conjunto de instrucciones) que esta arriba de el.
El diseño del nivel de microarquitectura depende de la ISA que se esta
implementando, asi como los objetivos de costo y desempeño de la computadora. Muchas
ISA modernas, en especial los diseños RISC, tienen instrucciones sencillas que
por lo regular pueden ejecutarse en un solo ciclo de reloj. Las ISA mas
complejas, como la de Pentium II, podrían requerir muchos ciclos para ejecutar
una sola instrucción. Dicha ejecución podría requerir encontrar los operandos
en la memoria, leerlos y almacenar
resultados de vuelta en la memoria. La secuencia de operaciones dentro de una
sola instrucción con frecuencia obliga a adoptar un diferente enfoque de
control que con las ISA sencillas.
4.1 Un ejemplo de
microarquitectura
Lo ideal sería
comenzar explicando los principios generales del diseño de microarquitecturas.
Lo malo es que no existen principios generales; cada una es un caso especial.
Por tanto, optaremos por analizar un ejemplo detallado. Como ejemplo de ISA
hemos escogido un subconjunto de la maquina virtual Java. Este subconjunto
contiene solo instrucciones para manejar enteros por lo que lo hemos llamado
IJVM (Integer Java Virtual Machine).
Comenzaremos por
describir la microarquitectura sobre la cual implementaremos IJVM. La IJVM
tiene algunas instrucciones relativamente complejas. Muchas arquitecturas de
este tipo se han implementado con microprogramación. Aunque IJVM es pequeña, es
un buen punto de partida para describir el control y las secuencias de las
instrucciones.
Nuestra
microarquitectura contendrá un microprograma (en ROM) cuya tarea es buscar,
decodificar y ejecutar instrucciones IJVM. No podemos usar el interprete de JVM
de Sun en vez del microprograma porque necesitamos un microprograma diminuto
que alimente de forma eficiente las compuertas individuales del hardware real.
El interprete de JVM de Sun, en cambio, se escribió en C para ser
transportable, y no puede controlar el hardware con el nivel de detalle que
necesitamos. Puesto que el hardware real empleado consite únicamente en los
componentes básicos que describimos en el capitulo 3, en teoría, después de
entender plenamente este capitulo, el lector deberá poder salir y comprar una
gran bolsa llena de transistores, y construir este subconjunto de la maquina
JVM. Un modelo conveniente para el diseño de la microarquitectura es ver el
diseño como un problema de programación, en el que cada instrucción en el nivel
ISA es una función que el programa maestro debe invocar. En este modelo el
programa maestro es un ciclo infinito simple que determina cual función se
invocara, llama a la función, y vuelve a comenzar.
El microprograma
tiene un conjunto de variables, llamadas estados de la computadora, al que
todas las funciones tienen acceso. Cada función modifica al menos una de las
variables que constituyen el estado. Por ejemplo, el contador de programa (PC)
forma parte del estado; indica la localidad de memoria que contiene la
siguiente función (o sea, instrucción de ISA) que se ejecutará. Durante la
ejecución de cada instrucción el PC se incrementa para apuntar a la siguiente
instrucción por ejecutar.
Las instrucciones de IJVM son cortas y
precisas. Cada instrucción tiene unos cuantos campos, por lo regular uno o dos,
cada uno de los cuales tiene un propósito especifico. El primer campo de cada
instrucción es el código de operación (opcode) que identifica la instrucción
como, por ejemplo, ADD (sumar) o BRANCH (ramificar) o algo mas. Muchas instrucciones
tienen un campo adicional que especifica el operando. Por ejemplo, las
instrucciones que accesan una variable local necesitan un campo para indicar
cual variable. Este modelo de ejecución, llamado ciclo de búsqueda-ejecucion,
es útil en lo abstracto y también podría ser la base para implementar ISA como
IJVM que tienen instrucciones complejas.
La trayectoria de datos es la parte de la CPU que contiene la ALU, sus entradas y sus salidas. La trayectoria de
datos de nuestro ejemplo de microarquitectura se muestra en la figura 4-1. Si
bien esta trayectoria de datos se optimó minuciosamente para interpretar programas
en IJVM, es muy similar al que se usa en la mayor parte de las máquinas. Esta
trayectoria contiene varios registros de 32 bits, a los que hemos asignado
nombres simbólicos como PC, SP y MDR. Aunque algunos de estos nombres son
conocidos, es importante entender que estos registros pueden accesarse sólo en
el nivel de microarquitectura (por el micro- programa). Reciben estos nombres
porque casi siempre contienen un valor que corresponde a la variable del mismo
nombre en la arquitectura del nivel ISA. Casi todos los registros pueden
colocar su contenido en el bus B. La salida de la ALU alimenta al desplazador y
luego al bus C, cuyo valor puede escribirse en uno o más registros al mismo
tiempo. No existe un bus A por el momento; lo añadiremos después.
La ALU es idéntica a la que se muestra en la
figura 3-19 y la figura 3-20. Su función está determinada por seis líneas de
control. La línea diagonal corta rotulada con “6” en la figura 4-1 indica que
hay seis líneas de control de la ALU. Éstas son F0 y F, para
determinar el funcionamiento de la ALU, ENA y ENB para
habilitar individualmente las salidas, INVA para invertir la entrada izquierda, e INC para
forzar un acarreo de entrada en el bit de orden bajo, lo que en realidad suma 1
al resultado. Sin embargo, no todas las 64 combinaciones de las líneas de
control de la ALU hacen algo útil.
Algunas de las combinaciones más
interesantes se muestran en la figura 4-2. No todas estas funciones se
necesitan en la IJVM, pero muchas de ellas serían útiles en la JVM completa.
En muchos casos hay varias posibilidades para obtener el mismo resultado. En
esta tabla, + significa suma aritmética y -significa resta aritmética, de modo
que, por ejemplo, -A indica el complemento a 2 de A.
La ALU de la figura 4-1 necesita dos
entradas de datos: una entrada izquierda (A) y una entrada derecha (B). La
entrada izquierda tiene conectado un registro de retención, H. La entrada
derecha está conectada al bus B, que puede cargarse desde cualquiera de nueve
orígenes, como indican las nueve flechas grises que lo tocan. Un diseño
alternativo, con dos buses completos, tiene un conjunto diferente de ventajas y
desventajas, mismas que veremos más adelante en este capítulo.
pueden colocar su contenido en el
bus B. La salida de la ALU alimenta al desplazador y luego al bus C, cuyo valor
puede escribirse en uno o más registros al mismo tiempo. No existe un bus A por
el momento; lo añadiremos después.
La ALU es idéntica a la que se
muestra en la figura 3-19 y la figura 3-20. Su función está determinada por
seis líneas de control. La línea diagonal corta rotulada con “6” en la figura
4-1 indica que hay seis líneas de control de la ALU. Éstas son F0 y F, para determinar el funcionamiento
de la ALU, ENA y ENB para
habilitar individualmente las salidas, INVA para
invertir la entrada izquierda, e INC para forzar un acarreo de entrada en el
bit de orden bajo, lo que en realidad suma 1 al resultado. Sin embargo, no
todas las 64 combinaciones de las líneas de control de la ALU hacen algo útil.
Algunas de
las combinaciones más interesantes se muestran en la figura 4-2. No todas estas
funciones se necesitan en la IJVM, pero muchas de ellas serían útiles en la JVM
completa. En muchos casos hay varias posibilidades para obtener el mismo
resultado. En esta tabla, + significa suma aritmética y -significa resta
aritmética, de modo que, por ejemplo, -A
indica el
complemento a 2 de A.
La ALU de la
figura 4-1 necesita dos entradas de datos: una entrada izquierda (A) y una
entrada derecha (B). La entrada izquierda tiene conectado un registro de
retención, H. La entrada derecha está conectada al bus B, que puede cargarse
desde cualquiera de nueve orígenes, como indican las nueve flechas grises que
lo tocan. Un diseño alternativo, con dos buses completos, tiene un conjunto
diferente de ventajas y desventajas, mismas que veremos más adelante en este
capítulo.
H se puede cargar inicialmente
escogiendo una función de la ALU que se limite a pasar la entrada derecha (del
bus B) hasta la salida de la ALU. Una función así sería sumar las entradas de
la ALU, sólo que con EN A deshabilitada para que la entrada
izquierda sea forzosamente cero. Sumar cero al valor del bus B produce ese
mismo valor. Luego, este resultado puede pasarse por el desplazador sin
modificación y guardarse en H.
Además de las funciones
anteriores, se pueden usar otras dos líneas de control de forma independiente
para controlar la salida de la ALU. SLL8 (desplazamiento lógico a la izquierda,
Shift Left Logical) desplaza el contenido un byte a la izquierda, llenando los ocho
bits menos significativos con ceros. SRA1 (desplazamiento
aritmético a la derecha, Shift Right Arithmetic) desplaza el contenido un bit a
la derecha, sin modificar el bit más significativo.
Es posible
explícitamente leer y escribir el mismo registro en un ciclo. Por ejemplo, está
permitido colocar SP en el bus B, deshabilitar la entrada izquierda de la ALU,
habilitar la señal INC y guardar el resultado en SP con lo que se incrementa SP
en 1 (vea la octava línea de la figura 4-2). ¿Cómo puede leerse y escribirse un
registro en el mismo ciclo sin producir basura? La solución es que la lectura y
la escritura se realizan realmente en diferentes momentos dentro del ciclo.
Cuando un registro se selecciona como entrada derecha de la ALU, su valor se
coloca en el bus B al principio del ciclo y se mantiene ahí continuamente
durante todo el ciclo. Luego, la ALU efectúa su trabajo y produce un resultado
que pasa al bus C a través del desplazador. Cerca del final del ciclo, cuando
se sabe que las salidas de la ALU y del desplazador están estables, una señal
de reloj activa el almacenamiento del bus C en uno más de los registros. Uno de
estos registros bien podría ser el que proporcionó su entrada al bus B. La
temporización precisa de la trayectoria de datos hace posible leer y escribir
el mismo registro en un ciclo, como se describe en seguida.
Temporización
de la trayectoria de datos
La temporización de estos sucesos
se muestra en la figura 4-3. Aquí se produce una pulsación corta al principio
de cada ciclo de reloj. La pulsación se puede sacar del reloj principal, como
se muestra en la figura 3-21(c). En el flanco descendente de la pulsación, ya
están preparados los bits que alimentarán todas las compuertas. Esto tarda un
tiempo finito conocido, Aw. Luego se selecciona el registro
que se necesita en el bus B y su valor se pasa a ese bus. Se requiere un tiempo
Ax para que el
valor se estabilice. Luego la ALU y el desplazador comienzan a operar con
datos válidos. Después de un tiempo Ay adicional, las salidas de la ALU y el
desplazador se han estabilizado. Después de otro intervalo Az, los resultados
se han propagado por el bus C hasta los registros, en los que pueden cargarse
durante el flanco ascendente de la siguiente pulsación. La carga debe ser
disparada por flanco y rápida, de modo que aun si algunos de los registros de
entrada se modifican, los efectos no se harán sentir en el bus C sino hasta
mucho después de haberse cargado todos los registros. También en el flanco ascendente
de la pulsación, el registro que alimenta al bus B deja de hacerlo, en
preparación del siguiente ciclo. MPC, MIR y la memoria se mencionan en la
figura; exploraremos sus papeles en breve.
Es importante darse cuenta de que
aunque no hay elementos de almacenamiento en la trayectoria de datos, hay un
tiempo de propagación finito a través suyo. Modificar el valor que está en el
bus B no hace que el bus C cambie sino hasta después de un tiempo finito
(debido a los retrasos finitos en cada paso). Por consiguiente, aun si una
operación de almacenamiento modifica uno de los registros de entrada, el valor
ya estará guardado a salvo en el registro mucho antes de que el valor (ahora
incorrecto) que se está colocando en el bus B (o en H) pueda llegar a la ALU.
Para que este diseño funcione se
requiere una temporización rígida, un ciclo de reloj largo, un tiempo de
propagación mínimo conocido a través de la ALU y un cargado rápido de los
registros desde el bus C. Sin embargo, con una ingeniería cuidadosa se puede
diseñar la trayectoria de datos de modo que funcione correctamente.
Una forma un tanto distinta de
ver la trayectoria de datos es dividirlo conceptualmente en subciclos
implícitos. El inicio del subciclo 1 se dispara por el flanco descendente del
reloj. A continuación mostramos las actividades que ocurren durante los
subciclos, junto con las longitudes de los subciclos (entre paréntesis).
1.
Se preparan
las señales de control (Aw).
2.
Los
registros se cargan en el bus B (Ax).
3.
La ALU y el
desplazador operan (Ay).
4.
Los
resultados se propagan por el bus C de regreso a los registros (Az).
En el flanco ascendente del
siguiente ciclo de reloj, los resultados se almacenan en los registros.
Dijimos que la mejor forma de ver
los subciclos es como algo implícito. Con esto queremos decir que no
hay pulsaciones de reloj ni otras señales implícitas que indiquen a la ALU
cuándo debe operar o que digan a los resultados que entren en el bus C. En
realidad, la ALU y que está en el bus B no hace que el bus C cambie sino hasta
después de un tiempo finito (debido a los retrasos finitos en cada paso). Por
consiguiente, aun si una operación de almacenamiento modifica uno de los
registros de entrada, el valor ya estará guardado a salvo en el registro mucho
antes de que el valor (ahora incorrecto) que se está colocando en el bus B (o
en H) pueda llegar a la ALU.
Para que este diseño funcione se
requiere una temporización rígida, un ciclo de reloj largo, un tiempo de
propagación mínimo conocido a través de la ALU y un cargado rápido de los
registros desde el bus C. Sin embargo, con una ingeniería cuidadosa se puede
diseñar la trayectoria de datos de modo que funcione correctamente.
Una forma un tanto distinta de
ver la trayectoria de datos es dividirlo conceptualmente en subciclos
implícitos. El inicio del subciclo 1 se dispara por el flanco descendente del
reloj. A continuación mostramos las actividades que ocurren durante los
subciclos, junto con las longitudes de los subciclos (entre paréntesis).
1.
Se preparan
las señales de control (Aw).
2.
Los
registros se cargan en el bus B (Ax).
3.
La ALU y el
desplazador operan (Ay).
4.
Los
resultados se propagan por el bus C de regreso a los registros (Az).
En el flanco ascendente del
siguiente ciclo de reloj, los resultados se almacenan en los registros.
Dijimos que
la mejor forma de ver los subciclos es como algo implícito. Con esto queremos decir que no
hay pulsaciones de reloj ni otras señales implícitas que indiquen a la ALU
cuándo debe operar o que digan a los resultados que entren en el bus C. En
realidad, la ALU y
el desplazador están operando todo el tiempo. Sin embargo, sus entradas son basura hasta un tiempo Aw + Ax después del flanco descendente del reloj. De igual manera, sus salidas son basura hasta que ha transcurrido Aw + Ax + Ay después del flanco descendente del reloj. Las únicas señales explícitas que controlan la trayectoria de datos son el flanco descendente del reloj, que inicia el ciclo de la trayectoria de datos, y el flanco ascendente del reloj, que carga los registros a partir del bus C. Las otras fronteras de los subciclos están determinadas implícitamente por los tiempos de propagación inherentes a los circuitos participantes. Es responsabilidad de los ingenieros de diseño asegurarse de que el tiempo Aw + Ax + Ay + Az termine con tiempo de sobra antes del flanco ascendente del reloj, si se quiere que el cargado de los registros funcione siempre.
el desplazador están operando todo el tiempo. Sin embargo, sus entradas son basura hasta un tiempo Aw + Ax después del flanco descendente del reloj. De igual manera, sus salidas son basura hasta que ha transcurrido Aw + Ax + Ay después del flanco descendente del reloj. Las únicas señales explícitas que controlan la trayectoria de datos son el flanco descendente del reloj, que inicia el ciclo de la trayectoria de datos, y el flanco ascendente del reloj, que carga los registros a partir del bus C. Las otras fronteras de los subciclos están determinadas implícitamente por los tiempos de propagación inherentes a los circuitos participantes. Es responsabilidad de los ingenieros de diseño asegurarse de que el tiempo Aw + Ax + Ay + Az termine con tiempo de sobra antes del flanco ascendente del reloj, si se quiere que el cargado de los registros funcione siempre.
Operación de la memoria
Nuestra máquina tiene dos formas
diferentes de comunicarse con la memoria: un puerto de memoria de 32 bits
direccionable por palabra y un puerto de memoria de 8 bits direccionable por
byte. El puerto de 32 bits está bajo el control de dos registros, MAR (registro
de dirección de memoria, Memory Address Register) y MDR (registro de datos de
memoria, Memory Data Register), como se muestra en la figura
4-1. El puerto de ocho bits está bajo el control de un registro, PC, que lee un
byte y lo coloca en los ocho bits de orden bajo de MBR. Este puerto sólo puede
leer datos de la memoria; no puede escribir datos en la memoria.
Cada uno de estos registros (y
todos los demás registros de la figura 4-1) operan con una o dos señales de
control. Una flecha blanca bajo un registro indica una señal de control que
habilita la colocación de la salida del registro en el bus B. Puesto que MAR no
está conectado con el bus B, no tiene una señal de habilitación. H tampoco
tiene una porque siempre está habilitado, al ser la única posible entrada
izquierda de la ALU.
Una flecha negra bajo un registro
indica una señal de control que escribe (es decir, carga) el registro a partir
del bus C. Puesto que MBR no se puede cargar a partir del bus C, no tiene una
señal de escritura (aunque sí tiene otras dos señales de habilitación, que se
describirán más adelante). Para iniciar una lectura o escritura de memoria, es
preciso cargar los registros de memoria apropiados, y luego emitir una señal de
lectura o escritura a la memoria (no se muestra en la figura 4-1).
MAR contiene direcciones de palabra, de modo que los valores 0,1,2,
etc., se refieren a palabras consecutivas. PC contiene direcciones de byte, de modo que los valores 0,1,2,
etc., se refieren a bytes consecutivos. Así, colocar un 2 en PC e iniciar una
lectura de memoria hará que se lea el byte 2 de la memoria y se coloque en los
8 bits de orden bajo de MBR. Colocar un 2 en MAR e iniciar una lectura de
memoria hará que se lean los bytes 8-11 (o sea, la palabra 2) de la memoria y
se coloquen en MDR.
Esta diferencia en funcionalidad
es necesaria porque MAR y PC se usarán para hacer referencia a dos partes diferentes
de la memoria. La necesidad de esta distinción se aclarará posteriormente. Por
ahora, basta decir que la combinación MAR/MDR se usa para leer y escribir
palabras de datos en el nivel ISA, y la combinación PC/MBR sirve para leer el
programa ejecutable en el nivel ISA, que consiste en un flujo de bytes. Todos
los demás registros que contienen direcciones emplean direcciones de palabra,
como MAR.
En la
implementación física real, sólo hay una memoria y está organizada en bytes. Un
sencillo truco permite a MAR contar en palabras (lo cual es necesario debido a
la forma en que se definió la JVM) mientras la memoria física cuenta en bytes.
Cuando MAR se coloca en el bus de direcciones, sus 32 bits no se corresponden
directamente con las 32 líneas de dirección, 0-31. En vez de ello, el bit 0 de
MAR se conecta con la línea 2 del bus de direcciones, el bit 1 de MAR se
conecta con la línea 3 del bus de direcciones, y así. Los 2 bits superiores de
MAR se desechan porque sólo se necesitan para las direcciones de palabra
mayores que 232, que no son válidas en nuestra máquina de 4 GB. Con
esta transformación, cuando MAR es 1, se coloca la dirección 4 en el bus;
cuando MAR es 2, se coloca la dirección 8 en el bus, y así. Este truco se
ilustra en la figura 4-4.
Como ya se
mencionó, los datos leídos de la memoria a través del puerto de memoria de ocho
bits se devuelven en MBR, un registro de 8 bits. MBR puede copiarse en el bus B
de una de dos formas: sin signo o con signo. Cuando se necesita un valor sin
signo, la palabra de 32 bits que se coloca en el bus B contiene el valor de MBR
en los 8 bits de orden bajo y ceros en los 24 bits superiores. Los valores sin
signo son útiles como índices de tablas, o cuando es preciso armar un entero de
16 bits con dos bytes (sin signo) consecutivos del flujo de instrucciones.
La otra opción para convertir el
MBR de 8 bits en una palabra de 32 bits es tratarlo como un valor con signo
entre -128 y +127 y usar este valor para generar una palabra de 32 bits que
tenga el mismo valor numérico. Esta conversión se efectúa copiando el bit de
signo de MBR (el bit de la extrema izquierda) en las 24 posiciones de bit
superiores del bus B, proceso que se llama extensión del signo. Cuando se
escoge esta opción, los 24 bits superiores serán todos ceros o todos unos,
dependiendo de si el bit de la izquierda del MBR es 0 o 1.
La decisión de convertir el MBR
de 8 bits en un valor de 32 bits con o sin signo en el bus B depende de cuál de
las dos líneas de control (flechas blancas debajo de MBR en la figura 4-1) está
habilitada. Hay dos flechas porque se necesitan estas dos opciones. La
capacidad de hacer que el MBR de 8 bits actúe como una fuente de 32 bits para
el bus B se indica con el rectángulo punteado a la izquierda de MBR en la
figura.
En la implementación
física real, sólo hay una memoria y está organizada en bytes. Un sencillo truco
permite a MAR contar en palabras (lo cual es necesario debido a la forma en que
se definió la JVM) mientras la memoria física cuenta en bytes. Cuando MAR se
coloca en el bus de direcciones, sus 32 bits no se corresponden directamente
con las 32 líneas de dirección, 0-31. En vez de ello, el bit 0 de MAR se
conecta con la línea 2 del bus de direcciones, el bit 1 de MAR se conecta con
la línea 3 del bus de direcciones, y así. Los 2 bits superiores de MAR se
desechan porque sólo se necesitan para las direcciones de palabra mayores que 232,
que no son válidas en nuestra máquina de 4 GB. Con esta transformación, cuando
MAR es 1, se coloca la dirección 4 en el bus; cuando MAR es 2, se coloca la
dirección 8 en el bus, y así. Este truco se ilustra en la figura 4-4.
Como ya se
mencionó, los datos leídos de la memoria a través del puerto de memoria de ocho
bits se devuelven en MBR, un registro de 8 bits. MBR puede copiarse en el bus B
de una de dos formas: sin signo o con signo. Cuando se necesita un valor sin
signo, la palabra de 32 bits que se coloca en el bus B contiene el valor de MBR
en los 8 bits de orden bajo y ceros en los 24 bits superiores. Los valores sin
signo son útiles como índices de tablas, o cuando es preciso armar un entero de
16 bits con dos bytes (sin signo) consecutivos del flujo de instrucciones.
La otra opción para convertir el
MBR de 8 bits en una palabra de 32 bits es tratarlo como un valor con signo
entre -128 y +127 y usar este valor para generar una palabra de 32 bits que
tenga el mismo valor numérico. Esta conversión se efectúa copiando el bit de
signo de MBR (el bit de la extrema izquierda) en las 24 posiciones de bit
superiores del bus B, proceso que se llama extensión del signo. Cuando se
escoge esta opción, los 24 bits superiores serán todos ceros o todos unos,
dependiendo de si el bit de la izquierda del MBR es 0 o 1.
La decisión de convertir el MBR
de 8 bits en un valor de 32 bits con o sin signo en el bus B depende de cuál de
las dos líneas de control (flechas blancas debajo de MBR en la figura 4-1) está
habilitada. Hay dos flechas porque se necesitan estas dos opciones. La
capacidad de hacer que el MBR de 8 bits actúe como una fuente de 32 bits para
el bus B se indica con el rectángulo punteado a la izquierda de MBR en la
figura.
4.1.2
Microinstrucciones
Para controlar la trayectoria de
datos necesitamos 29 señales. Éstas se pueden dividir en cinco grupos
funcionales, como se describe a continuación.
9 Señales para controlar la
escritura de datos del bus C en registros.
9 Señales para controlar la habilitación de
registros en el bus B para introducción en la ALU.
8 Señales para controlar las funciones de la
ALU y el desplazador.
2 Señales (que no se muestran) para indicar lectura/escritura de
memoria vía MAR/MDR.
1 Señal (que
no se muestra) para indicar obtención de memoria vía PC/MBR.
Los valores de estas 29 señales
de control especifican las operaciones durante un ciclo de la trayectoria de
datos. Un ciclo consiste en colocar valores de los registros en el bus B, propagar
las señales a través de la ALU y el desplazador, alimentarlas al bus C y por
último escribir los resultados en el registro o registros apropiados. Además,
si una señal de lectura de datos de memoria está habilitada, la operación de
memoria se inicia al final del ciclo de la trayectoria de datos, una vez que
se ha cargado MAR. Los datos de memoria están disponibles hasta el final del siguiente ciclo enMBRoMDRyse usa el ciclo que sigue. En otras palabras, una lectura
de memoria por cualquiera de los puertos, iniciada al final del ciclo k, proporciona datos que no se usan
en el ciclo k + 1, sino hasta el ciclo k
+ 2 o uno
posterior.
Este comportamiento que
aparentemente va contra la intuición se explica con la figura 4-3. Las señales
de control de la memoria no se generan en el ciclo de reloj 1 sino hasta
inmediatamente después de que MAR y PC se cargan en el flanco ascendente del
reloj, hacia el final del ciclo de reloj 1. Supondremos que la memoria coloca
sus resultados en los buses de memoria en el plazo de un ciclo, de modo que MBR
y/o MDR se puedan cargar en el siguiente flanco ascendente del reloj, junto
con los demás registros.
En otras palabras, cargamos MAR
al final de un ciclo de la trayectoria de datos e iniciamos la lectura de
memoria poco tiempo después. Por tanto, no podemos esperar realmente que los
resultados de una operación de lectura estén en MDR al principio del siguiente
ciclo, sobre todo si la pulsación de reloj es angosta. Si la memoria tarda un
ciclo de reloj, simplemente no hay suficiente tiempo. Debe transcurrir un
ciclo de la trayectoria de datos completa entre que se inicia una lectura de
memoria y que se usa el resultado. Desde luego, se efectúan otras operaciones
durante ese ciclo, pero no las que necesitan la palabra de la memoria.
El supuesto de que la memoria
tarda un ciclo en operar equivale a suponer que la tasa de aciertos en caché es
de 100%. Este supuesto nunca se cumple, pero la complejidad que implica tener
un ciclo de memoria de longitud variable es mayor que con la que queremos
lidiar aquí.
Puesto que MBR y MDR se cargan en
el flanco ascendente del reloj, junto con todos los demás registros, podrían
leerse durante ciclos en los que se está efectuando una nueva lectura de
memoria. Esa lectura devuelve los valores antiguos, ya que todavía no ha habido
tiempo de sobreescribirlos. No hay ambigüedad aquí; hasta que se carguen
valores nuevos en MBR y MDR en el flanco ascendente del reloj, estos registros
contendrán sus valores anteriores, los cuales se podrán usar. Cabe señalar que
es posible realizar lecturas una tras otra en dos ciclos consecutivos puesto
que una lectura sólo tarda un ciclo. Además, ambas memorias podrían operar al
mismo tiempo. Sin embargo, tratar de leer y escribir el mismo byte simultáneamente
produce resultados no definidos.
Si bien
podría ser deseable escribir la salida colocada en el bus C en más de un
registro, nunca es conveniente habilitar más de un registro para el bus B al mismo
tiempo. (De hecho, algunas implementaciones reales sufren daños físicos si se
hace esto.) Si se agregan unos circuitos más, podemos reducir el número de bits
necesarios para seleccionar una de las posibles fuentes que alimentan al bus B.
Sólo hay nueve registros de entrada que pueden alimentar al bus B (si contamos
por separado las versiones con signo y sin signo de MBR). Por tanto, podemos
codificar la información del bus B en cuatro bits y usar un decodificador para
generar las 16 señales de control, siete de las cuales no se necesitan. En un
diseño comercial, los arquitectos estarían fuertemente tentados a deshacerse de
uno de los registros para poder efectuar la tarea con sólo 3 bits. Como
académicos, podemos damos el lujo de desperdiciar un bit para contar con un
diseño más limpio y sencillo.
A estas alturas podemos controlar
la trayectoria de datos con 9 + 4 + 8 + 2+ 1 =24 señales, y por tanto 24 bits.
Sin embargo, estos 24 bits sólo controlan la trayectoria de datos durante un
ciclo. La segunda parte del control consiste en determinar qué debe hacerse en
el siguiente ciclo. Para incluir esto en el diseño del controlador, crearemos
un formato para describir las operaciones que se efectuarán usando los 24 bits
de control más dos campos adicionales: el campo NEXT_ADDRESS
y el campo JAM.
A
continuación explicaremos el contenido de cada uno de estos campos. La figura
4-5 muestra un posible formato, dividido en los seis grupos, y que contiene las
36 señales siguientes:
los cuales se podrán usar. Cabe
señalar que es posible realizar lecturas una tras otra en dos ciclos
consecutivos puesto que una lectura sólo tarda un ciclo. Además, ambas memorias
podrían operar al mismo tiempo. Sin embargo, tratar de leer y escribir el mismo
byte simultáneamente produce resultados no definidos.
Si bien
podría ser deseable escribir la salida colocada en el bus C en más de un
registro, nunca es conveniente habilitar más de un registro para el bus B al
mismo tiempo. (De hecho, algunas implementaciones reales sufren daños físicos
si se hace esto.) Si se agregan unos circuitos más, podemos reducir el número
de bits necesarios para seleccionar una de las posibles fuentes que alimentan
al bus B. Sólo hay nueve registros de entrada que pueden alimentar al bus B (si
contamos por separado las versiones con signo y sin signo de MBR). Por tanto,
podemos codificar la información del bus B en cuatro bits y usar un
decodificador para generar las 16 señales de control, siete de las cuales no se
necesitan. En un diseño comercial, los arquitectos estarían fuertemente
tentados a deshacerse de uno de los registros para poder efectuar la tarea con
sólo 3 bits. Como académicos, podemos damos el lujo de desperdiciar un bit para
contar con un diseño más limpio y sencillo.
A estas alturas podemos controlar
la trayectoria de datos con 9 + 4 + 8 + 2+ 1 =24 señales, y por tanto 24 bits.
Sin embargo, estos 24 bits sólo controlan la trayectoria de datos durante un
ciclo. La segunda parte del control consiste en determinar qué debe hacerse en
el siguiente ciclo. Para incluir esto en el diseño del controlador, crearemos
un formato para describir las operaciones que se efectuarán usando los 24 bits
de control más dos campos adicionales: el campo NEXT_ADDRESS
y el campo JAM.
A
continuación explicaremos el contenido de cada uno de estos campos. La figura
4-5 muestra un posible formato, dividido en los seis grupos, y que contiene las
36 señales siguientes:
Dir - Contiene la dirección de una
microinstrucción que podría ser la siguiente.
JAM - Determina cómo se selecciona la siguiente microinstrucción.
ALU
- Funciones
de la ALU y el desplazador.
C - Selecciona cuáles registros se escriben
desde el bus C.
Mem
- Funciones
de memoria.
B - Selecciona la fuente del bus
B; se codifica como se muestra.
El ordenamiento de los grupos es
arbitrario en principio, aunque en realidad se escogió con mucho cuidado a fin
de minimizar los cruces de líneas en la figura 4-6. Los cruces de línea en
diagramas esquemáticos como el de la figura 4-6 a menudo corresponden a cruces
de hilos en chips, que pueden causar problemas en los diseños bidimensionales y
deben evitarse.
4.1.3 Control de
microinstrucciones: el Mic-1
Hasta aquí hemos descrito cómo se
controla la trayectoria de datos, pero todavía no hemos explicado cómo se
decide cuál de las señales de control debe estar habilitada en cada ciclo. Esto
lo determina un secuenciador que se encarga de recorrer la
secuencia de operaciones necesarias para ejecutar una sola instrucción ISA.
El
secuenciador debe producir dos tipos de información en cada ciclo:
1.
El estado de
cada señal de control en el sistema.
2.
La dirección
de la microinstrucción que debe ejecutarse a continuación.
La figura 4-6 es un diagrama de
bloques detallado de la microarquitectura completa de nuestra máquina de
ejemplo, que llamaremos Mic-1. A primera vista este diagrama
podría parecer abrumador, pero vale la pena estudiarlo con detenimiento. Una
vez que entienda plenamente todos y cada uno de los bloques y líneas de esta
figura, habrá adelantado mucho en la comprensión del nivel de
microarquitectura. El diagrama de bloques tiene dos partes: la trayectoria de
datos, a la izquierda, que ya vimos con detalle, y la sección de control, a la
derecha, que examinaremos a continuación.
El elemento más grande e
importante de la porción de control de la máquina es una memoria llamada almacén
de control. Es
recomendable ver este “almacén” como una memoria que contiene todo el
microprograma, aunque a veces se implementa como un conjunto de compuertas
lógicas. En general, nos referiremos a él como almacén de control para evitar
confundirlo con la memoria principal, a la que se accede a través de MBR
y MDR.
Sin embargo,
en lo funcional, el almacén de control no es más que una memoria que contiene
microinstrucciones en lugar de instrucciones ISA. En nuestra máquina de
ejemplo, el almacén contiene 512 palabras, cada una de las cuales consiste en
una microinstrucción de 36 bits como las que se ilustran en la figura 4-5. En
realidad, no se necesitan todas estas palabras pero (por razones que se
explicarán en breve) necesitamos direcciones para 512 palabras distintas.
Es importante notar que el
almacén de control es muy diferente de la memoria principal: las instrucciones
de la memoria principal tienen la propiedad de que se ejecutan en orden según
su dirección (con excepción de las ramificaciones); las microinstrucciones no.
El acto de incrementar el contador de programa en la figura 2-3 expresa el
hecho de que la instrucción que se ejecutará por omisión después de la actual
es la que sigue a la actual en la memoria. Los microprogramas necesitan mayor
flexibilidad (porque las secuencias de microinstrucciones tienden a ser
cortas), así que generalmente r.o tienen esta propiedad. En lugar de esto, cada
microinstrucción especifica explícitamente su sucesora.
según su dirección (con excepción
de las ramificaciones); las microinstrucciones no. El acto de incrementar el
contador de programa en la figura 2-3 expresa el hecho de que la instrucción
que se ejecutará por omisión después de la actual es la que sigue a la actual
en la memoria. Los microprogramas necesitan mayor flexibilidad (porque las
secuencias de microinstrucciones tienden a ser cortas), así que generalmente
r.o tienen esta propiedad. En lugar de esto, cada microinstrucción especifica
explícitamente su sucesora.
Puesto que el almacén de control
es funcionalmente una memoria (sólo de lectura), necesita su propio registro
de direcciones de memoria y su propio registro de datos de memoria. No necesita
señales de leer y escribir, porque continuamente se está leyendo. Llamaremos al
registro de dirección de memoria del almacén de control MPC (contador de microprograma, MicroProgram
Counter). Este nombre
es irónico porque las localidades que contiene no están ordenadas,
explícitamente, de modo que el concepto de contar no es útil (pero, ¿quiénes
somos nosotros para discutir con la tradición?). El registro de datos de
memoria se llama MIR (registro
de microinstrucción, Microlnstruction Register). Su función es almacenar la
microinstrucción actual, cuyos bits alimentan las señales de control que operan
la trayectoria de datos.
El registro MIR de la figura 4-6
contiene los mismos seis grupos de la figura 4-5. Los grupos Dir y J (por JAM)
controlan la selección de la siguiente microinstrucción y se estudiarán más
adelante. El grupo ALU contiene ocho bits que seleccionan la función de la ALU
y controlan el desplazador. Los bits C hacen que registros individuales carguen
la salida de la ALU desde el bus C. Los bits M controlan las operaciones de
memoria.
Por último, los cuatro bits
finales controlan el decodificador que determina qué se coloca en el bus B. En
este caso hemos escogido un decodificador estándar 4 a 16, aunque sólo se
requieren nueve posibilidades. En un diseño más afinado se usaría un
decodificador de 4 a 9. La ventaja aquí es poder usar un circuito estándar
tomado de una biblioteca de circuitos en lugar de diseñar uno a la medida. El
uso del circuito estándar es más fácil y reduce la posibilidad de introducir
errores. Crear circuitos propios ocupa menos área de chip pero tarda más y
podríamos tener errores.
La operación de la figura 4-6 es
como sigue. Al principio de cada ciclo de reloj (el flanco descendente del
reloj de la figura 4-3), MIR se carga con la palabra del almacén de control a
la que MPC apunta. El tiempo de carga de MIR se indica en la figura con Aw. Si pensamos en términos de
subciclos, MIR se carga durante el primero.
Una vez que la microinstrucción
está en MIR, las diversas señales se propagan a la trayectoria de datos. Se
coloca un registro en el bus B, la ALU sabe qué instrucción debe ejecutar y
ocurren muchas actividades más. Éste es el segundo subciclo. Después de un
intervalo Aw + Ax a partir del principio del
ciclo, las entradas de la ALU se han estabilizado.
Después de transcurrir un tiempo
Ay adicional, todo se ha calmado otra vez y las salidas de la ALU, N, Z y el
desplazador están estables. Luego, los valores de N y Z se guardan en un par de
flip-flops de un bit. Estos bits, al igual que todos los registros que se
cargan a partir del bus C y la memoria, se guardan en el flanco ascendente del
reloj, cerca del final del ciclo de la trayectoria de datos. La salida de la
ALU no se almacena, sólo se alimenta al desplazador. La actividad de la ALU y
el desplazador ocurre durante el subciclo 3.
Después de un intervalo
adicional, Az, la salida del desplazador ha
llegado a los registros por el bus C. Luego los registros pueden cargarse cerca
del final del ciclo (en el flanco ascendente de la pulsación de reloj de la
figura 4-3). El subciclo 4 consiste en el cargado de los registros y los
flip-flops N y Z; termina un poco después del flanco ascendente del reloj,
cuando todos los resultados se han guardado y los resultados de las operaciones
de memoria anteriores están disponibles, y se ha cargado MPC. Este proceso
continúa hasta que alguien se aburre y apaga la máquina.
Al mismo tiempo que controla la
trayectoria de datos, el microprograma tiene que determinar qué
microinstrucción ejecutará a continuación, porque no se ejecutan en el orden en
que aparecen en el almacén de control. El cálculo de la dirección de la
siguiénte microinstrucción se inicia una vez que MIR se ha cargado y
estabilizado. Primero se copia el campo NEXT_ADDRESS de 9 bits en MPC. Mientras
se está efectuando este copiado, se inspecciona el campo JAM. Si tiene el
valor 000, no se hace nada más; una vez que concluye el copiado de
NEXT_ADDRESS, MPC apunta a la siguiente microinstrucción.
Si uno o más de los bits de JAM
son 1, hay que trabajar más. Si JAMN está activado, el flip-flop N de un bit se
hace OR con el bit de orden alto de MPC. Del mismo modo, si JAMZ está
establecido, el flip-flop Z de un bit realiza la operación OR. Si ambos están
establecidos, se realiza la operación OR de ellos. Se necesitan los flip-flops
N y Z porque después del flanco ascendente del reloj (mientras el reloj está
alto), el B bus ya no está siendo controlado, y no puede suponerse que las
salidas de la ALU sigan siendo correctas. Guardar las banderas de estado de la
ALU en N y Z hace que los valores correctos se estabilicen y estén disponibles
para el cálculo de MPC, sin importar qué esté ocurriendo en tomo de la ALU.
En la figura 4-6 la lógica que
efectúa este cálculo se denomina “Bit alto”. La función booleana que calcula es
F= (JAMZ AND Z) OR (JAMN AND N) OR
NEXT_ADDRESS[8]
Observe que, en todos los casos,
MPC puede adoptar sólo uno de dos posibles valores:
1.
El valor de
NEXT_ADDRESS.
2.
El valor de
NEXT_ADDRESS después de que se realizó el OR del bit de orden alto con 1.
No existen otras posibilidades.
Si el bit de orden alto de NEXT_ADDRESS era 1, no tiene sentido usar JAMN o
JAMZ.
Observe que cuando todos los bits
de JAM son ceros, la dirección de la siguiente microinstrucción a ejecutar es
simplemente el número de nueve bits que está en su campo NEXT_ADDRESS. Si JAMN
o JAMZ son 1, hay dos posibles sucesores, NEXT_ADDRESS y el resultado de la
operación OR de NEXT_ADDRESS con 0x100 (suponiendo que NEXT_ADDRESS < OxFF).
(Cabe señalar que Ox indica que el número que sigue está en hexadecimal.) Este
punto se ilustra en la figura 4-7. La microinstrucción en curso, en la posición
0x75, tiene NEXT_ADDRESS = 0x92 y JAMZ = 1. Por tanto, la siguiente dirección
de la microinstrucción depende del bit Z almacenado en la operación de ALU
previa. Si el bit Z es 0, la siguiente microinstrucción provendrá de 0x92. Si
el bit Z es 1, la siguiente microinstrucción provendrá de 0x192.
El tercer bit del campo JAM es
JMPC. Si está activo, se efectúa la operación OR bit por bit de los ocho bits
de MBR con los 8 bits de orden bajo del campo NEXT_ADDRESS de la
microinstrucción en curso. El resultado se envía a MPC. El cuadro rotulado “O”
en la figura 4-6 efectúa un OR de MBR con NEXT_ADDRESS si JMPC es 1, pero se limita
a pasar NEXT_ADDRESS directamente a MPC si JMPC es 0. Cuando JMPC es 1, los
ocho bits de orden bajo de NEXT_ADDRESS normalmente son cero. El bit de orden
alto puede ser 0 o 1, de modo que el valor de NEXT_ADDRESS que se usa con JMPC
normalmente es 0x000 o 0x100. La razón para usar a veces 0x000 y a veces 0x100
se explicará más adelante.
Al mismo tiempo que controla la
trayectoria de datos, el microprograma tiene que determinar qué
microinstrucción ejecutará a continuación, porque no se ejecutan en el orden en
que aparecen en el almacén de control. El cálculo de la dirección de la
siguiénte microinstrucción se inicia una vez que MIR se ha cargado y
estabilizado. Primero se copia el campo NEXT_ADDRESS de 9 bits en MPC. Mientras
se está efectuando este copiado, se inspecciona el campo JAM. Si tiene el
valor 000, no se hace nada más; una vez que concluye el copiado de
NEXT_ADDRESS, MPC apunta a la siguiente microinstrucción.
Si uno o más de los bits de JAM
son 1, hay que trabajar más. Si JAMN está activado, el flip-flop N de un bit se
hace OR con el bit de orden alto de MPC. Del mismo modo, si JAMZ está
establecido, el flip-flop Z de un bit realiza la operación OR. Si ambos están
establecidos, se realiza la operación OR de ellos. Se necesitan los flip-flops
N y Z porque después del flanco ascendente del reloj (mientras el reloj está
alto), el B bus ya no está siendo controlado, y no puede suponerse que las
salidas de la ALU sigan siendo correctas. Guardar las banderas de estado de la
ALU en N y Z hace que los valores correctos se estabilicen y estén disponibles
para el cálculo de MPC, sin importar qué esté ocurriendo en tomo de la ALU.
En la figura 4-6 la lógica que
efectúa este cálculo se denomina “Bit alto”. La función booleana que calcula es
F= (JAMZ AND Z) OR (JAMN AND N) OR
NEXT_ADDRESS[8]
Observe que, en todos los casos,
MPC puede adoptar sólo uno de dos posibles valores:
1.
El valor de
NEXT_ADDRESS.
2.
El valor de
NEXT_ADDRESS después de que se realizó el OR del bit de orden alto con 1.
No existen otras posibilidades.
Si el bit de orden alto de NEXT_ADDRESS era 1, no tiene sentido usar JAMN o
JAMZ.
Observe que cuando todos los bits
de JAM son ceros, la dirección de la siguiente microinstrucción a ejecutar es
simplemente el número de nueve bits que está en su campo NEXT_ADDRESS. Si JAMN
o JAMZ son 1, hay dos posibles sucesores, NEXT_ADDRESS y el resultado de la
operación OR de NEXT_ADDRESS con 0x100 (suponiendo que NEXT_ADDRESS < OxFF).
(Cabe señalar que Ox indica que el número que sigue está en hexadecimal.) Este
punto se ilustra en la figura 4-7. La microinstrucción en curso, en la posición
0x75, tiene NEXT_ADDRESS = 0x92 y JAMZ = 1. Por tanto, la siguiente dirección
de la microinstrucción depende del bit Z almacenado en la operación de ALU
previa. Si el bit Z es 0, la siguiente microinstrucción provendrá de 0x92. Si
el bit Z es 1, la siguiente microinstrucción provendrá de 0x192.
El tercer
bit del campo JAM es JMPC. Si está activo, se efectúa la operación OR bit por
bit de los ocho bits de MBR con los 8 bits de orden bajo del campo NEXT_ADDRESS
de la microinstrucción en curso. El resultado se envía a MPC. El cuadro
rotulado “O” en la figura 4-6 efectúa un OR de MBR con NEXT_ADDRESS si JMPC es
1, pero se limita a pasar NEXT_ADDRESS directamente a MPC si JMPC es 0. Cuando
JMPC es 1, los ocho bits de orden bajo de NEXT_ADDRESS normalmente son cero. El
bit de orden alto puede ser 0 o 1, de modo que el valor de NEXT_ADDRESS que se
usa con JMPC normalmente es 0x000 o 0x100. La razón para usar a veces 0x000 y a
veces 0x100 se explicará más adelante.
La capacidad para calcular el OR de MBR con NEXT_ADDRESS y
almacenar el resultado en MPC hace posible una implementación eficiente de una
ramificación (salto) de múltiples vías. Observe que es posible especificar
cualquiera de 256 direcciones, determinada exclusivamente por los bits
presentes en MBR. En un uso típico, MBR contiene un código de operación, de
modo que el uso de JMPC hará que se seleccione para ejecutarse en seguida una
microinstrucción única para cada posible código de operación. Este método es
útil para saltar directamente a la función que corresponde al código de
operación encontrado.
Para la explicación que sigue es
crucial entender la temporización de la máquina, así que tal vez valga la pena
repetirla. Lo haremos en términos de subciclos, ya que esto es más fácil de
visualizar, los sucesos de reloj reales son el flanco descendente, que inicia
el ciclo, y el flanco ascendente, que carga los registros y los flip-flops
N y Z. Remítase por favor a la
figura 4-3.
Durante el subciclo 1, iniciado
por el flanco descendente del reloj, MIR se carga con la dirección que
actualmente está en MPC. Durante el subciclo 2, las señales de MIR se propagan
hacia afuera y el bus B se carga a partir del registro seleccionado. Durante el
subciclo 3, la ALU y el desplazador operan y producen un resultado estable.
Durante el subciclo 4, el bus C, los buses de memoria y los valores de la ALU se estabilizan. En el flanco
ascendente del reloj los registros se cargan a partir del bus C, los flip-flops
N y Z se cargan, y MBR y MDR
obtienen sus resultados de la operación de memoria que se inició al final del
ciclo de trayectoria de datos anterior (si se inició alguna de esas operaciones).
Tan pronto como está disponible MBR, MPC se carga en preparación para la
siguiente microinstrucción. Así MPC obtiene su valor en algún momento durante
la parte media del intervalo en el que el reloj está alto pero después de que
MBR/MDR están listos. Esto podría ser disparado por nivel (en lugar de por
flanco) o disparado por flanco con un retraso fijo después del flanco
ascendente del reloj. Lo único que importa es que MPC no se cargue sino hasta
que los registros de los que depende (MBR, N y Z) estén listos. Tan pronto
como el reloj baja, MPC puede direccionar el almacén de control y se puede
iniciar un ciclo nuevo.
Cabe señalar que cada ciclo es
autosuficiente: especifica qué se coloca en el bus B, qué deben hacer la ALU y
el desplazador, dónde debe almacenarse el valor del bus C y, por último, qué
valor debe tener MPC a continuación.
Vale la pena un último comentario
acerca de la figura 4-6. Hemos estado tratando a MPC como un registro normal,
con 9 bits de capacidad, que se carga mientras el reloj está alto. En realidad,
no hay necesidad de tener un registro ahí. Todas sus entradas se pueden
almacenar directamente hasta el almacén de control. En tanto estén presentes en
el almacén de control en el flanco descendente del reloj cuando MIR se selecciona
y lee, será suficiente. No hay necesidad de almacenarlas realmente en MPC. Por
esta razón, MPC bien podría implementarse como un registro virtual, que no es
más que un punto de reunión de señales, más parecido a un panel de conexiones
electrónico que a un registro real. Hacer que MPC sea un registro virtual
simplifica la temporización: ahora sólo ocurren sucesos en los flancos
descendentes y ascendentes del reloj, y en ningún otro momento. Pero si para
usted es más fácil pensar en MPC como un registro real, también es un punto de
vista válido.
4.2 UN EJEMPLO DE ISA: UVM
Continuemos nuestro ejemplo
presentando el nivel ISA de la máquina para ser interpretada por el
microprograma que se ejecuta en la microarquitectura de la figura 4-6 (IJVM).
Por comodidad, algunas veces nos referiremos a la arquitectura del conjunto de
instrucciones (ISA) como la macroarquitectura, a fin de contrastarla con la
microarquitectura. Sin embargo, antes de describir IJVM haremos una breve
digresión para motivar la descripción.
Prácticamente
todos los lenguajes de programación soportan el concepto de procedimientos
(métodos) que tienen variables locales. Estas variables pueden accesarse desde
el interior del procedimiento pero dejan de ser accesibles una vez que el
procedimiento ha regresado. Así, surge la pregunta: “¿en qué lugar de la
memoria deben guardarse estas variables?’’
La solución más sencilla, asignar a cada
variable una dirección de memoria absoluta, no funciona. El problema es que un
procedimiento puede llamarse a sí mismo. Estudiaremos estos procedimientos
recursivos en el capítulo 5. Por el momento, basta decir que si un procedimiento
se activa (es decir, se llama) dos veces, es imposible almacenar sus variables
en posiciones de memoria absolutas porque la segunda invocación interferirá la
primera.
En vez de ello, se usa una estrategia
distinta. Se reserva un área de memoria, llamada pila,
para
variables, pero las variables individuales no reciben direcciones absolutas
dentro de la pila. En vez de ello se ajusta un registro, digamos LV (Local
Variable) de modo
que apunte a la base de las variables locales del procedimiento en curso. En la
figura 4-8(a) se ha invocado un procedimiento A, que tiene las variables locales al,
a2 y a3, así que se ha reservado memoria
para sus variables locales a partir de la posición a la que LV apunta. Otro
registro, SP (Stock Pointer, apuntador de la pila), apunta a
la palabra más alta de las variables locales de A. Si LV es 100 y las palabras
son de 4 bytes, entonces SP será 108. Se hace referencia a las variables especificando
a qué distancia están de LV. La estructura de datos entre LV y SP (que incluye
las dos palabras a las que se apunta) es el marco
de variables locales de
A.
Consideremos ahora qué sucede si A llama a otro procedimiento, B. ¿Dónde deberán almacenarse las
cuatro variables locales de B (bl,
b2, b3, b4f>. Respuesta:
en la pila, encima de las de A, como se muestra en la figura
4-8(b). Observe que la llamada de procedimiento