Comunicaciones Serie en Windows (y III) (2ª parte)

Las armas secretas

Si se ha explorado el código fuente se habrá visto que, una vez se abre el puerto, éste se configura automáticamente según los valores almacenados en las distintas clases que mantienen la configuración de usuario para el puerto. Vamos a hacer un repaso a este proceso de configuración deteniéndonos en aquellos aspectos que, en anteriores entregas, han quedado sin explicar.

Lo primero que se configura son aquellos eventos a monitorizar por el puerto (ver los procedimientos ‘TPuertoSerie.AlAbrir()’ y ‘TPuertoSerie.PonerEventos()’ en el módulo ‘PtoSerie.pas’). Aquí se establecen aquellos eventos que produce el puerto y que interesa controlar. Por defecto se monitorizan todos los eventos posibles pero, si se desea excluir alguno, bastará con modificar el campo ‘EventosSerie’ de la clase.

No debería excluirse el evento ‘EV_RXCHAR’ (constante ‘evRxChar’ de la propiedad indicada) ya que puede dar lugar a que determinados mensajes desde el módem no se “escuchen”. Como se sabe, este evento se produce cuando hay algún carácter o byte en el puerto y esto puede provenir de un módem remoto o del propio (por ejemplo, un error en el módem). Pero, por ejemplo, si la comunicación es por línea directa (módem nulo) podría querer desactivarse el evento ‘EV_RING’ ya que nunca se producirá (el hecho de que nunca se produzca un evento no retrasa la respuesta del puerto).

Se establecen los eventos a monitorizar mediante una llamada a la función del API de comunicaciones de Windows ‘SetCommMask()’ que recibe dos argumentos: el manejador o ‘Handle’ del puerto y una máscara que indica los eventos a monitorizar. Para poder llamar a esta función debe estar abierto el puerto (el ‘Handle’ debe ser válido) o fallará. Por ello, la función de la clase encargada de establecer los eventos es privada.

A continuación se establece el intervalo de tiempo de espera entre un byte y el siguiente: lo que se denomina “Timeouts”. Desde ‘TConfigPuerto.AplicarConfiguracion()’ se llama a ‘TTimeoutsPuerto.Aplicar()’ (ver el módulo ‘TimeoPto.pas’) que utiliza la función del API de comunicaciones de Windows ‘SetCommTimeouts()’ para establecer los intervalos apropiados. Los valores por defecto que toma son suficientes para la mayoría de las aplicaciones. Sin embargo, se explican a continuación los valores que pueden tomar por si en algún momento es necesario modificarlos.

La función ‘SetCommTimeouts()’ admite dos argumentos: el ‘Handle’ o manejador del puerto (para lo que este debe estar abierto, lógicamente) y una estructura (‘record’) de tipo ‘COMMTIMEOUTS’ (‘TCommTimeouts’, en terminología Delphi). Los campos de esta estructura, todos de tipo ‘DWORD’ (‘LongWord’, en Delphi) son:

ReadIntervalTimeout: Especifica el tiempo, en milisegundos, que debe pasar entre la llegada de dos bytes al puerto en una operación de lectura. Comienza a contarse este tiempo a la llegada de un byte. La llegada del siguiente byte debe ser menor que el tiempo especificado aquí. Si no se reciben bytes en un período mayor al especificado aquí, la función ‘ReadFile()’ retorna con el contenido del buffer, sin esperar a la llegada del número de bytes pedidos. Si se trabaja en una línea no muy rápida quizá halla que jugar con el valor de este campo y los dos siguientes. Un valor de ‘MAXDWORD’ ($FFFFFFFF) en este campo combinado con valores cero en ‘ReadTotalTimeoutMultiplier’ y ‘ReadTotalTimeoutConstant’ indica que no se utiliza. Se modifica este valor mediante la propiedad ‘ReadInterval’ de la clase ‘TTimeoutsPuerto’ que, por defecto, vale ‘MAXDWORD’ que, como digo, evita el uso de ‘Timeouts’ y hace que una operación de lectura sea completada.

