--- title: 字符设备驱动基础 tags: [embedded-linux, driver, chardev, kernel] created: 2026-09-17 updated: 2026-09-17 pdf_ref: - "46_第四十章 字符设备驱动开发.pdf" - "47_第四十一章 嵌入式Linux LED驱动开发实验.pdf" - "48_第四十二章 新字符设备驱动实验.pdf" --- # 字符设备驱动基础 ## 1. 概述与原理 ### 1.1 字符设备定义 字符设备是 Linux 驱动中最基本的一类设备驱动。字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。常见的字符设备包括:点灯、按键、IIC、SPI、LCD 等。 在 Linux 中一切皆为文件。驱动加载成功以后会在 `/dev` 目录下生成一个相应的文件,应用程序通过对这个名为 `/dev/xxx`(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。 ### 1.2 字符设备 vs 块设备 vs 网络设备 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(原书只定义了字符设备)。 | 特性 | 字符设备 | 块设备 | 网络设备 | | -------- | ---------------- | ---------------------- | ---------- | | 访问方式 | 字节流,顺序访问 | 块为单位,随机访问 | 数据包传输 | | 设备节点 | `/dev/xxx` | `/dev/xxx` | 无设备节点 | | 缓冲区 | 无 | 有 | 有 | | 典型设备 | 串口、LED、按键 | 硬盘、SD卡 | 网卡 | | 驱动框架 | file_operations | file_operations + 块层 | net_device | ### 1.3 字符设备驱动框架图 ```mermaid 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`),必须使用"系统调用"来实现从用户空间"陷入"到内核空间。 ```mermaid 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 详解 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书只说明 `filp->private_data` 的用法,并未展开 `struct file` 的完整定义。 `struct file` 代表一个打开的文件,在内核中每当 `open()` 一个设备文件时就会创建一个 `file` 结构体实例。 ```c 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_RDWR`、`O_NONBLOCK` | | `f_mode` | `fmode_t` | 访问模式,`FMODE_READ` 或 `FMODE_WRITE` | | `f_pos` | `loff_t` | 当前文件位置偏移量 | | `private_data` | `void *` | 私有数据,通常在 `open` 中设置 | ### 2.2 struct inode 详解 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书未给出 `struct inode` 的完整定义。 `struct inode` 代表一个文件系统中的文件/目录,包含文件的元数据信息。每个文件(设备文件)在内核中都有对应的 `inode`。 ```c 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` 中。这是字符设备驱动最核心的数据结构。 ```c 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 (*mremap)(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); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif }; ``` > 原书(内核 4.1.15 的 `include/linux/fs.h`)中 `mmap_capabilities` 位于 `#ifndef CONFIG_MMU` 条件编译块内,此处照原书保留。 **各字段详细说明:** | 字段 | 函数原型 | 说明 | | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `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)` | 与 `unlocked_ioctl` 功能相同,区别在于 64 位系统上运行 32 位应用程序时会调用此函数;32 位系统上运行 32 位程序调用的仍是 `unlocked_ioctl` | | `mmap` | `int (*)(struct file *, struct vm_area_struct *)` | 将设备内存映射到进程空间(用户空间),帧缓冲类设备常用 | | `mremap` | `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` 中: ```c typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t; ``` `dev_t` 是一个 32 位的数据类型,其中高 12 位为主设备号,低 20 位为次设备号。 设备号操作宏定义在 `include/linux/kdev_t.h` 中: ```c #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)) ``` **设备号操作示例:** ```c #include #include 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位 | ### 2.5 printk 与日志级别 内核中没有 `printf`,向控制台输出信息必须使用 `printk`。`printk` 与 `printf` 的最大区别是可以指定消息级别,级别定义在 `include/linux/kern_levels.h`: ```c #define KERN_SOH "\001" #define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */ #define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */ #define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误 */ #define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动中用 KERN_ERR 报告硬件错误 */ #define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */ #define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */ #define KERN_INFO KERN_SOH "6" /* 提示性的信息 */ #define KERN_DEBUG KERN_SOH "7" /* 调试信息 */ ``` 一共 8 个级别,数字越小优先级越高。用法是在消息前加上对应宏: ```c printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); ``` 若不显式设置级别,`printk` 采用默认级别 `MESSAGE_LOGLEVEL_DEFAULT`(默认为 4)。`include/linux/printk.h` 中的 `CONSOLE_LOGLEVEL_DEFAULT` 控制哪些级别的消息能显示在控制台上,默认为 7,即优先级高于 7 的消息(0~6)才能打印出来。默认级别 4 高于 7,所以直接使用 `printk` 输出的信息可以显示在控制台上。 --- ## 3. 核心 API 详解 ### 3.1 设备号分配 API #### 3.1.1 静态分配设备号 使用 `register_chrdev()` 函数进行静态分配,需要开发者自行指定一个未被使用的主设备号。 ```c 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 - 失败:返回负数错误码 **注销函数:** ```c static inline void unregister_chrdev(unsigned int major, const char *name); ``` | 参数 | 类型 | 说明 | | ------- | -------------- | -------------------------- | | `major` | `unsigned int` | 要注销的设备对应的主设备号 | | `name` | `const char *` | 要注销的设备对应的设备名 | **使用示例:** ```c #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 社区推荐使用动态分配设备号,可以避免冲突问题。 ```c 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 *` | 设备名字 | **释放函数:** ```c void unregister_chrdev_region(dev_t from, unsigned count); ``` | 参数 | 类型 | 说明 | | ------- | ---------- | -------------------------------- | | `from` | `dev_t` | 要释放的设备号 | | `count` | `unsigned` | 从 `from` 开始要释放的设备号数量 | **使用示例:** ```c 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.1.3 指定设备号注册(register_chrdev_region) 如果已经明确知道要使用的主设备号/次设备号,也可以不使用 `alloc_chrdev_region`,而是用下列函数直接注册一段设备号: ```c int register_chrdev_region(dev_t from, unsigned count, const char *name); ``` | 参数 | 类型 | 说明 | | ------- | -------------- | ---------------------------------- | | `from` | `dev_t` | 要申请的起始设备号(给定的设备号) | | `count` | `unsigned` | 要申请的设备号数量,一般为一个 | | `name` | `const char *` | 设备名字 | **返回值:** - 成功:返回 0 - 失败:返回负数错误码 不论是 `alloc_chrdev_region` 还是 `register_chrdev_region` 申请的设备号,卸载时统一使用 `unregister_chrdev_region` 释放。 **新字符设备驱动下的设备号分配模板(原书示例代码 42.1.1.1):** ```c int major; /* 主设备号 */ int minor; /* 次设备号 */ dev_t devid; /* 设备号 */ if (major) { /* 定义了主设备号 */ devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */ register_chrdev_region(devid, 1, "test"); } else { /* 没有定义设备号 */ alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */ major = MAJOR(devid); /* 获取分配的主设备号 */ minor = MINOR(devid); /* 获取分配的次设备号 */ } ``` ### 3.2 cdev 相关 API `struct cdev` 是内核中字符设备的抽象表示。 ```c 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 ```c 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 ```c 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 ```c void cdev_del(struct cdev *p); ``` **参数说明:** | 参数 | 类型 | 说明 | | ---- | --------------- | ------------------------ | | `p` | `struct cdev *` | 要删除的 cdev 结构体指针 | **功能:** 从内核中删除 cdev。 ### 3.3 自动创建设备节点 API 在嵌入式 Linux 中,通常使用 udev/mdev 自动创建设备节点,无需手动 `mknod`。 #### 3.3.1 class_create ```c 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 目录。 > 原书指出 `class_create` 本身是一个宏,展开后等价于 `struct class *class_create(struct module *owner, const char *name)`,内部调用 `__class_create(owner, name, &__key)`。 #### 3.3.2 device_create ```c 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 删除设备与类 卸载驱动时需要删除创建的设备与类,函数原型如下: ```c void device_destroy(struct class *class, dev_t devt); void class_destroy(struct class *cls); ``` | 函数 | 参数 | 说明 | | ---------------- | ----------------------------------- | --------------------------------- | | `device_destroy` | `class`:设备所处类;`devt`:设备号 | 删除用 `device_create` 创建的设备 | | `class_destroy` | `cls`:要删除的类 | 删除用 `class_create` 创建的类 | #### 3.3.4 完整示例 ```c #include #include #include #include #include #include #include #include #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 ```c 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 ```c 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 - 失败:返回未复制的字节数 **功能:** 将用户空间的数据复制到内核空间。 > 💡 `copy_to_user()` / `copy_from_user()` 返回的是**未能复制的字节数**(成功时为 0)。因此判断是否失败通常写作 `if (copy_to_user(...))` 或 `if (retvalue)`,而不是判断返回值是否为负数。 **使用示例:** ```c 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 字节。 ```c #include #include #include #include #include #include /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : chrdevbase.c 作者 : 左忠凯 版本 : V1.0 描述 : chrdevbase 驱动文件。 其他 : 无 论坛 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凯创建 ***************************************************************/ #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"); } //printk("chrdevbase read!\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"); } //printk("chrdevbase write!\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 ```c 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 ```c 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"); } //printk("chrdevbase read!\r\n"); return 0; } ``` **功能:** 当应用程序调用 `read()` 时,此函数被调用。 **参数:** - `filp`:设备文件,表示打开的文件描述符 - `buf`:返回给用户空间的数据缓冲区 - `cnt`:要读取的数据长度 - `offt`:相对于文件首地址的偏移 **数据流:** 1. 将 `kerneldata` 拷贝到内核缓冲区 `readbuf` 2. 使用 `copy_to_user()` 将数据从内核空间复制到用户空间 #### 4.2.3 chrdevbase_write ```c 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"); } //printk("chrdevbase write!\r\n"); return 0; } ``` **功能:** 当应用程序调用 `write()` 时,此函数被调用。 **数据流:** 1. 使用 `copy_from_user()` 将数据从用户空间复制到内核空间 2. 打印接收到的数据 #### 4.2.4 chrdevbase_release ```c 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 ```c 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()` 注册。 **执行时机:** 使用 `insmod` 或 `modprobe` 加载模块时调用。 **核心操作:** 调用 `register_chrdev()` 注册字符设备。 #### 4.2.6 chrdevbase_exit ```c static void __exit chrdevbase_exit(void) { unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME); printk("chrdevbase_exit()\r\n"); } ``` **功能:** 驱动出口函数,使用 `module_exit()` 注册。 **执行时机:** 使用 `rmmod` 或 `modprobe -r` 卸载模块时调用。 **核心操作:** 调用 `unregister_chrdev()` 注销字符设备。 ### 4.3 测试应用程序 chrdevbaseApp.c ```c #include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : chrdevbaseApp.c 作者 : 左忠凯 版本 : V1.0 描述 : chrdevbase 驱动测试APP。 其他 : 使用方法:./chrdevbaseApp /dev/chrdevbase <1>|<2> argv[2] 1:读文件 argv[2] 2:写文件 论坛 : www.openedv.com 日志 : 初版V1.0 2019/1/30 左忠凯创建 ***************************************************************/ 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; } ``` **使用方法:** ```bash # 读取测试 ./chrdevbaseApp /dev/chrdevbase 1 # 写入测试 ./chrdevbaseApp /dev/chrdevbase 2 ``` --- ## 5. 实验验证 ### 5.1 Makefile ```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 编译命令 ```bash # 编译驱动模块 make -j32 # 编译测试程序 (交叉编译) arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp ``` ### 5.3 加载模块与测试 ```bash # 1. 复制文件到开发板 sudo cp chrdevbase.ko chrdevbaseApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ # 2. 首次使用 modprobe 前需要生成 modules.dep 等依赖文件 depmod # 3. 加载驱动模块 insmod chrdevbase.ko # 或 modprobe chrdevbase.ko # 4. 查看已加载模块 lsmod # 5. 查看设备号 cat /proc/devices # 6. 创建设备节点 (首次使用) mknod /dev/chrdevbase c 200 0 # 7. 读操作测试 ./chrdevbaseApp /dev/chrdevbase 1 # 预期输出: read data:kernel data! # 8. 写操作测试 ./chrdevbaseApp /dev/chrdevbase 2 # 预期输出: kernel recevdata:usr data! # 9. 卸载驱动模块 rmmod chrdevbase.ko ``` > `modprobe` 默认到 `/lib/modules/<内核版本>` 目录查找模块(本书内核为 4.1.15,即 `/lib/modules/4.1.15`),该目录不存在时需自行创建。`insmod` 不解析依赖、也不要求此目录。 > 使用出厂系统做 LED 实验时,LED 默认被设为心跳灯会一直闪烁,可执行 `echo none > /sys/class/leds/sys-led/trigger` 关闭心跳功能。 ### 5.4 LED 驱动扩展 在 chrdevbase 基础上,LED 驱动需要额外操作硬件寄存器: ```c #define LED_MAJOR 200 /* 主设备号 */ #define LED_NAME "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()` 取消映射 **LED 实验步骤(原书第四十一章):** ```bash # 1. 编译驱动与测试程序 make -j32 # 生成 led.ko arm-linux-gnueabihf-gcc ledApp.c -o ledApp # 2. 拷贝到 rootfs/lib/modules/4.1.15 后,在开发板上执行 depmod # 第一次加载驱动时需要 modprobe led.ko # 3. 创建设备节点 mknod /dev/led c 200 0 # 4. 测试 ./ledApp /dev/led 1 # 打开 LED ./ledApp /dev/led 0 # 关闭 LED # 5. 卸载 rmmod led.ko ``` > led_switch() 中 `val &= ~(1 << 3)` 控制 GPIO1_DR 的 bit3:写 0 点亮、写 1 熄灭(低电平点亮)。 --- ## 6. 跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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 驱动开发通用流程 ```mermaid 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. 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(面试题整理)。 ### 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(简单但过时)** ```c // 注册 register_chrdev(major, name, fops); // 注销 unregister_chrdev(major, name); ``` **方法二:使用 cdev(推荐)** ```c // 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保护**:内存管理单元提供保护机制 **数据拷贝函数:** ```c // 内核 -> 用户空间 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. **内存保护**:通过映射可以设置访问权限 **使用示例:** ```c // 映射 void __iomem *vaddr = ioremap(phys_addr, size); // 使用 val = readl(vaddr); writel(val, vaddr); // 取消映射 iounmap(vaddr); ``` **注意:** 映射后的地址使用 `readl()`/`writel()` 等专用函数访问,不要直接使用指针解引用。 --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第四十章 字符设备驱动开发、第四十一章 嵌入式Linux LED驱动开发实验、第四十二章 新字符设备驱动实验