El 8253/4 es un chip temporizador que puede ser empleado como reloj de tiempo real, contador de
sucesos, generador de ritmo programable, generador de onda cuadrada, etc. En este capítulo, la información
vertida estará relacionada con el 8254 que equipa a los AT, algo más potente que el 8253 de los PC/XT; sin
embargo, las pocas diferencias serán comentadas cuando llegue el caso.
12.3.1 - DESCRIPCIÓN DEL INTEGRADO.
Este circuito integrado posee 3 contadores totalmente independientes, que pueden ser programados
de 6 formas diferentes.
El buffer del bus de datos, de 8 bits y tres estados, comunica el 8254 con la CPU. La lógica de
lectura y escritura acepta entradas del bus y genera señales de control para las partes funcionales del 8254.
Las líneas A0..A2 seleccionan uno de los tres contadores o el registro de la palabra de control, para poder
leerlos o escribirlos. El registro de la palabra de control es seleccionado cuando A0=A1=1, este registro
sólo puede ser escrito (se puede obtener información de estado, como se verá más adelante, con el comando
read-back del 8254, no disponible en el 8253). Los contadores 1, 2 y 3 son idénticos en su funcionamiento,
por lo que sólo se describirá uno; son totalmente independientes y cada uno de ellos puede ser programado
en una modalidad diferente. Si se observa el esquema de un contador, a la derecha, se verá el registro de la
palabra de control: aunque no es parte del contador propiamente dicho, afecta a su modo de funcionamiento.
El registro de estado, cuando es transferido al correspondiente latch, contiene el valor en curso del registro
de la palabra de control y alguna información adicional (como se verá después en el comando read-back).
El contador propiamente dicho está representado en la figura por CE (Counting Element) y es un contador
descendente síncrono de 16 bits que puede ser inicializado. OLM y OLL son dos latch de 8 bits (OL significa
Output Latch; los subíndices M y L están relacionados con el más y el menos significativo byte,
respectivamente); ambos son referenciados normalmente como un conjunto denominado OL a secas. Estos
latches siguen normalmente la cuenta descendente de CE, pero la CPU puede enviar un comando para
congelarlos y poder leerlos; tras la lectura continuarán siguiendo a CE. La lógica de control del contador se
encarga de que un sólo latch esté activo a un tiempo, ya que el bus interno del 8254 es de 8 bits. CE no
puede ser nunca leído directamente (lo que se lee es OL). De manera análoga, existen un par de registros
CRM y CRL (CR significa Count Register) que almacenan la cuenta del contador y se la transmiten
convenientemente a CE. Los valores de cuenta se escriben siempre sobre CR (y no directamente sobre CE).
La lógica de control gestiona la conexión con el exterior a través de las líneas CLK, GATE y OUT.
DESCRIPCIÓN OPERACIONAL
Tras el encendido del ordenador, el 8254 está en un estado indefinido; con un modo, valor de cuenta
y estado de salida aleatorios. Es entonces cuando hay que programar los contadores que se vayan a emplear;
el resto, no importa dejarlos de cualquier manera.
Programación del 8254.
Para programar un contador del 8254 hay que enviar primero una palabra de control y, después, un
valor de cuenta inicial. Los contadores se seleccionan con las líneas A0 y A1; el valor A0=A1=1 selecciona
la escritura de la palabra de control (en la que se identifica el contador implicado). Por tanto, el 8254 ocupa
normalmente 4 direcciones de E/S consecutivas ligadas a los contadores 0, 1, 2 y al registro de la palabra
de control. Para enviar la cuenta inicial se utiliza simplemente el puerto E/S ligado al contador que se trate.
El formato de la palabra de control es:
Operaciones de escritura.
El 8254 es muy flexible a la hora de ser programado. Basta con tener en cuenta dos cosas: por un
lado, escribir siempre primero la palabra de control, antes de enviar la cuenta inicial al contador. Por otro,
dicha cuenta inicial debe seguir exactamente el formato seleccionado en la palabra de control (enviar sólo
byte bajo, enviar sólo byte alto, o bien enviar ambos consecutivamente). Teniendo en cuenta que cada
contador tiene su propio puerto y que la palabra de control indica el contador al que está asociada, no hay
que seguir un orden especial a la hora de programar los contadores. Esto significa que, por ejemplo, se puede
enviar la palabra de control de cada contador seguida de su cuenta inicial, o bien enviar todas las palabras
de control para los 3 contadores y después las 3 cuentas iniciales; también es válida cualquier combinación
intermedia de estas secuencias (por ejemplo: enviar la palabra de control para el contador 0, después la
palabra de control para el contador 1, después la parte baja de la cuenta para el contador 0, luego la parte
baja de la cuenta para el contador 1, la parte alta de la cuenta para el contador 0, etc...).
Un nuevo valor de cuenta inicial puede ser almacenado en un contador en cualquier momento, sin que ello
afecte al modo en que ha sido programado (el resultado de esta operación dependerá del modo, como se verá
más adelante). Si se programa el contador para leer/escribir la cuenta como dos bytes consecutivos (bajo y
alto), el sentido común indica que entre ambos envíos/recepciones no conviene transferir el control a una
subrutina que utilice ese mismo contador para evitar un resultado incorrecto.
Operaciones de lectura.
Existen tres posibles métodos para leer el valor de un contador en el 8254. El primero es el comando
Read-Back, sólo disponible en el 8254 (y no en el 8253), como luego veremos. El segundo consiste en leer
simplemente el contador accediendo a su puerto correspondiente: este método requiere inhibir la entrada CLK
al contador (por ejemplo, a través de la línea GATE o utilizando circuitería exterior de apoyo) con objeto
de evitar leer la cuenta en medio de un proceso de actualización de la misma, lo que daría un resultado
incorrecto. El tercer método consiste en el comando de enclavamiento.
Comando de enclavamiento (Counter Latch Command).
Este comando se envía cual si de una palabra de control se tratara (A1=A0=1): para diferenciarlo de
ellas los bits 5 y 4 están a cero. En los bits 7 y 6 se indica el contador afectado. Los demás bits deben estar
a cero para compatibilizar con futuras versiones del chip. Cuando se envía el comando, el OL del contador
seleccionado queda congelado hasta que la CPU lo lee, momento en el que se descongela y pasa de nuevo
a seguir a CE. Esto permite leer los contadores al vuelo sin afectar la cuenta en curso. Se pueden enviar
varios de estos comandos a los diversos contadores, cuyos OL's quedarán enclavados hasta ser leídos. Si se
envían varios comandos de enclavamiento al mismo contador, separados por un cierto intervalo de tiempo,
sólo se considerará el primero (por tanto, la cuenta leída corresponderá al valor del contador cuando fue
enclavado por vez primera).
Por supuesto, el contador debe ser leído utilizando el formato que se definió al enviar la palabra de
control; aunque en el caso de leer 16 bits, las dos operaciones no han de ser necesariamente consecutivas (se
pueden insertar en el medio otras acciones relacionadas con otros contadores).
Otra característica interesante (¿disponible tal vez sólo en el 8254?) consiste en la posibilidad de
mezclar lecturas y escrituras del mismo contador. Por ejemplo, si ha sido programado para cuentas de 16 bits,
es válido hacer lo siguiente: 1) leer el byte menos significativo, 2) escribir el nuevo byte menos significativo,
3) leer el byte más significativo, 4) escribir el nuevo byte más significativo.
Comando Read-Back.
Sólo está disponible en el 8254, no en el 8253. Este comando permite leer el valor actual de la
cuenta, así como averiguar también el modo programado para un contador y el estado actual de la patilla
OUT, además de verificar el banderín de cuenta nula (Null Count) de los contadores que se indiquen. El
formato del comando Read-Back es el siguiente:
El comando Read-Back permite enclavar la cuenta en varios OL's de múltiples contadores de una
sola vez, sin requerir múltiples comandos de enclavamiento, poniendo el bit 5 a cero. Todo funciona a partir
de aquí como cabría esperar (los contadores permanecen enclavados hasta ser leídos, los que no son leídos
permanecen enclavados, si el comando se reitera sólo actúa la primera vez reteniendo la primera cuenta...).
También es posible enviar información de estado al latch OL, enclavándola para que puede ser leída con
comodidad por el puerto que corresponda a ese contador. La palabra de estado tiene el siguiente formato:
En D0..D5 se devuelve justo la misma información que se envió en la última palabra de control; en
el bit D7 se entrega el estado actual de la patilla OUT del 8254, lo que permite monitorizar por software las
salidas del temporizador economizando hardware en ciertas aplicaciones. El bit NULL COUNT (D6) indica
cuándo la última cuenta escrita en CR ha sido transferida a CE: el momento exacto depende del modo de
funcionamiento del contador. Desde que se programa un nuevo valor de cuenta, pasa un cierto tiempo hasta
que éste valor pasa de CR a CE: leer el contador antes de que se haya producido dicha transferencia implica
leer un valor no relacionado con la nueva cuenta. Por ello, según las aplicaciones, puede llegar a ser necesario
esperar a que NULL COUNT alcance el valor 0 antes de leer. El funcionamiento es el siguiente:
Operación | Consecuencias |
A -Escribir al registro de la palabra de control (1) | NULL COUNT = 1 |
B -Escribir al registro contador (CR) (2) | NULL COUNT = 1 |
C -Nueva cuenta cargada en CE (CR ->CE) | NULL COUNT = 0 |
Si se enclava varias veces seguidas la palabra de estado, todas serán ignoradas menos la primera, por
lo que el estado leído será el correspondiente al contador en el momento en que se enclavó por vez primera
la palabra de estado.
Se pueden enclavar simultáneamente la cuenta y la palabra de estado (en un comando Read-Back con
D5=D4=0), lo que equivale a enviar dos Read-Back consecutivos. En este caso, y con independencia de quién
de los dos hubiera sido enclavado primero, la primera lectura realizada devolverá la palabra de estado y la
segunda la cuenta enclavada (que automáticamente quedará de nuevo desenclavada).
MODOS DE OPERACIÓN DEL 8254
MODO 0: Interrupt On Terminal Count (Interrupción al final de la cuenta).
Es empleado típicamente para contar sucesos. Tras escribir la palabra de control, OUT está
inicialmente en estado bajo, y permanecerá así hasta que el contador alcance el cero: entonces se pone a 1
y no volverá a bajar hasta que se escriba una nueva cuenta o una nueva palabra de control. La entrada GATE
puesta a 0 permite inhibir la cuenta, sin afectar a OUT. El contador sigue evolucionando tras llegar a cero
(0FFFFh, 0FFFEh, ...) por lo que lecturas posteriores del mismo devuelven valores pseudoaleatorios.
Tras escribir la cuenta inicial y la palabra de control en el contador, la cuenta inicial será cargada en
el próximo pulso del reloj conectado (CLK), pulso que no decrementa el contador: para una cuenta inicial
N, OUT permanecerá a 0 durante N+1 pulsos del reloj tras escribir la cuenta inicial.
Si se escribe una nueva cuenta en el contador, será cargada en el próximo pulso del reloj y el
contador comenzará a decrementarse; si se envía una cuenta de dos bytes, el primer byte enviado inhibe la
cuenta y OUT es puesto a cero inmediatamente (sin esperar a CLK): tras escribir el segundo byte, la cuenta
será cargada en el siguiente pulso del reloj. Esto permite sincronizar la secuencia de conteo por software.
Si se escribe una nueva cuenta mientras GATE=0, ésta será cargada en cualquier caso en el siguiente
pulso del reloj: cuando GATE suba, OUT se pondrá en alto tras N pulsos del reloj (y no N+1 en este caso).
MODO 1: Hardware Retriggerable One-Shot (Monoestable programable).
OUT será inicialmente alta y bajará en el pulso de reloj que sigue al flanco de subida de GATE,
permaneciendo en bajo hasta que el contador alcance el cero. Entonces, OUT sube y permanece activo hasta
el pulso del reloj que siga al próximo flanco de subida de GATE.
Tras escribir la palabra de control y la cuenta inicial, el contador está preparado. Un flanco de subida
de GATE provoca la carga del contador (CR -< CE) y que OUT baje en el próximo pulso del reloj,
comenzando el pulso One-Shot de N ciclos de reloj de duración; el contador vuelve a ser recargado si se
produce un nuevo flanco de subida de GATE, de ahí que OUT permanezca en bajo durante N pulsos de reloj
tras la última vez que suceda esto. El pulso One-Shot puede repetirse sin necesidad de recargar el contador
con el mismo valor. GATE no influye directamente en OUT.
Si se escribe una nueva cuenta durante un pulso One-Shot, el One-Shot en curso no resulta afectado,
a menos, lógicamente, que se produzca un nuevo flanco de subida de GATE: en ese caso, el contador sería
recargado con el nuevo valor.
MODO 2: Rate Generator (Generador de ritmo).
En este modo, el contador funciona como un divisor por N. Es empleado típicamente para las
interrupciones de los relojes de tiempo real.
OUT estará inicialmente en alto. Cuando el contador se decremente hasta el valor 1, OUT pasará a
estado bajo durante un pulso del reloj; tras ello, volverá a subir y el contador se recargará con la cuenta
inicial, repitiéndose el proceso. Este modo es, por tanto, periódico, y la misma secuencia se repite
indefinidamente. Para una cuenta inicial N, la secuencia se repite cada N ciclos de reloj (CLK).
Si GATE=0 la cuenta descendiente se detiene: si GATE es bajado durante un pulso de salida, OUT
sube inmediatamente. Un flanco de subida en GATE provoca una recarga del contador con el valor de cuenta
inicial en el siguiente pulso del reloj (después, como cabría esperar, OUT bajará tras los N pulsos del reloj
correspondientes): GATE puede ser utilizado para sincronizar el contador.
Tras escribir la palabra de control y la cuenta inicial, el contador será cargado en el próximo pulso
del reloj: OUT bajará N pulsos de reloj después, lo que permite también una sincronización por software.
Escribir un nuevo valor de cuenta durante el funcionamiento del contador no afecta a la actual
secuencia de cuenta; si se recibe un flanco de subida de GATE antes del final del período el contador se
recargará con ese nuevo valor de cuenta inicial tras el próximo pulso del reloj y volverá a comenzar, en caso
contrario se recargará con el nuevo valor tras finalizar con normalidad el ciclo en curso.
MODO 3: Square Wave Mode (Generador de onda cuadrada).
Este modo es empleado normalmente para la generación de una señal de onda cuadrada. Este modo
es similar al 2, con la diferencia de que la salida OUT conmuta al transcurrir la mitad de la cuenta:
inicialmente está en alto, pero al pasar la mitad de la cuenta pasa a estado bajo hasta que la cuenta finaliza.
Este modo es también periódico: la onda resultante para una cuenta inicial N tiene un período de N ciclos.
Si GATE=0 la cuenta descendiente se detiene: si GATE es bajado durante un pulso de salida, OUT
sube inmediatamente sin esperar ningún CLK. Un flanco de subida en GATE provoca una recarga del
contador con el valor de cuenta inicial en el siguiente pulso del reloj: GATE puede ser utilizado para
sincronizar el contador.
Tras escribir la palabra de control y la cuenta inicial, el contador será cargado en el próximo pulso
del reloj: también puede ser sincronizado por software.
Escribir un nuevo valor de cuenta durante el funcionamiento del contador no afecta a la actual
secuencia de cuenta; si se recibe un flanco de subida de GATE antes del final del medio-período el contador
se recargará con ese nuevo valor de cuenta inicial tras el próximo pulso del reloj y volverá a comenzar, en
caso contrario se recargará con el nuevo valor tras finalizar con normalidad el medio-ciclo en curso.
Para valores de cuenta impares, la duración a nivel alto de OUT será un período de reloj mayor que
la duración a nivel bajo.
MODO 4: Software Triggered Mode (Pulso Strobe iniciado por software).
OUT está en alto al principio; cuando la cuenta inicial expira, OUT baja durante un pulso de reloj
y luego vuelve a subir. El proceso se inicia cuando se escribe la cuenta inicial.
GATE=0 inhibe el contador y GATE=1 lo habilita; GATE no influye en OUT. Tras escribir la
palabra de control y la cuenta inicial, el contador será cargado en el próximo pulso del reloj: como ese pulso
no decrementa el contador, para una cuanta inicial N, OUT no bajará hasta N+1 pulsos de CLK. Si se escribe
una nueva cuenta durante el proceso, se cargará en el próximo pulso CLK y continuará el proceso de cuenta
con la nueva cuenta escrita; si la cuenta es de 2 bytes, al escribir el primero no se altera el funcionamiento
del contador hasta que se envíe el segundo.
MODO 5: Hardware Triggered Strobe (Pulso Strobe iniciado por hardware).
OUT estará en alto al principio: con el flanco de subida de la señal GATE, el contador comienza a
decrementar la cuenta. Cuando llega a cero, OUT baja durante un pulso CLK y luego vuelve a subir.
Después de escribir la palabra de control y la cuenta inicial, el contador no será cargado hasta el
pulso de reloj posterior al flanco de subida de GATE. Este pulso CLK no decrementa el contador: por ello,
ante una cuenta inicial N, OUT no bajará hasta que pasen N+1 pulsos de reloj. GATE no afecta a OUT.
Si una nueva cuenta inicial es escrita durante el proceso, la actual secuencia del contador no será
alterada; si se produce un flanco de subida en GATE antes de que la nueva cuenta sea escrita pero después
de que expire la cuenta actual, el contador será cargado con la nueva cuenta en el próximo pulso del reloj.
12.3.2 - EL 8254 EN EL ORDENADOR.
Todos los AT y PS/2 llevan instalado un 8254 o algo equivalente; los PC/XT van equipados con un
8253, algo menos versátil; los PS/2 más avanzados tienen un temporizador con un cuarto contador ligado a
la interrupción no enmascarable, si bien no lo consideraremos aquí. Todos los contadores van conectados a
un reloj que oscila a una frecuencia de 1.193.180 ciclos por segundo (casi 1,2 Mhz). La dirección base en
el espacio de E/S del ordenador elegida por IBM cuando diseñó el PC es la 40h. Por tanto, los tres contadores
son accedidos, respectivamente, a través de los puertos 40h, 41h y 42h; la palabra de control se envía al
puerto 43h.
La señal GATE de los contadores 0 y 1 está siempre a 1; en el contador 2 es seleccionable el nivel
de la línea GATE a través de bit 0 del puerto E/S 61h. La BIOS programa por defecto el contador 0 en el
modo 3 (generador de onda cuadrada) y el contador 1 en el modo 2 (generador de ritmo); el usuario
normalmente programa el contador 2 en el modo 2 ó 3.
La salida del contador 0 está conectada a IRQ 0 (ligado a la INT 8, que a su vez invoca a INT 1Ch);
este contador está programado por defecto con el valor cero (equivalente a 65536), por lo que la cadencia
de los pulsos es de 1.193.180/65.536 = 18,2 veces por segundo, valor que determina la precisión del reloj
del sistema, ciertamente demasiado baja. Se puede modificar el valor de recarga de este contador en un
programa, llamando a la vieja INT 8 cada 1/18,2 segundos para no alterar el funcionamiento normal del
ordenador, si bien no es conveniente instalar programas residentes que cambien permanentemente esta
especificación: los programas del usuario esperan encontrarse el temporizador a la habitual y poco útil
frecuencia de 18,2 interrupciones/segundo.
La salida del contador 1 controla el refresco de memoria en todas las máquinas, su valor normal para
el divisor es 18; aumentándolo se puede acelerar el funcionamiento del ordenador, con el riesgo -eso sí- de
un fallo en la memoria, detectado por los chips de paridad -si los hay-, que provoca generalmente el bloqueo
del equipo. De todas maneras, en los PC/XT se puede aumentar entre 19 y 1000 sin demasiados riesgos,
acelerándose en ocasiones hasta casi un 10% la velocidad de proceso del equipo. En los AT la ganancia de
velocidad es mucho menor y además este es un punto demasiado sensible que conviene no tocar para no
correr riesgos, aunque se podría bajar hasta un valor 2-17 para ralentizar el sistema. Sin embargo, no es
conveniente alterar esta especificación porque, como se verá más adelante, hay un método para realizar
retardos (empleado por la BIOS y algunas aplicaciones) que se vería afectado.
El contador 2 puede estar conectado al altavoz del ordenador para producir sonido; alternativamente
puede emplearse para temporizar. Es el único contador que queda realmente libre para el usuario, lo que suele
dar quebraderos de cabeza a la hora de producir sonido.
12.3.3 - TEMPORIZACIÓN.
Los contadores 0 y 1, especialmente este último, ya están ocupados por el sistema; en la práctica el
único disponible es el 2. Este contador ha sido conectado con el doble propósito de temporizar y de generar
sonido. Para emplearlo en las temporizaciones, es preciso habilitar la puerta GATE activando el bit 0 del
puerto 61h; también hay que asegurarse de que la salida del contador no está conectada al altavoz (a menos
que se desee música mientras se cronometra) poniendo a 0 el bit 1 del mismo puerto (61h):
IN AL,61h AND AL,11111101b ; borrar bit 1 (conexión contador 2 con el altavoz) OR AL,00000001b ; activar bit 0 (línea GATE del contador 2) JMP SHORT $+2 ; estado de espera para E/S OUT 61h,AL
El siguiente programa de ejemplo, CRONOS.ASM, incluye dos subrutinas para hacer retardos de alta
precisión. La primera de ellas, inic_retardo, hay que llamarla al principio para que programe el contador 2
del temporizador; la rutina retardo se encarga de hacer el retardo que se indique en AX (en unidades de
1/1193180 segundos).
; ******************************************************************** ; * * ; * CRONOS.ASM - Subrutinas para hacer retardos de precisión. * ; * * ; * INIT_RETARDO: llamarla al principio del todo. * ; * RETARDO: Entregar en AX el nº de 1193180-avos de * ; * segundo que dura el retardo (máximo 65400). * ; * * ; ******************************************************************** programa SEGMENT ASSUME CS:programa, DS:programa ORG 100h inicio: CALL inic_retardo MOV CX,20 ; 20 retardos MOV AX,59659 ; de 50 milisegundos retard: CALL retardo LOOP retard INT 20h inic_retardo PROC PUSH AX IN AL,61h AND AL,11111101b OR AL,1 JMP SHORT $+2 OUT 61h,AL MOV AL,10110100b ; contador 2, modo 2, binario JMP SHORT $+2 OUT 43h,AL POP AX RET inic_retardo ENDP retardo PROC PUSH AX PUSH BX CLI OUT 42h,AL ; parte baja de la cuenta MOV AL,AH JMP SHORT $+2 OUT 42h,AL ; parte alta JMP SHORT $+2 IN AL,61h XOR AL,1 ; bajar GATE JMP SHORT $+2 OUT 61h,AL XOR AL,1 ; subir GATE JMP SHORT $+2 OUT 61h,AL STI JMP SHORT $+2 MOV BX,0FFFFh retardando: MOV AL,10000000b OUT 43h,AL ; enclavamiento JMP SHORT $+2 IN AL,42h ; leer contador MOV AH,AL JMP SHORT $+2 IN AL,42h XCHG AH,AL ; AX = valor del contador CMP AX,BX MOV BX,AX JBE retardando POP BX POP AX RET retardo ENDP programa ENDS END inicio
El procedimiento inic_retardo programa el contador 2 en el modo 2, con datos en binario y dejándolo
listo para enviar/recibir secuencias de 2 bytes para la cuenta (primero el byte menos significativo y luego el
alto). Las instrucciones JMP SHORT $+2 colocadas oportunamente (para saltar a la siguiente línea) evitan
que las máquinas AT más antiguas fallen en dos operaciones de E/S consecutivas demasiado rápidas. El
procedimiento retardo envía el nuevo valor de cuenta. A continuación baja y vuelve a subir la señal GATE,
con objeto de provocar un flanco de subida en esta línea, lo cual provoca que el contador se cargue con el
valor recién enviado de manera inmediata (de lo contrario, no se recargaría hasta acabar la cuenta anterior).
Finalmente, entramos en un bucle donde se enclava continuamente la cuenta y se espera hasta que acabe. Lo
más intuitivo sería comprobar si la cuenta es cero, pero esto es realmente difícil ya que cambia nada menos
que ¡más de 1 millón de veces por segundo!. Por tanto, nos limitamos a comprobar si tras dos lecturas
consecutivas la segunda es mayor que la primera ...¡no puede ser!... sí, si puede ser, si tras llegar a 0 el
contador se ha recargado. De esta manera, el mayor valor admitido en AX al llamar es 65535, aunque no
conviene que sea superior a 65400, para permitir que las recargas puedan ser detectadas en la máquina más
lenta (un XT a 4.77 y en 135/1193180 segundos dispone de unos 540 ciclos, en los que holgadamente cubre
este bucle).
A la hora de emplear las rutinas anteriores hay que tener en cuenta dos consideraciones. Por un lado,
están diseñadas para hacer pequeños retardos: llamándolas repetidamente, el bucle que hay que hacer (y las
interrupciones que se producen durante el proceso) provoca que retarden más de la cuenta. Por ejemplo, en
el programa principal, poniendo 1200 en CX en lugar de 20, el retardo debería ser de 60 segundos; sin
embargo, comparando este dato con el contador de hora de la BIOS (en una versión ligeramente modificada
del programa) resulta ser de casi 60,2 segundos. La segunda consideración está relacionada con las
interrupciones: de la manera que está el listado, se puede producir una interrupción en la que algún programa
residente utilice el contador 2 del temporizador, alterando el funcionamiento de las rutinas de retardo (por
ejemplo, una utilidad de click en el teclado) o incluso provocando un fallo en la misma (si a ésta no le da
tiempo a comprobar que ya es la hora): este es un aspecto a tener en cuenta en un caso serio. Se puede, por
ejemplo, inhibir todas las interrupciones (o enmascar sólo las más molestas), aunque anular la interrupción
del temporizador, la más peligrosa, provocaría un retraso de la hora del ordenador.
Para hacer retardos o temporizaciones de más de 50 milisegundos, es más conveniente emplear el
contador de hora de la BIOS (variable de 32 bits en 0040h:006Ch) que la INT 8 se encarga de incrementar
18,2 veces cada segundo y de volver a ponerlo a cero cada 24 horas. No es conveniente mirar el valor del
contador de hora de la BIOS, sumarle una cantidad y esperar a que alcance dicha cantidad fija: la experiencia
demuestra que eso produce a veces cuelgues del ordenador, no solo debido a que suele fallar cuando son las
23:59:59 sino también porque cuando se alcanza el valor esperado, por cualquier motivo (tal como un
alargamiento excepcional de la rutina que controla INT 8 ó INT 1Ch debido a algún programa residente)
puede que el programa principal no llegue a tiempo para comprobar que ya es la hora... y haya que esperar
otras 24 horas a probar suerte. Lo ideal es contar las veces que cambia el contador de hora de la BIOS.
Por último, como ejemplo ameno, el siguiente fragmento de programa hace que la hora del ordenador
vaya diez veces más rápida -poco recomendable, aunque muy divertido- programando el contador 0 con un
valor de cuenta 6553 (frente al 0=65536 habitual), de la siguiente manera:
MOV AL,00110110b ; contador 0, operación 11b, datos binarios OUT 43h,AL MOV BX,6553 ; valor de cuenta MOV AL,BL JMP SHORT $+2 OUT 40h,AL ; enviar byte bajo MOV AL,BH JMP SHORT $+2 OUT 40h,AL ; enviar byte altoUn método genial para hacer retardos y controlar timeouts en AT.
Aunque ausente en todos los manuales de referencia técnica y en todos los libros relacionados con
la programación de PC, existe un método muy fácil y eficiente para temporizar disponible en todos los
ordenadores AT. Pese a no estar documentado, un programa muy usual como es el KEYB del MS-DOS (a
partir de la versión 5.0 del sistema) lo utiliza en todos los AT, sin importar el modelo. Por ello, cabe suponer
que seguramente los futuros equipos mantendrán la compatibilidad en este aspecto. Sucede que la salida del
contador 1 del 8254, encargada del refresco de la memoria, controla de alguna manera desconocida (tal vez
a través de un flip-flop) la generación de una onda cuadrada de unos 33 KHz que puede leerse a través del
bit 4 del puerto 61h (no se trata de la salida OUT del contador 1: éste está programado en modo 2 y no
genera precisamente una onda cuadrada). El contador 1 es programado por la BIOS en todos los PC con una
cuenta 18, conmutando el nivel de la salida cada segundo 1193180/18 = 66287,77 veces. Para hacer un
determinado retardo basta con contar las veces que el bit cambia de nivel: la función en ensamblador
retardo_asm() del programa de ejemplo lo ilustra. Este método es especialmente interesante en los programas
residentes que precisen retardos de precisión, para sonido u otras tareas, tales como limitar la duración
máxima de una comprobación en un bit de estado a unos milisegundos o microsegundos (control de
timeouts); la principal ventaja es que no se modifica en absoluto la configuración de ningún chip que pueda
estar empleando el programa principal, empezando por el 8254. Además, no requiere preparación previa
alguna. Para los más curiosos, decir que el bit 5 del puerto 61h es la salida OUT del contador 2 del 8254 (la
línea OUT del contador 2 del 8253 de los PC/XT también puede consultarse a través del bit 5, pero del
puerto 62h).
El único inconveniente del método es la alta frecuencia con que cambia el bit: esta misma rutina
escrita en C podría no ser suficientemente ágil para detectar todas las transiciones en las máquinas AT más
lentas a 6 MHz. A partir de 8 MHz sí puede ser factible, como evidencian las pruebas realizadas, aunque hay
que extremar las precauciones para que el código compilado sea lo bastante rápido: utilizar las dos variables
registro que realmente soportan los compiladores y huir de la aritmética de 32 bits, como puede observarse
en la función retardo_c() del programa de ejemplo. Una mala codificación o compilador podrían hacer
inservible el método incluso en una máquina a 16 ó 20 MHz. Para no tener problemas, es mejor emplear la
versión en ensamblador, escrita en un C no mucho menos estándar. La macro MICRO() ayuda a seleccionar
con más comodidad el retardo, indicándolo en mus, aunque implica una operación en coma flotante que por
sí sola añade unos 100 mus de retardo adicionales en un 386-25 sin coprocesador y con las librerías de
Borland.
Anécdota: Para los más curiosos, decir que los programadores de Microsoft emplean este método en el KEYB en dos ocasiones: para
limitar a un tiempo razonable la espera hasta que el registro de entrada del 8042 se llene (15 ms) y, en otra ligera variante, para
controlar la duración del pitido de error. Los aficionados al ensamblador pueden comprobarlo personalmente aplicando el comando
U del DEBUG sobre el KEYB para desensamblar a partir de los offsets 0E39 y 0D60, respectivamente: en el primer caso, la subrutina
sólo es ejecutada en AT; en el segundo, veréis como el KEYB se asegura de que el equipo es un AT comprobando el valor de BP
antes de saltar a 0D70 (ejecuta un bucle vacío en las demás máquinas). Esta nueva técnica ha permitido eliminar respecto a anteriores
versiones del programa algunos test sobre tipos de ordenadores, cuya finalidad más común era ajustar las constantes de retardo. Son
válidos tanto el KEYB del MS-DOS 5.0 castellano como el del MS-DOS 6.0 en inglés o castellano indistintamente (¡las direcciones
indicadas coinciden!). También en las BIOS modernas suele haber ejemplos de esta técnica, aunque las direcciones ya no coinciden...
/********************************************************************/ /* */ /* Programa de demostración del método de retardo basado en la */ /* monitorización de los ciclos de refresco de memoria del AT. */ /* */ /********************************************************************/ #include <dos.h> #define MICRO(microseg) ((long)(microseg/15.08573727)) void retardo_asm(), retardo_c(); void main() { /* cuatro formas de hacer un mismo retardo de precisión */ retardo_asm (66267L); /* un segundo */ retardo_asm (MICRO(1000000L)); /* otro segundo (¡más claro!) */ retardo_c (66267L); /* ahora en C */ retardo_c (MICRO(1000000L)); /* la otra alternativa */ } void retardo_asm (long cuenta) /* método ensamblador recomendado */ { asm push ax asm push cx asm push dx asm mov cx,word ptr cuenta /* DX:CX = cuenta */ asm mov dx,word ptr [cuenta+2] asm jcxz fin_l /* posible cuenta baja nula */ esp_ref: asm in al,61h asm and al,10h /* aislar bit 5 */ asm cmp al,ah asm je esp_ref /* esperar cambio de nivel */ asm mov ah,al asm loop esp_ref /* completar cuenta baja */ fin_l: asm and dx,dx asm jz fin_ret /* posible cuenta alta nula */ asm dec dx asm jmp esp_ref /* completar cuenta alta */ fin_ret: asm pop dx asm pop cx asm pop ax } void retardo_c (long cuenta) /* método en C no recomendado */ { register a, b; unsigned cuenta_h, cuenta_l; cuenta_h=cuenta >> 16; cuenta_l=cuenta & 0xFFFF; do do { while (a==(b=inportb(0x61) & 0x10)); a=b; } while (cuenta_l--); while (cuenta_h--); }
La producción de sonido es uno de los puntos más débiles de los ordenadores compatibles, que sólo
superan por muy escaso margen a alguno de los micros legendarios de los 80, si bien las tarjetas de sonido
han solventado el problema. Pero aquí nos conformaremos con describir la programación del altavoz. En
todos los PCs existen dos métodos diferentes para generar sonido, con la utilización del 8254 o sin él, que
veremos por separado.
Control directo del altavoz.
El altavoz del ordenador está ligado en todas las máquinas al bit 1 del puerto E/S 61h. Si se hace
cambiar este bit (manteniéndolo durante cierto tiempo alto y durante cierto tiempo bajo, repitiendo el proceso
a gran velocidad) se puede generar una onda cuadrada de sonido. Cuanto más deprisa se realice el proceso,
mayor será la frecuencia del sonido. Por fortuna, la baja calidad del altavoz del PC redondea la onda
cuadrada y produce un sonido algo más musical de forma involuntaria. No existe, en cualquier caso, control
sobre el volumen, que dada la calidad del altavoz también está en función de la frecuencia. Este método de
producción de sonido tiene varios inconvenientes. Por un lado, la frecuencia con que se hace vibrar al bit que
lo produce, si no se tiene mucho cuidado, está a menudo más o menos ligada a la capacidad de proceso del
ordenador: esto significa que el sonido es más grave en máquinas lentas y más agudo en las rápidas. Esto
es particularmente grave y evidente cuando las temporizaciones se hacen con bucles de retardo con registros
de la CPU: la frecuencia del sonido está totalmente a merced de la velocidad de la máquina en que se
produce. Es por ello que el pitido de error que produce el teclado es a menudo distinto de unos ordenadores
a otros, aunque tengan el mismo KEYB instalado. Otro gran inconveniente de este método es que las
interrupciones, fundamentalmente la del temporizador, producen fuertes interferencias sobre el sonido. Por
ello, es normal tenerlas inhibidas, con el consiguiente retraso de la hora. Por último, un tercer gran
inconveniente es que la CPU está completamente dedicada a la producción de sonido, sin poder realizar otras
tareas mientras tanto.
Antes de comenzar a producir el sonido con este método hay que bajar la línea GATE del 8254, ya
que cuando está en alto y se activa también el bit 1 del puerto E/S 61h, el temporizador es el encargado de
producir el sonido (este es el segundo método, como veremos). Por tanto, es preciso poner primero a cero
el bit 0 del mismo puerto (61h):
CLI ; evitar posible INT 8, entre otras IN AL,61h AND AL,11111110b JMP SHORT $+2 ; estado de espera para E/S OUT 61h,AL ; bajar GATE del contador 2 del 8254 MOV CX,100h ; 256 vueltas otro_ciclo: PUSH CX IN AL,61h XOR AL,2 ; invertir bit 1 JMP SHORT $+2 OUT 61h,AL MOV CX,300 ; constante de retardo retardo: LOOP retardo POP CX LOOP otro_ciclo STIControl del altavoz por el temporizador.
El otro método posible consiste en emplear el contador 2 del temporizador conectado al altavoz; así,
enviando el período del sonido (1.193.180/frecuencia_en_Hz) a dicho contador (programado en modo 3), éste
se encarga de generar el sonido. Esto permite obtener sonidos idénticos en todos los ordenadores. Existe el
pequeño problema de que la duración del sonido ha de ser múltiplo de 1/18,2 segundos si se desea utilizar
el reloj del sistema para determinarla (un bucle de retardo sería, una vez más, dependiente de la máquina)
ya que el contador 2 está ahora ocupado en la producción de sonido y no se puede usar para temporizar (al
menos, no sin hacer malabarismos). Alternativamente, se podría evaluar la velocidad de la CPU para ajustar
las constantes de retardo o aumentar la velocidad de la interrupción periódica.
Para emplear este sistema, primero se prepara el contador 2 para temporizar (poniendo a 1 el bit 0
del puerto 61h) y luego se conecta su salida al altavoz (poniendo a 1 el bit 1 del puerto 61h). Al final,
conviene borrar ambos bits de nuevo. Ahora no es preciso inhibir las interrupciones para garantizar la calidad
del sonido:
MOV AL,10110110b ; contador 2, modo 3, operación 11b, datos binarios OUT 43h,AL ; programar contador 2 MOV AX,2711 ; 1.193.180 / 440 Hz (nota LA) = 2711 JMP SHORT $+2 OUT 42h,AL MOV AL,AH JMP SHORT $+2 OUT 42h,AL ; frecuencia programada JMP SHORT $+2 IN AL,61h OR AL,00000011b JMP SHORT $+2 OUT 61h,AL ; altavoz sonando MOV CX,0 demora: LOOP demora ; esperar un cierto tiempo por el peor método IN AL,61h AND AL,11111100b JMP SHORT $+2 OUT 61h,AL ; altavoz callado
Las frecuencias en Hz de las distintas notas musicales están oficialmente definidas y los músicos
suelen tenerlas en cuenta a la hora de afinar los instrumentos. La escala cromática temperada, adoptada por
la American Standards Asociation en 1936, establece el LA4 como nota de referencia en 440 Hz. En general,
una vez conocidas las frecuencias de las notas de una octava, las de la octava siguiente o anterior se obtienen
multiplicando y dividiendo por dos, respectivamente. La fórmula de abajo permite obtener las frecuencias
de las notas asignándolas un número (a partir de 6 y hasta 88; el LA de 440 Hz es la nota 49) con una
precisión razonable, máxime teniendo en cuenta que van a ir a parar al altavoz del PC. Tal curiosa relación
se verifica debido a que la respuesta del oído humano es logarítmica, lo que ha permitido reducir a simples
matemáticas el viejo saber milenario de los músicos.