Comunicaciones Serie en Windows I: bestiario. (3ª parte)

Cuando el ordenador (ETD) pretende transmitir un byte de información, le pasa este byte a un circuito especial denominado UART (Universal Asynchronous Receiver Transmitter, transmisor receptor asíncrono universal) que es el encargado de, entre otras cosas, diseccionar el byte a transmitir en bits. Asimismo, el UART del ordenador destino realiza el proceso inverso cuando se reciben bits por el puerto serie. La UART está situada entre el controlador o driver del puerto y el módem (sea nulo o no) tal y como se ve en la figura 5.

Figura 5. Elementos de una comunicación

Generalmente, las transmisiones serie son asíncronas. El método de transmisión asíncrona fue implementado para los terminales TTY de los años 50. Estos terminales utilizaban sistemas electromecánicos por lo que el mayor problema era ajustar la velocidad de los terminales emisor y receptor. Para ajustarla, se enviaba un primer bit antes de cada trama, el bit de arranque (bit 0), que hacía que ambos motores se pusiesen en funcionamiento al unísono. A continuación, se enviaban los bits de datos que en aquellos tiempos estaban limitados a 5: la idea era que cuanto menos tiempo estuviesen en marcha ambos motores menos probabilidad de desajuste habría. Finalmente, se enviaba un bit de paro (bit 1) y se repetía el proceso.

El bit de paro no es en sí parte de los datos sino que se mandaba una señal de bit 1 (si recuerda, el bit 1 corresponde al estado inactivo de la señal) durante un periodo de tiempo suficiente como para que se parasen los motores antes de la siguiente trama de bits. Si se fija en la figura 6 verá que existe la posibilidad de enviar ‘bit y medio’ (¿…?) como bit de paro. Esto significa que se mantiene la señal de paro durante un periodo de tiempo igual al de la transmisión de un bit de información ‘y medio’.

Aunque hoy los sistemas utilizados para la transmisión de datos son electrónicos se sigue utilizando el modo de transmisión asíncrono por su simplicidad. Lo único que ha variado ha sido que los 5 bits de datos han pasado a ser 6, 7 u 8.

Figura 6. Propiedades del puerto

Los módem síncronos no necesitan bits de paro. Así que no hay periodos de inactividad entre bytes. En estos módem, se envía un carácter de sincronismo (denotado SYN) al comienzo de una trama (un grupo de bytes), seguida de la trama en cuestión y finalizando ésta con otro carácter de sincronismo. De esta forma se consiguen velocidades muy superiores a las de los módem asíncronos. La desventaja es los módem síncronos son más caros por lo que la mayoría de los módem con los que toparemos serán asíncronos.

Durante el proceso de la transmisión de información puede producirse cualquier tipo de distorsión de la señal de tal forma que algunos bits no lleguen a su destino o lleguen alterados (los 1 como 0 y viceversa). Para prevenirlo, existen varios métodos de corrección de errores. El más simple de ellos es el método de paridad. Hay dos formas principales de implementar la paridad:

Paridad impar: se trata de contar el número de bits unos que tiene la trama a enviar. Si el número de unos es par, se pone un 1 al final de la trama. En caso contrario, se pone un cero. Dicho de otra forma, con la paridad impar, contando el número de unos más el bit de paridad debe dar un número impar.

Paridad par: se trata de contar el número de bits unos que tiene la trama a enviar. Si el número de unos es par, se pone un 0 al final de la trama. En caso contrario, se pone un uno. Dicho de otra forma, con la paridad par, contando el número de unos más el bit de paridad debe dar un número par.

Por ejemplo, enviar una ‘M’ (decimal 77 = 01001101) con paridad impar sería 010011011 y con paridad par sería 010011010. El método de paridad, como se puede comprobar fácilmente, no es capaz de detectar cambios en los bits que dejen invariante el bit de paridad: si en el 010011011 se intercambian dos bits quedando 010010001 seguimos teniendo paridad impar pero hemos transmitido una ‘H’. Es por ello que, generalmente, no se utiliza el control de errores por paridad en las trasferencias vía módem sobre líneas telefónicas: además de ser poco fiable, por cada byte que se envía, se envía un bit más.

Otra técnica de control de paridad realiza las mismas operaciones indicadas antes pero sobre un grupo de bytes o trama de tal forma que el total de todos los bits 1 de la trama que estén en la primera posición den un número impar (paridad impar) o par (paridad par); que el total de los bits 1 de la segunda posición den un número impar o par, etc. Es mucho más fiable que la anterior, pero su fiabilidad disminuye con el aumento del número de bytes de la trama.

Por fin, una técnica muy usada en transmisiones es la de la del código de redundancia cíclica (CRC) que puede ser de 16 o 32 bits (lógicamente, el de 32 bits detecta más errores que el de 16). Este tipo de control de errores se implementa, normalmente, en los programas de aplicación y son capaces de detectar errores de hasta el 99,9%.

La UART del terminal emisor es el circuito encargado de insertar en los datos a transmitir el bit de arranque al inicio de los datos, el bit de paridad (si existe) y el bit de paro al final. La UART del terminal receptor se encargará del proceso inverso. Es por ello muy importante a la hora de configurar las comunicaciones el asegurarse que ambos terminales tienen los mismos bits de datos, paridad y bit de paro. Además, si la comunicación es directa (otro ordenador, autómata, etc.) es importante que ambos posean la misma velocidad de transferencia ya que no habrá un módem que ajuste dicha velocidad.

En Windows, la configuración de la UART se realiza mediante una llamada a las funciones del API ‘SetCommState()’, ‘SetCommConfig()’ o ‘SetDefaultCommConfig()’. Las dos primeras requieren que el puerto de comunicaciones esté abierto antes. La última cambia la configuración de Windows: una vez modificada, cualquier programa que utilice el puerto, obtendrá esa configuración. En el programa de ejemplo que acompaña este artículo, se ha desactivado la posibilidad de cambiar la configuración global, aunque se puede activar la línea correspondiente en el código fuente del archivo CommConf.pas.

Trataremos ahora sobre cómo se abre el puerto de comunicaciones. Parece ser que Microsoft, siguiendo el ejemplo de otros sistemas más estables (léase Unix), se propuso en su día unificar las funciones de acceso a dispositivos. Así, mientras en Windows 3.x teníamos la función ‘OpenComm()’ (nombre claro donde los haya) ahora, en Windows 9.x, NT y 2000 la función se convierte en obsoleta (es más, ni aparece como posibilidad) y, en cambio, tenemos una función denominada ‘CreateFile()’ que, contra lo que pueda parecer, no crea siempre un archivo sino que unas veces lo abre, otras lo crea, otras lo trunca, otras… Para más ‘inri’ no aparece listada en el archivo de ayuda del API junto a las demás funciones de comunicaciones (búsquese ‘Communication Functions‘).

La función ‘CreateFile()‘ para un puerto de comunicaciones admite los siguientes argumentos:

lpFileName: un puntero de tipo PChar al nombre del puerto que queremos abrir. Así, para abrir el tercer puerto de comunicaciones serie le pasaríamos ‘PChar(‘COM3′)’

DwDesiredAccess: Método de acceso. Excepto en casos muy extraños en los que sólo se vaya a leer o a sólo a escribir aquí se debería poner los dos valores: GENERIC_READ or GENERIC_WRITE

dwShareMode: Modo de compartir. No puede compartirse el puerto de comunicaciones. Cuando una aplicación abre un puerto, lo abre en modo exclusivo y ningún otro proceso, excepto que sea un proceso hijo, podrá acceder al mismo recurso hasta que la aplicación lo libere. Por ello, este argumento siempre debe ser cero.

lpSecurityAttributes: Atributos de seguridad. En Windows 9.x se ignora del todo y, probablemente (la ayuda no dice nada al respecto) en Windows NT y 2000 sea similar. Así que este argumento se pondrá a ‘nil’.

dwCreationDisposition: Método de apertura y creación. Este es uno de los argumentos importantes. Si no se pone OPEN_EXISTING la función fallará. Esto es, el puerto de comunicaciones a abrir debe existir.

