driver-know-hows

device driver related stuff

View on GitHub

Chapter 8: Platform Device Drivers

Table of Contents

  1. Introduction to Platform Drivers
  2. Platform Device Model
  3. Device Tree Basics
  4. Writing Platform Drivers
  5. Platform Resources
  6. Power Management

Introduction to Platform Drivers

What are Platform Devices?

Platform devices are devices that:

Platform Driver Architecture

Device Tree / Platform Data
            ↓
┌──────────────────────────┐
│   Platform Bus           │
├──────────────────────────┤
│  Platform Device         │
│  - Resources (MMIO, IRQ) │
│  - Driver match          │
└──────────┬───────────────┘
           ↓
┌──────────────────────────┐
│  Platform Driver         │
│  - probe()               │
│  - remove()              │
│  - suspend/resume()      │
└──────────────────────────┘

Platform Device Model

Basic Structures

#include <linux/platform_device.h>

/*
 * struct platform_device - Platform device structure
 */
struct platform_device {
    const char *name;           /* Device name */
    int id;                     /* Instance ID (-1 for single instance) */
    struct device dev;          /* Embedded device structure */
    u32 num_resources;          /* Number of resources */
    struct resource *resource;  /* Resources (MMIO, IRQ, etc.) */
    /* ... more fields ... */
};

/*
 * struct platform_driver - Platform driver structure
 */
struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    bool prevent_deferred_probe;
};

/*
 * struct resource - Hardware resource
 */
struct resource {
    resource_size_t start;      /* Start address */
    resource_size_t end;        /* End address */
    const char *name;           /* Resource name */
    unsigned long flags;        /* Resource type flags */
    /* ... more fields ... */
};

/* Resource types */
#define IORESOURCE_IO           0x00000100  /* I/O port */
#define IORESOURCE_MEM          0x00000200  /* Memory region */
#define IORESOURCE_IRQ          0x00000400  /* IRQ line */
#define IORESOURCE_DMA          0x00000800  /* DMA channel */

Device Tree Basics

What is Device Tree?

Device tree describes hardware that exists in the system:

Device Tree Example

/* Example: my-device.dts */

/dts-v1/;

/ {
    compatible = "vendor,board";
    #address-cells = <1>;
    #size-cells = <1>;
    
    /* Device node */
    my_device: mydev@10000000 {
        compatible = "vendor,my-device-v1";
        reg = <0x10000000 0x1000>;  /* Register base and size */
        interrupts = <0 42 4>;       /* IRQ number */
        clocks = <&clk_bus>;
        clock-names = "bus_clk";
        status = "okay";
        
        /* Custom properties */
        vendor,max-speed = <100000>;
        vendor,num-channels = <4>;
    };
};

Device Tree API

#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>

/*
 * Device tree matching
 */
