深入学习Linux内核之v4l2驱动框架(一)

一,概述

V4L2(Video for Linux 2)是Linux操作系统中用于支持摄像头和视频设备的框架。它提供了一组API和驱动程序接口,用于在Linux系统中进行视频采集、视频流处理和视频播放等操作。

V4L2的设计目标是支持多种设备,包括摄像头、视频采集卡等,并允许应用程序与这些设备通信。通过V4L2,可以实现图片、视频和音频的采集,这些功能在远程会议、可视电话、视频监控系统和嵌入式多媒体终端等场景中都有广泛的应用。

V4L2框架具有设备抽象层,这使得应用程序可以与各种不同类型的视频设备进行通信,而无需关心具体的硬件细节。此外,V4L2还提供了一系列回调函数,用于设置设备的参数,如摄像头的频率、帧频、视频压缩格式和图像参数等。

在编程方面,V4L2是针对uvc免驱USB设备的编程框架,主要用于采集USB摄像头等。在Linux编程中,一般使用ioctl函数来对设备的I/O通道进行管理,通过不同的命令标志符来执行各种操作,如分配内存、查询驱动功能、获取视频格式、设置频捕获格式等。

总的来说,V4L2是Linux系统中用于支持摄像头和视频设备的重要框架,为开发者提供了丰富的功能和灵活的编程接口。

V4L2支持三类设备:视频输入输出设备、VBI设备和radio设备(其实还支持更多类型的设备,暂不讨论),分别会在/dev目录下产生videoX、radioX和vbiX设备节点。我们常见的视频输入设备主要是摄像头,也是本文主要分析对象。下图V4L2在Linux系统中的结构图:
在这里插入图片描述二,V4L2框架

在这里插入图片描述

Linux系统中视频输入设备主要包括以下四个部分:

字符设备驱动程序核心:V4L2本身就是一个字符设备,具有字符设备所有的特性,暴露接口给用户空间;

V4L2驱动核心:主要是构建一个内核中标准视频设备驱动的框架,为视频操作提供统一的接口函数;

平台V4L2设备驱动:在V4L2框架下,根据平台自身的特性实现与平台相关的V4L2驱动部分,包括注册video_device和v4l2_dev。

具体的sensor驱动:主要上电、提供工作时钟、视频图像裁剪、流IO开启等,实现各种设备控制方法供上层调用并注册v4l2_subdev。

V4L2的核心源码位于drivers/media/v4l2-core,源码以实现的功能可以划分为四类:

核心模块实现:由v4l2-dev.c实现,主要作用申请字符主设备号、注册class和提供video device注册注销等相关函数;

V4L2框架:由v4l2-device.c、v4l2-subdev.c、v4l2-fh.c、v4l2-ctrls.c等文件实现,构建V4L2框架;

Videobuf管理:由videobuf2-core.c、videobuf2-dma-contig.c、videobuf2-dma-sg.c、videobuf2-memops.c、videobuf2-vmalloc.c、v4l2-mem2mem.c等文件实现,完成videobuffer的分配、管理和注销。

Ioctl框架:由v4l2-ioctl.c文件实现,构建V4L2ioctl的框架。

video_device、v4l2_device和v4l2_subdev的关系:
在这里插入图片描述三,V4L2的数据结构

video_device
video_device结构体用于在/dev目录下生成设备节点文件,把操作设备的接口暴露给用户空间。

struct video_device
{/* character device */struct cdev *cdev;/* v4l2_device parent */struct v4l2_device *v4l2_dev;/* device ops */const struct v4l2_file_operations *fops;/* ioctl callbacks */const struct v4l2_ioctl_ops *ioctl_ops;
};

可以看到video_device中含有一个cdev还有v4l2_device,此外还有fops和ioctl_ops,从应用层进行系统调用会经过v4l2的核心层回调到这里

Video_device分配和释放,用于分配和释放video_device结构体:

struct video_device *video_device_alloc(void)void video_device_release(struct video_device *vdev)

video_device注册和注销,实现video_device结构体的相关成员后,就可以调用下面的接口进行注册:

