↓ Skip to main content

UEFI PCI Device Driver Tutorial: Implement Driver Binding in AMI Aptio

UEFI PCI Device Driver Tutorial: Implement Driver Binding in AMI Aptio

One common requirement in UEFI BIOS development is adding firmware support for a new hardware device. For example, a platform may integrate a custom PCIe controller or an internally developed PCI device that needs to expose a software interface to other firmware modules.

The hardware may already be visible to the PCI bus driver, but upper-layer firmware components still need a standardized way to access its functionality. This is where the UEFI Device Driver Model comes into play.

A device driver identifies a compatible controller, initializes the hardware, and installs one or more Protocol interfaces on the controller’s existing handle. Other firmware modules can then locate those protocols and use the services they provide.

This tutorial develops a reusable PCI device-driver skeleton following the UEFI Driver Binding model and AMI Aptio module conventions. It covers the complete development workflow, including CIF registration, SDL configuration, dependency expressions, C implementation, MAK compilation rules, resource management, and UEFI Shell verification.

The example uses a fictional PCI device with Vendor ID 0x1234 and Device ID 0x5678. Replace these values and the hardware-specific initialization routines with those appropriate for your actual device.

One important distinction before starting: the UEFI Driver Binding Protocol defines three lifecycle callbacks—Supported(), Start(), and Stop(). The driver’s entry point separately registers the Driver Binding Protocol. Together, these operations establish the complete driver lifecycle.

🧩 0. Understand the UEFI Device Driver Model
#

Before writing code, it is important to understand where a PCI device driver fits into the firmware architecture.

0.1 Where does a device driver sit?
#

A typical UEFI PCI device stack can be viewed as several functional layers:

Layer Component Example protocol or interface Responsibility
Application layer UEFI application or OS loader Block I/O, Simple File System Loads the operating system and accesses firmware services
Service layer Firmware service driver Partition, FAT Provides higher-level services
Device layer PCI device driver Device-specific I/O protocol Initializes hardware and exposes device functionality
Bus layer PCI bus driver PCI I/O Enumerates PCI controllers and manages bus resources
Root bridge layer PCI root bridge driver PCI Root Bridge I/O Provides access to PCI configuration space and bus resources
Hardware layer Physical device MMIO, I/O ports, DMA Implements the actual hardware functions

This is a conceptual hardware and software stack, not a list of six formally standardized UEFI driver classes.

The protagonist of this tutorial is the PCI device driver. It consumes the PCI I/O abstraction created by the PCI bus driver and publishes a device-specific protocol for other firmware components.

A critical rule applies: a PCI device driver attaches its protocols to the existing PCI controller handle. It does not create a new controller handle. Creating child handles is the responsibility of bus drivers.

For the formal requirements, see the UEFI Specification’s PCI Device Driver section.

0.2 The three Driver Binding callbacks
#

The UEFI Driver Binding Protocol contains three functions:

Function Trigger Responsibility
Supported() Firmware attempts to connect a driver to a controller Checks whether the controller is compatible
Start() Supported() succeeds and the driver is selected Opens required protocols, initializes the device, and installs its protocol interfaces
Stop() Firmware disconnects the driver from the controller Stops device activity, removes installed protocols, and releases resources

The driver entry point is a separate initialization stage. It registers the Driver Binding Protocol so that the DXE environment can discover and connect the driver.

A useful way to remember the sequence is:

  1. Entry: Register Driver Binding and Component Name protocols.
  2. Supported: Check the device identity without performing hardware initialization.
  3. Start: Take ownership of the controller and install the device-specific protocol.
  4. Stop: Reverse the initialization and release everything acquired by Start.

0.3 Device Driver versus Bus Driver
#

This example implements a device driver, not a PCI bus driver.

The distinction affects protocol ownership and resource management.

A device driver:

  • Opens the PCI I/O protocol on an existing controller handle.
  • Checks Vendor ID and Device ID.
  • Initializes the supported hardware.
  • Installs additional protocols on the same controller handle.
  • Does not create child device handles.

A bus driver has additional responsibilities. It enumerates subordinate devices, creates child handles, and establishes relationships using EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER.

For this tutorial, EFI_OPEN_PROTOCOL_BY_DRIVER establishes the relationship between the driver and the controller. It is not a parent-child relationship.

📁 1. Project Structure: Six Files Make Up the Module
#

The AMI Aptio implementation uses six primary files, each with a distinct responsibility.

File Purpose Responsibility
EFITrainingDriver.cif Component registration Registers the module with the build system
EFITrainingDriver.sdl Build configuration Defines support switches, IDs, paths, and linkage
EFITrainingDriver.mak Build rules Defines compilation, GUID, entry point, and unloading
EFITrainingDriver.dxs Dependency expression Waits for the required PCI I/O protocol
EFITrainingDriver.h Shared declarations Defines private data and the custom protocol
EFITrainingDriver.c Driver implementation Implements Driver Binding, initialization, and unloading

The directory structure looks like this:

EFITraining/
└── Driver/
    ├── EFITrainingDriver.cif
    ├── EFITrainingDriver.sdl
    ├── EFITrainingDriver.mak
    ├── EFITrainingDriver.dxs
    ├── EFITrainingDriver.h
    └── EFITrainingDriver.c

The following sections build the module one file at a time.

📝 2. Step 1 — CIF: Register the Component
#

The CIF file tells the AMI build system that the driver exists and identifies the source files belonging to the module.

EFITrainingDriver.cif
#

<component>
name = "EFITraining - DeviceDriver"
category = ModulePart
LocalRoot = "EFITraining\Driver"
RefName = "EFITrainingDriver"

[files]
"EFITrainingDriver.sdl"
"EFITrainingDriver.mak"
"EFITrainingDriver.dxs"
"EFITrainingDriver.c"
"EFITrainingDriver.h"
<endComponent>

The important fields are:

  • name: Human-readable module name.
  • category: Identifies the component as a firmware module part.
  • LocalRoot: Points to the module directory.
  • RefName: Provides the component reference used by the build system.
  • [files]: Lists the configuration, build, and source files.

If the CIF is not registered in the active project, the source code may be perfectly valid but never enter the firmware build.

⚙️ 3. Step 2 — SDL: Define Build Switches and PCI Device IDs
#

The SDL file controls whether the driver is included and exposes the hardware identifiers as configurable build tokens.

This makes the driver easier to reuse. Instead of hardcoding Vendor ID and Device ID values throughout the C source, the module obtains them from generated token definitions.

EFITrainingDriver.sdl
#

TOKEN
    Name = "EFITrainingDriver_SUPPORT"
    Value = "1"
    TokenType = Boolean
    TargetMAK = Yes
    TargetH = Yes
    Master = Yes
End

MODULE
    File = "EFITrainingDriver.mak"
End

PATH
    Name = "EFITraining_Driver_DIR"
    Path = "EFITraining\Driver"
End

ELINK
    Name = "$(BUILD_DIR)\EFITrainingDriver.ffs"
    Parent = "FV_MAIN"
    InvokeOrder = AfterParent
End

TOKEN
    Name = "EFITrainingDriver_VENDOR_ID"
    Value = "0x1234"
    TokenType = Integer
    TargetH = Yes
End

TOKEN
    Name = "EFITrainingDriver_DEVICE_ID"
    Value = "0x5678"
    TokenType = Integer
    TargetH = Yes
End

3.1 Why use SDL tokens for PCI IDs?
#

The two hardware identifiers are defined as independent tokens:

EFITrainingDriver_VENDOR_ID
EFITrainingDriver_DEVICE_ID

With TargetH = Yes, the build environment generates the corresponding definitions in its token header, typically Token.h.

The C implementation can then compare the detected PCI identifiers directly against these values:

PciVendorId == EFITrainingDriver_VENDOR_ID

This makes it easier to adapt the module to another device or board revision without changing the driver logic.

The EFITrainingDriver_SUPPORT token provides the main build switch. Setting it to 1 enables the module, while setting it to 0 allows the component to be excluded through the project’s configuration mechanism.

The exact token-generation and conditional-build behavior should be checked against the selected AMI Aptio project.

🔗 4. Step 3 — DXS: Depend on the PCI I/O Protocol
#

A PCI device driver must not be dispatched as though it were a PCI root bridge driver.

This distinction is especially important when writing the dependency expression.

The PCI root bridge I/O protocol belongs to the PCI host-controller layer. A PCI device driver instead consumes the PCI I/O protocol, which the PCI bus driver installs on individual PCI controller handles.

Therefore, the dependency for this example is the PCI I/O protocol.

EFITrainingDriver.dxs
#

#include <Protocol\PciIo.h>

DEPENDENCY_START
    EFI_PCI_IO_PROTOCOL_GUID
DEPENDENCY_END

The DXE dispatcher uses the dependency expression to determine when the driver is eligible for dispatch.

The PCI bus driver must first enumerate the relevant controller and install the PCI I/O protocol. Only then can the dependency be satisfied.

Why not depend on PCI Root Bridge I/O?
#

A root bridge protocol establishes access to the PCI host-controller infrastructure. Its presence does not necessarily mean that the PCI bus driver has finished enumerating individual devices.

Using the root bridge protocol as the only dependency could allow the device driver to become eligible before the target controller has a usable PCI I/O interface.

The Supported() function should also verify that the controller exposes a Device Path protocol. The DXS dependency is used here to wait for PCI I/O availability, while the runtime check verifies the required protocols on the specific controller handle.

🧱 5. Step 4 — Header: Define Private Data and the Device Protocol
#

The header contains the structures shared by the driver’s implementation.

Two structures are central to this design:

  1. MY_DEVICE_PROTOCOL: The interface published for upper-layer firmware components.
  2. DEVICE_PRIVATE_DATA: The per-controller data retained while the driver manages the hardware.

EFITrainingDriver.h
#

#ifndef _EFI_TRAINING_DRIVER_H
#define _EFI_TRAINING_DRIVER_H

#include <Efi.h>
#include <Token.h>
#include <Protocol\PciIo.h>
#include <Protocol\DevicePath.h>

/*
 * Generate a unique protocol GUID for the actual project.
 * This example value is illustrative only.
 */
#define MY_DEVICE_PROTOCOL_GUID \
    { 0x72A19C40, 0x2D51, 0x4E37, \
      { 0x91, 0xA2, 0x46, 0x53, 0x70, 0x18, 0xBC, 0xD4 } }

extern EFI_GUID gMyDeviceProtocolGuid;

#define DEVICE_PRIVATE_DATA_SIGNATURE \
    SIGNATURE_32('E', 'T', 'D', 'V')

typedef struct _MY_DEVICE_PROTOCOL MY_DEVICE_PROTOCOL;

/*
 * Custom device interface.
 * Replace the placeholder implementations with hardware-specific logic.
 */
typedef
EFI_STATUS
(EFIAPI *MY_DEVICE_RESET)(
    IN MY_DEVICE_PROTOCOL *This
    );

typedef
EFI_STATUS
(EFIAPI *MY_DEVICE_READ)(
    IN MY_DEVICE_PROTOCOL *This,
    IN UINTN Offset,
    OUT UINT8 *Value
    );

struct _MY_DEVICE_PROTOCOL {
    MY_DEVICE_RESET DeviceReset;
    MY_DEVICE_READ  DeviceRead;
};

/*
 * One private-data instance per driven PCI controller.
 */
typedef struct {
    UINTN Signature;

    EFI_HANDLE Controller;
    EFI_PCI_IO_PROTOCOL *PciIo;

    /*
     * Preserve the PCI attributes that were active before Start().
     * Stop() restores this original state.
     */
    UINT64 OriginalPciAttributes;
    BOOLEAN AttributesSaved;

    MY_DEVICE_PROTOCOL MyDeviceInterface;

} DEVICE_PRIVATE_DATA;

/*
 * Recover DEVICE_PRIVATE_DATA from its protocol interface.
 *
 * The CR() macro is supplied by the target firmware libraries.
 */
#define DEVICE_PRIVATE_FROM_IFACE(a) \
    CR(a, DEVICE_PRIVATE_DATA, \
       MyDeviceInterface, \
       DEVICE_PRIVATE_DATA_SIGNATURE)

/* Driver Binding lifecycle callbacks. */

EFI_STATUS
EFIAPI
EFITrainingDriverSupported(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL
    );

EFI_STATUS
EFIAPI
EFITrainingDriverStart(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL
    );

EFI_STATUS
EFIAPI
EFITrainingDriverStop(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN UINTN NumberOfChildren,
    IN EFI_HANDLE *ChildHandleBuffer
    );

EFI_STATUS
EFIAPI
EFITrainingDriverInit(
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    );

EFI_STATUS
EFIAPI
EFITrainingDriverUnload(
    IN EFI_HANDLE ImageHandle
    );

#endif

5.1 Why private data is necessary
#

A device driver may manage multiple controllers. Each controller needs its own state, including:

  • The controller handle.
  • The PCI I/O protocol interface.
  • The original PCI attribute configuration.
  • The custom protocol interface installed by the driver.

Storing this information in a global variable would prevent the driver from managing multiple devices independently.

Instead, the driver allocates one DEVICE_PRIVATE_DATA instance for each successfully started controller.

The custom protocol interface is embedded in this structure. When an upper-layer component invokes one of its functions, the CR() macro can recover the containing private-data structure.

5.2 Why preserve the original PCI attributes?
#

A PCI device driver should not assume that every controller begins with the same configuration.

The PCI bus driver or another firmware component may already have enabled certain attributes. The device driver must preserve the original state and restore it when disconnected.

For example, if memory decoding was already enabled before Start(), Stop() should not blindly disable it.

The UEFI specification explicitly describes this responsibility for PCI device drivers. See UEFI 2.11, PCI Device Drivers.

💻 6. Step 5 — C Source: Implement the Driver Binding Lifecycle
#

The C source implements the actual device driver.

It contains five major parts:

  1. Driver Binding and Component Name definitions.
  2. Supported() for device identification.
  3. Start() for hardware initialization and protocol installation.
  4. Stop() for resource release.
  5. Entry-point registration and unloading.

6.1 Global definitions and Component Name
#

Start by including the required headers and defining the protocol GUID.

#include <Efi.h>
#include <Token.h>

#include <AmiLib.h>
#include <AmiDxeLib.h>

#include <Protocol\DriverBinding.h>
#include <Protocol\ComponentName.h>
#include <Protocol\PciIo.h>
#include <Protocol\DevicePath.h>

#include "EFITrainingDriver.h"

EFI_GUID gMyDeviceProtocolGuid = MY_DEVICE_PROTOCOL_GUID;

static CHAR16 gDriverName[] =
    L"EFI Training Device Driver";

/*
 * Forward declarations for the custom protocol functions.
 */
STATIC
EFI_STATUS
EFIAPI
MyDeviceReset(
    IN MY_DEVICE_PROTOCOL *This
    );

STATIC
EFI_STATUS
EFIAPI
MyDeviceRead(
    IN MY_DEVICE_PROTOCOL *This,
    IN UINTN Offset,
    OUT UINT8 *Value
    );

/*
 * Global Driver Binding Protocol instance.
 */
EFI_DRIVER_BINDING_PROTOCOL gDriverBinding = {
    EFITrainingDriverSupported,
    EFITrainingDriverStart,
    EFITrainingDriverStop,
    0x10,
    NULL,
    NULL
};

The Driver Binding Protocol contains three function pointers and three additional fields:

Member Purpose
Supported Determines whether the driver supports a controller
Start Starts managing the controller
Stop Stops managing the controller
Version Driver Binding version used for driver priority
ImageHandle Handle of the loaded driver image
DriverBindingHandle Handle on which Driver Binding is installed

The example uses version 0x10. When multiple drivers compete for a controller, the Driver Binding version participates in determining priority. Set it deliberately according to the platform’s driver conventions.

Component Name implementation
#

The Component Name Protocol provides a readable driver name to firmware tools and diagnostic utilities.

EFI_STATUS
EFIAPI
EFITrainingDriverGetDriverName(
    IN EFI_COMPONENT_NAME_PROTOCOL *This,
    IN CHAR8 *Language,
    OUT CHAR16 **DriverName
    )
{
    if (DriverName == NULL) {
        return EFI_INVALID_PARAMETER;
    }

    *DriverName = gDriverName;

    return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
EFITrainingDriverGetControllerName(
    IN EFI_COMPONENT_NAME_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_HANDLE ChildHandle,
    IN CHAR8 *Language,
    OUT CHAR16 **ControllerName
    )
{
    return EFI_UNSUPPORTED;
}

EFI_COMPONENT_NAME_PROTOCOL gComponentName = {
    EFITrainingDriverGetDriverName,
    EFITrainingDriverGetControllerName,
    "eng"
};

The example provides the driver name but does not implement a controller-specific name.

A production driver can extend GetControllerName() to return information about the hardware instance being managed.

🔎 6.2 Supported(): Identify the PCI Device
#

Supported() is called when the firmware attempts to connect the driver to a controller.

Its job is to determine whether the target controller matches the hardware this driver supports.

The function should remain fast and avoid performing hardware initialization. It should not enable PCI decoding, configure DMA, reset the device, or install protocols.

The correct process is:

  1. Verify that the controller has a Device Path protocol.
  2. Open its PCI I/O protocol using EFI_OPEN_PROTOCOL_BY_DRIVER.
  3. Read the PCI configuration-space Vendor ID and Device ID.
  4. Close the PCI I/O protocol.
  5. Return EFI_SUCCESS for a matching device or EFI_UNSUPPORTED otherwise.

EFITrainingDriverSupported()
#

EFI_STATUS
EFIAPI
EFITrainingDriverSupported(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL
    )
{
    EFI_STATUS Status;
    EFI_STATUS CloseStatus;

    EFI_DEVICE_PATH_PROTOCOL *DevicePath;
    EFI_PCI_IO_PROTOCOL *PciIo;

    UINT32 PciId;
    UINT16 VendorId;
    UINT16 DeviceId;

    /*
     * 1. Verify Device Path protocol presence.
     */
    Status = pBS->HandleProtocol(
        ControllerHandle,
        &gEfiDevicePathProtocolGuid,
        (VOID **)&DevicePath
        );

    if (EFI_ERROR(Status) || DevicePath == NULL) {
        return EFI_UNSUPPORTED;
    }

    /*
     * 2. Open PCI I/O using BY_DRIVER.
     *    This is a temporary probe in Supported().
     */
    Status = pBS->OpenProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        (VOID **)&PciIo,
        This->DriverBindingHandle,
        ControllerHandle,
        EFI_OPEN_PROTOCOL_BY_DRIVER
        );

    if (EFI_ERROR(Status)) {
        return Status;
    }

    /*
     * 3. Read the first DWORD of PCI configuration space.
     *
     *    DWORD layout:
     *      Bits  0-15: Vendor ID
     *      Bits 16-31: Device ID
     */
    Status = PciIo->Pci.Read(
        PciIo,
        EfiPciIoWidthUint32,
        0,
        1,
        &PciId
        );

    /*
     * 4. Always close the temporary BY_DRIVER reference.
     */
    CloseStatus = pBS->CloseProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        This->DriverBindingHandle,
        ControllerHandle
        );

    if (EFI_ERROR(Status)) {
        return Status;
    }

    if (EFI_ERROR(CloseStatus)) {
        return CloseStatus;
    }

    /*
     * 5. Extract and compare the PCI identifiers.
     */
    VendorId = (UINT16)(PciId & 0xFFFF);
    DeviceId = (UINT16)((PciId >> 16) & 0xFFFF);

    if ((VendorId == EFITrainingDriver_VENDOR_ID) &&
        (DeviceId == EFITrainingDriver_DEVICE_ID)) {
        return EFI_SUCCESS;
    }

    return EFI_UNSUPPORTED;
}

Why must Supported() close PCI I/O?
#

The function opens PCI I/O using EFI_OPEN_PROTOCOL_BY_DRIVER.

This creates a tracked driver-to-controller relationship in the UEFI handle database. If the function returns without closing the temporary reference, the controller can remain incorrectly claimed by the driver.

That can prevent subsequent drivers from binding successfully.

The rule is simple:

Every successful OpenProtocol() using BY_DRIVER in Supported() must be matched by CloseProtocol() before the function returns.

The driver should then open the protocol again in Start() to establish its actual ownership of the controller.

🚀 6.3 Start(): Initialize the Hardware and Install the Protocol
#

Start() is called after Supported() confirms that the controller is compatible.

This is the first stage where the driver should perform its actual hardware initialization.

The implementation has several responsibilities:

  1. Allocate the private-data structure.
  2. Open PCI I/O using EFI_OPEN_PROTOCOL_BY_DRIVER.
  3. Retrieve and preserve the original PCI attributes.
  4. Enable only the PCI decoding attributes required by the device.
  5. Initialize the hardware.
  6. Initialize the custom protocol interface.
  7. Install the protocol on the existing controller handle.

A key requirement is that every failure path must reverse the resources already acquired.

Custom protocol function implementations
#

The following implementations are placeholders. They demonstrate how to recover the private-data structure from the custom protocol interface.

Replace the EFI_UNSUPPORTED return values with the actual register-access and device-control routines for your hardware.

STATIC
EFI_STATUS
EFIAPI
MyDeviceReset(
    IN MY_DEVICE_PROTOCOL *This
    )
{
    DEVICE_PRIVATE_DATA *Private;

    if (This == NULL) {
        return EFI_INVALID_PARAMETER;
    }

    Private = DEVICE_PRIVATE_FROM_IFACE(This);

    if (Private->Signature != DEVICE_PRIVATE_DATA_SIGNATURE) {
        return EFI_INVALID_PARAMETER;
    }

    /*
     * TODO:
     * Implement the real hardware reset sequence.
     */

    return EFI_UNSUPPORTED;
}

STATIC
EFI_STATUS
EFIAPI
MyDeviceRead(
    IN MY_DEVICE_PROTOCOL *This,
    IN UINTN Offset,
    OUT UINT8 *Value
    )
{
    DEVICE_PRIVATE_DATA *Private;

    if ((This == NULL) || (Value == NULL)) {
        return EFI_INVALID_PARAMETER;
    }

    Private = DEVICE_PRIVATE_FROM_IFACE(This);

    if (Private->Signature != DEVICE_PRIVATE_DATA_SIGNATURE) {
        return EFI_INVALID_PARAMETER;
    }

    /*
     * TODO:
     * Validate Offset and read the corresponding
     * device register through the appropriate PCI BAR.
     */

    return EFI_UNSUPPORTED;
}

