01-字符设备驱动基础.md 39 KB


title: 字符设备驱动基础 tags: [embedded-linux, driver, chardev, kernel] created: 2026-09-17 updated: 2026-09-17 pdf_ref:

  • "46_第四十章 字符设备驱动开发.pdf"
  • "47_第四十一章 嵌入式Linux LED驱动开发实验.pdf" ---

字符设备驱动基础

1. 概述与原理

1.1 字符设备定义

字符设备是 Linux 驱动中最基本的一类设备驱动。字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。常见的字符设备包括:点灯、按键、IIC、SPI、LCD 等。

在 Linux 中一切皆为文件。驱动加载成功以后会在 /dev 目录下生成一个相应的文件,应用程序通过对这个名为 /dev/xxx(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

1.2 字符设备 vs 块设备 vs 网络设备

特性 字符设备 块设备 网络设备
访问方式 字节流,顺序访问 块为单位,随机访问 数据包传输
设备节点 /dev/xxx /dev/xxx 无设备节点
缓冲区
典型设备 串口、LED、按键 硬盘、SD卡 网卡
驱动框架 file_operations file_operations + 块层 net_device

1.3 字符设备驱动框架图

graph TB
    subgraph "用户空间 (User Space)"
        APP["应用程序"]
        LIB["C 库 (glibc)"]
    end

    subgraph "内核空间 (Kernel Space)"
        SYSCALL["系统调用接口"]
        VFS["VFS 虚拟文件系统"]
        CHRDEV["字符设备驱动"]
        HW["硬件设备"]
    end

    APP -->|"open/read/write/close"| LIB
    LIB -->|"系统调用陷入内核"| SYSCALL
    SYSCALL --> VFS
    VFS -->|"file_operations"| CHRDEV
    CHRDEV -->|"寄存器操作"| HW

    style APP fill:#e1f5fe
    style LIB fill:#e8f5e9
    style SYSCALL fill:#fff3e0
    style VFS fill:#fce4ec
    style CHRDEV fill:#f3e5f5
    style HW fill:#eceff1

1.4 应用程序调用驱动的流程

应用程序运行在用户空间,Linux 驱动属于内核的一部分,运行于内核空间。当用户空间想要对内核进行操作(如使用 open 函数打开 /dev/led),必须使用"系统调用"来实现从用户空间"陷入"到内核空间。

sequenceDiagram
    participant App as 应用程序
    participant Lib as C库
    participant Kernel as 内核
    participant Driver as 驱动程序
    participant HW as 硬件

    App->>Lib: open("/dev/led", O_RDWR)
    Lib->>Kernel: 系统调用 (SWI/SVC)
    Kernel->>Driver: file_operations.open()
    Driver->>HW: 配置寄存器
    Driver-->>Kernel: 返回0(成功)
    Kernel-->>Lib: 返回文件描述符
    Lib-->>App: fd = 3

    App->>Lib: write(fd, "1", 1)
    Lib->>Kernel: 系统调用
    Kernel->>Driver: file_operations.write()
    Driver->>HW: 点亮LED
    Driver-->>Kernel: 返回写入字节数
    Kernel-->>Lib: 返回
    Lib-->>App: 返回1

    App->>Lib: close(fd)
    Lib->>Kernel: 系统调用
    Kernel->>Driver: file_operations.release()
    Driver-->>Kernel: 返回0
    Kernel-->>Lib: 返回
    Lib-->>App: 关闭成功

2. 关键数据结构

2.1 struct file 详解

struct file 代表一个打开的文件,在内核中每当 open() 一个设备文件时就会创建一个 file 结构体实例。

struct file {
    union {
        struct llist_node fu_llist;
        struct rcu_head fu_rcuhead;
    } f_u;

    struct path f_path;
    struct inode *f_inode;

    const struct file_operations *f_op;

    spinlock_t f_lock;
    atomic_long_t f_count;
    unsigned int f_flags;
    fmode_t f_mode;

    struct mutex f_pos_lock;
    loff_t f_pos;
    struct fown_struct f_owner;

    void *private_data;
    struct address_space *f_mapping;
};

关键成员说明:

成员 类型 说明
f_op const struct file_operations * 指向设备操作函数集合
f_flags unsigned int 文件打开标志,如 O_RDWRO_NONBLOCK
f_mode fmode_t 访问模式,FMODE_READFMODE_WRITE
f_pos loff_t 当前文件位置偏移量
private_data void * 私有数据,通常在 open 中设置

2.2 struct inode 详解

struct inode 代表一个文件系统中的文件/目录,包含文件的元数据信息。每个文件(设备文件)在内核中都有对应的 inode

struct inode {
    umode_t i_mode;
    kuid_t i_uid;
    kgid_t i_gid;

    unsigned int i_flags;

    const struct inode_operations *i_op;
    struct super_block *i_sb;
    struct address_space *i_mapping;

    unsigned long i_ino;

    dev_t i_rdev;
    loff_t i_size;

    struct timespec i_atime;
    struct timespec i_mtime;
    struct timespec i_ctime;

    unsigned short i_bytes;
    blkcnt_t i_blocks;

    union {
        struct pipe_inode_info *i_pipe;
        struct block_device *i_bdev;
        struct cdev *i_cdev;
        char *i_link;
        unsigned i_dir_seq;
    };
};

关键成员说明:

成员 类型 说明
i_mode umode_t 文件类型(字符设备、块设备等)和权限
i_rdev dev_t 设备号,包含主设备号和次设备号
i_cdev struct cdev * 指向字符设备结构体
i_op const struct inode_operations * inode 操作函数集合

2.3 struct file_operations 详解

struct file_operations 是 Linux 内核驱动操作函数集合,定义在 include/linux/fs.h 中。这是字符设备驱动最核心的数据结构。

struct file_operations {
    struct module *owner;

    loff_t (*llseek) (struct file *, loff_t, int);

    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

    int (*iterate) (struct file *, struct dir_context *);

    unsigned int (*poll) (struct file *, struct poll_table_struct *);

    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

    int (*mmap) (struct file *, struct vm_area_struct *);

    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);

    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);

    int (*fasync) (int, struct file *, int);

    int (*lock) (struct file *, int, struct file_lock *);

    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

    unsigned long (*get_unmapped_area)(struct file *, unsigned long,
                                       unsigned long, unsigned long, unsigned long);

    int (*check_flags)(int);

    int (*flock) (struct file *, int, struct file_lock *);

    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,
                            loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
                           size_t, unsigned int);

    int (*setlease)(struct file *, long, struct file_lock **, void *);

    long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);

    void (*show_fdinfo)(struct seq_file *m, struct file *f);
};

