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.


  • Mod

    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 (ohne A), denn sonst würde ja bei UNICODE ein falscher Parameter übergeben?!


  • Mod

    @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 (ohne A), denn sonst würde ja bei UNICODE 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 einfach ReadFile. 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 darauffolgendes FlushFileBuffers 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 als ATZ\r. Je weniger desto besser.


Anmelden zum Antworten