[Skip top navbar]

Andrew Gregory's Web Pages

Rocky outcrop at sunset, Approx 27°56'15"S 119°38'17"E


Serial I/O

Before you get started here, make sure you are familiar with the usage of the IOxxx functions.

This tutorial requires the use of the Application Manager modules (either Asynchronous or Synchronous, the I/O Event Manager module, and the RS-232 Utility module.


Allocate a buffer

You'll need to create a sizeable buffer - whatever your biggest 'chunk' of comms is going to be. Use a GLOBAL variable to keep the memory handle. NOTE: In the examples below there is a global variable called commlen% - it is used by the I/O routines to tell how much serial data has been received. It is set to the size of the buffer just before each I/O request and checked when each request is handled. More information is given under Implement an I/O handler.

... GLOBAL commbuf%, commlen% ... commbuf% = ALLOC( 1024 ) ...

Open the serial port

I would suggest using the IOxxx functions for this. I like to keep the LOPEN and related functions for emergencies like debugging logs, etc.

The $100 options flags work for me. I can send and receive binary data just fine.

... IOOPEN( shand%, "TTY:A", $100 ) ...

Configure the serial port

After the serial port is open, use my rsset: procedure. See the source code for a full explanation of the parameters. Use the term& bitmap carefully. You can improve responsiveness by asking for a large chunk of data (say 1024 bytes), but setting the terminator so that as soon as a control-character is received (such as a carriage-return), your comms request will complete and you can process the received data. Character-by-character reception will be very slow.

An alternative would be to use the IOCANCEL command. If you ask for a large chunk of data, it could be a long time before that request completes. The solution? Setup a timeout (say, one second) using the "TIM:" device and call IOCANCEL when it expires. The comms request will complete immediately and you will be able to read whatever has been received so far. The IOCANCEL command does not discard any received data!

As an example, the NMEA protocol used by many GPS receivers always terminates each message with a carriage return. The terminating bitmap for just a carriage return is &00002000. Using that terminator means that your I/O handler will be called only when a complete message has been received (assuming the comms buffer is big enough). Infrequent calls to your I/O handler are more efficient and OPL-friendly.

... rsset:( shand%, rsbaud%:( "19200" ), 0, 8, 1, 4, &00002000 ) ...

Register an I/O handler

Now that the serial port is configured, a procedure to handle the I/O needs to be registered.

... sslot% = ioAdd%:( "hdlcomm", 127 ) :REM create a new I/O handler ioShand:( sslot%, shand% ) :REM let the I/O manager know about the I/O handle ...

sslot% identifies a unique number for this I/O handler.

In this case, the OPL procedure called hdlcomm will be called whenever each I/O request you make completes. See Implement an I/O handler below.

Also, we only need to keep the I/O slot number as a global variable. It is not necessary to keep the I/O device handle as a global as we can get to it via the slot number: ioGhand%:( sslot% ).

Make a comms request

Your newly registered handler won't be doing anything unless you make a request!

... REM set the maximum amount of data that can be received at once commlen% = LENALLOC( commbuf% ) REM make the request IOC( ioGhand%:( sslot% ), 1, #ioGstat%:( sslot% ), #commbuf%, commlen% ) ...

When the IOC completes, commlen% will be set to the amount of data actually received.


In summary, the initialisation procedure may look like:

PROC main: GLOBAL commbuf%, commlen%, sslot% ... REM rest of application setup, etc here ... ENDP REM Hey!!! There's error checking here!!! :-) PROC init%: LOCAL shand% REM allocate a buffer commbuf% = ALLOC( 1024 ) IF commbuf% = 0 RETURN -1 ENDIF REM open the serial port IF IOOPEN( shand%, "TTY:A", $100 ) < 0 RETURN -2 ENDIF REM configure the serial port rsset:( shand%, rsbaud%:( "19200" ), 0, 8, 1, 4, &00002000 ) REM register an I/O handler sslot% = ioAdd%:( "hdlcomm", 127 ) :REM create a new I/O handler IF sslot% = 0 IOCLOSE( shand% ) FREEALLOC commbuf% RETURN -3 ENDIF ioShand:( sslot%, shand% ) :REM let the I/O manager know about the I/O handle REM make a comms request commlen% = LENALLOC( commbuf% ) IOC( ioGhand%:( sslot% ), 1, #ioGstat%:( sslot% ), #commbuf%, commlen% ) REM done RETURN 0 ENDP

Implement an I/O handler

This is the procedure that will be called every time your I/O request completes. It returns an integer value (which happens to be ignored!) and accepts a single integer parameter - the I/O slot value you created before. The slot value is useful if you decide to have your procedure handle two different I/O requests - say serial comms and a timeout. The slot value can be used to distinguish the two.

IMPORTANT NOTE: It is vital that when you've handled the I/O, you must make a new I/O request, otherwise your handler will never be called again!

PROC hdlcomm%:( slot% ) LOCAL i% REM The comms data is now in the buffer (commbuf%). Do something with it... REM This will be slow, but it is *something*... i% = 0 WHILE i% < commlen% PRINT CHR$( PEEKB( UADD( commbuf%, i% ) ) ); i% = i% + 1 ENDWH REM make a new request - VERY IMPORTANT commlen% = LENALLOC( commbuf% ) IOC( ioGhand%:( slot% ), 1, #ioGstat%:( slot% ), #commbuf%, commlen% ) ENDP

Clean up

When you're finished with the I/O stuff, be a good citizen and clean up after yourself...

... REM cancel the outstanding request IOCANCEL( ioGhand%:( sslot% ) ) IOWAITSTAT #ioGstat%:( sslot% ) REM close the serial port IOCLOSE( ioGhand%:( sslot% ) ) REM un-register the handler ioRem%:( sslot% ) REM free up the comms buffer FREEALLOC commbuf% ...