字符设备驱动——Linux驱动开发

参考正点原子阿尔法开发板Linux驱动开发指南

一、简介     

        字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节 流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC SPI LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

        在 Linux 中一切皆为文件,驱动加载成功以后会在“ /dev ”目录下生成一个相应的文件,应 用程序通过对这个名为“ /dev/xxx (xxx 是具体的驱动文件名字 ) 的文件进行相应的操作即可实 现对硬件的操作。比如现在有个叫做 /dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程 序使用 open 函数来打开文件 /dev/led ,使用完成以后使用 close 函数关闭 /dev/led 这个文件。 open close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led ,那么就使用 write 函数来操 作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取 led 灯的状态,就用 read 函数从驱动中读取相应的状态。
        应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。 当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开 /dev/led 这个驱动,因为用 户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空 间“陷入”到内核空间,这样才能实现对底层驱动的操作。 open close write read 等这些函 数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。

        其中关于 C 库以及如何通过系统调用“陷入”到内核空间这个我们不用去管,我们重点关 注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数, 比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一 个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所 示:
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
};
简单介绍一下 file_operation 结构体中比较重要的、常用的函数:
1 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE
2 行, llseek 函数用于修改文件当前的读写位置。
3 行, read 函数用于读取设备文件。
4 行, write 函数用于向设备文件写入 ( 发送 ) 数据。
8 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
9 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
10 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上, 32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是 unlocked_ioctl
11 行, mmap 函数用于将设备的内存映射到进程空间中 ( 也就是用户空间 ) ,一般帧缓 冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲 (LCD 显存 ) 映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
13 行, open 函数用于打开设备文件。
15 行, release 函数用于释放 ( 关闭 ) 设备文件,与应用程序中的 close 函数对应。
16 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
17 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的
数据。
        在字符设备驱动开发中最常用的就是上面这些函数,关于其他的函数大家可以查阅相关文档。我们在字符设备驱动开发中最主要的工作就是实现上面这些函数,不一定全部都要实现, 但是像 open release write read 等都是需要实现的,当然了,具体需要实现哪些函数还是要 看具体的驱动要求。

二、字符设备驱动开发步骤

        我们在学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器,在 Linux 动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在 Linux 驱动开发中 我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。

2.1 驱动模块的加载和卸载

        Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启 动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块 (Linux 下模块扩展名为 .ko) ,在 Linux 内核启动以后使用“ insmod ”命令加载驱动模块。在调试驱动的时候一般都选择将其编译 为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。 而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编 译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进 Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。
模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
        module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的 具体函数,当使用“ insmod ”命令加载驱动的时候, xxx_init 这个函数就会被调用。 module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用 用“ rmmod ”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。字符设备驱动模块加载和卸 载模板如下所示:
/* 驱动入口函数 */
static int __init xxx_init(void)
{
	/* 入口函数具体内容 */
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
 {
	/* 出口函数具体内容 */
 }

 /* 将上面两个函数指定为驱动的入口和出口函数 */
 module_init(xxx_init);
 module_exit(xxx_exit);
        驱动编译完成以后扩展名为.ko ,有两种命令可以加载驱动模块: insmod modprobe insmod 是最简单的模块加载命令,此命令用于加载指定的 .ko 模块,比如加载 drv.ko 这个驱动模块,命 令如下:
insmod drv.ko
insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。但是 modprobe 就不会存在这
modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。 modprobe 命令默认会去 /lib/modules/<kernel-version> 目录中查找模块,比如本文使用的 Linux kernel 的版本号为 4.1.15 因此 modprobe 命令默认会到 /lib/modules/4.1.15 这个目录中查找相应的驱动模块,一般自己制 作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
驱动模块的卸载使用命令“ rmmod ”即可,比如要卸载 drv.ko ,使用如下命令即可:
rmmod drv.ko
也可以使用“ modprobe -r ”命令卸载驱动,比如要卸载 drv.ko ,命令如下:
modprobe -r drv.ko
        使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没 有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是 推荐使用 rmmod 命令。

2.2 字符设备注册于注销

        对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模 块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示 :
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
        一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块 的出口函数 xxx_exit 中进行。 内容如下所示:
static struct file_operations test_fops;

/* 驱动入口函数 */
static int __init xxx_init(void)
{
	/* 入口函数具体内容 */
	int retvalue = 0;

	/* 注册字符设备驱动 */
	retvalue = register_chrdev(200, "chrtest", &test_fops);
	if(retvalue < 0){
	/* 字符设备注册失败,自行处理 */
	}
	return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
要注意的一点就是,需要选择没有被使用的主设备号, 输入命令“cat /proc/devices ”可以查看当前已经被使用掉的设备号

2.3 实现设备具体操作函数

        file_operations 结构体就是设备的具体操作函数,在示例代码 40.2.2.1 中我们定义了 file_operations 结构体类型的变量 test_fops ,但是还没对其进行初始化,也就是初始化其中的 open release read write 等具体的设备操作函数。本小节我们就完成变量 test_fops 的初始化,设置 好针对 chrtest 设备的操作函数。在初始化 test_fops 之前我们要分析一下需求,也就是要对 chrtest 这个设备进行哪些操作,只有确定了需求以后才知道我们应该实现哪些操作函数。假设对 chrtest 这个设备有如下两个要求:
1 、能够对 chrtest 进行打开和关闭操作
        设备打开和关闭是最基本的要求,几乎所有的设备都得提供打开和关闭的功能。因此我们 需要实现 file_operations 中的 open release 这两个函数。
2 、对 chrtest 进行读写操作
        假设 chrtest 这个设备控制着一段缓冲区 ( 内存 ) ,应用程序需要通过 read write 这两个函 数对 chrtest 的缓冲区进行读写操作。所以需要实现 file_operations 中的 read write 这两个函 数。
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,
                            size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp,
                             const char __user *buf,
                             size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}
/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}
static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if (retvalue < 0)
    {
        /* 字符设备注册失败,自行处理 */
    }
    return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

2.4 添加 LICENSE 和作者信息

在末尾添加:

MODULE_LICENSE("GPL");                 //使用 GPL 协议
MODULE_AUTHOR("lyh");

三、Linux设备号

3.1 设备号组成

        为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分 组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了 一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面,定义如下:
typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;
可以看出 dev_t __u32 类型的,其实就是 unsigned int 类型,这 32 位的数据构 成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095 ,所以大家在选择主设备号的时候一定不要超过这个范围。在文 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))

3.2 设备号的分配

3.3.1 静态分配设备号
        本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了,注 册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个 设备号,比如选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉 了,具体分配的内容可以查看文档 Documentation/devices.txt 。并不是说内核开发者已经分配掉 的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个 主设备号,使用“ cat /proc/devices ”命令即可查看当前系统中所有已经使用了的设备号。
3.3.2 动态分配设备号

        静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count:要申请的设备号数量。
name:设备名字。

void unregister_chrdev_region(dev_t from, unsigned count);
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。

四、开发实验

        字符设备驱动开发的基本步骤我们已经了解了,本节我们就以 chrdevbase 这个虚拟设备为 例,完整的编写一个字符设备驱动模块。 chrdevbase 不是实际存在的一个设备,是笔者为了方 便讲解字符设备的开发而引入的一个虚拟设备。 chrdevbase 设备有两个缓冲区,一个为读缓冲 区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向 chrdevbase 备的写缓冲区中写入数据,从读缓冲区中读取数据。 chrdevbase 这个虚拟设备的功能很简单, 但是它包含了字符设备的最基本功能。

4.1实验程序编写

1 、创建 VSCode 工程
        在 Ubuntu 中创建一个目录用来存放 Linux 驱动程序,比如我创建了一个名为 Linux_Drivers 的目录来存放所有的 Linux 驱动。在 Linux_Drivers 目录下新建一个名为 1_chrDev 的子目录来存放本实验所有文件,在 1_chrDev 目录中新建 VSCode 工程,并且新建 chrDev.c 文件, 完成以后 1_chrDev 目录中的文件:
2 、添加头文件路径
        因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux  源码中的头文件路径。打开 VSCode ,按下“ Crtl+Shift+P ”打开 VSCode 的控制台,然后输入 C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,打开以后会自动在 .vscode 目录下生成一个名为 c_cpp_properties.json 的文件, Linux 源码里面的头文件路径添加进来,也
就是我们前面移植的 Linux 源码中的头文件路径。添加头文件路径以后的 c_cpp_properties.json 的文件内容如下所示:(没有移植编译 Linux 可参考笔者以下博客)
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/lyh/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/lyh/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/lyh/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"
            ],
            "defines": [],
            "compilerPath": "/usr/local/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "gcc-x64"
        }
    ],
    "version": 4
}
        分别是开发板所使用的Linux 源码下的 include 、arch/arm/include arch/arm/include/generated 这三个目录的路径。
3 、编写实验程序
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 : lyh
版本 : V1.0
描述 : chrDev 驱动文件。
其他 : 无
日志 : 初版 V1.0 2024/4/9 lyh创建
***************************************************************/

#define CHRDEVBASE_MAJOR 200         /* 主设备号 */
#define CHRDEVBASE_NAME "chrDev" /* 设备名 */

static char readbuf[100];  /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};

/*
 * @description : 打开设备
 * @param – inode : 传递给驱动的 inode
 * @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
 * 一般在 open 的时候将 private_data 指向设备结构体。
 * @return : 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
    //printk("chrdevbase open!\r\n");
    return 0;
}

/*
 * @description : 从设备读取数据
 * @param - filp : 要打开的设备文件(文件描述符)
 * @param - buf : 返回给用户空间的数据缓冲区
 * @param - cnt : 要读取的数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf,
                               size_t cnt, loff_t *offt)
{
    int retvalue = 0;

    /* 向用户空间发送数据 */
    memcpy(readbuf, kerneldata, sizeof(kerneldata));
    retvalue = copy_to_user(buf, readbuf, cnt);
    if (retvalue == 0)
    {
        printk("kernel senddata ok!\r\n");
    }
    else
    {
        printk("kernel senddata failed!\r\n");
    }

    //printk("chrdevbase read!\r\n");
    return 0;
}

