Copyright (c) Hyperion Entertainment and contributors.

Difference between revisions of "Exec Device I/O"

From AmigaOS Documentation Wiki
Jump to navigation Jump to search
 
(55 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{NeedUpdate}}
 
 
== Exec Device I/O ==
 
== Exec Device I/O ==
   
Line 9: Line 8:
   
 
{| class="wikitable"
 
{| class="wikitable"
! colspan="2"|Amiga System Devices
+
! colspan="3"|Amiga System Devices
 
|-
 
|-
| [[Audio Device|Audio]] || Controls the use of the audio hardware
+
| [[Audio Device|Audio]] || audio.device || Controls the use of the audio hardware
 
|-
 
|-
| [[Clipboard Device|Clipboard]] || Manages the cutting and pasting of common data blocks
+
| [[Clipboard Device|Clipboard]] || clipboard.device || Manages the cutting and pasting of common data blocks
 
|-
 
|-
| [[Console Device|Console]] || Provides the text-oriented user interface.
+
| [[Console Device|Console]] || console.device || Provides the text-oriented user interface.
 
|-
 
|-
| [[Gameport Device|Gameport]] || Controls the two mouse/joystick ports.
+
| [[Gameport Device|Gameport]] || gameport.device || Controls the two mouse/joystick ports.
 
|-
 
|-
| [[Input Device|Input]] || Processes input from the gameport and keyboard devices.
+
| [[Input Device|Input]] || input.device || Processes input from the gameport and keyboard devices.
 
|-
 
|-
| [[Keyboard Device|Keyboard]] || Controls the keyboard.
+
| [[Keyboard Device|Keyboard]] || keyboard.device || Controls the keyboard.
 
|-
 
|-
| [[Narrator Device|Narrator]] || Produces the Amiga synthesized speech.
+
| [[Narrator Device|Narrator]] || narrator.device || Produces the Amiga synthesized speech.
 
|-
 
|-
| [[Parallel Device|Parallel]] || Controls the parallel port.
+
| [[Parallel Device|Parallel]] || parallel.device || Controls the parallel port.
 
|-
 
|-
| [[Printer Device|Printer]] || Converts a standard set of printer control codes to printer specific codes.
+
| [[Printer Device|Printer]] || printer.device || Converts a standard set of printer control codes to printer specific codes.
 
|-
 
|-
| [[SCSI Device|SCSI]] || Controls the Small Computer Standard Interface hardware.
+
| [[SCSI Device|SCSI]] || scsi.device || Controls the Small Computer Standard Interface hardware.
 
|-
 
|-
| [[Serial Device|Serial]] || Controls the serial port.
+
| [[Serial Device|Serial]] || serial.device || Controls the serial port.
 
|-
 
|-
| [[Timer Device|Timer]] || Provides timing functions to measure time intervals and send interrupts.
+
| [[Timer Device|Timer]] || timer.device || Provides timing functions to measure time intervals and send interrupts.
 
|-
 
|-
| [[Trackdisk Device|Trackdisk]] || Controls the Amiga floppy disk drives.
+
| [[Trackdisk Device|Trackdisk]] || trackdisk.device || Controls the Amiga floppy disk drives.
 
|}
 
|}
   
Line 59: Line 58:
   
 
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.
 
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.
 
Alternately, I/O requests can be created by declaring them as structures initialized to zero, or by dynamically allocating cleared shared memory for them, but in these cases you will be responsible for the IORequest structure initializations which are normally handled by the above functions. The message port pointer in the I/O request tells the device where to respond with messages for your application. You must set a pointer to the message port in the I/O request if you declare it as a structure or allocate memory for it using AllocVecTags().
 
   
 
=== Opening a Device ===
 
=== Opening a Device ===
Line 67: Line 64:
   
 
<syntaxhighlight>
 
<syntaxhighlight>
result = OpenDevice(device_name,unit_number,(struct IORequest *)IORequest,flags)
+
result = IExec->OpenDevice(device_name,unit_number,(struct IORequest *)IORequest,flags)
 
</syntaxhighlight>
 
</syntaxhighlight>
   
 
; device_name
 
; device_name
  +
