Serielle Portverbindung in C++

Muhammad Husnain 12 Oktober 2023
Serielle Portverbindung in C++

Die Grundlagen der Verwendung der Windows-API zum Öffnen, Lesen, Schreiben und Verwalten von seriellen Portverbindungen werden in diesem Artikel behandelt.

Der Hauptzweck dieses Tutorials besteht darin, Ihnen ein grundlegendes Verständnis dafür zu vermitteln, wie die serielle Kommunikation in der Programmierung funktioniert, und Ihnen den Einstieg in die richtige Richtung zu erleichtern.

In diesem Artikel wird davon ausgegangen, dass Sie über grundlegende Kenntnisse von C/C++ verfügen, Programme kompilieren und ausführen können und Ihre Entwicklungsumgebung für die Verwendung von Windows-API-Aufrufen eingerichtet ist.

Serielle Portverbindung in C++

Es gibt sechs Schritte zum Lesen von Daten oder Schreiben auf serielle Ports in C++:

  1. Öffnen Sie die seriellen Ports
  2. Legen Sie einige grundlegende Eigenschaften fest
  3. Stellen Sie die Zeitüberschreitung ein
  4. Lesen oder schreiben Sie die Daten
  5. Bereinigen Sie die Ports
  6. Einige erweiterte Funktionen

Öffnen Sie die serielle Schnittstelle

Der erste Schritt ist die Öffnung des Hafens. Dies ist einer der einfachsten und einfachsten Schritte, insbesondere wenn Sie die Windows-Datei-E/A bereits kennen.

Zunächst müssen Sie sicherstellen, dass Sie die erforderlichen Header-Dateien, also windows.h, in Ihre Datei eingebunden haben. Verwenden Sie dann den folgenden Code:

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.
}

Wir haben in der ersten Zeile eine Variable vom Typ HANDLE erstellt und dann die Funktion CreateFile aufgerufen, um sie zu initialisieren. Das erste Argument für diese Funktion ist der Name der Datei, die Sie öffnen müssen; In diesem Fall möchten wir einen seriellen Port öffnen, deshalb haben wir den Namen dieses Ports verwendet, dh COM1.

Das zweite Argument gibt an, ob wir die Daten lesen oder schreiben müssen. Wenn keine der Aufgaben erledigt werden soll, können Sie dieses Argument belassen.

Die nächsten beiden Argumente werden immer auf Null gehalten. Das nächste Argument besteht darin, anzugeben, ob eine vorhandene Datei geöffnet oder eine neue Datei erstellt werden muss.

In diesem Fall ist es der Port, also haben wir OPEN_EXISTING verwendet. Das nächste Argument ist, Windows mitzuteilen, dass wir nichts Besonderes brauchen, außer regelmäßigem Lesen oder Schreiben.

Das letzte Argument wird ebenfalls immer auf Null gehalten.

Legen Sie die grundlegenden Eigenschaften fest

Nachdem wir das HANDLE des Ports erhalten haben, müssen wir einige der grundlegenden Eigenschaften wie Bandrate, Bytegröße, Stoppbits usw. festlegen. Dies geschieht mit einer Struktur 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
}

Im obigen Codeausschnitt haben wir in der ersten Codezeile ein Objekt von DCB erstellt und es mit Null initialisiert, um die Werte zu löschen. In der folgenden Zeile haben wir die Länge dieser Struktur festgelegt, was ein obligatorischer Schritt von Windows ist.

Danach haben wir die Funktion GetCommState aufgerufen und ihr zwei Parameter übergeben, nämlich unser Port HANDLE- und DCB-Objekt, um die aktuell verwendeten Parameter auszufüllen.

Sobald wir diese haben, müssen wir die kritischen Parameter einstellen, z. B. BaudRate, ByteSize, StopBits und Parity.

Windows benötigt uns, um die BaudRate mit speziellen Konstanten bereitzustellen. Zum Beispiel steht CBR 19200 für 19200 Baud, CBR 9600 für 9600 Baud, CBR 57600 für 57600 Baud usw.

Wir können die ByteSize direkt definieren, aber StopBits und Parity erfordern zusätzliche Variablen. ONESTOPBIT, ONE5STOPBITS und TWOSTOPBITS sind die StopBits-Möglichkeiten.

EVENPARITY, NOPARITY und ODDPARITY sind die am häufigsten verwendeten Alternativen für Parity. Andere existieren, aber sie sind weniger bekannt.

Weitere Informationen finden Sie im MSDN-Bibliothekselement (Suche nach DCB).

Wir müssen diese Einstellungen auf die serielle Schnittstelle anwenden, nachdem wir die DCB-Struktur für unsere Prioritäten konfiguriert haben. Dazu wird die Funktion SetCommState verwendet.

