Fundamentos de computadores

Anuncio
Fundamentos de computadores
TEMA 1 JUEGOS DE INSTRUCCIONES Y MODOS DE DIRECCIONAMIENTO
1. INSTRUCCIONES GENERALES
Ya conocemos la secuencia de ejecución de instrucciones estudiada en la
asignatura Fundamentos de Computadores 1: Fetch, decodificación, lectura de
datos, realización de la operación, almacenamiento del resultado y actualización del
contador de programas. Debido a que las instrucciones se realizan una detrás de la
otra, se habla de flujo de ejecución lineal, excepción hecha de las instrucciones
de bifurcación que modifican el contador de programa.
El juego de instrucciones de un computador está formado por el
conjunto de instrucciones de máquina que es capaz de ejecutar. Éste suele ser
reducido, de unas 60 instrucciones para procesadores RISC a unas 250 para
procesadores CISC y constituye una característica muy importante de su
arquitectura.
Se denomina lenguaje nativo de máquina aquel que ésta puede
entender de forma directa. Los términos de dicho lenguaje se encuentran
determinados por el juego de instrucciones y se deben expresar en forma de
cadenas de unos y ceros. Las instrucciones de máquina cumplen las siguientes
propiedades:
 Realizan una función sencilla única: suma, resta, AND….
 Operan sobre uno o dos operandos simples e un formato determinado.
 Trabaja en un formato sistemático y lo más compacto posible a fin de
minimizar las necesidades de memoria.
 Son autocontenidas, es decir, incluyen toda la información necesaria para
ser ejecutadas.
 Son independientes del contexto, su efecto no depende de ningún
elemento externo a la instrucción en sí (a excepción de las bifurcaciones
condicionales).
 La ejecución de las instrucciones es atómica, es decir, o se lleva a cabo
toda ella de forma indivisible o completa; o bien, no se ejecuta.
El modelo de programación del computador Von Neumann se compone de
varios elementos:
 Mapa de memoria: El espacio de todas las posiciones de memoria que la
máquina es capaz de direccional. El espacio viene limitado por la longitud
de las direcciones con las que el PC trabaja. Suponiendo que estas tengan
m bits, sus posibles valores van de 0 a 2m-1, y, por tanto, el mapa de
memoria es de 2m posiciones.
 Mapa de E/S: Permite la comunicación entre los periféricos y la Unidad
Central de Proceso.
 Registro de usuario: Es el banco de registros que el programador puede
manipular de forma directa con las instrucciones de máquina. Se
seleccionan con una dirección de pocos bits, puesto que suelen tener de 8
a 32 registros de usuario.
 Registro de estado: Depende en gran medida, de la arquitectura
específica de que se trate, y podemos tener: Bits de resultado (transporte,
positivo, negativo, cero, desbordamiento…); nivel de ejecución (en núcleo
o kernel donde se ejecutan todo el juego de instrucciones o de usuario
donde solo se llevan a cabo un subconjunto de los primeros); máscara de
interrupción….
2 · Fundamentos de computadores
En cuanto al formato de las instrucciones como hemos dicho que estas son
autocontenidas, deben prever toda la información necesaria para su ejecución; por
ello y, en principio, deben especificar los siguientes elementos:
 Operación a efectuar
 Operandos sobre los que actúa la operación anterior
 Formato de representación de los datos
 Ubicación del resultado, o sea, registro o dirección de memoria donde se
debe almacenar
 Identidad de la siguiente instrucción
Esto es así en teoría, en realidad se utiliza el concepto de información
implícita con la que determinados valores se dan por defecto y solo se especifican
si son diferentes a ellos. Por ejemplo la identidad de la instrucción siguiente se
sobreentiende que es la siguiente, no se especifica (solo en caso de instrucciones
de bifurcación).
Modelos de ejecución encontramos varios, dependiendo de los dispositivos
que encontremos, estos son: la pila, los registros de usuario y la memoria principal.
2. MODOS DE DIRECCIONAMIENTO
Un modo de direccionamiento constituye un procedimiento que permite
acceder a un operando de una instrucción, tanto si es un dato, como un resultado,
como la dirección de la instrucción hacia donde se bifurca.
Fundamentos de computadores · 3
A primera vista quizá sorprenda la tremenda variedad de modos de
direccionamiento, pero esto se debe a varias causas:
 Ahorro de espacio: Hay que reducir al máximo el número de bits utilizados
en la instrucción.
 Si se pretende que el código sea reubicable (se puede cargar a partir de
cualquier posición de memoria y se ejecuta de forma correcta) se precisan
modos de direccionamiento relativos.
 El manejo de tablas, vectores o matrices se simplifica con los modos de
direccionamiento relativos.
1. Direccionamiento inmediato: Únicamente puede utilizarse para datos y
consiste en incluir el valor del dato en la instrucción, es un método muy
poco flexible, puesto que el valor es fijo y constante.
2. Direccionamiento directo absoluto: Se utiliza para expresar que la
dirección obtenida es realmente la del operando de la instrucción; la
instrucción contiene la dirección del operando, tanto a registro como a
memoria (a registro suele utilizar menos bits pues hay menos registro de
usuario, 16 ó 32, a memoria ocupa más bits por la razón opuesta).
3. Direccionamiento directo relativo: La instrucción no contiene la
dirección real del operando, sino una dirección D (desplazamiento) relativa
a un puntero RB (registro base). La dirección real del operando se calcula
realizando la operación de suma D+RB. Este modo se caracteriza por ser
mucho más compacto (ya que el número d de bits requerido es menor que
el número m de bits utilizado en el modo absoluto a memoria), permite la
reubicación (solo hay que cambiar el registro base para que se modifiquen
las direcciones de todos los datos e instrucciones), se utiliza para recorrer
muchas estructuras de datos de manera eficiente; además como requiere
una operación de suma, muchos computadores incluyen una unidad
sumadora específica para el cálculo de dirección, así se ahorra utilizar la
que se encuentra en la UAL.
4. Direccionamiento relativo a registro base: Se utiliza como registro
base RB uno de los registros de usuario o uno especial de direcciones;
dicho registro no se modifica en la operación de direccionamiento.
5. Direccionamiento relativo al contador de programa: Se toma como
registro base el PC, por esta razón se obtiene la dirección sumando el
desplazamiento al PC. Este direccionamiento permite acceder a posiciones
de memoria que se encuentren próximas a la instrucción actual, lo que
provoca que sea muy adecuado para bifurcaciones de programa y para
construir bucles.
6. Direccionamiento relativo al registro índice: Es un registro cuyo
contenido se modifica al establecer el direccionamiento, sumándole o
restándole un incremento. Así se pueden recorrer estructuras de datos,
puesto que sólo es necesario que el incremento sea igual al tamaño de los
datos de la estructura.
7. Direccionamiento en pila: Es un caso particular del caso anterior que
utiliza el puntero de pila como registro índice, éste apunta hacia el
encabezamiento de la pila y se debe incrementar o decrementar
adecuándose a los accesos de la pila.
8. Direccionamiento indirecto: Parte de otro tipo, absoluto o relativo, al
que añade un paso más que consiste en que la dirección calculada no
apunta al elemento X deseado, sino a una posición de memoria que
contiene la dirección del elemento.
9. Direccionamiento implícito: El formato de instrucción no incluye ningún
campo para especificar el operando, ya que es un lugar predeterminado,
constituyendo un registro usuario preferente denominado acumulador. La
ventaja es que no ocupa espacio en la instrucción, pero el inconveniente es
que reduce la flexibilidad de la misma, que solo puede tener el operando
en un lugar fijo.
4 · Fundamentos de computadores
Direccionamiento inmediato
1000000011000 / 010 / 00110101
Suma
Reg Inmediato
DL  DL + Inmediato
Direcciona a la posición Suma
Direccionamiento directo absoluto
Reg4

M(Absoluto)
Reg4
OR
M(Pg Báse)  Reg2 AND
M(Pg base)
1000
100
001111001
0010011001000111111100000111110
OR
Reg
tipo
datos
(Absoluto)
1100 010 110111000 0010001100100011
AND Reg Tipo datos Página base
Se hace OR del Reg4 y la M(Absoluta) y se guarda
en Reg4
Se hace AND del Reg2 y la M(Pg base) y se
guarda en esta última
Direccionamiento relativo al contador del programa
PC=0E25h
PC+IR=37FCh
M  M(PC+D)
IR=0004h;
dirección=
El valor del PC no es el de la instrucción actual en
curso, sino el de la siguiente (+ bits que ocupe)
Direccionamiento relativo a registro base
11110111 00100110 0100011110101101
Multipl.
Mod
R/m
Desplazamiento
M  M(D+RB)
Direccionamiento relativo al registro índice
RI=7436 , Inc: 4 y despl.: -65
Dirección  RI + D 7436 – 65 = 7371
RI  RI + Inc 7436 + 4 = 7440
RI=7436 , Inc: 4 y despl.: -65
Dirección  RI + D 7436 – 65 = 7371
RI  RI - Inc 7371 - 4 = 7375
RI=7436 , Inc: 4 y despl.: -65
RI  RI + Inc 7436 + 4 = 7440
Dirección  RI + D 7440 -65 = 7375
RI=7436 , Inc: 4 y despl.: -65
RI  RI - Inc 7436 - 4 = 7432
Dirección  RI + D 7432 – 65 = 7367
PostAutoincremento
PostAutodecremento
Preautoincremento
Preautodecremento
El Fe factor de escala, se aplicará en los casos
necesarios
Direccionamiento indirecto
D
(Dirección
en
operación)
D y puntero B en
instrucción
la
la
D  Dir(X)  X
Absoluto
D + Puntero B  Dir (X)  X
Relativo
3. INSTRUCCIONES DE MÁQUINA




Podemos clasificar las instrucciones de máquina según su función en:
Instrucciones de transferencia o movimiento de datos
Instrucciones de interrupción de la secuencia del programa
Instrucciones aritméticas y lógicas
Instrucciones de entrada/salida y misceláneas
Existe una correspondencia muy estrecha entre las instrucciones en código
máquina y en ensamblador, además como este último es mucho más cómodo para
las personas que la binaria, es el que vendremos utilizando.
Una instrucción o sentencia ensamblador se compone del mnemónico que
expresa la función de la operación y la segunda parte que especifica los operandos
que no nos implícitos en la operación.
Instrucciones de transferencia o movimiento de datos
Permiten copiar la información contenida en un registro o posición de
memoria a otro registro oposición de memoria, constan de dos operandos: el
origen y el destino. Pueden parecer instrucciones poco útiles, pero en realidad son
las más utilizadas, ya que suponen del 30 al 40% de las instrucciones que ejecuta
un computador. Los mnemónicos más importantes son:
Fundamentos de computadores · 5
MOVE
ST
LD
PUSH
POP
MOVEM
XCH
CLR
SET
Es una transferencia general entre registros y posiciones de memoria
Transmite el contenido de un registro a una posición de memoria
Transmite el contenido de una posición de memoria a un registro
Introduce el dato de origen en la pila y lo convierte en el encabezamiento
Extrae el dato de encabezamiento de la pila
Transfiere bloques de datos de una zona de memoria a otra
Intercambia el contenido de los dos operandos especificados
Pone el destino en ceros
Pone el destino en unos
Store
Load
Move Block
Exchange
Clear
Instrucciones de interrupción de la secuencia del programa
Rompen la cadena normal de ejecución, basada en el incremento del
programa PC, cargando un nuevo valor en éste. Suelen suponer de un 24 a un
33% del total de las instrucciones de uso ejecutadas. Y encontramos 3 tipos
principales:
 Bifurcaciones incondicionales: Se carga un nuevo valor en el contador
del programa, Se suelen utilizar los mnemotécnicos Jump y Branco
 Bifurcaciones condicionales: Constituyen las únicas cuya ejecución
depende del contexto. Se debe especificar una condición de bifurcación que
si se cumple se ejecutará la bifurcación y si no, no. Los nemotécnicos más
usuales son:
B[-]
Con [-] se indica la condición de bifurcación; por ejemplo BZ de Branco if Zero
bifurcará si el Bit 0 de la palabra de estado se encuentra activo.
Si se cumple la condición de bifurcación [-] se saltará la instrucción siguiente.
Branch
SKIP [-]
Y las condiciones de bifurcación más usuales son:
Z
E
C
P
V
PE
Zero
Equal
Carry
Positive
Overflow
Parity even
Cero
Igual
Transporte
Positivo
Desbordamiento
Paridad de pareja: Paridad impar si el numero de ceros o unos es par.

