See also part
two of this series
Last month, we covered the ins
and outs, so to speak, of blocking I/O APIs. API calls to input and output devices
are termed ‘blocking’ if other API calls
are stopped - blocked - by the currently executing API.
For example, let’s suppose you have issued an API
to a communications port that is waiting for a line signal
(DSR, say) to change. Then, on another thread, you issue
another API call to set a control signal (DTR, say).
You’ll find that the second API call will not complete
until the first call returns – even though they
are on different threads.
The reason is that the threading mechanisms at a user
process level are handled differently from the threading
at the driver level. Device drivers do have something
like ‘threads’ but they are much lighter
weight (that is, cheaper and faster to run) than process
level threads. So unless you take explicit action to
tell the driver to ‘thread’, in this circumstance,
you’ll effectively be running a single threaded
I/O application.
Ready to test! Remember to click the Initialize button
before you start.
The techniques – signals and mutexes - used in
non-blocking I/O are not often used in application level
programming, so you may be unfamiliar with them. Also,
in Visual Basic 6, because you were essentially restricted
to one thread, such niceties didn’t apply. But
now, because VB .NET supports threads, it does provide
a synchronisation mechanism at a high level that avoids
having to deal with the operating system synchronisation
primitives. This is the SyncLock
... End SyncLock construct
which uses a mutex to prevent two threads from executing
the same piece of code at the same time. It’s just
that the mutex is hidden by VB .NET.
Now while using non-blocking I/O is useful in high
performance backup programs, say - you can read from
the disk while writing to the tape and save a bit of
time - it’s not vital. But it most certainly is
when you have to deal with a serial port. It’s
no good at all issuing a read operation which never completes
because the far end has disconnected and gone for a beer.
And the signal change that tells you about this interesting
fact is blocked behind the read!
Tales Of The Unexpected This month, we’ll look at reading and writing
from the serial port. Writing should be easy, it’s
almost a ‘fire-and-forget’ operation. Almost.
What we have to consider is the case when the remote
station pulls the plug before the write completes. This
has to be taken into account because serial port writes
are not instantaneous. For example, take a 1000 byte
write down a serial line at 9600 bps. True, that’s
not lightning fast by today’s standards but it’s
still common, and it will take about 1 second. For 1
megabyte, that’s 1000 seconds or about 20 minutes – plenty
of time for something to go wrong.
Try
setting DTR or both ports. You should see DSR detected
on both and just as a precaution, click Check to see
that that is the case.
Equally, we have to consider the situation where the
remote station asks us to stop sending (by whatever mechanism – see
Controlling The Flow) and forgets to turn it on again.
We want the write to complete with some sensible error
code and not just disappear into the void. As a matter
of general programming practice, by the way, it’s
important to consider all these eventualities. It takes
a lot of effort to produce a program that does something
sensible when confronted with the unexpected.
So the basic write operation would look something like
this:
Public Sub Write( ByRef buf() As Byte)
Dim bytesWritten
As Integer
WriteFile(h, buf, buf.Length, bytesWritten,
0)
End Sub
The first parameter is the file’s handle, returned
when the file is opened by the CreateFile API, the second
is the address of the byte array containing the data
to be written, the third parameter is the length in bytes
and the last parameter is an OVERLAPPED structure, similar
to that which we discussed last month.
Now, a functioning Write routine has a couple more
wrinkles in it. To start with, we want to write something
like this from the Form:
cc1.Write("hello, world")
Here, “hello, world” is a string, which
in VB .NET is composed of Chars, each Char being a Unicode
character of 16 bits. On the other hand, the Write API
deals in good old-fashioned 8-bit bytes. So there needs
to be a bit of conversion code:
Dim b(buf.Length - 1) As Byte
For i = 0 To buf.Length
- 1
b(i) = Asc(buf.Chars(i))
Next
Actually, there is a better way to do this using the
Encoding class which is used for converting strings into
bytes, but the above technique is simple and clear.
Now we can issue the Write itself:
r = WriteFile(h, b, b.Length, bytesWritten, o)
If r <> 1
Then
r = GetLastError()
If r <> ERROR_IO_PENDING
Then
Throw New System.Exception("Write failed")
End If
End If
As we did last month, for an overlapped or asynchronous
I/O, we have to check not for a ‘completed’ result,
but for an IO_PENDING return code.
It
really is essential to pin everything that can be moved
by the .NET garbage collector. Not pinning the readBuffer
caused intermittent crashes.
The amount of data written to the port is returned
in bytesWritten and since this needs to
be around when the write completes, it first of all must
be in scope – that is, it must exist – and
it won’t if it’s declared in the Write procedure
itself. And secondly, it must be protected from the .NET
garbage collector by pinning it. It’s therefore
declared in the class data and pinned in the object initialization
code, in New:
GCHandle.Alloc(bytesWritten, GCHandleType.Pinned)
There’s one final piece necessary for a working
Write routine. Consider what would happen if more than
one write were to be issued at a time. There’s
nothing in principle wrong with that, but it’s
probably not what you intend to do. Having multiple write
requests in progress also makes debugging a bit tricky
... which one went wrong?
So there’s a Boolean flag write_in_progress that
acts as a protection against having more than one write
going at the same time. The flag is set by the Write
routine itself and cleared when the write completes.
But even that has a synchronization wrinkle. Whenever
a variable needs to be set and read from more than one
thread, you must make sure that the variable is properly
protected against ‘race-conditions’. You
do this by using a SyncLock
... End SyncLock block -
and, in the Write routine, the whole block of code has
been protected. In fact, this is overkill, since the ‘eventThread’ code
only ever sets the write_in_progress flag. Still, it
doesn’t cost very much to do and it’s good
practice anyway.
Turning to the eventThread code, it turns out that
this doesn’t need much modification at all. Only
two modifications are required: SyncLocking the write_in_progress flag (as we’ve discussed) and adding a new event
to the event mask for the transmit completion. The communications
event, EV_TXCOMPLETE, must be ORed into the event mask
set up in the OpenWithEvents code. If you forget to do
this, you’ll get an obscure looking error, ‘Invalid
Parameter’ (return code 87), when WaitCommEvent
returns in the eventThread code. Which is a good thing – sort
of. Instead of ignoring the event, if you forget to set
it in the mask, Windows does return an error. But why ‘Invalid
Parameter’?
Reading The Runes In one sense, reading isn’t anything like as
simple as writing. For one thing, when you write a stream
of characters to a serial port, it’s under your
control (more or less, anyway). But reading a serial
port is a whole different ball game. The number of characters
that arrive and the speed at which they arrive are not
under your control. The line speed sets a maximum rate
at which the characters can arrive; but whether they
arrive in ones, twos or the-whole-shooting-match-without-a-pause-for-breath
is most certainly not under your control.
There are a number of ways of dealing with this, and
I’ll just present one here. It may not be the most
efficient, but it works. Well, most of the time.
The Read routine itself doesn’t issue a ReadFile
API. Instead, it ‘drains’ a buffer of already
read characters. These characters get into the buffer
courtesy of the ‘eventThread’ code. Both
the Read routine and the eventThread code share access
to a ‘readBuffer’ – an array of Bytes – and
a readBufferPosition giving the next free slot in the
readBuffer array. When a new byte is read from the serial
port this is where it will be put.
There’s clearly a synchronization issue here:
it’s essential that access to the readBuffer and
the readBufferPosition be controlled between the two
routines. So, in the Read code, we have this:
Dim c As Char()
SyncLock GetType( CommControl)
ReDim c(readBufferPosition)
For i = 0 To readBufferPosition
c(i) = ChrW(readBuffer(i))
Next
readBufferPosition = 0
End SyncLock
The array of Chars, c, is re-dimensioned to the current
number of bytes in the readBuffer. Then each byte is
read from the buffer and converted to a Char ready for
conversion into a String later on. Lastly, the pointer
is reset to zero – in other words the buffer is
drained and left empty.
Note that the read does not complete in one go! The
read buffer is empty at one point due to the speed of
the transmit operation and so you get two RX events.
Now the other half of the show is in eventThread:
SyncLock GetType( CommControl)
If evtMask And EV_TXEMPTY
Then
write_in_progress = False
ElseIf evtMask
And EV_RXCHAR Then
Do
r = ReadFile(h,readBuffer(readBufferPosition),1,bytesRead,Nothing)
If r <> 1 Then
r = GetLastError()
Throw New System.Exception("ResetEvent
failed")
End If
If bytesRead <> 1 Then
Exit Do
Else
readBufferPosition += 1
End If
Loop
End If
End SyncLock
In addition to the EV_TXEMPTY event, there’s
code which handles the EV_RXCHAR event. Within a Do
... Loop construct, a synchronous ReadFile for a single character
is issued and if a character was found (given by the
bytesRead parameter), the ReadFile is issued again. Only
when no more characters are received is the loop exited.
You absolutely must use SyncLock statements to ensure
correct operation when you are using event driven multithreaded
code.
Now in the main, it’s pretty much complete, though
there’s still quite a lot of detail to see to:
there’s no checking on read buffer overflow, no
proper timeout control, it’s not very efficient
in reading data from the port, no handling of communication
line errors, the control doesn’t go into the right
control area on the form…
…and I still haven’t got round to doing
some decent error handling!
Why not use a parallel port?
With a serial port, you get a measly two output control
lines and four input lines. Plus of course the two serial
data lines themselves. But with a parallel port there
are 25 lines – 8 data, and a good number of the
rest can be used as bi-directional control lines. So
it would seem that the parallel port is the better choice
if you want to control, say, a stepper motor with typically
four control lines (now that’s an interesting project!).
Indeed, a parallel port is a lot better for doing that
that sort of thing than a serial port. But there are
one or two ‘gotchas’ in there. First, Windows
likes to think that parallel ports are used for parallel
port type of things – such as printing. It can
be mighty hard to persuade Windows that you really, really
don’t want to print: plug-and-play can quickly
turn into ‘plug-and-pain’.
The second problem is that to control a mechanical
device, you need access to the parallel port’s
registers in order to turn a bit on or off. This means
that you need a device driver that will let you do it
(not a printer driver!). You cannot (as you could in
DOS and earlier versions of Windows) just poke the port.
In Windows XP, that’s an absolute no-no.
The bad news is that standard Windows doesn’t
come with such a driver. The good news is that there
are several shareware and free drivers around, including
one that comes with the Microsoft device driver kit.
However, building and configuring the generic port driver,
as it’s called, isn’t a task for the faint
of heart.
On the other hand, there’s an advantage to using
a serial ports – they are more robust. Because
serial communications existed a long time before current
microprocessors, the signal levels are higher and the
chips are built to withstand more abuse. While you can’t
connect a serial port to the mains and hope to use it
afterwards, they tend to be a lot more forgiving of bad
wiring than parallel ports.
The
parallel port has many more data and control lines than
a serial port. But you need a special device driver to
communicate with it.
Why use one method when three will do?
One of the unexplained things about serial communications
is the way flow control works. It turns out that there
are three ways of doing it: there doesn’t seem
to be any good reason for this – it just happened
that way.
The first and most basic level is to detect if the
remote device is there or not. You do this by looking
for DSR and conversely you tell the other party that
you are in a fit state by setting DTR. You can use these
signals for flow control, but it’s more common
to use a similar pair, CTS and RTS to indicate that one
side should stop sending.
If you set RTS (‘assert’ in the jargon)
you are telling the remote device that you are ready
to receive data. The other member of the pair, CTS is
set by the remote end (at its end, it is the RTS line).
If you see that, the other end is ready to receive data.
So far, so good. But there’s a third mechanism
used in software, called XON/XOFF. If you send an XOFF
character (ASCII 19), the remote end should stop sending.
When you’re ready to start receiving again, you
send an XON character (ASCII 17).
Serial
port flow control can be done in three ways. In software
with XON/XOFF and in hardware with either RTS/CTS or
DTR/DSR ‘handshake’ pairs.
Timeouts For Serial Ports Can Be Confusing.
You may have noticed, but there’s something I’ve
not mentioned about the ReadFile in the eventThread code.
That is, it completes immediately even when there are
no characters in the serial port’s input buffer.
That’s because I inserted a SetCommTimout API
in the OpenWithEvents routine:
Dim cto As COMMTIMEOUTS
cto.ReadIntervalTimeout = INFINITE
cto.ReadTotalTimeoutConstant
= 0
cto.ReadTotalTimeoutMultiplier = 0
cto.WriteTotalTimeoutConstant
= 0
cto.WriteTotalTimeoutMultiplier = 0
r = SetCommTimeouts(h,
cto)
This uses a COMMTIMEOUTS structure – a simple
structure with five integer fields. Setting these fields
to various values allows you to control the timeout – that
is the amount of time that elapses when nothing is happening
before the API (ReadFile or WriteFile) returns.
A common configuration is that given above. Here, with
ReadIntervalTimeout set to –1 or &HFFFFFFFF,
and the other two read timeouts set to zero, the ReadFile API will return immediately even if there are no characters
to be read. If you don’t set these, the API will
return with an IO_PENDING status.
The ReadIntervalTimeout parameter determines the allowed
interval between any two characters being read. Zero
doesn’t mean return immediately, but rather don’t
timeout at all!
The multiplier settings just calculate the timeouts
by multiplying this with the number of characters to
be read.
Communication
port timeouts – the interval that
can elapse between characters before the operation is ‘timed
out’ by Windows) is set using the SetCommTimouts
API.
October 2005 |