ARM Linux 驱动开发篇----字符设备驱动开发(4)--- 编写chrdevbase 字符设备驱动开发实验--- Ubuntu20.04

🎬 渡水无言个人主页渡水无言

专栏传送门linux专栏

⭐️流水不争先,争的是滔滔不绝

目录

前言

一、实验简单介绍

二、程序编写

[2.1、创建 VSCode 工程](#2.1、创建 VSCode 工程)

2.2、添加头文件路径

2.3编写实验程序

总结


前言

上一期博客我们简单介绍了一下字符设备驱动开发的基本步骤,这次我们正式进行一个字符串设备驱动实验。


一、实验简单介绍

以 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。

相关推荐
小白同学_C7 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖7 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
不做无法实现的梦~9 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
陌上花开缓缓归以9 小时前
W25N01KVZEIR flash烧写
arm开发
默|笙11 小时前
【Linux】fd_重定向本质
linux·运维·服务器
陈苏同学11 小时前
[已解决] Solving environment: failed with repodata from current_repodata.json (python其实已经被AutoDL装好了!)
linux·python·conda
“αβ”11 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
不爱学习的老登12 小时前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
小王C语言13 小时前
进程状态和进程优先级
linux·运维·服务器
xlp666hub13 小时前
【字符设备驱动】:从基础到实战(下)
linux·面试