Stellen Sie die Zeitüberschreitung ein

Das Lesen von der seriellen Schnittstelle kann dazu führen, dass Ihre Anwendung anhält, während sie auf das Erscheinen von Daten wartet, wenn keine Daten an der seriellen Schnittstelle eingehen (z. B. wenn das Gerät der seriellen Schnittstelle ausgeschaltet oder getrennt ist). Es gibt zwei Möglichkeiten, mit dieser Situation umzugehen.

Erstens kann Multithreading in Ihrer Anwendung verwendet werden, wobei sich ein Thread mit den Problemen der seriellen Schnittstelle und der andere mit der eigentlichen Verarbeitung befasst. Dies kann umständlich und kompliziert werden und ist nicht erforderlich.

Die Alternative ist viel einfacher: Sagen Sie Windows, dass es aufhören soll, auf das Erscheinen von Daten zu warten! Dies wird durch Folgendes erreicht:

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
}

Die Struktur von COMMTIMEOUTS ist ziemlich einfach, mit den oben aufgeführten Feldern. Eine kurze Zusammenfassung:

  • ReadIntervalTimeout - gibt die Zeit an, die zwischen dem Empfang von Zeichen vergehen muss, bevor eine Zeitüberschreitung eintritt (in Millisekunden).
  • ReadTotalTimeoutConstant – gibt die Wartezeit vor der Rückkehr an (in Millisekunden).
  • ReadTotalTimeoutMultiplier – gibt die Zeitdauer an, die gewartet werden soll, bevor für jedes bei der Leseoperation angeforderte Byte geantwortet wird (in Millisekunden).
  • WriteTotalTimeoutConstant und WriteTotalTimeoutMultiplier – beide leisten dasselbe wie ReadTotalTimeoutConstant und WriteTotalTimeoutMultiplier, aber für Schreibvorgänge statt für Lesevorgänge.

Das Setzen von ReadIntervalTimeout auf MAXDWORD und sowohl ReadTotalTimeoutConstant als auch ReadTotalTimeoutMultiplier auf 0 bewirkt, dass alle Leseoperationen sofort mit allen Zeichen im Puffer zurückgegeben werden (d. h. bereits empfangen wurden), auch wenn keine vorhanden sind.

Nach der Konfiguration der COMMTIMEOUTS-Struktur müssen wir die SetCommTimeouts-Methode verwenden, um die Änderungen auf die serielle Schnittstelle anzuwenden.

Daten lesen/schreiben

Sie können mit dem Lesen von Daten nach einer offenen seriellen Schnittstelle mit den erforderlichen Parametern und Zeitüberschreitungen beginnen. Diese sind leicht verständlich.

Betrachten Sie das Szenario des Lesens von n Bytes von der seriellen Schnittstelle. Dann folgen wir einfach diesen Schritten:

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

Ein HANDLE an eine Datei (serielle Schnittstelle), ein Puffer, in dem Daten gespeichert werden, die Anzahl der zu lesenden Bytes, ein Verweis auf eine Ganzzahl, die auf die Anzahl der gelesenen Bytes gesetzt werden soll, und NULL werden alle an ReadFile übergeben. . Die Anzahl der von der Operation ReadFile gelesenen Bytes wird in dwRead gespeichert.

Das Schreiben der Daten ist auch das gleiche Verfahren. Der einzige Unterschied besteht darin, dass die Funktion WriteFile Daten schreiben kann.

Schließen Sie den Hafen

Wenn Sie mit der Verwendung des seriellen Anschlusses fertig sind, schließen Sie den Griff. Wenn Sie dies nicht tun, können seltsame Dinge passieren, einschließlich, dass niemand sonst den seriellen Anschluss verwenden kann, bis Sie neu starten.

In jedem Fall ist es ziemlich einfach zu bewerkstelligen, also behalte Folgendes im Hinterkopf:

CloseHandle(h_Serial);

Behandeln Sie die Fehler

Wie Sie vielleicht gesehen haben, haben wir nach jedem Systemaufruf einen Hinweis eingefügt, dass Sie Fehler behandeln sollten.

Dies ist immer eine ausgezeichnete Programmierpraxis, aber besonders wichtig bei E/A-Funktionen, die häufiger fehlschlagen. Alle folgenden Funktionen liefern in jedem Fall 0, wenn sie fehlschlagen, und alles andere als 0, wenn sie erfolgreich sind.

Um herauszufinden, was schief gelaufen ist, verwenden Sie die Funktion GetLastError(), die den Fehlercode als DWORD zurückgibt. Mit der Methode FormatMessage können Sie daraus einen sinnvollen String machen:

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