Capítulo VII: ARQUITECTURA DEL PC, AT Y PS/2 BAJO DOS






7.1. - LAS INTERRUPCIONES

     Son señales enviadas a la CPU para que termine la ejecución de la instrucción en curso y atienda una petición determinada, continuando más tarde con lo que estaba haciendo.

     Cada interrupción lleva asociado un número que identifica el tipo de servicio a realizar. A partir de dicho número se calcula la dirección de la rutina que lo atiende y cuando se retorna se continúa con la instrucción siguiente a la que se estaba ejecutando cuando se produjo la interrupción. La forma de calcular la dirección de la rutina es multiplicar por cuatro el valor de la interrupción para obtener un desplazamiento y, sobre el segmento 0, con dicho desplazamiento, se leen dos palabras: la primera es el desplazamiento y la segunda el segmento de la rutina deseada. Por tanto, en el primer kilobyte de memoria física del sistema, existe espacio suficiente para los 256 vectores de interrupción disponibles.

     Hay tres tipos básicos de interrupciones:

     Los vectores de interrupción pueden ser desviados hacia un programa propio que, además, podría quedar residente en memoria. Si se reprograma por completo una interrupción y ésta es de tipo hardware, hay que realizar una serie de tareas adicionales, como enviar una señal fin de interrupción hardware al chip controlador de interrupciones. Si se trata además de la interrupción del teclado del PC o XT, hay que enviar una señal de reconocimiento al mismo ... en resumen: conviene documentarse debidamente antes de intentar hacer nada. Todos estos problemas se evitan si la nueva rutina que controla la interrupción llama al principio (o al final) al anterior gestor de la misma, que es lo más normal, como se verá más adelante.

     Para cambiar un vector de interrupción existen cuatro métodos:

  1. «El elegante»: es además el más cómodo y compatible. De hecho, algunos programas de DOS funcionan también bajo OS/2 si han sido diseñados con esta técnica. Basta con llamar al servicio 25h del DOS (INT 21h) y decirle qué interrupción hay que desviar y a dónde:

                    MOV     AH,25h           ; servicio para cambiar vector
                    MOV     AL,vector        ; entre 0 y 255
                    LEA     DX,rutina        ; DS:DX nueva rutina de gestión
                    INT     21h              ; llamar al DOS
    
  2. El «psé»: es menos seguro y compatible (ningún programa que emplea esta técnica corre en OS/2) y consiste en hacer casi lo que hace el DOS pero sin llamarle. Es además mucho más incómodo y largo, pero muy usado por programadores despistados:

                    MOV     BL,vector*4      ; vector a cambiar en BL
                    MOV     BH,0             ; ahora en BX
                    MOV     AX,0
                    PUSH    DS               ; preservar DS
                    MOV     DS,AX            ; apuntar al segmento 0000
                    LEA     DX,rutina        ; CS:DX nueva rutina de gestión
                    CLI                      ; evitar posible interrupción
                    MOV     [BX],DX          ; cambiar vector (offset)
                    MOV     [BX+2],CS        ; cambiar vector (segmento)
                    STI                      ; permitir interrupciones
                    POP     DS               ; restaurar DS
    
  3. El «método correcto» es similar al «psé», consiste en cambiar el vector «de un tirón» (cambiar a la vez segmento y offset con un REP MOVS) con objeto de evitar una posible interrupción no enmascarable que se pueda producir en ese momento crítico en que ya se ha cambiado el offset pero todavía no el segmento (CLI no inhibe la interrupción no enmascarable). Este sistema es todavía algo más engorroso, pero es el mejor y es el que utiliza el DOS en el método (1).

  4. El «método incorrecto» es muy usado por los malos programadores. Es similar al «psé» sólo que sin inhibir las interrupciones mientras se cambia el vector, con el riesgo de que se produzca una interrupción cuando se ha cambiado sólo medio vector. Los peores programadores lo emplean sobre todo para cambiar INT 8 ó INT 1Ch, que se producen con una cadencia de 18,2 veces por segundo.


7.2. - LA MEMORIA. LOS PUERTOS DE ENTRADA Y SALIDA.

     Dentro del megabyte que puede direccionar un 8086, los primeros 1024 bytes están ocupados por la tabla de vectores de interrupción. A continuación existen 256 bytes de datos de la BIOS y otros tantos para el BASIC y el DOS. De 600h a 9FFFFh está la memoria del usuario (casi 640 Kb). En A0000h comienza el área de expansión de memoria de pantalla (EGA y VGA). En B0000h comienzan otros 64 Kb de los adaptadores de texto MDA y gráficos (CGA). De C0000h a EFFFFh aparecen las extensiones de la ROM (añadidas por las tarjetas gráficas, discos duros, etc.) y en F0000h suele estar colocada la BIOS del sistema (a veces tan sólo 8 Kb a partir de FE000h). Los modernos sistemas operativos (DR-DOS y MS-DOS 5.0 y posteriores) permiten colocar RAM en huecos «vacíos» por encima de los 640 Kb en las máquinas 386 (y algún 286 con cierto juego especial de chips). Esta zona de memoria sirve para cargar programas residentes. De hecho, el propio sistema operativo se sitúa (en 286 y superiores) en los primeros 64 Kb de la memoria extendida (HMA) que pueden ser direccionados desde el DOS, dejando más memoria libre al usuario dentro de los primeros 640 Kb. Para más información, puede consultarse el apéndice I y el capítulo 8.

     Los puertos de entrada y salida (E/S) permiten a la CPU comunicarse con los periféricos. Los 80x86 utilizan los buses de direcciones y datos ordinarios para acceder a los periféricos, pero habilitando una línea que distinga el acceso a los mismos de un acceso convencional a la memoria (si no existieran los puertos de entrada y salida, los periféricos deberían interceptar el acceso a la memoria y estar colocados en algún área de la misma). Para acceder a los puertos E/S se emplean las instrucciones IN y OUT. Véase el apéndice IV.


7.3.- LA PANTALLA EN MODO TEXTO.

     Cuando la pantalla está en modo de texto, si está activo un adaptador de vídeo monocromo, ocupa 4 Kb a partir del segmento 0B000h. Con un adaptador de color, son 16 Kb a partir del segmento 0B800h. Un método para averiguar el tipo de adaptador de vídeo es consultar a la BIOS el modo de vídeo activo: será 7 para un adaptador monocromo (tanto MDA como la EGA y VGA si el usuario las configura así) y un valor entre 0 y 4 para un adaptador de color. Los modos 0 y 1 son de 40 columnas y el 2 y 3 de 80. Los modos 0 y 2 son de «color suprimido», aunque en muchos monitores salen también en color (y no en tonos de gris). Cada carácter en la pantalla (empezando por arriba a la izquierda) ocupa dos bytes consecutivos: en el primero se almacena el código ASCII del carácter a visualizar y en el segundo los atributos de color. Obviamente, en un modo de 80x25 se utilizan 4000 bytes (los 96 restantes hasta los 4096 de los 4 Kb se desprecian). En los adaptadores de color, como hay 16 Kb de memoria para texto, se pueden definir entre 4 páginas de texto (80 columnas) y 8 (40 columnas). La página activa puede consultarse también llamando a la BIOS, con objeto de conocer el segmento real donde empieza la pantalla (B800 más un cierto offset). En el 97,5% de los casos sólo se emplea la página 0, lo que no quiere decir que los buenos programas deban asumirla como la única posible. La BIOS utiliza la interrupción 10h para comunicarse con el sistema operativo y los programas de usuario.

     El byte de atributos permite definir el color de fondo de los caracteres (0-7) con los bits 4-6, el de la tinta (0-15) con los bits 0-3 y el parpadeo con el bit 7. La función de este último bit puede ser redefinida para indicar el brillo de los caracteres de fondo (existiendo entonces también 16 colores de fondo), aunque en CGA es preciso para ello un acceso directo al hardware. En el adaptador monocromo, y para la tinta, el color 0 es el negro; el 1 es «subrayado normal», del 1 al 7 son colores «normales»; el 8 es negro, el 9 es «subrayado brillante» y del 10 al 15 son «brillantes». Para el papel todos los colores son negros menos el 7 (blanco), no obstante para escribir en vídeo inverso es necesario no sólo papel 7 sino además tinta 0 (al menos, en los auténticos adaptadores monocromos). El bit 7 siempre provoca parpadeo en este adaptador. En el adaptador de color no se pueden subrayar caracteres con los códigos de color (aunque sí en la EGA y VGA empleando otros métodos). Tabla de colores:

0 - Negro 4 - Rojo 8 - Gris 12 - Rojo claro
1 - Azul 5 - Magenta 9 - Azul claro 13 - Magenta claro
2 - Verde 6 - Marrón 10 - Verde claro 14 - Amarillo
3 - Cian 7 - Blanco 11 - Cian claro 15 - Blanco brillante

     Conviene tener cuidado con la tinta azul (1 y 9) ya que, en estos colores, los adaptadores monocromos subrayan -lo que puede ser un efecto indeseable-. Cuando se llama al DOS para imprimir, éste invoca a su vez a la BIOS, por lo que la escritura puede ser acelerada llamando directamente a este último, que además permite escribir en color. De todas maneras, lo mejor en programas de calidad es escribir directamente sobre la memoria de pantalla para obtener una velocidad máxima, aunque con ciertas precauciones -para convivir mejor con entornos pseudo-multitarea y CGA's con nieve-.

     Las pantallas de 132 columnas no son estándar y varían de unas tarjetas gráficas a otras, por lo que no las trataremos. Lo que sí se puede hacer -con cualquier EGA y VGA- es llamar a la BIOS para que cargue el juego de caracteres 8x8, lo que provoca un aumento del número de líneas a 43 (EGA) o 50 (VGA), así como un lógico aumento de la memoria de vídeo requerida (que como siempre, empieza en 0B800h).

     En las variables de la BIOS (apéndice III) los bytes 49h-66h están destinados a controlar la pantalla; su consulta puede ser interesante, como demostrará este ejemplo: el siguiente programa comprueba el tipo de pantalla, para determinar su segmento, llamando a la BIOS (véase el apéndice de las funciones del DOS y de la BIOS). Si no es una pantalla de texto estándar no realiza nada; en caso contrario la recorre y convierte todos sus caracteres a mayúsculas, sin alterar el color:

    mays           SEGMENT
                   ASSUME CS:mays, DS:mays
                   ORG    100h             ; programa .COM ordinario
    inicio:
                   MOV    AH,15       ; función para obtener modo de vídeo
                   INT    10h         ; llamar a la BIOS
                   MOV    BX,0B000h   ; segmento de pantalla monocroma
                   MOV    CX,2000     ; tamaño (caracteres) de la pantalla
                   CMP    AL,7        ; ¿es realmente modo monocromo?
                   JE     datos_ok    ; en efecto
                   MOV    BX,0B800h   ; segmento de pantalla de color
                   CMP    AL,3        ; ¿es modo de texto de 80 columnas?
                   JE     pant_color  ; en efecto
                   CMP    AL,2        ; ¿es modo de texto de 80 columnas?
                   JE     pant_color  ; en efecto
                   MOV    CX,1000     ; tamaño (caract.) pantalla 40 col.
                   CMP    AL,1        ; ¿es modo texto de 40 columnas?
                   JBE    pant_color  ; así es
                   MOV    AL,1        ; pantalla gráfica o desconocida:
                   JMP    final       ; fin de programa (errorlevel=1)

    pant_color:    MOV    AX,40h      ; considerar página activa<>0
                   MOV    DS,AX       ; DS = 40h (variables de la BIOS)
                   MOV    AX,DS:[4Eh] ; desplazamiento de la página activa
                   SHR    AX,1        ; desplazamiento / 2
                   SHR    AX,1        ; desplazamiento / 4
                   SHR    AX,1        ; desplazamiento / 8
                   SHR    AX,1        ; desplazamiento / 16 (párrafos)
                   ADD    BX,AX       ; segmento de vídeo efectivo

    datos_ok:      MOV    DS,BX            ; DS = segmento de pantalla
                   XOR    BX,BX            ; BX = 0 (primer carácter)
    otra_letra:    CMP    BYTE PTR [BX],'a'; ¿código ASCII menor que 'a'?
                   JB     no_minuscula     ; luego no puede ser minúscula
                   CMP    BYTE PTR [BX],'z'; ¿código ASCII mayor de 'z'?
                   JA     no_minuscula     ; luego no puede ser minúscula
                   AND    BYTE PTR [BX],0DFh ; poner en mayúsculas
    no_minuscula:  ADD    BX,2             ; apuntar siguiente carácter
                   LOOP   otra_letra       ; repetir con los CX caracteres

                   MOV    AL,0             ; fin programa (errorlevel=0)
    final:         MOV    AH,4Ch
                   INT    21h

    mays           ENDS
                   END   inicio

7.4 - LA PANTALLA EN MODO GRÁFICO.

7.4.1. - MODOS GRÁFICOS.

     Dada la inmensidad de estándares gráficos existentes para los ordenadores compatibles, que sucedieron al primer adaptador que sólo soportaba texto (MDA), y que de hecho llenan varias estanterías en las librerías, sólo se tratará de una manera general el tema. Se considerarán los estándares más comunes, con algunos ejemplos de programación de la pantalla gráfica CGA con la BIOS y programando la VGA directamente para obtener la velocidad y potencia del ensamblador. Las tarjetas gráficas tradicionales administran normalmente entre 16 Kb y 1 Mb de memoria de vídeo, en el segmento 0B800h las CGA/Hércules y en 0A000h las VGA. En los modos de vídeo que precisan más de 64 Kb se recurre a técnicas especiales, tales como planos de bits para los diferentes colores, o bien dividir la pantalla en pequeños fragmentos que se seleccionan en un puerto E/S. Las tarjetas EGA y posteriores vienen acompañadas de una extensión ROM que parchea la BIOS normal del sistema para añadir soporte al nuevo sistema de vídeo. A continuación se listan los principales modos gráficos disponibles en MDA, CGA, EGA y VGA, así como en las SuperVGA Paradise, Trident y Genoa. No se consideran las peculiaridades del PCJr.

     Modo   Texto   Resolución   Colores   Segmento  Tarjeta
     ----   -----   ----------   -------   --------  ---------------------
     04h    40x25     320x200       4        B800     CGA, EGA, MCGA, VGA
     05h    40x25     320x200    4 grises    B800     CGA, EGA
     05h    40x25     320x200       4        B800     CGA, VGA
     06h    80x25     640x200       2        B800     CGA, EGA, MCGA, VGA
     0Dh    40x25     320x200      16        A000     EGA, VGA
     0Eh    80x25     640x200      16        A000     EGA, VGA
     0Fh    80x25     640x350       2        A000     EGA, VGA
     10h    80x25     640x350       4        A000     EGA con 64K
     10h    80x25     640x350      16        A000     EGA con 256K, VGA
     11h    80x30     640x480       2        A000     VGA, MCGA
     12h    80x30     640x480    16/256k     A000     VGA
     13h    40x25     320x200    256/256k    A000     VGA, MCGA

     27h              720x512      16                 Genoa
     29h              800x600      16        A000     Genoa
     2Dh              640x350    256/256k    A000     Genoa
     2Eh              640x480    256/256k    A000     Genoa
     2Fh              720x512     256                 Genoa
     30h              800x600    256/256k    A000     Genoa
     37h             1024x768      16        A000     Genoa
     58h   100x75     800x600    16/256k     A000     Paradise VGA
     59h   100x75     800x600       2        A000     Paradise VGA
     5Bh   100x75     800x600    16/256k     A000     Trident TVGA 8800, 8900
     5Bh              640x350     256                 Genoa 6400
     5Ch    80x25     640x400     256        A000     Trident TVGA 8800
     5Ch              640x480     256                 Genoa 6400
     5Dh    80x30     640x480     256        A000     Trident TVGA 8800 (512K)
     5Eh    80x25     640x400     256                 Paradise VGA
     5Eh              800x600     256                 Trident 8900
     5Eh              800x600     256                 Genoa 6400
     5Fh    80x30     640x480     256                 Paradise VGA (512K)
     5Fh             1024x768    16/256k     A000     Trident TVGA 8800 (512K)
     5Fh             1024x768      16                 Genoa 6400
     61h    96x64    768x1024    16/256k     A000     Trident TVGA 8800 (512K)
     62h             1024x768     256                 Trident TVGA 8900
     6Ah              800x600      16                 Genoa 6400
     7Ch              512x512      16                 Genoa
     7Dh              512x512     256                 Genoa

     Las tarjetas gráficas son muy distintas entre sí a nivel de hardware, por la manera en que gestionan la memoria de vídeo. Las tarjetas SuperVGA complican aún más el panorama. En general, un programa que desee aprovechar al máximo el ordenador deberá apoyarse en drivers o subprogramas específicos, uno para cada tarjeta de vídeo del mercado. Esto es así porque aunque la BIOS del sistema (o el de la tarjeta) soporta una serie de funciones estándar para trabajar con gráficos, existen bastantes problemas. En primer lugar, su ineficiente diseño lo hace extremadamente lento para casi cualquier aplicación seria. Bastaría con que las funciones que implementa la BIOS (pintar y leer puntos de la pantalla) fueran rápidas, ¡sólo eso!, para lo que tan sólo hace falta una rutina específica para cada modo de pantalla, que la BIOS debería habilitar nada más cambiar de modo; casi todas las demás operaciones realizadas sobre la pantalla se apoyan en esas dos y ello no requeriría software adicional para mantener la compatibilidad entre tarjetas. Sin embargo, los programas comerciales no tienen más remedio que incluir sus propias rutinas rápidas para trazar puntos y líneas en drivers apropiados (y de paso añaden alguna función más compleja). Además, y por desgracia, no existe NI UNA SOLA función oficial en la BIOS que informe a los programas que se ejecutan de cosas tan elementales como los modos gráficos disponibles (con sus colores, resolución, etc.); esto no sólo es problemático en las tarjetas gráficas: la anarquía y ausencia de funciones de información también se repite con los discos, el teclado, ... aunque los programadores ya estamos acostumbrados a realizar la labor del detective para averiguar la información que los programas necesitan. Sin embargo, con los gráficos no podemos y nos vemos obligados a preguntar al usuario qué tarjeta tiene, de cuántos colores y resolución, en qué modo... y lo que es peor: la inexistencia de funciones de información se agrava con el hecho de que las VGA de los demás fabricantes hayan asignado de cualquier manera los números de modo. De esta manera, por ejemplo, una tarjeta Paradise en el modo 5Fh tiene de 640x400 puntos con 256 colores, mientras que una Trident tiene, en ese mismo modo, 1024x768 con 16 colores. En lo único que coinciden todas las tarjetas es en los primeros modos de pantalla, definidos inicialmente por IBM. Muchas SuperVGA tienen funciones que informan de sus modos, colores y resoluciones, lo que sucede es que en esto no se han podido poner de acuerdo los fabricantes y la función de la BIOS de la VGA a la que hay que invocar para obtener información, ¡difiere de unas tarjetas a otras!. Afortunadamente, existe un estándar industrial en tarjetas SuperVGA, el estándar VESA, que aunque ha llegado demasiado tarde, múltiples VGA lo soportan y a las que no, se les puede añadir soporte con un pequeño driver residente. Hablaremos de él más tarde.

     No conviene seguir adelante sin mencionar antes la tarjeta gráfica Hércules. Se trata de una tarjeta que apareció en el mercado muy poco después que la CGA de IBM, con el doble de resolución y manteniendo la calidad MDA en modo texto. Esta tarjeta no está soportada por la BIOS (manufacturada por IBM) y los fabricantes de SuperVGA tampoco se han molestado en soportarla por software, aunque sí por hardware. Está muy extendida en las máquinas antiguas, pero hoy en día no se utiliza y su programación obliga a acceder a los puertos de entrada y salida de manera directa al más bajo nivel.

7.4.2.- DETECCIÓN DE LA TARJETA GRÁFICA INSTALADA.

     El siguiente procedimiento es uno de tantos para evaluar la tarjeta gráfica instalada en el ordenador. Devuelve un valor en BL que es el mismo que retorna la INT 10h al llamarla con AX=1A00h (ver funciones de la BIOS en los apéndices): 0 ó 1 para indicar que no hay gráficos; 2 si hay CGA; 3, 4 ó 5 si existe una EGA; 6 si detecta una PGA; 7 u 8 si hay VGA o superior y 10, 11 ó 12 si existe MCGA. Retorna 255 si la tarjeta es desconocida (muy raro). La rutina funciona en todos los ordenadores, con o sin tarjetas gráficas instaladas y del tipo que sean.

    tipo_tarjeta   PROC
                   PUSH  DS
                   MOV   AX,1A00h
                   INT   10h               ; solicitar información VGA a la BIOS
                   CMP   AL,1Ah            ; BL = tipo de tarjeta
                   JE    tarjeta_ok        ; función soportada (hay VGA)
                   MOV   AX,40h
                   MOV   DS,AX
                   MOV   BL,10h
                   MOV   AH,12h
                   INT   10h               ; solicitar información EGA a la BIOS
                   CMP   BL,10h
                   JE    no_ega            ; de momento, no es EGA
                   MOV   BL,1              ; supuesto MDA
                   TEST  BYTE PTR DS:[87h],8      ; estado del control de vídeo
                   JNZ   tarjeta_ok        ; es MDA
                   MOV   BL,4              ; supuesto EGA color
                   OR    BH,BH
                   JZ    tarjeta_ok        ; así es
                   INC   BL                ; es EGA mono
                   JMP   tarjeta_ok
    no_ega:        MOV   BL,2              ; supuesto CGA
                   CMP   WORD PTR DS:[63h],3D4h   ; base del CRT
                   JE    tarjeta_ok        ; así es
                   DEC   BL                ; es MDA
    tarjeta_ok:    POP   DS
                   RET
    tipo_tarjeta   ENDP

