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驱动开发篇(六)—— 设备树的引入

文章目录

  • 一、字符设备抽象
    • 1、cdev 结构体
    • 2、file_operations 结构体
    • 3、设备号
      • 3.1、静态分配主设备号
      • 3.2、动态分配主设备号
    • 4、设备节点
      • 4.1 手动创建设备节点
      • 4.2 自动创建设备节点
  • 二 、字符设备驱动程序框架
  • 三 、字符设备驱动实例
    • 1、编写字符设备驱动程序。
    • 2、简单测试程序:
    • 3、编译make
    • 4、加载驱动测试
    • 5、创建chrdev设备
    • 6、运行测试程序
    • 7、卸载驱动模块
  • 四、总结


一、字符设备抽象

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结构体, 声明在文件中,实现在文件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的调用关系:
linux驱动开发篇(二)—— 字符设备驱动框架_第1张图片
驱动程序必须提供一些必要的函数,来与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

linux驱动开发篇(二)—— 字符设备驱动框架_第2张图片
使用静态分配,要避开系统已经使用的。

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 给我们提供了一个基本的框架,我们只需要按照这个框架来写驱动,内核就能很好的接收并且按我们所要求的那样工作。
字符设备驱动框架架构:
linux驱动开发篇(二)—— 字符设备驱动框架_第3张图片

我们创建一个字符设备的时候,首先要的到一个设备号,分配设备号的途径有静态分配和动态分配;拿到设备的唯一 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 
#include 
#include 
#include 
#include 
#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 
#include 
#include 
#include 

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

linux驱动开发篇(二)—— 字符设备驱动框架_第4张图片
编译成功后,实验目录下会生成两个名为” chrdev.ko”驱动模块文件和” chrdev_test”测试程序。

4、加载驱动测试

加载之后查看系统当前使用的设备号:
linux驱动开发篇(二)—— 字符设备驱动框架_第5张图片
以看到我们注册的字符设备 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 创建设备节点)

你可能感兴趣的:(linux,linux,驱动开发,arm开发)