目录
一、概述
字符设备驱动代码流程如下,以 led 驱动为例,此驱动基于 linux内核5.4.31 版本
- 申请设备号
- 创建class类
- 创建设备节点
- 硬件初始化
- 实现 file_operations操作接口
二、申请设备号
使用 register_chrdev 函数来申请设备号,同时向内核注册字符设备驱动
在 include/linux/fs.h 中,定义如下:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
这是一个内联函数,在调用时会在被调用处展开,相当于直接调用 __register_chrdev
在 fs/char_dev.c 中,定义 __register_chrdev
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
函数功能:
函数的功能:创建并注册一个或者多个不同次设备号的字符设备。
函数参数介绍:
- major: 主设备号,用于标识一类设备的驱动程序。
- baseminor: 表示设备号的起始次设备号
- count: 表示次设备号的数量
- name: 表示这些字符设备驱动的名称,可通过 cat /proc/devices 查看
- fops: 指向 struct file_operations结构的指针,该结构包含了设备驱动程序的操作函数,如读、写、打开、关闭等
函数返回值:
- 如果 major = 0,动态分配一个主设备号,成功返回主设备号
- 如何 major > 0,表示分配指定的一个主设备号,成功返回0
- 失败返回错误码
从 register_chrdev 的定义可知, linux 内核在注册字符设备驱动时,申请了从 0到 255,共256个连续的次设备号
struct file_operations
这个结构定义在 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 (*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);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
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);
} __randomize_layout;
接口调用示例如下:
const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_close,
.write = led_write
};
int led_major = 0;
#if 0
//静态指定
led_major = 241;
ret = register_chrdev(led_major,"led_chr", &led_fops);
if(ret<0){
printk("register_chrdev error\n");
return -EINVAL;
}
#else
//动态分配
led_major = 0;
led_major = register_chrdev(0,"led_chr", &led_fops);
if(led_major<0){
printk("register_chrdev error\n");
return -EINVAL;
}
#endif
成功调用之后会将该字符设备驱动保存在在 /proc/devices 文件中
其中 241 为主设备号,led_chr 是 register_chrdev 中传入参数
使用 unregister_chrdev 注销字符设备驱动,并释放设备号
在 include/linux/fs.h 中,定义如下:
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
unregister_chrdev 调用完成后,在 /proc/devices 文件中将删除 “241 led_chr”
释放主设备号,后续新注册的字符设备驱动可以使用 241 主设备号
三、创建类
使用 class_create 接口来创建类
该接口定义在 include/linux/device.h,定义如下:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
这是一个宏定义,实际调用 __class_creat, 利用这个类可以创建设备节点。
参数:
- owner:指向拥有这个设备类的模块的指针。这通常设置为 THIS_MODULE 宏,它表示当前正在编译的模块。
- name:设备类的名称,一个以null结尾的字符串。这个名称将用于在sysfs中创建相应的目录,并作为设备节点的属性之一。
返回值:
- 如果成功,class_create 返回一个指向新创建的 struct class 的指针。
- 如果失败(例如由于内存不足或名称冲突),则返回NULL。
__class_creat 定义在文件 drivers/base/class.c 中
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
{
struct class *cls;
int retval;
cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
retval = -ENOMEM;
goto error;
}
cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;
retval = __class_register(cls, key);
if (retval)
goto error;
return cls;
error:
kfree(cls);
return ERR_PTR(retval);
}
接口调用示例如下:
struct class * led_class = NULL;
led_class = class_create(THIS_MODULE,"led_class");
调用成功会在 /sys/class/ 目录下生成 led_class 目录
四、创建设备节点
1、手动创建
使用 mknod 命令手动创建,命令使用方法如下
mknod [options] name {b | c | p} [major] [minor]
2、在驱动中创建
使用 device_create 接口来创建设备节点
该接口定义在 drviers/base/core.c 文件中,接口原型如下
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
参数:
- class:上个章节创建的类结构体指针 :struct class
- parent:父节点,一般为:NULL
- dev_t:设备号:32位的整数,由主设备号和次设备号组成
- drvdata:私有数据,一般为:NULL
- fmt:使用变参的方式表示设备节点名称
返回值:
- 成功返回 struct device *结构体指针
- 失败返回 ERR_PTR() 修饰的错误码
设备号详解:
- 主设备号:占高12位,表示一类设备
- 次设备号:占低20位,表示具体的设备编号
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //从设备号中获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //从设备号中获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将主次设备号转为设备号
device_create_vargs 定义如下
struct device *device_create_vargs(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt,
va_list args)
{
return device_create_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, args);
}
作用是在sysfs文件系统中创建设备节点,使用户空间程序可以通过这些节点与设备进行交互。
接口调用示例如下
int ret = 0;
struct class * led_class = NULL;
struct device *led_device = NULL;
led_class = class_create(THIS_MODULE,"led_class");
if (!led_class )
{
ret = PTR_ERR(led->led_class);
return ret;
}
led_device = device_create(led->led_class, NULL, MKDEV(led->led_major, 1), NULL, "led_dev");
if (!led_device)
{
ret = PTR_ERR(led->led_device);
class_destroy(led->led_class);
return ret ;
}
此时在 /dev 目录下会生成 led_dev 设备文件
在 /sys/class/led_class 下也会有 led_dev 目录,用于存放设备的描述信息
应用层调用 open ,read, write, ioctl 函数便可以打开 /dev/led_dev 设备并对其进行操作
五、硬件初始化
主要用过调用 ioremap 实现寄存器映射,将物理地址映射为虚拟地址
static inline void __iomem *ioremap(phys_addr_t offset, size_t size);
参数:
- offset:物理地址
- size:要映射的空间大小
代码示例
#define GPIOZ 0x54004000
u32 *gpioz_moder = NULL;
gpioz_moder = ioremap(GPIOZ, 24);
通过上述操作操作虚拟地址 gpioz_moder 来操作相对应的寄存器
六、实现 file_operations操作接口
通过填充 const struct file_operations led_fops,可以实现应用层操作接口与应用层对接
int led_open (struct inode *inode, struct file *file)
{
printk("-----------[%s]-------------\n", __FUNCTION__);
//通过映射出的虚拟地址操作硬件
return 0;
}
const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
......
};
led_open 的参数,结构体 struct inode 中 存在成员 dev_t i_rdev,此 i_rdev 保存了字符驱动的主设备号和次设备号,以便以内核知道正在操作哪个设备。
1、应用层给 linux 内核拷贝数据
使用 copy_from_user ,以便应用层调用 write 接口
static __always_inline unsigned long __must_check
copy_from_user(void *to, const void __user *from, unsigned long n);
参数:
- to:内核空间虚拟地址
- from:应用数据的空间地址
- n:数据长度
返回值:
- 成功返回0
- 失败返回参数n
2、linux 内核给应用层拷贝数据
使用 copy_to_user ,以便应用层调用 write 接口
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n);
参数:
- to:应用数据的空间地址
- from:内核空间虚拟地址
- n:数据长度
返回值:
- 成功返回0
- 失败返回参数n
使用 copy_from_user 和 copy_to_user 的原因:
- 利用拷贝,避免了用户空间和内核空间之间访问数据找不到的情况
- 使用内存地址合法性检查,提高内核的安全性
七、操作寄存器的方式
1、直接通过位运算
//将gpio设置为输出模式
*gpioz_moder &= ~(0x3f<<10);
*gpioz_moder |= 0x15 << 10;
2、linux kernel 提供的方式
//向指定的内存地址写数据(寄存器映射的虚拟地址)
static inline void writeb(u8 value, volatile void __iomem *addr) //写1一个字节数据
static inline void writew(u16 value, volatile void __iomem *addr) //写2一个字节数据
static inline void writel(u32 value, volatile void __iomem *addr) //写4一个字节数据
static inline void writeq(u64 value, volatile void __iomem *addr) //写8一个字节数据
//从指定内存中读数据(寄存器映射的虚拟地址)
static inline u8 readb(const volatile void __iomem *addr) //读1一个字节数据
static inline u16 readw(const volatile void __iomem *addr) //读2一个字节数据
static inline u32 readl(const volatile void __iomem *addr) //读4一个字节数据
static inline u64 readq(const volatile void __iomem *addr) //读8一个字节数据
注意:以上操作都是操作虚拟地址,通过 ioremap 转换而来
由此可将 操作1 转化为
//将gpio设置为输出模式
writel(readl(gpioz_moder) & (~(0x3f << 10)), gpioz_moder);
writel(readl(gpioz_moder) | (0x15 << 10), gpioz_moder);
八、创建多个次设备号不同的设备节点
1、理论
当注册完字符设备驱动后,会拿到一个主设备号,主设备号会与 file_operations 关联,所有的主设备号相同的设备节点都会公用这一套 fops ,在调用 open 时会通过设备号(主设备号+次设备号)区分,到底打开了哪个设备节点。
/* struct inode 结构体中会有 i_rdev 成员,
实质上是一个 dev_t 类型,说明该成员是一个设备号
*/
int (*open) (struct inode *, struct file *);
2、实验
// 部分驱动代码
typedef struct led {
u32 led_major;
struct class * led_class;
struct device *led_device[3];
}led_t;
led_t *led;
int led_open (struct inode *inode, struct file *file)
{
static char *name[3] = {};
printk("-----------[%s]-------------\n", __FUNCTION__);
name[0] = "led1_dev";
name[1] = "led2_dev";
name[2] = "led3_dev";
if (inode->i_rdev == MKDEV(led->led_major, 1))
{
file->private_data = name[0];
}
else if (inode->i_rdev == MKDEV(led->led_major, 2))
{
file->private_data = name[1];
}
else if (inode->i_rdev == MKDEV(led->led_major, 3))
{
file->private_data = name[2];
}
return 0;
}
int led_close (struct inode *inode, struct file *file)
{
printk("-----------[%s]-------------\n", __FUNCTION__);
return 0;
}
ssize_t led_write (struct file *file, const char __user *buf, size_t size, loff_t *flags)
{
printk("private_data is [%s]\n", (char *)file->private_data);
return 0;
}
const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_close,
.write = led_write
};
static int __init led_init(void)
{
int ret = 0, i = 0;
printk("-----------[%s]-------------\n", __FUNCTION__);
led = kzalloc(sizeof(*led), GFP_KERNEL);
if (!led)
{
return -ENOMEM;
}
led->led_major = register_chrdev(0, "led_chr", &led_fops);
if(led->led_major < 0)
{
printk("register_chrdev error!\n");
ret = -EINVAL;
goto err_kfree;
}
led->led_class = class_create(THIS_MODULE,"led_class");
if (!led->led_class)
{
ret = PTR_ERR(led->led_class);
goto err_unregister_chrdev;
}
for (i = 0; i < 3; ++i)
{
led->led_device[i] = device_create(led->led_class, NULL, MKDEV(led->led_major, i+1), NULL, "led%d_dev", i+1);
if (!led->led_device[i])
{
if (0 == i)
{
ret = PTR_ERR(led->led_device[i]);
goto err_class_destroy;
}
else
{
ret = PTR_ERR(led->led_device[i]);
goto err_device_destroy;
}
}
}
return ret;
err_device_destroy:
for (; i >= 0; i--)
{
device_destroy(led->led_class, MKDEV(led->led_major, i+1));
}
err_class_destroy:
class_destroy(led->led_class);
err_unregister_chrdev:
unregister_chrdev(led->led_major, "led_chr");
err_kfree:
kzfree(led);
led = NULL;
return ret;
}
// 部分测试代码 led_test.c
typedef struct devices_info {
char name[20];
int fd;
}dev_info_t;
int main(void)
{
int i = 0;
dev_info_t dev_info[3] = {0};
for (i = 0; i < 3; ++i)
{
sprintf(dev_info[i].name, "/dev/led%d_dev", i+1);
dev_info[i].fd = open(dev_info[i].name, O_RDWR);
if (dev_info[i].fd < 0)
{
perror("open");
return -1;
}
}
write(dev_info[0].fd, NULL, 0);
write(dev_info[1].fd, NULL, 0);
write(dev_info[2].fd, NULL, 0);
for (i = 0; i < 3;++i)
{
close(dev_info[i].fd);
}
return 0;
}
实验结果如下
- 在 /dev 目录下生成了创建的三个设备节点
- 在 /sys/class/led_class 下也有三个设备描述信息
- led_test 出现三次 open 说明公用这了一套 fops
- 可以在 open 时利用 inode->i_rdev 来区分到底是哪个设备节点调用