Tema 7: Acceso Directo a Memoria 7.1 El concepto Qué es una transferencia por acceso directo a memoria? El modelo de transferencia de información visto en los capítulos anteriores se denomina transferencia por programa, porque la transferencia de un dato del controlador a memoria, o viceversa, se realiza como consecuencia de la ejecución de instrucciones de un programa (ver figura 1). IN AL, Rdat AL dato: Rdat procesador memoria controlador dato, AL Figura 1: Transferencia por programa, en el caso de una entrada de datos Una alternativa a la transferencia por programa es la transferencia por acceso directo a memoria (DMA) en la que el propio controlador se responsabiliza de leer o escribir los datos en memoria. Aunque la transferencia sea por DMA, seguirá siendo necesaria la sincronización entre el procesador y el controlador, para que el primero sepa cuándo ha terminado la transferencia. Esta sincronización puede realizarse por encuesta o por interrupción. Qué registros debe tener un controlador que realiza transferencia por DMA? Para que un controlador pueda realizar una transferencia por DMA (por ejemplo, enviar un dato a memoria), necesita conocer la dirección de memoria involucrada en la transferencia (es decir, la dirección donde debe escribir el dato o de donde debe leerlo). Por tanto, el controlador deberá tener un registro de dirección (Radr), en el que el procesador pueda escribir la dirección involucrada en la transferencia (ver figura 2). Como ahora el procesador no tendrá que leer ni escribir el dato directamente del controlador (que es lo que ocurría en la transferencia por programa), no será necesario que el registro de datos del controlador (Rdat en la figura 2) 1
sea visible desde el procesador (es decir, el controlador no tendrá registro de datos mapeado en el espacio de memoria ni en el espacio de entrada/salida). El hecho de que la transferencia sea por DMA no tiene ninguna implicación adicional sobre los otros registros del controlador. LEA EAX, dato OUT Radr, EAX dato: Radr Rdat procesador memoria controlador Figura 2: Transferencia por DMA, en el caso de una entrada de datos Cuándo es adecuada la transferencia por DMA? La transferencia por DMA no tiene ventajas sobre la transferencia por programa cuando se trata de transferir un dato aislado. Ese sería el caso de lecturas del teclado, o de escrituras en la impresora. Comparar, por ejemplo, las figuras 1 y 2 suponiendo que representan la lectura de un carácter del teclado. Se ve que no hay demasiadas diferencias en cuanto a trabajo que debe hacer el procesador o tiempo que tarda la transferencia. La transferencia por DMA es adecuada cuando debe transferirse un bloque de datos (es decir, una serie de bytes consecutivos de memoria). En el caso de la transferencia de un bloque de datos por DMA, el procesador debe escribir en el registro Radr la dirección de memoria involucrada en la transferencia. Cuando el controlador esté en disposición de transferir el siguiente byte del bloque usará la dirección que hay en Radr para leer o escribir la memoria (según el sentido de la transferencia) e incrementará el valor de la dirección dejándola preparada para la transferencia del siguiente byte. La operación de entrada/salida acabará cuando hayan sido transferidos todos los datos del bloque (ver figura 3). Las ventajas de la transferencia de un bloque de datos por DMA frente a la transferencia por programa son: La transferencia se hace mucho más rápido (no se pierden ciclos para ejecutar instrucciones que transfieran datos) 2
El procesador puede hacer otras cosas mientras el controlador está realizando la transferencia (siempre que la sincronización sea por interrupción) El ejemplo típico de controlador que realizar transferencias de bloque de datos (y no de datos aislados) es el controlador de disco, ya que en un disco la información se lee y se escribe por bloques. Veremos a continuación cómo funciona un controlador de disco que realiza transferencia por DMA. LEA EAX, bloque OUT Radr, EAX bloque: Radr Rdat procesador memoria controlador Figura 3: Transferencia por DMA de un bloque de datos 7.2 El controlador de disco El disco es un dispositivo en el que se leen o escriben bloques de LONG_BLOC bytes y que realiza la transferencia por DMA. Cada bloque se almacena en un sector del disco. Los sectores tienen un tamaño de LONG_BLOC bytes y están organizados en pistas que, a su vez, están agrupadas en caras. Para ordenar una lectura, el procesador debe indicar al controlador la cara, pista, sector en la que está el bloque de datos que hay que leer y la dirección de memoria a partir de la cual hay que dejar los LONG_BLOC bytes del bloque. En el caso de la escritura de un bloque, la dirección de memoria será aquella a partir de la cual están los LONG_BLOC bytes que hay que escribir. Los registros del controlador de disco son los siguientes: Radr_disc (32 bits): El procesador deberá escribir aquí la dirección física de memoria involucrada en la transferencia. Rsect_disc (8 bits): El procesador deberá escribir aquí el número de sector. Rpist_disc (8 bits): El procesador deberá escribir aquí el número de pista. Rcara_disc (8 bits): El procesador deberá escribir aquí el número de cara. Rest_disc (8 bits): Del registro de estado sólo importan los dos bits bajos: Bit 0: Se pone a 1 si el disco está preparado para aceptar una nueva operación y 0 en caso contrario. Bit 1: Se pone a 1 si la última transferencia a acabado con algún error (habrá que repetirla). 3
Rcon_disc (8 bits): Del registro de control sólo importan los tres bits bajos: Bit 0: El procesador debe escribir un 1 aquí si quiere que el controlador interrumpa al acabar una operación. Bit 1: El procesador debe escribir un 1 aquí cuando quiera que se inicie la operación. Bit 2: El procesador debe escribir un 0 si la operación es la lectura de un sector, y un 1 si la operación es una escritura. Otros datos importantes del controlador son: El controlador de disco genera una petición de interrupción cuando acaba la operación solicitada (lectura o escritura de un bloque), siempre y cuando el bit 0 de Rcon_disc esté a 1. La petición de interrupción baja automáticamente cuando el procesador decide atenderla. El identificador de interrupción es 13. Al acabar una operación (lectura o escritura de un sector) los registros Radr_disc, Rsect_disc, Rpist_disc y Rcara_disc quedan con un valor indefinido. 7.3 Ejemplo de programación En este ejemplo de programación supondremos que el sector S de la pista P de la cara C del disco contiene un vector de LONG_BLOC números enteros de un byte de tamaño cada uno. Debe leerse el sector sobre el vector de bytes llamado buffer. Una vez leído el sector, debe calcularse la suma de los elementos y dejar el resultado en la variable res. Para simplificar, supondremos que nunca se producirá un error en la lectura del sector. Veamos primero la solución en la que el procesador se sincroniza con el disco por encuesta. LONG_BLOC C ; tamaño del sector en bytes ; Cara en la que está el sector P ; Pista en la que está el sector S ; Sector a leer.data buffer db LONG_BLOC DUP (?) ; vector en el que hay que ; dejar los datos del sector res db? ; aquí debe quedar el resultado.code.startup LEA EAX,buffer ; cálculo de la dirección física BX,DS ; de buffer ZX SAL EBX,BX EBX,4 ADD EAX,EBX OUT Radr_disc, EAX ; escribimos la dirección física en ; el controlador de disco AL,S ; escribimos el número de sector 4
OUT Rsect_disc, AL AL,P ; escribimos el número de pista OUT Rpist_disc, AL AL,C ; escribimos el número de cara OUT Rcara_disc, AL IN AL, Rcon_disc ; programamos una lectura sin AND OR AL, 11111000b AL,00000010b ; interrupción OUT Rcon_disc, AL bucle: IN AL,Rest_disc ; bucle de encuesta TEST AL,00000001b JZ bucle AL,0 ; ya terminó la lectura ESI,0 ; los datos están en buffer sumar: CMP ESI,LONG_BLOC ; recorrido del vector leido JE ADD fin AL,buffer[ESI] INC ESI JMP sumar fin: res,al ; dejamos el resultado.exit end Veamos ahora la solución en la que el procesador se sincroniza con el disco por interrupción. Esta solución no aporta gran cosa respecto a la anterior porque el programa principal, en vez de esperar en un bucle consultando el registro de estado (como ocurre en la solución por encuesta), esperará a que una variable cambie de valor (la variable fin). Ese cambio de valor lo hará la rutina de atención a las interrupciones del disco, en el instante en que acabe la lectura. Obsérvese que en este caso no podemos beneficiarnos de la sincronización por interrupción porque el programa principal no puede hacer ningún trabajo mientras se realiza la operación de lectura del sector. Lo importante de este ejemplo es comparar la versión en ensamblador con la versión en alto nivel. Para ello, el código se presenta a dos columnas. En la columna izquierda aparece el código en ensamblador y en la columna derecha el código correspondiente en alto nivel. En general, los ejercicios de programación de operaciones de entrada/salida deberán escribirse en lenguaje de alto nivel. Declaramos en primer lugar las constantes y variables del programa. En la variable disc_ant salvaremos el contenido del vector de interrupciones, y utilizaremos la variable fin para tomar nota de que ha acabado la lectura de un sector. 5
LONG_BLOC P S.data C buffer db LONG_BLOC DUP (?) res db? disc_ant dd? fin db 0 LONG_BLOC = ; C = ; S = ; P = ; byte buffer [LONG_BLOC]; byte res; int disc_ant; boolean fin; byte a; Ahora viene la rutina de atención a las interrupciones de disco. Simplemente toma nota, en la variable fin, de que ha terminado la lectura de un sector, y avisa al controlador de interrupciones. rut_disc PROC PUSH EAX fin, FFh AL,20H OUT Rcon_int, AL POP EAX IRET rut_disc ENDP void interrupt rut_disc () { fin = TRUE; eoi (); } Ahora se inicia el programa principal. Lo primero que hace es programar la entrada 13 del vector de interrupciones..startup LEA CLI STI AX,0 ES,AX EBX, ES:[13*4] disc_ant,ebx AX,rut_disc ES:[13*4],AX ES:2[13*4],CS void main { fin = FALSE; isc_ant = modif_vi(13,@rut_disc); Ahora programamos el controlador de disco. Debemos escribir en los registros del controlador: la dirección física de buffer, el número de sector, pista y cara, y el tipo de operación (lectura con interrupción). LEA ZX SAL ADD OUT OUT OUT OUT IN AND OR OUT EAX,buffer BX,DS EBX,BX EBX,4 EAX,EBX Radr_disc, EAX AL,S Rsect_disc, AL AL,P Rpist_disc, AL AL,C Rcara_disc, AL AL, Rcon_disc AL, 11111000b AL,00000011b Rcon_disc, AL out_d (Radr_disc, @ F buffer); out_b (Rsect_disc,S); out_b (Rpist_disc,P); out_b (Rcara_disc,C); a = in_b (Rcon_disc); a = (a & 11111000b); a = (a 00000011b); out_b (Rcon_disc,a); Esperamos a que acabe la lectura del sector. bucle: CMP JE fin,0 bucle while (! fin) {}; 6
Recorremos el vector para sumar sus elementos. AL,0 ESI,0 sumar: CMP ESI,LONG_BLOC JE ADD fin AL,buffer[ESI] INC ESI fin: JMP sumar res,al res = 0 for (int i=0; i<long_bloc; i++) res = res + buffer [i]; Finalmente, restauramos el vector de interrupciones..exit end EAX,disc_ant ES:[13*4],EAX disc_ant = modif_vi (13, disc_ant); } 7
7.4 Ejercicios Supondremos ahora que debe resolverse el mismo problema que el considerado en el apartado 7.3, pero ahora el vector cuyos elementos hay que sumar ocupa N sectores consecutivos en el disco (los sectores del 0 al N-1 de la pista P de la cara C). Supondremos también que nunca se producen errores en la lectura de un sector. Hay que hacer tres versiones del programa. Versión A Se dispone de un buffer de tamaño LONG_BLOC*N bytes. El programa principal realiza la mayor parte del trabajo. La rutina de atención a las interrupciones sólo toma nota de que se ha producido una interrupción del disco (es decir, cuando ha finalizado la última operación de lectura del disco). Hay que completar el programa principal para que todo funcione correctamente. Pueden añadirse las variables que sean necesarias. LONG_BLOC = ; // declaramos en primer lugar las C = ; // constantes y variables del P = ; // programa N = ; byte buffer [LONG_BLOC*N]; byte res; int disc_ant; boolean fin; Escribir aquí las variables que faltan void interrupt rut_disc () { // Esta es la rutina de fin = TRUE; //atención a las eoi (); //interrupciones } // A continuación tenemos el programa principal void main { fin = FALSE; disc_ant = modif_vi (13,@rut_disc); // programamos el // vector de // interrupciones Escribir aquí la parte que falta del programa principal 8
// hacemos la suma de los elementos del vector res = 0 for (int i=0; i<long_bloc*n; i++) res = res + buffer [i]; disc_ant = modif_vi (13 disc_ant); // restauramos el } //vector de interrupciones Versión B Se dispone de un buffer de tamaño LONG_BLOC*N bytes. La rutina de atención a las interrupciones hace la mayor parte del trabajo. El programa principal simplemente espera a que se haya leído todo el vector. Hay que completar la rutina de atención a las interrupciones para que todo funcione correctamente. Pueden añadirse las variables que sean necesarias. LONG_BLOC = ; // declaramos en primer lugar las C = ; // constantes y variables del P = ; // programa N = ; byte buffer [LONG_BLOC*N]; byte res; int disc_ant; boolean fin; byte a; byte sector; Escribir aquí las variables que faltan void interrupt rut_disc () { Escribir aquí la parte que falta de la rutina 9
} eoi (); Versión C // A continuación tenemos el programa principal void main { fin = FALSE; sector = 0; disc_ant = modif_vi (13,@rut_disc); // programamos el // vector de // interrupciones // programamos el controlador de disco out_d (Radr_disc, @ F buffer); // dirección física out_b (Rsect_disc,sector); out_b (Rpist_disc,P); out_b (Rcara_disc,C); a = in_b (Rcon_disc); a = (a & 11111000b); a = (a 00000011b); out_b (Rcon_disc,a); // esperamos a que acabe la lectura del disco while (! fin) {}; // hacemos la suma de los elementos del vector res = 0 for (int i=0; i<long_bloc*n; i++) res = res + buffer [i]; disc_ant = modif_vi (13 disc_ant); // restauramos el } //vector de interrupciones Se dispone de un buffer de tamaño LONG_BLOC bytes (sólo cabe un bloque). Escribir el programa completo (programa principal y rutina de atención a las interrupciones. 10