Serial port bedienen
-
Ich versuche mich gerade am Thema Serial-Port Kommunikation und hoffe auf etwas Unterstützung diese Schritt für Schritt umsetzen, aber vor allem verstehen zu können.
Ziel
Eine Funktion die über alle aktuell verfügbaren COM-Ports einen Check durchführt um das "Richtige" Gerät zu anhand der Antwort auf ein "ATZ\r" zu finden. Die Programmierung würde ich gern mit der Win32API durchführen.Umgebung
Ich verwende die Code::Blocks IDE mit MinGW an einem Windows 10 PC. Da ich am Ende eine Windows GUI Anwendung möchte habe ich das entsprechende Framework dafür gewählt, benutze aktuell aber nur einen Debug-Build in als Ausgabegerät für printf() die sich damit öffnende Shell.Szenario
An meinem Windows PC habe ich drei per USB angeschlossene Geräte welches sich mittels virtuellem COM-Port Treiber wie serielle Schnittstellen verhalten. Nur eines dieser Geräte ist das mit dem ich kommunizieren möchte. Dieses Setup habe ich mir bewusst so ausgewählt.Aufgabe 1) Port-Auflistung
Meine erste Aufgabe bestand nun darin überhaupt zu erkennen welches das Device ist mit dem ich "sprechen" möchte. Zur Enumerierung gehe ich durch alle Werte des Registrierschlüssels SERIALCOMM:RegOpenKeyExA(HKEY_LOCAL_MACHINE, TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"), 0, KEY_QUERY_VALUE, &hk)
Durch das Ergebnis der obigen Query iteriere ich mit
RegEnumValueA(hk, dwIndex, lpValueName, &lpcchValueName, NULL, NULL, lpData, &lpcbData))
Das Ergebnis ist hier jeweils ein "COMx" Bezeichner im Buffer "lpData".
Aufgabe 2) Seriellen Port öffnen
Zum Zugriff auf den Port verwende ich dann den CreateFileA() Aufruf:sprintf(gszPort, "\\\\.\\%s", lpData); hComm = CreateFileA(gszPort, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0)
Frage hierzu: Warum heißt die Funktion eigentlich "CreateFileA()" und nicht einfach nur "CreateFile()"? Die letztere kennt meine IDE nicht.
Damit ich einen für CreateFileA() gültigen Filename habe formatiere ich einen String vom Muster "\.\COMx":
sprintf(gszPort, "\\\\.\\%s", lpData);
Aufgabe 3) Port-Parameter einstellen
Nun geht es wohl daran den geöffneten Port auf das richtige Protokoll zu bekommen. Das von mir gesuchte Ziel arbeitet mit 38400 8N1.
Dazu habe ich folgenden Ablauf zusammengeschraubt (Fehlerbehandlungsroutinen habe ich der Übersichtlichkeit wegen weggelassen):GetCommState(hComm, &dcbSerialParams); dcbSerialParams.BaudRate = CBR_38400; dcbSerialParams.ByteSize = 8; dcbSerialParams.StopBits = ONESTOPBIT; dcbSerialParams.Parity = NOPARITY; SetCommState(hComm, &dcbSerialParams);
Aufgabe 4) "ATZ" Kommando an serielles Gerät senden
Die eigentliche Senderoute würde ich mit WriteFile() (diesmal ohne das "A" am Ende?) machen:char cmd[] = "ATZ\r"; WriteFile(hComm, cmd, strlen(cmd), &BytesWritten, NULL);
-TODO: Wie erkenne ich das alles gesendet ist bzw. das es nicht funktioniert weil das Endgerät z.B. nichts einliest?
-TODO: Overlapped oder Nonoverlapped I/O??Aufgabe 5) Antwort (oder Timeout) auswerten
Einlesen würde ich dann wohl mit ReadFile() machen:ReadFile(hComm, &ReadData, sizeof(ReadData), &NoBytesRead, NULL)
-TODO: Das falsche Gerät antwortet nicht, oder mit Mist. Die Antwort die ich erwarte passt in ein Pattern, also Mischung als statischen und variablen Anteilen.
-
zu 2.
Die Windows API besteht aus Funktionen die ein char nehmen (Buchstabe A am Ende) und Funktionen, die ein wchar_t nehmen (Buchstabe W am Ende).
Ein Namen (CreateFile) ohne A/W ist nur ein ein Define auf den eingestellt "Charset" für deine Windows Entwicklungsumgebung also (CreateFileA/CReateFileW)
Es gibt auch eine alte TCHAR Notation, die beides kann und variabel auf UNICODE/Char funktionieren würde.hComm = CreateFile(_T("\\\\.\\COM4"), GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0)
Anmerkung: Dir fehlen gravierende Basics, was die WinAPI Entwicklung betrifft.
Zu 4.
Bei non Overlapped io kommt die Funktion nicht zurück, ehe ein Fehler auftritt oder alles versendet wurde.
Overlapped io spricht hier für sich selbst, entweder fertig oder Fehler.zu 5.
Wäre auch die Frage auf overlapped i/o umzustellen. Dann hast Du mehr Möglichkeiten mit Timeouts, oder teilweise gelesenen Daten.
Ansonsten (non-overlapped) kommt ReadFile nicht zurück bis eben ein Fehler auftritt oder eben alle Daten, die Du wolltest da sind.
-
@Knackwurst sagte in Serial port bedienen:
-TODO: Wie erkenne ich das alles gesendet ist bzw. das es nicht funktioniert weil das Endgerät z.B. nichts einliest?
Das geht nur beim Hardware-Handshake, wenn die Funktion in den Timeout läuft.
Dann sollte BytesWritten nicht mit strlen(cmd) übereinstimmen.Ansonsten werden die Daten einfach raus geschrieben und du musst auf Antwort hoffen.
Cool wäre natürlich, wenn das Gerät SCPI kann.
-
@Martin-Richter sagte in Serial port bedienen:
hComm = CreateFileA(_T("\\\\.\\COM4"), GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0)
Du meinst wohl
CreateFile
(ohneA
), denn sonst würde ja beiUNICODE
ein falscher Parameter übergeben?!
-
@Th69 sagte in Serial port bedienen:
@Martin-Richter sagte in Serial port bedienen:
hComm = CreateFileA(_T("\\\\.\\COM4"), GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0)
Du meinst wohl
CreateFile
(ohneA
), denn sonst würde ja beiUNICODE
ein falscher Parameter übergeben?!Du hast natürlich vollkommen recht. Typischer Copy & Paste Fehler. Korrigiert!
-
@Martin-Richter sagte in Serial port bedienen:
Anmerkung: Dir fehlen gravierende Basics, was die WinAPI Entwicklung betrifft.
Das hast Du leider recht, aber irgendwo muss man ja mit irgendwas anfangen zu lernen...
-
@Martin-Richter sagte in Serial port bedienen:
zu 5.
Wäre auch die Frage auf overlapped i/o umzustellen. Dann hast Du mehr Möglichkeiten mit Timeouts, oder teilweise gelesenen Daten.
Ansonsten (non-overlapped) kommt ReadFile nicht zurück bis eben ein Fehler auftritt oder eben alle Daten, die Du wolltest da sind.Ich dachte die Entscheidung Overlapped (Asynchron) oder Nonoverlapped (Synchron) würde nur beim öffnen des File-Handles gemacht und nicht bei einzelnen Funktionen? Heißt das ich muss das bei jeder IO-Funktion angeben?
Wenn ich beim CreateFile kein Flag angebe gilt ja Nonoverlapped IO, d.H. ich müsste dort beim 6. Parameter FILE_FLAG_OVERLAPPED angeben, richtig? Das heißt doch das ich im Synchronen Modus arbeite.
-
Hier mal mein aktueller Code (müsste Nonoverlapped IO sein):
HKEY hk; long h; // Enumerate COM ports h = RegOpenKeyExA(HKEY_LOCAL_MACHINE, TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"), 0, KEY_QUERY_VALUE, &hk); if (h == ERROR_SUCCESS) { DWORD cSubKeys; DWORD cValues; cSubKeys = 0; cValues = 0; RegQueryInfoKeyA(hk, NULL, NULL, NULL, &cSubKeys, NULL, NULL, &cValues, NULL, NULL, NULL, NULL); if (cValues == 0) { printf("DEBUG: No devices found\n"); break; } printf("DEBUG: Number of devices found: %ld\n", cValues); // Iterate through all COM ports found long rc; LPTSTR lpValueName = new TCHAR[50]; DWORD lpcchValueName = 50; DWORD dwIndex = 0; LPBYTE lpData = new BYTE[50]; DWORD lpcbData = 50; while ( (rc = RegEnumValueA(hk, dwIndex, lpValueName, &lpcchValueName, NULL, NULL, lpData, &lpcbData)) == ERROR_SUCCESS) { //printf("DEBUG: item #%d: %s (len %d) %s (len %d)\n", dwIndex, lpData, lpcbData, lpValueName, lpcchValueName); printf("DEBUG: Found device %s\n", lpData); dwIndex++; lpcchValueName = 50; lpcbData = 50; // open port HANDLE hComm; LPSTR gszPort = new TCHAR(50); sprintf(gszPort, "\\\\.\\%s", lpData); // create valid "filename" from COM port name hComm = CreateFileA( gszPort, // friendly name of port (cast to LPBYTE) GENERIC_READ | GENERIC_WRITE, // Read/Write Access 0, // No Sharing, ports cant be shared 0, // No Security OPEN_EXISTING, // Open existing port only //0, NULL, // Non Overlapped I/O 0 // Null for Comm Devices ); if (hComm == INVALID_HANDLE_VALUE) { printf("ERROR opening port %s\n", gszPort); continue; } //handle ERROR_FILE_NOT_FOUND? //Setting the Parameters for the SerialPort DCB dcbSerialParams = { 0 }; // Initializing DCB structure dcbSerialParams.DCBlength = sizeof(dcbSerialParams); //retreives the current settings if ( ! GetCommState(hComm, &dcbSerialParams)) { printf("ERROR getting the COM state\n"); CloseHandle(hComm);//Closing the Serial Port continue; } dcbSerialParams.BaudRate = CBR_38400; //BaudRate = 38400 dcbSerialParams.ByteSize = 8; //ByteSize = 8 dcbSerialParams.StopBits = ONESTOPBIT; //StopBits = 1 dcbSerialParams.Parity = NOPARITY; //Parity = None if ( ! SetCommState(hComm, &dcbSerialParams)) { printf("ERROR setting serial params\n"); CloseHandle(hComm);//Closing the Serial Port continue; } //Setting Timeouts COMMTIMEOUTS timeouts = { 0 }; //Initializing timeouts structure timeouts.ReadIntervalTimeout = 5; timeouts.ReadTotalTimeoutConstant = 5; timeouts.ReadTotalTimeoutMultiplier = 1; timeouts.WriteTotalTimeoutConstant = 5; timeouts.WriteTotalTimeoutMultiplier = 1; if (SetCommTimeouts(hComm, &timeouts) == FALSE) { printf("ERROR setting timeouts\n"); CloseHandle(hComm);//Closing the Serial Port continue; } //Writing data to Serial Port char cmd[] = "ATZ\r\n"; DWORD BytesWritten = 0; // No of bytes written to the port if ( ! WriteFile(hComm,// Handle to the Serialport cmd, // Data to be written to the port strlen(cmd), // sizeof(SerialBuffer), // No of bytes to write into the port &BytesWritten, // No of bytes written to the port NULL)) { int err = GetLastError(); printf("ERROR write %zu bytes to %s failed (error code is %d)\n", strlen(cmd), gszPort, err); CloseHandle(hComm);//Closing the Serial Port continue; } if (BytesWritten != strlen(cmd)) { printf("ERROR Only %ld bytes of %zu written to %s\n", BytesWritten, strlen(cmd), gszPort); CloseHandle(hComm);//Closing the Serial Port continue; } printf("DEBUG: Command '%s' (%ld bytes) written\n", cmd, BytesWritten); //Setting Receive Mask if ( ! SetCommMask(hComm, EV_RXCHAR)) { printf("ERROR Setting CommMask\n"); CloseHandle(hComm);//Closing the Serial Port continue; } // Wait for answer DWORD dwEventMask; // Event mask to trigger if ( ! WaitCommEvent(hComm, &dwEventMask, NULL)) { printf("ERROR in WaitCommEvent()\n"); CloseHandle(hComm);//Closing the Serial Port continue; } // read answer from device int loop = 0; DWORD NoBytesRead; char ReadData; char SerialBuffer[64] = { 0 }; printf("DEBUG read bytes from device...\n"); do { if ( ! ReadFile(hComm, &ReadData, sizeof(ReadData), &NoBytesRead, NULL)) { printf("ERROR in ReadFile()\n"); CloseHandle(hComm);//Closing the Serial Port break; } SerialBuffer[loop] = ReadData; ++loop; } while (NoBytesRead > 0); loop--; printf("Read from device: "); int index = 0; for (index = 0; index < loop; ++index) { printf("%c", SerialBuffer[index]); } printf("\n"); // close port printf("DEBUG: close port %s\n", gszPort); CloseHandle(hComm);//Closing the Serial Port } // check if loop ends because of an error if (rc != ERROR_NO_MORE_ITEMS) { if (rc == ERROR_MORE_DATA) { printf("ERROR - Buffer for 'lpData' or 'lpValueName' too small\n"); } else { printf("ERROR - Unknown error %ld\n", rc); } break; } } else { printf("ERROR - Can't open registry\n"); }
Leider bleibt er nach dem senden des ATZ zum ersten (falschen) COM stehen, kein Timeout, kein Fehler, einfach eingefroren.
-
@Knackwurst
Lösch den Teil raus://Setting Receive Mask if ( ! SetCommMask(hComm, EV_RXCHAR)) { printf("ERROR Setting CommMask\n"); CloseHandle(hComm);//Closing the Serial Port continue; } // Wait for answer DWORD dwEventMask; // Event mask to trigger if ( ! WaitCommEvent(hComm, &dwEventMask, NULL)) { printf("ERROR in WaitCommEvent()\n"); CloseHandle(hComm);//Closing the Serial Port continue; }
Und mach statt dessen das rein:
FlushFileBuffers(hComm);
WaitCommEvent
bricht nicht nach einem Timeout ab. Das wartet ggf. ewig.Und du brauchst es nicht. Stell das Timeout das du willst mit
SetCommTimeouts
ein, und dann verwende einfachReadFile
. Das bricht dann nämlich nach dem eingestellten Timeout ab.-TODO: Wie erkenne ich das alles gesendet ist bzw. das es nicht funktioniert weil das Endgerät z.B. nichts einliest?
Erkennen ob etwas gesendet wurde kann man halbwegs zuverlässig. Ich sag' mal wenn
WriteFile
und ein darauffolgendesFlushFileBuffers
beide keinen Fehler gemeldet haben, dann kannst du mit halbwegs guter Sicherheit davon ausgehen dass die Daten gesendet wurden.Ob es von der Gegenstelle aber auch empfangen wurde ist ne ganz andere Frage. Das kannst du bei seriellen Schnittstellen im Allgemeinen nicht erkennen. Im Speziellen u.U. schon, nämlich z.B. dadurch dass das Gerät antwortet.
Wobei ich allgemein hinterfragen würde ob es gut ist einfach an unbekannte Geräte ein
ATZ\r
zu schicken. Musst du wirklich alle Ports scannen? Wäre es nicht möglich statt dessen den Benutzer die Port-Nummer eingeben zu lassen?
-
@hustbaer sagte in Serial port bedienen:
Und mach statt dessen das rein:
FlushFileBuffers(hComm);
WaitCommEvent
bricht nicht nach einem Timeout ab. Das wartet ggf. ewig.JA, das funktioniert!
-
@hustbaer sagte in Serial port bedienen:
Wobei ich allgemein hinterfragen würde ob es gut ist einfach an unbekannte Geräte ein ATZ\r zu schicken. Musst du wirklich alle Ports scannen? Wäre es nicht möglich statt dessen den Benutzer die Port-Nummer eingeben zu lassen?
Ich hatte mir das so eingebildet, weil ich es besonders komfortabel haben wollte. Leider antwortet das Gerät welches ich identifizieren will nicht von selbst beim öffnen der Verbindung sondern erwartet aktiv ein Kommando. Anstelle ATZ könnte man auch was anderes senden was evtl. nicht so invasiv wäre? Ein AT oder ATV.
Das Ziel ist einen gültigen Adapter zu erkennen und es könnte theoretisch mehr als einer dran sein. Und was will man sonst anzeigen? In der Reg hat man ja nur den COM und den Devicepfad. Ggf. könnte man noch die Treibereigenschaften ermitteln, aber über das Device sagt das alles nichts aus. Eine Liste würde also auch nur "COM5, COM8, COM12" anzeigen und der User würde sich testweise durchklickern, was dann ja zum gleichen Ergebnis führt nicht wahr?
So schicke ich ein ATZ und erhalte im korrekten Fall ein "ELM327 v%d.%d" zurück auf das ich teste.
-
IIRC gibt es manche serielle Geräte die sich von sich aus melden wenn man bestimmte Signale über die Handshake-Leitungen sendet. Das wäre weniger invasiv. Ich weiss bloss leider nicht mehr wie das genau geht. Wenn's dich interessiert kannst du ja mal danach googeln - vielleicht findest du irgendwo ne Beschreibung.
Und ich weiss auch nicht ob Modems das unterstützen. Ich weiss dass serielle Mäuse es unterstützen und dass Windows diese Detection verwendet (bzw. bis zumindest Windows 7 verwendet hat). Weiss ich deswegen, weil wir das in meiner alten Firma extra im .inf File vom Treiber deaktivieren mussten damit Windows eben nicht die Handshake-Leitungen jedes mal beim Booten ansteuert. (Wir hatten die Handshake-Leitungen als digitale Ein- und Ausgänge misbraucht, und da konnten wir das Rumspielen von Windows gar nicht brauchen.)
ps:
Und ja,AT\r
ist natürlich weniger invasiv alsATZ\r
. Je weniger desto besser.