El emparejamiento por compatible: cómo el nodo encuentra su driver
El mecanismo por el que el kernel casa un nodo del device tree con su driver: la propiedad compatible como clave, la of_match_table del driver, MODULE_DEVICE_TABLE, el orden de prioridad dentro de platform_match, y cómo of_device_get_match_data lleva datos por variante hasta probe.
Tenemos un nodo del device tree que dice ser una acme,uart-9000 y tenemos un driver capaz de manejar esa familia. Falta el casamentero: el mecanismo que empareja ambos y dispara probe. En el mundo de PCI ese emparejamiento era numérico, por vendor:device id. En el device tree es textual: una cadena en la propiedad compatible del nodo contra una tabla de cadenas en el driver. Esa cadena es toda la superficie de contacto entre el silicio y el código, y entender cómo se compara es entender cómo Linux arranca en hardware que nunca vio antes.
- Entender la propiedad
compatiblecomo la clave textual del emparejamiento. - Declarar una
of_match_tableconstruct of_device_idy exportarla conMODULE_DEVICE_TABLE. - Conocer el orden de prioridad que sigue
platform_matchal buscar driver. - Usar
of_device_get_match_datapara llevar datos por variante desde la tabla hastaprobe.
compatible: la clave del emparejamiento
La propiedad compatible es una lista ordenada de cadenas, de la más específica a la más genérica. Un nodo puede declarar:
uart0: serial@50000000 {
compatible = "acme,uart-9000", "acme,uart-generic";
reg = <0x50000000 0x1000>;
interrupts = <0 42 4>;
};
Esto significa: soy exactamente una acme,uart-9000, pero si no hay un driver para ese modelo concreto, soy compatible con el genérico acme,uart-generic. El kernel busca driver para la primera cadena; si nadie la reclama, prueba la siguiente. Ese fallback es lo que permite que un driver genérico maneje hardware que salió al mercado después de escribirse, siempre que el fabricante prometa compatibilidad en la lista. La convención fabricante,modelo evita colisiones: el prefijo identifica al fabricante en un registro común.
of_match_table y of_device_id
El driver publica las cadenas que sabe manejar en una tabla de struct of_device_id, terminada por un centinela vacío, y la enlaza en .driver.of_match_table. El macro MODULE_DEVICE_TABLE(of, ...) copia esas cadenas a los metadatos del módulo para que depmod construya el mapa que permite el autoload: cuando aparece un nodo con ese compatible, udev carga el módulo correcto sin intervención.
static const struct of_device_id acme_of_match[] = {
{ .compatible = "acme,uart-9000" },
{ .compatible = "acme,uart-generic" },
{ /* centinela: el bucle de comparacion para al ver .compatible nulo */ }
};
MODULE_DEVICE_TABLE(of, acme_of_match); /* habilita el autoload por udev */
static struct platform_driver acme_driver = {
.probe = acme_probe,
.remove = acme_remove,
.driver = {
.name = "acme-uart",
.of_match_table = acme_of_match,
},
};
El baile del match: quién empareja con quién
Cuando aparece un nuevo platform_device —o un nuevo driver— el bus recorre el otro lado buscando pareja, y para cada candidato llama a platform_match. Esta función encierra el orden de prioridad exacto del kernel, y merece leerse tal cual está en drivers/base/platform.c:
static int platform_match(struct device *dev, const struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* 1. anulacion manual desde el usuario (driver_override) */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* 2. por device tree: compara compatible contra of_match_table */
if (of_driver_match_device(dev, drv))
return 1;
/* 3. por ACPI, para el hardware no enumerable de x86 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* 4. por la id_table del propio platform driver */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 5. ultimo recurso: por nombre, el metodo de los board files */
return !strcmp(pdev->name, drv->name);
}
El orden no es casual. Primero manda la anulación explícita del usuario. Luego el device tree, la vía moderna. Después ACPI, su equivalente en x86. Solo si nada de eso aplica se recurre a la id_table y, en último extremo, a la comparación de nombres cruda que heredamos de los board files. of_driver_match_device recorre la of_match_table del driver y, por cada entrada, comprueba si alguna de las cadenas compatible del nodo coincide, prefiriendo siempre la coincidencia más específica.
flowchart TB NODO[Nodo del device tree con compatible] -->|aparece en el platform bus| BUS[platform_match] DRV[platform_driver con of_match_table] -->|se registra| BUS BUS -->|of_driver_match_device compara cadenas| DEC[Alguna compatible coincide] DEC -->|si| PROBE[El core llama a probe del driver] DEC -->|no| SIG[Sigue probando otros drivers]
De la tabla a probe: of_device_get_match_data
El emparejamiento por device tree ofrece un regalo que la comparación por nombre no puede: llevar datos por variante hasta probe. Cada of_device_id tiene un campo .data opaco donde puedes colgar una estructura de configuración por modelo. En probe, of_device_get_match_data te devuelve el .data de la entrada que casó, y así un mismo driver ajusta su comportamiento según qué variante de hardware describió el nodo, sin una sola cadena de if comparando modelos.
struct acme_config {
unsigned int fifo_depth;
bool tiene_dma;
};
static const struct acme_config cfg_9000 = { .fifo_depth = 16, .tiene_dma = false };
static const struct acme_config cfg_9100 = { .fifo_depth = 64, .tiene_dma = true };
static const struct of_device_id acme_of_match[] = {
{ .compatible = "acme,uart-9000", .data = &cfg_9000 },
{ .compatible = "acme,uart-9100", .data = &cfg_9100 },
{ }
};
MODULE_DEVICE_TABLE(of, acme_of_match);
static int acme_probe(struct platform_device *pdev)
{
const struct acme_config *cfg;
cfg = of_device_get_match_data(&pdev->dev); /* el .data de la entrada que caso */
if (!cfg)
return -ENODEV;
dev_info(&pdev->dev, "fifo de %u entradas, dma=%d\n",
cfg->fifo_depth, cfg->tiene_dma);
return 0;
}
of_device_get_match_data es específica de device tree. Si quieres un driver que funcione igual con device tree y con ACPI, usa la versión genérica device_get_match_data(dev), que consulta la fuente de firmware que corresponda a través de la capa fwnode. La estructura de configuración por variante es idéntica; solo cambia de dónde se saca. Es el idioma recomendado para drivers que aspiran a correr en ambos mundos.
Fíjate en lo pequeño y lo enorme que es a la vez el punto de contacto entre hardware y software: una cadena de texto. Toda la relación entre una acme,uart-9000 de silicio y las miles de líneas de C que la manejan pasa por la comparación de una cadena en el nodo contra una cadena en la tabla del driver. Esa estrechez es deliberada y es poderosa. En PCI el contrato era un número grabado en el chip, inmutable; en el device tree es una promesa textual escrita por quien integra la placa, y esa diferencia lo cambia todo. Significa que el mismo driver, compilado una sola vez, empareja con cualquier placa presente o futura cuyo device tree prometa esa cadena, sin que el autor del driver supiera que esas placas existirían. Significa que un fabricante puede sacar una revisión de hardware y hacerla funcionar añadiendo una cadena de fallback en su compatible, sin tocar el kernel. Significa que la lista ordenada de compatibles codifica una jerarquía de especificidad —soy exactamente esto, pero me conformo con ser tratado como aquello— que permite degradar con elegancia hacia drivers genéricos. Y el campo .data cierra el círculo: la misma cadena que empareja transporta, de propina, la configuración precisa de la variante, de modo que el driver no interroga al hardware sobre su modelo sino que lo recibe ya resuelto en probe. Cuando internalizas que el kernel arranca en hardware que sus autores nunca vieron gracias a que el acoplamiento entre silicio y código se redujo a comparar cadenas y adjuntar datos, dejas de ver compatible como un detalle de sintaxis y lo ves como la bisagra sobre la que gira la portabilidad entera del Linux embebido.
- Dado un nodo con
compatible = "acme,uart-9100", "acme,uart-generic"y un driver cuya tabla solo tieneacme,uart-generic, explica con qué cadena empareja y por qué. - Ordena de memoria las cinco vías de
platform_matchy justifica por qué el device tree va antes que laid_tabley que el nombre. - Añade una tercera variante
acme,uart-9200con sustruct acme_configy describe qué recibeof_device_get_match_dataenprobepara cada nodo. - Explica qué hace
MODULE_DEVICE_TABLE(of, ...)y por qué sin él tu módulo no se autocarga aunque el emparejamiento en frío sí funcione. - Sustituye
of_device_get_match_datapordevice_get_match_datay argumenta qué ganas de cara a un futuro puerto a ACPI.