驱动程序——字符设备驱动框架

news2024/11/20 10:28:59

文章目录

  • 编写驱动程序的步骤
  • 代码
    • hello_drv.c
    • hello_drv_test.c
    • 驱动模块代码编译(编写Makefile,编译时间短)
    • 驱动模块代码编译(修改Makefile,编译时间长)
    • 测试
  • 应用调用open函数打开文件
    • 应用调用open函数打开设备文件
  • copy_from_user/copy_to_user 函数
  • class_destroy/device_create 浅析

学习驱动程序也可以看博文:树莓派——linux内核与驱动

编写驱动程序一般在Source Insight 里面,因为驱动程序需要依赖内核提供的函数

参考Misc.c(drivers\char)文件,该文件是经典的字符设备驱动程序

编写驱动程序的步骤

由上述函数的调用流程可知,编写驱动有如下步骤

① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核:register_chrdev(第一个参数为主设备号,若为0则表示又内核自动分配)
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点:class_create,device_create

代码

实现功能:

  • APP 调用 write 函数时,传入的数据保存在驱动中
  • APP 调用 read 函数时,把驱动中保存的数据返回给 APP

指令:

./hello_drv_test -w char_drv // 把字符串发给驱动程序
./hello_drv_test -r // 把驱动中保存的字符串读回来

hello_drv.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/* 1. 确定主设备号                                                                 */
static int major = 0;
static char kernel_buf[1024];//保存用户空间发送过来的字符串
static struct class *hello_class;


#define MIN(a, b) (a < b ? a : b)

/* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_to_user(buf, kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}

static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

static int hello_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int hello_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations hello_drv = {
	.owner	 = THIS_MODULE,
	.open    = hello_drv_open,
	.read    = hello_drv_read,
	.write   = hello_drv_write,
	.release = hello_drv_close,
};

/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init hello_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */
	//主设备号为0表示由内核进行分配


	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}
	
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	
	return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0));
	class_destroy(hello_class);
	unregister_chrdev(major, "hello");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(hello_init);//宏函数,将函数修饰为入口函数
module_exit(hello_exit);//宏函数,将函数修饰为出口函数

MODULE_LICENSE("GPL");//协议	 

hello_drv_test.c


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./hello_drv_test -w abc
 * ./hello_drv_test -r
 */
int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}

	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		write(fd, argv[2], len);
	}
	else
	{
		len = read(fd, buf, 1024);		
		buf[1023] = '\0';
		printf("APP read : %s\n", buf);
	}
	
	close(fd);
	
	return 0;
}

驱动模块代码编译(编写Makefile,编译时间短)

驱动程序中包含了很多头文件,这些头文件来自内核,不同的 ARM 板它的某些头文件可能不同。所以编译驱动程序时,需要指定板子所用的内核的源码路径

要编译哪个文件:设置 obj-m 变量即可

怎么把.c 文件编译为驱动程序.ko:这要借助内核的顶层 Makefile,。

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_stm32mp157_pro-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f hello_drv_test

obj-m	+= hello_drv.o

此时,目录下有编译好的内核模块hello_drv.ko和可执行程序hello_drv_test

在这里插入图片描述
移植到开发板上

驱动模块代码编译(修改Makefile,编译时间长)

驱动模块代码编译(模块的编译需要配置过的内核源码。放在指定的目录下,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)

