wandres.dev
PCI · config space, BAR, MSI

El bus PCI/PCIe: topología, enumeración y el BDF

La estructura del subsistema PCI: del bus paralelo compartido a la malla serie punto a punto de PCIe. Root complex, puentes y endpoints; cómo el kernel enumera el árbol recorriéndolo; y el identificador geográfico BDF (bus:device.function) frente a la identidad Vendor/Device ID.

⏱ 15 min

Casi todo lo interesante conectado a un ordenador de 2026 —la tarjeta de red, el SSD NVMe, la GPU, el controlador USB— cuelga del bus PCI Express. Su topología no es trivia: es el mapa que el kernel recorre para descubrir qué hardware existe, dónde vive y cómo hablarle. PCI le regaló al sistema operativo algo que las máquinas ISA no tenían: un hardware que se presenta a sí mismo, con una identidad y unos recursos legibles por software, sin un solo jumper.

🎯 Al terminar esta lección sabrás
  • Distinguir el bus paralelo compartido de PCI de la malla serie punto a punto de PCIe.
  • Situar las tres piezas de la topología: root complex, puentes y endpoints.
  • Entender la enumeración: cómo el kernel descubre cada dispositivo recorriendo el árbol.
  • Descifrar el identificador geográfico BDF y la identidad Vendor/Device ID.

Del bus compartido a la malla punto a punto

El PCI original (1992) era, eléctricamente, un bus de verdad: un haz de pistas paralelas que varios dispositivos compartían, con arbitraje para decidir quién transmitía en cada ciclo. PCI Express (2004) tiró esa electricidad a la basura y la sustituyó por enlaces serie punto a punto —los lanes— unidos por conmutadores, más parecidos a una red conmutada que a un bus. Y sin embargo el kernel sigue hablando de “buses”, “puentes” y “dispositivos”.

La razón es la joya de PCIe: conservó intacto el modelo de software de PCI. El mismo espacio de configuración, la misma enumeración, los mismos identificadores. Un dispositivo PCIe se presenta al software exactamente como uno PCI convencional, aunque por debajo sea una malla de enlaces serie. Por eso un único modelo de driver —el que verás en la lección 30.3— sirve para ambos, y por eso este nivel entero razona sobre una abstracción de bus que hace veinte años que dejó de ser un bus físico.

ℹ️
Compatibilidad como estrategia de diseño

PCIe pudo reinventar toda la capa física precisamente porque no tocó la capa de software. Esa disciplina —cambiarlo todo por debajo sin mover un byte de la interfaz visible— es la misma que permite al kernel evolucionar sus subsistemas internos sin romper el userspace. La abstracción estable es lo que compra libertad para la implementación.

La topología: raíz, puentes y endpoints

La malla PCIe es un árbol. En la raíz está el root complex: el puente entre la CPU con su controlador de memoria y el resto de la jerarquía. De él salen root ports, que son puentes PCI-a-PCI. Cada puente puede llevar a un switch (que contiene, a su vez, más puentes virtuales) o directamente a un endpoint, que es el dispositivo terminal: la NIC, el NVMe, la GPU.

flowchart TD
CPU[CPU y controlador de memoria] --- RC[Root Complex]
RC --> RP0[Root Port bus 0]
RC --> RP1[Root Port bus 0]
RP0 --> EP0[Endpoint NVMe]
RP1 --> SW[Switch con puentes internos]
SW --> EP1[Endpoint tarjeta de red]
SW --> EP2[Endpoint GPU]

La pieza estructural es el puente PCI-a-PCI. Cada puente define un tramo de la numeración: tiene un bus primario (el de arriba), un bus secundario (el que arranca debajo de él) y un bus subordinado (el mayor número de bus alcanzable a través suyo). Esos tres números convierten un árbol físico en un espacio de numeración plano que la CPU puede direccionar. Recorrer el árbol es, literalmente, seguir esos rangos de bus de puente en puente.

La enumeración: el kernel recorre el árbol

Al arrancar, alguien —la firmware UEFI y, después, el kernel— hace un recorrido en profundidad. Empieza en el bus 0 y, por cada posible dispositivo y función, lee el Vendor ID en el desplazamiento 0 del espacio de configuración. Si lee 0xFFFF, ahí no hay nadie. Si hay un dispositivo y resulta ser un puente (tipo de cabecera 1), le asigna números de bus al lado secundario y desciende recursivamente. El resultado es un árbol de objetos struct pci_bus y struct pci_dev colgando de pci_root_buses.

Una vez construido ese árbol, recorrerlo desde un módulo es trivial. for_each_pci_dev itera sobre cada dispositivo descubierto:

#include <linux/pci.h>

static void volcar_topologia(void)
{
	struct pci_dev *pdev = NULL;

	for_each_pci_dev(pdev) {
		pr_info("%s: [%04x:%04x] clase %06x rev %02x en bus %02x\n",
			pci_name(pdev),            /* dominio:bus:slot.func */
			pdev->vendor, pdev->device,
			pdev->class,               /* base, subclase, prog-if */
			pdev->revision,
			pdev->bus->number);
	}
}

for_each_pci_dev toma y cede referencias por ti mientras avanzas; si sales del bucle antes de tiempo debes soltar la referencia actual con pci_dev_put. Cada struct pci_dev es la representación viva de un endpoint: lleva su identidad, su posición en el árbol y —lo verás en 30.2— sus recursos de memoria ya descubiertos.

