Register Linux Character Device Driver- Free Linux Device Drivers Course

 

PREV_LEC | NEXT_LEC

Register Linux Character Device Driver

Free Linux Device Drivers Course — Chapter 4: Character Device Drivers

Lecture 4 of Chapter 4
Kernel 6.4+ API
Hands-On Driver

Topics Covered

register character device linux kernel
cdev_init cdev_add example
class_create device_create linux
copy_to_user copy_from_user example
free linux device drivers course
free embedded systems course

What You Will Learn

This lecture completes the character device basics in our free linux kernel development course. Using the file operations table from the previous lecture, you will now register a character device so it appears as a real node in /dev, and you will exchange real data with it using safe kernel-to-user-space copy functions.

  • The four steps needed to bring a character device to life in the kernel
  • How alloc_chrdev_region(), cdev_init(), and cdev_add() fit together
  • The modern kernel 6.4+ class_create() signature (the owner argument is gone)
  • How device_create() makes your driver show up in /dev automatically
  • A complete, working example driver you can build and test

Prerequisites

  • Completed the previous lecture on struct file_operations in this free linux device drivers course
  • A Linux 6.x machine or VM with kernel headers and build-essential installed
  • Root or sudo access to load and unload kernel modules

The Four Steps To Register A Character Device

Bringing a character device online in modern Linux always follows the same pattern, regardless of what the device actually does:

Character Device Registration Flow
1. alloc_chrdev_region() — reserve a major/minor range
2. class_create() — create a device class under /sys/class
3. cdev_init() + cdev_add() — attach file_operations and register with the kernel
4. device_create() — create the visible node under /dev

Step 1: Reserving a Major and Minor Number

alloc_chrdev_region() asks the kernel to dynamically hand out a major number and a range of minor numbers, instead of you hardcoding one that might already be taken. This is the recommended approach for any modern driver.

Reserving Device Numbers
dev_t ep_devnum;
alloc_chrdev_region(&ep_devnum, 0, 1, “ep_pulse”);

Step 2: Creating a Device Class (Kernel 6.4+ API)

Starting with kernel 6.4, class_create() no longer takes a struct module *owner argument — it now takes only the class name. If your reference material still shows two arguments, it was written for an older kernel and will fail to compile on 6.4 and newer.

Old vs New class_create() Signature
/* Before kernel 6.4 */
struct class *cls = class_create(THIS_MODULE, “ep_class”);/* Kernel 6.4 and later */
struct class *cls = class_create(“ep_class”);

Step 3: cdev_init() and cdev_add()

This is where your file operations table from the previous lecture finally gets attached to a live device object. cdev_init() links the struct cdev to your fops table, and cdev_add() registers it with the kernel core so system calls can start reaching it.

Step 4: device_create() — Making /dev/ep_pulse Appear

device_create() is what actually triggers udev to create the device node file under /dev. Without this step, your device would be registered internally but invisible to user space.

Complete Working Example: ep_pulse Driver

Below is a complete, original example driver named ep_pulse. It stores a small internal message, lets user space read it back, and lets user space overwrite it with write(). All kernel-to-user data movement uses copy_to_user() and copy_from_user(), the safe functions referenced in the previous lecture.

ep_pulse.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>#define EP_BUF_SIZE 64static dev_t ep_devnum;
static struct cdev ep_cdev;
static struct class *ep_class;
static char ep_msg[EP_BUF_SIZE] = “hello from ep_pulse\n”;
static size_t ep_msg_len = 20;static int ep_pulse_open(struct inode *inode, struct file *filp)
{
pr_info(“ep_pulse: opened\n”);
return 0;
}

static int ep_pulse_release(struct inode *inode, struct file *filp)
{
pr_info(“ep_pulse: closed\n”);
return 0;
}

static ssize_t ep_pulse_read(struct file *filp, char __user *buf,
size_t len, loff_t *pos)
{
if (*pos >= ep_msg_len)
return 0;
if (len > ep_msg_len – *pos)
len = ep_msg_len – *pos;
if (copy_to_user(buf, ep_msg + *pos, len))
return -EFAULT;
*pos += len;
return len;
}

static ssize_t ep_pulse_write(struct file *filp, const char __user *buf,
size_t len, loff_t *pos)
{
if (len >= EP_BUF_SIZE)
len = EP_BUF_SIZE – 1;
if (copy_from_user(ep_msg, buf, len))
return -EFAULT;
ep_msg[len] = ‘\0’;
ep_msg_len = len;
pr_info(“ep_pulse: stored %zu bytes\n”, len);
return len;
}

static const struct file_operations ep_pulse_fops = {
.owner     = THIS_MODULE,
.open      = ep_pulse_open,
.release   = ep_pulse_release,
.read      = ep_pulse_read,
.write     = ep_pulse_write,
};