EFITrainingDriverStart()
#

EFI_STATUS
EFIAPI
EFITrainingDriverStart(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath OPTIONAL
    )
{
    EFI_STATUS Status;

    DEVICE_PRIVATE_DATA *Private = NULL;

    /*
     * 1. Allocate per-controller private data.
     */
    Status = pBS->AllocatePool(
        EfiBootServicesData,
        sizeof(DEVICE_PRIVATE_DATA),
        (VOID **)&Private
        );

    if (EFI_ERROR(Status)) {
        return Status;
    }

    /*
     * Initialize fields that will be used during cleanup.
     */
    Private->Signature = DEVICE_PRIVATE_DATA_SIGNATURE;
    Private->Controller = ControllerHandle;
    Private->PciIo = NULL;
    Private->OriginalPciAttributes = 0;
    Private->AttributesSaved = FALSE;

    /*
     * 2. Open the controller's PCI I/O protocol.
     *
     *    Unlike Supported(), this reference remains open
     *    for the lifetime of the driver instance.
     */
    Status = pBS->OpenProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        (VOID **)&Private->PciIo,
        This->DriverBindingHandle,
        ControllerHandle,
        EFI_OPEN_PROTOCOL_BY_DRIVER
        );

    if (EFI_ERROR(Status)) {
        goto FREE_PRIVATE;
    }

    /*
     * 3. Retrieve the original PCI attribute state.
     */
    Status = Private->PciIo->Attributes(
        Private->PciIo,
        EfiPciIoAttributeOperationGet,
        0,
        &Private->OriginalPciAttributes
        );

    if (EFI_ERROR(Status)) {
        goto CLOSE_PCIIO;
    }

    Private->AttributesSaved = TRUE;

    /*
     * 4. Enable memory decoding for this example device.
     *
     *    This example assumes the device uses MMIO.
     *    Add EFI_PCI_IO_ATTRIBUTE_IO if it requires I/O BARs.
     *
     *    Do not enable BUS_MASTER until DMA resources and
     *    hardware state have been safely initialized.
     */
    Status = Private->PciIo->Attributes(
        Private->PciIo,
        EfiPciIoAttributeOperationEnable,
        EFI_PCI_IO_ATTRIBUTE_MEMORY,
        NULL
        );

    if (EFI_ERROR(Status)) {
        goto RESTORE_ATTRIBUTES;
    }

    /*
     * 5. Hardware initialization.
     *
     * TODO:
     *   - Identify and validate the required PCI BAR.
     *   - Map or access the device registers through PCI I/O.
     *   - Initialize hardware-specific state.
     *   - Configure DMA only when required.
     *   - Enable bus mastering only after DMA setup is safe.
     */

    /*
     * 6. Initialize the device-specific protocol interface.
     */
    Private->MyDeviceInterface.DeviceReset = MyDeviceReset;
    Private->MyDeviceInterface.DeviceRead = MyDeviceRead;

    /*
     * 7. Install the custom protocol on the existing controller.
     *
     *    A PCI device driver attaches its protocol to the
     *    controller handle. It does not create a child handle.
     */
    Status = pBS->InstallProtocolInterface(
        &ControllerHandle,
        &gMyDeviceProtocolGuid,
        EFI_NATIVE_INTERFACE,
        &Private->MyDeviceInterface
        );

    if (EFI_ERROR(Status)) {
        goto RESTORE_ATTRIBUTES;
    }

    TRACE((
        TRACE_ALWAYS,
        "EFITrainingDriver: device started successfully\n"
        ));

    return EFI_SUCCESS;


/*
 * Error handling: release resources in reverse order.
 */

RESTORE_ATTRIBUTES:

    if (Private->AttributesSaved) {
        Private->PciIo->Attributes(
            Private->PciIo,
            EfiPciIoAttributeOperationSet,
            Private->OriginalPciAttributes,
            NULL
            );
    }

CLOSE_PCIIO:

    pBS->CloseProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        This->DriverBindingHandle,
        ControllerHandle
        );

FREE_PRIVATE:

    pBS->FreePool(Private);

    return Status;
}

Why restore attributes on an error path?
#

Suppose Start() successfully enables memory decoding but subsequently fails to install the custom protocol.

Simply closing PCI I/O and freeing private data would leave the hardware in a modified state.

The rollback path therefore restores the original PCI attributes before releasing the protocol reference.

The same rule applies when the driver successfully starts and later receives a Stop() request.

What about Bus Master?
#

The example enables memory decoding but deliberately does not enable bus mastering automatically.

This is an important safety consideration.

If the hardware performs DMA, the driver must first establish valid DMA buffers, addresses, and device state. Only then should it enable the PCI bus-master attribute.

Blindly enabling bus mastering during generic initialization can expose the system to invalid DMA transactions.

For a production driver, determine the exact decoding and DMA requirements from the hardware specification.

🧹 6.4 Stop(): Release Resources Symmetrically
#

Stop() reverses the operations performed by Start().

The driver must stop outstanding device activity, remove the custom protocol, restore the original PCI attributes, close its PCI I/O reference, and release private data.

Because this is a device driver rather than a bus driver, it does not create child controllers. A nonzero NumberOfChildren is therefore not supported by this example.

EFITrainingDriverStop()
#

