3、字符设备驱动框架和开发步骤

news2024/9/27 7:26:19

一、Linux内核对文件的分类

Linux的文件种类

  • 1、-:普通文件
  • 2、d:目录文件
  • 3、p:管道文件
  • 4、s:本地socket文件
  • 5、l:链接文件
  • 6、c:字符设备
  • 7、b:块设备

Linux内核按驱动程序实现模型框架的不同,将设备分为三类

  • 1、字符设备:按字节流形式读取的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  • 2、块设备:按块进行读写的设备,最小的块大小为256字节(一个扇区),块的大小必须是扇区的整数倍。Linux块的大小一般为4096字节,随机访问,设有缓存以提高效率
  • 3、网络设备:针对网络数据收发的设备

框图

在这里插入图片描述

二、设备号

  • 用于区分内核中的同类设备

内核中用设备号来区分同类里不同的设备,设备号是一个无符号的32位整数,数据类型为dev_t,设备号分为两部分:

  • 1、主设备号:占高12位,用来表示驱动程序相同的一类设备
  • 2、次设备号:占低20位,用来表示被操作的具体设备

应用程序打开一个设备时,通过设备号来查找定位内核中管理的具体设备

MKDEV - 宏

  • 将主设备号和次设备号组合成32位的完整设备号,用法:
dev_t id;
int major = 251;//主设备号
int minor = 2;//次设备号
id = MKDEV(251, 2);

MAJOR - 宏

  • 将主设备号从设备号中分离出来,用法:
dev_t id = MKDEV(251, 2);
int major = MAJOR(id);

MINOR - 宏

  • 将次设备号从设备号中分离出来,用法:
dev_t id = MKDEV(251, 2);
int minor = MINOR(id);

如果已知一个设备的主次设备号,应用指定好设备文件名,可以用mknod命令在/dev目录下创建代表这个设备的文件,即此后应用程序对此文件的操作就是代表对此设备的操作,用法:

cd /dev
mknod 设备文件名 设备种类(c位字符设备,b为块设备)	主设备号  次设备号

在应用程序中如果要创建设备可以调用系统调用函数mknod,用法:

int mknod(const char *pathname,mode_t mode,dev_t dev);
pathname:带路径的设备文件名,无路径默认为当前目录,一般都创建在/dev下
mode:文件权限或S_IFCHR/S_IFBLK
dev:32位设备号

三申请和注册设备号

字符设备驱动的第一步是通过模块的入口函数__init向内核添加设备驱动的代码框架

  • 1、申请设备号
  • 2、定义、初始化、向内核添加代表本设备的结构体元素
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被使用,如果没有就申请该设备号
参数:
	from:自己指定的设备号
	count:申请的设备数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:
	成功为0,失败是负数,绝对值为错误码
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:自动分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:
	dev:分配设备号成功后用来存放分配到的设备号
	baseminor:起始的次设备号,一般为0
	count:申请设备号的数量
	name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:
	成功后为0,失败为负数,绝对值为错误码
  • 成功分配后在/proc/devices可以查看到申请到的主设备号和设备名

  • 3、释放设备号

void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
	from:已经成功分配到的设备号将被释放
	count:申请成功的设备数量
  • 释放后/proc/devices路径下对应的设备文件记录消失

四:注册字符设备

  • 1、字符设备结构体
    #include <linux/cdev.h>
struct cdev
{
	struct kobject kobj;//表示该类型实体是一种内核对象(kernel object)
	struct module *owner;//填THIS_MODULE,表示还字符设备属于哪个内核模块
	const struct file_operations *ops;//指向内核空间中存放该设备的各种操作函数地址
	struct list_head list;//链表指针域
	dev_t dev;//设备号
	unsigned int count;//设备数量
};

自己定义的结构体中必须要有一个成员为struct cdev cdev ,两种定义方法:

  • 1、直接定义:定义结构体全局变量

  • 2、动态申请:struct cdev * cdev_alloc()

  • 初始化

void cdev_init(struct cdev *cdev,const struct file_operations *fops)
功能:初始化字符设备结构体
参数:
	cdev:设备号
	fops:操作函数
  • 字符设备结构体与设备号绑定
