Tutorial:Serielle Kommunikation

From Freepascal Amiga wiki
Revision as of 18:42, 20 December 2018 by Alb42 (Talk | contribs) (Vorbereiten des seriellen Gerätes: image of the RS232 to TTL adapter)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Start Nächste

Dieses Tutorial zeigt wie man Geräte mit serieller Kommunikation (RS232 oder USB) am Amiga benutzt. Als Besipiel wird hier ein Arduino UNO board benutzt. Alles was hier gezeigt wird wurde auf einem Amiga 1200 (68030/50 Mhz, OS 3.9) und einer MorphOS machine getestet, aber sollte auch genauso auf einem AmigaOS4 oder nativen AROS (i386/x86_64/ARM) so funktionieren.

Vorbereiten des seriellen Gerätes

RS232TTL.jpg

Als Modelgerät benutzen wir ein Arduino UNO welches per RS232 am Amiga angeschlossen wird bzw. via USB am MorphOS Computer und dabei einen virtuellen seriellen Port zur Verfügung stellt. Achtung man kann nicht einfach die Leitungen des Arduino an den Amiga RS232 anschliessen, da braucht man ein wenig Elektronik um die Pegel zu regeln. Bei Interesse nach "RS232 Shield Arduino" oder "RS232 zu TTL-Konverter" googlen, da gibt es viel Auswahl.

Zuerst muss der Arduino programiert werden. Als sehr einfacher Anfang wir schreiben ein kurzes Program welches nur "Hello World" und eine laufende Nummer ausgibt. (damit man besser sieht das etwas passiert.)

const unsigned long BAUD_RATE = 9600;
int count;

void setup() {
  Serial.begin(BAUD_RATE);
  count = 1;
}

void loop() {
  Serial.print("Hello Amiga (Msg: ");
  Serial.print(count++);
  Serial.println(")");
  delay(500);
}

Dieses Programm sendet jede halbe Sekunde ein "Hello World (Msg: count)" and den serielle port. Dies kann man ganz gut mit dem "Serial Monitor" der Arduino IDE überprüfen

Serielle Ausgabe am Amiga

Nun wollen wir diesen Text auch am Amiga sehen, wenn der Arduino am RS232 angeschlossen ist muss man nur ein Terminalprogramm starten wie z.B. NComm[1]. Dort setzt man das Device zu serial.device, die Unitnummer zu 0 und Baudrate zu 9600 und schon sollte man den Output des obigen Programms sehen.

Wenn man den Arduino per USB anschließt sollte der installierte USB Stack (z.B. Poseidon) automatisch den richtigen Treiber (cdcacm.class) benutzen und damit den Virtuellen seriellen Port verfügbar machen. Das sollte ungefähr so aus sehen:

ArduinoUSB.png

Falls er nicht automatisch den richtigen Treiber benutzt (z.B. auf MorphOS) muss man diesen erzwingen für den 0. Endpunkt. (USB Prefs/Trident öffnen, Devices, Doppelklick auf"Arduino UNO", "CDC control interface (0)" auswählen, rechte Maustaste "forced binding" "cdcacm.class", Warnung abnicken, Usb rausziehen und wieder anschliessen). Dieser virtuelle serielle port soltle jetzt verfügbar sein über das usbmodem.device. Der Rest ist gleich wie beim RS232 Anschluss nur das das device usbmodem.device heisst.

Serielles Gerät mit FreePascal

Message port

Als erstes brauchen wir einen Messageport für die Kommunikation mit dem Triber, dafür gibt es CreateMsgPort() und DeleteMsgPort() in der Unit Exec.

program test;
uses
  Exec;
var
  Mp: PMsgPort;
begin
  Mp := CreateMsgPort;
  // do something
  DeleteMsgPort(Mp);
end.

Falls es nicht genug Speicher gibt um den Messageport anzulegen der Aufruf wird fehlschlagen und ein nil (=0) zurückgeben. Natürlich sollten wir das Ergebnis testen und das Program verlassen falls es ein Problem gibt. Man kann entweder den Messageport mit nil vergleichen oder die spezielle Funktion Assigned() benutzen.