EFI_STATUS
EFIAPI
EFITrainingDriverStop(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN UINTN NumberOfChildren,
    IN EFI_HANDLE *ChildHandleBuffer
    )
{
    EFI_STATUS Status;
    EFI_STATUS RestoreStatus;
    EFI_STATUS CloseStatus;

    MY_DEVICE_PROTOCOL *MyDev = NULL;
    DEVICE_PRIVATE_DATA *Private;

    /*
     * This device driver does not create child handles.
     */
    if (NumberOfChildren != 0) {
        return EFI_UNSUPPORTED;
    }

    /*
     * 1. Retrieve the custom protocol interface.
     *
     *    GET_PROTOCOL does not create a tracked BY_DRIVER
     *    reference and does not require CloseProtocol().
     */
    Status = pBS->OpenProtocol(
        ControllerHandle,
        &gMyDeviceProtocolGuid,
        (VOID **)&MyDev,
        This->DriverBindingHandle,
        ControllerHandle,
        EFI_OPEN_PROTOCOL_GET_PROTOCOL
        );

    if (EFI_ERROR(Status)) {
        return Status;
    }

    if (MyDev == NULL) {
        return EFI_DEVICE_ERROR;
    }

    /*
     * 2. Recover the private data.
     */
    Private = DEVICE_PRIVATE_FROM_IFACE(MyDev);

    if (Private->Signature != DEVICE_PRIVATE_DATA_SIGNATURE) {
        return EFI_INVALID_PARAMETER;
    }

    /*
     * 3. Stop device activity before removing its interface.
     *
     * TODO:
     *   - Stop active DMA.
     *   - Disable device interrupts.
     *   - Complete or cancel outstanding transactions.
     *   - Restore hardware-specific state.
     */

    /*
     * 4. Uninstall the custom protocol.
     *
     * If this fails, retain private data and the PCI I/O
     * reference because the published interface still exists.
     */
    Status = pBS->UninstallProtocolInterface(
        ControllerHandle,
        &gMyDeviceProtocolGuid,
        &Private->MyDeviceInterface
        );

    if (EFI_ERROR(Status)) {
        return Status;
    }

    /*
     * 5. Restore the original PCI attribute configuration.
     */
    RestoreStatus = EFI_SUCCESS;

    if (Private->AttributesSaved) {
        RestoreStatus = Private->PciIo->Attributes(
            Private->PciIo,
            EfiPciIoAttributeOperationSet,
            Private->OriginalPciAttributes,
            NULL
            );
    }

    /*
     * 6. Close the PCI I/O reference acquired in Start().
     */
    CloseStatus = pBS->CloseProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        This->DriverBindingHandle,
        ControllerHandle
        );

    /*
     * 7. Release private data.
     */
    pBS->FreePool(Private);

    TRACE((
        TRACE_ALWAYS,
        "EFITrainingDriver: device stopped\n"
        ));

    /*
     * Preserve cleanup errors for diagnostics.
     */
    if (EFI_ERROR(RestoreStatus)) {
        return RestoreStatus;
    }

    return CloseStatus;
}

Start and Stop must remain symmetric
#

The most useful way to review the driver is to compare every resource acquired in Start() with the corresponding release in Stop().

Start operation Stop operation
AllocatePool() FreePool()
OpenProtocol(BY_DRIVER) CloseProtocol()
Attributes(GET) Save original state
Attributes(ENABLE) Attributes(SET)
Hardware initialization Stop hardware and release DMA resources
InstallProtocolInterface() UninstallProtocolInterface()

The table also applies to failure paths in Start(). If initialization fails halfway through, every previously acquired resource must be released.

One additional consideration is protocol consumers. If another driver or application has an incompatible open reference to the custom protocol, uninstalling it may fail. The correct response is to preserve the driver’s private data and PCI I/O ownership rather than freeing memory while the published interface still points to it.

For real hardware, add complete handling for device-specific DMA, interrupt, and transaction cleanup.

🔧 6.5 Entry Point and Unload Support
#

The entry point registers the Driver Binding and Component Name protocols.

It should not initialize the PCI hardware directly. Hardware initialization belongs in Start(), where the controller has been identified and connected.

EFITrainingDriverInit()
#

EFI_STATUS
EFIAPI
EFITrainingDriverInit(
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    EFI_STATUS Status;

    /*
     * Initialize AMI library services before using pBS or TRACE.
     */
    InitAmiLib(ImageHandle, SystemTable);

    TRACE((
        TRACE_ALWAYS,
        "EFITrainingDriver: Driver Entry\n"
        ));

    /*
     * Populate Driver Binding image and binding handles.
     */
    gDriverBinding.ImageHandle = ImageHandle;
    gDriverBinding.DriverBindingHandle = ImageHandle;

    /*
     * Register Driver Binding and Component Name protocols.
     */
    Status = pBS->InstallMultipleProtocolInterfaces(
        &ImageHandle,
        &gEfiDriverBindingProtocolGuid,
        &gDriverBinding,
        &gEfiComponentNameProtocolGuid,
        &gComponentName,
        NULL
        );

    return Status;
}

The entry point registers the driver with the firmware. It does not know which controller will eventually be selected, so it should not access a specific device or allocate per-controller resources here.

EFITrainingDriverUnload()
#

EFI_STATUS
EFIAPI
EFITrainingDriverUnload(
    IN EFI_HANDLE ImageHandle
    )
{
    TRACE((
        TRACE_ALWAYS,
        "EFITrainingDriver: Unload requested\n"
        ));

    return pBS->UninstallMultipleProtocolInterfaces(
        ImageHandle,
        &gEfiDriverBindingProtocolGuid,
        &gDriverBinding,
        &gEfiComponentNameProtocolGuid,
        &gComponentName,
        NULL
        );
}

