sinzal
Goto Top

Zugriffsverletzung bei Schreiben auf serielle Schnittstelle COM2

Hallo Admins,

ich habe ein programmiertechnisches Problem:
Ich möchte mit einem Delphi-Programm ein Prüfgerät steuern, welches über eine serielle Schnittstelle (COM2) an einen Desktop-PC mit Windows XP angebunden ist. Zum Programmieren verwende ich Borland Delphi 5. Ich habe ein Programm geschrieben, welches beim Druck auf den Button "Button_Pruefung" die COM-Schnittstelle COM2 auf Verfügbarkeit testet, öffnet, ihr die Baudrate 9600 zuweist, eine Initialisierungssequenz sIdenti sendet, den (vom Prüfgerät automatisch gesendeten) Antwortstring an COM2 ausliest und auswertet und anschließend eine Startsequenz sStart für die Prüfung sendet. Innerhalb von ca. 30 Sekunden bekommt die Schnittstelle eine Rückantwort vom Messgerät mit den Messdaten, die dann ausgegeben werden sollen. (Dies ist der vereinfachte Ablauf.) Ich bekomme jedoch in meinem Programm beim Senden des Strings sStart stets eine Fehlermeldung mit der Ausschrift "Zugriffsverletzung bei Adresse 00000000. Lesen von Adresse 00000000.", mit der das Programm abbricht. Seltsamerweise erscheint die Meldung nicht, wenn ich direkt vor dem Senden des Strings einen MessageDlg aufploppen lasse, den der Nutzer bestätigen muss. Dies ist jedoch im normalen Ablauf i.d.R. nicht möglich, weshalb es ohne den MessageDlg gehen muss. Im Code unten ist die entsprechende Stelle mittels "@@@debug" gekennzeichnet.
Bisher habe ich mich noch nicht intensiv mit der Steerung von COM-Schnittstellen beschäftigt und stehe deshalb etwas auf dem Schlauch. Könnt Ihr mir sagen, was ich beim 2. Senden falsch gemacht habe? Ist die Schnittstelle durch den ersten Sendevorgang oder durch den Lesevorgang noch in einem Zustand, der kein weiteres Senden erlaubt? Wie kann man diesen Zustand sinnvoll wieder verlassen?

Viele Grüße,
Sinzal

Content-Key: 384232

Url: https://administrator.de/contentid/384232

Printed on: April 23, 2024 at 07:04 o'clock

Member: Sinzal
Sinzal Aug 23, 2018 at 14:30:30 (UTC)
Goto Top
Hier ist der Code zu meinem Projekt (zumindest die relevanten Ausschnitte):
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, Printers, Db, DBTables;

