
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 :linux专栏
⭐️流水不争先,争的是滔滔不绝
目录
[2.1、创建 VSCode 工程](#2.1、创建 VSCode 工程)
前言
上一期博客我们简单介绍了一下字符设备驱动开发的基本步骤,这次我们正式进行一个字符串设备驱动实验。
一、实验简单介绍
以 chrdevbase 这个虚拟设备为例,完整的编写一个字符设备驱动模块。
具体实现功能如下:此设备是虚拟的,其有两个缓冲区,一个为读缓冲区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。在应用程序中可以向设备的写缓冲区中写入数据,从读缓冲区中读取数据。这样便包含了字符设备的最基本功能。
二、程序编写
2.1、创建 VSCode 工程
在 Ubuntu 中创建一个目录用来存放 Linux 驱动程序,比如我创建了一个名为 Linux_Drivers 的目录来存放所有的 Linux 驱动。在 Linux_Drivers 目录下新建一个名为 1_chrdevbase 的子目录来存放本实验所有文件,如下图所示:

并新建 一个chrdevbase.c 文件,完成以后 1_chrdevbase目录中的文件如下图所示:

2.2 、添加头文件路径
编写 Linux 驱动时,一般会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux源码中的头文件路径。打开 VSCode,按键盘如下命令:
Crtl+Shift+P
打开 VSCode 的控制台,然后输入
C/C++: Edit configurations(JSON)
打开 C/C++编辑配置文件,如图所示:

打开以后会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件,然后把里边的代码改成如下:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include",
"/home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include",
"/home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "linux-clang-x64",
"configurationProvider": "ms-vscode.makefile-tools"
}
],
"version": 4
}
主要是修改includePath后边的内容,因为includePath 表示头文件路径,需要将 Linux 源码里面的头文件路径添加进来,也就是我们前面移植的 Linux 源码中的头文件路径。
2.3编写实验程序
工程建立好以后就可以开始编写驱动程序了,新建 chrdevbase.c,然后在里面输入如下内容:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
文件名 : chrdevbase.c
作者 : duan
版本 : V1.0
描述 : chrdevbase驱动文件。
***************************************************************/
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
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("duan");
chrdevbase_open 函数,当应用程序调用 open 函数的时候此函数就会调用,此函数只是输出一串字符,用于调试。
注意:这里使用了 printk 来输出信息,因为在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf运行在用户态,printk 运行在内核态。
在内核中想要向控制台输出或显示一些内容,必须使用 printk 这个函数。不同之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件include/linux/kern_levels.h 里面,定义如下:
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误 */
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用 KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低。如果要设置消息级别,参考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
上述代码就是设置"gsmi: Log Shutdown Reason\n"这行消息的级别为 KERN_EMERG。在具体的消息前面加上 KERN_EMERG 就可以将这条消息的级别设置为 KERN_EMERG。
接下来咱们继续往下看:
chrdevbase_read 函数,应用程序调用 read 函数从设备中读取数据的时候此函
数会执行。参数 buf 是用户空间的内存,读取到的数据存储在 buf 中,参数 cnt 是要读取的字节
数,参数 offt 是相对于文件首地址的偏移。
kerneldata 里面保存着用户空间要读取的数据。
memcpy函数先将 kerneldata 数组中的数据拷贝到读缓冲区 readbuf 中。
函数 copy_to_user 将 readbuf 中的数据复制到参数 buf 中。因为内核空间不能直接操作用户空间的内存,因此需要借 助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
chrdevbase_write 函数,应用程序调用 write 函数向设备写数据的时候此函数就会执行。
参数 buf 就是应用程序要写入设备的数据,也是用户空间的内存,参数 cnt 是要写入
的数据长度,参数 offt 是相对文件首地址的偏移。
再通过函数 copy_from_user 将 buf 中的 数据复制到写缓冲区 writebuf 中,因为用户空间内存不能直接访问内核空间的内存,所以需要借助函数 copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中。
chrdevbase_release 函数,应用程序调用 close 关闭设备文件的时候此函数会执行,一般会在此函数里面执行一些释放操作。如果在 open 函数中设置了 filp 的 private_data成员变量指向设备结构体,那么在 release 函数最终就要释放掉。
设备文件操作结构体 chrdevbase_fops,初始化 chrdevbase_fops。
驱动入口函数 chrdevbase_init,retvalue 调用函数 register_chrdev 来注册字符设备。
驱动出口函数 chrdevbase_exit,第 134 行调用函数 unregister_chrdev 来注销字符设备。
通过 module_init 和 module_exit 这两个函数来指定驱动的入口和出口函数。
最后两行代码添加 LICENSE 和作者信息。
总结
本期博客正式编写了一个字符串设备驱动实验,下一期咱们编写测试APP。
