1 DEVICES 1.1 INTRODUCTION The Amiga's operating system must be able to handle input events from the keyboard or gameports, monitoring the disk drivers, sending data to the parallel device and communicating with the serial port, playing stereo sound with its audio chips, etc... If this was not enough, all these things have to be monitored in a multitasking environment where several tasks (programs) may run simultaneously. As you understand this is a very complicated system that controls all these things. Despite the complexity it must also be very fast so as little processing time as possible is used. The part of the Amiga which takes care of all these routines is usually referred as "Exec". Exec handles tasks and task- switching, interrupts, messages and controls all Amiga's "devices". In this chapter we will look at how you can, with help of Exec, control the devices. A device is a collection of special routines which handles things like disk drivers, serial and parallel ports, input events, sound generating etc... Although all these routines are very different they are all handled in almost the same manner. This means that once you know how to control one of the devices, you do not need to read much more before you can control all of them. 1.2 REQUESTS When you want to do something with a device you fill a "request block" with your requirements and send it to Exec. Exec will now take care of everything. Once it has, successfully or not, completed your request it sends a message to you which tells you that your request has been completed. 1.2.1 IOREQUEST The "request block" is actually an IORequest structure defined in header file "exec/io.h". It looks like this: struct IORequest { struct Message io_Message; struct Device *io_Device; struct Unit *io_Unit; UWORD io_Command; UBYTE io_Flags; BYTE io_Error; }; io_Message: The top part of the request block consists of a Message structure. It is used by Exec when the request is sent around inside the Amiga. It is also used to queue requests at a device's port. You do not need to bother very much about this part since Exec takes care of most things. You must however initialize it correctly before you may start to use the request. io_Device: Pointer to the device which this request has been prepared for. Se below for more information about how to initialize a request block. io_Unit: Some devices, the trackdisk device for example, consists of several units (df0:, df1, and so on), and this part is used to identify which unit should be affected. io_Command: It is here you set your command. There exist both standard commands which are handle by most devices, but there exist also a lot of special unique commands for some devices. See below for more information about available commands. io_Flags: This field is used to set special flags. io_Error: When the request has been completed you can look at this field to see if your request was successfully or not completed. 1.2.2 IOSTDREQ The IORequest structure is the shortest request block you may use, and consists of only the most important fields. Normally you need to use a request block which is a bit larger. Since it is the most commonly used request block, it is called the "standard request block" (IOStdReq) and is defined like this: (Also declared in the header file "exec/io.h") struct IOStdReq { struct Message io_Message; struct Device *io_Device; struct Unit *io_Unit; UWORD io_Command; UBYTE io_Flags; BYTE io_Error; ULONG io_Actual; ULONG io_Length; APTR io_Data; ULONG io_Offset; }; As you can see the top part is identical to the IORequest structure, but there have been four extra fields added. io_Actual: When your request has been complete you can look here to find the actual number of bytes transferred. If this value is not the same as the number of bytes you wanted to transfer you know something went wrong. io_Length: The number of bytes you want to transfer. If you want to continue to send/retrieve information until some special external event, set this field to -1. io_Data: If you are going to send (write) data, this should be a pointer to the data that should be sent. If you want to retrieve (read) data this should be a pointer to a memory buffer were all data which is collected should be store. io_Offset: Special offset value used by some devices. To make life simpler there exist a special function which will automatically allocate and preinitialize the request block. The function is called CreateStdIO(). However, before you may use this function you have to create a message port to which the device can communicate with you. Simply use the CreatePort() function which is described in the "System" manual, chapter "Messages". CreatePort() allocates and initializes a MsgPort structure. Normally the message port should only be used by you and the device, so you do not need to give it any name. The priority should usually be set to 0 (normal priority). Synopsis: msg_port = CreatePort( name, pri ); msg_port: (struct MsgPort *) Pointer to the new MsgPort structure, or NULL if something went wrong. name: (char *) Pointer to a string containing the name of the message port, or NULL. When working with devices you normally do not need to use any name. msgp: (struct MsgPort *) Pointer to the MsgPort structure that should be allocated. pri: (BYTE) This message port's priority. Usually set to 0 (normal priority). Once you have successfully created the reply port you can allocate and initialize the standard request block with help of the CreateStdIO() function. Give it a pointer to the reply port as the only parameter. Synopsis: std_req = CreateStdIO( msg_port ); std_req: (struct IORequest *) Pointer to the new standard request block (struct IOStdReq *), or NULL if the request block could not be created. msg_port: (struct MsgPort *) Pointer to the message port the device should use to communicate with you. When your program terminates you must delete all request blocks you have created. Use the DeleteStdIO() function. Synopsis: DeleteStdIO( std_req ); std_req: (struct IOStdReq *) Pointer to the standard request block you want to delete. All message ports must also be deleted, but remember to first deallocate the standard request block to which the message port is connected to, before you delete the message port itself. Synopsis: DeletePort( msg_port ); msg_port: (struct MsgPort *) Pointer to the MsgPort structure that should be deallocated. Here is a short example: /* Pointer to the standard request block: */ struct IOStdReq *std_req; /* Pointer to the message (reply) port: */ struct MsgPort *msg_port; /* 1. Create a message port so the device can */ /* communicate with us: (No name, normal priority) */ msg_port = CreatePort( NULL, 0 ); if( !msg_port ) clean_up( "ERROR! Could not create message port!" ); /* 2. Allocate and initialize a new standard request block. */ /* It should use our new message port as reply port: */ std_req = CreateStdIO( msg_port ); if( !std_req ) clean_up( "ERROR! Could not allocate the request block!" ); ... /* Deallocate standard request block: */ DeleteStdIO( std_req ); /* Close message port: */ DeletePort( msg_port ); 1.2.3 IOEXTREQ As if the extra IOStdReq structure would not be enough. There exist an even larger request block, usually referred as the "extended request block". This request structure always looks different depending on which device it is used for. It always consists of a IORequest structure, or at least the same fields, at the top, but the rest may vary. See the following chapters for more information about this request block. Since the size of this extended request block varies you can not use the CreateStdIO() function to create and initialize it. Instead you have to use the CreateExtIO() function which will allocate a request block of any specified size. Except of allocating a larger request block it will do exactly the same things as CreateStdIO(). (Remember that you first have to create a message (reply) port as described above.) Synopsis: ext_req = CreateExtIO( msg_port, size ); ext_req: (struct IORequest *) Pointer to the new extended request block, or NULL if the request block could not be created. msg_port: (struct MsgPort *) Pointer to the message port the device should use to communicate with you. size: (long) The number of bytes that should be allocated for the extended request block. Use the function sizeof() to find the exact number of bytes needed. When your program terminates all request block have to be deleted. Extended request blocks are deleted with the special DeleteExtIO() function. Once the request block has been deleted you should also delete the message (reply) port. Synopsis: DeleteExtIO( std_req, size ); std_req: (struct IOStdReq *) Pointer to the extended request block you want to delete. size: (long) The size of the request block, in bytes. Here is a short example: (In this example we will use an extended request block designed for the "serial device".) /* Pointer to the extended request block: */ struct IOExtSer *ext_req; /* Pointer to the message (reply) port: */ struct MsgPort *msg_port; /* 1. Create a message port so the device can */ /* communicate with us: (No name, normal priority) */ msg_port = CreatePort( NULL, 0 ); if( !msg_port ) clean_up( "ERROR! Could not create message port!" ); /* 2. Allocate and initialize a new extended request block. */ /* It should use our new message port as reply port, and */ /* have the same size as an IOExtSer structure: */ ext_req = CreateExtIO( msg_port, sizeof( struct IOExtSer ) ); if( !ext_req ) clean_up( "ERROR! Could not allocate the request block!" ); ... /* Deallocate extended request block: (Must be the same */ /* size as when allocated!) */ DeleteExtIO( ext_req, sizeof( struct IOExtSer ) ); /* Close message port: */ DeletePort( msg_port ); 1.3 PREPARE THE DEVICE Once you have created a request block you may "open" the device to which the request block should be connected to. Note that some devices only need a standard request block (or just a IORequest structure), but others may need larger extended request blocks. See the following chapters for more information. To open a device simply use the OpenDevice() function. Synopsis: error = OpenDevice( name, unit, req, flags ); error: (long) If OpenDevice() managed to open the device it returns 0, else an error number is returned. name: (char *) Name of the device you want to open. See the following chapters for more information. unit: (long) Which unit you want to open. Some devices does not have several units and this field is then ignored. req: (struct IORequest *) Pointer to a request block. Note that some devices may need a larger extended request blocks. flags: (long) Special flags may sometimes be used here, otherwise this field is ignored. As usual, everything that you open must be closed before the program may terminated. To close a device simply call the CloseDevice() function. After you have closed the device you should delete the request block and then the message port. (Unless you want to reopen the device later on.) Synopsis: CloseDevice( req ); reg: (struct IORequest *) Pointer to the device's request block. 1.4 STANDARD EXEC COMMANDS There exist eight standard commands which most devices understand. If the device is already occupied by some other request, your request is queued on a FIFO basis (First In First Out). For some devices it may happen that the other requests are aborted and your started if your request has higher priority. The eight "standard" commands are: CMD_RESET This command will reset the device. All currently queued requests are removed (aborted) and the device is reinitialized to the default values. CMD_READ This command tells the device to start reading (collecting) data. The "io_Length" field of the request block states how many bytes should be read, and the "io_Actual" field will contain the number of bytes actually transferred. CMD_WRITE This command tells the device to start writing (sending) data. The "io_Length" field of the request block states how many bytes should be written, and the "io_Actual" field will contain the number of bytes actually transferred. CMD_UPDATE This command will update a device in that sense that any values still in the internal buffers are written out. Most times this is automatically done, but this command may be needed when you are working with the "trackdisk device" for example. CMD_CLEAR This command will clear any internal memory buffers. CMD_STOP When this command is sent to a device all communication to and from that device will be temporarily halted. Use the "CMD_START" command to restart the communication. CMD_START This command tells the device to restart the communication which has temporarily been halted by a "CMD_STOP" command. CMD_FLUSH If this command is sent to a device all currently queued requests will be removed (aborted). There exist several more commands, but those are device depending, and may therefore only be used with a certain device. See the following chapters for more information about these special device commands. 1.5 SEND REQUESTS Once you have initialized the request block and "opened" the desired device, which is described in more details in the following chapters, you only have to send the request to Exec and the rest will automatically be done for you. (Isn't that nice! At last you do not need to worry about everything.) When you send requests to a device the commands can either be synchronous or asynchronous. When you send synchronous commands your program is halted until the request has been completed. With asynchronous commands your program will continue to run while the device is executing your request. 1.5.1 SYNCHRONOUS COMMANDS Synchronous commands are very easy to handle. You simply send your request with the DoIO() function. Your program will automatically be put to sleep while the device is executing your command, and once it has finished your program will wake up. Since your program is put to sleep ("task sleep") no processing time is waisted while you are waiting for your request to be completed. Synopsis: error = DoIO( req ); error: (long) DoIO() will return first when the request has been completed or something has failed. If the request was successfully completed zero is returned, else an error number is returned. What error number depends on which device was used. See below for a complete list of standard error commands. req: (struct IORequest *) Pointer to the request you want to have executed. 1.5.2 ASYNCHRONOUS COMMANDS Asynchronous requests are a bit more complicated to handle. Since your program will continue to run while your request is being executed you do not know when it has been completed. Asynchronous requests can be very useful when you do not want to pause the program while waiting. (It would not be a very nice game if it was halted each time the user did not move the joystick for example.) You start asynchronous requests with help of the SendIO() function. Synopsis: SendIO( req ) req: (struct IORequest *) Pointer to the request you want to have executed. After you have issued an asynchronous request there exist three different ways to find out when it has been completed. 1.5.2.1 KEEP ON CHECKING THE REQUEST UNTIL IT HAS BEEN COMPLETED You can use the CheckIO() function which will either return NULL if the request has not been completed, or it will return a pointer to the request if it has been completed. Synopsis: ptr = CheckIO( req ); ptr: (long) CheckIO() will either return NULL if the request have not been completed or it will return a pointer to the request block. req: (struct IORequest *) Pointer to the request you want to check. When a request has been completed a message is sent to a specified reply port. Note that CheckIO() will not remove the message at the reply port. This should be done with the Remove() function. Synopsis: Remove( node ); node: (struct Node *) Pointer to the node that should be removed. 1.5.2.2 WAIT FOR THE REQUEST TO BE COMPLETED You can put your program to sleep and wait for the request to be completed, by calling the WaitIO() function. WaitIO() will automatically remove the message at the reply port. Synopsis: error = WaitIO( req ); error: (long) WaitIO() will return first when the request, that has previously been sent, has been completed or something has failed. If the request was successfully completed zero is returned, else an error number is returned. What error number depends on which device was used. req: (struct IORequest *) Pointer to the request you want to wait for to be completed. Note that the request must have already been sent to the device by either a SendIO() or BeginIO() function call. If the request has already been completed WaitIO() will immediately return. 1.5.2.4 WAIT FOR A REPLY MESSAGE Since a message is sent to the request block's reply port each time the request has been completed you can simply wait for a message to arrive. The simplest way is to use the WaitPort() function which will put your program to sleep while waiting for a message to arrive at the specified port. Note that it will not remove the message. Synopsis: msg = WaitPort( msg_port ); msg: (struct Message *) Pointer to the first message which has arrived. Remember that this function will not remove the message from the queue. Use the Remove() function to do this. msg_port: (struct MsgPort *) Pointer to the message port which you want to monitor. Your program will be put to sleep while waiting, and if no message arrive it will never wake up, so be careful. Instead of just waiting for one request to be completed you can wait fore several ones at the same time. This is extremely useful if your program is handling a lot of requests simultaneously. Now we need to use the multipurpose Wait() function which is described in the "System" manual, chapter "Messages". Synopsis: rsig = Wait( wsig ) rsig: (ULONG) When one of the specified signal bits was received, the task is woken up and Wait() returns the signal bit value that woke it up. wsig: (ULONG) The signals we are waiting for. Wait() will put the task to sleep, and it will only wake up if one of the specified signal bit values is received. 1.5.4 QUICK I/O Although the system with sending requests and messages is very fast it may sometimes not be fast enough. If you want to handle a lot of data extremely fast you can try to use the "Quick mode". When you are using this quick mode Exec tries to skip all routines that are not essential. No messages are sent, and there is no checking etc... The quick mode should only be used when really needed, and it can not always be used. When you want to try to use the quick mode set the IOF_QUICK flag in the "io_Flags" field of the request block. To pass the request to Exec you should not use the DoIO() or SendIO() functions, but instead the low level function BeginIO(). Synopsis: BeginIO( req ) req: (struct IORequest *) Pointer to the request you want to have executed. When you want to use the quick mode you have to use this low level function rather than SendIO() and DoIO(). If we were allowed to use the quick mode the request will be synchronous. (Your program is put to sleep since all computer time is needed to run this fast quick mode.) If we were not allowed to use the quick mode the request will be asynchronous, and is then handled exactly the same as if the request had been sent by a SendIO() call. After you have posted your quick request you must check if you were allowed to use this fast mode or not. If not you have to wait for the request to be completed and remove the reply message yourself. To check if you were allowed quick mode or not simply look at the "io_Flags" field of the request block to see if the flag IOF_QUICK is still there or not. If the flag is still set the request was using the quick mode, and you do not need to do anything more. If the flag is not there any more, you were not allowed quick mode, and thus you have to wait for the asynchronous request to be completed and remove the message yourself. /* Initialize the request block as normal. */ /* The pointer to it is called "ioreq". */ /* ... */ /* Try to use the "quick" mode: */ ioreq->IOSer.io_Flags = IOF_QUICK; /* Do the request: */ BeginIO( ioreq ); /* Check if the flag IOF_QUICK is still there or not: */ if( (ioreq->IOSer.io_Flags & IOF_QUICK) ) { /* OK! The request was using quick mode, which means */ /* that our task was put to sleep while the request was */ /* executed, and the request has now been completed. */ /* Since we are using quick mode there are no message */ /* that should be removed. */ } else { /* Too bad, we were not allowed to use the quick mode. */ /* The request should now be treated as if it was */ /* started with a SendIO() function call. This request */ /* is asynchronous - request returns immediately. */ /* Wait for the request to be completed. WaitIO() will */ /* also automatically remove the message at the reply */ /* port. */ WaitIO( ioreq ); } 1.5.3 ABORT REQUESTS You can abort a previously started request by calling the AbortIO() function. Of course, only asynchronous commands can be aborted, and if the request has already been completed it can not be aborted any more. Synopsis: AbortIO( req ) req: (struct IORequest *) Pointer to the request you want to abort. 1.6 ERRORS No one is perfect, and even the devices may sometimes not be able to do what you tell them to do (usually because you have done something wrong). If something goes fails an error number is returned. Each device has it own set of unique error codes which are described in the following chapters. There exist, however, four error codes that are sent by Exec. (They are all defined in the header file "exec/errors.h") IOERR_OPENFAIL The device (unit) could not be opened. If you receive this message there is normally some other program currently using the device with exclusive mode. IOERR_ABORTED When you abort a previously started request by calling the AbortIO() function, the io_Error filed of that request is set to IOERR_ABORTED. If you find a request block with this flag set, you know that it has been aborted. IOERR_NOCMD You tried to use a command that is not supported by that device. IOERR_BADLENGTH The length of the request was not valid. 1.7 DEVICES There exist a lot of different devices each specialized to do something unique. Some devices are so called "low level devices" and are handling a lot of fresh untranslated raw data which will be modified and later used by other so called "high level devices". In total there exist 12 devices (14 with the new V2.05 operating system) which all will be described in a chapter each. Although the devices are so different, they all are handled in much the same manner - allocate a message port and a request block, open the device and start to send your request, finally close everything and quit. Five devices are not covered in this edition, but will soon be added. All registered members will receive more information about this as soon as the chapters have been completed. 1.7.1 Timer Device The Timer Device can be used if you want to receive a message after a specified time limit. This is one of the so called "low level devices", and is used by many other devices. Although the timer device does not sound very interesting, it can really be useful. 1.7.2 Gameport Device The Gameport Device is a smart little device which reads data from the two gameport connectors on the Amiga. The device can handle both normal Joystick as well as Mouse movements. Even light-pens and other input devices may be connected and monitored by this device. These special input devices must however have its own drivers which translates the signals to for the gameport device understandable form. 1.7.3 Audio Device The Audio Device is used to produce sound. It monitors the four hardware sound channels, and can therefore play the sound in stereo. Everything from a simple beep to complete songs may be played. The audio device can both produce sounds itself or directly play sampled (digitized) sound effects. Sound is an important part of our life and should therefore in my opinion also be used a lot in computer programs. Sound can be used in most situations, from emergency beeps to entertaining melodies. 1.7.4 Narrator Device The Amiga is not only extremely good at producing high quality sound. with help of the Narrator Device it can also speak almost as a human. Although it is not a Frank Sinatra, it is possible to understand what it is saying, and this can be used in many situations. Used with care, it is possible to produce very good sound. Sadly, somehow many persons do not like this unique feature of the Amiga, and very few programs support the narrator device. I believe that artificial speech can be used in many situations, and can really be handy for the user. Most "professional" programs would really benefit from using the narrator device. 1.7.5 Trackdisk Device With help of the Trackdisk Device you can directly access the disk drivers. Data can not only be transferred to and from the disks, but you may even format specific sectors of a disk and do a lot of other low level stuff. 1.7.6 Serial Device The Serial Device is used to transfer data from and to the serial port. It can be used at different speeds (bauds) and use seven or eight bit characters. The device also support parity (error) checking. 1.7.7 Parallel Device The Parallel device is used to transfer data from and to the parallel port. The parallel device is very useful when you want very fast communications, and is therefore often used by sound samplers and video digitizers. However, the most commonly used external device is without question a printer, but you should then use the printer device which has specially been designed for this purpose. 1.7.8 Printer Device The printer device can use both the serial and parallel device depending on to which port the printer is connected to. With the printer device you can not only print normal text, special printer commands may be sent and you can even print graphics with it. The printer device uses the preferences settings to find out to which port the printer is connected, what printer it is, and other special settings like margins, paper length etc... 1.7.9 Keyboard Device The Keyboard Device is a "low level device" which is used by the "Input Device". The keyboard device collects untranslated (raw) key codes. This device is very fast and uses very little computer time. This device will be covered in the next version of the manual. All registered members will receive more information as soon as the next version has been completed. 1.7.10 Input Device The Input Device is a so called "high level device" which receives information from the timer, keyboard and gameport devices, and puts all this information into one single input stream. Very handy and useful device. This device will be covered in the next version of the manual. All registered members will receive more information as soon as the next version has been completed. 1.7.11 Console Device The Console Device enables you to handle input and output as an intelligent terminal for ASCII characters. Works very close with Intuition, and is also a very handy and useful device. This device will be covered in the next version of the manual. All registered members will receive more information as soon as the next version has been completed. 1.7.12 Clipboard Device With the Clipboard Device data can be cut, pasted and copied between different programs. Several clips may be used simultaneously, and the device supports the IFF standard. This device will be covered in the next version of the manual. All registered members will receive more information as soon as the next version has been completed. 1.7.13 SCSI AND PCMCIA DEVICE There exist two new devices. The SCSI device is only available with the new V2.0 operating system, and the PCMCIA device exist for the moment only on Amiga 600. These device will be covered in the next version of the manual. All registered members will receive more information as soon as the next version has been completed.