Build a minimal kernel module, implement a character device with file_operations, and write a platform driver — complete, annotated C examples included.
Use this file to discover all available pages before exploring further.
Writing a Linux device driver starts with understanding two things: the kernel module infrastructure that loads and unloads your code, and the driver model APIs that bind your driver to a device. This guide walks you through both, starting from the simplest possible module and building up to a character device driver and a platform driver.
1
Set up a minimal kernel module
Every loadable kernel module must declare an init function and an exit function, and announce its license.
2
Allocate a character device number
Use alloc_chrdev_region() to dynamically allocate a major number and register your device range.
3
Implement file_operations
Fill in open, release, read, write, and unlocked_ioctl to handle user-space system calls.
4
Register the cdev
Initialize a struct cdev and add it to the kernel with cdev_add().
5
Create a device node
Use class_create() and device_create() so that udev automatically creates the /dev entry.
6
Test and clean up
Load the module with insmod, verify it in /proc/modules and /sys, then remove it cleanly with rmmod.
The three macros at the bottom come from <linux/module.h>:
/* From include/linux/module.h *//* Called during do_initcalls() (built-in) or at module insertion time. */#define module_init(x) __initcall(x);/* Wraps cleanup code with cleanup_module() for rmmod. No effect if built-in. */#define module_exit(x) __exitcall(x);/* Embeds license info in the module ELF section. */#define MODULE_LICENSE(_license) MODULE_FILE MODULE_INFO(license, _license)/* Author and description metadata. */#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)#define MODULE_DESCRIPTION(_desc) MODULE_INFO(description, _desc)
Build the module with a minimal Makefile:
obj-m += minimal_module.oall: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
A character device driver exposes a device file under /dev. User-space programs call standard file operations on it. The driver provides the implementations through a struct file_operations.
alloc_chrdev_region() asks the kernel to assign an unused major number. You pass it the base minor and the count of minors you need:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
Pair every successful alloc_chrdev_region() with unregister_chrdev_region() in your exit path.
Always use alloc_chrdev_region() rather than hard-coding a major number. Hard-coded major numbers conflict with other drivers and are rejected upstream.
Platform drivers manage SoC peripherals that appear in the Device Tree or ACPI tables. The kernel populates a struct platform_device for each node and your driver binds to it through the name or the of_match_table.
/* myplatdrv.c — minimal platform driver */#include <linux/module.h>#include <linux/platform_device.h>#include <linux/of.h>#include <linux/io.h>struct myplatdrv_data { void __iomem *base;};static int myplatdrv_probe(struct platform_device *pdev){ struct myplatdrv_data *priv; struct resource *res; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; /* * devm_ioremap_resource() maps the first memory resource and * automatically unmaps it when the driver is removed or probe fails. */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); platform_set_drvdata(pdev, priv); dev_info(&pdev->dev, "probed at %pa\n", &res->start); return 0;}static void myplatdrv_remove(struct platform_device *pdev){ dev_info(&pdev->dev, "removed\n"); /* devm resources are freed automatically */}static const struct of_device_id myplatdrv_of_match[] = { { .compatible = "vendor,myplatdrv" }, { /* sentinel */ }};MODULE_DEVICE_TABLE(of, myplatdrv_of_match);static struct platform_driver myplatdrv_driver = { .probe = myplatdrv_probe, .remove = myplatdrv_remove, .driver = { .name = "myplatdrv", .of_match_table = myplatdrv_of_match, },};module_platform_driver(myplatdrv_driver);MODULE_LICENSE("GPL");MODULE_DESCRIPTION("Minimal platform driver example");
module_platform_driver() is a shorthand macro defined in <linux/platform_device.h>. It expands to a module_init() that calls platform_driver_register() and a module_exit() that calls platform_driver_unregister(), saving you the boilerplate.
Use devm_* (device-managed) resource allocation functions wherever they exist. Resources acquired with devm_kzalloc(), devm_ioremap_resource(), or devm_request_irq() are freed automatically when the device is unbound, eliminating a whole class of resource-leak bugs.
Most hardware drivers need to respond to interrupts. Call request_irq() inside probe() once the hardware is initialized, and call free_irq() inside remove().
/* From include/linux/interrupt.h (lines 172–177) */static inline int __must_checkrequest_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev){ return request_threaded_irq(irq, handler, NULL, flags | IRQF_COND_ONESHOT, name, dev);}
A typical interrupt handler looks like this:
static irqreturn_t mydrv_irq_handler(int irq, void *dev_id){ struct mydrv_data *priv = dev_id; /* Read the hardware status register to confirm the interrupt. */ u32 status = readl(priv->base + STATUS_REG); if (!(status & IRQ_PENDING)) return IRQ_NONE; /* Acknowledge the interrupt. */ writel(IRQ_CLEAR, priv->base + STATUS_REG); /* Wake up any waiting processes. */ wake_up_interruptible(&priv->wait_queue); return IRQ_HANDLED;}static int mydrv_probe(struct platform_device *pdev){ struct mydrv_data *priv; int irq, ret; /* ... allocate priv, map registers ... */ irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; ret = devm_request_irq(&pdev->dev, irq, mydrv_irq_handler, IRQF_SHARED, "mydrv", priv); if (ret) { dev_err(&pdev->dev, "request_irq failed: %d\n", ret); return ret; } return 0;}
Use devm_request_irq() instead of request_irq() when possible. It automatically calls free_irq() when the device is unbound, so you never need to pair it manually with a free_irq() call in remove().
IRQ_HANDLED tells the kernel your handler processed the interrupt. IRQ_NONE tells it the interrupt came from a different device sharing the same line. For shared interrupts (IRQF_SHARED), always check a hardware status register before returning IRQ_HANDLED.