Por Dario Alejandro Alpern
Como detalle constructivo, cabe mencionar que el 8087 posee 45.000 transistores y consume 3 watt.
Por precisión se entiende la exactitud de un número. En la representación de coma flotante, la mantisa es la encargada de la precisión. La mantisa contiene los dígitos significativos del número independientemente de dónde esté colocada la coma decimal. Si queremos aumentar la precisión de un esquema de representación de punto flotante, basta con añadir dígitos a la mantisa.
La precisión no es problema en el caso de los enteros, puesto que todo entero viene representado exactamente por su representación en complemento a dos. La representación precisa de los números reales sí es un problema ya que la mayoría de ellos tiene infinitos dígitos, cosa imposible de representar exactamente en una máquina con un número finito de componentes. Puesto que las computadoras permiten almacenar un número finito de dígitos, la representación de los números reales debe realizarse necesariamente por medio de aproximaciones.
El rango está relacionado con el tamaño de los números que se pueden representar. En los enteros, el rango depende del número de bits que se utilicen. Por ejemplo, con 16 bits pueden representarse números comprendidos entre -32768 y 32767. Para representar todos los enteros sería de nuevo necesario un número infinito de bits. Como puede verse, incluso los enteros tienen un rango restringido. En la notación de coma flotante, es el exponente el que fija el rango. Separando los problemas de precisión y de rango, la notación en coma flotante permite obtener rangos muy grandes con precisión razonable. El rango sigue siendo finito, porque sólo se puede representar un número finito de dígitos del exponente, pero tales dígitos permiten representar un número bastante grande de una forma compacta.
En contraste con la notación científica usual que utiliza la base 10, las computadoras utilizan la base 2 a la vez como base de la expresión exponencial, y como base para representar la mantisa y el exponente. Así, por ejemplo, el número 5,325 sería 4 + 1 + 1/4 + 1/8 = 101,111 (base 2) y se escribe en notación científica "binaria" como +1,01011E2. En este caso "E" debe leerse como "2 a la", y el incrementar o decrementar el exponente significa desplazar convenientemente la coma binaria.
Cuando se almacena un número binario en coma flotante en la máquina, se utiliza un bit para el signo, varios para la mantisa y varios para el exponente. Normalmente el bit de signo precede a los demás, después vienen los bits de exponente y finalmente los de la mantisa. La forma más común de guardar el exponente es adicionarle una constante, que recibe el nombre de "exceso". El número que se guarda recibe el nombre de "exponente en notación de exceso". A la inversa, dada la representación de un exponente, para recuperar su valor verdadero, basta con restarle el exceso. Como exceso se toma un número cuyo valor es aproximadamente igual a la mitad del rango de los posibles exponentes, para poder representar más o menos la misma cantidad de exponentes positivos que negativos.
Como ejemplo práctico, veremos cómo se representan los números reales en los registros del NDP 8087. Este formato particular recibe el nombre de formato real temporal. Todos los datos en el interior del NDP se almacenan de esta manera.
Descripción | Signo | Exponente | Mantisa |
---|---|---|---|
Bits | 79 | 78 ... 64 | 63 ... 0 |
El formato real temporal requiere 80 bits: uno para el signo, 15 para el exponente y 64 para la mantisa. El exceso es 2 ^ 14 - 1 = 16383. El menor valor positivo que se puede representar es 2 ^ -16382 = 3,36 x 10 ^ -4932, mientras que el mayor valor positivo representable es igual a 2 ^ 16384 = 1,19 x 10 ^ 4932.
Las instrucciones del NDP aparecen mezcladas con el flujo de instrucciones de la CPU. Sin embargo, cada cual selecciona y ejecuta sólo las que le corresponden. Todas las instrucciones correspondientes al NDP comienzan con una variante de la instrucción ESC del 8086/8088. Esta es una instrucción ficticia, con un operando ficticio que puede especificarse con cualquiera de los 24 modos de direccionamiento a memoria y otro operando ficticio que no es más que un registro.
Cada instrucción que lee la CPU, también lo lee el NDP. Cuando la CPU lee una instrucción ESC, el NDP se da por enterado que pronto tendrá que ponerse a trabajar. El NDP lee el código de operación, mientras que la CPU calcula la dirección de memoria, la que pone en el bus de direcciones. La CPU finalmente pasa el control al NDP, que extrae los bytes de datos necesarios, hace los cálculos correspondientes, y coloca los resultados sobre el bus.
Operación en punto flotante | 8087 | 8086 |
---|---|---|
Suma en signo y magnitud | 14 | 1600 |
Resta en signo y magnitud | 18 | 1600 |
Multiplicación (precisión simple) | 19 | 1600 |
Multiplicación (precisión doble) | 27 | 2100 |
División | 39 | 3200 |
Comparación | 9 | 1300 |
Carga (doble precisión) | 10 | 1700 |
Almacena (doble precisión) | 21 | 1200 |
Raíz cuadrada | 36 | 19600 |
Tangente | 90 | 13000 |
Exponenciación | 100 | 17100 |
El NDP amplía también el conjunto de registros de la CPU 8086/8088. Los registros de la CPU están en un chip (el 8086/8088, y hay ocho registros de coma flotante de 80 bits en el otro (el 8087). Dichos registros guardan números en el formato real temporal antes estudiado.
El coprocesador matemático trabaja simultáneamente con el procesador principal. Sin embargo, como el primero no puede manejar entrada o salida a dispositivos, la mayoría de los datos los origina la CPU. La CPU y el NDP tienen sus propios registros, que son completamente separados e inaccesibles al otro chip. Los datos se intercambian a través de la memoria, ya que ambos chips pueden acceder la memoria.
Los ocho registros se organizan como una pila. Una pila es tal vez la mejor manera de organizar los datos para evaluar expresiones algebraicas complejas. Los nombres de estos registros son ST(0), ST(1), ST(2), ..., ST(7). El nombre simbólico ST (Stack Top) es equivalente a ST(0). Al poner (push) un número en la pila, ST(0) contendrá el número recién ingresado, ST(1) será el valor anterior de ST(0), ST(2) será el valor anterior de ST(1), y así sucesivamente, con lo que se perderá el valor anterior de ST(7). El NDP 8087 tiene algunos registros más: un registro de estado de 16 bits, un registro de modo de 16 bits, un registro indicador de 8 bits y cuatro registros de 16 bits de salvaguarda de instrucciones y punteros a datos para la gestión de condiciones excepcionales.
El NDP es también una ampliación hardware desde el momento que no es autosuficiente. El 8087 necesita tener al lado un 8086 o un 8088 que le proporcione datos y direcciones, y controle el bus que a su vez suministrará instrucciones y operandos.
Hay varias líneas de control que interconectan el NDP y la CPU: la señal de TEST del CPU que se conecta a BUSY (ocupado) del NDP, la línea RQ0/GT0 (petición/concesión del manejo del bus), y las señales de estado de la cola (QS1, QS0).
El terminal de TEST del 8086/8088 se conecta al de BUSY del 8087. Esto permite que el 8086/8088 pueda utilizar la instrucción WAIT para sincronizar su actividad con el NDP. La forma correcta de hacerlo es conseguir que el ensamblador genere automáticamente una instrucción WAIT antes de cada instrucción del NDP, y que el programador ponga en el programa una instrucción FWAIT (sinónimo de WAIT) después de cada instrucción del NDP que cargue los datos en memoria para que puedan ser inmediatamente utilizadas por la CPU. Mientras el NDP 8087 está realizando una operación numérica, pone a 1 el terminal BUSY (y por lo tanto el terminal WAIT del CPU). Mientras la CPU ejecuta la instrucción WAIT, suspende toda actividad hasta que el terminal TEST vuelve a su estado normal (cero). Así, la secuencia de una instrucción numérica del NDP seguida de un WAIT de la CPU hace que la CPU llame al NDP y espere hasta que el último acabe.
La línea RQ/GT0 (petición/concesión del manejo del bus) la utiliza el NDP para conseguir el control del bus compartido por el NDP y la CPU. La línea de interconexión es bidireccional. Una señal (petición) del NDP a la CPU indica que el NDP quiere utilizar el bus. Para que el NDP tome control del bus, debe esperar a que la CPU le conteste (con una concesión). Cuando el NDP acaba de usar el bus, envía una señal por el mismo pin indicando que ha terminado. En este protocolo, la CPU juega el papel de amo y el NDP de esclavo. El esclavo es el que pide el bus y el amo es el que lo concede tan pronto como puede.
Hay dos terminales de estado de la cola, QS1 y QS0. Ambas líneas permiten al NDP sincronizar su cola de instrucciones con la de la CPU.
Los tipos de datos son los siguientes: palabra entera de 16 bits, entero corto de 32 bits, entero largo de 64 bits (en todos los casos en representación de complemento a dos), BCD empaquetado con 18 dígitos decimales (como son dos dígitos por byte, se requieren diez bytes, estando uno de ellos reservado para el signo), real corto de 32 bits (1 bit de signo, 8 de exponente y 23 de mantisa), real largo de 64 bits (1 de signo, 11 de exponente y 52 de mantisa) y real temporal (1 de signo, 15 de exponente y 64 de mantisa). Como se vio anteriormente, el primer bit de la mantisa siempre es uno (en caso contrario el número no estaría normalizado). Este uno no aparece en los formatos real corto y real largo, por lo que la precisión de la mantisa es de 24 y 53 bits, respectivamente.
Lo primero que se hace es hallar 3 al cuadrado, luego 4 al cuadrado, luego se suman ambos resultados y finalmente se halla la raíz cuadrada. En calculadoras que utilizan la notación polaca inversa (como las Hewlett-Packard) se apretarían las siguientes teclas: 3, x2, ENTER, 4, x2, +, raíz cuadrada. La tecla ENTER en estas calculadoras guardan un resultado intermedio en una pila para poder utilizarlo más adelante (en este caso, en la suma). La tecla + utiliza el número que está en la pantalla y el que se guardó en la pila anteriormente para realizar la suma.
Otro ejemplo más complicado: (2 + 8) / (25 - 53) sería: 2, ENTER, 8, +, ENTER, 25, ENTER, 5, ENTER, 3, x3, -, /.
Veremos la ejecución paso a paso de la secuencia recién mostrada (aquí ST(0) representa la pantalla, ST(1) lo que se puso en la pila mediante ENTER y así sucesivamente):
ST(0) | ST(1) | ST(2) | ST3 | |
---|---|---|---|---|
2 | 2 | |||
ENTER | 2 | 2 | ||
8 | 8 | 2 | ||
+ | 10 | |||
ENTER | 10 | 10 | ||
25 | 25 | 10 | ||
ENTER | 25 | 25 | 10 | |
5 | 5 | 25 | 10 | |
ENTER | 5 | 5 | 25 | 10 |
3 | 3 | 5 | 25 | 10 |
x3 | 125 | 25 | 10 | |
- | -100 | 10 | ||
/ | -0,1 |
Es fundamental haber entendido ambos ejemplos, ya que el coprocesador trabaja de esta manera (primero se ponen los operandos en la pila y después se realizan las operaciones).
Hay dos operaciones básicas de acceso de datos en la pila del 8087 que son las de extraer (pop) e introducir (push). La introducción funciona de la siguiente manera: tiene un operando que es la fuente. El puntero de la pila se decrementa en uno de manera que apunte a la posición inmediata superior y, a continuación, se el dato se transfiere a este nuevo elemento superior. El extraer funciona de la siguiente manera: en primer lugar se transfiere el dato correspondiente al elemento superior de la pila a su destino, y seguidamente el puntero de pila se incrementa en uno, de manera que apunta ahora al nuevo elemento superior de la pila (que está en una posición por encima del anterior).
En el NDP, la pila está numerada respecto a su elemento superior. Dicho elemento se denota por ST(0), o simplemente ST, y las posiciones inferiores se denotan respectivamente por ST(1), ST(2), ..., ST(7).
Registro del 8087 | Posición relativa con respecto al puntero de pila | |
---|---|---|
R0 | ST(4) | |
R1 | ST(5) | |
R2 | ST(6) | |
R3 | ST(7) | |
R4 | ST | Puntero de pila |
R5 | ST(1) | |
R6 | ST(2) | |
R7 | ST(3) |
No es posible acceder a Rn por programa, sólo a ST(n).
FLD ST(num): Introduce una copia de ST(num) en ST.
FILD mem: Introduce una copia de mem en ST. La fuente debe ser un operando de memoria de 2, 4 u 8 bytes, que se interpreta como un número entero y se convierte al formato real temporal.
FBLD mem: Introduce una copia de mem en ST. La fuente debe ser un operando de 10 bytes, que se interpreta como un valor BCD empaquetado y se convierte al formato real temporal.
FST mem: Copia ST a mem sin afectar el puntero de pila. El destino puede ser un operando real de 4 u 8 bytes (no el de 10 bytes).
FST ST(num): Copia ST al registro especificado.
FIST mem: Copia ST a mem. El destino debe ser un operando de 2 ó 4 bytes (no de 8 bytes) y se convierte automáticamente el número en formato temporal real a entero.
FSTP mem: Extrae una copia de ST en mem. El destino puede ser un operando de memoria de 4, 8 ó 10 bytes, donde se carga el número en punto flotante.
FSTP ST(num): Extrae ST hacia el registro especificado.
FISTP mem: Extrae una copia de ST en mem. El destino debe ser un operando de memoria de 2, 4 u 8 bytes y se convierte automáticamente el número en formato temporal real a entero.
FBSTP mem: Extrae una copia de ST en mem. El destino debe ser un operando de memoria de 10 bytes. El valor se redondea a un valor entero, si es necesario, y se convierte a BCD empaquetado.
FXCH: Intercambia ST(1) y ST.
FXCH ST(num): Intercambia ST(num) y ST.
FLDZ: Introduce el número cero en ST.
FLD1: Introduce el número uno en ST.
FLDPI: Introduce el valor de pi en ST.
FLDL2E: Introduce el valor de log(2) e en ST.
FLDL2T: Introduce el valor de log(2) 10 en ST.
FLDLG2: Introduce el valor de log(10) 2 en ST.
FLDLN2: Introduce el valor de log(e) 2 en ST.
Se puede transferir el área de datos del coprocesador, los registros de control, o simplemente la palabra de estado o de control.
Cada instrucción de carga tiene dos formas: La forma con espera verifica excepciones de errores numéricos no enmascarados y espera a que sean atendidos. La forma sin espera (cuyo mnemotécnico comienza con "FN") ignora excepciones sin enmascarar.
FLDCW mem2byte: Carga la palabra de control desde la memoria.
F[N]STCW mem2byte: Almacena la palabra de control en la memoria.
F[N]STSW mem2byte: Almacena la palabra de estado en la memoria.
FLENV mem14byte: Carga el entorno desde la memoria.
F[N]STENV mem14byte: Almacena el entorno en la memoria.
FRSTOR mem94byte: Restaura el estado completo del 8087.
F[N]SAVE mem94byte: Salva el estado completo del 8087.
FADD: Hace ST(1) más ST, ajusta el puntero de pila y pone el resultado en ST, por lo que ambos operandos se destruyen.
FADD mem: Hace ST <- ST + [mem]. En mem deberá haber un número real en punto flotante.
FIADD mem: Hace ST <- ST + [mem]. En mem deberá haber un número entero en complemento a dos.
FADD ST(num), ST: Realiza ST(num) <- ST(num) + ST.
FADD ST, ST(num): Realiza ST <- ST + ST(num).
FADDP ST(num), ST: Realiza ST(num) <- ST(num) + ST y retira el valor de ST de la pila, con lo que ambos operandos se destruyen.
FSUB: Hace ST(1) menos ST, ajusta el puntero de pila y pone el resultado en ST, por lo que ambos operandos se destruyen.
FSUB mem: Hace ST <- ST - [mem]. En mem deberá haber un número real en punto flotante.
FISUB mem: Hace ST <- ST - [mem]. En mem deberá haber un número entero en complemento a dos.
FSUB ST(num), ST: Realiza ST(num) <- ST(num) - ST.
FSUB ST, ST(num): Realiza ST <- ST - ST(num).
FSUBP ST(num), ST: Realiza ST(num) <- ST(num) - ST y retira el valor de ST de la pila, con lo que ambos operandos se destruyen.
FSUBR: Hace ST menos ST(1), ajusta el puntero de pila y pone el resultado en ST, por lo que ambos operandos se destruyen.
FSUBR mem: Hace ST <- [mem] - ST. En mem deberá haber un número real en punto flotante.
FISUBR mem: Hace ST <- [mem] - ST. En mem deberá haber un número entero en complemento a dos.
FSUBR ST(num), ST: Realiza ST(num) <- ST - ST(num).
FSUBR ST, ST(num): Realiza ST <- ST(num) - ST.
FSUBRP ST(num), ST: Realiza ST(num) <- ST - ST(num) y retira el valor de ST de la pila, con lo que ambos operandos se destruyen.
FMUL: Multiplicar el valor de ST(1) por ST, ajusta el puntero de pila y pone el resultado en ST, por lo que ambos operandos se destruyen.
FMUL mem: Hace ST <- ST * [mem]. En mem deberá haber un número real en punto flotante.
FIMUL mem: Hace ST <- ST * [mem]. En mem deberá haber un número entero en complemento a dos.
FMUL ST(num), ST: Realiza ST(num) <- ST(num) * ST.
FMUL ST, ST(num): Realiza ST <- ST * ST(num).
FMULP ST(num), ST: Realiza ST(num) <- ST(num) * ST y retira el valor de ST de la pila, con lo que ambos operandos se destruyen.
FDIV: Dividir el valor de ST(1) por ST, ajusta el puntero de pila y pone el resultado en ST, por lo que ambos operandos se destruyen.
FDIV mem: Hace ST <- ST / [mem]. En mem deberá haber un número real en punto flotante.
FIDIV mem: Hace ST <- ST / [mem]. En mem deberá haber un número entero en complemento a dos.
FDIV ST(num), ST: Realiza ST(num) <- ST(num) / ST.
FDIV ST, ST(num): Realiza ST <- ST / ST(num).
FDIVP ST(num), ST: Realiza ST(num) <- ST(num) / ST y retira el valor de ST de la pila, con lo que ambos operandos se destruyen.
FDIVR: Hace ST dividido ST(1), ajusta el puntero de pila y pone el resultado en ST, por lo que ambos operandos se destruyen.
FDIVR mem: Hace ST <- [mem] / ST. En mem deberá haber un número real en punto flotante.
FIDIVR mem: Hace ST <- [mem] / ST. En mem deberá haber un número entero en complemento a dos.
FDIVR ST(num), ST: Realiza ST(num) <- ST / ST(num).
FDIVR ST, ST(num): Realiza ST <- ST(num) / ST.
FDIVRP ST(num), ST: Realiza ST(num) <- ST / ST(num) y retira el valor de ST de la pila, con lo que ambos operandos se destruyen.
FABS: Pone el signo de ST a positivo (valor absoluto).
FCHS: Cambia el signo de ST.
FRNDINT: Redondea ST a un entero.
FSQRT: Reemplaza ST con su raíz cuadrada.
FSCALE: Suma el valor de ST(1) al exponente del valor en ST. Esto efectivamente multiplica ST por dos a la potencia contenida en ST(1). ST(1) debe ser un número entero.
FPREM: Calcula el resto parcial hallando el módulo de la división de los dos registros de la pila que están el tope. El valor de ST se divide por el de ST(1). El resto reemplaza el valor de ST. El valor de ST(1) no cambia. Como esta instrucción realiza sustracciones repetidas, puede tomar mucho tiempo si los operandos son muy diferentes en magnitud. Esta instrucción se utiliza a veces en funciones trigonométricas.
FXTRACT: Luego de esta operación, ST contiene el valor de la mantisa original y ST(1) el del exponente.
fstsw mem16 | ; Guardar palabra de estado en memoria. |
fwait | ; Asegurarse que el coprocesador lo hizo. |
mov ax,mem16 | ; Mover a AX. |
sahf | ; Almacenar byte más significativo en flags. |
Palabra de estado | C3 | C2 | C1 | C0 | (Bits 15-8) | ||||
---|---|---|---|---|---|---|---|---|---|
Indicadores 8088 | SF | ZF | X | AF | X | PF | X | CF | (Bits 7-0) |
De esta manera, C3 se escribe sobre el indicador de cero, C2 sobre el de paridad, C0 sobre el de arrastre y C1 sobre un bit indefinido, por lo que no puede utilizarse directamente con saltos condicionales, aunque se puede utilizar la instrucción TEST para verificar el estado del bit C1 en la memoria o en un registro de la CPU.
Las instrucciones de comparación afectan los indicadores C3, C2 y C0, como se puede ver a continuación.
Después de FCOM | Después de FTEST | C3 | C2 | C0 | Usar |
---|---|---|---|---|---|
ST > fuente | ST es positivo | 0 | 0 | 0 | JA |
ST < fuente | ST es negativo | 0 | 0 | 1 | JB |
ST = fuente | ST es cero | 1 | 0 | 0 | JE |
No comparables | ST es NAN o infinito proyectivo | 1 | 1 | 1 | JP |
FCOM: Compara ST y ST(1).
FCOM ST(num): Compara ST y ST(num).
FCOM mem: Compara ST y mem. El operando de memoria deberá ser un número real de 4 u 8 bytes (no de 10).
FICOM mem: Compara ST y mem. El operando deberá ser un número entero de 2 ó 4 bytes (no de 8).
FTST: Compara ST y cero.
FCOMP: Compara ST y ST(1) y extrae ST fuera de la pila.
FCOMP ST(num): Compara ST y ST(num) y extrae ST fuera de la pila.
FCOMP mem: Compara ST y mem y extrae ST fuera de la pila. El operando de memoria deberá ser un número real de 4 u 8 bytes (no de 10).
FICOMP mem: Compara ST y mem y extrae ST fuera de la pila. El operando deberá ser un número entero de 2 ó 4 bytes (no de 8).
FCOMPP: Compara ST y ST(1) y extrae dos elementos de la pila, perdiéndose ambos operandos.
FXAM: Pone el valor de los indicadores según el tipo de número en ST. La instrucción se utiliza para identificar y manejar valores especiales como infinito, cero, números no normalizados, NAN (Not a Number), etc. Ciertas operaciones matemáticas son capaces de producir estos números especiales. Una descripción de ellos va más allá del alcance de este apunte.
FYL2X: Calcula ST(1) * log(2) ST. El puntero de pila se actualiza y luego se deja el resultado en ST, por lo que ambos operandos se destruyen.
FYL2XP1: Calcula ST(1) * log(2) (ST + 1). El puntero de pila se actualiza y luego se deja el resultado en ST, por lo que ambos operandos se destruyen. El valor absoluto del valor previo de ST debe estar entre 0 y la raíz cuadrada de 2 dividido 2. FPTAN: Calcula la tangente del valor en ST. El resultado es una razón Y/X, donde X reemplaza el valor anterior de ST y Y se introduce en la pila así que, después de la instrucción, ST contiene Y y ST(1) contiene X. El valor previo de ST debe ser un número positivo menor que pi/4. El resultado de esta instrucción se puede utilizar para calcular otras funciones trigonométricas, incluyendo seno y coseno.
FPATAN: Calcula el arcotangente de la razón Y/X, donde X está en ST e Y está en ST(1). ST es extraído de la pila, dejando el resultado en ST, por lo que ambos operandos se destruyen. El valor de Y debe ser menor que el de X y ambos deben ser positivos. El resultado de esta instrucción se puede usar para calcular otras funciones trigonométricas inversas, incluyendo arcoseno y arcocoseno.
F[N]CLEX: Pone a cero los indicadores de excepción y el indicador de ocupado de la palabra de estado. También limpia el indicador de pedido de interrupción del 8087.
FINCSTP: Suma uno al puntero de pila en la palabra de estado. No se afecta ningún registro.
FDECSTP: Resta uno al puntero de pila en la palabra de estado. No se afecta ningún registro.
FREE ST(num): Marca el registro especificado como vacío.
FNOP: Copia ST a sí mismo tomando tiempo de procesamiento sin tener ningún efecto en registros o memoria.