| name | linux-device-driver |
| description | Use when writing, reviewing, or debugging Linux device drivers for embedded targets (automotive IVI, HUD, RSE on Linux/QNX-like kernels). Covers character device registration, struct file_operations, platform drivers with probe/remove, devm_* managed resources, memory-mapped I/O, and interrupt handling.
|
| argument-hint | <driver-name> [write|review|debug] |
Linux Device Driver Development
Practices for writing correct, resource-safe Linux device drivers for embedded
targets such as automotive IVI, HUD, and RSE devices.
Source of truth:
When to Use This Skill
- Writing a character device driver to expose hardware to userspace via
/dev/.
- Writing a platform driver to control a device described in the Device Tree.
- Handling memory-mapped I/O registers.
- Installing interrupt handlers for hardware events.
Character Device Driver
A character device exposes a file interface (read, write, ioctl) under /dev/.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DRIVER_NAME "mydev"
#define DEVICE_NAME "mydev0"
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int mydev_open(struct inode *inode, struct file *filp)
{
pr_info(DRIVER_NAME ": open\n");
return 0;
}
static int mydev_release(struct inode *inode, struct file *filp)
{
pr_info(DRIVER_NAME ": release\n");
return 0;
}
static ssize_t mydev_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
const char *data = "hello\n";
size_t len = strlen(data);
if (*ppos >= len)
return 0;
if (count > len - *ppos)
count = len - *ppos;
if (copy_to_user(buf, data + *ppos, count))
return -EFAULT;
*ppos += count;
return count;
}
static ssize_t mydev_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
char kbuf[64] = {0};
if (count >= sizeof(kbuf))
count = sizeof(kbuf) - 1;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
pr_info(DRIVER_NAME ": received: %s\n", kbuf);
return count;
}
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;
ret = alloc_chrdev_region(&dev_num, 0, 1, DRIVER_NAME);
if (ret < 0) {
pr_err(DRIVER_NAME ": alloc_chrdev_region failed: %d\n", ret);
return ret;
}
cdev_init(&my_cdev, &mydev_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err(DRIVER_NAME ": cdev_add failed: %d\n", ret);
goto err_cdev;
}
my_class = class_create(THIS_MODULE, DRIVER_NAME);
if (IS_ERR(my_class)) {
ret = PTR_ERR(my_class);
pr_err(DRIVER_NAME ": class_create failed: %d\n", ret);
goto err_class;
}
if (IS_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME))) {
ret = PTR_ERR(device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME));
pr_err(DRIVER_NAME ": device_create failed\n");
goto err_device;
}
pr_info(DRIVER_NAME ": registered major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
err_device:
class_destroy(my_class);
err_class:
cdev_del(&my_cdev);
err_cdev:
unregister_chrdev_region(dev_num, 1);
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);
pr_info(DRIVER_NAME ": unregistered\n");
}
module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");
Key rules:
copy_to_user / copy_from_user are mandatory for transferring data between kernel and userspace — never dereference a __user pointer directly.
- Always check the return value of kernel allocation/registration functions.
- Clean up in reverse order of setup on both the error path and in
__exit.
Platform Driver
A platform driver handles devices described in the Device Tree or board file.
It is matched by its compatible string.
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
struct mydriver_priv {
void __iomem *base;
int irq;
};
static irqreturn_t mydriver_irq_handler(int irq, void *dev_id)
{
struct mydriver_priv *priv = dev_id;
(void)priv;
return IRQ_HANDLED;
}
static int mydriver_probe(struct platform_device *pdev)
{
struct mydriver_priv *priv;
struct resource *res;
int ret;
priv = devm_kzalloc(&pdev->dev, (*priv), GFP_KERNEL);
(!priv)
-ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, );
priv->base = devm_ioremap_resource(&pdev->dev, res);
(IS_ERR(priv->base))
PTR_ERR(priv->base);
priv->irq = platform_get_irq(pdev, );
(priv->irq < )
priv->irq;
ret = devm_request_irq(&pdev->dev, priv->irq, mydriver_irq_handler,
, dev_name(&pdev->dev), priv);
(ret)
ret;
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, );
;
}
{
dev_info(&pdev->dev, );
;
}
{
{ .compatible = },
{ }
};
MODULE_DEVICE_TABLE(of, mydriver_of_match);
{
.probe = mydriver_probe,
.remove = mydriver_remove,
.driver = {
.name = ,
.of_match_table = mydriver_of_match,
},
};
module_platform_driver(mydriver);
MODULE_LICENSE();
MODULE_AUTHOR();
MODULE_DESCRIPTION();
devm_* — Managed Resources
Resources allocated with devm_* are automatically released when the device is
detached (.remove returns) or when the module is unloaded. This eliminates many
error-path and cleanup bugs.
| Manual API | Managed equivalent |
|---|
kzalloc / kfree | devm_kzalloc (no free needed) |
ioremap_resource / iounmap | devm_ioremap_resource |
request_irq / free_irq | devm_request_irq |
request_mem_region / release_mem_region | devm_request_mem_region |
clk_get / clk_put | devm_clk_get |
gpio_request / gpio_free | devm_gpio_request |
Rule: Prefer devm_* for all resource acquisition in .probe. Only mix manual
and managed resources when devm_ variants are not available for a specific API.
Memory-Mapped I/O Register Access
#include <linux/io.h>
uint32_t val = readl(priv->base + REG_STATUS);
writel(0x01, priv->base + REG_CONTROL);
uint32_t ctrl = readl(priv->base + REG_CONTROL);
ctrl |= BIT(3);
writel(ctrl, priv->base + REG_CONTROL);
Rule: Never access void __iomem * pointers directly with *ptr — always use
readl/writel (and their 8-/16-bit variants readb/writeb, readw/writew)
to ensure correct memory barriers on all architectures.
Interrupt Handler Rules
- Keep the interrupt handler fast — do minimal work.
- Cannot sleep in an interrupt handler — do not call
kmalloc(GFP_KERNEL) or mutex_lock from IRQ context.
- Use a work queue or tasklet to defer slow work.
- Return
IRQ_HANDLED if the interrupt was from this device; IRQ_NONE otherwise.
- Shared interrupts (
IRQF_SHARED) require IRQ_NONE when not yours.
Device Tree Binding (Example)
/* board .dts */
my_sensor: sensor@40010000 {
compatible = "vendor,my-hardware";
reg = <0x40010000 0x1000>; /* base address, size */
interrupts = <0 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_sensor>;
status = "okay";
};
The driver matches on compatible = "vendor,my-hardware" and retrieves reg
and interrupts via platform_get_resource and platform_get_irq.
ioctl — Custom Control Interface
#include <linux/ioctl.h>
#define MYDEV_IOC_MAGIC 'M'
#define MYDEV_GET_STATUS _IOR(MYDEV_IOC_MAGIC, 1, uint32_t)
#define MYDEV_SET_MODE _IOW(MYDEV_IOC_MAGIC, 2, uint32_t)
static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
uint32_t val;
switch (cmd) {
case MYDEV_GET_STATUS:
val = read_hardware_status();
if (copy_to_user((uint32_t __user *)arg, &val, sizeof(val)))
return -EFAULT;
return 0;
case MYDEV_SET_MODE:
if (copy_from_user(&val, (uint32_t __user *)arg, sizeof(val)))
return -EFAULT;
set_hardware_mode(val);
return 0;
default:
return -ENOTTY;
}
}
Prerequisites
- Linux host machine (Ubuntu 20.04 LTS or Debian 11 recommended).
- Cross-compilation toolchain:
arm-linux-gnueabihf-gcc or aarch64-linux-gnu-gcc.
- Target board with serial console access (USB-to-UART adapter).
ssh access to target (optional but recommended).
Step-by-Step Workflows
Step 1: Create the driver source file
Include <linux/module.h>, <linux/kernel.h>, and the subsystem-specific headers.
Step 2: Register the device
Implement probe() and remove() for platform drivers; use devm_* functions for all resources.
Step 3: Implement file operations (char device)
Fill struct file_operations; register with misc_register() or cdev_add().
Step 4: Handle memory-mapped I/O and interrupts
Use devm_ioremap_resource(); request IRQ with devm_request_irq().
Step 5: Build and test
Add to Kconfig and Makefile; load with insmod; verify with dmesg for registration messages.
Troubleshooting
insmod: ERROR: could not insert module — check dmesg immediately after; common causes: missing MODULE_LICENSE, symbol not found, or version mismatch.
devm_request_irq returns -ENODEV — the IRQ number from DTS/platform data is wrong; verify with /proc/interrupts.
ioremap returns NULL — the physical address is invalid or not reserved; check the memory map in the board DTS or BSP documentation.
- Driver
probe() not called — confirm the compatible string in the driver matches the DTS node exactly (case-sensitive).
Pre-Commit Checklist
References