See also, Part One and
Part Three
Last month, I looked at using the basic MsComm control
to read and write data between the two serial ports on
a PC connected using a special ‘null-modem’ cable.
As well as reading and writing characters, I also used
the DTR and RTS control lines to trigger events. However,
it turns out that the MsComm control, while suited to
general modem communication, is somewhat lacking when
it comes down to the fine control sometimes required.
A particular problem is changing the state of the control
lines: with the standard control, you have to close and
open the port to set DTR/RTS. This leaves a gap in the
communication link. Normally, this isn’t a problem,
but in some circumstances it most certainly can be.
Take, for example, a computer that controls a machine – a
robot. The operating system in such a computer is usually
a ‘real-time’ operating system. ‘Real
time’ means that the operating system guarantees
that it will respond to a particular event within a
certain time. The events are usually microcontroller
interrupts raised by some external hardware such as,
say a limit switch being triggered. Another circumstance
might be to indicate to the machine that the control
computer wishes to talk to it. In this situation, the
RTS signal going on might mean: ‘I want to send
you some information NOW!” The machine would
then stop what it is doing and start to pay some attention.
Take Your PIC
Now you might think that there isn’t much call
for exact computer control of machines from Visual
Basic programs, so I’ll give you a real life
example. One of my projects at the moment is building
a data recorder using a tiny microcomputer called a
PIC, from Microchip. There’s a whole family of
these wonderful little beasts, each optimized for a
particular market niche. But the main characteristic
of a typical PIC chip is the size of its memory and
the fact that the whole thing fits onto an 18 pin integrated
circuit chip. The number of pins relates back to a
point that I made in last month's article about the
number of wires - the fewer the better. But the size
of the memory is also interesting – a
typical PIC chip has about 1k of program memory and
about 50 bytes (yes, that’s 50 bytes – it’s
not a misprint) of RAM. In contrast, the PC that I’m
currently working on has over 500 MILLION bytes of
RAM (that’s a factor of 10,000,000 greater) and
so much disk space (equivalent to program memory) that
I’ve
lost track. PIC programming is a different world – and
a lot of fun too!
But back to the example. I need to talk to the PIC
from my PC. The problem is that the PIC chip I’m
using doesn’t have a serial communication device
in it (with so little memory it doesn’t have
much of anything). You have to count and time exactly
the sequence of bits sent from the PC’s serial
port. For my PIC, to do this accurately at a reasonable
communication speed, the PIC has to be ready for the
transmission to start. So what I need to do is signal
to the PIC to get ready by setting RTS (say) before
starting the actual transmission, and without opening
and closing the communication port.
API Programming In General
As I’ve pointed out, this is exactly what you
can’t do with the standard MsComm control. To do
what’s required, the Windows API has to be used.
Incidentally, I’ll use the communications API as
a general introduction of how to go about API programming
from within Visual Basic 6.
You can pick out the API
definitions using the API viewer tool. It’s a
lot easier than typing them in by hand.
All API calls reside in Dynamic Link Libraries (DLLs)
which are external to Visual Basic. So the first thing
to do is import the function calls you need. This is
done using the Declare keyword:
Private Declare Function ReadFile Lib "kernel32" ( _
ByVal hFile As Long, _
lpBuffer As Any, _
ByVal nNumberOfBytesToRead As Long, _
lpNumberOfBytesRead As Long, _
lpOverlapped As Any) As Long
The example here, ReadFile, is used to read data
from a file or a communications port. The first word
is Private. All (as far as I know, anyway) API calls
are functions – they
return something, usually a status code, handle or Boolean
value. The Lib part specifies the DLL where the function
is to be found, in this case the basic Windows kernel
file, “kernel32”. Then follow the argument
definitions and return value – just like a standard
function definition.
But there are two things to notice. First, quite
a few arguments are prefixed by the ByVal keyword.
Normally in Visual Basic 6, you don’t bother
with ByVal – at
least, I don’t. However, in API programming the
difference between passing something ‘by value’ and
its opposite ‘by reference’ is highly significant
(see: By Value and By Reference). If an integer is
passed by value to an API, then the value of the integer
(say 100) is given to the API. If, on the other hand,
it is passed by reference, the
address of the memory location where
the value 100 is stored is passed instead. The difference
means that the API can alter the value of a variable
if it is passed by reference; if it is passed by value,
it can’t.
In the above declaration, lpNumberOfBytesRead is
passed in the usual Visual Basic 6 fashion – that
is, by reference. When the function completes, the
number of bytes read by the API will be in the variable,
lpNumberOfBytesRead.
On the other hand, nNumberOfBytesToRead must
be passed by value, not by reference. If you get the
passing mechanism wrong, your program will almost certainly
come to a sudden and sticky end.
The second thing to note is the Any type value. Now
this is a kludge, an escape trick. In other languages
such as Delphi and C/C++, you can ‘cast’ the
type of one variable to that of another. A cast is an
instruction to the compiler to shut up and get on with
what you’ve told it to do and not complain that
it can’t, say, assign a floating point value to
an integer. However, casts aren’t allowed in Visual
Basic 6 – much more checking is done by the environment
and compiler than in Delphi or C/C++. But for API programming
there must be some sort of escape mechanism, particularly
if you need to make an argument ‘null’.
Null values are a pain in Visual Basic 6: there are
several different varieties of nothing. If the following
sounds like something out of the Zen of Visual Basic,
I apologize – but that’s the way it is!
First, there’s the Nothing that occurs when you
Set an object to Nothing:
Set x = Nothing
That really is the one, true, absolute Visual Basic
6 Nothing. Then there’s the second sort of nothing,
known as Empty, used to indicate an uninitialized variant.
It’s also the empty string, “”. Unfortunately,
the nothing or null required by an API is something
different to the above two: it’s the address
zero, otherwise known as a ‘null pointer’.
This is the third type of nothing. You specify a null
pointer by using ‘ByVal
0’. But for the Visual Basic compiler to accept
this, you have to specify the argument type as Any.
It’s
not very elegant. And beware – the empty string, “”,
is not the same as ByVal 0 - and neither
is Nothing.
The Communications API Now we’re ready to tackle the communications
API. Actually, the communications API uses to a large
extent the APIs used to create, read and write to files.
So the first API to use is CreateFile:
Dim hCom(1) As Long
hCom(0) = CreateFile("COM1",
GENERIC_READ Or GENERIC_WRITE, ByVal 0, ByVal 0, OPEN_EXISTING,
ByVal 0, ByVal 0)
The first argument is the name of the communication
port, COM1, the second argument tells Windows that
we want to read and write, the third and fourth are
not used, the fifth argument says that the port exists
(sort of obvious, really) and the sixth and seventh
arguments are not used. Notice that all the unused
argument must be set to null – that’s the
ByVal 0 parts. What’s returned by the
API is a ‘handle’ to
the device. Handles are often used in API programming:
you get a handle to something then pass that handle
as an argument to other API calls, as we’ll see
below.
The next thing to do is set up the port’s speed
and other characteristics. These are specified in a ‘Device
Control Block’ or DCB. Getting the DCB right is
absolutely fundamental to getting the port to work correctly.
I have to admit to spending a happy couple of hours futzing
around trying to get a correct DCB, before realising
that there’s a better way. After a bit of ferreting
around in the API documentation, I discovered the easy
way to set up a DCB - the BuildCommDCB API. Just like
the (very) old fashioned DOS MODE command this takes
a string and builds a perfect DCB for you.
r = BuildCommDCB("baud=9600 parity=n data=8 stop=1
xon=off odsr=off octs=off dtr=on rts=on idsr=off ", d(0))
r = SetCommState(hCom(0), d(0))
Having set up the DCB, the next thing to sort out is
the port’s timeouts. In asynchronous communications,
where you don’t know how many characters are going
to arrive, you must specify how long the read function
is to wait before returning with what it’s found.
If you don’t specify the timeout, the ReadFile
API will never return until it’s read the specified
number of characters – its default behaviour. At
this stage, I just want to poll the port as I did with
the MsComm control last month. The way to do this is
to use the SetCommTimeouts API.
SetCommTimeouts takes a COMMTIMEOUTS structure rather
than individual parameters. COMMTIMEOUTS can be used
to set almost any combination of timeout conditions,
but the most common ones that will cause the ReadFile
API to return immediately are these:
cto(0).ReadIntervalTimeout
= &HFFFF
cto(0).ReadTotalTimeoutMultiplier
= 0
cto(0).ReadTotalTimeoutConstant = 0
and it’s called like this:
r = SetCommTimeouts(hCom(0), cto(0))
Now we come to the last little problem before we
can read and write from a port, How do you tell the
API where the characters come from and where to put
the ones read? In the ReadFile API (above), you’ll
see an argument lpBuffer As
Any. Translated
into APIspeak, this means that the API wants the address
of some memory to put the characters it will read.
In Visual Basic 6 terms, we have to go through a couple
of hoops to get this working. For a single byte, the
easy way is just to pass the address of a long integer:
Dim xb as Long
r = ReadFile(hCom(0), xb, 4, bytesread,
ByVal 0)
However, this is a fairly crude trick and a better
way is to define a ‘buffer’:
Private Type Buffer
b(31) As Byte
End Type
and use it like this:
Dim b as Buffer
r = ReadFile(hCom(0), ByVal b, 32,
bytesread, ByVal 0)
You can then extract characters like this:
If bytesread > 0 Then
For i = 0 To bytesread - 1
Port2.Text = Port2.Text & Chr(b.b(i))
Next
End If
So far, the effect of using communications APIs is
much the same as with the MsComm control – but
as yet with no events.
This illustrates the difference between using ByVal and ByRef. Here, the subroutine Test has two arguments.
The first, x, is passed ‘by reference’ while
the second, y, is passed ‘by value’. The
subroutine just modifies the two values. But only the
ByRef value is altered after the subroutine returns.
Values of a and b before the call to:
Sub test(ByRef x, ByVal y)
x = 1
y = 2
End Sub
|
|
Values of a and b after:
a = 200
b = 100
test(a, b)
|
|
Next month, I’ll look at communication “events”.
Such as tripping over the modem cable and causing
a disconnect. It might be obvious to you what’s
happened but how does the PC know? And how does
it tell your program?
|
June 2005
|