时间轴
时间轴
2025-11-11
init
本文介绍了Linux字符设备驱动开发的基础知识,重点讲解了设备号的组成与操作。文章首先说明了主设备号和次设备号的作用,以及设备号dev_t在Linux内核中的存储格式(高12位为主设备号,低20位为次设备号)。随后详细介绍了设备号操作宏(MINORBITS、MINORMASK、MAJOR、MINOR、MKDEV)的用途,并对比了静态申请设备号函数register_chrdev_region()与动态申请设备号函数alloc_chrdev_region()的参数、返回值及适用场景,同时说明了设备号释放函数unregister_chrdev_region()的使用方法。最后,文章介绍了字符设备核心结构体cdev,包括其字段含义以及在内核中的注册与使用方式,为后续编写完整的字符设备驱动打下基础。
Linux 驱动笔记
| 目录 | 链接 |
|---|---|
| 1. Linux 驱动框架 | |
| 2. Linux 驱动加载逻辑 | |
| 3. 字符设备基础 | |
| 4. 并发与竞争 | |
| 5. 高级字符设备进阶 | |
| 6. 中断 | |
| 7. 平台总线 | |
| 8. 设备树 | |
| 9. 设备模型 | |
| 10. 热插拔 | |
| 11. pinctrl 子系统 | |
| 12. gpio 子系统 | |
| 13. 输入子系统 | |
| 14. 单总线 | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. 网络设备 | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
设备号
在 Linux 系统中每一个设备都有相应的设备号,设备号有主设备号与次设备号之分:
- 主设备号:标识设备类型。
- 次设备号:在同一个驱动管理的多个设备实例之间进行区分
举例:
- 主设备号 13 表示 input 子系统(处理输入设备)。
- 次设备号 64 →
/dev/input/event0(可能是 USB 键盘)- 次设备号 65 →
/dev/input/event1(可能是 USB 鼠标)- 主设备号 188 表示 USB 串行设备。
- 次设备号 0 →
/dev/ttyUSB0- 次设备号 1 →
/dev/ttyUSB1
在注册字符设备驱动之前需要先申请设备号。
include/linux/types.h
1234567 | typedef u32 __kernel_dev_t;typedef __kernel_fd_set fd_set;typedef __kernel_dev_t dev_t;typedef __kernel_ino_t ino_t;typedef __kernel_mode_t mode_t;typedef unsigned short umode_t; |
设备号 dev_t 是 u32 类型,高 12 位为主设备号,低 20 位为次设备号。
设备号操作宏
include/linux/kdev_t.h
123456 |
- MINORBITS表示次设备号的位数,一共 20 位
- MINORMASK次设备号掩码,用于从 dev_t 中提取次设备号(
(dev) & MINORMASK) - MAJOR表示从 dev_t 中获取主设备号,本质是将 dev_t 右移 20 位
- MINOR表示从 dev_t 中获取次设备号,本质是取低 20 位的值
- MKDEV用于将主设备号和次设备号组成 dev_t 类型的设备号
设备号申请函数
在 Linux 驱动中可以使用以下两种方法进行设备号的申请。
include/linux/fs.h
123 | extern int register_chrdev_region(dev_t, unsigned, const char *);extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *); |
register_chrdev_region()
通过
register_chrdev_region(dev_t from, unsigned count, const char *name)函数进行静态申请设备号。- 函数原型:
1register_chrdev_region(dev_t from, unsigned count, const char *name)- 函数作用:静态申请设备号,对指定好的设备号进行申请。
- 参数含义:
from: 自定义的 dev_t 类型设备号,比如 MKDEV(100,0)表示起始主设备号 100,起始次设备号为 0count: 次设备的数量,表示在主设备号相同的情况下有几个次设备号。name: 申请的设备名称
- 函数返回值:申请成功返回 0,申请失败返回负数
注意,静态申请的设备号是有限制的:
include/linux/fs.h中定义:
1234567 /* fs/char_dev.c *//* Marks the bottom of the first segment of free char majors *//* Marks the top and bottom of the second segment of free char majors */
alloc_chrdev_region()
- 通过
alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char* name)函数进行动态申请设备号。
- 函数原型:
1 | alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name) |
函数作用:动态申请设备号,内核会自动分配一个未使用的设备号,相较于静态申请设备号,动态申请会避免注册设备号相同引发冲突的问题。
参数含义:
dev *: 会将申请完成的设备号保存在 dev 变量中baseminor: 次设备号的起始地址,次设备号一般从 0 开始,所以这个参数一般设置成 0count: 申请设备的数量name: 申请的设备名称
函数返回值:申请成功返回 0,申请失败返回负数
设备号释放函数
unregister_chrdev_region()
include/linux/fs.h
1 | extern void unregister_chrdev_region(dev_t, unsigned); |
- 函数功能:设备号释放函数,注销字符设备以后要释放掉设备号
- 函数参数:
dev_t类型:要释放的设备号unsigned类型:释放的设备号的数量
示例
my_driver.c
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465 | static int major = 0;static int minor = 0;module_param(major, int, S_IRUGO);module_param(minor, int, S_IRUGO);static dev_t dev_id;static int __init my_driver_init(void){ int ret; if (major) { dev_id = MKDEV(major, minor); printk(KERN_INFO "major from module_param: %d", MAJOR(dev_id)); printk(KERN_INFO "minor from module_param: %d", MINOR(dev_id)); ret = register_chrdev_region(dev_id, 1, "my_driver device"); if (ret < 0) { printk(KERN_ERR "register_chrdev_region error\n"); } else { printk(KERN_INFO "register_chrdev_region ok\n"); } } else { ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device"); if (ret < 0) { printk(KERN_ERR "alloc_chrdev_region error\n"); } else { printk(KERN_INFO "alloc_chrdev_region ok\n"); printk(KERN_INFO "major allocated: %d", MAJOR(dev_id)); printk(KERN_INFO "minor allocated: %d", MINOR(dev_id)); } } printk(KERN_INFO "my_driver: Module loaded\n"); return 0;}static void __exit my_driver_exit(void){ unregister_chrdev_region(dev_id, 1); printk(KERN_INFO "unregister_chrdev_region ok\n"); printk(KERN_INFO "my_driver: Module unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("my_driver Kernel Module"); |
测试:
1234567891011121314151617181920212223242526272829303132 | ~ # insmod my_driver.ko[ 18.414274] my_driver: loading out-of-tree module taints kernel.[ 18.421489] alloc_chrdev_region ok[ 18.421652] major allocated: 511[ 18.421667] minor allocated: 0[ 18.421910] my_driver: Module loaded~ # cat /proc/devices | grep my_driver511 my_driver device~ # rmmod my_driver.ko[ 27.856484] unregister_chrdev_region ok[ 27.856638] my_driver: Module unloaded~ # cat /proc/devices | grep my_driver~ #~ # insmod my_driver.ko major=512 minor=0[ 16.721502] my_driver: loading out-of-tree module taints kernel.[ 16.728530] major from module_param: 512[ 16.728574] minor from module_param: 0[ 16.728753] CHRDEV "my_driver device" major requested (512) is greater than the maximum (511)[ 16.729588] register_chrdev_region error[ 16.729760] my_driver: Module loaded~ # rmmod my_driver.ko[ 36.863583] unregister_chrdev_region ok[ 36.864560] my_driver: Module unloaded~ # insmod my_driver.ko major=500 minor=0[ 40.101231] major from module_param: 500[ 40.101265] minor from module_param: 0[ 40.101430] register_chrdev_region ok[ 40.101657] my_driver: Module loaded~ # rmmod my_driver.ko[ 50.626705] unregister_chrdev_region ok[ 50.626876] my_driver: Module unloaded |
注册字符类设备
include/linux/cdev.h
123456789101112131415161718192021222324252627282930313233343536373839 | /* SPDX-License-Identifier: GPL-2.0 */struct file_operations;struct inode;struct module;struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count;} __randomize_layout;void cdev_init(struct cdev *, const struct file_operations *);struct cdev *cdev_alloc(void);void cdev_put(struct cdev *p);int cdev_add(struct cdev *, dev_t, unsigned);void cdev_set_parent(struct cdev *p, struct kobject *kobj);int cdev_device_add(struct cdev *cdev, struct device *dev);void cdev_device_del(struct cdev *cdev, struct device *dev);void cdev_del(struct cdev *);void cd_forget(struct inode *); |
在 C 语言中,如果你只需要使用某个结构体的 指针,而不需要知道它的内部成员(即不进行解引用或 sizeof),就可以只做 前置声明:
1struct file_operations; // 告诉编译器:存在一个叫 file_operations 的 struct 类型这样你就可以定义指针:
1struct file_operations *fops;但不能访问成员(因为编译器还不知道结构体内容):
1fops->read(...); // ❌ 编译错误:不完整类型这在头文件中非常常见,用于 解耦依赖 和 加快编译速度。
而
extern是 存储类说明符(storage class specifier),用于声明 变量 或 函数 的外部链接属性。它的作用是告诉编译器:“这个变量/函数在别处定义,这里只是声明”。
GCC/Clang attribute(
__randomize_layout),用于内核的 结构体布局随机化(struct layout randomization),增加攻击者预测结构偏移的难度,提高内核安全性。
cdev 结构体
| 字段 | 含义 |
|---|---|
kobj | 继承自struct kobject,使字符设备能挂在 sysfs 下(即/sys/class/...等路径)。 |
owner | 指向拥有该设备的模块(THIS_MODULE),防止模块卸载时设备仍在使用。 |
ops | 指向设备操作函数表(struct file_operations),定义 read/write/ioctl 等行为。 |
list | 内部链表,将所有共享该cdev的 inode(即/dev/xxx文件)通过inode->i_devices链接到一起,形成一个“反向引用链表”。 |
dev | 设备号,类型是dev_t(包含主设备号 major 和次设备号 minor)。 |
count | 表示这个cdev控制的连续设备编号数量(常为 1)。 |
cdev_init
fs/char_dev.c
123456789101112131415 | /** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev, remembering @fops, making it ready to add to the * system with cdev_add(). */void cdev_init(struct cdev *cdev, const struct file_operations *fops){ memset(cdev, 0, sizeof *cdev); INIT_LIST_HEAD(&cdev->list); kobject_init(&cdev->kobj, &ktype_cdev_default); cdev->ops = fops;} |
- 作用:初始化一个静态分配的
struct cdev。建立 cdev 和 file_operations 之间的联系
注意: 调用
cdev_init后最好调用cdev_test.owner = THIS_MODULE;将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_add
fs/char_dev.c
1234567891011121314151617181920212223242526272829 | /** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */int cdev_add(struct cdev *p, dev_t dev, unsigned count){ int error; p->dev = dev; p->count = count; if (WARN_ON(dev == WHITEOUT_DEV)) return -EBUSY; error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); if (error) return error; kobject_get(p->kobj.parent); return 0;} |
- 作用:向系统添加一个 cdev 结构体,也就是添加一个字符设备
cdev_map:全局kobj_map结构(哈希表)p:指向struct cdev- 注意:
cdev内嵌了struct kobject kobj,所以cdev可以被视为一个kobject
cdev_del
fs/char_dev.c
12345678910111213141516 | /** * cdev_del() - remove a cdev from the system * @p: the cdev structure to be removed * * cdev_del() removes @p from the system, possibly freeing the structure * itself. * * NOTE: This guarantees that cdev device will no longer be able to be * opened, however any cdevs already open will remain and their fops will * still be callable even after cdev_del returns. */void cdev_del(struct cdev *p){ cdev_unmap(p->dev, p->count); kobject_put(&p->kobj);} |
- 作用:系统中删除一个字符设备
示例
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 | static int major = 0;static int minor = 0;module_param(major, int, S_IRUGO);module_param(minor, int, S_IRUGO);static dev_t dev_id;static struct cdev cdev_test;static struct file_operations cdev_test_ops = { .owner = THIS_MODULE,};static int __init my_driver_init(void){ int ret; if (major) { dev_id = MKDEV(major, minor); pr_info( "major from module_param: %d", MAJOR(dev_id)); pr_info( "minor from module_param: %d", MINOR(dev_id)); ret = register_chrdev_region(dev_id, 1, "my_driver device"); if (ret < 0) { pr_err( "register_chrdev_region error\n"); return ret; } else { pr_info( "register_chrdev_region ok\n"); } } else { ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device"); if (ret < 0) { pr_err( "alloc_chrdev_region error\n"); return ret; } else { pr_info( "alloc_chrdev_region ok\n"); pr_info( "major allocated: %d", MAJOR(dev_id)); pr_info( "minor allocated: %d", MINOR(dev_id)); } } cdev_init(&cdev_test, &cdev_test_ops); //将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块 cdev_test.owner = THIS_MODULE; ret = cdev_add(&cdev_test, dev_id, 1); if (ret < 0) { pr_err( "cdev_add error\n"); unregister_chrdev_region(dev_id, 1); return ret; }else{ pr_info( "cdev_add ok\n"); } pr_info( "my_driver: Module loaded\n"); return 0;}static void __exit my_driver_exit(void){ cdev_del(&cdev_test); unregister_chrdev_region(dev_id, 1); pr_info( "unregister_chrdev_region ok\n"); pr_info( "my_driver: Module unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("my_driver Kernel Module"); |
注意先删除设备再释放设备号。
file_operations 结构体
Linux 有一个很重要的概念叫一切皆文件,也就是 Linux 中的设备就像普通的文件一样。访问一个设备好像是在访问一个文件。在应用程序中我们可以使用 open, read, write, close, ioctl 这几个系统调用来操作驱动。当我们在应用程序中调用 open 函数的时候,最终会去执行驱动中的 open 函数。所以 file_operations 将系统调用和驱动程序连接起来了。
应用中调用 open 函数
1 | fd = open("/dev/hello", O_RDWR); |
驱动中执行 open 函数
123456789101112 | static int cdev_test_open(struct inode *, struct file*){ printk("This is cdev_test open"); return 0;}static struct file_operations cdev_test_ops={ .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .write = cdev_test_write, .release = cdev_test_release} |
linux 中 file_operations 定义:
include/linux/fs.h
123456789101112131415161718192021222324252627282930313233343536373839404142 | 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 (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*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 *); unsigned long mmap_supported_flags; 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 (*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); unsigned (*mmap_capabilities)(struct file *); ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); bool may_pollfree;} __randomize_layout; |
常见字段解析:
| 函数指针 | 用户态调用 | 作用 |
|---|---|---|
owner | — | 通常写为THIS_MODULE,防止模块被正在使用时卸载 |
open | open() | 打开设备时调用,一般用于初始化或统计打开次数 |
release | close() | 关闭设备时调用,用于释放资源 |
read | read() | 用户读设备数据 |
write | write() | 用户写数据到设备 |
unlocked_ioctl | ioctl() | 用户发控制命令 |
poll | poll()/select() | 实现非阻塞 I/O(epoll 机制) |
mmap | mmap() | 将物理内存映射到用户空间 |
llseek | lseek() | 文件位置偏移 |
fasync | fcntl(F_SETFL, O_ASYNC) | 支持异步通知 |
返回值约定:
ssize_t:>=0 成功字节数,<0 errnoint/long:0 成功,<0 errno- 指针:成功返回指针,失败返回
ERR_PTR(-errno)
llseek
1 | loff_t (*llseek)(struct file *file, loff_t offset, int whence); |
- 作用:文件指针偏移操作(类似 lseek 系统调用)。
- 参数:
file:文件对象。offset:偏移量。whence:SEEK_SET:相对于文件开头SEEK_CUR:相对于当前文件指针SEEK_END:相对于文件末尾
- 返回值:
- 新的文件指针位置(
loff_t) - 出错返回负值(如
-EINVAL)
- 新的文件指针位置(
read
1 | ssize_t (*read)(struct file *file, char __user *buf, size_t count, loff_t *pos); |
- 作用:从文件读数据到用户空间。
- 参数:
file:文件对象。buf:用户空间缓冲区,用于接收从内核读取的数据。count:请求传输的数据大小(用户缓冲区的大小)。pos:表示从文件中读取数据的起始位置,读完后需要更新。
- 返回值:
- 成功:实际读取的字节数
- 出错:负数 errno
read 步骤
- 防止读操作超出文件大小,并返回文件结束:
12 | if (*pos >= filesize) return 0; /* 0 means EOF */ |
- 读取的字节数不能超过文件大小。适当调整 count:
12 | if (*pos + count > filesize) count = filesize - (*pos); |
- 找到读取的起始位置:
1 | void *from = pos_to_address (*pos); /* convert pos into valid address */ |
- 将数据复制到用户空间缓冲区,失败时返回错误:
123 | sent = copy_to_user(buf, from, count);if (sent) return -EFAULT; |
- 根据读取的字节数前移文件的当前位置,并返回复制的字节数:
123 | *pos += count;return count; |
write
1 | ssize_t (*write)(struct file *file, const char __user *buf, size_t count, loff_t *pos); |
- 作用:把用户数据写入文件/设备。
- 参数:
file:文件对象buf:用户缓冲区count:请求传输的数据长度pos:表示数据在文件中应写入的起始位置
- 返回值:
- 成功:实际写入的字节数
- 出错:负数 errno
write 步骤
- 检查来自用户空间的错误或无效请求。这个步骤只有在设备提供内存(电可擦编程只读存储器、I/O内存等)时才有意义,它可能有内存大小限制:
123 | /* 如果试图写入文件末尾之外,则返回错误。这里的 filesize 对应设备内存的大小(如果有的话) */if ( *pos >= filesize ) return -EINVAL; |
- 针对剩余字节数调整 count,以便不超出文件大小。这一步也不是必需的,与第(1)步中的相同条件相关:
123 | /* 文件大小强制响应设备内存的大小 */if (*pos + count > filesize) count = filesize - *pos; |
- 找到开始写入的位置。只有当设备具有内存,并且write()方法要在其中写入指定的数据时,此步骤才是相关的。和第(2)步一样,这一步不是必需的:
12 | /* 将pos转换为有效地址*/void *from = pos_to_address( *pos ); |
- 从用户空间复制数据,并将其写入相应的内核空间:
123456 | if (copy_from_user(dev->buffer, buf, count) != 0){ retval = -EFAULT; goto out;}/* 现在将数据从dev->buffer移动到物理设备 */ |
- 写入物理设备,在失败时返回错误:
1234 | write_error = device_write(dev->buffer, count);if ( write_error ) return -EFAULT; |
- 根据写入的字节数移动文件中光标的当前位置。最后,返回复制的字节数:
123 | *pos += count;return count; |
mmap
1 | int (*mmap)(struct file *, struct vm_area_struct *); |
- 作用:支持用户空间内存映射到设备。
- 参数:
file:文件对象vma:虚拟内存区域结构
- 返回值:
- 0 成功
- <0 错误
mmap_supported_flags:映射时支持的标志。
open
1 | int (*open)(struct inode *inode, struct file *file); |
- 作用:当用户态调用
open()打开设备或文件时被调用。 - 参数:
inode:指向设备对应的 inode 结构,包含文件系统层信息和设备号。file:指向文件对象(struct file),保存文件状态、偏移量、private_data等。
- 返回值:
0:成功打开<0:出错,返回对应 errno(如-EBUSY表示设备忙,-ENOMEM内存不足)
- 典型用途:
- 检查设备是否已经被占用
- 初始化
file->private_data指针 - 增加模块引用计数(
THIS_MODULE)
flush
1 | int (*flush)(struct file *file, fl_owner_t id); |
- 作用:在文件描述符的最后一个引用关闭时被调用,用于刷新设备缓冲区中的待处理数据。
- 参数:
file:文件对象id:文件句柄拥有者标识,一般是文件描述符对应的进程
- 返回值:
0:成功<0:出错
- 说明:
- 对于大多数字符设备驱动,
flush不需要特殊实现,直接返回 0 即可 - 在块设备或网络设备中,
flush可能用于提交缓存数据
- 对于大多数字符设备驱动,
release
1 | int (*release)(struct inode *inode, struct file *file); |
- 作用:当用户态调用
close()关闭设备或文件时被调用。 - 参数:
inode:设备对应 inodefile:文件对象
- 返回值:
0:成功关闭<0:出错
- 典型用途:
- 释放在
open()时分配的资源 - 恢复设备状态(如标记为未被占用)
- 减少模块引用计数
- 释放在
示例
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 | static int major = 0;module_param(major, int, S_IRUGO);MODULE_PARM_DESC(major, "cdev_test sample, major device number");static int minor = 0;module_param(minor, int, S_IRUGO);MODULE_PARM_DESC(minor, "cdev_test sample, minor device number");dev_t dev_num;struct cdev cdev;int cdev_test_open(struct inode *inode, struct file *file){ pr_info("cdev_test open"); return 0;}ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ pr_info("cdev_test read\n"); return 0;}ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t, loff_t *offset){ pr_info("cdev_test write\n"); return 0;}struct file_operations fops = { .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .write = cdev_test_write,};static int __init cdev_test_init(void){ int err; if (major) { dev_num = MKDEV(major, minor); err = register_chrdev_region(dev_num, 1, "cdev test dev num"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_err; } pr_info("register_chrdev_region success\n"); } else { err = alloc_chrdev_region(&dev_num, 0, 1, "cdev test dev num"); if (err < 0) { pr_err("alloc_chrdev_region error\n"); goto chrdev_region_err; } pr_info("alloc_chrdev_region success\n"); } pr_info("dev_num: major[%d] minor[%d]", MAJOR(dev_num), MINOR(dev_num)); cdev_init(&cdev, &fops); pr_info("cdev_init success\n"); cdev.owner = THIS_MODULE; // 将owner指向本模块,防止cdev操作时卸载模块 err = cdev_add(&cdev, dev_num, 1); pr_info("cdev_add success\n"); if (err < 0) { pr_err("cdev_add error\n"); goto cdev_add_err; } return 0;cdev_add_err: unregister_chrdev_region(dev_num, 1);chrdev_region_err: return err;}static void __exit cdev_test_exit(void){ cdev_del(&cdev); pr_info("cdev is deleted\n"); unregister_chrdev_region(dev_num, 1); pr_info("chrdev_region unregister success\n");}module_init(cdev_test_init);module_exit(cdev_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is just a cdev test sample"); |
设备结点
在 Linux 操作系统中一切皆文件,对于用来进行设备访问的文件称之为设备节点。程序通过操作这个"设备文件",便可以操作对应的硬件。
1 | fd = open("/dev/hello", O_RDWR); |
这个“设备文件”就是设备结点。所以 Linux 设备结点是应用程序和驱动程序沟通的一个桥梁。设备节点被创建在/dev 目录下。
12345678910111213141516 | [zhaohang@cyberboy linux_driver_learning]$ ll /devcrw------- 4,15 root 11 Nov 10:05 tty15crw------- 4,16 root 11 Nov 10:05 tty16crw------- 4,17 root 11 Nov 10:05 tty17crw------- 4,18 root 11 Nov 10:05 tty18crw------- 4,19 root 11 Nov 10:05 tty19crw------- 4,20 root 11 Nov 10:05 tty20crw------- 4,21 root 11 Nov 10:05 tty21crw------- 4,22 root 11 Nov 10:05 tty22crw------- 4,23 root 11 Nov 10:05 tty23crw------- 4,24 root 11 Nov 10:05 tty24crw------- 4,25 root 11 Nov 10:05 tty25crw------- 4,26 root 11 Nov 10:05 tty26crw------- 4,27 root 11 Nov 10:05 tty27crw------- 4,28 root 11 Nov 10:05 tty28crw------- 4,29 root 11 Nov 10:05 tty29 |
4,15表示主设备号和次设备号。crw 中 c 表示字符设备。
根据设备节点的创建方式不同,分为了手动创建设备节点和自动创建设备节点。
Linux 下创建结点的方式
手动创建设备结点
使用 mknod 命令手动创建设备节点,mknod 命令格式为:
1 | $ mknod NAME TYPE MAJOR MINOR |
参数含义:
NAME: 要创建的节点名称TYPE: b 表示块设备,c 表示字符设备,p 表示管道MAJOR:要链接设备的主设备号MINOR: 要链接设备的从设备号
例如: 使用以下命令创建一个名为 device_test 的字符设备节点,设备的主设备号和从设备号分别为 236 和 0。
1 | $ mknod /dev/device_test c 236 0 |
对于上面 file_operations 的示例:
123456789101112 | $ insmod cdev_test.ko[ 43.969734] cdev_test: loading out-of-tree module taints kernel.[ 43.979783] alloc_chrdev_region success[ 43.980242] dev_num: major[241] minor[0][ 43.980277] cdev_init success[ 43.981192] cdev_add success$ mknod /dev/cdev_test c 241 0$ ls /dev/cdev_test/dev/cdev_test$ cat /dev/cdev_test[ 68.723315] cdev_test open[ 68.723900] cdev_test read |
自动创建设备结点
自动创建设备节点是利用 udev 机制来实现的。
udev 是一个用户程序,通过检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。
自动创建设备节点需要在驱动中首先使用 class_create()函数创建一个类,创建的类位于于/sys/class/ 目录下,之后使用 device_create()函数在这个类下创建相应的设备,在加载驱动模块时,用户空间中的 udev 会自动响应根据并/sys/class/ 下的信息创建设备节点。
在嵌入式 Linux 中我们使用的是 mdev,mdev 是 udev 的简化版本。在使用 busybox 构建的跟文件系统的时候,busybox 会自动创建 mdev。
class_create 函数
linux/device/class.h
引用头文件使用 linux/device/device.h 因为 device.h 引用了 class.h
12345678 | /* This is a #define to keep the compiler from merging different * instances of the __key variable */ |
- 函数作用:用于动态创建设备类。
- 参数含义:
owner:struct module 结构体类型的指针。一般赋值为 THIS_MODULE。name:char 类型的指针,代表即将创建的 struct class 变量的名字。
- 返回值:
struct class *类型的结构体
class_destroy 函数
linux/device/class.h
1 | extern void class_destroy(struct class *cls); |
- 函数作用:用于删除设备类。
- 参数含义:
cls:调用class_create()函数返回的struct class结构体的指针。
device_create 函数
linux/device/device.h
使用 class_create 创建好类以后,还需要使用 device_create 函数在类下面创建一个设备。
123 | __printf(5, 6) struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); |
- 函数作用:用来在 class 类下创建一个设备文件。
- 参数含义:
cls:指定所要创建的设备所从属的类。parent:指定该设备的父设备,如果没有就指定为 NULL。devt:指定创建设备的设备号。drvdata:被添加到该设备回调的数据,没有则指定为 NULL。fmt:添加到系统的设备节点名称。
- 返回值:
struct device *类型结构体
device_destroy 函数
linux/device/device.h
1 | void device_destroy(struct class *cls, dev_t devt); |
- 函数作用:用来删除 class 类中的设备。
- 参数含义:
cls:指定所要创建的设备所从属的类。devt:指定创建设备的设备号。
示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 | static int major = 0;module_param(major, int, S_IRUGO);MODULE_PARM_DESC(major, "mknod_test dev major num");static int minor = 0;module_param(minor, int, S_IRUGO);MODULE_PARM_DESC(minor, "mknod_test dev minor num");dev_t dev_num;int mknod_test_open(struct inode *inode, struct file *file){ pr_info("This is mknod_test open\n"); return 0;}ssize_t mknod_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ pr_info("This is mknod test read\n"); return 0;}int mknod_test_release (struct inode *inode, struct file *file){ pr_info("This is mknod test release\n"); return 0;}struct file_operations fops = { .owner = THIS_MODULE, .open = mknod_test_open, .read = mknod_test_read, .release = mknod_test_release,};struct cdev cdev;struct class *class;struct device *dev;static int __init mknod_test_init(void){ int err; pr_info("mknod_test module init\n"); if (major) { dev_num = MKDEV(major, minor); err = register_chrdev_region(dev_num, 1, "mknod test device num"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_err; } pr_info("register_chrdev_region success\n"); } else { err = alloc_chrdev_region(&dev_num, 0, 1, "mknod test device num"); if (err < 0) { pr_err("alloc_chrdev_region error\n"); goto chrdev_region_err; } pr_info("alloc_chrdev_region success\n"); } pr_info("dev_t dev_num: major[%d], minor[%d]\n", MAJOR(dev_num), MINOR(dev_num)); cdev_init(&cdev, &fops); cdev.owner = THIS_MODULE; err = cdev_add(&cdev, dev_num, 1); if (err < 0) { pr_err("cdev add error\n"); goto cdev_add_err; } class = class_create(THIS_MODULE, "chrdev"); if (IS_ERR(class)) { err = PTR_ERR(class); pr_err("class_create error\n"); goto class_create_err; } dev = device_create(class, NULL, dev_num, NULL, "mknod_test_device"); if(IS_ERR(dev)){ err = PTR_ERR(dev); pr_err("device create error\n"); goto device_create_err; } return 0; device_create_err: class_destroy(class);class_create_err: cdev_del(&cdev);cdev_add_err: unregister_chrdev_region(dev_num, 1);chrdev_region_err: return err;}static void __exit mknod_test_exit(void){ device_destroy(class, dev_num); class_destroy(class); cdev_del(&cdev); unregister_chrdev_region(dev_num, 1); pr_info("mknod_test module exit\n");}module_init(mknod_test_init);module_exit(mknod_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is just mknod test sample"); |
测试:
12345678910111213 | $ insmod mknod_test.ko[ 11.793662] mknod_test: loading out-of-tree module taints kernel.[ 11.803110] mknod_test module init[ 11.803371] alloc_chrdev_region success[ 11.804103] dev_t dev_num: major[241], minor[0]$ ls /dev/mknod_test_device/dev/mknod_test_device$ cat /dev/mknod_test_device[ 22.546620] This is mknod_test open[ 22.547827] This is mknod test read[ 22.548435] This is mknod test release$ rmmod mknod_test.ko[ 28.103036] mknod_test module exit |
用户空间和内核空间的数据交换
内核空间和用户空间的内存是不能互相访问的。但是很多应用程序都需要和内核进行数据的交换,例如应用程序使用read()函数从驱动中读取数据,使用write()函数向驱动中写数据,上述功能就需要使用copy_from_user()和copy_to_user()两个函数来完成。
copy_from_user()函数是将用户空间的数据拷贝到内核空间。copy_to_user()函数是将内核空间的数据拷贝到用户空间。
内核空间获取用户空间数据的常见方式:
- 系统调用(如 read/write)
- 软中断(socket 通信等)
include/linux/uaccess.h
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 | /* * Architectures should provide two primitives (raw_copy_{to,from}_user()) * and get rid of their private instances of copy_{to,from}_user() and * __copy_{to,from}_user{,_inatomic}(). * * raw_copy_{to,from}_user(to, from, size) should copy up to size bytes and * return the amount left to copy. They should assume that access_ok() has * already been checked (and succeeded); they should *not* zero-pad anything. * No KASAN or object size checks either - those belong here. * * Both of these functions should attempt to copy size bytes starting at from * into the area starting at to. They must not fetch or store anything * outside of those areas. Return value must be between 0 (everything * copied successfully) and size (nothing copied). * * If raw_copy_{to,from}_user(to, from, size) returns N, size - N bytes starting * at to must become equal to the bytes fetched from the corresponding area * starting at from. All data past to + size - N must be left unmodified. * * If copying succeeds, the return value must be 0. If some data cannot be * fetched, it is permitted to copy less than had been fetched; the only * hard requirement is that not storing anything at all (i.e. returning size) * should happen only when nothing could be copied. In other words, you don't * have to squeeze as much as possible - it is allowed, but not necessary. * * For raw_copy_from_user() to always points to kernel memory and no faults * on store should happen. Interpretation of from is affected by set_fs(). * For raw_copy_to_user() it's the other way round. * * Both can be inlined - it's up to architectures whether it wants to bother * with that. They should not be used directly; they are used to implement * the 6 functions (copy_{to,from}_user(), __copy_{to,from}_user_inatomic()) * that are used instead. Out of those, __... ones are inlined. Plain * copy_{to,from}_user() might or might not be inlined. If you want them * inlined, have asm/uaccess.h define INLINE_COPY_{TO,FROM}_USER. * * NOTE: only copy_from_user() zero-pads the destination in case of short copy. * Neither __copy_from_user() nor __copy_from_user_inatomic() zero anything * at all; their callers absolutely must check the return value. * * Biarch ones should also provide raw_copy_in_user() - similar to the above, * but both source and destination are __user pointers (affected by set_fs() * as usual) and both source and destination can trigger faults. */static __always_inline __must_check unsigned long__copy_from_user_inatomic(void *to, const void __user *from, unsigned long n){ instrument_copy_from_user(to, from, n); check_object_size(to, n, false); return raw_copy_from_user(to, from, n);}static __always_inline __must_check unsigned long__copy_from_user(void *to, const void __user *from, unsigned long n){ might_fault(); if (should_fail_usercopy()) return n; instrument_copy_from_user(to, from, n); check_object_size(to, n, false); return raw_copy_from_user(to, from, n);}/** * __copy_to_user_inatomic: - Copy a block of data into user space, with less checking. * @to: Destination address, in user space. * @from: Source address, in kernel space. * @n: Number of bytes to copy. * * Context: User context only. * * Copy data from kernel space to user space. Caller must check * the specified block with access_ok() before calling this function. * The caller should also make sure he pins the user space address * so that we don't result in page fault and sleep. */static __always_inline __must_check unsigned long__copy_to_user_inatomic(void __user *to, const void *from, unsigned long n){ if (should_fail_usercopy()) return n; instrument_copy_to_user(to, from, n); check_object_size(from, n, true); return raw_copy_to_user(to, from, n);}static __always_inline __must_check unsigned long__copy_to_user(void __user *to, const void *from, unsigned long n){ might_fault(); if (should_fail_usercopy()) return n; instrument_copy_to_user(to, from, n); check_object_size(from, n, true); return raw_copy_to_user(to, from, n);}static inline __must_check unsigned long_copy_from_user(void *to, const void __user *from, unsigned long n){ unsigned long res = n; might_fault(); if (!should_fail_usercopy() && likely(access_ok(from, n))) { instrument_copy_from_user(to, from, n); res = raw_copy_from_user(to, from, n); } if (unlikely(res)) memset(to + (n - res), 0, res); return res;}extern __must_check unsigned long_copy_from_user(void *, const void __user *, unsigned long);static inline __must_check unsigned long_copy_to_user(void __user *to, const void *from, unsigned long n){ might_fault(); if (should_fail_usercopy()) return n; if (access_ok(to, n)) { instrument_copy_to_user(to, from, n); n = raw_copy_to_user(to, from, n); } return n;}extern __must_check unsigned long_copy_to_user(void __user *, const void *, unsigned long);static __always_inline unsigned long __must_checkcopy_from_user(void *to, const void __user *from, unsigned long n){ if (likely(check_copy_size(to, n, false))) n = _copy_from_user(to, from, n); return n;}static __always_inline unsigned long __must_checkcopy_to_user(void __user *to, const void *from, unsigned long n){ if (likely(check_copy_size(from, n, true))) n = _copy_to_user(to, from, n); return n;}static __always_inline unsigned long __must_checkcopy_in_user(void __user *to, const void __user *from, unsigned long n){ might_fault(); if (access_ok(to, n) && access_ok(from, n)) n = raw_copy_in_user(to, from, n); return n;}/* * Without arch opt-in this generic copy_mc_to_kernel() will not handle * #MC (or arch equivalent) during source read. */static inline unsigned long __must_checkcopy_mc_to_kernel(void *dst, const void *src, size_t cnt){ memcpy(dst, src, cnt); return 0;} |
整体调用链如下
123456789 | copy_from_user(to, from, n) └─ check_copy_size(to, n, false) // 编译时对象大小检查(FORTIFY_SOURCE) └─ _copy_from_user(to, from, n) ├─ might_fault() // 标记可能引起页错误(调度点) ├─ should_fail_usercopy() // 故障注入(用于测试) ├─ access_ok(from, n) // 检查用户指针是否合法(关键!) ├─ instrument_copy_from_user() // KASAN / UBSAN 检测 ├─ raw_copy_from_user(...) // 实际拷贝(arch-specific) └─ if (res) memset(..., 0, res) // **Zero-padding!** |
Zero-padding: 如果只拷贝了部分数据(比如因为页错误),剩余未拷贝的部分会被清零。这是为了防止内核信息泄露(例如栈上未初始化内存被用户读到)。
access_ok():确保from指向用户空间且长度合法(防止访问内核地址)
check_object_size():防止缓冲区溢出(配合 FORTIFY_SOURCE)
instrument_*:KASAN 检测越界访问即使拷贝失败,也不会 panic,而是返回未拷贝字节数,驱动可据此处理。
copy_to_user()
函数原型:
1234567 | static __always_inline unsigned long __must_checkcopy_to_user(void __user *to, const void *from, unsigned long n){ if (likely(check_copy_size(from, n, true))) n = _copy_to_user(to, from, n); return n;} |
- 函数作用:把内核空间的数据复制到用户空间。
- 参数含义:
*to是用户空间的指针from是内核空间的指针n是从内核空间向用户空间拷贝的字节数
- 返回值:等于 0 表示成功,其他表示失败
copy_from_user()
1234567 | static __always_inline unsigned long __must_checkcopy_from_user(void *to, const void __user *from, unsigned long n){ if (likely(check_copy_size(to, n, false))) n = _copy_from_user(to, from, n); return n;} |
- 函数作用:把用户空间的数据复制到内核空间。
- 参数含义
*to是内核空间的指针from是用户空间的指针n是从用户空间向内核空间拷贝的字节数
- 返回值:等于 0 表示成功,其他表示失败
copy_struct_from_user()
这是现代 syscall 推荐接口。
解决问题:用户空间传入结构体版本不同。
- 如果用户传入的 struct 小,则内核自动补零
- 如果用户传入的 struct 大,检查多出字段是否为 0
返回值:
0成功-EFAULT访问失败-E2BIG非零扩展字段
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 | /** * copy_struct_from_user: copy a struct from userspace * @dst: Destination address, in kernel space. This buffer must be @ksize * bytes long. * @ksize: Size of @dst struct. * @src: Source address, in userspace. * @usize: (Alleged) size of @src struct. * * Copies a struct from userspace to kernel space, in a way that guarantees * backwards-compatibility for struct syscall arguments (as long as future * struct extensions are made such that all new fields are *appended* to the * old struct, and zeroed-out new fields have the same meaning as the old * struct). * * @ksize is just sizeof(*dst), and @usize should've been passed by userspace. * The recommended usage is something like the following: * * SYSCALL_DEFINE2(foobar, const struct foo __user *, uarg, size_t, usize) * { * int err; * struct foo karg = {}; * * if (usize > PAGE_SIZE) * return -E2BIG; * if (usize < FOO_SIZE_VER0) * return -EINVAL; * * err = copy_struct_from_user(&karg, sizeof(karg), uarg, usize); * if (err) * return err; * * // ... * } * * There are three cases to consider: * * If @usize == @ksize, then it's copied verbatim. * * If @usize < @ksize, then the userspace has passed an old struct to a * newer kernel. The rest of the trailing bytes in @dst (@ksize - @usize) * are to be zero-filled. * * If @usize > @ksize, then the userspace has passed a new struct to an * older kernel. The trailing bytes unknown to the kernel (@usize - @ksize) * are checked to ensure they are zeroed, otherwise -E2BIG is returned. * * Returns (in all cases, some data may have been copied): * * -E2BIG: (@usize > @ksize) and there are non-zero trailing bytes in @src. * * -EFAULT: access to userspace failed. */static __always_inline __must_check intcopy_struct_from_user(void *dst, size_t ksize, const void __user *src, size_t usize){ size_t size = min(ksize, usize); size_t rest = max(ksize, usize) - size; /* Deal with trailing bytes. */ if (usize < ksize) { memset(dst + size, 0, rest); } else if (usize > ksize) { int ret = check_zeroed_user(src + size, rest); if (ret <= 0) return ret ?: -E2BIG; } /* Copy the interoperable parts of the struct. */ if (copy_from_user(dst, src, size)) return -EFAULT; return 0;} |
没有 copy_struct_to_user 这个函数
get_user()
123456789 | // include/asm-generic/uaccess.h |
读一个简单变量,内部处理 fault。宏将用户空间的变量值复制到内核空间。注意,错误时x被设置为0。
- x 表示存储结果的内核变量
- ptr 是用户空间的源地址。
间接引用ptr的结果必须在没有强制类型转换的情况下可赋值给x。
成功时返回0,错误时返回
-EFAULT。
put_user()
12345678910 | // include/asm-generic/uaccess.h |
该宏将内核空间变量值复制到用户空间。
- x 表示要复制到用户空间的值
- ptr 是用户空间中的目标地址。
x必须可分配给间接引用ptr的结果。换句话说,它们必须有(或指向)相同的类型。
成功时返回0,出错时返回
-EFAULT。
示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 | static int major = 0;static int minor = 0;module_param(major, int, S_IRUGO);module_param(minor, int, S_IRUGO);struct class *class;struct device *device;static dev_t dev_id;static struct cdev cdev_test;int cdev_test_open(struct inode *inode, struct file *file){ pr_info("cdev_test_open was called"); return 0;}ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off){ char kbuf[] = "This is my_driver read\n"; if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) { pr_err("copy kbuf to buf from kernel error\n"); return -1; } pr_info("copy kbuf to buf from kernel ok\n"); pr_info("cdev_test_read was called"); return 0;}ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off){ char kbuf[32] = { 0 }; if (copy_from_user(kbuf, buf, size) != 0) { pr_err("copy buf to kbuf from kernel error\n"); return -1; } printk("read from user: %s", kbuf); pr_info("cdev_test_write was called"); return 0;}int cdev_test_release(struct inode *inode, struct file *file){ pr_info("cdev_test_release was called"); return 0;}static struct file_operations cdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .write = cdev_test_write, .release = cdev_test_release };static int __init my_driver_init(void){ int ret; if (major) { dev_id = MKDEV(major, minor); pr_info("major from module_param: %d", MAJOR(dev_id)); pr_info("minor from module_param: %d", MINOR(dev_id)); ret = register_chrdev_region(dev_id, 1, "my_driver device"); if (ret < 0) { pr_err("register_chrdev_region error\n"); return ret; } else { pr_info("register_chrdev_region ok\n"); } } else { ret = alloc_chrdev_region(&dev_id, 0, 1, "my_driver device"); if (ret < 0) { pr_err("alloc_chrdev_region error\n"); return ret; } else { pr_info("alloc_chrdev_region ok\n"); pr_info("major allocated: %d", MAJOR(dev_id)); pr_info("minor allocated: %d", MINOR(dev_id)); } } cdev_init(&cdev_test, &cdev_test_ops); //将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块 cdev_test.owner = THIS_MODULE; ret = cdev_add(&cdev_test, dev_id, 1); if (ret < 0) { pr_err("cdev_add error\n"); unregister_chrdev_region(dev_id, 1); return ret; } else { pr_info("cdev_add ok\n"); } class = class_create(THIS_MODULE, "test"); // /dev/my_driver device = device_create(class, NULL, dev_id, NULL, "my_driver"); pr_info("my_driver: Module loaded\n"); return 0;}static void __exit my_driver_exit(void){ // 删除设备结点 device_destroy(class, dev_id); // 删除该设备结点的class class_destroy(class); // 删除cdev cdev_del(&cdev_test); // 释放设备号 unregister_chrdev_region(dev_id, 1); pr_info("unregister_chrdev_region ok\n"); pr_info("my_driver: Module unloaded\n");}module_init(my_driver_init);module_exit(my_driver_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Zhao Hang");MODULE_DESCRIPTION("my_driver Kernel Module"); |
测试
测试代码:
12345678910111213141516171819202122232425262728293031323334353637 | int main(int argc, char **argv){ int ret, fd; fd = open("/dev/my_driver", O_RDWR); if (fd < 0) { printf("open /dev/my_driver error\n"); return -1; } char rbuf[32] = { 0 }; ret = read(fd, rbuf, sizeof(rbuf)); if (ret < 0) { printf("read error\n"); goto err; } printf("read from driver:%s\n", rbuf); char wbuf[32] = "hello world\n"; ret = write(fd, wbuf, sizeof(wbuf)); if (ret < 0) { printf("write error\n"); goto err; }success: close(fd); return 0;err: close(fd); return -1;} |
makefile
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152 | TARGET_EXEC := testSRC_DIRS := ./BUILD_DIR := ./buildCC = aarch64-linux-gnu-gccCFLAGS = -g -Wall# 源文件收集SRCS := $(shell find $(SRC_DIRS) -name '*.c' -or -name '*.s' -or -name '*.S')# 目标文件映射OBJS := $(patsubst %.c,$(BUILD_DIR)/%.o,$(SRCS))OBJS := $(patsubst %.s,$(BUILD_DIR)/%.o,$(OBJS))OBJS := $(patsubst %.S,$(BUILD_DIR)/%.o,$(OBJS))# 包含路径INC_DIRS := $(shell find $(SRC_DIRS) -type d)INC_FLAGS := $(addprefix -I,$(INC_DIRS))# 自动生成依赖文件CPPFLAGS := $(INC_FLAGS) -MMD -MP# 链接最终可执行文件$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS) @mkdir -p $(dir $@) $(CC) $(OBJS) -o $@ $(LDFLAGS)# 编译规则 (C)$(BUILD_DIR)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# 编译规则 (汇编)# 不需要预处理的汇编代码$(BUILD_DIR)/%.o: %.s @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# 需要预处理的汇编代码$(BUILD_DIR)/%.o: %.S @mkdir -p $(dir $@) $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@# 清理clean: rm -rf $(BUILD_DIR)deploy: cp $(BUILD_DIR)/$(TARGET_EXEC) ~/tftp# 自动依赖包含-include $(OBJS:.o=.d) |
测试输出:
12345678910111213141516 | ~ # insmod my_driver.ko[ 7.250701] my_driver: loading out-of-tree module taints kernel.[ 7.257204] alloc_chrdev_region ok[ 7.257387] major allocated: 511[ 7.257406] minor allocated: 0[ 7.257563] cdev_add ok[ 7.258587] my_driver: Module loaded~ # mdev -s~ # ./test[ 18.006803] cdev_test_open was called[ 18.007016] copy kbuf to buf from kernel okread from driver:This is my_driver read[ 18.007275] cdev_test_read was called[ 18.010813] read from user: hello world[ 18.011205] cdev_test_write was called |
文件私有数据
通常在驱动开发中会为设备自定义设备结构体并将设备结构体设置为文件私有数据。设备结构体用来存放硬件相关信息,如设备号、类、设备名称等。
Linux 中并没有明确规定要必须要使用文件私有数据,但是在 linux 驱动源码中随处可见文件私有数据,因此可以认为使用文件私有数据是 Linux 驱动遵循的“潜规则”,实际上文件私有数据也体现了 Linux 面向对象的思想。
文件私有数据就是将私有数据 private_data 指向设备结构体。
123456 | struct device_test dev1;static int cdev_test_open(struct inode *inode, struct file *file){ file->private_data = &dev1; return 0;} |
然后在 read, write 等函数中通过 private_data 访问设备结构体
1234 | static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off_t){ struct device_test *test_dev = (struct device_test *)file->private_data; return 0;} |
用全局变量也可以实现这样的功能,但是随着驱动代码复杂度增多,使用 private_data 更利于管理
示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 | static int major = 0;module_param(major, int, S_IRUGO);MODULE_DESCRIPTION("device num: major");static int minor = 0;module_param(minor, int, S_IRUGO);MODULE_DESCRIPTION("device num: minor");static char buf[] = "Hello World from kernel\n";struct test_data { dev_t dev_num; struct cdev cdev; struct class *class; struct device *dev; char kbuf[32];};static struct test_data *dat;static int file_private_test_open(struct inode *inode, struct file *file){ pr_info("file_private_test_open is called\n"); // 设置私有数据 file->private_data = dat; pr_info("file->private_data is set\n"); return 0;}static ssize_t file_private_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ struct test_data *data; size_t len; if (*offset != 0) { // EOF return 0; } data = file->private_data; len = min(size, strlen(dat->kbuf)); pr_info("file_private_test_read is called\n"); if (copy_to_user(buf, data->kbuf, len) != 0) { pr_err("copy_to_user error\n"); return -EFAULT; } *offset += len; return sizeof(data->kbuf);}static int file_private_test_release(struct inode *inode, struct file *file){ pr_info("file_private_test_release is called\n"); return 0;}static struct file_operations fops = { .owner = THIS_MODULE, .open = file_private_test_open, .read = file_private_test_read, .release = file_private_test_release,};static int __init file_private_test_init(void){ int err; pr_info("file_private_test init\n"); dat = (struct test_data *)kmalloc(sizeof(struct test_data), GFP_KERNEL); if (dat == NULL) { pr_err("no memory\n"); goto kmalloc_fail; } strscpy(dat->kbuf, buf, sizeof(dat->kbuf)); // 申请设备号 if (major) { dat->dev_num = MKDEV(major, minor); err = register_chrdev_region(dat->dev_num, 1, "file_private_test chrdev region"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_fail; } } else { err = alloc_chrdev_region(&dat->dev_num, 0, 1, "file_private_test chrdev region"); if (err < 0) { pr_err("register_chrdev_region error\n"); goto chrdev_region_fail; } } // 添加cdev cdev_init(&dat->cdev, &fops); err = cdev_add(&dat->cdev, dat->dev_num, 1); if (err < 0) { pr_err("cdev_add error\n"); goto cdev_add_fail; } // 创建设备节点 dat->class = class_create(THIS_MODULE, "char_test"); if (IS_ERR(dat->class)) { err = PTR_ERR(dat->class); pr_err("create class error"); goto class_create_fail; } dat->dev = device_create(dat->class, NULL, dat->dev_num, NULL, "char_test_dev"); if (IS_ERR(dat->dev)) { err = PTR_ERR(dat->dev); pr_err("create device error\n"); goto device_create_fail; } return 0;device_create_fail: class_destroy(dat->class);class_create_fail: cdev_del(&dat->cdev);cdev_add_fail: unregister_chrdev_region(dat->dev_num, 1);chrdev_region_fail: kfree(dat);kmalloc_fail: return err;}static void __exit file_private_test_exit(void){ device_destroy(dat->class, dat->dev_num); class_destroy(dat->class); cdev_del(&dat->cdev); unregister_chrdev_region(dat->dev_num, 1); kfree(dat); pr_info("file_private_test exit\n");}module_init(file_private_test_init);module_exit(file_private_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is a test sample for file private data"); |
测试:
123456789 | $ insmod file_private_data_test.ko[ 8.022277] file_private_data_test: loading out-of-tree module taints kernel.[ 8.032581] file_private_test init$ cat /dev/char_test_dev[ 14.998329] file_private_test_open is called[ 14.998580] file->private_data is set[ 14.999659] file_private_test_read is calledHello World from kernel[ 15.000469] file_private_test_release is called |
使用文件私有数据的场景
在 Linux 中,使用主设备号来表示对应的某一类驱动。用次设备号表示这类驱动下的各个设备。
假如现在我们的驱动要支持主设备相同,但是次设备号不同的设备。我们的驱动就可以利用文件私有数据来写。
container_of 宏
include/linux/kernel.h
123456 |
ptr指向结构体中某个成员的指针。type目标结构体类型。member成员在结构体中的名字。BUILD_BUG_ON_MSG(...)编译期类型检查,保证 ptr 和结构体成员类型一致,避免类型错误。__mptr - offsetof(type, member)核心公式:用成员指针减去该成员在结构体中的偏移,得到结构体起始地址。offsetof(type, member)标准宏,计算成员在结构体内的字节偏移。
示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 | struct container_data { dev_t dev_num; struct cdev cdev; struct device *dev; char kbuf[KBUF_SIZE];};static struct class *class;static struct container_data *dat1;static struct container_data *dat2;int container_of_test_open(struct inode *inode, struct file *file){ struct container_data *dat; // 通过container_of获取特定的container_data dat = container_of(inode->i_cdev, struct container_data, cdev); file->private_data = dat; pr_info("container_of_test_open is called, dev major: %d, dev minor: %d\n", MAJOR(dat->dev_num), MINOR(dat->dev_num)); return 0;}ssize_t container_of_test_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ struct container_data *dat = file->private_data; size_t kbuf_len = strlen(dat->kbuf); size_t len = min(size, (size_t)(kbuf_len - *offset)); if (*offset >= kbuf_len) return 0; if (copy_to_user(buf, dat->kbuf + *offset, len)) return -EFAULT; *offset += len; return len;}ssize_t container_of_test_write(struct file *file, const char __user *buf, size_t size, loff_t *offset){ struct container_data *dat = file->private_data; size_t bytes = min(size, (size_t)(KBUF_SIZE - 1 - *offset)); if (*offset >= KBUF_SIZE - 1) return -ENOSPC; if (copy_from_user(dat->kbuf + *offset, buf, bytes) != 0) return -EFAULT; *offset += bytes; dat->kbuf[*offset] = '\0'; return bytes;}int container_of_test_release(struct inode *inode, struct file *file){ pr_info("container_of_test_release is called\n"); return 0;}static struct file_operations fops = { .owner = THIS_MODULE, .open = container_of_test_open, .read = container_of_test_read, .write = container_of_test_write, .release = container_of_test_release,};static int __init container_of_test_init(void){ int err; dat1 = (struct container_data *)kmalloc(sizeof(struct container_data), GFP_KERNEL); if (dat1 == NULL) { pr_err("no memory dat1"); err = -ENOMEM; goto kmalloc_dat1_fail; } memset(dat1, 0, sizeof(struct container_data)); dat2 = (struct container_data *)kmalloc(sizeof(struct container_data), GFP_KERNEL); if (dat2 == NULL) { pr_err("no memory dat2"); err = -ENOMEM; goto kmalloc_dat2_fail; } memset(dat2, 0, sizeof(struct container_data)); err = alloc_chrdev_region(&dat1->dev_num, 0, 2, "container_of_test chrdev region"); if (err < 0) { pr_err("alloc_chrdev_region error\n"); goto alloc_chrdev_region_fail; } dat2->dev_num = MKDEV(MAJOR(dat1->dev_num), MINOR(dat1->dev_num) + 1); cdev_init(&dat1->cdev, &fops); err = cdev_add(&dat1->cdev, dat1->dev_num, 1); if (err < 0) { pr_err("cdev_add dat1 error\n"); goto cdev_add_dat1_fail; } cdev_init(&dat2->cdev, &fops); err = cdev_add(&dat2->cdev, dat2->dev_num, 1); if (err < 0) { pr_err("cdev_add dat2 error\n"); goto cdev_add_dat2_fail; } class = class_create(THIS_MODULE, "chrdev"); if (IS_ERR(class)) { err = PTR_ERR(class); goto class_create_fail; } dat1->dev = device_create(class, NULL, dat1->dev_num, NULL, "container_of_test_dev%d", 0); if (IS_ERR(dat1->dev)) { err = PTR_ERR(dat1->dev); goto device_create_dat1_fail; } dat2->dev = device_create(class, NULL, dat2->dev_num, NULL, "container_of_test_dev%d", 1); if (IS_ERR(dat2->dev)) { err = PTR_ERR(dat2->dev); goto device_create_dat2_fail; } return 0;device_create_dat2_fail: device_destroy(class, dat1->dev_num);device_create_dat1_fail: class_destroy(class);class_create_fail: cdev_del(&dat2->cdev);cdev_add_dat2_fail: cdev_del(&dat1->cdev);cdev_add_dat1_fail: unregister_chrdev_region(dat1->dev_num, 2);alloc_chrdev_region_fail: kfree(dat2);kmalloc_dat2_fail: kfree(dat1);kmalloc_dat1_fail: return err;}static void __exit container_of_test_exit(void){ device_destroy(class, dat1->dev_num); device_destroy(class, dat2->dev_num); class_destroy(class); cdev_del(&dat2->cdev); cdev_del(&dat1->cdev); unregister_chrdev_region(dat1->dev_num, 2); kfree(dat2); kfree(dat1); pr_info("container_of_test exit\n");}module_init(container_of_test_init);module_exit(container_of_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is a test sample for container_of"); |
杂项设备驱动
杂项设备属于特殊的一种字符型设备,是对字符设备的一种封装,本质也是字符设备。
在 Linux 中,把无法归类的五花八门的设备定义成杂项设备。相较于字符设备,杂项设备有以下两个优点:
- 节省主设备号: 杂项设备的主设备号固定为 10,当系统中注册了多个杂项设备驱动时,只需使用子设备号进行区分即可。而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,进而造成了主设备号浪费。
- 使用简单:杂项设备驱动本身包含创建设备节点操作,不需要额外使用 class_create()函数和 device_create()函数实现自动创建设备节点操作。只需要填充驱动中的 file_operations 结构体中的成员即可。
定义
include/linux/miscdevice.h
1234567891011121314151617 | struct device;struct attribute_group;struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; const struct attribute_group **groups; const char *nodename; umode_t mode;};extern int misc_register(struct miscdevice *misc);extern void misc_deregister(struct miscdevice *misc); |
定义一个杂项设备,一般只需要填充 miscdevice 结构体中 minor、name、fops 这三个成员变量。
minor杂项指次设备号,可以从内核源码include/linux/miscdevice.h文件中预定义的次设备号挑选,也可以自行定义子设备号(没有被其他设备使用即可),通常情况下将该参数设置为 MISC_DYNAMIC_MINOR,表示自动分配子设备号。name表示杂项设备的名字。也就是杂项设备驱动注册成功之后,会在 dev 目录下生成名为 name 的设备节点。fops指向了 file_operations 的结构体,表示文件操作集。
misc_register
1 | extern int misc_register(struct miscdevice *misc); |
函数作用:基于 misc_class 构造一个设备,将 miscdevice 结构挂载到 misc_list 列表上,并初始化与 linux 设备模型相关的结构。进而起到杂项设备注册的作用。
参数含义:
- misc: 杂项设备的结构体指针
函数返回值:申请成功返回 0,申请失败返回负数
misc_deregister
1 | extern void misc_deregister(struct miscdevice *misc); |
- 函数作用:从 misc_list 中删除 miscdevice,进而起到杂项设备卸载的作用。
- 参数含义:
- misc: 杂项设备的结构体指针
示例
123456789101112131415161718192021222324252627282930 | static struct file_operations fops = { .owner = THIS_MODULE,};static struct miscdevice miscdev={ .minor=MISC_DYNAMIC_MINOR,// 动态申请次设备号 .name="miscdev", .fops = &fops,};static int __init misc_device_test_init(void){ misc_register(&miscdev); return 0;}static void __exit misc_device_test_exit(void){ misc_deregister(&miscdev);}module_init(misc_device_test_init);module_exit(misc_device_test_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("This is a misc device sample"); |
Linux 驱动错误处理
goto 逆序释放
逆序释放:后申请的先释放
123456789101112131415161718192021222324252627282930313233343536373839 | static int __init my_driver_init(void){ int ret; ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "my_driver"); if (ret < 0) goto fail_alloc; cdev_init(&dev1.cdev_test, &cdev_test_fops); dev1.cdev_test.owner = THIS_MODULE; ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1); if (ret < 0) goto fail_cdev_add; dev1.class = class_create(THIS_MODULE, "test1"); if (IS_ERR(dev1.class)) { ret = PTR_ERR(dev1.class); goto fail_class_create; } dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1"); if (IS_ERR(dev1.device)) { ret = PTR_ERR(dev1.device); goto fail_device_create; } pr_info("my_driver initialized successfully\n"); return 0;fail_device_create: class_destroy(dev1.class);fail_class_create: cdev_del(&dev1.cdev_test);fail_cdev_add: unregister_chrdev_region(dev1.dev_num, 1);fail_alloc: return ret;} |
- 标签顺序是“逆序释放”,即 后申请的先释放。
PTR_ERR()+IS_ERR()用于判断class_create()和device_create()的错误。PTR_ERR()将错误指针转换为错误码,并进行错误码的返回goto能避免重复写多次if检查和清理代码,使代码简洁。
IS_ERR()
如果一个函数返回值是指针类型,在调用出错的情况下会返回 NULL 指针,但是返回 NULL 不能知道问题的确切性,一些函数需要返回一个实际的错误码以便于工程师能够基于返回值作出正确的判断。对于任何一个指针来说,必然存在三种情况:
- 合法指针
- NULL(也就是空指针)
- 错误指针(也就是无效指针)
在 Linux 内核中,所谓的错误指针指向了内核空间的最后一页,例如,对于一个 64 位系统来说,最后一页的地址是 0xfffffffffffff000~0xffffffffffffffff,最后一页地址是被保留的,并这段地址与内核定义的错误码相关联,因此可以使用错误码用来指明对应出错的情况,如果一个指针指向了该页的地址范围就被定义为错误指针。在 Linux 内核源码中提供了错误指针相关的 API 函数,主要有 IS_ERR()、PTR_ERR()、ERR_PTR()函数等,其函数的源码在include/linux/err.h文件中。
include/linux/err.h
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 | /* SPDX-License-Identifier: GPL-2.0 *//* * Kernel pointers have redundant information, so we can use a * scheme where we can return either an error code or a normal * pointer with the same return value. * * This should be a per-architecture thing, to allow different * error and pointer decisions. */static inline void * __must_check ERR_PTR(long error){ return (void *) error;}static inline long __must_check PTR_ERR(__force const void *ptr){ return (long) ptr;}static inline bool __must_check IS_ERR(__force const void *ptr){ return IS_ERR_VALUE((unsigned long)ptr);}static inline bool __must_check IS_ERR_OR_NULL(__force const void *ptr){ return unlikely(!ptr) || IS_ERR_VALUE((unsigned long)ptr);}/** * ERR_CAST - Explicitly cast an error-valued pointer to another pointer type * @ptr: The pointer to cast. * * Explicitly cast an error-valued pointer to another pointer type in such a * way as to make it clear that's what's going on. */static inline void * __must_check ERR_CAST(__force const void *ptr){ /* cast away the const */ return (void *) ptr;}static inline int __must_check PTR_ERR_OR_ZERO(__force const void *ptr){ if (IS_ERR(ptr)) return PTR_ERR(ptr); else return 0;} |
PTR_ERR()
把错误指针转成错误码
include/linux/err.h
1234 | static inline long __must_check PTR_ERR(__force const void *ptr){ return (long) ptr;} |
ERR_PTR()
把错误码转为指针
1234 | static inline void * __must_check ERR_PTR(long error){ return (void *) error;} |
IS_ERR_OR_NULL()
判断是否是错误或者NULL
1234 | static inline bool __must_check IS_ERR_OR_NULL(__force const void *ptr){ return unlikely(!ptr) || IS_ERR_VALUE((unsigned long)ptr);} |
错误码
include/uapi/asm-generic/errno-base.h
1234567891011121314151617181920212223242526272829303132333435363738394041 | /* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ |
中文翻译:
12345678910111213141516171819202122232425262728293031323334 |
示例:
1234 | if (IS_ERR(dev1.class)) { ret = PTR_ERR(dev1.class); goto fail_class_create;} |
错误有时会跨越内核空间,传播到用户空间。如果返回的错误是对系统调用(open、read、ioctl、mmap)的响应,则该值将自动赋给用户空间errno全局变量,在该变量上调用strerror(errno)可以将错误转换为可读字符串:
123456 | if(wite(fd, buf, 1) < 0) { printf("something gone wrong! %s\n", strerror(errno)); } |
内核编码风格中:
- 如果函数名称是动作或命令式命令,则函数返回的错误代码应该是整数;
- 如果函数名称是一个谓词,则该函数应返回布尔值succeeded(成功的)。
例如,add_work是一个命令,add_work() 函数返回0表示成功,返回-EBUSY表示失败。
同样,PCI device present是谓词,pci_dev_present() 函数如果成功找到匹配设备,则返回 1;否则返回0。
RK3568 点亮 LED 灯
原理图
topeet RK3568 Working LED原理图如下

可以看到LED灯接在了GPIO0_B7这个GPIO口上。当 GPIO0_B7 为高电平时,三极管 Q16导通,LED9 点亮。当 GPIO0_B7 为低电平时,三极管 Q16 截止,LED9 不亮。
查询寄存器地址
GPIO0B 的引脚复用
首先我们得设置GPIO0B的引脚复用,从PMU_GRF查找

搜索GPIO0B7可以看到其对应寄存器为PMU_GRF_GPIO0B_IOMUX_H的14:12位的设置

而PMU_GRF寄存器地址从下面可以看到是0xFDC20000

所以复用寄存器地址=基地址+偏移地址=0xFDC20000+0xC=0xFDC2000C。
使用 io 命令查看此寄存器的地址:
1 | io -r -4 0xFDC2000C |
GPIO 寄存器
由TRM中的Address Mapping可知GPIO0地址位0xFDD60000

RK3568 TRM中GPIO章节介绍:

可知RK3568寄存器分为:
方向寄存器(DDR)
- GPIO_SWPORT_DR_L
- GPIO_SWPORT_DR_H
数据寄存器(DR)
- GPIO_SWPORT_DDR_L
- GPIO_SWPORT_DDR_H
外部输入寄存器(EXT_PORT)
- GPIO_EXT_PORT
即
- GPIO_SWPORT_DDR_L/H 决定 引脚是输入还是输出
- GPIO_SWPORT_DR_L/H 决定 输出引脚时输出的电平
- GPIO_EXT_PORT 反映 引脚当前的实际电平(只读)
数据寄存器和方向寄存器分为L和H
- GPIOA和GPIOB地址落在L上
- GPIOC和GPIOD地址落在H上
又:

可知GPIO0~GPIO4这五组的寄存器是不同的。
我们需要先设置GPIO方向寄存器为输出,然后设置输出电平为高电平。


GPIO 有四组 GPIO,分别是 GPIOA,GPIOB,GPIOC,GPIOD。每组又以 A0 ~ A7, B0 ~ B7, C0 ~ C7, D0 ~ D7 作为编号区分。
GPIO0B7 在L上,所以方向寄存器的偏移地址为 0x0008,数据寄存器偏移为0x0000。
GPIO0 的基地址为 0xFDD60000。因此
方向寄存器的地址=基地址+偏移地址=0xFDD60000+0x0008=0xFDD60008
数据寄存器的地址=基地址+偏移地址=0xFDD60000+0x0000=0xFDD60000
外部输入寄存器地址=基地址+偏移地址=0xFDD60000+0x0070=0xFDD60070
方向寄存器

可知31:16为可写位,因此GPIOB7应该是16+8+8-1=31,即bit 31设置write access。
而0+8+8-1=15,即bit 15对应设置GPIO0B7的方向为input或output
数据寄存器

同理16+8+8-1=31,即bit 31设置write access。
而0+8+8-1=15, 即bit 15对应设置GPIO0B7输出高电平或低电平。
总结
- 复用关系寄存器的基地址为 0xFDC20000
- GPIO0B_IOMUX_H偏移地址为 000C ,所以要操作的地址为基地址+偏移地址=0xFDC2000C
- GPIO0B_IOMUX_H的 bit 28 ~ bit 30 为bit write access, 0 为disable, 1为enable
- GPIO0B_IOMUX_H的 bit 12 ~ bit 14 设置为0为GPIO0_B7,1为PWM0_M0,2为CPU_AVS
- GPIO0B_IOMUX_H偏移地址为 000C ,所以要操作的地址为基地址+偏移地址=0xFDC2000C
- GPIO0 的基地址为 0xFDD60000
- 方向寄存器地址=基地址+偏移地址=0xFDD60008
- bit 31 对应GPIO0_B7的方向write access,0为disable,1为enable
- bit 15 对应GPIO0_B7的方向,0为input,1为output
- 数据寄存器地址=基地址+偏移地址=0xFDD60000
- bit 31 对应GPIO0_B7的数据write access,0为disable,1为enable
- bit 15 对应GPIO0_B7的数据,0为输出low,1为输出high
- 方向寄存器地址=基地址+偏移地址=0xFDD60008
使用io命令测试
12345678910111213141516171819202122 | su root# 复用关系寄存器sudo io -4 -w 0xFDC2000C 0x70001000# 验证是否写入成功,读sudo io -4 -r 0xFDC2000C# 设置输出sudo io -4 -w 0xFDD60008 0x80008000# 验证sudo io -4 -r 0xFDD60008# 读当前值sudo io -4 -r 0xFDD60000# 开灯sudo io -4 -w 0xFDD60000 0x8000C000# 关灯sudo io -4 -w 0xFDD60000 0x80004000 |
示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 | /* 复用寄存器 *//* 数据寄存器与方向寄存器 */// 方向寄存器// 数据寄存器// 外部输入寄存器,readonlystruct led_drv_data { dev_t dev_num; struct cdev cdev; struct class *class; struct device *dev; void __iomem *gpio0b_iomux; void __iomem *gpio0_swport_ddr_l; void __iomem *gpio0_swport_dr_l; void __iomem *gpio0_ext_port;};static struct led_drv_data *led_drv_data;int light_up_led_open(struct inode *inode, struct file *file){ file->private_data = led_drv_data; return 0;}ssize_t light_up_led_read(struct file *file, char __user *buf, size_t size, loff_t *offset){ struct led_drv_data *drv_data = file->private_data; u32 val; if (size != sizeof(u32)) { pr_info("read need 4 bytes\n"); return -ENOMEM; } val = readl(drv_data->gpio0_ext_port); val &= 1 << 15;// gpio0_b7 val = val >> 15; if (copy_to_user(buf, &val, sizeof(u32)) != 0) { return -EFAULT; } return sizeof(u32);}ssize_t light_up_led_write(struct file *file, const char __user *buf, size_t size, loff_t *offset){ struct led_drv_data *drv_data = file->private_data; u32 val; if (size != sizeof(u32)) { pr_err("write need 4 bytes\n"); return -ENOMEM; } if (copy_from_user(&val, buf, sizeof(u32)) != 0) { return -EFAULT; } if (val > 0) { // 配置为GPIO输出 val = readl(drv_data->gpio0_swport_ddr_l); val |= 0x80008000; writel(val, drv_data->gpio0_swport_ddr_l); // 打开 val = readl(drv_data->gpio0_swport_dr_l); val |= 0x80008000; writel(val, drv_data->gpio0_swport_dr_l); } else if (val == 0) { // 配置为GPIO输出 val = readl(drv_data->gpio0_swport_ddr_l); val |= 0x80008000; writel(val, drv_data->gpio0_swport_ddr_l); // 关闭 val = readl(drv_data->gpio0_swport_dr_l); val |= 0x80000000; val &= 0xffff7fff; writel(val, drv_data->gpio0_swport_dr_l); } return sizeof(u32);}int light_up_led_release(struct inode *inode, struct file *file){ return 0;}struct file_operations fops = { .owner = THIS_MODULE, .open = light_up_led_open, .read = light_up_led_read, .write = light_up_led_write, .release = light_up_led_release,};static int __init light_up_led_init(void){ int err; u32 val; led_drv_data = kzalloc(sizeof(struct led_drv_data), GFP_KERNEL); if (led_drv_data == NULL) { err = -ENOMEM; goto kzalloc_fail; } err = alloc_chrdev_region(&led_drv_data->dev_num, 0, 1, "led chrdev region"); if (err < 0) goto alloc_chrdev_region_fail; cdev_init(&led_drv_data->cdev, &fops); led_drv_data->cdev.owner = THIS_MODULE; err = cdev_add(&led_drv_data->cdev, led_drv_data->dev_num, 1); if (err < 0) goto cdev_add_fail; led_drv_data->class = class_create(THIS_MODULE, "test_led"); if (IS_ERR(led_drv_data->class)) { err = PTR_ERR(led_drv_data->class); goto create_class_fail; } led_drv_data->dev = device_create(led_drv_data->class, NULL, led_drv_data->dev_num, NULL, "test_led%d", 0); if (IS_ERR(led_drv_data->dev)) { err = PTR_ERR(led_drv_data->dev); goto device_create_fail; } led_drv_data->gpio0b_iomux = ioremap(GPIO0B_IOMUX, 4); led_drv_data->gpio0_swport_ddr_l = ioremap(GPIO0_SWPORT_DDR_L, 4); led_drv_data->gpio0_swport_dr_l = ioremap(GPIO0_SWPORT_DR_L, 4); led_drv_data->gpio0_ext_port = ioremap(GPIO0_EXT_PORT, 4); // 设置引脚复用为GPIO val = readl(led_drv_data->gpio0b_iomux); val |= 0x70000000; // write access val &= 0xFFFF8FFF; // gpio0_b7 writel(val, led_drv_data->gpio0b_iomux); return 0;device_create_fail: class_destroy(led_drv_data->class);create_class_fail: cdev_del(&led_drv_data->cdev);cdev_add_fail: unregister_chrdev_region(led_drv_data->dev_num, 1);alloc_chrdev_region_fail: kfree(led_drv_data);kzalloc_fail: return err;}static void __exit light_up_led_exit(void){ iounmap(led_drv_data->gpio0_ext_port); iounmap(led_drv_data->gpio0b_iomux); iounmap(led_drv_data->gpio0_swport_ddr_l); iounmap(led_drv_data->gpio0_swport_dr_l); device_destroy(led_drv_data->class, led_drv_data->dev_num); class_destroy(led_drv_data->class); cdev_del(&led_drv_data->cdev); unregister_chrdev_region(led_drv_data->dev_num, 1); kfree(led_drv_data);}module_init(light_up_led_init);module_exit(light_up_led_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@163.com>");MODULE_DESCRIPTION("light up a led on topeet RK3568 board"); |
测试代码:
123456789101112131415161718192021222324252627282930313233343536373839404142 | int main(int argc, char **argv){ int fd, err; uint32_t val = 0; fd = open("/dev/test_led0", O_RDWR); if (fd < 0) goto fail; if (argc == 1) { // read err = read(fd, &val, 4); if (err < 0) goto fail; printf("read:0x%08x\n", val); } else if (argc >= 2) { // write val = atoi(argv[1]); if (val < 0) { perror("unsupported\n"); exit(EXIT_FAILURE); } err = write(fd, &val, 4); if (err < 0) goto fail; printf("write val: %u success\n", val); } else { perror("argc error\n"); exit(EXIT_FAILURE); } return 0;fail: fprintf(stderr, "Error:%s[errno:%d]", strerror(errno), errno); exit(EXIT_FAILURE);} |