program test;
uses
  Exec;
var
  Mp: PMsgPort;
begin
  Mp := CreateMsgPort;
  if not Assigned(Mp) then
  begin
    writeln('Failed to create MsgPort');
    Exit;
  end;
  // do something
  DeleteMsgPort(Mp);
end.

Dieses Programm testet ob die Erzeugung des Messageports funktioniert hat und wenn nicht gibt es eine Nachricht aus und verlässt das Programm.

IO Request

Für den nächsten Schritt brauchen wir einen IORequest, praktisch ein Befehl an den Treiber, da wir serielle Kommunikation betreiben wollen benötigen wir eine spezialisierten IORequest namens PIOExtSer welchen wir in der serial unit finden. (Falls die serial unit nicht gefunden wird, muss das FreePascal aktualisiert werden) Um so einen IORequest zu erstellen benötigen wir die beiden Funktionen CreateExtIO() und DeleteExtIO() aus der exec unit. Wir erweitern unser Programm und erzeugen so ein PIOExtSer.

program test;
uses
  Exec;
var
  Mp: PMsgPort;
  Io: PIOExtSer;
begin
  // create Messageport
  Mp := CreateMsgPort;
  if not Assigned(Mp) then
  begin
    writeln('Failed to create MsgPort');
    Exit;
  end;
  // create IO Serial
  Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
  if not Assigned(Io) then
  begin
    Writeln('Cannot alloc IOExtSer');
    Exit;
  end;
  // do something
  DeleteExtIO(PIORequest(Io));
  DeleteMsgPort(Mp);
end.

An dieser Stelle möchte ich auf ein kleines Problem hinweisen. Falls der Messageport erfolgreich erzeugt wurde aber der IORequest schlägt fehl, das Programm wird einfach beendet und der Messageport wird nicht wieder zerstört. Man kann geschachtelte if Blöcke benutzen:

program test;
uses
  Exec, Serial;
var
  Mp: PMsgPort;
  Io: PIOExtSer;
begin
  // create Messageport
  Mp := CreateMsgPort;
  if  Assigned(Mp) then
  begin
    // create IO Serial
    Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
    if Assigned(Io) then
    begin
      // do something
      DeleteExtIO(PIORequest(Io));
    end
    else
    begin
      Writeln('Cannot alloc IOExtSer');
    end;
    DeleteMsgPort(Mp);
  end
  else
  begin
    writeln('Failed to create MsgPort');
  end;
end.

Man kann das so machen, aber wenn es sehr viele Ebenen hat wird es sehr schnell unübersichtlich. I bevorzuge den try...finally..end; Ansatz oder separate Initialisieren() und Zerstoere() Funktionen, welche bei besonders einfach sind bei Objektorientierter Programmierung da man einen Konstruktor und Destruktor der Klasse hat den man dafür benutzen kann. Achtung: Wenn man Klassen oder try finally Blöcke benutzen möchte muss man den Compiler im Delphi oder ObjectFPC Modus benutzen. z.B. {$mode objfpc}.

program test;
{$mode objfpc}
uses
  Exec, Serial;
var
  Mp: PMsgPort = nil;
  Io: PIOExtSer = nil;
begin
  try
    // create Messageport
    Mp := CreateMsgPort;
    if not Assigned(Mp) then
    begin
      writeln('Failed to create MsgPort');
      Exit;
    end;
    // create IO Serial
    Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
    if not Assigned(Io) then
    begin
      Writeln('Cannot alloc IOExtSer');
      Exit;
    end;

    // do something
 
  finally
    if Assigned(Io) then
      DeleteExtIO(PIORequest(Io));
    if Assigned(Mp) then
      DeleteMsgPort(Mp);
  end;
end.

Man beachte das alle Variablen mit nil initialisiert werden und dann im finally Teil auf nil getestet werden und nur dann zerstört werden. Diese Funktionen benötigen das eigentlich nicht, da sie intern auch auf nil testen. Aber viele andere Amiga API Funktionen testen nicht auf leere Pointer und stürzen einfach ab. Daher sollte man sich angewöhnen alle Pointer zu testen bevor man sie an Amiga API Funktionen übergibt.

