ENTRADA/SALIDA.

Anuncio
ARQUITECTURA DE COMPUTADORES
(2º ING. INFORMATICA)
PRACTICA 5 (CURSO 2004/05)
ENTRADA/SALIDA.
OBJETIVOS:
En esta práctica, se pretenden ilustrar las diferencias entre las distintas técnicas para las operaciones de
entrada/salida, así como programar algunos de los dispositivos E/S básicos. Para simplificar, usaremos
como dispositivos algunos de los que se encuentran en cualquier PC convencional, en particular teclado,
altavoz, etc. La programación se realizará en lenguaje C. Aunque puede usarse ensamblador, resulta
mucho más engorroso y eso dificulta centrarnos en los conceptos básicos. En cuanto al sistema operativo
usado, es necesario arrancar la máquina en modo MS-DOS para permitir a los programas de usuario
acceder tan a bajo nivel al sistema. Tenga en cuenta que el acceso a entrada/salida en Windows o Unix
debe hacerse a base de llamadas al sistema operativo y no directamente, como pretendemos nosotros.
DESCRIPCION:
Como dispositivo de entrada utilizaremos el teclado del PC, mientras que el dispositivo de salida será el
altavoz. En efecto, todos los PCs disponen de un pequeño altavoz muy simple (el que suena al arrancar, o
cuando se llena el buffer de teclado) conectado como muestra la figura. El propósito de la práctica es que
suene un pitido en el altavoz cada vez que pulsemos una tecla del teclado.
PortB (61h)
Amplificador +
filtro de paso
baja.
8254
Contador 0
Contador 1
Bit 1
clk
Contador 2
Bit 0
Se utilizan para controlar el altavoz los dos bits menos significativos del puerto 97 (61h) de E/S (en los
PC/XT se trataba del puerto B de un PPI 8255). El bit 0 va a la entrada Gate que activa el contador 2 de
un 8253/4, un temporizador que genera una señal periódica de una frecuencia concreta, programable (ver
apéndice al final de este boletín). El bit 1 simplemente conecta la señal periódica de salida del
temporizador (Out) al altavoz, permitiendo que suene. En las dos primeras partes de la práctica, no
alteraremos el estado del temporizador, es decir, mantendremos siempre una frecuencia constante. Por
tanto, cuando queramos que suene un pitido en el altavoz, simplemente activaremos durante un tiempo los
bits 1 y 0 del puerto 61h.
PREPARACIÓN.
Lea atentamente el resto de la práctica y traiga preparadas las siguientes tareas, antes de venir a la sesión
de prácticas:
•
•
Repase en sus apuntes de teoría los siguientes conceptos:
o E/S programada.
o E/S por interrupciones.
o Vector de interrupciones.
o PPI 8255
o Timer 8254 (mire en el apéndice de este boletín).
Complete el código de la parte 2, para que incluya el código necesario para producir un sonido por el
altavoz, mediante el manejador de interrupciones.
•
•
Calcule el valor de inicialización del contador 2 en la parte 3, para cada una de las frecuencias que se
enumera en la tabla. Convierta a hexadecimal dichos valores.
Responda a las siguientes cuestiones:
o ¿Porqué se guardan el registro de estado y el contador de programa cuando se entra en
una rutina de servicio de una interrupción?
o ¿Porqué no se puede llamar a funciones del MS-DOS cuando se está dentro de una
rutina de interrupción?
o ¿Qué logramos al escribir un dato en la dirección 61h de E/S? ¿En qué dispositivo
estamos escribiendo?
o ¿Para qué sirve el modo 3 del 8254? Explique brevemente.
PARTE 1.
En esta primera parte, se utilizará para las operaciones de E/S el método de E/S programada o de encuesta
(polling). Se trata del método más simple, que consiste en que la CPU controla en todo momento la
operación E/S: testeo de bits de status, activación de los bits de control, transferencia de datos, etc. Así,
nuestro programa testeará continuamente el teclado, y en cuanto se pulse cualquier tecla sonará el pitido.
En principio, la forma de leer caracteres de teclado en C bajo DOS es a través de funciones como kbhit()
–que detecta si alguna tecla ha sido pulsada- o getch() –que devuelve un char con el código ASCII de la
tecla pulsada. Así, un posible código sería:
#include <dos.h>
#include <stdio.h>
int main(int argc, char* argv[]){
long int i;
char caracter='x';
char a;
while(caracter!=' '){
while (!kbhit()); // espera típica de E/S programada. CPU ociosa
caracter=getch(); // permite abortar con ctrl-c
a=inportb(97);
// lee la direccion e/s 97 (61h)
a=a|0x03;
// activa los bits 0 y 1
outportb(97,a);
for (i=0;i<1000000;i++); // espera para hacer el pitido audible
a=inportb(97);
a=a&0xFC;
// desactiva los bits 0 y 1
outportb(97,a);
// escribe en la direccion e/s 97
}
return(0);
}
PARTE 2.
En esta segunda parte se usará el método de E/S por interrupciones. Aquí la CPU también lleva el control,
pero queda liberada mientras el dispositivo externo (en este caso el teclado) tarda en responder. En
realidad en este tipo de sistemas el teclado siempre se trata de esta forma, por interrupciones. En concreto,
en los PCs cada pulsación de una tecla hace que se active la interrupción número 9 de la BIOS. Entonces
se produce la respuesta automática ante interrupciones, es decir, salvar el estado en pila, y tomar la
dirección de la rutina de servicio (handler) de la tabla de vectores, para posteriormente saltar a dicha
rutina. El propósito de esta segunda parte de la práctica es cambiar la rutina original del sistema y
sustituirla por una escrita por nosotros. Más correctamente, nuestra rutina no sustituye totalmente a la
original. En realidad, sólo activará el altavoz para que suene el pitido, y finalmente saltará a la rutina
original para que se procese correctamente la tecla pulsada, y así el sistema pueda seguir funcionando
normalmente. A este proceso se denomina Interceptar Interrupciones. Un posible código sería:
#include <dos.h>
#define INTR 0x09
void interrupt (* oldhandler) ();
void interrupt handler();
main(){ // rutina principal original.
// esta rutina instala el manejador de interrupciones, y termina.
// capturo el vector de interrupciones original.
oldhandler=getvect(INTR);
// altero el vector para que llame a mi funcion manejador.
setvect(INTR,handler);
return(0);
}
void interrupt handler(){
// aquí vendría la activación del altavoz durante un tiempo.
// ATENCIÓN: no llamar a funciones de MSDOS dentro del cuerpo de un
// manejador de interrupciones, ya que dichas llamadas a funciones
// son llamadas a interrupciones síncronas.
// ejecución del manejador de interrupciones original.
oldhandler();
}
Nótese que la rutina principal del programa anterior tiene como propósito fundamental instalar el
manejador de interrupciones. Una vez hecho esto, el programa termina y es eliminado de la memoria,
mientras que el manejador de interrupciones sigue funcionando cada vez que se produce la interrupción
de teclado, por lo tanto, la instalación del manejador sólo hay que hacerla una vez, y durará hasta que otro
programa cambie el vector de interrupción asociado a esa interrupción.
Este sistema tiene una pega tal y como está implementado. ¿Qué pasa con el código del manejador de
interrupciones una vez que el programa que lo lanzó ha terminado? Al terminar el programa principal, su
código será borrado de la memoria una vez que sistema operativo estime oportuno tomar ese espacio para
la ejecución de otro programa, y el código de nuestro programa principal incluye el código del manejador
de interrupciones. En consecuencia, una vez que el programa principal termine, nuestro manejador
funcionará hasta que venga el recogedor de basura del sistema operativo y elimine el código. Si después
de ese instante salta la interrupción de teclado, el vector de interrupciones dirigirá a la CPU a un código
que ya no existe, y el procesador se parará como consecuencia de una excepción.
El único remedio es garantizar la existencia del programa principal mientras funcione el manejador de
interrupciones. En un sistema operativo no hay problema, puesto que el núcleo del sistema operativo, que
es el que inicia los manejadores de interrupción sigue existiendo hasta que la máquina se apaga. En
nuestro programa, tendremos que mantener “artificialmente” la existencia de la rutina principal hasta
que restauremos el vector de interrupciones. Para ello, ejecutaremos un bucle de espera activa que se
ejecutará hasta que se decida terminar con la ejecución del manejador de interrupciones (por ejemplo,
podemos indicar esto pulsando la barra espaciadora). Por lo tanto, el código de la rutina principal podrá
ser el siguiente:
main(){ // rutina principal con “mantenimiento artificial”
char tecla;
int seguir=1;
// capturo el vector de interrupciones original.
oldhandler=getvect(INTR);
// altero el vector para que llame a mi funcion manejador.
setvect(INTR,handler);
// Mantenimiento “artificial”:
// en esta espera la cpu puede hacer cosas, como comprobaciones, y
// recoger teclas, o lo que se quiera.
while(seguir) {
if (kbhit()) tecla=getch();
if (tecla==’ ‘) seguir=0;
}
// restauro el vector de interrupciones para que ejecute
// el viejo manejador de teclado.
setvect(INTR,oldhandler);
return(0);
}
Otra solución para “garantizar la supervivencia” de nuestro manejador de interrupciones es hacer que el
sistema operativo tenga en cuenta el área de memoria ocupada por el manejador y sepa que está ocupada
por un programa que no se debe de borrar, es decir, hacer el programa “residente en memoria”. Para
ello, algunos sistemas operativos (como MS-DOS en nuestro caso) incluyen los mecanismos necesarios a
través de funciones del sistema operativo (interrupciones 27h y 21h) que definen el área de memoria del
programa que se ha de quedar residente después de la terminación del mismo (que en nuestro caso será el
área de código y datos del manejador de interrupciones).
Para ello, el programador utilizará una de las dos funciones anteriormente citadas para terminar su
programa, en vez de la función de terminación normal (interrupción 20h) que eliminaría todo el código y
datos de nuestro programa, una vez que este halla terminado.
Por último, en caso de hacer el programa residente en memoria, no haría falta mantener “artificialmente”
nuestra rutina principal. Simplemente terminaríamos el programa llamando a la interrupción 27h, con lo
que nuestro manejador quedaría instalado (valdría el código de la rutina principal original).
PARTE 3.
Finalmente, si el pitido le sabe a poco, podría programarse el temporizador para variar la frecuencia
(tono), y así hacer que suene una nota distinta, una melodía o algo parecido. Por ejemplo, una de las
octavas sería (en herzios):
NOTA
Hz
DO
262
RE
294
MI
330
FA
349
SOL LA
392 440
SI
494
La dirección de E/S del registro de control del temporizador es la 43h, y la del contador 2 es la 42h. Debe
configurar este contador 2 para que funcione en modo 3 (genera una onda cuadrada en la señal out), e
inicializarlo a un valor tal que la frecuencia de la onda de salida sea una de las de la tabla anterior. Tenga
en cuenta que la frecuencia de la señal clk es 1.19318MHz. Por ejemplo, para una nota LA sería
1193180/440=2712=0A98h, que habría que escribir en dos partes: primero 98h y después 0Ah.
Una ampliación de esto consistiría en hacer que el sonido dependiera de la tecla pulsada. Para ello, podría
accederse directamente al controlador de teclado a través de los puertos 60h y 64h. Una lectura del puerto
64h lee el registro de status, del que sólo nos interesa el bit 0 que indica si está a 1 que el buffer de teclado
está lleno. En ese caso, el valor de la tecla puede leerse directamente del puerto 60h, obteniendo el scan
code que identifica la tecla pulsada (ver figura). Téngase en cuenta que el valor de la dirección 60h es
consultada por el manejador de teclado, por lo que la consulta de esta dirección una vez que se halla
producido la interrupción nos dará siempre la notificación de buffer de teclado vacio, si este se ha
ejecutado antes. No obstante, se puede saber si se ha pulsado una tecla mediante la consulta de la
dirección 64h: si el bit 7 de dicha dirección está a uno, querrá decir que no se ha pulsado una tecla, y por
lo tanto el scancode no será válido.
APENDICE: Contador ó temporizador programable (Timer)
En numerosas ocasiones, necesitamos un dispositivo capaz de generar una señal de reloj con un periodo
concreto, que nos sirva de referencia o base para otros sistemas, como por ejemplo para generación de
ondas periódicas (por ejemplo, para generar sonidos), contador de sucesos, retardos, generación de
interrupciones periódicas, envío de mensajes por vía serie síncrona, etc. Un ejemplo de estos dispositivos
es el timer i8254.
El i8254 es un dispositivo destinado a la familia 8086, con las
siguientes características:
- Tiene un bus de datos de 8 bits
8
- Tiene cuatro posiciones internas: Un registro de control y tres
CONTROL
A la
contadores independientes entre sí. Se elige la posición mediante las
CPU
Clk señales de dirección A1,A0.
___
Contador 0
Gate - Siempre la cuenta es hacia atrás.
WR
Out - Cada contador lleva tres señales de control:
RD
Clk (de entrada): Señal de reloj externa, que no tiene por qué
Contador 1
ser el de la CPU.
Gate(de entrada): Permite o no la cuenta.
A1
Out (de salida): Se activa al llegar al final de la cuenta.
Contador 2
A0
El funcionamiento es a grandes rasgos como sigue, aunque varía según el modo de funcionamiento. Los
contadores funcionan de forma independiente. Primero se configura el modo de funcionamiento, y a
continuación se inicializan los contadores. Nada más activarse la señal Gate, comienza la cuenta atrás de
los contadores al ritmo que indique la señal Clk (un avance por cada flanco de bajada). Cuando se llegue
al final de la cuenta, se activa la señal Out. El valor de los contadores puede leerse, pero el proceso es un
poco complicado y no será necesario realizarlo en esta práctica.
El tamaño de los contadores es de 16 bits, por lo que, al ser el bus de datos de 8 bits, para cargar un nuevo
valor en ellos hay que hacerlo en dos fases, o bien contentarnos con escribir sólo una de las dos mitades
(LSB ó MSB). El significado de los bits del registro de control, que es de 8 bits, es el siguiente:
Bit
7, 6
5, 4
3, 2, 1 0
un posterior acceso a Elige el 0 → binario
dicho cont. escribirá:
Modo: 1 → BCD
01: sólo el LSB
0-5
10: sólo el MSB
11: 1º el LSB y 2º el
MSB, en dos accesos
consecutivos.
A continuación describimos brevemente los distintos modos de funcionamiento:
Modo 0: Interrupción al final de cuenta.
Se utiliza típicamente para bucles de espera, sin el inconveniente de depender de la velocidad del
procesador. La cuenta comienza a la primera bajada del reloj que sigue a la inicializalización del
contador. Al final de la cuenta Out pasa de cero a uno.
Modo 1: “one-shot” redisparable por hardware.
Un "one-shot" es un dispositivo que al recibir un flanco de subida produce un pulso a la salida, cuya
duración depende de los valores de una resistencia y un condensador. Se llama redisparable si al recibir
un flanco de entrada durante el tiempo en que se está produciendo el pulso de salida, éste se vuelve a
iniciar. El Modo1 simula este dispositivo. Gate será el disparador y Out la salida. La duración del pulso
será la introducida en el contador.
Modo 2: Generador de frecuencias.
Cuando transcurren los ciclos indicados, se produce un pulso de Out, de un ciclo de duración y vuelve a
empezar el proceso. Así se obtiene una onda periódica mientras Gate se mantenga activa.
Modo 3: Generador de onda cuadrada.
Si el número cargado en el contador es N, se generará una onda cuadrada de N/2 ciclos arriba y N/2
ciclos abajo. Gate inicia la onda.
Modo 4: Strobe disparable por Software.
Al cargarse el contador, comienza la cuenta y al final se produce un pulso en Out de un ciclo.
Modo 5: Strobe redisparable por Hardware.
Como el modo anterior pero controla y se repite el proceso a cada flanco de subida de Gate.
Función
Indica el contador
al que se refiere el
resto de bits:
00 Cont. 0
01 Cont. 1
10 Cont. 2
ARQUITECTURA DE COMPUTADORES.
3º ING. INFORMÁTICA.
PRÁCTICA 4.
NUM GRUPO:
ALUMNOS:
Responda brevemente a las siguientes preguntas:
a) ¿Por qué las rutinas de servicio necesitan en C el adjetivo interrupt?
b) Si hubiéramos escrito la rutina de servicio en lenguaje ensamblador, la llamada final a la rutina
original hubiera tenido que hacerse de la siguiente forma:
pushf; salva en pila el registro de banderas.
call oldhandler;
iret;
Explique brevemente qué significa cada una de esas instrucciones y porqué no puede hacerse con
un simple call. ¿Puede pensar en una alternativa aún más fácil que la solución anterior?.
c) Para dejar el sistema intacto después de nuestra prueba, podemos usar un contador y cuando se
realice un cierto número de pulsaciones volver a grabar en la tabla de vectores la dirección original.
¿Cómo deberá definirse la variable contador? Por otro lado, la int 9 detecta tanto la pulsación
como la liberación de la tecla. Use la variable contador para que sólo suene el pitido una vez cada
pulsación.
d) Escriba la secuencia de código necesaria para generar un sonido de frecuencia 2093 herzios al
pulsar la tecla ESCAPE.
Descargar