linux驱动开发篇(二)—— 字符设备驱动框架

linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程
linux基础篇(二)——静态和动态链接
ARM裸机篇(一)——i.MX6ULL介绍
ARM裸机篇(二)——i.MX6ULL启动过程
ARM裸机篇(三)——i.MX6ULL第一个裸机程序
ARM裸机篇(四)——重定位和地址无关码
ARM裸机篇(五)——异常和中断
linux系统移植篇(一)—— linux系统组成
linux系统移植篇(二)—— Uboot使用介绍
linux系统移植篇(三)—— Linux 内核使用介绍
linux系统移植篇(四)—— 根文件系统使用介绍
linux驱动开发篇(一)—— Linux 内核模块介绍
linux驱动开发篇(二)—— 字符设备驱动框架
linux驱动开发篇(三)—— 总线设备驱动模型
linux驱动开发篇(四)—— platform平台设备驱动
linux驱动开发篇(五)—— linux设备驱动面向对象的编程思想
linux驱动开发篇(六)—— 设备树的引入


一、字符设备抽象

Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备的相关信息(设备号、内核对象),字符设备的打开、读写、关闭等操作接口(file_operations),在我们想要添加一个字符设备时,就是将这个对象注册到内核中,通过创建一个文件(设备节点)绑定对象的 cdev,当我们对这个文件进行读写操作时,就可以通过虚拟文件系统,在内核中找到这个对象及其操作接口,从而控制设备。

1、cdev 结构体

在linux内核中,使用cdev结构体描述一个字符设备,cdev结构体定义如下:

struct cdev {
	struct kobject kobj;/*内嵌的内核对象,通过它将设备统一加入到“Linux 设备驱动模型”中*/
	struct module *owner;/*字符设备驱动程序所在的内核模块对象的指针*/
	const struct file_operations *ops;/*文件操作结构体*/
	struct list_head list;
	dev_t dev;/*设备号*/
	unsigned int count;
};

Linux内核提供了一组函数用来操作cdev结构体, 声明在文件<include/linux/cdev.h>中,实现在文件fs/char_dev.c中:

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_del(struct cdev *);

void cd_forget(struct inode *);

(1)cdev_init函数用来初始化cdev结构体的成员,并建立cdev和file_operations之间的连接:

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;/*将传入的文件操作结构体指针赋值给cdev的ops*/
}

(2)cdev_alloc函数用来动态申请一个cdev内存:

struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

(3)cdev_add函数用来向系统中添加一个cdev,完成字符设备的注册:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;

	p->dev = dev;
	p->count = count;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}

(4)cdev_del函数用于从系统中删除一个cdev,完成字符设备的注销:

void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

2、file_operations 结构体

file_operations 结构体中成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行linux的open()、write()、read()、close()等系统调用时最终被内核调用,如图所示linux的调用关系:
在这里插入图片描述
驱动程序必须提供一些必要的函数,来与open、read、write、close这些函数相对应,file_operations结构体中定义了Linux内核驱动操作函数的集合,在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 (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};

3、设备号

在 linux 中,我们使用设备编号来表示设备,主设备号区分设备类别,次设备号标识具体的设备。一般来说,主设备号指向设备的驱动程序,次设备号指向某个具体的设备。
cdev 结构体的dev成员被内核用来记录设备号,dev_t其实就是一个u32类型,在include/linux/types.h文件中,定义如下:

typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t		dev_t;

主设备号占据高12位、范围是0-4095,次设备号占据低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))

使用MAJOR和MINOR宏可以得到主设备号和次设备号。

3.1、静态分配主设备号

有一些设备号已经被linux内核开发者给分配了,如果系统中已经使用了的设备号,可以使用下面的命令查看,那么我们就不能强行使用了,否则会造成冲突。

cat /proc/devices

在这里插入图片描述
使用静态分配,要避开系统已经使用的。

3.2、动态分配主设备号

静态分配设备号简单粗暴,但是很容易造成冲突。

Linux社区推荐使用动态分配设备号,在调用cdev_add()函数注册字符设备之前先申请一个设备号,系统会自动分配一个没有使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。
(1)申请设备号API

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
  • dev:用于保存申请到的设备号
  • baseminor:次设备号起始地址,一般为0
  • count:要申请的设备数量
  • name:设备名字

(2)释放设备号API

void unregister_chrdev_region(dev_t from, unsigned count);
  • from:要释放的设备号
  • count:要释放的设备号数量

4、设备节点

应用程序要实现对设备的操作,还需要一个在 /dev 目录下的设备节点,设备节点是 Linux 内核对设备的抽象,一个设备节点就是一个文件。Linux 中设备节点是通过“mknod”命令来创建的。应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。

4.1 手动创建设备节点

加载驱动到内核后,可以通过mknod命令,根据设备号手动创建一个设备节点:

mknod <设备目录> <设备类型> <主设备号> <次设备号>

比如:

mknod /dev/hello_drv c 200 0

这样就会在/dev目录下创建一个名为hello_drv的设备节点。

4.2 自动创建设备节点

Linux内核提供了自动创建设备节点的机制,具体使用如下,这些API在include/linux/device.h文件中声明。
(1)创建一个设备类

/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */
#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

该函数会在/sys/class目录下创建一个该类。
(2)创建一个设备并将其注册到文件系统:

struct device *device_create(struct class *cls, struct device *parent,
			     dev_t devt, void *drvdata,
			     const char *fmt, ...);

  • class:指向这个设备应该注册到的 struct 类的指针;
  • parent:指向此新设备的父结构设备(如果有)的指针;
  • devt:要添加的 char 设备的开发;
  • drvdata:要添加到设备进行回调的数据;
  • fmt:输入设备名称。

该函数会在/dev目录下创建该设备节点。
(3)删除设备

