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 #include /******************** * 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<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<UserPort->mp_SigBit | 1L<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<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.