Note
See FdtBusPkg Documentation Style and Terms Definitions first.
DT (Devicetree) device drivers manage DT controllers. Device handles for supported DT controllers are created by a Devicetree bus driver (e.g. FdtBusDxe).
There are two approaches to writing such drivers. The preferred mechanism is to follow the UEFI Driver Model by implementing driver binding. The alternative approach (called legacy in this document) may be suitable under some circumstances.
See SampleDeviceDxe for a basic template of a UEFI Driver Model driver.
See SampleBusDxe for a basic template of a UEFI Driver Model driver that is also a bus driver, producing DT controller children.
See HighMemDxe for an example of a driver that can be compiled as either a UEFI Driver Model driver or a legacy driver.
A DT device driver following the UEFI Driver Model does so by
installing an EFI_DRIVER_BINDING_PROTOCOL
on the the driver image
handle. The UEFI driver dispatch logic will then use installed
protocol to match device drivers against available device handles.
A DT device driver typically does not create any new device handles. Instead, it attaches a protocol instance to the device handle of the DT controller. These protocol instances are I/O abstractions that allow the DT controller to be used in the preboot environment. The most common I/O abstractions are used to boot an EFI compliant OS.
The following figure shows the device handle for a DT controller
before and after Start()
Driver Binding Protocol function is
called. In this example, a DT device driver is adding the Block I/O
Protocol to the device handle for the DT controller.
stateDiagram-v2
classDef Gray fill:grey,color:white;
state "DT Controller Device Handle" as C1 {
EFI_DEVICE_PATH_PROTOCOL1: EFI_DEVICE_PATH_PROTOCOL
EFI_DT_IO_PROTOCOL1: EFI_DT_IO_PROTOCOL
}
state "DT Controller Device Handle" as C2 {
EFI_DEVICE_PATH_PROTOCOL2: EFI_DEVICE_PATH_PROTOCOL
EFI_DT_IO_PROTOCOL2: EFI_DT_IO_PROTOCOL
EFI_BLOCK_IO_PROTOCOL
note left of EFI_BLOCK_IO_PROTOCOL: Installed by Start()\nUninstalled by Stop()
}
C1 --> C2: Start() opens DT I/O
C2 --> C1: Stop() closes DT I/O
class EFI_BLOCK_IO_PROTOCOL Gray
The Driver Binding Protocol contains three services. These are
Supported()
, Start()
, and Stop()
.
Supported()
tests to see if the DT
device driver can manage a device handle. A DT device driver can only
manage device handles that contain the Device Path Protocol and the
Devicetree I/O Protocol, so a DT device driver must look for these two
protocols on the device handle that is being tested.
In addition, the Supported()
function needs to check if the DT controller
can be managed. This is typically done by using Devicetree I/O Protocol
functions to check against supported compatible (identification) and
other expected property values (such as device status):
BOOLEAN Supported;
Supported = !EFI_ERROR (DtIo->IsCompatible (DtIo, "device-compat-string")) &&
DtIo->DeviceStatus == EFI_DT_STATUS_OKAY;
The Start()
function tells the DT device driver to start managing a
DT controller. First, the Start()
function needs to use the OpenProtocol()
Boot Service with BY_DRIVER
to open the relevant protocols, like the DT
I/O Protocol.
A DT device driver typically does not create any new device handles. Instead, it installs one or more additional protocol instances on the device handle for the DT controller.
The Stop()
function mirrors the Start()
function, so the Stop()
function completes any outstanding transactions to the DT controller
and removes the protocol interfaces that were installed in
Start()
.
Some DT device drivers may need to create new device handles. Such a driver acts like a bus driver.
VirtioFdtDxe is a good example of a DT controller device driver creating a new device handle that is NOT a DT controller.
In some situations, a DT controller's children are actually DT controllers that need to be enumerated. See SampleBusDxe for sample code. A good example may be supporting a Devicetree node for a composite device such as a NIC or graphics.
Let's examine a Devicetree snippet:
genet: ethernet@7d580000 {
compatible = "brcm,bcm2711-genet-v5";
...
genet_mdio: mdio@e14 {
compatible = "brcm,genet-mdio-v5";
...
phy1: ethernet-phy@1 {
compatible = "phy-driver-compat-string";
reg = <0x1>;
}
};
};
In this example, the NIC driver would bind to the genet
device and
enumerate the MDIO device (which would probably bind to the same
NIC driver). In the context of the MDIO device, it would then
enumerate the phy1
device, and continue initialization
once a PHY driver loads and publishes an interface.
Tip
How could a driver managing a DT controller and its children
be able to relate a DT controller EFI_HANDLE
to another
another device being managed? You can use the ParentDevice
structure field in EFI_DT_IO_PROTOCOL
.
Such drivers have additional EFI_DT_IO_PROTOCOL
-specific
operations they need to perform in their Start()
and Stop()
Driver
Binding Protocol functions.
Note
You might wonder why these child DT controllers cannot all be automatically enumerated by FdtBusDxe. That would imply FdtBusDxe would be managing every DT controller, which would prevent any other driver from starting on the created DT handles! FdtBusDxe binds to a very small set of DT controller types.
The Supported()
call may need to check if the child controller
specified by RemainingDevicePath
is supported.
DT device drivers that need to enumerate further child DT controllers
can do so via the ScanChildren()
Devicetree I/O Protocol function. A
driver has the option of creating all of its children in one call to
Start()
, or spreading it across several calls to Start()
. In
general, if it is possible to design a driver to create one child at a
time (e.g. the child is not some intrinsic criticial component of the
device), it should do so to support the rapid boot capability in the
UEFI Driver Model. DT device drivers enumerating child DT controllers
may also register callback via the SetCallbacks()
Devicetree I/O
Protocol function, to directly handle child register reads and writes.
In the example given above, PHY driver register accesses would be
generally handled by the MDIO driver.
SampleBusDxe demonstrates SetCallbacks()
use. You can use the DtReg tool to test I/O
against the register read callback.
If the DT device driver enumerated further child DT
controllers, these need to be cleaned up via the RemoveChild()
Devicetree I/O Protocol function. If DT bus driver callbacks were
registered, these must be unregistered via an appropriate SetCallbacks()
Devicetree I/O Protocol function call.
Legacy drivers are generally outside the realm of bus-enumerated
device handles. These typically directly publish a protocol and
hardcode the device details. However, there may be objective reasons
why a driver makes use of a device handle with an EFI_DT_IO_PROTOCOL
but bypasses driver binding.
Tiano has a notion of "library drivers". For example, there's a
generic Serial DXE driver, where the actual hardware interaction is
encapsulated entirely by SerialPortLib, where the SerialPortLib
interface is generic enough for a library to be linked to SEC, PEI, DXE
or even MM images. Similarly, PciHostBridgeDxe relies on
PciHostBridgeLib for discovery of PCIe RC information. These
drivers do not publish an EFI_DRIVER_BINDING_PROTOCOL
. In
an environment with DT controllers, they rely on libraries to
fully encapsulate any discovery and interaction with
EFI_DT_IO_PROTOCOL
-bearing device handles.
Tip
You may wonder if using DT I/O Protocol is warranted for a legacy driver, compared to older approaches like FdtClientDxe or even manual Devicetree parsing. Using DT I/O Protocol is always better and results in simpler, more correct and more portable code. For example, the DT I/O layer handles proper property parsing, translation of reg and ranges addresses, DMA and I/O mechanics, etc. PciHostBridgeLibEcam and HighMemDxe are great examples of code being improved by moving to the DT I/O Protocol (compare to OvmfPkg/Fdt/HighMemDxe and OvmfPkg/Library/PciHostBridgeUtilityLib).
The following figure demonstrates how a legacy driver or library can locate supported DT controllers.
stateDiagram-v2
C1: HandleBuffer = LocateHandleBuffer(gEfiDtIoProtocolGuid)
state "Foreach EFI_HANDLE in HandleBuffer" as C2 {
D1: DtIo = OpenProtocol (Handle, BY_DRIVER)
D2: DtIo->IsCompatible ("device-compat-string")
D3: DtIo->DeviceStatus == EFI_DT_STATUS_OKAY
D5: CloseProtocol (Handle)
D4: ProcessController (Handle)
}
C1 --> C2
C2 --> C3
D1 --> D2: EFI_SUCCESS
D2 --> D3: Yes
D2 --> D5: No
D3 --> D4: Yes
D3 --> D5: No
D4 --> D5
C3: FreePool(HandleBuffer)
The steps are:
- Call the
LocateHandleBuffer
UEFI Boot Service with thegEfiDtIoProtocolGuid
. - For every handle:
- Locate the DT I/O Protocol on the handle.
- If the controller is meant to be exclusively used by the driver or library
(e.g. talking to hardware), call
OpenProtocol()
Boot Service withBY_DRIVER
to get the DT I/O Protocol. This ensures the driver doesn't start using a controller that is already managed by another driver. It also ensures that other (well behaved) drivers won't use the controller until it is released. - If the resource is meant to be shared with other components (e.g. SerialPortLib),
or if the driver or library simply wants to query some information about the controller
(i.e. not talk to hardware), use
HandleProtocol()
.
- If the controller is meant to be exclusively used by the driver or library
(e.g. talking to hardware), call
- Use the
IsCompatible()
Devicetree I/O Protocol call to identify supported controllers. - Filter out controllers with
DtIo->DeviceStatus != EFI_DT_STATUS_OKAY
. - Call
CloseProtocol()
Boot Service on unsupported controllers ifOpenProtocol()
was used.
- Locate the DT I/O Protocol on the handle.
- Free handle buffer.
Caution
Failing to close unsupported controllers will result in other drivers not being able to start on their device handles!
See PciHostBridgeLibEcam for an example.
This library has a dependency on gEfiDtIoProtocolGuid
,
as the supported controllers are expected to be enumerated once
FdtBusDxe loads, so the availability of a DT controller present in the system is sufficient.
PciHostBridgeLibEcam only has a single user - PciHostBridgeLibEcam,
so the DT I/O protocol is located using OpenProtocol
with BY_DRIVER
.
It's easy to identify DT controllers that are managed by a legacy
driver that uses OpenProtocol()
as suggested. These are listed as Legacy-Managed Device
:
Shell> devtree
...
Ctrl[2A] DT(DtRoot)
Ctrl[2C] DT(reserved-memory)
Ctrl[2D] DT(fw-cfg@10100000)
Ctrl[2E] DT(flash@20000000)
Ctrl[2F] DT(chosen)
Ctrl[30] DT(poweroff)
Ctrl[31] DT(reboot)
Ctrl[32] DT(platform-bus@4000000)
Ctrl[33] Legacy-Managed Device
Ctrl[34] DT(cpus)
...
Instead of enumerating existing handles, a legacy driver could directly look up a device, if the path to the device is known.
DtIo = FbpGetDtRoot ();
ASSERT (DtIo != NULL);
Status = DtIo->Lookup (DtIo, "/soc/serial@10000000", TRUE, &Handle);
ASSERT_EFI_ERROR (Status);
Status = gBS->HandleProtocol (
Handle,
&gEfiDtIoProtocol,
(VOID **)&DtIo
);
ASSERT_EFI_ERROR (Status);
//
// Perform OpenProtocol (Handle, BY_DRIVER) and other steps
// as before.
//
The example first looks up the EFI_DT_IO_PROTOCOL
for the root DT
controller using the FbpGetDtRoot()
function provided by the
convenience FbpUtilsLib library. The code then does a lookup by
an absolute path or alias of a UART device, connecting any missing
DT controllers along the way (provided they have UEFI Driver Model
drivers!).
This approach is more straighforward and can deal with the looked-up device not being enumerated at the time of invocation, but requires knowing the full path or alias to the device, which is highly hardware and platform specific.
The following figure demonstrates supporting controllers enumerated after the legacy driver loads. Some controllers are handled immediately, while other ones are picked up in the future as they are enumerated.
stateDiagram-v2
C1: HandleBuffer = LocateHandleBuffer(gEfiDtIoProtocolGuid)
state "Process Handle" as C3 {
D1: DtIo = OpenProtocol (Handle, BY_DRIVER)
D2: DtIo->IsCompatible ("device-compat-string")
D3: DtIo->DeviceStatus == EFI_DT_STATUS_OKAY
D5: CloseProtocol (Handle)
D4: ProcessController (Handle)
}
state "Foreach EFI_HANDLE in HandleBuffer" as C2 {
C2P0: Process Handle
}
state "Register DT I/O Notification Callback" as C5 {
E1: Event = CreateEvent (DtIoInstallCallback)
E2: RegisterProtocolNotify (gEfiDtIoProtocolGuid, Event)
}
state "DtIoInstallCallback" as C6 {
C6P0: Process Handle
}
C1 --> C2: EFI_SUCCESS
C1 --> C5: EFI_NOT_FOUND
D1 --> D2: EFI_SUCCESS
D2 --> D3: Yes
D2 --> D5: No
D3 --> D4: Yes
D3 --> D5: No
D4 --> D5
C2P0 --> C3
C6P0 --> C3
E1 --> E2: EFI_SUCCESS
C2 --> C4
C4: FreePool(HandleBuffer)
The steps are:
- Register a protocol notification callback on
gEfiDtIoProtocolGuid
. - Identify the DT controller handles inside the notification callback via
LocateHandle
Boot Service usingByRegisterNotify
. - Locate the DT I/O Protocol as appropriate.
- Close the notification callback event if no more DT controllers are expected.
See FdtPciPcdProducerLib for an example.
This library is linked into a number of drivers,
including CpuDxe. The latter is a dependency for FdtBusDxe (as it
publishes EFI_CPU_IO2_PROTOCOL
), so it is not possible to use a
[Depex]
dependency on gEfiDtIoProtocolGuid
. Instead, if
LocateHandleBuffer
fails because the library is used before
FdtBusDxe is loads, a protocol notification callback
is set.
Because this library is linked into a number of drivers, and because
its interaction with the DT controller is limited to querying a few
properties about it, the DT I/O Protocol is located using HandleProtocol
and not OpenProtocol
.
Caution
If allocating resources in a library, don't forget to clean these up in a destructor function. Failure to close events in a library will cause crashes when a callback is invoked in an unloaded driver!
Yes, legacy drivers are awkward and messy. This is why the UEFI Driver Model exists!
Aside from figuring out device discovery (legacy vs. UEFI Driver Model) the next big question to solve is how to interact with a device.
The GetReg()
DT I/O Protocol function is similar to fetching the BAR info for a PCI device.
GetReg()
will populate an EFI_DT_REG
descriptor.
For some drivers, it will be easy enough to simply use appropriate DT
I/O Protocol functions (ReadReg()
and friends), which operate
directly on the EFI_DT_REG
descriptor.
PciSioSerialDxe is a
good example.
See notes on register access API.
Other drivers may be a bit more involved. Maybe you need the actual
CPU address. Maybe you'll need the untranslated bus address. Maybe
you'll need the length of the register
region. VirtioFdtDxe is a
good example: it actually creates a child device for the managed DT
controller, to which a generic (Virtio10) driver binds. This generic
driver doesn't know anything about FDT or PCI controllers, so the
VIRTIO_MMIO_DEVICE
needs the actual CPU address of the device.
The TranslatedBase
field of a register descriptor is a CPU address
if the BusDtIo
field is NULL, meaning that FdtBusDxe was able
to translate the bus address to a CPU address.
If the field is not NULL, then TranslatedBase
is a bus address that is valid in the
context of the ancestor DT controller referenced by BusDtIo
, and you
can only perform I/O using the DT I/O Protocol functions (ReadReg()
and
friends, and only if the ancestor device driver implements the I/O
callbacks).
You can use the FbpRegToPhysicalAddress()
function in the convenience
FbpUtilsLib library as a short cut.
Interrupts are not really used in the UEFI environment, outside of an
implementation of the EFI_TIMER_ARCH_PROTOCOL
.
The interrupt information is provided using the interrupts and (optionally)
interrupt-parent properties for a DT controller. This information can be used
to look up an EFI_DT_INTERRUPT_PROTOCOL
instance and register a handler.
In practice, the described interrupt may actually need to be looked up
via an interrupt nexus before arriving at an interrupt controller.
The FbpInterruptGet()
function in the convenience FbpInterruptUtilsLib
library implements this fairly complicated logic.
Note
FbpInterruptGet()
logic is not part of EFI_DT_IO_PROTOCOL
, as it is
functionality that is, in practice, used at most by one DT controller
driver.
Here's a code example:
...
//
// Grab the first interrupt described in the interrupts property,
// translating it into an EFI_HANDLE for the interrupt controller
// and the interrupt specifier.
//
Status = FbpInterruptGet (DtIo, 0, &InterruptParent, &InterruptData);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_ERROR, "%a: FbpInterruptGet: %r\n", __func__, Status));
goto out;
}
//
// Interrupt controller drivers don't go away, so can use the simpler HandleProtocol instead of
// OpenProtocol.
//
Status = gBS->HandleProtocol (InterruptParent, &gEfiDtInterruptProtocolGuid, (VOID **)&TimerInstance->DtInterrupt);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_ERROR, "%a: HandleProtocol(EfiDtInterruptProtocolGuid): %r\n", __func__, Status));
goto out;
}
Status = TimerInstance->DtInterrupt->RegisterInterrupt (
TimerInstance->DtInterrupt,
&InterruptData,
TimerInterruptHandler,
TimerInstance,
&TimerInstance->Cookie
);
...
Typically, a UEFI environment only initializes the devices required to boot an OS. Here we are, of course, only talking about device drives that comply to the UEFI Driver Model. UEFI firmware usually initializes every possible device (e.g. connects drivers to controllers) only in situations, when there is no known OS to boot (e.g. when there is a need to enumerate every possible boot device, or when entering a setup utility).
What about the devices that are not in the boot path?
Sometimes the device needs to be initialized at an early phase. For example, VariableRuntimeDxe requires the flash to be accessible, which may introduce a dependency on the NOR flash driver.
Another example are drivers that implement architectural protocols, such
as EFI_TIMER_ARCH_PROTOCOL
. These need to be available prior to the
BDS phase.
This can be resolved using the DXE APRIORI list, which will load the specified drivers in the specified order before dispatching anything else. For example:
APRIORI DXE {
...
INF FdtBusPkg/Drivers/VirtNorFlashDxe/VirtNorFlashDxe.inf
INF FdtBusPkg/Drivers/FdtBusDxe/FdtBusDxe.inf
}
This ensures that the DT bus driver is loaded before anything else. It also ensures that the flash driver is loaded before the DT bus driver, ensuring that it binds to the flash device as soon as the DT bus driver loads. Overall this ensures the flash device is initialized before VariableRuntimeDxe.
Here's an example for an environment, where the interrupt controller and timer drivers are DT controller drivers.
APRIORI DXE {
#
# ApicDxe has a depex on CPU arch protocol.
#
INF UefiCpuPkg/Drivers/CpuDxe/CpuDxe.inf
#
# ApicDxe must be loaded before FdtBusDxe for early binding, as
# it is required by HpetTimerDxe.
#
INF MyCrazyPkg/Drivers/ApicDxe/Driver.inf
#
# Needs ApicDxe, and must be loaded before
# FdtBusDxe for early binding, as it produces
# the timer architectural protocol.
#
INF MyCrazyPkg/Drivers/HpetTimerDxe/Driver.inf
#
# One bus driver to rule them all...
#
INF FdtBusPkg/Drivers/FdtBusDxe/FdtBusDxe.inf
}
A good example here are console devices - the actual devices required to connect are usually well known ahead of time. A Tiano implementation using MdeModulePkg/Universal/BdsDxe typically sets these up in the PlatformBootManagerLib component.
Here is an example of a PlatformBootManagerBeforeConsole()
excerpt,
that sets up a serial console (backed by
PciSioSerialDxe).
//
// Add the hardcoded serial console device path to ConIn, ConOut, ErrOut.
//
CopyGuid (&mSerialConsoleSuffix.TermType.Guid, &gEfiTtyTermGuid);
DtIo = FbpGetDtRoot ();
ASSERT (DtIo != NULL);
Status = DtIo->Lookup (DtIo, "/soc/serial@10000000", TRUE, &Handle);
ASSERT_EFI_ERROR (Status);
Status = gBS->HandleProtocol (
Handle,
&gEfiDevicePathProtocolGuid,
(VOID **)&DtDp
);
ASSERT_EFI_ERROR (Status);
NewDp = AppendDevicePath (DtDp, (VOID *)&mSerialConsoleSuffix);
EfiBootManagerUpdateConsoleVariable (ConIn, NewDp, NULL);
EfiBootManagerUpdateConsoleVariable (ConOut, NewDp, NULL);
EfiBootManagerUpdateConsoleVariable (ErrOut, NewDp, NULL)
This is quite similar to directly looking up devices in a legacy driver.
The third parameter to Lookup()
is Connect == TRUE
:
as we want the path to be parsed and resolved, with all missing drivers
bound to devices and all missing DT controllers enumerated. This
accomplishes initializing the bare minimum required -
a much better alternative to enumerating every devices (via
EfiBootManagerConnectAll()
) and connecting every console (via
EfiBootManagerConnectAllConsoles()
).
Note
Prior to the Lookup()
with Connect == TRUE
, the DT controller
for the UART may not even be enumerated, meaning that manually
iterating over all possible EFI handles matching the UART device
would come up short.
The code then grabs the EFI_DEVICE_PATH_PROTOCOL
for the
EFI_HANDLE
matching the UART. This is required by
EfiBootManagerUpdateConsoleVariable()
. It also appends a few
configuration nodes to the EFI device path, required to correctly
configure the UART and console drivers.
Note
You don't have to use the DT I/O Protocol Lookup()
function. It
just makes things much more convenient than manually hardcoding
an EFI_DEVICE_PATH_PROTOCOL
to a device, especially since the DT
controller components have a variable-length structure (see
EFI_DT_DEVICE_PATH_NODE
definition).
Sometimes it's awkward to programmatically lookup and connect devices. memory devices are a good example - there may be multiple of these, so programmatic lookup would involve connecting the parent and then enumerating and connecting all the children.
The alternative is to tag the devices with the fdtbuspkg,critical property. FdtBusDxe will connect all such devices when End-of-DXE event is signalled during the BDS phase.
Note
DT controllers of device_type memory are implicitly treated as having fdtbuspkg,critical.
HighMemDxe, when compiled as a UEFI Driver Model driver, is an example of a driver used with DT controllers that are marked as critical.
Another alternative may be to implement a legacy device driver, but this is usually not a good idea, given the additional complexity and fragility involved.
HighMemDxe, when compiled as a legacy driver, is an example of this approach.
The I/O Protocol only implements core functionality that arguably would otherwise be duplicated in every driver. A few additional libraries exist with useful wrappers or highly-specific functionality.
Name | Description |
---|---|
FbpUtilsLib | Generic useful functions. |
FbpPciUtilsLib | Specific to implementing PCIe root complex drivers. |
FbpInterruptUtilsLib | Specific to drivers that register interrupts. |