void device_destroy(struct class *cls, dev_t devt);

(4)删除类

extern void class_destroy(struct class *cls);

二 、字符设备驱动程序框架

实际上,在 Linux 上写驱动程序,都是做一些“填空题”。因为 Linux 给我们提供了一个基本的框架,我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。
字符设备驱动框架架构:
在这里插入图片描述

我们创建一个字符设备的时候,首先要的到一个设备号,分配设备号的途径有静态分配和动态分配;拿到设备的唯一 ID,我们需要实现 file_operation 并保存到 cdev 中,实现 cdev 的初始化;然后我们需要将我们所做的工作告诉内核,使用 cdev_add() 注册 cdev;最后我们还需要创建设备节点,以便我们后面调用 file_operation 接口。

注销设备时我们需释放内核中的 cdev,归还申请的设备号,删除创建的设备节点。

以open函数为例,当用户在C语言程序中调用open函数时,调用关系链如下图所示:
在这里插入图片描述
用户空间使用 open() 系统调用函数打开一个字符设备时 (int fd = open(“dev/xxx” , O_RDWR)) 大致有以下过程:

  • 在虚拟文件系统 VFS 中的查找对应与字符设备对应 struct inode 节点
  • 遍历散列表 cdev_map,根据 inod 节点中的 cdev_t 设备号找到 cdev 对象
  • 创建 struct file 对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件描述符作为数组下标标识了一个设备对象)
  • 初始化 struct file 对象,将 struct file 对象中的 file_operations 成员指向 struct cdev 对象中的file_operations 成员(file->fops = cdev->fops)
  • 回调 file->fops->open 函数

三 、字符设备驱动实例

结合前面所有的知识点,首先,字符设备驱动程序是以内核模块的形式存在的,因此,使用内核模块的程序框架是毫无疑问的。紧接着,我们要向系统注册一个新的字符设备,需要这几样东西:字符设备结构体 cdev,设备编号 devno,以及最最最重要的操作方式结构体 file_operations。

1、编写字符设备驱动程序。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_NAME            "EmbedCharDev"
#define DEV_CNT                 (1)
#define BUFF_SIZE               128
//定义字符设备的设备号
static dev_t devno;
//定义字符设备结构体chr_dev
static struct cdev chr_dev;
//数据缓冲区
static char vbuf[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
static struct file_operations  chr_dev_fops = 
{
    .owner = THIS_MODULE,
    .open = chr_dev_open,
    .release = chr_dev_release,
    .write = chr_dev_write,
    .read = chr_dev_read,
};

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\nopen\n");
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    if(p > BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    static int i = 0;
    i++;
    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

static int __init chrdev_init(void)
{
    int ret = 0;
    printk("chrdev init\n");
    //第一步
    //采用动态分配的方式,获取设备编号,次设备号为0,
    //设备名称为EmbedCharDev,可通过命令cat  /proc/devices查看
    //DEV_CNT为1,当前只申请一个设备编号
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
    if(ret < 0){
        printk("fail to alloc devno\n");
        goto alloc_err;
    }
    //第二步
    //关联字符设备结构体cdev与文件操作结构体file_operations
    cdev_init(&chr_dev, &chr_dev_fops);
    //第三步
    //添加设备至cdev_map散列表中
    ret = cdev_add(&chr_dev, devno, DEV_CNT);
    if(ret < 0)
    {
        printk("fail to add cdev\n");
        goto add_err;
    }
    return 0;

add_err:
    //添加设备失败时,需要注销设备号
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}
module_init(chrdev_init);

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);

    cdev_del(&chr_dev);
}
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");


2、简单测试程序:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

char *wbuf = "Hello World\n";
char rbuf[128];

int main(void)
{
    printf("EmbedCharDev test\n");
    //打开文件
    int fd = open("/dev/chrdev", O_RDWR);
    //写入数据
    write(fd, wbuf, strlen(wbuf));
    //写入完毕,关闭文件
    close(fd);
    //打开文件
     fd = open("/dev/chrdev", O_RDWR);
    //读取文件内容
    read(fd, rbuf, 128);
    //打印读取的内容
    printf("The content : %s", rbuf);
    //读取完毕,关闭文件
    close(fd);
    return 0;
}

makefile 文件:

KERNEL_DIR=../../ebf_linux_kernel

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILE

obj-m := chrdev.o
out =  chrdev_test

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
	$(CROSS_COMPILE)gcc -o $(out) main.c

.PHONY:clean
clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
	rm $(out)

3、编译make

在这里插入图片描述
编译成功后,实验目录下会生成两个名为” chrdev.ko”驱动模块文件和” chrdev_test”测试程序。

4、加载驱动测试

加载之后查看系统当前使用的设备号:
在这里插入图片描述
以看到我们注册的字符设备 EmbedCharDev 的主设备号为 244。

5、创建chrdev设备

mknod /dev/chrdev c 244 0

在这里插入图片描述

6、运行测试程序

在这里插入图片描述

7、卸载驱动模块

当我们不需要该内核模块的时候,我们可以执行以下命令:

sudo rmmod chrdev.ko
sudo rm /dev/chrdev

四、总结

编写驱动的套路大致流程可以总结如下:

  • 实现入口函数 xxx_init() 和卸载函数 xxx_exit()
  • 申请设备号 register_chrdev_region()
  • 初始化字符设备, cdev_init 函数、 cdev_add 函数
  • 构建 file_operation 结构体内容,实现硬件各个相关的操作
  • 在终端上使用 mknod 根据设备号来进行创建设备文件 (节点) (也可以在驱动使用
    class_create 创建设备类、在类的下面 device_create 创建设备节点)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

WALI-KANG

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

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

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

打赏作者

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

抵扣说明:

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

余额充值