|
Getting Started with Embedded Linux Driver Development: Writing Your First Character Device Driver

Getting Started with Embedded Linux Driver Development: Writing Your First Character Device Driver

Why Learn Driver Development?

Having worked in embedded development for so long, I’ve always been working in user space: writing applications, calling libraries, playing with frameworks. But sometimes you’ll find that some features just can’t be handled in user space—like precise timing control, direct register operations, or hardware interrupt handling. At this point, you need to step into kernel space and write drivers.

Many people are intimidated by kernel drivers, thinking “kernel programming is dangerous, if you mess up the system will crash.” That’s true, but it’s not that scary. Today we’ll start from scratch and write the simplest character device driver to help you take the first step.

What Do You Need?

ItemModel/SpecPrice
Development BoardRaspberry Pi 4B / Jetson Nano¥350-500
OrAny ARM board running Linux-
ComputerFor cross-compilationAlready have
Jumper WiresMale-to-female, several¥10
LED5mm red¥0.5
Resistor220Ω¥0.1
Total¥360-510

If you already have a Raspberry Pi or Jetson Nano, the cost is almost zero. If not, you can still learn most of the content using a virtual machine running Ubuntu (just can’t operate real GPIO).

Step 1: Set Up Kernel Development Environment

First, you need to install kernel headers and compilation toolchain. Taking Ubuntu/Debian as an example:

# Update package list
sudo apt-get update

# Install kernel headers and build tools
sudo apt-get install linux-headers-$(uname -r) build-essential dkms

# Verify installation
ls /lib/modules/$(uname -r)/build

If you see a bunch of header files, the installation was successful. ⚠️ Note: The kernel header version must match the currently running kernel version, otherwise the compiled module will fail to load.

Check kernel version:

uname -r
# Example output: 5.15.0-76-generic

Step 2: Write Your First Kernel Module

Let’s start with a “Hello World” level module to get a feel for kernel programming. Create the file hello_module.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel! Module loaded.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel! Module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

Key points:

  • MODULE_LICENSE("GPL"): Must declare the license, otherwise the kernel will give a “tainted” warning

  • __init and __exit: Tell the kernel these functions are only called during load/unload, memory can be freed

  • printk: The kernel’s printf, use macros like KERN_INFO to specify log level

Write the Makefile:

obj-m += hello_module.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

Compile:

make

If everything goes well, a hello_module.ko file will be generated. Load it:

# Load module
sudo insmod hello_module.ko

# Check kernel log
dmesg | tail -5
# Should see: Hello, Kernel! Module loaded.

# Unload module
sudo rmmod hello_module

# Check log again
dmesg | tail -5
# Should see: Goodbye, Kernel! Module unloaded.

Congratulations, you’ve successfully written and run your first kernel module!

Step 3: Write a Character Device Driver

Hello World is too simple, let’s do something real. We’ll write a character device driver that can be read and written through /dev files.

Create char_device.c:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardev"
#define CLASS_NAME "myclass"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

// Device number
static int major_number;
static struct class* char_class = NULL;
static struct device* char_device = NULL;
static struct cdev char_cdev;

// Device buffer
static char message_buffer[256] = {0};
static int message_size = 0;

// File operation functions
static int dev_open(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int dev_release(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static ssize_t dev_read(struct file *filep, char __user *buffer, 
                        size_t len, loff_t *offset)
{
    int bytes_to_read;
    int bytes_read;

    // Calculate bytes to read
    bytes_to_read = min((int)(message_size - *offset), (int)len);
    if (bytes_to_read <= 0) return 0;

    // Copy to user space
    bytes_read = bytes_to_read;
    if (copy_to_user(buffer, message_buffer + *offset, bytes_to_read)) {
        return -EFAULT;
    }

    *offset += bytes_read;
    return bytes_read;
}

static ssize_t dev_write(struct file *filep, const char __user *buffer,
                         size_t len, loff_t *offset)
{
    // Copy from user space
    if (copy_from_user(message_buffer, buffer, len)) {
        return -EFAULT;
    }

    message_size = len;
    printk(KERN_INFO "Received %zu bytes\n", len);
    return len;
}

// File operations structure
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = dev_open,
    .release = dev_release,
    .read = dev_read,
    .write = dev_write,
};

static int __init char_device_init(void)
{
    int ret;

    // Dynamically allocate major number
    ret = alloc_chrdev_region(&major_number, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ALERT "Failed to allocate major number\n");
        return ret;
    }

    // Initialize and add cdev
    cdev_init(&char_cdev, &fops);
    char_cdev.owner = THIS_MODULE;
    ret = cdev_add(&char_cdev, major_number, 1);
    if (ret < 0) {
        printk(KERN_ALERT "Failed to add cdev\n");
        unregister_chrdev_region(major_number, 1);
        return ret;
    }

    // Create device class
    char_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(char_class)) {
        printk(KERN_ALERT "Failed to create device class\n");
        cdev_del(&char_cdev);
        unregister_chrdev_region(major_number, 1);
        return PTR_ERR(char_class);
    }

    // Create device
    char_device = device_create(char_class, NULL, major_number, NULL, DEVICE_NAME);
    if (IS_ERR(char_device)) {
        printk(KERN_ALERT "Failed to create device\n");
        class_destroy(char_class);
        cdev_del(&char_cdev);
        unregister_chrdev_region(major_number, 1);
        return PTR_ERR(char_device);
    }

    printk(KERN_INFO "Character device driver loaded, major number: %d\n", major_number);
    return 0;
}