ReadTotalTimeoutMultiplier: Especifica el multiplicador, en milisegundos, usado para calcular el tiempo de espera en las operaciones de lectura. En cada operación de lectura, este valor es multiplicado por el número total de bytes a leer. Esto es, sería algo así como el tiempo medio de lectura por byte. Un valor de cero aquí y en ‘ReadTotalTimeoutConstant’ unido a ‘MAXDWORD’ en el campo ‘ReadIntervalTimeout’ indica que no se utiliza. Se modifica este valor mediante la propiedad ‘ReadTotalMultiplier’ de la clase ‘TTimeoutsPuerto’ que, por defecto, vale cero.

ReadTotalTimeoutConstant: Especifica una constante, en milisegundos, usada para calcular el tiempo de espera en las operaciones de lectura. En cada operación de lectura, este valor es añadido (sumado) al producto de ‘ReadTotalTimeoutMultiplier’ por el número total de bytes a leer. Esto es:


Timeout_Lectura := (ReadTotalTimeoutMultiplier * Bytes_A_Leer) + ReadTotalTimeoutConstant;

Un valor de cero aquí y en ‘ReadTotalTimeoutMultiplier’ unido a ‘MAXDWORD’ en el campo ‘ReadIntervalTimeout’ indica que no se utiliza. Se modifica este valor mediante la propiedad ‘ReadTotalConstant’ de la clase ‘TTimeoutsPuerto’ que, por defecto, vale cero.

WriteTotalTimeoutMultiplier: Especifica el multiplicador, en milisegundos, usado para calcular el tiempo de espera en las operaciones de escritura. En cada operación de escritura, este valor es multiplicado por el número total de bytes a escribir. Sería algo así como el tiempo medio de escritura por byte. Si se trabaja con una línea lenta quizá halla que modificar este campo y el siguiente. Sobretodo, claro está, si fallan las operaciones de escritura. Un valor de cero aquí, unido a un valor de cero en el campo ‘WriteTotalTimeoutConstant’ indica que no se usa el intervalo de espera en las escrituras. Se modifica este valor mediante la propiedad ‘WriteTotalMultiplier’ de la clase ‘TTimeoutsPuerto’ que, por defecto, vale cero.

WriteTotalTimeoutConstant: Especifica una constante, en milisegundos, usada para calcular el tiempo de espera en las operaciones de escritura. En cada operación de escritura, este valor es añadido (sumado) al producto de ‘WriteTotalTimeoutMultiplier’ por el número total de bytes a escribir. Esto es:


Timeout_Escritura := (WriteTotalTimeoutMultiplier * Bytes_A_Escribir) + WriteTotalTimeoutConstant;

Un valor de cero aquí, unido a un valor de cero en el campo ‘WriteTotalTimeoutMultiplier’ indica que no se usa el intervalo de espera en las escrituras. Se modifica este valor mediante la propiedad ‘WriteTotalConstant’ de la clase ‘TTimeoutsPuerto’ que, por defecto, vale cero.

Si se mira la forma de establecer estas propiedades se verá que, primero, se rellena la estructura ‘TCommTimeouts’ con los valores de los campos de la clase ‘TTimeoutsPuerto’ para, a continuación, llamar a la función ‘SetCommTimeouts()’ y establecer los valores. El retorno de esta función se recoge y, si es TRUE, se verifica el resultado de la operación mediante una llamada a la función del API de comunicaciones de Windows ‘GetCommTimeouts()’ y, si todo va bien, los campos de la clase reflejarán los nuevos valores. Esta última función puede utilizarse para saber el estado actual de los tiempos de espera para lecturas y escrituras (ver la función ‘TTimeoutsPuerto.Aplicar()’ en el módulo ‘TimeoPto.pas’).

