Tag: Linux device driver development

  • How to Write a Network Driver in Linux: A Beginner’s Complete Guide

    Learn how to write a network driver in Linux from scratch. This beginner-friendly guide covers kernel modules, net_device, sk_buff, ndo operations, and everything you need to build your first Linux network driver.

    If you’ve ever wondered what happens between the moment your application sends data and the moment it actually leaves your NIC (network interface card), you’re asking the right question. The answer lives inside the Linux kernel, specifically inside a network driver in Linux.

    Writing one from scratch sounds scary. It’s not. It’s actually one of the most satisfying things you can do as a Linux developer once you understand the moving parts. So let’s walk through it together, step by step, the way a senior kernel developer would explain it to you over coffee.

    What Is a Network Driver in Linux, Really?

    A Linux network driver is a piece of kernel code that acts as the translator between the Linux networking stack and your actual hardware (or virtual hardware). Think of it as a contract: the kernel says “I need you to send this data,” and your driver figures out how to talk to the hardware to make that happen.

    Unlike a userspace program, a Linux driver lives inside the kernel. That means it runs with full hardware access, no memory protection between itself and the kernel, and absolutely zero tolerance for bugs. One NULL pointer dereference and the whole system panics.

    That’s part of what makes Linux kernel module programming exciting, and also why understanding the fundamentals before writing a single line is worth your time.

    Types of Devices in the Linux Kernel

    Before writing anything, you should understand where network drivers sit in the kernel hierarchy.

    The Linux kernel organizes drivers into three main categories:

    Character devices handle data as a stream of bytes, like a serial port or a keyboard. Block devices deal with fixed-size blocks of data, like hard drives. Network devices are completely different from both.

    Network devices don’t have a corresponding file in /dev. You don’t open() or read() them like character devices. Instead, the Linux networking subsystem talks to them through a well-defined set of operations registered via a structure called net_device. That’s the core abstraction you need to understand for Linux network interface driver development.

    The Big Picture: How Linux Network Driver Development Works

    Here’s the high-level flow before we write a single line of code:

    1. You write a kernel module that registers itself with the networking subsystem.
    2. That module allocates and configures a net_device structure.
    3. You fill in the network device operations (the net_device_ops struct) that tell the kernel how to use your driver.
    4. The kernel’s networking stack calls those operations whenever it needs to send or receive data.
    5. On receive, your driver hands a packet up to the kernel using an sk_buff (socket buffer).
    6. On transmit, the kernel hands your driver an sk_buff and says “send this.”

    That’s the whole model. Everything else is implementation details.

    Setting Up Your Linux Driver Development Environment

    You’ll need the right tools before you start writing your Linux network driver example. Here’s what to set up on a typical Ubuntu or Debian system:

    sudo apt update
    sudo apt install build-essential linux-headers-$(uname -r) git
    

    The linux-headers package gives you the kernel header files you need to compile kernel modules. Always match them to your running kernel version using uname -r.

    Create a working directory:

    mkdir ~/mynetdrv && cd ~/mynetdrv
    

    You’ll need two files to get started: your driver source file (let’s call it mynetdrv.c) and a Makefile.

    Writing Your First Linux Kernel Module

    Every kernel module starts with two functions: module_init() and module_exit(). These are the entry and exit points, similar to main() in a regular C program but for kernel space.

    Here’s the skeleton:

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/init.h>
    #include <linux/netdevice.h>
    #include <linux/etherdevice.h>
    #include <linux/skbuff.h>
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Your Name");
    MODULE_DESCRIPTION("A simple virtual network driver");
    
    static int __init mynetdrv_init(void)
    {
        printk(KERN_INFO "mynetdrv: loaded\n");
        return 0;
    }
    
    static void __exit mynetdrv_exit(void)
    {
        printk(KERN_INFO "mynetdrv: unloaded\n");
    }
    
    module_init(mynetdrv_init);
    module_exit(mynetdrv_exit);
    

    This compiles and loads cleanly but does nothing useful yet. Let’s add the actual network device.

    Understanding the net_device Structure

    The net_device structure is the heart of Linux network driver development. It’s a massive struct defined in <linux/netdevice.h> that holds everything the kernel needs to know about your network interface: its name, its MAC address, its MTU, its statistics, and most importantly, a pointer to your net_device_ops.

    You don’t allocate net_device directly with kmalloc. You use alloc_netdev() or, for Ethernet specifically, alloc_etherdev(). These functions allocate the structure properly and set up sensible defaults.

    static struct net_device *mydev;
    
    mydev = alloc_etherdev(sizeof(struct mynetdrv_priv));
    if (!mydev) {
        printk(KERN_ERR "mynetdrv: alloc_etherdev failed\n");
        return -ENOMEM;
    }
    

    The sizeof(struct mynetdrv_priv) argument lets you piggyback your own private data right after the net_device in memory. You retrieve it later using netdev_priv(dev).

    Defining Your Private Driver Data

    Almost every real-world driver needs to track its own state, things like hardware registers, spin locks, statistics counters, or DMA buffers. This is where your private structure comes in:

    struct mynetdrv_priv {
        struct net_device_stats stats;
        spinlock_t lock;
        /* Add hardware-specific fields here */
    };
    

    You initialize this after alloc_etherdev():

    struct mynetdrv_priv *priv = netdev_priv(mydev);
    memset(priv, 0, sizeof(struct mynetdrv_priv));
    spin_lock_init(&priv->lock);
    

    Filling in the net_device_ops

    This is where you define what your driver can actually do. The net_device_ops structure contains function pointers for every operation the kernel might call on your device. For a minimal network driver in Linux, you need at least these:

    static const struct net_device_ops mynetdrv_ops = {
        .ndo_open        = mynetdrv_open,
        .ndo_stop        = mynetdrv_stop,
        .ndo_start_xmit  = mynetdrv_tx,
        .ndo_get_stats   = mynetdrv_stats,
    };
    

    Let’s implement each one.

    Implementing ndo_open and ndo_stop

    ndo_open is called when someone runs ifconfig mydev0 up or ip link set mydev0 up. This is where you allocate hardware resources, enable interrupts, and tell the kernel the device is ready to transmit.

    static int mynetdrv_open(struct net_device *dev)
    {
        /* In a real driver: enable hardware, request IRQ, start DMA */
        netif_start_queue(dev);
        printk(KERN_INFO "mynetdrv: interface opened\n");
        return 0;
    }
    

    netif_start_queue() tells the networking subsystem that this device is ready to accept outgoing packets. Without this call, the kernel won’t pass anything to your ndo_start_xmit.

    ndo_stop is the reverse. It’s called when the interface goes down:

    static int mynetdrv_stop(struct net_device *dev)
    {
        netif_stop_queue(dev);
        /* In a real driver: disable hardware, free IRQ, stop DMA */
        printk(KERN_INFO "mynetdrv: interface stopped\n");
        return 0;
    }
    

    The Most Important Function: ndo_start_xmit

    If there’s one function in Linux network driver development you need to truly understand, it’s ndo_start_xmit. This is what the kernel calls every time it wants to send a packet through your interface.

    The function receives a pointer to an sk_buff (socket buffer) and the net_device. Your job is to take that packet, do whatever hardware-specific magic is needed to transmit it, and return a status code.

    static netdev_tx_t mynetdrv_tx(struct sk_buff *skb, struct net_device *dev)
    {
        struct mynetdrv_priv *priv = netdev_priv(dev);
        int len = skb->len;
    
        /* For a loopback/virtual driver: just receive the packet back */
        /* In a real driver: write to hardware TX ring, trigger DMA */
    
        priv->stats.tx_packets++;
        priv->stats.tx_bytes += len;
    
        /* Free the socket buffer when done */
        dev_kfree_skb(skb);
    
        return NETDEV_TX_OK;
    }
    

    For a virtual or loopback driver, you can simulate reception by calling netif_rx() with a copy of the packet. For a real hardware driver, this is where you’d write to your hardware’s transmit ring buffer and fire off the DMA engine.

    Understanding sk_buff: The Linux Socket Buffer

    The sk_buff structure (socket buffer) is how the Linux networking stack passes packets between layers. It’s one of the most important data structures in the entire Linux networking subsystem. Understanding it is non-negotiable for serious Linux kernel networking work.

    Key fields you’ll use:

    • skb->data — pointer to the start of the packet data
    • skb->len — total length of the packet
    • skb->head and skb->end — boundaries of the buffer
    • skb->next / skb->prev — for when packets are queued

    When receiving a packet from hardware, you allocate a new sk_buff using dev_alloc_skb(), copy your packet data in, and hand it to the kernel with netif_rx() or napi_gro_receive().

    /* Simulated receive path */
    static void mynetdrv_rx(struct net_device *dev, int len, unsigned char *buf)
    {
        struct sk_buff *skb;
        struct mynetdrv_priv *priv = netdev_priv(dev);
    
        skb = dev_alloc_skb(len + 2);
        if (!skb) {
            priv->stats.rx_dropped++;
            return;
        }
    
        skb_reserve(skb, 2); /* align IP header on 16-byte boundary */
        memcpy(skb_put(skb, len), buf, len);
    
        skb->dev = dev;
        skb->protocol = eth_type_trans(skb, dev);
        skb->ip_summed = CHECKSUM_UNNECESSARY;
    
        priv->stats.rx_packets++;
        priv->stats.rx_bytes += len;
    
        netif_rx(skb);
    }
    

    Getting Statistics Back to the Kernel

    The ndo_get_stats function is simple but important. This is what populates the output when you run ifconfig or ip -s link:

    static struct net_device_stats *mynetdrv_stats(struct net_device *dev)
    {
        struct mynetdrv_priv *priv = netdev_priv(dev);
        return &priv->stats;
    }
    

    Always keep your TX/RX packet and byte counters accurate. Tools like ethtool, iftop, and monitoring systems like Prometheus depend on these numbers being right.

    Registering and Unregistering the Device

    Once all your operations are set up, you wire everything together in module_init and register the device with the kernel using register_netdev():

    static int __init mynetdrv_init(void)
    {
        int ret;
    
        mydev = alloc_etherdev(sizeof(struct mynetdrv_priv));
        if (!mydev)
            return -ENOMEM;
    
        /* Set a fake MAC address */
        eth_hw_addr_random(mydev);
    
        mydev->netdev_ops = &mynetdrv_ops;
        strncpy(mydev->name, "mydev%d", IFNAMSIZ);
    
        ret = register_netdev(mydev);
        if (ret) {
            printk(KERN_ERR "mynetdrv: register_netdev failed: %d\n", ret);
            free_netdev(mydev);
            return ret;
        }
    
        printk(KERN_INFO "mynetdrv: registered as %s\n", mydev->name);
        return 0;
    }
    
    static void __exit mynetdrv_exit(void)
    {
        unregister_netdev(mydev);
        free_netdev(mydev);
        printk(KERN_INFO "mynetdrv: unregistered\n");
    }
    

    register_netdev() is what makes your driver visible to the system. After this call succeeds, you’ll see the interface when you run ip link show.

    Writing the Makefile

    Compiling a Linux kernel module requires a specific Makefile format that hands control off to the kernel build system (kbuild):

    obj-m += mynetdrv.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    

    Build and load it:

    make
    sudo insmod mynetdrv.ko
    dmesg | tail -10
    ip link show
    

    You should see your new interface mydev0 appear in the output of ip link show. To remove it:

    sudo rmmod mynetdrv
    

    Interrupt Handling in Real Hardware Drivers

    A virtual driver gets away without interrupts, but a real Linux device driver development project requires an interrupt handler. When hardware finishes sending or receives a new packet, it fires an interrupt to tell the CPU.

    You request an IRQ line in ndo_open:

    ret = request_irq(dev->irq, mynetdrv_interrupt, IRQF_SHARED, dev->name, dev);
    

    And release it in ndo_stop:

    free_irq(dev->irq, dev);
    

    Your interrupt handler processes the event and schedules NAPI polling (more on that below) or directly calls netif_rx() if you’re on a simple non-NAPI design.

    NAPI: The Modern Way to Handle Receive in Linux Drivers

    If you’re writing a high-performance Linux network interface driver, you’ll want to use NAPI (New API). NAPI is a hybrid interrupt/polling mechanism that dramatically reduces interrupt overhead under high network load.

    Here’s how it works: when a packet arrives, your interrupt handler disables further RX interrupts and schedules a NAPI poll. The kernel then calls your poll function in a softirq context to drain as many packets as possible from the hardware ring buffer. When the ring is empty, you re-enable interrupts.

    /* In your private struct */
    struct napi_struct napi;
    
    /* During init */
    netif_napi_add(dev, &priv->napi, mynetdrv_poll, 64);
    
    /* In interrupt handler */
    napi_schedule(&priv->napi);
    
    /* Your poll function */
    static int mynetdrv_poll(struct napi_struct *napi, int budget)
    {
        struct mynetdrv_priv *priv = container_of(napi, struct mynetdrv_priv, napi);
        int work_done = 0;
    
        while (work_done < budget && /* packets available */) {
            /* process one packet */
            work_done++;
        }
    
        if (work_done < budget) {
            napi_complete_done(napi, work_done);
            /* re-enable hardware RX interrupts */
        }
    
        return work_done;
    }
    

    NAPI is used by virtually every production-quality Linux network driver today, from Intel’s igb to Broadcom’s bnxt_en.

    Debugging Your Linux Network Driver

    Debugging kernel code is different from debugging userspace apps. You can’t attach GDB directly (well, you can with KGDB but it’s complex). Your main tools are:

    printk / pr_info / netdev_info — Use these liberally during development. netdev_info(dev, "message\n") is preferred because it automatically prefixes your message with the interface name.

    dmesg — This is where your printk messages land. Use dmesg -w to watch in real time.

    dynamic debug — The kernel has a powerful mechanism to enable/disable debug messages at runtime without recompiling. Add pr_debug() calls and enable them with:

    echo "module mynetdrv +p" > /sys/kernel/debug/dynamic_debug/control
    

    QEMU + KVM — Seriously, do all your early development inside a VM. Crashing a virtual machine is painless. Crashing your workstation mid-session is not.

    addr2line — When you get an Oops in dmesg, the stack trace contains hex addresses. addr2line turns those back into file names and line numbers in your source.

    Common Mistakes Beginners Make

    Forgetting to free the sk_buff. If your transmit path doesn’t call dev_kfree_skb(skb), you leak memory on every sent packet. The system will eventually OOM.

    Not calling netif_start_queue. Your ndo_start_xmit will never be called if you forget this in ndo_open.

    Sleeping in interrupt context. You cannot call any function that might sleep (like kmalloc with GFP_KERNEL, mutex_lock, etc.) from an interrupt handler or softirq. Use GFP_ATOMIC for allocations in those contexts.

    Incorrect locking. The networking subsystem makes calls to your driver from multiple contexts. Use spin locks (not mutexes) for protecting shared state in paths that can run in interrupt context.

    Not handling errors. alloc_etherdev, register_netdev, request_irq — all of these can fail. Always check return values and unwind cleanly on failure.

    Real-World Linux Network Driver Examples to Study

    Once you have the basics down, the best way to level up is reading production drivers in the kernel source. Start with these:

    drivers/net/loopback.c — The Linux loopback driver. Tiny, clean, and a perfect reference for virtual devices.

    drivers/net/virtio_net.c — The VirtIO network driver used in QEMU/KVM virtual machines. Great for understanding the full NAPI receive path.

    drivers/net/ethernet/intel/e1000/ — Intel’s classic Gigabit Ethernet driver. Older but very well documented and readable.

    drivers/net/tun.c — The TUN/TAP driver used by VPNs and virtual networks. Shows how to bridge kernel and userspace cleanly.

    You can browse all of these at https://elixir.bootlin.com/linux/latest/source, which is the cross-referenced Linux kernel source. It’s an invaluable resource for Linux kernel module programming.

    Where to Go From Here

    Writing a network driver in Linux gives you a perspective on the system that most developers never get. You stop thinking about the network as a black box and start seeing it for what it is: a chain of well-defined interfaces, from your physical NIC all the way up to your application’s socket.

    Once you’re comfortable with the basics covered here, the natural next steps are:

    • Learn ethtool support so users can query driver settings from userspace
    • Add netpoll support for use with network console and net dumping
    • Study XDP (eXpress Data Path) for high-performance packet processing that bypasses the normal networking stack
    • Understand device tree bindings if you’re writing drivers for embedded ARM boards
    • Look into PCIe driver initialization using pci_register_driver() for real hardware drivers

    The Linux kernel documentation at https://www.kernel.org/doc/html/latest/networking/index.html is solid and worth bookmarking. The book Linux Device Drivers by Corbet, Rubini, and Kroah-Hartman (available free at lwn.net) is the canonical reference.

    Summary

    Here’s everything we covered in one place:

    • A network driver in Linux connects the kernel’s networking stack to your hardware
    • Use alloc_etherdev() to allocate a net_device, not raw kmalloc
    • Fill in net_device_ops with at minimum ndo_open, ndo_stop, ndo_start_xmit, and ndo_get_stats
    • sk_buff (socket buffer) is how packets move through the kernel — know its key fields
    • Use register_netdev() to make your interface visible to the system
    • For real hardware, request an IRQ in ndo_open and use NAPI for receive processing
    • Study loopback.c and virtio_net.c as clean reference implementations
    • Always develop inside a VM to keep your main system safe

    The Linux networking stack is one of the best-designed subsystems in the entire kernel. Once you’ve written a driver that registers an interface, sends and receives real packets, and handles errors cleanly, you’ll have a deep appreciation for why Linux powers everything from smartphones to the largest data centers in the world.

    Go build something.

    FAQ: How to Write a Network Driver in Linux

    Q1. What is a network driver in Linux?

    A network driver in Linux is a piece of kernel code that sits between the Linux networking stack and your physical or virtual hardware. It translates high-level kernel requests like “send this packet” into hardware-specific instructions. Unlike character or block device drivers, network drivers don’t appear as files in /dev. Instead they register a net_device structure with the kernel and expose a set of operations the networking subsystem calls directly.

    Q2. Do I need to know C to write a Linux network driver?

    Yes. Linux kernel development is done entirely in C, specifically a subset of C that avoids certain features like floating point and standard library functions. You don’t need to be a C expert to get started, but you should be comfortable with pointers, structs, function pointers, and memory management. If you can write a linked list implementation in C from scratch, you have enough foundation to follow along.

    Q3. What is the net_device structure in Linux?

    The net_device structure is the central data structure for any Linux network interface driver. It holds everything the kernel needs to know about your network interface including its name, MAC address, MTU, flags, and a pointer to your net_device_ops. You never allocate it directly with malloc or kmalloc. Instead you use alloc_etherdev() for Ethernet devices or alloc_netdev() for other types. After filling it in, you register it with register_netdev().

    Q4. What is sk_buff in Linux and why does it matter?

    sk_buff stands for socket buffer. It is the data structure the Linux networking stack uses to pass packets between layers, from your application all the way down to the driver and back up again. When you receive a packet from hardware, you allocate an sk_buff, copy the data in, set the protocol, and hand it to the kernel via netif_rx(). When the kernel wants to send a packet, it passes you an sk_buff in your ndo_start_xmit function. Understanding skb->data, skb->len, skb_put(), and skb_reserve() is essential for any real network driver work.

    Q5. What is ndo_start_xmit and how does it work?

    ndo_start_xmit is the transmit function in your net_device_ops. The kernel calls it every time it wants your driver to send a packet. It receives a pointer to an sk_buff containing the packet data and a pointer to the net_device. Your job is to take that data, push it to the hardware transmit ring or buffer, update your TX statistics, free the sk_buff with dev_kfree_skb(), and return NETDEV_TX_OK. If your hardware buffer is full you can return NETDEV_TX_BUSY, but you must also call netif_stop_queue() before returning and wake the queue back up with netif_wake_queue() once the hardware has room again.

    Q6. What is the difference between netif_rx and napi_gro_receive?

    netif_rx() is the older, simpler way to pass a received packet up to the kernel. You call it directly from your interrupt handler or any context where you have a ready sk_buff. It works fine for low-traffic or virtual drivers. napi_gro_receive() is the modern approach used with NAPI. GRO stands for Generic Receive Offload. It allows the kernel to coalesce multiple small TCP segments into fewer larger ones before passing them up the stack, which reduces CPU overhead significantly at high packet rates. For production hardware drivers, napi_gro_receive() is the right choice.

    Q7. What is NAPI and should I use it in my driver?

    NAPI stands for New API. It is a hybrid interrupt-driven and polling mechanism designed to improve network performance under high load. Without NAPI, every incoming packet triggers a hardware interrupt. At high packet rates this causes interrupt storms that can saturate the CPU. With NAPI, your interrupt handler fires once to signal that packets are available, then disables further RX interrupts and schedules a polling function. The kernel calls your poll function in a softirq context to drain as many packets as possible up to a configurable budget. When the ring is empty you re-enable interrupts. For any driver that will handle real traffic loads, yes, you should use NAPI.

    Q8. How do I compile a Linux kernel module?

    You need the linux-headers package matching your running kernel, which you get with sudo apt install linux-headers-$(uname -r) on Debian-based systems. Your Makefile should use the kbuild system like this: obj-m += yourdriver.o and then call make with the -C flag pointing at /lib/modules/$(shell uname -r)/build. Run make to build, sudo insmod yourdriver.ko to load, and sudo rmmod yourdriver to unload. Use dmesg to see your printk output.

    Q9. How do I debug a Linux network driver?

    Your main tool is printk or the preferred wrapper netdev_info(dev, “message”). All output goes to the kernel ring buffer which you read with dmesg or dmesg -w for live output. For more control, use pr_debug() calls combined with the dynamic debug system, which lets you enable and disable specific debug messages at runtime without recompiling. For serious development, run everything inside a QEMU virtual machine so kernel panics don’t affect your host system. When you do get an Oops, the stack trace in dmesg contains hex addresses you can decode back to source lines using addr2line against your compiled module.

    Q10. Can I write a network driver without real hardware?

    Yes, and it is actually the best way to learn. You can write a fully functional virtual network driver that creates a real interface visible to tools like ip link and ifconfig, sends and receives packets, and maintains proper statistics, all without touching any hardware registers. The Linux loopback driver in drivers/net/loopback.c is a great example of this. The TUN/TAP driver in drivers/net/tun.c is another one. These are legitimate kernel drivers used in production and they work entirely in software.

    Q11. What is register_netdev and when do I call it?

    register_netdev() is the function that makes your network interface visible to the rest of the Linux system. You call it after you have allocated your net_device with alloc_etherdev(), set the netdev_ops pointer, configured the interface name, and set the MAC address. Once register_netdev() returns successfully, the interface will appear in ip link show and users can bring it up. You must call unregister_netdev() in your cleanup path before calling free_netdev(), otherwise you will leak the device or cause a kernel warning.

    Q12. What is the difference between alloc_netdev and alloc_etherdev?

    alloc_etherdev() is just a convenience wrapper around alloc_netdev() that additionally sets up Ethernet-specific defaults like the header operations, the hardware header length, and the address length. If you are writing an Ethernet driver, use alloc_etherdev(). If you are writing a non-Ethernet driver such as a point-to-point link or a tunnel, use alloc_netdev() directly and configure the type-specific fields yourself.

    Q13. How do I handle interrupts in a network driver?

    In your ndo_open function, call request_irq() with your interrupt number, your handler function, the IRQF_SHARED flag if needed, your driver name, and a pointer to your net_device as the dev_id. Your interrupt handler should be fast. It should determine if the interrupt was from your hardware, acknowledge the interrupt to clear it, and then either directly call netif_rx() for simple designs or schedule NAPI polling for production designs. Release the IRQ in your ndo_stop function using free_irq().

    Q14. What are the best Linux network driver examples to study?

    Start with drivers/net/loopback.c because it is tiny and clean. Then look at drivers/net/virtio_net.c which is a full NAPI-based virtual Ethernet driver used in QEMU and KVM virtual machines. For real hardware, the Intel e1000 driver in drivers/net/ethernet/intel/e1000/ is well documented and widely studied. The TUN driver at drivers/net/tun.c is excellent for understanding userspace-kernel bridging. You can browse all of these online at elixir.bootlin.com with full cross-referencing.

    Q15. Is it safe to develop kernel drivers on my main machine?

    No, not when you are learning. A bug in kernel code can panic the entire system instantly. Always use a virtual machine for development. QEMU with KVM works great and you can snapshot your VM state before loading any new module. Once your driver is stable and well-tested, then you can consider running it on real hardware. Many developers keep a dedicated test machine for that purpose.

    Next Steps: Check out our guides on SPI driver development, platform device drivers, and writing device tree overlays on embeddedprep.com/.

  • Master How to Write Platform Devices and Drivers (2026)

    Learn how to write Platform Devices and Drivers from scratch using Device Tree, I2C, interrupts, and testing, explained clearly for beginners.

    If you are stepping into Linux device driver development, sooner or later you will hear one phrase again and again: Platform Devices and Drivers.
    They sound complex at first, but once you understand the flow, they are actually one of the cleanest and most structured ways to write Linux drivers for embedded systems.

    In this article, we will go from zero to a working platform driver.
    We will start with the basic idea, then move step by step through implementation, device tree integration, I2C communication, interrupt handling, and finally testing and debugging.

    What Is Platform Devices and Drivers?

    Platform Devices and Drivers are a Linux kernel mechanism used to describe and manage on-board hardware that is not discoverable dynamically.

    Unlike USB or PCI devices, platform devices do not announce themselves.
    They are usually fixed on the board, soldered directly to the SoC or connected via internal buses like I2C or SPI.

    Examples include:

    • GPIO controllers
    • I2C sensors
    • RTC chips
    • Watchdog timers
    • Custom hardware blocks

    Linux needs a way to:

    1. Describe this hardware
    2. Match it with the correct driver
    3. Initialize it safely during boot

    That is exactly what platform devices and drivers do.

    Why Platform Devices and Drivers Matter in Embedded Linux

    If you work with embedded boards, you will almost always use Platform Devices and Drivers because:

    • Hardware is board-specific
    • There is no auto-detection
    • Device Tree is used to describe hardware
    • Drivers must bind cleanly during boot

    This model separates hardware description (Device Tree) from driver logic (kernel module), which makes systems easier to maintain and port.

    High-Level Architecture

    Before writing any code, let’s understand the flow:

    1. You buy an I2C chip (for example, a temperature sensor)
    2. You connect it to your board
    3. You describe it in the Device Tree
    4. Linux creates a platform device
    5. Your platform driver registers itself
    6. Kernel matches device and driver
    7. probe() function runs
    8. Driver initializes hardware
    9. Device becomes usable from user space

    Once you understand this flow, everything else fits naturally.

    From Purchase to Product: Real-World Scenario

    Let’s make this real.

    Imagine you purchased an I2C temperature sensor that:

    • Works over I2C
    • Has an interrupt pin
    • Is connected to your SoC

    Your goal:

    • Write a Linux driver
    • Handle I2C communication
    • Handle interrupts
    • Expose data to user space

    We will build this using Platform Devices and Drivers.

    Step 1: Understand the Hardware

    Before touching the kernel:

    • Read the datasheet
    • Note the I2C address
    • Register map
    • Interrupt behavior
    • Voltage levels

    Example details:

    • I2C address: 0x48
    • Interrupt active low
    • Data register at 0x00

    Never skip this step. Drivers written without understanding hardware always fail.

    Step 2: Device Tree Basics

    In modern Linux, Device Tree describes platform devices.

    The Device Tree tells the kernel:

    • Where the device is
    • How it is connected
    • What driver should bind to it

    Example Device Tree Node

    i2c1 {
        status = "okay";
    
        temp_sensor@48 {
            compatible = "vendor,temp-sensor";
            reg = <0x48>;
            interrupt-parent = <&gpio1>;
            interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
        };
    };
    

    Important properties:

    • compatible → used for driver matching
    • reg → I2C address
    • interrupts → interrupt configuration

    This node becomes a platform device internally.

    Step 3: Platform Driver Structure

    A platform driver is a kernel module that registers itself using platform_driver.

    Basic Skeleton

    static int temp_probe(struct platform_device *pdev)
    {
        return 0;
    }
    
    static int temp_remove(struct platform_device *pdev)
    {
        return 0;
    }
    
    static const struct of_device_id temp_of_match[] = {
        { .compatible = "vendor,temp-sensor" },
        { }
    };
    MODULE_DEVICE_TABLE(of, temp_of_match);
    
    static struct platform_driver temp_driver = {
        .probe = temp_probe,
        .remove = temp_remove,
        .driver = {
            .name = "temp_sensor",
            .of_match_table = temp_of_match,
        },
    };
    
    module_platform_driver(temp_driver);
    

    This is the heart of Platform Devices and Drivers.

    Step 4: Driver and Device Matching

    Matching happens using the compatible string.

    • Device Tree provides compatible = "vendor,temp-sensor"
    • Driver declares the same string
    • Kernel binds them automatically

    If probe() is not called:

    • Check compatible string
    • Check Device Tree compilation
    • Check kernel logs

    Step 5: Accessing Device Tree Data

    Inside probe(), you can read properties from Device Tree.

    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    

    This lets you:

    • Read GPIO numbers
    • Read custom properties
    • Configure behavior per board

    This is why platform drivers are flexible.

    Step 6: Integrating I2C in Platform Drivers

    Many beginners get confused here.

    Yes, the device is on I2C, but the binding still happens via platform devices when using Device Tree.

    You usually:

    • Get the I2C adapter
    • Create an I2C client
    • Communicate using I2C APIs

    Example

    struct i2c_client *client;
    
    client = i2c_new_dummy_device(adapter, 0x48);
    

    Now you can read registers:

    i2c_smbus_read_byte_data(client, 0x00);
    

    This approach keeps hardware description separate from communication logic.

    Step 7: Interrupt Handling

    Interrupts are critical for real devices.

    Get IRQ Number

    int irq = platform_get_irq(pdev, 0);
    

    Register Interrupt Handler

    request_irq(irq, temp_irq_handler,
                IRQF_TRIGGER_FALLING,
                "temp_irq", dev);
    

    Interrupt Handler

    static irqreturn_t temp_irq_handler(int irq, void *dev_id)
    {
        // Read status register
        return IRQ_HANDLED;
    }
    

    Always keep interrupt handlers short and fast.

    Step 8: Exposing Data to User Space

    A driver is useless if user space cannot talk to it.

    Common approaches:

    • sysfs
    • character device
    • IIO subsystem (for sensors)

    Simple sysfs Example

    static ssize_t temp_show(struct device *dev,
                             struct device_attribute *attr,
                             char *buf)
    {
        return sprintf(buf, "25\n");
    }
    
    DEVICE_ATTR_RO(temp);
    

    This creates:

    /sys/devices/.../temp
    

    Step 9: Error Handling and Cleanup

    Good platform drivers always clean up.

    In remove():

    • Free IRQ
    • Unregister I2C client
    • Free memory
    free_irq(irq, dev);
    

    Never assume probe() always succeeds.

    Step 10: Building and Testing

    Compile the Driver

    • Add to kernel tree or as module
    • Enable in menuconfig

    Load Module

    insmod temp_driver.ko
    

    Check Logs

    dmesg | grep temp
    

    Verify Device Tree

    ls /proc/device-tree
    

    Testing is where most learning happens.

    Common Mistakes Beginners Make

    • Wrong compatible string
    • Forgetting to enable I2C bus
    • Interrupt polarity mismatch
    • Blocking code in IRQ handler
    • Ignoring return values

    Every one of these will cost you hours if you ignore basics.

    How to Write Platform Devices and Drivers

    // SPDX-License-Identifier: GPL-2.0
    /*
     * Production-ready Platform Driver example
     * - Device Tree based
     * - I2C sensor
     * - Interrupt handling
     * - sysfs interface
     */
    
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/platform_device.h>
    #include <linux/of.h>
    #include <linux/of_irq.h>
    #include <linux/i2c.h>
    #include <linux/interrupt.h>
    #include <linux/slab.h>
    #include <linux/mutex.h>
    #include <linux/pm.h>
    #include <linux/sysfs.h>
    
    /* ---------- Driver Private Data ---------- */
    struct temp_dev {
    	struct device       *dev;
    	struct i2c_client   *client;
    	int                  irq;
    	int                  temperature;
    	struct mutex         lock;
    };
    
    /* ---------- I2C Read Helper ---------- */
    static int temp_sensor_read(struct temp_dev *tdev)
    {
    	int ret;
    
    	ret = i2c_smbus_read_byte_data(tdev->client, 0x00);
    	if (ret < 0)
    		dev_err(tdev->dev, "I2C read failed\n");
    
    	return ret;
    }
    
    /* ---------- Interrupt Handler ---------- */
    static irqreturn_t temp_irq_handler(int irq, void *data)
    {
    	struct temp_dev *tdev = data;
    	int temp;
    
    	mutex_lock(&tdev->lock);
    
    	temp = temp_sensor_read(tdev);
    	if (temp >= 0)
    		tdev->temperature = temp;
    
    	mutex_unlock(&tdev->lock);
    
    	dev_info(tdev->dev, "Interrupt: temperature=%d\n",
    		 tdev->temperature);
    
    	return IRQ_HANDLED;
    }
    
    /* ---------- sysfs Interface ---------- */
    static ssize_t temperature_show(struct device *dev,
    				struct device_attribute *attr,
    				char *buf)
    {
    	struct temp_dev *tdev = dev_get_drvdata(dev);
    	int temp;
    
    	mutex_lock(&tdev->lock);
    	temp = tdev->temperature;
    	mutex_unlock(&tdev->lock);
    
    	return sysfs_emit(buf, "%d\n", temp);
    }
    
    static DEVICE_ATTR_RO(temperature);
    
    static struct attribute *temp_attrs[] = {
    	&dev_attr_temperature.attr,
    	NULL,
    };
    
    static const struct attribute_group temp_attr_group = {
    	.attrs = temp_attrs,
    };
    
    /* ---------- Probe Function ---------- */
    static int temp_probe(struct platform_device *pdev)
    {
    	struct temp_dev *tdev;
    	struct i2c_adapter *adapter;
    	struct device *dev = &pdev->dev;
    	int ret;
    
    	dev_info(dev, "Probing temperature sensor\n");
    
    	tdev = devm_kzalloc(dev, sizeof(*tdev), GFP_KERNEL);
    	if (!tdev)
    		return -ENOMEM;
    
    	tdev->dev = dev;
    	mutex_init(&tdev->lock);
    	platform_set_drvdata(pdev, tdev);
    
    	/* Get I2C adapter (from DT bus number) */
    	adapter = i2c_get_adapter(1);
    	if (!adapter) {
    		dev_err(dev, "Failed to get I2C adapter\n");
    		return -ENODEV;
    	}
    
    	/* Create I2C client */
    	tdev->client = i2c_new_dummy_device(adapter, 0x48);
    	i2c_put_adapter(adapter);
    
    	if (IS_ERR(tdev->client)) {
    		dev_err(dev, "Failed to create I2C client\n");
    		return PTR_ERR(tdev->client);
    	}
    
    	/* Read initial value */
    	ret = temp_sensor_read(tdev);
    	if (ret >= 0)
    		tdev->temperature = ret;
    
    	/* Get IRQ from Device Tree */
    	tdev->irq = platform_get_irq(pdev, 0);
    	if (tdev->irq < 0) {
    		dev_err(dev, "Failed to get IRQ\n");
    		return tdev->irq;
    	}
    
    	ret = devm_request_irq(dev,
    			       tdev->irq,
    			       temp_irq_handler,
    			       IRQF_TRIGGER_FALLING,
    			       dev_name(dev),
    			       tdev);
    	if (ret) {
    		dev_err(dev, "Failed to request IRQ\n");
    		return ret;
    	}
    
    	/* Create sysfs group */
    	ret = sysfs_create_group(&dev->kobj, &temp_attr_group);
    	if (ret) {
    		dev_err(dev, "Failed to create sysfs group\n");
    		return ret;
    	}
    
    	dev_info(dev, "Temperature sensor driver loaded\n");
    	return 0;
    }
    
    /* ---------- Remove Function ---------- */
    static int temp_remove(struct platform_device *pdev)
    {
    	struct temp_dev *tdev = platform_get_drvdata(pdev);
    
    	sysfs_remove_group(&pdev->dev.kobj, &temp_attr_group);
    
    	if (tdev->client)
    		i2c_unregister_device(tdev->client);
    
    	dev_info(&pdev->dev, "Temperature sensor removed\n");
    	return 0;
    }
    
    /* ---------- Power Management ---------- */
    #ifdef CONFIG_PM_SLEEP
    static int temp_suspend(struct device *dev)
    {
    	dev_info(dev, "Suspending temperature sensor\n");
    	return 0;
    }
    
    static int temp_resume(struct device *dev)
    {
    	dev_info(dev, "Resuming temperature sensor\n");
    	return 0;
    }
    #endif
    
    static SIMPLE_DEV_PM_OPS(temp_pm_ops,
    			 temp_suspend,
    			 temp_resume);
    
    /* ---------- Device Tree Match ---------- */
    static const struct of_device_id temp_of_match[] = {
    	{ .compatible = "demo,temp-sensor" },
    	{ }
    };
    MODULE_DEVICE_TABLE(of, temp_of_match);
    
    /* ---------- Platform Driver ---------- */
    static struct platform_driver temp_driver = {
    	.probe  = temp_probe,
    	.remove = temp_remove,
    	.driver = {
    		.name           = "temp_sensor",
    		.of_match_table = temp_of_match,
    		.pm             = &temp_pm_ops,
    	},
    };
    
    module_platform_driver(temp_driver);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Nishant Singh");
    MODULE_DESCRIPTION("Production-ready Platform Driver with I2C and Interrupt");
    

    Device Tree

    &i2c1 {
    	status = "okay";
    
    	temp_sensor@48 {
    		compatible = "demo,temp-sensor";
    		reg = <0x48>;
    		interrupt-parent = <&gpio1>;
    		interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
    	};
    };
    

    User Space Test

    cat /sys/devices/platform/temp_sensor/temperature
    

    Why Platform Devices and Drivers Scale Well

    Once you master Platform Devices and Drivers, you can:

    • Port drivers across boards
    • Reuse Device Tree files
    • Support multiple variants
    • Keep drivers clean and readable

    This is why the Linux kernel strongly encourages this model.

    FAQs : Platform Devices and Drivers.

    1. What are Platform Devices and Drivers in Linux?

    Platform Devices and Drivers are a Linux kernel mechanism used for non-discoverable hardware. These are devices that are permanently attached to the board, like I2C sensors, GPIO controllers, RTCs, and on-chip peripherals. Since such hardware cannot announce itself, Linux relies on the Device Tree to describe it and platform drivers to manage it.

    2. Why do we need Platform Drivers when using Device Tree?

    Device Tree only describes hardware.
    Platform drivers contain the actual code that talks to the hardware. The kernel uses the compatible string in the Device Tree to match a platform device with the correct platform driver and then calls the driver’s probe() function.

    3. What is the role of the probe() function?

    The probe() function is where everything starts. It is called when the kernel successfully matches a platform device with its driver. In probe(), you:

    • Allocate memory
    • Read Device Tree properties
    • Initialize hardware
    • Register interrupts
    • Create sysfs or character devices

    If probe() fails, the device will not work.

    4. How does Device Tree match a Platform Driver?

    Matching is done using the compatible string.
    The Device Tree node provides a compatible property, and the platform driver provides an of_device_id table with the same string. If they match exactly, the kernel binds them together.

    5. Can Platform Drivers be used for I2C or SPI devices?

    Yes. This is very common.
    Even though the device communicates over I2C or SPI, the binding still happens through Platform Devices and Drivers when using Device Tree. Inside the platform driver, you create or access an I2C or SPI client to communicate with the hardware.

    6. What is the difference between a Platform Driver and an I2C Driver?

    An I2C driver is tied directly to the I2C subsystem and is usually auto-created by the I2C core.
    A Platform Driver is more generic and is often used when:

    • You need board-specific logic
    • The device uses multiple resources (IRQ, GPIO, regulators)
    • You want tighter control over initialization

    Both approaches are valid and used in production.

    7. How are interrupts handled in Platform Drivers?

    Interrupts are described in the Device Tree using the interrupts property.
    Inside the driver, you retrieve the IRQ number using platform_get_irq() and register a handler using request_irq() or devm_request_irq(). The interrupt handler should be fast and should avoid sleeping or heavy processing.

    8. What is devm_* and why should it be used?

    devm_* APIs automatically free resources when the device is removed.
    They reduce memory leaks and simplify error handling. In production drivers, devm_kzalloc(), devm_request_irq(), and similar APIs are strongly recommended.

    9. How do Platform Drivers expose data to user space?

    Common methods include:

    • sysfs attributes
    • character devices
    • standard kernel subsystems like IIO, input, or hwmon

    For simple data like sensor readings, sysfs is often enough. For high-performance or streaming data, character devices or IIO are better choices.

    10. What are the most common reasons probe() is not called?

    The most common reasons are:

    • Mismatch in compatible string
    • Device Tree node not compiled or loaded
    • Driver not enabled in kernel configuration
    • Incorrect Device Tree hierarchy (wrong bus node)

    Checking dmesg usually points you in the right direction.

    11. Are Platform Drivers used in real production kernels?

    Absolutely.
    Most SoC drivers, board-level drivers, and embedded peripherals in the Linux kernel are implemented using Platform Devices and Drivers. This model scales well and is heavily used by vendors and upstream kernel developers.

    12. When should I avoid using Platform Devices and Drivers?

    You should avoid platform drivers if:

    • The hardware is self-discoverable (USB, PCI)
    • The kernel already provides a dedicated subsystem driver
    • You are writing a pure user-space driver

    In those cases, platform drivers would add unnecessary complexity.

    You can also read : Linked List Coding Questions

  • Platform Devices and Drivers on Linux

    Learn Platform Devices and Drivers in Linux from scratch with clear explanations, device tree examples, driver code, testing steps, and real-world insights.

    If you are getting into Linux device drivers, especially embedded Linux, you will keep hearing one phrase again and again: Platform Devices and Drivers.

    At first, it sounds complicated. Platform bus, platform_device, platform_driver, device tree, probe function, resources, matching. It feels like too many moving parts.

    But once you understand the why behind platform devices and drivers, everything starts to click.

    This guide is written for beginners, but with expert depth, so it also helps during interviews and real projects.

    What Are Platform Devices and Drivers in Linux?

    In simple words:

    Platform Devices and Drivers are used in Linux to handle on-chip hardware that is not discoverable automatically.

    That single sentence explains 80 percent of the concept.

    Unlike PCI or USB devices, many embedded peripherals are:

    • Built directly into the SoC
    • Always present
    • Not hot-pluggable
    • Not discoverable by scanning a bus

    Examples include:

    • GPIO controllers
    • I2C controllers
    • SPI controllers
    • UARTs
    • Timers
    • Watchdogs
    • On-chip ADCs

    Linux still needs a clean way to represent these hardware blocks. That is where Platform Devices and Drivers come in.

    Why Platform Devices and Drivers Exist

    To understand platform drivers, first understand what Linux expects from hardware.

    Linux follows a simple model:

    • A device describes hardware
    • A driver knows how to control that hardware
    • The kernel matches them and binds them

    For PCI or USB, the kernel can discover devices automatically. For embedded hardware, there is no discovery mechanism.

    So Linux needed:

    • A way to describe fixed hardware
    • A bus that does not rely on enumeration
    • A matching mechanism that still feels clean

    That solution is the platform bus.

    What Is the Platform Bus?

    The platform bus is a virtual bus inside the Linux kernel.

    It is not a real hardware bus like I2C or SPI.

    Think of it as a logical meeting place where:

    • Platform devices are registered
    • Platform drivers are registered
    • The kernel matches them by name or device tree

    This bus makes sure Linux can treat on-chip hardware the same way it treats external devices.

    Platform Device vs Platform Driver

    This confusion is very common, especially for beginners.

    Let us clear it properly.

    Platform Device

    A platform device represents the hardware.

    It describes things like:

    • Base memory address
    • IRQ number
    • DMA channels
    • Clock names
    • Device name

    The platform device does not contain logic. It only describes what exists.

    Platform Driver

    A platform driver contains the software logic.

    It knows:

    • How to initialize the hardware
    • How to read and write registers
    • How to handle interrupts
    • How to expose functionality to user space

    The driver does the actual work.

    How Platform Devices Were Defined Earlier

    Before device tree became common, platform devices were defined in board files.

    Example idea:

    static struct platform_device uart_device = {
        .name = "my_uart",
        .id = -1,
        .resource = uart_resources,
    };
    

    This worked but had problems:

    • Board specific code lived inside the kernel
    • Kernel had to be recompiled for hardware changes
    • Not scalable

    This is why Device Tree replaced board files.

    Role of Device Tree in Platform Devices and Drivers

    Today, Device Tree is the most common way platform devices are created.

    The device tree:

    • Describes hardware in a hardware-agnostic way
    • Is loaded by the bootloader
    • Is parsed by the kernel
    • Automatically creates platform devices

    So now, you usually do not manually register platform devices in C code.

    How Device Tree Creates Platform Devices

    When the kernel boots:

    1. Bootloader loads kernel + device tree blob
    2. Kernel parses the device tree
    3. Each compatible node becomes a platform device
    4. Kernel looks for a matching platform driver

    That is the entire flow.

    Example Device Tree Node

    uart0: serial@48020000 {
        compatible = "vendor,my-uart";
        reg = <0x48020000 0x1000>;
        interrupts = <32>;
        status = "okay";
    };
    

    This node creates a platform device automatically.

    The kernel then looks for a platform driver with:

    .compatible = "vendor,my-uart"
    

    How Platform Device and Driver Matching Works

    Linux supports multiple matching mechanisms, but device tree is the most common.

    Matching Using of_match_table

    static const struct of_device_id my_uart_of_match[] = {
        { .compatible = "vendor,my-uart" },
        {}
    };
    

    When the kernel sees a platform device with the same compatible string, it binds the driver.

    Basic Structure of a Platform Driver

    A minimal platform driver looks like this:

    static int my_driver_probe(struct platform_device *pdev)
    {
        return 0;
    }
    
    static int my_driver_remove(struct platform_device *pdev)
    {
        return 0;
    }
    
    static struct platform_driver my_driver = {
        .probe = my_driver_probe,
        .remove = my_driver_remove,
        .driver = {
            .name = "my_driver",
            .of_match_table = my_driver_of_match,
        },
    };
    

    This structure is the backbone of Platform Devices and Drivers in Linux.

    What Happens in the Probe Function

    The probe() function is the heart of a platform driver.

    This is where you:

    • Get memory resources
    • Map registers using ioremap
    • Request IRQs
    • Initialize hardware
    • Register character device or subsystem

    If probe fails, the device is not usable.

    Getting Resources from Platform Device

    Example:

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

    This resource came directly from the device tree reg property.

    Mapping Registers

    void __iomem *base;
    base = devm_ioremap_resource(&pdev->dev, res);
    

    This maps physical memory into kernel virtual address space.

    Interrupt Handling in Platform Drivers

    int irq = platform_get_irq(pdev, 0);
    request_irq(irq, my_isr, 0, "my_driver", dev);
    

    Again, IRQ info comes from device tree.

    Power Management in Platform Drivers

    Platform drivers integrate nicely with Linux power management.

    You can implement:

    • Suspend
    • Resume

    This is important for battery-powered embedded devices.

    How Platform Drivers Expose Functionality

    Platform drivers usually expose functionality using:

    • Character devices
    • sysfs
    • procfs
    • Input subsystem
    • Network stack

    Example for character device:

    alloc_chrdev_region(...)
    cdev_add(...)
    

    How to Write Platform Devices and Drivers in Linux Step by Step

    Here is a clean beginner flow.

    Step 1: Identify Hardware

    • Base address
    • IRQ number
    • Clock source

    Step 2: Write Device Tree Node

    Describe hardware correctly.

    Step 3: Write Platform Driver Skeleton

    • probe
    • remove
    • match table

    Step 4: Handle Resources

    • Memory
    • IRQ
    • Clocks

    Step 5: Initialize Hardware

    • Reset
    • Configure registers

    Step 6: Expose Interface

    • Char device or sysfs

    Common Mistakes Beginners Make

    • Forgetting of_match_table
    • Wrong compatible string
    • Not handling error paths
    • Hardcoding addresses
    • Ignoring device tree

    Avoid these and your driver becomes clean and reusable.

    Platform Devices and Drivers vs Character Drivers

    This is a popular interview question.

    A platform driver is about how the driver binds to hardware.

    A character driver is about how user space interacts.

    Many platform drivers internally register a character driver.

    They are not mutually exclusive.

    Testing and Verifying Platform Drivers

    Testing is where most tutorials stop. This one does not.

    Check Device Creation

    ls /sys/bus/platform/devices
    

    Check Driver Binding

    ls /sys/bus/platform/drivers
    

    Check Kernel Logs

    dmesg | grep my_driver
    

    Verify Probe Execution

    Add logs:

    dev_info(&pdev->dev, "Probe successful\n");
    

    Verify Resource Mapping

    Check /proc/iomem.

    Debugging Tips for Platform Drivers

    • Use dev_dbg()
    • Enable dynamic debug
    • Validate device tree addresses
    • Double-check IRQ numbers
    • Use ftrace if needed

    Debugging is part of learning. Do not avoid it.

    Real-World Use of Platform Devices and Drivers

    Almost every embedded Linux system relies on platform drivers:

    • Automotive ECUs
    • Medical devices
    • Industrial controllers
    • Consumer electronics
    • IoT gateways

    If you work in embedded Linux, you will write or debug platform drivers.

    Platform Driver Implementation From Scratch (Linux)

    What we are building

    We will create a basic platform driver that:

    • Matches a device tree node
    • Gets MMIO memory
    • Maps registers
    • Logs success

    This is the foundation of all real platform drivers (GPIO, UART, I2C, SPI, etc.).

    Step 1: Understand the Flow

    Before code, understand this flow:

    Device Tree
         ↓
    Platform Device created by kernel
         ↓
    Platform Driver registered
         ↓
    Matching via compatible string
         ↓
    probe() called
         ↓
    Hardware initialized
    

    If this flow is clear, platform drivers become easy.

    Step 2: Device Tree Node (Hardware Description)

    This is mandatory in modern Linux.

    Example Device Tree Node

    Add this to your .dts file:

    my_device@10000000 {
        compatible = "nish,my-platform-device";
        reg = <0x10000000 0x1000>;
        status = "okay";
    };
    

    What this means

    PropertyMeaning
    compatibleUsed for driver matching
    regBase address + size
    statusEnable device

    This node automatically creates a platform device.

    Step 3: Platform Driver Skeleton (Core Code)

    Create a file:

    my_platform_driver.c
    

    Include Required Headers

    #include <linux/module.h>
    #include <linux/platform_device.h>
    #include <linux/of.h>
    #include <linux/io.h>
    

    Step 4: Device Tree Match Table

    This is how Linux connects device ↔ driver

    static const struct of_device_id my_platform_of_match[] = {
        { .compatible = "nish,my-platform-device" },
        { }
    };
    MODULE_DEVICE_TABLE(of, my_platform_of_match);
    

    Compatible string must exactly match device tree.

    Step 5: Private Driver Data Structure

    struct my_platform_dev {
        void __iomem *base_addr;
    };
    

    This stores mapped register base.

    Step 6: Probe Function (Most Important Part)

    static int my_platform_probe(struct platform_device *pdev)
    {
        struct resource *res;
        struct my_platform_dev *dev;
    
        dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
        if (!dev)
            return -ENOMEM;
    
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        if (!res) {
            dev_err(&pdev->dev, "Memory resource not found\n");
            return -ENODEV;
        }
    
        dev->base_addr = devm_ioremap_resource(&pdev->dev, res);
        if (IS_ERR(dev->base_addr))
            return PTR_ERR(dev->base_addr);
    
        platform_set_drvdata(pdev, dev);
    
        dev_info(&pdev->dev, "Platform driver probed successfully\n");
    
        return 0;
    }
    

    What happens here (simple words)

    StepExplanation
    platform_get_resource()Gets address from DT
    ioremap()Maps physical → virtual
    platform_set_drvdata()Saves private data
    probe()Hardware is now usable

    Step 7: Remove Function

    static int my_platform_remove(struct platform_device *pdev)
    {
        dev_info(&pdev->dev, "Platform driver removed\n");
        return 0;
    }
    

    Cleanup is automatic because we used devm_ APIs.

    Step 8: Platform Driver Structure

    static struct platform_driver my_platform_driver = {
        .probe  = my_platform_probe,
        .remove = my_platform_remove,
        .driver = {
            .name = "my_platform_driver",
            .of_match_table = my_platform_of_match,
        },
    };
    

    Step 9: Module Init and Exit

    module_platform_driver(my_platform_driver);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Nish");
    MODULE_DESCRIPTION("Platform Driver from Scratch");
    

    This macro handles init and exit automatically.

    Step 10: Complete Driver Code (Final)

    #include <linux/module.h>
    #include <linux/platform_device.h>
    #include <linux/of.h>
    #include <linux/io.h>
    
    struct my_platform_dev {
        void __iomem *base_addr;
    };
    
    static const struct of_device_id my_platform_of_match[] = {
        { .compatible = "nish,my-platform-device" },
        { }
    };
    MODULE_DEVICE_TABLE(of, my_platform_of_match);
    
    static int my_platform_probe(struct platform_device *pdev)
    {
        struct resource *res;
        struct my_platform_dev *dev;
    
        dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
        if (!dev)
            return -ENOMEM;
    
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        if (!res)
            return -ENODEV;
    
        dev->base_addr = devm_ioremap_resource(&pdev->dev, res);
        if (IS_ERR(dev->base_addr))
            return PTR_ERR(dev->base_addr);
    
        platform_set_drvdata(pdev, dev);
    
        dev_info(&pdev->dev, "Platform driver probed\n");
        return 0;
    }
    
    static int my_platform_remove(struct platform_device *pdev)
    {
        dev_info(&pdev->dev, "Platform driver removed\n");
        return 0;
    }
    
    static struct platform_driver my_platform_driver = {
        .probe  = my_platform_probe,
        .remove = my_platform_remove,
        .driver = {
            .name = "my_platform_driver",
            .of_match_table = my_platform_of_match,
        },
    };
    
    module_platform_driver(my_platform_driver);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Nish");
    MODULE_DESCRIPTION("Platform Driver From Scratch");
    

    Step 11: Makefile

    obj-m += my_platform_driver.o
    

    Step 12: Build the Driver

    make -C /lib/modules/$(uname -r)/build M=$PWD modules
    

    Step 13: Insert and Test

    sudo insmod my_platform_driver.ko
    dmesg | tail
    

    Expected output:

    Platform driver probed
    

    Step 14: Verify Binding

    ls /sys/bus/platform/devices
    ls /sys/bus/platform/drivers/my_platform_driver
    

    If your device appears → success

    Key Interview Points

    • Platform drivers are for non-discoverable devices
    • Device tree creates platform devices
    • Matching happens via compatible
    • probe() initializes hardware
    • devm_ APIs simplify cleanup

    What You Should Do Next

    If you want real mastery, next steps:

    1. Add character device interface
    2. Add IRQ handling
    3. Add sysfs attributes
    4. Convert to GPIO / LED driver
    5. Learn power management callbacks

    Why Platform Devices and Drivers Matter for Your Career

    Understanding Platform Devices and Drivers shows that you:

    • Understand Linux internals
    • Can work close to hardware
    • Can read device tree files
    • Can debug kernel issues

    This skill separates application developers from embedded engineers.

    FAQ: Platform Devices and Drivers in Linux

    1. What are platform devices and drivers in Linux?

    Platform devices and drivers in Linux are used to manage hardware that is built directly into the system, such as GPIO controllers, UARTs, and timers. These devices cannot be auto-detected, so Linux uses the platform bus and device tree to describe and bind them to drivers.

    2. Why are platform drivers needed in embedded Linux?

    Platform drivers are needed in embedded Linux because most on-chip peripherals do not support hardware discovery. The platform driver framework allows Linux to cleanly separate hardware description from driver logic using device tree.

    3. What is the difference between a platform device and a platform driver?

    A platform device represents the hardware information like memory address and interrupts, while a platform driver contains the code that controls the hardware. Linux matches them and calls the driver’s probe function.

    4. How does device tree work with platform devices and drivers?

    The device tree describes hardware in a structured format. During boot, the Linux kernel parses the device tree and automatically creates platform devices, which are then matched with platform drivers using compatible strings.

    5. What is the platform bus in Linux?

    The platform bus is a virtual bus inside the Linux kernel that connects platform devices with platform drivers. It is mainly used for fixed hardware present on the SoC.

    6. How does Linux match a platform driver to a platform device?

    Linux matches a platform driver to a platform device using the compatible string defined in the device tree and the of_match_table in the driver code.

    7. What happens inside the probe function of a platform driver?

    The probe function is called when the driver matches a device. It initializes hardware, maps registers, requests interrupts, and prepares the device for use.

    8. Is a platform driver the same as a character driver?

    No. A platform driver defines how the driver binds to hardware, while a character driver defines how user space interacts with the driver. A platform driver can internally register a character driver.

    9. Do platform drivers always require a device tree?

    In modern Linux systems, platform drivers usually rely on device tree. Older systems used board files, but device tree is now the standard and recommended approach.

    10. How can I test if my platform driver is working correctly?

    You can test a platform driver by checking kernel logs using dmesg, verifying device creation in /sys/bus/platform, and confirming that the probe function is executed successfully.

    11. What are common mistakes when writing platform devices and drivers?

    Common mistakes include mismatched compatible strings, incorrect memory addresses in device tree, forgetting the of_match_table, and improper resource handling in the probe function.

    12. Where are platform devices and drivers used in real projects?

    Platform devices and drivers are widely used in automotive systems, industrial controllers, IoT devices, medical equipment, and consumer electronics running embedded Linux.

    Read More : Char Driver Model in Linux

  • Hello World Kernel Driver, Master How to Write in Linux (Step-by-Step Guide 2026)

    Hello World Kernel Driver : If you are starting your journey into Linux device driver development, writing a simple Hello World kernel module is the perfect first step. In this tutorial, we will walk you through creating, compiling, and loading a kernel driver in Linux — all in a beginner-friendly way.

    Learn how to write your first Hello World Linux kernel driver with this step-by-step tutorial for beginners. This complete Linux kernel module development guide covers everything from setting up your environment to compiling and loading a kernel module. You will understand the basics of Linux device driver development, including how to create a simple printk() output in the kernel log, use module_init and module_exit functions, and build your driver using a Makefile. Whether you’re new to kernel programming in Linux or want to get started with embedded systems and operating system internals, this beginner-friendly guide will help you master the fundamentals of writing and running your own kernel modules. Perfect for students, hobbyists, and professionals looking to learn Linux driver programming from scratch.

    What is a Hello World kernel module in Linux ?

    A Linux kernel module (LKM) is a piece of code that can be loaded and unloaded into the Linux kernel at runtime. This allows you to add new functionalities, such as hardware drivers, without rebuilding the entire kernel.

    A “Hello World” kernel driver is the simplest example — it just prints a message when loaded and unloaded.

    Prerequisites of Hello World Kernel Driver

    Before writing your kernel module, make sure you have:

    • A Linux system (Ubuntu, Debian, Fedora, or similar)
    • Basic knowledge of C programming
    • Installed kernel headers and build tools

    For Ubuntu/Debian-based systems, run:

    sudo apt update
    sudo apt install build-essential linux-headers-$(uname -r)
    

    Step 1: Create the Source Code

    Create a new directory for your driver and a file named hello.c:

    mkdir hello_driver
    cd hello_driver
    nano hello.c

    Paste the following code:

    #include <linux/init.h>     
    #include <linux/module.h>   
    #include <linux/kernel.h>   
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Your Name");
    MODULE_DESCRIPTION("A Simple Hello World Linux Kernel Module");
    MODULE_VERSION("1.0");
    
    static int __init hello_init(void) {
        printk(KERN_INFO "Hello, World! Kernel module loaded.\n");
        return 0;
    }
    
    static void __exit hello_exit(void) {
        printk(KERN_INFO "Goodbye, World! Kernel module unloaded.\n");
    }
    
    module_init(hello_init);
    module_exit(hello_exit);

    Explanation:

    • printk() prints messages to the kernel log (viewable with dmesg).
    • module_init() specifies the function to run when the module loads.
    • module_exit() specifies the function to run when the module unloads.

    Step 2: Create the Makefile

    The Makefile tells the Linux kernel build system how to compile your module.

    Create a file named Makefile:

    obj-m += hello.o
    
    all:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    
    clean:
    	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    

    Step 3: Compile the Kernel Module

    Run:

    make

    If successful, you will see a new file called hello.ko — this is your compiled kernel driver.

    Step 4: Load the Module

    To load your driver into the kernel:

    sudo insmod hello.ko

    Check the kernel log:

    dmesg | tail

    You should see:

    Hello, World! Kernel module loaded.

    Step 5: Unload the Module

    When you want to remove the module:

    sudo rmmod hello

    Check the log again:

    dmesg | tail

    Output:

    Goodbye, World! Kernel module unloaded.

    Step 6: Clean Up

    To remove build files:

    make clean

    Final Thoughts

    You’ve just created your first Hello World kernel driver in Linux! This small example lays the foundation for developing more advanced Linux device drivers. From here, you can explore working with hardware, file operations, and different driver types (character, block, and network drivers)

    Frequently Asked Questions (FAQ) | Hello World Linux Kernel Driver

    1. What is a Hello World Kernel Driver ?

    A Linux kernel driver, also called a kernel module, is a piece of code that runs in the kernel space of the Linux operating system. It allows you to add functionalities like hardware control, system calls, or device communication without rebuilding the entire kernel.

    2. What is the purpose of a Hello World kernel module?

    The Hello World kernel module is the simplest example used to demonstrate Linux driver development. It helps beginners understand how to load and unload kernel modules, print messages to the kernel log, and use essential kernel functions.

    3. Do I need special hardware to write a Hello World Linux kernel driver?

    No. You can write and test a Hello World kernel module on any Linux system, including virtual machines, without special hardware.

    4. How do I compile a Hello World kernel module?

    You can compile a kernel module using a Makefile and the Linux kernel build system. The make command with the correct -C /lib/modules/$(uname -r)/build path will generate the .ko file (kernel object file) for your module.

    5. How do I load a kernel driver in Linux?

    You can load a compiled kernel driver using the insmod command, for example:
    sudo insmod hello.ko
    To confirm it loaded successfully, check the kernel log using:
    dmesg | tail

    6. How do I remove a kernel driver in Linux?

    You can remove a kernel driver using the rmmod command:
    sudo rmmod hello
    Then, verify it was removed by checking the kernel log with dmesg.

    7. Is it safe to experiment with kernel programming?

    Kernel programming requires caution because faulty code can crash the system. However, starting with a simple Hello World module in a test environment or virtual machine is safe and recommended for beginners.

    8. Can I use this Hello World driver on any Linux distribution?

    Yes, as long as your distribution provides the required kernel headers and build tools, you can compile and run the Hello World kernel module on Ubuntu, Debian, Fedora, Arch Linux, and more.