static const struct of_device_id my_device_of_match[] = {
    { .compatible = "vendor,my-device-v1", },
    { .compatible = "vendor,my-device-v2", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_device_of_match);

/*
 * Reading properties
 */

/* Read u32 property */
int of_property_read_u32(const struct device_node *np,
                        const char *propname,
                        u32 *out_value);

/* Read string property */
int of_property_read_string(const struct device_node *np,
                            const char *propname,
                            const char **out_string);

/* Read array */
int of_property_read_u32_array(const struct device_node *np,
                               const char *propname,
                               u32 *out_values,
                               size_t sz);

/* Check if property exists */
bool of_property_read_bool(const struct device_node *np,
                          const char *propname);

/* Get IRQ from device tree */
int of_irq_get(struct device_node *np, int index);
int platform_get_irq(struct platform_device *pdev, unsigned int num);

Writing Platform Drivers

Complete Platform Driver Example

/*
 * my_platform_driver.c - Complete platform driver example
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/clk.h>

/* Device registers */
#define REG_CONTROL     0x00
#define REG_STATUS      0x04
#define REG_DATA        0x08
#define REG_IRQ_MASK    0x0C

/* Device private data */
struct my_device {
    struct device *dev;
    void __iomem *regs;         /* Mapped registers */
    int irq;
    struct clk *clk;
    u32 max_speed;
    u32 num_channels;
    spinlock_t lock;
};

/*
 * Device tree matching table
 */
static const struct of_device_id my_device_of_match[] = {
    {
        .compatible = "vendor,my-device-v1",
        .data = (void *)1,  /* Version 1 */
    },
    {
        .compatible = "vendor,my-device-v2",
        .data = (void *)2,  /* Version 2 */
    },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_device_of_match);

/*
 * Interrupt handler
 */
static irqreturn_t my_device_irq(int irq, void *dev_id)
{
    struct my_device *mydev = dev_id;
    u32 status;
    
    /* Read interrupt status */
    status = readl(mydev->regs + REG_STATUS);
    
    /* Handle interrupt */
    dev_dbg(mydev->dev, "IRQ: status=0x%08x\n", status);
    
    /* Clear interrupt */
    writel(status, mydev->regs + REG_STATUS);
    
    return IRQ_HANDLED;
}

/*
 * Parse device tree properties
 */
static int my_device_parse_dt(struct my_device *mydev)
{
    struct device *dev = mydev->dev;
    struct device_node *np = dev->of_node;
    int ret;
    
    /* Read custom properties */
    ret = of_property_read_u32(np, "vendor,max-speed", &mydev->max_speed);
    if (ret) {
        dev_err(dev, "Failed to read max-speed property\n");
        return ret;
    }
    
    ret = of_property_read_u32(np, "vendor,num-channels", &mydev->num_channels);
    if (ret) {
        dev_warn(dev, "num-channels not specified, using default\n");
        mydev->num_channels = 1;  /* Default */
    }
    
    dev_info(dev, "DT: max_speed=%u, num_channels=%u\n",
             mydev->max_speed, mydev->num_channels);
    
    return 0;
}

/*
 * Probe function - Called when device is matched
 */
static int my_device_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_device *mydev;
    struct resource *res;
    const struct of_device_id *match;
    int ret;
    
    dev_info(dev, "Probing device\n");
    
    /* Allocate device structure */
    mydev = devm_kzalloc(dev, sizeof(*mydev), GFP_KERNEL);
    if (!mydev)
        return -ENOMEM;
    
    mydev->dev = dev;
    spin_lock_init(&mydev->lock);
    
    /* Get device tree match */
    match = of_match_device(my_device_of_match, dev);
    if (match) {
        unsigned long version = (unsigned long)match->data;
        dev_info(dev, "Matched device version %lu\n", version);
    }
    
    /* Parse device tree */
    if (dev->of_node) {
        ret = my_device_parse_dt(mydev);
        if (ret)
            return ret;
    }
    
    /* Get MMIO resource */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(dev, "Failed to get MMIO resource\n");
        return -ENODEV;
    }
    
    dev_info(dev, "MMIO: start=0x%llx, size=0x%llx\n",
             (unsigned long long)res->start,
             (unsigned long long)resource_size(res));
    
    /* Map registers */
    mydev->regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(mydev->regs)) {
        dev_err(dev, "Failed to map registers\n");
        return PTR_ERR(mydev->regs);
    }
    
    /* Get IRQ */
    mydev->irq = platform_get_irq(pdev, 0);
    if (mydev->irq < 0) {
        dev_err(dev, "Failed to get IRQ\n");
        return mydev->irq;
    }
    
    dev_info(dev, "IRQ: %d\n", mydev->irq);
    
    /* Request IRQ */
    ret = devm_request_irq(dev, mydev->irq, my_device_irq,
                          0, dev_name(dev), mydev);
    if (ret) {
        dev_err(dev, "Failed to request IRQ\n");
        return ret;
    }
    
    /* Get clock */
    mydev->clk = devm_clk_get(dev, "bus_clk");
    if (IS_ERR(mydev->clk)) {
        dev_err(dev, "Failed to get clock\n");
        return PTR_ERR(mydev->clk);
    }
    
    /* Enable clock */
    ret = clk_prepare_enable(mydev->clk);
    if (ret) {
        dev_err(dev, "Failed to enable clock\n");
        return ret;
    }
    
    dev_info(dev, "Clock rate: %lu Hz\n", clk_get_rate(mydev->clk));
    
    /* Initialize hardware */
    writel(0, mydev->regs + REG_CONTROL);
    writel(0xFFFFFFFF, mydev->regs + REG_STATUS);
    writel(0xFFFFFFFF, mydev->regs + REG_IRQ_MASK);
    
    /* Save private data */
    platform_set_drvdata(pdev, mydev);
    
    dev_info(dev, "Probe successful\n");
    
    return 0;
}