各字段详细说明:

字段 函数原型 说明
owner struct module * 拥有该结构体的模块指针,一般设置为 THIS_MODULE
llseek loff_t (*)(struct file *, loff_t, int) 修改文件当前的读写位置
read ssize_t (*)(struct file *, char __user *, size_t, loff_t *) 从设备读取数据到用户空间
write ssize_t (*)(struct file *, const char __user *, size_t, loff_t *) 从用户空间写入数据到设备
poll unsigned int (*)(struct file *, struct poll_table_struct *) 轮询函数,查询设备是否可以非阻塞读写
unlocked_ioctl long (*)(struct file *, unsigned int, unsigned long) 设备控制功能,对应用户空间的 ioctl()
compat_ioctl long (*)(struct file *, unsigned int, unsigned long) 32位系统兼容的 ioctl
mmap int (*)(struct file *, struct vm_area_struct *) 将设备内存映射到用户空间
open int (*)(struct inode *, struct file *) 打开设备文件
release int (*)(struct inode *, struct file *) 关闭/释放设备文件,对应 close()
fsync int (*)(struct file *, loff_t, loff_t, int) 将缓冲区数据刷新到设备
fasync int (*)(int, struct file *, int) 异步通知

字符设备驱动中常用的函数:

  • open / release:打开和关闭设备(必需)
  • read / write:读写数据(根据需求)
  • unlocked_ioctl:设备控制(根据需求)
  • llseek:调整读写位置(根据需求)

2.4 设备号相关宏

Linux 中每个设备都有一个设备号,设备号由主设备号次设备号两部分组成。

  • 主设备号:表示某一个具体的驱动
  • 次设备号:表示使用这个驱动的各个设备

设备号的数据类型为 dev_t,定义在 include/linux/types.h 中:

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

