2. 字符设备驱动基础

字符设备驱动是嵌入式 Linux 驱动开发中最基础的驱动类型,触摸屏驱动在底层同样遵循字符设备的框架。本章将深入剖析字符设备驱动的三大核心要素:file_operations 结构体、cdev 注册机制以及设备号的分配与注销,为后续与 input 子系统的联动打下坚实基础。

2.1 字符设备驱动核心架构

字符设备驱动的整体工作流程可以概括为:用户空间通过系统调用(open、read、write 等)访问设备文件,VFS(虚拟文件系统)根据设备文件的主设备号找到对应的驱动程序,然后调用驱动中注册的 file_operations 回调函数。下图展示了这一核心架构:

字符设备驱动核心架构 用户空间应用程序 系统调用层 (VFS) 设备文件 (/dev/xxx) 内核空间 根据主设备号查找 cdev 对象 cdev 对象 (字符设备) file_operations (回调函数表) 驱动实现:open / read / write / ioctl / release 等回调函数 用户空间 → VFS → 设备号 → cdev → file_operations → 驱动回调
关键理解:字符设备驱动的本质是将设备文件的操作(open/read/write)映射到驱动开发者实现的回调函数。内核通过 cdev 结构体将设备号与 file_operations 绑定在一起。

2.2 file_operations 结构体详解

file_operations 是字符设备驱动的核心接口,定义在 <linux/fs.h> 中。它是一个函数指针结构体,每个成员对应一个系统调用。驱动开发者只需实现需要的回调函数,未实现的成员可以设为 NULL,内核会使用默认行为。

成员名称 对应系统调用 功能描述 触摸屏驱动常用性
owner — 指向拥有该模块的指针,通常设为 THIS_MODULE 必须
open open() 打开设备时调用,用于初始化硬件、分配资源 常用
release close() 关闭设备时调用,用于释放资源、关闭硬件 常用
read read() 从设备读取数据,返回给用户空间 常用(读取触摸坐标)
write write() 向设备写入数据,如发送配置命令 较少用
unlocked_ioctl ioctl() 设备控制命令,用于设置参数、获取状态 常用(校准、模式切换)
poll poll() / select() 查询设备是否可读写,用于非阻塞 I/O 常用(事件通知)
mmap mmap() 将设备内存映射到用户空间 较少用
触摸屏驱动典型 file_operations 实现:
static struct file_operations touch_fops = {
    .owner   = THIS_MODULE,
    .open    = touch_open,
    .release = touch_release,
    .read    = touch_read,
    .poll    = touch_poll,
    .unlocked_ioctl = touch_ioctl,
};

2.3 设备号分配与注销

设备号是内核识别字符设备的唯一标识,由主设备号(major)和次设备号(minor)组成。主设备号对应驱动程序,次设备号对应同一驱动管理的不同设备实例。

2.3.1 设备号的数据结构

内核使用 dev_t 类型(32 位无符号整数)表示设备号,其中高 12 位为主设备号,低 20 位为次设备号。操作宏定义在 <linux/kdev_t.h>:

宏/函数 功能 示例
MAJOR(dev) 从 dev_t 中提取主设备号 int major = MAJOR(dev);
MINOR(dev) 从 dev_t 中提取次设备号 int minor = MINOR(dev);
MKDEV(major, minor) 由主次设备号合成 dev_t dev_t dev = MKDEV(200, 0);

2.3.2 静态分配 vs 动态分配

内核提供两种设备号分配方式:

  • 静态分配:使用 register_chrdev_region(),需要开发者手动指定主设备号。适用于已知且未被占用的设备号,但容易与已有驱动冲突。
  • 动态分配:使用 alloc_chrdev_region(),由内核自动分配未使用的主设备号。推荐用于大多数驱动,避免冲突。
动态分配设备号示例:
#include <linux/fs.h>
#include <linux/device.h>

static dev_t touch_dev_num;  // 全局设备号

static int __init touch_init(void)
{
    int ret;

    // 动态分配设备号:起始次设备号 0,数量 1,名称 "touch"
    ret = alloc_chrdev_region(&touch_dev_num, 0, 1, "touch");
    if (ret < 0) {
        pr_err("Failed to allocate device number\n");
        return ret;
    }

    pr_info("Allocated device number: major=%d, minor=%d\n",
            MAJOR(touch_dev_num), MINOR(touch_dev_num));
    return 0;
}