static inline int __must_checkvideo_register_device(struct video_device *vdev,inttype, int nr)void video_unregister_device(struct video_device*vdev);

vdev:需要注册和注销的video_device;

type:设备类型,包括VFL_TYPE_GRABBER、VFL_TYPE_VBI、VFL_TYPE_RADIO和VFL_TYPE_SUBDEV。
ps: GRABBER在英文中的意思是掠夺者,video grabber 即 视频捕获器

nr:设备节点名编号,如/dev/video[nr]。

其中v4l2_file_operations和v4l2_ioctl_ops如下

struct v4l2_file_operations {struct module *owner;ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*ioctl) (struct file *, unsigned int, unsigned long);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);unsigned long (*get_unmapped_area) (struct file *, unsigned long,unsigned long, unsigned long, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct file *);int (*release) (struct file *);
};

熟悉v4l2应用编程的应该都知道v4l2有很多ioctl操作,具体实现都在这里

struct v4l2_ioctl_ops {int (*vidioc_querycap)(struct file *file, void *fh, struct v4l2_capability *cap);/* Buffer handlers */int (*vidioc_reqbufs) (struct file *file, void *fh, struct v4l2_requestbuffers *b);int (*vidioc_querybuf)(struct file *file, void *fh, struct v4l2_buffer *b);int (*vidioc_qbuf)    (struct file *file, void *fh, struct v4l2_buffer *b);int (*vidioc_dqbuf)   (struct file *file, void *fh, struct v4l2_buffer *b);/* Stream on/off */int (*vidioc_streamon) (struct file *file, void *fh, enum v4l2_buf_type i);int (*vidioc_streamoff)(struct file *file, void *fh, enum v4l2_buf_type i);...
};

v4l2_device

struct v4l2_device {/* used to keep track of the registered subdevs */struct list_head subdevs;...
};

可以看到v4l2_device中有一个v4l2_subdev的链表,v4l2_device的主要目的时用来管理v4l2_subdev

可以看出v4l2_device的主要作用是管理注册在其下的子设备,方便系统查找引用到。

V4l2_device的注册和注销:

int v4l2_device_register(struct device*dev, struct v4l2_device *v4l2_dev)static void v4l2_device_release(struct kref *ref)

v4l2_subdev

struct v4l2_subdev {struct list_head list;struct v4l2_device *v4l2_dev;const struct v4l2_subdev_ops *ops;
};

v4l2_subdev中有一个v4l2_subdev_ops,实现了一系列的操作,供v4l2_device调用

struct v4l2_subdev_ops {const struct v4l2_subdev_core_ops	*core;const struct v4l2_subdev_tuner_ops	*tuner;const struct v4l2_subdev_audio_ops	*audio;const struct v4l2_subdev_video_ops	*video;const struct v4l2_subdev_vbi_ops	*vbi;const struct v4l2_subdev_ir_ops		*ir;const struct v4l2_subdev_sensor_ops	*sensor;
};
struct v4l2_subdev_core_ops {...int (*s_config)(struct v4l2_subdev *sd, int irq, void *platform_data);int (*init)(struct v4l2_subdev *sd, u32 val);int (*s_gpio)(struct v4l2_subdev *sd, u32 val);int (*g_ctrl)(struct v4l2_subdev *sd, struct v4l2_control *ctrl);int (*s_ctrl)(struct v4l2_subdev *sd, struct v4l2_control *ctrl);int (*s_std)(struct v4l2_subdev *sd, v4l2_std_id norm);long (*ioctl)(struct v4l2_subdev *sd, unsigned int cmd, void *arg);...
};
struct v4l2_subdev_video_ops {...int (*enum_fmt)(struct v4l2_subdev *sd, struct v4l2_fmtdesc *fmtdesc);int (*g_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt);int (*try_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt);int (*s_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt);int (*cropcap)(struct v4l2_subdev *sd, struct v4l2_cropcap *cc);int (*g_crop)(struct v4l2_subdev *sd, struct v4l2_crop *crop);int (*s_crop)(struct v4l2_subdev *sd, struct v4l2_crop *crop);int (*g_parm)(struct v4l2_subdev *sd, struct v4l2_streamparm *param);int (*s_parm)(struct v4l2_subdev *sd, struct v4l2_streamparm *param);...
};