static int __init ep_pulse_init(void)
{
int ret;

ret = alloc_chrdev_region(&ep_devnum, 0, 1, “ep_pulse”);
if (ret)
return ret;

ep_class = class_create(“ep_class”);
if (IS_ERR(ep_class)) {
unregister_chrdev_region(ep_devnum, 1);
return PTR_ERR(ep_class);
}

cdev_init(&ep_cdev, &ep_pulse_fops);
ep_cdev.owner = THIS_MODULE;
ret = cdev_add(&ep_cdev, ep_devnum, 1);
if (ret) {
class_destroy(ep_class);
unregister_chrdev_region(ep_devnum, 1);
return ret;
}

device_create(ep_class, NULL, ep_devnum, NULL, “ep_pulse”);
pr_info(“ep_pulse: driver loaded, major=%d\n”, MAJOR(ep_devnum));
return 0;
}

static void __exit ep_pulse_exit(void)
{
device_destroy(ep_class, ep_devnum);
cdev_del(&ep_cdev);
class_destroy(ep_class);
unregister_chrdev_region(ep_devnum, 1);
pr_info(“ep_pulse: driver unloaded\n”);
}

module_init(ep_pulse_init);
module_exit(ep_pulse_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“EmbeddedPathashala”);
MODULE_DESCRIPTION(“Minimal character device demo driver”);

Building and Testing the Driver

Create a matching Makefile in the same folder:

Makefile

obj-m += ep_pulse.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Now build, load, and test the driver from a terminal:

Command Line Session
$ make
$ sudo insmod ep_pulse.ko
$ ls -l /dev/ep_pulse
crw——- 1 root root 240, 0 Jul 20 10:00 /dev/ep_pulse$ cat /dev/ep_pulse
hello from ep_pulse$ echo “test message” | sudo tee /dev/ep_pulse
test message$ cat /dev/ep_pulse
test message

$ dmesg | tail -n 4
[ 1234.567] ep_pulse: driver loaded, major=240
[ 1234.789] ep_pulse: opened
[ 1235.001] ep_pulse: stored 13 bytes
[ 1235.002] ep_pulse: closed

$ sudo rmmod ep_pulse

Notice that cat only prints the message once and then stops. That is the expected behavior — once the read position reaches the end of the stored message, ep_pulse_read() returns 0, which every standard tool interprets as end-of-file.

Real-World Use Cases

  • Exposing a small configuration string to user space, similar to a simplified sysfs attribute
  • Simple sensor drivers that report the latest reading as text on read()
  • Debug interfaces used during hardware bring-up before a full driver is ready

Common Mistakes and Troubleshooting

Mistake Symptom Fix
Passing THIS_MODULE to class_create() Build error on kernel 6.4+ Call class_create(“name”) with a single argument
Not checking IS_ERR() on class_create() Crash later if class creation silently failed Always check with IS_ERR() and unwind on failure
Never returning 0 from read() cat or similar tools hang forever Return 0 once *pos reaches the end of available data
Forgetting device_destroy()/cdev_del() in exit() Stale /dev node or leaked device number on unload Unwind every step from init() in reverse order

Best Practices

  • Always unwind setup steps in reverse order on both the error path and the exit path.
  • Keep the internal buffer size fixed and always null-terminate after a write.
  • Log meaningful messages with pr_info() so dmesg tells a clear story during testing.

Performance Considerations

For small control-style devices like this one, the cost of copy_to_user()/copy_from_user() is negligible. These functions matter far more once you are moving large buffers frequently, which is a topic for later chapters on DMA and memory mapping.

Security Considerations

This example clamps the write length to the buffer size before calling copy_from_user(), preventing a user program from overflowing ep_msg. Always bound-check lengths supplied by user space before using them in any copy or memory operation.

Summary and Key Takeaways

  • Registering a character device always follows the same four steps: reserve numbers, create a class, attach and add the cdev, then create the device node.
  • Kernel 6.4 removed the owner argument from class_create() — always check which kernel version your reference material targets.
  • copy_to_user() and copy_from_user() are the only safe way to move data across the kernel/user boundary.
  • Returning 0 from read() at the right time is what makes tools like cat behave correctly.

Frequently Asked Questions

Why does class_create() only take one argument now?

Since kernel 6.4, the struct module *owner argument was removed from class_create(). Older tutorials showing two arguments are written for kernels before 6.4.

What major number will my device get?

Whatever the kernel assigns dynamically through alloc_chrdev_region(). Check it with cat /proc/devices or the dmesg output printed in this lecture’s example.

Why did cat only print the message once?

Because ep_pulse_read() returns 0 once the current read position reaches the end of the stored message, which every standard tool treats as end-of-file.

Do I need udev rules to see the device node?

No. device_create() triggers udev automatically on a standard desktop or server Linux distribution, so the node appears in /dev without any manual rule.

What happens if I forget to call device_destroy() on unload?

The /dev node can be left behind or become stale, and reloading the module may fail or behave unpredictably until the system is rebooted.

Can this driver support multiple devices at once?

Not as written, since it uses a single dev_t and a single cdev. Supporting several devices means allocating more than one minor number and looping the cdev_init/cdev_add/device_create steps, which is covered later in this free linux kernel development course.

Continue This Free Linux Device Drivers Course

You now have a complete, working character device driver. The next lecture builds on this foundation with ioctl commands and device-specific control operations.

PREV_LEC | NEXT_LEC

 

Leave a Reply

Your email address will not be published. Required fields are marked *