第一百三十四章:PCI 枚举
合道期涉及内核源码:
一
林小源在设备山脉的半山腰处,发现了一条宽阔的大道。大道两侧立着无数高大的石柱,每根石柱上都刻着密密麻麻的数字。
"这是什么路?"林小源问。
"PCI 总线。"守山老者说,"这是连接 CPU 和外设的主干道。几乎所有的高性能设备——网卡、显卡、RAID 控制器——都挂在这条总线上。"
大道上,一个身穿黑色铠甲的武士正在缓步行走。他的铠甲上刻着两组编号:Vendor: 0x8086、Device: 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。这就是内核'发现'硬件的过程。"
/*
* 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");#include <stdio.h>
/*
* 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
*/
int main() {
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");
return 0;
}二
"配置空间我知道了,但驱动怎么访问设备的寄存器?"林小源问,"设备的寄存器不在内存地址空间里吧?"
"问得好。"老者蹲下来,在地上画了一个方框,"每个 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 枚举找到匹配设备后,驱动最先被调用来初始化硬件的入口通常叫什么?