Linux摄像头驱动1——vivid(转)
转载自 hceng blog: https://hceng.cn/2018/03/08/Linux%E6%91%84%E5%83%8F%E5%A4%B4%E9%A9%B1%E5%8A%A81%E2%80%94%E2%80%94vivid/
Linux摄像头驱动学习第一篇,对虚拟视频驱动Virtual Video Driver(vivid)进行测试、分析、编写。
V4L2(Video for Linux two)是Linux内核中关于视频设备的内核驱动框架,为上层的访问底层的视频设备提供了统一的接口。
V4L2可以支持多种设备,它可以有以下几种接口:
- Video capture interface(视频采集接口):从摄像头等设备上获取视频数据,是V4L2设计最初功能;
- Video output interface(视频输出接口):驱动计算机的外围视频、图像显示设备;
- Video overlay interface(直接传输视频接口):把从视频采集设备采集过来的信号直接输出到输出设备之上,而不用经过CPU;
- Video output overlay device(视频输出覆盖设备):也被称为OSD(On-Screen Display),即在显示画面上叠加一层显示,比如菜单设置界面;
- VBI interface(视频间隔消隐信号接口):提供对VBI(Vertical Blanking Interval)数据的控制,它可以使应用可以访问传输消隐期的视频信号;
- Radio interface(收音机接口):处理从AM或FM高频头设备接收来的音频流
1.V4L2框架分析
2.测试vivid
这里目的先加载vivid驱动,然后运行应用程序调用vivid驱动,初步体验效果。
2.1加载驱动
先在Ubuntu16.04上输入uname -a
,可以得到当前Ubuntu内核版本号:
Linux ubuntu 4.4.0-116-generic #140-Ubuntu SMP Mon Feb 12 21:23:04 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
当前内核版本是4.4.0-116-generic
,然后去Linux内核官网下载对应的内核,提取出其中的linux-4.13.9/drivers/media/
文件夹。
修改media/platform/vivid/
下的Makefile
:
|
然后执行make
编译,获得vivid.ko
此时加载模块sudo insmod vivid.ko
,发现报错如下:
insmod: ERROR: could not insert module vivid.ko: Unknown symbol in module
原因是模块中的一些依赖函数的模块,没有加载,通过dmesg
命令,可以看到很多函数:
|
需要先加载这些函数所在的模块才行。
这里有两个方法:
一是找到函数对应的文件,修改Makefile
,编译出来,先加载。
二是找到函数对应的文件,其实模块都已经编译好了,路径在/lib/modules/4.4.0-116-generic/kernel/drivers/media/v4l2-core/
里面,直接加载即可。
这两种方式都需要慢慢找对应的文件,比较麻烦,直接:
|
这里先使用modprobr
加载vivid,会将其依赖一并加载,然后再卸载vivid,最后加载上我们编译的vivid.ko
。
这里为什么使用自己编译的vivid.ko
,而不使用自带的?
因为后面修改vivid源码后,重新加载修改后的驱动,才知道修改后的效果。
2.2应用程序
Linux摄像头测试软件webcam、spcaview、luvcview、xawtv等,经测试,luvcview
和xawtv
比较靠谱。
luvcview
luvcview -h //帮助信息 luvcview -d /dev/videoX //指定设备 luvcview -L //查询当前摄像头所支持的所有数据格式以及分辨率信息 luvcview //运行摄像头
xawtv
xawtv -h //帮助信息 xawtv -c /dev/videoX //指定设备 xawtv -noalsa //关闭声卡启动 xawtv //运行摄像头
这里加载vivid驱动后,运行xawtv效果如下:
3.分析vivid
第一次接触V4L2,直接对内核提供的Virtual Video Driver(vivid)进行分析,只要熟悉了vivid,后续再对摄像头再进行分析,就会轻松很多。
vivid是内核提供的一个虚拟机的视频设备驱动,内核提供的vivid源码在linux-4.13.9/drivers/media/platform/vivid/
。
3.1 初始化、注册分析
在vivid_init()
里分别注册了vivid_pdev
和vivid_pdrv
,注册后,由于两者name
一致,则会调用probe()
。在probe()
里面主要进行初始化、注册等相关流程。
可以看到,在probe()
里,会调用vivid_create_instance()
,让后在里面先分配一个video_device
,然后设置video_device
,包括操作函数ops
,ioctl
操作函数,设备等。
然后对ctrl
属性进行详细的设置,最后注册video_device
,和进行常规的字符设备注册。
因此,写摄像头驱动程序的流程如下:
- 分配
video_device
:video_device_alloc()
或kzalloc()
;- 设置
video_device
:.fops
、.ioctl_ops
、dev
;- 注册
video_device
:video_register_device()
;
3.2 操作函数分析
再来看看操作函数是如何调用的:
当应用层open()
/read()
/write()
操作/dev/videox
时,先找到v4l2_fops
,
然后调用v4l2_open
/v4l2_read
/v4l2_write
(drivers/media/v4l2-core/v4l2-dev.c
),
再通过video_devdata
根据次设备号从数组中得到video_device
,再找到vivid_fops
里对应的操作函数。
ioctl
的前面流程类似,后面通过video_usercopy()
获取传入的ioctl
类型,找到对应ioctl_ops
,调用不同的ioctl
。
3.3 ioctl_ops分析
摄像头驱动有众多的ioctl
,这些ioctl
实现了对设备的控制:
|
提取出11个必须的ioctl
:
/* 表示它是一个摄像头设备 */
.vidioc_querycap = vidioc_querycap, /* 摄像头数据格式的操作 */
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid, //列举格式
.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 = vb2_ioctl_reqbufs, //申请
.vidioc_querybuf = vb2_ioctl_querybuf, //查询
.vidioc_qbuf = vb2_ioctl_qbuf, //放入
.vidioc_dqbuf = vb2_ioctl_dqbuf, //取出
.vidioc_streamon = vb2_ioctl_streamon, //启动
.vidioc_streamoff = vb2_ioctl_streamoff, //停止
3.4 数据获取过程分析
1.请求分配缓冲区
|
2.查询映射缓冲区
|
3.把缓冲区放入队列
|
4.启动摄像头
|
5.用select查询是否有数据
|
6.有数据后,从队列取出缓冲区
|
7.读取对应地址缓冲区
应用程序根据VIDIOC_DQBUF所得到缓冲区状态,知道是哪一个缓冲区有数据,就去读对应的地址(该地址来自前面的mmap)
调试技巧:
1.得到xawtv进行了哪些系统调用:
sudo strace -o xawtv.log xawtv
2.在串口终端下,修改打印等级:
sudo echo "8 4 1 7" >/proc/sys/kernel/printk
3.当无串口,即前面方法无效时,在SSH登陆时:
tail -f /var/log/kern.log &
可实现内核打印增量显示。
4.编写vivid
根据前面的分析,简单记录下虚拟视频驱动的编写流程:
1.注册平台设备和驱动;
2.probe()函数:
a.分配video_device
;
b.设置video_device
,包括:release
、fops
、ioctl_ops
、v4l2_dev
;
c.注册设置video_device
;
d.其它:定义/初始化自旋锁/定时器;
3.填充操作函数v4l2_file_operations
:
a.open()
:初始buf化队列和设置定时器;
b.close()
:删除定时器和释放buf队列;
c.mmap()
:调用videobuf_mmap_mapper
开辟虚拟内存;
d.poll()
:调用videobuf_poll_stream
实现poll机制非阻塞访问;
4.填充操作函数v4l2_ioctl_ops
:
前面介绍的11个必须ioctl,几乎都是调用内核提供的API;
5.填充操作函数videobuf_queue_ops
:
对buf进行一些操作;
6.填充数据:
利用定时器,不断产生数据并唤醒进程,实现获取到图像采集数据;
[my_vivid.c]link
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/font.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/videodev2.h>
#include <linux/v4l2-dv-timings.h>
#include <media/videobuf2-vmalloc.h>
#include <media/videobuf2-dma-contig.h>
#include <media/v4l2-dv-timings.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-fh.h>
#include <media/v4l2-event.h>
#include <media/v4l2-device.h>
#include <media/videobuf-core.h>
#include <media/videobuf-vmalloc.h>#include "fillbuf.c"/* 队列操作a: 定义自旋锁、定时器、buf队列 */
static spinlock_t my_vivid_queue_slock;
static struct timer_list my_vivid_timer;
static struct videobuf_queue my_vivid_vb_vidqueue;
static struct list_head my_vivid_vb_local_queue;static void my_vivid_timer_function(unsigned long data)
{struct videobuf_buffer *vb;void *vbuf;struct timeval ts;printk("enter %s\n", __func__);/* 1. 构造数据: 从队列头部取出第1个videobuf, 填充数据 *//* 1.1 从本地队列取出第1个videobuf */if (list_empty(&my_vivid_vb_local_queue)){goto out;}vb = list_entry(my_vivid_vb_local_queue.next,struct videobuf_buffer, queue);/* Nobody is waiting on this buffer, return */if (!waitqueue_active(&vb->done))goto out;/* 1.2 填充数据 */vbuf = videobuf_to_vmalloc(vb);//memset(vbuf, 0xFF, vb->size);my_vivid_fillbuff(vb);vb->field_count++;do_gettimeofday(&ts);vb->ts = ts;vb->state = VIDEOBUF_DONE;/* 1.3 把videobuf从本地队列中删除 */list_del(&vb->queue);/* 2. 唤醒进程: 唤醒videobuf->done上的进程 */wake_up(&vb->done);out:/* 3. 修改timer的超时时间 : 30fps, 1秒里有30帧数据* 每1/30 秒产生一帧数据*/mod_timer(&my_vivid_timer, jiffies + HZ / 30);
}/* 参考documentations/video4linux/v4l2-framework.txt:drivers\media\video\videobuf-core.cops->buf_setup - calculates the size of the video buffers and avoid they to waste more than some maximum limit of RAM;ops->buf_prepare - fills the video buffer structs and calls videobuf_iolock() to alloc and prepare mmaped memory;ops->buf_queue - advices the driver that another buffer were requested (by read() or by QBUF);ops->buf_release - frees any buffer that were allocated.*//* videobuf operations */
//APP调用ioctl VIDIOC_REQBUFS时会导致此函数被调用,它重新调整count和size
static int my_vivid_buffer_setup(struct videobuf_queue *vq, unsigned int *count, unsigned int *size)
{printk("enter %s\n", __func__);*size = my_vivid_format.fmt.pix.sizeimage;if (0 == *count)*count = 32;return 0;
}//APP调用ioctlVIDIOC_QBUF时导致此函数被调用,它会填充video_buffer结构体并调用videobuf_iolock来分配内存
static int my_vivid_buffer_prepare(struct videobuf_queue *vq, struct videobuf_buffer *vb,enum v4l2_field field)
{printk("enter %s\n", __func__);/* 1. 设置videobuf */vb->size = my_vivid_format.fmt.pix.sizeimage;vb->bytesperline = my_vivid_format.fmt.pix.bytesperline;vb->width = my_vivid_format.fmt.pix.width;vb->height = my_vivid_format.fmt.pix.height;vb->field = field;/* 2. 做些准备工作 */my_vivid_precalculate_bars(0);/* 3. 设置状态 */vb->state = VIDEOBUF_PREPARED;return 0;
}/* APP调用ioctl VIDIOC_QBUF时:* 1. 先调用buf_prepare进行一些准备工作* 2. 把buf放入stream队列* 3. 调用buf_queue(起通知、记录作用)*/
static void my_vivid_buffer_queue(struct videobuf_queue *vq, struct videobuf_buffer *vb)
{printk("enter %s\n", __func__);vb->state = VIDEOBUF_QUEUED;/* 把videobuf放入本地一个队列尾部* 定时器处理函数就可以从本地队列取出videobuf*/list_add_tail(&vb->queue, &my_vivid_vb_local_queue);
}/* APP不再使用队列时, 用它来释放内存 */
static void my_vivid_buffer_release(struct videobuf_queue *vq,struct videobuf_buffer *vb)
{printk("enter %s\n", __func__);videobuf_vmalloc_free(vb);vb->state = VIDEOBUF_NEEDS_INIT;
}static struct videobuf_queue_ops my_vivid_video_qops =
{.buf_setup = my_vivid_buffer_setup, /* 计算大小以免浪费 */.buf_prepare = my_vivid_buffer_prepare,.buf_queue = my_vivid_buffer_queue,.buf_release = my_vivid_buffer_release,
};/* v4l2_file_operations */
static int my_vivid_open(struct file *file)
{printk("enter %s\n", __func__);//队列操作c:初始化videobuf_queue_vmalloc_init(&my_vivid_vb_vidqueue, &my_vivid_video_qops,NULL, &my_vivid_queue_slock, V4L2_BUF_TYPE_VIDEO_CAPTURE, V4L2_FIELD_INTERLACED,sizeof(struct videobuf_buffer), NULL, NULL); /* 倒数第3个参数是buffer的头部大小 */my_vivid_timer.expires = jiffies + 1;add_timer(&my_vivid_timer);return 0;
}static int my_vivid_close(struct file *file)
{printk("enter %s\n", __func__);del_timer(&my_vivid_timer);videobuf_stop(&my_vivid_vb_vidqueue);videobuf_mmap_free(&my_vivid_vb_vidqueue);return 0;
}static int my_vivid_mmap(struct file *file, struct vm_area_struct *vma)
{printk("enter %s\n", __func__);return videobuf_mmap_mapper(&my_vivid_vb_vidqueue, vma);
}static unsigned int my_vivid_poll(struct file *file, struct poll_table_struct *wait)
{printk("enter %s\n", __func__);return videobuf_poll_stream(file, &my_vivid_vb_vidqueue, wait);
}/* v4l2_ioctl_ops */
static int my_vivid_vidioc_querycap(struct file *file, void *priv,struct v4l2_capability *cap)
{printk("enter %s\n", __func__);strcpy(cap->driver, "my_vivid");strcpy(cap->card, "my_vivid");cap->version = 0x0001;cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS;cap->capabilities = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_DEVICE_CAPS;return 0;
}static int my_vivid_vidioc_enum_fmt_vid_cap(struct file *file, void *priv,struct v4l2_fmtdesc *f)
{printk("enter %s\n", __func__);if (f->index >= 1)return -EINVAL;strcpy(f->description, "4:2:2, packed, YUYV");f->pixelformat = V4L2_PIX_FMT_YUYV;return 0;
}static int my_vivid_vidioc_g_fmt_vid_cap(struct file *file, void *priv,struct v4l2_format *f)
{printk("enter %s\n", __func__);memcpy(f, &my_vivid_format, sizeof(my_vivid_format));return 0;
}static int my_vivid_vidioc_try_fmt_vid_cap(struct file *file, void *priv,struct v4l2_format *f)
{unsigned int maxw, maxh;enum v4l2_field field;printk("enter %s\n", __func__);if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV)return -EINVAL;field = f->fmt.pix.field;if (field == V4L2_FIELD_ANY){field = V4L2_FIELD_INTERLACED;}else if (V4L2_FIELD_INTERLACED != field){return -EINVAL;}maxw = 1024;maxh = 768;/* 调整format的width, height,* 计算bytesperline, sizeimage*/v4l_bound_align_image(&f->fmt.pix.width, 48, maxw, 2,&f->fmt.pix.height, 32, maxh, 0, 0);f->fmt.pix.bytesperline =(f->fmt.pix.width * 16) >> 3;f->fmt.pix.sizeimage =f->fmt.pix.height * f->fmt.pix.bytesperline;return 0;
}static int my_vivid_vidioc_s_fmt_vid_cap(struct file *file, void *priv,struct v4l2_format *f)
{int ret;printk("enter %s\n", __func__);ret = my_vivid_vidioc_try_fmt_vid_cap(file, NULL, f);if (ret < 0)return ret;memcpy(&my_vivid_format, f, sizeof(my_vivid_format));return ret;
}static int my_vivid_vidioc_reqbufs(struct file *file, void *priv,struct v4l2_requestbuffers *p)
{printk("enter %s\n", __func__);return (videobuf_reqbufs(&my_vivid_vb_vidqueue, p));
}static int my_vivid_vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *p)
{printk("enter %s\n", __func__);return (videobuf_querybuf(&my_vivid_vb_vidqueue, p));
}static int my_vivid_vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *p)
{printk("enter %s\n", __func__);return (videobuf_qbuf(&my_vivid_vb_vidqueue, p));
}static int my_vivid_vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *p)
{printk("enter %s\n", __func__);return (videobuf_dqbuf(&my_vivid_vb_vidqueue, p, file->f_flags & O_NONBLOCK));
}static int my_vivid_vidioc_streamon(struct file *file, void *priv, enum v4l2_buf_type i)
{printk("enter %s\n", __func__);return videobuf_streamon(&my_vivid_vb_vidqueue);
}static int my_vivid_vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type i)
{printk("enter %s\n", __func__);videobuf_streamoff(&my_vivid_vb_vidqueue);return 0;
}static const struct v4l2_ioctl_ops my_vivid_ioctl_ops =
{// 表示它是一个摄像头设备.vidioc_querycap = my_vivid_vidioc_querycap,/* 用于列举、获得、测试、设置摄像头的数据的格式 */.vidioc_enum_fmt_vid_cap = my_vivid_vidioc_enum_fmt_vid_cap,.vidioc_g_fmt_vid_cap = my_vivid_vidioc_g_fmt_vid_cap,.vidioc_try_fmt_vid_cap = my_vivid_vidioc_try_fmt_vid_cap,.vidioc_s_fmt_vid_cap = my_vivid_vidioc_s_fmt_vid_cap,/* 缓冲区操作: 申请/查询/放入队列/取出队列 */.vidioc_reqbufs = my_vivid_vidioc_reqbufs,.vidioc_querybuf = my_vivid_vidioc_querybuf,.vidioc_qbuf = my_vivid_vidioc_qbuf,.vidioc_dqbuf = my_vivid_vidioc_dqbuf,// 启动/停止.vidioc_streamon = my_vivid_vidioc_streamon,.vidioc_streamoff = my_vivid_vidioc_streamoff,
};static const struct v4l2_file_operations my_vivid_fops =
{.owner = THIS_MODULE,.open = my_vivid_open,.release = my_vivid_close,.mmap = my_vivid_mmap,.unlocked_ioctl = video_ioctl2, /* V4L2 ioctl handler */.poll = my_vivid_poll,
};static struct video_device *my_vivid_dev;
static struct v4l2_device v4l2_dev;static void my_vivid_dev_release(struct video_device *vdev)
{printk("enter %s\n", __func__);
}static int my_vivid_probe(struct platform_device *pdev)
{int ret;printk("enter %s\n", __func__);/* 1.分配一个video_device结构体 */my_vivid_dev = video_device_alloc();if (NULL == my_vivid_dev){printk("Failed to alloc video device (%d)\n", ret);return -ENOMEM;}/* 2.设置 */my_vivid_dev->release = my_vivid_dev_release;my_vivid_dev->fops = &my_vivid_fops;my_vivid_dev->ioctl_ops = &my_vivid_ioctl_ops;my_vivid_dev->v4l2_dev = &v4l2_dev;//队列操作b:初始化自旋锁spin_lock_init(&my_vivid_queue_slock);/* 3.注册 */ret = video_register_device(my_vivid_dev, VFL_TYPE_GRABBER, -1);if (ret){printk("Failed to register as video device (%d)\n", ret);goto err_register_dev;}//用定时器产生数据并唤醒进程init_timer(&my_vivid_timer);my_vivid_timer.function = my_vivid_timer_function;INIT_LIST_HEAD(&my_vivid_vb_local_queue);return 0;err_register_dev:video_device_release(my_vivid_dev);return -ENODEV;
}static int my_vivid_remove(struct platform_device *pdev)
{printk("enter %s\n", __func__);v4l2_device_unregister(my_vivid_dev->v4l2_dev);video_device_release(my_vivid_dev);return 0;
}static void my_vivid_pdev_release(struct device *dev)
{printk("enter %s\n", __func__);
}static struct platform_device my_vivid_pdev =
{.name = "my_vivid",.dev.release = my_vivid_pdev_release,
};static struct platform_driver my_vivid_pdrv =
{.probe = my_vivid_probe,.remove = my_vivid_remove,.driver = {.name = "my_vivid",},
};static int my_vivid_init(void)
{int ret;printk("enter %s\n", __func__);ret = platform_device_register(&my_vivid_pdev);if (ret)return ret;ret = platform_driver_register(&my_vivid_pdrv);if (ret)platform_device_unregister(&my_vivid_pdev);return ret;}static void my_vivid_exit(void)
{printk("enter %s\n", __func__);platform_driver_unregister(&my_vivid_pdrv);platform_device_unregister(&my_vivid_pdev);
}module_init(my_vivid_init);
module_exit(my_vivid_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("hceng <huangcheng.job@foxmail.com>");
MODULE_DESCRIPTION("A Virtual Video Test Code For Learn.");
MODULE_ALIAS("My vivid");
MODULE_VERSION("V1.0");
5.测试效果
重新编译驱动,加载新驱动:
|
实测还差两个驱动依赖:
|
运行xawtv
:
Linux摄像头驱动1——vivid(转)相关推荐
- Linux摄像头驱动1——vivid
CSDN仅用于增加百度收录权重,排版未优化,日常不维护.请访问:www.hceng.cn 查看.评论. 本博文对应地址: https://hceng.cn/2018/03/08/Linux摄像头驱动1 ...
- linux v4l2系统详解,Linux摄像头驱动学习之:(一)V4L2_框架分析
这段时间开始搞安卓camera底层驱动了,把以前的的Linux视频驱动回顾一下,本篇主要概述一下vfl2(video for linux 2). 一. V4L2框架: video for linux ...
- linux cmos摄像头,Linux摄像头驱动4——CMOS摄像头
Linux摄像头驱动学习第四篇,对CMOS摄像头进行学习,实现在Tiny4412上使用CMOS摄像头采集图像,在LCD上显示图像. 坚持到了Linux摄像头学习的最后一部分--CMOS摄像头. 写完本 ...
- linux摄像头V4L2 subdev,linux 摄像头驱动 详解linux 摄像头驱动编写
想了解详解linux 摄像头驱动编写的相关内容吗,feixiaoxing在本文为您仔细讲解linux 摄像头驱动的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:linux,摄像头驱动,下面 ...
- linux摄像头教程,linux 摄像头驱动编写方法教程
从某个角度来说,摄像头是各个厂家主打的应用功能.那么,linux是如何支持摄像头的,我们可以来看一下?本篇文章主要介绍了详解linux 摄像头驱动编写,小编觉得挺不错的,现在分享给大家,也给大家做个参 ...
- Linux摄像头驱动第一篇之虚拟摄像头驱动vivi.c
本文学习自韦东山老师的摄像头驱动模块 目录 一 摄像头驱动程序学习切入点以及V4L2模型概览 二 简析虚拟视频驱动 VIVI.C 2.1 初始化.设置.注册过程 2.2 简析vivi.c的open,r ...
- linux驱动双摄像头,详解linux 摄像头驱动编写
对于现代嵌入式设备,特别是手机来说,摄像头是很重要的一个设备.很多同学买手机,一看颜值,第二就看摄像头拍照如何.所以,从某个角度来说,摄像头是各个厂家主打的应用功能.那么,linux是如何支持摄像头的 ...
- 【Java常用工具类汇总 2,linux摄像头驱动架构
System.out.println(Strings.padStart("zs", 2, '*'));//zs System.out.println(Strings.padStar ...
- 通过虚拟驱动vivi分析摄像头驱动
Linux摄像头驱动学习之:(二)通过虚拟驱动vivi分析摄像头驱动 一.通过指令 "strace -o xawtv.log xawtv" 得到以下调用信息: // 1~7都是在v ...
最新文章
- 求职Python开发,面试官最喜欢问的几个问题
- 剑指OFFER之从二叉搜索树的后序遍历序列(九度OJ1367)
- JZOJ 5629. 【NOI2018模拟4.4】Map
- 九宫怎么排列和使用_什么是九宫飞星,九宫飞星在生活中的应用
- docker 镜像命令
- [Spring Boot系列]1. 项目搭建之一
- [概率][lca][dfs][树形dp] Jzoj P4225 宝藏
- 浮点数的加减计算总结
- 详解卷积神经网络(CNN)在语音识别中的应用
- ZCuPb10Sn10铸造锡青铜板ZCuPb10Sn10力学性能
- oracle字典在线查字手写,在线字典手写输入
- kuangbin 基础DP1
- 黎活明给程序员的忠告
- 关于shiro报The security manager does not implement the WebSecurityManager interface
- 淘宝商品评论获取评论
- 好的计算机书籍 http://outmyth.blogdriver.com/outmyth/1122212.html
- 有趣的表情包购物网站
- 艾美捷利妥昔单抗Rituximab参数及应用
- html手机号输入框,手机号输入框自动格式化为344
- 终于有一篇能让小白更容易理解GC算法的文章了