Writing Custom UEFI Protocols for Expansion Cards

This article outlines the end-to-end engineering process that custom hardware manufacturers use to develop proprietary Unified Extensible Firmware Interface (UEFI) protocols for specialized expansion cards. It covers defining unique interfaces using Globally Unique Identifiers (GUIDs), writing driver logic that binds to specific PCI hardware, installing custom interfaces into the UEFI system database, flashing the driver as an Option ROM, and consuming the protocol for pre-boot management and diagnostics.

1. Protocol Architecture and Interface Definition

A UEFI protocol is a standard C structure containing function pointers and public data members, identified globally by a 128-bit GUID. When building a proprietary protocol for an expansion card (such as a custom FPGA accelerator, specialized crypto card, or industrial acquisition board), the manufacturer first defines this interface in a C header file.

The interface definition establishes the control plane for the hardware before the operating system loads:

// Example interface definition
typedef struct _MY_CUSTOM_HARDWARE_PROTOCOL MY_CUSTOM_HARDWARE_PROTOCOL;

typedef EFI_STATUS (EFIAPI *MY_HW_GET_STATUS)(
  IN  MY_CUSTOM_HARDWARE_PROTOCOL  *This,
  OUT UINT32                       *HardwareStatus
);

typedef EFI_STATUS (EFIAPI *MY_HW_EXECUTE_DIAGNOSTICS)(
  IN  MY_CUSTOM_HARDWARE_PROTOCOL  *This,
  IN  UINT32                       TestLevel
);

struct _MY_CUSTOM_HARDWARE_PROTOCOL {
  UINT64                     Revision;
  MY_HW_GET_STATUS           GetStatus;
  MY_HW_EXECUTE_DIAGNOSTICS  ExecuteDiagnostics;
};

extern EFI_GUID gMyCustomHardwareProtocolGuid;

A unique GUID is generated using standard UUID tools and registered in the driver's package declaration file (.dec) using the Tianocore EDK II build environment.

2. Implementing the UEFI Driver Model

Modern UEFI expansion cards follow the UEFI Driver Model, which separates device management into three entry points via the EFI_DRIVER_BINDING_PROTOCOL:

  • Supported(): Checks whether the target controller matches the vendor's device. It opens the standard EFI_PCI_IO_PROTOCOL on the controller handle to inspect the PCI Configuration Space. If the PCI Vendor ID and Device ID match the card, it returns EFI_SUCCESS.
  • Start(): Initializes the card. It enables memory-mapped I/O (MMIO) and bus mastering through the EFI_PCI_IO_PROTOCOL. It then allocates an internal context structure containing both the proprietary protocol and private driver state.
  • Stop(): Quiesces the device, halts ongoing DMA transfers, uninstalls protocol interfaces, and frees allocated memory.

3. Installing the Proprietary Protocol

Once the Start() function configures the base hardware, it installs the proprietary protocol onto the device handle using the UEFI Boot Services table (gBS):

Status = gBS->InstallMultipleProtocolInterfaces (
                &ControllerHandle,
                &gMyCustomHardwareProtocolGuid,
                &PrivateDevData->CustomProtocol,
                NULL
                );

By attaching the proprietary protocol directly to the hardware controller handle, the driver links the specialized API to the physical device. The private context structure encapsulates the generic protocol, allowing member functions to access device-specific registers and buffers via pointer arithmetic (such as the standard CR() macro).

4. Packaging and Flashing as an Option ROM

To allow the protocol to execute on any compatible host platform without requiring motherboards to pre-integrate the driver, manufacturers compile the code into a standard UEFI Driver Execution Environment (DXE) binary (.efi).

This binary is packaged into a standard PCI Option ROM layout compliant with the PCI Firmware Specification. The manufacturer flashes this binary image to the expansion card's on-board non-volatile memory (typically an SPI NOR flash or EEPROM).

During platform power-on (the DXE and Boot Device Selection phases), the system firmware enumerates the PCI bus, detects the expansion ROM header, loads the driver image into host memory, and invokes its entry point.

5. Consuming the Proprietary Protocol

Once published, pre-boot environments—such as custom UEFI Shell diagnostic utilities, platform provisioning tools, or custom bootloaders—can discover and use the interface:

  1. Discovery: Applications locate the protocol handle using gBS->LocateProtocol() or query specific devices using gBS->OpenProtocol().
  2. Execution: Callers execute the function pointers directly, performing tasks such as firmware flashing, running pre-boot memory integrity tests on accelerators, or reading telemetry data before the primary operating system boots.