The unload function removes the protocols installed by the entry point.

However, the driver must not be unloaded while it is still managing active controllers. Those controllers should first be disconnected so that Stop() can release their resources.

The firmware may reject an unload operation if the driver is still referenced by connected controllers or other protocol consumers.

🏗️ 7. Step 6 — MAK: Define Compilation and FFS Packaging
#

The MAK file defines how the module is compiled and packaged into a firmware file.

EFITrainingDriver.mak
#

all : EFITrainingDriver

EFITrainingDriver : $(BUILD_DIR)\EFITrainingDriver.mak EFITrainingDriverBin

$(BUILD_DIR)\EFITrainingDriver.mak : \
    $(EFITraining_Driver_DIR)\EFITrainingDriver.cif \
    $(EFITraining_Driver_DIR)\EFITrainingDriver.mak \
    $(BUILD_RULES)

	$(CIF2MAK) $(EFITraining_Driver_DIR)\EFITrainingDriver.cif $(CIF2MAK_DEFAULTS)

EFITrainingDriverBin : $(AMIDXELIB) $(AMICSPLIB)

	$(MAKE) /$(MAKEFLAGS) $(BUILD_DEFAULTS) \
		/f $(BUILD_DIR)\EFITrainingDriver.mak all \
		NAME=EFITrainingDriver \
		MAKEFILE=$(BUILD_DIR)\EFITrainingDriver.mak \
		OBJECTS="$(BUILD_DIR)\$(EFITraining_Driver_DIR)\EFITrainingDriver.obj" \
		GUID=5F6A7C8D-9E01-4B23-8C45-6D7E8F901A2B \
		ENTRY_POINT=EFITrainingDriverInit \
		TYPE=BS_DRIVER \
		DEPEX1=$(EFITraining_Driver_DIR)\EFITrainingDriver.dxs \
		DEPEXITYPE=EFI_SECTION_DXE_DEPEX \
		UNLOAD_IMAGE=EFITrainingDriverUnload \
		COMPRESS=1

The important build parameters are:

Parameter Purpose
NAME Identifies the module
OBJECTS Specifies the compiled object files
GUID Identifies the generated firmware file
ENTRY_POINT Identifies the DXE driver entry function
TYPE Defines the module type
DEPEX1 Identifies the dependency-expression source
DEPEXITYPE Specifies the DXE dependency section
UNLOAD_IMAGE Defines the optional unload entry point
COMPRESS Enables the configured firmware compression behavior

The UNLOAD_IMAGE setting is particularly relevant when testing dynamic driver unloading.

The GUID shown above is an illustrative module file GUID. Generate a unique GUID for every new firmware component rather than copying an identifier already used elsewhere in the platform.

The exact MAK syntax, library dependencies, and packaging rules depend on the AMI Aptio project and its build environment. Use the conventions of the target firmware tree when integrating the module.

🧪 8. Compile, Flash, and Verify the Driver
#

After all six files have been created, integrate the module into the platform build.

8.1 Compilation and firmware integration
#

A typical integration process looks like this:

  1. Copy EFITrainingDriver into the sample-code directory, such as Samplecode\EFITraining\Driver\.
  2. Register the CIF component in the project’s component-management system.
  3. Confirm that EFITrainingDriver_SUPPORT is set to 1.
  4. Verify that the Vendor ID and Device ID tokens are generated correctly.
  5. Build the BIOS image using the platform’s standard procedure.
  6. Inspect the build output and confirm that EFITrainingDriver.ffs was generated.
  7. Confirm that the FFS file is linked into FV_MAIN.
  8. Flash the image using the platform’s approved firmware-update process.

Before flashing, verify the module GUID and confirm that the generated DXE dependency section contains the expected PCI I/O protocol dependency.

8.2 UEFI Shell verification
#

After booting the test platform into the UEFI Shell, begin by checking whether the driver has been loaded.

Shell> drivers

The output should contain the driver name:

EFI Training Device Driver

Next, identify the target PCI controller handle. The exact command syntax for protocol-based handle enumeration varies by UEFI Shell implementation. Use the dh command and the PCI I/O protocol GUID or the platform’s available protocol-name aliases.

For example, assume the target controller handle is 0x5F.

Shell> connect 0x5F

The firmware attempts to connect the driver to the controller. If the Vendor ID and Device ID match, the Driver Binding sequence should proceed:

Supported() → Start()

The custom device protocol should then appear on the controller handle.

To test disconnect and cleanup:

Shell> disconnect 0x5F

The driver should receive a Stop() request, remove its custom protocol, restore the original PCI attributes, and release its PCI I/O reference.

Finally, reconnect the controller:

Shell> connect 0x5F

Successful reconnection demonstrates that the driver can acquire the controller again after completing its cleanup sequence.

The precise Shell command syntax and output vary by firmware implementation.

8.3 Four verification checkpoints
#

There are four useful checkpoints during testing:

Checkpoint Expected behavior
Driver enumeration drivers identifies EFI Training Device Driver
Controller matching Supported() returns success for the intended PCI ID
Protocol installation The custom protocol appears on the PCI controller handle after Start()
Rebinding Disconnect followed by reconnect successfully executes Stop() and Start() again

Serial trace output is particularly useful for confirming the callback sequence.

A typical successful sequence should resemble:

EFITrainingDriver: Driver Entry
EFITrainingDriver: device started successfully
EFITrainingDriver: device stopped
EFITrainingDriver: device started successfully

The exact messages depend on the trace statements enabled in the implementation.

⚠️ 9. Common UEFI PCI Driver Development Pitfalls
#

A Driver Binding implementation can compile successfully and still fail during firmware connection. The following issues are especially common when developing PCI device drivers.

