Ya conocemos el hardware, ahora hay que reconocer sus posibles configuraciones
y atenerse a ellas.
3.3.1.- Detección de la Sound Blaster
Ahora podemos analizar la parte del código que se encarga de
la detección de la SB y de sus características, en el siguiente
procedimiento que se encuentra en el módulo "VOC.C", pero
antes hay que conocer ciertas constantes y variables incluidas en el módulo
"VOC.H".
WORD longitud=0; WORD frecuencia=0; WORD alojado;
WORD BPORT=0x210; WORD XPORT=0x216; WORD WPORT=0x21c;
WORD RPORT=0x21a; WORD APORT=0x21e; BYTE READY=0xaa;
Y el procedimiento para la detección de la SB, que incluye la
explicación de lo que hace, en el propio código, es:
// Si se encuentra la Sound Blaster, devuelve un 1. Un 0 si no se encuentra
BOOL DetectarSB(void)
{
BYTE estado=0,cont;
// Intentamos encontrar el puerto de conexión
while ((estado!=READY)&&(BPORT<0x270))
{
EscribeEnPuerto(XPORT,1);
// Reseteo de la Sound Blaster
// Como mínimo
hay que esperar 3,3 milisegundos.5 para asegurarnos
delay(5);
EscribeEnPuerto(XPORT,0);
cont=0;
while ((estado!=READY)&&(cont<100))
{ estado=LeeDePuerto(RPORT);
cont++; }
// Se buscan los puertos
de la Sound Blaster
if (estado!=READY)
{
BPORT+=0x10; XPORT+=0x10; WPORT+=0x10;
PORT+=0x10; APORT+=0x10;
}
}
if (BPORT!=0x270)
{
while ((LeeDePuerto(WPORT)&128)!=0);
EscribeEnPuerto(WPORT,0xd1);
// Activa la salida
digital mediante el DAC
SpeakerOn();
return (1);
}
return (0);
}
Hasta ahora se ha presentado una visión introductoria de la SB,
ya que en sí misma es secundaria. Pero lo que nos importa de verdad
es lo que puede hacer por nosotros. Ya hemos detectado su presencia y reconocidos
los valores de los puertos BASE y del DSP. Pasemos a lo que realmente es
el motivo de su uso, aunque primero es imprescindible especificar el modo
de funcionamiento que podemos darle a la SB. Se basan en:
-
Temporizador del PC (Timer)
-
Acceso directo a memoria (DMA)
3.3.2.- Generación de Sonido por el Temporizador
Todo ordenador dispone de un temporizador (timer), el cual está
ubicado en una interrupción de usuario. Esta interrupción
es llamada por el ordenador 18,2 veces por segundo y sirve para mantener
la fecha, la hora del sistema y otros servicios internos.
Un aspecto importante de esta interrupción es que en todos los
procesadores, ya sea un 8086 o un 80486, esta interrupción es llamada
con la misma frecuencia, permitiendo así que la fecha o la hora
del sistema, por ejemplo, se actualicen a la misma velocidad en todo tipo
de procesadores. Si no fuese así, el tiempo "pasaría"
más deprisa en un ordenador más potente que en un ordenador
un poco más lento.
Las interrupciones, como se sabe, realizan diversas funciones. Para
entenderlo claramente podemos decir que una interrupción es un servicio
que nos permite simplificar una tarea determinada, tal como cambiar el
modo gráfico (int 10h), o gestionar el ratón (int 33h).
Mediante el Ensamblador Inmerso usado conjuntamente con el C,
el vector que apunta a una interrupción determinada puede ser cambiado,
y hacer que apunte a una rutina de usuario.
Me explico: si conseguimos "colgar" una rutina de usuario en
el vector de interrupción del teclado (9h), podremos hacer que nuestra
rutina se ejecute cada vez que nosotros pulsamos una tecla... ¿por
qué?. porque cada vez que se pulsa una tecla, el ordenador llama
a la interrupción 9h para gestionar el evento ocurrido, a partir
de esta llamada se llena el búfer del teclado, se realizan una serie
de procesos y aparece el carácter deseado en pantalla (normalmente).
Pero si cambiamos esa interrupción y hacemos que en vez de apuntar
a una posición de memoria determinada, apunte hacia nuestra rutina,
conseguiremos que el ordenador llame a otra dirección de memoria,
lo que provocará que se realice otro proceso distinto al original.
De la misma manera que podemos interceptar la interrupción 9h,
también podemos interceptar el vector de interrupción del
temporizador (8h). De esta manera nuestra rutina se ejecutaría 18.2
veces por segundo porque el ordenador llama él solito a la interrupción
8h 18.2 veces por segundo.
En definitiva, la generación de sonido vía temporizador
se basa en los siguientes pasos:
-
Crear una rutina que envíe un byte de un puntero al puerto de escritura
de la SB, de esta manera cada vez que se llame al timer (18,2 veces por
segundo) se enviará sólo un byte del puntero que contiene
el sonido digitalizado (un número de 8 bits).
-
Reprogramar el temporizador para que se ejecute más de 18,2 veces...
22000 veces, por ejemplo (algo totalmente posible). Este valor tomado como
ejemplo, representa en definitiva el número de bytes que queremos
mandar en un segundo. A este valor lo llamamos frecuencia de muestreo,
concepto presentado anteriormente, expresado en Hertzios. Por lo tanto,
si mandamos bytes a una frecuencia de 22000 bytes por segundo, nuestro
sonido sonará a 22000Hz (22 Khz).
-
"Colgar" nuestra rutina en la interrupción 8h, de esta manera
se ejecutará la rutina 22000 veces.
Todo esto es muy bonito y en realidad funciona tal y como se pretende.
Sería la forma más cómoda y fácil de generar
el sonido digitalizado. Pero a pesar de esto, tiene mucho que desear.
Si colgamos una rutina del temporizador, cada tantas veces por segundo,
el temporizador interrumpe a la CPU para realizar una operación
extremadamente lenta como es enviar un byte al puerto de la SB.
Este proyecto intenta ser lo más rápido posible para conseguir
hacer algo que realmente pueda calificarse como "multimedia", es
decir, utilizar los recursos del ordenador de forma conjunta para un determinado
objetivo. Así, si estamos interpretando un fichero de animación
como los FLI (utilizables gracias a este curso), que requieren un
gran número de operaciones y deseamos a la vez reproducir un sample
utilizando la técnica anteriormente expuesta, dependiendo del ordenador,
la animación se ralentizará demasiado (intolerable en este
curso), ya que el proceso de cálculo se verá interrumpido
numerosas veces por segundo, con lo que si queremos una buena frecuencia
de muestreo, 18000 por ejemplo, la animación se moverá a
duras penas, dando tirones y lástima.
3.3.3.- Generación de Sonido a través del DMA
Como se ha visto, la técnica anterior es válida si, por
ejemplo, tenemos una pantalla fija y queremos que a la vez suene un sample.
Pero en el momento en que se quieran hacer dos cosas que necesiten al procesador
al mismo tiempo, la técnica del timer no vale, sencillamente porque
no es óptima y no se puede combinar con la lectura de ficheros FLI,
uno de los objetivos de este Proyecto.
Sencillamente por esto, necesitamos un sistema por el cual pudiéramos
liberar a la CPU de todo el trabajo, permitiendo una generación
de sonido totalmente independiente del ordenador. Pero estando en el mundo
del PC, pensar en que el ordenador en su conjunto haga algo sin la interacción
de la CPU, motor de todo, es casi impensable.
A su vez, pensando en cómo hacer algo independiente de la CPU,
teniendo en cuenta que la SB incorpora un chip DSP que trabaja por sí
solo... una transmisión de datos sin que medie la CPU... una transmisión
directa desde la memoria a la SB que tiene sus propios registros (o sea,
memoria)... una transmisión directa de memoria a memoria... ¡pero
si para eso está el DMA!
Las transferencias por DMA (Direct Memory Access) permiten liberar
a la CPU de todo el trabajo, evitando así los problemas antes mencionados.
Pero toda transferencia por DMA requiere unos protocolos, unas leyes, unas
normas a seguir, y que se deben seguir estrictamente si no queremos bloquear
nuestro ordenador, o queremos obtener una calidad de reproducción
óptima a la vez que el fichero FLI se interpreta por parte de la
CPU.
Analicemos cómo usar el DMA.
Lo primero que hay que tener en cuenta es el nuevo modelo de representación
de posiciones de memoria que se usa, ya que estamos hasta ahora acostumbrados
al típico esquema de:
Segmento:Desplazamiento
Pues bien, si queremos transmitir un bloque de memoria de una dirección
a otra mediante DMA, no debemos usar este tipo de expresión: debemos
decirle al DMA la "página" de memoria y el "desplazamiento"
de memoria donde se encuentra el bloque a mover. Ahora,
Segmento:DesplazamientoS
apunta al bloque en cuestión. El proceso de transformación
de la dirección normal a la nueva es el siguiente.
DesplazamientoS = Segmento shl 4 + Desplazamiento
Página = (Segmento + Desplazamiento shr 4) shr 12
A partir de aquí, deberemos decir al DMA que envíe bytes
de la posición Página:DesplazamientoS a la SB, y deberemos
configurar el DSP para que simplemente vaya leyendo los bytes que le llegan
por DMA y los vaya interpretando por su DAC. De esta manera oiremos
el sonido.
Pero antes de hacer esto, deberemos decirle al DSP la frecuencia de
muestreo a la que debe reproducir el sonido. Aquí hay que tener
cuidado; no basta con decirle la frecuencia en Hertzios, sino que debemos
dársela de forma que él lo entienda (al igual que la conversión
a Página:Desplazamiento). El proceso para convertir de Hertzios
a la frecuencia del DSP es la siguiente:
FrecuenciaDSP = 256- (1000000-FrecuenciaHertzios)
Luego, deberemos enviarle este valor resultante al DSP... pero ¿como
escribimos valores en el DSP? Es muy sencillo (claro, una vez que has estado
dos días intentando comprender por qué se bloquea el ordenador
y de repente piensas: ¿y si pongo "mov al, byte ptr valor"
en lugar de "mov ax, word prt valor"?)
Para escribir cualquier tipo de valor en el DSP deberemos usar los puertos
de la SB antes mencionados. El siguiente procedimiento que también
se encuentra en el módulo "VOC.C" se encarga de esto, y en
él podrá apreciarse (de nuevo) el uso del Ensamblador
Inmerso.
void EscribeEnDSP(BYTE valor)
{
test1:
asm{
mov dx,WPORT
in al,dx
and al,0x80
cmp al,0
jnz test1
mov al,byte ptr valor
out dx,al
}
}
Según los valores que escribamos en el DSP, la tarjeta de sonido
realizará diversas funciones, tales como activar el DAC mediante
la función:
void SpeakerOn(void);
O desactivarlo mediante
void SpeakerOff(void);
Y otras más que posteriormente veremos.