7.4.3. - INTRODUCCIÓN AL ESTÁNDAR GRÁFICO VGA.

     La tarjeta VGA es el estándar actual en ordenadores personales, siendo el sistema de vídeo mínimo que incluye la máquina más asequible. En este apartado estudiaremos la forma básica de programar sus modos gráficos, haciendo un especial hincapié en el tema menos claramente explicado por lo general: el color. Se ignorarán por completo las tarjetas CGA y Hércules, aunque sí se indicará qué parte de lo expuesto se puede aplicar también a la EGA. Tampoco se considerará la MCGA, un híbrido entre EGA y VGA que solo equipa a los PS/2-30 de IBM, bastante incompatible además con la EGA y la VGA.

     La VGA soporta todos los modos gráficos estándar de las tarjetas anteriores, resumidos en la figura 7.4.3.1, si bien los correspondientes a la CGA (320x200 en 4 colores y 640x200 monocromo) son inservibles para prácticamente cualquier aplicación gráfica actual.

Modo (hex) Resolución Colores Segmento Organización Adaptador
4 y 5 320 x 200 4 B800 entrelazado CGA
6 640 x 200 2 B800 entrelazado CGA
0Dh 320 x 200 16 A000 planos de bit EGA
0Eh 640 x 200 16 A000 planos de bit EGA
0Fh 640 x 350 2 A000 planos de bit EGA
10h 640 x 350 4 A000 planos de bit EGA
10h 640 x 350 16 A000 planos de bit EGA (128K)
11h 640 x 480 2 A000 lineal VGA/MCGA
12h 640 x 480 16 A000 planos de bit VGA
13h 320 x 200 256 A000 lineal VGA/MCGA
FIGURA 7.4.3.1: MODOS GRÁFICOS DE VIDEO

     La organización de la memoria (entrelazado, planos de bit o lineal) es la manera en que se direcciona la memoria de vídeo por parte de la CPU. Por ejemplo, en el modo 6, cada pixel de la pantalla está asociado a un bit (8 pixels por byte) a partir de la dirección B800:0000; sin embargo, cuando se recorren 80 bytes en la memoria (640 bits o pixels,  primera  líneacompleta) no se pasa a la segunda línea de la pantalla sino unas cuantas más abajo, en una arquitectura relativamente compleja debida a las limitaciones del hardware de la CGA. Esto ha sido superado en las siguientes tarjetas, en las que las líneas están consecutivas de manera lógica en una organización lineal, si bien el límite de 64 Kb de memoria que puede direccionar en un segmento el 8086 ha obligado al truco de los planos de bit. Para establecer el modo de vídeo se puede emplear una función del lenguaje de programación que se trate o bien llamar directamente a la BIOS, si no se desea emplear la librería gráfica del compilador: la función 0 (AH=0) de servicios de vídeo de la BIOS (INT 10h) establece el modo de vídeo solicitado en AL. En Turbo C sería, por ejemplo:

#include <dos.h>
main()
{
    struct REGPACK r;

    r.r_ax=0x0012;     /* AH = 00, AL=12h */
    intr (0x10, &r);   /* ejecutar INT 10h */
}

7.4.3.1 - EL HARDWARE DE LA VGA.

     El chip VGA consta de varios módulos internos, que definen conjuntos de registros direccionables en el espacio E/S del 80x86. En la EGA eran de sólo escritura, aunque en la VGA pueden ser tanto escritos como leídos. Por un lado está el secuenciador, encargado de la temporización necesaria para el acceso a la memoria de vídeo. Por otro lado tenemos el controlador de gráficos, encargado del tráfico de información entre la CPU, la memoria de vídeo y el controlador de atributos; consta de 9 registros cuya programación es necesaria para trazar puntos a gran velocidad en los modos de 16 colores. El controlador de atributos gestiona la paleta de 16 colores y el color del borde. Por último, el DAC o Digital to Analog Converter se encarga en la VGA (no dispone de él la EGA) de gestionar los 262.144 colores que se pueden visualizar en pantalla. La parte del león son los ¡768 registros! de 6 bits que almacenan la intensidad en las componentes roja, verde y azul de cada color, de los 256 que como mucho puede haber simultáneamente en la pantalla (256*3=768).

7.4.3.2 - EL COLOR.

     La CGA puede generar 16 colores diferentes, utilizando un solo bit por componente de color más un cuarto que indica la intensidad. Sin embargo, la EGA emplea dos bits por cada una de las tres componentes de color, con lo que obtiene 26=64 colores diferentes. Para asociar estos 64 colores a los no más de 16 que puede haber en un momento determinado en la pantalla, se emplean los 16 registros de paleta del controlador de atributos: En cada uno de estos registros, de 6 bits significativos, se definen los 16 colores posibles. La BIOS de la EGA y la VGA carga los registros de paleta adecuadamente para emular los mismos colores de la CGA. Así, por ejemplo, en los modos de texto el color 0 es el negro y el 15 el blanco brillante, si bien se puede alterar esta asignación. Un cambio en un registro de paleta afecta instantáneamente a todo el área de pantalla pintado de ese color. El valor binario almacenado en los registros de paleta tiene el formato xxrgbRGB, siendo rgb los bits asociados a las componentes roja, verde y azul de baja intensidad, y RGB sus homólogos en alta intensidad. Así, el valor 010010b se corresponde con el verde más brillante.

Modos de 16 colores en VGA.
     En la VGA el tema del color en los modos de pantalla de 16 colores (tanto gráficos como de texto) se complica algo más, debido a la presencia del DAC: una matriz de 256 elementos que constan cada uno de 3 registros de 6 bits. Cada uno de los registros de paleta apunta a un elemento del DAC, que es quien realmente contiene el color; lo que sucede es que los registros del DAC son programados por la BIOS para emular los 64 colores de la EGA. Existen dos maneras diferentes de indexar en el DAC los registros de paleta, de manera que se puede dividir el DAC en 16 bloques de 16 elementos o bien en 4 bloques de 64 elementos: en un momento dado, sólo uno de los bloques (denominado página de color del DAC) está activo. Esto significa que se pueden crear 16 ó 4 subpaletas, pudiéndose activar una u otra libremente con una función de la BIOS de la VGA. Por defecto, la BIOS establece 4 páginas de 64 elementos en el DAC, de manera que valores en el rango 0-63 en los 16 registros de paleta referencien a posiciones distintas en el DAC (al área 0-63, al 64-127, al 128-191 ó al 192-255): por defecto, la BIOS emplea los elementos 0..63 del DAC que programa para emular los 64 colores de la EGA. Sin embargo, puede resultar más interesante disponer de 16 subpaletas de 16 elementos para conseguir determinados efectos gráficos: en este caso no tiene sentido que los registros de paleta almacenen valores fuera del rango 0-15 (de hecho, solo se consideran los 4 bits menos significativos de los mismos). La figura 7.4.3.2 expresa gráficamente la manera en que se genera el color. Se pueden definir, por ejemplo, las 16 subpaletas en tonos ascendentes de azul y, cambiando la página o subpaleta activa a cierta velocidad se puede hacer que la imagen se encienda y apague rítmica y suavemente. Por supuesto, también se pueden obtener efectos similares alterando directamente los registros del DAC, aunque es mucho más lento que conmutar entre varias paletas ya definidas. Conviene resaltar que el color del borde de la pantalla se define en la EGA y en la VGA en una especie de registro que sigue a los 16 registros de paleta: en la VGA no interviene el DAC en la generación del color del borde, del que solo existen por consiguiente 64 tonos (si bien el borde suele estar en color negro y su tamaño reducido y variable lo hace inservible para nada).

     Los pixels en los modos gráficos de 16 colores pueden parpadear, si bien es una técnica poco empleada: para ello, basta con cambiar un bit de un registro del controlador de atributos, aunque existe una función de la BIOS que realiza dicha tarea (llamar a la INT 10h con AX=1003h y BX=1 para activar el parpadeo -situación por defecto en los modos de texto- ó BX=0 para desactivarlo).


El truco del mono.
     Los monitores monocromos VGA solo admiten 64 tonos y se limitan siempre a presentar la componente verde del DAC. Lo que sucede es que la BIOS ajusta la intensidad de la señal verde para emular la presencia de las otras dos. En concreto, suma el 30% del valor rojo, el 59% del verde y el 11% del azul y el resultado lo fuerza al rango 0-63, lo cual simula aproximadamente la intensidad que percibiría el ojo humano con los colores reales. Si se accediera directamente al hardware sin ayuda de la BIOS, lo cual no es nuestro caso, este sería un aspecto a considerar. Por último, decir que en el modo de 4 colores y 350 líneas, solo se emplean los registros de paleta 0, 1, 4 y 5, si bien lo normal aquí es esperar que existan 16 colores (caso de la VGA, o incluso de la EGA con 128K).

Modo de 256 colores.
     En el modo 13h de 320x200 con 256 colores, la generación del color se aparta de lo estudiado hasta ahora para los demás modos gráficos y los de texto, ya que solo interviene el DAC: el byte de memoria de vídeo asociado a cada punto de la pantalla apunta directamente a un elemento del DAC. Por tanto, los registros de paleta del controlador de atributos no se emplean en este modo, siendo más sencillo el proceso de generación del color.

Cómo definir la paleta y los registros del DAC.
     A la hora de cambiar la paleta es conveniente emplear funciones de la BIOS o del lenguaje de programación, ya que un acceso directo al hardware sin más precauciones puede provocar interferencias con algunas tarjetas VGA. Conviene también emplear las funciones que cambian de una sola vez un conjunto de registros del DAC, ya que hacerlo uno por uno es demasiado lento. Otra ventaja de emplear la BIOS es que ésta hace automáticamente las conversiones necesarias para lograr la mejor visualización posible en pantallas monocromas. En algunos casos, las paletas que define por defecto la BIOS al establecer el modo de pantalla son apropiadas. Sin embargo, puede ser útil cambiarlas para lograr un degradado atractivo en los modos de 16 colores y casi obligatorio en el modo de 256 colores, dada la absurda paleta propuesta por la BIOS. Para definir un color en el DAC, basta con un poco de imaginación: si las tres componentes están a cero, saldrá el negro; si están a 63 (valor máximo) saldrá un blanco brillante; si se ponen la roja y la azul en 32 y la verde en 0, saldrá un morado de oscuridad mediana. Se puede realizar un bucle y llenar los primeros 64 elementos del DAC con valores crecientes en una componente de color, poniendo a 0 las demás: de esa manera, se genera una paleta óptima para hacer degradados (escalas de intensidad) de un color puro.

FIGURA 7.4.3.3:
/*********************************************************************
* EJEMPLO DE CAMBIO DE LA PALETA DE 16 COLORES (EGA/VGA) LLAMANDO AL *
* BIOS PARA ELEGIR LOS COLORES DESEADOS, ENTRE LOS 64 POSIBLES DE LA *
* EGA (POR DEFECTO EMULADOS POR EL DAC DE LA VGA).                   *
*********************************************************************/

#include <dos.h>
#include <graphics.h>

void main()
{
  struct REGPACK r;
  int gdrv, gmodo, coderr, i, x, color, pixel;
  char paleta[17];

  /* ESTABLECER MODO EGA/VGA 640x350 - 16 COLORES */

  detectgraph (&gdrv, &gmodo); coderr=graphresult();
  if (((gdrv!=EGA) && (gdrv!=VGA)) || (coderr!=grOk))
    { printf("\nNecesaria tarjeta EGA o VGA.\n"); exit(1); }
  gmodo=EGAHI; initgraph(&gdrv, &gmodo, ""); coderr=graphresult();
  if (coderr!=grOk)
    { printf("Error gráfico: %s.\n", grapherrormsg(coderr)); exit(1);}

  /* DIBUJAR BANDAS VERTICALES DE EJEMPLO */

  for (x=color=0; color<16; color++)
    for (pixel=0; pixel<getmaxx()/16; pixel++, x++) {
      setcolor (color); line (x, 0, x, getmaxy());
      }

  /* DEFINIR NUEVA PALETA */

  paleta[0]=0;       /* __rgbRGB = 0  --> negro                  */
  paleta[1]=4;       /* __000100 = 4  --> componente roja normal */
  paleta[2]=4*8;     /* __100000 = 32 --> componente roja oscura */
  paleta[3]=4*8+4;   /* __100100 = 36 --> ambas: rojo brillante  */
  for (i=4; i<17; i++) paleta[i]=0; /* resto colores y borde negros */

  r.r_es=FP_SEG(paleta); r.r_dx=FP_OFF(paleta);
  r.r_ax=0x1002; intr (0x10, &r);    /* establecer paleta y borde */

  getch();  closegraph();
}


     Para establecer la paleta se puede llamar a la BIOS (INT 10h) con AX=1002h y ES:DX apuntando a un buffer de 17 bytes: uno para cada registro de paleta más otro final para el color del borde de la pantalla. El Turbo C permite cambiar la paleta con instrucciones de alto nivel; sin embargo, quienes no deseen aprender las particularidades de cada compilador, siempre pueden recurrir a la BIOS, que cambiando la paleta es bastante solvente. Echemos un vistazo al ejemplo de la figura 7.4.3.3 (para ejecutar este programa hay que tener en cuenta que el fichero EGAVGA.BGI del compilador ha de estar en el directorio de trabajo). Al principio se trazan unas bandas verticales con la función line() que serán coloreadas con los 16 colores por defecto, aunque cambiarán instantáneamente al modificar la paleta. Al definir la paleta, los 4 primeros registros son asignados con los 4 posibles tonos de rojo, más bien 3 (el primero es el negro absoluto):  rojo, rojooscuro y rojo brillante. Todos los demás registros y el borde de la pantalla son puestos a 0 (negro) por lo que en la pantalla quedan visibles sólo las tres bandas verticales citadas. El cambio de la paleta es instantáneo, lo que permite hacer efectos especiales. En la VGA, recuérdese que los valores de la paleta son simples punteros al DAC y no los colores reales. Lo que sucede es que los registros del DAC son inicializados al cambiar el modo de pantalla de tal manera que emulan los colores que se obtendría en una EGA... a menos que se cambien los valores de dichos registros.

     Para ello, nada mejor que llamar de nuevo a la INT 10h con AX=1012h, indicando en BX el primer elemento del DAC a cambiar (típicamente 0) y en CX el número de elementos a modificar (a menudo los 256 posibles). También se pasa en ES:DX la dirección de la tabla de 768 bytes que contiene la información: 3 bytes consecutivos para cada elemento del DAC (rojo, verde y azul) aunque solo son significativos los 6 bits de menor orden de cada byte. Existe también otra función bastante interesante, invocable con AX=1013h y que consta de dos subservicios: el primero se selecciona poniendo un 0 en BL, e indicando en BH si se desean 4 páginas de 64 elementos en el DAC (BH=0) ó 16 páginas de 16 elementos (BH=1). El segundo servicio se indica llamando con BL=1, y permite seleccionar la página del DAC activa en BH (0-3 ó 0-15, según cómo esté estructurado). Obviamente, esta función no está disponible en el modo 13h de 256 colores, en el que no interviene la paleta (sólo el DAC y entero, no a trocitos). La figura 7.4.3.4 contiene un nuevo programa completo de demostración, desarrollado a partir del anterior, que requiere ya un auténtico adaptador VGA. Lo primero que se hace es seleccionar el modo de 16 páginas en el DAC, estableciendo la página 2 como activa (exclusivamente por antojo mio). Ello significa que se emplearán los elementos 32..47 del DAC (la página 0 apuntaría a los elementos 0..15, la 1 hubieran sido los elementos 16..31 y así sucesivamente). Los registros de paleta, simples índices en el DAC, toman los valores 0,1,...,15 (excepto el 17º byte, color del borde, puesto a 0 para seleccionar el negro). A continuación, basta programar los registros 32..47 del DAC con los colores deseados, entre los 262.144 posibles. Como cada componente puede variar entre 0 y 63, elegimos 16 valores espaciados proporcionalmente (0, 4, 8,..., 60) y los asignamos a las componentes roja y verde (rojo+verde=amarillo), apareciendo en la pantalla una escala de 16 amarillos (el primero, negro absoluto) de intensidad creciente. Si bien 16 colores son pocos, son suficientes para representar con relativa precisión algunas imágenes, especialmente en las que predomina un color determinado (los ficheros gráficos se ven normalmente tan mal en los modos de 16 colores debido a que respetan la paleta de la EGA, en la VGA sería otra historia).

FIGURA 7.4.3.4:
/*********************************************************************
*  EJEMPLO DE CAMBIO DE LA PALETA DE 16 COLORES Y REPROGRAMACION DEL *
* DAC DE LA VGA POR EL BIOS PARA ELEGIR LOS 16 COLORES ENTRE 262.144 *
*********************************************************************/

#include <dos.h>
#include <graphics.h>

void main()
{
  struct REGPACK r;
  int gdrv, gmodo, coderr, pagina, i, x, color, pixel;
  char paleta[17], dac[256][3];

  /* ESTABLECER MODO VGA 640x480 - 16 COLORES */

  detectgraph (&gdrv, &gmodo); coderr=graphresult();
  if ((gdrv!=VGA) || (coderr!=grOk))
    { printf("\nNecesaria tarjeta VGA.\n"); exit(1); }
  gmodo=VGAHI; initgraph(&gdrv, &gmodo, ""); coderr=graphresult();
  if (coderr!=grOk)
    { printf("Error gráfico: %s.\n", grapherrormsg(coderr)); exit(1);}


  /* DIBUJAR BANDAS VERTICALES DE EJEMPLO */

  for (x=color=0; color<16; color++)
    for (pixel=0; pixel<getmaxx()/16; pixel++, x++) {
      setcolor (color); line (x, 0, x, getmaxy());
      }

  /* SELECCIONAR 16 BLOQUES DE 16 ELEMENTOS EN EL DAC */

  r.r_ax=0x1013; r.r_bx=0x0100; intr (0x10, &r);

  /* PAGINA 2: LA PALETA SE APOYARA EN ELEMENTOS 32..47 DEL DAC */

  pagina=2; r.r_ax=0x1013; r.r_bx=(pagina<<8) | 1; intr (0x10, &r);

  /* APUNTAR REGISTROS DE PALETA A ELEMENTOS CONSECUTIVOS DEL DAC */

  for (i=0; i<16; i++) paleta[i]=i;
  paleta[16]=0;                      /* color del borde */

  r.r_es=FP_SEG(paleta); r.r_dx=FP_OFF(paleta);
  r.r_ax=0x1002; intr (0x10, &r);    /* establecer paleta y borde */

  /* LLENAR ELEMENTOS 32..47 DEL DAC DE AMARILLOS CRECIENTES */

  for (i=32; i<48; i++) {
    dac[i][0]=i*4;   /* valores crecientes 0..60 de rojo */
    dac[i][1]=i*4;   /* valores crecientes 0..60 de verde */
    dac[i][2]=0;     /* sin componente azul */
    }

  r.r_bx=32;   /* primer elemento del DAC */
  r.r_cx=16;  /* número de elementos a definir */
  r.r_es=FP_SEG(dac[32]); r.r_dx=FP_OFF(dac[32]);
  r.r_ax=0x1012; intr (0x10, &r);  /* programar elementos del DAC */

  getch();
  closegraph();
}

     Por supuesto, existen más funciones que éstas, entre ellas las que permiten cambiar sólo un registro de paleta o un elemento del DAC (y no un bloque); sin embargo, son más lentas cuando se va a cambiar un conjunto de registros. En cualquier caso, el lector puede consultarlas en el fichero INTERRUP.LST si lo desea. También existen en la VGA las funciones inversas (obtener paletas y registros del DAC). El acceso por medio de la BIOS para cambiar la paleta es a menudo más cómodo que emplear funciones del lenguaje de programación y garantiza en ocasiones un mayor nivel de independencia respecto a la evolución futura del hardware (aunque si la librería gráfica llama a la BIOS...). Sin embargo, para otras aplicaciones, es mejor no usar la BIOS. Por ejemplo, el programa de la figura 7.4.3.5 accede directamente a los registros de la VGA para modificar la paleta en dos bucles, en el primero disminuyendo la luminosidad de la pantalla (hasta dejarla negra) y en el segundo restaurándola de nuevo. Este efecto cinematográfico hubiera sido imposible a través de la BIOS por razones de velocidad: el acceso directo al hardware, con precauciones (en este caso, esperar el retrazado vertical para evitar interferencias) es a veces inevitable. El programa de ejemplo funciona también en monitores monocromos, aunque en la práctica sólo actúe en ellos sobre la componente verde. El lector deberá consultar bibliografía especializada para realizar este tipo de programación.

FIGURA 7.4.3.5:
/*********************************************************************
*       EFECTO «CINEMATOGRAFICO» DE DESVANECIMIENTO Y POSTERIOR      *
*    REAPARICION DE LA PANTALLA CON ACCESO DIRECTO AL HARDWARE VGA.  *
*********************************************************************/

#include <dos.h>