int cdev_add(struct cdev *p,dev_t dev,unsigned int count)
功能:将指定字符设备添加到内核
参数:
	p:指向被添加的设备
	dev:设备号
	count:设备数量
  • 移除字符设备
void cdev_del(struct cdev *p)
功能:从内核中移除一个字符设备
参数:
	p:设备号
  • 系统调用函数结构体
    #include <linux/fs.h>
struct file_operations 
{
   struct module *owner;           //填THIS_MODULE,表示该结构体对象从属于哪个内核模块
   int (*open) (struct inode *, struct file *);	//打开设备
   int (*release) (struct inode *, struct file *);	//关闭设备
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);	//读设备
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备
   loff_t (*llseek) (struct file *, loff_t, int);		//定位
   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
   unsigned int (*poll) (struct file *, struct poll_table_struct *);	//POLL机制,实现多路复用的支持
   int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层
   int (*fasync) (int, struct file *, int); //信号驱动
   //......
};

该对象的函数指针成员对应相应的系统调用函数,应用层通过系统调用函数来间接调用这些函数指针指向的设备驱动函数
在这里插入图片描述
一般定义一个struct file_operations类型的全局变量,并用自己实现的各种操作函数名对其初始化

总结:
字符设备开发步骤:

  • 如果设备有自己的控制数据,则定义一个包含struct cdev cdev成员的结构体(方便管理),其他成员根据设备需求。设备简单直接用struct cdev
  • 2、定义一个struct cdev的全局变量来表示该设备
  • 3、定义三个全局变量分别来表示:主设备号、次设备号、设备数量
  • 4、定义一个struct file_operations 结构体变量,其owner成员赋值为THIS_MODULE
  • 5、module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员,设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核
  • 6、module exit函数:a. 注销设备号 b. 从内核中移除struct cdev. 如果如果是全局设备指针则释放其指向空间
  • 7、编写各个操作函数并将函数名初始化给struct file_operations结构体变量

验证步骤:

  • 1、编写驱动代码
  • 2、make生成ko文件
  • 3、insmod内核模块
  • 4、查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
  • 5、创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
  • 6、编写app验证驱动(testmychar_app.c)
  • 7、编译运行app,dmesg命令查看内核打印信息

示例:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

//设备结构体
struct cdev cdev;

//设备信息
dev_t id;
int major, minor;

//驱动操作函数
//打开设备
int char_open (struct inode * inode, struct file * f)	//打开设备
{

	
	return 0;
}

//读
ssize_t char_read (struct file * f, char __user * user, size_t size, loff_t * loff)	//读设备
{

	return size;
}

//写
ssize_t char_write (struct file * d , const char __user * user, size_t size, loff_t * loff)    //写设备
{
	
	return size;
}

//关闭
int char_close (struct inode * inod, struct file * f)	//关闭设备
{
	return 0;
}

struct file_operations fops = {

	.owner = THIS_MODULE,
	.open = char_open,
	.release = char_close,
	.write = char_write,
	.read = char_read,
};

int __init init_test(void)
{
	int ret;
	
	//申请设备号(自动分配)
	alloc_chrdev_region(&id, 0, 1, "char");
	if(ret)
	{
		printk("alloc err \r\n");
		return -EIO;
	}
	
	//初始化设备结构体
	cdev_init(&cdev, &fops);
	
	//设置cdev的owner成员为THIS_MODULE
	cdev.owner = THIS_MODULE;

	//添加字符设备到内核
	cdev_add(&cdev, id, 1);
		return 0;
}

void __exit exit_test(void)
{
	//移除设备
	cdev_del(&cdev);

	//注销设备号
	unregister_chrdev_region(id, 1);
}	

MODULE_LICENSE("GPL");
module_init(init_test);
module_exit(exit_test);

五、字符设备驱动框架解析

设备的操作函数如果比喻为桩的话,则:

  • 驱动实现操作函数:做桩
  • insmod调用init函数:钉桩
  • 应用层通过系统调用函数间接调用这些设备的操作函数:用桩
  • rmmod调用exit函数:拔桩

操作函数中常用的结构体

  • 1、内核中记录文件信息的结构体
