Linux 字符设备驱动

目录

 一、概述

 二、申请设备号

 三、创建类

 四、创建设备节点

1、手动创建

2、在驱动中创建

五、硬件初始化

六、实现 file_operations操作接口

1、应用层给 linux 内核拷贝数据

2、linux 内核给应用层拷贝数据

七、操作寄存器的方式

1、直接通过位运算

2、linux kernel 提供的方式

八、创建多个次设备号不同的设备节点

1、理论

2、实验


一、概述

字符设备驱动代码流程如下,以 led 驱动为例,此驱动基于 linux内核5.4.31 版本

  1. 申请设备号
  2. 创建class类
  3. 创建设备节点
  4. 硬件初始化
  5. 实现 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;
}

 函数功能:

        函数的功能:创建并注册一个或者多个不同次设备号的字符设备。

函数参数介绍:

  1. major: 主设备号,用于标识一类设备的驱动程序。
  2. baseminor: 表示设备号的起始次设备号
  3. count: 表示次设备号的数量
  4. name: 表示这些字符设备驱动的名称,可通过 cat /proc/devices 查看
  5. fops: 指向 struct file_operations结构的指针,该结构包含了设备驱动程序的操作函数,如读、写、打开、关闭等

 函数返回值:

  1. 如果 major = 0,动态分配一个主设备号,成功返回主设备号
  2. 如何 major > 0,表示分配指定的一个主设备号,成功返回0
  3. 失败返回错误码

从 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, 利用这个类可以创建设备节点。

 参数:

  1. owner:指向拥有这个设备类的模块的指针。这通常设置为 THIS_MODULE 宏,它表示当前正在编译的模块。
  2. name:设备类的名称,一个以null结尾的字符串。这个名称将用于在sysfs中创建相应的目录,并作为设备节点的属性之一。

 返回值:

  1. 如果成功,class_create 返回一个指向新创建的 struct class 的指针。
  2. 如果失败(例如由于内存不足或名称冲突),则返回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;
}

参数:

  1. class:上个章节创建的类结构体指针 :struct class
  2. parent:父节点,一般为:NULL
  3. dev_t:设备号:32位的整数,由主设备号和次设备号组成
  4. drvdata:私有数据,一般为:NULL
  5. fmt:使用变参的方式表示设备节点名称

 返回值:

  1. 成功返回 struct device *结构体指针
  2. 失败返回 ERR_PTR() 修饰的错误码

设备号详解:

  1. 主设备号:占高12位,表示一类设备
  2. 次设备号:占低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);

参数:

  1. offset:物理地址
  2. 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);

 参数:

  1. to:内核空间虚拟地址
  2. from:应用数据的空间地址
  3. n:数据长度

返回值:

  1. 成功返回0
  2. 失败返回参数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);

参数:

  1. to:应用数据的空间地址
  2. from:内核空间虚拟地址
  3. n:数据长度

返回值:

  1.  成功返回0
  2. 失败返回参数n

 使用 copy_from_user  和 copy_to_user  的原因:

  1. 利用拷贝,避免了用户空间和内核空间之间访问数据找不到的情况
  2. 使用内存地址合法性检查,提高内核的安全性

七、操作寄存器的方式

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;
}

实验结果如下

  1.  在 /dev 目录下生成了创建的三个设备节点
  2. 在 /sys/class/led_class 下也有三个设备描述信息
  3. led_test 出现三次 open 说明公用这了一套 fops
  4. 可以在 open 时利用 inode->i_rdev 来区分到底是哪个设备节点调用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暗里い着迷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值