dev_t 是一个 32 位的数据类型,其中高 12 位为主设备号,低 20 位为次设备号。

设备号操作宏定义在 include/linux/kdev_t.h 中:

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

设备号操作示例:

#include <linux/types.h>
#include <linux/kdev_t.h>

void device_number_demo(void)
{
    dev_t dev;
    unsigned int major = 200;
    unsigned int minor = 0;

    dev = MKDEV(major, minor);  // dev = (200 << 20) | 0 = 209715200
    major = MAJOR(dev);         // major = 200
    minor = MINOR(dev);         // minor = 0

    printk(KERN_INFO "dev=%u, major=%u, minor=%u\n", dev, major, minor);
}

设备号范围:

位数 范围 说明
主设备号 0 ~ 4095 高12位
次设备号 0 ~ 1048575 低20位

3. 核心 API 详解

3.1 设备号分配 API

3.1.1 静态分配设备号

使用 register_chrdev() 函数进行静态分配,需要开发者自行指定一个未被使用的主设备号。

static inline int register_chrdev(unsigned int major,
                                   const char *name,
                                   const struct file_operations *fops);

参数说明:

参数 类型 说明
major unsigned int 主设备号,需要开发者指定一个未被使用的设备号
name const char * 设备名字,字符串指针
fops const struct file_operations * 设备操作函数集合指针

返回值:

  • 成功:返回 0
  • 失败:返回负数错误码

注销函数:

static inline void unregister_chrdev(unsigned int major, const char *name);
参数 类型 说明
major unsigned int 要注销的设备对应的主设备号
name const char * 要注销的设备对应的设备名

使用示例:

#define MY_MAJOR 200
#define MY_NAME "my_device"

static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

static int __init my_init(void)
{
    int ret;

    ret = register_chrdev(MY_MAJOR, MY_NAME, &my_fops);
    if (ret < 0) {
        printk(KERN_ERR "register_chrdev failed: %d\n", ret);
        return ret;
    }
    printk(KERN_INFO "Device registered with major %d\n", MY_MAJOR);
    return 0;
}

static void __exit my_exit(void)
{
    unregister_chrdev(MY_MAJOR, MY_NAME);
    printk(KERN_INFO "Device unregistered\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

3.1.2 动态分配设备号

Linux 社区推荐使用动态分配设备号,可以避免冲突问题。

int alloc_chrdev_region(dev_t *dev,
                         unsigned baseminor,
                         unsigned count,
                         const char *name);

参数说明:

参数 类型 说明
dev dev_t * 输出参数,保存申请到的设备号
baseminor unsigned 次设备号起始地址,一般为 0
count unsigned 要申请的设备号数量
name const char * 设备名字

释放函数:

void unregister_chrdev_region(dev_t from, unsigned count);
参数 类型 说明
from dev_t 要释放的设备号
count unsigned from 开始要释放的设备号数量

使用示例:

static dev_t my_devno;
static struct cdev my_cdev;

static int __init my_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&my_devno, 0, 1, "my_device");
    if (ret < 0) {
        printk(KERN_ERR "alloc_chrdev_region failed: %d\n", ret);
        return ret;
    }

    printk(KERN_INFO "Allocated major=%d, minor=%d\n",
           MAJOR(my_devno), MINOR(my_devno));
    return 0;
}