struct inode{
	//....
	dev_t  i_rdev;//设备号
	struct cdev  *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象
	//....
}
/*
	1、该结构体对象对应着一个实际的设备,一对一
	2、open一个文件时,如果内核中存在该文件对应的inode对象就不再创建,不存在则创建
	3、内核中用此类型关联到此文件的操作函数集(对设备而言就是关联到具体的驱动代码)
*/
  • 2、读写文件内容过程中用到的一些控制数据组合的对象
struct file
{
	//...
	mode_t f_mode;//不同用户的操作权限,驱动一般不用
	loff_t f_pos;//数据位置指示器,需要控制数据开始读写位置的设备有用
	unsignedint f_flags;//open是的第二个参数flags存放在此,驱动中常用
	structfile_operations*f_op;//open时从struct inode中i_cdev的对应成员获得地址。驱动开发中用来协助理解工作原理,内核中使用
	void*private_data;//本次打开文件的私有数据驱动中常用来在几个操作函数中传递数据
	structdentry*f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
    int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象
	//...
};
/*
	1、open函数被调用一次,则创建一个该对象,可以认为一个该对象对应一次指定文件的操作
	2、open一个文件多次,每次open都会创建一个该对象
	3、文件描述符中存放的地址指向该类型的对象
	4、每个文件描述符都对应一个struct file对象的地址
*/

字符设备驱动程序框架

  • 驱动实现端:
    在这里插入图片描述

  • 驱动使用端:
    在这里插入图片描述

参考原理图

在这里插入图片描述

常用操作函数

int (*open) (struct inode *, struct file *);	//打开设备
/*
	指向函数一般用来对设备进行硬件上的初始化,对于一些简单的设备该函数只需要return 0,对应open系统调用,是open系统调用函数实现过程中调用的函数,
*/

int (*release) (struct inode *, struct file *);	//关闭设备
/*
	,指向函数一般用来对设备进行硬件上的关闭操作,对于一些简单的设备该函数只需要return 0,对应close系统调用,是close系统调用函数实现过程中调用的函数
*/

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);	//读设备
/*
	指向函数用来将设备产生的数据读到用户空间,对应read系统调用,是read系统调用函数实现过程中调用的函数
*/

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备
/*
	指向函数用来将用户空间的数据写进设备,对应write系统调用,是write系统调用函数实现过程中调用的函数
*/

loff_t (*llseek) (struct file *, loff_t, int);		//数据操作位置的定位
/*
	指向函数用来获取或设置设备数据的开始操作位置(位置指示器),对应lseek系统调用,是lseek系统调用函数实现过程中调用的函数
*/


long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
/*
	指向函数用来获取、设置设备一些属性或设备的工作方式等非数据读写操作,对应ioctl系统调用,是ioctl系统调用函数实现过程中调用的函数
*/

unsigned int (*poll) (struct file *, struct poll_table_struct *);//POLL机制,实现对设备的多路复用方式的访问
/*
	指向函数用来协助多路复用机制完成对本设备可读、可写数据的监控,对应select、poll、epoll_wait系统调用,是select、poll、epoll_wait系统调用函数实现过程中调用的函数
*/
  
int (*fasync) (int, struct file *, int); //信号驱动
/*
	指向函数用来创建信号驱动机制的引擎,对应fcntl系统调用的FASYNC标记设置,是fcntl系统调用函数FASYNC标记设置过程中调用的函数
*/

六、读操作实现

ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次read对应的那次open
    pbuf:指向用户空间一块内存,用来保存读到的数据
    count:用户期望读取的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置
 返回值:
    本次成功读取的字节数,失败返回-1
  • 由于内核不能直接和用户空间进行数据传输,因此要使用函数实现:
  • 从内核中复制数据到用户空间
    #include <asm-generic/uaccess.h>
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)
成功为返回0,失败非0

七、写操作实现

ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);  
完成功能:向设备写入数据
参数:
    filp:指向open产生的struct file类型的对象,表示本次write对应的那次open
    pbuf:指向用户空间一块内存,用来保存被写的数据
    count:用户期望写入的字节数
    ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置
 返回值:
    本次成功写入的字节数,失败返回-1
  • 从用户空间复制数据到内核中
    #include <asm-generic/uaccess.h>
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)