type
  TForm1 = class(TForm)
    //...
    Button_Pruefung: TButton;
    Timer1: TTimer;
    Timer_Sleep: TTimer;
    //...
    procedure Button_PruefungClick(Sender: TObject);
    function OpenCOM(Port: byte): boolean; stdcall;
    procedure InitOverlapped(var Overlapped : TOverlapped);
    procedure CloseCOM(portHandleClose: Integer); stdcall;
    function ComAvailable(ComNr: byte): boolean; stdcall;
    function SetBaudRate(baud: cardinal; portHandleBaud: Integer; portDCBbaud: TDCB): boolean; stdcall;
    function ReadString(var Buf: PChar; portHandleRead:Integer; readOL:TOverlapped): Cardinal; stdcall;
    function LiesAusCOM(var Buf: PChar; portHandleRead:Integer; readOL:TOverlapped): string;
    function SendString(Str: String; portHandleSend:Integer; writeOL:TOverlapped): boolean; stdcall;
    procedure warteAufTaste(zeit : longInt);
    procedure Timer_SleepTimer(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;
  globalBaudrateTransponder:Integer =9600;
  globalPruefungLaeuft:boolean = false;
  globalSleeping:boolean = false;

  { Variablen für COM-Schnittstelle }
  PortTimeout : _COMMTIMEOUTS;
  PortHandle1, PortHandle2 : Integer;
  PortDCB1, PortDCB2 : TDCB;
  WriteOverlapped1,ReadOverlapped1,StatusOs1: TOverlapped;
  WriteOverlapped2,ReadOverlapped2,StatusOs2: TOverlapped;

  { Übergabe-String für ausgelesene Zeichen }
  StringReceived : String;

implementation

uses Unit2, Unit3;

{$R *.DFM}

procedure TForm1.Button_PruefungClick(Sender: TObject);
var Buf: string[255];
    sReceive: string;
    PBuf: PChar;
    BytesRead: cardinal;
    bKommunikationstestOK,bPruefungbeendet,bCOMmunicationOK:boolean;
    iCOMNummer: Integer;
    PortHandleX : Integer;
    PortDCBX : TDCB;
    WriteOverlappedX,ReadOverlappedX,StatusOsX: TOverlapped;
begin
  globalPruefungLaeuft:=true;
  iCOMNummer:=2; // COM1 verwenden
  {*** Kommunikation mit COM2 aufbauen ***}
    if not(ComAvailable(iCOMNummer)) then
    begin
      MessageDlg('Fehler: Prüfschnittstelle COM2 ist gerade in Benutzung.',mtError,[mbOK],0);  
    end
    else
    begin
      if not(OpenCOM(iCOMNummer)) then
      begin
        MessageDlg('Fehler beim Öffnen der Prüfschnittstelle COM2.',mtError,[mbOK],0);  
      end
      else
      begin
        // COM2 im Normalbetrieb verwenden
        PortHandleX:=PortHandle2;
        PortDCBX:=PortDCB2;
        WriteOverlappedX:=WriteOverlapped2;
        ReadOverlappedX:=ReadOverlapped2;
        StatusOsX:=StatusOs2;
        if not(SetBaudRate(globalBaudrateTransponder,PortHandleX,PortDCBX)) then
        begin
          MessageDlg('Fehler beim Setzen der Baudrate an der Prüfschnittstelle COM2.',mtError,[mbOK],0);  
        end
        else
        begin
  {*** Identi-Sequenz senden, um zu sehen, ob das Prüfgerät angeschlossen ist ***}
          // Vorbereiten der Hilfsvariablen für den Empfang
          sReceive:='';  
          PBuf:=@Buf;
          BytesRead:=0; // Zurücksetzen
          bKommunikationstestOK:=true;
          bCOMmunicationOK:=SendString(sIdenti,PortHandleX,WriteOverlappedX);
          if not(bCOMmunicationOK) then
          begin
            MessageDlg('Fehler bei Test der Kommunikation mit Prüfgerät: Senden von IDENTI.',mtError,[mbOK],0);  
            bKommunikationstestOK:=false;
          end
          else
          begin
            // Empfang von Antwortstring des Geräts über die Read-Abfrage
            warteAufTaste(1); // Sicherheits-Warten (1Sek), weil String gerade aktiv beschrieben werden könnte
            sReceive:=LiesAusCOM(Pchar(PBuf),PortHandleX,ReadOverlappedX);
            // Auswerten, ob sinnvolle Antwort erfolgte
            // 1.: ist nix empfangen worden?
            if (sReceive='') then  
            begin
              MessageDlg('Fehler bei Test der Kommunikation mit Prüfgerät: Keine Rückantwort.',mtError,[mbOK],0);  
              bKommunikationstestOK:=false;
            end
            else
            begin
            // 2.: ist Antwort mit erwartbarem Antwortstring identisch
              if (copy(sReceive,1,13)='AntwortAntwor') then  
              begin
                bKommunikationstestOK:=true;
              end
              else
              begin
                MessageDlg('Fehler bei Test der Kommunikation mit Prüfgerät: Falsche Rückantwort:'+sReceive,mtError,[mbOK],0);  
                bKommunikationstestOK:=false;
              end;
            end;
            if bKommunikationstestOK then
            begin
  {*** Startsequenz senden ***}
              // MessageDlg('Debug: Ist dieser Dialog aktiv, klappt das Senden.',mtInformation,[mbOK],0); //@@@debug   
              bCOMmunicationOK:=SendString(sStart,PortHandleX,WriteOverlappedX);
              if not(bCOMmunicationOK) then
              begin
                MessageDlg('Fehler bei Senden von Startsequenz.',mtError,[mbOK],0);  
                bKommunikationstestOK:=false;
              end
              else
              begin
                warteAufTaste(40);
                sReceive:=LiesAusCOM(Pchar(PBuf),PortHandleX,ReadOverlappedX);
                MessageDlg('Messergebnisstring: '+sReceive,mtInformation,[mbOK],0);  
                //...
              end; //Startsequenz senden und Prüfung ausführen
            end; // Kommunikationstest bestanden
          end; // Senden des ersten Identi-Strings
        end; // von Test Baudrate setzen
  {*** Schnittstelle COM2 schließen ***}
        CloseCOM(PortHandleX);
      end; // von Test OpenCom
    end; // von Test ComAvailable
end;

{ Schnittstelle öffnen/schließen }
function TForm1.OpenCOM(Port: byte): boolean; stdcall;
begin
 if (Port=1) then
 begin
  PortHandle1 :=
  CreateFile(PChar('\\.\COM'+IntToStr(Port)),GENERIC_READ or GENERIC_WRITE,0,  
             nil,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,LongInt(0));
  //Overlapped = Asynchron auf Microsoftianisch
  if PortHandle1 > 0 then
  begin
    Result := true;
    InitOverlapped(WriteOverlapped1);
    InitOverlapped(ReadOverlapped1);
    InitOverlapped(StatusOs1);
  end else Result := false;
 end;
 if (Port=2) then
 begin
  PortHandle2 :=
  CreateFile(PChar('\\.\COM'+IntToStr(Port)),GENERIC_READ or GENERIC_WRITE,0,  
             nil,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,LongInt(0));
  //Overlapped = Asynchron auf Microsoftianisch
  if PortHandle2 > 0 then
  begin
    Result := true;
    InitOverlapped(WriteOverlapped2);
    InitOverlapped(ReadOverlapped2);
    InitOverlapped(StatusOs2);
  end else Result := false;
 end;
end;

{ Initialisieren von Overlapped - Nötig für Öffnen des COM-Ports}
procedure TForm1.InitOverlapped(var Overlapped : TOverlapped);
begin
  Overlapped.Offset := 0;
  Overlapped.OffsetHigh := 0;
  Overlapped.Internal := 0;
  Overlapped.InternalHigh := 0;
  Overlapped.hEvent := CreateEvent(nil,True,False,'');  
end;

{ Schnittstelle schließen }
procedure TForm1.CloseCOM(portHandleClose: Integer); stdcall;
begin
  PurgeComm(portHandleClose, PURGE_RXABORT or PURGE_RXCLEAR or PURGE_TXABORT or PURGE_TXCLEAR);
  SetCommMask(portHandleClose,0); //unterbricht WaitCommEvent im Polling thread
  CloseHandle(portHandleClose);
  portHandleClose := 0;
end;

{ Schnittstelle prüfen }
function TForm1.ComAvailable(ComNr: byte): boolean; stdcall;
var
  TestHandle : integer;
begin
  TestHandle :=
  CreateFile(PChar('\\.\COM'+IntToStr(ComNr)),GENERIC_READ or GENERIC_WRITE,0,  
             nil,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,LongInt(0));
  if (TestHandle <= 0) then
    Result := false
  else
  begin
    Result := true;
    CloseHandle(TestHandle);
  end;
end;

{ Baudrate setzen }
function TForm1.SetBaudRate(baud: cardinal; portHandleBaud: Integer; portDCBbaud: TDCB): boolean; stdcall;
begin
  GetCommState(portHandleBaud,portDCBbaud);
  portDCBbaud.BaudRate := baud;
  Result := SetCommState(portHandleBaud,portDCBbaud);
end; 

{ Lesen aus COM }
function TForm1.ReadString(var Buf: PChar; portHandleRead:Integer; readOL:TOverlapped): Cardinal; stdcall;
var
  StrBuf: string;
  Error,BytesRead: DWORD;
  Status: TComStat;
  RdSuccess: boolean;
begin
  BytesRead := 0;
  if (portHandleRead <> -1) then
  begin
    ClearCommError(portHandleRead,Error,@Status);
    case Error of
      CE_BREAK,CE_DNS,CE_FRAME,CE_IOE,CE_MODE,CE_OOP,CE_OVERRUN,CE_PTO,CE_RXOVER,
      CE_RXPARITY,CE_TXFULL:
      begin
        BytesRead := 0;
      end
    else
      begin
        SetLength(StrBuf,Status.cbInQue);
        RdSuccess := ReadFile(portHandleRead,StrBuf[1],Status.cbInQue,BytesRead,@readOL);
        if not RdSuccess then BytesRead := 0;
      end;
    end;
  end;
  if (BytesRead > 0) then
  begin
    Result := BytesRead;
    try
      StrPCopy(Buf,StrBuf);
    except
      Result := 0;
    end;
  end else Result := 0;
end;

{ Lesen aus COM und Rückgabe von ausgelesenem Wert }
function TForm1.LiesAusCOM(var Buf: PChar; portHandleRead:Integer; readOL:TOverlapped): string;
var
  StringReceived: string;
  BytesRead2: cardinal;
begin
            StringReceived:='';  
            BytesRead2:=Form1.ReadString(Buf,portHandleRead,readOL);
            // aus irgendeinem Grund wird beim 1. Einlesen per se nix empfangen -> deshalb nochmal einlesen
            BytesRead2:=Form1.ReadString(Buf,portHandleRead,readOL);
            StringReceived:=copy(Buf,1,BytesRead2);
            result:=StringReceived;
end;

{ Schreiben auf COM }
function TForm1.SendString(Str: String; portHandleSend:Integer; writeOL:TOverlapped): boolean; stdcall;
var
  written  : cardinal;
  tmpStr : string;
  i: LongBool;
begin
  if (portHandleSend <> 0) then
  begin
    tmpStr := string(Str);
    Result := not WriteFile(portHandleSend,tmpStr[1],Length(tmpStr),written,@writeOL);
  end;
end;

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
  if key=char(VK_ESCAPE) then
  begin
    globalPruefungLaeuft:=false;
  end;
end;

procedure TForm1.OnAppMsg(var Msg: TMsg; var Handled: Boolean); //@@@test
begin
  Handled:=false;
  if (Msg.message=WM_KEYDOWN) then // bei Tastendruck
  begin
    if (Msg.wParam=VK_ESCAPE) then // wenn ESC gedrückt wurde
    begin
      globalPruefungLaeuft:=false;
      Handled:=true;
    end;
  end;
end;

{ Pause für *zeit* Sekunden, jedoch mit Reaktion auf Events }
procedure TForm1.warteAufTaste(zeit : Integer);
begin
  globalSleeping:=true;
  Timer_Sleep.Enabled:=false;
  Timer_Sleep.Interval:=zeit*1000;
  Timer_Sleep.Enabled:=true;
  while ((globalSleeping) and (globalPruefungLaeuft)) do
  begin
    Application.ProcessMessages;
  end;
  Timer_Sleep.Enabled:=false;
  globalSleeping:=false;
end;

procedure TForm1.Timer_SleepTimer(Sender: TObject);
begin
  globalSleeping:=false;
  Timer_Sleep.Enabled:=false;
end;


end.
Member: Kraemer
Kraemer Aug 23, 2018 updated at 14:58:31 (UTC)
Goto Top
Moin,

ist jetzt nur geraten, aber dein "Kommunikationstest" und dein Daten senden scheint wohl zeitlich zu nahe aneinander zu liegen. Keine Ahnung ob das Gerät noch ne Sekunde braucht. Du kannst testhalber mal statt des Dialogs mit einem "sleep" (hoffe das hieß so) "warten".

BTW: Du solltest dir dringend mal Prozeduren und Funktionen ansehen. Ist ja grausam der Code

Gruß
Member: Penny.Cilin
Penny.Cilin Aug 23, 2018 at 15:09:56 (UTC)
Goto Top
Zitat von @Kraemer:

Moin,

BTW: Du solltest dir dringend mal Prozeduren und Funktionen ansehen. Ist ja grausam der Code
Wieso er hat doch Prozeduren und Funktionen drin. face-wink

Gruß

Gruss Penny
Member: em-pie
em-pie Aug 23, 2018 at 15:20:33 (UTC)
Goto Top
Moin,

Zitat von @Kraemer:
ist jetzt nur geraten, aber dein "Kommunikationstest" und dein Daten senden scheint wohl zeitlich zu nahe aneinander zu liegen. Keine Ahnung ob das Gerät noch ne Sekunde braucht. Du kannst testhalber mal statt des Dialogs mit einem "sleep" (hoffe das hieß so) "warten".


ich denke auch mal, das wird es sein.
Hatten mal einen ähnlichen Fall. Ein Messgerät wurde einst auf einem 386er ausgeliefert und die Messpulenspulen (bzw. das Steuergerät) hingen per RS232 am PC
Nun ist der PC irgendwann mal "abgeraucht" und wir haben den via VMware Player virtualisiert. Der neue Host, ein Core2Duo, war zu schnell für das Messgerät. Startete die VM, hat die Applikation die durchgereichte COM-Schnittstelle abgefragt, aber nicht schnell genug eine Antwort bekommen.
Ein Spiel zwischen "Pause-Taste" und Return verlangsamte dann die VM, sodass die Applikation Zeit hatte, die Spulen zu initialisieren. Hat man dann die VM in den Suspend geschickt und anschließend aufgeweckt, lief das System wie eine 1. Das ganze dann zwei Jahre lang. Danach haben wir das Uraltsystem mal ausgetauscht face-smile


Gruß
em-pie
Member: Friemler
Friemler Aug 23, 2018 at 17:49:46 (UTC)
Goto Top
Hallo Sinzal,

Du hast anscheinend das ganze Konzept von Overlapped I/O unter Windows nicht verstanden. Es genügt nicht, einfach eine OVERLAPPED-Struktur zu erzeugen und den Lese-/Schreib-Routinen des Betriebssystems immer schön brav einen Pointer darauf mit zugeben, man muss den Mechanismus auch richtig nutzen. Lies z.B. mal das hier und das hier. Im letztgenannten Artikel heißt es dann auch:

A data buffer must not be read or written until its corresponding I/O operation has completed; reading or writing the buffer may cause errors and corrupted data.

Genau das passiert in Deinem Fall.

Mit Overlapped I/O kann Dein Programm z.B. weiterhin die Benutzerschnittstelle (Menüzeile, Fensteraktionen wie verschieben oder Größe ändern) reaktionsfähig halten, während das OS eine I/O-Operation ausführt. Das OS benachrichtigt Dein Programm, wenn der I/O fertig ist.

Das kann man auch über Polling erfahren, dann verbrät Dein Programm aber seine Rechenzeit damit, in einer Warteschleife rundzulaufen und belästigt das System mit dauernden Abfragen "Bist Du schon fertig?". Sleep ist auch keine wirkliche Alternative, da die angegebene "Schlafzeit" auf dem einen Rechner ausreichend sein kann, auf einem anderen Rechner jedoch zu kurz ist, das kommt dann auch noch darauf an, womit das System sonst noch so beschäftigt ist. Die Win32 API GetOverlappedResult mit dem Parameter bWait auf TRUE zu benutzen ist auch nicht sinnvoll, dann kannst Du gleich Non-Overlapped I/O verwenden.

Ich würde Dir für den Anfang raten, auf Overlapped I/O zu verzichten und erstmal Deine Routinen von der Ablauflogik her ans Laufen zu bekommen.

Grüße
Friemler
Member: Sinzal
Sinzal Aug 24, 2018 at 05:52:18 (UTC)
Goto Top
Hallo Leute,

vielen Dank für die Antworten.
Ich habe sleep() mit diversen Pausenzeiten schon getestet. Das bringt in dem Fall nichts. Ich vermute, dabei beendet das Programm für die sleep-Zeit alle Aktivitäten, auch die an der COM-Schnittstelle und nimmt sie nachher wieder auf, sodass der Versand über COM während der sleep-Zeit kein Stück weiter geht. Während das Programm mit dem MessageDlg-Fenster pausiert, scheint der COM-Versand im Hintergrund jedoch zu laufen.

@Friemler: Hast du ein Code-Beispiel für eine Polling-Routine, um abzufragen, ob die RS232 COM2 zum Versenden bereit ist?

Viele Grüße,
Sinzal
Member: Friemler
Friemler Aug 24, 2018 at 06:16:13 (UTC)
Goto Top
Moin,

Zitat von @Sinzal:

@Friemler: Hast du ein Code-Beispiel für eine Polling-Routine, um abzufragen, ob die RS232 COM2 zum Versenden bereit ist?

nein, leider nicht. Es gibt aber hier eine COM-Port Bibliothek für Delphi 3 bis XE, vielleicht hilft Dir das weiter.

Grüße
Friemler
Member: Sinzal
Sinzal Sep 11, 2018 at 09:57:51 (UTC)
Goto Top
Hallo Leute,

ich habe die Ansteuerung der seriellen Schnittstelle nach mehreren vergeblichen Versuchen nun doch mit einer Zusatz-Funktionsbibliothek realisiert. Diese ist Async Professional von TurboPower (Quelle: http://tpapro.sourceforge.net/). Mit dem Tool habe ich eine Delphi-Komponente ApdComPort zur Verfügung, über die ich mit dem Befehl PutString Strings auf die serielle Schnittstelle schicken, über die Routine TriggerAvailable eine Empfangsprozedur bei ankommenden Daten auslösen und über die Routine GetChar einzelne empfangene Zeichen der seriellen Schnittstelle übernehmen kann. Mit dieser Komponente spare ich mir das Zeitmanagement der seriellen Schnittstelle, da ApdComPort dieses integriert und selbst regelt. Das Programm läuft jetzt auch ohne Zugriffsverletzungen.

Vielen Dank für eure Tipps!

Viele Grüße,
Sinzal
Member: Friemler
Friemler Sep 11, 2018 at 10:50:52 (UTC)
Goto Top
Hallo Sinzal,

schön, dass Du Dein Problem lösen konntest.

Hast Du evtl. auch die von mir genannte Bibliothek getestet und kannst Deine Erfahrungen damit schildern?

Grüße
Friemler
Member: Sinzal
Sinzal Sep 17, 2018 at 13:02:55 (UTC)
Goto Top
Hallo Friemler,

nein, deine Bibliothek habe ich leider nicht ausprobiert. Mein Programm läuft bisher stabil, deshalb habe ich die anderen Ansätze dann nicht weiter verfolgt.

Viele Grüße,
Sinzal