Öffnen des seriellen Devices

Aber zurück zu der seriellen Kommunikation. Es ist alles vorbereitet damit wir das seriellen Sevice öffnen können dafür bietet die exec Unit OpenDevice() und CloseDevice().

var
  Res: LongInt;
  DevOpen: Boolean = False;
  DeviceName: AnsiString = 'serial.device'; // or usbmodem.device
  UnitNumber: Integer = 0;

    // ...
    Res := OpenDevice(PChar(DeviceName), UnitNumber, PIORequest(io), 0);
    if Res <> 0 then
    begin
      Writeln('Unable to open device "' + DeviceName + ' ' + IntToStr(UnitNumber) + '" :' +  IntToStr(Res));
      Exit;
    end;
    DevOpen := True;
    
    // ...
    if DevOpen then
      CloseDevice(PIORequest(io));

Serielle Device Parameter setzen

Bevor wir das Device wirklich benutzen können müssen wir noch einige Parameter setzen, wie Baudrate und Flow control. Dafür müssen wir das erste mal eine Nachricht and den Serielle Device Treiber schicken mit dem IORequest den vorhin erzeugt haben. Zuerst wir konfigurieren den IORequest:

    // Parity und XON/XOFF ausschalten 
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); 
    io^.io_Baud := Baud;                     // Baud rate setzen
    io^.io_ReadLen := 8;                     // 8 Bits pro Character
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 Stopbit 
    io^.IOSer.io_Command := SDCMD_SETPARAMS; // Message Kommando = Setze Parameter

Um die Nachricht zu versenden wir können DoIO() oder SendIO() neutzen. DoIO() blockiert bis die Nachricht abgearbeitet ist. SendIO() kehrt sofort zurück, läuft im Hintergrund weiter und wir müssen testen ob die Nachricht beendet ist. I prefer SendIO() because you have more control about what happens and you can break the request if it needs too long time. With CheckIO() you can test if the request is already finished or use WaitIO() for the end of operation. All together it looks like that:

    // Parity und XON/XOFF ausschalten
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); 
    io^.io_Baud := Baud;                     // Baud rate setzen
    io^.io_ReadLen := 8;                     // 8 Bits pro Character
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 Stopbit 
    io^.IOSer.io_Command := SDCMD_SETPARAMS; // Message Kommando = Setze Parameter
    SendIO(PIORequest(io));  // sende die Nachricht
    WaitIO(PIORequest(io));  // Warte auf die Antwort

Text lesen

Um Text vom seriellen Port zu lesen, welches der Arduino sendet, wir müssen wieder eine Nachricht senden mit dem Kommando CMD_READ und einem Puffer wo der Text abgelegt werden soll.