/*
 * Remove function - Called when device is removed
 */
static int my_device_remove(struct platform_device *pdev)
{
    struct my_device *mydev = platform_get_drvdata(pdev);
    
    dev_info(mydev->dev, "Removing device\n");
    
    /* Disable interrupts */
    writel(0, mydev->regs + REG_IRQ_MASK);
    
    /* Disable clock */
    clk_disable_unprepare(mydev->clk);
    
    /* devm_* resources are automatically freed */
    
    dev_info(mydev->dev, "Device removed\n");
    
    return 0;
}

/*
 * Platform driver structure
 */
static struct platform_driver my_platform_driver = {
    .probe  = my_device_probe,
    .remove = my_device_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_device_of_match,
    },
};

/*
 * Module registration
 */
module_platform_driver(my_platform_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tutorial Author");
MODULE_DESCRIPTION("Example platform device driver");
MODULE_ALIAS("platform:my-device");

Platform Resources

Accessing Resources

/*
 * platform_get_resource - Get resource by type and index
 */
struct resource *platform_get_resource(struct platform_device *pdev,
                                      unsigned int type,
                                      unsigned int num);

/* Get MMIO resource */
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

/* Get IRQ resource */
struct resource *res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);

/*
 * platform_get_irq - Get IRQ number
 */
int irq = platform_get_irq(pdev, 0);

/*
 * Managed resource mapping (automatic cleanup)
 */

/* Map MMIO region */
void __iomem *base = devm_ioremap_resource(dev, res);

/* Map by physical address */
void __iomem *base = devm_ioremap(dev, phys_addr, size);

/* Request memory region and map */
void __iomem *base = devm_ioremap_resource(dev, res);

/*
 * Reading/Writing MMIO registers
 */
#include <linux/io.h>

u32 readl(const volatile void __iomem *addr);
void writel(u32 val, volatile void __iomem *addr);

u16 readw(const volatile void __iomem *addr);
void writew(u16 val, volatile void __iomem *addr);

u8 readb(const volatile void __iomem *addr);
void writeb(u8 val, volatile void __iomem *addr);

/* Relaxed versions (no memory barriers) */
u32 readl_relaxed(const volatile void __iomem *addr);
void writel_relaxed(u32 val, volatile void __iomem *addr);

Power Management

Suspend and Resume

/*
 * Power management support
 */

#ifdef CONFIG_PM_SLEEP
static int my_device_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    
    dev_info(dev, "Suspending device\n");
    
    /* Disable interrupts */
    writel(0, mydev->regs + REG_IRQ_MASK);
    
    /* Save device state if needed */
    
    /* Disable clock */
    clk_disable_unprepare(mydev->clk);
    
    return 0;
}

static int my_device_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);
    int ret;
    
    dev_info(dev, "Resuming device\n");
    
    /* Enable clock */
    ret = clk_prepare_enable(mydev->clk);
    if (ret)
        return ret;
    
    /* Restore device state if needed */
    
    /* Re-enable interrupts */
    writel(0xFFFFFFFF, mydev->regs + REG_IRQ_MASK);
    
    return 0;
}
#endif

static const struct dev_pm_ops my_device_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(my_device_suspend, my_device_resume)
};

/* Add to platform_driver */
static struct platform_driver my_platform_driver = {
    .driver = {
        .pm = &my_device_pm_ops,
        /* ... */
    },
};

Summary

Platform drivers provide access to SoC-integrated devices using device tree or platform data.

Next Steps

Proceed to 09-block-drivers.md