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.
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.
- 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_deviceystruct resourcecomo 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.
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.
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.
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.
- Ejecuta
lspci -nnen tu máquina y observa cómo cada dispositivo lleva suvendor:device id: esa es su autodescripción. Contrasta conls /sys/bus/platform/devices/, donde los nombres los puso alguien. - 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.
- Abre
arch/arm/mach-*/en un árbol de fuentes y localiza unstruct resourceconIORESOURCE_MEM. Argumenta por qué ese dato es “código que en realidad son datos”. - 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.