Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

VISUAL BASIC COMMUNICATIONS #2

In the second part of this series, we look at API programming in VB6
by Dermot Hogan

Requirements:
Visual Basic 6 (service pack 5)

 

Download The Source Code:
vb2src.zip

 

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.

 


By Value and By Reference

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 
a 200
b 100

Values of a and b after:

a = 200
b = 100
test(a, b)

a 1
b 100
 

                        

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

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission