|C H A P T E R 6|
This chapter covers the following topics:
A package is the set of methods and properties that resides in a device node. A support package is a group of functions or methods that implements a specific interface. A package implements a library of functions that may then be called by FCode programs.
For many devices this is not particularly useful, but it will be useful for FCode programs that:
A plug-in package is a package that is not permanently resident in the main OpenBoot PROM. Plug-in packages are written in FCode. Since FCode is represented with a machine-independent binary format, it lets the same plug-in packages be used on machines with different CPU instruction sets.
During the linking process, a package's references to OpenBoot PROM system functions are resolved and the functions defined by the package made available to other parts of OpenBoot. This occurs at run-time when OpenBoot interprets (probes) the package. Thus, plug-in packages do not need to be prelinked with a particular OpenBoot implementation.
OpenBoot only needs the beginning address of the package in order to probe it. Once probed, the package becomes a working part of OpenBoot until the system is reset or turned off. A package exports its interface to OpenBoot, and to other packages, as a vocabulary of Forth words.
Many packages implement a specific interface which uses a standard set of functions. Different packages may implement the same interface. For example, there may be two display device driver packages, each implementing the standard display device interface, but for two different display devices.
There may also be multiple instances of a single package. For example, a plug-in disk driver may have as many instances as there are disks of that type.
A package consists of:
The active package is the package whose methods are currently visible. dev and find-device can be used to change the active package. However, they only make a package's methods visible; they do not enable the execution of those methods.
Before a package's methods may be executed, an instance of the package must be created. Think of an instance as a working copy of the package. An instance contains a working copy of all of the package's private data.
An instance is created from a package by opening that package. The act of opening a package allocates memory for the instance's data and sets the contents of that memory to the initial values stored in the package. The instance exists until it is terminated by closing it. When it is closed, the memory used to hold that instance's private data is freed. Multiple instances may be created from the same package and exist simultaneously.
The current instance is the instance whose private data and methods are available for direct use, that is, directly by name without having to use $call-method.
When a package method accesses a data item, it refers to the copy of that data item associated with the current instance. The private data of the current instance is accessible; the private data of all other instances is inaccessible. Furthermore, to use the methods of a package, an instance of that package must be (at least temporarily) the current instance.
A package to be opened is described by a device path or device alias. The process of opening the package includes opening each of the nodes in the device path from the root to the specified device (from the top of the chain to the bottom). As each of these nodes is opened, an instance is created for the node and all of these instances are linked together in an instance chain as shown in FIGURE 6-1. When a method is accessed using the ihandle of the chain, each node in the chain is able to access the methods of its parent with $call-parent using the links provided by the instance chain.
When the chain is no longer needed, the individual instances of the chain may be closed or the entire chain may be closed. When closing the entire chain, the chain is closed from bottom to top to enable a given node's close method to use parental methods.
The current instance is a dynamic entity. It is changed in several different ways under several different circumstances. Specifically:
This causes any instance data/methods that are subsequently created (prior to the execution of finish-device) to be added to this node, and enables their later execution when an instance of this node is made current.
If a package is in the node /packages, then $open-package can be used to create an instance of the package. Unlike packages opened with open-dev, packages opened with $open-package are opened without opening their ancestors. Each time a package instance is created by $open-package, that instance is attached to the one that called $open-package. FIGURE 6-2 shows the modified instance chain that results when the /iommu/sbus/ledma/le instance opens the obp-tftp support package using $open-package.
Notice that the only additional instance created is one for the obp-tftp package and that this instance is linked to the /iommu/sbus/ledma/le instance. If another instance of obp-tftp were opened by an instance in another instance chain, the resulting instance of obp-tftp would have no association with the instance shown in FIGURE 6-2.
Package data is named, read/write RAM storage used by package methods. Individual data items can be either initialized or zero-filled and either static or instance-specific.
Initialized data items are created by the Forth defining words defer, value and variable. Uninitialized data items are created by buffer:. Preceding the defining word with the Forth word instance causes the defining word to create an instance-specific item; otherwise it creates a static data item.
Static data items are used for information that applies equally to all instances of the associated package. For example, virtual addresses of shared hardware resources, reference counts, and hardware-dependent configuration data are often stored as static data.
Instance-specific data items are used for information that differs between instances of the same package. For example, a package that provides a driver for a SCSI host adapter might have several simultaneous instances on behalf of several different target devices; each instance might need to maintain individual state information (for example, the negotiated synchronous transfer rate) for its target.
There are several different kinds of package methods, depending on the environment in which they are called and their use of static and instance-specific data.
Static methods do not:
Static methods can be called when there is no open instance of their package. When there is no instance, there is also no parent instance (which is the reason for the prohibition about calling parent methods).
The most important example of static methods is the decode-unit method which is called by the system during the process of searching the device tree without opening all of the nodes that are encountered.
Instance-specific methods are permitted to:
There is no structural difference between static and instance-specific methods. The concept of static methods is just a terse way of saying that some methods have to obey the restrictions outlined above. Instance-specific methods are the usual case; the static methods restrictions apply only to a very small set of special-purpose methods.
A method is identified by its execution token, xt. For words in the package being defined, the Forth word ['] returns an execution token. The execution token is returned by find-method for other packages. (See the following sections for more details.)
The execution token is used to execute a method in another package, and also to schedule a method for automatic, repeated execution by the system clock interrupt. See the alarm FCode in Chapter 14.
A package can call its own methods simply by naming the target method in a Forth colon definition. Such calls require neither a call-time name search nor a change of the current instance. The binding of name to execution behavior occurs at compile time, so subsequent redefinitions of a name do not affect previously-compiled references to old versions of that named method.
Infrequently, it may be desirable to call a method in the same package so that the name search happens at run-time. To do so, use either $call-method or find-method/call-package with my-self as the ihandle argument. (See the next section for details.)
Packages often use methods of other previously-defined packages. There are two types of packages whose methods can be used directly:
A package definition is identified by its phandle. find-package returns the phandle of a package in the /packages node. The phandle can then used to open that support package or to examine its properties. For example:
returns either false (package not found), or phandle true.
Opening a support package with open-package returns an ihandle. This ihandle is used primarily to call the methods of the support package and to close the support package when it is no longer needed.
The ihandle of the current instance is returned by my-self. An instance argument string must be supplied when opening any package (it may be null). The instance argument string can then be accessed from within the opened package with the my-args FCode (see below for details). For example (assume that phandle has already been found):
If the package cannot be opened, an ihandle of 0 is returned.
$open-package includes the functions of find-package and open-package. In most cases, it can be used in their place. The primitive functions find-package and open-package are rarely used directly, although find-package is sometimes used when it's necessary to examine a support package's properties without opening it.
The following FCode functions are used to find and open packages (in the packages node):
Here is an example of using $open-package:
Don't confuse phandle with ihandle. Here's how to use them:
1. Open the package with $open-package which returns an ihandle.
2. Use the ihandle to call the methods of the package.
3. When done calling the methods of the package, use the ihandle to close the instance of the package with close-package.
A package's phandle is primarily used to access the package's properties which are never instance-specific. Use ihandle>phandle to find the phandle of an open package. my-self and my-parent return ihandles, which can be converted into phandles with ihandle>phandle.
The following functions enable the calling of methods of other packages:
$call-parent is used most-often, but is the least flexible of the preceding methods; it is exactly equivalent to the sequence my-parent $call-method. Most inter-package method calling involves calling the methods of one's parent; $call-parent conveniently encapsulates that process.
$call-method can call methods of non-parent packages. It is most commonly used for calling methods of support packages. The ihandle argument of
$call-method identifies the package instance whose method is to be called.
Both $call-parent and $call-method identify their target method by name. The method-str method-len arguments denote a text string that $call-parent or $call-method uses to search for a method of the same name in the target instance's list of methods. Obviously, this run-time name search is not as fast as directly executing a method whose address is already known. However:
Consequently, the length of time spent searching is usually not a limiting factor.
A more complete example demonstrates the use of $open-package and $call-method:
When method name search time is a limiting factor, use find-method to perform the name search once. Then use call-package repetitively thereafter. find-method returns, and call-package expects, an execution token by which a method can be called quickly.
A more complex example that is somewhat faster if called repeatedly:
Because device access time often dominates I/O operations, the benefit of this extra code probably won't be noticed. It is only justified if the particular method will be called often.
Another use of find-method is to determine whether or not a package has a method with a particular name. This allows you to add new methods to an existing package interface definition without requiring version numbers to denote which new or optional methods a package implements.
With $call-method and $call-parent, the method name search is performed on every call. Consequently, if a new method (either one with a new name or with the same name as a previously-existing name) is created, any subsequent uses of $call-method or $call-parent naming that method will find the new one. On the other hand, find-method binds a name to an execution token and subsequent redefinitions of that name do not affect the previous execution token, so subsequent uses of $call-method continue to call the previous definition. In practice, this difference is rarely important, since it is quite unusual for new methods to be created when a package is already open. The one case where methods are routinely redefined under these circumstances is when a programmer does it explicitly during a debugging session; such redefinition is a powerful debugging technique.
All of the method calling functions described previously change the current instance to the instance of the callee for the duration of the call, restoring it to the instance of the caller on return.
In addition to the inter- and intra-package method calling techniques just described, there is another way of calling methods. execute-device-method and its variant apply allow a user to invoke a method of a particular package as a self-contained operation without explicitly opening and closing the package as separate operations. execute-device-method first opens all the package's parents, then calls the named method, and then closes all the parents. apply performs the same functions as execute-device-method, but it takes its arguments from the command line instead of from the Forth stack.
execute-device-method and apply are most often used for methods like selftest. selftest methods are usually called with the test user interface command, which is usually implemented with execute-device-method.
Methods that are intended to be called with execute-device-method or its equivalent must not assume that the package's open method has been called, because execute-device-method does not call the open method of the package containing the target method, although it opens all of the package's parents. Consequently, the target method must explicitly perform whatever initialization actions it requires, perhaps by calling the open method in the same package, or by executing some sub-sequence thereof. Before exiting, the target method must perform the corresponding close actions to undo its initialization actions.
execute-device-method was intentionally designed not to call the target's open and close methods automatically since the complete initialization sequence of open is not always appropriate for methods intended for use with execute-device-method. In particular, an open method usually puts its device in a fully operational state, while methods like selftest often need to perform a partial initialization of selected device functions.
Plug-in device drivers are plug-in packages implementing simple device drivers. The interfaces to these drivers are designed to provide basic I/O capability.
Plug-in drivers are used for such functions as booting the operating system from a device or displaying text on a device before the operating system has activated its own drivers. Plug-in drivers are added to the device tree during the probing phase of the OpenBoot PROM startup sequence.
Plug-in drivers must be programmed to handle portability issues, such as hardware alignment restrictions and byte ordering of external devices. With care, you can write a driver so that it is portable to all of the systems in which the device could be used.
Plug-in drivers are usually stored in PROM located on the device itself, so that the act of installing the device automatically makes its plug-in driver available to the OpenBoot PROM.
For devices with no provision for such a plug-in driver PROM, the plug-in driver can be located elsewhere, perhaps in PROM located on a different device or in an otherwise unused portion of the main OpenBoot PROM. However, use of such a strategy limits such a device to certain systems and system configurations.
Different packages have different collections of methods depending on the job(s) that the packages have to do. The following four methods are found in many device drivers. None of them can be considered to be required, however, since the nature of a given driver governs the methods that the driver needs.
open and close are found in many drivers, but even they are not universally required. open and close are needed only if the device will be used with open-dev or another method that calls open-dev. Any device that has read and/or write methods needs open and close, as does any parent device whose children could possibly be opened.
Another way of looking at this is that open and close are needed for devices that are used to perform a series of related operations distributed over a period of time, relative to some other calling package. open initializes the device state that is maintained during the series of later operations, and close destroys that state after the series is complete.
To illustrate, a series of write calls generated by another package is such a series. Conversely, selftest is not such a series; selftest happens as an indivisible, self-contained operation.
( -- ok? )
Prepares a package for subsequent use. open typically allocates resources, maps, initializes devices, and performs a brief sanity check (making no check at all may be acceptable). true is returned if successful, false if not. When open is called, the parent instance chain has already been opened, so this method may call its parent's methods.
( -- )
Restores a package to its "not in use" state. close typically turns off devices, unmaps, and deallocates resources. close is executed before the package's parent is closed, so the parent's methods are available to close. It is an error to close a package which is not open.
The following methods are highly recommended.
( -- )
Put the package into a "quiet" state. reset is primarily for packages that do not automatically assume a quiet state after a hardware reset, such as devices that turn on with interrupt requests asserted.
( -- error# )
Test the package. selftest is invoked by the OpenBoot test word. It returns 0 if no error is found or a package-specific error number if a failure is noticed.
test does not open the package before executing selftest, so selftest is responsible for establishing any state necessary to perform its function prior to starting the tests, and for releasing any resources allocated after completing the tests. There should be no user interaction with selftest, as the word may be called from a program with no user present.
If the device was already open when selftest is called, a new instance will still be created and destroyed. A well-written selftest should handle this possibility correctly, if appropriate.
If the device is already open, but it is not possible to perform a complete selftest without destroying the state of the device, the integrity of the open device should take precedence, and the selftest process should test only those aspects of the device that can be tested without destroying device state. The inability to fully test the device should not be reported as an error result; an error result should occur only if selftest actually finds a device fault.
The "device already open" case happens most commonly for display devices, which are often used as the console output device, and thus remain open for long periods of time. When testing a display device that is already open, it is not necessary to preserve text that may already be on the screen, but the device state should be preserved to the extent that further text output can occur and be visible after selftest exits. Any error messages that are displayed by the selftest method will be sent to the console output device. When testing an already-open display device, such error messages should be avoided during times when selftest has the device in a state where it is unable to display text.
selftest is not executed in an open/close pair. When selftest executes, a new instance is created (and destroyed). It will have its own set of variables, values, and so forth. These quantities are not normally shared with an instance opened with the normal open routine for the package.
The following examples show how to create static data items:
The data areas are shared among all open instances of the package. If a value is changed, for example, the new value will persist until it is changed again, independent of the creation and destruction of package instances.
Any open instance of a package can access and change the value of a static data item, which changes it for all other instances.
The following examples show how to create instance-specific data items whose values are not shared among open instances:
Instance-specific data areas are reinitialized when a package instance is created (usually by opening the package), so each instance gets its own copy of the data area. For example, changes to bar in one instance will not affect the contents of it in another instance. (Note that create operates across all the instances and cannot be made instance-specific.)
The total amount of data space needed for a package's instance-specific data items is remembered as part of the package definition when finish-device finishes the package definition. Also, the contents of all the variables, values, and defers at the time finish-device executes are stored as part of the package definition.
An instance of the package is created when that package is opened. Data space is allocated for that instance (the amount of which was remembered in the package definition). The portion of that data space created with variable, value, or defer is initialized from the values stored in the package definition. Data space created with buffer: is set to zero.
You can add new methods and new properties to a package definition at any time, even after finish-device has been executed for that package. To do so, select the package and create definitions or properties.
However, you cannot add new data items to a package definition after finish-device has been executed for that package. finish-device sets the size of the data space for that package, and subsequently, the size is fixed.
An instance argument (my-args) is a string that is passed to a package when it is opened. The string may contain parameters of any sort, based on the requirements of the package, or may simply be a null-string if no parameters are needed. A null string can be generated with either " " or 0 0.
The instance argument passed can be accessed from inside the package with the my-args FCode.
If the argument string contains several parameters separated by delimiter characters, you can extract the subsections from the package with left-parse-string. You can use any character as the delimiter; a comma is commonly used.
A new value for my-args is passed when a package is opened. This can happen under a number of circumstances:
The above three instances happen only once, when the package FCode is interpreted for the first time. If you want to preserve the initial value for my-args, the FCode program should copy it into a static buffer to preserve the information.
Whenever a package is re-opened, a new value for my-args is supplied. The method for supplying this new value depends on the method used to open the package, as described below.
Here is a more complicated example:
Here the string test is passed to the SUNW,fremly package as it is opened, the string print is passed to the grumpin package as it is opened, and the string 1034,5 is passed to the SUNW,fht package as it is opened.
A package's address relative to its parent package is another piece of information available to a package. Again, there are two main ways to pass this address to the package:
As an example of the first method, suppose the following package is being opened:
Then the address of the /sd package relative to the /esp package is 3,0.
The package can find its relative address with my-unit, which returns the address as a pair of numbers. The first number (high) is the number before the comma in the previous example, and the second number (low) is the number after the comma. Note that these are numbers, not strings.
As an example of the second method, suppose a test version of an FCode package is being interpreted:
Here the my-args parameters for the new FCode are null, the initial address is 3,0, and it will be placed under the /sbus node.
The initial address can be obtained through my-address and my-space. Typically, you use my-space and my-address (plus an offset) to create the package's reg property and to map in needed regions of the device.
Mappings set up by a package persist across instances unless they are explicitly unmapped. It is usually best for each new instance to do its own mappings, being sure to unmap resources as they are no longer needed.
Machines that support packages will generally also support the nvramrc facility. nvramrc is a special area in the NVRAM that can contain user interface commands to be executed by OpenBoot as the machine starts up. These commands can be used to specify behavior during startup or to define changes for later execution.
For example: Assume a card in SBus slot #2 (named XYZ,me) needs custom attributes set by the user. The contents of nvramrc would include:
After editing nvramrc, turn on the NVRAM parameter use-nvramrc? and reset the machine to activate the contents of nvramrc. See nvedit in Chapter 14 for more about editing nvramrc contents.
To modify the properties of a package, first probe the package to get it into memory, then create or modify properties by executing property or one of its short-hand forms. Normally, probing is done automatically after the nvramrc commands are executed.
See Chapter 7 for more information about properties.
The /packages node of the device tree is unique. It has children, but instead of describing a physical bus, /packages serves as a parent node for support packages. The children of /packages are general-purpose software packages not attached to any particular hardware device. The physical address space defined by /packages is a trivial one: there are no addresses. Its children are distinguished by name alone.
The children of /packages are used by other packages to perform commonly used functions. They may be opened with the FCodes open-package or $open-package and closed with close-package. IEEE Standard 1275-1994 Standard for Boot Firmware defines three support packages that are children of /packages: the Sun Disk Label Support Package, the TFTP Booting Support Package, and the Deblocker Support Package.
Disk (block) devices are random-access, block-oriented storage devices with fixed-length blocks. Disks may be subdivided into several logical partitions, as defined by a disk label--a special disk block, usually the first one, containing information about the disk. The disk driver is responsible for appropriately interpreting a disk label. The driver may use the standard support package /disk-label if it does not implement a specialized label.
/disk-label interprets a standard Sun disk label, reading any partitioning information contained in it. It includes a first-stage disk boot protocol for the standard label. load is the most important method defined by this package.
This package uses the read and seek methods of its parent (in practice, the package which opens this one to use the support routines). /disk-label defines the following methods:
Reads and verifies the disk label accessed by the read and seek methods of its parent instance. Selects a disk partition based on the text string returned by my-args. For the standard Sun disk label format, the argument is interpreted as follows:
Reads a standalone program from the standard disk boot block location for the partition specified when the package was opened. Puts the program at memory address adr, returning its length size. For the standard Sun disk format, the standalone program is 7.5 Kbytes beginning 512 bytes from the start of the partition.
Returns the 64-bit absolute byte offset x.abs corresponding to the 64-bit partition-relative byte offset x.rel. In other words, adds the byte location of the beginning of the selected partition to the number on the stack.
The /obp-tftp package implements the Internet Trivial File Transfer Protocol (TFTP) for use in network booting. It is typically used by a network device driver for its first stage network boot protocol. Again, load is the most important method defined by this package.
This package uses the read and write methods of its parent and defines the following methods:
Reads the default standalone program from the default TFTP server, putting the program at memory address adr and returning its length size. For the standard Sun TFTP booting protocol, RARP (Reverse Address Resolution Protocol) is used to acquire the IP address corresponding to the system's MAC address (equivalent to its Ethernet address). From the IP address, the default file name is constructed, of the form <Hex-IP-Address>.<architecture> (for example, C0092E49.SUN4C). Then obp-tftp tries to TFTP read that file, first trying the server that responded to the RARP request, and if that fails, then broadcasting the TFTP read request.
The /deblocker package makes it easy to implement byte-oriented device methods, using the block-oriented or record-oriented methods defined by devices such as disks or tapes. It provides a layer of buffering between the high-level byte-oriented interface and the low-level block-oriented interface. /deblocker uses the max-transfer, block-size, read-blocks and write-blocks methods of its parent and defines the following methods:
Prepares the package for subsequent use, allocating the buffers used by the deblocking process based on the values returned by the parent instance's max-transfer and block-size methods. Returns -1 if the operation succeeds, 0 otherwise.
Reads at most len bytes from the device into the memory buffer beginning at adr. Returns actual, the number of bytes actually read, or 0 if the read operation failed. Uses the parent's read-blocks method as necessary to satisfy the request, buffering any unused bytes for the next request.
Writes at most len bytes from the device into the memory buffer beginning at adr. Returns actual, the number of bytes actually read, or 0 if the write operation failed. Uses the parent's write-blocks method as necessary to satisfy the request, buffering any unused bytes for the next request.