Siguiendo con el periplo de la configuración del puerto, a continuación se establecen los valores de los bufferes de lectura y escritura: desde el procedimiento ‘TConfigPuerto.AplicarConfiguracion()’ (ver módulo ‘PtoSerie.pas’) se llama a la función ‘TBuffersPuerto.Aplicar()’ en el módulo ‘BuferPto.pas’. Esta función llama, a su vez, a la función del API de comunicaciones de Windows ‘SetupComm()’ para establecer los tamaños de los bufferes y, a continuación, comprueba el efecto de la llamada, obteniendo los valores de dichos bufferes mediante otra función del API: ‘GetCommProperties()’. Este proceso fue explicado en la anterior entrega de esta serie por lo que aquí se obviará. Recalcar, eso sí, que los bufferes deben ajustarse al tamaño de las tramas a enviar y recibir o pueden perderse datos.

A continuación, desde la misma rutina anterior, ‘TConfigPuerto.AplicarConfiguracion()’, se llama a la función encargada de aplicar el resto de los parámetros del puerto: ‘AplicarDCB()’. De la estructura DCB y sus campos se comentó en la primera entrega de esta serie. Aquí sólo se tratará sobre aquellos aspectos de los que no se habló entonces.

El control de flujo se refiere a cómo se comunica el módem (al que denominábamos en el primer artículo ETCD, Equipo de Terminación del Circuito de Datos o, en inglés, DCE, Data Communications Equipment) con el ordenador (al que denominamos ETD, Equipo Terminal de Datos o, en inglés, DTE, Data Terminal Equipment). La comunicación, esto es, el control de flujo, puede establecerse mediante software o mediante hardware (ver la Figura 3 y la primera entrega de esta serie).

El control de flujo por software implica que hay una serie de caracteres, denominados caracteres de control, que utiliza el módem para comunicarse con el ordenador y viceversa. Cuando se utiliza este tipo de control de flujo existen dos caracteres que se usan para poner en alto o en bajo una señal: el carácter ‘XON’, que habitualmente es el #17 y que se sitúa en el registro S31 del módem, y el carácter ‘XOFF’, que suele ser el #19 y se sitúa en el registro S32 del módem.

Estos dos caracteres se establecen mediante los campos de la estructura DCB denominados ‘XonChar’ y ‘XoffChar’. La clase ‘TFlujoPuerto’ (ver el módulo ‘FlujoPto.pas’) expone dos propiedades que permiten modificar sencillamente estos valores: ‘XonChar’ y ‘XoffChar’.

Cuando se está escribiendo o leyendo del puerto y se está utilizando el control de flujo software, se deberá atender a la llegada del carácter XOFF desde el puerto: en ese momento la comunicación se interrumpe hasta que el módem recibe un carácter XON. La interrupción de la comunicación depende del valor que se haya establecido en el campo ‘fTXContinueOnXoff’ de la estructura DCB, al que se accede a través de la propiedad ‘TxContinueOnXoff’ de la clase ‘TFlujoPuerto’: si es TRUE (valor por defecto) la comunicación se interrumpe; si es FALSE la comunicación continúa, lo que puede dar lugar a pérdida de datos.

Los componentes entregados se encargan automáticamente de esta contingencia (la llegada del carácter XOFF hace que se envíe un XON automáticamente al puerto) lo que, dicho sea de paso, no es exactamente lo mejor. Se ha implementado así para evitar que la aplicación tenga que estar atenta a este evento. Modificar el comportamiento requiere cambiar la función ‘TControlModem.PonerXonXoff()’ del módulo ‘ConModem.pas’ y lo considero recomendable para una aplicación seria de comunicaciones que utilice el control de flujo software.