Subdev的注册和注销

当我们把v4l2_subdev需要实现的成员都已经实现,就可以调用以下函数把子设备注册到V4L2核心层:

int v4l2_device_register_subdev(struct v4l2_device*v4l2_dev, struct v4l2_subdev *sd)

当卸载子设备时,可以调用以下函数进行注销:

void v4l2_device_unregister_subdev(struct v4l2_subdev*sd)

四,ioctl框架

你可能观察到用户空间对V4L2设备的操作基本都是ioctl来实现的,V4L2设备都有大量可操作的功能(配置寄存器),所以V4L2的ioctl也是十分庞大的。它是一个怎样的框架,是怎么实现的呢?

Ioctl框架是由v4l2_ioctl.c文件实现,文件中定义结构体数组v4l2_ioctls,可以看做是ioctl指令和回调函数的关系表。用户空间调用系统调用ioctl,传递下来ioctl指令,然后通过查找此关系表找到对应回调函数。

以下是截取数组的两项:

IOCTL_INFO_FNC(VIDIOC_QUERYBUF, v4l_querybuf,v4l_print_buffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_buffer, length)),

IOCTL_INFO_STD(VIDIOC_G_FBUF, vidioc_g_fbuf,v4l_print_framebuffer, 0),

内核提供两个宏(IOCTL_INFO_FNC和IOCTL_INFO_STD)来初始化结构体,参数依次是ioctl指令、回调函数或者v4l2_ioctl_ops结构体成员、debug函数、flag。如果回调函数是v4l2_ioctl_ops结构体成员,则使用IOCTL_INFO_STD;如果回调函数是v4l2_ioctl.c自己实现的,则使用IOCTL_INFO_FNC。

IOCTL调用的流程图如下:
在这里插入图片描述
用户空间通过打开/dev/目录下的设备节点,获取到文件的file结构体,通过系统调用ioctl把cmd和arg传入到内核。通过一系列的调用后最终会调用到__video_do_ioctl函数,然后通过cmd检索v4l2_ioctls[],判断是INFO_FL_STD还是INFO_FL_FUNC。如果是INFO_FL_STD会直接调用到视频设备驱动中video_device->v4l2_ioctl_ops函数集。如果是INFO_FL_FUNC会先调用到v4l2自己实现的标准回调函数,然后根据arg再调用到video_device->v4l2_ioctl_ops或v4l2_fh->v4l2_ctrl_handler函数集。

五,IO访问

V4L2支持三种不同IO访问方式(内核中还支持了其它的访问方式,暂不讨论):

read和write,是基本帧IO访问方式,通过read读取每一帧数据,数据需要在内核和用户之间拷贝,这种方式访问速度可能会非常慢;

内存映射缓冲区(V4L2_MEMORY_MMAP),是在内核空间开辟缓冲区,应用通过mmap()系统调用映射到用户地址空间。这些缓冲区可以是大而连续DMA缓冲区、通过vmalloc()创建的虚拟缓冲区,或者直接在设备的IO内存中开辟的缓冲区(如果硬件支持);

用户空间缓冲区(V4L2_MEMORY_USERPTR),是用户空间的应用中开辟缓冲区,用户与内核空间之间交换缓冲区指针。很明显,在这种情况下是不需要mmap()调用的,但驱动为有效的支持用户空间缓冲区,其工作将也会更困难。

Read和write方式属于帧IO访问方式,每一帧都要通过IO操作,需要用户和内核之间数据拷贝,而后两种是流IO访问方式,不需要内存拷贝,访问速度比较快。内存映射缓冲区访问方式是比较常用的方式。

内存映射缓存区方式

硬件层的数据流传输

Camerasensor捕捉到图像数据通过并口或MIPI传输到CAMIF(camera interface),CAMIF可以对图像数据进行调整(翻转、裁剪和格式转换等)。然后DMA控制器设置DMA通道请求AHB将图像数据传到分配好的DMA缓冲区。

