Character Device Driver Registration and I/O Memory Mapping in Linux

Early Registration Method

The register_chrdev family of functions, combined with APIs for creating device classes and device files, was commonly used in the 2.4 kernel series. Its main advantage is simplicity, but it does not allow specifying a minor device number.

int register_chrdev(unsigned int major, const char *name,
                    const struct file_operations *fops);
/*
 * Register a major number for a character device.
 * major – desired major number, or 0 for dynamic allocation.
 * name  – driver/device identifier shown in /proc/devices.
 * fops  – file operations associated with this device.
 */

void unregister_chrdev(unsigned int major, const char *name);
/*
 * Release the major number previously obtained with register_chrdev.
 */

The cdev Registration Method

Beginning with the 2.6 kernel, character device registration moved to the cdev model. The driver can control both the major and minor numbers explicitly, but the process involves more functions and steps.

A cdev structure represents a character device. It contains a dev_t device number and a file_operations pointer that defines the driver methods (identical in concept to the earlier method).

The device number is a 32‑bit unsigend quantity: 12 bits for the major number (identifying the driver) and 20 bits for the minor number (identifying the sepcific device instance). For example, an LED driver might have one major number, while two individual LEDs would receive minor numbers 1 and 2.

struct cdev {
    struct kobject kobj;               /* kernel object, managed by core */
    struct module *owner;
    const struct file_operations *ops; /* driver methods, filled by the driver */
    struct list_head list;
    dev_t dev;                         /* device number */
    unsigned int count;
};

The device number is stored in a u32 type defined in the kernel headers:

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t     dev_t;

The following kernel helper macros manipulate device numbers:

dev_t mkdev = MKDEV(major, minor);   /* combine major and minor */
int maj     = MAJOR(dev_id);         /* extract major */
int min     = MINOR(dev_id);         /* extract minor */

Device Number Allocation and cdev Registration APIs

/* Static allocation – driver specifies the desired range */
int register_chrdev_region(dev_t from, unsigned int count,
                           const char *name);
/*
 * from  – starting device number (minor often 0).
 * count – number of consecutive device numbers.
 * name  – identifier associated with the range.
 * Returns 0 on success.
 */

/* Dynamic allocation – kernel picks an available major */
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
                        unsigned int count, const char *name);
/*
 * dev       – output parameter, receives the allocated base device number.
 * firstminor – first requested minor number.
 * count     – number of minors.
 * name      – device identifier.
 * Returns 0 on success.
 */

/* Initialise a cdev structure */
void cdev_init(struct cdev *cdev, const struct file_operations *fops);

/* Add (register) a cdev to the system */
int cdev_add(struct cdev *p, dev_t dev, unsigned int count);
/*
 * p     – pointer to the cdev.
 * dev   – base device number.
 * count – number of minors covered by this cdev.
 */

/* Remove a previously added cdev */
void cdev_del(struct cdev *p);

/* Release a device number range */
void unregister_chrdev_region(dev_t from, unsigned int count);

Static Registration Example

#define MYDEV_MAJOR 250
#define MYDEV_MINOR 0

static dev_t      dev_id;
static struct cdev example_cdev;

struct file_operations example_fops = {
    .owner = THIS_MODULE,
    .open  = mydev_open,
};

int __init mydev_init(void)
{
    dev_id = MKDEV(MYDEV_MAJOR, MYDEV_MINOR);

    if (register_chrdev_region(dev_id, 1, "mydev") != 0) {
        printk(KERN_ERR "Failed to register device number\n");
        return -ENODEV;
    }

    cdev_init(&example_cdev, &example_fops);
    example_cdev.owner = THIS_MODULE;
    example_cdev.ops   = &example_fops;

    if (cdev_add(&example_cdev, dev_id, 1) != 0) {
        unregister_chrdev_region(dev_id, 1);
        return -ENODEV;
    }
    return 0;
}

void __exit mydev_exit(void)
{
    cdev_del(&example_cdev);
    unregister_chrdev_region(dev_id, 1);
}

Dynamic Registration Example

static dev_t      dev_id;
static struct cdev example_cdev;

struct file_operations example_fops = {
    .owner = THIS_MODULE,
    .open  = mydev_open,
};

int __init mydev_init(void)
{
    if (alloc_chrdev_region(&dev_id, 0, 1, "mydev") != 0) {
        printk(KERN_ERR "Device number allocation failed\n");
        return -ENODEV;
    }
    printk(KERN_INFO "mydev: major = %d, minor = %d\n",
           MAJOR(dev_id), MINOR(dev_id));

    cdev_init(&example_cdev, &example_fops);
    example_cdev.owner = THIS_MODULE;
    example_cdev.ops   = &example_fops;

    if (cdev_add(&example_cdev, dev_id, 1) != 0) {
        unregister_chrdev_region(dev_id, 1);
        return -ENODEV;
    }
    return 0;
}

void __exit mydev_exit(void)
{
    cdev_del(&example_cdev);
    unregister_chrdev_region(dev_id, 1);
}

Creating Device Nodes

After the cdev is registered, a device node must be made available to user space. The following example builds on the dynamic registration pattern and automatically creates the node in /dev:

static dev_t      dev_id;
static struct cdev example_cdev;
static struct class *samp_class;

struct file_operations example_fops = {
    .owner = THIS_MODULE,
    .open  = mydev_open,
};

int __init mydev_init(void)
{
    if (alloc_chrdev_region(&dev_id, 0, 1, "mydev") != 0)
        return -ENODEV;

    printk(KERN_INFO "mydev: major = %d, minor = %d\n",
           MAJOR(dev_id), MINOR(dev_id));

    cdev_init(&example_cdev, &example_fops);
    example_cdev.owner = THIS_MODULE;
    example_cdev.ops   = &example_fops;

    if (cdev_add(&example_cdev, dev_id, 1) != 0) {
        unregister_chrdev_region(dev_id, 1);
        return -ENODEV;
    }

    samp_class = class_create(THIS_MODULE, "mydev");
    if (IS_ERR(samp_class)) {
        cdev_del(&example_cdev);
        unregister_chrdev_region(dev_id, 1);
        return PTR_ERR(samp_class);
    }

    class_device_create(samp_class, NULL, dev_id, NULL, "mydev0");
    return 0;
}

void __exit mydev_exit(void)
{
    class_device_destroy(samp_class, dev_id);
    class_destroy(samp_class);
    cdev_del(&example_cdev);
    unregister_chrdev_region(dev_id, 1);
}

ioremap: Mapping Physical Addresses

Device drivers often need to access hardware registers. The ioremap() function maps a physical address range into kernel virtual memory, and iounmap() releases the mapping.

#define GPIO_CTL_BASE   0x01C20000
#define GPIO_DATA_BASE  0x01C20004

static unsigned long __iomem *ctl_reg;
static unsigned long __iomem *data_reg;

/* ... inside the init function ... */

ctl_reg  = ioremap(GPIO_CTL_BASE, 8);
data_reg = ioremap(GPIO_DATA_BASE, 8);
if (!ctl_reg || !data_reg) {
    /* handle error */
}

/* ... inside the exit function ... */
iounmap(ctl_reg);
iounmap(data_reg);

The same cleanup order applies: unmap regions, destroy the device node, delete the cdev, and release the device number.

Tags: Linux kernel character driver cdev device number register_chrdev_region

Posted on Fri, 23 Oct 2026 16:26:10 +0000 by bubbasheeko