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驱动管理器会按依赖关系自动加载驱动,构建完整的驱动栈。
我画了一张图,帮你理解驱动栈的结构:
你看,每一层都通过协议向上提供服务。上层不需要知道底层细节,底层也不需要关心上层怎么用。这种分层设计,让驱动开发变得非常模块化。
注意:驱动栈的顺序不能乱。如果Block I/O驱动还没加载,文件系统驱动就尝试打开设备,那肯定会失败。UEFI驱动管理器会处理依赖关系,但你自己写驱动时,一定要在Start()里检查依赖协议是否已经存在。
4.4 UEFI驱动开发实例:写一个简单的PCI驱动
光说不练假把式。咱们写一个简单的PCI驱动,控制一个LED灯。这个驱动会:
- 通过驱动绑定协议匹配设备
- 通过PCI I/O协议读写寄存器
- 提供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驱动了。
可可精品资料库