版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须>注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
2.13 摄像头V4L2编程
2.13.1 V4L2 简介
Video for Linux two 简称 V4L2,是 Linux 内核中视频类设备的一套驱动框架,为视频设备驱动开发和应用层提供了一套统一的接口规范,使用 V4L2 设备驱动框架注册的设备会在 Linux 系统的 /dev 目录下生成对应的设备节点文件,设备节点文件通常为 videoX(X 是数字编号,从 0 开始),每一个 videoX 设备文件就代表一个视频设备,V4L2 像一个优秀的快递员,将视频采集设备的图像数据安全,高效的传递给不同需求的用户。
在本章我们使用的是 USB 摄像头,只要将摄像头插上开发板上的 USB 接口,就可以在 /dev 文件夹下看到新增的 videoX 设备文件。
2.13.2 V4L2 视频采集流程
V4L2 设备驱动框架向应用层提供了统一的,标准的接口规范,其流程如下。
可以看到 V4L2 的应用编程的内容并不复杂,其实就是 open,ioctl,mmap,munmap,close 这几个调用,其中只有 ioctl 的使用稍微复杂。
在本章中,ioctl 需要传入的参数主要有如下几个。
VIDIOC_QUERYCAP // 查询设备属性和功能
VIDIOC_ENUM_FMT // 列举数据帧格式
VIDIOC_G_FMT // 获取数据帧格式
VIDIOC_S_FMT // 设置数据帧格式
VIDIOC_REQBUFS // 申请帧缓冲区
VIDIOC_QUERYBUF // 查询帧缓冲区
VIDIOC_STREAMON // 开始视频采集
VIDIOC_DQBUF // 帧缓冲出队
VIDIOC_QBUF // 帧缓冲入队
VIDIOC_STREAMOFF // 停止视频采集
本章要完成的目标是将 USB 摄像头采集到的数据展现到 LCD 屏幕上,因此在过程中会涉及到 LCD 的相关操作。
2.13.3 V4L2 视频采集原理
通过 V4L2 采集图像之前,我们需要做的很多,但是很重要的一步是分配帧缓冲区,并将分配的帧缓冲区从内核空间映射到用户空间,然后将申请到的帧缓冲区在视频采集输入队列排队,剩下的就是等待视频数据的到来。
其具体过程为,当启动视频采集后,驱动程序开始采集一帧图像数据,会把采集的图像数据放入视频采集输入队列的第一个帧缓冲区,一阵图像数据就算采集完成了。第一个帧缓冲区存满一帧图像数据后,驱动程序将该帧缓冲区移至视频采集输出队列,等待应用程序从输出队列取出,应用程序取出图像数据可以对图像数据进行处理或存储操作,然后将帧该缓冲区放入视频采集输入队列的尾部。驱动程序接下来采集下一帧数据,放入第二个缓冲区,同样的帧缓冲区存满一帧数据后,驱动程序将该缓冲区移至视频采集输出队列,应用程序将该帧缓冲区的图像数据取出后又将该帧缓冲区放入视频输入队列尾部,这样循环往复就实现了循环采集。
2.13.4 视频采集代码分析
2.13.4.1 对于全局变量和宏的解释
首先定义了一些全局变量和宏如下。
/* 注,屏幕尺寸要大于帧的尺寸 */
#define FRAME_WIDTH 640 // 视频帧的宽度
#define FRAME_HEIGH 480 // 视频帧的高度
#define LCD_WIDTH 800 // 屏幕的宽度
#define LCD_HEIGH 480 // 屏幕的高度
/* 所申请的单个 buffer 结构体
* 包含 buffer 的起始地址和长度
* 改变了用于之后视频帧的入队和出队
*/
struct v4l2_buffer_unit {
void *start;
size_t length;
};
int fd_camera = -1; // 摄像头的文件描述符
int fd_lcd = -1; // lcd 的文件描述符
void *screen_base = NULL; // 屏幕的基地址
struct v4l2_buffer_unit *buffer_unit = NULL; // 帧缓冲区单元,用于存储单帧数据,多个单元就可以组成帧缓冲队列
void *rgb_buffer = NULL; // 存储 rgb 格式数据的 buffer
视频帧的宽度和高度,不同的 USB 摄像头所支持的种类不同,所支持的种类数量也不相同,以本章的 USB 摄像头为例,可以查看摄像头支持的视频帧尺寸。
root@igkboard:~# lsusb -v
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 1
bmCapabilities 0x00
Still image unsupported
wWidth 640
wHeight 480
dwMinBitRate 283115520
dwMaxBitRate 283115520
dwMaxVideoFrameBufferSize 1179648
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 2
bmCapabilities 0x00
Still image unsupported
wWidth 352
wHeight 288
dwMinBitRate 377487360
dwMaxBitRate 377487360
dwMaxVideoFrameBufferSize 1572864
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 3
bmCapabilities 0x00
Still image unsupported
wWidth 424
wHeight 240
dwMinBitRate 629145600
dwMaxBitRate 629145600
dwMaxVideoFrameBufferSize 2621440
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 4
bmCapabilities 0x00
Still image unsupported
wWidth 640
wHeight 360
dwMinBitRate 230400000
dwMaxBitRate 230400000
dwMaxVideoFrameBufferSize 960000
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 5
bmCapabilities 0x00
Still image unsupported
wWidth 800
wHeight 480
dwMinBitRate 283115520
dwMaxBitRate 283115520
dwMaxVideoFrameBufferSize 1179648
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 6
bmCapabilities 0x00
Still image unsupported
wWidth 800
wHeight 600
dwMinBitRate 283115520
dwMaxBitRate 283115520
dwMaxVideoFrameBufferSize 1179648
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 7
bmCapabilities 0x00
Still image unsupported
wWidth 1024
wHeight 576
dwMinBitRate 283115520
dwMaxBitRate 283115520
dwMaxVideoFrameBufferSize 1179648
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
VideoStreaming Interface Descriptor:
bLength 30
bDescriptorType 36
bDescriptorSubtype 7 (FRAME_MJPEG)
bFrameIndex 8
bmCapabilities 0x00
Still image unsupported
wWidth 1280
wHeight 720
dwMinBitRate 283115520
dwMaxBitRate 283115520
dwMaxVideoFrameBufferSize 1179648
dwDefaultFrameInterval 333333
bFrameIntervalType 1
dwFrameInterval( 0) 333333
可以看到打印的信息很多,我们需要找到 VideoStreaming Interface Descriptor 就是用于 USB 数据传输的接口,然后查看 wWidth 和 wHeight 这两个属性就可以知道摄像头所支持的视频帧的尺寸,本章选用的是 640 * 480 的视频帧大小,所选用的视频帧大小最好是小于 LCD 的尺寸,这样才能将采集到的数据全部显示到 LCD 上,由于 LCD 的尺寸为 800 * 480 因此这里选用 640 * 480 的视频帧大小。
除此之外 rgb_buffer 是由于一般摄像头不支持直接采集 RGB 格式的数据,一般支持采集 YUV 格式的数据,而我们的 LCD 屏幕是 RGB 屏幕,因此需要把 YUV 数据通过算法转化成 RGB 格式的数据才能显示到屏幕上,因此 rgb_buffer 就是用于存储转换后的数据。
v4l2_buffer_unit 这个结构体用于之后的多个帧缓冲区出队与入队操作。
2.13.4.2 摄像头和 LCD 的初始化
首先插上 USB 摄像头,查看 /dev 下新增的 videoX 设备节点,此处新增的设备节点为 video1
root@igkboard:~# ls /dev/video*
/dev/video0 /dev/video1
/* 打开屏幕和摄像头的设备节点,并映射 lcd 的用户空间到内核空间 */
int init_camera_lcd()
{
/* 打开 /dev/vide1 设备节点 */
fd_camera = open("/dev/video1", O_RDWR | O_NONBLOCK, 0);
if(fd_camera < 0)
{
printf("%s : open camera error\n", __FUNCTION__);
return -1;
}
/* 打开 lcd 的设备节点 */
fd_lcd = open("/dev/fb0", O_RDWR);
if(fd_lcd < 0)
{
printf("%s : open lcd error\n", __FUNCTION__);
return -2;
}
/* 使用 mmap 映射一个和屏幕大小相同的缓冲区,缓冲区的首地址就是屏幕的基地址
* LCD_WIDTH*LCD_HEIGH*2 乘 2 的原因是屏幕支持的是 RGB565 类型的数据,每一个像素占 5+6+5 一共 16 个位也就是两个字节
*/
screen_base = mmap(NULL, LCD_WIDTH*LCD_HEIGH*2, PROT_READ|PROT_WRITE, MAP_SHARED, fd_lcd, 0);
if(NULL == screen_base)
{
printf("%s : framebuffer mmap error\n", __FUNCTION__);
return -3;
}
/* 为 rgb_buffer 开辟一段内存空间,空间大小就是帧数据的尺寸,乘 2 的原因和上面相同 */
rgb_buffer = malloc(FRAME_WIDTH*FRAME_HEIGH*2);
/* 在初始化时,将整个屏幕涂黑,以防 lcd 显示发生重叠 */
memset(screen_base, 0x0, LCD_WIDTH*LCD_HEIGH*2);
return 0;
}
2.13.4.3 查询设备的属性和功能
首先需要了解 v4l2_capability 结构体,这个结构体该结构体描述了视频采集设备的属性和功能。
struct v4l2_capability
{
__u8 driver[16]; // 驱动名字
__u8 card[32]; // 设备名字
__u8 bus_info[32]; // 设备在系统中的位置
__u32 version; // 驱动版本号
__u32 capabilities; // 设备支持的操作
__u32 reserved[4]; // 保留字段
};
其中,需要关注的是 capabilities 这一个成员,该成员描述了设备的功能,我们想要采集视频,所以需要的功能也就是视频采集,V4L2_CAP_VIDEO_CAPTURE 这个宏就表示该设备有视频采集的功能。
/* 查询设备功能属性 */
int v4l2_query_capability()
{
struct v4l2_capability cap;
int ret;
/* 使用 ioctl 将设备属性存入 v4l2_capability 结构体中 */
ret = ioctl(fd_camera, VIDIOC_QUERYCAP, &cap);
if(ret < 0)
{
printf("%s : VIDIOC_QUERYCAP error\n", __FUNCTION__);
return -1;
}
/* #define V4L2_CAP_VIDEO_CAPTURE 0x00000001
* 判断获取到设备的能力,也就是判断设备是否支持视频采集
* 判断最低位是否为 1,如果为 1,则说明该设备具有视频采集能力
*/
if ((V4L2_CAP_VIDEO_CAPTURE & cap.capabilities) == 0x00)
{
printf("%s : no capture device\n", __FUNCTION__);
return -2;
}
return 0;
}
除了 capabilities 之外的其他属性,都可以打印出来查看,但最重要的就是 capabilities 因此此处只判断了该成员。
2.13.4.4 查询设备支持的帧格式并设置帧格式
首先需要了解 v4l2_fmtdesc 结构体和 v4l2_format 结构体。
v4l2_fmtdesc 用于描述当前摄像头支持的帧数据格式信息。
struct v4l2_fmtdesc
{
__u32 index; // 要查询的格式序号,应用程序设置
enum v4l2_buf_type type; // 帧类型,应用程序设置
__u32 flags; // 是否为压缩格式
__u8 description[32]; // 格式名称
__u32 pixelformat; // 所支持的格式
__u32 reserved[4]; // 保留
};
其中,需要关注的成员是 index,type 和 pixelformat,index 我们在查询时需要提供编号,需要设置从 0 开始,type 则是帧类型,由于我们的设备是摄像头设备因此需要设置为 V4L2_BUF_TYPE_VIDEO_CAPTURE 才能开始查询,pixelformat 则是所支持的帧数据格式,通过 ioctl 查询可以获取。
v4l2_format 该结构体用于描述每帧图像的具体格式,包括帧类型以及图像的长宽等信息。
struct v4l2_format
{
enum v4l2_buf_type type; // 帧类型,应用程序设置
union fmt
{
struct v4l2_pix_format pix; // 视频设备使用
structv 4l2_window win;
struct v4l2_vbi_format vbi;
struct v4l2_sliced_vbi_format sliced;
__u8 raw_data[200];
};
};
这个结构体只需要关注联合体 fmt 中的 pix 成员,也就是视频帧数据格式。
v4l2_pix_format 结构体内容如下,只需要关注如下三项即可。
struct v4l2_pix_format {
__u32 width; // 视频帧的宽度
__u32 height; // 视频帧的高度
__u32 pixelformat; // 像素格式
...
}
/* 列举设备支持的数据格式 */
int v4l2_enum_format()
{
int ret = 0;
int found = 0;
struct v4l2_fmtdesc fmtdesc;
/* 枚举摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/* index 从 0 开始一直累加,直到找到支持类型为 V4L2_PIX_FMT_YUYV 格式为止
* 因为,我们需要摄像头支持 V4L2_PIX_FMT_YUYV 格式,才能用对应的算法把该格式转化为 RGB 格式
*/
while(found == 0 || ret == 0)
{
ret = ioctl(fd_camera, VIDIOC_ENUM_FMT, &fmtdesc);
if(fmtdesc.pixelformat = V4L2_PIX_FMT_YUYV)
{
found = 1;
}
fmtdesc.index++;
}
/* 如果找到,则打印支持 V4L2_PIX_FMT_YUYV 格式,否则失败 */
if(found != 1)
{
printf("%s : device don't support V4L2_PIX_FMT_YUYV\n", __FUNCTION__);
return -1;
}
else
{
printf("device support V4L2_PIX_FMT_YUYV\n");
}
return 0;
}
/* 获取帧数据格式 */
void v4l2_set_format()
{
int ret;
struct v4l2_format format;
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 用户需要传入的帧类型参数,表示需要设置的是摄像头帧格式
format.fmt.pix.width = FRAME_WIDTH; // 设置帧数据的宽度
format.fmt.pix.height = FRAME_HEIGH; // 设置帧数据的长度
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; // 设置帧数据的格式
ret = ioctl(fd_camera, VIDIOC_S_FMT, &format);
if(ret < 0)
{
printf("%s : VIDIOC_S_FMT error\n", __FUNCTION__);
}
}
/* 获取帧数据格式
* 主要获取帧数据的宽和高用于之后的设置
* 这一步并非必要步骤,只是用来确认 v4l2_set_format 是否成功
*/
void v4l2_get_format()
{
int ret;
struct v4l2_format format;
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd_camera, VIDIOC_G_FMT, &format);
if(ret < 0)
{
printf("%s : VIDIOC_G_FMT error\n", __FUNCTION__);
}
printf("width:%d height:%d\n", format.fmt.pix.width, format.fmt.pix.height);
}
2.13.4.5 申请和查询帧缓冲
在进行数据传输前,先要申请帧缓冲,申请帧缓冲就需要了解 v4l2_requestbuffers 结构体。
struct v4l2_requestbuffers
{
__u32 count; // 缓冲区内缓冲帧的数目
enum v4l2_buf_type type; // 缓冲帧数据类型
enum v4l2_memorymemory; // 区别是内存映射还是用户指针方式
__u32 reserved[2];
};
/* 申请帧数据缓冲区 */
void v4l2_require_buffer()
{
int ret;
struct v4l2_requestbuffers req;
req.count = 4; // 设置申请帧缓冲区的数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置申请帧缓冲区的类型
req.memory = V4L2_MEMORY_MMAP; // 设置帧缓冲为内存映射方式
ret = ioctl(fd_camera, VIDIOC_REQBUFS, &req);
if(ret < 0)
{
printf("%s : VIDIOC_REQBUFS error\n", __FUNCTION__);
}
}
查询帧缓冲,则需要了解 v4l2_buffer,申请 v4l2_requestbuffers 时,其实就相当于在内核中申请了数量为 count 的 v4l2_buffer,我们定义的 v4l2_buffer_unit 结构体,就是为了映射内核中的 v4l2_buffer。
/* 查询申请到的 buffer 信息,并映射到用户空间 */
int v4l2_query_buffer()
{
int ret;
int count;
struct v4l2_buffer buf;
/* 在用户空间分配 4 个大小为 v4l2_buffer_unit 结构体大小的 buffer */
buffer_unit = calloc(4, sizeof(*buffer_unit));
if(!buffer_unit)
{
printf("%s : calloc buffer_unit error\n", __FUNCTION__);
}
/* 获取所申请的 v4l2_requestbuffers 的信息
* 并将这些信息传给用户空间的 buffer_unit
* 然后将 buffer_unit 映射到内核中申请的 buffer 上去
* 映射过程是按照,申请 buffer 的编号一个一个映射
*/
for(count=0; count<4; count++)
{
memset(&buf,0,sizeof(buf));
/* 设置 v4l2_buffer 的类型,从而获取对应的 v4l2_buffer 的信息 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 说明要查找的 v4l2_buffer 为采集视频的类型
buf.memory = V4L2_BUF_FLAG_MAPPED; // 说明要查找的 v4l2_buffer 为内存映射类型
buf.index = count;
ret = ioctl(fd_camera, VIDIOC_QUERYBUF, &buf);
if(ret < 0)
{
printf("%s : VIDIOC_QUERYBUF error\n", __FUNCTION__);
return -1;
}
/* 查找到后,将 v4l2_buffer 的信息传给用户空间的 buffer_unit 数组,并在映射到内核空间 */
buffer_unit[count].length = buf.length;
buffer_unit[count].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_camera, buf.m.offset);
if (NULL == buffer_unit[count].start)
{
printf("%s : mmap buffer_unit error\n", __FUNCTION__);
return -2;
}
}
return 0;
}
2.13.4.6 开始视频采集并将帧缓冲入队
/* 开始视频数据采集,并将 buffer 入队 */
int v4l2_stream_on()
{
int i;
int ret;
enum v4l2_buf_type type;
struct v4l2_buffer buf;
/* 先将之前申请的 4 个 v4l2_buffer 帧缓冲加入缓冲队列 */
for(i=0; i<4; i++)
{
/* 和查询帧缓冲一样需要提供帧缓冲的属性 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
/* 将帧缓冲依次入队 */
ret=ioctl(fd_camera, VIDIOC_QBUF, &buf);
if(ret < 0)
{
printf("%s : VIDIOC_QBUF error\n", __FUNCTION__);
return -1;
}
}
/* 开始视频采集,这样视频数据就会传到帧缓冲中 */
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd_camera, VIDIOC_STREAMON, &type);
if(ret < 0)
{
printf("%s : VIDIOC_STREAMON error\n", __FUNCTION__);
return -2;
}
return 0;
}
2.13.4.7 帧缓冲出队并输出到屏幕
/* 用 select 监听摄像头的文件描述符
* 有数据到来,就从缓冲队列中取出数据
*/
int v4l2_select()
{
while(1)
{
int ret;
struct timeval tv;
fd_set fds;
/* 设置超时时间 */
tv.tv_sec = 2;
tv.tv_usec = 0;
/* 把摄像头的文件描述符加入需要监听的文件描述符中 */
FD_ZERO(&fds);
FD_SET(fd_camera, &fds);
/* 开始监听 */
ret = select(fd_camera+1, &fds, NULL, NULL, &tv);
if(ret < 0) // 传输出错
{
if(errno = EINTR)
{
printf("%s : select error\n", __FUNCTION__);
}
return -1;
}
if(0 == ret) // 传输超时
{
printf("%s : select timeout\n", __FUNCTION__);
return -2;
}
/* 没有出错也没有超时,就将帧缓冲从队列中取出,并将数据传到 lcd 上显示 */
v4l2_dequeue_buffer();
}
return 0;
}
/* 用 select 监听,一有数据就从帧缓冲队列中取出一个缓冲区数据,并显示到屏幕上 */
int v4l2_dequeue_buffer()
{
int ret;
int i;
struct v4l2_buffer buffer;
unsigned short *base;
unsigned short *start;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
/* 将帧缓冲从队列中取出 */
ret = ioctl(fd_camera, VIDIOC_DQBUF, &buffer);
if(ret < 0)
{
printf("%s : VIDIOC_STREAMON error\n", __FUNCTION__);
return -1;
}
/* 如果 buffer.index < 4 为假,则会打印
* 因为,之前只开辟了 4 个 buffer_uint,编号从 0 到 3
*/
assert(buffer.index < 4);
/* 将数据转换格式后,将数据存到 rgb_buffer,并放到屏幕申请 frame buffer 的空间中 */
yuv422_rgb565(buffer_unit[buffer.index].start, rgb_buffer, FRAME_WIDTH, FRAME_HEIGH);
/* 将数据一行一行的刷到屏幕上 */
for (i=0, base=screen_base, start=rgb_buffer; i<FRAME_HEIGH; i++)
{
memcpy(base, start, FRAME_WIDTH * 2); // RGB565 一个像素占 2 个字节
base += LCD_WIDTH; // lcd 显示指向下一行
start += FRAME_WIDTH; // 指向下一行数据
}
/* 刷到屏幕上后就将 rgb_buffer 清空 */
memset(rgb_buffer, 0x0, FRAME_WIDTH*FRAME_HEIGH*2);
/* 然后,让先前出队的 buffer 重新入队,一直循环 */
ioctl(fd_camera, VIDIOC_QBUF, &buffer);
return 0;
}
出队和入队过程如图所示,就是一个循环的过程,内核中的就是 v4l2_buffer 组成的队列。
除此之外,还有一个算法就 YUV422 格式的数据转换为 RGB565 的数据,算法内容如下,不做过多的解释。
/* yuv 格式转为 rgb 格式的算法
* 将 yuv422 格式的帧数据转换为 rgb565 格式,以显示在 lcd 屏幕上
*/
int yuv422_rgb565(unsigned char *yuv_buf, unsigned char *rgb_buf, unsigned int width, unsigned int height)
{
int yuvdata[4];
int rgbdata[3];
unsigned char *rgb_temp;
unsigned int i, j;
rgb_temp = rgb_buf;
for (i = 0; i < height * 2; i++)
{
for (j = 0; j < width; j += 4)
{
/* get Y0 U Y1 V */
yuvdata[Y0] = *(yuv_buf + i * width + j + 0);
yuvdata[U] = *(yuv_buf + i * width + j + 1);
yuvdata[Y1] = *(yuv_buf + i * width + j + 2);
yuvdata[V] = *(yuv_buf + i * width + j + 3);
/* the first pixel */
rgbdata[R] = yuvdata[Y0] + (yuvdata[V] - 128) + (((yuvdata[V] - 128) * 104 ) >> 8);
rgbdata[G] = yuvdata[Y0] - (((yuvdata[U] - 128) * 89) >> 8) - (((yuvdata[V] - 128) * 183) >> 8);
rgbdata[B] = yuvdata[Y0] + (yuvdata[U] - 128) + (((yuvdata[U] - 128) * 199) >> 8);
if (rgbdata[R] > 255) rgbdata[R] = 255;
if (rgbdata[R] < 0) rgbdata[R] = 0;
if (rgbdata[G] > 255) rgbdata[G] = 255;
if (rgbdata[G] < 0) rgbdata[G] = 0;
if (rgbdata[B] > 255) rgbdata[B] = 255;
if (rgbdata[B] < 0) rgbdata[B] = 0;
*(rgb_temp++) =( ((rgbdata[G]& 0x1C) << 3) | (rgbdata[B] >> 3) );
*(rgb_temp++) =( (rgbdata[R]& 0xF8) | (rgbdata[G] >> 5) );
/* the second pixel */
rgbdata[R] = yuvdata[Y1] + (yuvdata[V] - 128) + (((yuvdata[V] - 128) * 104 ) >> 8);
rgbdata[G] = yuvdata[Y1] - (((yuvdata[U] - 128) * 89) >> 8) - (((yuvdata[V] - 128) * 183) >> 8);
rgbdata[B] = yuvdata[Y1] + (yuvdata[U] - 128) + (((yuvdata[U] - 128) * 199) >> 8);
if (rgbdata[R] > 255) rgbdata[R] = 255;
if (rgbdata[R] < 0) rgbdata[R] = 0;
if (rgbdata[G] > 255) rgbdata[G] = 255;
if (rgbdata[G] < 0) rgbdata[G] = 0;
if (rgbdata[B] > 255) rgbdata[B] = 255;
if (rgbdata[B] < 0) rgbdata[B] = 0;
*(rgb_temp++) =( ((rgbdata[G]& 0x1C) << 3) | (rgbdata[B] >> 3) );
*(rgb_temp++) =( (rgbdata[R]& 0xF8) | (rgbdata[G] >> 5) );
}
}
return 0;
}
2.13.4.8 结束视频采集并取消映射
/* 结束视频数据采集 */
int v4l2_stream_off()
{
int ret;
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd_camera, VIDIOC_STREAMOFF, &type);
if(ret < 0)
{
printf("%s : VIDIOC_STREAMOFF error\n", __FUNCTION__);
return -1;
}
return 0;
}
/* 解除 buffer_unit 到内核中申请 buffer 的映射 */
void v4l2_unmmap()
{
int i;
int ret;
free(rgb_buffer);
for(i=0; i<4; i++)
{
ret = munmap(buffer_unit[i].start, buffer_unit[i].length);
if (ret < 0)
{
printf("%s : munmap error\n", __FUNCTION__);
}
}
}
2.13.4.9 完整代码
#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 Y0 0
#define U 1
#define Y1 2
#define V 3
#define R 0
#define G 1
#define B 2
/* 注,屏幕尺寸要大于帧的尺寸 */
#define FRAME_WIDTH 640
#define FRAME_HEIGH 480
#define LCD_WIDTH 800
#define LCD_HEIGH 480
/* 所申请的单个 buffer 结构体
* 包含 buffer 的起始地址和长度
*/
struct v4l2_buffer_unit {
void *start;
size_t length;
};
int fd_camera = -1;
int fd_lcd = -1;
void *screen_base = NULL;
struct v4l2_buffer_unit *buffer_unit = NULL;
void *rgb_buffer = NULL;
/* yuv 格式转为 rgb 格式的算法
* 将 yuv422 格式的帧数据转换为 rgb565 格式,以显示在 lcd 屏幕上
*/
int yuv422_rgb565(unsigned char *yuv_buf, unsigned char *rgb_buf, unsigned int width, unsigned int height)
{
int yuvdata[4];
int rgbdata[3];
unsigned char *rgb_temp;
unsigned int i, j;
rgb_temp = rgb_buf;
for (i = 0; i < height * 2; i++)
{
for (j = 0; j < width; j += 4)
{
/* get Y0 U Y1 V */
yuvdata[Y0] = *(yuv_buf + i * width + j + 0);
yuvdata[U] = *(yuv_buf + i * width + j + 1);
yuvdata[Y1] = *(yuv_buf + i * width + j + 2);
yuvdata[V] = *(yuv_buf + i * width + j + 3);
/* the first pixel */
rgbdata[R] = yuvdata[Y0] + (yuvdata[V] - 128) + (((yuvdata[V] - 128) * 104 ) >> 8);
rgbdata[G] = yuvdata[Y0] - (((yuvdata[U] - 128) * 89) >> 8) - (((yuvdata[V] - 128) * 183) >> 8);
rgbdata[B] = yuvdata[Y0] + (yuvdata[U] - 128) + (((yuvdata[U] - 128) * 199) >> 8);
if (rgbdata[R] > 255) rgbdata[R] = 255;
if (rgbdata[R] < 0) rgbdata[R] = 0;
if (rgbdata[G] > 255) rgbdata[G] = 255;
if (rgbdata[G] < 0) rgbdata[G] = 0;
if (rgbdata[B] > 255) rgbdata[B] = 255;
if (rgbdata[B] < 0) rgbdata[B] = 0;
*(rgb_temp++) =( ((rgbdata[G]& 0x1C) << 3) | (rgbdata[B] >> 3) );
*(rgb_temp++) =( (rgbdata[R]& 0xF8) | (rgbdata[G] >> 5) );
/* the second pixel */
rgbdata[R] = yuvdata[Y1] + (yuvdata[V] - 128) + (((yuvdata[V] - 128) * 104 ) >> 8);
rgbdata[G] = yuvdata[Y1] - (((yuvdata[U] - 128) * 89) >> 8) - (((yuvdata[V] - 128) * 183) >> 8);
rgbdata[B] = yuvdata[Y1] + (yuvdata[U] - 128) + (((yuvdata[U] - 128) * 199) >> 8);
if (rgbdata[R] > 255) rgbdata[R] = 255;
if (rgbdata[R] < 0) rgbdata[R] = 0;
if (rgbdata[G] > 255) rgbdata[G] = 255;
if (rgbdata[G] < 0) rgbdata[G] = 0;
if (rgbdata[B] > 255) rgbdata[B] = 255;
if (rgbdata[B] < 0) rgbdata[B] = 0;
*(rgb_temp++) =( ((rgbdata[G]& 0x1C) << 3) | (rgbdata[B] >> 3) );
*(rgb_temp++) =( (rgbdata[R]& 0xF8) | (rgbdata[G] >> 5) );
}
}
return 0;
}
/* 打开屏幕和摄像头的设备节点,并映射 lcd 的用户空间到内核空间 */
int init_camera_lcd()
{
fd_camera = open("/dev/video1", O_RDWR | O_NONBLOCK, 0);
if(fd_camera < 0)
{
printf("%s : open camera error\n", __FUNCTION__);
return -1;
}
fd_lcd = open("/dev/fb0", O_RDWR);
if(fd_lcd < 0)
{
printf("%s : open lcd error\n", __FUNCTION__);
return -2;
}
screen_base = mmap(NULL, LCD_WIDTH*LCD_HEIGH*2, PROT_READ|PROT_WRITE, MAP_SHARED, fd_lcd, 0);
if(NULL == screen_base)
{
printf("%s : framebuffer mmap error\n", __FUNCTION__);
return -3;
}
rgb_buffer = malloc(FRAME_WIDTH*FRAME_HEIGH*2);
memset(screen_base, 0x0, LCD_WIDTH*LCD_HEIGH*2);
return 0;
}
/* 查询设备功能属性 */
int v4l2_query_capability()
{
struct v4l2_capability cap;
int ret;
ret = ioctl(fd_camera, VIDIOC_QUERYCAP, &cap);
if(ret < 0)
{
printf("%s : VIDIOC_QUERYCAP error\n", __FUNCTION__);
return -1;
}
/* #define V4L2_CAP_VIDEO_CAPTURE 0x00000001
* 获取到设备的能力
* 判断最低位是否为 1,如果为 1,则说明该设备具有视频采集能力
*/
if ((V4L2_CAP_VIDEO_CAPTURE & cap.capabilities) == 0x00)
{
printf("%s : no capture device\n", __FUNCTION__);
return -2;
}
return 0;
}
/* 列举设备支持的数据格式 */
int v4l2_enum_format()
{
int ret = 0;
int found = 0;
struct v4l2_fmtdesc fmtdesc;
/* 枚举摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while(found == 0 || ret == 0)
{
ret = ioctl(fd_camera, VIDIOC_ENUM_FMT, &fmtdesc);
if(fmtdesc.pixelformat = V4L2_PIX_FMT_YUYV)
{
found = 1;
}
fmtdesc.index++;
}
if(found != 1)
{
printf("%s : device don't support V4L2_PIX_FMT_YUYV\n", __FUNCTION__);
return -1;
}
else
{
printf("device support V4L2_PIX_FMT_YUYV\n");
}
return 0;
}
/* 获取帧数据格式
* 主要获取帧数据的宽和高用于之后的设置
*/
void v4l2_get_format()
{
int ret;
struct v4l2_format format;
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd_camera, VIDIOC_G_FMT, &format);
if(ret < 0)
{
printf("%s : VIDIOC_G_FMT error\n", __FUNCTION__);
}
printf("width:%d height:%d\n", format.fmt.pix.width, format.fmt.pix.height);
}
/* 获取帧数据格式 */
void v4l2_set_format()
{
int ret;
struct v4l2_format format;
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
format.fmt.pix.width = FRAME_WIDTH;
format.fmt.pix.height = FRAME_HEIGH;
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
format.fmt.pix.field = V4L2_FIELD_INTERLACED;
ret = ioctl(fd_camera, VIDIOC_S_FMT, &format);
if(ret < 0)
{
printf("%s : VIDIOC_S_FMT error\n", __FUNCTION__);
}
}
/* 申请帧数据缓冲区 */
void v4l2_require_buffer()
{
int ret;
struct v4l2_requestbuffers req;
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd_camera, VIDIOC_REQBUFS, &req);
if(ret < 0)
{
printf("%s : VIDIOC_REQBUFS error\n", __FUNCTION__);
}
}
/* 查询申请到的 buffer 信息,并映射到用户空间 */
int v4l2_query_buffer()
{
int ret;
int count;
struct v4l2_buffer buf;
/* 分配 4 个大小为 v4l2_buffer_unit 结构体大小的 buffer */
buffer_unit = calloc(4, sizeof(*buffer_unit));
if(!buffer_unit)
{
printf("%s : calloc buffer_unit error\n", __FUNCTION__);
}
/* 获取之前申请的 v4l2_requestbuffers 的信息
* 并将这些信息传给用户空间的 buffer_unit
* 然后将 buffer_unit 映射到内核中申请的 buffer 上去
* 映射过程是按照,申请 buffer 的编号一个一个映射
*/
for(count=0; count<4; count++)
{
memset(&buf,0,sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_BUF_FLAG_MAPPED;
buf.index = count;
ret = ioctl(fd_camera, VIDIOC_QUERYBUF, &buf);
if(ret < 0)
{
printf("%s : VIDIOC_QUERYBUF error\n", __FUNCTION__);
return -1;
}
buffer_unit[count].length = buf.length;
buffer_unit[count].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_camera, buf.m.offset);
if (NULL == buffer_unit[count].start)
{
printf("%s : mmap buffer_unit error\n", __FUNCTION__);
return -2;
}
}
return 0;
}
/* 开始视频数据采集,并将 buffer 入队 */
int v4l2_stream_on()
{
int i;
int ret;
enum v4l2_buf_type type;
struct v4l2_buffer buffer;
for(i=0; i<4; i++)
{
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
buffer.index = i;
ret=ioctl(fd_camera, VIDIOC_QBUF, &buffer);
if(ret < 0)
{
printf("%s : VIDIOC_QBUF error\n", __FUNCTION__);
return -1;
}
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd_camera, VIDIOC_STREAMON, &type);
if(ret < 0)
{
printf("%s : VIDIOC_STREAMON error\n", __FUNCTION__);
return -2;
}
return 0;
}
/* 从帧缓冲队列中取出一个缓冲区数据,并显示到屏幕上 */
int v4l2_dequeue_buffer()
{
int ret;
int i;
struct v4l2_buffer buffer;
unsigned short *base;
unsigned short *start;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
ret = ioctl(fd_camera, VIDIOC_DQBUF, &buffer);
if(ret < 0)
{
printf("%s : VIDIOC_STREAMON error\n", __FUNCTION__);
return -1;
}
/* 如果 buffer.index < 4 为假,则会打印
* 因为,之前只开辟了 4 个 buffer_uint,编号从 0 到 3
*/
assert(buffer.index < 4);
/* 将数据转换格式后,放到屏幕申请 frame buffer 的空间中 */
yuv422_rgb565(buffer_unit[buffer.index].start, rgb_buffer, FRAME_WIDTH, FRAME_HEIGH);
for (i=0, base=screen_base, start=rgb_buffer; i<FRAME_HEIGH; i++)
{
memcpy(base, start, FRAME_WIDTH * 2); // RGB565 一个像素占 2 个字节
base += LCD_WIDTH; // lcd 显示指向下一行
start += FRAME_WIDTH; // 指向下一行数据
}
memset(rgb_buffer, 0x0, FRAME_WIDTH*FRAME_HEIGH*2);
ioctl(fd_camera, VIDIOC_QBUF, &buffer);
return 0;
}
/* 用 select 监听数据
* 有数据到来,就从缓冲队列中取出数据
*/
int v4l2_select()
{
while(1)
{
int ret;
struct timeval tv;
fd_set fds;
tv.tv_sec = 2;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET(fd_camera, &fds);
ret = select(fd_camera+1, &fds, NULL, NULL, &tv);
if(ret < 0)
{
if(errno = EINTR)
{
printf("%s : select error\n", __FUNCTION__);
}
return -1;
}
if(0 == ret)
{
printf("%s : select timeout\n", __FUNCTION__);
return -2;
}
v4l2_dequeue_buffer();
}
return 0;
}
/* 结束视频数据采集 */
int v4l2_stream_off()
{
int ret;
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ret = ioctl(fd_camera, VIDIOC_STREAMOFF, &type);
if(ret < 0)
{
printf("%s : VIDIOC_STREAMOFF error\n", __FUNCTION__);
return -1;
}
return 0;
}
/* 解除 buffer_unit 到内核中申请 buffer 的映射 */
void v4l2_unmmap()
{
int i;
int ret;
free(rgb_buffer);
for(i=0; i<4; i++)
{
ret = munmap(buffer_unit[i].start, buffer_unit[i].length);
if (ret < 0)
{
printf("%s : munmap error\n", __FUNCTION__);
}
}
}
int main(int argc, char **argv)
{
init_camera_lcd();
/* 获取设备的能力和帧数据格式,并设置数据格式 */
v4l2_query_capability();
v4l2_enum_format();
v4l2_set_format();
v4l2_get_format();
/* 在内核中申请帧缓冲区
* 并获取缓冲区信息,用以将用户空间的内存映射到内核空间
*/
v4l2_require_buffer();
v4l2_query_buffer();
/* 开始视频数据采集
* 使用 select 监听数据,有数据则从帧缓冲队列取出数据
* 采集结束后,停止采集,并解除映射,关闭文件描述符
*/
v4l2_stream_on();
v4l2_select();
v4l2_stream_off();
v4l2_unmmap();
close(fd_camera);
close(fd_lcd);
return 0;
}
2.13.5 实现现象
2.13.5.1 交叉编译并将可执行文件传到开发板
guowenxue@ubuntu:~$ ls
video2lcd.c
guowenxue@ubuntu:~$ arm-linux-gnueabihf-gcc video2lcd.c -o video2lcd
guowenxue@ubuntu:~$ ls
video2lcd video2lcd.c
将 video2lcd 传到开发板上,并添加可执行权限。
root@igkboard:~# chmod +x video2lcd
2.13.5.2 运行效果
root@igkboard:~# ./video2lcd
device support V4L2_PIX_FMT_YUYV
width:640 height:480