4、UEFI驱动模型:驱动绑定协议、I/O抽象、驱动栈、UEFI驱动开发实例

好,咱们今天聊聊UEFI驱动模型。说实话,很多做固件开发的朋友,一开始接触UEFI驱动时都会觉得有点绕。为什么?因为它跟传统的PCI驱动、Linux内核驱动完全不是一个路子。我当年刚转过来时,也花了不少时间才摸清楚门道。

UEFI驱动模型的核心,说白了就是三件事:驱动怎么找到设备、驱动怎么操作设备、多个驱动怎么协作。咱们一个一个拆开讲。

4.1 驱动绑定协议:驱动怎么找到设备

在UEFI世界里,驱动和设备不是硬绑定的。驱动通过一个叫EFI_DRIVER_BINDING_PROTOCOL的协议来声明自己能管哪些设备。这个协议有三个关键函数:

  • Supported():检查设备是不是我能管的
  • Start():接管设备,开始干活
  • Stop():释放设备,退出

我举个例子。假设你写了一个USB键盘驱动,那Supported()里就会检查设备的Class Code是不是HID键盘。如果是,就返回成功;不是,就返回失败。UEFI的驱动管理器会遍历所有未绑定的设备,挨个调用驱动的Supported(),直到找到匹配的。

关键点:驱动不主动扫描设备,而是由驱动管理器统一调度。这种设计让驱动和设备解耦,非常灵活。

嗯,这里要注意:Supported()不能修改硬件状态。你只是看一眼,不能碰设备。我见过有人在这个函数里初始化硬件,结果导致设备被多个驱动重复初始化,系统直接挂掉。避坑指南:Supported()只做检查,不做操作。

4.2 I/O抽象:驱动怎么操作设备

驱动找到设备后,怎么读写寄存器?直接操作内存映射?不行。UEFI要求驱动通过I/O抽象层来访问硬件。这个抽象层就是EFI_IO_ACCESS协议族,包括:

  • EFI_PCI_IO_PROTOCOL:PCI设备的I/O访问
  • EFI_USB_IO_PROTOCOL:USB设备的I/O访问
  • EFI_BLOCK_IO_PROTOCOL:块设备的I/O访问

为什么要搞这么一层?你想想看,如果驱动直接操作物理地址,那换个平台怎么办?ARM和x86的内存映射完全不同。有了I/O抽象,驱动只需要调用统一的接口,底层由固件帮你搞定。

我个人习惯,写驱动时先看设备属于哪类总线,然后获取对应的I/O协议。比如PCI设备:

EFI_PCI_IO_PROTOCOL *PciIo;
Status = gBS->OpenProtocol(
    ControllerHandle,
    &gEfiPciIoProtocolGuid,
    (VOID **)&PciIo,
    This->DriverBindingHandle,
    ControllerHandle,
    EFI_OPEN_PROTOCOL_BY_DRIVER
);

拿到PciIo后,读写PCI配置空间、BAR空间、DMA操作,全通过它完成。我在项目中遇到过一个问题:某块网卡在x86上跑得好好的,移植到ARM平台就死机。查了半天,发现是驱动直接用了MmioRead/Write,绕过了PCI I/O协议。换成PciIo->Mem.Read/Write后,问题解决。

小技巧:调试驱动时,可以在Supported()里加个DEBUG打印,输出设备的Vendor ID和Device ID。这样你能清楚看到驱动管理器是怎么匹配设备的。

4.3 驱动栈:多个驱动怎么协作

一个设备往往需要多个驱动协作。比如一个NVMe硬盘:

  • PCI总线驱动:枚举NVMe控制器,分配资源
  • NVMe驱动:初始化控制器,创建I/O队列
  • Block I/O驱动:提供读写扇区的接口
  • 文件系统驱动:把扇区组织成文件

这就是驱动栈。每个驱动只负责一层,通过协议向上层提供服务。UEFI驱动管理器会按依赖关系自动加载驱动,构建完整的驱动栈。

我画了一张图,帮你理解驱动栈的结构:

UEFI驱动栈结构 UEFI应用程序 / OS Loader 文件系统驱动 (FAT/NTFS) Block I/O驱动 (EFI_BLOCK_IO_PROTOCOL) NVMe驱动 (EFI_DRIVER_BINDING_PROTOCOL) NVMe SSD硬件 通过协议调用 通过协议调用 通过协议调用 驱动绑定协议

你看,每一层都通过协议向上提供服务。上层不需要知道底层细节,底层也不需要关心上层怎么用。这种分层设计,让驱动开发变得非常模块化。

注意:驱动栈的顺序不能乱。如果Block I/O驱动还没加载,文件系统驱动就尝试打开设备,那肯定会失败。UEFI驱动管理器会处理依赖关系,但你自己写驱动时,一定要在Start()里检查依赖协议是否已经存在。