static void __exit my_exit(void)
{
    unregister_chrdev_region(my_devno, 1);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

3.2 cdev 相关 API

struct cdev 是内核中字符设备的抽象表示。

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

3.2.1 cdev_init

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

参数说明:

参数 类型 说明
cdev struct cdev * 要初始化的 cdev 结构体指针
fops const struct file_operations * 操作函数集合指针

功能: 初始化 cdev 结构体,将 fops 关联到 cdev

3.2.2 cdev_add

int cdev_add(struct cdev *p, dev_t dev, unsigned count);

参数说明:

参数 类型 说明
p struct cdev * 要添加的 cdev 结构体指针
dev dev_t 设备号
count unsigned 该设备号对应的设备数量

返回值:

  • 成功:返回 0
  • 失败:返回负数错误码

3.2.3 cdev_del

void cdev_del(struct cdev *p);

参数说明:

参数 类型 说明
p struct cdev * 要删除的 cdev 结构体指针

功能: 从内核中删除 cdev。

3.3 自动创建设备节点 API

在嵌入式 Linux 中,通常使用 udev/mdev 自动创建设备节点,无需手动 mknod

3.3.1 class_create

struct class *class_create(struct module *owner, const char *name);

参数说明:

参数 类型 说明
owner struct module * 拥有该 class 的模块,一般为 THIS_MODULE
name const char * class 名字

返回值:

  • 成功:返回 struct class * 指针
  • 失败:返回 ERR_PTR 错误码

功能:/sys/class/ 目录下创建一个新的 class 目录。

3.3.2 device_create

struct device *device_create(struct class *cls,
                              struct device *parent,
                              dev_t devt,
                              void *drvdata,
                              const char *fmt, ...);

参数说明:

参数 类型 说明
cls struct class * 设备所属的 class
parent struct device * 父设备,一般为 NULL
devt dev_t 设备号
drvdata void * 设备私有数据
fmt const char * 设备名字格式化字符串

返回值:

  • 成功:返回 struct device * 指针
  • 失败:返回 ERR_PTR 错误码

功能:/dev/ 目录下自动创建设备节点。

3.3.3 完整示例

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define DEV_NAME "my_chardev"
#define DEV_CLASS "my_char_class"

static dev_t my_devno;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;

static int my_open(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int my_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t cnt, loff_t *offt)
{
    printk(KERN_INFO "Device read\n");
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t cnt, loff_t *offt)
{
    printk(KERN_INFO "Device write\n");
    return cnt;
}

static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .read = my_read,
    .write = my_write,
};

static int __init my_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&my_devno, 0, 1, DEV_NAME);
    if (ret < 0)
        return ret;

    printk(KERN_INFO "Allocated major=%d, minor=%d\n",
           MAJOR(my_devno), MINOR(my_devno));

    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    ret = cdev_add(&my_cdev, my_devno, 1);
    if (ret < 0)
        goto err_cdev;

    my_class = class_create(THIS_MODULE, DEV_CLASS);
    if (IS_ERR(my_class)) {
        ret = PTR_ERR(my_class);
        goto err_class;
    }

    my_device = device_create(my_class, NULL, my_devno, NULL, DEV_NAME);
    if (IS_ERR(my_device)) {
        ret = PTR_ERR(my_device);
        goto err_device;
    }

    printk(KERN_INFO "Driver loaded successfully\n");
    return 0;

err_device:
    class_destroy(my_class);
err_class:
    cdev_del(&my_cdev);
err_cdev:
    unregister_chrdev_region(my_devno, 1);
    return ret;
}

static void __exit my_exit(void)
{
    device_destroy(my_class, my_devno);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(my_devno, 1);
    printk(KERN_INFO "Driver unloaded\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("embedded");
MODULE_DESCRIPTION("Complete char device example");

3.4 数据拷贝 API

由于内核空间和用户空间不能直接访问对方的内存,需要使用专门的函数进行数据拷贝。

3.4.1 copy_to_user

static inline long copy_to_user(void __user *to, const void *from, unsigned long n);

参数说明:

参数 类型 说明
to void __user * 用户空间目标地址
from const void * 内核空间源地址
n unsigned long 要复制的数据长度

返回值:

  • 成功:返回 0
  • 失败:返回未复制的字节数

功能: 将内核空间的数据复制到用户空间。

3.4.2 copy_from_user

static inline long copy_from_user(void *to, const void __user *from, unsigned long n);

参数说明:

参数 类型 说明
to void * 内核空间目标地址
from const void __user * 用户空间源地址
n unsigned long 要复制的数据长度

返回值:

  • 成功:返回 0
  • 失败:返回未复制的字节数

功能: 将用户空间的数据复制到内核空间。

使用示例:

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t cnt, loff_t *offt)
{
    int ret;
    char data[] = "Hello from kernel!";
    size_t len = strlen(data) + 1;

    if (cnt < len)
        len = cnt;

    ret = copy_to_user(buf, data, len);
    if (ret) {
        printk(KERN_ERR "copy_to_user failed\n");
        return -EFAULT;
    }

    return len;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t cnt, loff_t *offt)
{
    int ret;
    char kbuf[100];

    if (cnt > sizeof(kbuf) - 1)
        cnt = sizeof(kbuf) - 1;

    ret = copy_from_user(kbuf, buf, cnt);
    if (ret) {
        printk(KERN_ERR "copy_from_user failed\n");
        return -EFAULT;
    }

    kbuf[cnt] = '\0';
    printk(KERN_INFO "Received from user: %s\n", kbuf);

    return cnt;
}

4. 完整源码分析

4.1 chrdevbase.c 完整源码

chrdevbase 是一个虚拟字符设备,包含读缓冲区和写缓冲区,各 100 字节。

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

#define CHRDEVBASE_MAJOR 200
#define CHRDEVBASE_NAME "chrdevbase"

static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data!"};

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    printk("chrdevbase open!\r\n");
    return 0;
}

