天天看點

《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

開發環境:

主機: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)      
《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照
《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照
Device Drivers --->
[*]usb support
        [*] usb announce new device      
《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

【注】選項框内為星号*表示開啟并編譯進核心,空白表示不開啟,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攝像頭就會有相應的裝置。

《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

如果插入多個攝像頭,裝置名字尾數字依次增加,如: video1 video2 video3。

5.2 V4L2拍照應用實作

5.2.1 V4L2拍照原理

在Linux下,所有外設都被看成一種特殊的檔案,也就是一切皆檔案,Linux中所有的外設均可像通路普通檔案一樣對其進行讀寫操作。

V4L2在include/linux/videodev.h檔案中定義了一些重要的資料結構,在采集圖像的過程中,就是通過對這些資料的操作來獲得最終的圖像資料。Linux系統V4L2的能力可在Linux核心編譯階段配置,預設情況下都有此開發接口。

在Linux中V4L2拍照的調用過程如下圖所示。

《Linux作業系統 - RK3568開發筆記》第5章 基于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.編譯

《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

值得注意的是,上面的IP位址是開發闆的IP,密碼是開發闆的登陸密碼。

2.測試

接下來在RK3568中運作拍照程式。

《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

筆者一次拍兩張,當然也可以連續拍很多,在代碼中可以修改。最後将照片傳到主機檢視。

《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

這樣就是可以檢視前面拍得的圖檔了。

值得注意的是,上面的IP位址是主機的IP,密碼是主機開發闆的登陸密碼。

我們在Windows中檢視拍的照片。

《Linux作業系統 - RK3568開發筆記》第5章 基于V4L2拍照

照片大小在代碼中可以調整,可以通過參數傳進去。

繼續閱讀