/*
 * @description : 向设备写数据
 * @param - filp : 设备文件,表示打开的文件描述符
 * @param - buf : 要写给设备写入的数据
 * @param - cnt : 要写入的数据长度
 * @param - offt : 相对于文件首地址的偏移
 * @return : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp,
                                const char __user *buf,
                                size_t cnt, loff_t *offt)
{
    int retvalue = 0;
    /* 接收用户空间传递给内核的数据并且打印出来 */
    retvalue = copy_from_user(writebuf, buf, cnt);
    if (retvalue == 0)
    {
        printk("kernel recevdata:%s\r\n", writebuf);
    }
    else
    {
        printk("kernel recevdata failed!\r\n");
    }

    //printk("chrdevbase write!\r\n");
    return 0;
}

/*
 * @description : 关闭/释放设备
 * @param - filp : 要关闭的设备文件(文件描述符)
 * @return : 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode,
                              struct file *filp)
{
    //printk("chrdevbase release!\r\n");
    return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
    .owner = THIS_MODULE,
    .open = chrdevbase_open,
    .read = chrdevbase_read,
    .write = chrdevbase_write,
    .release = chrdevbase_release,
};

/*
 * @description : 驱动入口函数
 * @param : 无
 * @return : 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
                               &chrdevbase_fops);
    if (retvalue < 0)
    {
        printk("chrdevbase driver register failed\r\n");
    }
    printk("chrdevbase_init()\r\n");
    return 0;
}

/*
 * @description : 驱动出口函数
 * @param : 无
 * @return : 无
 */
static void __exit chrdevbase_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    printk("chrdevbase_exit()\r\n");
}

/* 
 * 将上面两个函数指定为驱动的入口和出口函数
 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

/* 
 * LICENSE 和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lyh")
copy_to_user 将 readbuf 中的数据复制到参数 buf 中。因为内核空间不能直接操作用户空间的内存,因此需要借 copy_to_user 函数来完成内核空间的数据到用户空间的复制。 copy_to_user 函数原型如下:( copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n);
参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。

4.2 应用程序编写

在工程 目录中新建 chrDevApp.c 文件,在此文件中输入如下内容:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
 文件名 : chrdevbaseApp.c
 作者 : lyh
 版本 : V1.0
 描述 : chrdevbase 驱测试 APP。
 其他 : 使用方法:./chrDevApp /dev/chrDev <1>|<2>
 argv[2] 1:读文件
 argv[2] 2:写文件 
 日志 : 初版 V1.0 2024/4/9 lyh创建
***************************************************************/

static char usrdata[] = {"usr data!"};

/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
    int fd, retvalue;
    char *filename;
    char readbuf[100], writebuf[100];

    if (argc != 3)
    {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];

    /* 打开驱动文件 */
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf("Can't open file %s\r\n", filename);
        return -1;
    }

    if (atoi(argv[2]) == 1)
    { /* 从驱动文件读取数据 */
        retvalue = read(fd, readbuf, 50);
        if (retvalue < 0)
        {
            printf("read file %s failed!\r\n", filename);
        }
        else
        {
            /* 读取成功,打印出读取成功的数据 */
            printf("read data:%s\r\n", readbuf);
        }
    }

    if (atoi(argv[2]) == 2)
    {
        /* 向设备驱动写数据 */
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);
        if (retvalue < 0)
        {
            printf("write file %s failed!\r\n", filename);
        }
    }

    /* 关闭设备 */
    retvalue = close(fd);
    if (retvalue < 0)
    {
        printf("Can't close file %s\r\n", filename);
        return -1;
    }

    return 0;
}

4.3 编译程序

1、编译驱动
创建 Makefile 文件:
KERNELDIR := /home/lyh/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := chrDev.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
1 行, KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根据自己的实际情况填写即可。
2 行, CURRENT_PATH 表示当前路径,直接通过运行“ pwd ”命令来获取当前所处路径。
3 行, obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。
8 行,具体的编译命令,后面的 modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,“ make modules ”命令 中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为 .ko 文件。

        编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动模块。

2、编译应用

交叉编译即可

arm-linux-gnueabihf-gcc chrDevApp.c -o chrDevApp

五、测试

sudo cp chrDev.ko chrDevApp ../../lib/modules/4.1.15/ -f

depmod

modprobe chrDev.ko

驱动加载成功需要在 /dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建 /dev/chrDev 这个设备节 点文件:
mknod /dev/chrDev c 200 0
        其中“mknod ”是创建节点命令,“ /dev/chrDev ”是要创建的节点文件,“ c ”表示这是个字符设备,“ 200 ”是设备的主设备号,“ 0 ”是设备的次设备号。创建完成以后就会存在 /dev/chrDev 这个文件,可以使用“ ls /dev/chrDev -l ”命令查看。
./chrDevApp /dev/chrDev 1
./chrDevApp /dev/chrDev 2
如果不再使用某个设备的话可以将其驱动卸载掉.
  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值