void main()
{
  unsigned char dac[256][3];
  register i, j;


  for (i=0; i<256; i++) {          /* anotar la paleta activa */
    disable();
    outportb (0x3C7, i);
    dac [i][0] = inportb (0x3C9);  /* R */
    dac [i][1] = inportb (0x3C9);  /* G */
    dac [i][2] = inportb (0x3C9);  /* B */
    enable();
    }
                             /* claridad descendente desde el
                                64/64-avo al 0/64-avo de intensidad */
  for (i=64; i>=0; i--) {
    while (!((inportb(0x3DA) & 8)==8)); /* esperar retrazo vertical */
    while (!((inportb(0x3DA) & 8)==0)); /* esperar su fin */
    for (j=0; j<256; j++) {
      disable();
      outportb (0x3C8, j);
      outportb (0x3C9, dac[j][0]*i >> 6);
      outportb (0x3C9, dac[j][1]*i >> 6);
      outportb (0x3C9, dac[j][2]*i >> 6);
      enable();
      }
    }
                             /* claridad ascendente desde el
                                0/64-avo al 64/64-avo de intensidad */
  for (i=0; i<=64; i++) {
    while (!((inportb(0x3DA) & 8)==8)); /* esperar retrazo vertical */
    while (!((inportb(0x3DA) & 8)==0)); /* esperar su fin */
    for (j=0; j<256; j++) {
      disable();
      outportb (0x3C8, j);
      outportb (0x3C9, dac[j][0]*i >> 6);
      outportb (0x3C9, dac[j][1]*i >> 6);
      outportb (0x3C9, dac[j][2]*i >> 6);
      enable();
      }
    }
}

7.4.3.3 - DIRECCIONAMIENTO DE PIXELS.

     Para pintar pixels en la pantalla y para consultar su color, existen funciones de la BIOS de uso no recomendado. La razón estriba en el mal diseño de la BIOS inicial de IBM, no mejorado tampoco por las VGA clónicas. El problema es que las BIOS emplean 4, 5 y hasta 10 veces más tiempo del necesario para trazar los puntos. La causa de este problema no reside en que empleen rutinas multipropósito para todos los modos, ya que existen básicamente sólo tres tipos de arquitectura de pantalla (modos CGA, 16 colores y 256 colores). El fallo reside, simplemente, en que han sido desarrollados sin pensar en la velocidad. Por ejemplo, la BIOS emplea el algoritmo más lento posible que existe para trazar puntos en los modos de 16 colores. Lo más conveniente es utilizar los recursos del lenguaje de programación o, mejor aún, acceder directamente a la memoria de pantalla con subrutinas en ensamblador. Este es el procedimiento seguido por la mayoría de las aplicaciones comerciales. Sin embargo, la BIOS tiene la ventaja de que permite normalizar el acceso a la pantalla. Así, un programa puede fácilmente trazar un punto en el modo 1024x768x256 de una SuperVGA (y nunca mejor dicho, porque como sean muchos más de uno...). Para trazar un punto se coloca en CX la coordenada X, en DX la coordenada Y, en AL el color, en BH la página y en AH el valor 0Ch.  A  continuación  se  llama,como es costumbre, a la INT 10h. Para consultar el color de un punto en la pantalla, se cargan CX y DX con sus coordenadas y BH con la página, haciendo AH=0Dh antes de llamar a la INT 10h, la cual devuelve el color del pixel en AL. La página será normalmente la 0, aunque en los modos de vídeo que soportan varias páginas ésta se puede seleccionar con la función 5 de la INT 10h. La existencia de varias páginas de vídeo se produce cuando en el segmento de 64 Kb de la memoria de vídeo se puede almacenar más de una imagen completa (caso por ejemplo del modo 640x350x16): existen entonces varias páginas (2, 4, etc.) que se reparten el segmento a partes iguales. Se puede en estas circunstancias visualizar una página cualquiera mientras se trabaja en las otras, que mientras tanto permanecen ocultas a los ojos del usuario.

Modo 13h de 256 colores.
     Este modo, de organización lineal, no presenta complicación alguna: los pixels se suceden en la memoria de vídeo de izquierda a derecha y de arriba a abajo, a partir del segmento A000. Cada punto está asociado a un byte, cuyo valor (0-255) referencia directamente a un elemento del DAC. En la figura 7.4.3.6 hay un nuevo listado de ejemplo, en este caso sin emplear la librería gráfica del Turbo C. El programa se limita a activar este modo de pantalla pintando las 200 líneas con los valores 0..199. A continuación define los elementos 0..199 del DAC de la siguiente manera: los primeros 100 en tonos ascendentes de azul, y los siguientes 100 elementos en tonos descendentes de naranja, lo que divide automáticamente la pantalla en dos zonas con la estructura citada. Conseguir el naranja no es complicado: basta sumar rojo con amarillo; como el amarillo es a su vez rojo más verde, el naranja se obtiene sumando dos cantidades de rojo por cada una de verde. Los elementos 200..255 del DAC, no empleados en este ejemplo, podrían ser definidos con otros colores para dibujar alguna otra cosa.

FIGURA 7.4.3.6:
/*********************************************************************
*        EJEMPLO DE USO DEL MODO DE 320x200 CON 256 COLORES          *
*        SIN EMPLEAR LA LIBRERIA GRAFICA DEL COMPILADOR.             *
*********************************************************************/

#include <dos.h>

void main()
{
  struct REGPACK r;
  char dac[256][3], far *vram;
  register x, y;
  int i,ii;

  /* ESTABLECER MODO DE PANTALLA */

  r.r_ax=0x13; intr (0x10, &r); vram=MK_FP(0xA000, 0);

  /* LLENAR LA PANTALLA CON LINEAS HORIZONTALES DE COLOR 0..199 */

  for (y=0; y<200; y++) for (x=0; x<320; x++) *vram++=y;

  /* DEFINIR PALETA EN EL DAC */

  for (i=0; i<100; i++) {
    dac[i][0]=0;
    dac[i][1]=0;         /* definir azules */
    dac[i][2]=i >> 1;
    }

  for (i=100; i<200; i++) {
    ii=200-i;
    dac[i][0]=ii >> 1;
    dac[i][1]=ii >> 2;   /* definir naranjas */
    dac[i][2]=0;
    }

  r.r_ax=0x1012; r.r_bx=0; r.r_cx=200;
  r.r_es=FP_SEG(dac); r.r_dx=FP_OFF(dac); intr (0x10, &r);

  getch(); r.r_ax=3; intr (0x10, &r);
}

Modos de 16 colores.
     Para direccionar puntos en los modos de 16 colores, en los que actúan interrelacionados los registros de paleta y el DAC de la manera descrita con anterioridad, es necesario un acceso directo al hardware por cuestiones de velocidad. Los lectores que no vayan a emplear las funciones del lenguaje de programación deberán consultar bibliografía especializada en gráficos.

Y nada más.
     La única diferencia de la VGA respecto a la EGA, de hecho, se debe a su peculiar manera de gestionar el color, así como a la inclusión del modo de 320x200 con 256 colores (el modo de 640x480 es idéntico en funcionamiento al de 640x350 de la EGA, solo cambia la altura de la pantalla). Existe también la posibilidad de colocar la VGA en dos modos de 256 colores alternativos al 13h y basados en el mismo; en uno se alcanzan 320x240 puntos y en el otro 320x400. La bibliografía especializada en gráficos explica los pasos a realizar para conseguir esto, factible en la totalidad de las tarjetas VGA del mercado. Sin embargo, estos modos requieren un cambio en el modo de direccionamiento de los pixels, que pasa a ser más complejo -aunque más potente para algunas aplicaciones-.

7.4.4. - EJEMPLO DE GRÁFICOS EMPLEANDO LA BIOS.

     Este programa ejemplo accede a la pantalla empleando las funciones de la BIOS para trazar puntos (ver apéndice sobre funciones de la BIOS). Utiliza el modo CGA de 640x200 puntos, aunque se puede configurar para cualquier otro modo. El programa dibuja una conocida red en las cuatro esquinas de la pantalla, trazando líneas. El algoritmo empleado es el de Bresseham con cálculo incremental de puntos (aunque al estar separada la rutina que traza el punto esta característica no se aprovecha, pero es fácil de implementar si en vez de llamar a la BIOS para pintar se emplea una rutina propia mezclada con la que traza la recta). La velocidad del algoritmo es muy elevada, sobre todo con las líneas largas, máxime teniendo en cuenta que se trata posiblemente de una de sus implementaciones más optimizada (sólo usa una variable y mantiene todos los demás valores en los 7 registros de datos de la CPU, sin emplear demasiado la pila y duplicando código cuando es preciso en los puntos críticos). No entraré en explicaciones matemáticas del método, del que hay pautas en su listado. Existen versiones de este método que consideran de manera especial las líneas verticales y horizontales para pintarlas de manera más rápida, aunque yo personalmente prefiero rutinas independientes para esas tareas con objeto de no ralentizar el trazado de rectas normales.

    ; ********************************************************************
    ; *                                                                  *
    ; *  RED.ASM   -   Demostración de gráfica en CGA utilizando BIOS    *
    ; *                                                                  *
    ; ********************************************************************

    modo           EQU   6                 ; modo de vídeo
    max_x          EQU   640
    max_y          EQU   200
    max_color      EQU   2

    red            SEGMENT
                   ASSUME CS:red, DS:red

                   ORG   100h
    inicio:
                   MOV   AX,modo
                   INT   10h               ; modo de pantalla
                   MOV   AL,max_color-1    ; color visible
                   MOV   BX,0              ; contador para eje Y
                   MOV   BP,0              ; contador para eje X
    otras_cuatro:  MOV   CX,0
                   MOV   DX,BX
                   MOV   SI,BP
                   MOV   DI,max_y-1
                   CALL  recta             ; primera recta
                   MOV   CX,max_x-1
                   MOV   SI,max_x-1
                   SUB   SI,BP
                   CALL  recta             ; segunda
                   MOV   CX,BP
                   MOV   DX,0
                   MOV   SI,0
                   MOV   DI,max_y-1
                   SUB   DI,BX
                   CALL  recta             ; tercera
                   MOV   CX,max_x-1
                   SUB   CX,BP
                   MOV   SI,max_x-1
                   CALL  recta             ; cuarta
                   ADD   BX,6
                   ADD   BP,14
                   CMP   BX,max_y
                   JB    otras_cuatro
                   MOV   AH,0
                   INT   16h               ; esperar pulsación de tecla
                   MOV   AX,3
                   INT   10h               ; volver a modo texto
                   INT   20h               ; fin de programa

    recta          PROC
                   PUSH  AX                ; de (CX,DX) a (SI,DI) color AL
                   PUSH  BX
                   PUSH  CX
                   PUSH  DX
                   PUSH  SI
                   PUSH  DI
                   PUSH  BP
                   MOV   color,AL
                   MOV   AX,SI
                   SUB   AX,CX             ; AX = X2-X1
                   JNC   absx2x1
                   NEG   AX
                   XCHG  CX,SI
                   XCHG  DX,DI
    absx2x1:       MOV   BX,DI             ; AX = ABS(X2-X1) = «dx»
                   SUB   BX,DX
                   MOV   BP,1              ; BP = 1  = «yincr» si  Y2>Y1
                   JNC   absy2y1
                   NEG   BP                ; BP = -1  = «yincr» si  Y2<=Y1
                   NEG   BX
    absy2y1:       CMP   AX,BX             ; BX = ABS(Y2-Y1) = «dy»
                   PUSHF
                   JA    noswap            ; ABS(pendiente) menor de 1
                   XCHG  AX,BX
    noswap:        SHL   BX,1              ; BX = «dy» * 2
                   MOV   SI,BX
                   SUB   SI,AX             ; SI = «dy» * 2 - «dx» = «d»
                   MOV   DI,BX
                   SUB   DI,AX
                   SUB   DI,AX             ; DI = «dy»*2-«dx»*2 = «incr2»
                   POPF
                   JBE   penmay1           ; pendiente mayor de 1
    penmen1:       PUSH  AX
                   MOV   AL,color
                   CALL  punto             ; en (CX, DX) = («x», «y»)
                   POP   AX
                   INC   CX                ; «x»++
                   AND   SI,SI             ; (SI>0) ?   ->  «d» > 0 ?
                   JS    noincy
                   ADD   SI,DI             ; «d» > 0 : «d» = «d» + «incr2»
                   ADD   DX,BP             ; «y» = «y» + «yincr»
                   DEC   AX                ; «dx»--
                   JNZ   penmen1
                   JMP   fin
    noincy:        ADD   SI,BX             ; «d» < 0 : «d» = «d» + «incr1»
                   DEC   AX
                   JNZ   penmen1
                   JMP   fin
    penmay1:       PUSH  AX
                   MOV   AL,color
                   CALL  punto             ; en (CX, DX) = («x», «y»)
                   POP   AX
                   ADD   DX,BP             ; «y» = «y» + «yincr»
                   AND   SI,SI             ; (SI>0) ?   ->  «d» > 0 ?
                   JS    noincx
                   ADD   SI,DI             ; «d» > 0 : «d» = «d» + «incr2»
                   INC   CX                ; «x»++
                   DEC   AX                ; «dx»--
                   JNZ   penmay1
                   JMP   fin
    noincx:        ADD   SI,BX             ; «d» = «d» + «incr1»
                   DEC   AX                ; «dx»--               
                   JNZ   penmay1
    fin:           POP   BP
                   POP   DI
                   POP   SI
                   POP   DX
                   POP   CX
                   POP   BX
                   POP   AX
                   RET
    color          DB    0
    recta          ENDP

    punto          PROC
                   PUSH  BX               ; preservar registros (salvo AX)
                   PUSH  CX
                   PUSH  DX               
                   PUSH  BP
                   PUSH  SI
                   PUSH  DI
                   MOV   AH,0Ch            ; trazar punto usando BIOS
                   XOR   BX,BX
                   INT   10h
                   POP   DI
                   POP   SI
                   POP   BP
                   POP   DX
                   POP   CX
                   POP   BX
                   RET
    punto          ENDP

    red            ENDS
                   END   inicio

     Quizá el lector opine que RED.ASM no es tan rápido. Y tiene razón: la culpa es de la BIOS, que consume un alto porcentaje del tiempo de proceso. Sustituyendo la rutina «punto» por una rutina de trazado de puntos propia, como la que se lista a continuación, la velocidad puede llegar a quintuplicarse en un hipotético RED2.ASM que la invocara.

    punto640x200_C PROC            ; en (CX, DX) de color AL (CGA 640x200)
                   PUSH  DS        ; sólo se corrompe AX
                   PUSH  BX
                   PUSH  CX
                   PUSH  DX
                   MOV   BX,0B800h ; segmento de pantalla CGA
                   MOV   DS,BX
                   MOV   AH,CL     ; preservar parte baja de «cx»
                   XCHG  BX,CX     ; BX = «cx»
                   MOV   CL,3
                   SHR   BX,CL     ; BX = «cx» / 8
                   SHR   DX,1      ; DX = int («cy» / 2)
                   JNC   no_add
                   ADD   BX,8192   ; BX = «cx» / 8 + («cy» MOD 2) * 8192
    no_add:        INC   CL        ; CL = 4
                   SHL   DX,CL     ; DX = («cy» / 2) * 16
                   ADD   BX,DX     ; BX = BX + («cy» / 2) * 16
                   SHL   DX,1
                   SHL   DX,1      ; DX = («cy» / 2) * 64
                   ADD   BX,DX     ; BX = BX + («cy» / 2) * 80
                   MOV   CL,AH     ; recuperar parte baja de «cx»
                   AND   CL,7      ; dejar nº de bit a pintar (0..7)
                   XOR   CL,7      ; invertir orden de numeración
                   MOV   AH,1      ; bit a borrar de la pantalla en AH
                   SHL   AX,CL     ; AH = bit a borrar, AL = bit a pintar
                   NOT   AH
                   AND   [BX],AH   ; borrar punto anterior
                   OR    [BX],AL   ; ubicar nuevo punto (1/0)
                   POP   DX
                   POP   CX
                   POP   BX
                   POP   DS
                   RET
    punto640x200_C ENDP

     Para estudiar el funcionamiento de la pantalla CGA el lector puede hacer un programa que recorra la memoria de vídeo para comprender la manera en que está organizada, un tanto peculiar pero no demasiado complicada. Sin embargo, con EGA y VGA no es tan sencillo realizar operaciones sobre la pantalla debido a la presencia de planos de bit; salvo contadas excepciones como la del siguiente apartado.

7.4.5. - EJEMPLO DE GRÁFICOS ACCEDIENDO AL HARDWARE.

     El siguiente programa de ejemplo accede directamente al segmento de vídeo de la VGA (0A000h) para trazar los puntos. Dibuja un vistoso ovillo basado en circunferencias con centro ubicado en una circunferencia base imaginaria, aprovechando los 256 colores de la VGA estándar en el modo 320x200. Como la paleta establecida por defecto es poco interesante, se define previamente una paleta con apoyo directo en el hardware (el método empleado es sencillo pero no recomendable, provoca nieve con algunas tarjetas). Se emplea el color verde, único visualizable en monitores monocromos (aunque cambiando la paleta con las funciones de la BIOS no hubiera sido necesario). La VGA en modo 13h asocia cada punto de pantalla a un byte, por lo que la pantalla es una matriz de 64000 bytes en el segmento 0A000h. Recordar que la fórmula para calcular el desplazamiento para un punto (cx,cy) es 320*cy+cx.

     Si se sustituye la rutina «punto», que traza el punto, por otra que lo haga llamando a la BIOS, en una VGA Paradise (BIOS de 14/7/88) se emplean 4 segundos y 8 centésimas en generar la imagen, mientras que tal y como está el programa lo dibuja en 40,4 centésimas (10,1 veces más rápido); todos estos datos cronometrados con precisión sobre un 386-25 sin memoria caché teniendo instalada la opción de «SHADOW ROM» (la lenta ROM copiada en RAM, incluida la BIOS de la VGA, por tanto no compite con desventaja).

     El algoritmo empleado para trazar la circunferencia es de J. Michener, quien se basó a su vez en otro de J. Bresseham desarrollado para plotter. La versión que incluyo genera circunferencias en pantallas de relación de aspecto 1:1, en otras (ej., de 640 x 200) produciría elipses. No entraré en su demostración matemática, que nada tiene que ver con el ensamblador; baste decir que la rutina se basa exclusivamente en la aritmética entera calculando un solo octante de la circunferencia (los demás los obtiene por simetría).

    ; ********************************************************************
    ; *                                                                  *
    ; *  OVILLO.ASM - Demostración de gráfica en VGA utilizando hardware *
    ; *                                                                  *
    ; ********************************************************************

    modo           EQU   13h               ; modo de vídeo
    max_x          EQU   320
    max_y          EQU   200
    max_color      EQU   256

    oviseg         SEGMENT
                   ASSUME CS:oviseg, DS:oviseg

                   ORG   100h
    inicio:
                   MOV   AX,modo
                   INT   10h
                   CALL  paleta_verde
                   MOV   CX,max_x
                   SHR   CX,1              ; CX = max_x / 2
                   MOV   DX,max_y
                   SHR   DX,1              ; DX = max_y / 2
                   MOV   BX,DX
                   SHR   BX,1              ; BX = ma_y / 4
                   CALL  ovillo            ; en (CX, DX) de radio BX
                   MOV   AH,0
                   INT   16h               ; esperar pulsación de tecla
                   MOV   AX,3
                   INT   10h               ; volver a modo texto
                   INT   20h               ; fin de programa

    paleta_verde   PROC
                   MOV   CX,256            ; los 256 registros
                   MOV   DX,3C8h
    otro_reg:      MOV   AL,CL
                   OUT   DX,AL             ; registro a programar
                   INC   DX
                   XOR   AL,AL
                   OUT   DX,AL             ; componente roja
                   MOV   AL,CL
                   REPT  max_x/320
                   SHR   AL,1
                   ENDM
                   OUT   DX,AL             ; componente verde
                   XOR   AL,AL
                   OUT   DX,AL             ; componente azul
                   DEC   DX
                   LOOP  otro_reg
                   RET
    paleta_verde   ENDP

    ovillo         PROC              ; circunferencia de circunferencias
                   MOV   BP,BX       ; en (CX, DX) con radio BX y color AL
                   MOV   AL,0
                   MOV   SI,BX
                   XOR   DI,DI
                   SHL   BP,1
                   SUB   BP,3
                   NEG   BP                ; BP = 3 - 2 * BX
    ovillo_acaba:  CMP   DI,SI
                   JG    ovillo_ok         ; ovillo completado
                   ADD   CX,SI
                   ADD   DX,DI
                   CALL  circunferencia    ; en (x+SI, y+DI)
                   INC   AL
                   SUB   CX,SI
                   SUB   CX,SI
                   CALL  circunferencia    ; en (x-SI, y+DI)
                   INC   AL
                   SUB   DX,DI
                   SUB   DX,DI
                   CALL  circunferencia    ; en (x-SI, y-DI)
                   INC   AL
                   ADD   CX,SI
                   ADD   CX,SI
                   CALL  circunferencia    ; en (x+SI, y-DI)
                   INC   AL
                   SUB   CX,SI
                   ADD   DX,DI
                   ADD   CX,DI
                   ADD   DX,SI
                   CALL  circunferencia    ; en (x+DI, y+SI)
                   INC   AL
                   SUB   CX,DI
                   SUB   CX,DI
                   CALL  circunferencia    ; en (x-DI, y+SI)
                   INC   AL
                   SUB   DX,SI
                   SUB   DX,SI
                   CALL  circunferencia    ; en (x-DI, y-SI)
                   INC   AL
                   ADD   CX,DI
                   ADD   CX,DI
                   CALL  circunferencia    ; en (x+DI, y-SI)
                   INC   AL
                   SUB   CX,DI
                   ADD   DX,SI             ; CX = x, DX = y
                   CMP   BP,0
                   JG    ovillo_decx
                   ADD   BP,DI
                   ADD   BP,DI
                   ADD   BP,DI
                   ADD   BP,DI
                   ADD   BP,6
                   JMP   ovillo_incy
    ovillo_decx:   DEC   SI
                   PUSH  AX
                   MOV   AX,DI
                   SUB   AX,SI
                   SHL   AX,1
                   SHL   AX,1
                   ADD   BP,AX
                   POP   AX
                   ADD   BP,10
    ovillo_incy:   INC   DI
                   JMP   ovillo_acaba
    ovillo_ok:     RET
    ovillo         ENDP

    circunferencia PROC               ; en (CX,DX) con radio BX y color AL
                   PUSH  BX
                   PUSH  CX
                   PUSH  DX
                   PUSH  SI
                   PUSH  DI
                   MOV   SI,BX
                   XOR   DI,DI
                   SHL   BX,1
                   SUB   BX,3
                   NEG   BX                ; BX = 3 - 2 * BX
    circunf_acaba: CMP   DI,SI
                   JG    circunf_ok        ; circunferencia completada
                   ADD   CX,SI
                   ADD   DX,DI
                   CALL  punto             ; en (x+SI, y+DI)
                   SUB   CX,SI
                   SUB   CX,SI
                   CALL  punto             ; en (x-SI, y+DI)
                   SUB   DX,DI
                   SUB   DX,DI
                   CALL  punto             ; en (x-SI, y-DI)
                   ADD   CX,SI
                   ADD   CX,SI
                   CALL  punto             ; en (x+SI, y-DI)
                   SUB   CX,SI
                   ADD   DX,DI
                   ADD   CX,DI
                   ADD   DX,SI
                   CALL  punto             ; en (x+DI, y+SI)
                   SUB   CX,DI
                   SUB   CX,DI
                   CALL  punto             ; en (x-DI, y+SI)
                   SUB   DX,SI
                   SUB   DX,SI
                   CALL  punto             ; en (x-DI, y-SI)
                   ADD   CX,DI
                   ADD   CX,DI
                   CALL  punto             ; en (x+DI, y-SI)
                   SUB   CX,DI
                   ADD   DX,SI             ; CX = x, DX = y
                   CMP   BX,0
                   JG    circunf_decx
                   ADD   BX,DI
                   ADD   BX,DI
                   ADD   BX,DI
                   ADD   BX,DI
                   ADD   BX,6
                   JMP   circunf_incy
    circunf_decx:  DEC   SI
                   PUSH  AX
                   MOV   AX,DI
                   SUB   AX,SI
                   SHL   AX,1
                   SHL   AX,1
                   ADD   BX,AX
                   POP   AX
                   ADD   BX,10
    circunf_incy:  INC   DI
                   JMP   circunf_acaba
    circunf_ok:    POP   DI
                   POP   SI
                   POP   DX
                   POP   CX
                   POP   BX
                   RET
    circunferencia ENDP

    punto          PROC             ; trazar punto en 320x200 con 256 col.
                   PUSH  DS         ; en (CX, DX) con color AL
                   PUSH  CX
                   PUSH  DX
                   XCHG  DH,DL      ; DX = «cy» * 256
                   ADD   CX,DX      ; CX = «cy» * 256 + «cx»
                   SHR   DX,1
                   SHR   DX,1       ; DX = «cy» * 64
                   ADD   CX,DX      ; CX = «cy» * 320 + «cx»
                   MOV   DX,0A000h
                   MOV   DS,DX      ; segmento VGA
                   XCHG  BX,CX      ; preservar BX en CX, BX = offset
                   MOV   [BX],AL    ; pintar el punto
                   XCHG  BX,CX      ; restaurar BX
                   POP   DX         ; restaurar demás registros
                   POP   CX
                   POP   DS
                   RET
    punto          ENDP

    oviseg         ENDS
                   END   inicio

