Sunteți pe pagina 1din 28

 textos sobre programación 

Artículos con Clase


Artículos con Clase
Estilo: Normal Activar

Visitas desde 2002-04-27

Usuarios en línea

Con Clase
C++
Win API 32
HTML y CSS
Gráficos
MySQL
Artículos
Listas

Comentario

Novedades
Colabora

Algoritmos
Matemáticos
Ordenación
Gráficos
Windows (José Navarro)
Juegos
Multimedia
Opinión
Encriptación
Comunicación
El puerto serie en Windows
Generalidades
Abrir un canal
Modificar los parámetros
Escribir en el puerto
Leer desde el puerto serie
Programa de ejemplo
Operaciones superpuestas
Notas finales

Firmar libro
Ver libro

El puerto serie en Windows


^
Generalidades
^

En Windows no es posible acceder a los dispositívos físicos directamente, a través de las direcciones de sus
puertos. Por el contrario, todos los accesos (salvo que estemos programando un driver) deben hacerse a través
de funciones del API.

Los puertos serie, por tratarse de dispositivos incluidos como parte de los PC desde sus comienzos, están muy
bien integrados en el API de Windows, por lo tanto, tenemos un amplio repertorio de funciones para
manejarlos.

El presente artículo no pretende estudiar en profundidad todas las funciones y opciones del API de Windows
con referencia al puerto serie, sólo intentaré que aquellos que necesiten usar estos puertos en sus programas
tengan unas nociones más precisas sobre cómo lograrlo.

Windows trata los puertos serie (y también el paralelo), como si se tratase de ficheros de entrada y salida. La
única peculiaridad es que su comportamiento es asíncrono, y esta característica influye mucho en el modo en
que tenemos que programar nuestras aplicaciones cuando usen uno de estos puertos.

El comportamiento asíncrono se debe a varias características de este tipo de comunicación, para empezar, los
datos se envían secuencialmente, a una velocidad relativamente baja. El sistema tiene que estar preparado
para recibir los datos en el momento en que están disponibles, ya que si no actúa así, se perderán
irremisiblemente.

En los ficheros normales, somos nosotros los que decidimos cuándo y cómo leemos o escribimos los datos. Y
como la velocidad de respuesta de estos ficheros es bastante buena, generalmenten no notamos que el
programa se para mientras se procesan estas órdenes de lectura y escritura.

Esto no pasa cuando se lee o se escribe de un puerto serie. Los datos que se reciben por uno de estos canales
hay que leerlos cuando llegan, casi nunca sabremos cuándo el dispositivo que tenemos conectado al otro
extremo del cable va a decidir enviarnos datos. En cuanto a la escritura, pasa algo parecido, no podemos
preveer con precisión si el dispositivo al que enviamos los datos los va a procesar con la velocidad a la que se
los enviamos, o si está o no preparado para recibirlos.

Aunque el sistema operativo dispone de un buffer para almacenar los datos que se reciben, ese buffer es
finito, y si nuestro programa no retira esos datos con cierta frecuencia, los perderá.

En cuanto a las transmisiones hacia afuera, pasa algo parecido. El dispositivo receptor puede tener una
capacidad limitada para procesar los datos, de modo que el sistema debe estar preparado para esperar
mientras el receptor procesa los datos y nos avisa de que está preparado para recibir más.

Estas características hacen que nuestro programa se complique un poco más de lo que en principio
pudiéramos esperar. Pero como veremos, tampoco es para tanto.
Abrir un canal asociado a un puerto serie
^

Lo primero que necesitamos es un fichero asociado a nuestro puerto serie. Para eso usaremos la función del
API CreateFile. CreateFile es una función con muchas opciones y que sirve para muchas cosas, pero ahora nos
centraremos en el puerto serie.

Veremos ahora los valores posibles de los siete parámetros que necesita esta función:

1. LPCTSTR lpFileName: nombre del fichero. Se trata de una cadena que contiene el nombre del puerto que
queremos abrir. Los valores posibles son "COM1", "COM2", "COM3" y "COM4".
2. DWORD dwDesiredAccess: tipo de acceso. En general querremos leer y escribir en el puerto, por lo tanto
especificaremos los valores GENERIC_READ | GENERIC_WRITE.
3. DWORD dwShareMode: modo en que se comparte el fichero. En nuestro caso, un puerto serie no puede
ser compartido, de modo que usaremos 0 para este parámetro.
4. LPSECURITY_ATTRIBUTES lpSecurityAttributes: atributos de seguridad, especifican el modo en que el
fichero se puede heredar por procesos hijos. En nuestro caso no queremos que eso ocurra, de modo que
usamos el valor NULL.
5. DWORD dwCreationDistribution: modo de creación. Los puertos serie son dispositivos físicos, por lo
tanto, existen. El modo de creación será OPEN_EXISTING.
6. DWORD dwFlagsAndAttributes: atributos del fichero. Por ahora no nos interesa ninguno de estos
atributos, usaremos el valor 0.
7. HANDLE hTemplateFile: plantilla de fichero. Se puede especificar un fichero existente del cual se
copirarán los atributos. En nuestro caso no usaremos esta opción, y usaremos el valor NULL para este
parámetro.

idComDev = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE,


0, NULL, OPEN_EXISTING, 0, NULL);

Modificar los parámetros de transmisión


^

Empezaremos por definir el tamaño de los buffers de entrada y salida para el puerto serie. Para ello usaremos
la función SetupComm, usando como primer parámetro el manipulador del puerto obgenido de CreateFile,
como segundo el tamaño del buffer de entrada y para el tercero, el tamaño del buffer de salida:

// Iniciar parámetros de comunicación


if(!SetupComm(idComDev, 1024, 1024)) {
return false;
}

En general, necesitaremos establecer los parámetros de la línea serie que vamos a usar. Tendremos que fijar la
velocidad de transmisión, el número de bits de datos, la paridad, y los bits de stop. Y a veces algunos
parámetros más.

Para hacer esto, primero recuperaremos los parámetros del canal que acabamos de abrir, los modificaremos y
actualizaremos la configuración del canal.
Para recuperar los parámetros usaremos la función GetCommState. Esta función nos devuelve una estructura
DCB, que contiene la configuración actual del puerto serie.

fSuccess = GetCommState(idComDev, &dcb);

De todos los valores que incluye la estructura DCB, de momento sólo nos preocuparemos por unos pocos:

1. DWORD BaudRate: velocidad en baudios. Este parámetro puede tener los siguientes valores: CBR_110,
CBR_300, CBR_600, CBR_1200, CBR_2400, CBR_4800, CBR_9600, CBR_14400, CBR_19200, CBR_38400,
CBR_56000, CBR_57600, CBR_115200, CBR_128000 y CBR_256000.
2. BYTE ByteSize: tamaño de los datos en bits. Tradicionalmente 7 u 8.
3. BYTE Parity: valor de paridad. Se admiten los valores EVENPARITY, para paridad par; MARKPARITY;
NOPARITY, para no paridad y ODDPARITY, para paridad impar.
4. BYTE StopBits: Bits de stop. Admite los valores ONESTOPBIT, 1 bit de stop; ONE5STOPBITS, 1.5 bits de stop
y TWOSTOPBITS, 2 bits de stop.
5. DWORD fBinary:1 Una bandera, en un campo de bits, que indica el modo de transmisión: binario o texto.
1 para modo binario, 0 para modo texto. La diferencia es que en modo texto se verifica la recepción del
carácter EOF, que detiene la transmisión.
6. DWORD fAbortOnError:1 Otra bandera que indica si interrumpe la transmisión o recepción en caso de
error. 1 para interrumpir, 0 para no interrumpir.

Una vez que hemos actualizado la estructura de datos podemos configurar el puerto enviándosela mediante la
función SetCommState.

m_dcb.BaudRate = CBR_115200;
m_dcb.ByteSize = 8;
m_dcb.Parity = NOPARITY;
m_dcb.StopBits = ONESTOPBIT;
m_dcb.fBinary = FALSE;
m_dcb.fAbortOnError = TRUE;

SetCommState(idComDev, &dcb);

Por último, podemos modificar los timeouts. Se trata de unos intervalos de tiempo definidos para que las
rutinas de lectura y escritura distingan entre diferentes paquetes de datos.

Generalmente, las comunicaciones por el puerto serie se harán por bloques, de los que no siempre
conoceremos el tamaño. Estos tiempos nos permiten detectar divisiones entre bloques, de modo que el API las
detecta y pone a nuestra disposición la información leída.

El sistema calcula dos tiempos, uno para lectura y otro para escritura, en función de ciertos parámetros (que
veremos ahora como fijar), y de los caracteres requeridos o escritos.

Estos tiempos se definen en una estructura COMMTIMEOUTS, y se puede ver el uso de cada campo en la
descripción de ella.

La elección de los tiempos y la necesidad de modificarlos dependerá del tipo de comunicación serie que
necesitemos para nuestra aplicación. Estos tiempos harán que las operaciones de lectura y escritura puedan
terminar antes de que se haya completado las funciones de lectura o escritura, para los datos indicados. Su uso
es, de todos modos, opcional.

Para modificarlos usaremos la función GetCommTimeouts para obtener los timeouts actuales en una
estructura COMMTIMEOUTS. Modificaremos esos tiempos a nuestro criterior, y los activaremos usando la
función SetCommTimeouts.

// Leer time-outs actuales:


GetCommTimeouts (hComm, &m_CommTimeouts));

// Nuevos valores de timeout:


m_CommTimeouts.ReadIntervalTimeout = 50;
m_CommTimeouts.ReadTotalTimeoutConstant = 50;
m_CommTimeouts.ReadTotalTimeoutMultiplier = 10;
m_CommTimeouts.WriteTotalTimeoutConstant = 50;
m_CommTimeouts.WriteTotalTimeoutMultiplier = 10;

// Establecer timeouts:
SetCommTimeouts (hComm, &m_CommTimeouts));

Escribir en el puerto serie


^

Para enviar caracteres al puerto serie se usa la función WriteFile.

Sin embargo, como ya hemos explicado, no basta con enviar los caracteres al puerto serie, el destinatario
puede interrumpir la transmisión si no es capaz de procesar los datos a la misma velocidad que se los
enviamos, de modo que los datos que intentamos enviar pueden no ser enviados por completo.

Para estar seguros de que enviamos toda la información que queremos, usaremos uno de los parámetros que
devuelve la función, y que nos dice cuántos caracteres se han enviado.

Colocando la función WriteFile en un bucle, podemos enviar los caracteres que aún están pendientes hasta
que todos hayan sido enviados.

WriteFile puede retornar con valor false si se ha producido un error. Sin embargo, uno de los errores no es tal,
el error ERROR_IO_PENDING en realidad sólo nos informa de que no se ha completado la operación de
escritura. En caso de recibir ese error, debemos continuar enviado datos al puerto serie.

Si estamos seguros de que el receptor puede procesar los datos enviados, o si hemos diseñado un mecanismo
para que el receptor nos informe de que los datos no han llegado correctamente, podemos ignorar el valor de
retorno y el de iBytesWritten de la función WriteFile, y continuar asumiendo que los datos fueron enviados.

void EscribirSerie(HANDLE idComDev, char *buf)


{
char oBuffer[256]; /* Buffer de salida */
DWORD iBytesWritten;

iBytesWritten = 0;
strcpy(oBuffer, buf);
WriteFile(idComDev, oBuffer, strlen(oBuffer), &iBytesWritten, NULL);
}

En la mayor parte de los casos, y sobre todo cuando somos los responsables de programar los dos extremos de
la comunicación serie, este modo de trabajar puede ser suficiente.

Si por el contrario, el otro extremo de la comunicación no es responsabilidad nuestra, puede que debamos
depurar algo más esta función.

Podemos usar el valor del parámetro iBytesWriten posterior a la ejecución para actualizar el buffer de salida.
Indicaremos algún valor de retorno en EscribirSerie para indicar que la operación de escritura no ha enviado
todos los caracteres del buffer, y el proceso que la ha invocado debe llamar de nuevo para completar la
transmisión.

Leer desde el puerto serie


^

Por la misma naturaleza de las transmisiones, y debido a que nuestro ordenador normalmente será muy
rápido en comparación con las velocidades de transmisión, la mayor parte del tiempo no estaremos
recibiendo nada por el puerto serie.

Necesitamos un mecanismo que avise a nuestro programa cuando existan datos para leer, pero no debemos
bloquear el sistema preguntando constantemente si hay algo preparado. Ni siquiera debemos bloquear
nuestro programa, a menudo hay otras cosas que hacer además de esperar la llegada de nueva información.

La mejor forma es introducir crear un hilo distinto del de nuestro programa principal. Podemos hacer que ese
hilo espere a que haya datos para leer, y cuando eso ocurra, que lo procese.

Lo siguiente que necesitamos saber es cuantos caracteres hay en el buffer de entrada. Para esas dos
necesidades podemos usar la función ClearCommError. Esta función nos actualiza una estructura COMSTAT,
uno de cuyos miembros es cbInQue, que nos dice cuantos caracteres hay en el buffer de entrada. Si no hay
ninguno, no hay nada que hacer, si hay caracteres en el buffer, los leemos y procesamos.

