wandres.dev
PLATFORM Y DEVICE TREE · platform_driver, DT, probe

Platform devices: el hardware que no sabe presentarse

Por qué PCI y USB pueden enumerarse solos mientras que los periféricos integrados en un SoC son mudos: no existe forma de preguntarles qué son ni dónde viven. El platform bus y los recursos declarados a mano como respuesta del kernel a ese silencio.

⏱ 14 min

Conectas una tarjeta PCIe y el kernel sabe al instante quién es, qué registros tiene y qué driver cargar. Enciendes el SoC de un móvil con cuarenta periféricos integrados y el kernel no sabe absolutamente nada: ni que existen, ni en qué dirección viven, ni qué interrupción usan. La diferencia no es de complejidad, es de información. PCI y USB llevan su descripción encima; el controlador de UART soldado a la malla interna de un SoC es una porción de silicio muda. Los platform devices son la abstracción del kernel para ese hardware que no sabe presentarse.

🎯 Al terminar esta lección sabrás
  • Distinguir un bus autodescubrible como PCI o USB de un hardware que no puede enumerarse.
  • Entender por qué el kernel no puede preguntarle a un periférico de SoC qué es ni dónde está.
  • Conocer el platform bus como el bus sintético de los dispositivos sin bus real.
  • Ver struct platform_device y struct resource como una descripción que alguien debe aportar desde fuera.

Buses que se describen a sí mismos

Un bus autodescubrible es aquel que ofrece un mecanismo estándar para preguntarle a cada dispositivo conectado qué es. En PCI ese mecanismo es el espacio de configuración: cada función expone un bloque de registros con su vendor ID, su device ID, sus BAR (los rangos de registros que reclama) y su línea de interrupción. El kernel recorre el bus, lee esos identificadores y busca un driver cuyo id_table los reconozca. Nadie tuvo que decirle al kernel que ahí había una tarjeta de red: la tarjeta lo dijo.

/* PCI: el dispositivo lleva su identidad en el espacio de configuracion */
static const struct pci_device_id igc_ids[] = {
	{ PCI_VDEVICE(INTEL, 0x15f2) },   /* el kernel casa por vendor/device id */
	{ 0, }
};
MODULE_DEVICE_TABLE(pci, igc_ids);

USB funciona igual con sus descriptores: al enchufar el dispositivo, el host le pide su descriptor y obtiene clase, fabricante, endpoints y potencia. La enumeración es, en el fondo, un acto de lectura de metadatos que el propio hardware guarda. Ese es el privilegio de los buses modernos: la descripción viaja pegada al dispositivo.

El silencio del SoC

Ahora mira el interior de un SoC de ARM o RISC-V. Un controlador I2C, un temporizador, un GPIO, una UART: todos cuelgan de una malla interna de interconexión y responden en direcciones físicas fijas grabadas por el fabricante en el diseño del chip. No hay espacio de configuración. No hay descriptores. Si el kernel lee la dirección 0x50000000 puede encontrar los registros de una UART, pero nada en el hardware le dice que ahí hay una UART, ni de qué modelo, ni qué número de interrupción dispara. El periférico es funcional pero mudo: cumple su trabajo si lo programas bien, y no dice una palabra sobre sí mismo.

🔌

Autodescubrible

PCI y USB. El dispositivo guarda su identidad y sus recursos. El kernel enumera el bus, lee los metadatos y elige el driver. La descripción viaja con el hardware.

🔇

No enumerable

Periféricos de un SoC. Viven en direcciones fijas sin metadatos. El kernel no puede preguntar nada. Alguien externo debe describir qué hay y dónde.

Esta es la raíz de todo el nivel. Cuando la información no vive en el dispositivo, tiene que vivir en otro sitio: en el código (los antiguos board files) o en un archivo de datos (el device tree, nivel 29.3). El problema del hardware embebido no es controlarlo, es saber que está ahí.

flowchart TB
subgraph Autodescubrible
  PCI[Dispositivo PCI] -->|espacio de configuracion| K1[Kernel lee vendor y device id]
  USB[Dispositivo USB] -->|descriptores| K1
  K1 --> M1[Elige el driver por id_table]
end
subgraph Mudo
  SOC[Periferico del SoC] -. no se anuncia .- K2[Kernel]
  DESC[Descripcion externa] -->|le dice que hay ahi| K2
  K2 --> M2[Elige el driver por compatible]
end

El platform bus y los recursos a mano

El modelo de dispositivos del kernel se organiza en buses: todo struct device cuelga de un struct bus_type. Pero los periféricos de un SoC no cuelgan de ningún bus físico real. La solución es un bus sintético: el platform_bus_type, un bus ficticio inventado por el kernel para dar cobijo a los dispositivos que no tienen bus propio. De él cuelgan como struct platform_device, y sus drivers son struct platform_driver (nivel 29.2). Todo aparece bajo /sys/bus/platform/.

struct platform_device {
	const char	*name;        /* nombre para el emparejamiento clasico */
	int		id;           /* instancia, o PLATFORM_DEVID_NONE */
	struct device	dev;          /* el dispositivo generico embebido */
	u32		num_resources;
	struct resource	*resource;    /* registros e IRQ declarados a mano */
	/* ... */
};

