DEV Community

Cover image for Creating a Character Device Driver for Raspberry Pi Using Buildroot
Devontae Reid
Devontae Reid

Posted on • Edited on

Creating a Character Device Driver for Raspberry Pi Using Buildroot

Introduction

Welcome to those who have read my previous posts, and a warm welcome to all who are new to my blog. As an embedded developer, I continue to navigate the complexities of learning embedded engineering. Imposter syndrome often makes me wonder if I truly grasp the concepts as well as my peers. I am committed to gaining more knowledge in order to overcome numerous challenges that may arise in the embedded world. In this article, I will introduce you to the world of character device drivers, understand their core concepts, and walk you through the steps of creating and building one for a Raspberry Pi using Buildroot.

Understanding Character Device Drivers

What is a Character Device Driver?

Character device drivers are a type of device driver that manage devices performing I/O operations sequentially. Character devices, on the other hand, deal with data streams, making them compatible with a variety of peripherals, like serial ports, keyboards, and custom hardware.

Key Concepts

  1. Device Files: In Linux, character devices are represented by device files located in the /dev directory. These files provide an interface for user-space applications to interact with the hardware.
  2. Major and Minor Numbers: Each device file contains both a Major and Minor Number. The major number identifies the driver associated with the device, while the minor number identifies the specific device handled by the driver.
  3. File Operations: Character device drivers define a set of file operations, such as open, close, read, write, and ioctl, to handle interactions between the user-space and the hardware.

Creating a Character Device Driver

Prerequisites

Before you embark on your development adventure, make sure you have the basic setup below:

  • A Linux development environment (preferably Ubuntu or similar).
  • Basic knowledge of C programming and Linux kernel modules.
  • Root access to the system for module loading and device file creation.
  • A Raspberry Pi board.
  • Buildroot configured for the Raspberry Pi.

Step 1: Setting Up Buildroot for Raspberry Pi

First, download and configure Buildroot for your Raspberry Pi as described in the previous article excluding the building step for the moment.

Step 2: Writing the Driver Code

Create a directory within your Buildroot package directory for your driver code:

mkdir -p buildroot/package/simple_char_driver
cd buildroot/package/simple_char_driver
touch char_driver.c
Enter fullscreen mode Exit fullscreen mode

Open char_driver.c in your preferred text editor:

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

#define DEVICE_NAME "char_device"
#define BUFFER_SIZE 1024

#define CHAR_DEBUG_PRINT(fmt, ...) printk(KERN_INFO "CHAR_DRIVER_INFO: " fmt, ##__VA_ARGS__)

static int major_number;
static char device_buffer[BUFFER_SIZE];
static int open_count = 0;

static int device_open(struct inode *inode, struct file *file) {
    open_count++;
    CHAR_DEBUG_PRINT("Device opened %d times\n", open_count);
    return 0;
}

static int device_release(struct inode *inode, struct file *file) {
    CHAR_DEBUG_PRINT("Device closed\n");
    return 0;
}

static ssize_t device_read(struct file *file, char __user *user_buffer, size_t size, loff_t *offset) {
    int bytes_read = size < BUFFER_SIZE ? size : BUFFER_SIZE;
    if (copy_to_user(user_buffer, device_buffer, bytes_read)) {
        return -EFAULT;
    }
    return bytes_read;
}

static ssize_t device_write(struct file *file, const char __user *user_buffer, size_t size, loff_t *offset) {
    int bytes_to_write = size < BUFFER_SIZE ? size : BUFFER_SIZE;
    if (copy_from_user(device_buffer, user_buffer, bytes_to_write)) {
        return -EFAULT;
    }
    return bytes_to_write;
}

static struct file_operations fops = {
    .open = device_open,
    .release = device_release,
    .read = device_read,
    .write = device_write,
};

static int __init char_device_init(void) {
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        CHAR_DEBUG_PRINT("Failed to register a major number\n");
        return major_number;
    }
    CHAR_DEBUG_PRINT("Registered with major number %d\n", major_number);
    return 0;
}

static void __exit char_device_exit(void) {
    unregister_chrdev(major_number, DEVICE_NAME);
    CHAR_DEBUG_PRINT("Unregistered device\n");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Dev Reid");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("0.1");

module_init(char_device_init);
module_exit(char_device_exit);

Enter fullscreen mode Exit fullscreen mode

Step 3: Integrating the Driver with Buildroot

Create a Config.in file in the simple_char_driver directory:

touch Config.in
Enter fullscreen mode Exit fullscreen mode

Edit Config.in:

config BR2_PACKAGE_SIMPLE_CHAR_DRIVER
    bool "Simple Character Device Driver"
    help
      This is a simple character device driver example for my raspberry pi 4.
Enter fullscreen mode Exit fullscreen mode

Create a simple_char_driver.mk file in the same directory:

touch simple_char_driver.mk
Enter fullscreen mode Exit fullscreen mode

Edit simple_char_driver.mk:

################################################################################
# Simple Character Device Driver Buildroot Package Makefile
################################################################################

# Define package variables
SIMPLE_CHAR_DRIVER_VERSION = 0.1
SIMPLE_CHAR_DRIVER_SITE = $(TOPDIR)/package/simple_char_driver
SIMPLE_CHAR_DRIVER_SITE_METHOD = local
SIMPLE_CHAR_DRIVER_LICENSE = GPL-2.0+
SIMPLE_CHAR_DRIVER_LICENSE_FILES = char_driver.c

# Path to the Buildroot cross-compiler
CROSS_COMPILE := $(TOPDIR)/output/host/usr/bin/arm-buildroot-linux-gnueabihf-

# Define build commands
define SIMPLE_CHAR_DRIVER_BUILD_CMDS
 $(MAKE) -C $(LINUX_DIR) M=$(SIMPLE_CHAR_DRIVER_BUILD_DIR) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE)