dwFlagsAndAttributes: Este argumento puede dejarse a cero si no se va a implementar las lecturas y escrituras a través de hilos, esto es, si toda la lectura y escritura se realiza desde el hilo principal de la aplicación. En caso contrario, se pondrá a FILE_FLAG_OVERLAPPED y se deberá pasar una estructura de solapamiento (OVERLAPPED o TOverlapped) a las funciones de lectura y escritura.

hTemplateFile: Este argumento debe ponerse a ‘nil’. En Win95 no está soportado. En cualquier otro sistema (Windows NT, 2000) se debe poner igualmente a ‘nil’ para puertos de comunicaciones.

La llamada a ‘CreateFile()’ abre el puerto de comunicaciones especificado y devuelve el manejador o handle del mismo. Si se produce algún error, devolverá la constante INVALID_HANDLE_VALUE y el error se recuperará con la función del API ‘GetLastError()’.

Una vez se tiene abierto el puerto, el siguiente paso es configurar los parámetros del mismo. Para ello, se utilizan las funciones comentadas antes ‘SetCommState()’, ‘SetCommConfig()’ o ‘SetDefaultCommConfig()’. La primera función admite un argumento de tipo record DCB. Las dos últimas admiten un argumento de tipo record COMMCONFIG, pero uno de los campos de esta estructura es un DCB, que es precisamente donde se pueden especificar los parámetros del puerto. La estructura DCB tiene los siguientes campos:

DCBlength: tamaño de la estructura. Se utiliza el SizeOf(DCB) para ello.

BaudRate: velocidad en baudios para el puerto.

Flags: si estuviésemos en C diría que es un campo de bits pero no es el caso. Si se mira la ayuda se verá que la definición de la estructura difiere en este punto y es porque ni Pascal ni Delphi soportan los campos de bits. Así que se ha implementado como un número ‘Longint’. Lo malo es que aquí tenemos que especificar varios parámetros del control de flujo del puerto. Un ejemplo de su utilización se puede ver en el código del programa de ejemplo: se declaran varias constantes y se realizan operaciones ‘or’ con ellas. Es importante indicar que las comunicaciones en Windows siempre se realizan en modo binario (lo que se indica con un valor de $01 en este campo).

WReserved: claro ¿no?

XonLim: Durante el proceso de transmisión, la aplicación envía grupos de bytes hasta que recibe un carácter XOFF que indicará la saturación del buffer de salida. El puerto sigue transmitiendo los bytes del buffer de salida y, cuando éste alcanza un límite mínimo le indica a la aplicación, mediante un XON, que puede reanudar el envío de bytes. Aquí se especifica el número menor de bytes que debe haber en el buffer de salida antes de que se envíe un carácter XON. Este campo no tendrá efecto si no se ha activado el control de flujo software. Para ello, se debe hacer un ‘or’ al campo Flags anterior con un $100.

XoffLim: Aquí se especifica el máximo número de bytes que debe haber en el buffer de salida antes de que el módem envíe un carácter XOFF al ordenador para que éste deje de enviar datos. Como el campo anterior, no tendrá efecto si no se ha especificado el control de flujo software.

ByteSize: Tamaño de los bits de datos: 5, 6, 7 u 8.

Parity: Tipo de paridad a utilizar.

StopBits: Bits de paro

XonChar: Este es el carácter que enviará el módem como XON. Su valor predeterminado es #17 y se conoce también con el nombre de DC1 (Device Control 1, o control de dispositivo 1). El carácter es almacenado en el registro S31 del módem (ver tabla 2). No se tiene en cuenta si no se activado el control de flujo software.

XoffChar: Este es el carácter que enviará el módem como XOFF. Su valor predeterminado es #19 y se conoce también con el nombre de DC3 (Device Control 3, o control de dispositivo 3). No se tiene en cuenta si no se activado el control de flujo software.

ErrorChar: Este carácter se utilizará para reemplazar cualquier carácter que halla llegado con error de paridad lo que permitirá a la aplicación comprobar si la transmisión ha sido correcta. No se tiene en cuenta si no se ha especificado en el campo Flags que se desea el reemplazamiento mediante un ‘or’ con $02.

