Practical Guide to Linux Character Device Drivers
1. Core Concepts Review (Understand the Basics First)
1.1 What is a Character Device Driver?
A character device driver is a kernel program that operates character devices, where data is accessed sequentially as a "byte stream." Its core functions are:
- Providing unified file operation interfaces (open/read/write/close) to applications;
- Interfacing with hardware to implement specific hardware control logic (for demonstration purposes, printk is used here instead of actual hardware operations).
1.2 Three Essential Elements of Driver Implementation (Your Focus)
- Hardware Operation Methods: Implement functions like open/read/write/close to encapsulate hardware control logic;
- Device Number Allocation: The device number is the driver's "ID card" (32-bit, with 12 bits for the major number and 20 bits for the minor number). The major number represents the device type, while the minor number distinguishes devices of the same type;
- Driver Registration: Register the driver's operation methods and device number with the kernel so the kernel can recognize and associate the device.
1.3 Key Tools and Commands
| Tool/Command | Purpose |
|---|---|
arm-linux-gnueabihf-gcc |
Cross-compiler for compiling ARM architecture applications |
make zImage |
Compile the Linux kernel, bundling the driver module into the kernel image |
cat /proc/devices |
View registered driver device numbers (major number + device name) |
mknod |
Manually create a device node (applications access the driver via this node) |
dmesg |
View kernel print messages (output from printk in the driver) |
2. In-Depth Analysis of Driver Code (Your demo_driver.c)
Your demo_driver.c perfectly implements the three essential elements of a character device driver. Let's break down the core logic step by step:
2.1 Step 1: Implement Hardware Operation Methods (file_operations Structure)
The core of the driver is the struct file_operations structure, which defines the operation interfaces callable by applications. The corresponding code is:
c
// Define hardware operation methods (for demonstration, only print debug messages)
static int open(struct inode *inode, struct file *file) {
printk("demo1_init open\n"); // Triggered when the application calls open
return 0;
}
static ssize_t read(struct file *file, char __user *buf, size_t size, loff_t *loff) {
printk("demo1_init read\n"); // Triggered when the application calls read
return 0;
}
static ssize_t write(struct file *file, const char __user *buf, size_t size, loff_t *loff) {
printk("demo1_init write\n"); // Triggered when the application calls write
return 0;
}
static int close(struct inode *inode, struct file *file) {
printk("demo1_init close\n"); // Triggered when the application calls close
return 0;
}
// Bind operation methods to the file_operations structure
static struct file_operations fops = {
.owner = THIS_MODULE, // Declare the module owning the driver (prevents accidental unloading)
.open = open, // Associate the open function
.read = read, // Associate the read function
.write = write, // Associate the write function
.release = close // Associate the close function (release is called when the application closes)
};
Principle : When an application calls open("/dev/demo1", O_RDWR), the kernel finds the corresponding driver via the device number and then calls the bound open function in the driver.
2.2 Step 2: Apply for a Device Number (Static Allocation Method)
The device number is the "agreement credential" between the driver and the kernel. You used static allocation (specifying a fixed major number 255 and minor number 0). Code analysis:
c
#define DEV_MAJOR 255 // Major number (custom, must ensure it's not already in use)
#define DEV_MINOR 0 // Minor number
#define DEV_NAME "demo1" // Device name
static dev_t dev; // Stores the combined 32-bit device number
// Combine major and minor numbers: MKDEV(major, minor)
dev = MKDEV(DEV_MAJOR, DEV_MINOR);
// Statically allocate the device number: parameters (device number, number of devices, device name)
register_chrdev_region(dev, 1, DEV_NAME);
Note : Static allocation requires ensuring the major number is not already used by another driver. Check occupied major numbers via cat /proc/devices. If you don't want to specify manually, you can use alloc_chrdev_region for dynamic allocation (the kernel automatically assigns an unused major number).
2.3 Step 3: Register the Driver with the System (cdev Structure)
The kernel manages character device drivers via the cdev structure. You need to associate file_operations with the device number and register it with the kernel. Code analysis:
c
static struct cdev cdev; // Core structure for character device drivers
// Initialize cdev: bind cdev with file_operations
cdev_init(&cdev, &fops);
// Register cdev with the kernel: parameters (cdev pointer, device number, number of devices)
cdev_add(&cdev, dev, 1);
Driver Loading/Unloading : Define the driver's loading and unloading entry points via the module_init and module_exit macros:
c
// Executed when the driver loads (during insmod or kernel startup)
static int __init demo1_init(void) {
// Device number allocation + driver registration logic (explained above)
printk("-------------------------------------- demo1_init\n");
return 0;
}
// Executed when the driver unloads (rmmod)
static void __exit demo1_exit(void) {
cdev_del(&cdev); // Remove cdev from the kernel
unregister_chrdev_region(dev, 1); // Release the device number
printk("-------------------------------------- demo1_exit\n");
}
module_init(demo1_init); // Register the loading entry
module_exit(demo1_exit); // Register the unloading entry
3. Application Code Analysis (Your main.c)
The application accesses the driver via the "device node" (/dev/demo1), essentially calling the driver's file_operations interfaces. Code logic:
c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
// Open the device node: corresponds to the driver's open function
int fd = open("/dev/demo1", O_RDWR);
if (fd < 0) {
perror("open demo1 failed"); // Open failed (e.g., device node not created)
return 1;
}
char buf[10] = {0};
read(fd, buf, sizeof buf); // Read from the driver: corresponds to the driver's read function
write(fd, buf, sizeof buf); // Write to the driver: corresponds to the driver's write function
close(fd); // Close the driver: corresponds to the driver's close function
return 0;
}
Interaction Flow Between Application and Driver :
app: open() → kernel: sys_open() → driver: open() → kernel returns file descriptor fd → app: read/write/close() → driver: corresponding function executes
4. Complete Practical Steps (From Compilation to Verification)
You've completed the code writing and application compilation. Now follow these steps to compile, flash, and verify the driver:
4.1 Step 1: Add the Driver to the Kernel (Critical!)
To load the driver during kernel startup, add demo_driver.c to the kernel source and modify the corresponding Makefile and Kconfig (refer to previous kernel compilation notes):
-
Copy
demo_driver.cto the kernel source'sdrivers/char/directory (default directory for character device drivers); -
Modify
drivers/char/Makefile, adding the line:makefileobj-$(CONFIG_DEMO_DRIVER) += demo_driver.o -
Modify
drivers/char/Kconfig, adding the driver configuration option:kconfigconfig DEMO_DRIVER tristate "Demo Character Device Driver" help This is a demo character device driver for IMX6ULL. -
Enable the driver via
menuconfig:bashmake ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfigNavigate to "Character devices" → select "Demo Character Device Driver" (choose
Yto compile into the kernel), then save and exit.
4.2 Step 2: Compile the Kernel to Generate zImage
bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
After successful compilation, the zImage image containing the demo driver will be generated in the arch/arm/boot/ directory.
4.3 Step 3: Flash the Kernel and Boot the Development Board
Download zImage and the corresponding device tree (imx6ull.dtb) to the development board via TFTP or flash them to an SD card, then boot the development board.
4.4 Step 4: Verify Driver Registration Success
-
Check Device Number : Execute the following command on the development board terminal. If you see
255 demo1, it indicates successful driver registration:bashcat /proc/devices -
Create Device Node : Applications access the driver through device nodes. Run the following command to create one (ensure the major/minor numbers match the driver):
bashmknod /dev/demo1 c 255 0c: Denotes a character device.255: Major device number.0: Minor device number.
4.5 Step 5: Run Application for Verification
-
Copy the compiled
demo1appto the development board via NFS (refer to earlier NFS mounting notes). -
Run the application:
bash./demo1app -
Check Driver Output : Execute
dmesg. If the following output appears, it confirms successful interaction between the application and driver:-------------------------------------- demo1_init demo1_init open demo1_init read demo1_init write demo1_init close
5. Troubleshooting Common Issues
5.1 demo1 Not Listed in cat /proc/devices
- Driver Not Compiled into Kernel : Verify
DEMO_DRIVERis enabled inmenuconfigand recompile the kernel. - Major Number Conflict : Modify
DEV_MAJORto an unused value (e.g., 240) and recompile.
5.2 Application Fails to open("/dev/demo1")
- Missing Device Node : Execute
mknod /dev/demo1 c 255 0. - Insufficient Permissions : Run
chmod 777 /dev/demo1to grant full access.
5.3 printk Output Not Visible in dmesg
- Kernel Log Level Restriction : Execute
echo 8 > /proc/sys/kernel/printkto enable all log levels.