Tutorial:Serial Communication

From Freepascal Amiga wiki
Jump to: navigation, search

Start Next

This tutorial shows how to use devices on Amiga which uses serial communication, either by RS232 or USB. As an example an Arduino UNO board is used. Everything shown here is tested on an Amiga 1200 (68030/50 Mhz, OS 3.9) and on MorphOS machine, but should work the same way on a AmigaOS4 or native AROS (i386/x86_64/ARM) installation.

Preparing the serial device

RS232TTL.jpg

As an example of a serial device we will use an Arduino UNO connected via serial to TTL adapter to the Amiga computer or USB providing a virtual serial port to MorphOS. Be aware you can not directly connect the TX/RX of the Arduino to the Amiga RS232 you need some more electronics, search Google for e.g. "RS232 Shield Arduino" or "RS232 to TTL adapter".

First we need to program the Arduino. As a start we keep it simple and just use a very simple program which just print out a "Hello World" with a number.

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);
}

It sends a "Hello World (Msg: count)" every half second to the serial interface. Which we can easily check with the Arduino IDE Serial Monitor.

Serial output on Amiga

Now we want to see the same on the Amiga, with RS232 you only need to open any terminal program for example NComm[1] set the device to serial.device Unit to 0 and baud rate to 9600 then you should already see the Hello world output.

If you connect the Arduino via USB the installed USB stack (e.g. Poseidon) should automatically find and use the right driver cdcacm.class for the virtual serial port.

ArduinoUSB.png

If it does not automatically connect the cdcacm.class to it (e.g. on MorphOS) you have to force it by hand to the Endpoint number 0. (open USB Prefs/Trident, Devices, double click "Arduino UNO", Select "CDC control interface (0)", right click "forced binding" "cdcacm.class", acknowledge the warning, disconnect the device, connect the device). This virtual serial port is now available via the usbmodem.device. Now its the same as for the RS232 case, open terminal program choose usbmodem.device, unit 0, 9600 baud and the output should appear.

Serial device with FreePascal

Message port

As first we need a message port for the communication with the serial device, for that you can find a CreateMsgPort() and DeleteMsgPort() in the exec unit for that purpose.

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

If there is not enough memory the creation of message port can fail and a nil value is returned, of course we should test that and leave if there is a problem. You can either compare the messageport with nil or use the specialized function Assigned() for it.

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.

This source will now test the creation of the message port and show a message and leave the program if there is a problem.

IO Request

As next item we need a IORequest, because we want to do serial communication it must be the specialized IORequest named PIOExtSer from the serial unit. (if you do not have a serial unit, you have to update your FreePascal version). The IORequest basically is the message we send to the serial device if we want send or get data or change settings. Again, to create it there is a specialized function for in the exec unit CreateExtIO() and DeleteExtIO(). Let us extent the code with creating the 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.

Let me point out a little problem created here. If the message port is created successfully but the IORequest cannot be created it will leave the program and leave the message port undestroyed. You could use stacked if blocks like this:

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.

And you can certainly do that, but you can imagine that it really becomes messy and complicated if you have many of such items to check. I prefer the try...finally..end; approach or separate the init and destruction to different routines, which is especially easy if you use classes where you have a constructor and destructor. Attention: If you want to use classes or try finally you have to be in Delphi or ObjFPC mode e.g. {$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.

Notice that all the variables are initialized to nil and then in the finally part they are checked against nil before get destroyed. For this special functions this is not really needed because they will also check the supplied pointers, but many other Amiga API calls does not check for empty pointer and just crash. Therefore it's better to get used to check the pointers before supply to a Amiga API call.

Open serial device

But back to the serial communication. Everything is set up that we can open the actual serial device with OpenDevice() and CloseDevice() from exec.

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));

Set serial device parameter

Before we actually can use the device we have to configure the communications, like baud rate and flow control, for that we have the first time send a message using the IORequest we created before. Starting with configuring the message we like to send

    // Disable parity and XON/XOFF
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); 
    io^.io_Baud := Baud;                      // Set baud rate
    io^.io_ReadLen := 8;                     // 8 bits per char for read and write
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 stop bit 
    io^.IOSer.io_Command := SDCMD_SETPARAMS;  // message command = Set Parameter

to actually send the message we can either use DoIO() or SendIO(). DoIO() blocks until the message is processed, SendIO() returns directly and runs asynchronously and we have to check if it is finished. 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:

    // Disable parity and XON/XOFF
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); 
    io^.io_Baud := Baud;                     // Set baud rate
    io^.io_ReadLen := 8;                     // 8 bits per char for read and write
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 stop bit 
    io^.IOSer.io_Command := SDCMD_SETPARAMS; // message type = Set Parameter
    SendIO(PIORequest(io));                  // send the message
    WaitIO(PIORequest(io));                  // Wait for answer

