Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 68

Platform Drivers with Managed Resources

Recommended resources

Hello! Welcome to your next lesson in the Embedded Interview Preparation Bootcamp.

In our last session, we demystified the Linux platform driver model. You learned how the Device Tree describes hardware, how the compatible string creates the crucial link between a device and its driver, and how to use the probe function to retrieve hardware resources like memory addresses and IRQ numbers.

However, the manual resource handling we used—calling platform_get_resource, ioremap, and request_irq in probe, and then manually undoing everything in remove—has a significant drawback. It requires meticulous bookkeeping, especially in the probe function, where a failure at any step requires a complex chain of goto statements to unwind all previously acquired resources. This is tedious and a common source of resource-leak bugs.

Today, we will learn the modern, robust solution to this problem.

Your learning outcome for this lesson is to: Implement a platform driver that uses devm_* managed functions for automatic resource cleanup (e.g., memory, IRQs).


1. The Problem with Manual Resource Management

Let's consider a typical probe function that manually acquires several resources:

// A typical (but fragile) probe function
static int my_driver_probe(struct platform_device *pdev) {
    struct my_driver_data *data;
    struct resource *mem_res;
    void __iomem *regs;
    int irq;
    int ret;

    // 1. Allocate private data structure
    data = kzalloc(sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    platform_set_drvdata(pdev, data);

    // 2. Get and map memory-mapped I/O
    mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!mem_res) {
        ret = -EINVAL;
        goto err_free_data;
    }
    regs = ioremap(mem_res->start, resource_size(mem_res));
    if (IS_ERR(regs)) {
        ret = PTR_ERR(regs);
        goto err_free_data;
    }
    data->regs = regs;

    // 3. Get and request IRQ
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        ret = irq;
        goto err_unmap_regs;
    }
    ret = request_irq(irq, my_irq_handler, 0, "my_device", data);
    if (ret) {
        goto err_unmap_regs;
    }
    data->irq = irq;

    // ... initialization continues ...
    dev_info(&pdev->dev, "Probe successful!\n");
    return 0;

// Error-unwinding chain
err_unmap_regs:
    iounmap(data->regs);
err_free_data:
    kfree(data);
    return ret;
}

static int my_driver_remove(struct platform_device *pdev) {
    struct my_driver_data *data = platform_get_drvdata(pdev);

    free_irq(data->irq, data);
    iounmap(data->regs);
    kfree(data);

    return 0;
}

Notice the goto cascade required for error handling. If request_irq fails, we must jump to a label that unmaps the registers and then falls through to free the private data. This becomes exponentially more complex with each new resource. The remove function essentially duplicates this cleanup logic. This pattern is fragile and difficult to maintain.


2. Device-Managed Resources: The devm_* API

The kernel provides a much cleaner solution: device-managed resources. The core idea is to tie the lifetime of a resource directly to the struct device it is allocated for.

When you use a devm_* variant of an allocation function (e.g., devm_kzalloc instead of kzalloc), the kernel does two things:

  1. It allocates the resource as requested.
  2. It registers a corresponding "release" action with the device.

When the device is later detached from the driver (either through a call to remove or because probe fails and returns an error), the kernel automatically walks the list of registered resources and frees them in the reverse order of allocation.

This has two profound benefits:

  • Simplified probe(): Error handling becomes a simple return error_code;. No more goto chains for cleanup.
  • Simplified remove(): The function often becomes empty, as the framework handles all the cleanup.

This is analogous to the RAII (Resource Acquisition Is Initialization) principle in C++, where an object's destructor automatically releases the resources it holds. Here, the "object" is the struct device, and the "destructor" is the driver core's detach process.

How It Works Internally

Let's visualize the process for devm_kzalloc.

devm_kzalloc() Memory Allocation and Device Binding Process
This diagram shows that when `devm_kzalloc` is called, it allocates the requested memory and also a `devres` (device resource) management struct. This `devres` struct is added to a linked list within the `struct device` (`pdev->dev`). When the driver is unbound, the kernel iterates through this list and executes the release function associated with each entry.

3. Refactoring a Driver with devm_*

Let's refactor our previous example using the devm_* API. The most common managed functions have direct counterparts to the manual ones.

Manual Function Device-Managed (devm_*) Equivalent
kzalloc, kmalloc devm_kzalloc, devm_kmalloc
request_irq devm_request_irq
platform_get_resource + ioremap devm_ioremap_resource
gpiod_get devm_gpiod_get
clk_get devm_clk_get