static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
                               size_t cnt, loff_t *offt)
{
    int retvalue = 0;

    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if (retvalue == 0) {
        printk("kernel senddata ok!\r\n");
    } else {
        printk("kernel senddata failed!\r\n");
    }

    return 0;
}

static ssize_t chrdevbase_write(struct file *filp,
                                const char __user *buf,
                                size_t cnt, loff_t *offt)
{
    int retvalue = 0;

    retvalue = copy_from_user(writebuf, buf, cnt);
    if (retvalue == 0) {
        printk("kernel recevdata:%s\r\n", writebuf);
    } else {
        printk("kernel recevdata failed!\r\n");
    }

    return 0;
}

static int chrdevbase_release(struct inode *inode,
                              struct file *filp)
{
    printk("chrdevbase release!\r\n");
    return 0;
}

static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};

static int __init chrdevbase_init(void)
{
    int retvalue = 0;

    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
                               &chrdevbase_fops);
    if (retvalue < 0) {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init()\r\n");
    return 0;
}

static void __exit chrdevbase_exit(void)
{
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\r\n");
}

module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

4.2 逐函数分析

4.2.1 chrdevbase_open

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    printk("chrdevbase open!\r\n");
    return 0;
}

功能: 当应用程序调用 open() 打开设备文件时,此函数被调用。

参数:

  • inode:传递给驱动的 inode 结构体,包含设备号等信息
  • filp:指向 file 结构体,可以设置 filp->private_data

典型用法:

  • 初始化设备
  • 设置 filp->private_data 指向设备结构体

4.2.2 chrdevbase_read

static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
                               size_t cnt, loff_t *offt)
{
    int retvalue = 0;

    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if (retvalue == 0) {
        printk("kernel senddata ok!\r\n");
    } else {
        printk("kernel senddata failed!\r\n");
    }

    return 0;
}

功能: 当应用程序调用 read() 时,此函数被调用。

参数:

  • filp:设备文件,表示打开的文件描述符
  • buf:返回给用户空间的数据缓冲区
  • cnt:要读取的数据长度
  • offt:相对于文件首地址的偏移

数据流:

  1. kerneldata 拷贝到内核缓冲区 readbuf
  2. 使用 copy_to_user() 将数据从内核空间复制到用户空间

4.2.3 chrdevbase_write

static ssize_t chrdevbase_write(struct file *filp,
                                const char __user *buf,
                                size_t cnt, loff_t *offt)
{
    int retvalue = 0;

    retvalue = copy_from_user(writebuf, buf, cnt);
    if (retvalue == 0) {
        printk("kernel recevdata:%s\r\n", writebuf);
    } else {
        printk("kernel recevdata failed!\r\n");
    }

    return 0;
}

功能: 当应用程序调用 write() 时,此函数被调用。

数据流:

  1. 使用 copy_from_user() 将数据从用户空间复制到内核空间
  2. 打印接收到的数据

4.2.4 chrdevbase_release

static int chrdevbase_release(struct inode *inode,
                              struct file *filp)
{
    printk("chrdevbase release!\r\n");
    return 0;
}

功能: 当应用程序调用 close() 关闭设备文件时,此函数被调用。

典型用法:

  • 释放资源
  • 如果在 open 中设置了 filp->private_data,需要在此释放

4.2.5 chrdevbase_init

static int __init chrdevbase_init(void)
{
    int retvalue = 0;

    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
                               &chrdevbase_fops);
    if (retvalue < 0) {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init()\r\n");
    return 0;
}

功能: 驱动入口函数,使用 module_init() 注册。