7.4.6. - EL ESTÁNDAR GRÁFICO VESA.

     Debido a la anarquía reinante en el mundo de las tarjetas gráficas, en 1989 se reunieron un grupo importante de fabricantes (ATI, Genoa, Intel, Paradise, etc) para intentar crear una norma común. El resultado de la misma fue el estándar VESA. Este estándar define una interface software común a todas las BIOS para permitir a los programadores adaptarse con facilidad a las diversas tarjetas sin tener en cuenta sus diferencias de hardware.

     Actualmente, las principales tarjetas soportan la norma VESA. Las más antiguas pueden también soportarla gracias a pequeños programas residentes que el usuario puede instalar opcionalmente. Para desarrollar una aplicación profesional, es una buena norma soportar algún modo estándar de la VGA y, para obtener más prestaciones, algún modo VESA para los usuarios que estén equipados con dicho soporte. Intentar acceder directamente al hardware o a las funciones BIOS propias de cada tarjeta del mercado por separado, salvo para aplicaciones muy concretas, es ciertamente poco menos que imposible.

Modos gráficos.

     El estándar VESA soporta multitud de modos gráficos, numerados a partir de 100h, si bien algunos de los más avanzados (con 32000 o 16 millones de colores) sólo están soportados por las versiones más recientes de la norma. Entre 100h y 107h se definen los modos más comunes de 16 y 256 colores de todas las SuperVGA, aunque el modo 6Ah también es VESA (800x600x16) al estar soportado por múltiples tarjetas.

     Una de las grandes ventajas del estándar VESA es la enorme información que pone a disposición del programador. Es posible conocer todos los modos y qué características de resolución, colores y arquitectura tienen. Además, hay funciones adicionales muy útiles para guardar y recuperar el estado de la tarjeta, de especial utilidad para programas residentes: así, estos pueden fácilmente conmutar a modo texto (con la precaución de preservar antes los 4 primeros Kbytes de la RAM de vídeo empleados para definir los caracteres) y volver al modo gráfico original dejando la pantalla en el estado inicial.

El programa de ejemplo.

     En el apéndice donde se resumen las funciones del DOS y la BIOS aparecen también las funciones VESA de vídeo. Estas funciones se invocan vía INT 10h, con AX tomando valores por lo general desde 4F00h hasta 4F08h. Para realizar programas que utilicen la norma, el lector deberá consultar dicha información. Sin embargo, se expone aquí un sencillo programa de demostración que recoge prácticamente todos los pasos necesarios para trabajar con un modo VESA.

     El primer paso consiste en detectar la presencia de soporte VESA en el sistema, tarea que realiza la función testvesa(). La función getbest256() se limita a buscar el modo de mayor resolución de 256 colores soportado por la tarjeta gráfica de ese equipo, barriendo sistemáticamente todos los modos de pantalla desde el "mejor" hasta el "peor". Para comprobar la existencia de un determinado modo gráfico, existe_modo() invoca también a la BIOS VESA. La función setmode() establece un modo gráfico VESA, devolviendo además dos informaciones interesantes: la dirección de memoria de la rutina de conmutación de bancos (ya veremos para qué sirve) y el segmento de memoria de vídeo, que será normalmente 0A000h. Finalmente, getinfo() devuelve información sobre cualquier modo gráfico. En principio, los modos utilizados por este programa de demostración son conocidos. Sin embargo, la lista de modos de vídeo puede ser mayor en algunas tarjetas, sobre todo en el futuro. Por tanto, un esquema alternativo podría consistir no en buscar ciertos modos concretos sino en ir recorriendo todos y elegir el que cumpla ciertas características de resolución o colores, entre todos los disponibles.

     De toda la información que devuelve getinfo() es particularmente interesante el número de bancos que necesita ese modo de vídeo. Hay que tener en cuenta que todos los modos de 256 colores de más de 320x200 ocupan más de 64 Kb de memoria. De esta manera, por ejemplo, una imagen de 640x480 con 256 colores utiliza unos 256 Kb de RAM, dividida en 4 bancos. En un momento dado, sólo uno de los 4 bancos puede estar direccionado en el segmento de memoria de vídeo. Para elegir el banco activo (más bien, el inicio de la ventana lógica sobre el total de la memoria de vídeo, aunque nuestro ejemplo es una simplificación) existe una función de la BIOS VESA o, mejor aún: podemos llamar directamente a una subrutina que realiza rápidamente esa tarea (sin tener que utilizar interrupciones) cuya dirección nos devolvió setmode(). De esta manera, el interface VESA evita que tengamos que hacer accesos directos al hardware. La rutina setbank() se limita a cargar el registro DX con el banco necesario antes de ejecutar el CALL. De todas maneras, esta modalidad de llamada no tiene por qué estar soportada por todas las BIOS VESA (en cuyo caso devuelven una dirección 0000:0000 para el CALL) aunque la inmensa mayoría, por fortuna, lo soportan.

     El único cometido de este programa de demostración es buscar el mejor modo de 256 colores, entre los normales de las SuperVGA, activarlo e ir recorriendo todos los bancos que componen la memoria de vídeo (excepto el último, que podría estar incompleto) para llenar la pantalla con bytes de valor 55h y 0AAh. Finalmente, antes de terminar, se imprime la resolución y cantidad de memoria consumida por ese modo.

/*********************************************************************
*                                                                    *
*  ESTANDAR GRAFICO VESA: EJEMPLO DE USO DEL MEJOR MODO DE 256       *
*                         COLORES EN CUALQUIER SUPERVGA.             *
*                                                                    *
*********************************************************************/


#include <dos.h>
#include <alloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#define M640x400x256   0x100   /* modos VESA normales de 256c */
#define M640x480x256   0x101
#define M800x600x256   0x103
#define M1024x768x256  0x105
#define M1280x1024x256 0x107


unsigned
  testvesa (void),             /* Detectar soporte VESA */
  existe_modo (unsigned),      /* Comprobar si un modo es soportado */
  getbest256 (void);           /* Obtener mejor modo de 256c */
void
  setbank (long, unsigned),    /* Conmutar banco de memoria */
  setmode (unsigned, long *,   /* Establecer modo VESA */
    unsigned *),
  getinfo (unsigned,           /* Obtener información del modo */
    unsigned *,
    unsigned *, unsigned *, unsigned *);


   /* DEMOSTRACION */

void main()
{
  struct REGPACK r;
  long
    ConmutaBanco;   /* dirección FAR del conmutador de banco */
  unsigned
    video_seg,      /* dirección del segmento de vídeo */
    far *pantalla,
    i, modo, max_x, max_y, vram, bancos, banco, limite;


  if (!testvesa()) {
    printf ("\nNecesario soporte VESA para este programa.\n");
    exit (1);
    }

  modo = getbest256();
  setmode (modo, &ConmutaBanco, &video_seg);
  getinfo (modo, &max_x, &max_y, &vram, &bancos);

  for (banco=0; banco<bancos; banco++) {
    setbank (ConmutaBanco, banco);             /* direccionar banco */
    pantalla=MK_FP(video_seg, 0);           /* normalmente 0xA000:0 */

    if (banco!=bancos-1)
        limite=32768;                  /* todo el segmento de 64 Kb */
      else
        limite=(vram-banco*64)*512;        /* palabras último banco */

    for (i=0; i<=limite; i++) *pantalla++=0x55AA;         /* pintar */
    }

  setbank (ConmutaBanco, 0);
  printf ("Modo de %dx%dx256 con %d Kb\n\n", max_x, max_y, vram);
}


   /* COMPROBAR QUE EXISTE SOPORTE VESA */

unsigned testvesa(void)
{
  struct REGPACK r;
  char far *mem;
  unsigned vesa;

  mem = farmalloc (256L);
  r.r_es = FP_SEG (mem);  r.r_di = FP_OFF (mem);
  r.r_ax = 0x4F00; intr (0x10, &r);
  mem[4]=0; if (strcmp (mem, "VESA")==0) vesa=1; else vesa=0;
  farfree (mem);
  return (vesa);
}
   /* BUSCAR EL MODO DE 256 COLORES DE MAYOR RESOLUCION */

unsigned getbest256 (void)
{
  if (existe_modo (M1280x1024x256)) return (M1280x1024x256);
  if (existe_modo (M1024x768x256)) return (M1024x768x256);
  if (existe_modo (M800x600x256)) return (M800x600x256);
  if (existe_modo (M640x480x256)) return (M640x480x256);
  if (existe_modo (M640x400x256)) return (M640x400x256);
  return (0);
}


   /* COMPROBAR LA EXISTENCIA DE UN MODO GRAFICO */

unsigned existe_modo (unsigned modo)
{
  struct REGPACK r;
  unsigned far *mem, far *array;

  mem = farmalloc (256L);
  r.r_es = FP_SEG (mem);  r.r_di = FP_OFF (mem);
  r.r_ax=0x4F00; intr (0x10, &r);
  array = MK_FP (mem[8], mem[7]);
  farfree (mem);

  while ((*array!=0xFFFF) && (*array!=modo)) array++;
  return (*array==modo);
}


   /* ESTABLECER UN MODO GRAFICO VESA Y DEVOLVER LA DIRECCION DE */
   /* LA RUTINA DE CONMUTACION DE BANCOS Y EL SEGMENTO DE VIDEO  */

void setmode (unsigned modo, long *conmutar, unsigned *videoseg)
{
  struct REGPACK r;
  long far *mem;

  mem = farmalloc (256L);
  r.r_es = FP_SEG (mem);  r.r_di = FP_OFF (mem);
  r.r_ax = 0x4F01; r.r_cx = modo; intr (0x10, &r);
  *conmutar = *(mem+3);
  *videoseg = *(mem+2);
  farfree (mem);
  r.r_ax=0x4F02; r.r_bx=modo; intr (0x10, &r);
}


   /* OBTENER INFORMACION SOBRE UN MODO GRAFICO VESA */

void getinfo (unsigned modo, unsigned *max_x, unsigned *max_y,
              unsigned *vram, unsigned *bancos)
{
  struct REGPACK r;
  unsigned far *mem;

  mem = farmalloc (256L);
  r.r_es = FP_SEG (mem);  r.r_di = FP_OFF (mem);
  r.r_ax = 0x4F01; r.r_cx = modo; intr (0x10, &r);

  *max_x = mem[9]; *max_y = mem[10];
  *vram = (unsigned) ( (long) mem[8] * mem[10] / 1024L);
  farfree (mem);
  *bancos = *vram / 64;
  if (*vram % 64) (*bancos)++;
}


   /* CONMUTAR DE BANCO CON LA MAXIMA VELOCIDAD */

void setbank (long direccion, unsigned banco)
{
  asm {
    mov  ax,4f02h
    mov  dx,banco
    mov  bx,0
    call dword ptr direccion
    }
}

7.5. - EL TECLADO.

     En este apartado se estudiará a fondo el funcionamiento del teclado en los ordenadores compatibles, a tres niveles: bajo, intermedio y alto. En el capítulo 12 se documenta el funcionamiento del hardware del teclado, interesante para ciertas aplicaciones concretas, aunque para la mayor parte de las labores de programación no es necesario llegar a tanto.

7.5.1. - BAJO NIVEL.

Funcionamiento general del teclado.

     Al pulsar una tecla se genera una interrupción 9 (IRQ 1) y el código de rastreo que identifica la tecla pulsada puede leerse en el puerto de E/S 60h, tanto en XT como en AT (se corresponde en los AT con el registro de salida del 8042); si se suelta la tecla se produce otra interrupción y se genera el mismo código de rastreo+128 (bit 7 activo). Por ejemplo, si se pulsa la 'A' se generará una INT 9 y aparecerá en el puerto del teclado (60h) el byte 1Eh, al soltar la 'A' se generará otra INT 9 y se podrá leer el byte 9Eh del puerto del teclado (véase la tabla del apéndice V, donde se listan los códigos de rastreo del teclado).

     Bajo el sistema DOS, el teclado del AT es idéntico al del XT en los códigos de rastreo y comportamiento, debido a la traducción que efectúa el 8042 en el primero. No obstante, el teclado del AT posee unos comandos adicionales para controlar los LEDs. En otros sistemas operativos (normalmente UNIX) el teclado del AT es programado para trabajar en modo AT y pierde la compatibilidad con el del XT (los códigos de rastreo son distintos y al soltar una tecla se producen dos interrupciones) pero bajo DOS esto no sucede en ningún caso y la compatibilidad es casi del 100%.

     Las teclas expandidas -las que han sido añadidas al teclado estándar de 83/84 teclas- tienen un comportamiento especial, ya que pueden generar hasta 4 interrupciones consecutivas (con un intervalo de unos 1,5 milisegundos, ó 3 ms en los códigos dobles que convierte en uno el 8042) con objeto de emular, aunque bastante mal, ciertas combinaciones de las teclas no expandidas; en general es bastante deficiente la emulación por hardware y el controlador del teclado (KEYB) tiene que tratarlas de manera especial en la práctica. Así, por ejemplo, cuando está inactivo NUM LOCK y se pulsa el cursor derecho expandido, se generan dos interrupciones consecutivas: en la primera aparece un valor 0E0h en el puerto del teclado que indica que es una tecla expandida; en la segunda interrupción aparece el valor 4Dh: el mismo que hubiera aparecido pulsando el '6' del teclado numérico. Sin embargo, si NUM LOCK está activo, en un teclado normal de 83 teclas hay que pulsar el '6' del teclado numérico junto con shift para que el cursor avance. Esto se simula en el teclado expandido por medio de 4 interrupciones: En las dos primeras puede aparecer la secuencia 0E0h-2Ah ó bien 0E0h-36h (2Ah y 36h son los códigos de las teclas shift normales): con esto se simula que está pulsado shift aunque ello no sea realmente cierto (las BIOS más antiguas ignoran la mayoría de los bytes mayores de 128, entre ellos el 0E0h); después aparecen otras dos interrupciones con los valores 0E0h-4Dh (con objeto de simular que se pulsa el '6' del teclado numérico): como el estado NUM LOCK está activo y en teoría se ha pulsado shift y el 6 del teclado numérico, el cursor avanza a la derecha; al soltar la tecla aparecerá la secuencia de interrupciones 0E0h-CDh-0E0h-0AAh, o en su defecto la secuencia equivalente 0E0h-CDh-0E0h-0B6h. En general, estos códigos shift fantasma dan problemas cuando las teclas de SHIFT adquieren otro significado diferente que el de conmutar el estado NUM LOCK, lo que sucede en casi todos los editores de texto de los modernos compiladores. Por ello, la BIOS o el KEYB tratan de manera especial las teclas expandidas; en los ordenadores más antiguos (con BIOS -o al menos su tecnología- anterior a Noviembre de 1985), si no se carga el KEYB, el teclado expandido funcionará mal, incluso en Estados Unidos -aunque las teclas estén bien colocadas-. Cuando se lee un valor 0E0h en una interrupción de teclado, el KEYB o la BIOS activan el bit 1 (el que vale 2) de la posición de memoria 0040h:0096h; en la siguiente interrupción ese bit se borra y ya se sabe que el código leído es el de una tecla expandida. El bit 0 de esa misma posición de memoria indica si se leyó un byte 0E1h en lugar de 0E0h (la tecla expandida «pause» o «pausa» es un caso especial -por fortuna, el único- y genera un prefijo 0E1h en vez del 0E0h habitual; de hecho, esta tecla no genera códigos al ser soltada, pero al pulsarla aparece la secuencia E1-1D-45-E1-9D-C5).

El buffer del teclado.

     Cuando se pulsa una tecla normal, la rutina que gestiona INT 9 deposita en un buffer dos bytes con su código ASCII y el código de rastreo, para cuando el programa principal decida explorar el teclado -lo hará siempre consultando el buffer-. Si el código ASCII depositado es cero ó 0E0h, se trata de una tecla especial (ALT-x, cursor, etc.) y el segundo byte indica cuál (son los denominados códigos secundarios). El código ASCII 0E0h sólo es generado en los teclados expandidos por las teclas expandidas (marcadas como 'Ex' en la tabla de códigos de rastreo del apéndice V), aunque las funciones estándar de la BIOS y del DOS que informan del teclado lo convierten en cero para compatibilizar con teclados no expandidos. Así mismo, el código ASCII 0F0h está reservado para indicar las combinaciones de ALT-tecla que no fueron consideradas inicialmente en el software de soporte de los teclados no expandidos, pero sí actualmente (de esta manera, las rutinas de la BIOS saben si deben informar de estas teclas o no según se esté empleando una función avanzada u obsoleta, para compatibilizar). En todo caso, las secuencias introducidas por medio de ALT-teclado_numérico llevan asociado un código de rastreo 0, por lo que el usuario puede generar los caracteres ASCII 0E0h y 0F0h sin que se confundan con combinaciones especiales; además, según IBM, si el código ASCII 0 va acompañado de un código de rastreo 3 los programas deberían interpretarlo como un auténtico código ASCII 0 (esta secuencia se obtiene con Ctrl-2) lo que permite recuperar ese código perdido en indicar combinaciones especiales.

     Es importante señalar que aunque el buffer (organizado como cola circular) normalmente está situado entre 0040h:001Eh y 0040h:003Eh, ello no siempre es así; realmente el offset del inicio y el fin del buffer respecto al segmento 0040h lo determinan las variables (tamaño palabra) situadas en 0040h:0080h y 0040h:0082h en todos los ordenadores posteriores a 1981. Por ello, la inmensa mayoría de las pequeñas utilidades de las revistas y los ejemplos de los libros son, por desgracia, incorrectos: la manera correcta de colocar un valor en el buffer -para simular, por ejemplo, la pulsación de una tecla- o extraerlo del mismo es comprobando adecuadamente los desbordamientos de los punteros teniendo en cuenta las variables mencionadas. El puntero al inicio del buffer es una variable tamaño palabra almacenada en la posición 0040h:001Ah y el fin otra ubicada en 0040h:001Ch. El siguiente ejemplo introduce un carácter de código ASCII AL y código de rastreo AH (es cómodo y válido hacer AH=0) en el buffer del teclado:

                 MOV     BX,40h          ; meter carácter AX en el buffer del teclado
                 MOV     DS,BX
                 CLI                     ; evitar conflictos con interrupciones
                 MOV     BX,DS:[1Ch]     ; puntero a la cola del buffer
                 MOV     CX,BX
                 ADD     CX,2            ; apuntar CX al siguiente dato
                 CMP     CX,DS:[82h]     ; más allá del fin del buffer
                 JB      no_desb
                 MOV     CX,DS:[80h]     ; inicio de la cola circular
     no_desb:    CMP     CX,DS:[1Ah]     ; puntero al inicio del buffer
                 JE      fin_rutina      ; ZF = 1 --> buffer lleno
                 MOV     DS:[BX],AX      ; introducir carácter ASCII (AL) en el buffer
                 MOV     DS:[1Ch],CX     ; actualizar puntero al final del buffer
                 CMP     SP,0            ; ZF=0 (SP siempre <> 0) --> buffer no lleno
     fin_rutina: STI

     El valor 0 para el código de rastreo es usado para introducir también algunos caracteres especiales, como las vocales acentuadas, etc., aunque por lo general no es demasiado importante su valor (de hecho, los programas suelen comprobar preferentemente el código ASCII; de lo contrario, en un teclado español y otro francés, ¡la tecla Z tendría distinto código!). No estaría de más en este ejemplo comprobar si las variables 40h:80h y 40h:82h son distintas de cero por si el ordenador es demasiado antiguo, medida de seguridad que de hecho toma el KEYB del DR-DOS (en estas máquinas además no es conveniente ampliar el tamaño del buffer cambiándolo de sitio, por ejemplo; lo normal es que esté entre 40h:1Eh y 40h:3Eh). En el apéndice V se listan los códigos secundarios: son el segundo byte (el más significativo) de la palabra depositada en el buffer del teclado por la BIOS o el KEYB.

