Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/DeelerDev/linux/llms.txt

Use this file to discover all available pages before exploring further.

Linux organizes hardware into bus subsystems, each with its own discovery mechanism, device identity format, and driver registration API. Choosing the correct bus type is the first decision you make when writing a new driver, because it determines how the kernel finds your hardware, what data structures you work with, and how your driver fits into the power management and hotplug infrastructure. This page covers the five most common bus types and the Device Tree binding layer used on ARM and embedded platforms.

Platform bus

The platform bus is a pseudo-bus for SoC-integrated peripherals and legacy PC-style hardware that is directly addressable by the CPU but has no dynamic discovery protocol. Devices are described statically—either in board setup code or, on modern systems, in Device Tree or ACPI tables.
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>

/* Device ID table for non-DT matching */
static const struct platform_device_id mydrv_id_table[] = {
    { "my-device-v1", 0 },
    { "my-device-v2", 1 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(platform, mydrv_id_table);

static int mydrv_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    /* Get the first memory resource from DT/board file */
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    dev_info(&pdev->dev, "probed successfully\n");
    return 0;
}

static void mydrv_remove(struct platform_device *pdev)
{
    /* devm resources released automatically */
}

static struct platform_driver mydrv_driver = {
    .probe  = mydrv_probe,
    .remove = mydrv_remove,
    .driver = {
        .name           = "my-device",
        .of_match_table = mydrv_of_match,  /* see Device Tree section */
    },
    .id_table = mydrv_id_table,
};

module_platform_driver(mydrv_driver);
MODULE_LICENSE("GPL");
module_platform_driver() expands to module_init() and module_exit() wrappers that call platform_driver_register() and platform_driver_unregister(). To register a platform device from board code (not Device Tree):
static struct resource mydev_resources[] = {
    {
        .start = 0xFE200000,
        .end   = 0xFE2000FF,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = 42,
        .end   = 42,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device mydev = {
    .name          = "my-device",
    .id            = 0,
    .num_resources = ARRAY_SIZE(mydev_resources),
    .resource      = mydev_resources,
};

platform_device_register(&mydev);

PCI

PCI and PCIe devices are discovered automatically at boot by the PCI subsystem. Each device advertises its vendor ID, device ID, class code, and subsystem IDs. Drivers declare an id_table listing the combinations they support.
#include <linux/pci.h>

#define MY_VENDOR_ID 0x10EC
#define MY_DEVICE_ID 0x8168

static const struct pci_device_id mydrv_pci_tbl[] = {
    { PCI_DEVICE(MY_VENDOR_ID, MY_DEVICE_ID) },
    { PCI_DEVICE_CLASS(PCI_CLASS_NETWORK_ETHERNET, 0xFFFF00) },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(pci, mydrv_pci_tbl);

static int mydrv_pci_probe(struct pci_dev *pdev,
                            const struct pci_device_id *ent)
{
    int ret;

    ret = pcim_enable_device(pdev);
    if (ret)
        return ret;

    pci_set_master(pdev);

    ret = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "PCI device probed: %04x:%04x\n",
             pdev->vendor, pdev->device);
    return 0;
}

static void mydrv_pci_remove(struct pci_dev *pdev)
{
    /* pcim_* resources released automatically */
}

static struct pci_driver mydrv_pci_driver = {
    .name     = "my-pci-driver",
    .id_table = mydrv_pci_tbl,
    .probe    = mydrv_pci_probe,
    .remove   = mydrv_pci_remove,
};

module_pci_driver(mydrv_pci_driver);
MODULE_LICENSE("GPL");
Prefer pcim_enable_device() over pci_enable_device(). The pcim_* family is managed (analogous to devm_*) and automatically disables and releases resources when the driver unbinds.

USB

USB devices are discovered and enumerated by the USB host controller driver. Each device and each interface within a device has a class, subclass, protocol, vendor, and product ID. USB drivers typically bind to interfaces, not to whole devices.
#include <linux/usb.h>

static const struct usb_device_id mydrv_usb_tbl[] = {
    { USB_DEVICE(0x1234, 0x5678) },                 /* exact vendor:product */
    { USB_DEVICE_INFO(USB_CLASS_HID, 0, 0) },       /* all HID devices */
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(usb, mydrv_usb_tbl);

static int mydrv_usb_probe(struct usb_interface *intf,
                            const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);

    dev_info(&intf->dev, "USB device plugged in: %04x:%04x\n",
             le16_to_cpu(udev->descriptor.idVendor),
             le16_to_cpu(udev->descriptor.idProduct));
    return 0;
}

static void mydrv_usb_disconnect(struct usb_interface *intf)
{
    dev_info(&intf->dev, "USB device removed\n");
}

static struct usb_driver mydrv_usb_driver = {
    .name       = "my-usb-driver",
    .id_table   = mydrv_usb_tbl,
    .probe      = mydrv_usb_probe,
    .disconnect = mydrv_usb_disconnect,
};

module_usb_driver(mydrv_usb_driver);
MODULE_LICENSE("GPL");
USB drivers use usb_register() and usb_deregister() under the hood, but the module_usb_driver() macro handles both.

I2C

I2C is a two-wire serial bus widely used for sensors, EEPROMs, and power management ICs. Devices are not discoverable by the bus itself; they must be described in Device Tree or board code.
#include <linux/i2c.h>

static const struct i2c_device_id mydrv_i2c_id[] = {
    { "my-sensor", 0 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(i2c, mydrv_i2c_id);

static int mydrv_i2c_probe(struct i2c_client *client)
{
    if (!i2c_check_functionality(client->adapter,
                                  I2C_FUNC_SMBUS_BYTE_DATA))
        return -EOPNOTSUPP;

    dev_info(&client->dev, "I2C device at address 0x%02x\n",
             client->addr);
    return 0;
}

static void mydrv_i2c_remove(struct i2c_client *client)
{
}

static struct i2c_driver mydrv_i2c_driver = {
    .driver = {
        .name           = "my-sensor",
        .of_match_table = mydrv_of_match,
    },
    .probe    = mydrv_i2c_probe,
    .remove   = mydrv_i2c_remove,
    .id_table = mydrv_i2c_id,
};

module_i2c_driver(mydrv_i2c_driver);
MODULE_LICENSE("GPL");
i2c_add_driver() is the low-level registration function; module_i2c_driver() wraps it into a complete module init/exit pair.

SPI

SPI is a four-wire synchronous serial bus for high-speed peripherals like flash memory, displays, and ADCs. Like I2C, SPI devices must be described in Device Tree.
#include <linux/spi/spi.h>

static const struct spi_device_id mydrv_spi_id[] = {
    { "my-spi-device", 0 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(spi, mydrv_spi_id);

static int mydrv_spi_probe(struct spi_device *spi)
{
    /* Configure SPI mode and max speed */
    spi->mode        = SPI_MODE_0;
    spi->max_speed_hz = 1000000;
    spi->bits_per_word = 8;

    return spi_setup(spi);
}

static void mydrv_spi_remove(struct spi_device *spi)
{
}

static struct spi_driver mydrv_spi_driver = {
    .driver = {
        .name           = "my-spi-device",
        .of_match_table = mydrv_of_match,
    },
    .probe    = mydrv_spi_probe,
    .remove   = mydrv_spi_remove,
    .id_table = mydrv_spi_id,
};

module_spi_driver(mydrv_spi_driver);
MODULE_LICENSE("GPL");
spi_register_driver() is the underlying function. module_spi_driver() generates the module init/exit boilerplate.

Device Tree bindings and of_device_id

On ARM and RISC-V systems, hardware is described in Device Tree source (.dts) files. Drivers match against DT nodes using a compatible string. Each driver declares the set of compatible strings it supports in an of_match_table:
#include <linux/of.h>
#include <linux/of_device.h>

static const struct of_device_id mydrv_of_match[] = {
    { .compatible = "vendor,my-device-v1", .data = (void *)0 },
    { .compatible = "vendor,my-device-v2", .data = (void *)1 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);
The of_match_table is placed in the driver’s driver.of_match_table field (for platform, I2C, SPI) or the equivalent for other buses. The kernel calls of_match_device() during probe to find the matching entry, which your driver can read to discover hardware revision data:
static int mydrv_probe(struct platform_device *pdev)
{
    const struct of_device_id *match;
    unsigned long variant;

    match = of_match_device(mydrv_of_match, &pdev->dev);
    if (match)
        variant = (unsigned long)match->data;

    /* ... */
    return 0;
}
A minimal Device Tree node for the platform example above:
mydevice@fe200000 {
    compatible = "vendor,my-device-v2";
    reg = <0xfe200000 0x100>;
    interrupts = <0 42 4>;
};

Choosing the right bus type

Platform

SoC-integrated peripherals, legacy ports, memory-mapped hardware without a discovery protocol. Use when hardware is described in Device Tree or ACPI.

PCI / PCIe

Expansion cards, NVMe storage, Ethernet controllers, GPUs. Use when the hardware plugs into a PCI slot or is wired as a PCIe endpoint.

USB

Externally attached peripherals: storage, HID, audio, cameras. Use when the hardware connects via USB and has a USB vendor/product ID.

I2C / SPI

Sensors, power management ICs, displays, flash. I2C for low-speed (400 kHz–1 MHz); SPI for higher throughput. Both require Device Tree descriptions.
If your device appears in Device Tree with a compatible string and is memory-mapped, it is almost certainly a platform device. Start with platform_driver and module_platform_driver().

Build docs developers (and LLMs) love