UEFI中的PassThru()

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: 这两个字段共同与AtaSectorNumberAtaDeviceHead一起定义了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
  );
  1. 这个typedef定义了一个函数指针类型,名为EFI_NVM_EXPRESS_PASS_THRU_PASSTHRU。
  2. 这个函数指针类型的签名(参数列表和返回类型)与NvmExpressPassThru()函数完全匹配。
  3. 在UEFI驱动模型中,这种typedef通常用于定义协议接口的函数。
  4. EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL结构体中会有一个这种类型的成员,通常命名为PassThru。
  5. 当驱动程序初始化时,它会将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驱动程序模型中的一个关键组件。它的结构和用途:

  1. 结构解释:
    • NvmExpressDriverBindingSupported: 用于检测驱动程序是否支持特定设备的函数。
    • NvmExpressDriverBindingStart: 用于启动驱动程序管理设备的函数。
    • NvmExpressDriverBindingStop: 用于停止驱动程序管理设备的函数。
    • 0x10: 驱动程序的版本号。
    • NULL, NULL: 预留字段,通常设为NULL。
  2. 使用时机:
    • 驱动程序加载:当UEFI固件加载驱动程序时,它会查找并使用这个结构。
    • 设备检测:系统会调用Supported函数来确定驱动程序是否支持某个设备。
    • 驱动程序启动:如果Supported返回成功,系统会调用Start函数来初始化设备。
    • 驱动程序停止:当需要卸载驱动程序或重新配置设备时,会调用Stop函数。
  3. 实际应用:
    • 在驱动程序的入口点函数中,通常会安装这个协议:
EFI_STATUS
EFIAPI
NvmExpressDriverEntry (
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
    return EfiLibInstallDriverBindingComponentName2 (
        ImageHandle,
        SystemTable,
        &gNvmExpressDriverBinding,
        ImageHandle,
        &gNvmExpressComponentName,
        &gNvmExpressComponentName2
    );
}
  1. 工作流程:
    • UEFI固件加载驱动程序并调用其入口点函数。
    • 入口点函数安装DriverBinding协议。
    • 当系统检测到新设备时,它会遍历所有已安装的DriverBinding协议。
    • 对每个协议,系统调用Supported函数检查兼容性。
    • 如果Supported返回成功,系统调用Start函数初始化设备。
    • 如果需要停止驱动程序管理设备,系统调用Stop函数。

这个结构是UEFI驱动程序与系统其他部分交互的主要接口,它定义了驱动程序的生命周期管理方法。通过这个机制,UEFI可以动态地加载、启动和停止设备驱动程序,实现了高度的模块化和灵活性。

而在EDK2的代码中,我并没有找到NvmExpressDriverEntry函数的显示调用,为什么它能够生效?我一开始猜想是不是在 gBS->LocateHandleBuffer的时候底层有实现,然而查询资料后发现并不是。

实际上,NvmExpressDriverEntry 函数通常不会在代码中被显式调用。这是因为它是驱动程序的入口点函数,由 UEFI 固件自动调用。解释一下这个过程:

  1. 驱动程序加载: 当 UEFI 固件加载驱动程序时(通常是在系统启动过程中),它会自动调用驱动程序的入口点函数,即 NvmExpressDriverEntry。
  2. 入口点函数的作用: NvmExpressDriverEntry 函数通常会安装 DriverBinding 协议和其他必要的协议(如 ComponentName)。
  3. DriverBinding 协议的安装: 入口点函数安装 DriverBinding 协议后,UEFI 固件就能够通过这个协议与驱动程序交互。
  4. gBS->LocateHandleBuffer 的作用: 这个函数不是用来调用 NvmExpressDriverEntry 的。它通常用于查找已安装的协议或设备句柄。在驱动程序的上下文中,它可能用于:
    • 查找支持特定协议的设备句柄
    • 检测系统中是否存在某种特定类型的设备
  5. 实际调用过程:
    • 系统启动时,UEFI 固件加载驱动程序并调用 NvmExpressDriverEntry
    • NvmExpressDriverEntry 安装 DriverBinding 协议
    • 当系统需要管理 NVMe 设备时,它会通过已安装的 DriverBinding 协议找到并使用 NVMe 驱动程序
  6. 为什么看不到显式调用: 因为入口点函数的调用是由 UEFI 固件自动处理的,所以在驱动程序的源代码中通常看不到对它的直接调用。
  7. 编译和链接: 在编译和链接过程中,驱动程序的入口点会被正确地设置,使得 UEFI 固件能够找到并调用它。

总之,NvmExpressDriverEntry 函数是驱动程序与 UEFI 系统集成的关键点,但它的调用是由系统自动处理的,而不是在代码中显式调用的。gBS->LocateHandleBuffer 是用于其他目的的 UEFI 引导服务函数,不直接涉及驱动程序入口点的调用。

  • 33
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值