执行时机: 使用 insmodmodprobe 加载模块时调用。

核心操作: 调用 register_chrdev() 注册字符设备。

4.2.6 chrdevbase_exit

static void __exit chrdevbase_exit(void)
{
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\r\n");
}

功能: 驱动出口函数,使用 module_exit() 注册。

执行时机: 使用 rmmodmodprobe -r 卸载模块时调用。

核心操作: 调用 unregister_chrdev() 注销字符设备。

4.3 测试应用程序 chrdevbaseApp.c

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

static char usrdata[] = {"usr data!"};

int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

    if (argc != 3) {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    fd = open(filename, O_RDWR);
    if (fd < 0) {
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    if (atoi(argv[2]) == 1) {
        retvalue = read(fd, readbuf, 50);
        if (retvalue < 0) {
            printf("read file %s failed!\r\n", filename);
        } else {
            printf("read data:%s\r\n", readbuf);
        }
    }

    if (atoi(argv[2]) == 2) {
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);
        if (retvalue < 0) {
            printf("write file %s failed!\r\n", filename);
        }
    }

    retvalue = close(fd);
    if (retvalue < 0) {
        printf("Can't close file %s\r\n", filename);
        return -1;
    }

    return 0;
}

使用方法:

# 读取测试
./chrdevbaseApp /dev/chrdevbase 1

# 写入测试
./chrdevbaseApp /dev/chrdevbase 2

5. 实验验证

5.1 Makefile

KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

变量说明:

变量 说明
KERNELDIR /home/.../linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek Linux 内核源码目录
CURRENT_PATH $(shell pwd) 当前路径
obj-m chrdevbase.o 编译为 chrdevbase.ko 模块

5.2 编译命令

# 编译驱动模块
make -j32

# 编译测试程序 (交叉编译)
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

5.3 加载模块与测试

# 1. 复制文件到开发板
sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/

# 2. 加载驱动模块
insmod chrdevbase.ko
# 或
modprobe chrdevbase.ko

# 3. 查看已加载模块
lsmod

# 4. 查看设备号
cat /proc/devices

# 5. 创建设备节点 (首次使用)
mknod /dev/chrdevbase c 200 0

# 6. 读操作测试
./chrdevbaseApp /dev/chrdevbase 1
# 预期输出: read data:kernel data!

# 7. 写操作测试
./chrdevbaseApp /dev/chrdevbase 2
# 预期输出: kernel recevdata:usr data!

# 8. 卸载驱动模块
rmmod chrdevbase.ko

5.4 LED 驱动扩展

在 chrdevbase 基础上,LED 驱动需要额外操作硬件寄存器:

// 寄存器物理地址
#define CCM_CCGR1_BASE          (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE  (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE  (0X020E02F4)
#define GPIO1_DR_BASE           (0X0209C000)
#define GPIO1_GDIR_BASE         (0X0209C004)

// 映射后的虚拟地址
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

// LED初始化函数
static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;

    // 1、寄存器地址映射
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    // 2、使能GPIO1时钟
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val, IMX6U_CCM_CCGR1);

    // 3、设置GPIO1_IO03的复用功能
    writel(5, SW_MUX_GPIO1_IO03);

    // 4、设置IO属性
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    // 5、设置GPIO1_IO03为输出功能
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);
    val |= (1 << 3);
    writel(val, GPIO1_GDIR);

    // 6、默认关闭LED
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);

    // 7、注册字符设备驱动
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if (retvalue < 0) {
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    return 0;
}

// LED退出函数
static void __exit led_exit(void)
{
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    unregister_chrdev(LED_MAJOR, LED_NAME);
}

LED 驱动关键点:

  1. 地址映射:使用 ioremap() 将物理地址映射为虚拟地址
  2. 寄存器操作:使用 readl() / writel() 读写寄存器
  3. 资源释放:在出口函数中使用 iounmap() 取消映射

6. 跨平台对比

6.1 IMX6ULL vs STM32 vs RK3568 字符设备差异

特性 IMX6ULL STM32 (Linux) RK3568
内核版本 4.1.15 5.10+ 5.10+
设备树 支持 支持 支持
GPIO框架 旧版gpio_desc gpiod API gpiod API
时钟管理 CCM寄存器 RCC寄存器 CCM寄存器
地址映射 需要ioremap 需要ioremap 需要ioremap
自动创建节点 class_create class_create class_create
设备号分配 推荐动态 推荐动态 推荐动态

