The driver environment provides a series of interfaces for writing hardware device drivers. These interfaces consist of hardware access interfaces and driver interfaces.
The goal of this environment is to make it possible to implement hardware device drivers that can be used in various projects that may have different system environments. By providing a valid implementation of this driver environment, different projects should be able to reuse compatible device drivers in other projects without requiring a large porting effort. Additionally, by relying on well-defined driver interfaces, new drivers can be implemented that depend on other without compatibility issues.
Most of the interfaces described below can be accessed via a handle to the ps_io_ops_t interface. When a system environment is initialised, functions required for implementation of drivers will be initialised and registered to the ps_io_ops_t interface via sub-interfaces. There is a core set of interfaces that provide functionality needed for implementing basic drivers, and a possibility for additional interfaces to be registered in order to extend the driver environment for building more complicated drivers. This allows drivers that depend on other drivers to be created, as they can find interfaces to other driver systems inside ps_io_ops_t.
The following core interfaces provide access to hardware resources and are expected to be available for any driver to use:
ps_dma_man_t: Direct Memory Access (DMA) management interface for allocating and managing memory that is used in DMA transactions with hardware.ps_io_mapper_t: Memory Mapped I/O (MMIO) interface for creating virtual memory mappings to device registers.ps_io_port_ops_t: Architecture-specific I/O operations interface to facilitate any hardware I/O that is not possible through MMIO.ps_irq_ops_t Hardware interrupt handling interface for registering interrupt handlers against hardware interrupts.ps_malloc_ops_t: Memory allocation interface for performing anonymous memory allocation.ps_io_fdt_t: Device tree interface for providing access to a flattened device tree (FDT) used to query platform information.ps_interface_registration_ops_t: Interface for allowing the registration and locating of specific driver interfaces.These interfaces should be sufficient for creating a typical device driver where other devices aren't required to enable the device to be used.
Many drivers require another device to perform some sort of operation to allow their device to function correctly. One example is an UART driver that needs to communicate with a clock driver to configure its input frequency. Access to other drivers are going to depend on the system configuration as many systems will try to isolate drivers from each other for better fault-tolerance and access control policies. Therefore many driver interfaces would not exist in certain software components. The ps_interface_registration_ops_t interface is used for co-ordinating variable interface availability for a particular driver environment. If a driver has a dependency on another driver, then it can try to find the interface instance within the list of registered driver interfaces. If the environment is configured correctly, the driver interface should be available. It is also expected that once a driver has initialised it registers its own interface in the ps_interface_registration_ops_t for future users to find it.
The initialisation process refers to how a driver is initialised. The driver's entry point or constructor is how the driver environment passes the initial ps_io_ops_t reference and any initial configuration for the driver to know how to initialise itself. This entry point function can be registered under a compatibility string in a known location so that the driver environment can call it if it wishes to instantiate a driver for a particular device.
int imx6_uart_init(ps_io_ops_t *io_ops, const char* device_path)
In the above function, a i.MX6 UART driver can be created by calling imx6_uart_init with a ps_io_ops_t and a string to a UART device in the FDT. These initial variables are enough for the driver to locate the device node in the FDT, create MMIO mappings, register an interrupt handler, configure the input clock frequency, take the device out of reset before initialising the device registers, and finally registering a serial driver interface with ps_io_ops_t.
Driver configuration is usually provided via properties in its device node in the FDT. This allows a driver to define its own configuration values that it will understand. The only initial configuration required would be to specify which device in the device tree that the driver should be configuring. When the driver finishes initialising and registers an initialised driver interface, it can provide configuration information to any future users via setting key-value pair attributes. This is intended to be a way to provide any configuration information that is ‘outside’ of the interface that is being registered. This is how multiple interfaces of the same type can be registered while still allowing the eventual user to make an informed choice about which interface instance to use.
In order to ensure re-usability of drivers in many different operating system environments that may have different software execution models, we require that all driver environments serialise any calls to driver functions essentially treating anything that ps_io_ops_t has been given to as a critical section. This means that drivers should also be written with this assumption in mind and not perform operations that expect to block for indefinite amounts of time. Most devices tend to be designed in a way to allow drivers to be designed as event handlers that maintain a state machine and perform fairly short running operations in response to new events that may be generated by either a hardware event, or a call from a user. This is also why there are no interfaces for creating additional threads of control. Mechanisms to support concurrency may be added in the future but are currently out of scope of this driver environment.
Drivers can assume the existence of some sort of standard C library existing that they can link against. However, they should be careful to only use functions that don't violate the assumptions of this driver environment. Additional libraries are also able to be linked for implementations of data structures a driver needs to operate correctly. In the future the available standard library functions may be more restricted.
Access control relates to how certain operations may be restricted or allowed depending on some sort of contextual information. Access control policies can be implemented and enforced by any interface implementation. A driver should generally trust any interface that it is calling but not necessarily trust things calling it. Generally it is assumed that all software executing in the same protection domain trusts itself. This assumption means that drivers being called across a local interface will usually trust the caller. (This doesn‘t mean that it should expect all calls to be correct). A driver should expect that any resources that it doesn’t have access to will have strong access control mechanisms that prevent it from being able to access them, usually implemented with hardware mechanisms, and that any access control policies that a driver wants to implement will be able to be reinforced by hardware mechanisms also.
Fault tolerance refers to how a system will respond due to errors that occur while it is in operation. Strong component isolation is expected to provide most of the fault tolerance in a system and as such some drivers may not be required to be implemented to a high degree of reliability if they aren‘t expected to be used in any critical pathways. This is in contrast to systems without strong component isolation where a crashing driver could result in the entire system crashing. This doesn’t mean that a driver should abort if it gets unexpected inputs. It is encouraged that all drivers at least return an error response and consider logging an error in situations where they encounter unrecoverable situations.
Remote drivers are a name given to drivers that operate in a different software component from their users. This means that driver requests cannot be performed via a simple function call and some sort of remote communication mechanism is required. Remote drivers are able to more effectively implement access control policies and distrust their users as they have an isolation boundary protecting them from any attempted malicious actions by their users. A remote driver may have a different interface type than if the driver was in the same software component as it is not always possible to transparently move software into a different execution domain. A driver that depends on other device drivers that may be remote may need to be able to handle interracting with the other driver using different interfaces. Implementing remote drivers is out of scope of this current section as providing operating system independent interfaces for implementing communication with remote components is out of scope of this environment at the moment. Remote drivers can still be accessed via interfaces that are registered with ps_interface_registration_ops_t, only the interface implementations are out of scope.
As the goal of this driver environment is to make reusing device drivers across projects easier, mechanisms will likely be needed for handling compatibility issues as the common interfaces are evolved. Given that this framework is currently considered under development, there isn't expected to be many mechanisms for ensuring compatibility. We currently expect version control software to be used to synchronise sources, and that binary distribution of artifacts is unsupported. Informal compatibility mechanisms such as interface and CHANGES documentation and more formal mechanisms such as typed interfaces should be used to provide some form of compatibility assurance.
This driver environment is designed with the intention to support both dynamic and static implementations. A static environment refers to an environment where the topology of software and hardware components is unchanging after intitialisation and resource consumption behavior is known ahead of time. In static environments, many of the interface implementations already expect to give specific resources to certain components and also don't expect to have to destroy instances in response to changing environment. Dynamic environments should also be supported and the interfaces listed above should support dynamic implementations such as on-demand memory mapping or destruction of entire components. Supporting static environments is currently a higher priority than dynamic environments, so there may be areas where mechanisms for dynamic implementations are hindered based on static assumptions. This is an area where interfaces should evolve to be more accomodating to dynamic implementations.
This driver environment is also designed with the assumption that drivers don‘t all exist in the same software protection domain. This prevents drivers from being able to informally access other resources that aren’t explicitly available as they may be located in different protection domains. Additionally, certain interfaces may be designed with certain trade-offs made in favour of remote implementations. These special cases explained just now may indicate that a different interface is needed that is more specialised to a particular use case.
Driver initialisation starts with a driver function call that starts the initialisation. There are not currently a wide range of protocols in place for a driver to make itself discoverable to an environment. The current mechanisms either require the driver to provide a unique symbol name that the environment somehow knows to call, or by placing the function in a unique global variable with some metadata that is then added to a special linker section that the driver environment can perform searches on. As the amount of drivers that are available grows, a wider range of driver discovery mechanisms may be provided. This would then require additional support to be added to each driver if it wanted to take advantage of the additional mechanisms.
As mentioned in the usage section, concurrency is not supported and there are no mechanisms for creating additional execution contexts. This functionality may be added in the future once more use-cases are available to enable a satisfactory design and these mechanisms become more prioritised.
The core set of library functions available could become more well defined in the future. This may be by either making it possible for drivers to define their own standard library, or by developing a smaller, libcore that is usable in all driver environments. Each environment likely wants to control its own standard library and so it may be difficult to take a strong policy choice here.