var
  Buffer: array[0..256] of char;
    // ...
    FillChar(Buffer[0], 257, #0);        // Buffer löschen (immer ein  #0 am Ende)
    io^.IOSer.io_Length := 256;          // einer weniger als die wirkliche Groesse dahaer immer ein #0 am ende
    io^.IOSer.io_Data := @Buffer[0];     // Zeiger auf den ersten wert als Start 
    io^.IOSer.io_Command := CMD_READ;    // Kommando = Lesen
    
    SendIO(PIORequest(io));
    WaitIO(PIORequest(io));
    // ...

Damit haben wir alles was wir brauchen um den Text vom Arduino zu lesen. Alles zusammen:

program test;
{$mode objfpc}{$H+}
uses
  SysUtils,
  exec, serial;
const
  DefDevice = 'serial.device'; // or usbmodem.device
  DefUnit = 0;
  DefBaud = 9600;
var
  Mp: PMsgPort = nil;
  Io: PIOExtSer = nil;
  DevOpen: Boolean = False;
  Res: LongInt;
  DeviceName: string = DefDevice;
  UnitNumber: Integer = DefUnit;
  Baud: Integer = DefBaud;
  Buffer: array[0..256] of char;
begin
  try
    // create Messageport
    Mp := CreateMsgPort;
    if not Assigned(Mp) then
    begin
      writeln('Failed to create MsgPort');
      Exit;
    end;
    // create IO Serial
    Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
    if not Assigned(Io) then
    begin
      Writeln('Cannot alloc IOExtSer');
      Exit;
    end;
    // Open the device
    Res := OpenDevice(PChar(DeviceName), UnitNumber, PIORequest(io), 0);
    if Res <> 0 then
    begin
      Writeln('Unable to open device "' + DeviceName + ' ' + IntToStr(UnitNumber) + '" :' +  IntToStr(Res));
      Exit;
    end;
    DevOpen := True;
    // configure serial interface
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON);
    io^.io_Baud := Baud;
    io^.io_ReadLen := 8;                     // 8 Bits pro Character
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 Stopbit 
    io^.IOSer.io_Command := SDCMD_SETPARAMS;
    SendIO(PIORequest(io));
    WaitIO(PIORequest(io));

    // read 256 chars from the serial port
    FillChar(Buffer[0], 257, #0); 
    io^.IOSer.io_Length := 256;
    io^.IOSer.io_Data := @Buffer[0];
    io^.IOSer.io_Command := CMD_READ;
    SendIO(PIORequest(io));
    WaitIO(PIORequest(io));

    // everything ok - print out the Buffer
    writeln('Everything ok, Buffer: ', Buffer);
  finally
    if DevOpen then
      CloseDevice(PIORequest(io));
    if Assigned(Io) then
      DeleteExtIO(PIORequest(Io));
    if Assigned(Mp) then
      DeleteMsgPort(Mp);
  end;
end.

Wenn man das kompiliert und auf dem Amiga starten mit dem Arduino angeschlossen, man sollte folgendes erhalten:

Work:Sources/Serial> test
Everything ok, Buffer:
 Amiga (Msg: 10)
Hello Amiga (Msg: 11)
Hello Amiga (Msg: 12)
Hello Amiga (Msg: 13)
Hello Amiga (Msg: 14)
Hello Amiga (Msg: 15
Work:Sources/Serial>

Wir haben unseren ersten Text vom Arduino. Wie man sehen kann wartet der WaitIO() aufruf bis 256 Zeichen eingetroffen sind. Was aber etwas störend sein kann, wenn man eine direkte Reaktion auf einen Text haben möchte und nicht soviel Text gesendet wird.

EOF Modus

Dafür können wir den EOF Moudus des seriellen Devicetreibers benutzen. Dieser stoppt die Textaufnahme wenn ein "Ende des Files" erhalten wird im Zeichenstrom. Jede Zeile vom Arduino sollte mit einem Return (#13 #10) enden so wir könnten das #10 =($0a) Zeichen benutzen um die Textaufnahme vorzeitig abzubrechen. Um das zu erreichen ändern wir die Parameter die wir am Anfang mir SETPARAM gesetzt haben zu:

    // Parity und XON/XOFF ausschalten, EOF anschalten
    io^.io_SerFlags := (io^.io_SerFlags or SERF_EOFMODE or SERF_XDISABLED) and (not SERF_PARTY_ON); 
    io^.io_Baud := Baud;                      // Baudrate setzen
    io^.io_ReadLen := 8;                      // 8 Bits pro Character
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                     // 1 Stopbit 
    io^.io_TermArray.TermArray0 := $0a030303; // Ende des Files Zeichen #10 Rest gefüllt mit $03 = End of Text
    io^.io_TermArray.TermArray1 := $03030303;
    io^.IOSer.io_Command := SDCMD_SETPARAMS;  // Nachtichten Kommando type = Setze Parameter
    SendIO(PIORequest(io));  // Sende Nachricht
    WaitIO(PIORequest(io));  // Warte auf Antwort

Wenn man das so ändert erhält man schliesslich:

Work:Sources/Serial> serialtest
Everything ok, Buffer: Hello Amiga (Msg: 23)
Work:Sources/Serial>

Das ist schon viel besser, wir erhalten nur eine einzige Zeile des Texts und diese daher auch sofort. Falls der Text länger als 256 Zeichen ist bricht er natürlich immer noch dort ab und man muss nochmal nach dem Rest fragen (oder einen grösseren Buffer bereitstellen).

Aber was passiert, wenn kein Text mehr gesendet wird? Der WaitIO() Aufruf blockiert für immer und man kann ihn auch nicht unterbrechen (ausser mit einem Computerneustart). Um das zu umgehen programmieren wir einen Timeout dazu.

Timeout

Wir brauchen die vorher erwähnte Funktion CheckIO() um zu prüfen ob das Kommando abgearbeitet wurd und eine Funktion um einen IORequest abzubrechen wenn ein Timeout eintritt, diese Funktion heisst Abort(IO). Um die Zeit zu überprüfen kann man sehr einfach die Funktion GetTickCount() aus SysUtils benutzen, diese gibt uns die Zeit in Millisekunden, welche wir benutzen können um sehr genau Zeitabstände zu messen. Für den Timeout ersetzen wir SendIO()/WaitIO() für das Kommando CMD_READ mit:

var
  t: LongWord;
    // ...
    t1 := GetTickCount;
    repeat
      if CheckIO(PIORequest(io)) <> nil then
        Break;
      Sleep(25);
    until GetTickCount - t1 > 10000;
    if CheckIO(PIORequest(io)) <> nil then
    begin
      WaitIO(PIORequest(io));
      writeln('Everything ok, Buffer: ', Buffer);
    end
    else
    begin
      AbortIO(PIORequest(io));
      writeln('Timeout, not enough data: ', Buffer);
    end;

Von hier an gibt es nur noch Teile des Sopurce, da es sonst zu lang wird, allerdings kannst du den kompletten Source hier runterladen: Source1.pas[2]

Temperatursensor

Da die serielle Übertragung jetzt funktioniert, können wir etwas mehr sinnvolles mit dem Arduino machen. Zum Beispiel Sendoren and den Arduino anschliessen, die Werte auslesen und an den Amiga schicken. Fangen wir mit einem Temperatursensor an, dies is relativ einfach weil es nicht viel Programmierung benötigt und die Werte siuch nicht sehr schnell ändern. Ich habe dafür einen DS18B20 Temperatursensor welches mittels 1-Wite Protokoll kommuniziert. Es gibt dafür schon fertig Libraries in der Arduino Library Kollektion. Die Library, die man hier benötigt heisst "MAX31850 DallasTemp" (diese braucht auch eine weitere Library zum 1-Wire zugriff, diese wird aber normalerweise automatisch mitinstalliert). Nachdem die Library installiert ist muss man den Sensor noch an den Arduino anschliessen. Ich benutze den GPIO Pin 10 dafür (weil bei meinem Arduino andere Pins schon für andere Sachen belegt sind) man kann auch einen anderen Pin benutzen.

TemperatureArduino.png


Schliesse die Kabel wie im Bild gezeigt an, wie im Bild zu sehen benötigt man nur den Temperatursensor, einen 4.7 kOhm Widerstand und einige Kabel (und vielleicht ein Breadboard um alles draufzustecken). Um die Temperatur vom Sensor auszulesen schreiben wir ein neues kleines Arduinoprogram welches die Temperatur an den seriellen Port sendet.

#include <OneWire.h>
#include <DallasTemperature.h>

const unsigned long BAUD_RATE = 9600;
const unsigned char ONE_WIRE_BUS = 10; // Pin des Temperatur sensors

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensoren(&oneWire);

void setup() {
  sensoren.begin();
  Serial.begin(BAUD_RATE);
}

void loop() {
  sensoren.requestTemperatures();
  if (sensoren.getDeviceCount() > 0)
  {
    float temperatur = sensoren.getTempCByIndex(0);
    Serial.println(temperatur);
  }
  else
  {
    Serial.println("N");
  }
  delay(200);
}

Wenn wir jetzt unser FreePascal-Programm starten zeigt es die Temperatur welcher der Sensor misst, ohne das wir das Programm ändern müssten.

Graphische Benutzeroberfläche

Mehrere Sensoren