EofChar: Este es el carácter empleado por el módem para indicar el final de los datos. Generalmente no se usa (#0)

EvtChar: Algunos módem pueden producir otros eventos de aquellos que son estándar. Si deseamos que se nos avise de estos eventos con un carácter especial este es nuestro campo: se pone aquí y el módem contestará (por favor, si vuestro módem contesta, avisarme: yo no lo he conseguido).

wReserved1: debería llamarse wReserved2.

Antes de seguir una cuestión. Hay una cosa clara: si estamos enviando o recibiendo un archivo binario y se activa el control de flujo software, cuando lleguen los caracteres XonChar, XoffChar, ErrorChar, EofChar o EvtChar se deberá ser muy hábil para separar esos caracteres de los bytes que se leen del puerto. Nunca se podrá asegurar que en un archivo binario cualquiera no vayan a existir los bytes correspondientes a esos caracteres. Para obviarlo, el controlador genera una interrupción que deberá ser tratada por el programa, debiendo éste restaurar la condición de escritura antes de continuar el envío.

Por otra parte, el control de flujo software es más lento que el hardware ya que debe transmitirse un byte desde el puerto al controlador del puerto. Con el control de flujo hardware sólo se activa un bit y se pasa al programa de aplicación en una máscara (un entero) que contiene otra información: varios eventos pueden llegar al mismo tiempo al programa de aplicación.

Bien, una vez se ha rellenado la estructura se le pasa a la función deseada (‘SetCommState()’, ‘SetCommConfig()’ o ‘SetDefaultCommConfig()’) y el puerto quedará configurado si todo va bien. En caso contrario, se puede saber la causa del error con ‘GetLastError()’ y generalmente será porque nos hemos equivocado en el relleno de la estructura. Traduzco más o menos de la ayuda:

1.- El número de bits de datos (ByteSize) debe estar entre 5 y 8.

2.- El uso de 5 bits de datos con 2 bits de paro (StopBits) es inválido.

3.- El uso de 6, 7, o 8 bits de datos con 1,5 bits de paro es inválido.

Ahora ya se puede escribir en el puerto mediante la función ‘WriteFile()’ o leer del mismo mediante ‘ReadFile()’. Cuando finalice, cierre el puerto con la función ‘CloseHandle()’ (tanto hablar de funciones que acaban en File y he aquí que para cerrarlo NO se usa un ‘CloseFile()’).

Fin de etapa

El código que acompaña a este artículo he intentado comentarlo lo máximo posible para que sea fácil de entender y de adaptar a las necesidades de cada cual. El programa puede compilarse de dos maneras:

a) Si se define la constante USAR_HILO_EVENTOS (por defecto), desde Project, Options, Directories/Conditionals, el programa utiliza un hilo para monitorizar los eventos del puerto y avisar cuando hay algo que leer en el mismo.

b) Si no está definida, se utiliza un timer que sondea a cada intervalo si en el buffer de entrada hay algo, y de haberlo, lo lee. No es muy fiable esté método debido a que, si se reciben más caracteres del puerto de lo especificado en su buffer antes del siguiente intervalo del timer, los caracteres se perderán.

Tampoco la forma de tratar los eventos del hilo es la correcta ya que, si se recibe un evento mientras se está en el método ‘Synchronize()’ del hilo puede perderse (de hecho, en algunas circunstancias, parece funcionar peor que el timer). Sin embargo, se puede ver como el transvase de mensajes entre el hilo y la aplicación no se interrumpe: enviando un comando como AT&V repetidas veces (manteniendo pulsado el Enter se consigue) se verá que, mientras que el timer se detiene, el hilo continúa. De todas formas, el trasiego de información entre el hilo secundario y el principal debería tardar lo menos posible. Para ello el hilo debería poner la información en algún lugar y volver a ‘Execute()’ cuanto antes.

El programa muestra 3 formas de configurar el puerto: desde la propia aplicación ‘a mano’ (esto es, mediante un formulario); obteniendo los parámetros por defecto del puerto en cuestión mediante la llamada a ‘GetDefaultCommConfig()’; y, por último, mostrando el diálogo de configuración de puertos de Windows llamando a ‘CommConfigDialog()’.

A lo largo del programa encontrará la mayoría de las funciones de configuración de puertos de Windows. Nos hemos dejado en el tintero unas pocas que veremos en una próxima ocasión. Hasta entonces.