endef

# Define install commands
define SIMPLE_CHAR_DRIVER_INSTALL_TARGET_CMDS
 $(INSTALL) -D -m 0755 $(SIMPLE_CHAR_DRIVER_BUILD_DIR)/char_driver.ko $(TARGET_DIR)/lib/modules/char_driver.ko
endef

# Define clean commands
define SIMPLE_CHAR_DRIVER_CLEAN_CMDS
 $(MAKE) -C $(LINUX_DIR) M=$(SIMPLE_CHAR_DRIVER_BUILD_DIR) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean
endef

# Set directory variables
SIMPLE_CHAR_DRIVER_BUILD_DIR := $(BUILD_DIR)/simple_char_driver-$(SIMPLE_CHAR_DRIVER_VERSION)

# Evaluate kernel module and generic package rules
$(eval $(kernel-module))
$(eval $(generic-package))
Enter fullscreen mode Exit fullscreen mode

Create a Makefile file in the same directory:

touch Makefile
Enter fullscreen mode Exit fullscreen mode

Edit Makefile:

obj-m += char_driver.o

# Path to the kernel source
KERNELDIR := $(SIMPLE_CHAR_DRIVER_SITE)

# Compilation flags
ccflags-y := -DDEBUG -g -std=gnu99 -Wno-declaration-after-statement

.PHONY: all clean

all:
    $(MAKE) -C $(SIMPLE_CHAR_DRIVER_SITE) M=$(PWD) modules

clean:
    $(MAKE) -C $(SIMPLE_CHAR_DRIVER_SITE) M=$(PWD) clean
Enter fullscreen mode Exit fullscreen mode

Add the package to Buildroot's package/Config.in:

cd ..
echo "source \"package/char_device_driver/Config.in\"" >> Config.in
Enter fullscreen mode Exit fullscreen mode

Step 4: Building the Driver with Buildroot

Reconfigure Buildroot to include your driver:

cd ..
make menuconfig
Enter fullscreen mode Exit fullscreen mode

Navigate to:

  • Simple Device Driver and enable it.

Main Menu

Build the Buildroot system:

make
Enter fullscreen mode Exit fullscreen mode

After two hours later...

Two Hours Later Meme

Verify that the kernel module file (char_driver.ko) was added to the rootfs. There are two methods to verify this, and they should have the same output:

  1. This can be done by check the current folder output/target/lib/modules.

  2. Or you can mount the rootfs.ext4:

sudo mount -o loop output/images/rootfs.ext4 /mnt
Enter fullscreen mode Exit fullscreen mode

Check the lib/modules folder

Modules Folder

Step 5: Running the Driver on Raspberry Pi

  1. Flash the Buildroot image to your SD card and boot your Raspberry Pi with it.

  2. Load the driver module:

insmod /lib/modules/$(uname -r)/char_driver.ko
Enter fullscreen mode Exit fullscreen mode
  1. Create the device file:
mknod /dev/char_device c <major_number> 0
chmod 666 /dev/char_device
Enter fullscreen mode Exit fullscreen mode

Replace <major_number> with the major number from the kernel log. To read the logs from the module, use the following command:

dmesg | grep char_device
Enter fullscreen mode Exit fullscreen mode
  1. Test the driver:
echo "Hello, Device!" > /dev/char_device
cat /dev/char_device
Enter fullscreen mode Exit fullscreen mode

Raspberry Pi Console

Step 6: Unloading the Driver

When you're done, unload the driver:

rmmod char_driver
Enter fullscreen mode Exit fullscreen mode

Remove the device file:

rm /dev/char_device
Enter fullscreen mode Exit fullscreen mode

Conclusion

Understanding the core concepts of device files, major and minor numbers, and file operations is important for creating a character device driver for Raspberry Pi using Buildroot. You can build and test a character device driver on your Raspberry Pi by following the instructions in this article. This fundamental understanding paves the way for more intricate driver development and deeper interactions with hardware components in embedded systems.

SOLI DEO GLORIA

Helpful Articles:

Writing and Inserting a 'Hello World' Kernel Module for the Beaglebone Black (Buildroot)

Top comments (0)