How to Write a UEFI DXE Driver in AMI Aptio 4: Complete Guide
Developing a UEFI DXE driver in AMI Aptio 4 involves more than writing a C function and compiling it. The module must be registered with the build system, configured through SDL tokens, compiled and packaged through the appropriate MAK rules, assigned a firmware file GUID, and integrated into the correct firmware volume.
Even after a successful build, the driver may never execute if its dependency expression is incorrect or the corresponding protocol is unavailable. Runtime issues such as invalid interface pointers, missing protocol instances, and incorrect resource management can introduce additional problems.
This tutorial develops a practical example named EFITrainingDXE. The driver acts as a consumer of a protocol published by another DXE driver, EFITrainingPTL. It locates the protocol, obtains its interface, invokes the CheckPoint() and Stall() services, and returns control to the firmware.
The implementation covers the five principal module files:
EFITrainingDXE.cif— component registration.EFITrainingDXE.sdl— build configuration and firmware integration.EFITrainingDXE.mak— compilation and packaging rules.EFITrainingDXE.c— driver entry point and protocol interaction.EFITrainingDXE.dxs— DXE dispatch dependencies.
The examples follow the AMI Aptio 4 build conventions described here. Exact syntax, library dependencies, and integration points can vary between BIOS codebases, so adapt the templates to the conventions used by your target project.
🧩 1. Understanding the DXE Phase and Protocol Architecture #
The Driver Execution Environment (DXE) is a major phase of the UEFI Platform Initialization process. During DXE, firmware dispatches drivers that initialize hardware, enumerate buses, publish interfaces, and provide services required by subsequent boot stages.
The central mechanism for communication between many DXE drivers is the UEFI Protocol.
A protocol is an interface identified by a globally unique identifier (GUID). It usually consists of a structure containing function pointers and other interface data. A driver can install a protocol on a handle, allowing other drivers to discover and use its functionality.
For example, EFITrainingPTL might publish a protocol containing two services:
CheckPoint()— emits a debugging checkpoint, such as a POST code on port0x80.Stall()— introduces a delay using the time unit defined by the protocol contract.
The consumer, EFITrainingDXE, does not need to know how these services are implemented internally. It only needs the correct protocol GUID, interface definition, and valid function pointers.
1.1 The Locate → Handle → Call Pattern #
The example follows three fundamental operations.
| Operation | UEFI Function | Purpose |
|---|---|---|
| Locate | LocateHandleBuffer() |
Finds handles that support a specified protocol |
| Handle | HandleProtocol() |
Retrieves the protocol interface from a handle |
| Call | CheckPoint() and Stall() |
Invokes services supplied by the protocol |
UEFI also provides LocateProtocol() for obtaining a matching protocol interface directly when only one suitable instance is needed. This tutorial retains the LocateHandleBuffer() and HandleProtocol() sequence because it demonstrates the explicit relationship between handles and protocol interfaces.
1.2 Understand the Provider and Consumer #
The protocol provider and consumer have separate responsibilities.
The EFITrainingPTL driver must define the protocol structure and GUID, implement the services, and install the interface using the appropriate DXE boot service.
The EFITrainingDXE driver must include the same interface definition, declare its dependency, locate the installed protocol, and invoke the services safely.
Both modules must agree on the GUID and protocol structure. A mismatch between the provider’s definition and the consumer’s header can cause compilation errors or incorrect runtime behavior.
For additional background, see KAD’s guide to AMI Aptio 4 OEM module architecture, linking, and overrides.
📁 2. The Five Files in an AMI Aptio 4 DXE Module #
A typical Aptio 4 ModulePart requires coordinated configuration, build, source, and dependency files. These files serve different purposes and cannot substitute for one another.
| File | Primary Responsibility |
|---|---|
.cif |
Registers the component and lists its files |
.sdl |
Defines feature tokens, paths, build integration, and firmware-volume linkage |
.mak |
Defines compilation, linking, and firmware file generation |
.c |
Implements the driver’s runtime behavior |
.dxs |
Describes dispatch prerequisites for the DXE driver |
A correctly written C source file can still be excluded from the build if its component is not registered. Likewise, a compiled module may never enter the final firmware image if the packaging and firmware-volume integration rules are missing.
The complete workflow is therefore:
- Register the component with the AMI build system.
- Enable it and define its configuration in SDL.
- Configure compilation and packaging in MAK.
- Implement the protocol consumer in C.
- Declare the required protocol dependency in DXS.
- Build, inspect, and validate the resulting firmware file.
🪪 3. Step 1: Register the Component in EFITrainingDXE.cif #
The CIF file describes the component to the AMI project-management and build environment. It identifies the module, its location, and the configuration files belonging to it.
Complete CIF Example #
<component>
name = "EFITraining - DXE"
category = ModulePart
LocalRoot = "EFITraining\DXE"
RefName = "EFITrainingDXE"
[files]
"EFITrainingDXE.sdl"
"EFITrainingDXE.mak"
"EFITrainingDXE.dxs"
"EFITrainingDXE.c"
</component>
Key Fields Explained #
namedefines the component’s display name.category = ModulePartclassifies the component as a module part.LocalRootidentifies the module directory within the sample-code structure.RefNamesupplies the internal reference used by the build environment.[files]lists the configuration and source files associated with the component.
The component should be registered through the mechanism required by the target Aptio project, such as its component manager.
Simply copying these files into a directory does not necessarily make the module part of the active build.
Verification: Confirm that the project recognizes EFITrainingDXE and can resolve its SDL, MAK, DXS, and C source paths before proceeding.
⚙️ 4. Step 2: Configure the Module in EFITrainingDXE.sdl #
The SDL file controls the module’s configuration within the AMI build environment.
For this example, four sections are important: TOKEN, MODULE, PATH, and ELINK.
4.1 TOKEN: Enable the Module #
The token controls whether the DXE module is enabled by default.
TOKEN
Name = "EFITrainingDXE_SUPPORT"
Value = "1"
Help = "Main switch to enable EFITrainingDXE support in Project"
TokenType = Boolean
TargetMAK = Yes
TargetH = Yes
Master = Yes
Token = "EFITrainingPTL_SUPPORT" "=" "1"
End
The important settings are:
Value = "1"enables the module by default.TargetMAK = Yesmakes the token available to the build configuration.TargetH = Yesrequests token exposure through the generated header mechanism used by the project.Master = Yesmarks the token as a master configuration control in the relevant AMI build environment.
The dependency expression on EFITrainingPTL_SUPPORT expresses the intended configuration relationship: enabling the consumer should also enable the protocol provider.
Verify the behavior against the token syntax and dependency semantics supported by your specific Aptio 4 release.
If the generated token.h is unavailable, check whether the SDL token is included in the build and whether the build system actually generates the expected header.
4.2 MODULE: Include the Compilation Rules #
The MODULE section incorporates the module’s MAK file into the project.
MODULE
Help = "Includes EFITrainingDXE.mak to Project"
File = "EFITrainingDXE.mak"
End
Without this integration, the project may recognize the component but fail to invoke the intended compilation rules.
4.3 PATH: Define the Source Directory #
The PATH section defines the module directory used by the build scripts.
PATH
Name = "EFITraining_DXE_DIR"
Path = "EFITraining\DXE"
End
The EFITraining_DXE_DIR variable is referenced by the MAK rules to locate source files, dependency expressions, and module configuration inputs.
Check that this path resolves correctly from the project’s actual build root. Path conventions can differ between Aptio branches and OEM project layouts.
4.4 ELINK: Integrate the FFS File into the Firmware Volume #
The ELINK section connects the generated firmware file to the target firmware volume.
ELINK
Name = "$(BUILD_DIR)\EFITrainingDXE.ffs"
Parent = "FV_MAIN"
InvokeOrder = AfterParent
End
Here, FV_MAIN represents the intended parent firmware volume in this example.
The critical distinction is between generating an FFS file and including it in the final BIOS image. Successful compilation and packaging do not automatically prove that the firmware file has been integrated into the desired volume.
Verify the actual parent-volume name, eLink syntax, generated output path, and integration behavior against the target project.
An incorrect ELINK configuration can leave a correctly compiled DXE driver absent from the final image.
🛠️ 5. Step 3: Define Build Rules in EFITrainingDXE.mak #
The MAK file connects the module source to the AMI build process. The exact commands depend on the libraries, macros, and build conventions supplied by the platform.
The following example preserves the build structure described in this tutorial.
Complete MAK Example #
all : EFITrainingDXE
EFITrainingDXE : $(BUILD_DIR)\EFITrainingDXE.mak EFITrainingDXEBin
$(BUILD_DIR)\EFITrainingDXE.mak : $(EFITraining_DXE_DIR)\EFITrainingDXE.cif \
$(EFITraining_DXE_DIR)\EFITrainingDXE.mak \
$(BUILD_RULES)
$(CIF2MAK) $(EFITraining_DXE_DIR)\EFITrainingDXE.cif $(CIF2MAK_DEFAULTS)
EFITrainingDXEBin : $(AMIDXELIB) $(AMICSPLib)
$(MAKE) /$(MAKEFLAGS) $(BUILD_DEFAULTS) \
/f $(BUILD_DIR)\EFITrainingDXE.mak all \
NAME=EFITrainingDXE \
MAKEFILE=$(BUILD_DIR)\EFITrainingDXE.mak \
OBJECTS="$(BUILD_DIR)\$(EFITraining_DXE_DIR)\EFITrainingDXE.obj" \
GUID=58A23D88-3AF5-4153-AB71-DDCA1EA80B63 \
ENTRY_POINT=EFITraining_DXE_Test \
TYPE=BS_DRIVER \
DEPEX1=$(EFITraining_DXE_DIR)\EFITrainingDXE.dxs \
DEPEX1_TYPE=EFI_SECTION_DXE_DEPEX \
COMPRESS=1
This is an AMI-specific build template, not a portable makefile for EDK II or arbitrary UEFI build systems. Validate target names, library dependencies, macro expansion, and indentation requirements against your actual project.
5.1 Understand the Build Parameters #
| Parameter | Purpose |
|---|---|
NAME |
Defines the module name |
MAKEFILE |
Selects the generated build-directory makefile |
OBJECTS |
Identifies the compiled object file |
GUID |
Assigns the firmware file identifier |
ENTRY_POINT |
Names the DXE driver entry-point function |
TYPE=BS_DRIVER |
Selects the AMI build type used for this boot-services driver |
DEPEX1 |
Identifies the dependency-expression source |
DEPEX1_TYPE |
Specifies the dependency section type |
COMPRESS=1 |
Requests the configured FFS compression behavior |
The GUID in this example is illustrative. Before using it in a real project, generate a unique identifier and check the firmware repository for collisions.
The entry-point name must match the function implemented in the C source and the conventions expected by the selected AMI build rules.
5.2 Compilation and FFS Packaging #
Conceptually, the build process performs these operations:
- Processes the component and build configuration.
- Compiles the C source into an object file.
- Links the object against the required libraries.
- Packages the module and its dependency metadata into an FFS file.
- Makes the resulting file available to the firmware-volume integration process.
A successful object compilation does not establish that the driver has been linked correctly, packaged properly, or included in the final firmware image.
Inspect each build stage when troubleshooting rather than assuming that all later stages succeeded because the C compiler returned successfully.
For a deeper explanation of firmware packaging, see KAD’s guide to UEFI firmware volumes, FFS files, and HOBs.
💻 6. Step 4: Implement the DXE Driver in EFITrainingDXE.c #
The C source implements the driver entry point and protocol-consumption logic.
The example performs the following operations:
- Initializes the AMI library.
- Searches for handles that support
EFI_TRAINING_PTL_GUID. - Retrieves the protocol interface from the first matching handle.
- Checks the interface before using it.
- Calls the checkpoint and delay services.
- Releases the allocated handle buffer.
The example assumes the provider installs a protocol interface with CheckPoint() and Stall() function pointers, as declared by the shared EFITrainingPTL.h header.
Complete C Example #
#include "Efi.h"
#include "token.h"
#include <AmiLib.h>
#include <AmiDxeLib.h>
#include "..\PTL\EFITrainingPTL.h"
EFI_GUID gEFITrainingPtlGuid = EFI_TRAINING_PTL_GUID;
EFI_STATUS EFITraining_DXE_Test(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UINTN HandleCount;
EFI_HANDLE *HandleBuffer;
EFI_TRAINING_PTL *EFITrainingPtl;
HandleCount = 0;
HandleBuffer = NULL;
EFITrainingPtl = NULL;
/* Initialize the AMI library before using pBS or TRACE. */
InitAmiLib(ImageHandle, SystemTable);
TRACE((TRACE_ALWAYS, "EFI Training DXE testing....\n"));
/* 1. Locate handles that publish the requested protocol. */
Status = pBS->LocateHandleBuffer(
ByProtocol,
&gEFITrainingPtlGuid,
NULL,
&HandleCount,
&HandleBuffer
);
if (EFI_ERROR(Status) ||
HandleCount == 0 ||
HandleBuffer == NULL)
{
TRACE((
TRACE_ALWAYS,
"Cannot locate EFI Training Protocol....\n"
));
if (HandleBuffer != NULL) {
pBS->FreePool(HandleBuffer);
}
/* This diagnostic module must not block boot. */
return EFI_SUCCESS;
}
/* 2. Retrieve the protocol interface from the first handle. */
Status = pBS->HandleProtocol(
HandleBuffer[0],
&gEFITrainingPtlGuid,
(VOID **)&EFITrainingPtl
);
if (EFI_ERROR(Status) || EFITrainingPtl == NULL) {
TRACE((
TRACE_ALWAYS,
"Cannot open EFI Training Protocol interface....\n"
));
pBS->FreePool(HandleBuffer);
return EFI_SUCCESS;
}
/* 3. Validate the service pointers before calling them. */
if (EFITrainingPtl->CheckPoint == NULL ||
EFITrainingPtl->Stall == NULL)
{
TRACE((
TRACE_ALWAYS,
"EFI Training Protocol services are unavailable....\n"
));
pBS->FreePool(HandleBuffer);
return EFI_SUCCESS;
}
/* 4. Invoke the services supplied by the protocol provider. */
EFITrainingPtl->CheckPoint(0x99);
EFITrainingPtl->Stall(1000000);
EFITrainingPtl->CheckPoint(0x00);
/* Release the buffer allocated by LocateHandleBuffer(). */
pBS->FreePool(HandleBuffer);
return EFI_SUCCESS;
}Integration notes: The example uses the interface members and function signatures described by the supplied training scenario. Confirm the actual EFI_TRAINING_PTL structure in EFITrainingPTL.h before compiling. The source-level include path must also match the real PTL header location in your tree.
The sample intentionally returns EFI_SUCCESS when the diagnostic protocol is unavailable so the module does not deliberately fail the boot path. Production drivers may need a different error policy.
6.1 Initialize the AMI Library #
The call to InitAmiLib() initializes the AMI library environment used by this example, including the boot-services pointer and debugging facilities.
InitAmiLib(ImageHandle, SystemTable);
It must be called before using pBS and the AMI TRACE facility.
If these facilities are used before their required initialization has completed, the driver may crash or fail to report useful diagnostic information.
This is a requirement of the AMI library pattern used here, not a universal requirement for every UEFI implementation.
6.2 Locate the Protocol #
The call to LocateHandleBuffer() searches for handles that publish the specified protocol GUID.
Status = pBS->LocateHandleBuffer(
ByProtocol,
&gEFITrainingPtlGuid,
NULL,
&HandleCount,
&HandleBuffer
);The parameters specify the search method, protocol GUID, optional search key, handle count, and output buffer.
The returned buffer is allocated by the firmware boot-services implementation. If it is returned successfully, the driver must release it when it is no longer needed.
The example selects HandleBuffer[0], which is appropriate only if the training scenario expects one provider or does not care which matching instance is used. A production driver should define its selection policy explicitly if multiple instances are possible.
6.3 Retrieve and Validate the Interface #
Finding a matching handle does not itself provide the protocol’s callable interface. The next operation retrieves the interface pointer.
Status = pBS->HandleProtocol(
HandleBuffer[0],
&gEFITrainingPtlGuid,
(VOID **)&EFITrainingPtl
);The driver checks the returned status and interface pointer before proceeding.
It also checks that the two required function pointers are non-null. These checks help catch malformed or incompatible protocol implementations before the consumer attempts to call them.
The interface structure and its function signatures must match the provider’s definition. Avoid duplicating protocol definitions independently in consumer code.
6.4 Call the Protocol Services #
The diagnostic sequence is:
EFITrainingPtl->CheckPoint(0x99);
EFITrainingPtl->Stall(1000000);
EFITrainingPtl->CheckPoint(0x00);
In the supplied example, CheckPoint(0x99) emits a checkpoint value, Stall(1000000) introduces a delay of approximately one second, and CheckPoint(0x00) emits the final checkpoint.
The assumed time unit is microseconds; confirm this against the custom PTL interface contract.
Likewise, the checkpoint values represent what the protocol implementation is instructed to report. Actual output on a POST-code display or diagnostic device depends on the provider and the target hardware.
6.5 Release Resources and Preserve Boot Reliability #
LocateHandleBuffer() allocates a buffer containing matching handles. The consumer must call FreePool() when it has finished using that buffer.
The example frees the buffer on the success path and on the protocol-retrieval failure paths. This avoids leaving the temporary allocation behind when the function returns.
The code also treats the diagnostic service as optional: if the protocol cannot be used, the module reports the problem and returns successfully. A driver responsible for essential platform functionality should instead follow its documented error-handling and recovery requirements.
🔗 7. Step 5: Declare Dependencies in EFITrainingDXE.dxs #
The DXS file specifies the protocol required before the DXE driver can be dispatched.
Complete DXS Example #
#include "..\PTL\EFITrainingPTL.h"
DEPENDENCY_START
EFI_TRAINING_PTL_GUID
DEPENDENCY_ENDThe dependency references the protocol GUID defined by the shared PTL header.
This declares that the driver depends on the protocol identified by EFI_TRAINING_PTL_GUID. The AMI build process translates the expression into the DXE dependency metadata used by the dispatcher.
7.1 Why DEPEX Matters #
The dependency expression and runtime lookup serve related but distinct purposes.
- DEPEX controls dispatch eligibility. The DXE driver is not eligible to execute until its declared protocol dependency is satisfied.
- LocateHandleBuffer() discovers matching handles when the entry point runs.
- HandleProtocol() retrieves the interface that the driver intends to call.
With the dependency correctly declared, the driver should not normally be dispatched while the required protocol remains unavailable. The runtime checks remain useful as defensive handling for unexpected conditions and interface failures.
One important troubleshooting implication follows: if the dependency is never satisfied, the driver may never execute its entry point at all. In that case, the missing-protocol message in the C source will not appear.
Check both the provider’s installation of the protocol and the generated DXE dependency section.
🔨 8. Build, Integrate, and Verify the Firmware Image #
Once the five module files are in place, verify the project configuration before building and testing the firmware.
8.1 Register the Module and Enable the Tokens #
Place the module in the directory expected by the project. For the example structure, the location is:
Samplecode\EFITraining\DXE\
Register EFITrainingDXE.cif using the target project’s component-management procedure.
Confirm that EFITrainingDXE_SUPPORT is enabled and that the required PTL provider is enabled and included in the project. Verify that all source and header paths resolve correctly.
8.2 Run the Build and Inspect the Output #
Execute the appropriate project build for the target Aptio 4 configuration.
Inspect the build logs for:
- Successful compilation of
EFITrainingDXE.c. - Correct resolution of AMI libraries and protocol headers.
- Successful generation of the module’s FFS output.
- Successful processing of the DXS dependency expression.
- Inclusion of the resulting FFS file in the intended firmware volume.
The expected artifact in this example is:
$(BUILD_DIR)\EFITrainingDXE.ffs
The file’s presence is useful evidence, but it does not by itself prove that the module is present in the final firmware volume or that its dependency expression is valid.
Inspect the final image with the tools approved for the project. Verify that the FFS file is located in the intended volume and contains the expected driver and dependency information.
8.3 Flash and Validate on Target Hardware #
Test the resulting image using the platform’s approved firmware validation and recovery procedure.
Because incorrect firmware images can render a system unbootable, do not flash a production system until the target board, image layout, recovery path, and flashing procedure have been verified.
If the driver is dispatched and the PTL provider performs the expected operations, the anticipated observations are:
- The serial debug log contains
EFI Training DXE testing..... - The checkpoint implementation reports
0x99. - The delay service introduces approximately one second of delay.
- The checkpoint implementation subsequently reports
0x00.
The actual POST-code display behavior and serial output depend on the platform configuration and on how the PTL services are implemented.
If the expected behavior does not occur, inspect the dispatch dependencies, protocol installation, module integration, and available debugging output before assuming that the C implementation itself is defective.
⚠️ 9. Common DXE Driver Development Pitfalls #
| Problem | Likely Consequence | Recommended Action |
|---|---|---|
| Duplicate firmware file GUID | Ambiguous module identity or packaging conflicts | Generate a unique GUID and search the project for collisions |
Missing InitAmiLib() |
Uninitialized AMI library services or debugging failures | Initialize the library before using pBS or TRACE |
| Missing CIF registration | The component is absent from the active build | Register the component using the project’s normal process |
| Incorrect SDL token or source path | Compilation rules are not activated or inputs cannot be found | Verify generated tokens and resolved paths |
| Missing or incorrect ELINK | The FFS file may not appear in the expected firmware volume | Inspect the generated linkage and final firmware image |
| Incorrect DEPEX | The driver may not be dispatched when expected | Check the provider’s protocol GUID and generated dependency section |
| Missing status or pointer checks | Invalid interface access or a boot-time crash | Validate returned status, interface pointers, and required function pointers |
| Handle-buffer leak | Unnecessary memory allocation remains outstanding | Free the buffer returned by LocateHandleBuffer() |
| Provider and consumer use different protocol definitions | Incorrect interface layout or invalid function calls | Share the same protocol header and verify its signatures |
| Assuming the first matching handle is always correct | The consumer may invoke the wrong provider instance | Define explicit selection logic when multiple instances exist |
| Assuming a successful FFS build proves image integration | The module may be missing from the actual firmware image | Inspect the final image and the target firmware volume |
Additional Debugging Recommendations #
When a driver does not behave as expected, troubleshoot the boot flow in stages.
First, establish that the component participates in the build. Next, verify that the module is present in the final image. Then confirm that the DXE dispatcher can satisfy the driver’s dependencies. Finally, inspect protocol discovery, interface validation, and service invocation.
This sequence separates build-system errors from dispatch problems and runtime faults, making the underlying issue easier to identify.
📝 10. Conclusion: Master the Five-File Workflow #
Developing a DXE driver in AMI Aptio 4 requires coordinated work across component registration, configuration, compilation, runtime implementation, and dependency management.
The five-file workflow can be summarized as follows:
- CIF: Register the module and its files.
- SDL: Define feature tokens, paths, and firmware-volume integration.
- MAK: Compile, link, and package the module.
- C: Implement the entry point and protocol-consumer logic.
- DXS: Declare the prerequisites required for DXE dispatch.
At runtime, the key pattern is Locate → Handle → Call. The consumer locates a matching protocol handle, obtains the interface, validates it, and invokes the provider’s services.
The build and runtime paths must both be correct. A working C implementation cannot compensate for a missing firmware-volume entry, and a correctly packaged DXE driver cannot perform its intended task if its protocol dependency is unavailable.
Once these relationships are understood, the same modular development approach can be adapted to other Aptio 4 projects involving platform customization, hardware diagnostics, chipset services, and vendor-specific protocol integration.