摘要
在系统开机过程中,设备寻址是一个关键环节,它涉及到 UEFI 固件对硬件设备的初步探测和资源分配,以及 Linux 内核在此基础上进行的二次加工和冲突化解。
本文以 AMD 集成 GPU(ACPI 描述)和 NVIDIA 独立显卡(PCIe 设备)为例,详细介绍这一过程。
UEFI 固件阶段:硬件探测与地址预分配
集成 GPU(通过 ACPI 描述静态分配资源)
在 UEFI 阶段,系统会对硬件设备进行扫描,并为它们分配初始的资源,包括<font style="color:rgba(0, 0, 0, 0.85);">内存地址</font>
和<font style="color:rgba(0, 0, 0, 0.85);">中断号</font>
等。
- 集成 GPU 之前在
开发板上的「集成设备」:PCIe or 非 PCIe ]
文章中已经详细分析过了,通常它们都是固定在系统总线上。 - 这些设备的资源通常是由
ACPI 表
或者设备树
进行静态描述完成的。 - 在内核中通过虚拟的 platform 总线完成设备的挂载,并由 platform 驱动完成设备的加载。
以下是 UEFI 阶段处理集成 GPU 和独立显卡的伪代码示例:
// UEFI阶段:处理ACPI描述的集成GPU(如AMD Ryzen 7945HX的Radeon 780M)
if (acpi_table_exists("DSDT")) {
device = acpi_find_device("GFX0"); // ACPI设备名
if (device->hid == "AMD0079") { // 集成GPU的硬件ID
// 读取ACPI资源描述(_CRS)配置空间,_CRS含义见本文附加内容
resource = acpi_eval("_CRS");
mmio_reg = resource->mem[0]; // 寄存器M
vram_shared = resource->mem[1];// 共享显存:系统内存
// 标记为保留内存(避免被OS分配)
e820_mark(mmio_reg.base, mmio_reg.size, E820_RESERVED);
e820_mark(vram_shared.base, vram_shared.size, E820_RESERVED);
// 写入设备树(供Linux内核使用)
dtb_add_node("/arm-io/gpu", {
"reg" = [mmio_reg, vram_shared],
"compatible" = "amd,renoir-igpu"
});
}
}
对于集成 GPU,通过解析 ACPI 表获取其所需的资源信息,包括寄存器 MMIO 地址和共享显存地址,并将这些地址标记为保留,同时写入设备树供内核使用。
独立 PCIe 显卡(通过 bar 的定义动态分配资源)
PCIe 显卡通常在上面会通过接出一根 PCIe 总线,用于挂载 GPU、VPU、NPU 等设备,这些设备也都称之为 PCIe 设备。
- 它们会在自己的寄存器空间有一个或若干个 Bar 空间,用于定义自己需要的资源描述。
- 当
PCI 总线驱动
扫描到这些设备后,会探测它们的 bar 空间,获取对应则资源描述; - 然后动态地为这些
PCIe 设备
分配内存、寄存器等资源。
// UEFI PCIe枚举:为RTX 4090分配临时BAR(x86平台)
for (bus = 0; bus < MAX_PCIE_BUS; bus++) {
device = pcie_scan_device(bus, 0, 0); // 假设显卡在0号总线
if (device->vendor_id == 0x10DE && device->device_id == 0x2644) {
// 读取初始BAR(全1表示未分配)
bar0 = pcie_read_bar(device, 0); // 寄存器BAR
bar1 = pcie_read_bar(device, 1); // 显存BAR
// 从PCIe MMIO池(0x80000000-0xFFFF0000)分配地址
mmio_pool = 0x80000000;
bar0_addr = mmio_pool; mmio_pool += 0x10000; // 64KB寄存器
bar1_addr = mmio_pool; mmio_pool += 0x40000000; // 1GB显存
// 写入BAR并标记保留
pcie_write_bar(device, 0, bar0_addr | PCIE_BAR_MEM);
pcie_write_bar(device, 1, bar1_addr | PCIE_BAR_MEM);
e820_reserve(bar0_addr, 0x10000, "GPU-MMIO");
e820_reserve(bar1_addr, 0x40000000, "GPU-VRAM");
// 记录到PCI设备树(x86通过ACPI,ARM64通过DTB)
acpi_add_pci_device(device, bar0_addr, bar1_addr);
}
}
Linux 内核:将地址映射到虚拟地址空间
集成 GPU 驱动处理
集成 GPU 的驱动需要根据 UEFI 阶段写入设备树的信息,将设备的寄存器和显存映射到内核的虚拟地址空间。
以下是集成 GPU 驱动的伪代码示例:
// 映射寄存器 MMIO 到内核虚拟地址
regs = devm_ioremap_resource(&pdev->dev, res);
if (!regs) {
dev_err(&pdev->dev, "Failed to map register MMIO\n");
return -ENOMEM;
}
// 初始化共享显存(系统内存)
pages = vmalloc_to_page((void *)vram->start);
if (!pages) {
dev_err(&pdev->dev, "Failed to get pages for VRAM\n");
return -ENOMEM;
}
if (gpu_ttm_bo_create(&pdev->dev, vram->size, pages)) {
dev_err(&pdev->dev, "Failed to create GPU BO for VRAM\n");
return -EFAULT;
}
// 验证:dmesg 输出
dev_info(&pdev->dev, "iGPU MMIO: %pa - %pa", &res->start, &res->end);
独立显卡驱动处理
独立显卡的驱动需要检查 UEFI 阶段分配的 BAR 地址是否与其他设备冲突,如果冲突则需要重新分配地址。
以下是独立显卡驱动的伪代码示例:
resource_size_t pci_addr;
void __iomem *mmio;
// 检查 UEFI 分配的 BAR 是否与集成 GPU 冲突(如 0xA0000000 重叠)
if (pci_resource_start(pdev, 1) < 0xB0000000) {
// 动态调整显存 BAR 到高位
/* ...调整代码省略... */
if (pci_request_resource(pdev, &pdev->resource[1], "nvidia - vram")) {
dev_err(&pdev->dev, "Failed to request new VRAM resource\n");
return -ENODEV;
}
pci_addr = pci_resource_start(pdev, 1); // 新地址 0xB0000000
} else {
pci_addr = pci_resource_start(pdev, 1);
}
// 映射寄存器 BAR 到内核虚拟地址
mmio = pci_iomap(pdev, 0, pci_resource_len(pdev, 0));
if (!mmio) {
dev_err(&pdev->dev, "Failed to map register BAR\n");
return -ENOMEM;
}
// 初始化独立显存(通过 PCIe BAR 直接访问)
if (nvidia_gpu_init(pdev, pci_addr)) {
dev_err(&pdev->dev, "Failed to initialize GPU VRAM\n");
return -EFAULT;
}
// 验证:/proc/iomem 输出
// 0000:01:00.0 BAR1 [mem 0xb0000000 - 0xbfffffff]
资源查看
集成 GPU
# 查看 ACPI 定义的资源
cat /sys/firmware/acpi/tables/DSDT | grep -A 3 "Device (GFX0)"
# 查看内核映射
cat /proc/iomem | grep "amdgpu"
PCIe-GPU
# 查看 PCI 设备 BAR
lspci -vvv -s 01:00.0 | grep BAR
# 验证地址不冲突
cat /proc/iomem | grep -E "a0000000|b0000000"
总结
在系统开机过程中,UEFI 固件负责对硬件设备进行初步的探测和资源预分配,而 Linux 内核则在 UEFI 的基础上进行地址的二次加工和冲突化解。集成 GPU 通过 ACPI 表获取静态资源,而独立显卡则通过 PCIe 枚举进行动态资源分配。通过合理的资源分配和冲突解决机制,系统可以确保各个设备能够正常工作。
附加
ACPI 中 CRS 的描述
假设系统中有一个集成显卡设备,其ACPI描述如下:
// ACPI
Scope (_SB)
{
Device (DC)
{
Name (_HID, "PNP0C0A") // _HID: Hardware ID
Name (_UID, Zero) // _UID: Unique ID
Name (_CCA, One) // _CCA: Cache Coherency Attribute
Name (PIPE, 0x03)
Name (_CRS, ResourceTemplate () // _CRS: Current Resource Settings
{
Memory32Fixed (ReadWrite, // 寄存器空间(必选)
0x28000000, // Address Base
0x00008000, // Address Length
)
Memory32Fixed (ReadWrite, // 预留内存(可选,全0表示未使用)
0x00000000, // Address Base
0x00000000, // Address Length
)
Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive, ,, )
{
0x0000006C, // 中断配置(必选)
}
GpioIo (Exclusive, PullUp, 0x0000, 0x0000, IoRestrictionOutputOnly,
"\\_SB.GPI3", 0x00, ResourceConsumer, ,
)
{ // Pin list
0x0002 // 输出引脚2(必选)
}
GpioIo (Exclusive, PullUp, 0x0000, 0x0000, IoRestrictionOutputOnly,
"\\_SB.GPI3", 0x00, ResourceConsumer, ,
)
{ // Pin list
0x0003 // 输出引脚3(必选)
}
})
``
- 设备对象 (Device)
这个案例描述了一个DC
设备,其设备标识符为PNP0C0A
。设备对象中包含了设备的标识符、唯一标识符,以及资源描述。 - CRS(当前资源设置)
CRS (_CRS
) 描述了集成显卡在实际运行时当前使用的资源,确保操作系统能够正确配置和管理硬件资源。- 在系统启动时,ACPI解析器会读取集成显卡的
_CRS
。 - 操作系统的资源管理器会根据
_CRS
确保集成显卡的资源分配不会与其他设备冲突。
- 在系统启动时,ACPI解析器会读取集成显卡的