BDF: la dirección, y Vendor/Device ID: la identidad

Todo dispositivo tiene dos etiquetas que no hay que confundir. La primera es dónde está: el BDF, o dominio:bus:device.function. El dominio (o segmento) permite superar los 256 buses de un solo espacio; el bus ocupa 8 bits; el device (o slot) 5 bits; y la function 3 bits, porque un mismo chip físico puede exponer hasta ocho funciones lógicas independientes. El kernel empaqueta slot y función en el campo devfn:

u8 bus  = pdev->bus->number;
u8 slot = PCI_SLOT(pdev->devfn);   /* (devfn >> 3) & 0x1f */
u8 func = PCI_FUNC(pdev->devfn);   /* devfn & 0x07        */
int dom = pci_domain_nr(pdev->bus);
/* pci_name(pdev) formatea justo esto: p. ej. 0000:03:00.0 */

La segunda etiqueta es qué eres: el par Vendor ID / Device ID, dos valores de 16 bits grabados en el silicio. 0x8086 es Intel; 0x10de, NVIDIA; 0x1af4, los dispositivos virtio. El Device ID identifica el modelo concreto de chip. Por debajo hay además un subsystem vendor/subsystem device que distingue al fabricante de la tarjeta integradora, y un class code que clasifica el dispositivo de forma genérica —almacenamiento, red, display— para que un driver genérico pueda vincularse sin conocer el modelo exacto.

📍

BDF: la geografía

dominio:bus:device.function. Dice dónde vive el dispositivo en el árbol. Cambia si lo mueves de ranura. Al driver le da igual: nunca vincula por posición.

🪪

Vendor/Device ID: la identidad

Dos valores de 16 bits grabados en el chip. Dicen qué es. El kernel empareja drivers por esta identidad, no por la posición, y por eso un driver sirve a miles de tarjetas iguales.

El árbol en sysfs

Toda esa estructura no se queda encerrada en la RAM del kernel: se proyecta en sysfs, bajo /sys/bus/pci/, donde cada dispositivo es un directorio nombrado por su BDF y cada driver publica a qué dispositivos está vinculado. Es la cara visible del modelo de dispositivos (nivel 28): el bus PCI es solo un bus más colgado de esa jerarquía unificada, y explorarlo no requiere escribir una línea de C.

# cada dispositivo, por su BDF, con su identidad y su driver
ls /sys/bus/pci/devices/
cat /sys/bus/pci/devices/0000:03:00.0/vendor      # p. ej. 0x8086
cat /sys/bus/pci/devices/0000:03:00.0/class       # base, subclase, prog-if
readlink /sys/bus/pci/devices/0000:03:00.0/driver # quien lo maneja

Recorrer sysfs es recorrer el mismo árbol que enumeró el kernel al arrancar. Cada enlace driver es un emparejamiento por identidad que ya ocurrió; cada carpeta nueva que brota al enchufar una tarjeta es una rama reenumerada en caliente. El BDF que da nombre al directorio y el Vendor/Device ID que hay dentro son, otra vez, las dos etiquetas: dónde estás y qué eres.

PCI es hardware que se describe a sí mismo: el fin del jumper

Detente en la inversión que fundó toda la computación plug and play. En la era ISA, el hardware era mudo: fijabas su dirección de E/S y su línea de interrupción con jumpers físicos, y el sistema operativo no tenía forma de preguntarle qué era ni qué necesitaba; el conocimiento vivía en la cabeza del usuario y en un manual. PCI invierte por completo la relación de poder. El dispositivo trae un espacio de configuración estándar, en una posición conocida, que el software puede leer para averiguar su identidad, su clase y sus apetitos de recursos —y esos recursos se los asigna el software, no el usuario. De esa única idea —hardware autodescriptivo con recursos asignables— brota, en cascada, todo lo que viene después. La enumeración es posible porque hay un formato común que leer. Los drivers vinculan por Vendor/Device ID porque la identidad es un dato, no una suposición. El hotplug funciona porque enchufar una tarjeta es, para el kernel, releer una rama del árbol. Y la separación tajante entre el BDF (dónde estás) y el Device ID (qué eres) es lo que permite que el mismo binario de driver sirva a la tarjeta del slot 3 y a la del slot 7 sin cambiar una línea: la posición es circunstancia, la identidad es esencia. Cuando dejas de ver PCI como “un conector” y empiezas a verlo como “un protocolo de auto-presentación del hardware”, el modelo de dispositivos entero —buses, clases, la tríada del nivel 28— deja de ser burocracia y se revela como la consecuencia natural de haber enseñado al hardware a hablar de sí mismo.

⚔️ Lee el árbol de tu máquina
  1. Ejecuta lspci -tv y dibuja el árbol: identifica el root complex, algún puente o switch, y tres endpoints.
  2. Toma el BDF de tu NVMe o tu tarjeta de red y localízalo en /sys/bus/pci/devices/; abre vendor, device y class dentro de ese directorio.
  3. Con lspci -nn descifra el par [vendor:device] de la GPU y busca los códigos en pci.ids.
  4. Explica en tres líneas por qué mover la tarjeta a otra ranura cambia su BDF pero no impide que el mismo driver la reconozca.