Con ese dato podemos llamar a la función ReadFile, y leer todos los caracteres que haya en el buffer.

// Creamos el hilo:
hHilo = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Hilo, (LPDWORD)&idComDev, 0, &id);
...

// Hilo de escucha del puerto serie:


DWORD Hilo(LPDWORD lpdwParam)
{
DWORD leidos;
COMSTAT cs;
char *cad;
HANDLE idComDev = *((HANDLE*)lpdwParam);

do {
ClearCommError(idComDev, &leidos, &cs);
leidos=0;
/* Leer buffer desde puerto serie */
if(cs.cbInQue) {
cad = new char[cs.cbInQue+3]; // Caracteres en buffer, más retorno de línea, más nulo
ReadFile(idComDev, cad, cs.cbInQue, &leidos, NULL);
ProcesarDatos(cad);
delete[] cad;
}
Sleep(100); // Esperar entre una lectura y otra.
} while(true);
return 0;
}

El tiempo de espera en la función Sleep está elegido arbitrariamente, se puede indicar un tiempo menor, si se
necesita una respuesta más rápida por parte del programa. Sin embargo siempre es necesario dejar algún
tiempo, para no sobrecargar la CPU excesivamente.

Programa de ejemplo
^

Con esto ya es suficiente para crear un programa de comunicaciones serie sencillo, pero que ilustra la mayor
parte de los casos que necesiten comunicación serie.

El siguiente ejemplo es un monitor de comunicación serie en modo texto. Pueden usarse dos programas en
ordenadores diferentes, conectados con un cable serie, y es posible comunicarse entre ellos. Yo he probado
este programa como terminar serie de un VAX, y salvo por las secuencias de escape propias de estos
terminales, el programa funciona perfectamente.

  Nombre Fichero Fecha Tamaño Contador Descarga


Ejemplo Serie API Windows serie.zip 2013-01-31 2545 bytes 891 Descargar

Operaciones superpuestas
^

Todo lo dicho es para usar el puerto serie de forma no superpuesta, no overlapped. La ventaja de este modo es
que es más sencillo de implemetar y más portable.

Existe otro modo, el superpuesto, u overlapped. Este modo es más eficiente, pero es más complicado de
implementar y menos portable.

En este artículo no hablaremos del modo overlapped, y lo dejaremos para otro momento.

Notas finales
^

En casi todos los ejemplos del uso del puerto serie usando el API de Windows se hace uso de las funciones
SetCommMask y WaitCommEvent para leer datos desde el puerto. He de decir que no he coseguido que
ningún programa que haga uso de ellas funcione correctamente. Siempre se quedan bloquedos en la llamada
a WaitCommEvent, y jamás se reciben datos, ni se pueden enviar más de una vez.

Supongo que estaré haciendo algo mal, pero por más que he buscado en la red y por más pruebas que he
hecho, no he conseguido mejores resultados.

Si alguien sabe qué está pasando, por favor, que me lo cuente. Estaré encantado de completar este artículo con
la nueva información.

Incluyo un segundo ejemplo que hace uso de esas funciones en el hilo de lectura.

  Nombre Fichero Fecha Tamaño Contador Descarga


Ejemplo Serie 2 serie2.zip 2013-01-31 2736 bytes 324 Descargar

Comentarios de los usuarios (2)

TOMAS HERNANDEZ
2014-08-16 05:21:37

en que lenguaje esta escrito este codigo?

y con que lo compilo?

Meta
2016-02-04 19:31:08

Hola:

Puerto serie.

Estoy intentando en adaptar el código en formulario Windows con Visual Studio Community 2015 en dos
proyectos. Primero me centro en MFC y más adelante con Win32, por ahora, en modo visual.

En la página 30 de este documento creas el formulario con MFC.


http://www.slideshare.net/Metaconta2/formulario-windows-con-visual-c

Supongamos que quiero enviar al puerto serie el comando ON con un botón y el otro botón el comando OFF.

A parte de esto, con un richTextBox muestra los datos recibidos del puerto serie.

¿Alguna ayuda en este aspecto?

Un cordial saludo y buen trabajo por este tutorial.

PD: Me alegra que hagan ejemplos del puerto serie ya que se usa mucho en los PIC, Arduino e incluso hasta
Raspberry Pi.

Buscar Buscar en: Con Clase


suministrado por FreeFind

© Enero de 2013, Salvador Pozo, salvador@conclase.net