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
-
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. - 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.
-
File Operations: Character device drivers define a set of file operations, such as
open
,close
,read
,write
, andioctl
, 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
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);
Step 3: Integrating the Driver with Buildroot
Create a Config.in
file in the simple_char_driver
directory:
touch Config.in
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.
Create a simple_char_driver.mk
file in the same directory:
touch simple_char_driver.mk
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))
Create a Makefile
file in the same directory:
touch Makefile
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
Add the package to Buildroot's package/Config.in
:
cd ..
echo "source \"package/char_device_driver/Config.in\"" >> Config.in
Step 4: Building the Driver with Buildroot
Reconfigure Buildroot to include your driver:
cd ..
make menuconfig
Navigate to:
-
Simple Device Driver
and enable it.
Build the Buildroot system:
make
After two hours later...
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:
This can be done by check the current folder
output/target/lib/modules
.Or you can mount the
rootfs.ext4
:
sudo mount -o loop output/images/rootfs.ext4 /mnt
Check the lib/modules
folder
Step 5: Running the Driver on Raspberry Pi
Flash the Buildroot image to your SD card and boot your Raspberry Pi with it.
Load the driver module:
insmod /lib/modules/$(uname -r)/char_driver.ko
- Create the device file:
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:
dmesg | grep char_device
- Test the driver:
echo "Hello, Device!" > /dev/char_device
cat /dev/char_device
Step 6: Unloading the Driver
When you're done, unload the driver:
rmmod char_driver
Remove the device file:
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)
Top comments (0)