Conexión de puerto serie en C++

Muhammad Husnain 12 octubre 2023
Conexión de puerto serie en C++

En este artículo se tratan los aspectos básicos del uso de la API de Windows para abrir, leer, escribir y administrar conexiones de puerto serie.

El objetivo principal de este tutorial es brindarle una comprensión básica de cómo funcionan las comunicaciones en serie en la programación y ayudarlo a comenzar en la dirección correcta.

Este artículo supondrá que tiene conocimientos básicos de C/C++, que puede compilar y ejecutar programas y que su entorno de desarrollo está configurado para usar llamadas a la API de Windows.

Conexión de puerto serie en C++

Hay seis pasos para leer datos o escribir en puertos serie en C++:

  1. Abra los puertos serie
  2. Establecer algunas propiedades básicas
  3. Establecer el tiempo de espera
  4. Leer o escribir los datos
  5. Limpia los puertos
  6. Algunas funciones avanzadas

Abra el puerto serie

El primer paso es la apertura del puerto. Este es uno de los pasos más fáciles y sencillos, especialmente si ya conoce la E/S de archivos de Windows.

Primero, debe asegurarse de haber incluido los archivos de encabezado requeridos, es decir, windows.h en su archivo. Luego, usa el siguiente código:

HANDLE h_Serial;
h_Serial = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL, 0);
if (h_Serial == INVALID_HANDLE_VALUE) {
  if (GetLastError() == ERROR_FILE_NOT_FOUND) {
    // serial port not found. Handle error here.
  }
  // any other error. Handle error here.
}

Hicimos una variable de tipo HANDLE en la primera línea y luego llamamos a la función CreateFile para inicializarla. El primer argumento para esta función es el nombre del archivo que necesita abrir; en este caso queremos abrir un puerto serial, por eso usamos el nombre de ese puerto, es decir, COM1.

El segundo argumento es para especificar si necesitamos leer o escribir los datos. Si no se va a realizar ninguna de las tareas, puede dejar este argumento.

Los siguientes dos argumentos siempre se mantienen en cero. El siguiente argumento es especificar si se necesita abrir un archivo existente o crear uno nuevo.

En este caso, es el puerto, por lo que usamos OPEN_EXISTING. El siguiente argumento es especificarle a Windows que no necesitamos nada sofisticado sino lectura o escritura regulares.

El último argumento también se mantiene siempre en cero.

Establecer las propiedades básicas

Después de obtener el HANDLE del puerto, debemos establecer algunas de las propiedades básicas como la tasa de banda, el tamaño de byte, los bits de parada, etc. Esto se hace usando una estructura DCB.

DCB dcbSerialParam = {0};
dcbSerial.DCBlength = sizeof(dcbSerialParam);

if (!GetCommState(h_Serial, &dcbSerialParam)) {
  // handle error here
}

dcbSerialParam.BaudRate = CBR_19200;
dcbSerialParam.ByteSize = 8;
dcbSerialParam.StopBits = ONESTOPBIT;
dcbSerialParam.Parity = NOPARITY;

if (!SetCommState(h_Serial, &dcbSerialParam)) {
  // handle error here
}

En el fragmento de código anterior, creamos un objeto de DCB en la primera línea de código y lo inicializamos con cero para borrar los valores. En la siguiente línea, hemos establecido la longitud de esta estructura, que es un paso obligatorio de Windows.

Después de eso, llamamos a la función GetCommState y le pasamos dos parámetros, es decir, nuestro puerto HANDLE y el objeto DCB, para completar los parámetros actualmente en uso.

Una vez que tengamos esto, debemos configurar los parámetros críticos, es decir, BaudRate, ByteSize, StopBits y Parity.

Windows necesita que proporcionemos el BaudRate usando constantes especiales. Por ejemplo, CBR 19200 representa 19200 baudios, CBR 9600 representa 9600 baudios, CBR 57600 representa 57600 baudios, etc.

Podemos definir el ByteSize directamente, pero StopBits y Parity requieren variables adicionales. ONESTOPBIT, ONE5STOPBITS y TWOSTOPBITS son las posibilidades de StopBits.

EVENPARITY, NOPARITY y ODDPARITY son las alternativas más utilizadas para Parity. Existen otros, pero son menos conocidos.

Consulte el elemento de la biblioteca de MSDN (busque DCB) para obtener más información.

Necesitamos aplicar esta configuración al puerto serie después de configurar la estructura DCB según nuestras prioridades. La función SetCommState se utiliza para lograr esto.

Establecer el tiempo de espera