Problem Possible consequence Recommended solution
Performing hardware initialization in the entry point Device access occurs before the controller is ready Move device initialization into Start()
Performing expensive operations in Supported() Slower device connection and boot Restrict the function to protocol and ID checks
Forgetting CloseProtocol() in Supported() Leaves an unintended driver reference on the controller Close every successful temporary BY_DRIVER open
Depending on PCI Root Bridge I/O instead of PCI I/O Driver may become eligible before the target PCI controller is enumerated Use the PCI I/O protocol as the DXE dependency
Failing to check Device Path Driver may accept an unsuitable controller handle Verify both required controller protocols
Using GET_PROTOCOL instead of BY_DRIVER in Start() The driver does not establish the expected controller ownership relationship Use EFI_OPEN_PROTOCOL_BY_DRIVER consistently
Enabling Bus Master too early Device may perform unsafe DMA Initialize and validate DMA resources first
Failing to preserve PCI attributes Stop() may incorrectly disable resources that were already enabled Retrieve and restore the original attribute state
Missing rollback in Start() Partial initialization leaves resources or hardware state behind Use structured cleanup paths
Freeing private data before uninstall succeeds Installed protocol may reference invalid memory Retain private data if protocol removal fails
Failing to stop DMA or interrupts Device may continue accessing memory after disconnection Quiesce hardware before releasing resources
Missing EFIAPI in callback declarations Calling-convention mismatch on supported architectures Use EFIAPI consistently in UEFI function declarations and definitions
Reusing a firmware file GUID Packaging collisions or incorrect module identification Generate a unique GUID
Forgetting Component Name Shell diagnostics may display an unknown driver name Implement Component Name or Component Name 2
Unloading a driver while controllers remain connected Unload may fail or leave active resources Disconnect controllers before unloading
Ignoring surprise removal Hardware can disappear without normal cleanup Design hot-plug handling and DMA safety around the device’s removal behavior

9.1 Driver Binding Version is not arbitrary
#

The Version field can affect which driver is selected when multiple drivers support the same controller.

Setting it to zero without considering the platform’s other driver versions may result in an unexpected binding priority.

Use a deliberate version value consistent with the project’s driver conventions.

9.2 BY_DRIVER is not BY_CHILD_CONTROLLER
#

These attributes have different purposes.

EFI_OPEN_PROTOCOL_BY_DRIVER records the relationship between a driver and the controller it manages.

EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER is used by bus drivers to record relationships between a parent controller and child handles created by that bus driver.

Because this tutorial implements a device driver that does not create child handles, it uses BY_DRIVER for its PCI I/O relationship.

9.3 PCI drivers must respect bus resource ownership
#

A PCI device driver should not arbitrarily reconfigure PCI BAR assignments or modify PCI-to-PCI bridge resource allocation.

Those resources are managed by the PCI bus driver or other firmware components responsible for PCI enumeration and configuration.

The device driver should use the PCI I/O protocol to access its assigned resources.

This separation is important for maintaining platform stability, especially when multiple PCI devices share the same bus hierarchy.

9.4 Surprise removal requires additional design work
#

The normal DisconnectController() flow allows Stop() to release resources cleanly.

However, hot-plug devices may disappear unexpectedly. A driver cannot assume that every hardware-removal scenario will provide an opportunity to execute its normal cleanup routine.

For devices that support hot-plug or surprise removal, the implementation must account for outstanding DMA, interrupt handling, hardware state, and invalidated register access.

The simple lifecycle skeleton in this tutorial is intended as a starting point for a conventional PCI device, not a complete hot-plug driver.

🏁 10. Summary: The Complete UEFI Device Driver Workflow
#

The UEFI PCI device-driver model becomes much easier to understand once the responsibility of each lifecycle stage is clear.

The entire sequence can be condensed into the following workflow:

DXE Dispatcher
      |
      v
EFITrainingDriverInit()
      |
      | Install Driver Binding Protocol
      v
ConnectController()
      |
      v
Supported()
      |
      | Check Device Path
      | Open PCI I/O with BY_DRIVER
      | Read Vendor ID / Device ID
      | Close PCI I/O
      |
      +---- Unsupported ----------------> Return
      |
      v
Start()
      |
      | Allocate private data
      | Open PCI I/O with BY_DRIVER
      | Save original PCI attributes
      | Enable required device decoding
      | Initialize hardware
      | Install custom device protocol
      |
      v
Device Driver Active
      |
      | Upper-layer modules use custom protocol
      |
      v
DisconnectController()
      |
      v
Stop()
      |
      | Stop hardware and DMA
      | Uninstall custom protocol
      | Restore original PCI attributes
      | Close PCI I/O
      | Free private data
      |
      v
Controller Released

The most important engineering rules are:

  • Entry registers; it does not initialize hardware.
  • Supported identifies the device quickly and closes all temporary protocol references.
  • Start acquires resources, initializes the hardware, and installs the device protocol.
  • Stop removes the interface and reverses every resource acquisition.
  • PCI attributes must be restored to their original state.
  • A PCI device driver attaches its interface to the existing controller handle rather than creating child handles.

Once this pattern is understood, more advanced firmware modules become easier to design.

For example, a PCI bus driver extends the model by creating child handles and managing parent-child relationships. A service driver can consume protocols from multiple devices without directly managing their hardware. Both build on the same foundational UEFI protocol and driver-binding mechanisms.

For related AMI Aptio development examples, see How to Write a UEFI DXE Driver in AMI Aptio 4 and How to Write a UEFI SMI Module in AMI Aptio 4.

Master the relationship between Supported(), Start(), and Stop(), and the core of the UEFI Device Driver Model becomes much more intuitive.

Related