: is the name of the device.
: is one of the following NULL-terminated strings for system devices:
 
 
{| class="wikitable"
 
| audio.device || parallel.device || clipboard.device
 
|-
 
| printer.device || console.device || scsi.device
 
|-
 
| gameport.device || serial.device || input.device
 
|-
 
| timer.device || keyboard.device || trackdisk.device
 
|-
 
| narrator.device || ||
 
|}
 
   
 
; unit_number
 
; unit_number
Line 97: Line 82:
 
: 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.
 
: 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.
   
  +
{{Note|title=''Zero Equals Success for OpenDevice().''|Unlike most Amiga system functions, OpenDevice() returns zero for success and a device-specific error value for failure.}}
{| class="wikitable"
 
| ''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 ==
+
=== 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 message port 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.
+
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 finished with the I/O request. DoIO() will wait, if necessary, for the request to complete, and will remove (GetMsg()) any reply message from the message port.
 
  +
: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. SendIO is normally used when your application has other work to do while the I/O request is being acted upon, or if your application wishes to allow the user to cancel the I/O. Using SendIO() requires that you wait on or check for completion of the request, and remove the completed request from the message port with GetMsg().
 
  +
: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()
* BeginIO() is commonly used to control the QuickIO bit when sending an I/O request to a device. When the QuickIO 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 QuickIO flag will remain set. If the request cannot be completed immediately, the QUICK_IO flag will be clear. DoIO() normally requests QuickIO; SendIO() does not.
 
  +
: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.
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.
 
  +
  +
{{Note|title=BeginIO() Side Effects|text=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.
   
 
<syntaxhighlight>
 
<syntaxhighlight>
Line 120: Line 113:
 
</syntaxhighlight>
 
</syntaxhighlight>
   
Commands consist of two parts (separated by an underscore, all in upper case): a prefix and the command word. The prefix indicates whether the command is an Exec or device specific command. All Exec standard commands have "CMD" as the prefix. They are defined in the include file &lt;exec/io.h&gt;.
+
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 Device Commands
+
=== Standard Exec Commands ===
   
  +
This section lists the command and error numbers which are predefined in the Amiga system for all types of device drivers.
{| class="wikitable"
 
| CMD_READ || CMD_WRITE
 
|-
 
| CMD_START || CMD_STOP
 
|-
 
| CMD_UPDATE || CMD_FLUSH
 
|-
 
| CMD_CLEAR || CMD_RESET
 
|}
 
   
  +
The following commands are reserved for all devices:
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.
 
  +
* CMD_INVALID
  +
* CMD_RESET
  +
* CMD_READ
  +
* CMD_WRITE
  +
* CMD_UPDATE
  +
* CMD_CLEAR
  +
* CMD_STOP
  +
* CMD_START
  +
* CMD_FLUSH
  +
* CMD_NONSTD
   
  +
It is seen that command number zero is invalid, and command numbers greater than 9 are custom defined.
System Device Command Prefixes
 
  +
  +
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 [[NSD_Standard|New Style Device (NSD) Standard]].
  +
  +
==== CMD_RESET ====
  +
  +
This resets the device to a known initial state. Pending I/O requests not processed at the time of this command should be returned with an error.
  +
  +
==== CMD_READ ====
  +
  +
This requests that "io_Length" items of data be read from location "io_Offset" on the unit, and stored at "io_Data" in the caller's memory. The actual amount of data transferred is returned in "io_Actual". The specifics depend on the device type.
  +
  +
==== CMD_WRITE ====
  +
  +
This requests that data be transferred from the caller's memory to the I/O unit. The arguments are the same as for CMD_READ.
  +
  +
==== CMD_UPDATE ====
  +
  +
This requests that all buffered, but unwritten data be forced out to the I/O unit. It might write out the track buffer in a disk device, for example.
  +
  +
==== CMD_CLEAR ====
  +
  +
This requests that all data buffered by the device for the given unit be invalidated. Thus, for example, it would throw away data waiting in a serial input buffer.
  +
  +
==== CMD_STOP ====
  +
  +
This requests that the unit stop processing commands. I/O requests not processed at the time of the CMD_STOP will wait until a CMD_START or CMD_RESET is received or they are aborted.
  +
  +
==== CMD_START ====
  +
  +
This requests that the unit clear a CMD_STOP condition and resume processing commands. Only one CMD_START is required, regardless of how many CMD_STOPs have been received.
  +
  +
==== CMD_FLUSH ====
  +
  +
This requests that the unit flush all pending commands. All I/O requests queued but not yet processed should be sent back with an error.
  +
  +
==== Error Numbers ====
  +
  +
The include file "exec/errors.h" lists the following standard error numbers.
   
 
{| class="wikitable"
 
{| class="wikitable"
  +
! Constant !! Description
! Device
 
! Prefix
 
! Example
 
 
|-
 
|-
  +
| IOERR_SUCCESS || not an error (success)
| Audio || ADCMD || ADCMD_ALLOCATE
 
 
|-
 
|-
  +
| IOERR_OPENFAIL || device/unit failed to open
| Clipboard || CBD || CBD_POST
 
 
|-
 
|-
  +
| IOERR_ABORTED || request aborted
| Console || CD || CD_ASKKEYMAP
 
 
|-
 
|-
  +
| IOERR_NOCMD || command not supported
| Gameport || GPD || GPD_SETCTYPE
 
 
|-
 
|-
  +
| IOERR_BADLENGTH || not a valid length
| Input || IND || IND_SETMPORT
 
 
|-
 
|-
  +
| IOERR_BADADDRESS || invalid address (misaligned or bad range)
| Keyboard || KBD || KBD_READMATRIX
 
 
|-
 
|-
| Narrator || no device specific commands || -
+
| IOERR_UNITBUSY || device opens ok, but requested unit is busy
 
|-
 
|-
  +
| IOERR_SELFTEST || device is currently performing a self-test
| 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 ===
 
=== 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.
 
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.
+
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.
+
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.
   
  +
{{Note|title=''Do Not Touch!''|text=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.}}
{| class="wikitable"
 
| ''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.
 
|}
 
   
 
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.
 
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.
Line 188: Line 214:
 
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.
 
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:
+
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()
* 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 if the I/O request does not return.
 
  +
: 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()
* 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.
 
  +
: 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.
* 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.
 
   
  +
{{Note|text=WaitIO() and CheckIO() know about the IOF_QUICK flag and will behave accordingly.}}
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 ===
 
=== 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 &lt;exec/errors.h&gt;; device specific errors are defined in the include file for each device. You should always check that the operation you requested was successful.
+
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 &lt;exec/errors.h&gt;; 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:
+
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:
   
 
<syntaxhighlight>
 
<syntaxhighlight>
Line 223: Line 255:
 
</syntaxhighlight>
 
</syntaxhighlight>
   
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.
+
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.
{| class="wikitable"
 
  +
| ''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.
 
  +
{{Note|title=Exiting The Correct Way|text=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.}}
|}
 
   
==== Closing the Device ====
+
=== 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!
 
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!
Line 235: Line 267:
 
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.
 
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:
+
A device is closed by calling the '''CloseDevice()''' function. The '''CloseDevice()''' function does not return a value. It has this format:
   
 
<syntaxhighlight>
 
<syntaxhighlight>
CloseDevice(IORequest);
+
IExec->CloseDevice(IORequest);
 
</syntaxhighlight>
 
</syntaxhighlight>
   
where IORequest is the I/O request used to open the device.
+
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.
 
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.
Line 249: Line 281:
 
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.
 
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.
+
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.
   
  +
{{Note|title=''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.}}
{| class="wikitable"
 
| ''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.
 
|}
 
 
==== Ending Device Access ====
 
   
 
Here is the checklist for gracefully exiting:
 
Here is the checklist for gracefully exiting:
   
* Abort any outstanding I/O requests with AbortIO().
+
# 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 ===
* Wait for the completion of any outstanding or aborted I/O requests with WaitIO().
 
   
  +
A device can be deleted from the system using this function:
* Close the device with CloseDevice().
 
   
  +
<syntaxhighlight>
* Release the I/O request memory with FreeSysObject() using the ASOT_IOREQUEST object type.
 
  +
error = IExec->RemDevice(device);
  +
</syntaxhighlight>
   
  +
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.
* Delete the message port with FreeSysObject() using the ASOT_PORT object type.
 
  +
  +
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 ==
 
== Devices With Functions ==
Line 273: Line 311:
 
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.
 
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|Exec Libraries]] chapter for more information.
+
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 [[Exec_Libraries|Exec Libraries]] for more information.
   
 
The procedure for accessing a device's functions is as follows:
 
The procedure for accessing a device's functions is as follows:
Line 283: Line 321:
 
* 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.
   
  +
* Get the interface pointer for this device using the above device base address.
* 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.
 
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.
Line 317: Line 357:
 
ITimer = (struct TimerIFace *)IExec->GetInterface(TimerBase, "main", 1, NULL);
 
ITimer = (struct TimerIFace *)IExec->GetInterface(TimerBase, "main", 1, NULL);
   
/* Use timer device library-style functions such as CmpTime() ...*/
+
/* Use timer device library-style functions such as CmpTime() through ITimer...*/
   
 
IExec->CloseDevice(TimerIO); /* Close the timer device. */
 
IExec->CloseDevice(TimerIO); /* Close the timer device. */
Line 334: Line 374:
 
== Using An Exec Device ==
 
== 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.
+
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.
 
<pre>;/* 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
 
   
  +
<syntaxhighlight>
 
/* DeviceUse.c - an example of using an Amiga device (here, serial device) */
 
/* 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 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 */
+
/* If we encounter an error at any time, we will gracefully exit. */
/* 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 &lt;exec/types.h&gt;
 
#include &lt;exec/memory.h&gt;
 
#include &lt;exec/io.h&gt;
 
#include &lt;devices/serial.h&gt;
 
 
#include &lt;clib/exec_protos.h&gt; /* Prototypes for Exec library functions */
 
#include &lt;clib/alib_protos.h&gt; /* Prototypes for amiga.lib functions */
 
   
#include &lt;stdio.h&gt;
+
#include <exec/types.h>
  +
#include <exec/memory.h>
  +
#include <exec/io.h>
  +
#include <devices/serial.h>
   
  +
#include <proto/exec.h>
#ifdef LATTICE
 
  +
#include <proto/dos.h>
int CXBRK(void) { return(0); } /* Disable SAS CTRL/C handling */
 
int chkabort(void) { return(0); } /* really */
 
#endif
 
   
void main(void)
+
int main()
 
{
 
{
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 */
 
struct IOExtSer *reply; /* for use with GetMsg */
   
  +
struct MsgPort *serialMP = IExec->AllocSysObjectTags(ASOT_PORT, TAG_END);
if (serialMP=CreatePort(NULL,NULL)) /* Create the message port. */
 
  +
  +
if (serialMP != NULL)
 
{
 
{
/* Create the I/O request. Note that &lt;devices/serial.h&gt; defines the type */
+
/* Create the I/O request. Note that <devices/serial.h> defines the type */
 
/* of IORequest required by the serial device--an IOExtSer. Many devices */
 
/* of IORequest required by the serial device--an IOExtSer. Many devices */
 
/* require specialized extended IO requests which start with an embedded */
 
/* require specialized extended IO requests which start with an embedded */
Line 381: Line 404:
 
/* are prototyped for IORequest, so some pointer casting is necessary. */
 
/* are prototyped for IORequest, so some pointer casting is necessary. */
   
  +
struct IOExtSer *serialIO = IExec->AllocSysObjectTags(ASOT_IOREQUEST,
if (serialIO = (struct IOExtSer *)CreateExtIO(serialMP,sizeof(struct IOExtSer)))
 
  +
ASOIOR_Size, sizeof(struct IOExtSer),
  +
ASOIOR_ReplyPort, serialMP,
  +
TAG_END);
  +
  +
if (serialIO != NULL)
 
{
 
{
 
/* Open the serial device (non-zero return value means failure here). */
 
/* Open the serial device (non-zero return value means failure here). */
if (OpenDevice( SERIALNAME, 0, (struct IORequest *)serialIO, 0L))
+
if (IExec->OpenDevice( SERIALNAME, 0, (struct IORequest *)serialIO, 0))
printf(&quot;Error: %s did not open\n&quot;,SERIALNAME);
+
IDOS->Printf("Error: %s did not open\n", SERIALNAME);
 
else
 
else
 
{
 
{
 
/* Device is open */ /* DoIO - demonstrates synchronous */
 
/* Device is open */ /* DoIO - demonstrates synchronous */
serialIO-&gt;IOSer.io_Command = SDCMD_QUERY; /* device use, returns error or 0. */
+
serialIO->IOSer.io_Command = SDCMD_QUERY; /* device use, returns error or 0. */
if (DoIO((struct IORequest *)serialIO))
+
if (IExec->DoIO((struct IORequest *)serialIO))
printf(&quot;Query failed. Error - %d\n&quot;,serialIO-&gt;IOSer.io_Error);
+
IDOS->Printf("Query failed. Error - %d\n", serialIO->IOSer.io_Error);
 
else
 
else
 
/* Print serial device status - see include file for meaning */
 
/* Print serial device status - see include file for meaning */
 
/* Note that with DoIO, the Wait and GetMsg are done by Exec */
 
/* Note that with DoIO, the Wait and GetMsg are done by Exec */
printf(&quot;Serial device status: $%x\n\n&quot;,serialIO-&gt;io_Status);
+
IDOS->Printf("Serial device status: $%x\n\n", serialIO->io_Status);
   
serialIO-&gt;IOSer.io_Command = SDCMD_QUERY; /* SendIO - demonstrates asynchronous */
+
serialIO->IOSer.io_Command = SDCMD_QUERY; /* SendIO - demonstrates asynchronous */
SendIO((struct IORequest *)serialIO); /* device use (returns immediately). */
+
IExec->SendIO((struct IORequest *)serialIO); /* device use (returns immediately). */
   
 
/* We could do other things here while the query is being done. */
 
/* We could do other things here while the query is being done. */
Line 406: Line 434:
 
/* - we can WaitPort(serialMP) to wait for any serial port reply */
 
/* - 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 WaitIO(serialIO) to wait for this specific IO request */
/* OR we can Wait(1L &lt;&lt; serialMP_&gt;mp_SigBit) for reply port signal */
+
/* OR we can Wait(1L << serialMP_>mp_SigBit) for reply port signal */
   
Wait(1L &lt;&lt; serialMP-&gt;mp_SigBit);
+
IExec->Wait(1U << serialMP->mp_SigBit);
   
while(reply = (struct IOExtSer *)GetMsg(serialMP))
+
while(reply = (struct IOExtSer *)IExec->GetMsg(serialMP))
 
{ /* Since we sent out only one serialIO request the while loop is */
 
{ /* Since we sent out only one serialIO request the while loop is */
 
/* not really needed--we only expect one reply to our one query */
 
/* not really needed--we only expect one reply to our one query */
Line 416: Line 444:
 
/* will just be another pointer to our one serialIO request. */
 
/* will just be another pointer to our one serialIO request. */
 
/* With Wait() or WaitPort(), you must GetMsg() the message. */
 
/* With Wait() or WaitPort(), you must GetMsg() the message. */
if(reply-&gt;IOSer.io_Error)
+
if(reply->IOSer.io_Error)
printf(&quot;Query failed. Error - %d\n&quot;,reply-&gt;IOSer.io_Error);
+
IDOS->Printf("Query failed. Error - %d\n", reply->IOSer.io_Error);
 
else
 
else
printf(&quot;Serial device status: $%x\n\n&quot;,reply-&gt;io_Status);
+
IDOS->Printf("Serial device status: $%x\n\n", reply->io_Status);
 
}
 
}
CloseDevice((struct IORequest *)serialIO); /* Close the serial device. */
+
IExec->CloseDevice((struct IORequest *)serialIO); /* Close the serial device. */
 
}
 
}
DeleteExtIO(serialIO); /* Delete the I/O request. */
+
IExec->FreeSysObject(ASOT_IOREQUEST, serialIO); /* Delete the I/O request. */
 
}
 
}
else printf(&quot;Error: Could create I/O request\n&quot;); /* Inform user that the I/O */
+
else IDOS->Printf("Error: Could create I/O request\n"); /* Inform user that the I/O */
/* request could be created. */
+
/* request could be created. */
DeletePort(serialMP); /* Delete the message port. */
+
IExec->FreeSysObject(ASOT_PORT, serialMP); /* Delete the message port. */
 
}
 
}
else printf(&quot;Error: Could not create message port\n&quot;); /* Inform user that the message*/
+
else IDOS->Printf("Error: Could not create message port\n"); /* Inform user that the message*/
} /* port could not be created. */</pre>
+
/* port could not be created. */
  +
  +
return 0;
  +
}
  +
</syntaxhighlight>
  +
  +
== 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:
  +
  +
<syntaxhighlight>
  +
struct Unit {
  +
struct MsgPort unit_MsgPort;
  +
uint8 unit_flags;
  +
/* UNITF_ACTIVE = 1
  +
UNITF_INTASK = 2 */
  +
uint8 unit_pad;
  +
uint16 unit_OpenCnt;
  +
};
  +
</syntaxhighlight>
  +
  +
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 ==
 
== Function Reference ==
   
Line 469: Line 542:
 
| Initiate an asynchronous device I/O request.
 
| Initiate an asynchronous device I/O request.
 
|}
 
|}
  +
  +
== Credits ==
  +
  +
Special thanks to [http://wandel.ca Markus Wandel] for allowing us to use material from his [http://wandel.ca/homepage/execdis/devices_doc.txt Amiga Device Driver Guide].

Latest revision as of 17:30, 3 October 2016

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

This section lists the command and error numbers which are predefined in the Amiga system for all types of device drivers.

The following commands are reserved for all devices:

  • CMD_INVALID
  • CMD_RESET
  • CMD_READ
  • CMD_WRITE
  • CMD_UPDATE
  • CMD_CLEAR
  • CMD_STOP
  • CMD_START
  • CMD_FLUSH
  • CMD_NONSTD

It is seen that command number zero is invalid, and command numbers greater than 9 are custom defined.

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.

CMD_RESET

This resets the device to a known initial state. Pending I/O requests not processed at the time of this command should be returned with an error.

CMD_READ

This requests that "io_Length" items of data be read from location "io_Offset" on the unit, and stored at "io_Data" in the caller's memory. The actual amount of data transferred is returned in "io_Actual". The specifics depend on the device type.

CMD_WRITE

This requests that data be transferred from the caller's memory to the I/O unit. The arguments are the same as for CMD_READ.

CMD_UPDATE

This requests that all buffered, but unwritten data be forced out to the I/O unit. It might write out the track buffer in a disk device, for example.

CMD_CLEAR

This requests that all data buffered by the device for the given unit be invalidated. Thus, for example, it would throw away data waiting in a serial input buffer.

CMD_STOP

This requests that the unit stop processing commands. I/O requests not processed at the time of the CMD_STOP will wait until a CMD_START or CMD_RESET is received or they are aborted.

CMD_START

This requests that the unit clear a CMD_STOP condition and resume processing commands. Only one CMD_START is required, regardless of how many CMD_STOPs have been received.

CMD_FLUSH

This requests that the unit flush all pending commands. All I/O requests queued but not yet processed should be sent back with an error.

Error Numbers

The include file "exec/errors.h" lists the following standard error numbers.

Constant Description
IOERR_SUCCESS not an error (success)
IOERR_OPENFAIL device/unit failed to open
IOERR_ABORTED request aborted
IOERR_NOCMD command not supported
IOERR_BADLENGTH not a valid length
IOERR_BADADDRESS invalid address (misaligned or bad range)
IOERR_UNITBUSY device opens ok, but requested unit is busy
IOERR_SELFTEST device is currently performing a self-test

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.

Note
WaitIO() and CheckIO() know about the IOF_QUICK flag and will behave accordingly.

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.

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 Exec Libraries 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 (IExec->OpenDevice( SERIALNAME, 0, (struct IORequest *)serialIO, 0))
                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 */
                IExec->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.

Credits

Special thanks to Markus Wandel for allowing us to use material from his Amiga Device Driver Guide.