La lectura desde el puerto serie puede hacer que su aplicación se detenga mientras espera que aparezcan los datos si no ingresan datos al puerto serie (por ejemplo, el dispositivo del puerto serie está apagado o desconectado). Hay dos maneras de lidiar con esta situación.

En primer lugar, se pueden usar subprocesos múltiples en su aplicación, con un subproceso que se ocupa de las dificultades del puerto serie y el otro con el procesamiento real. Esto puede volverse engorroso y complicado, y no es necesario.

La alternativa es mucho más simple: ¡dígale a Windows que deje de esperar a que aparezcan los datos! Esto se logra a través de lo siguiente:

COMMTIMEOUTS timeout = {0};
timeout.ReadIntervalTimeout = 60;
timeout.ReadTotalTimeoutConstant = 60;
timeout.ReadTotalTimeoutMultiplier = 15;
timeout.WriteTotalTimeoutConstant = 60;
timeout.WriteTotalTimeoutMultiplier = 8;
if (!SetCommTimeouts(h_Serial, &timeout)) {
  // handle error here
}

La estructura COMMTIMEOUTS es bastante simple, con simplemente los campos enumerados anteriormente. Un resumen rápido:

  • ReadIntervalTimeout: especifica el tiempo que debe transcurrir entre la recepción de caracteres antes de que se agote el tiempo de espera (en milisegundos).
  • ReadTotalTimeoutConstant: proporciona la cantidad de tiempo de espera antes de regresar (en milisegundos).
  • ReadTotalTimeoutMultiplier: especifica el tiempo de espera antes de responder a cada byte solicitado en la operación de lectura (en milisegundos).
  • WriteTotalTimeoutConstant y WriteTotalTimeoutMultiplier: ambos logran lo mismo que ReadTotalTimeoutConstant y WriteTotalTimeoutMultiplier, pero para escrituras en lugar de lecturas.

Establecer ReadIntervalTimeout en MAXDWORD y tanto ReadTotalTimeoutConstant como ReadTotalTimeoutMultiplier en 0 hace que cualquier operación de lectura regrese instantáneamente con cualquier carácter en el búfer (es decir, ya se ha recibido), incluso si no hay ninguno presente.

Después de configurar la estructura COMMTIMEOUTS, necesitaremos usar el método SetCommTimeouts para aplicar los cambios al puerto serie.

Leer/Escribir los datos

Puede comenzar a leer datos después de un puerto serie abierto con los parámetros y tiempos de espera necesarios. Estos son fáciles de entender.

Considere el escenario de leer n bytes desde el puerto serie. Luego, simplemente seguimos estos pasos:

char sBuff[n + 1] = {0};
DWORD dwRead = 0;
if (!ReadFile(h_Serial, sBuff, n, &dwRead, NULL)) {
  // handle error here
}

Un HANDLE a un archivo (puerto serie), el búfer donde se almacenan los datos, el número de bytes para leer, la referencia a un número entero para establecer el número de bytes leídos y NULL se pasan a ReadFile . El número de bytes leídos por la operación ReadFile se almacenará en dwRead.

Escribir los datos también es el mismo procedimiento. La única diferencia es que la función WriteFile puede escribir datos.

cerrar el puerto

Cuando haya terminado de usar el puerto serie, cierre el asa. Si no lo hace, pueden suceder cosas extrañas, incluido que nadie más pueda usar el puerto serie hasta que reinicie.

En cualquier caso, es bastante fácil de lograr, así que ten en cuenta lo siguiente:

CloseHandle(h_Serial);

Manejar los errores

Como habrá visto, hemos insertado una nota después de cada llamada al sistema que indica que debe manejar los errores.

Esta siempre es una excelente práctica de programación, pero es especialmente crucial con las funciones de E/S, que fallan con más frecuencia. Todas las siguientes funciones, en cualquier caso, devuelven 0 cuando fallan y cualquier cosa que no sea 0 cuando tienen éxito.

Para averiguar qué salió mal, utilice la función GetLastError(), que devuelve el código de error como DWORD. Puedes usar el método FormatMessage para convertir esto en una cadena que tenga sentido:

char lastErr[1020];
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
              GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
              lastErr, 1020, NULL);
Muhammad Husnain avatar Muhammad Husnain avatar

Husnain is a professional Software Engineer and a researcher who loves to learn, build, write, and teach. Having worked various jobs in the IT industry, he especially enjoys finding ways to express complex ideas in simple ways through his content. In his free time, Husnain unwinds by thinking about tech fiction to solve problems around him.

LinkedIn