title: 字符设备驱动基础 tags: [embedded-linux, driver, chardev, kernel] created: 2026-09-17 updated: 2026-09-17 pdf_ref:
字符设备是 Linux 驱动中最基本的一类设备驱动。字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。常见的字符设备包括:点灯、按键、IIC、SPI、LCD 等。
在 Linux 中一切皆为文件。驱动加载成功以后会在 /dev 目录下生成一个相应的文件,应用程序通过对这个名为 /dev/xxx(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
| 特性 | 字符设备 | 块设备 | 网络设备 |
|---|---|---|---|
| 访问方式 | 字节流,顺序访问 | 块为单位,随机访问 | 数据包传输 |
| 设备节点 | /dev/xxx |
/dev/xxx |
无设备节点 |
| 缓冲区 | 无 | 有 | 有 |
| 典型设备 | 串口、LED、按键 | 硬盘、SD卡 | 网卡 |
| 驱动框架 | file_operations | file_operations + 块层 | net_device |
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
应用程序运行在用户空间,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: 关闭成功
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_RDWR、O_NONBLOCK |
f_mode |
fmode_t |
访问模式,FMODE_READ 或 FMODE_WRITE |
f_pos |
loff_t |
当前文件位置偏移量 |
private_data |
void * |
私有数据,通常在 open 中设置 |
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 操作函数集合 |
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:调整读写位置(根据需求)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位 |
使用 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 * |
设备操作函数集合指针 |
返回值:
注销函数:
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");
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");
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;
};
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
cdev |
struct cdev * |
要初始化的 cdev 结构体指针 |
fops |
const struct file_operations * |
操作函数集合指针 |
功能: 初始化 cdev 结构体,将 fops 关联到 cdev。
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
p |
struct cdev * |
要添加的 cdev 结构体指针 |
dev |
dev_t |
设备号 |
count |
unsigned |
该设备号对应的设备数量 |
返回值:
void cdev_del(struct cdev *p);
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
p |
struct cdev * |
要删除的 cdev 结构体指针 |
功能: 从内核中删除 cdev。
在嵌入式 Linux 中,通常使用 udev/mdev 自动创建设备节点,无需手动 mknod。
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 目录。
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/ 目录下自动创建设备节点。
#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");
由于内核空间和用户空间不能直接访问对方的内存,需要使用专门的函数进行数据拷贝。
static inline long copy_to_user(void __user *to, const void *from, unsigned long n);
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
to |
void __user * |
用户空间目标地址 |
from |
const void * |
内核空间源地址 |
n |
unsigned long |
要复制的数据长度 |
返回值:
功能: 将内核空间的数据复制到用户空间。
static inline long copy_from_user(void *to, const void __user *from, unsigned long n);
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
to |
void * |
内核空间目标地址 |
from |
const void __user * |
用户空间源地址 |
n |
unsigned long |
要复制的数据长度 |
返回值:
功能: 将用户空间的数据复制到内核空间。
使用示例:
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;
}
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");
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 指向设备结构体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:相对于文件首地址的偏移数据流:
kerneldata 拷贝到内核缓冲区 readbufcopy_to_user() 将数据从内核空间复制到用户空间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() 时,此函数被调用。
数据流:
copy_from_user() 将数据从用户空间复制到内核空间static int chrdevbase_release(struct inode *inode,
struct file *filp)
{
printk("chrdevbase release!\r\n");
return 0;
}
功能: 当应用程序调用 close() 关闭设备文件时,此函数被调用。
典型用法:
open 中设置了 filp->private_data,需要在此释放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() 注册字符设备。
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit()\r\n");
}
功能: 驱动出口函数,使用 module_exit() 注册。
执行时机: 使用 rmmod 或 modprobe -r 卸载模块时调用。
核心操作: 调用 unregister_chrdev() 注销字符设备。
#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
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 模块 |
# 编译驱动模块
make -j32
# 编译测试程序 (交叉编译)
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
# 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
在 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 驱动关键点:
ioremap() 将物理地址映射为虚拟地址readl() / writel() 读写寄存器iounmap() 取消映射| 特性 | 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 |
| 设备号分配 | 推荐动态 | 推荐动态 | 推荐动态 |
IMX6ULL:
STM32 (Linux):
RK3568:
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
答案:
字符设备是 Linux 中按字节流顺序访问的设备,读写操作不经过缓冲区。块设备以固定大小的块(如512字节、4KB)为单位进行访问,支持随机访问,且通常有缓冲区优化。
主要区别:
/dev/ 下file_operations,块设备额外使用请求队列答案:
file_operations 是内核驱动操作函数集合,定义了设备的所有操作接口。它是用户空间系统调用和内核驱动之间的桥梁。
常用字段:
open:打开设备release:关闭设备read:从设备读取数据write:向设备写入数据unlocked_ioctl:设备控制llseek:调整文件位置owner:模块拥有者,防止模块在使用时被卸载答案:
方法一:使用 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);
答案:
内核空间和用户空间不能直接访问对方内存,原因:
数据拷贝函数:
// 内核 -> 用户空间
copy_to_user(void __user *to, const void *from, unsigned long n);
// 用户空间 -> 内核
copy_from_user(void *to, const void __user *from, unsigned long n);
这两个函数会检查地址的合法性,确保不会访问非法内存。
答案:
ioremap 用于将物理地址映射为虚拟地址。在开启了 MMU 的 Linux 系统中,CPU 访问的是虚拟地址,不能直接访问物理地址。
为什么需要地址映射:
使用示例:
// 映射
void __iomem *vaddr = ioremap(phys_addr, size);
// 使用
val = readl(vaddr);
writel(val, vaddr);
// 取消映射
iounmap(vaddr);
注意: 映射后的地址使用 readl()/writel() 等专用函数访问,不要直接使用指针解引用。