操作步骤:

  1. 由于操作的是字符设备驱动,所以将该驱动代码hello_drv.c拷贝到ubuntu的 /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4/drivers/char 目录下的文件夹中(也可选择设备目录下其它文件)

  2. 修改该文件夹下Makefile文件(驱动代码放到哪个目录,就修改该目录下的Makefile),将字符驱动代码编译为模块,文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),因为最终是要将模块加载到开发板上而不是虚拟机中,所以要生成模块然后移植到开发板挂载,所以只需要将obj-m += hello_drv.o添加到Makefile中即可。

  3. 回到/home/book/100ask_stm32mp157_pro-sdk/Linux-5.4/编译驱动文件,因为Makefile文件之间有关联,在最外层进行编译,如果配置好环境变量,执行make编译生成驱动模块

  4. 编译完成后在/Linux-5.4/drivers/char目录下会生成以下几个文件:.o的文件是object文件,.ko是kernel object
    在这里插入图片描述

  5. 将生成的.ko文件发送给开发板

    cp hello_drv.ko /home/book/nfs_rootfs
    
  6. 将上层调用代码(hello_drv_test.c)进行交叉编译后发送给开发板,开发板目录下存在发送过来的hello_drv.ko文件和hello_drv_test.c这两个文件, 都已经完成编译

  7. 在开发板执行加载内核驱动insmod hello_drv.ko。此时dev下就有该驱动设备名称