待图像数据传输到DMA缓冲区之后,mmap操作把缓冲区映射到用户空间,应用就可以直接访问缓冲区的数据。

vb2_queue

为了使设备支持流IO这种方式,驱动需要实现struct vb2_queue,来看下这个结构体:

 struct vb2_queue {enumv4l2_buf_type                  type;  //buffer类型unsignedint                        io_modes;  //访问IO的方式:mmap、userptr etcconststruct vb2_ops                 *ops;   //buffer队列操作函数集合conststruct vb2_mem_ops     *mem_ops;  //buffer memory操作集合structvb2_buffer              *bufs[VIDEO_MAX_FRAME];  //代表每个bufferunsignedint                        num_buffers;    //分配的buffer个数……};

Vb2_queue代表一个videobuffer队列,vb2_buffer是这个队列中的成员,vb2_mem_ops是缓冲内存的操作函数集,vb2_ops用来管理队列。

vb2_mem_ops

vb2_mem_ops包含了内存映射缓冲区、用户空间缓冲区的内存操作方法:

 struct vb2_mem_ops {void           *(*alloc)(void *alloc_ctx, unsignedlong size);  //分配视频缓存void           (*put)(void *buf_priv);            //释放视频缓存//获取用户空间视频缓冲区指针void           *(*get_userptr)(void *alloc_ctx,unsigned long vaddr, unsignedlong size, int write);void           (*put_userptr)(void *buf_priv);       //释放用户空间视频缓冲区指针//用于缓存同步void           (*prepare)(void *buf_priv);void           (*finish)(void *buf_priv);void           *(*vaddr)(void *buf_priv);void           *(*cookie)(void *buf_priv);unsignedint     (*num_users)(void *buf_priv);         //返回当期在用户空间的buffer数int              (*mmap)(void *buf_priv, structvm_area_struct *vma);  //把缓冲区映射到用户空间};

这是一个相当庞大的结构体,这么多的结构体需要实现还不得累死,幸运的是内核都已经帮我们实现了。提供了三种类型的视频缓存区操作方法:连续的DMA缓冲区、集散的DMA缓冲区以及vmalloc创建的缓冲区,分别由videobuf2-dma-contig.c、videobuf2-dma-sg.c和videobuf-vmalloc.c文件实现,可以根据实际情况来使用。

vb2_buffer是缓存队列的基本单位,内嵌在其中v4l2_buffer是核心成员。当开始流IO时,帧以v4l2_buffer的格式在应用和驱动之间传输。一个缓冲区可以有三种状态:

在驱动的传入队列中,驱动程序将会对此队列中的缓冲区进行处理,用户空间通过IOCTL:VIDIOC_QBUF把缓冲区放入到队列。对于一个视频捕获设备,传入队列中的缓冲区是空的,驱动会往其中填充数据;

在驱动的传出队列中,这些缓冲区已由驱动处理过,对于一个视频捕获设备,缓存区已经填充了视频数据,正等用户空间来认领;

用户空间状态的队列,已经通过IOCTL:VIDIOC_DQBUF传出到用户空间的缓冲区,此时缓冲区由用户空间拥有,驱动无法访问。

这三种状态的切换如下图所示:
在这里插入图片描述v4l2_buffer结构如下:

 struct v4l2_buffer {__u32                          index;  //buffer 序号__u32                          type;   //buffer类型__u32                          bytesused;  缓冲区已使用byte数__u32                          flags;__u32                          field;structtimeval           timestamp;  //时间戳,代表帧捕获的时间structv4l2_timecode       timecode;__u32                          sequence;/*memory location */__u32                          memory;  //表示缓冲区是内存映射缓冲区还是用户空间缓冲区union {__u32           offset;  //内核缓冲区的位置unsignedlong   userptr;   //缓冲区的用户空间地址structv4l2_plane *planes;__s32                 fd;} m;__u32                          length;   //缓冲区大小,单位byte};

当用户空间拿到v4l2_buffer,可以获取到缓冲区的相关信息。Byteused是图像数据所占的字节数,如果是V4L2_MEMORY_MMAP方式,m.offset是内核空间图像数据存放的开始地址,会传递给mmap函数作为一个偏移,通过mmap映射返回一个缓冲区指针p,p+byteused是图像数据在进程的虚拟地址空间所占区域;如果是用户指针缓冲区的方式,可以获取的图像数据开始地址的指针m.userptr,userptr是一个用户空间的指针,userptr+byteused便是所占的虚拟地址空间,应用可以直接访问。

六,源码剖析

V4L2驱动模板

此示例中并没有设计到v4l2_subdev,这部分将会在后面分析具体的驱动中出现

#include <...>static struct video_device* video_dev;
static struct v4l2_device v4l2_dev;/* 实现各种系统调用 */
static const struct v4l2_file_operations video_dev_fops = {.owner		    = THIS_MODULE,.release        = vdev_close,.read           = vdev_read,.poll		    = vdev_poll,.ioctl          = video_ioctl2,.mmap           = vdev_mmap,
};/* 实现各种系统调用 */
static const struct v4l2_ioctl_ops video_dev_ioctl_ops = {.vidioc_querycap      = vidioc_querycap,.vidioc_enum_fmt_vid_cap  = vidioc_enum_fmt_vid_cap,.vidioc_g_fmt_vid_cap     = vidioc_g_fmt_vid_cap,.vidioc_try_fmt_vid_cap   = vidioc_try_fmt_vid_cap,.vidioc_s_fmt_vid_cap     = vidioc_s_fmt_vid_cap,.vidioc_reqbufs       = vidioc_reqbufs,.vidioc_querybuf      = vidioc_querybuf,.vidioc_qbuf          = vidioc_qbuf,.vidioc_dqbuf         = vidioc_dqbuf,.vidioc_enum_input    = vidioc_enum_input,.vidioc_g_input       = vidioc_g_input,.vidioc_s_input       = vidioc_s_input,.vidioc_streamon      = vidioc_streamon,.vidioc_streamoff     = vidioc_streamoff,
};static int __init video_init(void)
{/* 分配并设置一个video_device */video_dev = video_device_alloc();video_dev->fops = &video_dev_fops;video_dev->ioctl_ops = &video_dev_ioctl_ops;video_dev->release = video_device_release;/* 注册一个v4l2_device */v4l2_device_register(video_dev->dev, &v4l2_dev);    video_dev->v4l2_dev = &video_dev;/* 注册一个video_device字符设备 */video_register_device(video_dev, VFL_TYPE_GRABBER, -1);return 0;
}static void __exit video_exit(void)
{video_unregister_device(video_dev);v4l2_device_unregister(&v4l2_dev);video_device_release(video_dev);
}module_init(video_init);
module_exit(video_exit);

如果你熟悉v4l2应用编程的话,你应该知道v4l2有许多ioctl操作,上面的模板就实现了很多ioctl操作

下面我们来分析分析源码

在上面的video_init中,我们分配并设置了video_dev,注册了v4l2_device(v4l2_device_register),然后向v4l2核心层注册video_device(video_register_device)

我们先来看v4l2_device_register

int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
{INIT_LIST_HEAD(&v4l2_dev->subdevs);spin_lock_init(&v4l2_dev->lock);dev_set_drvdata(dev, v4l2_dev);...
}

从源码中可以看出v4l2_device_register并没有做什么事,只是初始化链表,自旋锁,还有设置数据,这函数并不是我们的重点

下面来仔细分析video_register_device

int video_register_device(struct video_device *vdev, int type, int nr)
{   /* 分配字符设备 */vdev->cdev = cdev_alloc();/* 设置fops */vdev->cdev->ops = &v4l2_fops;/* 注册字符设备 */cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);/* 生成设备节点 */dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);device_register(&vdev->dev);/* 设置全局数组 */video_device[vdev->minor] = vdev;
}

可以看到这个函数会为video_device分配一个cdev,然后设置fops,向内核注册字符设备,再者生成设备节点

然后设置video_device全局数组,video_device一个全局数组

static struct video_device *video_device[VIDEO_NUM_DEVICES];

保存着注册的video_device

另外,在sys/class/video4linux/下可以找到我们的设备节点。

我们注册的设备类型为VFL_TYPE_GRABBER,对应的节点名称video
第一个注册的设备从0开始,以此类推,因此节点名称为video0

用户可以打开dev/video0节点,通过IOCTL命令和内核空间进行通信。

接下来看一下其中设置的fops(v4l2_fops)

static const struct file_operations v4l2_fops = {.owner = THIS_MODULE,.read = v4l2_read,.write = v4l2_write,.open = v4l2_open,.get_unmapped_area = v4l2_get_unmapped_area,.mmap = v4l2_mmap,.ioctl = v4l2_ioctl,.release = v4l2_release,.poll = v4l2_poll,.llseek = no_llseek,
};

这个是video_device中的字符设备对应的fops,应用层发生系统调用会率先调用到这里,我们来好好看一看这些调用

v4l2_open

static int v4l2_open(struct inode *inode, struct file *filp)
{struct video_device *vdev;/* 根据次设备获得video_device */vdev = video_devdata(filp);/* 回调video_device的fops */if (vdev->fops->open)ret = vdev->fops->open(filp); //回调video
}

从这个函数可以看到,发生系统调用首先来到v4l2核心层的字符设备,然后再回调到对应的video_device,video_device在前面已经实现了v4l2_file_operations和v4l2_ioctl_ops一系列回调

v4l2_ioctl

V4L2的应用编程会有非常多的ioctl,会先调用到此处

static int v4l2_ioctl(struct inode *inode, struct file *filp,unsigned int cmd, unsigned long arg)
{struct video_device *vdev = video_devdata(filp);/* 回调到video_device中 */return vdev->fops->ioctl(filp, cmd, arg);
}

下面来看一看video_device怎么实现ioctl

static const struct v4l2_file_operations video_dev_fops = {.owner		    = THIS_MODULE,.release        = vdev_close,.read           = vdev_read,.poll		    = vdev_poll,.ioctl          = video_ioctl2,.mmap           = vdev_mmap,
};

从上面驱动程序的编写,我们可以知道video_device对应ioctl就是video_ioctl2,这个函数是内核提供的,我们看一看这个函数的内容

long video_ioctl2(struct file *file,unsigned int cmd, unsigned long arg)
{__video_do_ioctl(file, cmd, parg);
}
static long __video_do_ioctl(struct file *file,unsigned int cmd, void *arg)
{/* 获取video_device */struct video_device *vfd = video_devdata(file);/* 获取video_device的ioctl_ops */const struct v4l2_ioctl_ops *ops = vfd->ioctl_ops;switch (cmd) {case VIDIOC_QUERYCAP:ops->vidioc_querycap(file, fh, cap);case VIDIOC_ENUM_FMT:ops->vidioc_enum_fmt_vid_cap(file, fh, f);...}
}

可以看出,最终会调用到video_device实现的v4l2_ioctl_ops

/* 实现各种系统调用 */
static const struct v4l2_ioctl_ops video_dev_ioctl_ops = {.vidioc_querycap      = vidioc_querycap,.vidioc_enum_fmt_vid_cap  = vidioc_enum_fmt_vid_cap,.vidioc_g_fmt_vid_cap     = vidioc_g_fmt_vid_cap,.vidioc_try_fmt_vid_cap   = vidioc_try_fmt_vid_cap,.vidioc_s_fmt_vid_cap     = vidioc_s_fmt_vid_cap,.vidioc_reqbufs       = vidioc_reqbufs,.vidioc_querybuf      = vidioc_querybuf,.vidioc_qbuf          = vidioc_qbuf,.vidioc_dqbuf         = vidioc_dqbuf,.vidioc_enum_input    = vidioc_enum_input,.vidioc_g_input       = vidioc_g_input,.vidioc_s_input       = vidioc_s_input,.vidioc_streamon      = vidioc_streamon,.vidioc_streamoff     = vidioc_streamoff,
};

所以系统调用最先都会调用到字符设备的fops,然后经过v4l2核心层最终调用到video_device这里

关于v4l2的驱动框架就分析到这里,关于更加详细的实现v4l2驱动,将在后续文章中通过实例讲解

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

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

相关文章

游戏中的设计模式一

游戏开发是一个快速迭代的过程&#xff0c;代码复杂度也很高&#xff0c;借助于设计模式&#xff0c;可以帮助我们降低复杂度&#xff0c;降低系统间的耦合&#xff0c;从而高效高质的做出交付。 最近读了这本书&#xff1a;《游戏编程模式》[1]&#xff0c;很受启发&#xff…

win10系统解除微软账户和本地账户绑定

折腾了好久&#xff0c;终于找到一种方法可以退出微软账号了&#xff0c;不过这种方法我测试是成功的&#xff0c;有人留言自己不成功&#xff0c;具体解决方法只能看这些留言了 win10当中没有注销按钮&#xff0c;win x 弹出的菜单里面有关闭或注销&#xff0c;可以选择注销…

最短木板长度 - 贪心思维

系列文章目录 文章目录 系列文章目录前言一、题目描述二、输入描述三、输出描述四、java代码五、测试用例 前言 本人最近再练习算法&#xff0c;所以会发布自己的解题思路&#xff0c;希望大家多指教 一、题目描述 小明有 n 块木板&#xff0c;第 i ( 1 ≤ i ≤ n ) 块木板长…

NASA数据即——Aqua AIRS 第 3 级光谱出射长波辐射 (OLR) 月报 (AIRSIL3MSOLR)

Aqua AIRS Level 3 Spectral Outgoing Longwave Radiation (OLR) Monthly (AIRSIL3MSOLR) Aqua AIRS 第 3 级光谱出射长波辐射 (OLR) 月报 (AIRSIL3MSOLR) 简介 这个 L3 光谱出射长波辐射&#xff08;OLR&#xff09;是根据密歇根大学黄向磊开发的算法&#xff0c;利用 AIRS…

前端XHR请求数据

axios封装了XHR(XMLHttpRequest) 效果 项目结构 Jakarta EE9&#xff0c;Web项目。 无额外的maven依赖 1、Web页面 index.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title&…

【JS红宝书学习笔记】第1、2章 初识JS

第1章 什么是JavaScript JavaScript 是一门用来与网页交互的脚本语言&#xff0c;包含以下三个组成部分。 ECMAScript&#xff1a;由 ECMA-262 定义并提供核心功能。文档对象模型&#xff08;DOM&#xff09;&#xff1a;提供与网页内容交互的方法和接口。浏览器对象模型&…

鸿蒙内核源码分析 (内存池管理) | 如何高效切割合并内存块

动态分配 系列篇将动态分配分成上下两篇&#xff0c;本篇为下篇&#xff0c;阅读之前建议翻看上篇。 鸿蒙内核源码分析(TLFS算法) 结合图表从理论视角说清楚 TLFS 算法鸿蒙内核源码分析(内存池管理) 结合源码说清楚鸿蒙内核动态内存池实现过程&#xff0c;个人认为这部分代码…

羊大师分析,羊奶助力共筑健康中国新生活

羊大师分析&#xff0c;羊奶助力共筑健康中国新生活 在健康中国行动的大背景下&#xff0c;我们越来越注重生活方式的健康与营养。羊大师发现&#xff0c;羊奶作为一种营养丰富、易于吸收的天然食品&#xff0c;正逐渐成为我们追求健康生活的得力助手。 羊奶富含优质蛋白质、矿…

vue3自定义指令​(通过指令钩子获得dom和钩子参数)

实现文本框自动获得焦点 Index.vue: <script setup> import { ref, onMounted } from vue import ./index.cssconst vFocus {mounted: (el, binding) > {el.focus()console.log(binding)} }onMounted(() > {}) </script><template><div class&qu…

设计说明-行为型-状态模式-State

状态接口 public interface State {//状态接口void insertQuarter();//投币void ejectQuarter();//退币void turnCrank();//按下“出纸巾”按钮void dispense();//出纸巾 } 有纸巾类 public class HasQuarterState implements State {private TissueMachine tissueMachine;O…

Python中tkinter编程入门4

在Python中tkinter编程入门3-CSDN博客中创建了Button控件&#xff0c;点击该控件就会产生一个点击事件&#xff0c;在创建Button控件时指定该点击事件的处理程序后&#xff0c;按键控件就会对用户的点击事件产生响应。 1 定义事件处理器 定义事件处理器就是一个自定义的函数。…

前端连续发送同一个请求时,终止上一次请求

场景&#xff1a;几个tab页之间快速的切换&#xff08;tab页只是参数不同&#xff0c;下边的数据渲染给同一个data&#xff09;就会导致如果我在1,2,3&#xff0c;tab页按照顺序快速点击&#xff0c;发送三个请求&#xff0c;我想要展示的是3但是如果1或者2请求响应的时间比3长…

Python tensor向量维度转换,不同维度的向量转化为相同的维度,经过全连接层MLP的维度转换,代码实战

问题&#xff1a;在机器学习特征工程中&#xff0c;假如每类特征需要转化为相同的维度进行拼接&#xff0c;那该怎么办呢&#xff1f;接一个全连接层MLP就可以了。 例子&#xff1a;将&#xff08;128,64&#xff09; 维度的向量转化为&#xff08;128,32&#xff09;维。 impo…

Stm32串口搭配DMA实现自定义printf、scanf

前言:本文仅供学习参考使用&#xff0c;主要目的是让大家快速使用串口调试&#xff0c;文章所提及的GCC适用于Clion&#xff0c;Vscode等第三方编辑器的用户。作者有时间会继续更新^_^ 一、GCC环境 1、标准库 (1)、使用方法 在主函数while(1)初始化中&#xff0c;添加Seria…

柯桥法语学习-5大法语听写网站:全力助攻你的dictée!

提到法语dicte&#xff0c;绝对可是法语学生们的老大难&#xff0c;简直就是心痛得不能自已啊&#xff01;所以今天&#xff0c;法语君整理了5个听写网站助攻大家的dicte哦&#xff01; Projet Voltaire 01 一个很容易让你对dicte上瘾的APP 写邮件、实习报告或者动机信时&…

Nat Plants | 植物抽核单细胞!多组学探究大豆根瘤成熟过程

发表时间&#xff1a;2023-04 发表期刊&#xff1a;Nature Plants 影响因子&#xff1a;17.352 DOI&#xff1a;10.1038/s41477-023-01387-z 研究背景 根瘤菌是亲和互作寄主植物&#xff0c;感染宿主并在根部形成共生器官根瘤&#xff0c;具有固氮…

jmeter中HttpClient4发送失败,java方法请求成功

jmeter中HttpClient4请求失败 上传文件时&#xff1a;Httpclient4: 请求体 请求头 响应结果 ,后端服务都总是提示存在非法标签。 jmeter中使用java请求成功 修改使用java方式&#xff0c;访问正常&#xff1b; 根据分析可能因为HC4对一些特殊字符会进行转义&#xff0c;转义后…

idea配置MySQL提示

点击sql语句&#xff0c;然后再选择show context actions 然后再选择Inject language or reference 然后再选择MySQL 然后我们会发现sql语句变颜色了 如果表是红色 那么需要我们连接mysql的对于的数据库

做私域不止是积累流量,生态也很重要!

如今&#xff0c;私域流水占比已经逼近整个零售市场的30%&#xff0c;达到4万亿规模&#xff0c;百度、阿里、腾讯等头部玩家也都在加速布局&#xff0c;私域运营&#xff0c;已不再是一个单一的商业模式或者运营手段&#xff0c;而是逐渐构成一种可持续的行业生态。 一、什么…

全球量子计算已开始商业化!应用最多的行业你一定想不到

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨卉可 排版丨沛贤 深度好文&#xff1a;2600字丨5 分钟阅读 01 量子计算是一场高风险的游戏 近日&#xff0c;PsiQuantum从澳大利亚联邦政府和地方政府获得了10亿澳元资金&#xff0c;这…