Durante la lectura el módem envía un carácter XON cuando se llena el buffer hasta un tamaño igual al especificado en el campo ‘XonLim’ de la estructura ‘DCB’: a partir de ahí la aplicación puede leer el buffer. Es una forma de estar atento a un determinado número mínimo de bytes en el buffer de entrada. La saturación del buffer hasta un tamaño igual al especificado en el campo ‘XoffLim’ de la estructura ‘DBC’ hace que el módem envíe un carácter XOFF: a partir de ese momento se pueden producir pérdida de datos si la aplicación no lee el buffer. Estos dos valores de la estructura ‘DCB’ pueden ser modificados mediante las propiedades ‘XonLim’ y ‘XoffLim’ de la clase ‘TBuffersPuerto’.

El control de flujo hardware, al igual que el de software, tiene dos vertientes: a la entrada y a la salida (ver Figura 3). Este tipo de control de flujo se basa en una serie de señales, esto es, la activación o desactivación de determinados circuitos del módem (ver la primera entrega de esta serie) y, en general, es preferible a la de software cuando la comunicación se establece en modo binario: si se envían datos binarios nada garantiza que un carácter XOFF no se encuentre entre los datos a enviar y, si ese es el caso, la comunicación puede ser interrumpida a la llegada de este carácter. Además, es más rápido el control de flujo hardware que el software, estando recomendado para velocidades superiores a 9.600 baudios.

Para terminar con la configuración se ha de hacer notar que la velocidad a la que se transmiten los datos, los baudios (propiedad ‘Baudios’ de la clase ‘TConfigPuerto’), tiene importancia cuando se comunica directamente mediante módem nulo en el sentido de que tanto emisor como receptor deben tener la misma velocidad. Lo mismo pasa con otras características que tienen que ver con el formato de los datos (bits de datos, paridad y bit de paro, sobretodo): en una comunicación directa ambos terminales deben ser configurados de la misma forma.

Cuando se establece una comunicación a través de módem, la velocidad en baudios es una indicación de cómo negociarán ambos módem. Conviene situarla a lo máximo permitido: los módem, durante la negociación establecer este valor y devuelven el resultado de la negociación (que la aplicación recibe mediante el evento ‘OnCambiarBaudios’ de la clase ‘TControlModem’) y, generalmente, será la menor velocidad a la que los dos módem puedan alcanzar. En ocasiones una negociación entre módem no consigue llegar a esta velocidad común máxima y se verá que la velocidad de comunicación decae.

El resto de la configuración del puerto se realiza dentro de la función ‘TConfigPuerto.AplicarDCB()’ en el módulo ‘PtoSerie.pas’, donde se utiliza la función del API de comunicaciones de Windows ‘GetCommState()’ para recuperar los valores que en ese momento tenga el puerto y rellenar con ellos la estructura DCB. A continuación se modifican los campos de esta estructura estableciendo en ella los valores de los campos de las clases o componentes que mantienen la configuración del puerto y se aplica la nueva configuración mediante la función del API ‘SetCommState()’.

Final del juego

Dado que todo el proceso de configuración del puerto se realiza automáticamente en el mismo momento en que se llama a la función ‘Abrir()’ de ‘TPuertoSerie’ o de ‘TControlModem’ (dependiendo del componente que se esté usando) la aplicación puede, a continuación y si la función devuelve TRUE, comenzar a escribir y leer del puerto inmediatamente. Ya se comentó que la llegada de bytes al puerto y su lectura consiguiente puede hacerse mediante las funciones ‘Leer()’ o ‘LeerString()’ o en el evento ‘OnRecibirDatos’ de ‘TPuertoSerie’ y puede escribirse en el puerto mediante ‘Escribir()’ o ‘EscribirString()’ de la misma clase. El proceso de transferencia de datos queda ahora a merced del protocolo de transferencia de archivos que se utilice.

En fin, espero que las clases entregadas tengan la suficiente flexibilidad como para permitir que las pueda adaptar a sus necesidades y, sobretodo, que se divierta con ellas. Los métodos de transferencia de archivos permiten en ocasiones conseguir transvases de datos que llegan a alcanzar velocidades superiores a las que los módem dicen soportar de fábrica. Un ejemplo de transferencia se muestra en la Figura 4.