UEFI中的PassThru()
最开始的时候,我是学习SATA-AHCI协议探索EDK2的源码,而在ATA中,PassThru 函数是实现ATA通过协议时最重要的功能,它执行以下操作:
- 初始化内部寄存器以进行命令/数据传输。
- 将有效的ATA命令放到特定于硬件的内存或寄存器位置。
- 启动传输。
- 可选择等待执行的完成。
该函数中更好的错误处理机制有助于开发一个更健壮的驱动程序。尽管大多数ATA主机控制器同时支持阻塞和非阻塞数据传输,但有些控制器可能只支持阻塞传输。
ReadUsingNCQ示例
这里演示如何使用NCQ(Native Command Queuing)执行ATA读操作的过程,我们需要理解NCQ的工作原理。NCQ允许硬盘驱动器内部重新排序命令执行顺序,以提高存储设备的性能和效率。以下是使用EFI_ATA_PASS_THRU_PROTOCOL发送NCQ读命令的简化示例。请注意,因为这是一个高度简化的示例,实际应用中可能需要更多的错误检查和设备初始化步骤。
首先,假设我们已经有了一个有效的EFI_ATA_PASS_THRU_PROTOCOL实例,称为AtaPassThru,以及我们想要通信的ATA设备的端口号和端口乘数位置(对于不使用端口乘数的情况,端口乘数位置通常为0xFFFF)。
#include <Uefi.h>
#include <Protocol/AtaPassThru.h>
EFI_STATUS ReadUsingNCQ(
EFI_ATA_PASS_THRU_PROTOCOL *AtaPassThru,
UINT16 Port,
UINT16 PortMultiplier,
UINT64 Lba,
UINT32 SectorCount,
VOID *Buffer
)
{
EFI_ATA_COMMAND_BLOCK Acb = {0};
EFI_ATA_STATUS_BLOCK Asb = {0};
EFI_ATA_PASS_THRU_COMMAND_PACKET Packet = {0};
EFI_STATUS Status;
// 填充命令块
Acb.AtaCommand = 0x60; // READ FPDMA QUEUED
Acb.AtaSectorNumber = (UINT8)(Lba & 0xFF);
Acb.AtaCylinderLow = (UINT8)((Lba >> 8) & 0xFF);
Acb.AtaCylinderHigh = (UINT8)((Lba >> 16) & 0xFF);
Acb.AtaDeviceHead = (UINT8)(0x40 | ((Lba >> 24) & 0x0F)); // LBA模式
Acb.AtaSectorCount = (UINT8)(SectorCount & 0xFF); // 仅使用SectorCount的低字节
// 准备命令包
Packet.Protocol = EFI_ATA_PASS_THRU_PROTOCOL_PIO_DATA_IN;
Packet.Length = EFI_ATA_PASS_THRU_LENGTH_BYTES | EFI_ATA_PASS_THRU_LENGTH_SECTOR_COUNT;
Packet.Acb = &Acb;
Packet.Asb = &Asb;
Packet.Timeout = 3000; // 3秒超时
Packet.InDataBuffer = Buffer;
Packet.InTransferLength = SectorCount * 512; // 假设每个扇区512字节
// 发送命令
Status = AtaPassThru->PassThru(AtaPassThru, Port, PortMultiplier, &Packet, NULL);
return Status;
}
在这个示例中,我们首先填充了一个EFI_ATA_COMMAND_BLOCK结构,指定了NCQ读命令(命令码0x60)和目标LBA地址。然后,我们创建了一个EFI_ATA_PASS_THRU_COMMAND_PACKET结构,指定了数据传输的方向、数据和命令的长度、以及指向命令和状态块的指针。最后,我们通过调用AtaPassThru->PassThru函数发送命令。
在UEFI规范中,EFI_ATA_PASS_THRU_PROTOCOL用于提供一个标准方式来发送ATA命令到ATA设备。这个协议中使用了几个结构体来封装命令的细节。下面是对EFI_ATA_COMMAND_BLOCK (Acb)、EFI_ATA_STATUS_BLOCK (Asb)、EFI_ATA_PASS_THRU_COMMAND_PACKET (Packet)以及LBA的介绍。
EFI_ATA_COMMAND_BLOCK (Acb)
这个结构体包含了发送到ATA设备的命令的具体细节。它直接映射到ATA命令的结构,包括命令码、LBA地址、扇区数等。这些字段直接对应于ATA命令协议。
- **AtaCommand: **ATA命令码,比如读命令0x25代表读取扇区。
- **AtaFeatures: **用于指定命令的特定功能。
- **AtaSectorNumber: **LBA的低8位。
- AtaCylinderLow 和 AtaCylinderHigh: 这两个字段共同与AtaSectorNumber和AtaDeviceHead一起定义了28位或48位的LBA地址。
- AtaDeviceHead: 包含了设备/头选择位。
- **AtaSectorCount: **指定要操作的扇区数。
EFI_ATA_STATUS_BLOCK (Asb)
这个结构体包含了执行ATA命令后的状态信息。它包括了错误码和状态码,可以用来判断命令是否成功执行。
- **AtaStatus: **包含了设备的状态信息,比如命令是否成功完成。
- **AtaError: **如果AtaStatus指示命令执行出错,这个字段包含错误的具体信息。
EFI_ATA_PASS_THRU_COMMAND_PACKET (Packet)
这个结构体封装了一个ATA命令的所有信息,包括命令本身、数据传输的方向、超时时间等。它是EFI_ATA_PASS_THRU_PROTOCOL中用于发送命令的主要数据结构。
- **Timeout: **命令执行的超时时间,以100纳秒单位表示。
- **Protocol: **指定数据传输协议,如PIO或DMA。
- **Length: **指定Acb、Asb和数据缓冲区的长度。
- **Acb: **指向EFI_ATA_COMMAND_BLOCK的指针,定义了要发送的命令。
- **Asb: **指向EFI_ATA_STATUS_BLOCK的指针,接收命令执行的状态。
- **InDataBuffer/OutDataBuffer: **数据传输的缓冲区。
- **InTransferLength/OutTransferLength: **数据传输的长度。
LBA (Logical Block Addressing)
LBA是一种硬盘寻址方式,它允许系统以线性方式寻址硬盘上的扇区,而不是传统的柱面-磁头-扇区(CHS)寻址方式。LBA使得操作系统和应用程序不需要知道硬盘的物理几何结构。在ATA命令中,LBA用来指定要读写的数据的位置。
- LBA地址通常为28位或48位,允许访问的存储容量远大于CHS寻址方式。
AllocateAlignedPages()// 分配对齐的内存页
AllocateAlignedPages(EFI_SIZE_TO_PAGES(sizeof(EFI_ATA_PASS_THRU_COMMAND_PACKET));
EFI_SIZE_TO_PAGES(sizeof(EFI_ATA_PASS_THRU_COMMAND_PACKET)):
EFI_SIZE_TO_PAGES 是一个宏,用于将字节数转换为页数。
sizeof(EFI_ATA_PASS_THRU_COMMAND_PACKET) 计算 EFI_ATA_PASS_THRU_COMMAND_PACKET 结构的大小(以字节为单位)。
该宏会根据系统页大小(通常为4KB)计算需要多少页来容纳这个结构。
AllocateAlignedPages:
这是一个用于分配对齐的内存页的UEFI函数。
这个函数通常有两个参数:页数和对齐要求。
Passthru()源码到底在哪?
在刚开始学习EDK2时,不理解驱动的加载和启动过程,想要找到相关协议的具体流程也十分困难。因为最近在学习Nvme相关协议,因此更以Nvme为例探究一下。
在NvmExpressPassthru.h中存在与ATA类似的EFI_NVM_EXPRESS_PASS_THRU_PASSTHRU
它的功能为向 NVM Express 控制器或命名空间发送 NVM Express 命令包。该功能支持阻塞 I/O 和非阻塞 I/O。阻塞 I/O 功能是必需的,而非阻塞 I/O 功能是可选的。
typedef
EFI_STATUS
(EFIAPI *EFI_NVM_EXPRESS_PASS_THRU_PASSTHRU)(
IN EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL *This,
IN UINT32 NamespaceId,
IN OUT EFI_NVM_EXPRESS_PASS_THRU_COMMAND_PACKET *Packet,
IN EFI_EVENT Event OPTIONAL
);
- 这个typedef定义了一个函数指针类型,名为EFI_NVM_EXPRESS_PASS_THRU_PASSTHRU。
- 这个函数指针类型的签名(参数列表和返回类型)与NvmExpressPassThru()函数完全匹配。
- 在UEFI驱动模型中,这种typedef通常用于定义协议接口的函数。
- EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL结构体中会有一个这种类型的成员,通常命名为PassThru。
- 当驱动程序初始化时,它会将NvmExpressPassThru函数的地址赋值给协议实例的PassThru成员。
所以,它的实现其实是在NvmExpressPassthru.c中的NvmExpressPassThru函数,在驱动程序的某块地方是实现了绑定的。
例如,在NvmExpress.c中的NvmExpressDriverBindingStart函数实现中存在如下
EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL *NvmePassThru;
// ... 初始化代码 ...
NvmePassThru->PassThru = NvmExpressPassThru;
如此,当其他组件通过协议接口调用PassThru函数时,实际上就是在调用NvmExpressPassThru函数。
这种设计模式允许UEFI在不知道具体实现的情况下,通过统一的接口调用特定驱动程序的功能。它是UEFI驱动程序模型中实现多态性和模块化的关键机制之一。
那么进一步 NvmExpressDriverBindingStart 函数又在什么时候使用了呢?在NvmExpress.c的最开头有
EFI_DRIVER_BINDING_PROTOCOL gNvmExpressDriverBinding = {
NvmExpressDriverBindingSupported,
NvmExpressDriverBindingStart,
NvmExpressDriverBindingStop,
0x10,
NULL,
NULL
};
这段代码定义了一个EFI_DRIVER_BINDING_PROTOCOL结构体实例,名为gNvmExpressDriverBinding。这是UEFI驱动程序模型中的一个关键组件。它的结构和用途:
- 结构解释:
- NvmExpressDriverBindingSupported: 用于检测驱动程序是否支持特定设备的函数。
- NvmExpressDriverBindingStart: 用于启动驱动程序管理设备的函数。
- NvmExpressDriverBindingStop: 用于停止驱动程序管理设备的函数。
- 0x10: 驱动程序的版本号。
- NULL, NULL: 预留字段,通常设为NULL。
- 使用时机:
- 驱动程序加载:当UEFI固件加载驱动程序时,它会查找并使用这个结构。
- 设备检测:系统会调用Supported函数来确定驱动程序是否支持某个设备。
- 驱动程序启动:如果Supported返回成功,系统会调用Start函数来初始化设备。
- 驱动程序停止:当需要卸载驱动程序或重新配置设备时,会调用Stop函数。
- 实际应用:
- 在驱动程序的入口点函数中,通常会安装这个协议:
EFI_STATUS
EFIAPI
NvmExpressDriverEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
return EfiLibInstallDriverBindingComponentName2 (
ImageHandle,
SystemTable,
&gNvmExpressDriverBinding,
ImageHandle,
&gNvmExpressComponentName,
&gNvmExpressComponentName2
);
}
- 工作流程:
- UEFI固件加载驱动程序并调用其入口点函数。
- 入口点函数安装DriverBinding协议。
- 当系统检测到新设备时,它会遍历所有已安装的DriverBinding协议。
- 对每个协议,系统调用Supported函数检查兼容性。
- 如果Supported返回成功,系统调用Start函数初始化设备。
- 如果需要停止驱动程序管理设备,系统调用Stop函数。
这个结构是UEFI驱动程序与系统其他部分交互的主要接口,它定义了驱动程序的生命周期管理方法。通过这个机制,UEFI可以动态地加载、启动和停止设备驱动程序,实现了高度的模块化和灵活性。
而在EDK2的代码中,我并没有找到NvmExpressDriverEntry函数的显示调用,为什么它能够生效?我一开始猜想是不是在 gBS->LocateHandleBuffer的时候底层有实现,然而查询资料后发现并不是。
实际上,NvmExpressDriverEntry 函数通常不会在代码中被显式调用。这是因为它是驱动程序的入口点函数,由 UEFI 固件自动调用。解释一下这个过程:
- 驱动程序加载: 当 UEFI 固件加载驱动程序时(通常是在系统启动过程中),它会自动调用驱动程序的入口点函数,即 NvmExpressDriverEntry。
- 入口点函数的作用: NvmExpressDriverEntry 函数通常会安装 DriverBinding 协议和其他必要的协议(如 ComponentName)。
- DriverBinding 协议的安装: 入口点函数安装 DriverBinding 协议后,UEFI 固件就能够通过这个协议与驱动程序交互。
- gBS->LocateHandleBuffer 的作用: 这个函数不是用来调用 NvmExpressDriverEntry 的。它通常用于查找已安装的协议或设备句柄。在驱动程序的上下文中,它可能用于:
- 查找支持特定协议的设备句柄
- 检测系统中是否存在某种特定类型的设备
- 实际调用过程:
- 系统启动时,UEFI 固件加载驱动程序并调用 NvmExpressDriverEntry
- NvmExpressDriverEntry 安装 DriverBinding 协议
- 当系统需要管理 NVMe 设备时,它会通过已安装的 DriverBinding 协议找到并使用 NVMe 驱动程序
- 为什么看不到显式调用: 因为入口点函数的调用是由 UEFI 固件自动处理的,所以在驱动程序的源代码中通常看不到对它的直接调用。
- 编译和链接: 在编译和链接过程中,驱动程序的入口点会被正确地设置,使得 UEFI 固件能够找到并调用它。
总之,NvmExpressDriverEntry 函数是驱动程序与 UEFI 系统集成的关键点,但它的调用是由系统自动处理的,而不是在代码中显式调用的。gBS->LocateHandleBuffer 是用于其他目的的 UEFI 引导服务函数,不直接涉及驱动程序入口点的调用。
标签:PassThru,驱动程序,ATA,EFI,UEFI,PASS,THRU From: https://blog.csdn.net/Kev1nnn/article/details/140439365