4.4 UEFI驱动开发实例:写一个简单的PCI驱动

光说不练假把式。咱们写一个简单的PCI驱动,控制一个LED灯。这个驱动会:

  1. 通过驱动绑定协议匹配设备
  2. 通过PCI I/O协议读写寄存器
  3. 提供LED控制接口

先看驱动绑定协议的定义:

EFI_DRIVER_BINDING_PROTOCOL gLedDriverBinding = {
    LedDriverSupported,
    LedDriverStart,
    LedDriverStop,
    0x10,  // Version
    NULL,  // ImageHandle
    NULL   // ControllerHandle
};

Supported()函数:

EFI_STATUS
EFIAPI
LedDriverSupported(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
)
{
    EFI_STATUS Status;
    EFI_PCI_IO_PROTOCOL *PciIo;
    
    // 打开PCI I/O协议,只检查不占用
    Status = gBS->OpenProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        (VOID **)&PciIo,
        This->DriverBindingHandle,
        ControllerHandle,
        EFI_OPEN_PROTOCOL_BY_DRIVER
    );
    if (EFI_ERROR(Status)) {
        return Status;
    }
    
    // 检查Vendor ID和Device ID
    UINT16 VendorId, DeviceId;
    PciIo->Pci.Read(PciIo, EfiPciIoWidthUint16, 0, 1, &VendorId);
    PciIo->Pci.Read(PciIo, EfiPciIoWidthUint16, 2, 1, &DeviceId);
    
    if (VendorId != 0x1234 || DeviceId != 0x5678) {
        gBS->CloseProtocol(ControllerHandle, &gEfiPciIoProtocolGuid,
                           This->DriverBindingHandle, ControllerHandle);
        return EFI_UNSUPPORTED;
    }
    
    // 关闭协议,让Start()重新打开
    gBS->CloseProtocol(ControllerHandle, &gEfiPciIoProtocolGuid,
                       This->DriverBindingHandle, ControllerHandle);
    return EFI_SUCCESS;
}

Start()函数:

EFI_STATUS
EFIAPI
LedDriverStart(
    IN EFI_DRIVER_BINDING_PROTOCOL *This,
    IN EFI_HANDLE ControllerHandle,
    IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
)
{
    EFI_STATUS Status;
    EFI_PCI_IO_PROTOCOL *PciIo;
    
    // 打开PCI I/O协议,这次是真正占用
    Status = gBS->OpenProtocol(
        ControllerHandle,
        &gEfiPciIoProtocolGuid,
        (VOID **)&PciIo,
        This->DriverBindingHandle,
        ControllerHandle,
        EFI_OPEN_PROTOCOL_BY_DRIVER
    );
    if (EFI_ERROR(Status)) {
        return Status;
    }
    
    // 配置PCI设备:开启总线控制、内存空间
    UINT32 Command;
    PciIo->Pci.Read(PciIo, EfiPciIoWidthUint16, 4, 1, &Command);
    Command |= 0x07;  // I/O空间、内存空间、总线控制
    PciIo->Pci.Write(PciIo, EfiPciIoWidthUint16, 4, 1, &Command);
    
    // 获取BAR0地址,用于控制LED
    UINT64 BarAddr;
    PciIo->GetBarAttributes(PciIo, 0, NULL, (VOID **)&BarAddr);
    
    // 保存私有数据
    LED_DRIVER_PRIVATE *Private = AllocatePool(sizeof(LED_DRIVER_PRIVATE));
    Private->PciIo = PciIo;
    Private->BarAddr = BarAddr;
    
    // 安装LED控制协议
    Status = gBS->InstallProtocolInterface(
        &ControllerHandle,
        &gLedControlProtocolGuid,
        EFI_NATIVE_INTERFACE,
        &Private->LedControl
    );
    
    return Status;
}

这个例子虽然简单,但涵盖了UEFI驱动的核心流程。我在实际项目中,曾经用类似的结构写过一款FPGA加速卡驱动。当时最头疼的是调试Supported()的匹配逻辑,因为FPGA的Vendor ID经常变。后来我加了个配置表,把支持的设备ID列表放在一个数组里,方便维护。

经验之谈:写UEFI驱动时,DEBUG打印是你的好朋友。在Supported()、Start()、Stop()的入口和出口都加打印,能帮你快速定位问题。我曾经靠这个习惯,半天就定位了一个驱动加载顺序导致的死锁问题。

好了,UEFI驱动模型的核心内容就这些。驱动绑定协议负责匹配,I/O抽象负责访问,驱动栈负责协作。掌握了这三块,你就能写出结构清晰、可移植性强的UEFI驱动了。

可可精品资料库