Forth Lesson 11
Review
In the previous lesson, we learned:
- That a device node can have executable "methods" that are its device driver
- The names of a few standard methods
- That some nodes are "support packages" used by other drivers
- That some nodes contain information about OFW in general
Device Instances
A device driver - the set of methods in the device node - can maintain private data expressing the current state of the device. It is possible for several instances of a given driver to be active simultaneously. The private data can be either per-instance or per-driver.
Before you can use the driver, you have to open it. The opening process allocates and initializes storage for the per-instance data and then calls the node's "open" method, which does whatever device-specific work is necessary. An "instance handle" is thereby created. The instance handle is a reference number that is subsequently used to call the node's other methods. It lets the system establish the correct context in which those methods execute, e.g. so they can get to the private data.
The most basic way to open a driver is with open-dev, which opens a driver non-interactively, returning an "instance handle" that programs can use to call the driver's methods.
open-dev ( pathname$ -- ihandle ) Opens a device node for non-interactive use close-dev ( ihandle -- ) Close a previously-opened device instance
The pathname$ argument is a string that names either a full device pathname or an abbreviated "device alias", a shorthand name for a full pathname. For example:
ok 0 value disk-ih ok " /usb/disk" open-dev to disk-ih ok here 200 " read" disk-ih $call-method . ok disk-ih close-dev
When you open a device with open-dev, that does not affect the Forth interpreter context. open-dev does not let you access the device's methods directly from the ok prompt; instead, you must access them with $call-method as shown above. This is appropriate for using the device from other programs.
For interactive use, it is tedious to type " read" disk-ih $call-method instead of just read, which is where iselect comes into play:
iselect ( ihandle -- ) Adds an already-open device driver to the Forth interpreter context iunselect ( -- ) Removes the current device driver from the Forth interpreter context
iselect uses dev (described in a previous lesson) to add the device node's methods to the Forth search order, so you can type them directly without using $call-method. iselect also sets the system value my-self to ihandle, giving access to the per-instance data associated with that instance. If you see the message "Tried to access instance-specific data with no current instance", it means that my-self was not set - i.e. you didn't execute iselect - so Open Firmware does not know which set of per-instance data you mean.
ok " /usb/disk" open-dev to disk-ih ok disk-ih iselect ok here 200 read . ok iunselect ok disk-ih close-dev
select combines open-dev and iselect conveniently for interactive use.
select pathname ( -- ) Open the device and set the Forth interpreter context to it unselect ( -- ) Close the device that the Forth interpret context is accessing
For example:
ok select /usb/disk ok here 200 read . ok unselect
As you can see, select requires much less typing than open-dev. But select is not appropriate for use within a program. select can only access one device at a time and the method name is typed to the interactive Forth interpreter. Inside a program, you must use $call-method, which lets you compile the method name into a colon definition (as a string) and lets you deal with multiple device instances within the same program.
The main uses for the three variants are:
- open-dev - used within programs
- select - for interactive use when the device is not already open
- iselect - for interactive use a device driver that is already open - for example the display driver
Instance Chains
A device driver usually needs help from other devices, particularly those bus bridges that attach it to the system. For example, a USB mass storage driver needs help from the USB host bridge, while the USB host bridge in turn needs help from a PCI bridge to which it is attached.
The device tree structure is designed so that these dependencies "flow up the tree". A device driver can request services from its direct parent, which can request services from the grandparent, and so on. Each node is responsible for providing all the services its children need, thus isolating an individual driver from global system knowledge. This makes drivers very portable across OFW systems.
In the example above, the mass storage driver needs help from the USB host bridge, so you can't open the former until the latter is open. Similarly, the PCI bridge driver must be opened before the USB host bridge driver is open, and so on until you reach the driver at the root of the tree, which must be self-sufficient.
So the overall process of opening a device driver must start at the root of the device tree, opening each path component in order, until the final device is open. At each step, the next device can call the methods of its parent, which is already open. During the process, OFW creates an "instance chain", whereby each device instance is linked to an open instance of its parent. The final instance handle refers to the final device, but that is implicitly linked to the rest of the chain.
Closing a device proceeds in reverse order, calling first the "close" method of the final device, deallocating its per-instance data, then proceeding up the chain.
Instance Arguments
So far we have seen that a pathname can look like:
/pci/usb@f,5/scsi/disk@1
But there is an addition wrinkle that can be added - arguments:
/pci/usb@f,5:debug/scsi/disk@1:\tmp\log
In this example, "debug" is an argument to the "usb" node and "\tmp\log" is an argument to the "disk" node. Arguments can be arbitrary text strings, except that they cannot contain '/', which would interfere with the overall parsing of the pathname. (Now you know why OFW file pathnames must use '\' instead of '/'.) The argument string is made available to the node's methods, which can interpret it any way they want to.
Arguments work with devaliases too. If you have
devalias com1 /pci/isa/uart@i3f8
and you write
com1:38400
that is expanded to
/pci/isa/uart@i3f8:38400
If you want to attach an argument to an interior component of an existing devalias, you would have to either use the pathname or make a new devalias, such as:
devalias newcom /pci:debug/isa/uart@i3f8
Thus endeth the lesson.