Creating a Character Device Driver for Raspberry Pi Using Buildroot
Tags: embedded, linux, cpp
Published: 2024-06-15 04:16:15

#### 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](https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch03s02.html). 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](https://dev.to/devdoesit17/build-a-simple-linux-kernel-using-buildroot-4d29) 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:

```sh
mkdir -p buildroot/package/simple_char_driver
cd buildroot/package/simple_char_driver
touch char_driver.c
```

Open `char_driver.c` in your preferred text editor:

```c
#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);

```

### Step 3: Integrating the Driver with Buildroot

Create a `Config.in` file in the `simple_char_driver` directory:

```sh
touch Config.in
```

Edit `Config.in`:

```sh
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.
```

Create a `simple_char_driver.mk` file in the same directory:

```sh
touch simple_char_driver.mk
```

Edit `simple_char_driver.mk`:

```makefile
################################################################################
# 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))
```

Create a `Makefile` file in the same directory:

```sh
touch Makefile
```

Edit `Makefile`:

```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
```

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

```sh
cd ..
echo "source \"package/char_device_driver/Config.in\"" >> Config.in
```

### Step 4: Building the Driver with Buildroot

Reconfigure Buildroot to include your driver:

```sh
cd ..
make menuconfig
```

Navigate to:

- `Simple Device Driver` and enable it.

![Main Menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqm1cxxyn5gpc2uerazm.png)

Build the Buildroot system:

```sh
make
```

After two hours later...

![Two Hours Later Meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2sy29ldczvoqkzm9etw.jpg)

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`:

```sh
sudo mount -o loop output/images/rootfs.ext4 /mnt
```

Check the `lib/modules` folder

![Modules Folder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6sz0qqeinhrn3u1efp1l.png)

### 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**:

    ```sh
    insmod /lib/modules/$(uname -r)/char_driver.ko
    ```

3. **Create the device file**:

    ```sh
    mknod /dev/char_device c <major_number> 0
    chmod 666 /dev/char_device
    ```

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

    ```sh
    dmesg | grep char_device
    ```

4. **Test the driver**:

    ```sh
    echo "Hello, Device!" > /dev/char_device
    cat /dev/char_device
    ```

![Raspberry Pi Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8uujmlj1ub37zrcz539.png)

### Step 6: Unloading the Driver

When you're done, unload the driver:

```sh
rmmod char_driver
```

Remove the device file:

```sh
rm /dev/char_device
```

## 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)](https://blog.billvanleeuwen.ca/writing-and-inserting-a-hello-world-kernel-module-for-the-beaglebone-black-buildroot)