Gestión de la interrupción del teclado.

     He aquí un ejemplo de una subrutina que intercepta la interrupción del teclado apoyándose en el controlador habitual y limitándose a detectar las teclas pulsadas, espiando lo que sucede pero sin alterar la operación normal del teclado:

     nueva_int9:  STI                      ; permitir interrupción periódica
                  PUSH   AX                ; preservar registros modificados
                  IN     AL,60h            ; código de la tecla pulsada
                  PUSHF                    ; preparar la pila para IRET
                  CALL   CS:anterior_int9  ; llamar a la INT 9 original
                  . . .                    ; hacer algo con esa tecla
                  POP    AX                ; restaurar registros modificados
                  IRET                     ; volver al programa principal

     Evidentemente, es necesario preservar y restaurar todos los registros modificados, como en cualquier otra interrupción hardware, dado que puede producirse en el momento más insospechado y no debe afectar a la marcha del programa principal, anterior_int9 es una variable de 32 bits que contiene la dirección de la interrupción del teclado antes de instalar la nueva rutina. Es necesario hacer PUSHF antes de llamar porque la subrutina invocada va a retornar con IRET y no con RETF. En general, el duo PUSHF/CALL es una manera alternativa de simular una instrucción INT.

     Si se implementa totalmente el control de una tecla en una rutina que gestione INT 9 -sin llamar al principio o al final al anterior gestor-, en los XT hay que enviar una señal de reconocimiento al teclado poniendo a 1 y después a 0 el bit 7 del puerto de E/S 61h (en AT no es necesario, aunque tampoco resulta perjudicial hurgar en ese bit en las máquinas fabricadas hasta ahora); es importante no enviar más de una señal de reconocimiento, algo innecesario por otra parte, de cara a evitar anomalías importantes en el teclado de los XT. Además, tanto en XT como AT hay que enviar en este caso una señal de fin de interrupción hardware (EOI) al 8259 (con un simple MOV AL,20h; OUT 20h,AL) al igual que cuando se gestiona cualquier otra interrupción hardware. El ejemplo anterior quedaría como sigue:

     nueva_int9:  STI
                  PUSH   AX
                  IN     AL,60h            ; código de la tecla pulsada
                  CMP    AL,tecla          ; ¿es nuestra tecla?
                  JNE    fin               ; no
                  PUSH   AX                ; vamos a «manchar» AX
                  IN     AL,61h
                  OR     AL,10000000b
                  OUT    61h,AL
                  AND    AL,01111111b
                  OUT    61h,AL            ; señal de reconocimiento enviada
                  POP    AX                ; AL = tecla pulsada
                  . . .                    ; gestionarla
                  MOV    AL,20h     
                  OUT    20h,AL            ; EOI al 8259
                  POP    AX                ; AX del programa principal
                  IRET                     ; volver al programa principal
     fin:         POP    AX                ; AX del programa principal
                  JMP    CS:anterior_int9  ; saltar al gestor previo de INT 9

     Como se puede observar, esta rutina gestiona una tecla y las demás se las deja al KEYB o la BIOS. Sólo en el caso de que la gestione él es preciso enviar una señal de reconocimiento y un EOI al 8259. En caso contrario, se salta al controlador previo a esta rutina con un JMP largo (segmento:offset); ahora no es preciso el PUSHF, como en el caso del CALL, por razones obvias. La instrucción STI del principio habilita las interrupciones, siempre inhibidas al principio de una interrupción -valga la redundancia-, lo que es conveniente para permitir que se produzcan más interrupciones -por ejemplo, la del temporizador, que lleva nada menos que la hora interna del ordenador-. En el ejemplo, el EOI es enviado justo antes de terminar de gestionar esa tecla; ello significa que mientras se la procesa, las interrupciones hardware de menor prioridad -todas, menos el temporizador- están inhibidas por mucho que se haga STI; el programador ha de decidir pues si es preciso enviar antes o no el EOI (véase la documentación sobre el controlador de interrupciones 8259 de los capítulos posteriores), aunque si la rutina es corta no habrá demasiada prisa.

     Es habitual en los controladores de teclado de AT (tanto la BIOS como el KEYB del MS-DOS) deshabilitar el teclado mientras se procesa la tecla recién leída, habilitándolo de nuevo al final, por medio de los comandos 0ADh y 0AEh enviados al 8042. Sin embargo, la mayoría de las utilidades residentes no toman estas precauciones tan sofisticadas (de hecho, el KEYB del DR-DOS tampoco). Lógicamente sólo se pueden enviar comandos al 8042 cuando el registro de entrada del mismo está vacío, lo que puede verificarse chequeando el bit 1 del registro de estado: no es conveniente realizar un bucle infinito que dejaría colgado el ordenador de fallar el 8042, de ahí que sea recomendable un bucle que repita sólo durante un cierto tiempo; en el ejemplo se utiliza la temporización del refresco de la memoria dinámica de los AT para no emplear más de 15 ms esperando al 8042. Además las interrupciones han de estar inhibidas en el momento crítico en que dura el envío del comando, aunque cuidando de que sea durante el menor tiempo posible:

     nueva_int9:  STI               ; breve ventana para interrupciones
                  PUSH   AX
                  CALL   espera
                  MOV    AL,0ADh
                  OUT    60h,AL     ; inhibir teclado
                  CALL   espera
                  IN     AL,60h     ; ¿tecla?
                  STI               ; permitir rápidamente interrupciones
                  ...               ; procesar tecla y enviar EOI al 8259
                  CALL   espera
                  MOV    AL,0AEh
                  OUT    60h,AL     ; desinhibir teclado
                  POP    AX
                  IRET              ; no merece la pena hacer STI

     espera:      PUSH   AX
                  PUSH   CX
                  MOV    CX,995     ; constante para 15 ms
                  CLI
     testref:     IN     AL,61h
                  AND    AL,10h     ; método válido solo en AT
                  CMP    AL,AH
                  JZ     testref
                  MOV    AH,AL
                  IN     AL,64h     ; registro de estado del 8042
                  TEST   AL,2       ; ¿buffer de entrada lleno?
                  LOOPNZ testref    ; así es
                  POP    CX
                  POP    AX
                  RET

7.5.2. - NIVEL INTERMEDIO.

Consulta de SHIFT, CTRL, ALT, etc (marcas de teclado).

     Estas teclas pueden ser pulsadas para modificar el resultado de la pulsación de otras. IBM no ha definido combinaciones con ellas (excepto CTRL-ALT, que sirve para reinicializar el sistema si se pulsa en conjunción con DEL) por lo que los programas residentes suelen precisamente emplear combinaciones de dos o más teclas de estas para activarse sin eliminar prestaciones al teclado; por defecto, si se pulsan dos o más teclas de estas la BIOS o el KEYB asignan prioridades y consideran sólo una de ellas: ALT es la tecla de mayor prioridad, seguida de CTRL y de SHIFT. Por otra parte, cabe destacar el hecho de que CTRL, ALT y SHIFT (al igual que Num Lock, Caps Lock, Scroll Lock e Ins) no poseen la característica de autorepetición de las demás teclas debido a la gestión que realiza la BIOS o el KEYB.

- Teclado no expandido.

     Llamando con AH=2 a la INT 16h (función 2 de la BIOS para el teclado), se devuelve en AL un byte con información sobre las teclas de control (SHIFT, CTRL, etc.) que es el mismo byte almacenado en 0040h:0017h (véase en el apéndice III el área de datos de la BIOS y las funciones de la BIOS para teclado). En 0040h:0018h, existe otro byte de información adicional, aunque no hay función BIOS para consultarlo en los teclados no expandidos, por lo que a menudo es necesario leerlo directamente. Por lo general es mejor emplear las funciones BIOS, si existen, que consultar directamente un bit, por razones de compatibilidad. Evidentemente, todas las funciones para teclados no expandidos pueden usarse también con los expandidos.

- Teclado expandido.

     A partir de 0040h:0096h hay otros bytes con información adicional y específica sobre el teclado del AT y los teclados expandidos: parte de esta información, así como de la de 0040:0018h, puede ser consultada en los teclados expandidos con la función 12h de la BIOS del teclado expandido, que devuelve en AX una palabra: en AL de nuevo el byte de 0040h:0017h y en AH otro byte mezcla de diversas posiciones de memoria con información útil (consultar funciones de la BIOS para teclado).

     Los bits de 40h:96h sólo son fiables si está instalado el KEYB del MS-DOS o 99% compatible; por ejemplo, el KEYB del DR-DOS 5.0/6.0 (excepto en modo KEYB US) no gestiona correctamente el bit de AltGr, aunque sí los demás bits. Antes de usar esta función conviene asegurarse de que está soportada por la BIOS o el KEYB instalado.

Lectura de teclas ordinarias.

     Con la función 0 de la INT 16h (AH=0 al llamar) se lee una tecla del buffer del teclado, esperando su pulsación si es preciso, y se devuelve en AX (AH código de rastreo y AL código ASCII); con la función 1 (AH=1 al llamar a INT 16h) se devuelve también en AX el carácter del buffer pero sin sacarlo (habrá que llamar de nuevo con AH=0), aunque en este caso no se espera a que se pulse una tecla (si el buffer estaba vacío se retorna con ZF=1 en el registro de estado). En los equipos con soporte para teclado expandido existen además las funciones 10h y 11h (correspondientes a la 0 y 1) que permiten detectar alguna tecla más (como F11 y F12) y diferenciar entre las expandidas y las que no lo son al no convertir los códigos 0E0h en 0, así como la función 5 (introducir caracteres en el buffer).

Combinaciones especiales de teclas.

- BREAK: se obtiene pulsando CTRL-PAUSE en los teclados expandidos (CTRL-SCROLL LOCK en los no expandidos). El controlador del teclado introduce una palabra a cero en el buffer e invoca la interrupción 1Bh. Los programas pueden interceptar esta interrupción para realizar ciertas tareas críticas antes de terminar su ejecución (ciertas rutinas del DOS, básicamente las de impresión por pantalla, detectan BREAK y abortan el programa en curso).

- PAUSE: se obtiene con dicha tecla o bien con CTRL-NUM LOCK (teclados no expandidos); provoca que el ordenador se detenga hasta que se pulse una tecla no modificadora (ni SHIFT, ni ALT, etc.), tecla que será ignorada pero servirá para abandonar la pausa. La pausa es interna a la rutina de control del teclado.

- PTR SCR (SHIFT con el (*) del teclado numérico en teclados no expandidos): vuelca la pantalla por impresora al ejecutar una INT 5.

- SYS REQ: al pulsarla genera una INT 15h (AX=8500h) y al soltarla otra INT 15h (AX=8501h).

- CTRL-ALT-DEL: el controlador del teclado coloca la palabra 1234h en 0040h:0072h (para evitar el chequeo de la memoria) y salta a la dirección 0FFFFh:0 reinicializando el ordenador.

- ALT-teclado_numérico: manteniendo pulsada ALT se puede teclear en el teclado numérico un valor numérico en decimal; al soltar ALT el código ASCII que representa se introducirá en el buffer. El controlador del teclado almacena en 40h:19h el número en proceso de formación: cada vez que llega un nuevo dígito multiplica el contenido anterior por 10 y se lo suma. Al soltar ALT, se hace 40h:19h=0.

Detección de soporte para teclado expandido.

     Normalmente no será necesario distinguir entre un teclado expandido o estándar, aunque en algunos casos habrá que tener en cuenta la posible pulsación de una tecla expandida y su código 0E0h asociado. En todo caso, el bit 4 de 0040h:0096h indica si el teclado es expandido; sin embargo es suicida fiarse de esto y es más seguro chequear por otros medios la presencia de funciones de la BIOS para teclado expandido antes de usarlas. En teoría, las BIOS de AT del 15 de noviembre de 1985 en adelante soportan las funciones 5, 10h y 11h; los de XT a partir del 10 de enero de 1986 soportan la 10h y la 11h. Sin embargo, en la práctica todas ellas normalmente están disponibles también en cualquier máquina más antigua si tiene instalado un KEYB eficiente, venga equipada o no con teclado expandido. Por ello, lo ideal es chequear la presencia de estas funciones por otros procedimientos. Por ejemplo: llamar a la función 12h con AL=0. Por desgracia, si la función no está implementada no devuelve el acarreo activo para indicar el error. Pero hay un truco: si el resultado sigue siendo AX=1200h, las funciones de teclado expandido no están soportadas. Esto se debe a que al no estar implementada la función, nadie ha cambiado el valor de AX: además, en caso de estar implementada no podría devolver 1200h porque ello significaría una contradicción entre AH y AL.

                  MOV    AX,1200h
                  INT    16h              ; invocar función teclado expandido
                  CMP    AX,1200h
                  JE     no_expandido     ; función no soportada
                  JMP    si_expandido     ; función soportada

Posibilidades avanzadas.

     La rutina de la BIOS del AT (y de los KEYB) que lee el buffer del teclado, cuando no hay teclas y tiene que esperar por las mismas ejecuta de manera regular la función 90h (AH=90h) de la interrupción 15h indicando una espera de teclado al llamar (AL=2). De esta manera, un hipotético avanzado sistema operativo podría aprovechar ese tiempo muerto para algo más útil. Así mismo, cuando un carácter acaba de ser introducido en el buffer del teclado, se ejecuta la función 91h para indicar que ya ha finalizado la entrada y hay caracteres disponibles. En general, estas características no son útiles en el entorno DOS y, por otra parte, han sido deficientemente normalizadas. Por ejemplo, al acentuar incorrectamente se generan dos caracteres (además del familiar pitido): el KEYB del MS-DOS sólo ejecuta una llamada a la INT 15h con la función 91h (pese a haber introducido dos caracteres en el buffer) y el de DR-DOS hace las dos llamadas...

     Lo que sí puede resultar más interesante es la función de intercepción de código del teclado: las BIOS de AT no demasiado antiguas y el programa KEYB, tras leer el código de rastreo en AL, activan el acarreo y ejecutan inmediatamente la función 4Fh de la INT 15h para permitir que alguien se de por enterado de la tecla y opcionalmente aproveche para manipular AL y simular que se ha pulsado otra tecla: ese alguien puede devolver además el acarreo borrado para indicar al KEYB que no continúe procesando esa tecla y que la ignore (en caso contrario se procedería a interpretarla normalmente). Para verificar si esta función está disponible en la BIOS basta con ejecutar la función 0C0h de la INT 15h que devuelve un puntero en ES:BX y comprobar que el bit 4 de la posición direccionada por ES:[BX+5] está activo. Alternativamente, puede verificarse la presencia del programa KEYB, lo que también permite emplear esta función en los PC/XT, aunque es más arriesgado. Para detectar la presencia del KEYB del MS-DOS en memoria basta con llamar a la interrupción 2Fh con AX=0AD80h y comprobar que devuelve AL=0FFh (esta función devuelve la versión del KEYB en BX y un puntero a un área de datos en ES:DI). [DR-DOS usa AX=0AD00h].

Consideraciones finales.

     Conviene señalar que los teclados de AT pueden generar interrupciones aunque no se pulsen teclas, normalmente para devolver una señal de reconocimiento cuando alguien les ha enviado algo -por ejemplo, la BIOS puede enviar un comando para cambiar los led's-; por ello, en el momento más insospechado puede producirse una INT 9 con el código de rastreo 0FAh, y la secuencia de interrupciones generada por las teclas que tienen asociado un led en los AT, debido a los códigos 0FAh, no es exactamente idéntica a la de los XT, aunque se trata de un detalle poco relevante -incluso para quienes pretendan hacer algo especial con estas teclas-. También es conveniente indicar que en los AT se puede leer puerto del teclado, para averiguar la última tecla pulsada o soltada, en casi cualquier momento -por ejemplo, periódicamente desde la interrupción del temporizador-. De todas formas, esta práctica tiene efectos secundarios debidos al mal diseño del software del sistema de los AT (tales como teclas shift que se enganchan, como si se quedaran pulsadas, numeritos que aparecen al pulsar los cursores expandidos, etc.). Además, en los XT sólo se obtendrá una lectura correcta inmediatamente después de producirse la interrupción del teclado y antes de enviar la correspondiente señal de reconocimiento al mismo -por tanto, no desde una interrupción periódica-. Todo esto desaconseja la lectura del puerto del teclado desde cualquier otro sitio que no sea INT 9, salvo contadas excepciones.

     Por último indicar que en los AT se puede modificar el estado de CAPS LOCK, NUM LOCK o SCROLL LOCK por el simple procedimiento de alterar el bit correspondiente en 40h:17h; dicho cambio se verá reflejado en los led's cuando el usuario pulse una tecla o el programa lea el teclado con cualquier función -en la práctica, de manera casi instantánea-. Sin embargo, para aplicar esta técnica es aconsejable verificar que se trata de un AT porque en los PC/XT el led -si existe- no se actualiza y pasa a indicar una información incorrecta. Realmente, en los XT, el control de los led lo lleva la propia circuitería del teclado de manera independiente al ordenador.

7.5.3. - ALTO NIVEL.

     El acceso al teclado a alto nivel puede realizarse a través de las funciones 1, 6, 7, 8 y 0Ah del DOS, considerándolo como dispositivo de entrada estándar. Algunas de estas funciones, si devuelven un 0, se trata de una tecla especial y la siguiente lectura devuelve el código secundario. El DOS utiliza las funciones BIOS.


7.6. - LOS DISCOS.

7.6.1. - ESTRUCTURA FISICA.

     Los discos son el principal medio de almacenamiento externo de los ordenadores compatibles. Pueden ser unidades de disco flexible, removibles, o discos duros -fijos-. Constan básicamente de una superficie magnética circular dividida en pistas concéntricas, cada una de las cuales se subdivide a su vez en cierto número de sectores de tamaño fijo. Como normalmente se emplean ambas caras de la superficie, la unidad más elemental posee en la actualidad dos cabezas de lectura/escritura, una para cada lado del disco. Los tres parámetros comunes a todos los discos son, por tanto: el número de cabezas, el de pistas y el de sectores. El término cilindro i hace referencia a la totalidad de las pistas i de todas las caras. Bajo DOS, los sectores tienen un tamaño de 512 bytes (tanto en discos duros como en disquetes) que es difícil cambiar (aunque no imposible). Los sectores se numeran a partir de 1, mientras que las pistas y las caras lo hacen desde 0. El DOS convierte esta estructura física de tres parámetros a otra: el número de sector lógico, que se numera a partir de 0 (los sectores físicos les denominaremos a partir de ahora sectores BIOS para distinguirlos de los sectores lógicos del DOS). Para un disco de SECTPISTA sectores BIOS por pista y NUMCAB cabezas, los sectores lógicos se relacionan con la estructura física por la siguiente fórmula:

Sector lógico = (sector_BIOS - 1) + cara * SECTPISTA + cilindro * SECTPISTA * NUMCAB - X1

     Es decir, el DOS recorre el disco empezando la pista 0 (la exterior, la más alejada del centro) y por la cara o cabezal 0, recorriendo todos los sectores; luego avanza una cara y recorre de nuevo todos los sectores; después pasa al siguiente cilindro... y repite de nuevo el proceso. De esta manera, varios cabezales podrían -hipotéticamente- leer bloques de información consecutivos simultáneamente. En los disquetes, X1=0, pero en los discos duros se resta un cierto factor de compensación X1, ya que éstos pueden estar divididos en varias particiones y la que usa el DOS puede no estar al principio del mismo. En general, un disco duro dividido en varias particiones de tipo DOS determina varias unidades lógicas de disco, cada una de las cuales dispone de un conjunto de sectores lógicos numerados a partir de 0 y un factor de compensación propio para la fórmula. Las siguientes fórmulas transforman sectores DOS en sus correspondientes BIOS:

                   Sector_BIOS = (sector MOD SECTPISTA) + 1
                   Cara = (sector / SECTPISTA) MOD  NUMCAB
                   Cilindro = sector / (SECTPISTA * NUMCAB) + X2

     Como la partición del DOS no suele empezar en el cilindro 0 (reservado en gran parte para la tabla de particiones) sino más bien en el 1 ó en otro posterior (cuando hay más particiones antes que la del DOS) será necesario añadir un cierto valor adicional de compensación X2 a la última fórmula para calcular el cilindro efectivo; esto es así porque en la práctica las particiones suelen empezar y acabar ocupando cilindros enteros y exactos (aunque en realidad, y dada la arquitectura de la tabla de partición, podrían empezar y acabar no sólo en un determinado cilindro sino también en cierto sector y cara del disco, pero no es frecuente). X1 y X2 se obtienen consultando e interpretando la tabla de particiones o el sector de arranque.

7.6.2. - CABEZA 0. PISTA 0. SECTOR 1.

     El primer sector físico de todos los discos contiene información especial (el sector_BIOS 1 del cilindro 0 y cabezal 0). Tanto en disquetes como en discos duros, contiene un pequeño programa que se encarga de poner en marcha el ordenador: es el sector de arranque de los disquetes, o bien el código de la tabla de particiones de los discos duros. En este último caso, ese programa realiza una tarea muy sencilla: consulta la tabla de particiones ubicada en ese mismo sector, determina cuál es la partición activa y dónde empieza y acaba; a continuación carga el sector lógico 0 de esa partición (sector de arranque) y lo ejecuta. En los disquetes no existe este paso intermedio: el sector físico 0 del disquete, en terminos absolutos, es ya el sector de arranque y no el de partición. Esto es así porque los disquetes contienen poca información y son baratos, no siendo preciso particionarlos para compartirlos con varios sistemas operativos. El programa ubicado en el sector de arranque busca el fichero oculto del sistema IBMBIO.COM o IO.SYS, lo carga y le entrega el control. El programa contenido en este fichero cargará a su vez IBMDOS.COM o MSDOS.SYS, el cual a su vez cargará finalmente el intérprete de comandos (normalmente, COMMAND.COM).

* Formato de la tabla de partición de los discos duros:

160;    Esta tabla comienza en un offset 1BEh del sector (al principio está el código ejecutable); cada partición de las 4 posibles ocupa 16 bytes; al final de las cuatro está la marca 0AA55h, ubicada en el offset 1FEh, que indica que la tabla es válida. Los 16 bytes que la forman se interpretan como indica el cuadro:

+-----------------------------------------------------------------------------+
| byte 0: 0 para partición inactiva, 80h en la de arranque.                   |
| byte 1: cabeza donde comienza la partición.                                 |
| byte 2: bits 0 al 5: sector de inicio de la partición; 6, 7: parte alta del |
|         número de cilindro.                                                 |
| byte 3: parte baja del número de cilindro de inicio de la partición.        |
| byte 4: tipo de partición, las más comunes son 0: No usada; 1: DOS-12 (FAT  |
|         12 bits); 4: DOS-16 (FAT 16 bits); 5: DOS Extendida; 6:BIGDOS (más  |
|         de 32Mb); 7: OS/2 HPFS ó WinNT NTFS; 0Ah: OS/2 Boot Manager;  0Bh:  |
|         32-bit FAT Win95 (0Ch con LBA);  0Eh y 0Fh  (como 06 y 05 pero con  |
|         LBA);  81h Linux; 82h Linux swap; 83h: Linux native; 0A5h: FreeBSD  |
|         o BSD/386; 0F2h: partición secundaria (no estudiada en este libro). |
| byte 5: cabeza donde termina la partición.                                  |
| byte 6: bits 0 al 5: sector de fin de la partición; 6, 7: parte alta del    |
|         número de cilindro.                                                 |
| byte 7: parte baja del número de cilindro de fin de la partición.           |
| bytes 8 al 11: Doble palabra que indica el sector relativo (en todo el      |
|         disco) en que comienza la partición, expresado en sectores.         |
| bytes 12 al 15: Doble palabra con el tamaño de esa partición en sectores.   |
+-----------------------------------------------------------------------------+
                                               Formato de la TABLA DE PARTICIÓN

     Habitualmente, las particiones suelen empezar en el segundo cabezal del cilindro 0, con lo que toda la primera pista física del disco duro está vacía. Lugar ideal para virus, algunos fabricantes han utilizado esta interesante característica para mejorar el arranque, colocando una falsa tabla de partición que muestre un menú en pantalla y cargue después la partición de verdad, permitiendo también más de 4 particiones. Sin embargo, estas maniobras suelen reducir la compatibilidad. Existen también código de particiones sofisticado que permite seleccionar una de las 4 particiones manteniendo pulsada una tecla en el arranque, sin tener que andar ejecutando FDISK para seleccionar la partición activa... ¡lo que se puede hacer con 400 bytes de código!. Realmente, la arquitectura global de las particiones de un equipo (en particular si tiene más de 4, una mezcla de sistemas operativos y/o varios discos duros), puede llegar a ser compleja: practíquese con un buen editor de disco para aprender más (ej. el DISKEDIT de las Norton Utilities o las PC-Tools).

     Las particiones extendidas llevan su propio sector de partición adicional, en el que no hay código de programa sino, en su lugar, una lista de dispositivos. Hay dos entradas por cada dispositivo: la primera indica el tipo (1-FAT12, 4-FAT16); la segunda entrada apunta al siguiente dispositivo (caso de existir) o es 0 (no hay más dispositivos). El DOS 4.0 y posteriores eliminaron la limitación de los 32 Mb en las particiones y el software actual, ya actualizado, no da problemas con los discos de más de 32 Mb. Por ello, en discos de más de 32 ó 40 Mb lo normal es instalar DOS 4.0 ó superior.

* Formato del sector de arranque:

     En el sector de arranque, además del sencillo programa de puesta en marcha del sistema, hay cierta información útil acerca de las características del disco o partición. Los primeros 3 bytes no son significativos: contienen el código de operación de una instrucción JMP que salta a donde realmente comienza el código, aunque conviene que dicha instrucción de salto esté al principio del sector de arranque para que algunos sistemas validen dicho sector (es válido un salto corto seguido de NOP o un salto completo de 3 bytes). A partir del cuarto (offset 3) se puede encontrar la información válida. En el sector de arranque del disquete está contenido el BPB (Bios Parameter Block) que analizaremos más tarde.

+-----------------------------------------------------------------------------------------------------------------------+
|  offset  3 (8 bytes):     Identificación del sistema (ej., "IBM  3.3")                                                |
|  offset 11 (1 palabra):   Bytes por sector, ej. 512.                                                                  |
|  offset 13 (1 byte):      Sectores por cluster (ej. 2)                                                                |
|  offset 14 (1 palabra):   Sectores reservados al principio (1 en diquettes)                                           |
|  offset 16 (1 byte):      Número de copias de la FAT (2 normalmente)                                                  |
|  offset 17 (1 palabra):   Número de entradas al directorio raíz (112 en discos de 360 Kb)                             |
|  offset 19 (1 palabra):   Número total de sectores del disco (0 en discos de más de 32 Mb)                            |
|  offset 21 (1 byte):      Byte de tipo de disco (véase tabla más adelante)                                            |
|  offset 22 (1 palabra):   Número de sectores ocupados por cada FAT                                                    |
|  offset 24 (1 palabra):   Número de sectores por pista                                                                |
|  offset 26 (1 palabra):   Número de cabezas (2 en disquetes de doble cara)                                            |
|  offset 28 (2 palabras):  Número de sectores especiales reservados. Nota: sólo se debe considerar la primera mitad de |
|                           esta doble palabra en versiones del sistema 3.30 o anteriores (no hay problemas con DR-DOS, |
|                           que en todas sus versiones, hasta la 6.0 incluida, es un DOS 3.31).  El valor de este campo |
|                           depende de la posición relativa que ocupe la partición dentro del disco duro (será 0 en los |
|                           disquetes),  este valor ha de sumarse al del número de sector del DOS antes de traducirlo a |
|                           un número de sector de la BIOS.                                                             |
|  offset 32 (2 palabras):  Número total de sectores del disco en discos de más de 32 Mb (esta información sólo debe    |
|                           obtenerse de aquí si la palabra ubicada en el offset 19 es cero).                           |
| offset 36 (1 byte):       Número de unidad física (a partir del DOS 4.0).                                             |
| offset 37 (1 byte):       Reservado.                                                                                  |
| offset 38 (1 byte):       valor 29h desde DOS 4.0 (marca de validación que indica que los bytes ubicados desde el     |
|                           offset 36 al offset 61 están definidos).                                                    |
| offset 39 (2 palabras):   Número de serie del disco (a partir de DOS 4.0).                                            |
| offset 43 (11 bytes):     Título del disco (desde DOS 4.0); por defecto se inicializa con "NO NAME    ", aunque tanto |
|                           el DOS 4.0 como el 5.0 y 6.X siguen empleando además las tradicionales etiquetas de volumen.|
| offset 54 (8 bytes):      Sistema de ficheros (a partir de DOS 4.0): puede ser "FAT12   " o  "FAT16   ".              |
+-----------------------------------------------------------------------------------------------------------------------+
                                                                                           Formato del SECTOR DE ARRANQUE

     El byte del tipo de disco (offset 21) intenta identificar el tipo de disco, aunque no lo consigue en muchos casos dada la ilógica utilización que se ha hecho de él. La recomendación es hacer lo que viene haciendo el DOS desde la 3.30: no hacer caso de lo que dice este byte para identificar los discos. La única excepción tal vez sea el valor 0F8h que identifica a los dispositivos no removibles:

+---------------------------------------------------------------------+
| 0FEh - discos de 5¼-160 Kb (1 cara, 8 sectores/pista, 40 pistas)    |
| 0FFh - discos de 5¼-320 Kb (2 caras, 8 sectores/pista, 40 pistas)   |
| 0FCh - discos de 5¼-180 Kb (1 cara, 9 sectores/pista, 40 pistas)    |
| 0FDh - discos de 5¼-360 Kb (2 caras, 9 sectores/pista, 40 pistas)   |
| 0F9h - discos de 5¼-1,2 Mb (2 caras, 15 sectores/pista, 80 pistas)  |
| 0F9h - discos de 3½-720 Kb (2 caras, 9 sectores/pista, 80 pistas)   |
| 0F8h - discos duros y algunos virtuales                             |
| 0F0h - discos de 3½-1,44 Mb (2 caras, 18 sectores/pista, 80 pistas) |
| 0F0h - discos de 3½-2,88 Mb (2 caras, 36 sectores/pista, 80 pistas) |
| 0F0h - restantes formatos de disco                                  |
+---------------------------------------------------------------------+
                                                        Tipos de Discos
7.6.3. - LA FAT.

     Después del sector de arranque, aparecen en el disco una serie de sectores que constituyen la Tabla de Localización de Ficheros (File Alocation Table o FAT). Consiste en una especie de mapa que indica qué zonas del disco están libres, cuáles ocupadas, dónde están los sectores defectuosos, etc. Normalmente hay dos copias consecutivas de la FAT (véase el offset 16 del sector de arranque), ya que es el área más importante del disco de la que dependen todos los demás datos almacenados en él. No deja de resultar extraño que ambas copias de la FAT estén físicamente consecutivas en el disco: si accidentalmente se estropeara una de ellas (por ejemplo, rayando con un bolígrafo el disco) lo más normal es que la otra también resultara dañada. En general, muchos programas de chequeo de disco no se molestan en verificar si ambas FAT son idénticas (empezando por algunas versiones de CHKDSK). Por otra parte, hubiera sido mejor elección haberla colocado en el centro del disco: dada la frecuencia de los accesos a la misma, de cara a localizar los diferentes fragmentos de los ficheros, ello mejoraría notablemente el tiempo de acceso medio. Aunque cierto es que los cachés de disco y los buffers del config.sys pueden hacer casi milagros... a costa de memoria.

     Antes de seguir adelante, conviene hacer un pequeño paréntesis y explicar el concepto de cluster: un cluster es la unidad mínima de información a la que accede el DOS, desde el punto de vista lógico. Normalmente consta de varios sectores (ver offset 13 del sector de arranque): dos en un disquete de 360 Kb, uno en un disquete de alta densidad, y entre 4 y 16 -normalmente- en un disco duro. El disco queda dividido, por tanto, en un cierto número de clusters. La FAT es realmente un mapa que contiene 12 ó 16 bits -como veremos- por cada cluster, indicando su estado:

          cluster libre: valor 0
          cluster defectuoso: valores 0FF7h (ó 0FFF7h).
          cluster no utilizable: valores 0FF5 al 0FF6h (ó 0FFF5 al 0FFF6h).
          último cluster del fichero: valor 0FF8 al 0FFFh (ó 0FFF8h al 0FFFFh).
          otro valor: puntero al siguiente cluster del fichero.

     Los ficheros en disco no siempre ocupan posiciones contiguas: normalmente están más o menos fragmentados debido a que se aprovechan los huecos dejados por otros ficheros borrados, de ahí el auge de los programas que compactan los discos con objeto de acelerar el acceso a los datos. Por tanto, cada fichero consta de un cluster inicial indicado en la entrada del directorio -como se verá- que inicia una cadena tan larga como la longitud del mismo (expresada en clusters), existiendo normalmente un valor 0FFFh ó 0FFFFh en el último cluster para señalar el final (del 0FF8h al 0FFEh y del 0FFF8h al 0FFFEh no se emplean). Consultando la FAT se puede determinar la ubicación de los fragmentos en que están físicamente divididos los ficheros en los discos, así como qué zonas están aún disponibles y cuáles son defectuosas en el mismo. Los cluster se numeran a partir de 2, ya que las dos primeras entradas en la FAT están reservadas para el sistema. Los clusters hacen referencia exclusiva a la zona de datos: el área que va detrás del sector de arranque, la FAT y el directorio. Por ello, en un disquete de 360 Kb, con clusters de 1 Kb y 354 Kb libres para datos, hay 354 clusters (numerados de 2 a 355) y los 6 Kb misteriosos que faltan son el sector de arranque, las dos FAT y -como veremos después- el directorio raíz. Puede ser válida, por ejemplo, la siguiente FAT de 12 bits habiendo un fichero A que ocupe los clusters 2, 3, 5 y 6:

  Elemento de la FAT     Valor                        Interpretación
           0              FFD          El disco es de tipo 0FDh (despreciar restantes bits)
           1              FFF          Entrada no utilizada
           2              003          El siguiente cluster del fichero A es el 3
           3              005          El siguiente cluster del fichero A es el 5
           4              FF7          Cluster defectuoso
           5              006          El siguiente cluster del fichero A es el 6
           6              FFF          Este es el último cluster del fichero A
           7              013          El siguiente cluster del fichero B es el 013
          ...             ...

     Como se ve, el primer byte de la primera entrada a la FAT es inicializado con el mismo valor que el byte de tipo de disco del sector de arranque. Los restantes bits de las dos primeras entradas suelen estar todos a 1. Para determinar el número de clusters del disco, ha de restarse del número total de sectores la cifra correspondiente al número de sectores reservados (normalmente 1 en los disquetes, correspondiente al sector de arranque), los que ocupa la FAT y los empleados por el directorio raíz (que se verá más adelante); a continuación se divide ese número de sectores de datos resultante por el número de sectores por cluster.

     El hecho de emplear FAT's de 12 bits es debido a que con menos bits (ej., un byte) sólo podría haber unos 250 clusters en el disco. En un disco de 1,2 Mb ello significaría que la unidad mínima de información sería 1200/250 = 5 Kb: el fichero más pequeño (de 1 byte) ocuparía ¡5 Kb!. Empleando FAT's de 16 bits se podrían hacer clusters incluso de tamaño menor que el sector (menos de 512 bytes), aprovechando más el espacio del disco. Sin embargo, ello haría que la propia FAT ocupase demasiado espacio en el disco. Por ello, en los disquetes se emplean FAT's de 12 bits (1 byte y medio): para un programa en código máquina ello no ralentiza los cálculos (aunque al ser humano no se le de muy bien trabajar con medios bytes). En la práctica, se toman palabras de 16 bits y se desprecian los 4 bits más significativos en los clusters pares y los 4 menos significativos en los impares.

     A continuación se listan dos rutinas que permiten acceder a una FAT de 12 bits previamente cargada en memoria, con objeto de consultar o modificar alguna entrada. Evidentemente, después habrá que volver a grabar la FAT en disco, tantas veces como copias de la misma existan en éste. Las rutinas necesitan que la FAT esté completamente cargada en memoria, lo cual no es un requerimiento demasiado costoso, habida cuenta de que no puede ocupar más de 4085 * 1,5 = 6128 bytes.

    ; ************ Escribir un elemento en una FAT de 12 bits
                  ; Entrada: AX    = posición de dicho elemento
                  ;          DS:BX = FAT completamente cargada en memoria
                  ;          DX    = nuevo valor de dicho elemento

    poke_fat       PROC
                   PUSH  AX                ; preservar registros
                   PUSH  BX
                   PUSH  DX
                   ADD   BX,AX             ; BX = BX + cluster
                   SHR   AX,1              ; AX = cluster / 2
                   PUSHF                   ; CF = 1 si impar
                   ADD   BX,AX             ; BX = BX + cluster * 1,5
                   MOV   AX,[BX]           ; AX = palabra con dato 12 bits
                   POPF
                   JC    poke_fat_imp
                   AND   AX,1111000000000000b  ; preservar la otra entrada
                   JMP   poke_fat_ok
    poke_fat_imp:  AND   AX,0000000000001111b  ; preservar la otra entrada
                   PUSH  CX
                   MOV   CL,4
                   SHL   DX,CL             ; colocarlo: 4 bits a la izda
                   POP   CX
    poke_fat_ok:   OR    AX,DX             ; «mezclar»
                   MOV   [BX],AX           ; nuevo valor en la FAT
                   POP   DX
                   POP   BX
                   POP   AX
                   RET                     ; retorno sin alterar registros
    poke_fat       ENDP

    ; ************ Leer un elemento de una FAT de 12 bits
                  ; Entrada: AX    = posición de dicho elemento
                  ;          DS:BX = FAT completamente cargada en memoria
                  ; Salida:  DX    = valor de dicho elemento

    peek_fat       PROC
                   PUSH  AX                ; preservar registros
                   PUSH  BX
                   ADD   BX,AX             ; BX = BX + cluster
                   SHR   AX,1              ; AX = cluster / 2
                   PUSHF                   ; CF = 0 si par
                   ADD   BX,AX             ; BX = BX + cluster * 1,5
                   MOV   DX,[BX]
                   POPF
                   JNC   peek_fat_par
                   PUSH  CX
                   MOV   CL,4
                   SHR   DX,CL             ; DX=DX/16: si DX=xyz0, DX=0xyz
                   POP   CX
    peek_fat_par:  AND   DH,00001111b      ; borrar posible dígito izdo
                   POP   BX
                   POP   AX
                   RET                     ; retornar sólo DX modificado
    peek_fat       ENDP

     Tal vez, en futuros disquetes de elevada capacidad sea necesario pasar a una FAT de 16 bits, aparecida con el DOS 3.0, que es la usada por todos los discos duros excepto el de 10 Mb del XT original de IBM. Con una FAT de 12 bits el nº de cluster más alto posible es 4085, que se corresponde con un disco de 4084 clusters (numerados de 2 a 4085). En principio, no existe ninguna manera sencilla de averiguar el tipo de FAT de un disco, ya que el fabricante olvidó incluir un byte de identificación al efecto. La documentación publicada es contradictoria en las diversas fuentes que he consultado, y en todas es por desgracia incorrecta (unos dicen que la FAT 16 comienza a partir de 4078 clusters, otros que a partir de 4086, otros confunden el número de clusters con el número más alto de cluster...). Sin embargo, todas las versiones del DOS comprobadas (MS-DOS 3.1, 3.3, 4.0, 5.0 y DR-DOS 5.0 y 6.0) operan con una FAT de 16 bits en discos de 4085 clusters (inclusive) en adelante; esto es, a partir de 4086 como número de cluster más alto. Esto puede verificarse fácilmente creando discos virtuales con 4084/4085 clusters, copiando algunos ficheros y mirando la FAT con algún programa de utilidad (a simple vista se distingue si las entradas son de 12 ó 16 bits). Por desgracia, salvo en MS-DOS 3.3 y en DR-DOS 6.0, los comandos CHKDSK del sistema consideran erróneamente que los discos de 4085, 4086 y 4087 clusters ¡poseen una FAT de 12 bits!, lo cual resulta además completamente absurdo, dado que 4087 (0FF7h) es la marca de cluster defectuoso en una FAT de 12 bits y ¡en ningún caso podría ser un número de cluster cualquiera!. Sin embargo, pese a este problema de CHKDSK, los discos con más de 4084 clusters han de ser diseñados con una FAT de 16 bit, ya que es mucho más grave tener problemas con el DOS que con CHKDSK. Otra solución es procurar no crear discos de ese número crítico de clusters, o confiar que el usuario no ejecute el casi olvidado CHKDSK sobre ellos. Por fortuna, los discos normales no están por ahora en la frontera crítica entre la FAT de 12 y la de 16 bits, aunque con los discos virtuales sí se pueden crear unidades con esos tamaños críticos: la casi totalidad de los discos virtuales del mercado tienen problemas en estos casos. En algunos discos duros se puede determinar también el tipo de FAT consultando la tabla de particiones, aunque no es el método más conveniente. Debe tener en cuenta el lector que manipular una FAT sin conocer su tipo supone destrozar la información almacenada en el disco. Sin embargo, tampoco hay que tener tanto miedo: lo que sí puede resultar peligroso es llegar al extremo de preguntar al usuario el tipo de FAT...

     Ahora puede surgir la pregunta: si la FAT mantiene una cadena que indica cómo está distribuido un fichero en el disco, ¿dónde se almacena el inicio de esa cadena, esto es, la primera entrada en la FAT del fichero?.