Como el hardware no declara sus recursos, hay que declararlos por él. Un struct resource describe un rango: una región de registros (IORESOURCE_MEM), una línea de interrupción (IORESOURCE_IRQ) y demás. Así se veía la vieja escuela de los board files, donde cada placa aportaba a mano las direcciones que el silicio callaba:

/* estilo board file: la descripcion escrita a mano, compilada en el kernel */
static struct resource acme_uart_res[] = {
	{
		.start = 0x50000000,
		.end   = 0x50000fff,
		.flags = IORESOURCE_MEM,   /* los registros: base y tamano */
	},
	{
		.start = 42,
		.end   = 42,
		.flags = IORESOURCE_IRQ,   /* la linea de interrupcion */
	},
};

static struct platform_device acme_uart = {
	.name          = "acme-uart",
	.id            = PLATFORM_DEVID_NONE,
	.num_resources = ARRAY_SIZE(acme_uart_res),
	.resource      = acme_uart_res,
};

El driver, más tarde, no hará pci_read_config ni nada parecido: pedirá esos recursos ya declarados con platform_get_resource y platform_get_irq. La descripción fue inyectada desde fuera; el driver solo la consume.

/sys/bus/platform: el bus sintético a la vista

Como cualquier bus del modelo de dispositivos, el platform bus se expone en sysfs, y ahí puedes verlo con tus propios ojos. Cada dispositivo no enumerable aparece como un directorio; cada platform_driver registrado, también; y cuando ambos casan, un enlace simbólico une al dispositivo con su driver.

ls /sys/bus/platform/devices/     # cada periferico no enumerable, un directorio
ls /sys/bus/platform/drivers/     # los platform_driver registrados
# el enlace device -> driver aparece solo cuando hubo emparejamiento:
readlink /sys/bus/platform/devices/50000000.serial/driver

El nombre 50000000.serial no es casual: cuando el dispositivo nace de un device tree, el kernel lo bautiza con su dirección de unidad y su nombre de nodo (nivel 29.3). Desde aquí puedes forzar el emparejamiento a mano escribiendo en los archivos bind y unbind del driver, o imponer un driver concreto con driver_override. Esa visibilidad no es un adorno: es la prueba de que hasta el hardware más mudo termina siendo un ciudadano de pleno derecho del mismo modelo de dispositivos que gobierna PCI o USB.

⚠️
El problema de los board files: código que describe una placa

Ese código tenía un defecto estructural. Cada placa nueva significaba compilar un .c distinto en el kernel: miles de archivos en arch/arm/mach-*/ que no eran lógica, sino datos disfrazados de código. Describir un cambio de dirección obligaba a recompilar. La comunidad de ARM acabó ahogada en esos archivos, y de esa crisis nació el device tree (nivel 29.3): sacar la descripción del hardware fuera del binario del kernel.

ℹ️
No todo platform device viene de un SoC

El platform bus también hospeda dispositivos que tampoco se enumeran: nodos de ACPI en x86, dispositivos MFD hijos de otro, o los que creas con platform_device_register_simple para pruebas. El denominador común no es “estar en un SoC”, sino no colgar de un bus autodescubrible. Por eso el platform bus es el hogar de todos los huérfanos del modelo de dispositivos.

La enumeración es información, y la información tiene que vivir en algún lado

Detente en la idea que gobierna este nivel entero, porque es más profunda que cualquier API. Un driver, en abstracto, es una función pura del hardware: dado un conjunto de registros, una interrupción y unos parámetros, sabe hacer funcionar el dispositivo. Lo que separa a PCI de un periférico de SoC no es esa función, que es idéntica, sino de dónde salen sus argumentos. En un bus autodescubrible los argumentos vienen del propio dispositivo: el silicio carga su identidad y sus recursos, y la enumeración es simplemente el acto de leerlos. El periférico embebido es funcionalmente igual pero amnésico: no recuerda quién es. Y aquí aparece una ley de conservación que el kernel no puede violar: si la información no está en el hardware, tiene que estar en otro sitio, porque de la nada no se enumera. Durante años ese “otro sitio” fue el código, los board files, con el coste ruinoso de recompilar el kernel por cada tornillo movido. La historia de este nivel es la migración de esa información desde el código hacia un formato de datos, el device tree, y desde la placa hacia el arranque. Cuando interiorizas que enumerar es leer metadatos y que los metadatos son indestructibles —solo se pueden mudar de lugar—, dejas de ver el platform bus como una rareza y lo ves como lo que es: la confesión honesta del kernel de que hay hardware que no habla, y de que alguien, en alguna parte, tendrá que hablar por él.

⚔️ Localiza el silencio del hardware
  1. Ejecuta lspci -nn en tu máquina y observa cómo cada dispositivo lleva su vendor:device id: esa es su autodescripción. Contrasta con ls /sys/bus/platform/devices/, donde los nombres los puso alguien.
  2. Explica en tres líneas por qué el kernel no puede escribir un bucle que “escanee” la malla interna de un SoC como escanea el bus PCI.
  3. Abre arch/arm/mach-*/ en un árbol de fuentes y localiza un struct resource con IORESOURCE_MEM. Argumenta por qué ese dato es “código que en realidad son datos”.
  4. Razona qué información concreta (dirección, tamaño, IRQ, modelo) tendría que aportar una descripción externa para que un driver pudiera manejar una UART que el hardware no anuncia.