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 standardEFI_PCI_IO_PROTOCOLon the controller handle to inspect the PCI Configuration Space. If the PCI Vendor ID and Device ID match the card, it returnsEFI_SUCCESS.Start(): Initializes the card. It enables memory-mapped I/O (MMIO) and bus mastering through theEFI_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:
- Discovery: Applications locate the protocol handle
using
gBS->LocateProtocol()or query specific devices usinggBS->OpenProtocol(). - 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.