7.6.4.- EL DIRECTORIO RAÍZ.

     Inmediatamente después de la FAT y su(s) réplica(s) de seguridad viene el directorio raíz. Detrás de éste ya vienen los clusters conteniendo la información del disco propiamente dicha. El directorio consta de 32 bytes por cada fichero/subdirectorio (los subdirectorios no son más que un tipo especial de fichero). En los discos de 360 Kb, por ejemplo, el directorio se extiende a lo largo de 7 sectores (3584 bytes = 112 entradas como máximo). El tamaño y ubicación del directorio pueden obtenerse del sector de arranque, como se vio al principio. La información almacenada en los 32 bytes es la siguiente:

+-----------------------------------------------------------+ +--------------------------------------------------+
| offset 0 (8 bytes):   Nombre del fichero                  | | bit 0: activo si el fichero es de sólo lectura   |
| offset 8 (3 bytes):   Extensión del nombre del fichero    | | bit 1: activo si el fichero es oculto            |
| offset 11 (1 byte):   Byte de atributos                   | | bit 2: activo si el fichero es de sistema        |
| offset 12 (10 bytes): Reservado (PASSWORD cifrada DR-DOS) | | bit 3: activo si esa entrada de directorio es    |
| offset 22 (2 bytes):  Hora*2048 + minutos*32 + segundos/2 | |        la etiqueta de volumen                    |
| offset 24 (2 bytes):  (año-1980)*512 + mes*32 + día       | | bit 4: activo si es un subdirectorio             |
| offset 26 (2 bytes):  Primera entrada en la FAT           | | bit 5: bit de archivo usado por BACKUP y RESTORE |
| offset 28 (4 bytes):  Tamaño del fichero en bytes         | | bits 6,7: no utilizados                          |
+-----------------------------------------------------------+ +--------------------------------------------------+
                                        ENTRADA DE DIRECTORIO                                    BYTE DE ATRIBUTOS

     En el byte de atributos, varios bits pueden estar activos a un tiempo. El atributo de sistema no tiene un significado en particular, es una reliquia heredada del CP/M (los ficheros ocultos del sistema lo tienen activo). En un mismo disco sólo puede haber una entrada con el bit 3 activo; además, en este caso se interpretan el nombre y la extensión como un único conjunto de 11 caracteres. Las entradas de tipo subdirectorio (bit 4 del byte de atributos activo) tienen un valor cero en el campo de tamaño (offset 28): el tamaño de un fichero subdirectorio está determinado por el número de entradas que ocupa en la FAT (en la práctica, esto sucede con cualquier otro fichero, aunque si no es de directorio en el offset 28 esta información se indica con precisión de bytes).

     El nombre del fichero puede comenzar por 0E5h, lo que indica que el fichero que estuvo ahí ha sido borrado. Si empieza por 2Eh (código ASCII del punto (.)) ó por 2Eh, 2Eh (dos puntos consecutivos) se trata de una entrada que referencia a un fichero subdirectorio.

7.6.5. - LOS SUBDIRECTORIOS.

     Como hemos visto, un subdirectorio en principio puede ser una simple entrada del directorio raíz. El subdirectorio, físicamente, es a su vez un fichero un tanto especial: contiene datos binarios ... que son nada más y nada menos que otras entradas de directorio para otros ficheros, de 32 bytes como siempre. Dentro de cada subdirectorio hay al menos dos entradas especiales: un fichero con un nombre punto (.) que referencia al propio subdirectorio -que así puede autolocalizarse- y otro con doble punto (..) que referencia al directorio padre -del que cuelga- siendo posible, gracias a ello, retroceder cuanto se desee por el árbol de directorios sin necesidad de que todos los caminos partan del raíz. Si la primera entrada en la FAT del fichero (..) es un 0, quiere decir que ese subdirectorio cuelga del raíz, de lo contrario apuntará al primer cluster del fichero subdirectorio padre.

     El tamaño de un fichero subdirectorio es ilimitado -sin exceder, evidentemente, la capacidad del disco-. Por ello, en un subdirectorio puede haber una gran cantidad de ficheros (muchos más de 112 ó 500) sin problemas. Cada fichero que se crea en un subdirectorio aumenta el tamaño del fichero subdirectorio en 32 bytes. Por ello, en un disco de 360 Kb (354 Kb libres) se puede crear un subdirectorio y en él se pueden introducir, en caso extremo, 11326 ficheros (más el (.) y el (..)) de tamaño cero que paradójicamente llenarían el disco (recordar que cada entrada al directorio ocupa 32 bytes). Normalmente nadie suele cometer esos excesos. Si en un subdirectorio había demasiados ficheros y se borra una buena parte de los mismos, el tamaño del fichero subdirectorio debería reducirse, pero en la práctica el DOS no se ocupa de estas pequeñeces, habida cuenta de que los ficheros subdirectorio son unos pequeños islotes en el gran océano disco (los usuarios más tacaños siempre pueden optar por crear un nuevo subdirectorio y mover todos los ficheros a él, borrando el anterior para recuperar el espacio libre).

     Considerando el nombre completo de un fichero, con toda la trayectoria de directorios, el proceso a seguir para localizarlo en el disco es ir recorriendo los ficheros subdirectorio de uno en uno, hasta llegar al fichero subdirectorio donde está registrado el fichero y, en la posición correspondiente, obtener su punto de entrada en la FAT.

     Dicho sea de paso, tal vez sea una pena que el disco no conste de un único «fichero raíz» privilegiado de directorio, que podríamos denominar «subdirectorio raíz». Ello permitiría también un número ilimitado de entradas (en vez de 112, 224, etc.) y sería más lógico que una ristra de sectores. Sin embargo, esta peculiar circunstancia también aparece en otros sistemas operativos, como el UNIX. Sus motivos tendrá.

7.6.6. - EL BPB Y DPB.

     El BPB (Bios Parameter Block) es una estructura de datos que contiene información relativa a la unidad de disco. El BPB es una pieza vital en los controladores de dispositivo de bloques, como veremos en un futuro capítulo, por lo que a continuación se expone su contenido (idéntico a una parte del sector 0):

+---------------------------------------------------------------------------+
|  offset  0    DW bytes_por_sector                                         |
|  offset  2    DB sectores_por_cluster                                     |
|  offset  3    DW sectores_reservados_al_comienzo_del_disco                |
|  offset  5    DB número_de_FATs                                           |
|  offset  6    DW número_de_entradas_en_el_directorio_raíz                 |
|  offset  8    DW número_total_de_sectores (0 con nº de sector de 32 bits) |
|  offset 10    DB byte_descriptor_de_medio                                 |
|  offset 11    DW numero_de_sectores_por_FAT                               |
|  -- A partir del DOS 3.0:                                                 |
|  offset 13    DW sectores_por_pista                                       |
|  offset 15    DW número_de_cabezas                                        |
|  offset 17    DD número_de_sectores_ocultos                               |
|  -- A partir del DOS 4.0 (más bien DOS 3.31)                              |
|  offset 21    DD número_de_sectores (unidades con direccionamiento de     |
|                  sector de 32 bits)                                       |
|  offset 25    DB 6 DUP (?)    (6 bytes no documentados)                   |
|  offset 31    DW número_de_cilindros                                      |
|  offset 33    DB tipo_de_dispositivo                                      |
|  offset 34    DW atributos_del_dispositivo                                |
+---------------------------------------------------------------------------+

     El DOS convierte internamente el BPB en DPB (Drive Parameter Block), una estructura similar con más información útil. Para obtener el DPB de una unidad determinada, puede utilizarse la función 32h del DOS, Get Drive Parameter Block (indocumentada); la cadena de DPBs del DOS puede recorrerse a partir del primer DPB (obtenido con la función 52h del DOS, Get List of Lists, también indocumentada).

7.6.7. - LA BIOS Y LOS DISQUETES.

     Resulta interesante conocer el comportamiento de la BIOS en relación a los disquetes, ya que las aplicaciones desarrolladas bajo DOS de una u otra manera habrán de cooperar con la BIOS por razones de compatibilidad (o al menos respetar ciertas especificaciones). El funcionamiento del disquete se controla a través de funciones de la INT 13h, aunque esta interrupción por lo general acaba llamando a la INT 40h que es quien realmente gestiona el disco en las BIOS modernas de AT. Las funciones soportadas por esta interrupción son: reset del sistema de disco (reset del controlador de disquetes, envío del comando specify y recalibramiento del cabezal), consulta del estado del disco (obtener resultado de la última operación), lectura, escritura y verificación de sectores, formateo de pistas, obtención de información del disco y las disqueteras, detección del cambio de disco, establecimiento del tipo de soporte para formateo... algunas de estas últimas funciones no están disponibles en las máquinas PC/XT. La BIOS se apoya en varias variables ubicadas en el segmento 40h de la memoria. Estas variables son las siguientes (para más información, consultar el apéndice al final del libro):

Byte 40h:3EhEstado de recalibramiento del disquete. Esta variable indica varias cosas: si se ha producido una interrupción de disquete, o si es preciso recalibrar alguna disquetera debido a un reset anterior.
Byte 40h:3FhEstado de los motores. En esta variable se indica, además del estado de los motores de las 4 posibles disqueteras (si están encendidos o no), la última unidad que fue seleccionada y la operación en curso sobre la misma.
Byte 40h:40hCuenta para la detención del motor. Este byte es decrementado por la interrupción periódica del temporizador; cuando llega a 0 todos los motores de las disqueteras (realmente, el único que estaba girando) son detenidos. Dejar el motor girando unos segundos tras la última operación evita tener que esperar a que el motor acelere antes de la siguiente (si esta llega poco después).
Byte 40h:41hEstado de la última operación: se actualiza tras cada acceso al disco, indicando los errores producidos (0 = ninguno).
Bytes 40h:42hA partir de esta dirección, 7 bytes almacenan el resultado de la última operación de disquete o disco duro. Se trata de los 7 bytes que devuelve el NEC765 tras los principales comandos.
Byte 40h:8BhControl del soporte (AT). Esta variable almacena, entre otros, la última velocidad de transferencia seleccionada.
Byte 40h:8FhInformación del controlador de disquete (AT). Se indica si la unidad soporta 80 cilindros (pues sí, la verdad) y si soporta varias velocidades de transferencia.
Byte 40h:90hEstado del soporte en la unidad A. Se indica la velocidad de transferencia a emplear en el disquete introducido en esta unidad, si precisa o no saltos dobles del cabezal (caso de los disquetes de 40 cilindros en unidades de 80), y el resultado de los intentos de la BIOS (la velocidad puede ser correcta o no, según se haya logrado determinar el tipo de soporte).
Byte 40h:91hLo mismo que el byte anterior, pero para la unidad B.
Byte 40h:92hEstado del soporte en la unidad A al inicio de la operación.
Byte 40h:93hEstado del soporte en la unidad B al inicio de la operación.
Byte 40h:94hNúmero de cilindro en curso en la unidad A.
Byte 40h:95hNúmero de cilindro en curso en la unidad B.

     Además de estas variables, la BIOS utiliza también una tabla de parámetros apuntada por la INT 1Eh. Los valores para programar ciertas características del FDC según el tipo de disco pueden variar, aunque algunos son comunes. Esta tabla determina las principales características de operación del disco. Dicha tabla está inicialmente en la ROM, en la posición 0F000h:0EFC7h de todas las BIOS compatibles (prácticamente el 100%), aunque el DOS suele desviarla a la RAM para poder actualizarla. El formato de la misma es:

byte 0:Se corresponde con el byte 1 del comando 'Specify' del 765, que indica el step rate (el tiempo de acceso cilindro-cilindro, a menudo es 0Dh = 3 ó6 ms) y el head unload time (normalmente, 0Fh = 240 ó480 ms).
byte 1:Es el byte 2 del comando 'Specify': los bits 7..1 indican el head load time (normalmente 01h = 2 ó4 ms) y el bit 0 suele estar a 0 para indicar modo DMA.
byte 2:Tics de reloj (pulsos de la interrupción 8) que transcurren tras el acceso hasta que se para el motor.
byte 3:Bytes por sector (0=128, 1=256, 2=512, 3=1024).
byte 4:Sectores por pista.
byte 5:Longitud del GAP entre sectores (normalmente 2Ah en unidades de 5¼ y 1Bh en las de 3½).
byte 6:Longitud de sector (ignorado si el byte 3 no es 0).
byte 7:Longitud del GAP 3 al formatear (80 en 5¼ y 3½-DD, 84 en 5¼-HD y 108 en 3½-HD).
byte 8:Byte de relleno al formatear (normalmente 0F6h).
byte 9:Tiempo de estabilización del cabezal en ms.
byte 10:Tiempo de aceleración del motor (en unidades de 1/8 de segundo).

     El tiempo de estabilización del cabezal es el tiempo que hay que esperar tras mover el cabezal al cilindro adecuado, hasta que éste se asiente, con objeto de garantizar el éxito de las operaciones futuras; esta breve pausa es establecida en 25 milisegundos en la BIOS del PC original, aunque otras BIOS y el propio DOS suelen bajarlo a 15. Del mismo modo, el tiempo de aceleración del motor (byte 10) es el tiempo que se espera a que el motor adquiera la velocidad de rotación correcta, nada más ponerlo en marcha. En cualquier caso, es norma general intentar tres veces el acceso a disco (con resets de por medio) hasta considerar que un error es real. En general, pese a estos valores usuales, la flexibilidad del sistema de disco es extraordinaria y suele responder favorablemente con unos altísimos niveles de tolerancia en las temporizaciones. Una excepción quizá la constituye el valor de GAP empleado al formatear, al ser un parámetro demasiado importante.

7.6.8. - DISQUETES FLOPTICAL 3½ DE 20 MB.

     Las unidades que soportan estos disquetes, que también admiten los de 720K y 1.44M (aunque a menudo no los de 2.88M) trabajan con controladoras SCSI e incorporan una BIOS propia para dar soporte a estos dispositivos. El secreto de estos disquetes está en el posicionamiento óptico del cabezal, lo que permite elevar notablemente el número de pistas. Por ejemplo, las unidades de 20 Mb parecen estar equipadas con 753 cilindros y 27 sectores/pista. Aunque en el sector de arranque indica que posee 251 cilindros y 6 cabezales, el sentido común nos permite deducir que esto no puede ser así. Lo de los 27 sectores por pista parece indicar que la velocidad de transferencia de estos disquetes es exactamente un 50% mayor que la de los convencionales de 1.44M (750 Kbit/seg frente a 500 Kbit/seg).

     El FORMAT del DOS 5.0 y posteriores puede formatear los disquetes floptical, pero lo hace a bajo nivel, con lo que tarda cerca de 30-45 minutos en inicializarlos. Como ya vienen formateados de fábrica, en realidad basta con añadirles un sector de arranque e inicializar la FAT y el directorio raíz. También se puede verificar la superficie magnética para detectar posibles sectores defectuosos. Los programas de utilidad que acompañan estas unidades realizan todas estas tareas en unos 4 minutos. El tipo de FAT asignado puede ser seleccionado por el usuario (12 ó 16 bits), así como otros parámetros técnicos (tamaño de clusters, etc.).

     Las tarjetas controladoras suelen permitir un cierto grado de flexibilidad, de cara a seleccionar la letra de unidad que se desea asignar al floptical. Configurándolo como A: se puede incluso arrancar desde un disquete de éstos.

7.6.9. - EJEMPLO DE ACCESO AL DISCO A ALTO NIVEL.

     Se puede acceder a varios niveles, siendo mejor el más alto por razones de compatibilidad:

     1) Programando directamente el controlador de disquetes/disco duro para acceder a sectores físicos.
     2) Llamando a la BIOS para leer cierto sector, de cierta cara y cierto cilindro.
     3) Llamando al DOS para leer un sector lógico determinado en la unidad que se le indique.
     4) Llamando al DOS para acceder a un fichero por su nombre y ruta.

     El método (1) es apropiado para realizar formateos especiales en sistemas de protección anticopia; el (2) es útil para acceder a otras particiones de otros sistemas operativos o a disquetes formateados por otros sistemas operativos; las opciones (3) y (4) son las más cómodas e interesantes. En general, en la medida de lo posible es conveniente no bajar del nivel (3); de lo contrario se pierde la posibilidad de acceder a ciertas unidades (por ejemplo, un disco virtual no existe en absoluto para la BIOS).

     A continuación se muestra un programa de ejemplo que solicita el nombre de un fichero y lo visualiza por pantalla, cargándolo por fragmentos y apoyándose en las funciones del DOS que se comentan en el apéndice que resume las funciones del sistema operativo. Paradójicamente, el acceso se realiza a alto nivel pese a tratarse de un programa en ensamblador. Como se puede observar, al final del programa se definen dos buffers de datos de 80 y 2048 bytes. Si no se desea que estos buffers alarguen el tamaño del programa ejecutable, pueden definirse de la siguiente manera:

                    fichnom     EQU     $
                    buffer          EQU     $+80

     Sin embargo, si se procede de esta última manera convendría asegurarse primero de que existen 2128 bytes de memoria libres tras el código del programa, ya que de esta manera el DOS no realiza la comprobación por nosotros (se limita a cargar cualquier programa que quepa en memoria). De todas maneras, normalmente suele haber más de 2128 bytes libres de memoria tras cargar cualquier programa... Conviene hacer notar que si en lugar de DUP (0) se coloca DUP (?), el linkador de Borland (TLINK 3.0), al contrario que el LINK de Microsoft, TAMPOCO reserva espacio efectivo para esas variables. Esto sólo sucede, lógicamente, cuando el DUP (?) está al final del programa y no hay nada más a continuación -ni más código ni datos que no sean DUP (?)-.

    ; ********************************************************************
    ; *                                                                  *
    ; *  MIRA.ASM  -  Utilidad para visualizar ficheros de texto.        *
    ; *                                                                  *
    ; ********************************************************************

    mira           SEGMENT
                   ASSUME CS:mira, DS:mira

                   ORG   100h              ; programa de tipo .COM
    inicio:
                   LEA   DX,input_txt      ; mensaje
                   MOV   AH,9              ; función de impresión
                   INT   21h               ; llamar al DOS
                   LEA   DX,fichnom        ; dirección para el «input»
                   MOV   BYTE PTR [fichnom],60  ; no más de 60 caracteres
                   MOV   AH,10             ; función de entrada de teclado
                   INT   21h               ; llamar al DOS
                   MOV   BL,[fichnom+1]    ; longitud efectiva tecleada
                   MOV   BH,0              ; en BX
                   ADD   BX,OFFSET fichnom ; apuntar al final
                   MOV   BYTE PTR [BX+2],0 ; poner un cero al final

                   LEA   DX,fichnom+2      ; offset a cadena ASCIIZ nombre
                   MOV   AL,0              ; modo de lectura
                   MOV   AH,3Dh            ; función para abrir fichero
                   INT   21h               ; llamar al DOS
                   JC    error             ; CF=1 --> error
                   MOV   handle,AX         ; código de acceso al fichero

    trocito:       MOV   BX,handle         ; código de acceso al fichero
                   MOV   CX,2048           ; número de bytes a leer
                   LEA   DX,buffer         ; dirección del buffer
                   MOV   AH,3Fh            ; función para leer del fichero
                   INT   21h               ; llamar al DOS
                   JC    error             ; CF=1 --> error
                   MOV   CX,AX             ; bytes leídos realmente
                   JCXZ  cerrar            ; no hay nada que imprimir
                   PUSH  AX                ; preservarlos
                   LEA   BX,buffer         ; imprimir buffer ...
    imprime:       MOV   DL,[BX]           ; carácter a carácter
                   MOV   AH,2              ; ir llamando al servicio 2 del
                   INT   21h               ; DOS para imprimir en pantalla
                   INC   BX                ; siguiente carácter
                   LOOP  imprime           ; acabar caracteres
                   POP   AX                ; recuperar nº de bytes leídos
                   CMP   AX,2048           ; ¿leidos 2048 bytes?
                   JE    trocito           ; sí, leer otro trocito más

    cerrar:        MOV   BX,handle         ; código de acceso al fichero
                   MOV   AH,3Eh            ; cerrar fichero
                   INT   21h               ; llamar al DOS
                   JC    error             ; CF = 1 --> error
                   INT   20h               ; fin del programa

    error:         LEA   DX,fallo_txt      ; mensaje de error
                   MOV   AH,9              ; función de impresión
                   INT   21h               ; llamar al DOS
                   CMP   handle,0          ; ¿fichero abierto?
                   JNE   cerrar            ; sí: cerrarlo
                   INT   20h               ; fin del programa

    ; ------------ datos y variables

    handle         DW    0            ; handle de control del fichero
    input_txt      DB    13,10,"Nombre del fichero: $"
    fallo_txt      DB    13,10,"*** Error ***",13,10,10,"$"
    fichnom        DB    80 DUP (0)   ; buffer para leer desde el teclado
    buffer         DB    2048 DUP (0) ;   "     "     "     "  el disco

    mira           ENDS
                   END   inicio

