Skip to main content
Linux device drivers are the primary mechanism by which the kernel interacts with hardware. A driver translates the kernel’s generic device model operations into the hardware-specific sequences required by a particular device. This guide covers the foundational concepts needed to write, build, and register a kernel module.

Driver types

Character devices

Provide a byte-stream interface. Examples: serial ports (tty), sensors, custom hardware. Registered via cdev_add() and accessed through /dev nodes.

Block devices

Transfer data in fixed-size blocks. Examples: storage controllers (NVMe, SATA), SD/MMC. Interact with the block I/O layer via register_blkdev().

Network devices

Present a net_device to the networking stack. Examples: Ethernet, Wi-Fi, loopback. Registered with register_netdev().

Platform drivers

Drive SoC-integrated peripherals described in device tree or ACPI. Use platform_driver_register() and match by compatible string or device name.

PCI drivers

Enumerate PCI/PCIe devices via pci_register_driver() and a pci_device_id table. Handle BAR mapping, MSI/MSI-X interrupts, and power management.

USB drivers

Bind to USB interfaces or devices using usb_register() and a usb_device_id table. The USB core handles enumeration and bandwidth negotiation.

I2C drivers

Communicate over the I2C bus using i2c_add_driver(). Devices described in the device tree under an I2C controller node.

SPI drivers

Full-duplex serial drivers registered via spi_register_driver(). The SPI core manages chip-select and clock configuration.

Essential headers

Every kernel module needs a minimal set of headers. Additional includes depend on the subsystem your driver targets.
/* Core module infrastructure */
#include <linux/module.h>       /* MODULE_LICENSE, module_init/exit */
#include <linux/kernel.h>       /* printk, KERN_INFO */
#include <linux/init.h>         /* __init, __exit */

/* Device model */
#include <linux/device.h>       /* struct device, dev_err/dev_info */
#include <linux/platform_device.h> /* struct platform_device */

/* Character devices */
#include <linux/fs.h>           /* struct file_operations, register_chrdev */
#include <linux/cdev.h>         /* struct cdev, cdev_init, cdev_add */
#include <linux/uaccess.h>      /* copy_to_user, copy_from_user */

/* Memory allocation */
#include <linux/slab.h>         /* kmalloc, kfree, kzalloc */

/* I/O memory */
#include <linux/io.h>           /* ioremap, ioread32, iowrite32 */
#include <linux/ioport.h>       /* request_mem_region, release_mem_region */

/* Interrupts */
#include <linux/interrupt.h>    /* request_irq, free_irq, irqreturn_t */

Module boilerplate

Every loadable kernel module must provide entry and exit points, a license declaration, and optionally author and description metadata.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init mydriver_init(void)
{
    pr_info("mydriver: loaded\n");
    return 0;  /* non-zero means init failed; module won't load */
}

static void __exit mydriver_exit(void)
{
    pr_info("mydriver: unloaded\n");
}

module_init(mydriver_init);
module_exit(mydriver_exit);

MODULE_LICENSE("GPL");          /* required; affects symbol visibility */
MODULE_AUTHOR("Your Name <[email protected]>");
MODULE_DESCRIPTION("Example driver");
MODULE_VERSION("1.0");
__init marks functions that are discarded from memory after initialization completes. __exit marks functions that are omitted entirely in non-modular (built-in) kernels.
MODULE_LICENSE controls which kernel symbols are accessible to the module. Modules declaring "GPL" can use GPL-only exports (marked EXPORT_SYMBOL_GPL). Declaring "Proprietary" restricts access and taints the kernel.

Driver development workflow

1

Identify the subsystem

Determine which bus or subsystem your device belongs to (platform, PCI, USB, I2C, SPI, etc.). This determines which registration API and probe/remove callbacks to implement.
2

Define the driver structure

Declare a bus-specific driver struct (e.g., struct platform_driver, struct pci_driver) embedding a struct device_driver. Populate the probe, remove, and id_table fields.
3

Implement probe()

The probe() callback is called by the driver core when a matching device is found. Allocate resources, map I/O memory, request IRQs, and initialize hardware here. Use devm_ prefixed helpers so resources are automatically released on driver detach.
4

Implement remove()

Called when the device is unbound. If you used devm_ helpers, most cleanup is automatic. Stop hardware activity, unregister any sub-devices, and release manually acquired resources.
5

Register the driver

Call the appropriate registration function (e.g., platform_driver_register()) — typically via a convenience macro like module_platform_driver() which generates both module_init and module_exit.
6

Build and test

Add the driver to a Kconfig and Makefile, build the module with make M=drivers/mydriver, load with insmod or modprobe, and check dmesg output and /sys/bus/<bus>/drivers/.

Complete minimal character device driver

The following driver creates a single character device that accepts writes and echoes them back on read. It demonstrates all the boilerplate required for a functional cdev-based driver.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "mychrdev"
#define BUF_SIZE    4096

