NVMe开发——PCIe配置空间和地址空间

2024-03-04 20:20

本文主要是介绍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端口的接口。

  1. 配置访问的端口地址

在CPU的0xCF8处理配置端口地址,端口地址定义如下:

  • Bit[31]配置1表示使能访问配置空间
  • Bits[7:2]的值表示对应的PCI兼容配置空间的64个DWORD。
  1. 读取配置地址的数据

配置完访问端口的地址之后,就可以读取配置空间的数据。通过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. 注意项

  1. 设备不存在时,RC设备向系统响应的是全1(FFFFH)的数据。
  2. 设备未准备好,针对低速设备,至少在复位后100ms之后再来枚举设备,针对高速设备如速率大于5.0 GT/s(Gen3速度),则软件必须等到链路训练完成后的100ms才能尝试这样做。这是因为速度越高,越需要更多的时候来完成链路的训练。
  3. 枚举时,通过判断 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. 示例

  1. 偏移0x34上的0x80即为Capabilities Pointer.
  2. 偏移0x80上的0x10表示此处存储的是PCIe Capability,Next Pointer偏移为0xd0。
  3. 偏移0xd0上的0x11表示此处存储的是MSI-X Capability,Next Pointer偏移为0xe0。
  4. 偏移0xe0上的0x05表示此处存储的是MSI Capability,Next Pointer偏移为0xf8。
  5. 偏移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位的物理地址。

  1. 向BAR Pair写全1,然后读出来。如果读出来的值为1,表明此地址位是可以操作的,为0表明不可操作。如上图(2)中,最低可操作位为26位,即2的26次方64K,即BAR Pair的最小操作单位为64K。最高操作们来64位,上限非常大。
  2. 如图(3)中,通过BAR Pair给设备配置0x2,4000,0000的物理内存地址,并配置64位地址、可预取的IO请求。

这篇关于NVMe开发——PCIe配置空间和地址空间的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/774332

相关文章

如何为Yarn配置国内源的详细教程

《如何为Yarn配置国内源的详细教程》在使用Yarn进行项目开发时,由于网络原因,直接使用官方源可能会导致下载速度慢或连接失败,配置国内源可以显著提高包的下载速度和稳定性,本文将详细介绍如何为Yarn... 目录一、查询当前使用的镜像源二、设置国内源1. 设置为淘宝镜像源2. 设置为其他国内源三、还原为官方

CentOS7更改默认SSH端口与配置指南

《CentOS7更改默认SSH端口与配置指南》SSH是Linux服务器远程管理的核心工具,其默认监听端口为22,由于端口22众所周知,这也使得服务器容易受到自动化扫描和暴力破解攻击,本文将系统性地介绍... 目录引言为什么要更改 SSH 默认端口?步骤详解:如何更改 Centos 7 的 SSH 默认端口1

Maven的使用和配置国内源的保姆级教程

《Maven的使用和配置国内源的保姆级教程》Maven是⼀个项目管理工具,基于POM(ProjectObjectModel,项目对象模型)的概念,Maven可以通过一小段描述信息来管理项目的构建,报告... 目录1. 什么是Maven?2.创建⼀个Maven项目3.Maven 核心功能4.使用Maven H

SpringBoot多数据源配置完整指南

《SpringBoot多数据源配置完整指南》在复杂的企业应用中,经常需要连接多个数据库,SpringBoot提供了灵活的多数据源配置方式,以下是详细的实现方案,需要的朋友可以参考下... 目录一、基础多数据源配置1. 添加依赖2. 配置多个数据源3. 配置数据源Bean二、JPA多数据源配置1. 配置主数据

Spring 基于XML配置 bean管理 Bean-IOC的方法

《Spring基于XML配置bean管理Bean-IOC的方法》:本文主要介绍Spring基于XML配置bean管理Bean-IOC的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录一. spring学习的核心内容二. 基于 XML 配置 bean1. 通过类型来获取 bean2. 通过

如何使用Nginx配置将80端口重定向到443端口

《如何使用Nginx配置将80端口重定向到443端口》这篇文章主要为大家详细介绍了如何将Nginx配置为将HTTP(80端口)请求重定向到HTTPS(443端口),文中的示例代码讲解详细,有需要的小伙... 目录1. 创建或编辑Nginx配置文件2. 配置HTTP重定向到HTTPS3. 配置HTTPS服务器

SpringBoot中配置Redis连接池的完整指南

《SpringBoot中配置Redis连接池的完整指南》这篇文章主要为大家详细介绍了SpringBoot中配置Redis连接池的完整指南,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以... 目录一、添加依赖二、配置 Redis 连接池三、测试 Redis 操作四、完整示例代码(一)pom.

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

IDEA自动生成注释模板的配置教程

《IDEA自动生成注释模板的配置教程》本文介绍了如何在IntelliJIDEA中配置类和方法的注释模板,包括自动生成项目名称、包名、日期和时间等内容,以及如何定制参数和返回值的注释格式,需要的朋友可以... 目录项目场景配置方法类注释模板定义类开头的注释步骤类注释效果方法注释模板定义方法开头的注释步骤方法注

如何在Mac上安装并配置JDK环境变量详细步骤

《如何在Mac上安装并配置JDK环境变量详细步骤》:本文主要介绍如何在Mac上安装并配置JDK环境变量详细步骤,包括下载JDK、安装JDK、配置环境变量、验证JDK配置以及可选地设置PowerSh... 目录步骤 1:下载JDK步骤 2:安装JDK步骤 3:配置环境变量1. 编辑~/.zshrc(对于zsh