Difference between revisions of "Tutorial:Serial Communication"

From Freepascal Amiga wiki
Jump to navigation Jump to search
(Inital serial device description)
 
(To the first output from serial)
Line 38: Line 38:
  
 
== Serial device with FreePascal ==
 
== Serial device with FreePascal ==
 +
As first we need a message port for the communication with the serial device, for that you can find a <code>CreateMsgPort()</code> and <code>DeleteMsgPort()</code> in the exec unit for that purpose.
 +
 +
<source lang="pascal">
 +
program test;
 +
uses
 +
  Exec;
 +
var
 +
  Mp: PMsgPort;
 +
begin
 +
  Mp := CreateMsgPort;
 +
  // do something
 +
  DeleteMsgPort(Mp);
 +
end.
 +
</source>
 +
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 <code>Assigned()</code> for it.
 +
<source lang="pascal">
 +
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.
 +
</source>
 +
This source will now test the creation of the message port and show a message and leave the program if there is a problem.
 +
 +
As next item we need a <code>IORequest</code>, because we want to do serial communication it must be the specialized IORequest named <code>PIOExtSer</code> 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 <code>CreateExtIO()</code> and <code>DeleteExtIO()</code>. Let us extent the code with creating the <code>PIOExtSer</code>
 +
<source lang="pascal">
 +
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.
 +
</source>
 +
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 <code>if</code> blocks like this:
 +
<source lang="pascal">
 +
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.
 +
</source>
 +
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 <code>try...finally..end;</code> 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. <code>{$mode objfpc}</code>.
 +
 +
<source lang="pascal">
 +
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.
 +
</source>
 +
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.
 +
 +
But back to the serial communication. Everything is set up that we can open the actual serial device with <code>OpenDevice()</code> and <code>CloseDevice()</code> from exec.
 +
<source lang="pascal">
 +
var
 +
  Res: LongInt;
 +
  DevOpen: Boolean = False;
 +
  DeviceName: AnsiString = 'usbmode.device'; // or serial 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));
 +
</source>
 +
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
 +
<source lang="pascal">
 +
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); // Disable parity and XON/XOFF
 +
    io^.io_Baud := Baud;                                                            // Set baud rate
 +
    io^.IOSer.io_Command := SDCMD_SETPARAMS;                                        // message command = Set Parameter
 +
</source>
 +
to actually send the message we can either use <code>DoIO()</code> or <code>SendIO()</code>. 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 <code>CheckIO()</code> you can test if the request is already finished or use WaitIO() for the end of operation. All together it looks like that:
 +
<source lang="pascal">
 +
    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); // Disable parity and XON/XOFF
 +
    io^.io_Baud := Baud;                                                            // Set baud rate
 +
    io^.IOSer.io_Command := SDCMD_SETPARAMS;                                        // message type = Set Parameter
 +
    SendIO(PIORequest(io));  // send the message
 +
    WaitIO(PIORequest(io));  // Wait for answer
 +
</source>
 +
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.
 +
<source lang="pascal">
 +
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));
 +
    // ...
 +
</source>
 +
With that, we have everything we need to read what the Arduino sends. Lets put all together:
 +
<source lang="pascal">
 +
program test;
 +
{$mode objfpc}{$H+}
 +
uses
 +
  SysUtils,
 +
  exec, serial;
 +
const
 +
  DefDevice = '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^.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.
 +
</source>

Revision as of 19:23, 8 December 2018

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

As an example of a serial device we will use an Arduino UNO connected via USB providing a virtual serial port to the Amiga computer. One could also connect the Arduino to the RS232 of the Amiga (if you do not have a USB port) but 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".

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

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.

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.

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 = 'usbmode.device'; // or serial 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));

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

    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); // Disable parity and XON/XOFF
    io^.io_Baud := Baud;                                                            // Set baud rate
    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:

    io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); // Disable parity and XON/XOFF
    io^.io_Baud := Baud;                                                            // Set baud rate
    io^.IOSer.io_Command := SDCMD_SETPARAMS;                                        // message type = Set Parameter
    SendIO(PIORequest(io));  // send the message
    WaitIO(PIORequest(io));  // Wait for answer

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 = '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^.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.