这几天在学习写一个基于v4l2的摄像头应用程序,网上找了很多资料和程序,终于成功的采集到了图像。关于vl42的应用网上很多资料,我根据自己的理解总结了一下自己的学习成果。
对vl42的应用层编写就两点:相关数据结构和ioctl命令字。我参考的是这篇文章:http://blog.youkuaiyun.com/eastmoon502136/article/details/8190262
一、相关数据结构:参见 include/linux/videodev2.h
1、设备能力结构
struct v4l2_capability {
__u8 driver[16]; /*驱动名 */
__u8 card[32]; /*设备名*/
__u8 bus_info[32]; /*设备在系统中的位置 */
__u32 version; /*驱动版本号 */
__u32 capabilities; /*设备能力,即设备支持的操作*/
__u32 reserved[4]; /*保留字段*/
};
struct v4l2_fmtdesc {
__u32 index; /*要查询的帧格式序号,由应用程序设置 */
enum v4l2_buf_type type; /*帧类型,由应用程序设置 */
__u32 flags; /*是否为压缩格式*/
__u8 description[32]; /*格式名称 */
__u32 pixelformat; /*格式 */
__u32 reserved[4];
};
2、数据格式结构
struct v4l2_format {
enum v4l2_buf_type type; //帧类型,由应用程序设置
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE,视频设备使用 */
struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */
struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
3、像素格式结构
struct v4l2_pix_format {
__u32 width; //宽度
__u32 height; //高度
__u32 pixelformat;
enum v4l2_field field; //帧格式
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
enum v4l2_colorspace colorspace;
__u32 priv; /* private data, depends on pixelformat */
};
4、请求缓冲
struct v4l2_requestbuffers {
__u32 count; //缓存数量
enum v4l2_buf_type type; //数据流类型
enum v4l2_memory memory;
__u32 reserved[2];
};
5、数据流类型
enum v4l2_buf_type {
V4L2_BUF_TYPE_VIDEO_CAPTURE = 1,
V4L2_BUF_TYPE_VIDEO_OUTPUT = 2,
V4L2_BUF_TYPE_VIDEO_OVERLAY = 3,
V4L2_BUF_TYPE_VBI_CAPTURE = 4,
V4L2_BUF_TYPE_VBI_OUTPUT = 5,
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE = 6,
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT = 7,
#if 1
/* Experimental */
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8,
#endif
V4L2_BUF_TYPE_PRIVATE = 0x80,
};
enum v4l2_memory {
V4L2_MEMORY_MMAP = 1,
V4L2_MEMORY_USERPTR = 2,
V4L2_MEMORY_OVERLAY = 3,
};
二、ioctl命令字
命令标字 | 功能 |
VIDIOC_REQBUFS | 申请缓存区 |
VIDIOC_QUERYBUF | 把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址 |
VIDIOC_QUERYCAP | 查询设备属性 |
VIDIOC_ENUM_FMT | 获取当前驱动支持的视频格式 |
VIDIOC_S_FMT | 设置当视频采集图像格式 |
VIDIOC_G_FMT | 读取当视频采集图格式 |
VIDIOC_TRY_FMT | 验证当前驱动的显示格式 |
VIDIOC_CROPCAP | 查询设备的修剪能力 |
VIDIOC_S_CROP | 设置视频信号的边框 |
VIDIOC_G_CROP | 读取视频信号的边框 |
VIDIOC_QBUF | 把数据从缓存区中读取出来 |
VIDIOC_DQBUF | 把缓存区内存放回缓存队列 |
VIDIOC_STREAMON | 开启视频采集 |
VIDIOC_STREAMOFF | 结束视频采集 |
VIDIOC_QUERYSTD | 检查当前视频采集设备支持的标准 |
三、成功运行的程序:show.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <malloc.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#include <linux/videodev2.h>
#define CLEAR(x) memset(&(x),0,sizeof(x))
//定义一个缓存结构体
struct buffer
{
void* start; //空指针
size_t length; //长度大小
};
#define CAPTURE_FILE "test.yuyv"
static char* dev_name = NULL; //设备名
static int fd = -1; //文件序列
static struct buffer* buffers = NULL; //缓存对象
static unsigned int n_buffers = 0; //缓存数
static void errno_exit(const char* s) //错误输出
{
fprintf(stderr,"%s error %d, %s\n",s,errno,strerror(errno));
exit(EXIT_FAILURE);
}
/*
static void process_image(const void* p, size_t length) //处理获取图像
{
// fputc('!',stdout);
//fflush(stdout);
FILE *fp = fopen(CAPTURE_FILE, "a");
if (fp<0)
{
printf("open frame data file failed\n");
exit(EXIT_FAILURE);
}
fwrite(p,length, 1, fp);
perror("fwrite error");
fclose(fp);
printf("Capture one frame saved in %s\n", CAPTURE_FILE);
}
*/
static void read_frame(FILE *fp) //读取数据帧,用的都是mmap
{
struct v4l2_buffer buf;
unsigned int i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if(-1 == ioctl(fd,VIDIOC_DQBUF,&buf)) //读取
errno_exit("VIDIOC_DQBUF");
fwrite(buffers[buf.index].start, buf.length, 1, fp);
if(-1 == ioctl(fd,VIDIOC_QBUF,&buf)) //放回缓存
errno_exit("VIDIOC_QBUF");
}
static void mainloop(void)
{
unsigned int count;
count = 100;
FILE *fp = fopen(CAPTURE_FILE, "w");
if (fp<0)
{
printf("open frame data file failed\n");
exit(EXIT_FAILURE);
}
while(count-- > 0)
{
read_frame(fp);
}
fclose(fp);
printf("Capture one frame saved in %s\n", CAPTURE_FILE);
}
static void stop_capturing(void) //停止捕获帧
{
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(-1 == ioctl(fd,VIDIOC_STREAMOFF,&type)) //关闭视频流
errno_exit("VIDIOC_STREAMOFF");
printf("\nStreamOff success!\n");
}
static void start_capturing(void) //开始捕获帧
{
unsigned int i;
enum v4l2_buf_type type;
for(i = 0;i < n_buffers;i++)
{
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if(-1 == ioctl(fd,VIDIOC_QBUF,&buf)) //放入缓存
errno_exit("VIDIOC_QBUF");
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(-1 == ioctl(fd,VIDIOC_STREAMON,&type)) //打开视频流
errno_exit("VIDIOC_STREAMON");
else printf("StreamOn success!\n");
}
static void uninit_device(void) //释放存储空间
{
unsigned int i;
for(i =0;i < n_buffers;i++)
{
if(-1 == munmap(buffers[i].start,buffers[i].length))
errno_exit("munmap");
}
free(buffers);
}
static void init_mmap(void) //初始化读取方式
{
struct v4l2_requestbuffers req;
CLEAR(req);
req.count = 2;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if(-1 == ioctl(fd,VIDIOC_REQBUFS,&req)) //分配内存
{
if(EINVAL == errno)
{
fprintf(stderr,"%s does not support memory mapping\n",dev_name);
exit(EXIT_FAILURE);
}
else
errno_exit("VIDIOC_REQBUFS");
}
buffers = calloc(req.count,sizeof(*buffers)); //分配缓存
if(!buffers)
{
fprintf(stderr,"Out of memory\n");
exit(EXIT_FAILURE);
}
for(n_buffers = 0;n_buffers < req.count;n_buffers++)
{
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
if(-1 == ioctl(fd,VIDIOC_QUERYBUF,&buf))
errno_exit("VIDIOC_QUERYBUF");
buffers[n_buffers].length = buf.length; //设置映射方式为mmap
buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
if(MAP_FAILED == buffers[n_buffers].start)
errno_exit("mmap");
}
}
static void init_device(void) //初始化设备
{
struct v4l2_capability cap;
struct v4l2_format fmt;
unsigned int min;
if(-1 == ioctl(fd,VIDIOC_QUERYCAP,&cap))
{
if(EINVAL == errno)
{
fprintf(stderr,"%s is no V4L2 device\n",dev_name);
exit(EXIT_FAILURE);
}
else
errno_exit("VIDIOC_QUERYCAP");
}
if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
{
fprintf(stderr,"%s is no video capture device\n",dev_name);
exit(EXIT_FAILURE);
}
if(!(cap.capabilities & V4L2_CAP_STREAMING))
{
fprintf(stderr,"%s does not support streaming i/o\n",dev_name);
exit(EXIT_FAILURE);
}
//////////////////////////////////////////
CLEAR(fmt); //设置帧格式
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if(-1 == ioctl(fd,VIDIOC_S_FMT,&fmt))
errno_exit("VIDIOC_S_FMT");
printf("Set frame format successful!\n");
init_mmap();
}
static void close_device(void) //关闭设备
{
if(-1 == close(fd))
errno_exit("close");
fd = -1;
printf("Close the device successful!\n");
}
static void open_device(void) //打开设备
{
fd = open(dev_name,O_RDWR,0);
if(-1 ==fd)
{
fprintf(stderr,"Cannot open %s\n",dev_name);
exit(EXIT_FAILURE);
}
printf("Open the %s successful\n",dev_name);
}
int main(int argc,char* argv[])
{
dev_name = "/dev/video0";
open_device();
init_device();
start_capturing();
mainloop();
stop_capturing();
uninit_device();
close_device();
exit(EXIT_SUCCESS);
return 0;
}
//---------------------------------------------------------------------
Makefile编写:
KERNELDIR ?=/home/student/linux-2.6.32.2
show: show.c
gcc -I$(KERNELDIR) -o $@ $^
clean :
rm show
//------------------------------------------------------------------------
将摄像头直接连接到PC机的usb口,会在虚拟机linux下有检测到摄像头的提示,表示摄像头可以使用,然后make,./show 就可以运行完毕
四、对上述show.c 程序的说明
网上很多程序会把上述相关数据结构整合到一个自定义的数据结构中,我认为没有必要,因为核心是通过mmap映射后的指针,所以相关数据结构只需要函数中局部变量就行,没必要整成全局变量,而buffers则需要全局变量,便于使用。
show.c源程序中最重要的是buffers指针,在init_mmap()函数中,首先 通过buffers = calloc(req.count,sizeof(*buffers)); 使它指向count个内存空间,然后通过
buffers[n_buffers].length = buf.length; //设置映射方式为mmap
buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,fd,buf.m.offset);
length的大小是640*480*2个字节,即一副图像大小。
将buffers和驱动层的缓存通过mmap联系起来,将驱动层的缓存映射到应用层。
最后通过read_frame()函数将buffers指向的地址数据写道fp中,fp指向"test.yuyv"
程序运行后会得到一个test.yuyv文件,通过samba服务器从虚拟机拷贝到Windows,用YUVViewer.exe软件打开,设置一下格式为YUYV,640X480然后play即可。