本文主要是介绍NVMe开发——PCIe配置空间和地址空间,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1. 配置空间
PCI通过引入标准化的配置寄存器和能力结构,提供了一种统一的方式来分配、配置和管理系统资源。PCIe的配置空间在PCI的基础上做了扩展,支持更多功能。
1.1. PCI兼容配置空间
PCI的配置空间为256字节,PCIe兼容此结构。PCI只用前面64字节,剩余的192字节,PCIe扩展了如下功能:
- PCI Express Capability(PCI Express能力)
- Power Management(电源管理)
- MSI and/or MSI-X(MSI和/或MSI-X)
而配置空间,主要关注配置头,因为PCIe设备有两种,一种是终端设备,一种是转换器设备。下文主要关注PCIe终端设备。Vendor ID、Device ID、Revision ID、Subsystem Vendor ID、Subsystem Device ID、Base Class Code、Sub Class Code等字段。
- Vendor ID:厂商标识。是由PCI SIG授权分配的一个ID。
- Device ID:设备标识。这个是Pcie 规范给客户自定义的一个ID,用于区分对同款主控(如marvell、SMI)进行2次开发的不同厂商。
- Revision ID:芯片版本;
- Subsystem Vendor ID:子系统厂商标识;
- Subsystem Device ID:子系统设备标识;
- Base Class Code:设备类型的编码,比如01h表示mass storage controller;
- Sub Class Code:设备子类型编码,比如08h表示ssd controller。
1.2. PCIe扩展配置空间
PCI的256字节信息不够PCIe使用,所以PCIe在此基础上将配置空间扩展到4K字节,如下图中列出了一些主要的扩展寄存器。
1.3. 配置读写机制
1.3.1. 传统PCI机制
传统PCI机制是直接IO访问,针对X86处理器,这种方式只能访问64K IO地址空间。CPU提供了1字节、2字节和4字节这3种IO大小读写IO端口的接口。
- 配置访问的端口地址
在CPU的0xCF8处理配置端口地址,端口地址定义如下:
- Bit[31]配置1表示使能访问配置空间
- Bits[7:2]的值表示对应的PCI兼容配置空间的64个DWORD。
- 读取配置地址的数据
配置完访问端口的地址之后,就可以读取配置空间的数据。通过0xCFC的CPU地址来直接读取。
1.3.2. 增强配置访问机制
传统的访问方式分为两步,这在多核访问时,可能存在竞态的情况,加锁处理影响性能。另外传统方式只能访问256字节,这也限制了对PCIe扩展空间的访问。为此,提出了一个新的增强配置访问机制,即将每个功能的配置空间映射到设备的256MB空间中,按4K对齐来分配访问。
U64 phyAddr = baseAddr + bus<<20 + device<<15 + function<<12;
baseAddr来自ACPI的GFCM值
1.4. 操作示例
Linux下的PCIe设备信息都直接映射成了文件,如可以直接读取/sys/bus/pci/devices/0000:01:00.0/config文件即是配置空间信息。以下示例主要针对Windows系统,并且使用WinIO驱动来实现。
1.4.1. 直接端口访问
SetPortVal和GetPortVal均为WinIO的接口。
int GetPCIConfiguation(int bus, int dev, int fun, PCI_COMMON_CONFIG &pci_config)
{DWORD addr, data;addr = 1<<31 | (bus << 16) | (dev << 11) | (fun << 8);if (!SetPortVal(0xcf8, addr, 4)){return -1;}if (!GetPortVal(0xcfc, &data, 4)){return -2;}// 当0号寄存器的内容为0xffffffff时,即表明无设备if (i == 0 && data == 0xffffffff){return 0;}memcpy(((PUCHAR)&pci_config) + i, &data, 4);return 1;
}
1.4.2. 内存映射
DWORD bufferSize = GetSystemFirmwareTable('ACPI', 0, nullptr, 0);if (bufferSize == 0) {return 1;}BYTE* pBuffer = new BYTE[bufferSize]();DWORD result = GetSystemFirmwareTable('ACPI', 'GFCM', pBuffer, bufferSize);__int64 baseAddr = *(__int64 *)(pBuffer + 44);int GetPCIConfiguation(int bus, int dev, int fun, PCI_COMMON_CONFIG& pci_config)
{PDWORD pdwLinAddr;tagPhysStruct PhysStruct;PhysStruct.dwPhysMemSizeInBytes = 256;// baseAddr为基地址PhysStruct.pvPhysAddress = baseAddr | (bus << 20) | (dev << 15) | (fun << 12);pdwLinAddr = (PDWORD)MapPhysToLin(PhysStruct);if (pdwLinAddr == NULL){return -1;}else if (*pdwLinAddr == 0xffffffff){return 0;}// 这种拷贝方式可能拷贝出奇奇怪怪的值,可以使用上面的方法memcpy(((PUCHAR)&pci_config), pdwLinAddr, 256);UnmapPhysicalMemory(PhysStruct);return 1;
}
2. 设备枚举
2.1. 结构
PCIe系统所有设备构成一个拓扑结构,由Root Complex(相当于Host Controller),P2P(桥接设备)和 EndPoint Device(终端设备)构成。可以根据此信息来进行下一级设备的枚举。如下图:
2.2. P2P
每个桥设备(P2P)包含有最后一级总线号(Subordinate Bus Number)、下一级总线号(Secondary Bus Number)和上一级总线号(Primary Bus Number)等信息。然后我们就可以从RC的Pri/Sec/Sub信号获取其下一级的设备信息,然后通过深度优化来遍历整个设备树。
2.3. 设备类型
如何判断设备是Type0还是Type1呢?Header Type寄存器描述了相关信息。
- Bits[6:0]描述设备类型
-
- 0表示设备为终端设备
- 1表示设备为P2P(PCI-to-PCI)设备,连接2个Bus。
- 2表示卡总线桥接设备,传统接口,现在不常用。
- Bit[7]表示是否为多功能设备,0表示单功能设备,1表示多功能设备。
2.4. 注意项
- 设备不存在时,RC设备向系统响应的是全1(FFFFH)的数据。
- 设备未准备好,针对低速设备,至少在复位后100ms之后再来枚举设备,针对高速设备如速率大于5.0 GT/s(Gen3速度),则软件必须等到链路训练完成后的100ms才能尝试这样做。这是因为速度越高,越需要更多的时候来完成链路的训练。
- 枚举时,通过判断 Vendor ID为非FFFF来确定设备存在。
2.5. Secondary Bus
Hot Reset需要通过Secondary Bus Reset来控制,所以获取获取指定设备的Secondary Bus。除了通过遍历整个拓扑结构图来查询设备的次级总线外,Linux下还可以直接获取设备路径来更方便地查看次级总线BDF地址。
root@ubuntu22:~# readlink /sys/bus/pci/devices/0000:0f:00.0
../../../devices/pci0000:00/0000:00:02.2/0000:0f:00.0
# 0000:00:02.2即为次级总线,0000表示PCIe总线逻辑域,表示为RC0。
2.6. 示例
以下示例为Linux下的深度优先搜索。
__int64 BaseAddr()
{FILE* file = fopen("/sys/firmware/acpi/tables/MCFG", "rb");if (file == NULL) {perror("Failed to open MCFG table file");return 0;}// 获取文件大小fseek(file, 0, SEEK_END);long fileSize = ftell(file);rewind(file);// 分配缓冲区uint8_t* buffer = (uint8_t*)malloc(fileSize);if (buffer == NULL) {perror("Failed to allocate memory for buffer");fclose(file);return 0;}// 读取文件内容size_t bytesRead = fread(buffer, 1, fileSize, file);if (bytesRead != fileSize) {perror("Failed to read MCFG table file"); free(buffer);fclose(file);return 0;}fclose(file);__int64 baseAddr = *(__int64 *)(buffer + 44);return baseAddr;
}// 深度优先搜索PCI设备
void DFSPCI(uint8_t bus, int fd, uint64_t baseAddr, int indentationNum)
{uint64_t addr;uint8_t* ptrdata;uint8_t type;for (int dev = 0; dev < 32; dev++){for (int fun = 0; fun < 8; fun++){addr = baseAddr | (bus<<20) | (dev<<15) | (fun<<12);//要寻找的偏移地址,根据PCIe的物理内存偏移ptrdata = (uint8_t *)mmap(NULL, 64, PROT_READ | PROT_WRITE, MAP_SHARED, fd, addr);//映射后返回的首地址 ---物理地址if (ptrdata == NULL){printf("mmap函数映射失败.\n");munmap (ptrdata, 64);close(fd);return;}else if (*ptrdata == 0xff || *ptrdata == 0){munmap (ptrdata, 64);if (fun == 0)break;elsecontinue;}type = *(ptrdata + 0xe) & 0x01;for (int i = 0; i < indentationNum; i++) {printf("| ");}printf("+--- %02x:%02x.%x\n", bus, dev, fun);if (type == 1){uint8_t subBus = *(ptrdata + 0x19);DFSPCI(subBus, fd, baseAddr, indentationNum + 1);}munmap (ptrdata, 64);}}
}
3. Capabilities
Capabilities是一组描述PCIe设备功能相关的结构,它是一个链式的结构,一个指向下一个,直到最后。
3.1. Capability Pointer
下图中的Capability Pointer的值指向第1个Capability在配置空间中的偏移。
3.2. Power Management Capability
如果Capability ID为01h,则表明当前结构为电源管理能力寄存器结构。
3.3. MSI Capability
3.4. MSI-X Capability
3.5. PCIe Capability
PCIe Capability结构不同偏移对应不同的功能,详细如下图:
3.6. 示例
- 偏移0x34上的0x80即为Capabilities Pointer.
- 偏移0x80上的0x10表示此处存储的是PCIe Capability,Next Pointer偏移为0xd0。
- 偏移0xd0上的0x11表示此处存储的是MSI-X Capability,Next Pointer偏移为0xe0。
- 偏移0xe0上的0x05表示此处存储的是MSI Capability,Next Pointer偏移为0xf8。
- 偏移0xf8上的0x01表示此处存储的是Power Management Capability,Next Pointer为0表示结束。
4. 地址空间
在早期的PC时期,CPU提供接口直接通过IO地址空间来访问IO设备,然而因为多CPU访问时导致竞态的限制。目前系统更多是将IO地址空间直接映射到系统的物理地址空间中(MMIO,Memory-mapped I/O),这样软件可以像访问普通内存一样访问IO设备。
4.1. P-MMIO和NP-MMIO
在PCIe中,可预取(Prefetchable)和非可预取(Non-Prefetchable)内存空间之间的区别在于对于读取操作是否具有副作用以及是否允许写合并。
可预取空间的特点包括:
- 读取操作没有副作用:从可预取空间读取数据不会改变目标设备的状态信息。
- 允许写合并:当写入多个连续地址时,可预取空间可以将这些写操作合并为更少的传输请求,提高性能。
而非可预取空间则不具备上述特点。
在可预取空间中,数据可能会被提前获取,因为预测到请求方可能会在不久的将来需要更多的数据。这种缓存数据的预取可以提高性能,因为当真正需要数据时,它已经位于缓存中。然而,如果请求方实际上并未使用额外的数据,那么缓存中的数据最终会被丢弃以释放缓冲区空间。由于读取操作本身没有副作用,因此在稍后重新获取原始数据是可行的。
由于PCIe的优势和先进性,通常更倾向于使用可预取内存空间来获得更好的性能。
4.2. BAR
PCIe支持不同功能的设备,不同的设备其需要操作的内存大小也不同,为了更方便地实现这一种,BAR(Base Address Registers)产生了。PCIe可以通过配置空间中的BAR来为不同的设备配备不同的内存方案。NVMe存储设备是Type0,并且一般只用BAR0-1或BAR4-5。
4.3. BAR配置
BAR有32位和64位地址两种,现在基本都是64位系统,此处只讲64位地址。即BAR0-1存储的是一个64位的物理地址。
- 向BAR Pair写全1,然后读出来。如果读出来的值为1,表明此地址位是可以操作的,为0表明不可操作。如上图(2)中,最低可操作位为26位,即2的26次方64K,即BAR Pair的最小操作单位为64K。最高操作们来64位,上限非常大。
- 如图(3)中,通过BAR Pair给设备配置0x2,4000,0000的物理内存地址,并配置64位地址、可预取的IO请求。
这篇关于NVMe开发——PCIe配置空间和地址空间的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!