Read text

To read the text from the serial port, which the Arduino sends, we also need to send a message to the serial device together with a Buffer.

var
  Buffer: array[0..256] of char;
    // ...
    FillChar(Buffer[0], 257, #0);        // Clear the Buffer area (always a #0 at end)
    io^.IOSer.io_Length := 256;          // one less than the actual size, to make sure a #0 at end
    io^.IOSer.io_Data := @Buffer[0];     // pointer to a Buffer 
    io^.IOSer.io_Command := CMD_READ;    // message command = read
    
    SendIO(PIORequest(io));
    WaitIO(PIORequest(io));
    // ...

With that, we have everything we need to read what the Arduino sends. Lets put all together:

program test;
{$mode objfpc}{$H+}
uses
  SysUtils,
  exec, serial;
const
  DefDevice: AnsiString = '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 per char for read and write
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 stop bit 
    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.

if you compile that and start on your Amiga with the Arduino connected you should see something like:

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>

We have our first output. As you can see the WaitIO() waits until the 256 Chars arrived. Which could be a bit annoying if you want to have immediate reaction to a change in the output.

EOF Mode

For that purpose we can use the EOFMode of the the serial device driver. It stops the collection of data when a end of file is recognized in the stream. Every output we get from the Arduino should be ending with a "return" at the end (#13 #10) so we could trigger on the #10 =($0a) to end the collection. To achieve that we change the configuration to:

    // Disable parity and XON/XOFF, enable EOF
    io^.io_SerFlags := (io^.io_SerFlags or SERF_EOFMODE or SERF_XDISABLED) and (not SERF_PARTY_ON); 
    io^.io_Baud := Baud;                      // Set baud rate
    io^.io_ReadLen := 8;                     // 8 bits per char for read and write
    io^.io_WriteLen := 8;
    io^.io_StopBits := 1;                    // 1 stop bit 
    io^.io_TermArray.TermArray0 := $0a030303; // termination character #10 filled with $03 = end of Text
    io^.io_TermArray.TermArray1 := $03030303;
    io^.IOSer.io_Command := SDCMD_SETPARAMS;  // message type = Set Parameter
    SendIO(PIORequest(io));                   // send the message
    WaitIO(PIORequest(io));                   // Wait for answer

if you use that you end up with something like that:

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

Thats much better, we only get a single line of output immediately when the output is done the result can be read. But what happens when the message is longer than the 256 bytes in the Buffer, then it will terminate after 256 bytes and I have to read a second time to get the rest of message which will terminate at the return.

But what happens, when suddenly there are no messages anymore, then the WaitIO() call will never return and you also cannot break it, just restart your computer. To prevent that I want to introduce a timeout for the IORequest to the serial device.

Timeout

We need the aforementioned CheckIO() for it and an other call to stop a running IORequest, which is called Abort(IO). To gather the time we use the function GetTickCount() from SysUtils, which gives us the milliseconds since the computer is started, which we can use to measure times precisely. To introduce the timeout we replace the SendIO()/WaitIO() Lines for the CMD_READ with:

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
      AbortIO(PIORequest(io));
      writeln('Timeout, not enough data: ', Buffer);
      Exit;
    end;
    // WaitIO again to remove the message from the list
    Res := WaitIO(PIORequest(io));
    if Res <> 0 then
    begin
      writeln('IORequest failed with error: ', Res);
      Exit;
    end;
    writeln('Everything ok, Buffer: ', Buffer);

The complete Source as Download: Serial1.pas[2]

Temperatur sensor

As we have the serial communication is control, we make something more useful with the Arduino. For example we can attach some sensor to the Arduino and read out the value with the Amiga. Let us start with a temperature sensor, which is fairly easy because it does not need much programming and the value does not change too often. As an example I use a DS18B20 Temperature sensor which uses 1-Wire protocol. There are already ready to use libraries in the Arduino library collection. The library is called "MAX31850 DallasTemp" (wich also have a dependancy to a 1-wire library). With the library installed you need to connect the sensor to your Arduino. I use the GPIO Pin 10 (because the others are occupied with other things), but you can use any Pin you like.

TemperatureArduino.png

Connect the lines as shown in the picture, as shown you only need a 4.7 k ohm resistor and some cables (and a bread board to build on it). To read the temperature from the sensor we write a little Arduino program which prints out the temperature to serial port.

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

const unsigned long BAUD_RATE = 9600;
const unsigned char ONE_WIRE_BUS = 10;

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);
}

If we try that with our freepascal program we can nicely read the temperature without need to change the program.

Graphical User Interface

Multiple Sensors