home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
The Fred Fish Collection 1.5
/
ffcollection-1-5-1992-11.iso
/
ff_disks
/
400-499
/
ff429.lzh
/
Timer
/
Article
< prev
next >
Wrap
Text File
|
1991-01-10
|
15KB
|
349 lines
WAIT A MINUTE!
© Copyright 1990 by Timm Martin
In the good ol' days, all a programmer had to do to wait within a program was
to create a simple for-loop and experiment with the test variable until the
program waited the desired length of time. With the ever-increasing Amiga
technology, however, one can no longer rely on the program being run on a
7.16MHz 68000 Amiga. Amigas can now come equipped with any one of the 680x0
processors in the Motorola family, from the 7.16Mhz 68000 to the soon-to-be-
released 50MHz 68040. In addition, a math coprocessor or memory management
unit--both standard on the new Amiga 3000--will affect a program's speed.
The answer to this dilemma is to use one of the handiest built-in Amiga
facilities--the timer device. The simplest way to access the timer device is
to use the dos.library Delay() function. The format of that function is:
void Delay( long ticks );
where ticks is the number of "ticks" to wait. A tick is an AmigaDOS unit of
time that occurs fifty times per second. So, for example, if you wanted to
wait for two seconds, you could specify
Delay( 100L );
A more reliable method is to use the TICKS_PER_SECOND definition in the
libraries/dos.h include file:
Delay( 2 * TICKS_PER_SECOND );
There are a few problems inherent with the Delay() function, however. The
most obvious problem is that the time resolution is limited to the duration
of a tick. In other words, the shortest time you can wait is one fiftieth of
a second. Another problem is that the wait is synchronous, meaning your
program has to sit idle for the specified amount of time.
The timer device solves both of these problems. The following code is a
self-contained module that you can place in its own source file, compile, and
link with your program:
#include <devices/timer.h>
#include <exec/types.h>
/********************
* SHARED VARIABLES
*********************/
long timer_error = 1;
struct timerequest timer_req;
struct MsgPort * timer_port = NULL;
/***************
* TIMER CLOSE
****************/
/*
This function closes the timer device and deletes the timer port.
*/
void timer_close( void )
{
if (!timer_error)
{
CloseDevice( (struct IORequest *)&timer_req );
timer_error = NULL;
}
if (timer_port)
{
DeletePort( timer_port );
timer_port = NULL;
}
}
/**************
* TIMER OPEN
***************/
/*
This function opens the timer device and initializes it.
*/
BOOL timer_open( void )
{
if (!(timer_port = CreatePort( NULL, 0L )) ||
(timer_error = OpenDevice( TIMERNAME, UNIT_VBLANK,
(struct IORequest *)&timer_req, NULL )))
return (0);
timer_req.tr_node.io_Message.mn_ReplyPort = timer_port;
timer_req.tr_node.io_Command = TR_ADDREQUEST;
timer_req.tr_node.io_Flags = 0;
return (1);
}
/**************
* TIMER WAIT
***************/
/*
This function waits for the specified number of microseconds.
*/
#define MICROS_PER_SEC 1000000L
void timer_wait( long micros )
{
long secs;
/* a bug in Kickstart v1.3 requires this check */
if (micros < 2) return;
secs = micros / MICROS_PER_SEC;
micros %= MICROS_PER_SEC;
timer_req.tr_time.tv_secs = secs;
timer_req.tr_time.tv_micro = micros;
SendIO( &timer_req.tr_node );
/* wait until time is up */
Wait( 1L<<timer_port->mp_SigBit );
GetMsg( timer_port );
}
You should call the timer_open() function during program startup when you
open other things such as libraries, windows, devices, etc. The first thing
this function does is create a message port so it can communicate with the
timer device. A message port is analogous to a real-world mailbox; it's a
depository for messages--in this case reply messages--from the timer device.
Your program will send a message to the timer device telling it to notify you
in a certain amount of time. When that time has elapsed, the timer device
will reply to your message, placing the reply in the message port you created.
The CreatePort() function allows you to easily create message ports. This
function is not located in ROM, but rather in the amiga.lib (for Lattice C
users) or c.lib (for Manx C users) link-time libraries. This function
allocates memory for the message port, allocates a signal bit to notify your
program when a message has been received, and properly initializes the
message port. You pass it the name of the port you want to create and the
priority of the port. Since you will not be looking for this port with the
FindPort() function, you can set the name of the port to NULL, and the system
will not add the port to the public message port list. The priority is a
value between -128 and +127 that represents the importance of messages
received in this port. Unless you are creating multiple ports and expect
contention between the ports, you can safely set this value to zero. A
pointer to the new message port is returned or NULL if the CreatePort()
function fails.
The next step is to open the timer device for use by your program. The
OpenDevice() function requires four arguments: the name of the device, the
unit number, a pointer to a timerequest structure, and a flags value. The
name of the timer device is always the same and is defined in the devices/
timer.h include file as TIMERNAME.
The unit number can either be UNIT_VBLANK or UNIT_MICROHZ as defined in
devices/timer.h. The MICROHZ timer has a very high resolution of one
microsecond, meaning it updates its counter one million times per second. As
you would expect, this requires quite a lot of overhead to maintain, and the
MICROHZ timer can fall behind when the system is busy with other tasks.
Therefore, it should only be used for short, critical time measurements. The
VBLANK timer, on the other hand, is better suited for most applications. It
updates its counter sixty times per second, meaning it is accurate to within
.0167 seconds (which is close to the resolution of the Delay() function) and
requires very little overhead. The VBLANK timer maintains its accuracy
regardless of how busy the system is.
The next argument is a pointer to a timerequest structure that you have
allocated. The structure is defined as follows:
struct timerequest
{
struct IORequest tr_node;
struct timeval tr_time;
};
The tr_node member is a standard IORequest structure used to communicate with
Amiga devices. It contains information such as a Message structure for
communicating with the device, a pointer to the Device and Unit structures,
etc. Most of this information is used internally by Exec. You just need to
allocate memory for the timerequest structure and pass its address, and the
OpenDevice() function will do the rest.
The final argument for the OpenDevice() function is the Flags variable. This
is not used by the timer device and should be set to zero.
If the OpenDevice() function fails, it will return a non-zero error number.
Notice that the way the timer_open() function is written, if either the
CreatePort() or OpenDevice() function fails, the function will return 0, in
which case your program should act accordingly. Accessing the timer device
without having successfully opened it will surely cause problems.
Assuming everything opened OK, a few entries in the timerequest structure
need to be initialized. First you need to set the mn_ReplyPort pointer in
the Message structure of the timerequest to the timer_port you created. This
tells the timer device where to reply to messages you send it. Next, you
need to specify what you want the timer device to do. (In addition to simple
time counting, you can ask the timer device what time it is or even set the
system time). The TR_ADDREQUEST command indicates that you will be asking
the timer device to count time. Finally, setting the io_Flags value to zero
indicates that you want normal I/O as opposed to Quick I/O. With Quick I/O,
the device will respond to your request immediately. This is handy for a
device such as the serial device in which your program may not want to wait
for the device to service your request before responding. But in this case,
the goal is for the timer device to notify you after the specified time has
elapsed.
While we are on the subject of opening the timer device, let's discuss
closing it. Your program should call the timer_close() function during
program shutdown when you close other things such as libraries, windows, etc.
Notice the timer_error, timer_port, and timer_req variables are global to all
three functions, allowing each function to access them. These variables act
as their own "flags," indicating when the corresponding device or port has
been opened. For example, if the timer_error variable is zero, then the
timer device has been opened and is consequently closed by the timer_close()
function. Also, if the timer_port variable is not NULL, then the timer
message port has been created and is freed by timer_close(). The variables
are initialized globally and then reset in the timer_close() function so that
it is safe to call timer_close() even if timer_open() was never called. This
is handy in case your program terminates prematurely before calling
timer_open() (for example, because you could not open a library).
Once you have opened the timer device, you can use the timer_wait() function
to wait for a specified number of microseconds. For example:
timer_wait( 1000000L ); /* wait one second */
timer_wait( 500000L ); /* wait one-half second */
timer_wait( 5000000L ); /* wait five seconds */
The first line in the timer_wait() function checks to make sure micros is not
less than 2 microseconds. A bug in Kickstart v1.3 or earlier will cause the
system to crash if you specify 0 or 1 microseconds. The next two lines
break the microsecond value up into its seconds and microseconds components.
The next two lines fill in the amount of time you want to wait. Even if the
time was the same for each request, you must reinitialize it because the
timer device destroys your old values (it actually uses the tv_secs and
tv_micro structure members to count down your time). The SendIO() function
then sends your time request to the timer device. By using SendIO() instead
of DoIO(), your program will continue executing even though the timer device
has not yet responded to your request (allowing you to perform an
asynchronous wait if desired).
The next step is to wait until you receive a signal from the timer device
that the specified amount of time has elapsed. When the timer device
finishes counting the time, it will reply to your time request by sending a
message to the timer_port you created earlier. When it places the message in
your timer port, Exec will set the signal bit associated with that port.
Hence, you Wait() for that signal bit to be set. There is no need to then
ReplyMsg() since the timer device was replying to your original message. The
GetMsg() function then removes the message from the timer port.
As you may have guessed, the timer_wait() function is similar to the Delay()
function in that it is a synchronous wait--the program halts until the
specified amount of time has elapsed. (Note that this is NOT a busy wait.
Your program sleeps while in the Wait() function.) Using similar code,
however, it is easy to create an asynchronous wait.
Suppose you want to create a clock that sits in a small window on the
Workbench screen. In addition to having the timer device notify you once
each second so you can update the time, you would also want to monitor for
the user clicking on the close gadget in the window to end the program. You
can create a function that is identical to timer_wait() except that it does
not wait for the time to count down:
/***************
* TIMER START
****************/
/*
This function issues a request to the timer device to notify the program
in the specified number of microseconds. This function does not wait for
a reply from the timer device.
*/
void timer_start( long micros )
{
long secs;
/* a bug in Kickstart v1.3 requires this check */
if (micros < 2) return;
secs = micros / MICROS_PER_SEC;
micros %= MICROS_PER_SEC;
timer_req.tr_time.tv_secs = secs;
timer_req.tr_time.tv_micro = micros;
SendIO( &timer_req.tr_node );
}
You could then wait for both window input and the timer device signal in the
same Wait() call:
struct Window *window;
struct IntuiMessage *imessage;
Wait( 1L<<window->UserPort->mp_SigBit | 1L<<timer_port->mp_SigBit );
while (imessage = (struct IntuiMessage *)GetMsg( window->UserPort ))
{
/* handle the window input */
ReplyMsg( (struct Message *)imessage );
}
if (GetMsg( timer_port ))
/* time is up! */
A word of caution here--never use a timerequest structure that is currently
being serviced by the timer device. In other words, don't send a new time
request until the previous time request is complete (though you could create
multiple time requests by allocating multiple timerequest structures). If
for some reason you want to cancel a time request (so you could issue
another, for example), you need to use the AbortIO() function:
/***************
* TIMER ABORT
****************/
/*
This function cancels an existing time request.
*/
void timer_abort( void )
{
AbortIO( &timer_req.tr_node );
Wait( 1L<<timer_port->mp_SigBit );
GetMsg( timer_port );
}
The AbortIO() function will force the timer device to respond to your request
immediately whether or not the requested time has elapsed. The Wait()
function then clears the signal bit, and GetMsg() removes the reply message
for the aborted request. It is safe to call AbortIO() even if the time
request may have been satisfied. However, there MUST be a pending time
request (satisfied or not) or the system will crash.
As you can see, using the timer device is not only easy but has many
advantages over the old for-loop method: 1) it's not a "busy" wait, 2) it
can be asynchronous, and 3) you are guaranteed of waiting an exact amount of
time regardless of the hardware conditions.
REFERENCES
Commodore-Amiga, Inc., Amiga ROM Kernal Manual: Libraries and Devices,
Addison-Wesley Publishing Company, Inc., New York, 1989, pp. 289-98, 871-82.
Mortimore, Eugene P., Amiga Programmer's Handbook: Volume II, SYBEX, Inc.,
San Francisco, 1987, pp. 305-20.