Copyright (c) Hyperion Entertainment and contributors.
Exec Device I/O
Contents
Exec Device I/O
The Amiga system devices are software engines that provide access to the Amiga hardware. Through these devices, a programmer can operate a modem, spin a disk drive motor, time an event, and blast a trumpet sound in stereo. Yet, for all that variety, the programmer uses each device in the same manner.
What is a Device?
An Amiga device is a software module that accepts commands and data and performs I/O operations based on the commands it receives. In most cases, it interacts with either internal or external hardware, (the exceptions are the clipboard device and ramdrive device which simply use memory). Generally, an Amiga device runs as a separate task which is capable of processing your commands while your application attends to other things.
Amiga System Devices | ||
---|---|---|
Audio | audio.device | Controls the use of the audio hardware |
Clipboard | clipboard.device | Manages the cutting and pasting of common data blocks |
Console | console.device | Provides the text-oriented user interface. |
Gameport | gameport.device | Controls the two mouse/joystick ports. |
Input | input.device | Processes input from the gameport and keyboard devices. |
Keyboard | keyboard.device | Controls the keyboard. |
Narrator | narrator.device | Produces the Amiga synthesized speech. |
Parallel | parallel.device | Controls the parallel port. |
Printer | printer.device | Converts a standard set of printer control codes to printer specific codes. |
SCSI | scsi.device | Controls the Small Computer Standard Interface hardware. |
Serial | serial.device | Controls the serial port. |
Timer | timer.device | Provides timing functions to measure time intervals and send interrupts. |
Trackdisk | trackdisk.device | Controls the Amiga floppy disk drives. |
The philosophy behind the devices is that I/O operations should be consistent and uniform. You print a file in the same manner as you play an audio sample, i.e., you send the device in question a WRITE command and the address of the buffer holding the data you wish to write.
The result is that the interface presented to the programmer is essentially device independent and accessible from any computer language. This greatly expands the power the Amiga brings to the programmer and, ultimately, to the user.
Devices support two types of commands: Exec standard commands like READ and WRITE, and device specific commands like the trackdisk device MOTOR command which controls the floppy drive motor, and the keyboard device READMATRIX command which returns the state of each key on the keyboard. You should keep in mind, however, that supporting standard commands does not mean that all devices execute them in exactly the same manner.
Consult the Amiga devices and the commands they support.
Accessing a Device
Accessing a device requires obtaining a message port, allocating memory for a specialized message packet called an I/O request, setting a pointer to the message port in the I/O request, and finally, establishing the link to the device itself by opening it. An example of how to do this will be provided later in this section.
Creating a Message Port
When a device completes the command in a message, it will return the message to the message port specifed as the reply port in the message. A message port is obtained by calling the AllocSysObject() function with an object type of ASOT_PORT. You must delete the message port when you are finished by calling the FreeSysObject() function.
Creating an IO Request
The I/O request is used to send commands and data from your application to the device. The I/O request consists of fields used to hold the command you wish to execute and any parameters it requires. You set up the fields with the appropriate information and send it to the device by using Exec I/O functions. Different Amiga devices often require different I/O request structures. These structures all start with a simple IORequest or IOStdReq structure (see <exec/io.h>) which may be followed by various device-specific fields. Consult the Autodoc and include file for each device to determine the type and size I/O request required to access the device.
I/O request structures are commonly created and deleted with the AllocSysObject() and FreeSysObject() functions using an object type of ASOT_IOREQUEST. Any size and type of I/O request may be created with these functions.
Opening a Device
The device is opened by calling the OpenDevice() function. In addition to establishing the link to the device, OpenDevice() also initializes fields in the I/O request. OpenDevice() has this format:
result = IExec->OpenDevice(device_name,unit_number,(struct IORequest *)IORequest,flags)
- device_name
- is the name of the device.
- unit_number
- refers to one of the logical units of the device. Devices with one unit always use unit 0. Multiple unit devices like the trackdisk device and the timer device use the different units for specific purposes.
- IORequest
- is the structure discussed above. Some of the devices have their own I/O requests defined in their include files and others use standard I/O requests, (IOStdReq). Refer to Devices for more information.
- flags
- are bits set to indicate options for some of the devices. This field is set to zero for devices which don't accept options when they are opened. The flags for each device are explained in Devices.
- result
- is an indication of whether the OpenDevice() was successful with zero indicating success. Never assume that a device will successfully open. Check the return value and act accordingly.
Zero Equals Success for OpenDevice(). |
---|
Unlike most Amiga system functions, OpenDevice() returns zero for success and a device-specific error value for failure. |
Using A Device
Once a device has been opened, you use it by passing the I/O request to it. When the device processes the I/O request, it acts on the information the I/O request contains and returns a reply message, i.e., the I/O request, to the reply port specified in the I/O request when it is finished. The I/O request is passed to a device using one of three functions, DoIO(), SendIO() and BeginIO(). They take only one argument: the I/O request you wish to pass to the device.
- DoIO()
- DoIO() is a synchronous function. It will not return until the device has responded to the I/O request.
- This function calls the BeginIO() entry point in the device driver with IOF_QUICK set. If the device driver leaves IOF_QUICK set, it returns to the caller immediately. The return value is the extended value of the "io_Error" field in the I/O request. If the IOF_QUICK bit is cleared, it falls through to WaitIO().
- SendIO()
- SendIO() is an asynchronous function. It can return immediately, but the I/O operation it initiates may take a short or long time. Using SendIO() requires you to monitor the message port for a return message from the device. In addition, some devices do not actually respond asynchronously even though you called SendIO(); they will return when the I/O operation is finished.
- This function calls the BeginIO() entry point in the device driver with IOF_QUICK clear. This means that the device driver should return the I/O request with ReplyMsg().
- BeginIO()
- There is one operation which DoIO() and SendIO() cannot handle, and that is sending an I/O request with the IOF_QUICK flag set, but not waiting for it to complete. That is "run this as quickly as possible but if it's going to take a while, don't wait for it". For this operation, the user must set IOF_QUICK manually, then call the device driver directly through its BeginIO() entry point.
- When the quick I/O flag (IOF_QUICK) is set in the I/O request, a device is allowed to take certain shortcuts in performing and completing a request. If the request can complete immediately, the device will not return a reply message and the quick I/O flag will remain set. If the request cannot be completed immediately, the QUICK_IO flag will be clear. DoIO() normally requests quick I/O; SendIO() does not.
DoIO() and SendIO() are most commonly used.
BeginIO() Side Effects |
---|
BeginIO() will run on the context of the caller if quick I/O is used by the device driver. This means BeginIO() may allocate memory, wait or perform other functions which are illegal or dangerous during interrupts. |
An I/O request typically has three fields set for every command sent to a device. You set the command itself in the io_Command field, a pointer to the data for the command in the io_Data field, and the length of the data in the io_Length field.
SerialIO->IOSer.io_Length = sizeof(ReadBuffer); SerialIO->IOSer.io_Data = ReadBuffer; SerialIO->IOSer.io_Command = CMD_READ; IExec->SendIO((struct IORequest *)SerialIO);
Commands consist of two parts—a prefix and the command word separated by an underscore, all in upper case. The prefix indicates whether the command is an Exec or device specific command. All Exec commands have CMD as the prefix. They are defined in the include file exec/io.h.
Standard Exec Commands
CMD_CLEAR | CMD_READ | CMD_STOP |
CMD_FLUSH | CMD_RESET | CMD_WRITE |
CMD_INVALID | CMD_START | CMD_UPDATE |
There are 65536 command values possible with io_Command:
$0000 - $3FFF old style and 3rd party commands $4000 - $7FFF RESERVED AREA! $8000 - $BFFF old style and 3rd party commands $C000 - $FFFF RESERVED AREA!
Commands in the reserved areas may only be assigned and specified by the AmigaOS Development Team. Any "custom" implementation of these commands or other commands in these reserved areas is illegal and violates the New Style Device (NSD) Standard.
Synchronous vs. Asynchronous Requests
Each device maintains its own I/O request queue. When a device receives an I/O request, it either processes the request immediately or puts it in the queue because one is already being processed. After an I/O request is completely processed, the device checks its queue and if it finds another I/O request, begins to process that request.
As stated above, you can send I/O requests to a device synchronously or asynchronously. The choice of which to use is largely a function of your application.
Synchronous requests use the DoIO() function. DoIO() will not return control to your application until the I/O request has been satisfied by the device. The advantage of this is that you don’t have to monitor the message port for the device reply because DoIO() takes care of all the message handling. The disadvantage is that your application will be tied up while the I/O request is being processed, and should the request not complete for some reason, DoIO() will not return and your application will hang.
Asynchronous requests use the SendIO() and BeginIO() functions. Both return to your application almost immediately after you call them. This allows you to do other operations, including sending more I/O requests to the device. Note that any additional I/O requests you send must use separate I/O request structures. Outstanding I/O requests are not available for re-use until the device is finished with them.
Do Not Touch! |
---|
When you use SendIO() or BeginIO(), the I/O request you pass to the device and any associated data buffers should be considered read-only. Once you send it to the device, you must not modify it in any way until you receive the reply message from the device or abort the request (though you must still wait for a reply). Any exceptions to this rule are documented in the autodoc for the device. |
Sending multiple asynchronous I/O requests to a device can be tricky because devices require them to be unique and initialized. This means you can't use an I/O request that's still in the queue, but you need the fields which were initialized in it when you opened the device. The solution is to copy the initialized I/O request to another I/O request(s) before sending anything to the device.
Regardless of what you do while you are waiting for an asynchronous I/O request to return, you need to have some mechanism for knowing when the request has been done. There are two basic methods for doing this.
The first involves putting your application into a wait state until the device returns the I/O request to the message port of your application. You can use the WaitIO(), Wait() or WaitPort() function to wait for the return of the I/O request. It is important to note that all of the above functions and also DoIO() may Wait() on the message reply port's mp_SigBit. For this reason, the task that created the port must be the same task the waits for completion of the I/O. There are three ways to wait:
- WaitIO()
- not only waits for the return of the I/O request, it also takes care of all the message handling functions. This is very convenient, but you can pay for this convenience: your application will hang in the unlikely event that the I/O request does not return.
- If the I/O request has the IOF_QUICK flag set, it cannot possibly be in progress, so it returns immediately.
- Wait()
- waits for a signal to be sent to the message port. It will awaken your task when the signal arrives, but you are responsible for all of the message handling.
- WaitPort()
- waits for the message port to be non-empty. It returns a pointer to the message in the port, but you are responsible for all of the message handling.
The second method to detect when the request is complete involves using the CheckIO() function. CheckIO() takes an I/O request as its argument and returns an indication of whether or not it has been completed. When CheckIO() returns the completed indication, you will still have to remove the I/O request from the message port.
I/O Request Completion
A device will set the io_Error field of the I/O request to indicate the success or failure of an operation. The indication will be either zero for success or a non-zero error code for failure. There are two types of error codes: Exec I/O and device specific. Exec I/O errors are defined in the include file <exec/errors.h>; device specific errors are defined in the include file for each device. You should always check that the operation you requested was successful.
The exact method for checking io_Error can depend on whether you use DoIO() or SendIO(). In both cases, io_Error will be set when the I/O request is returned, but in the case of DoIO(), the DoIO() function itself returns the same value as io_Error. This gives you the option of checking the function return value:
SerialIO->IOSer.io_Length = sizeof(ReadBuffer); SerialIO->IOSer.io_Data = ReadBuffer; SerialIO->IOSer.io_Command = CMD_READ; if (IExec->DoIO((struct IORequest *)SerialIO) IDOS->Printf("Read failed. Error: %ld\n", SerialIO->IOSer.io_Error);
Or you can check io_Error directly:
SerialIO->IOSer.io_Length = sizeof(ReadBuffer); SerialIO->IOSer.io_Data = ReadBuffer; SerialIO->IOSer.io_Command = CMD_READ; IExec->DoIO((struct IORequest *)SerialIO); if (SerialIO->IOSer.io_Error) IDOS->Printf("Read failed. Error: %ld\n", SerialIO->IOSer.io_Error);
Keep in mind that checking io_Error is the only way that I/O requests sent by SendIO() can be checked.
Testing for a failed I/O request is a minimum step, what you do beyond that depends on your application. In some instances, you may decide to resend the I/O request and in others, you may decide to stop your application. One thing you'll almost always want to do is to inform the user that an error has occurred.
Exiting The Correct Way |
---|
If you decide that you must prematurely end your application, you should deallocate, release, give back and let go of everything you took to run the application. In other words, you should exit gracefully. |
Ending Device Access
You end device access by reversing the steps you did to access it. This means you close the device, deallocate the I/O request memory and delete the message port. In that order!
Closing a device is how you tell Exec that you are finished using a device and any associated resources. This can result in housecleaning being performed by the device. However, before you close a device, you might have to do some housecleaning of your own.
A device is closed by calling the CloseDevice() function. The CloseDevice() function does not return a value. It has this format:
IExec->CloseDevice(IORequest);
where IORequest is the I/O request used to open the device.
You should not close a device while there are outstanding I/O requests, otherwise you can cause major and minor problems. Let's begin with the minor problem: memory. If an I/O request is outstanding at the time you close a device, you won't be able to reclaim the memory you allocated for it.
The major problem: the device will try to respond to the I/O request. If the device tries to respond to an I/O request, and you've deleted the message port (which is covered below), you will probably crash the system.
One solution would be to wait until all I/O requests you sent to the device return. This is not always practical if you've sent a few requests and the user wants to exit the application immediately.
In that case, the only solution is to abort and remove any outstanding I/O requests. You do this with the functions AbortIO() and WaitIO(). They must be used together for cleaning up. AbortIO() will abort an I/O request, but will not prevent a reply message from being sent to the application requesting the abort. WaitIO() will wait for an I/O request to complete and remove it from the message port. This is why they must be used together.
Be Careful With AbortIO()! |
---|
Do not AbortIO() an I/O request which has not been sent to a device. If you do, you may crash the system. |
Here is the checklist for gracefully exiting:
- Abort any outstanding I/O requests with AbortIO().
- Wait for the completion of any outstanding or aborted I/O requests with WaitIO().
- Close the device with CloseDevice().
- Release the I/O request memory with FreeSysObject() using the ASOT_IOREQUEST object type.
- Delete the message port with FreeSysObject() using the ASOT_PORT object type.
Expunging a Device
A device can be deleted from the system using this function:
error = IExec->RemDevice(device);
The system issues this function itself if it runs out of memory and tries to reclaim space. The device is called through its Expunge() entry point.
The device should now shut down its activity, i.e. remove interrupt servers, deallocate buffers, and so on. Then it should unlink its device node from the device list, and deallocate the node, thus restoring things to the state they were in just before it was started up with InitResident(). Finally, it should return the "segList" parameter which was passed to it at initialization time. If the device came from disk, the system will use this to unload its code.
If the device is not idle when the Expunge() call arrives, it can defer the operation. To do this, it sets the LIBF_DELEXP flag in the library structure, and returns zero. This indicates that it will delete itself at the earliest opportunity.
Devices With Functions
Some devices, in addition to their commands, provide library-style functions which can be directly called by applications. These functions are documented by each device.
Devices with functions behave much like Amiga libraries, i.e., you set up a base address pointer and call the functions as offsets from the pointer. See the Exec Libraries chapter for more information.
The procedure for accessing a device's functions is as follows:
- Declare the device base address variable in the global data area. The correct name for the base address can be found in the device's proto file.
- Create a message port.
- Create an I/O request.
- Call OpenDevice(), passing the I/O request. If OpenDevice() is successful (returns 0), the address of the device base may be found in the io_Device field of the I/O request structure. Consult the include file for the structure you are using to determine the full name of the io_Device field. The base address is only valid while the device is open.
- Set the device base address variable to the pointer returned in the io_Device field.
- Get the interface pointer for this device using the above device base address.
We will use the timer device to illustrate the above method. The name of the timer device base address is listed in its proto file as TimerBase.
#include <devices/timer.h> struct Library *TimerBase; /* device base address pointer */ struct TimerIFace *ITimer; /* interface for timer device */ struct MsgPort *TimerMP; /* message port pointer */ struct TimeRequest *TimerIO; /* I/O request pointer */ /* Create the message port. */ TimerMP = IExec->AllocSysObjectTags(ASOT_PORT, TAG_END); if (TimerMP != NULL) { /* Create the I/O request. */ TimerIO = IExec->AllocSysObjectTags(ASOT_IOREQUEST, ASOIOR_Size, sizeof(struct TimeRequest), ASOIOR_ReplyPort, TimerMP, TAG_END); if ( TimerIO != NULL ) { /* Open the timer device. */ if ( !(IExec->OpenDevice(TIMERNAME,UNIT_MICROHZ,TimerIO,0)) ) { /* Set up pointer for timer functions. */ TimerBase = (struct Library *)TimerIO->tr_node.io_Device; ITimer = (struct TimerIFace *)IExec->GetInterface(TimerBase, "main", 1, NULL); /* Use timer device library-style functions such as CmpTime() through ITimer...*/ IExec->CloseDevice(TimerIO); /* Close the timer device. */ } else IDOS->Printf("Error: Could not open %s\n", TIMERNAME); } else IDOS->Printf("Error: Could not create I/O request\n"); } else IDOS->Printf("Error: Could not create message port\n"); }
Using An Exec Device
The following short example demonstrates use of an Amiga device. The example opens the serial.device and then demonstrates both synchronous (DoIO()) and asynchronous (SendIO()) use of the serial command SDCMD_QUERY. This command is used to determine the status of the serial device lines and registers.
/* DeviceUse.c - an example of using an Amiga device (here, serial device) */ /* */ /* If successful, use the serial command SDCMD_QUERY, then reverse our steps. */ /* If we encounter an error at any time, we will gracefully exit. */ #include <exec/types.h> #include <exec/memory.h> #include <exec/io.h> #include <devices/serial.h> #include <proto/exec.h> #include <proto/dos.h> int main() { struct IOExtSer *reply; /* for use with GetMsg */ struct MsgPort *serialMP = IExec->AllocSysObjectTags(ASOT_PORT, TAG_END); if (serialMP != NULL) { /* Create the I/O request. Note that <devices/serial.h> defines the type */ /* of IORequest required by the serial device--an IOExtSer. Many devices */ /* require specialized extended IO requests which start with an embedded */ /* struct IORequest. The generic Exec and amiga.lib device IO functions */ /* are prototyped for IORequest, so some pointer casting is necessary. */ struct IOExtSer *serialIO = IExec->AllocSysObjectTags(ASOT_IOREQUEST, ASOIOR_Size, sizeof(struct IOExtSer), ASOIOR_ReplyPort, serialMP, TAG_END); if (serialIO != NULL) { /* Open the serial device (non-zero return value means failure here). */ if (OpenDevice( SERIALNAME, 0, (struct IORequest *)serialIO, 0L)) IDOS->Printf("Error: %s did not open\n", SERIALNAME); else { /* Device is open */ /* DoIO - demonstrates synchronous */ serialIO->IOSer.io_Command = SDCMD_QUERY; /* device use, returns error or 0. */ if (IExec->DoIO((struct IORequest *)serialIO)) IDOS->Printf("Query failed. Error - %d\n", serialIO->IOSer.io_Error); else /* Print serial device status - see include file for meaning */ /* Note that with DoIO, the Wait and GetMsg are done by Exec */ IDOS->Printf("Serial device status: $%x\n\n", serialIO->io_Status); serialIO->IOSer.io_Command = SDCMD_QUERY; /* SendIO - demonstrates asynchronous */ IDOS->SendIO((struct IORequest *)serialIO); /* device use (returns immediately). */ /* We could do other things here while the query is being done. */ /* And to manage our asynchronous device IO: */ /* - we can CheckIO(serialIO) to check for completion */ /* - we can AbortIO(serialIO) to abort the command */ /* - we can WaitPort(serialMP) to wait for any serial port reply */ /* OR we can WaitIO(serialIO) to wait for this specific IO request */ /* OR we can Wait(1L << serialMP_>mp_SigBit) for reply port signal */ IExec->Wait(1U << serialMP->mp_SigBit); while(reply = (struct IOExtSer *)IExec->GetMsg(serialMP)) { /* Since we sent out only one serialIO request the while loop is */ /* not really needed--we only expect one reply to our one query */ /* command, and the reply message pointer returned by GetMsg() */ /* will just be another pointer to our one serialIO request. */ /* With Wait() or WaitPort(), you must GetMsg() the message. */ if(reply->IOSer.io_Error) IDOS->Printf("Query failed. Error - %d\n", reply->IOSer.io_Error); else IDOS->Printf("Serial device status: $%x\n\n", reply->io_Status); } IExec->CloseDevice((struct IORequest *)serialIO); /* Close the serial device. */ } IExec->FreeSysObject(ASOT_IOREQUEST, serialIO); /* Delete the I/O request. */ } else IDOS->Printf("Error: Could create I/O request\n"); /* Inform user that the I/O */ /* request could be created. */ IExec->FreeSysObject(ASOT_PORT, serialMP); /* Delete the message port. */ } else IDOS->Printf("Error: Could not create message port\n"); /* Inform user that the message*/ /* port could not be created. */ return 0; }
Creating An Exec Device
The BeginIO Entry Point
All I/O requests enter the device driver through the BeginIO() entry point. The device driver is entered in the context of the requesting task.
Normally, the device driver will now use PutMsg() to enqueue the I/O request on a message port (in a Unit structure) for processing by an internal task. Then it can return from the BeginIO() function. When Exec checks to see if the I/O request is completed yet, it checks its type field, and if it is NT_MESSAGE (as results from the PutMsg() call) it knows that it is still in progress. Eventually, the internal task receives the I/O request, operates on it, and does a ReplyMsg(). This returns the I/O request to the caller's reply port, and also sets its type to NT_REPLYMSG, signaling that it is finished.
It is clear that the device driver does not have to follow this procedure exactly. Short commands (such as checking if a disk is ready) can just be done immediately, in the caller's context. The I/O request must simply be returned with ReplyMsg() at the end, and its type field must be something other than NT_REPLYMSG if the BeginIO() function returns with the I/O request not completed yet.
A special case of I/O processing is signaled by the IOF_QUICK flag. When it is set, it means that the requester has used the DoIO() function, and thus will be doing nothing until the I/O is complete. In this case, the device driver can run the whole I/O operation in the caller's context and return immediately. Message passing and task switch overhead is eliminated. When the BeginIO() function returns with the IOF_QUICK bit still set, it means that the I/O operation is complete.
If the device driver sees the IOF_QUICK flag set but cannot perform the I/O processing inline, it can simply clear the flag and return the I/O request with ReplyMsg() as usual.
The BeginIO() function operates on the command and parameters in the I/O request, and sets the "io_Error" field to indicate the result. Exec I/O functions return the value of this field to the caller; BeginIO() itself does not return a value. "io_Error" set to zero means that no error has occurred.
The AbortIO Entry Point
Some device driver operations, such as waiting for a timeout or input on a serial port, may need to be aborted before they complete. The AbortIO() function is provided for this. The device driver is entered through its AbortIO() entry point with the address of the I/O request to be aborted and the device node pointer. If the device driver determines that the I/O request is indeed in progress and can successfully abort it, it returns zero, otherwise it returns a non-zero error code.
A successfully aborted I/O request is returned by the normal method, i.e. ReplyMsg(). The "io_Error" field should indicate that it did not complete normally.
Unit Structures
Many device drivers manage more than one functional unit. For example, the trackdisk driver can handle up to four floppy drives. The preferred approach is to use a separate "Unit" structure for each functional unit (e.g. drive). Normally, a unit structure consists of at least the following:
struct Unit { struct MsgPort unit_MsgPort; uint8 unit_flags; /* UNITF_ACTIVE = 1 UNITF_INTASK = 2 */ uint8 unit_pad; uint16 unit_OpenCnt; };
When the device driver is opened, it uses the unit number to select the appropriate unit structure, and stores the pointer to this structure in the I/O request. Later, it can queue up pending I/O requests on the message port for processing by the unit task.
Function Reference
The following chart gives a brief description of the Exec functions that control device I/O. See the SDK for details about each call.
Exec Device I/O Function | Description |
---|---|
AllocSysObject(ASOT_IOREQUEST) | Create an IORequest structure. |
FreeSysObject(ASOT_IOREQUEST) | Delete an IORequest created by AllocSysObject(). |
OpenDevice() | Gain access to an Exec device. |
CloseDevice() | Close Exec device opened with OpenDevice(). |
DoIO() | Perform a device I/O command and wait for completion. |
SendIO() | Initiate an I/O command. Do not wait for it to complete. |
CheckIO() | Get the status of an IORequest. |
WaitIO() | Wait for completion of an I/O request. |
AbortIO() | Attempt to abort an I/O request that is in progress. |
BeginIO() | Initiate an asynchronous device I/O request. |