加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中)
加载完成后就可以在dev下面看到名字为hello_drv的设备驱动(这个和驱动代码里面static char *module_name=“hello_drv”;//模块名这行代码有关),设备号也和代码里面相关。

  1. lsmod查看系统的驱动模块是否完成

  2. 在装完驱动后可以使用指令: rmmod +驱动名(不需要写.ko)将驱动卸载。

测试

打开内核的打印信息,有些板子默认打开了

echo "7 4 1 7" > /proc/sys/kernel/printk 

运行测试

./hello_drv_test -w char_drv // 把字符串发给驱动程序
./hello_drv_test -r // 把驱动中保存的字符串读回来

运行结果:
在这里插入图片描述
如果没有打开内核信息,可以通过dmesg指令查看该驱动在内核打印的信息, dmesg |grep hello_drv(报错信息不影响)
在这里插入图片描述加粗样式

应用调用open函数打开文件

应用层打开文件函数:

int open(const char *pathname, int flags, mode_t mode);

APP 打开文件时,可以得到一个整数,这个整数被称为文件句柄。对于 APP的每一个文件句柄,在内核里面都有一个“struct file”与之对应,所以传入的 flags、mode 等参数会被记录在内核中对应的 struct file 结构体里(f_flags、f_mode)。

在这里插入图片描述
去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的f_pos 成员里。

所以应用于内核之间的函数调用关系如下:
在这里插入图片描述

应用调用open函数打开设备文件

打开字符设备节点时,内核中也有对应的 struct file

应用调用open/read/write就会调用该结构体中对应的函数(编写在驱动程序中),调用关系如下:
在这里插入图片描述
该结构体中的结构体:struct file_operations *f_op,是由驱动程序提供的,编写驱动程序是要定义该结构体,该结构体的定义如下:

在这里插入图片描述

copy_from_user/copy_to_user 函数

驱动程序和应用程序之间传递数据,要使用copy_from_user/copy_to_user 函数

这两个函数分别是将用户空间的数据拷贝到内核空间以及将内核空间中的数据拷贝到用户空间

函数copy_from_user原型:

copy_from_user(void *to, const void __user *from, unsigned long n)

返回值:

  • 失败返回没有被拷贝成功的字节数,
  • 成功返回0

参数详解:

  • to 将数据拷贝到内核的地址,即内核空间的数据目标地址指针
  • from 需要拷贝数据的地址,即用户空间的数据源地址指针
  • n 拷贝数据的长度(字节)也就是将@from地址中的数据拷贝到@to地址中去,拷贝长度是n

class_destroy/device_create 浅析

驱动程序的核心是 file_operations 结构体:分配、设置、注册它。“class_destroy/device_create”函数知识起一些辅助作用:在/sys 目录下创建一些目录、文件,这样 Linux 系统中的 APP(比如 udev、mdev)就可以根据这些目录或文件来创建设备节点。

以下代码将会在“/sys/class”目录下创建一个子目录“hello_class”:

hello_class = class_create(THIS_MODULE, "hello_class");

以下代码将会在“/sys/class/hello_class”目录下创建一个文件“hello”:

device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");

更详细的信息请看图 :
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/755735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习使用FAsyncTask执行异步任务

目标 要想在另一个线程中执行代码&#xff0c;使用FRunnable是一种方式。而使用FAsyncTask是另一种方式&#xff08;这也是UE的DDC相关代码中所使用的方式&#xff09;。 本篇尝试运行一个 FAsyncTask 的最简单的例子。 1. FAsyncTask 对于任务类的要求 FAsyncTask是一个类…

SPEC CPU 2006 在 CentOS 5.0 x86_64 古老系统测试【4】-O3 不支持 编译失败

gcc -O3 编译失败 结论&#xff1a;默认情况下SPEC CPU 2006 1.2 不支持 gcc -O3编译参数。

谈 Delphi 中 JSON 的简便操作(非常简单)

我曾有讲过Delphi操作JSON的方法&#xff0c;特别是这一篇【delphi】类和记录的 helpers&#xff08;助手&#xff09;。但是因为当时是主要介绍的是Delphi的Helper&#xff0c;大家可能并没注意到Delphi中JSON的简便操作方法。 早期Delphi并没有自己的JSON操作库&#xff0c;大…

51. N 皇后 (递归+回溯)

题目链接&#xff1a;力扣 解题思路&#xff1a;递归回溯&#xff0c;n个皇后不能在同一行&#xff0c;同一列&#xff0c;同一斜线上&#xff0c;需要将n个皇后放在n*n的棋盘上&#xff0c;所以棋盘的每一行一定有一个皇后&#xff0c;因此可以第一行开始&#xff0c;在当前行…

计算机毕设 大数据电商用户行为分析及可视化

文章目录 1. 数据集说明2. 数据处理2.1 数据导入2.2 数据清洗 3.数据分析可视化3.1 用户流量及购物情况3.2 用户行为转换率3.3 用户行为习惯3.4 基于 RFM 模型找出有价值的用户3.5 商品维度的分析 4 最后 1. 数据集说明 这是一份来自淘宝的用户行为数据&#xff0c;时间区间为…

大数据分析平台能为企业带来哪些好处_光点科技

大数据分析平台是近年来迅速发展的一项关键技术&#xff0c;它利用先进的数据处理和分析技术&#xff0c;帮助企业从海量数据中提取有价值的信息和见解。对于企业来说&#xff0c;拥有一个强大的大数据分析平台可以带来许多好处。 首先&#xff0c;大数据分析平台可以帮助企业更…

【ArcGIS微课1000例】0070:制作宾馆酒店分布热度热力图

本文讲解在ArcGIS中,基于长沙市酒店宾馆分布矢量点数据(POI数据)绘制酒店分布热力图。 相关阅读: 【GeoDa实用技巧100例】004:绘制长沙市宾馆热度图 【ArcGIS Pro微课1000例】0028:绘制酒店分布热力图(POI数据) 文章目录 一、加载宾馆分布数据二、绘制热度图一、加载宾…

[QT编程系列-11]:C++图形用户界面编程,QT框架快速入门培训 - 5- QT主要控件与自定义控件

目录 5. QT主要控件 5.1 预定义控件 5.2 自定义控件 5.3 用预定义容器橙子和提升自定义控件 5.3 后记 5. QT主要控件 5.1 预定义控件 在Qt中&#xff0c;有许多预定义的控件&#xff08;Widgets&#xff09;可用于创建用户界面。这些控件提供了各种常见的用户界面元素&am…

五笔没落之五笔学习热潮

1998年&#xff0c;一个风雨交加的下午&#xff0c;有一个年轻人骑着一辆破旧的自行车来到电脑培训班&#xff0c;学习五笔打字。那时电脑还很贵&#xff0c;他一个月的工资只有几百元&#xff0c;根本买不起电脑&#xff0c;所以选择参加培训班。那时会打字就像现在的硕士生那…

Java----Nacos服务注册

除了通过Eureka&#xff0c;还可以通过我们的Nacos进行注册服务&#xff0c;方法也很简单。 前提&#xff01;需要开启Nacos服务&#xff0c;在官网中下载对应的压缩包&#xff0c;然后启动&#xff0c;如下windows需要输入命令&#xff1a; startup.cmd -m standalone 然后会…

Qt+C++多线程thread-QThread-QTimer视频-控件动画-混合应用实例

程序示例精选 QtC多线程thread-QThread-QTimer混合应用实例 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<QtC多线程thread-QThread-QTimer混合应用实例>>编写代码&#xff0c…

事务@transactional执行产生重复数据

背景 系统设计之初&#xff0c;每次来新请求&#xff0c;业务层会先查询数据库&#xff0c;判断是否存在相同的id数据&#xff08;id是唯一标识产品的&#xff09;&#xff0c;有则返回当前数据库查到的数据&#xff0c;根据数据决定下一步动作&#xff0c;没有则认为是初次请…

【ArcGIS Pro二次开发】(46):要素类从上到下、从左到右排序

要素类经过编辑之后&#xff0c;【OBJECTID】字段会变得不规律。应部分网友要求&#xff0c;做了这个从上到下、从左到右排序的工具。 不过后来在ArcGIS Pro中发现了一个【排序】工具&#xff0c;已经可以完美实现这个功能需求&#xff0c;发现自己做了个白工。 不过做了不能白…

火山引擎徐广治:边缘云,下一代云计算

6月30日&#xff0c;2023稀土开发者大会在北京举办。大会以「代码不止&#xff0c;掘金不停」为主题&#xff0c;与上百位海内外技术专家一起剖析行业最新动态&#xff0c;为一直在路上的技术开发者们&#xff0c;拓宽技术视野&#xff0c;传播前沿的技术理念。火山引擎边缘云资…

图腾柱电路

驱动MOS或者IGBT管&#xff0c;需要比较大的驱动电流或者灌电流 使用图腾柱电路或许是一个好的办法 电流路径是这样的 当CTL1端口输出为高电平的时候 三极管Q2的2脚为高&#xff0c;三极管Q2不导通 三极管Q1的2脚为高&#xff0c;三极管导通 所以Q1的3脚和1脚导通 VCC--…

C++笔记(总)

15重载 为什么使用重载 可以让函数名相同 提高复用性 函数重载需要满足的条件 1.函数的作用域相同 2.函数的返回值相同 3.函数的参数类型不同,或者参数的个数不相同或者参数的顺序不相同或者参数的类型不相同 4.不能用函数的返回值作为判断重载判断的条件。 举个例子 根…

月薪9.8K!打破年龄瓶颈~30岁前台测试转后台优化,他说:未来可期!

海压竹枝低复举&#xff0c;风吹山角晦还明&#xff01; 是啊&#xff0c;人这一辈子&#xff0c;该走的弯路&#xff0c;该吃的苦&#xff0c;该撞的南墙&#xff0c;一样都少不了&#xff0c;但只要我们能坚强挺住&#xff0c;熬过去&#xff0c;跨过去&#xff0c;好运自然来…

《Python机器学习:基于PyTorch和Scikit-Learn》——小解送书第五期

目录 书籍介绍 内容简介 作者简介 参与抽奖 书籍介绍 近年来&#xff0c;机器学习方法凭借其理解海量数据和自主决策的能力&#xff0c;已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务等行业得到了广泛的应用。自从AlexNet模型在2012年ImageNet大赛被提出以来…

【踩坑】gin框架middleware中间件如何中途跳出

gin框架middleware中间件如何中途跳出 背景 我在是使用gin web框架的时候需要自定义鉴权方式&#xff0c;那当然就要用到middleware这个方式了&#xff0c;代码如下所示&#xff0c;需要判断uid和token是否合法&#xff0c;不合法直接返回401状态&#xff0c;不继续往下执行 …

Android JNI引用类型管理 (十)

🔥 Android Studio 版本 🔥 🔥 JNI三种引用类型 🔥 全局引用、局部引用、弱引用 如果使用得当可以提升程序的性能, 否则会造成程序崩溃或者内存泄漏 🔥 创建包含JNI的类 JNIReference.java 🔥 package com.cmake.ndk1.jni;public class JNIReference {static {S…