CALL
RET
Return
Bifurcaciones con retorno: Una de las estructuras de programación más
utilizadas es la subrutina. Es un programa autocontenido que se incorpora
a otro programa en una o varias posiciones. Al llegar a la posición de
incorporación, el control pasa a la subrutina, hasta que cabe su ejecución;
luego el control retorna al programa. Las dos grandes ventajas son la
economía (pues puede llamarse varias veces al mismo programa y
economizar líneas de código) y la modularidad (se permite dividir el
programa en varios trozos más pequeños y manejables. Tenemos dos
instrucciones:
Instrucción que cede el control a la subrutina
Devuelve el control al programa que realiza la llamada
Cuando una subrutina llama a otra, se considera una nidificación de
subrutinas. La instrucción Call solo tiene un operando que especifica la dirección a
partir de la cual se encuentra almacenada la subrutina; además esta instrucción
debe guardar la dirección de la instrucción que la sigue (dirección de retorno) para
poder volver a este punto cuando acabe de ejecutarse la subrutina; y eso es lo que
hace la instrucción Ret, que no necesita ningún operador más, pues la dirección de
retorno ya hemos dicho que se encuentra implícito.
Instrucciones aritméticas y lógicas y misceláneas
Son instrucciones muy variadas, según sea monádicas o diádicas
necesitaran uno o dos registros sobre los que operar. Las instrucciones aritméticas
que solemos encontrar son:
6 · Fundamentos de computadores
ADD
ADDC
SUB
SUBC
MUL
DIV
INC
DEC
NEG
ABS
Addition
Addition with carry
Subtract
Subtract with borrow
Multiply
Divide
Increment
Decrement
Negative
Absolute
Suma
Suña añadiendo el bit de transporte de la palabra de estado
Resta
Resta teniendo en cuenta el transporte de la palabra de estado
Multiplica
Divide
Incrementa una unidad
Decrementa una unidad
Cambia de signo o niega
Obtiene el valor absoluto
Entre las instrucciones lógicas encontramos:
SHR
SHL
SHRA
ROR
ROLC
Shift rifht
Shift Left
Shift right Arithmethic
Rotate Right
Rotate left through carry
Desplazamiento
Desplazamiento
Desplazamiento
Desplazamiento
Desplazamiento
lógico a la derecha
lógico a la izquierda
aritmético a la derecha
circular a la derecha
circular a la izquierda con transporte
Entre estos dos grupos de instrucciones, venimos a tener el 26 a 35% de
las instrucciones generadas para un programa. Otros tipos de instrucciones son las
de comparación, que no modifican el resultado, solo actualizan el registro de estado
(probablemente para efectuar un cálculo o un salto posterior)
CMP
TEST
Compare
Realiza una operación SUB entre los dos operandos sin guardar el resultado
Compara el operando con cero
Las instrucciones de entrada/salida suelen ser privilegiadas, es decir,
solo se reservan al nivel de ejecución del núcleo, no de usuario. Su función consiste
en realizar una transferencia entre una posición del mapa de e/s y un registro o
posición de memoria: con este tipo de funciones se controlan los periféricos.
Tenemos las instrucciones:
IN
Input
OUT
Output
Transmite información de un puerto de entrada a un registro o pos. de
memoria
Transfiere información de un registro o memoria a un puerto de salida
Luego encontramos otras instrucciones difíciles de clasificar, entre las que
podemos estudiar:
NOP
SETC
CLRC
SETV
CLRV
EI
DI
HALT
WAIT
No operation
Set carry
Clear carry
Set overflow
Clear overflow
Enable interrupt
Disable interrupt
Instrucción que no hace nada, se puede utilizar en bucles de espera
Activa el bit de trnasporte de la palabra de estado
Desactiva el bit de transporte de la palabra de estado
Activa el bit de desbordamiento de la palabra de estado
Desactiva el bit de desbordamiento de la palabra de estado
Activa el bit de interrupción y permite que se acepten interrupciones
Desactiva el bit de interrupción e inhibe las interrupciones
Es muy raro su uso pues para volver a arrancar hay que volver a encender la
máquina
Intrucción privilegiada que para el procesador, pero con cualquier interrupción
se vuelve a poner en marcha, se utiliza mucho para reducir el consumo
Fundamentos de computadores · 7
4. MÁQUINAS CISC Y RISC
El diseño de los computadores hasta mediados de los 80 propugnaba un
rico y variado juego de instrucciones complementando la arquitectura del PC. Cada
fabricante se esforzaba por sacar a la luz un juego de instrucciones amplio, aunque
de estructura irregular y que presentaban muchas excepciones.
Pero a mediado de los 80 esto cambió, se propugna la arquitectura RISC
(Reduced instrucción set computer) con un juego de instrucciones sencillo, ante la
clásica CISC (Complex) como podían ser los IBM370, Motorota 680xx e Intel xx86.
Hoy día se acepta una buena arquitectura (y por tanto un buen juego de
instrucciones) que sea regular y no presente casos excepcionales, es decir que sea
regular y ortogonal. Ello permite:
 Diseñar compiladores más sencillos y eficaces.
 Programar más fácilmente en ensamblador, pues el usuario no debe
recordar casos especiales ni saber utilizarlos.
El hecho de que un juego de instrucciones sea regular, viene determinado por
la ortogonalidad, esto quiere decir que cada operación se debe poder efectuar con
cualquier tipo de operando y de direccionamiento; es necesario por tanto que
disponga de todas las posibilidades que tengan sentido, aunque existan
combinaciones de poca frecuencia de uso. Pero en cambio, se gana en que las
funciones que realiza el computador son simples y por tanto rápidas, y un juego de
instrucciones sencillo simplifica el diseño (y posiblemente abarate costes). El caso
es que, aunque desde mediados de los 80 se conoce la mejora de los formatos
RISC hasta casi hoy día no ha evolucionado, porque claro, el coste de cambiar,
equipos, nuevos diseños, nuevo software y paquetes de programa que lo soporten,
es muy elevado.
8 · Fundamentos de computadores
TEMA 2 LOS SISTEMAS DE ENTRADA/SALIDA
1. ASPECTOS BÁSICOS DE LA ENTRADA/SALIDA
El objetivo del sistema entrada/salida del computador consiste en
intercambiar información entre los periféricos y la memoria principal o los registros
del procesador. Evidentemente, todos los periféricos no son iguales y nos interesa
clasificarlos por un parámetro fundamental para el proceso de entrada/salida como
es el ancho de banda, es decir, la capacidad máxima que tiene de intercambiar
información el periférico con el resto del sistema por unidad de tiempo.
Características básicas de los periféricos
Dispositivo
Sentido
transferencias
Teclado
Entrada
Ratón
Entrada
Monitor gráfico
Salida
Disco magnético
Entrada/salida
Cd-Rom (12x)
Entrada
Impresora láser
Salida
Desencadenante
transferencias
Usuario
Usuario
Computador
Computador
Computador
Computador
Ancho de banda
10 bytes/s
20 bytes/s
30 Mb/s
2 Mb/s
1,8 Mb/s
100 Kb/s
Al observar la tabla anterior es preciso comprender que existen multitud de
periféricos con características distintas y el hecho de intercambiar datos con ellos
implica amoldarse a su sentido, desencadenante y, sobre todo, ancho de banda
para realizar la transferencia.
En principio se podría crear un software específico para cada dispositivo, no
obstante debido al gran número de ellos que existen la opción es impracticable, por
ello se definen módulos normalizados de interconexión que se denominan
módulos o controladores de entrada/salida. La función básica de estos
módulos es ocultar al computador las particularidades de cada periférico y facilitar
una visión y un control genéricos de éstos
El módulo de entrada/salida constituye un controlador específico de
uno o varios periféricos del mismo tipo y tiene dos elementos básicos: una interfaz
específica con el periférico que controlan y una conexión normalizada con el
resto del sistema. Es decir, existen unas especificaciones de todas sus
características básicas como:
 El conjunto de líneas de conexión que lo componen
 Los circuitos equivalentes de todos los elementos conectados al bus
normalizado
 El tamaño y otras características físicas de los conectores
 Cronogramas de funcionamiento
Hablando en plata: la conexión entre el computador y el módulo de
entrada/salida es una conexión normalizada a la que se deben adaptar tanto el PC
como el módulo de entrada/salida una vez que se haya definido un estándar de
conexión. Con frecuencia, un computador tiene conexiones normalizadas de varios
tipos, así admite periféricos desarrollados según diferentes normas.
Esta conexión se establece mediante un conjunto de registros (ver gráfico) que
se agrupan según el tipo de información que almacenan.
La transferencia de información a través de los módulos de entrada/salida
tienen dos partes primordiales: la sincronización y el intercambio o transferencia de
la información en sí.
¿Porqué hace falta una sincronización? Pues muy sencillo, el computador y el
periférico tienen temporizaciones diferentes, como la velocidad de ambos es
distinta, se hace necesario establecer un protocolo mediante el cual ambos
subsistemas puedan determinar cual es el momento apropiado para intercambiar
cada elemento de información. La sincronización es necesaria para evitar las
Fundamentos de computadores · 9
pérdidas de información. Por ejemplo para enviar datos a una impresora de matriz
de puntos, es preciso saber en qué momento está dispuesta a recibir el carácter
siguiente y esto dependerá generalmente de cual ha sido el carácter anterior
recibido; así el tiempo de espera será mayor en los retornos de carro, cambios de
línea o tabulaciones.
Una vez hecho lo anterior, se consigue que el periférico y el PC estén
disponibles para establecer el intercambio o transferencia de información,
que es cuando uno se envía al otro (dependiendo del sentido de la transferencia) el
dato que constituye el objeto de la transacción. En cualquier computador
convencional (un único procesador) es éste el que gobierna cada transacción de
entrada/salida, o sea, decide leer o escribir información de/sobre un controlador de
periférico; pero no es el procesador el que decide en qué momento o de qué forma
se va a realizar la transferencia; es el periférico el que debe poder indicarle al
procesador en qué momento se encuentra dispuesto para transmitir o aceptar
información hacia o del procesador.
En función del método utilizado para establecer la sincronización y llevar a cabo
el intercambio de información entre procesador y controlador de periférico,
encontrados tres técnicas básicas de entrada/salida que ahora vamos a enumerar y
posteriormente desarrollar:
1.
Entrada/salida programada: La sincronización y el intercambio de
información son responsabilidad del procesador.
2.
Entrada/salida por interrupciones: El controlador de periférico se
responsabiliza de la sincronización y notifica al procesador cuando está
dispuesto para realizar el intercambio de información. Esto último lo
lleva a cabo el procesador.
3.
Entrada/salida por DMA: El procesador se encarga de preparar la
transferencia entre periférico y memoria y luego es el controlador del
periférico el que asegura la sincronización y lleva a cabo sin la
intervención del procesador, el intercambio de información.
2. ENTRADA/SALIDA PROGRAMADA
También se denomina directa y se caracteriza porque toda la
responsabilidad de la transferencia de información recae sobre el procesador. Éste,
por medio de la ejecución del programa adecuado se encarga de establecer la
sincronización con el periférico y la posterior transferencia de información.
En el caso de un periférico de entrada, cada vez que el procesador
comprueba que el controlador de periférico dispone de un nuevo dato, lo toma y
queda disponible para recibir otro posterior; si el periférico es de salida, cuando el
procesador dispone de un dato para enviar al periférico y detecta que éste está
disponible, efectúa el envío.
A continuación, estudiaremos algunos aspectos relacionados con la
entrada/salida programada, que influyen también en el funcionamiento de otras
técnicas de entrada/salida.
Mapa de entrada/salida
La comunicación con los periféricos se efectúa mediante sus módulos de
entrada/salida correspondientes y, por tanto, es preciso que el procesador tenga
acceso a los registros de datos, control y estado de estos módulos. Para poder
identificar cada uno de los registros existen dos posibilidades:
 Mapa de memoria: los registros se encuentran en determinadas posiciones
del mapa de memoria. En este caso se utilizan decodificadores que se
activan a partir de las líneas de bus de direcciones y utilizan las mismas
señales de control que sirven para seleccionar la memoria
 Que los registros se ubiquen en un espacio de direcciones independientes
de la memoria, el espacio ENTRADA/SALIDA o mapa independiente de
entrada/salida. Aquí se suelen compartir las líneas de direcciones y para
distinguirlas se dan conjuntos de señales de control:
10 · Fundamentos de computadores
o
o
READ, WRITE e IO/MEM: marcan la temporización de la
transferencia.
MEMR, MEMW, IOR e IOW: Hacen la transferencia según el tipo a
que se refieran.
Cronograma
Los accesos al registro de datos, control o estado de un módulo de
entrada/salida son similares a los de acceso a memoria que ya hemos estudiado en
el semestre anterior, la única diferencia es que se utilizan otras señales de control
en el caso de los sistemas con entrada/salida independiente de la memoria. Los
accesos pueden ser:
 Síncronos: De duración predeterminada. En el ejemplo del gráfico (página
anterior) se efectúa la carga del dato en el extremo de bajada del reloj.
 Asíncronos: De duración arbitraria, establecida por el periférico. En el
ejemplo, la carga en el registro de datos se produce en el momento en que
la señal WAIT deja de estar activa.
 Semisíncronos: De duración establecida por el periférico pero siempre
múltiplo del período de reloj. En el ejemplo, la carga del dato se produce al
detectarse el flanco de bajada de la señal de reloj, pero sólo si la señal
WAIT está desactivada.
Instrucciones de entrada/salida
No todos los computadores disponen de instrucciones específicas de
entrada/salida, solo aquellos en los que se establece una distinción entre el mapa
de memoria y el de entrada/salida. Cada vez que estos computadores ejecuten una
instrucción de lectura o escritura en memoria, además de activar el resto de las
líneas de conexión con el exterior, ponen en marcha la línea o líneas que distinguen
entre entrada/salida y memoria y las pasan al estado que corresponde a una
operación con memoria (por ejemplo IO/MEM pasaría al estado lógico L).
Las ventajas de disponer de instrucciones específicas de entrada/salida son
cuestionables; de hecho muchos diseñadores han optado por no distinguir entre
entrada/salida y memoria al hablar de arquitectura básica de la máquina; no
obstante con instrucciones específicas de entrada/salida se consigue establecer con
más facilidad un mecanismo de protección adecuado, ya que se tratan de
instrucciones privilegiadas que solo pueden ejecutarse en la modalidad de
supervisor y así, un usuario cualquiera no puede acceder a estos controladores de
entrada/salida.
Identificación del periférico
Esta función que realiza el procesador es importantísima pues cuando éste
debe intercambiar información con más de un periférico, debe identificar en cada
Fundamentos de computadores · 11
instante qué periférico está disponible para llevar a cabo el intercambio y, en ese
caso, ejecutarlo.
Es más típico del caso siguiente la entrada/salida por interrupciones; no
obstante en entrada/salida programada también se puede plantear
esta situación, es decir, que en el mismo instante haya varios
periféricos con información disponible para el procesador. Aquí
debe decidirse si todos ellos se deben tratar de manera equitativa
o si conviene establecer algún esquema de prioridades entre
periféricos. En este último caso hay que tener en cuenta que este
esquema de prioridades se implementará en forma de programa,
sin disponer de ayuda hardware de ningún tipo.
El procedimiento consiste en realizar una consulta o
polling sobre todos aquellos periféricos que en un momento
determinado puedan disponer de información para intercambiar.
Existen dos tipos de prioridad que se puede establecer en estos
casos:
 Prioridad fija: El programa de consulta comienza
siempre comunicándose con el mismo controlador de periférico y
los recorre todos siguiendo la misma secuencia fija.
 Prioridad rotativa: Se asigna un número de orden a
cada periférico. Tras atender a la petición de uno de ellos, se pasa
a consultar el estado del siguiente de acuerdo con el número de
orden, en lugar de volver a empezar por el primero.
3. ENTRADA/SALIDA POR INTERRUPCIONES
El sistema de entrada/salida por interrupciones, como ya apuntábamos
anteriormente, se caracteriza porque la responsabilidad de la sincronización entre
procesador y periférico se encuentra en el módulo de entrada/salida. Para hacer
esto posible es preciso que el procesador disponga de una línea de entrada,
denominada línea de petición de interrupción (INT), mediante la cual el
módulo de entrada/salida puede reclamar la atención del procesador cada vez que
esté dispuesto a transferir un dato desde o hacia el periférico.
La ventaja de este sistema frente al de entrada/salida programada es que aquí
no es preciso que el procesador consulte de forma reiterada el estado del
periférico, sino que puede estar ejecutando otro programa. El periférico se encarga
de activar la INT en el momento en que está dispuesto a intercambiar información
con el procesador; y esto sucede en las siguientes etapas:
1. Antes de iniciar cada instrucción, el procesador comprueba el estado de la
INT.
2. Si la línea INT no está activada, realiza la lectura y ejecución de la
instrucción y pasa a la siguiente.
3. Si la línea INT está activada, por el contrario, se altera la ejecución del flujo
normal del programa:
a. Se pasa a la rutina de servicio de interrupción
b. Para ello es necesario pasar de la clase que se encuentra (usuario
o supervisor) a la de supervisor.
c. Una vez ejecutada y finalizada la interrupción el procesador debe
continuar ejecutando el programa tal como se venía realizando,
como si nada hubiese pasado, y en este sentido será fundamental
la salvaguarda del estado del procesador.
Un aspecto importante es la inhibición de las interrupciones; pues
existen situaciones excepcionales que requerirán el ignorarlas. Esta situación se
almacena normalmente como un bit de registro de estado; si el procesador tiene
permitida la interrupción al comprobar este bit de registro, entonces valora el
estado de la línea INT antes de cada instrucción. Además la línea INT se considera
activa por nivel y no por flanco, lo cual quiere decir que cuanto se vuelvan a
12 · Fundamentos de computadores
permitir las interrupciones tras un tiempo de inhibición, solo se atenderán las de
aquel periférico que la esté solicitando todavía.
Dado que las interrupciones pueden llegar (y llegan) al procesador en
cualquier momento, es fundamental la salvaguarda del estado del procesador,
como ya apuntábamos anteriormente para que, una vez terminada la ejecución de
la interrupción, el programa continúe como si nada hubiese pasado. La salvaguarda
del sistema consiste en guardar el valor de los flags de estado y de ciertos o todos
los registros para poder seguir utilizándolos después de la interrupción. Esto se
puede hacer de dos formas:
 De manera automática: El procesador está obligado a almacenar todo
el estado, es decir, los flags de estado y el conjunto de registros de
toda la arquitectura. Es un método más lento, pues se guardan más
cantidades de información, pero más seguro.
 De manera manual: Se guardan los valores de los flags de estado y
las instrucciones adecuadas para almacenar el estado actual. Lo más
habitual y eficiente consiste en que el procesador guarda de forma
automática los flags de estado y el contador de programa (PC) y el
programador de la rutina de servicio de interrupción guarda el resto de
los registros que la rutina modifica.
Explicado todo lo anterior, deducimos 3 formas diferentes de conectar los
controladores de periféricos, es decir, cual es la conexión física entre el procesador
y los controladores de periféricos que interrumpen, y esto debe resolvernos 3
dudas importantes:
 Poder determinar de entre todos los periféricos conectados al sistema,
cual es el que está reclamando la atención del procesador.
 Cual es el periférico prioritario o prioritarios que debemos atender
cuando varios reclamen la atención del procesador
 Decidir si durante una interrupción, puede activarse otra del mismo o
de otros periféricos.
Como decía antes tres posibilidades importantísimas se nos abren y son las
que vamos a estudiar: la línea única de petición de interrupción, la conexión en
Daisy Caín y las líneas independientes de petición y reconocimiento de interrupción.
LA LÍNEA ÚNICA DE PETICIÓN DE INTERRUPCIÓN
Es el método más sencillo de
conexión entre un procesador y varios
periféricos (página siguiente) y consiste en
compartir la línea de petición de
interrupción entre todos los controladores
de periférico conectados al sistema. De esta
forma el procesador es capaz de determinar
si algún procesador está activado, pero
para saber cual es en concreto, necesita
establecer la lectura del registro de estado
de cada uno de ellos y decidir por programa
cual es (software).
La ventaja evidente de este método
es la sencillez y flexibilidad a la hora de
determinar a cual de los periféricos que solicitan una interrupción es preciso
atender en primer lugar. No obstante el inconveniente es el retraso que introduce
(que no es despreciable ni mucho menos, pues es comparable en tiempo a la
ejecución de varias líneas de código de programa), pues cada vez que un periférico
activa la interrupción es preciso ejecutar una única rutina de atención a
interruptores: salvaguardar los registros, identificar que periférico es el que
interrumpe (realizando un muestreo de todos ellos por si es solo uno o hay más de
Fundamentos de computadores · 13
uno). Otra gran ventaja es la de permitir con el programa de atención a periféricos
si establecemos una prioridad a uno o más de un periférico sobre el resto a la hora
de activar su interrupción.
Otro inconveniente es la nidificación de interrupciones al compartir la
misma línea. Esta nidificación consiste en permitir que un periférico más prioritario
sea capaz de interrumpir a su vez la rutina de servicio de interrupción
correspondiente a otro menos prioritario, pero no en el caso contrario o cuando los
dos tengan el mismo grado de prioridad. Al compartir una única línea y estar
ejecutándose ya una interrupción y producirse otra, es imposible saber a priori si la
nueva interrupción es prioritaria sobre la que ya se está ejecutando: es necesario
activar de nuevo la rutina de interrupciones y ver cual es el periférico que
interrumpe y qué grado de prioridad tiene. Así que si activamos la atención a
interruptores puede ocurrir la degradación del sistema por agotamiento de la
memoria (y ni que decir que cuando se ha empezado a atender una interrupción
ésta debe desactivarse, pues de lo contrario y teniendo activada la posibilidad de
interrumpir entraríamos en un bucle infinito)
Línea única de
petición de
interrupción
Ventajas
Inconvenientes
Sencillez en las conexiones
Flexibilidad por software para atender por prioridades a los periféricos
Velocidad lenta en cualquier interrupción por poco prioritaria que sea
Imposibilidad de nidificación de interrupciones al no saber, a priori, si la
nueva interrupción es más prioritaria que la anterior.
CONEXIÓN DE INTERRUPCIONES EN DAISY CHAIN
Una forma de evitar el retraso que supone el tener que ejecutar un
programa cada vez que hay una interrupción se basa en la utilización de una nueva
línea de control: la línea de reconocimiento de interrupción, conocida como
INTA o INTACK.
Si están habilitadas las interrupciones, el procesador comprueba al principio
de cada instrucción si la línea de petición está activada (hasta aquí, como antes).
En caso de estar activada, activa la INTA para que el periférico reconozca que el
procesador está atendiendo su petición y deja de hacer la solicitud de interrupción.
Para poder conectar más de un periférico se establece una cadena daisy Caín. El
primer controlador de periférico si no es el activo, activa una prolongación de línea
hacia el segundo, y así hasta que se encuentre cual esta solicitando interrupción.
Como esta circuitería es muy sencilla, la identificación del periférico que interrumpe
es muy rápida y así no se ejecuta un programa específico para esta función (lo
hemos resuelto por hardware). No obstante, solo hemos hecho saber al periférico
que conocemos de su solicitud de interrupción, pero todavía no sabemos cual es, ni
que grado de prioridad tiene. Para ello se utiliza la técnica de vectorización, que
consiste en que el periférico
cuando recibe la señal por el
INTA, deposita en el bus de
datos un determinado valor,
denominado
valor
de
interrupción. Al procesador le
llega este valor y lo utiliza para
saber, a través de este índice
recibido en una tabla de
vectores de interrupción (con un
valor único, obviamente para
cada periférico), y así se activa
la rutina de servicio de
interrupción
para
ese
controlador.
La ventaja de una línea de petición compartida y líneas de reconocimiento
encadenadas (Daisy Chain) radica que en un único ciclo de bus, sabemos que
periférico está interrumpiendo y activamos la rutina asociada a este periférico; y
14 · Fundamentos de computadores
aunque sean varios los que interrumpen, de manera rápida y automática
determinamos el grado de prioridad (se comienza siempre por los más próximos al
procesador).
Pero la desventaja principal es la rigidez del esquema basado en esta prioridad
por cercanía (que es además fija e inalterable, debido a que es una solución
hardware) y además seguimos sin resolver el problema de nidificación de
interrupciones, pues seguimos sin conocer si la nueva interrupción es más
prioritaria que la que ya pudiésemos estar llevando a cabo.
Daisy Chain:
Línea de petición
compartida y líneas
de
reconocimiento
encadenadas
Ventajas
Inconvenientes
Sencillez en las conexiones
Velocidad en la detección del periférico/s que interrumpe/n (un ciclo de bus)
Rápida determinación de la prioridad de los periféricos que interrumpen.
Rigidez y poca flexibilidad para variar la prioridad de los periféricos, dado que se
trata de una solución hardware fija e inmutable.
Seguimos sin resolver la nidificación de interrupciones pues desconocemos si la
nueva interrupción es más prioritaria que la que ya estamos llevando a cabo.
LÍNEAS INDEPENDIENTES DE PETICIÓN Y RECONOCIMIENTO DE INTERRUPCIÓN
La única posibilidad para distinguir de forma inmediata (sin la obligación de
ejecutar un programa) si una interrupción que acaba de llegar al procesador es más
prioritaria que la que ya estamos ejecutando, consiste en disponer de líneas de
petición independientes para los diferentes controladores de entrada/salida (ver
figura en la página siguiente).
La identificación del periférico que interrumpe es directa pues cada línea de
petición se encuentra conectada con un solo periférico; pero además se flexibiliza la
gestión de prioridades al poder decidir que línea consideramos ordenadamente más
prioritaria que otras. Así resolvemos el problema que antes ya nos habíamos
planteado es decir, la nidificación de interrupciones. El proceso que se sigue es el
siguiente para cada código de instrucción:
 Se valora si existe una interrupción activada, de no ser así, se ejecuta
la siguiente instrucción.
 Si existe una activación de
interrupción, se valora si es
de mayor nivel que la que se
tiene almacenada (de no ser
de mayor nivel, se continúa
como
si
nada
hubiese
ocurrido).
 En caso de ser de mayor
nivel, el proceso consiste en
salvaguardar
PC
y
los
registros de estado; en
activar la modalidad de
supervisor, en actualizar el
nivel
de
interrupción
almacenado, en leer el vector
de
interrupción
y
posteriomrnete en finalizar
esta rutina de interrupción.
Esquema de un ciclo fetch en un procesador con líneas independientes de petición y reconocimiento de interrupción
¿Hay una línea de interrupción activa?
NO
Ir al final del fetch
SI
¿Es nivel de la interrupción > Nivel_alamacenado?
NO
Ir al final del fetch
SI
Salvaguardar PC y registros de estado | Actualizar modalidad supervisor | Actualizar el nivel
almacenado=Nivel de interrupción | Leer vector de interrupción | Encontrar dirección | Ir a
final fecth
Final Fetch:
Leer instrucción señalada por el PC | Actualizar PC | Descodificar y ejecutar
Fundamentos de computadores · 15
El problema es que no todos los
procesadores disponen de un número
suficiente de líneas de petición y
reconocimiento de interrupción para
implementar el esquema descrito
anteriormente. Como muchos de ellos
solo disponen de una línea de petición
y de reconocimiento, lo que se hace
es
añadir
un
controlador
de
interrupciones,
o
sea,
circuitos
especializados en la gestión de
interrupciones
de
sistema.
Así
conseguimos prácticamente la misma
funcionalidad pero con dos diferencias
apreciables:
1.
La
salvaguarda
y
recuperación del nivel de ejecución no
se lleva a cabo de forma automática,
dado
que
el
controlador
de
interrupciones
es
externo
al
procesador, y este, sin ejecutar un
programa, no es capaz de decidir el
nivel de interrupción que está
atendiendo.
2.
Como el procesador solo
dispone de una línea de petición de
interrupción, se debe identificar el
periférico que interrumpe igual que si
se tratase de un encadenamiento,
estableciendo
un
esquema
de
vectorización o bien a partir de la
lectura de los registros internos del
controlador de periférico.
Ahora bien, en el esquema inicial
de líneas independientes de petición y reconocimiento de interrupción no todo son
ventajas, ya que el número de periféricos conectables al sistema se encuentra
limitado por las líneas disponibles. Esto se solventa con los dos sistemas
representados a continuación: un esquema híbrido de líneas de petición y
reconocimiento de interrupción y conexión de controladores de interrupciones en
cascada.
Las famosas interrupciones de software, excepciones o traps, se tratan de
igual forma que si se tratasen de una interrupción de entrada/salida ; con la
16 · Fundamentos de computadores
salvedad de que las interrupciones de software se señalan por medio de la
activación de una señal externa, mientras que las de entrada/salida se generan
dentro del procesador. Entre estas excepciones se encuentran las correspondientes
a los intentos de accesos a recursos restringidos, operaciones aritméticas como la
división por cero o la raiz cuadrada de un número negativo, etc.
Repasemos ahora el conjunto de operaciones que debe llevar a cabo un
sistema genérico con entrada/salida por interrupciones. Éstas corresponden a un
procesador con un número suficiente de líneas de petición y con capacidad para
tratar interrupciones sectorizadas y autovectorizadas:
Fase Inicial
Fase de
funcionamiento
normal
Fase de retorno
de interrupción
Se activa el sistema y se programan los dispositivos que se deben utilizar y se define el valor inicial de la
variables utilizadas

Programación de los controladores de entrada/salida, definición del vector de interrupción, etc.

Se especifica el nivel de ejecución menos prioritario (programa principal).

Se habilitan las interrupciones.

Se pasa a ejecutar el programa principal
Se ejecuta en esta fase el programa principal, constituido por norma general, por un bucle de espera donde se
comprueba el estado del sistema. En la fase de fetch se realiza:

Comprobación de las líneas de petición de interrupción con nivel superior al actual de ejecución, por si
existe alguna activada.

Si no es así, se continúa la lectura del código de la operación de la instrucción actual.

Si alguna línea de petición con suficiente prioridad está activada se ejecuta el ciclo de reconocimiento de
interrupción que consiste en:
o
Si la entrada al procesador indica que es una interrupción autovectorizada, utiliza como vector
de ésta el número de línea de interrupción que está activo. Como tabla de vectores de
interrupción utiliza la de autovectores (pasa al punto 4)
o
Si no es autovectorizada, activa la línea de reconocimiento de interrupción correspondiente al
nivel detectado.
o
Lee el vector de interrupción proporcionado por el controlador de entrada/salida.
o
Accede a la entrada de la tabla de vectores o autovectores de interrupción señalada por el
vector que se acaba de almacenar.
o
Obtiene de la tabla correspondiente, la dirección de la rutina de servicio de interrupción
o
Almacena el contador de programa, registro de estado actual en la pila de modalidad supervisor
del sistema.
o
Actualiza el registro de estado y almacena el nuevo nivel de ejecución y se activa el bit que
indica la modalidad de ejecución correspondiente al supervisor
o
Actualiza el contador del programa con el valor obtenido de la tabla correspondiente (vectores
o autovectores).
o
Reinicia la ejecución de instrucciones (empieza un nuevo ciclo de fetch).
Se desarrolla al ejecutar la instrucción RETI, que debe ser la última de cada rutina de servicio de interrupción:
solo debe recuperar el estado (que incluye información sobre el nivel de ejecución) que se había guardado en
la pila del sistema en el momento de responder a la interrupción.

Se recupera el contador de programa almacenado en la pila del supervisor.

Se recupera el registro de estado almacenado en la pila del sistema (con esto además se recuperan el
nivel de ejecución y el bit de modalidad usuario/supervisor).

Se ejecuta un nuevo ciclo de fetch utilizando los nuevos valores de los registros contador de programa y
registro de estado
4. ENTRADA/SALIDA POR DMA
En las entrada/salida que hemos visto antes, para cada interrupción que se
genera es preciso desviar el flujo normal del programa, por lo que es necesario
realizar una salvaguarda del estado actual, para luego volver al mismo estado que
había antes de la interrupción. Esto se traduce en un consumo de tiempo de
ejecución para cada interrupción, suponiendo asimismo una importante limitación
del ancho de banda máximo que queda libre para la comunicación con el periférico.
Esto hace que la entrada/salida por interrupciones sea adecuada para
dispositivos de ancho de banda no muy grande, pero para los más rápidos, el
procesador estaría prácticamente dedicado a ejecutar las instrucciones de los
servicios de interrupción.
El objetivo de la entrada/salida por DMA es que el procesador pueda
desentenderse de la sincronización y de la transferencia de información con el
periférico, pero para ello es imprescindible que el módulo de entrada/salida sea
capaz de acceder de manera automática a la memoria del sistema. No obstante
Fundamentos de computadores · 17
aunque el procesador se desentienda de este proceso si necesita programar la
operación con antelación y además necesita ser avisado por el periférico cuando el
intercambio de información haya finalizado y esto suele hacerse generando una
interrupción.
Así que resumiendo, la transferencia por DMA está compuesta de:
 Programación de la operación llevada a cabo por el procesador.
 Transferencia de información entre periférico y memoria principal,
gestionado por el controlador de entrada/salida.
 Culminación de la operación, donde el controlador de
entrada/salida avisa al procesador del desenlace de la
transferencia.
Claro, observando este proceso
que ha de desarrollarse se comprende de
la ineficacia de utilizar DMA para transferir
datos individuales; por este motivo se
utiliza para transferir bloques de datos
relativamente grandes, por ejemplo
sectores entre el disco y la memoria, pero
no para enviar datos del teclado al
procesador.
Pero este tipo de entrada/salida
añade un nuevo problema que no se había
presentado hasta ahora, es preciso
establecer un mecanismo mediante el cual
ambos elementos utilicen la memoria sin
que por ello se genere ningún tipo de
conflicto. Hay dos posibilidades:
 Utilizar una conexión a memoria
independiente para cada elemento,
memoria con doble puerta (no se usa
demasiado pues es un tipo de memoria
más cara).
 Aplicar una única conexión a memoria,
compartida por el procesador y el módulo
de
entrada/salida
proporcionando
asimismo un mecanismo para evitar
conflictos en el acceso mutuo a memoria.
Esto se denomina robo de ciclo.
Este robo de ciclo es el sistema
que suele utilizarse y que vamos a
describir. Para llevarlo a cabo se necesitan
líneas triestado para la conexión del
procesador
y
los
módulos
de
entrada/salida en el bus (direcciones,
datos y señales de control) y además dos
nuevas líneas de control: BUSREQ y
BUSACK.
El funcionamiento se basa en que
el procesador es más prioritario a la hora
de cualquier transacción, pero comprueba
con frecuencia el estado de la señal de
petición del bus BUSREQ, de manera que
cuando el procesador la encuentre
activada responda a tal demanda. Esto
consiste en activar BUSACK; lo que hace
que el bus quede ahora gobernado por el
18 · Fundamentos de computadores
controlador de entrada/salida, y así se asegura que solo uno de los
dos dispositivos gobierna la memoria.
Ahora bien, nos encontramos con diferentes maneras de conectar los
módulos: la forma más sencilla es la primera gráfica de la figura de la página
anterior. Tenemos un único módulo entrada/salida que interactúa con el procesador
para la petición de los buses; pero claro, si necesitamos más de un módulo (que es
lo lógico) se puede actuar creando un daisy Caín de los módulos de entrada/salida
por DMA (figura siguiente) o finalmente introduciendo un nuevo dispositivo: el
controlador DMA: que es un dispositivo capaz de gestionar las transferencias de
varios periféricos.
El funcionamiento del sistema por DMA tiene las 3 partes ya anunciadas
anteriormente: Programación de la operación, transferencia de los bloques de datos
y finalización de la operación.
PROGRAMACIÓN DE LA OPERACIÓN
El procesador debe informar al controlador de Entrada/salida por DMA al
menos de los siguientes datos:
 Número de datos que se deben transferir.
 Dirección de memoria a partir de la cual se deben almacenar o depositar
los datos que se intercambian.
 Sentido de la transferencia.
 Si el módulo entrada/salida se utiliza para controlar varios periféricos,
también es preciso identificar el periférico involucrado en la transferencia.
Evidentemente, para que el controlador DMA sea capaz de guardar estos datos,
hay que dotarlo de nuevos registros, estos son: RCOMPT: contador que guarda el
número de datos a transferir; y RADRM: se almacena la dirección inicial de la
memoria involucrada en la transferencia.
TRANSFERENCIA DE LA INFORMACIÓN
Una vez programada la transferencia, el módulo entrada/salida debe
esperar hasta que el periférico que controla se encuentre disponible para transmitir
o recibir y entonces solicita el bus y tras serle concedido por el procesador efectúa
la transferencia.
En el proceso de entrada/salida por interrupciones podíamos inhibir la
transferencia si ésta era de un nivel prioritario inferior a la que ya estábamos
ejecutando; esta opción por DMA no se contempla (el bus se cede de forma
automática ante la petición de BUSREQ). Esto se debe a que la cesión del bus
constituye una operación muy rápida y que además no afecta al estado del
procesador, no tiene que iniciar sistemas de salvaguarda de datos, solamente
congela la ejecución de las instrucciones que está ejecutando (salvo que exista una
transferencia a la memoria en ese momento, con lo cual el procesador termina la
transferencia y luego cede el bus o, en otros casos, corta inmediatamente la
transferencia para iniciarla posteriormente a la transferencia DMA).
Además, como la cesión siempre se encuentra habilitada, el tiempo de
respuesta es muy breve. En caso de una lectura del periférico cada vez que éste
indica a su controlador que dispone de un dato para transferir y siempre que el
contenido de RCOMPT sea distinto de 0 (esto indica que existen datos pendientes
de transmitir), el controlador lo almacena en RDATO, activa BUSREQ para solicitar
el bus al procesador, éste lo cede activando BUSACK, el módulo entrada/salida
lanza un ciclo de escritura en memoria por el bus y deposita sobre el bus de
direcciones el contenido del registro RADRM y sobre el bus de datos el de RDATO;
tras finalizar el ciclo de escritura en memoria, el controlador desactiva BUSREQ y
actualiza los registros RADRM (incrementa para aumentar a la posición de memoria
Fundamentos de computadores · 19
siguiente) y RCOMPT (Decrementa, para contener el número de datos pendiente de
transmitir). Si después de esta actualización RCOMPT es distinto de cero, el
controlador vuelve a esperar la llegada de un nuevo dato; si es cero se pasa a la
fase de finalización de la operación.
En el caso de que se utilice un controlador de acceso directo a memoria
para gestionar varios periféricos, varía ligeramente el funcionamiento, puesto que
el registro de datos se encuentra por norma general en el módulo entrada/salida
mientras que el de direcciones forma parte del controlador DMA; estas son las
únicas diferencias importantes.
Funcionamiento del controlador de periférico por DMA
Programación de la operación
If OP=WRITE
While RCOMPT>0

Activar BUSREQ

Esperar BUSACK

Leer [ADRM] y llevar el dato leído a RDATO

Desactivar BUSREQ

Esperar a periférico preparado y enviarle el contenido de RDATO

Incrementar ADRM y decrementar RCOMPT
End-while
If OP=READ
While RCOMPT>0

Esperar DATO del periférico y llevarlo a RDATO

Activar BUSREQ

Esperar BUSACK

Escribir sobre [ADRM] el valor contenido en RDATO

Desactivar BUSREQ

Incrementar ADRM y decrementar RCOMPT
End-while
Finalización de la operación
Una forma de optimizar el rendimiento de esta actividad consiste en
minimizar el tiempo que se pierde mientras se obtiene el control del bus y se
retorna al procesador; esto se realiza con lo que se denomina modalidad de
ráfagas, consistente en acumular en el módulo de entrada/salida o el controlador
de DMA una cantidad importante de datos antes de solicitar el bus (eso sí, este
dispositivo debe tener capacidad para acumular todos estos datos que luego
queremos enviar). Así una vez acumulado el número requerido de datos, o pasado
el número de ciclos necesario para poder realizar otra transferencia; pedimos el
bus, realizamos la transferencia de todos los datos cumulados y evitamos liberar y
retomar el bus cada vez que tenemos un dato que enviar.
De una u otra forma, una vez que RCOMPT es cero, pasamos a la siguiente
fase.
CULMINACIÓN DE LA OPERACIÓN
Una vez transferido el bloque o bloques de información programado por el
procesador, el módulo entrada/salida o controlador DMA genera una interrupción
INT con la que se notifica al procesador que ya se ha transferido el bloque
solicitado y con la ejecución de la correspondiente rutina de servicio de la
interrupción, se considera acabada la operación de transferencia del bloque.
5. COMPARACIÓN DE LAS TÉCNICAS BÁSICAS DE ENTRADA/SALIDA
Vamos a estudiar el rendimiento del sistema, para ello vamos a poner el
ejemplo de que se quiere analizar el tiempo de transferencia de información de un
20 · Fundamentos de computadores
disco magnético utilizando estas 3 técnicas de entrada/salida. Los datos del
problema son los siguientes:
 Ejecución de cada instrucción por el procesador: 150 ns.
 Ciclo de lectura o escritura: 100 ns.
 Cesión / recuperación de los buses: 50 ns.
 Tiempo de latencia del sico: 15 ms.
 Velocidad de transferencia: 2Mb/s
 Conexión de 32 bits entre el módulo de entrada/salida y el procesador
Con estos datos, e independientemente de la técnica de entrada/salida que
se trate, se invertirá el tiempo siguiente (dado que es necesario programar la
operación, esperar el tiempo de latencia adecuado y transferir la información:
Total: Latencia + Transferencia = 15 ms +
210 bytes(1kbyte)
=15,49 ms
2 * 2 20 ( Bytes / s )
En este tiempo, el procesador es capaz de realizar (como máximo y sin
15,49·10 6
interrupciones)
= 103267 instrucciones.
150
A través de la entrada/salida programada el controlador dice que proporciona
un dato cada:
1
=1,91  s; el número de instrucciones, por tanto que se puede
2·2 bytes / s
4 Bytes / palabra
20
ejecutar entre dato y dato es de 1910/150 = 12 instrucciones; normalmente con 12
instrucciones hay de sobra para realizar el bucle de sincronización con el
controlador y toda la rutina de ejecución: Hay que tener en cuenta que si la
conexión no fuera de 32 bits, sino de 8, la cosa se complicaría, pues se dispondría
de un dato cada 0,4775 milisegundos y ahí si que no daría tiempo a ejecutarse.
En fin, en resumen, tardamos 15,49 ms en transferir la información, pero el
procesador queda ocupado durante todo este tiempo.
A través de entrada/salida por interrupciones, el tiempo en que se
dispone de un dato sigue siendo 1,91  s, igual que antes, y ello permite ejecutar
12 instrucciones; en este caso hay que preparar una rutina un pelín ajustada; la
rutina de servicio de interrupción puede constar como máximo de 11 instrucciones,
pues se debe prever el tiempo empleado en el reconocimiento de la instrucciones,
que suponemos es similar al de la ejecución. Conseguimos hacer una rutina de
ejemplo de 11 instrucciones (ajustado como digo), no obstante seguimos adelante;
y suponiendo que la programación de la operación como su culminación se
efectúan con 30 instrucciones, el tiempo que el procesador utiliza para llevar a cabo
la transferencia es de:
Tiempo ocupado = 30·150ns +
210 Bytes
·11·150 ns + 30 ·150 ns =
4 Bytes / palabra
0,43ms;
Por tanto, disponemos de tiempo libre del procesador: 15,49 - 0,43 = 15,06 ms; en
los que puede efectuar
15,06·10 6
=100.400 instrucciones de las 103.267 posibles;
150
lo cual no está nada mal.
A través de la entrada/salida por DMA suponiendo que la programación
de la transferencia y la rutina de interrupción duren unas 40 instrucciones, el
tiempo de ocupación del procesador es:
Tiempo ocupado = 40·150 ns +Tiempo transferencia + 40·150 ns.
Fundamentos de computadores · 21
El tiempo de trasnferencia es igual al tiempo que tarda en ceder el bit,
efectuar una lectura, recuperar el bit y todo ello multiplicado por buses de 1024/4
datos; en total:
Tiempo transferencia = (1024/4) · (50 ns + 100 ns + 50 ns) = 51.200 ns.
Por tanto, el tiempo ocupado es: 6000 + 51200 + 6000 = 63200 ns.
Con lo que el tiempo disponible es 15,49 – 0,0632 = 15,43; y se pueden
efectuar
15,43·10 6
= 102.867 instrucciones de trabajo, de las 100400 disponibles.
150
Todavía podemos mejorar más si suponemos que trabamos en modalidad de ráfaga
y por tanto cedemos y recuperamos el bus cada, digamos 8 palabras, por lo que el
tiempo de transferencia pasa a ser:
Tiempo transferencia = (256/8)·(50ns + 8·100 ns + 50 ns) = 28800 ns; y
ello nos hace reducir el tiempo de ocupación a 40,800 ns y por tanto poder ejecutar
muchas más instrucciones.
6. ASPECTOS COMPLEMENTARIOS DE LA ENTRADA/SALIDA
Dos apartados vamos a ver de forma escueta como aspectos
complementarios de la entrada/salida que no hemos tratado con anterioridad. Estos
aspectos son la relación de la entrada/salida con el sistema de memoria y mejoras
que añade el controlador DMA en las transferencias de este tipo.
Hasta ahora hemos hecho un estudio conciso de la relación entre el sistema
de entrada/salida y la memoria del ordenador; pero en realidad la memoria de un
ordenador suele estar compuesta por una memoria caché, una principal y un
mecanismo de memoria virtual; esta jerarquía la estudiaremos en el capítulo
siguiente pero se diseña con la finalidad de incrementar la velocidad de los accesos
que realiza el procesador.
La relación de la entrada/salida con la memoria caché es un poco caótica.
Existen dos opciones: o conectar el módulo entrada/salida con la memoria caché o
hacerlo con la principal (en realidad con el bus que va a la principal. En el caso de
que se conecte a la memoria caché tenemos dos grandes desventajas:
 Disminución del ancho de banda en
la comunicación entre el procesador y la memoria
caché.
 Aumento de la tasa de errores de
caché pues se almacena información que no se
corresponde a lo que el procesador espera utilizar.
Debido a ello, éste no es el método de
conexión más utilizado; pero la conexión con la
memoria principal del controlador DMA tampoco está
exenta de desventajas: la principal dificultad es
asegurar la coherencia de la información entre estos
dos niveles de memoria; se suele resolver haciendo
las operaciones sobre una zona de memoria no
cacheable.
Otro problema lo establece la memoria virtual. Si el dispositivo que
establece el acceso al DMA lo hace a través de direcciones virtuales; en cada
acceso a memoria lanzado por el dispositivo DMA es preciso realizar la traducción a
la dirección completa correspondiente, con la consecuente pérdida de prestaciones.
Esto suele resolverse añadiendo un dispositivo para acelerar la traducción: el TLB
(solución eficaz pero compleja)
Si, por el contrario, el programador DMA utiliza direcciones físicas, se hace
preciso limitar el número de datos para transferir en una sola operación al tamaño
22 · Fundamentos de computadores
de una página, pues no se puede asegurar que las páginas virtuales consecutivas
se encuentren localizadas en un determinado momento sobre marcos de página
contiguos.
Por otro lado, una mejora importante del funcionamiento de la
entrada/salida por DMA lo constituyen los canales de entrada/salida: su función
consiste en facilitar la programación aprovechando la capacidad que poseen de
acceder de forma autónoma a la memoria.; así se consiguen varias ventajas:
 Programación de las operaciones de entrada/salida más rápida.
 Si el controlador DMA está ocupado en una transferencia cuando el
procesador está programando una nueva operación; éste la puede generar,
llevarla a memoria y arrancarse en cuanto
el controlador DMA esté disponible (de
nuevo eficiencia temporal).
 El lenguaje de programación propio del
canal de Entrada/salida, aún siendo
básico, puede tener una instrucción que
indique el final de la operación de
transferencia, con lo que solo se ejecuta
una única instrucción de finalización y no
al finalizar la transferencia de
cada
bloque.
 Elimina el conflicto con la memoria virtual
en los marcos de páginas contiguos.
7. EL BUS DE ENTRADA/SALIDA
Tal como hemos estudiado la estructura
básica del computador , parece que únicamente
exista un bus de propósito general para conectar
el procesador con la memoria y con los
adaptadores de entrada/salida (figura a).
Esto realmente no es así ya ni el los
ordenadores más sencillos, está todo basado en
una jerarquía de buses que, aunque no está tan
definida como la jerarquía de memorias, empieza
a estar cada vez más delimitado.
Actualmente, los ordenadores poseen un
adaptador conmutador de memoria bus PCI
(figura b), cuya función consiste en conectar en
cada instante los dos elementos involucrados en
una transferencia, por ejemplo, la memoria caché
de segundo nivel con la memoria principal o con
un controlador de entrada/salida. Se pueden
establecer así flujos simultáneos de información
que aumenta la velocidad y eficiencia de la
máquina.
Características del Bus entrada/sal:
 Tipo de funcionamiento y temporización
(síncrono, asíncrono, semisíncrono).
 Multiplexación de direcciones/datos
 Admisión de varios maestros del bus.
 Número de líneas dedicadas a transferir datos
(incide directamente en el ancho de banda
que puede alcanzar).
 Número de líneas dedicadas a propagar
direcciones de memoria o entrada/salida
(espacio de direccionamiento)
 Normalización del bus que, a la larga,
Fundamentos de computadores · 23
redundará en una mayor difusión de éste.
Aunque no hay una clara definición del bus de entrada/salida diremos que
se trata de un bus normalizado, encargado de conectar los diferentes módulos
de entrada/salida o bien los periféricos de un tipo determinado con el
computador.
Así, observamos en la figura lateral, que los buses local y de memoria no
interconexionan ni periféricos ni controladores; por el contrario los buses EISA
y SCSI (incluso el PCI) sí.
Es hoy día característica primordial de los buses de entrada/salida que se
deben normalizar, de namera que varios fabricantes de controladores de E7s o
de dispositivos periféricos, pueden adoptar sus diseños en la especificación de
cada bus normalizado. Otras características más particulares del Bus dependen
del propósito que se le quiera dar, de manera que se pueden observar
diferencias importantes entre:
 Bus de almacenamiento: Se trata de conseguir un ancho de banda
elevado a un coste de interconexión bajo y sin que la latencia sea
importante
 Bus de soporte: Para la interconexión de módulos de entrada/salida, que
proporciona un ancho de banda elevado, con baja latencia y debe admitir
varios maestros de bus.
 Bus serie: Para la conexión de periféricos, cuyo objetivo principal es la
reducción de costes, pero no de rendimiento.
Es fundamental el direccionamiento del bus, que se lleva a cabo en una sola
etapa y que puede ser:
 Direccionamiento lógico: Los diferentes módulos tanto de memoria
como de entrada/salida se deben encargar de decodificar las señales del
bus de direcciones y determinar si se trata de una transacción dirigida a
ellos. Para llevarlo a cabo a cada módulo del sistema se asigna un rango de
direcciones único (a través de microinterruptores que se deben configurar
en el momento de colocar la placa correspondiente dentro del computador)
y tiene el inconveniente de constituir una importante fuente de errores de
configuración.
 Direccionamiento geográfico: Es el más seguro dado que se obvian los
posibles errores de configuración por parte del usuario (lo realiza
automáticamente el sistema operativo) y el que más se utilizada en la
actualidad. Consiste en separar la selección de un módulo de memoria o
entrada/salida del direccionamiento de éste en sus diferentes celdas de
memoria o posiciones de entrada/salida. Se establece un mecanismo para
seleccionar el módulo conectado a una determinara ranura o slot para
facilitar la configuración automática de los sistemas y para proporcionar la
selección de una posición concreta del módulo. Así no hay que configurar
los adaptadores de entrada/salida manualmente, puesto que la dirección
que ocupan en el espacio de direcciones se encuentra determinada por la
ranura donde se ha situado.
Otra característica es la gestión del bus: el problema se establece a la hora
de utilizar buses que admiten más de un maestro potencias que pretende emplear
el bus para realizar una transacción, y es entonces cuando hace falta un maestro
que determine cual tiene la autorización para poder utilizarlo. Para ello existe el
gestor del bus único, con varios pares de líneas de petición y concesión del bus,
similar al gestor de interrupciones visto anteriormente.
Como ejemplos de buses de entrada salida tenemos el ISA: utilizado
anteriormente como base de los ordenadores iBM-PC, ha quedado relegado como
soporte de módulos entrada/salida y simplemente por un problema de
compatibilidad con los periféricos; el bus PCI: es el sucesor del anterior iSA y es el
más generalizado, inicialmente solo iba a ser soporte del microprocesador Pentium,
pero se cedieron sus especificaciones al dominio público y de ahí su generalización,
el bus SCSI tiene un elevado ancho de banda y es típico en unidades de disco o
cinta magnética; y por último el bus USB que contrario al resto, solo utiliza dos
hilos (señal y referencia) para transmitir información entre los periféricos.
24 · Fundamentos de computadores
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
Conector de alimentación
Conector de teclado
Pila
BIOS
Ranuras expansión con buses ISA de 8 ó 16 bits.
Ranuras expansión con buses locales, PCI, EISA, de 32 bits.
Chips controladores DMA
Zócalo ZIF para el procesador.
Buses
10. Memoria caché
Zócalos de memoria RAM
Fundamentos de computadores · 25
TEMA 3 INTRODUCCIÓN A LA JERARQUÍA DE MEMORIAS
Desde los orígenes de la informática, los programadores han pretendido disponer de
computadores con mucha capacidad de memoria; actualmente esta cantidad de memoria
solo constituye un problema de coste. La rapidez de los procesadores se ha multiplicado en
los últimos 20 años por 3000 mientras que la de los dispositivos de memoria solo por 10.
Esta gran diferencia que además tiende a aumentar constituye uno e los factores críticos en
el rendimiento de un computador.
La solución ofrecida por los diseñadores para este problema consiste en el uso de
una jerarquía de memorias: memorias estructuradas en distintos niveles con distintas
velocidades y capacidades. El objetivo de la jerarquía de memorias consiste en que la
información que el procesador requiera en cada momento se encuentra almacenada en el
nivel más próximo y rápido posible y, por otro, en que la transferencia de información entre
los distintos niveles se lleve a cabo por medio de mecanismos automáticos (sin la
intervención explícita del programador o usuario).
1. TIPOS DE MEMORIAS



Según su perdurabilidad: Memoria volátil (pierde la información almacenada cuando
deja de tener alimentación eléctrica o memoria no volátil.
Escritura por parte del usuario o no escribibles por parte del usuario
Según estos antecedentes tenemos lo siguientes tipos de memoria “comerciales”:
o ROM: (Read only memory): Memoria de solo lectura de semiconductor
(como la BIOS).
o RAM: (Randon Access Memoru): Memoria de acceso aleatorio o directo,
memoria de semiconductor. A su vez encontramos los tipos SRAM
(estáticas) o DRAM (dinámicas que pierden el contenido de la memoria a
los pocos nanosegundos, son muy utilizadas porque son baratas y rápidas,
aunque haya que “refrescar” constantemente sus contenidos).
o Disco: Dispositivos magnéticos de acceso directo.
o Cinta: Su acceso es secuencial y lento, por ello solo se utilizan como copia
de seguridad.
o CD.
2. ACCESO A LOS DATOS Y A LA JERARQUIA DE LAS MEMORIAS
La memoria se puede considerar un vector unidimensional en el que es preciso
especificar un número que indique su posición dentro de ella para acceder a los elementos
individuales. Esta cifra se denomina dirección de memoria. El objetivo que se busca en el
diseño del sistema de memoria de un computador es que tenga gran capacidad y un tiempo
de acceso reducido al menor precio posible. Como no existe ninguna tecnología que cumpla
de forma simultánea ambos requisitos, la memoria del computador se estructura en varios
niveles, formando la jerarquía de memorias.
El objetivo final de esta jerarquía es conseguir que cuando el procesador acceda a
un dato, éste se encuentre en el nivel más rápido de la jerarquía (y por ello el de menor
capacidad). Se suelen dividir por ello en:

Primer nivel: constituido por los registros visibles del lenguaje máquina. Es de
poca capacidad.

Segundo nivel de memoria caché: constituye una memoria
RAM auxiliar de alta velocidad. Puede existir más de un nivel de esta
modalidad (como en el ejemplo de la imagen)

Memoria principal

Niveles posteriores o memorias secundarias: cds, discos
duros, etc.
Como además se puede ver en la imagen, cada nivel se relaciona
solo con los siguientes, y el procesador solo puede acceder al nivel
más bajo. Si el dato que necesita el procesador no se encuentra en él,
debe transferirse del siguiente o de los siguientes niveles hasta este
para que el procesador pueda leerlo. Por otro lado si se modifica algún
dato, el procesador lo hará en el nivel más bajo y es necesario que tal
modificación se efectúe en el resto de niveles donde se encuentra
almacenada una copia del dato original (de lo contrario podríamos
incurrir en un problema de coherencia).
26 · Fundamentos de computadores
Pero claro, nosotros podríamos pensar ¿Tanto flujo de información de un nivel a
otro tiene que perjudicar al rendimiento del procesador en lugar de ayudarle y además como
vamos a tener todos los datos que necesitamos en un área de memoria muy pequeña, como
es la memoria más próxima al ordenador?
Esto tiene fácil respuesta:

El flujo de datos que se mueve entre niveles está controlado por el sistema de
entrada/salida, que, como ya sabemos, apenas interrumpe el número de
instrucciones que es capaz de desarrollar el procesador.

Por otro lado, el conjunto de datos que se necesitan para ejecutar un programa y el
de instrucciones, a pesar de ser inicialmente grande, en realidad es pequeño: se
sabe que el 1% de código de un programa corresponde al 96% de las instrucciones
realizadas; y el 6% de todas las posiciones de memoria suele abarcar el 90% de los
accesos a la misma. Esto se debe a una proximidad referencial: en un intervalo
de tiempo, un proceso determinado solo utiliza una pequeña parte de toda la
información almacenada, debida sobre todo a:
o Proximidad temporal: En un intervalo de tiempo determinado, la
probabilidad de que un proceso acceda varias veces a las mismas
posiciones de memoria es muy alta; los bucles en los programas producen
proximidad temporal y las llamadas repetidas a subrutinas que emplean las
mismas variables locales y los mismos parámetros.
o Proximidad espacial: la distancia entre referencias próximas es muy
pequeña en intervalos de tiempo determinados: es decir a excepción de
las bifurcaciones el flujo de ejecución es lineal; además los accesos a
estructuras de datos recorridas de manera secuencial es muy frecuente.
3. LA MEMORIA CACHÉ
La memoria caché se caracteriza por tener una alta velocidad y un tamaño
reducido. Es en esta memoria donde vamos a ejemplificar todo lo visto anteriormente. Una
cuestión importante es la tasa de acierto/fallo, es decir, la tasa de fallos en es número de
veces que se accede a la memoria caché y no se encuentra allí el dato, dividido por el
número de accesos a memoria; es justo lo contrario a la tasa de aciertos, que es lo que nos
interesa tener. Cuando se lanza un acceso a la memoria, el algoritmo de emplazamiento
es el encargado de buscar este dato:

Se busca este dato en cualquier posición de la memoria caché (asociativa por
completo)

Este dato solo puede encontrarse en una sola posición de la memoria caché
(emplazamiento directo).

Se puede ubicar en un conjunto limitado de posiciones de la memoria caché (nasociativa).
La memoria caché completamente asociativa es la que produce porcentaje de fallos más
bajos, pero debido a su elevado precio y lentitud no suele utilizarse; la directa es la que
produce más fallos, pero al ser más barata y sobre todo rápida, compensa este elevado
número de fallos con el rápido tratamiento que hace de los mismos.
Cuando se encuentra un fallo (o sea el dato buscado por el procesador no está en la
memoria caché), el hardware de la memoria caché se encarga de las siguientes tareas:

Solicitar el dato que ha producido el fallo en el siguiente nivel de jerarquía (si este
no lo encuentra ira al siguiente).

Esperar a que llegue el dato

Buscar un lugar en la memoria caché para el dato leído y almacenarlo en esta.

Servir el dato al procesador como si se hubiera producido un acierto en la memoria
caché.
Este proceso es entre 5 y 10 veces más costoso en tiempo que si hubiese sido un
acierto, es por ello muy importante intentar obtener una tasa de fallos baja. Hemos
evidenciado que cuando se produce un fallo, los datos se trasladan secundariamente de la
memoria principal a la caché, pero se traslada una línea entera pues es muy posible por la
proximidad referencial, que necesitemos esos datos que rodean al que buscamos, por ello se
traslada una línea o bloque completa a la memoria caché, pues es muy probable que la
necesitemos. El tamaño de esta línea cuenta con unos cuantos bytes de información ( que
no debe ser ni muy grande –pues borra otras líneas de la limitada caché que quizá sigamos
necesitando- ni muy pequeña –pues corremos el riesgo de dejarnos atrás datos que
posiblemente necesitemos).
Fundamentos de computadores · 27
Eso sí, cuando una línea puede almacenarse en varios lugares de la memoria caché
(asociativa o n-asociativa) es necesario determinar el algoritmo de reemplazo que se
encargará de esta tarea, puede ser:

Aleatorio: Es un algoritmo simple y funciona razonablemente bien.

FIFO: Se elimina la línea que lleva más tiempo almacenada en la memoria;
puede producir resultados indeseables puesto que esta línea no tiene porque
ser la que menos se utilice.

LRU: se elige la línea que lleva más tiempo sin utilizarse. Teóricamente es el
más eficiente, pero es muy difícil implementarlo en forma hardware, se suelen
utilizar algoritmos pseudo-LRU.
La política de escritura se refiera al momento en que se actualiza la memoria
principal; es decir cuando en lugar de tener un acceso a memoria de lectura es de escritura,
el dato que se pasa al procesador es variado en la memoria más próxima del procesador,
pero es necesario variar este dato también en el resto de copias, se puede hacer mediante
dos sistemas:

Escritura inmediata: Como su nombre indica se hace el cambio a la vez en
todas las copias, es un sistema fácil de implementar pero que provoca un
elevado intercambio de información entre ambas memorias (write no allocate).

Escritura aplazada: Se efectúa siempre las escrituras sobre la memoria
caché, y solo cuando se borra la línea afectada en la memoria caché, se
trasladan los cambios a la memoria principal (write allocate).
El rendimiento de la memoria caché por tanto, se medirá como el tiempo medio de
acceso a memoria, que consistirá en: la tasa de aciertos por el tiempo en caso de acierto
más la tasa de fallos por el tiempo que tarda en efectuar el acceso en caso de fallo. Lo ideal,
evidentemente, es reducir la tasa de fallos todo lo posible.
28 · Fundamentos de computadores
TEMA 4 ENSAMBLADOR
1. HERRAMIENTAS DE PROGRAMACIÓN
Para poder escribir un programa, sea en alto nivel, sea en ensamblador,
precisaremos de un editor. Éste tiene que ser un editor de texto sin formato, ya
que el fichero debe contener exclusivamente las instrucciones del programa, sin
códigos de control sobre el estilo de la letra, tamaño, colores, márgenes del
documento, etc.
El fichero que contendrá el programa de origen (source), es decir, en
formato texto, recibe el nombre de fichero de origen o fichero de font. Si el fichero
de origen contiene un programa escrito con un lenguaje de alto nivel, el proceso de
traducción será realizado mediante un compilador. El programa compilador, por su
parte, analiza el significado del programa en alto nivel y genera instrucciones de
lenguaje de máquina que llevan a cabo la tarea especificada en el alto nivel. De
otro lado, si el fichero de origen contiene un programa escrito con lenguaje de
ensamblador, el proceso de traducción se genera mediante un programa
ensamblador.
El ensamblaje es un proceso más sencillo porque la traducción de lenguaje
de ensamblador a lenguaje de máquina es directa: cada instrucción de
ensamblador representa una instrucción en lenguaje de máquina. Como resultado
de la compilación o del ensamblaje, se genera un fichero objeto que ya contiene
instrucciones de lenguaje de máquina, pero que todavía no se puede ejecutar.
Para finalizar, un fichero objeto o varios de ellos se pueden combinar y
generar un fichero ejecutable. Este proceso se desarrolla mediante un montador o
linker. Aunque se puede realizar el montaje con varios ficheros objeto, también es
un paso necesario en caso de tener un solo fichero objeto. El fichero ejecutable ya
es el fichero binario con el lenguaje de máquina y el formato adecuado para poder
llevar a cabo su ejecución.
2. ESTRUCTURA DE UN PROGRAMA EN ENSAMBLADOR
En este primer ejemplo tenemos el código de un programa en
ensamblador que escribe por pantalla el mensaje "¡Buenos días!".


El bloque identificado por la directiva .data que contiene los datos del
programa (segmento de datos), que contiene una única variable
llamada mensaje, que inicialmente contiene el texto que nos interesa
escribir por pantalla.
El bloque identificado por la directiva .code que contiene las
instrucciones del programa (segmento de código), además de unas
macros que indican el inicio y el final del programa, contiene tres
instrucciones. Sin entrar en detalles sobre la función de estas
instrucciones, podéis observar que cada una de ellas siempre ocupa
una línea.
En función del modelo de programa que vamos a seguir durante el módulo,
siempre empezaremos el segmento de código con una etiqueta (en este caso inicio)
seguida de la macro STARTUPCODE, que inicializa algunos registros del procesador.
Para finalizar el programa, utilizaremos la macro EXITCODE 0, y para indicar la
finalización del segmento de código tendremos que poner end seguido del nombre
de la etiqueta de comienzo de código: inicio. Las tres instrucciones restantes, por
tanto, son las que consiguen que se escriba un mensaje por pantalla.
Los comentarios son otro aspecto importante de cualquier programa en
ensamblador, son especialmente útiles en ensamblador debido a que el bajo nivel
Fundamentos de computadores · 29
de este lenguaje provoca que los programas presenten algunas dificultades de
comprensión. Así pues, en ensamblador, los comentarios son el texto que hay
detrás de un punto y coma y llega hasta el final de línea.
Una vez tenemos el fichero con el código ensamblador, hay que traducir el
código escrito en lenguaje de ensamblador a lenguaje de máquina y generar, así,
un programa ejecutable. El proceso de traducción consta de dos pasos:

Ensamblaje (assembling). El ensamblaje traduce cada línea de código
ensamblador a la correspondiente instrucción en lenguaje de máquina. Si el
código ensamblador no contiene ningún error, se genera, entonces, un fichero
objeto cuyo contenido consta de instrucciones en lenguaje de máquina.
Nosotros utilizaremos el programa Turbo Assembler para ensamblar, un
programa que genera un fichero objeto con extensión .obj.

Montaje (linking). El montaje toma un fichero objeto (o más de uno) y, si no
hay ningún error, genera el fichero ejecutable. Para montar, utilizaremos el
programa Turbo Linker, que genera un fichero ejecutable con extensión .exe.
En concreto, para ensamblar y montar el programa del ejemplo, ejecutaremos
los siguientes comandos:
tasm /zi buenosdias.asm y tlink /v buenosdias.obj
3. LECTURAS Y ESCRITURAS
En este apartado estudiaremos como hacer lecturas de teclado y escrituras
por pantalla de los elementos que nos interesen:
Para poder escribir un texto por pantalla, éste tiene que estar inicialmente
almacenado en la memoria en forma de cadena de texto. Podemos especificar el
código ASCII de los caracteres poniendo directamente los caracteres entre comillas,
o bien, en caso de encontrarnos con caracteres de control, poniendo el código
ASCII separado por comas. Existen diferentes maneras de escribir texto por
pantalla y de leer caracteres del teclado. De todas las posibilidades existentes, en
un primer momento vamos a utilizar la interrupción 21h, mecanismo basado en
interrupciones de software que permite utilizar rutinas del sistema operativo para
acceder a los recursos del sistema. Para especificar qué función de sistema se
quiere utilizar, se pone el valor correspondiente al registro ah.
El ejemplo de la página siguiente nos muestra el código de un programa
que escribe un mensaje inicial por pantalla, lee una tecla y escribe un segundo
mensaje antes de finalizar.
Para poder escribir un texto, éste tiene que estar inicialmente almacenado
en la memoria. En nuestro caso tenemos las variables de memoria mensaje1,
mensaje2 y saltolinea, cada una de las cuales contiene una cadena de texto y la
variable de memoria tecla, que no está inicializada. Las dos primeras cadenas de
texto contienen un mensaje y acaban con los caracteres 10 y 13, que codifican el
salto de línea en código ASCII. La variable saltolinea, por su parte, sólo contiene un
salto de línea. Y, por último, la variable tecla, que está sin inicializar (especificado
con el carácter ?), servirá para guardar la tecla leída por el teclado.
Para escribir un texto por pantalla, el registro dx debe contener la dirección
de memoria a partir de la cual, y hasta encontrar el carácter "$", se halla el texto
que estamos interesados en escribir. A continuación, tenemos que activar la
interrupción 21h, poniendo antes 09h en el registro ah, que indica que se quiere
escribir una cadena de caracteres por pantalla.
De manera similar, para leer un carácter del teclado activaremos la
interrupción 21h, pero, en este caso, poniendo previamente un 01h en el registro
ah (de esta manera, el programa se esperará hasta que el usuario pulse una tecla).
Veremos por pantalla la tecla pulsada, y ésta es almacenada en el registro
al. Por lo general, es importante guardar el valor de la tecla pulsada en alguna
30 · Fundamentos de computadores
variable de memoria (en el ejemplo, la variable tecla), ya que el registro al puede
sufrir modificaciones en las interrupciones que se produzcan a continuación.
Escritura de un texto por pantalla
La forma de escribir un texto por pantalla es
la que se ha descrito en el ejemplo 2; es decir:
lea dx, [text] mov ah, 09 h int 21 h,
donde text es la variable que contiene el texto
(acabado con el carácter '$') que deseamos escribir.
Escritura de un carácter por pantalla
Si nuestra intención es escribir un solo
carácter, no es necesario crear una cadena de dos
caracteres acabada con la letra '$', puesto que hay
otra interrupción que permite escribir un carácter con
más facilidad, que es la siguiente:
mov dl, car mov ah, 02 h int 21 h,
donde car es el código ASCII del carácter que
queremos escribir, o una variable o registro que
contiene este código.
Escritura de un número por pantalla
No existe ninguna rutina de sistema que
escriba directamente números por pantalla. Dado que
la escritura por pantalla siempre se realiza con el
código ASCII del carácter que nos interesa escribir, la
escritura de números es una tarea más complicada.
Si el número que hay que escribir es de un solo dígito, y teniendo en
cuenta que sabemos que el código ASCII de los dígitos 0, ..., 9 son valores
consecutivos, podemos utilizar la interrupción descrita en el caso anterior de este
modo:
mov dl,'0' add dl, núm mov ah, 02h int 21h,
donde núm es el valor del número entre 0 y 9 que estamos interesados en escribir.
Fijaos en que el valor del código ASCII del dígito '0' se suma al valor del número
que se quiere escribir. El resultado de esta suma es el valor del código ASCII del
dígito que queremos escribir.
Si el número que queremos escribir contiene varios dígitos, hay que hacer
la escritura dígito a dígito. Por esta razón, se deben realizar sucesivas divisiones por
10 y hay que escribir cada uno de los dígitos; pero la operación de división no la
veremos hasta un poco más adelante.
Lectura de un carácter por el teclado
La lectura del teclado mediante la interrupción 21h es de tipo carácter a
carácter. Existen dos variantes para realizar la lectura del teclado: con echo o sin
echo. Pues bien, con echo significa que el carácter que se introduce por el teclado
es escrito al mismo tiempo por la pantalla. Sin echo, significa que el carácter
introducido por el teclado no se escribe por pantalla. En ambos casos, y de manera
similar a la que vimos en el ejemplo 2, el carácter introducido queda almacenado
en el registro al.
La lectura con echo se hace de la siguiente manera:
mov ah, 01 h int 21 h mov [car], al,
donde car es una variable de memoria en la que guardamos el valor introducido por
el teclado. De manera análoga, la lectura sin echo se realiza como veremos acto
seguido:
mov ah, 08 h int 21 h mov [car], al.
Fundamentos de computadores · 31
4. ESTRUCTURAS CONDICIONALES
En los ejemplos introducidos hasta ahora, las
instrucciones han sido ejecutadas secuencialmente.
Por lo general, sin embargo, podemos encontrar
diferentes partes de un programa que se ejecutan en
función de una serie de valores introducidos o
calculados durante la ejecución del programa. Para
poder identificar la instrucción a la que nos interesa
saltar, es necesario poner una etiqueta.
A continuación vamos a ver el código de un
programa (página siguiente) que escribe un mensaje
por pantalla en función del valor de la tecla
introducida.



Si se pulsa una "D", escribe "Buenos días".
Si se pulsa una "T", escribe "Buenas tardes".
Y, de lo contrario, no escribe nada.
En concreto, se compara la tecla introducida con
la letra "D". Si la comparación es igual, no se salta y el
código continúa ejecutándose de forma secuencial, de
manera que se escribe el mensaje2. De lo contrario,
se salta a la instrucción identificada por la etiqueta
tardes. De manera análoga, si la tecla introducida no
es una "D" y se salta a la instrucción etiquetada con
tardes, se realiza, entonces, una nueva comparación.
En este caso, se compara la tecla introducida con la
letra "T": si es igual, no se salta y se sigue ejecutando
el código de forma secuencial y escribiendo el
mensaje3; de lo contrario, se salta a la instrucción
identificada por la etiqueta final.
Así pues, el mensaje2 sólo se escribirá si la tecla introducida es la "D", y el
mensaje3, sólo si la tecla pulsada es la "T". Fijaos en que cuando nos encontramos
con dos condiciones excluyentes (es decir, que se ejecuta un bloque de
instrucciones u otro bloque diferente) es necesario poner una instrucción de salto
incondicional jmp al final del primer bloque de instrucciones para, de este modo,
evitar que el segundo bloque sea ejecutado a continuación del primero.
Además, prestad atención al hecho de que la etiqueta días carece de uso
alguno. En general, sin embargo, las etiquetas (de la misma manera que los
comentarios) también sirven para aportar más claridad al programa. En este caso,
podéis observar que, en el programa del ejemplo, las etiquetas nos identifican
cuatro bloques: inicio, días, tardes y final.
Debemos hacer hincapié en el hecho de que las comparaciones se realizan con
los caracteres "D" y " T", caracteres que corresponden a letras en mayúscula. En
realidad, lo que se está comparando es el código ASCII de estos caracteres con el
código ASCII introducido por el teclado.
32 · Fundamentos de computadores
Instrucciones de salto
Formato:
Cmp al, “D”
Jne tardes
La primera instrucción compara el valor guardado en al, con la letra D (además mayúscula).
La segunda instrucción provocará un salto hasta la etiqueta tardes, siempre y cuando el valor
en Al y D sean distintos.
Comparación de valores con signo
jg
Salta si el primer valor es mayor que el segundo valor
jge
Salta si el primer valor es mayor o igual que el segundo valor
jl
Salta si el primer valor es menor que el segundo
jle
Salta si el primer valor es menor o igual que el segundo
Comparación de valores sin signo
ja
Salta si el primer valor es mayor que el segundo
jae
Salta si el primer valor es mayor o igual que el segundo
jb
Salta si el primer valor es menor que el segundo
jbe
Salta si el primer valor es menor o igual que el segundo
Otras instrucciones de salto
je
Salta si los dos valores comparados son iguales
jz
jne
Salta si los dos valores comparados son diferentes
jnz
jmp
Salta incondicionalmente
5. ESTRUCTURAS ITERATIVAS
Otra técnica importante de la programación (tanto en alto nivel como en
ensamblador) son las estructuras iterativas, es decir, la ejecución de un conjunto
de instrucciones un número repetido de veces. En general, el conjunto de
instrucciones que se encuentran dentro de una estructura iterativa se puede
ejecutar en un número fijo de iteraciones, o bien hasta que se cumpla una
determinada condición.
El programa que tenemos a continuación escribe un mensaje por pantalla un
número repetido de veces. En una primera versión de este código, el mensaje es
escrito diez veces; en la segunda versión, el mensaje es escrito cada vez que se
pulsa una tecla, hasta que la tecla pulsada es la "X". Como podéis ver en el
ejemplo, esta estructura iterativa está formada por una estructura condicional y un
salto hacia atrás.
La estructura condicional evalúa el número de veces que se ha ejecutado el código
sabiendo que cx cuenta el número de iteraciones. Cuando cx es mayor o igual que
10, se produce un salto hacia final; de lo contrario, se ejecutan las instrucciones
que se encuentran en el interior de la estructura iterativa y se salta hacia atrás, es
decir, hacia las instrucciones que vuelven a evaluar la condición.
Por lo general, para que el tipo de estructura iterativa que aparece en el siguiente
ejemplo funcione de forma correcta es necesario:


Inicializar el contador (en este caso el registro cx).
Incrementar el contador al final de cada iteración.
Fijaos en que si no hubiésemos inicializado cx, este registro podría
corresponderse con cualquier valor, por ejemplo 10. En este caso, el código que se
encuentra en el interior de la estructura iterativa no se ejecutaría nunca.
Fundamentos de computadores · 33
Ejemplo 1
Ejemplo 2
De la misma forma, si cx no se hubiera incrementado al final de cada
iteración, el contador valdría siempre 0 y, por lo tanto, la estructura iterativa nunca
acabaría y el programa se iteraría de manera indefinida.
En esta segunda versión del mismo programa, cada vez que se pulsa una
tecla se escribe el mensaje, hasta que la tecla pulsada es "X". Fijaos en esta
segunda versión del programa, en la que la estructura de evaluación de la
condición es muy similar a la del ejemplo de la versión anterior. En este caso, sin
embargo, se compara la tecla pulsada con el carácter "X".
Pues bien, si éstas son iguales, se salta hacia el final del programa; de lo
contrario, se escribe el mensaje y se vuelve a saltar hacia atrás (hacia la
introducción de la tecla). En este caso, la condición de evaluación es el valor de la
tecla introducida por el teclado. Tened en cuenta que antes de comparar por
primera vez, siempre se lee una tecla, y antes de cada nueva iteración, se lee una
nueva tecla.
Las estructuras iterativas mostradas en los ejemplos 4.1 y 4.2 sirven tanto
para contar un número determinado de iteraciones como para iterar hasta que se
cumpla una condición. En el primer caso, sin embargo, en el que se pretende
contar un número determinado de iteraciones, el lenguaje de ensamblador nos
ofrece otra manera más eficiente de hacer lo mismo: la instrucción loop.
La instrucción loop sirve para contar iteraciones utilizando de forma
implícita el registro cx para el recuento. Al ejecutarse la instrucción loop, el registro
cx decrece en una unidad y, si su valor es diferente de 0, se salta a la etiqueta
especificada en la misma instrucción.
6. TIPOS DE OPERANDO
Una instrucción en ensamblador ejecuta una determinada acción, utilizando
uno o varios operandos de origen, sobre (por lo general) un operando de destino.
Además, tanto el operando de origen como el de destino deben tener el mismo
tamaño. Cada uno de los operandos de origen, es decir, los operandos que son
consultados para ejecutar la instrucción, puede ser de tres tipos:
 una constante,
 un registro,
 una variable de memoria.
34 · Fundamentos de computadores
Del mismo modo, el operando de destino, es decir, el operando que se
actualiza como consecuencia de la ejecución de una instrucción, puede ser de dos
tipos:
 un registro,
 una variable de memoria.
No tiene ningún sentido hablar de operando de destino constante, porque el
operando de destino es, por definición, el operando que se actualiza al ejecutar una
instrucción, y una constante no varía.
A pesar de que tanto el operando de origen como el de destino pueden ser
de tipo variable de memoria, el ensamblador del i8086 posee una restricción: los
dos operandos no pueden ser de tipo variable de memoria en la misma instrucción;
por lo que, o bien lo es uno, o bien lo es el otro.
En el ejemplo, aparece en negrita la instrucción mov, que
ya conocéis. Esta instrucción tiene dos operandos y se comporta
de la siguiente manera:
mov destino, origen
Es decir, copia el valor del operando de origen (lo lee)
sobre el operando de destino (lo escribe).
En este caso concreto, podéis ver que la instrucción utiliza
como operando de origen un valor constante (aquí el número 65).
La constante especificada puede estar expresada en los diferentes
formatos que mostramos a continuación:
 en decimal: mov dl, 65;
 en hexadecimal: mov dl, 41h;
 en ASCII: mov dl, 'A'
A continuación, en el caso2 podéis observar que el operando
de origen de la instrucción mov es un registro; de hecho, en esta
instrucción el operando de destino también es un registro. El
acceso a un registro es muy rápido, pero, a pesar de todo, existen
pocos registros disponibles. Así pues, se acostumbra a utilizar
operandos de tipo registro siempre que esto sea posible. Además,
hay operaciones que fuerzan el uso de algún registro concreto.
Prestad atención, ahora, al hecho de que en el caso3, la instrucción mov utiliza
un operando de origen de tipo variable de memoria. En este caso, aparece
especificada entre corchetes una variable de memoria declarada en el segmento de
datos. Como en el caso anterior, para utilizar un operando de origen de tipo
variable de memoria, ésta tiene que haber sido declarada e inicializada con
anterioridad. Tened en cuenta el hecho de que la variable carácter1 ha sido
declarada en el segmento de datos e inicializada en el valor 65 durante su
definición.
Utilizaremos un operando de tipo variable de memoria para guardar los datos de
nuestro programa temporalmente (debido a que como no suele ser suficiente con
los registros de que disponemos, éste es el lugar habitual en el que se guardan los
datos).
Y ya por último, a continuación pasamos a ver (en negrita) un caso algo
diferente. Fijaos en que la variable de memoria carácter2 ha sido declarada en el
segmento de datos con dw, en lugar de con db. db significa que la variable de
memoria ocupa 8 bits (los necesarios para codificar un carácter en código ASCII);
dw, por su parte, significa que la variable de memoria ocupa 16 bits. En
consecuencia, en este segundo caso, la constante 65 se codifica con 16 bits (los 8
bits de mayor peso serán cero). En el caso4, el operando de origen es, del mismo
modo, una variable de memoria; no obstante, en este caso es una variable de 16
bits. Como ya hemos comentado, tanto el operando de origen como el de destino
tienen que ser del mismo tamaño, motivo por el que ahora se utilizará un registro
de 16 bits (dx).
Fundamentos de computadores · 35
7. VARIABLES DE MEMORIA: DECLARACIÓN Y USO
Las variables de memoria sirven para almacenar información en memoria
de forma temporal. Esta información puede ir cambiando durante la ejecución del
programa, o bien puede mantenerse constante (según nos interese). Con el fin de
especificar a qué parte de la información nos interesa acceder, identificaremos cada
variable de memoria con un nombre.
Una variable de memoria se representa mediante un nombre declarado
dentro del segmento de datos, para el que se define un tamaño y, de forma
opcional, un valor inicial.
Cuando se ejecuta el programa ensamblador, el sistema reserva en la
memoria el espacio correspondiente al tamaño de las variables. Estas posiciones de
memoria estarán inicializadas o no en función de la manera en que hayamos
declarado la variable. Desde el programa ensamblador podremos acceder al valor
de la variable especificando su nombre entre corchetes.
Las variables de memoria aparecen declaradas en el segmento
de datos. Al declarar una variable de memoria, hay que indicar su
tamaño y, de forma opcional, el valor inicial de la misma. En el ejemplo,
en negrita, podéis ver las tres posibilidades que nos ofrece el
ensamblador para indicar el tamaño:
 db indica que el tamaño es un byte.
 dw indica que el tamaño es un word (dos bytes).
 dd indica que el tamaño es un double (cuatro bytes).
Por lo tanto, tened en cuenta que var1 es una variable de 8 bits,
var2, una de 16 bits y var3, una de 32 bits.
Después del tamaño, aparecerá especificado el valor inicial. Dicho
valor puede aparecer especificado en cualquier formato: decimal,
hexadecimal o bien en código ASCII. En el ejemplo, hemos inicializado
todas las variables en 1. No obstante, fijaos en que la primera variable
ocupa 8 bits, la segunda, 16 bits y la tercera, 32 bits.
Si nos es indiferente el valor en el que queremos inicializar una
variable, lo podemos especificar de la siguiente manera:
var dw ?
Se puede declarar una lista de variables del mismo tipo (un
vector) tras haber indicado el tamaño que queremos que tenga cada
elemento (db, dw o dd), poniendo los valores iniciales de forma consecutiva.
En el siguiente ejemplo podéis observar que var4 es una lista de diez
elementos de tamaño byte inicializados con códigos ASCII. Podemos especificar los
códigos ASCII escribiendo los caracteres entre comillas, o bien escribiendo los
códigos separados por comas.
De forma similar, var5 es una lista de cinco elementos de tamaño word
inicializados en los valores 1, 3, 5, 7, y 9, respectivamente. En caso de que
tengamos números naturales o enteros, éstos se ponen directamente separados
por comas.
Por último, disponemos de una alternativa para declarar listas de una gran
cantidad de elementos. Ésta es la forma en la que se ha declarado la variable var6,
es decir, se ha indicado que consta de diez elementos de tamaño word, inicializado
cada uno de ellos en el valor cero.
Se puede declarar una lista de variables del mismo tipo (un vector) tras haber
indicado el tamaño que queremos que tenga cada elemento (db, dw o dd),
poniendo los valores iniciales de forma consecutiva.
En el siguiente ejemplo podéis observar que var4 es una lista de diez
elementos de tamaño byte inicializados con códigos ASCII. Podemos especificar los
códigos ASCII escribiendo los caracteres entre comillas, o bien escribiendo los
códigos separados por comas.
De forma similar, var5 es una lista de cinco elementos de tamaño word
inicializados en los valores 1, 3, 5, 7, y 9, respectivamente. En caso de que
36 · Fundamentos de computadores
tengamos números naturales o enteros, éstos se ponen directamente separados
por comas.
Por último, disponemos de una alternativa para declarar listas de una gran
cantidad de elementos. Ésta es la forma en la que se ha declarado la variable var6,
es decir, se ha indicado que consta de diez elementos de tamaño word, inicializado
cada uno de ellos en el valor cero. Se accede al resto de los elementos de la lista
sumando un desplazamiento con respecto al elemento inicial. Así pues, para
acceder al quinto elemento de la lista de caracteres, se suma un desplazamiento de
4 con respecto al elemento inicial.
Tened en cuenta que se han creado los dos accesos mencionados de
diferente manera: en el primer caso, se ha movido una constante, y, en el
segundo, un registro. En cualquier caso, es importante que veamos que el tamaño
de ambos operandos en cada instrucción es el mismo: 8 bits.
Al acceder a los elementos de una lista de words debemos tener en cuenta
que cada elemento ocupa dos bytes. En consecuencia, para llevar a cabo el acceso,
hay que sumar un desplazamiento de dos por cada elemento.
Las siguientes instrucciones no tienen ningún efecto sobre lo que se escribe por
pantalla, pero podéis apreciar el acceso a la lista de words por el desplazamiento de
8 con respecto al inicio de la variable de memoria var5. Es decir, que se accede al
quinto elemento de esta lista (el último elemento, que contiene el valor 9).
Además, dado que se accede a un elemento de 16 bits, tendremos que utilizar un
registro de 16 bits (en este caso, el registro ax). Como habréis podido observar,
estas instrucciones copian el último elemento de la lista sobre el elemento inicial.
En teoría, podríamos hacer esta acción de la siguiente manera:
mov [var5], [var5 + 8]
Sin embargo, recordad que los dos operandos de una misma instrucción no
pueden ser variables de memoria, motivo por el que el movimiento se ha tenido
que efectuar en dos instrucciones. Tras la ejecución de estas dos instrucciones, el
contenido del vector será: 9, 3, 5, 7 y 9.
8. VARIABLES DE MEMORIA: MODALIDADES DE ACCESO
Las variables de memoria están declaradas en el segmento de datos y se
identifican mediante un nombre; utilizaremos este nombre para acceder a la
variable. Para acceder a una variable en concreto, sólo tenemos que poner el
nombre entre corchetes. De forma parecida, si nos interesa acceder a una posición
concreta de una variable de tipo lista, sólo tenemos que que poner entre corchetes
el nombre de la variable y el desplazamiento apropiado.
En algunos casos, puede ser interesante acceder a una variable a priori
indeterminada, o bien a una posición indeterminada (o variable) de una variable de
tipo lista. Pues bien, en estos casos, para especificar el dato de memoria al que
queremos acceder, es necesario utilizar registros entre corchetes.Para el acceso de
variables de memoria con registros, podemos utilizar dos tipos de registros: los
registros base y los registros índice.
Los registros base sirven para especificar la dirección inicial (dirección
base) de una variable de memoria. En ensamblador, podemos encontrar un registro
base para referenciar datos llamado bx. Existe un segundo registro base que se
utiliza para referenciar información de la pila llamado bp, aunque, de momento,
sólo utilizaremos el bx.
Los registros índice sirven para especificar un desplazamiento (índice) a
partir de una dirección base. En ensamblador, podemos encontrar dos registros
índice, los cuales reciben el nombre de si y di.
El programa que encontraréis a continuación escribe una lista de números pares o
impares, según la tecla que el usuario haya introducido. En pricipio, hay dos
vectores con números pares, y a uno de éstos se le suma +1 para hacerlos
impares. La utilidad de este programa sólo es ilustrar las diferentes modalidades de
acceso a las variables de memoria. Fijaos en el segmento de datos, en el que a
Fundamentos de computadores · 37
parte del mensaje hay dos listas de números: par e impar. En un principio, la lista
impar contiene números pares, así que el primer bucle del programa recorre esta
lista e incrementa cada uno de sus elementos. Para hacer esto, la instrucción
marcada en negrita utiliza el nombre de la variable en cuestión y suma el valor del
registro índice si.
Como podéis observar, el registro si vale inicialmente cero, y en cada
iteración del bucle se incrementa en dos unidades. Así, se va generando el
desplazamiento adecuado para acceder a cada elemento del vector. En tal caso,
diremos que esta instrucción accede a unas posiciones indeterminadas (de hecho,
unas posiciones variables) de una variable determinada (concreta) de tipo lista.
Observad que, como siempre, el registro índice se ha inicializado con
anterioridad, en este caso en cero, puesto que, de lo contrario, se accedería a
posiciones indeterminadas de memoria (dependiendo del valor del registro si).
Después del primer bucle que incrementa los elementos del vector impar, se
escribe el mensaje por pantalla y se pide al usuario que pulse una tecla.
A continuación tenemos el condicional que comprueba si la tecla pulsada es
una "P" o una "S". En caso de que no sea ni una "P" ni una "S", el programa salta
al final y no hace nada. Dependiendo de la tecla pulsada, se ejecuta la instrucción
lea utilizando como operando de origen el vector par o el vector impar.
La instrucción lea pone la dirección de memoria que
tiene el operando de origen en el operando de destino. Por lo
tanto, el operando de origen tiene que ser por fuerza de tipo
variable de memoria. En este caso, en el registro bx ponemos la
dirección de memoria del vector par o del vector impar, según la
tecla introducida. Fijaos en la diferencia existente entre la
instrucción mov y la instrucción lea. En el primer caso,
accedemos al valor de la variable de memoria, y, en el segundo,
a la dirección de la variable de memoria.
El objetivo de poner la dirección de una variable de
memoria en el registro bx es poder utilizar el registro entre
corchetes para efectuar el acceso a la variable de memoria a la
que apunta.
Una vez tenemos una dirección de memoria en el
registro bx, podemos acceder a ella poniendo el registro bx
entre corchetes de la siguiente manera (de hecho, podemos
utilizar ax o cualquier otro registro):
mov ax, [bx]
En el ejemplo podéis observar que se accede a una
dirección de memoria formada por la suma de bx y si. Es decir,
a una variable no fija (la que apunta al registro bx) con un
desplazamiento variable (el que indica el registro si). Es
necesario tener en cuenta que para realizar este tipo de acceso
no podemos utilizar ningún registro diferente del registro base o
del índice.
De momento, utilizaremos bx como registro base, y si o
di, como registros índice. Más adelante, cuando estemos en la
pila, también utilizaremos el registro base bp.
En general, este hito podemos resumirlo determinando
que se puede acceder a una posición de memoria mediante la
siguiente modalidad de direccionamiento:
mov ax, [base + indice + despl],
donde base puede ser el nombre de una variable o un registro base (o nada),
indice hace referencia en un registro índice (o nada), y despl puede ser cualquier
constante (o nada).
La expresión que especifica la dirección de memoria a la que se quiere
acceder sólo puede contener el operando "+", la suma, o para constantes, también
el operando "-", la resta. Por lo tanto, los siguientes accesos serían incorrectos:
mov ax, [var * 2]
mov ax, [bx – si]
mov ax, [var + 2 * si]
38 · Fundamentos de computadores
9. INSTRUCCIONES ARITMÉTICAS
Consideremos las instrucciones aritméticas que podemos implementar de
forma directa con el lenguaje de ensamblador. Las instrucciones aritméticas del
lenguaje de ensamblador permiten realizar una operación aritmética con uno o dos
operandos de origen y guardar el resultado sobre un operando de destino. Las
instrucciones de ensamblador del i8086 no permiten especificar más de dos
operandos. Así pues, uno de los operandos de origen siempre es, además, el
operando de destino.
Por ejemplo, para sumar dos números, la sintaxis es:
add num1, num2
En este caso, se desarrolla la operación: num1 = num1 + num2.
De forma similar, otras operaciones que requieren un solo operando de
origen, como el incremento (sumar + 1), se especifican con un solo operando. Es
decir:
inc num
En este caso, la operación que se realiza es: núm = núm + 1.
Existen otras instrucciones en las que, para ejecutar la operación, se toma
siempre un operando (por lo general un registro) de manera implícita. Es decir, uno
de los operandos no se especifica y se toma siempre un determinado registro.
Ejemplos de estas instrucciones son la multiplicación y la división, que veremos más
adelante.
El primer bloque de instrucciones hace una suma. Cuando queremos
sumar dos números y guardar el resultado obtenido, tenemos que hacer,
forzosamente, el cálculo por medio de un registro auxiliar (en el ejemplo
utilizamos el registro ax) y después guardar el resultado en la variable de
memoria correspondiente.
Otra posibilidad la encontramos cuando queremos sumar dos números
y guardar el resultado obtenido sobre uno de estos dos números. En este caso,
en teoría podríamos hacer:
add [resultado], [numero]
Sin embargo, recordad que existe la limitación de que los dos
operandos no pueden ser variables de memoria. En consecuencia, en este caso
también tendremos que efectuar el cálculo por medio del registro ax, pero de
una manera algo más eficiente que en el ejemplo:
mov ax, [numero]
add [resultado], ax
Para la resta sólo debemos tener cuidado con el orden en que se
especifican los operandos, ya que la operación resta el operando de origen del
operando de destino (sobre el que guarda el resultado). Es decir, la operación
op1 = op1 - op2 se hace de la siguiente manera:
sub op1, op1
Ahora bien, la operación op1 = op2 - op1, siendo op1 y op2 variables de
memoria, tendría que llevarse a cabo de una forma más sutil:
mov ax, [op1]
mov bx, [op2]
sub bx, ax
mov [op1], bx
Fundamentos de computadores · 39
Las operaciones de suma y resta se ejecutan igual si se realizan sobre
números naturales o enteros, ya que el algoritmo de suma (o resta) para números
naturales y enteros (codificados en complemento a 2) es el mismo.
La operación de multiplicación es más delicada que la suma y la resta; su
funcionamiento depende del tamaño del operando que se especifique:
 Si el operando es de 8 bits, se ejecuta: ax = operando × al.
 Si el operando es de 16 bits, se ejecuta: dx _ ax = operando × ax.
Cuando la multiplicación es de 8 bits, se multiplica el operando por al y se
guarda el resultado obtenido en el registro ax. Es decir, se utiliza el registro ax de
manera implícita. Además, fijaos en que el resultado de haber multiplicado dos
números de 8 bits podría ocupar hasta 16 bits; por consiguiente, esta operación
guarda el resultado en ax.
De forma parecida, cuando la multiplicación es de 16 bits, ésta utiliza de
manera implícita el registro ax como segundo operando y guarda el resultado en
los registros dx (los 16 bits de más peso) y ax (los 16 bits de menos peso), ya que,
en este caso, el resultado ocupará hasta 32 bits.
Aquí, sí debemos diferenciar si la multiplicación se realiza con números
naturales o enteros. De hecho, la instrucción imul multiplica números enteros,
mientras que la instrucción mul, hace lo propio con números naturales. Tanto en la
instrucción mul como en la instrucción imul, el operando que se especifica tiene
que ser, por fuerza, de tipo registro o variable de memoria.
Por último, veamos la operación de división, que también depende del
tamaño del operando especificado:
 Si el operando es de 8 bits, se ejecuta:
al = ax / operando ah =
ax % operando.
 Si el operando es de 16 bits, se ejecuta: ax = dx_ax / operando
dx = dx_ax % operando.
Donde / representa el cociente, y %, el resto de la división. Es decir, el
operando especificado en la instrucción representa el divisor, y el dividendo se
toma de forma implícita del registro ax (y del dx para divisores de 16 bits). En este
segundo caso, el dividendo será un número de 32 bits formado por dx (los 16 bits
de más peso) y ax (los 16 bits de menos peso).
De manera similar, hallamos las instrucciones div y idiv para hacer
divisiones: la primera la realiza asumiendo que los números son naturales, y la
segunda, asumiendo que los números son enteros. Fijaos en que si queremos
dividir dos números de 16 bits, la operación de división utiliza igualmente el registro
dx de manera implícita. Por lo tanto, en dx tiene que estar la extensión de signo, si
los números son enteros, o simplemente cero, si los números son naturales. De
manera análoga, si estamos interesados en dividir dos números de 8 bits, en la
parte alta del registro al (ah) tendrá que estar la correspondiente extensión de
signo.
La instrucción cbw representa una extensión de signo del valor de al sobre
ah. Asimismo, la instrucción cwd forma una extensión de signo del valor de ax
sobre dx. Prestad atención al hecho de que en el ejemplo hay que utilizar la
instrucción cwd para realizar la extensión de signo correspondiente sobre el registro
dx.
Las operaciones aritméticas mostradas en el ejemplo son las operaciones
básicas, suficientes para realizar cualquier cálculo. A continuación, pasamos a
describir otras operaciones aritméticas que también pueden ser interesantes:
Incremento y decremento
Existen unas instrucciones más eficientes que las instrucciones básicas de
suma y resta para sumar o restar +1 a un operando, y son éstas:
inc operando
dec operando
40 · Fundamentos de computadores
Fijaos en que el operando debe ser un registro o una variable de memoria,
ya que carece de sentido incrementar o decrementar una constante.
Complemento a 2
Para cambiar un número entero de signo disponemos de la siguiente
instrucción:
neg operando
Puesto que el i8086 representa los números enteros con complemento a 2,
de hecho, esta instrucción hace el complemento a 2 del operando especificado.
Como siempre, este operando tiene que ser un registro o una variable de memoria.
Multiplicación por números potencia de 2
En binario, una multiplicación por un número potencia de 2 (por ejemplo,
2N) equivale a desplazar este número N posiciones hacia la izquierda; es decir, a
poner N ceros por la derecha (pensad en decimal: una multiplicación por 1.000
equivale a poner tres ceros a la derecha del número en cuestión).
Es evidente que desplazar un número a la izquierda es más fácil que hacer
una multiplicación. Por lo tanto, en ensamblador se acostumbran a utilizar las
instrucciones de desplazamiento para efectuar una multiplicación por un número
potencia de 2. Estas instrucciones son:
shl operando, 1
shl operando, cl
sal operando, 1
sal operando, cl
shl se utiliza para números naturales y sal, para números enteros. Cuando
deseamos desplazar una sola posición hacia la izquierda (multiplicación por 2),
podemos utilizar la constante 1 como segundo operando; de lo contrario, hay que
poner en el registro cl el número de posiciones que queréis desplazar hacia la
izquierda.
División por números potencia de 2
Parecido al caso de la multiplicación, es el de la división en
binario por un número potencia de 2, que equivale a desplazar este
número las posiciones correspondientes hacia la derecha. Las
instrucciones en ensamblador para realizar desplazamientos hacia la
derecha son las siguientes:
shr operando, 1
shr operando, cl
sar operando, 1
sar operando, cl
10. INSTRUCCIONES LÓGICAS
Las instrucciones aritméticas realizan cálculos sobre valores
de 8, 16 ó 32 bits, interpretando todos los bits de manera unificada
según una codificación. De forma complementaria, también hay otro
tipo de operaciones que trabajan bit a bit, que son las instrucciones
lógicas.
Las instrucciones lógicas tienen mucha importancia en áreas
específicas como, por ejemplo, la programación lógica. Sin embargo,
para nosotros serán útiles de un modo especial cuando manipulemos
los registros de los controladores de los periféricos.
Las instrucciones lógicas del lenguaje de ensamblador
permiten desarrollar una operación lógica bit a bit utilizando uno o
Fundamentos de computadores · 41
dos operandos de origen y guardar el resultado obtenido sobre un operando de
destino.
Las operaciones lógicas básicas son and, or y not, aunque el lenguaje de
ensamblador también nos ofrece la operación xor. Además, de manera similar a las
instrucciones aritméticas, uno de los operandos de origen es siempre el operando
de destino.
En el ejemplo hallamos declaradas dos variables de memoria de 8 bits: el valor
original (una con valor inicial igual a 75 especificado en decimal) y la copia (con un
valor inicial cualquiera especificado, en este caso, en hexadecimal). Nuestra
intención es observar bit a bit el valor original y, según este bit, poner un 1 o un 0
sobre la copia.
En concreto, el valor original vale 75 y el valor inicial de la copia es f0h. Para aislar
el valor de un solo bit utilizaremos lo que se conoce como una máscara de bits. Una
máscara de bits es una lista de bits en la que sólo los bits que nos interesa tratar
valen 1; los demás valen 0. En nuestro caso, dado que queremos tratar los bits
uno a uno, inicializamos el registro al con una máscara de bits en la que todos los
bits valen 0 excepto uno. En el ejemplo, hemos inicializado el registro al en la
constante 1, pero hemos expresado esta constante en binario. Éste es un recurso
común cuando se manipulan bits, ya que queda mucho más claro lo que
pretendemos hacer si el valor 00000001 b está especificado. Utilizaremos esta
máscara para aislar el bit que nos interese tratar. Teniendo en cuenta que
querremos tratar todos los bits, al final del bucle hacemos un desplazamiento hacia
la izquierda del registro al; es decir, desplazamos este 1 a la siguiente posición de
la izquierda. Haremos este desplazamiento ocho veces para, de este modo, tratar
todos los bits del valor original.
Con la máscara que tenemos en el registro al podemos aislar un bit del valor
original haciendo una operación and lógica del valor original con la máscara.
Fijaos en que la operación and de cualquier valor con 0 es siempre 0. Por
lo tanto, el resultado de la operación and de cualquier valor con la máscara dejará
todos los bits en 0, excepto el bit que se encuentra en la misma posición que el 1
que hay en la máscara. Este bit se mantendrá en 1, si el valor original era 1, o bien
se quedará en 0, si el valor original era 0.
Prestad atención al hecho de que antes de desarrollar la operación and
hemos copiado el valor original en el registro bl, ya que and modifica el operando
de destino, y en este programa nos interesa mantener el valor original. Así pues,
tras haber ejecutado la operación and, el registro bl valdrá 0, si el bit
correspondiente era un 0, ó 1, en caso contrario.
Continuamos con el ejemplo. Se ejecutará el código de la etiqueta bit1, si el
bit analizado es un 1, o bien se ejecutará el código que hay en la etiqueta bit0, si el
bit analizado es un 0.
Cuando el bit es un 1, queremos forzar la escritura de un 1 en el valor copia,
respetando el valor de los otros bits. Para forzar la escritura de un bit en 1 (el bit
correspondiente a la máscara), se ejecuta una operación or de la máscara sobre el
operando de destino. Fijaos en que la operación or con ceros mantiene los valores
originales, y con un 1, fuerza que el bit correspondiente valga 1.De manera
análoga, cuando el bit es un 0, queremos forzar la escritura de un 0 en el valor
copia, respetando el valor que tienen los otros bits.
Para forzar la escritura de un bit en 0 (el bit correspondiente a la
máscara), se ejecuta una operación and del inverso de la máscara sobre el
operando de destino. Así pues, para forzar la escritura de un 0 necesitamos el
inverso de la máscara, es decir, nos hace falta otra máscara con los bits en 0, si los
bits de la máscara original eran 1, y los bits en 1, en caso de que los bits de la
máscara original fuesen 0. Y, esto se consigue mediante la instrucción not. Tened
en cuenta que al hacer la operación and con 1 se mantienen los valores de los bits
originales; y al hacerla con 0, se fuerza la escritura de un 0.
En el ejemplo habéis podido ver el mecanismo para la consulta de un bit a
partir de la operación lógica and, que modifica el operando de destino. El
inconveniente de este mecanismo es que si sólo nos interesa consultar el valor (sin
modificarlo), primero hay que hacer una copia para que la modificación se realice
sobre la copia.
42 · Fundamentos de computadores
Existe otra instrucción que permite realizar la misma consulta, pero sin
modificar ningún operando: ésta es la instrucción test. Su funcionamiento es el
mismo que el de la instrucción and, excepto en el hecho de que no modifica el
operando de destino. Por lo tanto, el mismo ejemplo se habría podido hacer de
forma más eficiente si se hubiera utilizado la instrucción test.
Por otra parte, la instrucción xor hace la O exclusiva bit a bit entre los dos
operandos especificados. Esta instrucción, además de utilizarse para realizar la
operación lógica correspondiente, a menudo se utiliza para inicializar un registro en
cero de la siguiente manera:
xor reg, reg
Apreciad que la O exclusiva de cualquier valor consigo mismo es siempre cero.
Este mecanismo es más eficiente que mover directamente un cero al
registro, ya que el acceso a un operando registro es más eficiente que el acceso a
un operando constante.
11. LA PILA


Nos podemos imaginar la pila como una estructura vertical:
en la cual se puede insertar un dato encima, o bien
de la cual podemos recuperar el dato que se había insertado encima.
La pila es una zona auxiliar de memoria que sirve para guardar
información de manera temporal. Por lo general se accede a la pila utilizando unas
instrucciones específicas. El funcionamiento de la pila es parecido al de un
conjunto de elementos colocados uno encima del otro en forma de pila (de aquí
viene su nombre). En esta pila, se puede insertar un elemento encima, o se puede
sacar su elemento de encima, tal y como podemos ver a continuación:
Es importante que al representar una pila marquemos siempre la cima,
puesto que de esta manera siempre sabremos dónde es "encima". Para insertar un
dato en la cima de la pila, utilizaremos una instrucción llamada push. Asimismo,
para extraer el dato que se encuentra en la cima de la pila, utilizaremos una
instrucción llamada pop.
De momento, vamos a ver que la finalidad de una pila es guardar de forma
temporal el contenido de un registro que necesitamos para desarrollar otra tarea y
así poder recuperar, más adelante, el valor de este registro, o bien servirá para
guardar temporalmente cualquiera otro dato en un orden determinado.
Una pila debe estar declarada en el segmento correspondiente
para que se pueda llevar a cabo su utilización. De forma parecida a los
segmentos de datos y código, utilizamos la directiva .stack para hacer la
declaración.
La declaración de la pila se realiza mediante la directiva .stack
seguida del tamaño, en bytes, deseado para la pila. Es difícil saber con
exactitud qué tamaño debe tener la pila, así que se acostumbra a declarar
un tamaño lo bastante grande como para que no tengamos problemas.
Por lo general, en nuestros programas, suele ser suficiente con un tamaño
de pila de 1.024 bytes. Prestad atención al código. Está formado por dos
bucles, el primero sirve para recorrer los elementos del vector, en el que
se utiliza el registro como registro índice. Para inicializar el registro si en
cero hemos utilizado la operación lógica xor tal como hemos explicado. En
general, siempre es mejor utilizar operandos registros que operandos
constantes.
Como siempre, recordad que si tenemos que recurrir a un vector
de elementos de tamaño word, el índice debe verse incrementado en dos
unidades. En el primer bucle del ejemplo hallamos colocados en la pila los
elementos del vector mediante la instrucción push. Recordad que esta
instrucción inserta el operando especificado en la cima de la pila. La
instrucción push inserta un dato de 16 bits en la cima de la pila. El
Fundamentos de computadores · 43
operando de esta instrucción tiene que ser, obligatoriamente, de tipo registro o
variable de memoria. Dado que se han insertado los elementos del vector de
forma ordenada, desde el primer elemento hasta el último.
En el segundo bucle se extraen de la pila los elementos
del vector mediante la instrucción pop. Recordad que esta
instrucción extrae el elemento de la cima de la pila y lo guarda
sobre el operando especificado. La instrucción pop extrae un
dato de 16 bits de la cima de la pila. Así como en la instrucción
push, el operando de esta instrucción tiene que ser
obligatoriamente de tipo registro o variable de memoria.
Dado que los elementos se han extraído igualmente
de manera ordenada desde el primer elemento hasta el último,
al acabar el segundo bucle tendremos la pila vacía. No
obstante, además, el vector se habrá invertido, ya que el primer
elemento que se extrae fue el último elemento insertado; el
segundo elemento que se extrae fue el penúltimo elemento
insertado, y así sucesivamente para todos los elementos.
De hecho, la pila es una zona de memoria que se
gestiona de forma implícita con dos registros: ss y sp. Podemos
considerar que el registro ss apunta estáticamente al inicio de la
zona de memoria que representa la pila, algo que en este curso
vamos a pasar por alto. El registro sp apunta constantemente a
la cima de la pila; por norma general, tampoco lo tocaremos
directamente, aunque las instrucciones push y pop sí lo
modifican de forma implícita.
Por último, existe un tercer
registro, el bp, relacionado con la pila, un registro que utilizaremos más adelante
para tareas de soporte a la pila.
12. SUBRUTINAS: LLAMADAS Y RETORNO
Por lo general, en un programa existen tareas comunes (por ejemplo,
escribir un mensaje por pantalla) que tienen que ejecutarse desde diferentes partes
del programa, para lo cual contamos, básicamente, con dos posibilidades:
 Replicar el código correspondiente (escribir el mensaje por
pantalla) a cada parte del programa en el que queremos ejecutar la
tarea.
 Definir una subrutina que escribe el mensaje y llamarla desde cada
parte del programa en que nos interesa ejecutar la tarea.
El objetivo de este hito es conocer los mecanismos para declarar una
subrutina en ensamblador, así como las instrucciones de llamada y retorno a la
subrutina. En primer lugar definiremos qué es una subrutina. Una subrutina es un
bloque de código que desempeña una tarea determinada. Este bloque de código
puede ser llamado desde diferentes partes del programa y, al finalizar, vuelve al
lugar desde donde ha sido llamado.
Las subrutinas, por diferentes motivos, son preferibles a la replicación de
código. Por una parte, replicar código implica hacer mayor el programa (diferentes
copias del mismo código); por otra, si se pretende modificar el comportamiento de
este bloque de código (porque existe un error o porque queremos mejorarlo) y el
código está replicado, entonces hay que modificar cada una de las copias del
mencionado código. De hecho, sin embargo, la principal ventaja de las subrutinas
es que permiten un diseño modular del programa, hecho que facilita su diseño y
comprensión.
Veamos en el siguiente ejemplo un programa que llama tres veces a una
subrutina. Esta subrutina escribe un mensaje por pantalla, así que, este programa
escribe el mensaje tres veces por pantalla.
Para poder llamar una subrutina, en primer lugar es necesario tener una
pila declarada. El motivo de esto es que las subrutinas utilizan la pila de manera
44 · Fundamentos de computadores
implícita, tal y como veremos más adelante. Como siempre, el tamaño de la pila
tiene que ser lo suficientemente grande.
Para llamar una subrutina se utiliza la instrucción call. Un llamamiento a
una subrutina es parecido a una instrucción de salto con la diferencia de que,
después de ejecutar el código correspondiente, se vuelve al punto en el que se ha
llamado la subrutina.
Fijaos en que el código del ejemplo llama tres veces consecutivas a una
subrutina con el nombre escribir.
Para escribir una subrutina, ésta tiene que encontrarse fuera de los límites
marcados por las macros STARTUPCODE y EXITCODE, tal y como vemos en el
ejemplo. Apreciad el hecho de que la subrutina se identifica con una etiqueta que
marca el punto de entrada a la subrutina. Cualquier subrutina acaba con la
instrucción ret, que provoca un salto de retorno a la instrucción posterior a la que
ha llamado la subrutina.
La instrucción call se comporta, en apariencia, como una instrucción de
salto hacia una etiqueta. La instrucción ret, sin embargo, se comporta como una
instrucción de salto hacia la instrucción posterior a la que ha ejecutado el call, algo
que conseguimos gracias a que la instrucción call, a diferencia de una instrucción
de salto, guarda la dirección a la que tiene que volver: la dirección de retorno.
Una tarea importantísima que conviene realizar al programar una subrutina
es salvar los registros que utiliza la subrutina. Tal y como podéis ver en el ejemplo,
sólo se salvan los registros que se modifican dentro de la subrutina; los registros se
salvan en la pila. Así pues, para salvar los registros utilizaremos la instrucción push
y para restaurarlos, la instrucción pop.
La subrutina del ejemplo escribe un mensaje determinado por la pantalla.
Fijaos en cómo se salvan los registros ax y dx, que son los que se utilizan
para ejecutar el código correspondiente. Por lo tanto, el siguiente programa
escribe tres veces el mensaje por la pantalla.
13. SUBRUTINAS: NO PARÁMETROS Y RETORNO DE RESULTADO
En algunas ocasiones, puede sernos de interés el hecho de que el
código de la subrutina sea ejecutado sobre unos determinados valores de
entrada; por ejemplo, una subrutina que escriba por pantalla un número
determinado. En este caso, hay que definir un mecanismo para poder pasar a
la subrutina uno o varios parámetros.
Asimismo, puede interesarnos que la subrutina devuelva un resultado
tras su ejecución; por ejemplo, una subrutina que sume dos números
determinados y que devuelva el resultado. En este caso, sería conveniente
definir un mecanismo para el retorno del resultado. Una de las ventajas del
diseño modular es que permite descomponer un programa en tareas
independientes (las subrutinas), de manera que diferentes programadores se
puedan repartir la implementación de dichas tareas. En este caso, es
corriente que un programador escriba un código que llame a subrutinas
escritas por otro programador.
A una subrutina se le puede pasar cualquier número de parámetros.
El mecanismo de paso de parámetros más utilizado es el paso de parámetros
por la pila, ya que permite pasar un número de parámetros en principio
ilimitado. Pues bien, éste va a ser el mecanismo que utilizaremos en nuestros
programas.
Por otra parte, también existen muchas formas diferentes de
devolver un resultado. A diferencia del paso de parámetros, por norma
general las subrutinas devuelven un solo resultado. El mecanismo para el
retorno del resultado que utilizaremos nosotros es el retorno de resultado por
medio de un registro. En este ejemplo tenemos un programa que llama a una
subrutina que devuelve como resultado la suma de dos números que se han
pasado como parámetros.
Fundamentos de computadores · 45
El paso de parámetros a una subrutina se hace mediante la pila, lo cual
significa que antes de llamar a una subrutina, hay que dejar los parámetros
correspondientes en la pila.
De esta manera, al ejecutarse la subrutina, los valores determinados de
entrada sobre los que se ejecuta el código de la subrutina se encuentran en la pila.
En el ejemplo han aparecido destacadas en negrita las instrucciones de paso de
parámetros y llamamiento de la subrutina. En este caso, se pasan como parámetro
los valores de las variables de memoria núm1 y núm2. Recordad que, al llamar a
una subrutina, la dirección de retorno queda guardada en la pila. Así pues, el
estado de la pila tras haber saltado a la subrutina es el que podemos ver en la
figura.
Fijaos en que los parámetros se deben pasar antes de llamar a la subrutina,
puesto que, de lo contrario, se llamaría a la subrutina sin haber puesto ningún valor
en la pila y, al ejecutarse la subrutina, no encontraría los parámetros de entrada.
Por último, es importante tener en cuenta el orden en el que se ponen los
parámetros en la pila, ya que la subrutina tiene que saber en qué posición se hallan
los parámetros. En este ejemplo, sin embargo, no es importante el orden, dado que
la suma es conmutativa.
Una vez se han pasado los parámetros y se ha llamado a la subrutina, ésta tiene
que poder acceder al valor de los parámetros para, de este modo, ejecutar el
código correspondiente; algo que se consigue mediante el registro bp.
El registro bp es un registro base que se utiliza como registro auxiliar para
la manipulación de la pila. Y, puesto que es un registro base, se puede utilizar para
acceder a una variable de memoria; en nuestro caso, servirá para acceder a los
parámetros de la pila.
Así pues, en una subrutina, en primer lugar se hace el enlace dinámico, que
consiste en salvar el registro bp (que es un registro que se utiliza en la subrutina),
y después se apunta el bp en una posición fija de la pila (por ejemplo, la posición
de la cima). Y esto podéis verlo con las dos primeras instrucciones de la subrutina.
Para deshacer el enlace dinámico, sólo hay que restaurar el registro bp que se ha
salvado al inicio de la subrutina.
Cuando ya tenemos el registro bp apuntando a una posición fija de la
subrutina, entonces podemos salvar los registros (tantos como sean necesarios, ya
que este hecho no va a afectar a la posición a la que el bp apunta). El estado de la
pila es el que aparece reflejado en la figura. Tened en cuenta, por lo tanto, que los
parámetros se ubican a partir de la posición [bp + 4] en adelante. Conociendo la
posición inicial a partir de la que se encuentran los parámetros, el acceso se
realizará mediante el uso del registro bp, tal y como muestran las instrucciones del
ejemplo resaltadas en negrita: [bp + 4], corresponde al segundo parámetro (el
valor 4), y [bp + 6], al primer parámetro (el valor 3).Así pues, la subrutina accede
a los dos operandos, los guarda en bx y cx, los suma y un registro devuelve el
resultado obtenido.
En general, es indiferente qué registro se utiliza para devolver el resultado;
lo más importante es que tanto al devolver el resultado (dentro de la subrutina),
como al tratarlo (fuera de la subrutina) se utilice el mismo registro. El registro ax
es el que se utiliza convencionalmente, de manera que nosotros también lo
utilizaremos.
Fijaos en que si hay que devolver el resultado por el registro ax, no es
necesario salvar este registro dentro de la subrutina (y, por lo tanto, tampoco hay
que restaurarlo ). Este hecho implicará que, cuando el programador llame a una
subrutina que devuelve un resultado, en el registro ax ya no habrá ningún valor
importante. Por último, al volver de la subrutina (una vez se ha ejecutado la
instrucción ret que extrae la dirección de retorno de la pila) sería conveniente tratar
el resultado y sacar los parámetros de la pila:
Para tratar el resultado basta con saber que éste se halla en el registro ax,
así que si nos interesa asignar el resultado a la variable de memoria nada, sólo
tenemos que ejecutar la instrucción mov que muestra el ejemplo.
En cuanto a extraer los parámetros de la pila, es responsabilidad de quien
46 · Fundamentos de computadores
pone los parámetros sacarlos más adelante, algo que se puede hacer rápidamente
si se suma la medida adecuada al registro sp.
La mayor parte de los lenguajes de programación en alto nivel permiten
tener variables locales dentro de una subrutina, es decir, variables que se crean al
activarse la subrutina y que se destruyen cuando ésta acaba.
Las variables locales son variables de memoria que ocupan de forma
temporal un espacio en la pila, tal y como muestra la siguiente figura:
Por lo tanto, la declaración de la subrutina será un código parecido al siguiente:
push bp
mov bp, sp120
sub sp, N
push ...
push ...
donde N es una constante igual al tamaño que necesitan las variables
locales. Así pues, el acceso a las variables locales se realizará mediante el
uso de [bp - x], mientras que el acceso a los parámetros se realizará
utilizando [bp + x], donde x es, en ambos casos, el desplazamiento
necesario para acceder a la variable deseada.
Al acabar la subrutina, hay que liberar el espacio reservado para
las variables locales, algo que podemos hacer rápidamente de la siguiente
manera:
pop ...
pop ...
mov sp, bp
pop bp
ret
14. SUBRUTINAS: PARAMETROS POR VALOR O POR REFERENCIA
Otro recurso común utilizado por los lenguajes de programación
en alto nivel al programar una subrutina es el paso de parámetros por
referencia. El paso de parámetros por referencia consiste en pasar como
parámetro una referencia (la dirección de memoria) a una variable que
contiene el valor en lugar de pasar una copia de un valor. La diferencia
entre pasar un parámetro por valor o por referencia consiste en que, en el
segundo caso, el valor original de la variable se puede modificar.
Los parámetros se pueden pasar por valor o por referencia. Se
pasa un parámetro por valor cuando se pone en la pila una copia del valor
de este parámetro. En tal caso, si el parámetro se modifica, también se
modifica la copia, de manera que el valor original no se ve alterado. Se
pasa un parámetro por referencia cuando la dirección de memoria de la
variable que contiene el valor está colocada en la pila. En tal caso, si se
modifica el contenido de la dirección de memoria que se ha pasado como
parámetro, se modifica, también, la variable original.
En general, se pasa un parámetro por valor cuando no interesa
que el valor original cambie por la ejecución de la subrutina; y se pasa un
parámetro por referencia, cuando nos interesa que el valor original quede
modificado por la ejecución de la subrutina.
Este ejemplo, igual que el ejemplo 12, realiza la suma de dos
números pasados como parámetros. Sin embargo, en este caso, en lugar
Fundamentos de computadores · 47
de devolver el resultado por el registro ax, se devuelve mediante un tercer
parámetro que se pasa por referencia.
Como ya habéis visto, el paso de parámetros por valor se realiza sólo
poniendo en la pila una copia del valor correspondiente mediante la instrucción
push, tal y como muestran las dos primeras instrucciones marcadas en negrita. No
obstante, para pasar un parámetro por referencia es necesario poner en la pila la
dirección de la variable de memoria correspondiente.
Para poner en la pila la dirección de una variable de memoria es necesario
acceder primero a la dirección con la instrucción lea. Entonces ya se puede poner la
dirección de la variable de memoria utilizando de la misma forma la instrucción
push. Tras haber pasado los tres parámetros, el estado de la pila será el que
muestra la figura. Fijaos en que tendremos debajo la copia de los dos números
núm1 y núm2, a continuación, la dirección de la variable de memoria nada y, por
último, hallaremos la dirección de retorno después de ejecutar el call.
Para acceder a los parámetros pasados por valor se utiliza igualmente el
registro bp. Es necesario que tengamos cuidado a la hora de calcular el
desplazamiento de los parámetros, ya que, a diferencia del ejemplo 12, existe un
tercer parámetro (la dirección de la variable nada) entre núm2 y la dirección de
retorno).
Una vez calculada la suma, la subrutina del ejemplo guarda el resultado en
el tercer parámetro pasado por referencia. Es decir, en la pila se encuentra la
dirección de memoria en la que queremos escribir. Para acceder a la misma,
utilizamos el registro base bx, que permite escribir en una variable de memoria
inicialmente desconocida (la que haya en la pila). Así pues, ponemos la dirección
correspondiente en el registro bx y escribimos en la posición de memoria a la que
apunta bx, es decir, a la variable nada.
Aquí la subrutina no devuelve ningún resultado por el registro ax. En
consecuencia, si se utiliza este registro dentro de la subrutina, primero será
necesario salvarlo y restaurarlo al final de la subrutina. El hecho de que la subrutina
devuelva o no resultado no depende de si existe algún parámetro pasado por
referencia, puesto que en este caso ha sido casualidad. Fijaos, por ejemplo, en el
código del ejemplo 11 en el que no hay parámetro alguno (ni por valor ni por
referencia), y tampoco devuelve ningún resultado.
Y ya para acabar, recordad que, al volver de la subrutina, tenemos que
sacar los parámetros que han sido colocados de antemano en la pila. Esta
operación podemos hacerla fácilmente sumando a sp el tamaño que ocupan los
parámetros (en este caso, 6 bytes). Fijaos en que no es necesario recuperar el
resultado, ya que al volver de la subrutina el resultado ya está en la variable de
memoria nada.
Un parámetro pasado por referencia sirve, por lo general, para pasar la
dirección de una variable de memoria que nos interesa poder modificar dentro de la
subrutina; aunque, también sirve para pasar un vector como parámetro.
Así pues, cuando estemos interesados en pasar un vector como
parámetro, éste será pasado por referencia; esto es, se pondrá en la pila la
dirección del primer elemento del vector. Para acceder a los elementos del vector,
se puede hacer de dos formas: con el registro bx, apuntando al inicio del vector, y
con el registro si, especificando el desplazamiento correspondiente.
Por último, recordad que tanto el operando de origen como el operando de
destino siempre deben tener el mismo tamaño. Cuando se utilizan registros o
variables de memoria declaradas en el segmento de datos, se puede saber el
tamaño del operando según el nombre del registro o la declaración de la variable
en el segmento de datos.
Cuando se accede a la dirección de una variable que no conocemos a priori,
sin embargo, puede suceder que no esté claro el tamaño de los operandos. Por
ejemplo, si se especifica la siguiente instrucción:
mov [bx + si], al
48 · Fundamentos de computadores
está claro que el tamaño de los operandos es 8 bits, dado que el registro al es un
registro de 8 bits. Sin embargo, si se especifica la siguiente instrucción:
mov [bx + si], 0
entonces, no está claro si la dirección [bx + si] hace referencia a una variable de 8
bits o a una de 16 bits. En este caso, al ensamblar el código, el mismo ensamblador
detecta la posible confusión y da el mensaje que encontramos a continuación:
*Warning* fitxer.asm(...) Argument needs type override
Es decir, es preciso que esté especificado el tipo del argumento (el
operando). En este caso, se pueden utilizar los siguientes modificadores:
mov byte ptr [bx + si], 0
mov word ptr [bx + si], 0
para informar de que la variable de memoria es una variable de 8 bits o 16 bits.
15. INSTRUCCIONES DE SOPORTE AL SISTEMA DE E/S
Los dispositivos periféricos son manipulados mediante
lecturas y/o escrituras en los registros del correspondiente
controlador. Estos registros pueden estar guardados en memoria,
como es el caso de la pantalla, o bien en el espacio de direcciones
de entrada/salida. Para acceder a registros guardados en memoria,
se utilizan las instrucciones normales de acceso a memoria que
hemos visto hasta ahora; pero, para acceder a registros guardados
en el espacio de direcciones de entrada/salida, el lenguaje de
ensamblador del i8086 proporciona unas instrucciones específicas.
Veamos ahora un programa en el que se capturan 10 movimientos
de las teclas: make o break. En cada caso, se escribe por pantalla
un mensaje que indica el suceso acaecido.
Después de escribir el mensaje en el que se pide que se
pulsen las teclas, hallamos un bucle principal con un contador de 10
iteraciones. Para la construcción de este programa, dado que no
existe ninguna subrutina, no se declara la pila.
Al pulsar o soltar una tecla, se pone en 1 el bit cero del registro de
estado del controlador de teclado y la encuesta finaliza. En este
momento, la siguiente acción que tenemos que realizar será
comprobar el motivo de esta acción: se ha pulsado la tecla (make) o
se ha dejado la tecla (break).
El registro que contiene información acerca de este hecho es el
registro de datos del controlador de teclado. El bit 7 de este registro
indica que la tecla ha sido pulsada (bit7 = 0) o bien que la tecla ha
sido soltada (bit7 = 1). Los restantes 7 bits (del 0 al 6) codifican la
tecla que se ha pulsado (o soltado).
Para comprobar si es un make o un break, leemos el registro de
datos (dirección 60h) mediante la instrucción in y utilizamos la
máscara 80h que permite aislar el valor del bit 7. Si este bit vale 0,
es un make; de lo contrario, es un break.
Antes de continuar con el análisis del código, es importante que
conozcamos el funcionamiento de los registros del controlador del teclado. En
concreto, nos interesa gestionar dos registros: el registro de estado y el registro de
datos, ambos de 8 bits, que se encuentran en las direcciones 64h y 60h,
respectivamente, del espacio de direccionamiento de entrada/salida.
Fundamentos de computadores · 49
Los bits que nos interesan de estos registros son los siguientes: Cuando el
bit 0 del registro de estado vale 1, esto indica que se ha tocado alguna tecla. Al leer
del registro de datos, este bit se pone de forma automática en cero. Cuando se ha
tocado una tecla, el bit 7 del registro de datos vale 0, si ésta ha sido pulsada
(make), o 1, si ha sido liberada (break).
Una vez tenemos claro el funcionamiento de los registros del controlador
del teclado, podemos implementar el acceso al teclado mediante encuesta.
El acceso mediante encuesta consiste en leer de forma constante el
registro de estado del controlador del dispositivo hasta que detectamos que ha
ocurrido un acontecimiento determinado. Por último, podéis observar en el
ejemplo que, dependiendo de si se pulsa una tecla o se suelta, el registro dx será
inicializado con la dirección del mensaje que nos interesa escribir: <make> o
<break>. A continuación, y antes de acabar el bucle, se escribe el mensaje
correspondiente.
Este ejemplo detecta los 10 primeros acontecimientos. Así pues, si queréis
que detecte cinco veces que se pulsa y se suelta una tecla, es necesario pulsarla
rápidamente para que, de este modo, no se generen repeticiones de la tecla.
Las instrucciones in y out
El acceso en ensamblador a los registros guardados en el espacio de
entrada/salida se realiza mediante las instrucciones in y out. Estas instrucciones se
pueden utilizar de la siguiente manera:
in al, [xxh], para leer un valor de 8 bits.
in ax, [xxh], para leer un valor de 16 bits.
Podemos utilizar esta modalidad para direcciones entre la 000h y la 0 ffh
del espacio de entrada/salida. Si la dirección es superior, entonces hay que utilizar
el registro dx para especificarla:
in al, dx, para leer un valor de 8 bits.
in ax, dx, para leer un valor de 16 bits.
De manera similar, para escribir un registro del espacio de direccionamiento
de entrada/salida:
out
out
out
out
[xxh], al, para escribir un valor de 8 bits.
[xxh], ax, para escribir un valor de 16 bits.
dx, al, para escribir un valor de 8 bits en direcciones > 255.
dx, ax, para escribir un valor de 16 bits a direcciones > 255.
El registro de datos del controlador de teclado
Tal y como hemos visto, el registro de datos del controlador de teclado
codifica la tecla pulsada en los siete bits de menor peso. La codificación de este
registro no se corresponde con el código ASCII de la tecla, ya que el teclado tiene
que codificar teclas especiales que no aparecen en el código ASCII (por ejemplo,
las teclas F1, F2, ..., Shift, Intro, Ctrl, Alt, etc.).
El controlador de teclado define el código de rastreo, y este código codifica
la posición que ocupa la tecla en el teclado. Por consiguiente, para las siguientes
teclas tenemos los códigos:
código
código
código
...
código
código
código
código
código
...
1: tecla <ESC>
2: tecla <1>
3: tecla <2>
16:
17:
18:
19:
20:
tecla
tecla
tecla
tecla
tecla
<q>
<w>
<e>
<r>
<t>
50 · Fundamentos de computadores
Para pasar del código de rastreo al código ASCII, se acostumbra a definir
un vector que contenga en cada posición correspondiente al código de rastreo el
código ASCII de la tecla asociada. Es decir, el vector:
traduccion db - 1,-1,'1234567890',-1,-1,-1,-1,'qwertyuiop'...
De manera que, dado un código de rastreo C, el valor traducción[C]
corresponde al código ASCII de la tecla asociada al código de rastreo C, o bien -1,
si la tecla en cuestión no tiene código ASCII asociado.
En el caso del controlador del teclado, el acontecimiento que nos interesa
detectar es que se pulsa una tecla. En el ejemplo podéis ver cómo se lee del
registro de la dirección 64h del espacio de direccionamiento de entrada/salida
mediante la instrucción in. A continuación, se compara el bit cero del valor leído
con la máscara de bits 01h para, de este modo, aislar el bit cero. Mientras este bit
sea 0, el código salta de nuevo a la instrucción que lee el registro de estado. Fijaos,
también, en que la etiqueta encuesta es utilizada tanto por el bucle que cuenta 10
iteraciones, como por el bucle que implementa la encuesta. Este hecho no va a
suponer ningún problema desde el punto de vista del lenguaje de ensamblador.
16. RUTINAS DE ATENCIÓN A LOS PERIFÉRICOS
Los dispositivos periféricos leen o escriben información
en el mundo exterior de manera asíncrona con respecto a la
ejecución de las instrucciones por parte del procesador. Es decir,
un usuario pulsa las teclas sin tener que sincronizarse para ello
con el reloj del procesador. Con el fin de acceder a esta
información, el procesador se tiene que sincronizar con el
controlador de los dispositivos periféricos.
En este tema 16 se repasarán las técnicas de
sincronización con los controladores de los dispositivos
periféricos. El objetivo es que aprendáis a implementar en
ensamblador las rutinas de atención a la interrupción de los
dispositivos periféricos.
Existen dos técnicas básicas que permiten la
sincronización entre el procesador y los controladores de los
dispositivos de entrada/salida, la sincronización por encuesta y la
sincronización por interrupción.
La sincronización por encuesta es una técnica que
consulta de forma activa el registro de estado del controlador
(por lo general registrado en el espacio de direccionamiento de
entrada/salida) hasta que detecta que se ha producido un
acontecimiento (por ejemplo, se pulsa una tecla).
La sincronización por interrupción es un mecanismo
del procesador que permite que éste pueda ejecutar cualquier
código y que, al producirse un acontecimiento (por ejemplo, se
pulse una tecla), se ejecute una rutina de atención a la
interrupción que alcance este acontecimiento.
Para asociar una rutina a la interrupción de un dispositivo
periférico disponemos del vector de interrupciones. Este vector
está situado al principio de la memoria (a partir de la dirección 0)
que contiene la dirección de una rutina asociada, a su vez, a cada
dispositivo. Cuando se produce una interrupción, se ejecuta la
rutina asociada al correspondiente dispositivo. Por lo tanto, si
queremos que se ejecute una rutina determinada al producirse la
interrupción de un dispositivo, sólo es necesario que pongamos la
dirección de la rutina en cuestión en la posición correspondiente
del vector de interrupciones.
Una rutina de atención a la interrupción es una rutina
cualquiera, como las que hemos visto hasta ahora, pero con dos
Fundamentos de computadores · 51
particularidades:
 Al acabar, el controlador de interrupciones debe informar de que ya ha
finalizado, algo que hay que hacer escribiendo el código EOI (End Of
Interruption, 'final de interrupción') en el registro de control del controlador de
interrupciones.
 En vez de utilizar la instrucción habitual de retorno, hay que utilizar IRET
(retorno de una interrupción).
Fijaos en que una rutina de atención a la interrupción se puede ejecutar en
cualquier momento, de forma independiente de la ejecución de nuestro programa.
Por lo tanto, es difícil que podamos pasar parámetros desde nuestro programa, ya
que no sabemos en qué momento se llevará a cabo la ejecución de la rutina. Antes
de programar la rutina de atención a la interrupción de un dispositivo periférico,
tenemos que saber qué posición del vector de interrupciones ocupa este
dispositivo. El teclado ocupa la posición 9.
El vector de interrupciones está situado en memoria a partir de la dirección
física cero, y cada dirección de una rutina de atención a la interrupción ocupa 4
bytes. Por lo tanto, la rutina de atención a la interrupción del teclado estará situada
a partir de la dirección 36.
Para que se ejecute una determinada rutina cuando haya interrupción del
teclado, sólo es necesario que se ponga la dirección de esta rutina a partir de la
dirección 36 de la memoria; aunque, de forma previa, será necesario guardar la
dirección de la rutina que haya en aquella posición y restaurarla antes de acabar
nuestro programa.
Para acceder a las direcciones de nuestro programa, ponemos la dirección
entre corchetes, tal y como hemos visto hasta ahora; y, para acceder a direcciones
fuera del espacio de direccionamiento de nuestro programa, sin embargo,
utilizaremos de forma adicional unos registros de propósito específico llamados
registros de segmento.
Existen cuatro registros de segmento, llamados cs, ds, es y ss, de los
cuales utilizaremos sólo el registro es para acceder a direcciones fuera de los límites
de nuestro programa. Los otros registros tienen un uso muy determinado, y no los
tenemos que manipular explícitamente.
En el ejemplo podéis ver que se utiliza el registro es para generar la
dirección cero y salvar la dirección de la rutina que hay a partir de la dirección 36
de memoria:
xor ax, ax
mov es, ax ; ponemos cero en el registro es
push es:[36] ; salvamos los dos primeros bytes
push es:[38] ; salvamos los dos bytes restantes
Asimismo, se utiliza este registro para poner la dirección de nuestra rutina a
partir de la dirección 36 de memoria:
lea ax, [RAIteclado] ; tomamos la dirección de la rutina
mov es:[36], ax
; ponemos la dirección
mov es:[38], cs
; ponemos el segmento
Fijaos en que primero ponemos la dirección de la rutina en el registro ax y
después la ponemos en la posición 36 de memoria. A continuación completamos la
dirección (de 4 bytes) para que la rutina pueda ser ejecutada desde fuera de
nuestro programa. Para ello, utilizaremos el registro cs.
En este curso no entraremos en detalles sobre el uso de los registros de
segmento. Pero lo que sí debemos tener claro es que hay que modificar el vector
de interrupciones y la manera de llevar a cabo dicha modificación.
Por último, fijaos en las instrucciones cli y sti, que sirven para inhibir
interrupciones y desinhibir interrupciones, respectivamente, y es conveniente
ponerlas antes y después de modificar el vector de interrupciones, puesto que, de
lo contrario, si por casualidad se recibe una interrupción de teclado cuando sólo se
52 · Fundamentos de computadores
ha modificado la mitad de la dirección (los 2 primers bytes), se saltaría a una
posición aleatoria de memoria y seguramente el programa se quedaría colgado
Una vez modificado el vector de interrupciones (después de haber salvado
el valor original), ya podemos escribir el programa. En este ejemplo, tras escribir un
mensaje, tenemos una espera activa: mientras la variable numTeclas sea menor
que 10, nos esperamos.
Atended al hecho de que si el programa contuviera sólo este código, se
quedaría permanentemente bloqueado. Sin embargo, en realidad existe otro
fragmento de código que se ejecuta de forma concurrente con éste: la rutina de
atención a la interrupción del teclado Raiteclado. Cada vez que se pulse una tecla,
la ejecución del bucle de espera activa se detendrá y se ejecutará la rutina de
atención a la interrupción. En esta rutina, podemos hacer lo que queramos: por
ejemplo, incrementar el número de acontecimientos de teclado númTeclas.
La rutina de atención a la interrupción es una subrutina normal y corriente que
puede desarrollar cualquier tarea (por lo general relacionada con la gestión del
dispositivo que ocasiona la interrupción). En nuestro ejemplo, la subrutina salva y
restaura los registros, escribe un mensaje según la tecla pulsada, e incrementa el
número de acontecimientos detectados relacionados con el teclado.
Ahora bien, tal y como podemos ver en el código, existen dos
particularidades en las rutinas de atención a la interrupción:
 Por una parte, hay que poner el valor 20 h en el registro del espacio de
entrada/salida 20 h para generar la señal de final de interrupción (End Of
Interruption):
mov al, 20 h out [20 h], al
 Por otra, al volver de la subrutina se debe utilizar la instrucción iret
(interruption return), en lugar de la instrucción ret.
Por último, tenemos que comentar que antes de acabar el programa, si
queremos que el teclado funcione tal y como funcionaba antes de ejecutar el
programa, se tienen que restaurar los valores originales de la rutina de atención a
la interrupción en el teclado (que habíamos salvado con anterioridad) en el vector
de interrupciones.
Si el registro es no se ha modificado y todavía mantiene el valor 0, restauramos
directamente los valores salvados en la pila y los ponemos en el vector de
interrupciones en las posiciones correspondientes:
pop es:[38]
pop es:[36]
Igual que al modificar el vector de interrupciones al inicio del programa, es
necesario restaurarlo inhibiendo con anterioridad las interrupciones y
desinhibiéndolas más adelante, mediante el uso de las instrucciones cli y sti.
En este curso gestionaremos sólo dos dispositivos: el teclado y el reloj. La
gestión del teclado ya la hemos visto en los dos últimos hitos, por lo tanto, nos
falta conocer el dispositivo reloj.
El reloj es un dispositivo muy sencillo que permite medir el tiempo. Genera de
manera fija unas 18 interrupciones por segundo y, por lo tanto, para medir el
tiempo tiene que ir contabilizándose el número de interrupciones que lleva
generadas. Cada 18 interrupciones contabilizadas implica que ha pasado un
segundo.
El reloj trabaja siempre por interrupción, y la rutina de atención a la
interrupción del reloj está en la posición 8 del vector de interrupciones. No dispone
de ningún registro de datos ni de control, así que su gestión se limita a poner en el
vector de interrupciones la rutina de atención en la interrupción adecuada.
Registros en el i8086
Los registros del i8086 son todos de 16 bits, aunque en algunos se puede
acceder por separado a la parte baja (8 bits) y a la alta (8 bits). Existen cuatro
Fundamentos de computadores · 53
registros de uso general, que son los que tenemos ocasión de ver en la siguiente
figura:
Como podéis comprobar, los registros ax, bx, cx y dx son de 16 bits, pero
se puede acceder sólo a los 8 bits de más o menos peso (por ejemplo, el registro
ah contiene los 8 bits de más peso del registro ax, y el registro al, los 8 bits de
menos peso del registro ax). A pesar de que estos registros son de propósito
general, veremos que el registro ax y el dx se suelen utilizar para algunas tareas
predeterminadas, que bx es utilizado para guardar en él direcciones de memoria, y
que cx se utiliza como contador.
Asimismo, son registros muy utilizados el registro si y el di. Estos registros,
siempre de 16 bits, se pueden utilizar como registros de propósito general, aunque,
por norma general, serán usados como registros índice.
Otros registros que vamos a utilizar son los registros con un uso más
dependiente; esto es, el registro bp y el registro sp. Los mencionados registros
sirven para gestionar un bloque de memoria llamado pila, aunque no los
utilizaremos ahora, sino más adelante.
Por último, hay otros cuatro registros de segmento (cs, ds, es, ss) que
sirven para gestionar las direcciones de memoria. En esta asignatura, no veremos
el uso de estos registros, excepto el registro es, que utilizaremos al finalizar el
módulo para una tarea muy concreta.
Texto elaborado a partir de:
Fundamentos de computadores I
Montse Peiron Guàrdia, Antonio José Velasco González, Jordi Carrabina Bordoll, Fermín Sánchez
Carracedo, Anna M. del Corral González,
Enric Pastor Llorens, Pedro de Miguel Anasagasti
Febrero 2002
Descargar