static dev_t dev_num;           /* major/minor allocated by the kernel */
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;

static char *kernel_buf;

static int mydev_open(struct inode *inode, struct file *file)
{
    pr_info("%s: open\n", DEVICE_NAME);
    return 0;
}

static int mydev_release(struct inode *inode, struct file *file)
{
    pr_info("%s: release\n", DEVICE_NAME);
    return 0;
}

static ssize_t mydev_read(struct file *file, char __user *buf,
                          size_t count, loff_t *ppos)
{
    ssize_t bytes = min(count, (size_t)(BUF_SIZE - *ppos));

    if (bytes <= 0)
        return 0;
    if (copy_to_user(buf, kernel_buf + *ppos, bytes))
        return -EFAULT;
    *ppos += bytes;
    return bytes;
}

static ssize_t mydev_write(struct file *file, const char __user *buf,
                           size_t count, loff_t *ppos)
{
    ssize_t bytes = min(count, (size_t)(BUF_SIZE - *ppos));

    if (bytes <= 0)
        return -ENOSPC;
    if (copy_from_user(kernel_buf + *ppos, buf, bytes))
        return -EFAULT;
    *ppos += bytes;
    return bytes;
}

static const struct file_operations mydev_fops = {
    .owner   = THIS_MODULE,
    .open    = mydev_open,
    .release = mydev_release,
    .read    = mydev_read,
    .write   = mydev_write,
};

static int __init mydev_init(void)
{
    int ret;

    /* Allocate kernel buffer */
    kernel_buf = kzalloc(BUF_SIZE, GFP_KERNEL);
    if (!kernel_buf)
        return -ENOMEM;

    /* Dynamically allocate a major/minor number */
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("%s: alloc_chrdev_region failed: %d\n", DEVICE_NAME, ret);
        goto err_free_buf;
    }

    /* Initialize and add the cdev */
    cdev_init(&my_cdev, &mydev_fops);
    my_cdev.owner = THIS_MODULE;
    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret < 0) {
        pr_err("%s: cdev_add failed: %d\n", DEVICE_NAME, ret);
        goto err_unregister;
    }

    /* Create a class so udev creates the /dev node automatically */
    my_class = class_create(DEVICE_NAME);
    if (IS_ERR(my_class)) {
        ret = PTR_ERR(my_class);
        goto err_cdev_del;
    }

    my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(my_device)) {
        ret = PTR_ERR(my_device);
        goto err_class_destroy;
    }

    pr_info("%s: registered as %d:%d\n", DEVICE_NAME,
            MAJOR(dev_num), MINOR(dev_num));
    return 0;

err_class_destroy:
    class_destroy(my_class);
err_cdev_del:
    cdev_del(&my_cdev);
err_unregister:
    unregister_chrdev_region(dev_num, 1);
err_free_buf:
    kfree(kernel_buf);
    return ret;
}

static void __exit mydev_exit(void)
{
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    kfree(kernel_buf);
    pr_info("%s: unregistered\n", DEVICE_NAME);
}

module_init(mydev_init);
module_exit(mydev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example Author");
MODULE_DESCRIPTION("Minimal character device driver");
Always use alloc_chrdev_region() to let the kernel assign a major number dynamically. Hard-coding major numbers conflicts with Documentation/admin-guide/devices.txt assignments and breaks on systems that already use that number.

Registering and unregistering drivers

Each bus type exposes its own registration pair. Most drivers use the module_<bus>_driver() convenience macro, which generates a module_init / module_exit wrapper automatically.
#include <linux/platform_device.h>

static const struct of_device_id mydrv_of_match[] = {
    { .compatible = "vendor,mydevice" },
    { }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);

static struct platform_driver mydrv = {
    .probe  = mydrv_probe,
    .remove = mydrv_remove,
    .driver = {
        .name           = "mydevice",
        .of_match_table = mydrv_of_match,
    },
};

module_platform_driver(mydrv);

Building the module

Add the driver to the kernel build system by creating a Kconfig entry and a Makefile:
config MY_DRIVER
    tristate "My example driver"
    depends on OF
    help
      Driver for the hypothetical MyDevice peripheral.
      If unsure, say N.
obj-$(CONFIG_MY_DRIVER) += mydriver.o
To build out-of-tree against a running kernel:
# Build
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# Load
sudo insmod mydriver.ko

# Verify
dmesg | tail -20
lsmod | grep mydriver

# Unload
sudo rmmod mydriver

Further reading

Device model

The Linux device model: buses, devices, drivers, probe lifecycle, and sysfs.

DMA API

Coherent and streaming DMA mappings, scatter-gather, and IOMMU considerations.

Power management

Runtime PM, system sleep states, dev_pm_ops, and autosuspend.

Build docs developers (and LLMs) love