Contents
Overview
This page describes various ways of accessing hardware devices on Lazarus. These devices include, but are not limited to: ISA, PCI, USB, parallel port, serial port.Uniform multi-platform access to hardware devices is not implemented by the Free Pascal Runtime Library (RTL) or by the LCL - the underlying operating systems are often different enough to make that very difficult. Therefore, this article will basically cover hardware access methods on different platforms. The code can be compiled on different environments using conditional compiles, like this:
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls,
{$IFDEF WIN32}
Windows;
{$ENDIF}
{$IFDEF Unix}
ports;
{$ENDIF}
Parallel and Serial Comparison
ISA Cards, PCI Cards and the Parallel Port communicate with the computer using a parallel protocol. The Serial Port and USB devices work with a serial protocol. Because the processor and thus programming languages all work on a parallel approach to data, access to this kinds of protocols is easier to be implemented on the software side. When you access an Integer variable, for example, you can access it's value with a single command. With a serial protocol, however, you can only know one bit at a time, and you need to glue the pieces together to understand the data.Serial communication is difficult to be implemented directly, but it can be pretty easy if you use a pre-made component. It is also harder on the hardware side, so many devices use specialised Integrated Circuits or even Microcontrolers to implement it.
Now a brief comparison of hardware access protocols will be given:
|
Speed | Hardware implementation difficulty |
---|---|---|
Serial Port | Very slow (< E5 bit/s) | Medium |
Parallel Port | Slow (~ E6 bit/s) | Easy |
ISA Card | Medium (~ E7 bit/s) | Medium |
USB | Medium (~ E7 bit/s) | Hard |
PCI Card | Very Fast (> E9 bit/s) | Very Hard |
Parallel Communication
Using inpout32.dll for Windows
Windows has different ways to access hardware devices on the 9x series and on the NT series. On the 9x series (95, 98, Me) programs can access the hardware directly, just like they did on DOS. The NT series (Windows NT and XP), however, don't allow this approach. On this architecture, all communication with hardware ports must be through a device driver. This is a security mechanism, but developing a driver can cost too much in terms of time and money for small projects.Happily there is a library that solves this problem. If Windows NT is detected, it decompresses HWInterface.sys kernel device driver and installs it. If Windows 9x is detected, it simply uses assembler opcodes to access the hardware.
But how do I use the library? Simple! It has only two functions, Inp32 and Out32, and their use is quite intuitive.
We will load the library dynamically, so let's define both functions first:
type
TInp32 = function(Address: SmallInt): SmallInt; stdcall;
TOut32 = procedure(Address: SmallInt; Data: SmallInt); stdcall;
- Address represents the address of the port you desire to access
- Out32 sends Data to the port you specify by Address
- Inp32 returns a byte from the port you specify by Address
uses
....dynlibs...
type
TMyForm = class(TForm)
.........
private
{ private declarations }
Inpout32: THandle;
Inp32: TInp32;
Out32: TOut32;
.........
implementation
.........
procedure TMyForm.FormCreate(Sender: TObject);
begin
{$IFDEF WIN32}
Inpout32 := LoadLibrary('inpout32.dll');
if (Inpout32 <> 0) then
begin
// needs overtyping, plain Delphi's @Inp32 = GetProc... leads to compile errors
Inp32 := TInp32(GetProcAddress(Inpout32, 'Inp32'));
if (@Inp32 = nil) then Caption := 'Error';
Out32 := TOut32(GetProcAddress(Inpout32, 'Out32'));
if (@Out32 = nil) then Caption := 'Error';
end
else Caption := 'Error';
{$ENDIF}
end;
procedure TMyForm.FormDestroy(Sender: TObject);
begin
{$IFDEF WIN32}
FreeLibrary(Inpout32);
{$ENDIF}
end;
{$IFDEF WIN32}
myLabel.Caption := IntToStr(Inp32($0220));
{$ENDIF}
Note: For
deployment you need to include "inpout32.dll" in the same directory of
our application. Also library have to be registered in system using
administrator user on Windows NT/XP/2000 or elevated privileges on
Windows Vista/7. This can be done by installation program such
InnoSetup:
Filename: {sys}\rundll32.exe; Parameters: "inpout32.dll,IsInpOutDriverOpen"; WorkingDir: {app}; Flags: 32bit;
This is the homepage for the library: www.logix4u.net/inpout32.htm *see discussion*
Using assembler on Windows 9x
On Windows 9x you can also use assembler code. Suppose you wish to write $CC to the $320 port. The following code will do it: {$ASMMODE ATT}
...
asm
movl $0x320, %edx
movb $0xCC, %al
outb %al, %dx
end ['EAX','EDX'];
Troubleshooting on Windows
One possible source of trouble using parallel hardware that does not support Plug And Play on Windows is that Windows may assign the port utilized by your hardware to another device. You can find instructions on the URL below about how to tell Windows not to assign the address of your device to Plug And Play devices:http://support.microsoft.com/kb/135168
Using ioperm to access ports on Linux
The best way to access the hardware on Linux is through device drivers, but, due to the complexity of the task of creating a driver, sometimes a quick method is very useful.In order to use the "ports" unit under Linux your program must be run as root, and IOPerm must be called to set appropriate permissions on the port access. You can find documentation about the "ports" unit here.
The first thing to do is link to (g)libc and call IOPerm. A unit that links to the entire (g)libc exists on free pascal, but this unit gives problems when used directly by application and linking statically to the entire (g)libc library is not a very good idea because it changes often between version in an incompatible manner. Functions like ioperm, however, are unlikely to change.
{$IFDEF Linux}
function ioperm(from: Cardinal; num: Cardinal; turn_on: Integer): Integer; cdecl; external 'libc';
{$ENDIF}
- "from" represents the first port to be accessed.
- "num" is the number of ports after the first to be accessed, so ioperm($220, 8, 1) will give access for the program for all ports between and including $220 and $227.
{$IFDEF Linux}
i := ioperm($220, 8, 1);
port[$220] := $00;
myLabel.Caption := 'ioperm: ' + IntToStr(i);
i := Integer(port[$220]);
myOtherLabel.Caption := 'response: ' + IntToStr(i);
{$ENDIF}
General UNIX Hardware Access
{$IFDEF Unix}
Uses Clib; // retrieve libc library name.
{$ENDIF}
{$IFDEF Unix}
function ioperm(from: Cardinal; num: Cardinal; turn_on: Integer): Integer; cdecl; external clib;
{$ENDIF}
Note that FPC provides an abstraction for ioperm called "fpioperm" in unit x86, and also defines fpIOPL and out-/inport functions. These functions are currently implemented for Linux/x86 and FreeBSD/x86.
It is not recommended to link to libc unless absolutely necessary due to possible deployment and portability functions. Also manual linking to libc (by declaring ad hoc libc imports for functions that are available elsewhere) like done above is not recommended (e.g. the above libc import line will unnecessarily fail if the standard C lib is not called libc, like e.g. libroot on BeOS, or on platforms with a non standard C symbol mangling).
Note 2 Using unit libc is not recommended under any circumstances other than Kylix compatibility. See libc unit
Status and control
Besides data lines, the parallel port also has status and control lines which are accessed using the status and control registers. While the base address accesses the data lines and reads or writes data bytes from/to them, the Status register is accessed on the address offset by +1 and Control register is accessed on the offset +2. For example, LPT1 (first parallel port on a PC) has the base address $378, so its Status register is at $379 and Control register at $380. To get individual status line states, you read a byte from its address and its bits represent those lines. Setting control lines is similarly done by writing a byte with accordingly set bits to the Control register.Newer bidirectional parallel port versions have more registers on higher offsets. More details about them, together with information which bits map to which lines can be found here.
Most directly accessed hardware devices other than PC parallel ports are controlled in a similar way. Depending on the device in question, it is necessary to find out what registers are available (above mentioned control and status, but also address and other registers) and which bits represent which hardware functions.
Serial Communication
Synaser
It is very easy to build a serial communication software using the Synaser library. The example when used together with the Synaser documentation should be trivial to understand. The most important part is TBlockSerial.Config to configure the speed (in bits per second), data bits, parity bits, stop bits and handshake protocol, if any. The following code was tested with a serial mouse connected to COM 1.program comm;
{$apptype console}
uses
Classes, SysUtils, Synaser;
var
ser: TBlockSerial;
begin
ser:=TBlockSerial.Create;
try
ser.Connect('COM1');
ser.config(1200, 7, 'N', SB1, False, False);
while True do
Write(IntToHex(ser.RecvByte(10000), 2), ' ');
finally
ser.free;
end;
end.
The reason is not difficult to understand: The application is in the while true do - loop, which is, to be more precisely, an endless loop. There is no abort-condition, so the only way to close the application is to close the terminal or to press CTRL-C. But if you quit the application this way, the important part with "ser.free", which frees the serial port, will never be called. This problem is described in the following thread in the German Lazarus-Forum http://www.lazarusforum.de/viewtopic.php?f=10&t=2082
There is a bit code around the main application to make every user clear, not to press CTRL-C. If anyone is worrying, why /dev/ttyUSB0 is used for com-port: this is due to the USB to Serial Adapter (from Digitus) on the test-system. If you have an built-in serial port, please use the 'Com0' - declaration like in the code - example above.
program serialtest;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Classes,SysUtils,Synaser,Crt
{ you can add units after this };
var l:boolean;
function check_affirmation():boolean;
var k:string;
begin
Writeln('To quit the application please do NOT use CTRL-C! Instead, please press any key to quit the application! '+
'Please confirm this notification before the application continues! '+
'[0]=Quit, [1]=Confirm, please continue! ');
Writeln('Your decision: ');
Read(k);
if StrtoInt(k) = 1 then
begin
check_affirmation:=true;
Writeln('OK, application continues ...');
end
else
begin
check_affirmation:=false;
Writeln('Abort');
end
end;
procedure RS232_connect;
var
ser: TBlockSerial;
begin
ser:=TBlockSerial.Create;
try
ser.Connect('/dev/ttyUSB0'); //ComPort
Sleep(1000);
ser.config(1200, 7, 'N', SB1, False, False);
Write('Device: ' + ser.Device + ' Status: ' + ser.LastErrorDesc +' '+
Inttostr(ser.LastError));
Sleep(1000);
repeat
Write(IntToHex(ser.RecvByte(10000), 2), ' ');
until keypressed; //Important!!!
finally
Writeln('Serial Port will be freed...');
ser.free;
Writeln('Serial Port was freed successfully!');
end;
end;
begin
l:=check_affirmation();
if l=true then
RS232_connect()
else
Writeln('Program quit! ');
end.
It is also worth noting the function of the TBlockSerial.LinuxLock parameter under linux. When set to default of True, a connect will try to create a lock file (eg. "LCK..ttyUSB0") under /var/lock and fail if a lock already exists for the requested port. The lock file will be left over if Free was not called. Setting LinuxLock to False will make Synaser ignore port locking.
There are alternatives to Synaser; see below.
5dpo
There is also a visual component 5dpo that is based on Synaser.TLazSerial
Based on 5dpo (and therefore Synapse): http://forum.lazarus.freepascal.org/index.php/topic,20481.0.htmlFPC built in Serial unit
Another very simple fpc serial unit is now part of FreePascal (since version 2.2.2): just put Serial in your Uses list however there does not seem to be any documentation other than the Serial.pp source file and some discussions.A simple example using FPC 2.6.2 on WIN7.
Program TestSerialPortCom;
{
Usage options:
TestSerialPortCom => uses default COM1
TestSerialPortCom 8 'Hello' => uses COM8 and output 'Hello' to port before waiting for an answer
the program will open an serialport and output Hello, after that the code will wait unitil a CR (#13) is recieved
or a key is pressed.
}
uses
serial, crt;
VAR
serialhandle : LongInt;
ComPortName : String;
s,tmpstr,txt : String;
ComOut,ComIn : String;
ComPortNr : integer;
writecount : integer;
status : LongInt;
BitsPerSec : LongInt;
ByteSize : Integer;
Parity : TParityType; { TParityType = (NoneParity, OddParity, EvenParity); }
StopBits : Integer;
Flags : TSerialFlags; { TSerialFlags = set of (RtsCtsFlowControl); }
ErrorCode : Integer;
BEGIN
ComPortNr:=1;
tmpstr:='';
txt:='';
writeln('Parameters ',ParamCount);
if (ParamCount>0) then
begin
tmpstr:= ParamStr(1);
val(tmpstr,ComPortNr,ErrorCode);
if (ParamCount>1) then
begin
txt:= ParamStr(2);
{val(tmpstr,ComPortNr,ErrorCode);}
end;
end;
str(ComPortNr,tmpstr);
ComPortName:= 'COM'+tmpstr+':';
writeln('Using '+ComPortname);
serialhandle := SerOpen(ComPortName);
Flags:= [ ]; // None
SerSetParams(serialhandle,9600,8,NoneParity,1,Flags);
s:=txt; // use the input text
writeln('OUT '+s);
s:=s+#13+#10; { CR + LF }
writecount := length(s);
status := SerWrite(serialhandle, s[1], writecount );
// The next line is for debugging only!
writeln('status: ', status, ' writecount: ', writecount);
if status > 0 then
begin
writeln('Waiting for answer');
{ wait for an answer }
s:='';
ComIn:='';
while (Length(Comin)<10) and (status>=0) and not keypressed do begin
status:= SerRead(serialhandle, s[1], 10);
if (s[1]=#13) then status:=-1; { CR => end serial read }
if (status>0) then ComIn:=ComIn+s[1];
if (status>0) then begin
writeln(status,' ',length(ComIn),' ASCII ',ord(s[1]),' INP ',ComIn);
end;
end;
end
else
writeln('ERROR - Unable to send.');
SerSync(serialhandle); { flush out any remaining before closure }
SerFlushOutput(serialhandle); { discard any remaining output }
SerClose(serialhandle);
END.
Serial port names on Windows
COM ports are named with a number on Windows 9x-based OSes (95,98,ME), e.g. COM1, COM30.On Windows NT-based systems (NT, 2000, XP, Vista, Windows 7, Windows 8), COM ports are numbered too, but only for compatibility with DOS/Win9x.
Use this code to get the real name:
// ComNr is obviously the number of the COM port
if ComNr > 9 then
Result := Format('\\\\.\\COM%d', [ComNr])
else
Result := Format('COM%d', [ComNr]);
USB
libusb
A cross platform possibility for Linux, BSDs and Mac OS X is libusb.Headers are listed in http://www.freepascal.org/contrib/db.php3?category=Miscellaneous:
name | author | version | date | link | remarks |
---|---|---|---|---|---|
libusb.pp | Uwe Zimmermann | 0.1.12 | 2006-06-29 | http://www.sciencetronics.com/download/fpc_libusb.tgz |
|
libusb.pas | Johann Glaser | |
2012-09-23 | https://github.com/hansiglaser/pas-libusb | includes OOP wrapper, see branch "libusb-1.0" for libusb 1.0 |
fpcusb | Joe Jared | 0.11-14 | 2006-02-02 | http://relays.osirusoft.com/fpcusb.tgz | download link broken |
libusb.pp | Marko Medic | 1.0 | 2010-12-14 | http://www.lazarus.freepascal.org/index.php/topic,11435.0.html |
|
No comments:
Post a Comment