罗冰:系统安全(特别是物理隔离领域)专家,主导开发网络隔离卡、双网隔离机、国产隔离系统、单向光传输等各类安全产品,拥有十几项发明和实用新型专利。致力于UEFI技术的研究、实践和推广,在CSDN和知乎上设有“UEFI开发探索”专栏。译著《现代X86汇编语言程序设计》、《21世纪机器人》,目前就职于国内某网络安全公司,担任总工程师。
目录
进行项目开发、构建产品框架的时候,最开始需要考虑的就是采用哪种通信方式让软件可以访问外部设备(简称外设)。计算机经过多年的发展,提供了非常丰富的通信协议供程序员选择。从古老的串口协议,到使用广泛的 PCI/PCIE 协议,再到现在无处不在的 USB 协议等,可供选择的方式实在太多。
在 Legacy BIOS 下,第三方开发者编写访问外设的代码是件非常辛苦的事情。主要原因在于,Legacy BIOS 对很多协议支持得并不好。以笔者常用的 SMBus 协议为例,直到现在,也没有 BIOS 厂家提供标准的中断接口。大多数时候,只能通过阅读主板芯片组规格书,了解与 SMBus 协议相关的寄存器信息,再根据标准的 SMBus 总线读写协议编写代码。市场上主板芯片组太多,这种方法写出来的代码,兼容性、稳定性都不理想,并且工作量非常大。
UEFI BIOS 的出现,解决了上述这些问题。从 UEFI 标准和 EDK2 的源码也可以看出,常用的总线协议如 PCI/PCIE、SMBus、串口等,UEFI 都已经提供了支持。对于依赖于 BIOS 接口进行产品开发的厂商来说,这是一个非常好的消息,产品的开发速度将大幅提高,稳定性和兼容性也能得到保障。
本章将介绍如何在 UEFI 环境下使用 UEFI 规范提供的接口(即各类 Protocol),通过 PCI/PCIE、SMBus 和串口访问外设。
从硬件结构角度看,PCI 和 PCIE 有很大的不同。PCI 总线采用并行总线结构,而 PCIE 总线使用了高速差分总线结构,使用端到端的连接方式,这使得两者采用的拓扑结构差异较大。随着技术的发展,目前市场上 PCI 设备越来越少,很多主板现在只提供 PCIE 的接口了。
这些差异对在 UEFI 下进行编程影响不大。UEFI 系统已经屏蔽了这些差异,提供了一致的访问接口,下面详细介绍如何访问 PCI/PCIE 设备。
PCI 协议和 PCIE 协议经过多年的发展,其内容已经非常庞大。本书主要论述的是如何在 UEFI 下进行编程。站在软件工程师的角度,在访问 PCI/PCIE 设备时,实际上只要回答以下两个问题就可以了。
Ø如何在系统中找到需要访问的设备?
Ø找到设备后,如何访问设备内的寄存器或其他资源?
UEFI 规范中,抽象了 PCI 的系统架构,典型的桌面系统的 PCI 架构如图 7-1 所示。
图 7-1 单 PCI Root Bridge 的桌面系统
一般的桌面系统只有一个 PCI Host Bus(PCI 主机总线),用于完成 CPU 与 PCI 设备之间的数据交换。PCI Root Bridge(PCI 根桥)一般也只有一个,它管理一个局部总线,下挂一棵 PCI 总线树。我们所要访问的 PCI 设备,就挂在这棵总线树上,它们属于同一总线空间,如图 7-2 所示。
从图 7-2 中可以看出,PCI 总线树上包含 PCI 总线、PCI 桥和 PCI 设备。系统通过三段编码的方式进行编码,即通过 Bus Number(总线号)、Device Number(设备号)和 Function Number(功能号)来编码,这种编码一般简称为 BDF 码。BDF 码在 BIOS 进行 PCI 总线扫描和枚举过程中确定,可以用来作为查找 PCI 设备的索引。
图 7-2 PCI 总线树
找到 PCI 设备后,如何确定此设备就是自己要找的设备呢?每个 PCI 设备,除了主总线桥外,都会实现配置空间(主总线桥可以有选择地实现),而在配置空间中,包含了设备厂商用来标志自身的 Vendor ID(供应商 ID)和 Device ID(设备 ID)。通过比对 PCI 设备的供应商 ID 和设备 ID,可以确定所找的设备是否为目标设备。
以 X86 平台为例,可通过 CONFIG_ADDR 寄存器(0xCF8)和 CONFIG_DATA 寄存器(0xCFC),以 BDF 码的形式访问 PCI 设备,以得到设备的配置空间。图 7-3 所示为 PCI设备的基本配置空间。
图 7-3 PCI 设备的配置空间
PCI 设备的基本配置空间由 64 字节组成,地址范围为 0x00~0x3F,主要用来识别设备、定义主机访问 PCI 卡的方式。从图 7-3 中可以看出,最开始的两个寄存器就是 Vendor ID 和 Device ID 寄存器,这是用来标志设备自身的寄存器,由 PCISIG 协会分配。比如Intel 集成显卡 HD620,其 Vendor ID 为 0x8086,而 Device ID 为 0x5917。
在配置空间中,从地址 0x10 至 0x24,包含了 6 个 Base Address Registe(r 基址寄存器)。这组寄存器被称为 BAR,保存了 PCI 设备使用的地址空间的基地址,也即该设备在 PCI 总线域中的地址。每个 PCI 设备最多可以有 6 个基址空间,但多数设备不会使用这么多,笔者以前常用的南京沁恒的 CH366 芯片,只使用了第一个 BAR。
BAR 可寻址 IO 地址空间或者 Memory 地址空间,其最低位是只读位,显示了可以访问哪种地址空间。值为 0 表示寄存器是 Memory 地址译码,值为 1 表示寄存器是 IO 地址译码,如图 7-4 所示。
图 7-4 基地址寄存器的位分配
那么如何访问 PCI 设备内的寄存器和其他资源?答案是通过 BAR 寄存器,加上内部寄存器相对于 BAR 的偏移地址。芯片手册中,会提供关于内部资源的使用说明,以 CH366 的芯片为例,其内部寄存器说明如图 7-5 所示。
图 7-5 CH366 寄存器说明(节选自《CH366 中文手册》)
CH366 的第一个 BAR 可以使用,它是 IO 地址译码的,其他 BAR 在芯片中是无效的。图 7-4 表明,第一个 BAR 加上偏移地址,就可以成为芯片内部相应功能的寄存器。至于这些内部寄存器有什么作用,则需要详细了解芯片手册才能知道。
总结来说,访问 PCI/PCIE 设备的过程如下。
1)扫描整个系统空间,通过 BDF 获取 PCI/PCIE 设备的配置空间。同一总线域上(即存在一个主桥),PCIE 一共支持 256 个总线、32 个设备、8 个功能,也就是说总线号最大值为 255、设备号最大值为 31、功能号最大为 7。
2)读取 PCI/PCIE 设备配置空间中的 Vendor ID 和 Device ID,确定是否为需要访问的设备。
3)找到 PCI/PCIE 设备后,获取其配置空间中的 BAR,参照芯片手册,访问设备的内部寄存器和资源。
UEFI 中提供了两个主要的模块来支持 PCI 总线,一是 PCI Host Bridge(PCI 主桥)控制器驱动,另一个是 PCI 总线驱动。这两个模块是和特定的平台硬件绑定的,在这种机制下,屏蔽了不同的 CPU 架构差异,为软件开发者提供了比较一致的 Protocol 接口。下一节详细介绍访问 PCI/PCIE 设备的 Protocol。
UEFI 标准中提供了两类访问 PCI/PCIE 设备的 Protocol—EFI_PCI_ROOT_BRIDGE_ IO_PROTOCOL 和 EFI_PCI_IO_PROTOCOL。前者为 PCI 根桥提供了抽象的 IO 功能,它由 PCI Host Bus Controller(PCI 主总线驱动器)产生,一般由 PCI/PCIE 总线驱动用来枚举设备、获得 Option ROM、分配 PCI 设备资源等;后者由 PCI/PCIE 总线驱动为 PCI/PCIE 设备产生,一般由 PCI/PCIE 设备驱动用来访问 PCI/PCIE 设备的 IO 空间、Memory 空间和配置空间。
这两种 Protocol 的使用方法如下。
1.使用 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 中提供了基本的访问接口,包括访问 IO 空间、Memory 空间和配置空间的接口。该 Protocol 主要由 PCI/PCIE 总线驱动使用,当然, UEFI 应用也可以使用它来遍历 PCI/PCIE 设备。
该 Protocol 中还提供了 DMA 接口,以支持总线驱动访问系统内存。代码清单 7-1 给出了 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 的函数接口。
代码清单 7-1 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 函数接口
typedef struct _EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL { EFI_HANDLE ParentHandle; //Protocol的父句柄
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_POLL_IO_MEM PollMem; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_POLL_IO_MEM PollIo;
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS Mem; //读写Memory空间
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS Io; //读写IO空间
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS Pci; //读写配置空间EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_COPY_MEM CopyMem; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_MAP Map; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_UNMAP Unmap;
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ALLOCATE_BUFFER AllocateBuffer; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_FREE_BUFFER FreeBuffer; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_FLUSH Flush; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_GET_ATTRIBUTES GetAttributes; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_SET_ATTRIBUTES SetAttributes; EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_CONFIGURATION Configuration;
UINT32 SegmentNumber;
} EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL;
从代码清单 7-1 中可以看出,此 Protocol 提供了非常丰富的访问接口。由于篇幅所限, 无法将所有接口都介绍清楚,这里主要介绍如何读写 PCI/PCIE 设备的 3 种空间。
从 7.1.1 节的介绍中我们知道,PCI/PCIE 设备能访问的空间包括 Memory 空间、IO 空间和配置空间。对于这 3 种空间,每个 PCI/PCIE 设备必须实现配置空间,而 Memory 空间和 IO 空间的功能,则不一定实现。7.1.1 节介绍的 PCIE 芯片 CH366 就只支持 IO 空间的访问。
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 提供了访问 Memory 空间的接口 Mem、访问 IO 空间的接口 Io 和访问配置空间的接口 Pci。这 3 个接口的参数类型都是一样的,均为EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS,详见代码清单 7-2。
代码清单 7-2 访问 IO 空间、Memory 空间和配置空间的接口
typedef struct {
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_IO_MEM Read; //读数据EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_IO_MEM Write; //写数据
} EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_ACCESS;
typedef EFI_STATUS (EFIAPI *EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_IO_MEM) (
IN EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL *This, //实例
IN EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_WIDTH Width, //读写宽度IN UINT64 Address, //IO空间/Memory空间/配置空间的地址
IN UINTN Count, //读写的数据个数,单位为读写宽度Width
IN OUT VOID *Buffer //对读操作,这是目的缓冲区;对写操作,这是要写的数据缓冲区);
访问 3 种空间的接口都包含读数据和写数据两种操作,并且使用了同样的数据结构EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_IO_MEM。在此结构中,Width 是指读写宽度, 其值由枚举类型 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL_WIDTH 给出,一般包括 8 位、16 位、32 位和 64 位几种。
需要注意的是,参数 Address 在访问 IO 空间、Memory 空间和配置空间时,其含义是不同的。对配置空间而言,Address 由 BDF 地址和 Register 偏移决定,即总线号、设备号、功能号和 Register 共同给出的寻址用索引。这是一个 64 位长的参数,一般使用宏 EFI_ PCI_ADDRESS 来组合 BDF 和 Register 偏移。在 EDK2 中,这个宏定义于头文件 MdePkg\ Include\Protocol\PciRootBridgeIo.h 中,其内容如下。
#define EFI_PCI_ADDRESS(bus, dev, func, reg) \
(UINT64) ( \
(((UINTN) bus) << 24) | \ (((UINTN) dev) << 16) | \ (((UINTN) func) << 8) | \
(((UINTN) (reg)) < 256 ? ((UINTN) (reg)): (UINT64) (LShiftU64 ((UINT64)\ (reg), 32))))
对 IO 空间而言,参数 Address 是指 PCI 设备 IO 空间的 IO 地址;对 Memory 空间而言,参数 Address 是指 PCI 设备 Memory 空间的 Memory 地址。参考 4.2.1 节可知,IO 地址和 Memory 地址是由 BAR 和偏移决定的,每个地址的作用还需要查看对应芯片的说明手册。
2.使用 EFI_PCI_IO_PROTOCOL
在 PCI/PCIE 设备驱动中,一般使用 EFI_PCI_IO_PROTOCOL 来访问设备的内部资源, Protocol 挂载在 PCI/PCIE 控制器上,运行在 EFI 启动服务环境中,对 PCI/PCIE 设备进行Memory 空间和 IO 空间访问。其函数接口如代码清单 7-3 所示。
代码清单 7-3 EFI_PCI_IO_PROTOCOL 函数接口
typedef struct _EFI_PCI_IO_PROTOCOL { EFI_PCI_IO_PROTOCOL_POLL_IO_MEM PollMem; EFI_PCI_IO_PROTOCOL_POLL_IO_MEM PollIo;
EFI_PCI_IO_PROTOCOL_ACCESS Mem; //读写Memory空间
EFI_PCI_IO_PROTOCOL_ACCESS Io; //读写IO空间
EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS Pci; //读写配置空间EFI_PCI_IO_PROTOCOL_COPY_MEM CopyMem; EFI_PCI_IO_PROTOCOL_MAP Map; EFI_PCI_IO_PROTOCOL_UNMAP Unmap;
EFI_PCI_IO_PROTOCOL_ALLOCATE_BUFFER AllocateBuffer; EFI_PCI_IO_PROTOCOL_FREE_BUFFER FreeBuffer; EFI_PCI_IO_PROTOCOL_FLUSH Flush;
EFI_PCI_IO_PROTOCOL_GET_LOCATION GetLocation; EFI_PCI_IO_PROTOCOL_ATTRIBUTES Attributes; EFI_PCI_IO_PROTOCOL_GET_BAR_ATTRIBUTES GetBarAttributes; EFI_PCI_IO_PROTOCOL_SET_BAR_ATTRIBUTES SetBarAttributes; UINT64 RomSize;
VOID *RomImage;
} EFI_PCI_IO_PROTOCOL;
代码清单 7-4 访问 IO 空间和 Memory 空间的接口
typedef struct {
EFI_PCI_IO_PROTOCOL_IO_MEM Read; //读数据EFI_PCI_IO_PROTOCOL_IO_MEM Write; //写数据
} EFI_PCI_IO_PROTOCOL_ACCESS;
typedef EFI_STATUS (EFIAPI *EFI_PCI_IO_PROTOCOL_IO_MEM) (
IN EFI_PCI_IO_PROTOCOL *This, //EFI_PCI_IO_PROTOCOL实例
IN EFI_PCI_IO_PROTOCOL_WIDTH Width, //读写宽度,8位、16位、32位、64位IN UINT8 BarIndex, //在配置空间中的BAR索引值
IN UINT64 Offset, //偏移寄存器,用来进行IO空间/Memory空间读写IN UINTN Count, //读写的数据个数,单位为读写宽度Width
IN OUT VOID *Buffer //对读操作,这是目的缓冲区;对写操作,这是要写的数据缓冲区
);
上述代码中的参数 This 指向的是与 PCI/PCIE 设备本身相关的 EFI_PCI_IO_PROTOCOL实例,因此,在访问设备时比较直接,不需要通过 BDF 等方式给出设备的地址。
IO 空间读写使用的函数为 Io.Read() 和 Io.Write() ;Memory 空间读写使用的函数为Mem.Read() 和 Mem.Write()。读写数据的时候,所需要访问的地址由 BarIndex 和 Offset 共同规定。图 7-3 所示为 PCI/PCIE 设备的配置空间,从图中可知,BAR 总共有6 个, BarIndex 值的范围为 0 至 5。最终访问的地址,等于 BarIndex 所指向的 BAR 加上 Offset。至于此地址的含义,仍旧得查看 PCI/PCIE 芯片厂家提供的说明手册。
访问配置空间使用的数据结构为 EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS,其接口说明如代码清单 7-5 所示。
代码清单 7-5 访问配置空间的接口
typedef struct {
EFI_PCI_IO_PROTOCOL_CONFIG Read; //读数据EFI_PCI_IO_PROTOCOL_CONFIG Write; //写数据
} EFI_PCI_IO_PROTOCOL_CONFIG_ACCESS;
typedef EFI_STATUS (EFIAPI *EFI_PCI_IO_PROTOCOL_CONFIG) (
IN EFI_PCI_IO_PROTOCOL *This, //EFI_PCI_IO_PROTOCOL实例
IN EFI_PCI_IO_PROTOCOL_WIDTH Width, //读写宽度,8位、16位、32位、64位IN UINT32 Offset, //偏移,在配置空间内的偏移地址
IN UINTN Count, //读写的个数,以Width为单位
IN OUT VOID *Buffer //对读操作,这是目的缓冲区;对写操作,这是要写的数据缓冲区
);
Pci.Read() 和 Pci.Write() 函数用来访问 PCI/PCIE 设备的配置空间。参数 Offset 用来指定在配置空间内的偏移地址,比如 Offset=0x10 时,是指 BAR0 寄存器。
PCI 设备的基本配置空间是由 64 字节(0x00~0x3F)组成的,这是所有 PCI/PCIE 设备必须支持的。此外,PCI/PCIE 设备还扩展了 0x40~0xFF 这段配置空间,主要用来存放于MSI 中断机制和电源管理相关的 Capability 结构。另外,PCIE 设备还支持 0x100~0xFFF 这段配置空间,这段配置空间用于存放 PCIE 设备独有的 Capability 结构。
这些配置空间的信息,都可以通过 EFI_PCI_IO_PROTOCOL 获取。至于配置空间内寄存器的具体含义,读者可以参考 PCI 标准和 PCIE 标准进行深入学习。
本节准备了相应的示例,演示如何使用 7.1.2 节介绍的两种 Protocol 来遍历系统内的PCI/PCIE 设备。大多数的机器上,只存在一个 PCI 总线域(PCI Segment),即一个主桥。因此,在使用 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 的时候,应该只会找到一个实例。我们设计的程序,其主要功能如下。
使用 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL,通过 BDF,遍历所有 PCI/PCIE设备,打印出设备的相关信息。
寻找所有的 EFI_PCI_IO_PROTOCOL 实例,直接访问每个实例的配置空间,将其信息打印出来。
本节提供的示例程序位于随书代码的 RobinPkg\Applications\ListPCIMsg 目录下。示例
7-1 演示了如何获取两类 Protocol 的实例。
【示例 7-1】获取 Protocol 的实例。
EFI_STATUS LocatePCIRootBridgeIO(void)
{
EFI_STATUS Status;
EFI_HANDLE *PciHandleBuffer = NULL;
UINTN HandleIndex = 0;
UINTN HandleCount = 0;
//获取PciRootBridgeIOProtocol的所有句柄Status = gBS->LocateHandleBuffer(
ByProtocol, &gEfiPciRootBridgeIoProtocolGuid, NULL,
&HandleCount, &PciHandleBuffer
);
if (EFI_ERROR(Status)) return Status;
Print(L"Find PCI Root Bridge I/O Protocol: %d\n",HandleCount);
//获取PciRootBridgeIOProtocol实例
for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++)
{
Status = gBS->HandleProtocol( PciHandleBuffer[HandleIndex], &gEfiPciRootBridgeIoProtocolGuid, (VOID**)&gPCIRootBridgeIO);
if (EFI_ERROR(Status)) continue; else
return EFI_SUCCESS;
}
return Status;
}
EFI_STATUS LocatePCIIO(void)
{
EFI_STATUS Status;
EFI_HANDLE *PciHandleBuffer = NULL;
UINTN HandleIndex = 0;
UINTN HandleCount = 0;
//获取PciIoProtocol的所有句柄 Status = gBS->LocateHandleBuffer(
ByProtocol, &gEfiPciIoProtocolGuid, NULL,
&HandleCount, &PciHandleBuffer
);
if (EFI_ERROR(Status)) return Status; //unsupport gPCIIO_Count = HandleCount;
Print(L"Find PCI I/O Protocol: %d\n",HandleCount);
//获取PciIoProtocol实例,并存储在全局变量gPCIIOArray中
for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++)
{
Status = gBS->HandleProtocol( PciHandleBuffer[HandleIndex], &gEfiPciIoProtocolGuid, (VOID**)&(gPCIIOArray[HandleIndex]));
}
return Status;
}
示例 7-1 中提供了两个函数—LocatePCIRootBridgeIO() 和 LocatePCIIO(),用来获取需要测试的两类 Protocol 的实例。获取实例的方法在 3.5 节中已经介绍过了,本节的例程用了同样的方法。EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 的实例,在大部分办公用的个人电脑中只存在一个,因此直接用全局指针变量 gPCIRootBridgeIO 存储;而 EFI_PCI_IO_ PROTOCOL 的实例存在多个,一般有多少个 PCI/PCIE 设备,就存在多少个实例,因此使用全局指针数组 gPCIIOArray[256] 来存储这些实例。
为遍历全部的 PCI/PCIE 设备,可以使用 gPCIRootBridgeIO 和 BDF 码,循环查找挂载总线上的设备,代码如示例 7-2 所示。
【示例 7-2】使用 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL 遍历 PCI/PCIE 设备。
EFI_STATUS ListPCIMessage1(void)
{
EFI_STATUS Status=EFI_SUCCESS; PCI_TYPE00 Pci;
UINT16 i,j,k,count=0; for(k=0;k<=PCI_MAX_BUS;k++)
for(i=0;i<=PCI_MAX_DEVICE;i++) for(j=0;j<=PCI_MAX_FUNC;j++)
{
//判断设备是否存在
Status = PciDevicePresent(gPCIRootBridgeIO,&Pci,\ (UINT8)k,(UINT8)i,(UINT8)j);
if (Status == EFI_SUCCESS) //找到了设备
{
++count;
Print(L"%02d. Bus-%02x Dev-%02x Func-%02x: ",\ count,(UINT8)k,(UINT8)i,(UINT8)j);
Print(L"VendorID-%x DeviceID-%x ClassCode-%x",\ Pci.Hdr.VendorId,Pci.Hdr.DeviceId,Pci.Hdr.ClassCode[0]);
Print(L"\n");
}
}
return EFI_SUCCESS;
}
从代码中可以看出,函数使用了 3 个 for 循环调用函数 PciDevicePresent(),依次寻找PCI/PCIE 设备是否存在。如果存在,则取出已经读取到的配置空间的数据,将设备的一些信息打印出来。
使用 EFI_PCI_IO_PROTOCOL 遍历设备则比较简单,因为之前所得到的此 Protocol 的实例,就是为 PCI/PCIE 设备产生的,实际上相当于找到了设备,只需要将设备的信息打印出来即可。相应的代码见示例 7-3 所示。
【示例 7-3】使用 EFI_PCI_IO_PROTOCOL 遍历 PCI/PCIE 设备。
EFI_STATUS ListPCIMessage2(void)
{
UINTN i,count=0;
PCI_TYPE00 Pci;
for(i=0;i
{
gPCIIOArray[i]->Pci.Read(gPCIIOArray[i],EfiPciWidthUint32,0,\
sizeof (PCI_TYPE00) / sizeof (UINT32),&Pci);
++count;
Print(L"%02d. VendorID-%x DeviceID-%x ClassCode-%x",\ count,Pci.Hdr.VendorId,Pci.Hdr.DeviceId,Pci.Hdr.ClassCode[0]);
Print(L"\n");
}
return EFI_SUCCESS;
}
本节所准备的示例,主要是为了演示如何使用与 PCI/PCIE 相关的两个 Protocol。代码本身还有许多不完善的地方,比如对多个总线域情况的处理、内存的释放、Protocol 的关闭等,都没有考虑。本书的代码,包括本节的代码在内,建议读者只用来学习使用,如果想商用,则应该在代码中将所有情况考虑到。
可参照 2.1.3 节的方法,设置编译的环境变量,并使用如下命令编译程序:
C:\UEFIWorkspace\edk2\build -p RobinPkg\RobinPkg.dsc \
-m RobinPkg\Applications\ListPCIMsg\ ListPCIMsg.inf -a X64
所编译的程序最好在实际的机器上测试运行。笔者使用 2.2.2 节搭建的 QEMU 环境来运行编译好的 64 位 UEFI 程序,程序运行的结果如图 7-6 所示。
图 7-6 测试 ListPCIMsg 程序
第七章完整内容可点击下面链接或识别二维码查看
https://oxz.xet.tech/s/T4gOj
如需进群请添加小月微信邀请:linuxer2016
Ftrace训练营火热报名中:Ftrace训练营:站在设计者的角度来理解ftrace(限50人)。训练营第一期报名已圆满成功,好评如潮。第二期课程正在进行中,第三期报名正在火爆进行中(咨询小月微信:linuxer2016)。
ARM安全架构训练营2期火热报名中:阅码场训练营:ARM安全架构之Trustzone-TEE实战报名咨询客服(小月微信:linuxer2016)。
ARM架构与调优调试训练营火热报名中:阅码场训练营:ARM架构与调试调优。报名咨询客服(小月微信:linuxer2016)。