6.2 各平台 LED 驱动差异

IMX6ULL:

  • 使用直接寄存器操作
  • 需要手动配置时钟、复用、电气属性
  • 寄存器地址固定

STM32 (Linux):

  • 使用 gpiod API 更安全
  • 设备树定义 GPIO
  • 支持 pinctrl 框架

RK3568:

  • 使用标准 Linux GPIO 子系统
  • 设备树定义硬件资源
  • 支持 GPIO 按键、LED 子系统

6.3 驱动开发通用流程

graph TD
    A[定义设备号] --> B[初始化 cdev]
    B --> C[实现 file_operations]
    C --> D[注册字符设备]
    D --> E[创建设备节点]
    E --> F[实现 open/release]
    F --> G[实现 read/write]
    G --> H[实现 ioctl]
    H --> I[测试验证]
    I --> J[发布驱动]

    style A fill:#e1f5fe
    style D fill:#e8f5e9
    style E fill:#fff3e0
    style I fill:#fce4ec

7. 面试精选

7.1 什么是字符设备?与块设备有什么区别?

答案:

字符设备是 Linux 中按字节流顺序访问的设备,读写操作不经过缓冲区。块设备以固定大小的块(如512字节、4KB)为单位进行访问,支持随机访问,且通常有缓冲区优化。

主要区别:

  • 访问方式:字符设备是字节流,块设备是块
  • 缓冲区:字符设备无缓冲,块设备有缓冲
  • 设备节点:都在 /dev/
  • 驱动框架:字符设备使用 file_operations,块设备额外使用请求队列

7.2 file_operations 结构体的作用是什么?常用的字段有哪些?

答案:

file_operations 是内核驱动操作函数集合,定义了设备的所有操作接口。它是用户空间系统调用和内核驱动之间的桥梁。

常用字段:

  • open:打开设备
  • release:关闭设备
  • read:从设备读取数据
  • write:向设备写入数据
  • unlocked_ioctl:设备控制
  • llseek:调整文件位置
  • owner:模块拥有者,防止模块在使用时被卸载

7.3 如何在内核中注册和注销字符设备?

答案:

方法一:使用 register_chrdev(简单但过时)

// 注册
register_chrdev(major, name, fops);

// 注销
unregister_chrdev(major, name);

方法二:使用 cdev(推荐)

// 1. 动态分配设备号
alloc_chrdev_region(&devno, 0, 1, name);

// 2. 初始化 cdev
cdev_init(&cdev, fops);

// 3. 添加 cdev
cdev_add(&cdev, devno, 1);

// 4. 注销
cdev_del(&cdev);
unregister_chrdev_region(devno, 1);

7.4 内核空间和用户空间如何进行数据拷贝?为什么不能直接访问?

答案:

内核空间和用户空间不能直接访问对方内存,原因:

  1. 地址空间隔离:用户空间使用虚拟地址,内核空间有独立的地址空间
  2. 安全考虑:防止内核直接访问用户空间数据导致安全问题
  3. MMU保护:内存管理单元提供保护机制

数据拷贝函数:

// 内核 -> 用户空间
copy_to_user(void __user *to, const void *from, unsigned long n);

// 用户空间 -> 内核
copy_from_user(void *to, const void __user *from, unsigned long n);

这两个函数会检查地址的合法性,确保不会访问非法内存。

7.5 什么是 ioremap?为什么需要地址映射?

答案:

ioremap 用于将物理地址映射为虚拟地址。在开启了 MMU 的 Linux 系统中,CPU 访问的是虚拟地址,不能直接访问物理地址。

为什么需要地址映射:

  1. MMU 启用:Linux 启用 MMU 后,CPU 只能访问虚拟地址
  2. 地址转换:物理地址需要映射到虚拟地址空间
  3. 内存保护:通过映射可以设置访问权限

使用示例:

// 映射
void __iomem *vaddr = ioremap(phys_addr, size);

// 使用
val = readl(vaddr);
writel(val, vaddr);

// 取消映射
iounmap(vaddr);

注意: 映射后的地址使用 readl()/writel() 等专用函数访问,不要直接使用指针解引用。