static void __exit touch_exit(void)
{
    // 注销设备号
    unregister_chrdev_region(touch_dev_num, 1);
    pr_info("Device number unregistered\n");
}
注意:设备号是有限的资源,主设备号范围 0~511(部分架构可能不同)。动态分配可以避免手动指定带来的冲突,但需要在模块退出时使用 unregister_chrdev_region() 正确释放,否则下次加载时可能分配失败。

2.4 cdev 结构体注册与注销

cdev 是内核中表示字符设备的核心结构体,定义在 <linux/cdev.h>。它将设备号与 file_operations 绑定在一起,并加入内核的设备链表。

2.4.1 cdev 初始化与注册流程

注册 cdev 需要以下步骤:

  1. 分配 cdev 对象:使用 cdev_alloc() 动态分配,或静态定义后使用 cdev_init() 初始化。
  2. 设置 file_operations:将 cdev 的 ops 成员指向已实现的 file_operations 结构体。
  3. 设置 owner:通常设为 THIS_MODULE,用于模块引用计数。
  4. 注册到内核:调用 cdev_add(),将 cdev 与设备号关联。
完整 cdev 注册示例:
#include <linux/cdev.h>

static struct cdev touch_cdev;  // 静态定义 cdev 对象

static int __init touch_init(void)
{
    int ret;
    dev_t dev_num;

    // 1. 动态分配设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, "touch");
    if (ret < 0)
        return ret;

    // 2. 初始化 cdev
    cdev_init(&touch_cdev, &touch_fops);
    touch_cdev.owner = THIS_MODULE;

    // 3. 注册 cdev 到内核
    ret = cdev_add(&touch_cdev, dev_num, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev_num, 1);
        pr_err("Failed to add cdev\n");
        return ret;
    }

    pr_info("Touch device registered: major=%d minor=%d\n",
            MAJOR(dev_num), MINOR(dev_num));
    return 0;
}

static void __exit touch_exit(void)
{
    // 1. 注销 cdev
    cdev_del(&touch_cdev);

    // 2. 释放设备号
    unregister_chrdev_region(touch_dev_num, 1);

    pr_info("Touch device unregistered\n");
}

2.4.2 cdev 注册与注销的完整生命周期

下图展示了从设备号分配到 cdev 注册,再到模块卸载时释放的完整流程:

cdev 注册与注销生命周期 1. 分配设备号 alloc_chrdev_region() 2. 初始化 cdev cdev_init() + ops 3. 注册 cdev cdev_add() 设备运行中:用户访问 /dev/touch open / read / write / ioctl / poll 4. 注销 cdev cdev_del() 5. 释放设备号 unregister_chrdev_region() 注册顺序:alloc_chrdev_region → cdev_init → cdev_add | 注销顺序:cdev_del → unregister_chrdev_region

2.5 设备节点自动创建:udev 与 class

仅注册 cdev 还不够,用户空间需要通过设备文件(如 /dev/touch0)访问设备。传统方式使用 mknod 手动创建设备节点,现代 Linux 系统使用 udev 自动管理。驱动开发者需要创建 device class 和 device,udev 会自动在 /dev/ 下生成对应的设备文件。

自动创建设备节点示例:
#include <linux/device.h>

static struct class *touch_class;
static struct device *touch_device;

static int __init touch_init(void)
{
    // ... 分配设备号、注册 cdev ...

    // 创建设备类
    touch_class = class_create(THIS_MODULE, "touch_class");
    if (IS_ERR(touch_class)) {
        // 错误处理
    }

    // 创建设备节点 /dev/touch0
    touch_device = device_create(touch_class, NULL, dev_num,
                                 NULL, "touch%d", 0);
    if (IS_ERR(touch_device)) {
        // 错误处理
    }
    return 0;
}

static void __exit touch_exit(void)
{
    // 销毁设备节点
    device_destroy(touch_class, dev_num);
    // 销毁设备类
    class_destroy(touch_class);

    cdev_del(&touch_cdev);
    unregister_chrdev_region(dev_num, 1);
}

2.6 本章小结

核心概念 关键函数/结构体 注意事项
file_operations struct file_operations 未使用的回调设为 NULL;owner 必须设为 THIS_MODULE
设备号分配 alloc_chrdev_region() / register_chrdev_region() 优先使用动态分配;模块退出时必须释放
cdev 注册 cdev_init() / cdev_add() cdev_add 成功后,设备立即可用;注销时先 cdev_del
设备节点 class_create() / device_create() 配合 udev 自动生成 /dev 节点;卸载时需销毁