NitrOS-9's Unified I/O System
Contents
NitrOS-9’s Unified Input/Output System
Chapter 1 mentioned that NitrOS-9 has a unified I/O system, consisting of all modules except those at the kernel level. This chapter discusses the I/O modules in detail.
The VDG interface performs both interface and low-level routines for VDG Color Computer 2 compatible modes and has limited support for high resolution screen allocation.
The CoGrf interface provides the standard code interpretations and interface functions.
The CoWin interface, available in the Multi-View package, contains all the functionality of CoGrf along with additional support features. If you use CoWin, do not include Grflnt.
Both CoWIn and Grflnt use the low-level driver GrfDrv to perform drawing on the bitmap screens.
Term_VDG uses VTIO CoVDG while Term_win and all window descriptors use VTIO/(CoWin/Grflnt)/GrfDrv modules.
The I/O system provides system-wide, hardware-independent I/O services for user programs and OS-9 itself. All I/O system calls are received by the kernel and passed to the I/O manager for processing.
The I/O manager performs some processing, such as the allocation of data structures for the I/O path. Then, it calls the file managers and device drivers to do most of the work. Additional file manager, device driver, and device descriptor modules can be loaded into memory from files and used while the system is running.
The I/O Manager
The I/O manager provides the first level of service of I/O system calls. It routes data on I/O process paths to and from the appropriate file managers and device drivers.
The I/O Manager also maintains two important internal OS-9 data structures: the device table and the path table. Never modify the I/O manager.
When a path is opened, the I/O manager tries to link to a memory module that has the device name given or implied in the pathlist. This module is the device descriptor. It contains the names of the device driver and file manager for the device. The I/O manager saves the names so later system calls can be routed to these modules.
File Managers
NitrOS-9 can have any number of file manager modules. Each of these modules processes the raw data stream to or from a class of device drivers that have similar operational characteristics. It removes as many unique characteristics as possible from I/O operations. Thus, it assures that similar devices conform to the NitrOS-9 standard I/O and file structure.
The file manager also is responsible for mass storage allocation and directory processing, if these are applicable to the class of devices it serves. File managers usually buffer the data stream and issue requests to the kernel for dynamic allocation of buffer memory. They can also monitor and process the data stream, for example, adding linefeed characters after carriage-return characters.
The file managers are re-entrant. The three standard NitrOS-9 file managers are:
- Random block file manager: The RBF manager supports random-access, block-structured devices such as disk systems and bubble memories. (Chapter 5 discusses the RBF manager in detail.)
- Sequential Character File Manager: The SCF manager supports single-character-oriented devices, such as CRTs or hardcopy terminals, printers, and modems. (Chapter 6 discusses SCF in detail.)
- Pipe File Manager (PIPEMAN): The pipe manager supports interprocess communication via pipes.
File Manager Structure
Every file manager must have a branch table in exactly the following format. Routines that are not used by the file manager must branch to an error routine that sets the carry and loads B with an appropriate error code before returning. Routines returning without error must ensure that the carry bit is clear.
* All routines are entered with: * (Y) = Path Descriptor pointer * (U) = Caller’s register stack pointer * EntryPt equ * lbra Create lbra Open lbra MakDir lbra ChgDir lbra Delete lbra Seek lbra Read lbra Write lbra ReadLn lbra WriteLn lbra GetStat lbra PutStat lbra Close
Create, Open
Create and Open handle file creating and opening for devices. Typically, the process involves allocating any required buffers, initializing path descriptor variables, and establishing the path name. If the file manager controls multi-file devices (RBF), directory searching is performed to find or create the specified file.
MakDir
MakDir creates a directory file on multi-file devices. MakDir is neither preceded by a Create nor followed by a Close. File managers that are incapable of supporting directories need to return carry set with an appropriate error code in Register B.
ChgDir
On multi-file devices, ChgDir searches for a directory file. If ChgDir finds the directory, it saves the address of the directory (up to four bytes) in the caller’s process descriptor. The descriptor is located at P$DIO + 2 (for a data directory) or P$DIO + 8 (for an execution directory).
In the case of the RBF manager, the address of the directory’s file descriptor is saved. Open/Create begins searching in the current directory when the caller’s pathlist does not begin with a slash. File managers that do not support directories should return the carry set and an appropriate error code in Register B.
Delete
Multi-file device managers handle file delete requests by initiating a directory search that is similar to Open. Once a device manager finds the file, it removes the file from the directory.
Any media in use by the file are returned to unused status. In the case of the RBF manager, space is returned for system use and is marked as available in the free cluster bitmap on the disk. File managers that do not support multi-file devices return an error.
Seek
File managers that support random access devices use Seek to position file pointers of an already open path to the byte specified. Typically, the positioning is a logical movement. No error is produced at the time of the seek if the position is beyond the current “end of file.”
Normally, file managers that do not support random access ignore Seek, However, an SCF-type manager can use Seek to perform cursor positioning.
Read
Read returns the number of bytes requested to the user’s data buffer. Make sure Read returns an EOF error if there is no data available. Read must be capable of copying pure binary data, and generally performs no editing on the data. Generally, the file manager calls the device driver to actually read the data into the buffer. Then, the file manager copies the data from the buffer into the user’s data area to keep file managers device independent.
Write
The Write request, like Read, must be capable of recording pure binary data without alteration. The routines for Read and Write are almost identical with the exception that Write uses the device driver’s output routine instead of the input routine. The RBF manager and similar random access devices that use fixed length records (sectors) must often pre-read a sector before writing it, unless they are writing the entire sector. In OS-9, writing past the end of file on a device expands the file with new data.
ReadLn
ReadLn differs from Read in two respects. First, ReadLn terminates when the first end-of-line (carriage return) is encountered. ReadLn performs any input editing that is appropriate for the device. In the case of SCF, editing involves handling functions such as backspace, line deletion, and the removal of the high order bit from characters.
WriteLn
WriteLn is the counterpart of ReadLn. It calls the device driver to transfer data up to and including the first (if any) carriage return encountered. Appropriate output editing can also be performed. For example, SCF outputs a line feed, a carriage return character, and nulls (if appropriate for the device). It also pauses at the end of a screen page.
GetStat, PutStat
The GetStat (get status) and PutStat (put status) system calls are wildcard calls designed to provide a method of accessing features of a device (or file manager) that are not generally device independent. The file manager can perform specific functions such as setting the size of a file to a given value. Pass unknown status calls to the driver to provide further means of device independence. For example, a PutStat call to format a disk track might behave differently on different types of disk controllers.
Close
Close is responsible for ensuring that any output to a device is completed. (If necessary, Close writes out the last buffer.) It releases any buffer space allocated in an Open or Create. Close does not execute the device driver’s terminate routine, but can do specific end-of-file processing if you want it to, such as writing end-of-file records on disks, or form feeds on printers.
Interfacing with Device Drivers
Strictly speaking, device drivers must conform to the general format presented in this manual. The I/O Manager is slightly different because it only uses the Init and Terminate entry points.
Other entry points need only be compatible with the file manager for which the driver is written. For example, the Read entry point of an SCF driver is expected to return one byte from the device. The Read entry point of an RBF driver, on the other hand, expects Read to return an entire sector.
The following code is part of an SCF file manager. The code shows how a file manager might call a driver.
******************** * IOEXEC * Execute Device's Read/Write Routine * * Passed: (A) = Output character (write) * (X) = Device Table entry ptr * (Y) = Path Descriptor pointer * (U) = Offset of routine (D$Read,D$Write) * * Returns: (A) = Input char (read) * (B) = Error code, CC set if error * * Destroys B,CC IOEXEC pshs a,x,y,u save registers ldu V$STAT,x get static storage for driver ldx V$DRIV,x get driver module address ldd M$EXEC,x and offset of execution entries addd 5,s offset by read/write leax d,x absolute entry address lda ,s+ restore char (for write) jsr ,x execute driver read/write puls x,y,u,pc return (A)=char, (B)=error emod Module CRC Size equ * size of sequential file manager
Device Driver Modules
The device driver modules are subroutine packages that perform basic, low-level I/O transfers to or from a specific type of I/O device hardware controller. These modules are re-entrant. So, one copy of the module can concurrently run several devices that use identical I/O controllers.
Device driver modules use a standard module header, in which the module type is specified as code $Ex (device driver). The execution offset address in the module header points to a branch table that has a minimum of six 3-byte entries.
Each entry is typically an LBRA to the corresponding subroutine. The file managers call specific routines in the device driver through this table, passing a pointer to a path descriptor and passing the hardware control register address in the 6809 registers. The branch table looks like this:
Code | Meaning |
$00 | Device initialization routine |
$03 | Read form device |
$06 | Write to device |
$09 | Get device status |
$0C | Set device status |
$0F | Device termination routine |
(For a complete description of the parameters passed to these subroutines, see the “Device Driver Subroutines” sections in Chapters 5 and 6.)
NitrOS-9 Interaction with Devices
Device drivers often must wait for hardware to complete a task or for a user to enter data. Such a wait situation occurs if an SCF device driver receives a Read but there is no data is available, or if it receives a Write and no buffer space is available. NitrOS-9 drivers that encounter this situation should suspend the current process (via F$Sleep
). In this way the driver allows other processes to continue using CPU time.
The most efficient way for a driver to awaken itself and resume processing data is by using interrupt requests (IRQs). It is possible for the driver to sleep for a number of system clock ticks and then check the device or buffer for a ready signal. The drawbacks to this technique are:
- It requires the system clock to always remain active.
- It might require a large number of ticks (perhaps 20) for the device to become ready. Such a case leaves you with a dilemma. If you make the program sleep for two ticks, the system wastes CPU time while checking for device ready. If the driver sleeps 20 ticks, it does not have a good response time.
An interrupt system allows the hardware to report to the CPU and the device drivers when the device is finished with an operation. Using interrupts to its advantage, a device driver can setup interrupt handling to occur when a character is sent or received or when a disk operation is complete. There is a built-in polling facility for pausing and awakening processes. Here is a technique for handling interrupts in a device driver:
- Use the Init routine to place the driver interrupt service call (IRQSVC) routine in the IRQ polling sequence via an
F$IRQ
system call:
ldd V.Port,u get address to poll leax IRQPOLL,pcr point to IRQ packet leay IRQSERVC,pcr point to IRQ routine os9 F$IRQ add dev to poll Sequence bcs Error abnormal exit if error
- Ensure that driver programs waiting for their hardware call the sleep routine. The sleep routine copies V.Busy to V.Wake. Then, it goes to sleep for some period of time.
- When the driver program wakes up, have it check to see whether it was awakened by an interrupt or by a signal sent from some other process.
Usually, the driver performs this check by reading the V.Wake storage byte. The V.Busy byte is maintained by the file manager to be used as the process ID of the process using the driver. When V.Busy is copied into V.Wake, then V.Wake becomes a flag byte and an information byte. A non-zero Wake byte indicates that there is a process awaiting an interrupt. The value in the Wake byte indicates the process to be awakened by sending a wakeup signal as shown in the following code:
lda V.Busy,u get proc ID sta V.Wake,u arrange for wakeup andcc #^IntMasks prep for interrupts Sleep50 ldx #0 or any other tick time (if signal test ) OS9 F$Sleep await an IRQ ldx D.Proc get proc desc ptr if signal test ldb P$Signal,x i5 signal present? (if signal test) bne SigTest bra if 50 if Signal test tst V.Wake,u IRQ occur? bne Sleep50 bra if not
Note that the code labeled “if signal test” is only necessary if the driver wishes to return to the caller if a signal is sent without waiting for the device to finish. Also note that IRQs and FIRQs must be masked between the time a command is given to the device and the moving of V.Busy and V.Wake. If they are not masked, it is possible for the device IRQ to occur and the IRQSERVC routine to become confused as to whether it is sending a wakeup signal or not.
- When the device issues an interrupt, NitrOS-9 calls the routine at the address given in
F$IRQ
with the interrupts masked. Make the routine as short as possible, and have it return with an RTS instruction. IRQSERVC can verify that an interrupt has occurred for the device. It needs to clear the interrupt to retrieve any data in the device. Then the V.Wake byte communicates with the main driver module. If V.Wake is non-zero, clear it to indicate a true device interrupt and use its contents as the process ID for anF$Send
system call. TheF$Send
call sends a wakeup signal to the process. Here is an example:
ldx V.Port,u get device address tst ?? is it real interrupt from device? bne IRQSVC90 bra to error if not lda Data,x get data from device sta 0,y lda V.Wake,u beq IRQSVC80 bra if none clr V.Wake,u clear it as flag to main routine ldb #S$Wake,u get wakeup signal os9 F$Send Send Signal to driver IRQSVC80 clrb clear carry bit (all is well) rts IROSVC90 comb Set carry bit (is an IRQ call) rts
Suspend State (NitrOS-9 Level 2 only)
The Suspend State allows the elimination of the F$Send system call during interrupt handling. Because the process is already in the active queue, it need not be moved from one queue to another. The device driver IRQSERVC routine can now wake up the suspended main driver by clearing the process status byte suspend bit in the process state. Following are sample routines for the Sleep and IRQSERVC calls:
lda D.Proc get process ptr sta V.Wake,u prep for re-awakening enable device to IRQ, give command, etc. bra Cmd5O enter suspend loop Cmd30 ldx D.Proc get ptr to process desc lda P$State,x get state flag ora Suspend put proc in suspend state sta P$State,x save it in proc desc andcc #^IntMasks unmask interrupts ldx #1 give up time slice OS9 F$Sleep suspend (in active queue) Cmd50 orcc #IntMasks mask interrupts while changing state ldx D.Proc get proc desc addr (if signal test) lda P$Signal,x get signal (if signal test) beq SigProc bra if signal to be handled lda V.Wake,u true interrupt? bne Cmd30 bra if not andcc #^IntMasks assure interrupts unmasked
Note that D.Proc
is a pointer to the process descriptor of the current process. Process descriptors are always allocated on 256 byte page boundaries. Thus, having the high order byte of the address is adequate to locate the descriptor. D.Proc
is put in V.Wake
as a dual value. In one instance, it is a flag byte indicating that a process is indeed suspended. In the other instance, it is a pointer to the process descriptor which enables the IRQSERVC routine to clear the suspend bit. It is necessary to have the interrupts masked from the time the device is enabled until the suspend bit has been set. Making the interrupts ensure that the IRQSERVC routine does not think it has cleared the suspend bit before it is even set. If this happens, when the bit is set the process might go into permanent suspension. The IRQSERVC routine sample follows:
ldy V.Port,u get dev addr tst V.Wake,u is process awaiting IRQ? beq IRQSVCER no exit clear device interrupt exit if IRQ not from this device lda V.Wake,u get process ptr clrb stb V.Wake,u clear proc waiting flag tfr d,x get process descriptor ptr lda P$State,x get state flag anda #Suspend clear suspend state sta P$State,x save it clrb clear carry bit rts IRQSVCER comb Set carry bit rts
Device Descriptor Modules
Device descriptor modules are small, non-executable modules. Each one provides information that associates a specific I/O device with its logical name, hardware controller address(es), device driver, file manager name, and initialization parameters.
Unlike the device drivers and file managers, which operate on classes of devices, each device descriptor tailors its functions to a specific device. Each device must have a device descriptor.
Device descriptor modules use a standard module header, in which the module type is specified as code $Fx (device descriptor). The name of the module is the name by which the system and user know the device (the device name given in path lists).
The rest of the device descriptor header consists of the information in the following chart:
Relative Address(es) | Use |
$09,$0A | The relative address of the file manager name string address |
$0B,$0C | The relative address of the device driver name string |
$0D | Mode/Capabilities: D S PE PW PR E W R (directory, single user, public execute, public write, public read, execute, write, read) |
$0E,$0F,$10 | The absolute physical (24-bit) address of the device controller |
$11 | The number of bytes (n bytes) in the initialization table |
$12,$12 + n | Initialization table |
When OS-9 opens a path to the device, the system copies the initialization table into the option section (PD.OPT) of the path descriptor. (See “Path Descriptors” in this chapter.)
The values in this table can be used to define the operating parameters that are alterable by the Get Status and Set Status system calls (I$GetStt and I$SetStt). For example, parameters that are used when initializing terminals define which control characters are to be used for functions such as backspace and delete.
The initialization table can be a maximum of 32 bytes long. If the table is fewer than 32 bytes long, OS-9 sets the remaining values in the path descriptor to 0.
You might wish to add devices to your system. If a similar device driver already exists, all you need to do is add the new hardware and load another device descriptor. Device descriptors can be in the boot module or they can be loaded into RAM from mass-storage files while the system is running.
The following diagram illustrates the device descriptor format:
Device Descriptor Format
Name | Relative Address | Bytes | Use |
M$ID | $00-$01 | 2 | Sync Bytes ($87CD) |
M$Size | $02-$03 | 2 | Module Size (bytes) |
M$Name | $04-$05 | 2 | Offset to Module Name |
M$Type | $06 | 1 | Type / Language |
M$Revs | $07 | 1 | Attributes / Revision Level |
M$Parity | $08 | 1 | Header Parity Check |
M$FMgr | $09-$0A | 2 | File Manager Name Offset |
M$PDev | $0B-$0C | 2 | Device Driver Name Offset |
M$Mode | $0D | 1 | Mode |
M$Port | $0E-$10 | 3 | Port Address |
M$Opt | $11 | 1 | Initialization Table Size |
$12,$12 n | n | Initialization table | |
Name Strings, and so on | |||
CRC Check Value |
Path Descriptors
Every open path is represented by a data structure called a path descriptor (PD). The PD contains the information the file managers and device drivers require to perform I/O functions.
PDs are 64 bytes long and are dynamically allocated and deallocated by the I/O manager as paths are opened and closed.
They are internal data structures that are not normally referenced from user or applications programs. The description of PDs is presented here mainly for those programmers who need to write custom file managers, device drivers, or other extensions to OS-9.
PDs have three sections. The first section, which is ten bytes long, is the same for all file managers and device drivers. The information in the first section is shown in the following chart.
Path Descriptor: Standard Information
Name | Address | Bytes | Use |
PD.PD | $00 | 1 | Path number |
PD.MOD | $01 | 1 | Access mode: 1 = read, 2 = write, 3 = update |
PD.CNT | $02 | 1 | Number of open paths using this PD |
PD.DEV | $03 | 2 | Address of the associated device table entry |
PD.CPR | $05 | 1 | Current process ID |
PD.RGS | $06 | 2 | Address of the caller’s register stack |
PD.BUF | $08 | 2 | Address of the 256-byte data buffer (if used) |
PD.FST | $0A | 22 | Defined by the file manager |
PD.OPT | $20 | 32 | Reserved for the GetStat/SetStat options |
PD.FST is 22-byte storage reserved for and defined by each type of file manager for file pointers, permanent variables, and so on.
PD.OPT is a 32-byte option area used for file or device operating parameters that are dynamically alterable. When the path is opened, the I/O manager initializes these variables by copying the initialization table that is in the device descriptor module. User programs can change the values later, using the Get Status and Set Status system calls.
PD.FST and PD.OPT are defined for the file manager in the assembly-language equate file (SCFDefs for the SCF manager or RBFDefs for the RBF manager).