成功为返回0,失败非0

八、ioctl的实现

  • 可以已知结构体成员获得所在结构体变量的地址
container_of(成员地址,结构体类名,成员的名称)
  • ioctl

long xxx_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);
功能:对相应设备做指定的控制操作(各种属性的设置获取等等)
参数:
	filp:指向open产生的struct file类型的对象,表示本次ioctl对应的那次open
	cmd:用来表示做的是哪一个操作
    arg:和cmd配合用的参数
返回值:成功为0,失败-1
  • cmd的组成
    在这里插入图片描述
1. dir(direction),ioctl 命令访问模式(属性数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;
2. type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如 
   ‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;
3. nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;
4. size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;

九、创建多个次设备

每一个具体设备(次设备不一样),必须有一个struct cdev来代表它
并且需要以下三个操作:

  • cdev_init
  • cdev.owner赋值
  • cdev_add

十、(多个次设备同时获取设备号、同时创建设备)在驱动中创建设备

内核中 实现 创建 设备文件 /dev/xxx

1、创建设备

  • 手动方式 创建设备文件
	sudo mknod /dev/xxx  c      243      0 
			设备节点名  字符型  主设备号 次设备号 
	在Linux内核 3.0后 引入了 udev服务  该服务可以用于 检查
	驱动的 设备文件添加请求, 然后添加对应的设备文件 
  • 自动创建设备文件

#include <linux/device.h>

struct class * class_create(owner, name);
owner 所有者  THIS_MODULE 
name  类的名字 创建之后可以看到该名字 `cat /proc/devices | grep 类的名字`

返回值:  struct class *  出差 返回 错误指针

2、关联设备节点与设备文件名

#include <linux/device.h>

struct device *device_create(struct class *cls, struct device *parent,
			     dev_t devt, void *drvdata,
			     const char *fmt, ...);
例子:
	dev = device_create(cls, NULL, id, NULL, "char%d", 1);

3、卸载驱动时销毁设备节点

void device_destroy(struct class *cls, dev_t devt);

4、销毁类

void class_destroy(struct class *cls);

示例:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

//设备结构体
struct cdev cdev;

//设备信息
dev_t id;
int major, minor;

//class类型
struct class *cls;

struct device *dev;

//驱动操作函数
//打开设备
int char_open (struct inode * inode, struct file * f)	//打开设备
{

	
	return 0;
}

//读
ssize_t char_read (struct file * f, char __user * user, size_t size, loff_t * loff)	//读设备
{

	return size;
}

//写
ssize_t char_write (struct file * d , const char __user * user, size_t size, loff_t * loff)    //写设备
{
	
	return size;
}

//关闭
int char_close (struct inode * inod, struct file * f)	//关闭设备
{
	return 0;
}

struct file_operations fops = {

	.owner = THIS_MODULE,
	.open = char_open,
	.release = char_close,
	.write = char_write,
	.read = char_read,
};

int __init init_test(void)
{
	int ret;
	int i;
	//申请设备号(自动分配)四个设备
	alloc_chrdev_region(&id, 0, 4, "char_dev");
	if(ret)
	{
		printk("alloc err \r\n");
		return -EIO;
	}
	
	//初始化设备结构体
	cdev_init(&cdev, &fops);
	
	//设置cdev的owner成员为THIS_MODULE
	cdev.owner = THIS_MODULE;

	//添加字符设备到内核
	ret = cdev_add(&cdev, id, 4);
	if(ret)
	{
		printk("cdev add err \r\n");
		goto add_err;
	}
	//生成设备文件
	cls = class_create(THIS_MODULE, "char_class");
	if(IS_ERR(cls))
	{
		printk("class create err\r\n");
		goto class_err;		
		
	}
	//关联设备节点和设备名  循环创建4个

	for(i =0 ;i < 4; i++)
	{
		dev = device_create(cls, NULL, id + i, NULL, "char_dev%d", i);
		if(IS_ERR(dev))
		{
			printk("device create err\r\n");
			goto device_err;	
		}	
	}
	return 0;

//回滚
device_err:
	for(i--; i >= 0; i--)
	{
		device_destroy(cls, id + i);
	}
	//销毁class
	class_destroy(cls);

class_err:
	//移除设备
	cdev_del(&cdev);

add_err:
	//注销设备号
	unregister_chrdev_region(id, 4);

	return -EIO;
}

void __exit exit_test(void)
{
	int i;
	//取消设备节点
	for(i =0 ;i < 4; i++)
	{
		device_destroy(cls, id + i);
	}
	//销毁class
	class_destroy(cls);

	//移除设备
	cdev_del(&cdev);

	//注销设备号
	unregister_chrdev_region(id, 4);
}	

MODULE_LICENSE("GPL");
module_init(init_test);
module_exit(exit_test);

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

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

相关文章

深图SONTU医用X射线高压发生器维修SONTU-HFG50

X射线高压发生器的高压开不起来常见故障分析&#xff1a; 这是X射线荧光光谱仪较常见的故障&#xff0c;一般发生在开机时&#xff0c;偶尔也发生在仪器运行中。故障的产生原因可以从三个方面去分析&#xff1a;1、X射线防护系统;2、内部水冷系统;3、高压发生器及X射线光管。 …

离散傅里叶变换(DFT)的推导及C语言实现

1、傅里叶变换&#xff08;FT&#xff09; 傅里叶变换&#xff08;连续时间傅里叶变换&#xff09;是该部分内容的理论基础&#xff0c;回顾一下&#xff1a; 傅里叶变换&#xff1a; 傅里叶逆变换&#xff1a; 以上是连续时间傅里叶变换&#xff0c;但计算机只能处理离散的数…

practical on mifare

抽象的。 mifare Classic 是市场上使用最广泛的非接触式智能卡。 其设计和实施细节由制造商保密。 本文研究了该卡的体系结构以及卡与读卡器之间的通信协议。 然后&#xff0c;它提供了一种实用的、低成本的攻击&#xff0c;可以从卡的内存中恢复秘密信息。 由于伪随机生成器的…

计算32位二进制整数中1的个数(包括负数补码)

引言&#xff1a; 在计算机科学和编程中&#xff0c;位操作是一项重要的技能。一个常见的任务是计算一个32位二进制整数中1的个数&#xff0c;包括负数的补码表示。这个问题有多种解决方法&#xff0c;本博客将介绍一种高效的解决方案&#xff0c;同时提供详细的代码案例。 背…

“优化STM32单片机处理大量网络数据的方法“

"优化STM32单片机处理大量网络数据的方法" 在处理大量网络数据时&#xff0c;对STM32单片机的裸机程序&#xff0c;可采用以下处理方法&#xff1a;1.使用DMA实现直接内存访问&#xff0c;减轻CPU负担。2.优化缓冲区管理&#xff0c;使用循环缓冲区或多缓冲区。3.利…

智能垃圾桶在线监测方案

提高垃圾收集率、减少乱丢垃圾是我们共同努力的目标&#xff0c;在人口稠密的城市&#xff0c;垃圾是必要的服务&#xff0c;在许多城市&#xff0c;垃圾车是空的&#xff0c;垃圾随处可见沿线&#xff0c;但它不是有效的方法&#xff0c;因为有很多空间可以很多垃圾离开的路上…

Java Agent之ByteBuddy

1&#xff1a;前言 在上一篇文章介绍 Java Agent 技术时&#xff0c;结合 Byte Buddy 技术实现了统计方法执行时间的功能。本次分享深入介绍 Byte Buddy 的一些基础知识&#xff0c;SkyWalking Agent 强大的地方就是重度使用该工具实现探针数据动态生成代码填充参数的。 2&am…

基于springboot实现在线动漫信息交流分享平台项目【项目源码+论文说明】

基于springboot实现在线动漫信息交流分享平台演示 摘要 随着社会互联网技术的快速发展&#xff0c;每个行业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于在线动漫信息平台当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xf…

公司文件防泄密软件——「天锐绿盾」@德人合科技

天锐绿盾是一款企业级数据安全解决方案&#xff0c;主要用于保护企业的知识产权、客户资料、财务数据、技术图纸、应用系统等机密信息化数据不外泄。 PC访问地址&#xff1a; &#x1f517;isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 该软件解决方案…

Three.js如何计算3DObject的2D包围框?

推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 在Three.js应用开发中&#xff0c;有时你可能需要为3D场景中的网格绘制2D的包围框&#xff0c;应该怎么做&#xff1f; 朴素的想法是把网格的3D包围框投影到屏幕空间&#xff0c;例如&#xff0c;下图中的绿色框 3D包围框…

Linux 部署1Panel 现代化运维管理面板进行公网远程访问

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《速学数据结构》 《C语言进阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 前言1. Linux 安装1Panel2. 安装cpolar内网穿透2.1 使用一键脚本安装命令 2.2向系统添加服务2.3 启动cpolar服务…

【名城优企游学】国轩高科,用数字化带来强劲发展动力

成立于2006 年5月&#xff0c;系中国动力电池产业最早进入资本市场的民族企业&#xff1b;2015年5月上市&#xff0c;股票代码SZ.002074&#xff0c;拥有新能源汽车动力锂电池、储能、输配电设备等业务板块&#xff0c;建有独立成熟的研发、采购、生产、销售体系。 它就是新能…

五分钟了解一下什么是「贪心算法 」‼️‼️‼️

五分钟了解一下什么是「贪心算法 」‼️‼️‼️ 1 概念 贪心的意思在于在作出选择时&#xff0c;每次都要选择对自身最为有利的结果&#xff0c;保证自身利益的最大化。贪心算法就是利用这种贪心思想而得出一种算法。 贪心算法作为五大算法之一&#xff0c;在数据结构中的应…

如何进行lidar和imu的外参标定

我们使用的lidar_align这个算法来进行标定。 1.下载源码 在ros工作空间下的src文件夹下运行这个命令。 git clone https://github.com/ethz-asl/lidar_align.gitsudo apt-get install libnlopt-devcd ..catkin_make 这里编译的时候会爆一些错误。我遇到的是&#xff1a; 这…

渗透测试成功的8个关键

渗透测试 (penetration test)并没有一个标准的定义&#xff0c;国外一些安全组织达成共识的通用说法是&#xff1a;渗透测试是通过模拟恶意黑客的攻击方法&#xff0c;来评估计算机网络系统安全的一种评估方法。这个过程包括对系统的任何弱点、技术缺陷或漏洞的主动分析&#x…

考试中心|学习资料|学习情况|纯净无广|在线组卷刷题

土著刷题Plus专业版v1.2版本已全面对其个人版功能&#xff0c;完全满足学员培训/刷题考察全套流程&#xff0c;提供完整的服务流程。接下来将主要介绍一下这一版的新功能 考试中心 满足培训机构/刷题组织者考察刷题用户的管理需求&#xff0c;【围绕考试展开】&#xff0c;提供…

SQLite数据库使用时碰到的问题

背景 1、最近使用sqlite数据库&#xff0c;因为轻量&#xff0c;所以&#xff0c;不需要特别的部署服务器环境。 开发的平台使用的是Qt 碰到的问题 1、我在向数据库中插入字段的时候&#xff0c;少写了一个字段的内容&#xff0c;报这个错误 QSqlError("", "…

火焰原子吸收光谱法、容量法和电感耦合等离子体发射光谱法

声明 本文是学习GB-T 1871.5-2022 磷矿石和磷精矿中氧化镁含量的测定 火焰原子吸收光谱法、容量法和电感耦合等离子体发射光谱法. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件描述了在磷矿石和磷精矿中测定氧化镁含量的火焰原子吸收…

史上最全 结构型模式之 代理 适配器 装饰者 模式

创建型设计模式 原型模式 建造者模式 创建者模式对比_软工菜鸡的博客-CSDN博客 5&#xff0c;结构型模式 day03 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承机制来组织接口和类&#xff0c;后者釆用组…

大模型部署手记(8)LLaMa2+Windows+llama.cpp+英文文本补齐

1.简介&#xff1a; 组织机构&#xff1a;Meta&#xff08;Facebook&#xff09; 代码仓&#xff1a;https://github.com/facebookresearch/llama 模型&#xff1a;llama-2-7b 下载&#xff1a;使用download.sh下载 硬件环境&#xff1a;暗影精灵7Plus Windows版本&#…