Copyright (c) Hyperion Entertainment and contributors.

Difference between revisions of "Exec Device I/O"

From AmigaOS Documentation Wiki
Jump to navigation Jump to search
Line 279: Line 279:
 
* Create an I/O request.
 
* 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.
+
* 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.
 
* Set the device base address variable to the pointer returned in the io_Device field.

Revision as of 17:44, 22 May 2012

Warning.png This page is not yet fully updated to AmigaOS 4.x some of the information contained here may not be applicable in part or totally.

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 Controls the use of the audio hardware
Clipboard Manages the cutting and pasting of common data blocks
Console Provides the text-oriented user interface.
Gameport Controls the two mouse/joystick ports.
Input Processes input from the gameport and keyboard devices.
Keyboard Controls the keyboard.
Narrator Produces the Amiga synthesized speech.
Parallel Controls the parallel port.
Printer Converts a standard set of printer control codes to printer specific codes.
SCSI Controls the Small Computer Standard Interface hardware.
Serial Controls the serial port.
Timer Provides timing functions to measure time intervals and send interrupts.
Trackdisk 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 one of the following NULL-terminated strings for system devices:
audio.device keyboard.device serial.device
clipboard.device narrator.device timer.device
console.device parallel.device trackdisk.device
gameport.device printer.device
input.device scsi.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()
is a synchronous function. It will not return until the device has responded to the I/O request.
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.
BeginIO()
is commonly used to control the quick I/O bit when sending an I/O request to a device. 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.

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

You should not assume that a device supports all standard Exec device commands. Always check the documentation before attempting to use one of them. Device-specific command prefixes vary with the device.

Standard Device Command Prefixes and Examples

Device Prefix Example
Audio ADCMD ADCMD_ALLOCATE
Clipboard CBD CBD_POST
Console CD CD_ASKKEYMAP
Gameport GPD GPD_SETCTYPE
Input IND IND_SETMPORT
Keyboard KBD KBD_READMATRIX
Narrator no device specific commands
Parallel PDCMD PDCMD_QUERY
Printer PRD PRD_PRTCOMMAND
SCSI HD HD_SCSICMD
Serial SDCMD SDCMD_BREAK
Timer TR TR_ADDREQUEST
Trackdisk TD and ETD TD_MOTOR/ETD_MOTOR

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.

Synchronous vs. Asynchronous Requests

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.
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:

  1. Abort any outstanding I/O requests with AbortIO().
  2. Wait for the completion of any outstanding or aborted I/O requests with WaitIO().
  3. Close the device with CloseDevice().
  4. Release the I/O request memory with FreeSysObject() using the ASOT_IOREQUEST object type.
  5. Delete the message port with FreeSysObject() using the ASOT_PORT object type.

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.

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() ...*/
 
            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. The example uses the backward compatible amiga.lib functions for creation and deletion of the message port and I/O request.

;/* DeviceUse.c - Execute me to compile me with SAS C 5.10
LC -b1 -cfistq -v -y -j73 DeviceUse.c
Blink FROM LIB:c.o,DeviceUse.o TO DeviceUse LIBRARY LIB:LC.lib,LIB:Amiga.lib
quit

/* DeviceUse.c - an example of using an Amiga device (here, serial device)    */
/*    - attempt to create a message port with CreatePort()   (from amiga.lib) */
/*    - attempt to create the I/O request with CreateExtIO() (from amiga.lib) */
/*    - attempt to open the serial device with Exec OpenDevice()              */
/*                                                                            */
/* If successful, use the serial command SDCMD_QUERY, then reverse our steps. */
/* If we encounter an error at any time, we will gracefully exit.  Note that  */
/* applications which require at least V37 OS should use the Exec functions   */
/* CreateMsgPort()/DeleteMsgPort() and CreateIORequest()/DeleteIORequest()    */
/* instead of the similar amiga.lib functions which are used in this example. */

#include <exec/types.h>
#include <exec/memory.h>
#include <exec/io.h>
#include <devices/serial.h>

#include <clib/exec_protos.h> /* Prototypes for Exec library functions */
#include <clib/alib_protos.h> /* Prototypes for amiga.lib functions    */

#include <stdio.h>

#ifdef LATTICE
int CXBRK(void) { return(0); }     /* Disable SAS CTRL/C handling */
int chkabort(void) { return(0); }  /* really */
#endif

void main(void)
{
    struct MsgPort  *serialMP;      /* for pointer to our message port */
    struct IOExtSer *serialIO;      /* for pointer to our I/O request  */
    struct IOExtSer *reply;         /* for use with GetMsg             */

    if (serialMP=CreatePort(NULL,NULL)) /* Create the message port. */
    {
        /* 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.   */

        if (serialIO = (struct IOExtSer *)CreateExtIO(serialMP,sizeof(struct IOExtSer)))
        {
            /* Open the serial device (non-zero return value means failure here). */
            if (OpenDevice( SERIALNAME, 0, (struct IORequest *)serialIO, 0L))
                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 (DoIO((struct IORequest *)serialIO))
                    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 */
                    printf("Serial device status: $%x\n\n",serialIO->io_Status);

                serialIO->IOSer.io_Command  = SDCMD_QUERY; /* SendIO - demonstrates asynchronous */
                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  */

                Wait(1L << serialMP->mp_SigBit);

                while(reply = (struct IOExtSer *)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)
                        printf("Query  failed. Error - %d\n",reply->IOSer.io_Error);
                    else
                        printf("Serial device status: $%x\n\n",reply->io_Status);
                }
                CloseDevice((struct IORequest *)serialIO);  /* Close the serial device.    */
            }
            DeleteExtIO(serialIO);                          /* Delete the I/O request.     */
        }
        else printf("Error: Could create I/O request\n");   /* Inform user that the I/O    */
                                                            /* request could be created.   */
        DeletePort(serialMP);                               /* Delete the message port.    */
    }
    else printf("Error: Could not create message port\n");  /* Inform user that the message*/
}                                                           /* port could not be created.  */

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.