7.6.10. - EJEMPLO DE ACCESO AL DISCO A BAJO NIVEL.

     El programa de ejemplo desarrollado requiere un adaptador VGA ya que utiliza el modo de 640 por 480 con 16 colores para obtener una representación gráfica de alta calidad del contenido del disco, en lugar de la tradicional y pobre representación habitual en modo texto. Además, se reprograman los registros de paleta y el DAC de la VGA para elegir colores más atractivos. El funcionamiento del programa se basa en acceder a la FAT y crear una imagen gráfica de la misma. Para ello, calcula cuantos puntos de pantalla debe trazar por cada cluster de disco (utiliza una ventana de 636x326 = 207336 puntos). Aunque este número no es entero, por razones de eficiencia se trabaja con fracciones para evitar el empleo de coma flotante. Muchas veces el ensamblador no es suficiente para asegurar la velocidad: la primera versión del programa tardaba 18 segundos en dibujar un mapa en un 386-25, con una rutina escrita en su mayor parte en ensamblador. Tras mejorar el algoritmo y optimizar el código en la zona crítica donde se trazan los puntos, se redujo a menos de 0,66 segundos el tiempo necesario (¡314000 puntos por segundo a 25 MHz!). Para leer los sectores del disco no se utiliza la función absread() del Borland C 2.0, ya que posee una errata por la que falla con unidades de más de 32767 clusters. En su lugar, una rutina en ensamblador se encarga de llamar a la interrupción 25h teniendo cuidado con el tipo de disco (particiones de más de 32 Mb o de menos de esa cantidad). La FAT se lee en una matriz, ya que no ocupa más de 128 Kb en el peor de los casos. Se lee de tres veces para evitar que en un sólo acceso a disco, vía INT 25h, se rebasen los 64 Kb permitidos si la FAT ocupa más de 64 Kb (el puntero al buffer apunta al inicio del segmento al ser de tipo HUGE). A continuación, se interpreta la FAT (según sea de 12 ó 16 bits) y se crea otra matriz de tamaño equivalente al número de clusters del disco. Esta última matriz -que indica los clusters libres, ocupados y defectuosos- es la que se volcará en pantalla adecuadamente. El programa también imprime información general sobre el disco, utilizando la función de impresión de la BIOS. Se imprime todo lo necesario antes de dibujar ya que para trazar los puntos es preciso programar el adaptador de vídeo de una manera diferente a la que emplea la BIOS (por razones de velocidad): después de ejecutar prepara_punto(), la BIOS no es capaz de escribir en pantalla. La inclusión de ensamblador en los programas en C se verá con detalle en un capítulo posterior.

7.7. - EL PSP.

     Como se vio en el capítulo anterior, antes de que el COMMAND.COM pase el control al programa que se pretende ejecutar, se crea un bloque de 256 bytes llamado PSP (Program Segment Prefix), cuya descripción detallada se da a continuación.
 
     La dirección del PSP en los programas COM viene determinada por la de cualquier registro de segmento (CS=DS=ES=SS) nada más comenzar la ejecución del mismo. Sin embargo, en los programas de tipo EXE sólo viene determinada por DS y ES. En cualquier caso, existe una función del DOS para obtener la dirección del PSP, cuyo uso recomienda el fabricante del sistema en aras de una mayor compatibilidad con futuras versiones del sistema operativo. La función es la 62h y está disponible a partir del DOS 3.0.

     En la siguiente información, los campos del PSP que ocupen un byte o una palabra han de interpretarse como tal; los que ocupen 4 bytes deben interpretarse en la forma segmento:offset. En negrita se resaltan los campos más importantes.

- offsets 0 al 1: palabra 20CDh, correspondiente a la instrucción INT 20h. En CP/M se podía terminar un programa ejecutando un salto a la posición 0. En MS-DOS, un programa COM ¡también!.

- offsets 2 al 3: una palabra con la dirección de memoria (segmento) del último párrafo disponible en el sistema. Teniendo en cuenta dónde acaba la memoria y el punto en que está cargado nuestro programa, no es difícil saber la memoria que queda libre. Supuesto ES apuntando al PSP:

                   MOV     AX,ES:[2]   ; párrafo más alto disponible
                   MOV     CX,ES       ; segmento del PSP
                   SUB     AX,CX       ; AX = párrafos libres
                   MOV     CX,16
                   MUL     CX          ; DX:AX bytes libres

- offset 4: no utilizado.

- offsets 5 al 9: salto al despachador de funciones del DOS (en CP/M se ejecutaba un CALL 5, el MS-DOS ¡también lo permite!). No es recomendable llamar al DOS de esta manera. Los PSP creados por la función 4Bh en algunas versiones del DOS no tienen correctamente inicializado este campo.

- offsets 0Ah al 0Dh: contenido previo del vector de terminación (INT 22h).

- offsets 0Eh al 11h: contenido previo del vector de Ctrl-Break (INT 23h).

- offsets 12h al 15h: contenido previo del vector de manipulación de errores críticos (INT 24h).

- offsets 16h al 17h: segmento del PSP padre.

- offsets 18h al 2Bh: tabla de trabajo del sistema con los ficheros (Job File Table o JFT) : un byte por handle (a 0FFh si cerrado; los primeros son los dispositivos CON, NUL, ... y siempre están abiertos). Sólo hasta 20 ficheros (si no, véase offset 32h).

- offsets 2Ch al 2Dh: desde el DOS 2.0, una palabra que apunta al segmento del espacio de entorno, donde se puede encontrar el valor de variables de entorno tan interesantes como PATH, COMSPEC,... y hasta el nombre del propio programa que se está ejecutando en ese momento y el directorio de donde se cargó (no siempre es el actual; el programa pudo cargarse, apoyándose en el PATH, en cualquier otro directorio diferente del directorio en curso). Véase el capítulo 8 para más información de las variables de entorno.

- offsets 2Eh al 31h: desde el DOS 2.0, valor de SS:SP en la entrada a la última INT 21h invocada.

- offsets 32h al 33h: desde el DOS 3.0, número de entradas en la JFT (por defecto, 20).

- offsets 34h al 37h: desde el DOS 3.0, puntero al JFT (por defecto, PSP:18h). Desde el DOS 3.0 puede haber más de 20 ficheros abiertos a la vez gracias a este campo, que puede ser movido de sitio. Sin embargo, es sólo a partir del DOS 3.3 cuando en un PSP hijo (por ejemplo, creado con la función EXEC) se copia la información de más que de los 20 primeros ficheros, si hay más de 20. Se puede saber si un fichero es remoto (en la MS-net) comprobando si el byte de la JFT está comprendido entre 80h-0FEh, aunque es mejor siempre acceder antes a las funciones del DOS.

- offsets 38h al 3Bh: desde el DOS 3.0, puntero al PSP previo (por defecto, 0FFFFh:0FFFFh en las versiones del DOS 3.x); es utilizado por SHARE en el DOS 3.3.

- offsets 3Ch al 3Fh: no usados hasta ahora.

- offsets 40h al 41h: desde el DOS 5.0, versión del sistema a devolver cuando se invoca la función 30h.

- offsets 42h al 47h: no usados hasta ahora.

- offset 48h: desde Windows 3, el bit 0 está activo si la aplicación es no-Windows.

- offsets 49h al 4Fh: no usados hasta ahora.

- offsets 50h al 52h: código de INT 21h/RETF. No recomendado hacer CALL PSP:5Ch para llamar al DOS.

- offsets 53h al 5Bh: no usados hasta ahora.

- offsets 5Ch al 7Bh: apuntan a los dos FCB's (File Control Blocks) usados antaño para acceder a los ficheros (uno en 5Ch y el otro en 6Ch). Es una reliquia en desuso, y además este área no se inicializa si el programa es cargado en memoria superior con el comando LOADHIGH del MS-DOS 5.0 y posteriores, por lo que no conviene usarlo ni siquiera para captar parámetros, al menos en programas residentes -susceptibles de ser instalados con LOADHIGH-. Si se utiliza el primer FCB se sobreescribe además el segundo.

- offsets 7Ch al 7Fh: no usados hasta ahora.

- offsets 80h al 0FFh: es la zona donde aparecen los parámetros suministrados al programa. El primer byte indica la longitud de los parámetros, después vienen los mismos y al final un retorno de carro (ASCII 13) que es un tanto redundante -a fin de cuentas, ya se sabe la longitud de los parámetros-. Ese retorno de carro, sin embargo, no «se cuenta» en el byte que indica la longitud. Téngase en cuenta que no son mayusculizados automáticamente (están tal y como los tecleó el usuario), y además los parámetros pueden estar separados por uno o más espacios en blanco o tabuladores (ASCII 9).

     En general, comprobar los valores que recibe el PSP cuando se carga un programa es una tarea que se realiza de manera sencilla con el programa DEBUG/SYMDEB. Para ello basta una orden tal como "DEBUG PROGRAMA.COM HOLA /T": al entrar en el DEBUG (o SYMDEB) basta con hacer «D 0» para examinar el PSP de PROGRAMA. Para ver los parámetros (HOLA /T en el ejemplo) se haría «D 80».


7.8. - EL PROCESO DE ARRANQUE DEL PC.

     Al conectar el PC éste comienza a ejecutar código en los 16 últimos bytes de la memoria (dirección 0FFFF0h en PC/XT, 0FFFFF0h en 286 y 0FFFFFFF0h en 386 y superiores). En esa posición de memoria, en la que hay ROM, existe un salto a donde realmente comienza el código de la BIOS. Este salto suele ser de tipo largo (segmento:offset) con objeto de cargar en CS un valor que referencie al primer mega de memoria, donde también está direccionada la ROM (todos los microprocesadores arrancan en modo real). El programa de la ROM inicialmente se limita a chequear los registros de la CPU, primero el de estado y luego los demás (en caso de fallo, se detiene el sistema). A continuación, se inicializan los principales chips (interrupciones, DMA, temporizador...); se detecta la configuración del sistema, accediendo directamente a los puertos de E/S y también consultando los switches de configuración de la placa base (PC/XT) o la CMOS (AT); se establecen los vectores de interrupción y se chequea la memoria RAM si el contenido de la dirección 40h:72h es distinto de 1234h (el contenido de la memoria es aleatorio inicialmente). Por último, se entrega el control sucesivamente a las posibles memorias ROM adicionales que existan (la de la VGA, el disco duro en XT, etc.) con objeto de que desvíen los vectores que necesiten. Al final del todo, se intenta acceder a la primera unidad de disquetes: si no hay disquete, se procede igualmente con el primer disco duro (en los PC de IBM, si no hay disco duro ni disquete se ejecuta la ROM BASIC). Se carga el primer sector en la dirección 0:7C00h y se entrega el control a la misma. Ese sector cargado será el sector de arranque del disquete o la tabla de partición del disco duro (el código que contiene se encargará de cargar el sector de arranque del propio disco duro, según la partición activa). El programa del sector de arranque busca el fichero del sistema IO.SYS (o IBMBIO.COM en PC-DOS) y lo carga, entregándole el control (programa SYSINIT) o mostrando un mensaje de error si no lo encuentra. Las versiones más modernas del DOS no requieren que IO.SYS ó IBMBIO.COM comience en el primer cluster de datos del disco, aunque sí que se encuentre en el directorio raíz. Puede que también se cargue al principio el fichero MSDOS.SYS (o IBMDOS.COM) o bien puede que el encargado de cargar dicho fichero sea el propio IO.SYS o IBMBIO.COM. El nombre de los ficheros del sistema depende de si éste es PC-DOS (o DR-DOS) o MS-DOS. Teniendo en cuenta que el MS-DOS y el PC-DOS son prácticamente idénticos desde la versión 2.0 (PC-DOS funciona en máquinas no IBM), la existencia de las dos versiones se explica sólo por razones comerciales. El fichero IO.SYS o IBMBIO.COM en teoría debería ser entregado por el vendedor del ordenador: este fichero provee soporte a las diferencias específicas que existen en el hardware de las diferentes máquinas. Sin embargo, como todos los PC compatibles son casi idénticos a nivel hardware (salvo algunas de las primeras máquinas que intentaron imitar al PC) en la práctica es el fabricante del DOS (Microsoft o Digital Research) quien entrega dicho fichero. Ese fichero es como una capa que se interpone entre la BIOS del PC y el código del sistema operativo contenido en MSDOS.SYS o IBMDOS.COM. Este último fichero es el encargado de inicializar los vectores 20h-2Fh y completar las tablas de datos internas del sistema. También se interpreta el CONFIG.SYS para instalar los controladores de dispositivo que den soporte a las características peculiares de la configuración del ordenador. Finalmente, se carga el intérprete de mandatos: por defecto es COMMAND.COM aunque no hay razón para que ello tenga que ser así necesariamente (pruebe el lector a poner en CONFIG.SYS la orden SHELL C:\DOS\QBASIC.EXE; aunque si se abandona QBASIC algunas versiones modernas del DOS son aún capaces de cargar el COMMAND por sus propios medios, después del error pertinente, en vez de bloquear el ordenador). En las versiones más recientes del DOS, el sistema puede residir en memoria superior o en el HMA: en ese caso, el proceso de arranque se complica ya que es necesario localizar el DOS en esa zona después de cargar los controladores de memoria.


7.9. - FORMATO DE LAS EXTENSIONES ROM.

     Las memorias ROM que incorporan diversas tarjetas (de vídeo, controladoras de disco duro, de red) pueden estar ubicadas en cualquier punto del área 0C0000h-0FFFFFh. La ROM BIOS del ordenador se encarga de ir recorriéndolas y entregándolas el control durante la inicialización, con objeto de permitirlas desviar vectores de interrupción y ejecutar otras tareas propias de su inicialización.

     La BIOS recorre este área en incrementos de 2 Kb buscando la signatura 55h, 0AAh: estos dos bytes consecutivos tienen que aparecer al principio para considerar que ahí hay una ROM. El tercer byte, que va detrás de éstos, indica el tamaño de esa extensión ROM en bloques de 512 bytes. Por razones de seguridad, se realiza una suma de comprobación de toda la extensión ROM y si el resultado es 0 se considera una auténtica ROM válida. En ese caso, se entrega el control (con un CALL entre segmentos) al cuarto byte de la extensión ROM. Ahí habrá de estar ubicado el código de la extensión ROM (habitualmente un salto a donde realmente comienza). Al final del todo, el código de la extensión ROM debe devolver de nuevo el control a la BIOS del sistema, por medio de un retorno lejano (RETF).

     El código almacenado en estas extensiones ROM puede contener accesos directos al hardware y llamadas a la ROM BIOS del sistema. Sin embargo, conviene recordar que el DOS no ha sido cargado aún y no se pueden emplear sus funciones. La ventaja de las extensiones ROM es que aumentan las prestaciones del sistema antes de cargar el DOS. El inconveniente es que en otros sistemas operativos (UNIX, etc.) que emplean el modo protegido, estas memorias ROM en general no son accesibles. En la actualidad, con la disponibilidad de memoria superior bajo DOS, resulta más conveniente que las extensiones de hardware vengan acompañadas de drivers para DOS, WINDOWS, OS/2,... que no con una ROM, mucho más difícil de actualizar. Un ejemplo de memoria ROM podría ser:

          bios      DB     55h, 0AAh
                    DB     32          ; 16 Kb de ROM
                    JMP    inicio
                    ...
                    ...
          fin_bios  ...               ; la suma de todos los bytes = 0

     Los primeros ordenadores de IBM incorporaban una memoria ROM con el BASIC. El COMMAND de aquellas versiones del DOS (desconozco si el actual también) era capaz de ejecutar comandos internos definidos en estas ROM, al igual que un CLS o un DIR, vamos. El formato era, por ejemplo:

     bios_basic     DB     55h, 0AAh
                    DB     64         ; 32 Kb de ROM-BASIC
                    JMP    inicio
                    DB     5          ; longitud del siguiente comando
                    DB     "BASIC"
                    JMP    basic      ; salto al comienzo del BASIC
                    DB     6          ; longitud del siguiente comando
                    DB     "BASICA"
                    JMP    basic      ; salto al comienzo (el mismo del BASIC)
                    DB     0          ; no más comandos
          basic     ...
                    ...
          fin_bios  ...               ; la suma de todos los bytes = 0

     Si esto le parece una tontería al lector, es que no ha visto lo que vamos a ver ahora. Resulta que también se pueden almacenar programas en BASIC (el código fuente, aunque tokenizado) en las BIOS. ¡Sí, un listado en ROM!:

       mortgagebas  DB     55h, 0AAh
                    DB     48         ; 24 Kb de contabilidad
                    RETF              ; nada que hacer
                    DB     0AAh, 55h  ; esto es un listado BASIC
                    ...               ; aquí, el programa
       fin_bios     ...               ; la suma de todos los bytes = 0

7.10. - FORMATO FÍSICO DE LOS FICHEROS EXE.

     Los ficheros EXE poseen una estructura en el disco distinta de su imagen en memoria, al contrario que los COM. Es conveniente conocer esta estructura para ciertas tareas, como por ejemplo la creación de antivirus -y también la de virus-, que requiere modificar un fichero ejecutable ya ensamblado o compilado. Analizaremos como ejemplo de programa EXE el del capítulo 6, que reúne las principales características necesarias para nuestro estudio. Se comentarán los principales bytes que componen el fichero ejecutable en el disco (1088 en total). A continuación se lista un volcado del fichero ejecutable a estudiar. Todos los datos están en hexadecimal (parte central) y ASCII (derecha); la columna de la izquierda es el offset del primer byte de la línea. Donde hay puntos suspensivos, se repite la línea de arriba tantas veces como sea preciso:

0000  4D 5A 40 00 03 00 01 00-20 00 00 00 FF FF 04 00  MZ@..... .......
0010  00 02 00 00 00 00 02 00-3E 00 00 00 01 00 FB 30  ........>.....{0
0020  6A 72 00 00 00 00 00 00-00 00 00 00 00 00 00 00  jr..............
0030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 05 00  ................
0040  02 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
 .    .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .                   
01F0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0200  0D 0A 54 65 78 74 6F 20-61 20 69 6D 70 72 69 6D  ..Texto a imprim
0210  69 72 0D 0A 24 00 00 00-00 00 00 00 00 00 00 00  ir..$...........
0220  1E 33 C0 50 B8 00 00 8E-D8 BA 00 00 B4 09 CD 21  .3@P8...X:..4.M!
0230  CB 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  K...............
0240  70 69 6C 61 70 69 6C 61-70 69 6C 61 70 69 6C 61  pilapilapilapila
 .    .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .                   

     Los ficheros EXE constan de una cabecera, seguida de los segmentos de código, datos y pila; esta cabecera se carga en un buffer auxiliar y no formará parte de la imagen definitiva del programa en memoria. A continuación se explica el contenido de los bytes de la cabecera:

Offset 0 (2 bytes): Valores fijos 4Dh y 5Ah (en ASCII, 'MZ') ó 5Ah y 4Dh ('ZM'); esta información indica que el fichero es realmente de tipo EXE y no lleva esa extensión por antojo de nadie.

Offset 2 (2 palabras): Tamaño del fichero en el disco. La palabra más significativa (offset 4) da el número total de sectores que ocupa: 3 en este caso (3 * 512 = 1536). El tercer sector no está totalmente lleno, pero para eso está la palabra menos significativa (offset 2) que indica que el último sector sólo tiene ocupados los primeros 40h bytes. Por tanto, el tamaño efectivo del fichero es de 1024 + 64 = 1088 bytes, lo que se corresponde con la realidad.

Offset 6 (1 palabra): Número de reubicaciones a realizar. Indica cuántas veces se hace referencia a un segmento absoluto: el montador del sistema operativo tendrá que relocalizar en memoria todas las referencias a segmentos absolutos según en qué dirección se cargue el programa para su ejecución. En el ejemplo sólo hay 1 (correspondiente a la instrucción MOV AX,datos).

Offset 8 (1 palabra): Tamaño de esta cabecera del fichero EXE. La cabecera que estamos analizando y que precede al código y datos del programa será más o menos larga en función del tamaño de la tabla de reubicaciones, como luego veremos. En el ejemplo son 200h (=512) bytes, el tamaño mínimo, habida cuenta que sólo hay una reubicación (de hecho, aún cabrían muchas más).

Offset 0Ah (1 palabra): Mínima cantidad de memoria requerida por el programa, en párrafos, en adición al tamaño del mismo. En el ejemplo es 0 (el programa se conforma con lo que ocupa en disco).

Offset 0Ch (1 palabra): Máxima cantidad de memoria requerida (párrafos). Si es 0, el programa se cargará lo más alto posible en la memoria (opción /H del LINK de Microsoft); si es 0FFFFh, como en el ejemplo, el programa se cargará lo más abajo posible en la memoria -lo más normal-.

Offset 0Eh (2 palabras): Valores para inicializar SS (offset 0Eh) y SP (offset 10h). Evidentemente, el valor para SS está aún sin reubicar (habrá de sumársele el segmento en que se cargue el programa). En el ejemplo, el SS relativo es 4 y SP = 200h (=512 bytes de tamaño de pila definido).

Offset 12h (1 palabra): Suma de comprobación: son en teoría los 16 bits de menos peso de la negación de la suma de todas las palabras del fichero. El DOS debe hacer poco caso, porque TLINK no se molesta ni en inicializarlo (El LINK de Microsoft sí). Olvidar este campo.

Offset 14h (2 palabras): Valores para inicializar CS (offset 16h) e IP (offset 14h). El valor para CS está aún sin reubicar y habrá de sumársele el segmento definitivo en que se cargue el programa. En el ejemplo, el valor relativo de CS es 2, siendo IP = 0.

Offset 18h (1 palabra): Inicio de la tabla de reubicación, expresado como offset. En el ejemplo es 3Eh, lo que indica que la tabla comienza en el offset 3Eh. Cada entrada en la tabla ocupa 4 bytes. La única entrada de que consta este programa tiene el valor 0002:0005 = 25h, lo que indica que en el offset 200h+25h (225h) hay una palabra a reubicar -se suma 200h que es el tamaño de la cabecera-. En efecto, en el offset 225h hay una palabra a cero, a la que habrá de sumársele el segmento donde sea cargado el programa. Esta palabra a cero es el operando de la instrucción MOV AX,datos (el código de operación de MOV AX,n es 0B8h).

Offset 1Ah (1 palabra): Número de overlay (0 en el ejemplo, es un programa principal).

Offset 1Ch al 3Dh: Valores desconocidos (dependientes de la versión de LINK o TLINK).

  • Volver al Índice