¿Por qué razón una persona que ya tenga amplios conocimientos y experiencia en un lenguaje como FORTRAN estaría interesada en invertir nuevamente recursos económicos y tiempo de su vida para aprender otro lenguaje de alto nivel como C? Por una razón extraordinariamente importante: el lenguaje C, a diferencia de otros lenguajes de alto nivel, nos permite tener acceso directo a los recursos de hardware de un sistema, a los componentes esenciales de su arquitectura interna, en la forma en que lo hace un ensamblador, pero con las ventajas de un lenguaje de alto nivel. Es por ello que el lenguaje C ha sido descrito como “un lenguaje ensamblador de alto nivel”.
Si C nos ha de dar la capacidad de poder comunicarnos directamente con el hardware de una máquina, esta comunicación se debe poder llevar a cabo al nivel de lo que entiende la máquina, a través de los “unos” y “ceros” de la lógica binaria digital. A diferencia de otros lenguajes, C contiene operadores especiales que incrementan su poder y flexibilidad, especialmente para la programación al nivel de sistemas. Se trata de los operadores “al modo de bits” (bitwise, se pronuncia “bitgüais”). Puesto que C fue diseñado para reemplazar el lenguaje ensamblador para la mayoría de las tareas de programación, era importante que tuviera la capacidad de dar apoyo a todas (o al menos muchas) de las operaciones lógicas que se pueden efectuar en ensamblador. Las operaciones “al modo de bits” son operaciones capaces de poder probar, cambiar o desplazar los bits individuales en un byte o en una palabra binaria que correspondan a los tidpos de datos tales como los char, los int y los long, números enteros todos ellos (sin punto decimal). Las operaciones bitwise no pueden ser usadas en los tipos float, double, long double y void. Las operaciones AND, OR y NOT que corresponden a las tres funciones lógicas básicas tienen la misma tabla de verdad que sus equivalentes lógicos excepto que trabajan en un nivel de bit por bit. La operación XOR (OR exclusivo), por ejemplo, para la cual se usa en C el carete (“^”) tiene la siguiente tabla de verdad:
p | q | p^q |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Los operadores lógicos “al modo de bits” no deben ser confundidos jamás con los operadores lógicos “&&” (AND), “||” (OR) y “!” (NOT) usados sobre variables para estructuras de control, bucles y saltos condicionales, tales como:
char ch1 = 'A';
char ch2 = 'B';
if (ch2 > ch1) printf("mayor que");
en virtud de que dichos operadores operan sobre cantidades, mientras que los operadores “al modo de bits” operan sobre bits individuales de un byte o de una palabra binaria. Puesto de otro manera, los operadores al modo de bits efectúan una labor mucho más quirúrgica, yendo directamente a los “unos” y “ceros” con los que se forma una cantidad. Si p representa un bit y q representa otro bit, es imposible afirmar que p sea mayor que q del mismo modo que es imposible afirmar que, al nivel del hardware, un “1” lógico sea mayor o menor que un “0” lógico; lo más que se puede decir es que p y q son iguales (ambos son un “uno” o un “cero” lógico) o que son desiguales.
Los símbolos usados en C para los operadores al modo de bits son los siguientes:
Operador | Acción |
& | AND |
| | OR |
~ | NOT |
^ | OR-EXCLUSIVO |
>> | Corrimiento hacia la derecha |
<< | Corrimiento hacia la izquierda |
Las operaciones al modo de bits encuentran aplicaciones en programas para manejo de dispositivos (device drivers), rutinas para manejo de archivos en el disco duro, rutinas para comunicación externa a través del modem, etcétera, porque entre otras cosas pueden ser usadas en una operación AND para “enmascarar” fuera ciertos bits (algo así como ponerle una “máscara” a un byte o una palabra binaria dejando que se pueda ver únicamente un cierto bit en particular ocultando los bits restantes que no nos interesan, operación llamada apropiadamente en literatura técnica inglesa como masking), como el bit de paridad (el bit de paridad es usado en sistemas de detección de errores para confirmar que el resto de los bits en un byte fueron transmitidos exitosamente sin que hubiera cambio alguno por un deterioro en la transmisión de la señal digital).
En términos de uso común, podemos imaginar al AND bitwise como una manera de “apagar” bits en forma selectiva. Cualquier bit que sea cero en cualquier operando causará que el bit correspondiente en la variable sea puesto a un “cero” lógico. La “máscara” tendrá todos sus bits puestos a un “1” lógico excepto uno de ellos que siendo “0” será “enmascarado”. A modo de ejemplo, la siguiente función puede leer un caracter del puerto del módem usando una función C de biblioteca llamada bioscom(), reseteando en forma selectiva el bit de paridad a cero (esta función hizo su aparición en los puertos de comunicación serial asíncrona de las computadoras IBM-compatibles):
char obtener_caracter_del_modem(void)
{
char ch;
ch = bioscom(2,0,0); /* obtener un caracter de COM1 */
return ch & 127; /* operación AND de enmascarado */
}
Usualmente el bit de paridad es el que corresponde al octavo bit (el bit más alto) del byte, y es puesto a cero (eliminando con ello el bit de paridad) mediante una operación AND en la cual la “máscara” tiene los bits 1 al 7 puestos a un “1” lógico y tiene el bit 8 puesto a un “0” lógico, lo cual implica que la máscara usada es el byte 01111111 (cuyo equivalente en sistema decimal es 127). La expresión ch & 127 hace que se lleve a cabo el AND junto con los bits en ch que forman el número 127 (el compilador C se encarga de llevar a cabo la conversión del 127 decimal al 01111111 binario). El resultado final es que el octavo bit de ch será fijado en un cero lógico, enmascarando hacia afuera el bit de paridad. En el siguiente ejemplo se supone que ch ha recibido el caracter “A” con el bit de paridad fijado a “1”:
bit de paridad | ||||||||||
↓ | ||||||||||
1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | ch contiene un caracter "A" con la paridad activada (puesta en 1) |
||
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 01111111 binario = 127 decimal | ||
& | ------------------ | efectuar AND bitwise | ||||||||
0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | "A" con la paridad removida |
Así pues, en el ejemplo anterior el byte 01111111 es la “máscara” o “mascarilla” que se encarga de “enmascarar” el bit del cual nos queremos deshacer, dejando los bits restantes del byte intactos.
El reverso del AND bitwise, si es que queremos “encender” cierto bit en particular dentro de una palabra binaria, es el OR bitwise. Cualquier bit que sea fijado a “1” en cualquiera de los operandos ocasionará que el bit correspondiente en la variable sea encendido a “1”. El siguiente ejemplo nos muestra lo que ocurre con la operación bitwise 96|3:
0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 96 en binario | |
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 3 en binario | |
| | ----------------- | OR bitwise | |||||||
0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | resultado, 99 |
Por otro lado, la operación OR exclusivo, usualmente abreviada como XOR, encenderá un bit en el resultado final sí y solo sí los bits que están siendo comparados en cada byte o palabra binaria son diferentes. El siguiente ejemplo nos muestra lo que ocurre cuando se lleva a cabo la operación XOR con los números 115 y 53:
0 | 1 | 1 | 1 | 0 | 0 | 1 | 1 | 115 en binario | |
0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 53 en binario | |
^ | ----------------- | XOR bitwise | |||||||
0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | resultado, 70 |
Los operadores AND, OR y XOR al modo de bits aplican sus operaciones directamente y en forma individual a cada bit de la variable. Por esta razón las operaciones bitwise no son usadas en enunciados condicionales del modo en que son usados los operadores relacionales y lógicos. Por ejemplo, si X es igual a 7, entonces X && 8 es evaluado a verdadero (1), mientras que al modo de bits X & 8 es evaluado a falso (0). Los operadores relacionales y lógicos siempre producen un resultado que es 0 ó 1, mientras que las operaciones similares efectuadas al modo de bits pueden producir un valor arbitrario de acuerdo con la operación específica. Puesto de otra manera, las operaciones al modo de bits pueden tener otros valores que no sean 0 ó 1, mientras que los operadores lógicos siempre terminan evaluados a 0 (falso) ó 1 (verdadero).
La operación AND al modo de bits es útil cuando se quiere verificar si cierto bit en un byte o una palabra binaria está encendido o apagado. El siguiente enunciado checa si el bit 5 en la variable registro está encendido:
if(registro & 16) printf("el bit 5 esta encendido");
La razón por la cual usamos 16 es que en el sistema binario su representación es 10000; el número 16 convertido al sistema binario tiene únicamente el bit 5 encendido. Por lo tanto, el enunciado if solo puede tener éxito cuando el quinto bit de registro está encendido. Una aplicación interesante de este procedimiento es la función mostrarbinario(), la cual pone en formato binario el patrón de bits de su argumento:
void mostrarbinario(int i)
{
int n;
for(n=128; n>0; n = n/2)
if(i & n) printf("1 ");
else printf("0 ");
printf("\n");
}
La función mostrarbinario() logra su cometido probando sucesivamente cada bit del byte proporcionado como argumento, usando el AND al modo de bits para determinar si está encendido o apagado. Si está encendido, se pone el dígito “1”, en caso contrario, se pone el dígito “0”, checándose cada bit hasta que ya no hay más bits por verificar.
Los operadores de corrimiento o desplazamiento, >> y <<, mueven todos los bits en un valor entero hacia la derecha o hacia la izquierda según se especifica. La sintaxis para un enunciado de corrimiento hacia la derecha es:
valor >> número de posiciones de bits
mientras que el enunciado de un corrimiento hacia la izquierda es:
valor << número de posiciones de bits
Todos los bits son desplazados hacia afuera en un extremo, y se van introduciendo ceros en el otro extremo. O sea que los bits que van saliendo fuera por un lado no son regresados por el otro lado, ya que la operación de corrimiento no es una operación de rotación. Los bits que son corridos hacia afuera se pierden.
En los sistemas computacionales, las operaciones de corrimiento de bits pueden ser muy útiles para implementar la multiplicación así como la división rápida de números enteros. Un corrimiento hacia la izquierda efectivamente multiplicará un número por 2, y un corrimiento hacia la derecha lo dividirá entre 2 (de hecho, en las unidades de procesamiento central CPU en donde se implementa este tipo de operación matemática en hardware la implementación se lleva a cabo precisamente mediante operaciones de corrimiento de bits; lo cual muestra una vez más que lo que se puede hacer en el hardware se puede hacer mediante software y viceversa).
A modo de ejemplo, si:
unsigned char x = 7;
siendo 00000111 el equivalente binario de 7, entonces el enunciado:
x = x << 1;
produce el siguiente resultado:
x = 14
al ser 00001110 el equivalente binario de 14.
Otras posibilidades son:
x = 7 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 7 |
x = x << 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 14 |
x = x << 3 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 56 |
x = x << 5 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 224 |
x = x << 6 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 192 |
x = x << 7 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 128 |
El siguiente programa, que hace uso de la función mostrarbinario() definida arriba, muestra el efecto de los operadores de corrimiento:
#include <stdio.h>
void mostrarbinario(int i);
main(void)
{
int i=1, t;
for(t=0; t<8; t++) {
mostrarbinario(i);
i = i << 1;
}
printf("\n");
for(t=0; t<8; t++) {
i = i >> 1;
mostrarbinario(i);
}
return 0;
}
void mostrarbinario(int i)
{
int t;
for(t=128; t>0; t=t/2)
if(i & t) printf("1 ");
else printf("0 ");
printf("\n");
}
Al ejecutarse el programa anterior, se produce la siguiente salida:
00000001Para quienes tienen en mente las equivalencias funcionales que hay entre lo que se puede lograr con el hardware y lo que se puede lograr con el software, a estas alturas será evidente que C no contiene un operador de rotación. Sin embargo, aunque no lo contiene, es fácil crear en C una función que pueda llevar a cabo esta tarea. Una rotación es semejante a una operación de corrimiento hacia la izquierda excepto que los bits que son empujados hacia afuera en un extremo van siendo desplazados (introducidos) por el otro extremo. Una manera de llevar a cabo una rotación hace uso de la union de dos tipos de datos. El primer tipo es un array de dos elementos del tipo de datos que queremos rotar. El segundo es un tipo que será mayor que los datos que serán rotados. En el ejemplo que se dará aquí, se llevará a cabo una rotación al modo de bits hacia la izquierda, usándose la siguiente unión:
00000010
00000100
00001000
00010000
00100000
01000000
10000000
10000000
01000000
00100000
00010000
00001000
00000100
00000010
00000001
union rotar {
char ch[2];
unsigned int i;
} rot;
Esta es la función que llevará a cabo la rotación:
void rotarlo(union rotar *rot)
{
rot->ch[1] = 0; /* limpiar el byte de orden alto */
rot->i = rot-> i << 1; /* desplazar una vez hacia la izquierda */
/* checar si un bit ha sido desplazado fuera de ch[0] */
if(rot->ch[1]) rot->i = rot->i | 1; /* operacion OR del bit */
}
La función rotarlo() primero “limpia” el byte de orden alto del entero i con la intención de que si por corrimiento entra un bit, dicho bit puede ser detectado. Aplicando el operador de corrimiento izquierdo al entero completo (byte alto y byte bajo), un bit que deje el byte ch[0] no será perdido sino que será movido hacia ch[1]. Si un bit es desplazado fuera, será reciclado mediante una operación OR hacia el bit de orden bajo de ch[0]. El siguiente programa muestra la manera en la que se emplea la función usada para llevar a cabo la rotación:
#include <stdio.h>
union rotar {
char ch[2];
unsigned int i;
} rot;
void mostrarbinario(int i);
void rotarlo(union rotar *rot);
main(void)
{
int t;
rot.ch[0] = 101;
for(t=0; t<7; t++) {
mostrarbinario(rot.i);
rotarlo(&rot);
}
return 0;
}
void rotarlo(union rotar *rot)
{
rot->ch[1] = 0; /* limpiar el byte de orden alto */
rot->i = rot-> i << 1; /* desplazar una vez hacia la izquierda */
/* checar si un bit ha sido desplazado fuera de ch[0] */
if(rot->ch[1]) rot->i = rot->i | 1; /* operacion OR del bit */
}
void mostrarbinario(int i)
{
int t;
for(t=128; t>0; t=t/2)
if(i & t) printf("1 ");
else printf("0 ");
printf("\n");
}
El programa produce la siguiente salida rotanto el byte original, 01100101, siete veces:
01100101El operador ~ de negación (inversión lógica, complemento, etcétera) al modo de bits invierte el valor lógico de cada bit en la variable especificada. Todos los “ceros” son cambiados a “unos” y todos los “unos” son cambiados a “ceros”. Una aplicación interesante para el operador de negación es para poder ver el conjunto extendido de caracteres. El conjunto de caracteres mostrado en el teclado ordinario representa únicamente una parte del conjunto completo de caracterers a los cuales dá soporte la computadora. El programa que se muestra a continuación recurre al operador de negación para invertir los bits en los caracteres tecleados por el usuario. Los patrones invertidos de bits corresponden a mucho de lo que forma parte del conjunto extendido de caracteres (la ejecución del programa se lleva a cabo oprimiendo la tecla “q”, como corresponde al comando DOS “quit”).
11001010
10010101
00101011
01010110
10101100
01011001
#include <stdio.h>
#include <conio.h>
main(void)
{
char caracter;
do {
caracter = getche();
printf("%c", ~caracter);
} while(caracter!='q');
return 0;
}
Habiendo visto la manera en la cual podemos manejar individualmente los bits dentro de un byte o una palabra binaria que represente alguna variable relacionada con el hardware de la máquina, el siguiente paso para poder llevar a cabo una programación de sistemas consiste naturalmente en explorar la forma en la cual podemos enviar información hacia afuera de la computadora a través de los recursos proporcionados por el fabricante para tal efecto en la tarjeta madre en la cual está montado el procesador CPU, o tomar información llegada del exterior también usando recursos proporcionados por el fabricante para tal efecto. En esto hay una variabilidad enorme de máquina a máquina, inclusive en las familias de máquinas consideradas compatibles con la arquitectura “IBM compatible” tan popular en las primeras computadoras personales caseras y sus numerosas descendientes, habido el hecho de que por ser expandible la arquitectura IBM a una misma máquina se le podían agregar (en las ranuras disponibles para ello en la tarjeta madre) una gran variedad de distintas tarjetas elaboradas por distintos fabricantes para producir sonido de distinta calidad, así como una gran variedad de tarjetas elaboradas por distintos fabricantes para producir gráficas compatibles con monitores de diversas resoluciones y disponibilidad de colores conforme iba evolucionando la tecnología, siendo posible tener cientos y cientos de combinaciones distintas de máquinas dependiendo de las preferencias individuales de cada usuario. El sistema operativo Windows de Microsoft hizo lo mejor que pudo para facilitar su universalidad hacia todas las combinaciones posibles, hasta que llegó el tiempo en el que no es posible sacarle una mayor diversidad de colores a los monitores por el simple hecho de que el ojo humano es incapaz de distinguir una cierta cantidad de colores distintos, al igual que no es deseable usar una cantidad mayor de bits para digitalizar el sonido en virtud de que el oido humano es incapaz de distinguir las diferencias más allá de ciertos bits de resolución en la reproducción digital del sonido. En pocas palabras, el tope teórico a varios de los dispositivos que se pueden agregar a una computadora ultimadamente no está limitado por la tecnología, sino por las limitaciones de los sentidos humanos para distinguir diferencias más allá de cierto nivel. Sin embargo, hay otras cosas en las cuales la tecnología puede seguir evolucionando, tales como la capacidad para intercomunicarse inalámbricamente con dispositivos Wi-Fi, la capacidad para manejar impresoras 3D, más las sorpresas que puedan traer las tecnologías del futuro.
Habiendo visto lo anterior, tendremos ahora aquí nuestro primer contacto con lo que se tiene que hacer al nivel de la programación para permitirnos escribir no solo un programa ejecutable como Word o Excel e incluso escribir un sistema operativo completo como Linux o Windows, sino también escribir programas capaces de tomar un control directo sobre monitores, impresoras, bocinas. Se trata de tomar un control directo de todo por la vía de la programación. ¡Hasta podemos construír con la ayuda de un compilador C otro compilador que a su vez sea un compilador funcionalmente completo para convertir en lenguaje de máquina un programa escrito en un lenguaje de alto nivel como FORTRAN, PASCAL, y hasta el mismo lenguaje C! (Obviamente, el creador del lenguaje C no tuvo al alcance de su mano un compilador C para ayudarle a construír su primer compilador C, no podemos ir hacia atrás indefinidamente en el tiempo sin toparnos con que alguien tuvo la necesidad de tener recurrir al uso de programas ensambladores para la construcción de los primeros compiladores, y yendo aún más atrás alguien tuvo que escribir el primer ensamblador directamente en lenguaje de máquina).
Al empezar la revolución de las microcomputadoras basadas en circuitos integrados tales como los chips Intel 8086 y 8088, ambos de 8 bits, una aplicación comercial importante fue el uso del Intel 8088 en las primeras computadoras personales IBM PC. Los principios que veremos a continuación son aplicables a todas las computadoras que puedan manejar el conjunto de instrucciones de la familia 8086/8088.
Una computadora como cualquiera de las IBM PC originales (IBM PC XT e IBM PC AT) así como los “clones” de dichas computadoras tienen, al igual que las máquinas actuales, algo más que un circuito integrado 8088. Tienen un teclado, una bocina, un disco duro, un monitor, e inclusive hasta otros microprocesadores para controlar el flujo de los datos. La unidad de procesamiento central CPU (incorporada dentro del circuito integrado 8088) necesita alguna manera de poder comunicarse con las demás partes de la computadora. Como ya hemos visto con anterioridad, algo de esto se lleva a cabo utilizando “domicilios” de memoria. Pero otra parte se lleva a cabo a través de lo que se conoce como los puertos de entrada/salida. El chip 8088 de Intel cuenta con 65,536 puertos de entrada/salida que pueden ser usados individualmente para comunicarse con el exterior. A cada dispositivo se le asigna un puerto o puertos particulares para poder comunicarse con el chip 8088. (¡No todos los 65,536 puertos posibles son utilizados!). A modo de ejemplo, los puertos 992, 993 y desde el 1,000 hasta el 1,004 eran usados para comunicarse con las tarjetas para graficados que enviaban imágenes a los monitores de aquél entonces, mientras que la bocina de la computadora era manipulada a través del puerto 97. Siendo la bocina algo de mayor sencillez que los adaptadores para graficados, empezaremos utilizando a la bocina para ilustrar el uso de los puertos de entrada/salida.
El puerto 97 no controla directamente a la bocina. Lo que lleva a cabo esta función es otro circuito integrado, el Controlador de Interfaz Paralela Programable (PPI, Programmable Peripheral Interface) Intel 8255, el cual en sí por su complejidad interna puede ser considerado como un microprocesador elemental. Este circuito integrado cuenta con tres registros, cada uno de los cuales puede almacenar un número binario. La siguiente figura muestra el diagrama esquemático del chip 8255:
Los números binarios puestos en estos tres registros son lo que controla lo que hace el dispositivo. Cada registro está conectado con el microprocesador 8088 a través de un puerto, y el puerto 97 es el que está conectado al registro que controla la bocina interna. Programamos el control usando el puerto del microprocesador para cambiar el número en el registro del 8255. El número correcto puede hacer que la bocina emita un tono audible, mientras que el número equivocado puede ocasionar problemas. Por lo tanto, debemos saber qué número enviar y cómo enviarlo. En particular, tenemos que saber cómo usaremos el lenguaje C para enviar el número que queremos enviar. La siguiente ilustración nos muestra la manera en la que se lleva a cabo la interconexión entre el microprocesador Intel 8088 y el circuito integrado Intel PPI 8255:
Veamos primero cuáles son los números que podemos enviar. Cada registro del 8255 acepta un número binario de 8 bits, tal como el número 01011011. Cada uno de los 8 bits actúa como un interruptor del tipo “encendido-apagado” para algún dispositivo o acción, y la presencia de un “1” o un “0” en cierta posición dentro del número binario determina si el dispositivo está encendido o apagado. El bit 3, por ejemplo (si contamos los bits del 0 al 7 yendo de izquierda a derecha) determina si el motor de una grabadora de cinta tipo “cassette” está encendido, mientras que el bit 7 habilita e inhabilita el teclado. Obsérvese el por qué es necesario ser precavido. Si encendemos la bocina pero ignoramos los demás bits, ¡podemos terminar apagandof el teclado! Así que veamos en mayor detalle lo que hace cada bit examinando la siguiente tabla con información tomada directamente del manual de referencia técnica IBM para la computadora personal basada en el chip 8088 (la información se reproduce tal y como aparece en el manual de referencia IBM, que usa un lenguaje críptico que obviamente no estaba dirigido al público en general):
En esta tabla, cada “+” representa un “1” y cada “-” representa un “0” y a su vez, cada “1” indica que la condición es válida, mientras que un “0” es lo que se requiere para que la condición sea válida. De este modo, un “1” puesto en la posición del bit 3 significa que el motor del cassette está apagado, con lo cual haciéndolo “0” se enciende el motor del cassette, mientras que un “0” puesto en la posición del bit 4 significa que la línea leer/escribir de la memoria está habilitada, y para deshabilitar el acceso de la memoria es necesario poner un “1” en la posición del bit 4.
¿Cómo encendemos entonces la bocina de la computadora? Resulta que para que la bocina pueda ser encendida, tanto el bit “time 2 gate speaker” (bit 0) como el bit “speaker data” (bit 1) deben estar “encendidos”, o sea con un “1” puesto en cada uno de ellos. Esto significa que podemos “encender” la bocina enviándole el número binario 11 (cuyo equivalente es el número decimal 3) a través del puerto 97. Pero si enviamos dicho número tal cual entonces estaríamos enviando realmente el número binario 00000011, lo cual puede tener efectos colaterales tales como poner al bit 4 en la posición de “apagado”, y tal vez eso no sea lo que queramos que ocurra. Es precisamente por razones de este tipo por las cuales, cuando se tiene un acceso directo a las funciones del hardware, estamos obligados a ejercer precaución para evitar efectos indeseables.
Para estar del lado seguro, es prudente verificar el estado en el cual se encuentra el puerto 97 antes de tratar de cambiar el número binario que contiene. Y resulta que el número que normalmente contiene dicho puerto en una computadora IBM PC XT o IBM PC AT o compatible puede ser “76” o “77”. Traducidos estos dos números a su equivalente en el sistema binario, se tiene lo siguiente (el bit 7 es el que está más a la izquierda):
76 decimal equivale a 01001100
77 decimal equivale a 01001101
Sin entrar en detalles sobre lo que pueda significar “hold keyboard clock low”, resulta evidente que para estar más “a la segura” es mejor dejar intactos todos los bits desde el bit 2 hasta el bit 7, cambiando únicamente los bits 0 y 1 al valor de “1”. Esto corresponde a enviar al registro el número binario 01001111 (decimal 79) al registro. Como una precaución adicional, en el programa que escribamos en el lenguaje C podemos “leer” el valor original que se encuentra en el registro almacenándolo en alguna variable creada temporalmente para tal efecto, y después de haberse ejecutado el programa (después de haber “sonado” la bocina), restaurar el registro a este valor.
Hay dos cosas que podemos hacer con un puerto. Podemos enviar información desde el microprocesador al dispositivo anexado, o podemos leer información del dispositivo anexado enviándola al microprocesador. En un lenguaje ensamblador, estas tareas se llevan a cabo con instrucciones tales como OUT e IN. Al usar un compilador C, el método que se use depende del compilador. Algunos compiladores como Lattice C y Microsoft C ofrecen funciones análogas, y en este caso las palabras clave reservadas en el lenguaje C para el uso de los puertos son:
inp() out(p)
Si estamos usando un compilador C que no ofrezca funciones como éstas, entonces siempre podemos usar un lenguaje ensamblador para definirlas, y hacer que el compilador C invoque tales funciones elaboradas en lenguaje ensamblador.
A continuación se presenta un programa escrito en lenguaje C para “encender” y “apagar” la bocina de la computadora usando estas dos funciones para manipulación de los puertos.
/* Programa bip 1 */
/* un programa para hacer sacarle un sonido */
/* de "bip" a la computadora */
#include <stdio.h>
#include <dos.h>
/* El archivo de cabecera DOS.H es requerido para poder */
/* accesar a través del sistema operativo DOS las funciones */
/* inp() y outp() que corresponden a los puertos de */
/* entrada y salida */
void main(void)
{
int almacenar;
almacenar = inp(97);
/* obtener el valor actual del puerto 97 */
printf("El valor del puerto 97 es %d\n", almacenar);
/* confirmar */
outp(97,79);
/* enviar 79 al puerto 97, para encender la bocina */
outp(97,almacenar);
/* restaurar el puerto 97 a su valor inicial */
}
Un resultado típico de la ejecución del programa anterior es la siguiente impresión en la pantalla:
El valor del puerto 97 es 48
A estas alturas podemos darle un sentido a lo que hacen las funciones inp() y outp():
inp(numerodepuerto): La función nos regresa un valor entero de 8 bits (el cual es convertido a un valor int de 16 bits agregando ceros a la izquierda) sacado dicho valor del puerto de entrada numerodepuerto.
outp(numerodepuerto, valor): La función envía un valor entero de 8 bits al puerto de salida numerodepuerto.
Obsérvese que un mismo puerto puede ser tanto un puerto de entrada como un puerto de salida, dependiendo de la manera en que sea utilizado.
En las primeras computadoras personales caseras modeladas sobre la arquitectura pionera IBM, y no habiendo en ellas capacidades de generación sonido digital de alta calidad integradas en la tarjeta madre de la electrónica, en ausencia de ello los gabinetes tenían montada una pequeña bocina usada para emitir tonos audibles no con la finalidad de generar música de alta calidad sino para llamar la atención del usuario al estilo de mensajes de alerta audibles. El programa que se acaba de proporcionar, una vez compilado a lenguaje de máquina y ejecutado en una máquina apropiada (por ejemplo, a través de un emulador de las máquinas IBM-compatibles), hace que se emita un pequeño sonido a través de la bocina de la computadora. Este programa no es muy satisfactorio en virtud de que el tiempo programado es breve e invariable. Podemos mejorar esto de la siguiente manera:
/* Programa bip 2 */
/* un programa para prolongar el sonido "bip" */
/* de la computadora */
#include <stdio.h>
#include <dos.h>
/* Archivo de cabecera requerido para las funciones */
/* inp() y outp() */
#define LIMITE 10000
void main(void)
{
int almacenar;
int conteo = 0;
/* creacion de un contador de software */
almacenar = inp(97);
/* obtener el valor actual del puerto 97 */
while(conteo++ < LIMITE)
;
outp(97,79);
/* enviar 79 al puerto 97, encender la bocina */
outp(97,almacenar);
/* restaurar el puerto a su valor inicial */
}
Obsérvese que el único propósito del enunciado while es “quemar” tiempo, incrementando el valor de conteo que fue inicializado en cero hasta que llega al valor LIMITE, en cuyo punto se da por terminada la acción del bucle. El semicolon que sigue al enunciado while se puede tomar como un enunciado “nulo” que no hace esencialmente nada excepto fijar el alcance de la acción del bucle. De este modo, el programa enciende la bocina, cuenta hasta 10000, y apaga la bocina. Podemos ajustar el valor de LIMITE para controlar el tiempo que la bocina permanece encendida. O bien podemos reemplazar a LIMITE con una variable y utilizar la palabra reservada scanf() para leer un valor que controle la duración.
Hemos visto cómo controlar la duración de un tono emitido por la bocina que está integrada a la tarjeta madre de las computadoras compatibles IBM. Mejoraremos el programa anterior definiendo una función tono() que nos permitirá controlar también el tono (determinado por la frecuencia) del sonido. Si podemos controlar la duración de un tono y la frecuencia del mismo, tenemos entonces los principios esenciales para el diseño de música computarizada. Sin embargo, los tonos obtenidos no son de buena calidad musical en virtud de que la señal de audio en vez de estar formada por ondas senoidales está formada por un tren de pulsos que refleja los “unos” y “ceros” que se están enviando a la bocinita de la computadora (y de hecho, no es el nivel lógico lo que genera el sonido audible, sino el cambio ya sea de “cero” a “uno” o viceversa).
El prototipo de la función tono() será definido del modo siguiente:
void tono(int frecuencia, int tiempo);
Aquí la variable frecuencia representa la frecuencia del tono en ciclos por segundo (Hertz, que viene siendo lo mismo que lo que antes era llamado ciclos por segundo). La variable tiempo representa la duración del tono en décimas de segundo, un valor de 10 para el tiempo implica una duración de diez décimas, o un segundo.
Podemos controlar la duración del tono de la misma manera en que lo hicimos arriba, usando el puerto 97 para “encender” la bocina, usar un bucle repetitivo para “quemar” el tiempo que sea necesario, y usar nuevamente el puerto 97 para “apagar” la bocina. El siguiente fragmento de código en lenguaje C puede ayudarnos en lo que queremos hacer:
#define ESCALATIEMPO 1270
/* número de conteos en 0.1 segundo */
#define PUERTOBIP 97
/* el puerto que controla la bocina */
#define ENCENDIDO 79
/* requido para encender la bocina */
conteo = ESCALATIEMPO * tiempo;
/* convertir el tiempo a unidades de conteo */
puerto = inp(PUERTOBIP);
/* guardar el valor actual del puerto */
outp(PUERTOBIT,ENCENDIDO);
/* encender la bocina */
for (i=0; i<conteo, i++)
;
/* marcar el tiempo */
outp(PUERTOBIP,puerto);
/* apagar la bocina, restaurar condición original */
Aquí el valor de conteo determina qué tanto tiempo dura la bocina encendida. El factor ESCALATIEMPO convierte las décimas de segundo a un número equivalente de conteos (que debe ser un número entero). Obviamente, antes de encender la bocina queremos fijar la frecuencia del sonido emitido.
La frecuencia del tono puede ser fijada por otro dispositivo, el Temporizador de Intervalo Programable Intel 8253 (Programamble Interval Timer). Este controlador timer de temporización determina, entre otras cosas, cuántos pulsos por segundo son enviados a la bocina. El temporizador 8253 genera una frecuencia base de 1,190,000 Hertz, lo cual está mucho más allá de la frecuencia que puede ser percibida por el oído humano. Pero le podemos enviar al 8253 un número para dividir el batido de base. Por ejemplo, si le enviamos un divisor de 5000, podemos obtener una razón de pulsos de:
1,190,000/5000 = 238 Hertz
esto está ligeramente abajo de la nota “do” media del pentagrama musical. Por otro lado, si sabemos cuál es la frecuencia que queremos, podemos calcular el divisor que necesitamos simplemente diciendo:
divisor = 1,190,000/frecuencia
Nuestra función logrará llevar esto a cabo. Todo lo que necesitamos ahora es saber cómo alimentaremos el valor de divisor al 8253. Esto requiere el uso de dos puertos adicionales.
El primer paso consiste en poner al temporizador 8253 en el modo correcto de operación para recibir el divisor. Esto se logra enviando el valor decimal 182 (0xB6 en hexadecimal) al puerto 67. Una vez que hemos hecho esto, podemos enviar el divisor al puerto 66.
El envío del divisor presenta un pequeño problema. El divisor es un número de 16 bits, pero tiene que ser enviado en dos partes. Primero enviamos el byte de orden bajo, o sea los 8 bits finales del número. Tras esto, enviamos el byte de orden alto, o sea los 8 bits iniciales del número. En el programa que será elaborado llamaremos a estas dos partes bytebajo y bytealto, calculando sus valores con la ayuda de divisor de la siguiente manera:
bytebajo = divisor % 256;
bytealto = divisor / 256;
Alternativamente, también podríamos utilizar las siguientes operaciones usando los operadores bitwise (al modo de bits) que hemos estudiado arriba:
bytebajo = divisor & 255;
bytealto = divisor >> 8;
En esto último, el primer enunciado de cada par convierte los primeros 8 bits a ceros, dejando los últimos 8 bits como un número de 1 byte. Esto se puede apreciar mejor viendo en detalle la forma en la que funciona el operador de módulo y el operador bitwise AND (&). El segundo enunciado de cada par toma el valor original del divisor y desplaza todos los bits ocho lugares hacia la derecha (lo cual es equivalente a dividir entre 2n o 256). Los ocho bits más a la izquierda son fijados a 0, dejando un número de 8 bits consistente en los bits originales que estaban más a la izquierda.
A continuación tenemos la función tono() completa proporcionando los comentarios apropiados:
/* la funcion tono(frecuencia,tiempo) es usada para */
/* producir un tono de cierta frecuencia y duración */
#define MODOTIMER 182
/* código requerido para poner el timer en */
/* el modo correcto */
#define ESCALAFREC 1190000L
/* frecuencia basica en Hertz del temporizador */
#define ESCALATIEMPO 1230L
/* número de conteos en 0.1 segundo */
#define T_PUERTOMODO 67
/* el puerto controla el modo del temporizador */
#define PUERTOFREC 66
/* el puerto controla la frecuencia del tono */
#define PUERTOBIP 97
/* el puerto que controla la bocina */
#define ENCENDIDO 79
/* requido para encender la bocina */
void tono(int frecuencia, int tiempo)
{
int bytebajo, bytealto, puerto;
long i, conteo, divisor;
divisor = ESCALAFREC/frecuencia;
/* escalar frecuencia a unidades del timer */
bytebajo = divisor % 256;
bytealto = divisor / 256;
conteo = ESCALATIEMPO * tiempo;
/* convertir tiempo a unidades del timer */
outp(T_PUERTOMODO,MODOTIMER);
/* preparar el temporizador para entrada */
outp(PUERTOFREC,bytebajo);
/* fijar byte bajo del registro del timer */
outp(PUERTOFREC,bytealto);
/* fijar byte alto del registro del timer */
puerto = inp(PUERTOBIP);
/* encender la bocina */
for (i=0; i << conteo; i++)
;
/* marcar el tiempo */
outp(PUERTOBIP,puerto);
/* apagar la bocina, restaurar condiciones */
}
Hemos definido con #define a ESCALATIEMPO como un entero de tipo long de modo tal que los cálculos de ESCALATIEMPO = tiempo sean llevados a cabo en un entero de tipo long en lugar de un entero de tipo int ya que, en caso contrario, si el resultado resultara ser mayor que 32767, terminaría siendo truncado antes de que sea colocado en la variable conteo.
La función tono() que hemos definido en lenguaje C es una función que en buena medida duplica el enunciado SOUND del lenguaje BASIC que se utiliza en las computadoras personales IBM. Aquí la estaremos usando para crear un “teclado” algo limitado de ocho notas musicales, una octava. Nuestro “teclado” musical estará formado por las teclas que corresponden al renglón medio del teclado de la computadora, o sea las teclas a, s, d, f, g, h, j y k, las cuales corresponderán a las teclas del piano do, re, mi, fa, sol, la, si, do, y segundo do.
Este es el programa que nos permitirá convertir el teclado de la computadora en un teclado musical (en lugar de la notación musical do, re, mi, fa, etc., usaremos la notación de letras latinas C, D, E, F, etc.):
/* Programa teclado musical */
#include <stdio.h>
#include <conio.h>
#include <ctype.h>
/* definicion de las frecuencias de tonos musicales */
#define C 262
#define D 294
#define E 330
#define F 349
#define G 392
#define A 440
#define B 494
#define C2 524
#define MODOTIMER 182
/* código requerido para poner el timer en */
/* el modo correcto */
#define ESCALAFREC 1190000L
/* frecuencia basica en Hertz del temporizador */
#define ESCALATIEMPO 1230L
/* número de conteos en 0.1 segundo */
#define T_PUERTOMODO 67
/* puerto controla el modo del temporizador */
#define PUERTOFREC 66
/* puerto controla la frecuencia del tono */
#define PUERTOBIP 97
/* el puerto que controla la bocina */
#define ENCENDIDO 79
/* requido para encender la bocina */
void tono(int frecuencia, int tiempo);
void main(void)
{
int tecla, frecuencia, tempo, tiempo;
puts("Escribe el tempo basico: 10 = 1 segundo.");
scanf("%d", &tempo);
puts("Ahora usa las teclas A-K del teclado para tocar la nota.\n");
puts("La tecla Shift duplica la duracion. Un ! detiene la ejecucion.");
while ( (tecla = getch()) != '!')
{
tiempo = isupper(tecla)? 2 * tempo : tempo;
tecla = tolower(tecla);
switch (tecla)
{
case 'a' : tono( C, tiempo);
break;
case 's' : tono( D, tiempo);
break;
case 'd' : tono( E, tiempo);
break;
case 'f' : tono( F, tiempo);
break;
case 'g' : tono( G, tiempo);
break;
case 'h' : tono( A, tiempo);
break;
case 'j' : tono( B, tiempo);
break;
case 'k' : tono( C2, tiempo);
break;
default : break;
}
}
puts("El programa ha concluido\n");
}
void tono(int frecuencia, int tiempo)
{
int bytebajo, bytealto, puerto;
long i, conteo, divisor;
divisor = ESCALAFREC/frecuencia;
/* escalar frecuencia a unidades del timer */
bytebajo = divisor % 256;
bytealto = divisor / 256;
conteo = ESCALATIEMPO * tiempo;
/* convertir tiempo a unidades del timer */
outp(T_PUERTOMODO,MODOTIMER);
/* preparar el temporizador para entrada */
outp(PUERTOFREC,bytebajo);
/* fijar byte bajo del registro del timer */
outp(PUERTOFREC,bytealto);
/* fijar byte alto del registro del timer */
puerto = inp(PUERTOBIP);
/* encender la bocina */
for (i=0; i << conteo; i++)
;
/* quemar tiempo */
outp(PUERTOBIP,puerto);
/* apagar la bocina, restaurar condiciones */
}
Al ejecutarse el programa, aparece el query (interrogador):
Escribe el tiempo basico: 10 = 1 segundo.
Esto nos pide escribir el tiempo basico de duracion para cada sonido; escribimos 10 si queremos que el tono dure 1 segundo, escribimos 15 si queremos que dure segundo y medio, y así sucesivamente. Suponiendo que escogemos 10 para especificar 1 segundo de duración y oprimimos la tecla [Enter], a continuación aparecen los mensajes:
Ahora usa las teclas A-K del teclado para tocar la nota.
La tecla Shift duplica la duracion. Un ! detiene la ejecucion.
con lo cual se le informa al usuario que con la opresión en el teclado de las teclas a, s, d, f, g, h, j y k se podrá “tocar” la melodía que se quiera (en tono de C mayor).
La característica principal de este programa radica en el enunciado switch que asigna diferentes tonos musicales a las ocho teclas A-K que corresponden al renglón medio del teclado de la computadora. Además, por la manera en la cual se ha escrito el programa, el programa duplica la duración de cada nota musical si se utilizan letras mayúsculas, lo cual es checado con la función de biblioteca isupper(). Esta duración (tiempo) es fijada antes del enunciado switch, entonces las mayúsculas son convertidas a minúsculas con la función tolower() para reducir el número de etiquetas requeridas para la elaboración del programa.
Dependiendo del tipo de compilador C o del entorno IDE C usado, es posible que el programa anterior pueda ser compilado a un archivo ejecutable. Sin embargo, es igualmente posible que al ejecutarlo no se escuche absolutamente nada. ¿Por qué? En primer lugar, en muchas computadoras de cuño reciente se ha descontinuado el uso de la bocinita interna que solía formar parte del gabinete en virtud de que las tarjetas madre ya incluyen casi todas en su hardware el equivalente de una tarjeta de audio capaz de producir sonidos de alta calidad que son enviados hacia afuera haciendo obsoleta la bocinita, y para sacarle “tonos” a la máquina es necesario elaborar un programa C que se pueda comunicar con el hardware de sonido, para lo cual hay que tener a la mano la documentación de las funciones con las cuales se puede establecer una comunicación con la tarjeta de sonido. Un programa ejecutable elaborado para sacar sonido de una computadora añeja no producirá sonido audible si no se cuenta con esa bocinita interna. Y tiene que ser una computadora cuya electrónica debe poder reconocer las funciones diseñadas para ser procesadas por el viejo sistema operativo PC-DOS (MS-DOS), y esto tal vez sea pedir demasiado.
Históricamente, otra interacción directa de los programas en C con el hardware de la máquina ha estado ocurriendo al ir evolucionando los monitores de video. Hay dos modos posibles de interacción con el usuario:
1) El modo de texto
2) El modo gráfico
El modo de texto es el modo natural usado en los sistemas operativos de líneas de comandos (DOS, UNIX, etcétera), lo único que se puede poner en la pantalla son caracteres imprimibles desde el teclado, y era el modo predeterminado de operación en las primeras computadoras personales tipo IBM; mientras que el modo gráfico permite subdividir la pantalla en muchos puntitos con los cuales se pueden formar no solo caracteres de texto sino trazar elementos gráficos como líneas, círculos, elipses, etc. Las funciones de biblioteca como printf() y scanf() fueron concebidas para trabajar exclusivamente con el modo de texto, mientras que para poder trabajar en el modo gráfico fue necesario desarrollar otro tipo de funciones de biblioteca, altamente dependientes del tipo de hardware usado.
Inicialmente, las computadoras personales caseras solo permitían conectar monitores monocromáticos (de un solo color) del tipo CRT (tubo de rayos catódicos) que solo podían funcionar en el modo de texto. Esta fue la manera de trabajar por algunos años. Paulatinamente, se fueron introduciendo monitores de color CRT que permitían trabajar en el modo de texto con una cantidad limitada de colores. Esto fue seguido por la introducción de tarjetas capaces de producir no solo texto en colores sino capaces de trabajar tanto en modo de texto como en modo gráfico, aumentando en capacidad de resolución y variedad de colores. El trazado de las figuras requiere en la máquina de un adaptador de video (también llamado adaptador gráfico) con capacidad cromática para producir colores, lo cual puede ser un circuito integrado o bien una tarjeta de hardware insertable en una de las ranuras vacías slot disponibles en las tarjetas madre en las que va montado el procesador CPU. Las primeras computadoras caseras basadas en los procesadores Intel solo podían poner en las pantallas de los monitores monocromáticos CRT líneas de texto, eran las computadoras que funcionaban con ventanas de líneas de comandos tipo DOS y UNIX; y poco después aparecieron en el mercado los adaptadores de video Hercules que subdividieron la pantalla monocromática CRT en 720×348 puntitos de imagen (o sea, una capacidad para poner 720 puntos horizontales y 348 puntos verticales); para un total de 250,560 puntitos luminosos en la pantalla. Pero eran puntitos de un solo color. Posteriormente llegaron los adaptadores de video CGA que podían poner puntitos de color en la pantalla del tubo de rayos catódicos, un total de 320×200 puntitos, aunque con una cantidad muy limitada de 16 colores; requiriéndose también obviamente) no un monitor monocromático sino un monitor capaz de poder reproducir colores. Pero poco tiempo después, beneficiándose de la disponibilidad cada vez más amplia de tarjetas de video con mayores resoluciones y mayor variedad de colores, hicieron su aparición los sistemas operativos Windows (empezando con Windows 95) que tomaron un control completo de todo lo que era puesto en la pantalla. Esta fue una época de convivencia en la cual Windows se las arregló para permitir que programas con uso intensivo de graficados (juegos de ajedrez, simuladores de vuelos, etcétera) se pudieran ejecutar en una máquina con el sistema operativo Windows instalado en ella. Pero al ir evolucionando Windows, la convivencia se tornó precaria, y cualquier programa basado en graficados y animaciones tenía que plegarse a las normas internas usadas por los sistemas operativos Windows. Y muchos programas gráficos que antes se podían ejecutar sin interferencia alguna de parte del sistema operativo dejaron de ejecutarse, y muchos de los que se podían ejecutar empezaron a producir efectos inesperados como el tomar posesión aparente de toda la pantalla sacando fuera la barra de tareas y el escritorio en la pantalla de Windows, y cosas por el estilo. Eventualmente llegó la nueva tecnología de pantallas planas construídas con diodos emisores de luz LED que mandaron a la obsolescencia a los viejos y voluminosos tubos de rayos catódicos.
Puesto que la traducción al castellano de la palabra Windows es “ventana”, muchos podrían suponer (erróneamente) que el concepto de las ventanas nació con la interfaz Windows 1.0 de Microsoft. Sin embargo, desde antes de la introducción de Windows, inclusive desde antes de la introducción de los adaptadores de video para permitir la activación en las computadoras del modo gráfico, ya se manejaba el concepto de las “ventanas”, definiéndose una ventana como un portal rectangular usado por el programa para enviarle mensajes al usuario. Las primeras ventanas de amplio uso fueron las ventanas de texto. La ventana predeterminada era la pantalla completa, esto con la intención de que el programador no tuviera que preocuparse por crear una ventana especial para poder usar las rutinas de texto y graficas.
Como una muestra de la creación de “ventanas” con una computadora trabajando en el modo de texto, se presenta un programa C diseñado para correr en un monitor monocromático que va produciendo sucesivamente varias ventanas de texto que se van acumulando en la pantalla produciendo un efecto visual como el siguiente (importante: los caracteres asterisco con los cuales se trazan los barandales para cada ventana NO son de colores, ya que se supone que se está usando un monitor monocromático, y son realmente del mismo color gris que el de las letras; los barandales fueron resaltados con colores distintos para una mejor comprensión didáctica; recomendándose la ampliación de la figura para mejor apreciación de los detalles):
El programa C en cuestión que produce lo anterior es el siguiente:
#include <stdio.h>
#include <conio.h>
void barandal(int, int, int, int);
main(void)
{
clrscr();
/* trazar un barandal grande en torno a toda la pantalla visible */
barandal(1, 1, 79, 25);
/* crear la primera ventana */
window(3, 2, 45, 9);
barandal(3, 2, 45, 9);
gotoxy(3, 2);
printf("primera ventana");
getch();
/* crear la segunda ventana */
window(30, 10, 65, 18);
barandal(30, 10, 65, 18);
gotoxy(3, 2);
printf("segunda ventana");
gotoxy(5,4);
printf("todavia en la segunda ventana");
getch();
/* regresar a la primera ventana */
window(3, 2, 40, 9);
gotoxy(3, 3);
printf("de regreso en la primera ventana");
getch();
/* creacion de una ventana traslapante */
window(5, 5, 50, 15);
barandal(5, 5, 50, 15);
gotoxy(2, 2);
printf("esta ventana traslapa las primeras dos");
getch();
return 0;
}
/* funcion para trazar un barandal en torno a una ventana */
void barandal(int iniciox, int inicioy, int finx, int finy)
{
register int i;
gotoxy(1, 1);
for(i=0; i<=finx-iniciox; i++)
putch('*');
gotoxy(1, finy-inicioy);
for(i=0; i<=finx-iniciox; i++)
putch('*');
for(i=2; i<finy-inicioy; i++) {
gotoxy(1, i);
putch('*');
gotoxy(finx-iniciox+1, i);
putch('*');
}
}
Obsérvese el modificador de tipo en la función barandal() que pide que todo lo que sea manejado por la variable de tipo entero i sea manejado por un registro interno de la unidad de procesamiento central CPU. Estos modificadores suelen ser usados para incrementar de modo significativo la velocidad de procesamiento, habido el hecho de que las operaciones que involucran transferencias del RAM al CPU (y viceversa) son más tardadas que las operaciones llevadas a cabo usando registros internos del CPU.
La primera función del programa anterior es clrscr(), usada para “limpiar” el interior de la ventana activa (no la pantalla completa del monitor), que consiste en borrar todo el texto que pueda haber en el interior de la ventana activa. La función barandal() tiene como propósito poner un borde visible en torno a cada ventana, ya que de otro modo las ventanas creadas con la función window() no serían visibles. Y la función gotoxy() se encarga de posicionar el cursor (en el modo de texto) dentro de la ventana activa. Con el primer conjunto de instrucciones:
window(3, 2, 45, 9);
barandal(3, 2, 45, 9);
gotoxy(3, 2);
printf("primera ventana");
getch();
se crea una primera ventana dentro de la cual se escribe el texto “primera ventana”. Con el siguiente conjunto de instrucciones:
window(30, 10, 65, 18);
barandal(30, 10, 65, 18);
gotoxy(3, 2);
printf("segunda ventana");
gotoxy(5,4);
printf("todavia en la segunda ventana");
getch();
se crea una segunda ventana, que se vuelve la ventana activa, escribiéndose el texto “segunda ventana” a partir de la posición (3,2), y el texto “todavia en la segunda ventana” a partir de la posicion (5,4). Con el siguiente conjunto de instrucciones:
window(3, 2, 40, 9);
gotoxy(3, 3);
printf("de regreso en la primera ventana");
getch();
la segunda ventana deja de ser la ventana activa y la primera ventana se vuelve la ventana activa. Obsérvese que lo único que podemos poner dentro de cualquier ventana es texto, no podemos trazar líneas o círculos porque para ello se requiere que la computadora tenga instalado un adaptador gráfico que pueda ponerla en modo gráfico.
El modo de texto no ofrece muchos problemas para su emulación en un entorno IDE contemporáneo. Pero si queremos elaborar un programa C para trabajar en el modo gráfico, lo más seguro es que nos toparemos con problemas. Considérese el siguiente programa. Aún si el programa puede ser compilado a un archivo ejecutable, lo más probable es que el programa no se pueda correr a menos de que el lector tenga disponible una computadora añeja que debe corresponder a cierta época muy específica, ni antes ni después:
#include <graphics.h>
#include <conio.h>
main(void)
{
int driver, modo;
register int i;
driver = VGA;
modo = VGAMED;
initgraph(&driver, &modo, "");
line(0, 0, 200, 150);
line(50, 100, 200, 125);
for(i=0; i<319; i+=10)
putpixel(i,100,RED);
circle(50,50,35);
circle(100,160,100);
getch();
restorecrtmode();
return 0;
}
En el programa anterior, obsérvese que se incluye el archivo de cabecera GRAPHICS.H, el cual se requiere para activar la tarjeta gráfica (o el chip de video), y cuyo nombre será diferente de un sistema a otro. Usualmente y en forma predeterminada, al encender una computadora el adaptador gráfico se encuentra en el modo de texto. Desde el inicio del programa C anterior, en una operación conocida como la inicialización del adaptador de video, el adaptador de video (en caso de que no sea puesto en el modo gráfico previamente por acción del sistema operativo) es puesto en el modo gráfico específicamente con las instrucciones:
driver = VGA;
modo = VGAMED;
initgraph(&driver, &modo, "");
Inspeccionando las variables driver y modo con las que se ha especificado el tipo de monitor, esto implica que el programa solo podrá ser utilizado en máquinas con tarjetas de video y monitores VGA, lo cual se puede considerar sumamente restrictivo al no poder usarse el programa en otros monitores con otros adaptadores de video como CGA y EGA. Un programa más amplio que permite adecuarse al adaptador gráfico y el monitor que tenga instalados el sistema usa el macro DETECT para detectar lo que esté presente, con las siguientes instrucciones:
driver = DETECT;
initgraph(&driver, &modo, "");
Cuando se usa algo como el macro DETECT, la función de inicialización initgraph() detecta automáticamente el tipo de hardware de video presente en el sistema y selecciona el modo de video con la mayor resolución posible, lo cual hace que las variablas driver y modo sean puestas a los valores apropiados. Sin esto, sería necesario estar consultando una tabla como la siguiente (la tabla refleja la evolución e inclusión gradual de monitores de color de tecnología de tubos de rayos catódicos, y aunque los monitores CRT hoy son obsoletos, las variedades de monitores y pantallas que continuamente son introducidos en el mercado requiere del conocimiento actualizado de lo que hay disponible, para poder adaptar los programas elaborados a la mayor cantidad posible de máquinas en las cuales se puedan ejecutar):
Driver | Modo | Equivalente | Resolucion |
CGA | CGAC0 | 0 | 320x200 |
CGAC1 | 1 | 320x200 | |
CGAC2 | 2 | 320x200 | |
CGAC3 | 3 | 320x200 | |
CGAHI | 4 | 640x200 | |
MCGA | MCGAC0 | 0 | 320x200 |
MCGAC1 | 1 | 320x200 | |
MCGAC2 | 2 | 320x200 | |
MCGAC3 | 3 | 320x200 | |
MCGAMED | 4 | 640x200 | |
MCGAHI | 5 | 640x480 | |
EGA | EGALO | 0 | 640x200 |
EGAHI | 1 | 640x350 | |
EGA64 | EGA64LO | 0 | 640x200 |
EGA64HI | 1 | 640x350 | |
EGAMONO | EGAMONOHI | 3 | 640x350 |
HERC | HERCMONOHI | 0 | 720x348 |
VGA | VGALO | 0 | 640x200 |
VGAMED | 1 | 640x350 | |
VGAHI | 2 | 640x480 | |
PC3270 | PC3270HI | 0 | 720z350 |
IBM8514 | IBM8514LO | 0 | 640x480 |
IBM8514HI | 1 | 1024x768 |
Se ha destacado en color gris el tipo de adaptador gráfico que se usó comercialmente a gran escala en las computadoras personales caseras para empezar a salir del modo de texto y empezar a manipular todo lo que aparece en la pantalla en forma gráfica (incluyendo los mismos caracteres de texto), la Hercules Graphic Card, que obviamente era para monitores monocromáticos. La tabla anterior refleja, de un modo incompleto y parcial, válida para el año 1989, la lista creciente de adaptadores gráficos y monitores disponibles para uso casero conforme iba avanzando la tecnología basada en los tubos de rayos catódicos CRT.
Ya con el modo gráfico activado, se puede “encender” individualmente en cualquier lugar de la pantalla un puntito luminoso con una función como putpixel(), y se pueden trazar figuras geométricas en cualquier parte de la pantalla tales como líneas rectas con la función line(), rectángulos con la función rectangle(), o círculos con la función circle(). Asimismo, se puede rellenar una figura geométrica con cierto color usando una función como floodfill(). Y para regresar del modo gráfico al modo de texto se usan funciones como closegraph() y restorecrtmode().
El problema es que si queremos correr el programa que acabamos de ver en una computadora que esté funcionando bajo un sistema operativo como Windows Vista (o en general, cualquier Windows) o un sistema operativo MacOS, o cualquier otro sistema operativo contemporáneo, de inmediato se produce un conflicto porque el sistema operativo desde un principio toma control total del modo de texto o bien el modo de graficado. Para poder correr el programa anterior, el sistema operativo tendría que ceder el control del monitor en todos sus detalles, perdiendo su propio control sobre lo que pueda aparecer en la pantalla. Aunque algunos de los primeros sistemas operativos Windows llegaron a permitir esto para que ciertos programas DOS se pudieran seguir ejecutando bajo Windows, llegó el momento en que era casi imposible sostener la alianza.
La ejecución del programa C que hemos visto arriba ciertamente requiere que la computadora tenga instalado un adaptador gráfico (o un chip de video). Pero las funciones gráficas serán ignoradas en una computadora que tenga un sistema operativo como Windows instalado en ella. O sea, el programa solo se puede ejecutar en máquinas posteriores al tiempo en el cual hicieron su aparición los adaptadores gráficos, pero previos al tiempo en el cual la instalación de los sistemas operativos Windows fue la norma, ni antes ni después.
¿Significa lo anterior que todo programa que haga uso de funciones gráficas pueden quedar en una obsolescencia total? No necesariamente. Bajo cualquier convención de graficado posible en cualquier lenguaje de programación, sin importar las cosas que vayan incorporando los avances en la tecnología, tendrá que haber funciones y bibliotecas disponibles para poder lograr tales cosas, ya que de lo contrario el sistema se volvería inutilizable. Y tan importante como las interfaces visuales con el usuario lo son los algoritmos que hacen posible muchas cosas. Detrás del diseño de un buen programa de ajedrez hay una serie de rutinas que codifican estrategias que el usuario del programa no llega a ver jamás excepto por sus efectos en la evolución del juego, y aunque cambie la presentación visual del tablero las estrategias bajo un cierto algoritmo siguen siendo las mismas y lo único que se requiere es volver a compilar las rutinas de las estrategias para los nuevos entornos y sistemas operativos. Desde esta perspectiva, los conocimientos adquiridos por los programadores en realidad nunca caen en la obsolescencia, solo tienen que ser actualizados para poder aprovechar el hardware que esté disponible.
En una máquina que tenga un sistema operativo Windows instalado en ella, no es posible invocar funciones de graficado para trazar figuras geométricas puestas encima de todo lo que se muestra en el Escritorio (Desktop). Sin embargo, se pueden trazar figuras geométricas adentro de una ventana Windows creada para tales propósitos. Y para ello, las funciones de graficado que se invocan son suministradas por el mismo Windows a través de una biblioteca originalmente conocida como la Interfaz de Programación de Aplicaciones Windows API (Application Programming Interface). Los programas se pueden seguir escribiendo en C, pero se requiere de modificaciones que introducen un mayor grado de complejidad al tener que mantener compatibilidad e interacción con lo que permite y no permite el sistema operativo.
En lo que hemos visto, queda claro que un programa C diseñado para sacarle sonido a una computadora añeja a través de su bocinita interna no funcionará en una computadora moderna que en lugar de la bocinita tenga un sistema integrado de sonido digital, y un programa C diseñado para dibujar figuras geométricas en la pantalla del monitor tampoco funcionará si las funciones de graficado usadas en el programa son obsoletas para la gran capacidad de resolución y variedad de colores que pueden generar las computadoras modernas, esto además de las instrucciones no sean reconocidas o no sean permitidas por el sistema operativo. ¿Entonces qué hay de la supuesta portabilidad de los programas elaborados en C? La cruda realidad es que la portabilidad de los programas elaborados en lenguaje C es una verdad a medias; los programas en C son portátiles en tanto que no incluyan funciones que requieran de una interacción directa con el hardware del sistema. Si se requiere tomar control directo de algún componente del hardware de cierta máquina, el programa C diseñado para tal máquina no puede ser compilado para otra máquina que use un hardware distinto, no sin efectuar lo que pueden ser cambios mayúsculos en las funciones que el programa tiene que utilizar. La programación de sistemas, sin importar el lenguaje de programación que se utilice, no puede ser portátil en virtud de que los programas de sistemas necesariamente tienen que reflejar el tipo de arquitectura y los recursos de hardware que son peculiares a cada tipo de máquina, como igual ocurre con los lenguajes ensambladores. Afortunadamente, hay otras cosas que deben funcionar sin cambio alguno al pasar de una máquina a otra, tales como la implementación de algoritmos para resolver ciertos problemas tales como la evaluación de funciones trigonométricas, o las estructuras de dato usadas para crear bases de datos y módulos que puedan funcionar eficientemente. Conclusión: en el arte de la programación, aunque la práctica es importante para poder familiarizarse con las capacidades del hardware, igualmente lo es la teoría, porque los principios generales de la informática son de alcance universal y esos no cambian.