Here is the clean, modern version of our driver:

// A robust probe function using devm_*
static int my_driver_probe(struct platform_device *pdev) {
    struct my_driver_data *data;
    void __iomem *regs;
    int irq;
    int ret;

    // 1. Allocate private data structure (managed)
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    platform_set_drvdata(pdev, data);

    // 2. Get and map memory-mapped I/O (managed)
    // devm_ioremap_resource combines platform_get_resource and ioremap
    regs = devm_ioremap_resource(&pdev->dev, 0);
    if (IS_ERR(regs)) {
        // If this fails, devm_kzalloc'd memory is auto-freed!
        return PTR_ERR(regs);
    }
    data->regs = regs;

    // 3. Get and request IRQ (managed)
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;
    
    ret = devm_request_irq(&pdev->dev, irq, my_irq_handler, 0, "my_device", data);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ %d\n", irq);
        // If this fails, regs and data are auto-freed!
        return ret;
    }
    data->irq = irq;

    dev_info(&pdev->dev, "Probe successful!\n");
    return 0; // No error cleanup path needed!
}

// The remove function is now wonderfully simple.
static int my_driver_remove(struct platform_device *pdev) {
    // All resources allocated with devm_* are automatically freed
    // by the kernel after this function returns.
    dev_info(&pdev->dev, "Driver removed.\n");
    return 0;
}

The difference is night and day. The code is linear, readable, and fundamentally more robust.


4. Analyzing Real-World Examples

Let's examine how this pattern is used in professional drivers. We will look at two excellent examples that demonstrate slightly different use cases.

First, let's study a complete driver that requests a GPIO and an IRQ.

Implementing IRQs on the Nitrogen93

The following article from Ezurio provides a perfect, self-contained GPIO interrupt driver that is written using modern best practices, including extensive use of devm_* functions.

Read the brief 'Overview' and then carefully study the 'Kernel Driver Source Code' (gpio-irq-demo.c). Pay close attention to the gpio_irq_probe and gpio_irq_remove functions. Identify every call that starts with devm_ and notice how simple the remove function is as a result.

From that code, you should have identified:

  • devm_kzalloc: Allocates the gpio_irq_dev private data structure.
  • devm_gpiod_get: Acquires the GPIO descriptor from the device tree.
  • devm_request_irq: Registers the interrupt handler.
  • gpio_irq_remove: This function only contains a call to pm_runtime_disable. It doesn't need to free the IRQ, the GPIO descriptor, or the private data, because the framework handles it all.

Next, let's look at a snippet that deals with memory-mapped registers, which is extremely common in SoC drivers.

[PDF] Introduction to Linux kernel driver programming

This presentation slide from Bootlin shows a code snippet from a real I2C controller driver. It's a classic example of mapping hardware registers in a managed way.

Focus on slide 22, titled 'Usage of the platform bus'. Analyze the omap_i2c_probe function. Specifically, note the sequence of platform_get_resource followed by devm_ioremap_resource.

This example shows another common pattern: devm_ioremap_resource. It's a helper that combines fetching the IORESOURCE_MEM resource and mapping it into the kernel's virtual address space in one managed step. This is the preferred way to map device registers.


Conclusion

Mastering device-managed resources is a key differentiator for an experienced engineer. It demonstrates that you not only know how to write a functional driver, but how to write a robust, maintainable, and modern one. In an interview, being able to contrast the manual and managed approaches shows a deep understanding of kernel development best practices.

Key Takeaways:

  • Managed Resources: The devm_* family of functions allocates resources (memory, IRQs, clocks, GPIOs) and ties their lifetime to a struct device.
  • Automatic Cleanup: The kernel automatically frees all managed resources when a driver is unbound from its device, either during a normal remove or upon probe failure.
  • Simplified Code: This pattern eliminates complex error-handling goto chains in probe and often reduces the remove function to being empty.
  • Core Functions: devm_kzalloc, devm_request_irq, and devm_ioremap_resource are fundamental tools for building robust platform drivers.

Preview of the Next Lesson:

We now have a solid, safe framework for acquiring and managing an interrupt request line. But what should we actually do inside the interrupt handler? Interrupt context is highly constrained—you cannot sleep, and you must be extremely fast. In the next lesson, we will dive into implementing kernel interrupt handling using request_irq and partitioning the handler into a fast "top half" and a deferrable "bottom half" (using a workqueue).