Skip to content

第一百三十四章:PCI 枚举

合道期

涉及内核源码:

林小源在设备山脉的半山腰处,发现了一条宽阔的大道。大道两侧立着无数高大的石柱,每根石柱上都刻着密密麻麻的数字。

"这是什么路?"林小源问。

"PCI 总线。"守山老者说,"这是连接 CPU 和外设的主干道。几乎所有的高性能设备——网卡、显卡、RAID 控制器——都挂在这条总线上。"

大道上,一个身穿黑色铠甲的武士正在缓步行走。他的铠甲上刻着两组编号:Vendor: 0x8086Device: 0x100E。每走一步,铠甲上的编号就闪烁一次。

"那是 PCI 设备。"老者说,"每个 PCI 设备都有两个身份标识——Vendor ID 是厂商标识,Device ID 是设备标识。你看到的那个,是 Intel 的 82540EM 网卡。"

林小源跟着那个武士走了一段路,来到一个巨大的圆形广场。广场中央立着一块石碑,上面刻着 lspci 三个大字。武士走到石碑前,石碑立刻亮了起来,显示出他的完整信息:

00:03.0 Ethernet controller: Intel 82540EM (rev 03)

"这就是设备枚举。"老者说,"内核在启动时会扫描整条 PCI 总线——读取每个设备的配置空间,获取 Vendor ID、Device ID、Class Code。这就是内核'发现'硬件的过程。"

c
/*
 * PCI 设备的标识:
 *
 *   Vendor ID — 厂商标识
 *   Device ID — 设备标识
 *   Class Code — 设备类别
 *
 *   例: Intel 网卡
 *   Vendor: 0x8086 (Intel)
 *   Device: 0x100E (82540EM)
 *   Class: 0x0200 (网络控制器)
 *
 * PCI 配置空间:
 *   256 字节的配置寄存器
 *   包含厂商 ID、设备 ID、中断号、BAR 等
 *
 * BAR (Base Address Register):
 *   设备的内存映射 I/O 地址
 *   驱动通过 BAR 访问设备寄存器
 *
 * PCI 驱动注册:
 *   pci_register_driver(&my_driver)
 *   匹配 Vendor ID 和 Device ID
 */

printf("=== PCI — 设备发现与枚举 ===\n\n");

printf("PCI 设备标识:\n");
printf("  Vendor ID — 厂商标识\n");
printf("  Device ID — 设备标识\n");
printf("  Class Code — 设备类别\n\n");

printf("--- PCI 配置空间 ---\n");
printf("256 字节的配置寄存器:\n");
printf("  偏移 0x00: Vendor ID\n");
printf("  偏移 0x02: Device ID\n");
printf("  偏移 0x08: Class Code\n");
printf("  偏移 0x10: BAR0\n");
printf("  偏移 0x14: BAR1\n");
printf("  偏移 0x3C: Interrupt Line\n\n");

printf("--- BAR ---\n");
printf("Base Address Register:\n");
printf("  设备的内存映射 I/O 地址\n");
printf("  驱动通过 BAR 访问设备寄存器\n\n");
printf("BAR0 = 0xFE000000\n");
printf("  驱动: regs = ioremap(BAR0, size)\n\n");

printf("--- PCI 驱动注册 ---\n");
printf("struct pci_driver my_driver = {\n");
printf("  .name = \"my_pci\",\n");
printf("  .id_table = my_ids,\n");
printf("  .probe = my_probe,\n");
printf("  .remove = my_remove,\n");
printf("};\n\n");
printf("pci_register_driver(&my_driver);\n\n");

printf("--- 枚举过程 ---\n");
printf("1. 内核扫描 PCI 总线\n");
printf("2. 发现设备,读取配置空间\n");
printf("3. 匹配驱动(Vendor/Device ID)\n");
printf("4. 调用驱动的 probe 函数\n");
printf("5. 驱动初始化设备\n\n");

printf("--- lspci ---\n");
printf("lspci 命令查看 PCI 设备:\n");
printf("  00:03.0 Ethernet controller:\n");
printf("    Intel 82540EM (rev 03)\n");

"配置空间我知道了,但驱动怎么访问设备的寄存器?"林小源问,"设备的寄存器不在内存地址空间里吧?"

"问得好。"老者蹲下来,在地上画了一个方框,"每个 PCI 设备都有一个或多个 BAR——Base Address Register。BAR 是设备告诉内核'我的寄存器在内存地址空间的哪个位置'的方式。"

他指着那个 Intel 网卡武士:"比如他,BAR0 是 0xFE000000。这意味着他的设备寄存器被映射到了物理地址 0xFE000000 开始的一段内存区域。"

"但是,内核用的是虚拟地址啊。"林小源说。

"所以需要 。"老者说,"ioremap(BAR0, size) 把这段物理地址映射到内核虚拟地址空间。之后驱动就像访问普通内存一样访问设备寄存器——读寄存器、写寄存器、配置设备。"

林小源想象着这个过程:设备把自己的寄存器"挂"在内存地址空间的某个位置,驱动通过 ioremap 找到这个位置,然后就可以直接操控硬件。这就像在内核的虚拟世界里,给硬件设备开了一扇窗户。

"那驱动和设备是怎么匹配上的?"林小源继续问。

"靠 probe。"老者说。

就在这时,一个穿着驱动修士袍子的人从大道另一头走了过来。他的袍子上绣着一张 id_table,表里列着好几组 Vendor/Device ID。他走到那个 Intel 网卡武士面前,低头看了看武士铠甲上的编号,又看了看自己袍子上的表。

"0x8086:0x100E——匹配。"驱动修士点了点头。

下一刻,驱动修士的掌心亮起一道光芒,光芒笼罩了网卡武士。武士铠甲上的配置寄存器开始闪烁——驱动修士正在执行 函数:映射 BAR、分配资源、注册中断、初始化设备。

"这就是 probe 的作用。"老者说,"当内核发现一个匹配的设备时,调用驱动的 probe 函数。probe 负责把设备从'发现了'变成'能用了'——映射寄存器、分配资源、注册中断、初始化硬件。probe 是设备初始化的入口。"

林小源看着那个驱动修士忙前忙后,心中感慨:发现设备只是开始,probe 才是真正的考验。


道藏笔记

内核启示

PCI 是连接 CPU 和外设的总线标准。

PCI 设备标识:

  • Vendor ID — 厂商标识
  • Device ID — 设备标识
  • Class Code — 设备类别

BAR(Base Address Register):

  • 设备的内存映射 I/O 地址
  • 驱动通过 BAR 访问设备寄存器

PCI 驱动注册:

  • — 注册驱动
  • 匹配 Vendor/Device ID
  • 调用 初始化设备

PCI 是"发现"的机制——让内核找到硬件设备。


破关试炼

PCI 之试

PCI 枚举找到匹配设备后,驱动最先被调用来初始化硬件的入口通常叫什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道