開發環境:
主機:Ubuntu 18.04
開發闆:OK3568-C開發闆
Video for Linuxtwo(Video4Linux2)簡稱V4L2,是V4L的改進版。V4L2是linux作業系統下用于采集圖檔、視訊和音頻資料的API接口,配合适當的視訊采集裝置和相應的驅動程式,可以實作圖檔、視訊、音頻等的采集。在遠端會議、可視電話、視訊監控系統和嵌入式多媒體終端中都有廣泛的應用。本文将基于V4L2使用usb攝像頭(UVC)拍照。
5.1啟用linux核心對usb攝像頭的支援
1.配置核心
進入核心目錄,配置linux核心
$ make ARCH=arm64 menuconfig
2.啟用攝像頭支援
最後一步時根據自己需要進行選擇攝像頭配置。
- Linux 4.19
Device Drivers --->
<*>Multimedia support --->
[*] Cameras/video grabbers support
[*] Media usb adapters---->
<*> USB video class (uvc)
Device Drivers --->
[*]usb support
[*] usb announce new device
【注】選項框内為星号*表示開啟并編譯進核心,空白表示不開啟,M表示開啟并編譯為子產品
3.編譯
修改完後,可以開始編譯linux源碼 執行以下指令:
退出後将修改内容儲存到配置檔案:
$ make ARCH=arm64 savedefconfig
$ mv defconfig arch/arm64/configs/OK3568-C-linux_defconfig
回到 SDK 根目錄進行編譯:
# 選擇闆型配置檔案
$./build.sh BoardConfig-ok3568.mk
# 編譯核心
./build.sh kernel
這樣就把 UVC 編譯進核心,當然也可把 UVC 編譯成子產品。
當插入UVC攝像頭就會有相應的裝置。
如果插入多個攝像頭,裝置名字尾數字依次增加,如: video1 video2 video3。
5.2 V4L2拍照應用實作
5.2.1 V4L2拍照原理
在Linux下,所有外設都被看成一種特殊的檔案,也就是一切皆檔案,Linux中所有的外設均可像通路普通檔案一樣對其進行讀寫操作。
V4L2在include/linux/videodev.h檔案中定義了一些重要的資料結構,在采集圖像的過程中,就是通過對這些資料的操作來獲得最終的圖像資料。Linux系統V4L2的能力可在Linux核心編譯階段配置,預設情況下都有此開發接口。
在Linux中V4L2拍照的調用過程如下圖所示。
V4L2支援兩種方式來采集圖像:記憶體映射方式(mmap)和直接讀取方式(read)。前者一般用于連續視訊資料的采集,後者常用于靜态圖檔資料的采集。
主要分為五個步驟:
首先,打開裝置檔案,參數初始化,通過V4L2接口設定圖像的采集視窗、采集的點陣大小和格式。
其次,申請若幹圖像采集的幀緩沖區,便于應用程式讀取/處理視訊資料。
第三,将申請到的幀緩沖區在資料采集輸入隊列排隊,并啟動圖檔采集。
第四,驅動開始圖像資料的采集,應用程式從資料采集輸出隊列取出幀緩沖區,處理完後,将幀緩沖區重新放入資料采集輸入隊列,循環往複采集連續的資料;
第五,停止資料采集。
完整代碼如下:
【usb_camera.c】
/**
******************************************************************************
* @file usb_camera.c
* @author BruceOu
* @version V1.0
* @date 2022-04-24
* @blog https://blog.bruceou.cn/
* @Official Accounts 嵌入式實驗樓
* @brief USB CAMERA
******************************************************************************
*/
/**Includes*********************************************************************/
#include "usb_camera.h"
#define DEBUG
/**【全局變量聲明】*************************************************************/
buffer *user_buf = NULL;
static unsigned int n_buffer = 0;
static unsigned long file_length;
char picture_name[20] ="rk_picture";
int num = 0;
/**
* @brief 打開攝像頭裝置函數
* @param None
* @retval fd 攝像頭裝置
*/
int open_camer_device(char * videoDev)
{
int fd;
/*1.打開裝置檔案。*/
if((fd = open(videoDev,O_RDWR | O_NONBLOCK)) < 0)
{
perror("Fail to open");
pthread_exit(NULL);
}
return fd;
}
/**
* @brief 初始化視訊裝置函數
* @param fd 攝像頭裝置
* @retval
*/
int init_camer_device(int fd)
{
struct v4l2_fmtdesc fmt;
struct v4l2_capability cap;
struct v4l2_format stream_fmt;
int ret;
/*2.取得裝置的capability,查詢視訊裝置驅動的功能
比如是否具有視訊輸入,或者音頻輸入輸出等。VIDIOC_QUERYCAP,struct v4l2_capability*/
ret = ioctl(fd,VIDIOC_QUERYCAP,&cap);
if(ret < 0)
{
perror("FAIL to ioctl VIDIOC_QUERYCAP");
exit(EXIT_FAILURE);
}
//判斷是否是一個視訊捕捉裝置
if(!(cap.capabilities & V4L2_BUF_TYPE_VIDEO_CAPTURE))
{
perror("The Current device is not a video capture device\n");
exit(EXIT_FAILURE);
}
//判斷是否支援視訊流形式
if(!(cap.capabilities & V4L2_CAP_STREAMING))
{
perror("The Current device does not support streaming i/o\n");
exit(EXIT_FAILURE);
}
/*3.設定視訊的制式和幀格式,制式包括PAL,NTSC,幀的格式個包括寬度和高度等。*/
memset(&fmt,0,sizeof(fmt));
fmt.index = 0;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while((ret = ioctl(fd,VIDIOC_ENUM_FMT,&fmt)) == 0)
{
fmt.index ++ ;
#ifdef DEBUG
printf("{pixelformat = %c%c%c%c},description = '%s'\n",
fmt.pixelformat & 0xff,(fmt.pixelformat >> 8)&0xff,
(fmt.pixelformat >> 16) & 0xff,(fmt.pixelformat >> 24)&0xff,
fmt.description);
#endif
}
//設定攝像頭采集資料格式,如設定采集資料的
//長,寬,圖像格式(JPEG,YUYV,MJPEG等格式)
stream_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;//資料流類型,必須永遠是V4L2_BUF_TYPE_VIDEO_CAPTURE
stream_fmt.fmt.pix.width = 680;//寬,必須是16的倍數
stream_fmt.fmt.pix.height = 480;//高,必須是16的倍數
stream_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;//視訊資料存儲類型//V4L2_PIX_FMT_YUYV;//V4L2_PIX_FMT_YVU420;//V4L2_PIX_FMT_YUYV;
stream_fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
//設定目前驅動的頻捕獲格式
if(-1 == ioctl(fd,VIDIOC_S_FMT,&stream_fmt))
{
perror("Fail to ioctl");
exit(EXIT_FAILURE);
}
//計算圖檔大小
file_length = stream_fmt.fmt.pix.bytesperline * stream_fmt.fmt.pix.height;
//初始化視訊采集方式(mmap)
init_mmap(fd);
return 0;
}
/**
* @brief 初始化視訊采集方式(mmap)
* @param fd 攝像頭裝置
* @retval
*/
int init_mmap(int fd)
{
int i = 0;
struct v4l2_requestbuffers reqbuf;
/*4.向驅動申請幀緩沖,一般不超過5個。struct v4l2_requestbuffers*/
bzero(&reqbuf,sizeof(reqbuf));
reqbuf.count = 4;//緩存數量,也就是說在緩存隊列裡保持多少張照片
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;//或V4L2_MEMORY_USERPTR
//申請視訊緩沖區(這個緩沖區位于核心空間,需要通過mmap映射)
//這一步操作可能會修改reqbuf.count的值,修改為實際成功申請緩沖區個數
if(-1 == ioctl(fd,VIDIOC_REQBUFS,&reqbuf))
{
perror("Fail to ioctl 'VIDIOC_REQBUFS'");
exit(EXIT_FAILURE);
}
n_buffer = reqbuf.count;
#ifdef DEBUG
printf("n_buffer = %d\n",n_buffer);
#endif
user_buf = calloc(reqbuf.count,sizeof(*user_buf));//記憶體中建立對應空間
if(user_buf == NULL)
{
fprintf(stderr,"Out of memory\n");
exit(EXIT_FAILURE);
}
/*5.将申請到的幀緩沖映射到使用者空間,這樣就可以直接操作采集到的幀了,
而不必去複制。mmap*/
for(i = 0; i < n_buffer; i ++)
{
struct v4l2_buffer buf;//驅動中的一幀
bzero(&buf,sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
//查詢申請到核心緩沖區的資訊
if(-1 == ioctl(fd,VIDIOC_QUERYBUF,&buf)) //映射使用者空間
{
perror("Fail to ioctl : VIDIOC_QUERYBUF");
exit(EXIT_FAILURE);
}
user_buf[i].length = buf.length;
user_buf[i].start =
mmap(
NULL,/*start anywhere*/
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,buf.m.offset//通過mmap建立映射關系,傳回映射區的起始位址
);
if(MAP_FAILED == user_buf[i].start)
{
perror("Fail to mmap");
exit(EXIT_FAILURE);
}
}
return 0;
}
int start_capturing(int fd)
{
unsigned int i;
enum v4l2_buf_type type;
/*6.将申請到的幀緩沖全部入隊列,以便存放采集到的資料.VIDIOC_QBUF,struct v4l2_buffer*/
for(i = 0;i < n_buffer;i ++)
{
struct v4l2_buffer buf;
bzero(&buf,sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
//把資料從緩存中讀取出來
if(-1 == ioctl(fd,VIDIOC_QBUF,&buf))//申請到的緩沖進入列隊
{
perror("Fail to ioctl 'VIDIOC_QBUF'");
exit(EXIT_FAILURE);
}
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/*7.開始視訊的采集。VIDIOC_STREAMON*/
if(-1 == ioctl(fd,VIDIOC_STREAMON,&type)) //開始捕捉圖像資料
{
perror("Fail to ioctl 'VIDIOC_STREAMON'");
exit(EXIT_FAILURE);
}
return 0;
}
int mainloop(int fd)
{
int count = 2;
/*8.循環采集圖檔。*/
while(count-- > 0)
{
for(;;)
{
fd_set fds;
struct timeval tv;
int r;
FD_ZERO(&fds);//将指定的檔案描述符集清空
FD_SET(fd,&fds);//在檔案描述符集合中增加新的檔案描述符
/*Timeout*/
tv.tv_sec = 2;
tv.tv_usec = 0;
r = select(fd + 1,&fds,NULL,NULL,&tv);//判斷是否可讀(即攝像頭是否準備好),tv是定時
if(-1 == r)
{
if(EINTR == errno)
continue;
perror("Fail to select");
exit(EXIT_FAILURE);
}
if(0 == r)
{
fprintf(stderr,"select Timeout\n");
exit(EXIT_FAILURE);
}
if(read_frame(fd))//如果可讀,執行read_frame ()函數,并跳出循環
break;
}
}
return 0;
}
//将采集好的資料放到檔案中
int process_image(void *addr,int length)
{
FILE *fp;
char name[20];
sprintf(name,"%s%d.jpg",picture_name,num ++);
if((fp = fopen(name,"w")) == NULL)
{
perror("Fail to fopen");
exit(EXIT_FAILURE);
}
fwrite(addr,length,1,fp);
usleep(500);
fclose(fp);
return 0;
}
int read_frame(int fd)
{
struct v4l2_buffer buf;
unsigned int i;
bzero(&buf,sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
/*9.出隊列以取得已采集資料的幀緩沖,取得原始采集資料。VIDIOC_DQBUF*/
if(-1 == ioctl(fd,VIDIOC_DQBUF,&buf))
{
perror("Fail to ioctl 'VIDIOC_DQBUF'");
exit(EXIT_FAILURE);
}
assert(buf.index < n_buffer);
{
#ifdef DEBUG
printf ("buf.index dq is %d,\n",buf.index);
#endif
}
//讀取程序空間的資料到一個檔案中
process_image(user_buf[buf.index].start,user_buf[buf.index].length);
/*10.将緩沖重新入隊列尾,這樣可以循環采集。VIDIOC_QBUF*/
if(-1 == ioctl(fd,VIDIOC_QBUF,&buf))//把資料從緩存中讀取出來
{
perror("Fail to ioctl 'VIDIOC_QBUF'");
exit(EXIT_FAILURE);
}
return 1;
}
void stop_capturing(int fd)
{
enum v4l2_buf_type type;
/*11.停止視訊的采集。VIDIOC_STREAMOFF*/
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if(-1 == ioctl(fd,VIDIOC_STREAMOFF,&type))
{
perror("Fail to ioctl 'VIDIOC_STREAMOFF'");
exit(EXIT_FAILURE);
}
return;
}
void uninit_camer_device()
{
unsigned int i;
for(i = 0;i < n_buffer;i ++)
{
if(-1 == munmap(user_buf[i].start,user_buf[i].length))
{
exit(EXIT_FAILURE);
}
}
free(user_buf);
return;
}
void close_camer_device(int fd)
{
if(-1 == close(fd))
{
perror("Fail to close fd");
exit(EXIT_FAILURE);
}
return;
}
/**
* @brief 攝像頭拍照函數
* @param void
* @retval Nono
*/
int main(int argc, char* argv[])
{
int camera_fd;
if(argc == 2 )
{
camera_fd = open_camer_device(argv[1]);
init_camer_device(camera_fd);
start_capturing(camera_fd);
num = 0;
mainloop(camera_fd);
stop_capturing(camera_fd);
uninit_camer_device(camera_fd);
close_camer_device(camera_fd);
printf("Camera get pic success!\n");
}
else
{
printf("Please input video device!\n");
}
return 0;
}
【usb_camera.h】
#ifndef _USB_CAMERA_H_
#define _USB_CAMERA_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
#include <getopt.h>
#include <fcntl.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 VIDEO_DEV "/dev/video9"//攝像頭裝置名
typedef struct _buffer
{
void *start;
size_t length;
}buffer;
int open_camer_device(char * videoDev);
int init_mmap(int fd);
int init_camer_device(int fd);
int start_capturing(int fd);
int process_image(void *addr,int length);
int read_frame(int fd);
int mainloop(int fd);
void stop_capturing(int fd);
void uninit_camer_device();
void close_camer_device(int fd);
void camera_get_image(void);
#endif
【Makefile】
ARCH=arm64
CROSS=aarch64-linux-gnu-
all: usb_camera
sudo scp usb_camera [email protected]:/root
usb_camera:usb_camera.c
$(CROSS)gcc -o usb_camera usb_camera.c
$(CROSS)strip usb_camera
clean:
@rm -vf usb_camera *.o *~
值得注意的是,Makefile中通過scp将編譯好的程式拷貝到開發闆,需要根據修改相應開發闆的IP位址。當然也可通過其他方式拷貝程式。
5.2.2編譯測試
接下來就是編譯下載下傳測試了。
1.編譯
值得注意的是,上面的IP位址是開發闆的IP,密碼是開發闆的登陸密碼。
2.測試
接下來在RK3568中運作拍照程式。
筆者一次拍兩張,當然也可以連續拍很多,在代碼中可以修改。最後将照片傳到主機檢視。
這樣就是可以檢視前面拍得的圖檔了。
值得注意的是,上面的IP位址是主機的IP,密碼是主機開發闆的登陸密碼。
我們在Windows中檢視拍的照片。
照片大小在代碼中可以調整,可以通過參數傳進去。