static void __exit char_device_exit(void)
{
    device_destroy(char_class, major_number);
    class_destroy(char_class);
    cdev_del(&char_cdev);
    unregister_chrdev_region(major_number, 1);
    printk(KERN_INFO "Character device driver unloaded\n");
}

module_init(char_device_init);
module_exit(char_device_exit);

Update the Makefile:

obj-m += char_device.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

Compile and load:

make
sudo insmod char_device.ko

# Check if device file was created
ls -l /dev/mychardev

# Write data
echo "Hello from user space!" > /dev/mychardev

# Read data
cat /dev/mychardev
# Should output: Hello from user space!

# Check kernel log
dmesg | tail -10

Step 4: Add GPIO Control Functionality

Just reading and writing strings is too boring, let’s add some hardware interaction. Add GPIO control to make an LED blink.

Add GPIO operations to the existing code (using Raspberry Pi GPIO 17 as an example):

#include <linux/gpio.h>
#include <linux/ioctl.h>

#define LED_GPIO 17

// Add to initialization function
ret = gpio_request(LED_GPIO, "mychardev-led");
if (ret) {
    printk(KERN_ALERT "Failed to request GPIO %d\n", LED_GPIO);
    // Error handling...
}
gpio_direction_output(LED_GPIO, 0);

// Add ioctl commands to control LED
#define IOCTL_MAGIC 'k'
#define IOCTL_LED_ON _IO(IOCTL_MAGIC, 1)
#define IOCTL_LED_OFF _IO(IOCTL_MAGIC, 2)
#define IOCTL_LED_TOGGLE _IO(IOCTL_MAGIC, 3)

static long dev_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
        case IOCTL_LED_ON:
            gpio_set_value(LED_GPIO, 1);
            printk(KERN_INFO "LED turned ON\n");
            break;
        case IOCTL_LED_OFF:
            gpio_set_value(LED_GPIO, 0);
            printk(KERN_INFO "LED turned OFF\n");
            break;
        case IOCTL_LED_TOGGLE:
            gpio_set_value(LED_GPIO, !gpio_get_value(LED_GPIO));
            printk(KERN_INFO "LED toggled\n");
            break;
        default:
            return -EINVAL;
    }
    return 0;
}

// Update file_operations
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = dev_open,
    .release = dev_release,
    .read = dev_read,
    .write = dev_write,
    .unlocked_ioctl = dev_ioctl,
};

Free GPIO in the exit function:

gpio_free(LED_GPIO);

User space test program test_gpio.c:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define IOCTL_MAGIC 'k'
#define IOCTL_LED_ON _IO(IOCTL_MAGIC, 1)
#define IOCTL_LED_OFF _IO(IOCTL_MAGIC, 2)
#define IOCTL_LED_TOGGLE _IO(IOCTL_MAGIC, 3)

int main()
{
    int fd = open("/dev/mychardev", O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return -1;
    }

    printf("Turning LED ON\n");
    ioctl(fd, IOCTL_LED_ON);
    sleep(2);

    printf("Turning LED OFF\n");
    ioctl(fd, IOCTL_LED_OFF);
    sleep(1);

    printf("Toggling LED 3 times\n");
    for (int i = 0; i < 3; i++) {
        ioctl(fd, IOCTL_LED_TOGGLE);
        sleep(1);
    }

    close(fd);
    return 0;
}

Compile and run:

gcc -o test_gpio test_gpio.c
sudo ./test_gpio

Common Problem Troubleshooting

Problem 1: Module fails to load, error “Invalid module format”

  • Cause: Kernel header version doesn’t match running kernel version

  • Solution:

# Check current kernel version
uname -r

# Install matching kernel headers
sudo apt-get install linux-headers-$(uname -r)

# Recompile module
make clean && make

Problem 2: Device file /dev/mychardev not created

  • Cause: Device class or device creation failed

  • Solution: Check kernel log dmesg | tail -20 for error messages, confirm class_create and device_create returned correctly

Problem 3: Permission denied when writing to device

  • Cause: Device file permissions issue

  • Solution:

# Modify device file permissions
sudo chmod 666 /dev/mychardev

# Or add udev rule
echo 'KERNEL=="mychardev", MODE="0666"' | sudo tee /etc/udev/rules.d/99-mychardev.rules
sudo udevadm control --reload-rules

Problem 4: GPIO control doesn’t work

  • Cause: GPIO number error or GPIO already occupied

  • Solution:

# Check GPIO occupation status
cat /sys/kernel/debug/gpio

# Confirm GPIO number is correct
# Raspberry Pi GPIO 17 corresponds to BCM number 17

Problem 5: Too many kernel logs flooding the screen

  • Cause: printk level set too low

  • Solution: Use KERN_DEBUG instead of KERN_INFO, or adjust console log level:

echo "4" > /proc/sys/kernel/printk

Summary

Writing kernel drivers from scratch isn’t as scary as imagined. Key steps:

  1. Set up kernel development environment, install headers matching current kernel version

  2. Write Hello World kernel module, get familiar with basic patterns like printk, module_init/module_exit

  3. Implement character device driver’s file operation functions like open, read, write, release

  4. Add hardware control capabilities (like GPIO operations) through ioctl

  5. Use insmod/rmmod to load/unload modules, use dmesg to check kernel logs for troubleshooting

Character device drivers are the most basic driver type. Once you master this, learning advanced content like platform device drivers, device trees, and interrupt handling will be much easier.

Advanced suggestions:

  • Learn Device Tree configuration

  • Understand interrupt handling and bottom half mechanisms

  • Study platform device driver model

  • Read reference drivers in kernel source code

Hope this blog article is helpful to you!


Related resources: