一、前言
本文中的程序适用于Ubuntu或者ARM linux平台上外接USB摄像头,将摄像头插入USB口后在/dev目录下会出现名为video*的设备。需要注意的是,电脑自带的USB设备也可以接入Ubuntu系统中,并且在/dev目录下也会出现名为video*的设备,但是本文的例程不适用于电脑自带的摄像头。
二、代码
1 /** 2 * filename: camera.c 3 * author: Suzkfly 4 * date: 2021-08-15 5 * platform: S3C2416或Ubuntu 6 * 程序运行成功后会在当前目录下生成pic.jpg文件,如果在Ubuntu上运行,需要超级用权限。 7 */ 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <getopt.h> 12 #include <fcntl.h> 13 #include <unistd.h> 14 #include <errno.h> 15 #include <malloc.h> 16 #include <sys/stat.h> 17 #include <sys/types.h> 18 #include <sys/time.h> 19 #include <sys/mman.h> 20 #include <sys/ioctl.h> 21 #include <asm/types.h> 22 #include <linux/videodev2.h> 23 #include <time.h> 24 25 /** <\brief 定义摄像头设备路径 */ 26 #define CAMERA_DEV "/dev/video0" 27 28 /** <\brief 定义清零数据的宏 */ 29 #define CLEAR(x) memset (&(x), 0, sizeof (x)) 30 31 /** <\brief 定义保存摄像头数据的结构体 */ 32 struct buffer { 33 void * start; /* 起始地址 */ 34 size_t length; /* 数据长度 */ 35 }; 36 37 #define BUFFER_CNT 4 /* 缓冲帧的个数 */ 38 39 /** 40 * \brief 初始化v4l2 41 * 42 * \param[in] p_dev_name:摄像头所处路径 43 * \param[out] p_fd:得到的摄像头设备文件描述符 44 * \param[out] pp_buffers:得到的缓冲帧的地址,注意这是一个二级指针,该变量保存的数据是一个地址 45 * 46 * \retval 成功返回0,失败返回-1 47 */ 48 int v4l2_init(const char *p_dev_name, int *p_fd, struct buffer **pp_buffers) 49 { 50 int fd = -1; 51 struct v4l2_capability cap; 52 struct v4l2_format fmt; 53 enum v4l2_buf_type type; 54 struct v4l2_requestbuffers req; 55 time_t t; 56 struct tm *ptm; 57 unsigned int i; 58 struct v4l2_buffer buf; //驱动中的一帧 59 60 /* 打开摄像头设备 */ 61 if ((fd = open (p_dev_name, O_RDWR | O_NONBLOCK, 0)) < 0) { 62 perror("fail to open\n"); 63 return -1; 64 } 65 66 /* ioctl是一个强大的函数,它的功能取决于传入的第2个参数,这里第2个参数传入 67 VIDIOC_QUERYCAP表示获取设备属性,获取到的属性会保存在第3个参数中,第3个 68 参数是一个struct v4l2_capability类型的结构体指针,struct v4l2_capability 69 结构定义如下: 70 struct v4l2_capability 71 { 72 u8 driver[16]; // 驱动名字 73 u8 card[32]; // 设备名字 74 u8 bus_info[32]; // 设备在系统中的位置 75 u32 version; // 驱动版本号 76 u32 capabilities; // 设备支持的操作 77 u32 reserved[4]; // 保留字段 78 }; 79 80 capabilities是能力的意思,其中支持的能力如下所示: 81 #V4L2_CAP_VIDEO_CAPTURE 0x00000001 //是视频采集设备 82 #V4L2_CAP_VIDEO_OUTPUT 0x00000002 //是视频输出设备 83 #V4L2_CAP_VIDEO_OVERLAY 0x00000004 //可以做视频叠加 84 #V4L2_CAP_VBI_CAPTURE 0x00000010 //是一个原始的 VBI 捕获设备 85 #V4L2_CAP_VBI_OUTPUT 0x00000020 //是一个原始的 VBI 输出设备 86 #V4L2_CAP_SLICED_VBI_CAPTURE 0x00000040 //是一个切片(sliced)的 VBI 捕获设备 87 #V4L2_CAP_SLICED_VBI_OUTPUT 0x00000080 //是一个切片(sliced)的 VBI 输出设备 88 #V4L2_CAP_RDS_CAPTURE 0x00000100 //RDS数据采集 89 #V4L2_CAP_VIDEO_OUTPUT_OVERLAY 0x00000200 //可以做视频输出叠加 90 #V4L2_CAP_HW_FREQ_SEEK 0x00000400 //可以做硬件寻频 91 92 #V4L2_CAP_TUNER 0x00010000 //有一个协调器 93 #V4L2_CAP_AUDIO 0x00020000 //支持音频 94 #V4L2_CAP_RADIO 0x00040000 //是无线电设备 95 96 #V4L2_CAP_READWRITE 0x01000000 //读/写系统调用 97 #V4L2_CAP_ASYNCIO 0x02000000 // 异步 I/O 98 #V4L2_CAP_STREAMING 0x04000000 // streaming I/O ioctls 99 */ 100 101 /* 获取设备参数 */ 102 if (0 != ioctl(fd, VIDIOC_QUERYCAP, &cap)) { 103 perror("fail to ioctl\n"); 104 return -1; 105 } 106 #if 0 107 printf("cap.driver = %s\n", cap.driver); 108 printf("cap.card = %s\n", cap.card); 109 printf("cap.bus_info = %s\n", cap.bus_info); 110 printf("cap.version = %u\n", cap.version); 111 printf("cap.capabilities = %#08x\n", cap.capabilities); 112 #endif 113 /* struct v4l2_format结构定义如下: 114 struct v4l2_format { 115 enum v4l2_buf_type type; 116 union { 117 struct v4l2_pix_format pix; // V4L2_BUF_TYPE_VIDEO_CAPTURE 118 struct v4l2_window win; // V4L2_BUF_TYPE_VIDEO_OVERLAY 119 struct v4l2_vbi_format vbi; // V4L2_BUF_TYPE_VBI_CAPTURE 120 struct v4l2_sliced_vbi_format sliced; // V4L2_BUF_TYPE_SLICED_VBI_CAPTURE 121 __u8 raw_data[200]; // user-defined 122 } fmt; 123 }; 124 可以看出struct v4l2_format结构中有1个枚举类型的type和一个共用体fmt,数据保存 125 在fmt中,fmt共用体中具体使用哪个结构由type的值决定,本程序中需要设置图像格式, 126 因此type的值设为V4L2_BUF_TYPE_VIDEO_CAPTURE,使用fmt中的pix成员。 127 struct v4l2_pix_format结构定义如下: 128 struct v4l2_pix_format { 129 __u32 width; //图像宽度 130 __u32 height; //图像高度 131 __u32 pixelformat; //像素格式 132 enum v4l2_field field; //场格式,见下文 133 __u32 bytesperline; //表明缓冲区中有多少字节用于表示图像中一行像素的所有像素值。 134 //由于一个像素可能有多个字节表示,所以 bytesperline 可能是字段 width 值的若干倍 135 __u32 sizeimage; //图像大小 136 enum v4l2_colorspace colorspace; //色彩空间,其中V4L2_COLORSPACE_JPEG = 7 137 __u32 priv; //私有数据,取决于像素格式 138 }; 139 pixelformat表示像素格式,可以设置的格式有很多,比如 140 #V4L2_PIX_FMT_RGB565 141 #V4L2_PIX_FMT_RGB24 142 #V4L2_PIX_FMT_YUV565 143 #V4L2_PIX_FMT_JPEG 144 #V4L2_PIX_FMT_MPEG 145 等。 146 147 enum v4l2_field { 148 V4L2_FIELD_ANY = 0, //驱动程序可以选择无、顶部、底部、隔行扫描... 149 V4L2_FIELD_NONE = 1, //此设备没有场 150 V4L2_FIELD_TOP = 2, //只有顶场 151 V4L2_FIELD_BOTTOM = 3, //只有底场 152 V4L2_FIELD_INTERLACED = 4, //两个场交错 153 V4L2_FIELD_SEQ_TB = 5, //两个场顺序合并到一个缓冲区中,从上到下顺序 154 V4L2_FIELD_SEQ_BT = 6, //同上,加自上而下的顺序 155 V4L2_FIELD_ALTERNATE = 7, //两个场交替进入单独的缓冲区 156 V4L2_FIELD_INTERLACED_TB = 8, //两个场交错,顶场在前,顶场先传输 157 V4L2_FIELD_INTERLACED_BT = 9, //两个场交错,前场先传输,后场先传输 158 }; */ 159 160 /* 设置图像格式 */ 161 CLEAR(fmt); 162 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 163 fmt.fmt.pix.width = 800; 164 fmt.fmt.pix.height = 600; 165 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_JPEG; 166 fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; 167 if (0 != ioctl(fd, VIDIOC_S_FMT, &fmt)) { 168 perror("fail to ioctl\n"); 169 return -1; 170 } 171 172 /* 设置结果可能与写入的数值不一样,可以打印出来看一下 */ 173 #if 0 174 printf("fmt.type = %d\n", fmt.type); 175 printf("fmt.fmt.pix.width = %d\n", fmt.fmt.pix.width); 176 printf("fmt.fmt.pix.height = %d\n", fmt.fmt.pix.height); 177 printf("fmt.fmt.pix.pixelformat = %c%c%c%c\n", 178 (fmt.fmt.pix.pixelformat >> 0) & 0xFF, 179 (fmt.fmt.pix.pixelformat >> 8) & 0xFF, 180 (fmt.fmt.pix.pixelformat >> 16) & 0xFF, 181 (fmt.fmt.pix.pixelformat >> 24) & 0xFF 182 ); 183 printf("fmt.fmt.pix.field = %d\n", fmt.fmt.pix.field); 184 printf("fmt.fmt.pix.bytesperline = %d\n", fmt.fmt.pix.bytesperline); 185 printf("fmt.fmt.pix.sizeimage = %d\n", fmt.fmt.pix.sizeimage); 186 printf("fmt.fmt.pix.colorspace = %d\n", fmt.fmt.pix.colorspace); 187 printf("fmt.fmt.pix.priv = %d\n", fmt.fmt.pix.priv); 188 printf("V4L2_BUF_TYPE_VIDEO_CAPTURE = %d\n", V4L2_BUF_TYPE_VIDEO_CAPTURE); 189 #endif 190 191 //file_length = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height; //计算图片大小 192 193 /* struct v4l2_requestbuffers结构定义如下: 194 struct v4l2_requestbuffers { 195 __u32 count; // 缓冲区内缓冲帧的数目 196 enum v4l2_buf_type type; // 缓冲帧数据格式 197 enum v4l2_memory memory; // 区别是内存映射还是用户指针方式, 198 V4L2_MEMORY_MMAP 为内存映射, V4L2_MEMORY_USERPTR 为用户指针 199 __u32 reserved[2]; 200 }; */ 201 202 /* 向设备申请缓冲区 */ 203 CLEAR (req); 204 req.count = BUFFER_CNT; /* \todo 为什么要申请4个缓冲帧 */ 205 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 206 req.memory = V4L2_MEMORY_MMAP; 207 ioctl(fd, VIDIOC_REQBUFS, &req); 208 209 if (req.count < 2) { 210 printf("Insufficient buffer memory\n"); /* 缓冲帧数量不够 */ 211 } 212 213 /* 内存中建立对应空间 */ 214 *pp_buffers = (struct buffer *)calloc(req.count, sizeof(struct buffer)); 215 if (*pp_buffers == NULL) { 216 perror("calloc failed\n"); 217 return -1; 218 } 219 220 /* 得到缓冲帧的起始地址和长度 */ 221 for (i = 0; i < req.count; i++) { 222 /* struct v4l2_buffer { 223 __u32 index; //缓存编号 224 enum v4l2_buf_type type; //视频捕获模式 225 __u32 bytesused; //缓存已使用空间大小 226 __u32 flags; //缓存当前状态,可取下列值 227 #V4L2_BUF_FLAG_MAPPED 0x0001 //当前缓存已经映射 228 #V4L2_BUF_FLAG_QUEUED 0x0002 //缓存可以采集数据 229 #V4L2_BUF_FLAG_DONE 0x0004 //缓存可以提取数据 230 #V4L2_BUF_FLAG_KEYFRAME 0x0008 //Image is a keyframe (I-frame) 231 #V4L2_BUF_FLAG_PFRAME 0x0010 //Image is a P-frame 232 #V4L2_BUF_FLAG_BFRAME 0x0020 //Image is a B-frame 233 #V4L2_BUF_FLAG_TIMECODE 0x0100 //timecode field is valid 234 #V4L2_BUF_FLAG_INPUT 0x0200 //input field is valid 235 enum v4l2_field field; 236 struct timeval timestamp; //获取第一个字节时的系统时间 237 struct v4l2_timecode timecode; 238 __u32 sequence; //队列中的序号 239 240 // memory location 241 enum v4l2_memory memory; //IO 方式,被应用程序设置 242 union { 243 __u32 offset; //缓冲帧地址偏移量,只对MMAP 有效 244 unsigned long userptr; 245 } m; 246 __u32 length; //缓冲帧长度 247 __u32 input; 248 __u32 reserved; 249 }; 250 */ 251 252 /* 获取缓冲帧的地址,长度 */ 253 CLEAR (buf); 254 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 255 buf.memory = V4L2_MEMORY_MMAP; 256 buf.index = i; 257 if (-1 == ioctl (fd, VIDIOC_QUERYBUF, &buf)) { 258 printf ("VIDIOC_QUERYBUF error\n"); 259 } 260 261 (*pp_buffers)[i].length = buf.length; 262 /* 通过mmap建立映射关系 */ 263 (*pp_buffers)[i].start = mmap (NULL, 264 buf.length, 265 PROT_READ | PROT_WRITE, 266 MAP_SHARED, 267 fd, 268 buf.m.offset); /* 被映射内容的偏移量 */ 269 if (MAP_FAILED == (*pp_buffers)[i].start) { /* MAP_FAILED其实是((void *) -1),mmap失败时返回 */ 270 printf ("mmap failed\n"); 271 } 272 } 273 274 for (i = 0; i < req.count; i++) { 275 CLEAR (buf); 276 277 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 278 buf.memory = V4L2_MEMORY_MMAP; 279 buf.index = i; 280 281 /* 把帧放入队列 */ 282 if (0 != ioctl (fd, VIDIOC_QBUF, &buf)) { 283 printf ("VIDIOC_QBUF failed\n"); 284 } 285 } 286 287 /* 开始捕捉图像数据 */ 288 type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 289 if (-1 == ioctl (fd, VIDIOC_STREAMON, &type)) { 290 printf ("VIDIOC_STREAMON failed\n"); 291 } 292 293 *p_fd = fd; 294 295 return 0; 296 } 297 298 /** 299 * \brief v4l2去除初始化 300 * 301 * \param[in] camera_fd:打开的摄像头设备文件描述符 302 * \param[in] p_buffers:缓冲帧地址 303 * 304 * \retval 成功返回0,失败返回-1 305 */ 306 void v4l2_deinit(int camera_fd, struct buffer *p_buffers) 307 { 308 int i; 309 310 /* 解除映射关系 */ 311 for (i = 0; i < BUFFER_CNT; i++) { 312 if (-1 == munmap (p_buffers[i].start, p_buffers[i].length)) { 313 printf ("munmap error\n"); 314 } 315 } 316 317 /* 释放申请的内存 */ 318 free(p_buffers); 319 320 /* 关闭文件 */ 321 close(camera_fd); 322 } 323 324 /** 325 * \brief 读取一帧数据 326 * 327 * \param[in] camera_fd:打开摄像头设备得到的文件描述符 328 * \param[in] p_buffers:缓冲帧地址 329 * \param[in] filename:保存的jpg图片的路径 330 * 331 * \retval 成功返回0,失败返回-1 332 */ 333 static int __read_frame (int camera_fd, struct buffer *p_buffers, char *filename) 334 { 335 FILE *file_fd = NULL; 336 struct v4l2_buffer buf; 337 unsigned int i; 338 339 /* 以"w+"方式打开文件,如果文件不存在则会创建文件,但前提是文件所在目录对于 340 其他用户有写权限,可以用umask命令查看和修改掩码 */ 341 if((file_fd = fopen(filename, "w+")) == NULL) { 342 perror("fail to fopen\n"); 343 return -1; 344 } 345 346 /* 从队列中取出帧 */ 347 CLEAR(buf); 348 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 349 buf.memory = V4L2_MEMORY_MMAP; 350 ioctl(camera_fd, VIDIOC_DQBUF, &buf); 351 352 fwrite(p_buffers[buf.index].start, p_buffers[buf.index].length, 1, file_fd); //将其写入文件中 353 354 ioctl(camera_fd, VIDIOC_QBUF, &buf); /* 把帧放入队列 */ 355 356 fclose(file_fd); 357 358 return 0; 359 } 360 361 /** 362 * \brief 得到一张jpg图片 363 * 364 * \param[in] camera_fd:打开摄像头设备得到的文件描述符 365 * \param[in] p_buffers:缓冲帧地址 366 * \param[in] filename:保存的jpg图片的路径 367 * 368 * \retval 成功返回0,失败返回-1 369 */ 370 int camera_jpg (int camera_fd, struct buffer *p_buffers, char *filename) 371 { 372 unsigned int i; 373 fd_set fds; 374 struct timeval tv; 375 int r; 376 377 /* 设定超时时间 */ 378 tv.tv_sec = 2; 379 tv.tv_usec = 0; 380 381 while (1) { //这一段涉及到异步IO 382 FD_ZERO (&fds); //将指定的文件描述符集清空 383 FD_SET (camera_fd, &fds); //在文件描述符集合中增加一个新的文件描述符 384 385 /* select函数执行成功返回集合内包含的文件描述符数量,失败返回-1,超时返回0 */ 386 r = select (camera_fd + 1, &fds, NULL, NULL, &tv);//判断是否可读(即摄像头是否准备好),tv是定时 387 388 if (-1 == r) { 389 if (EINTR == errno) { /* 系统调用被中断 */ 390 continue; 391 } 392 printf ("select err\n"); 393 return -1; 394 } else if (0 == r) { /* 超时 */ 395 fprintf (stderr, "select timeout\n"); 396 return -1; 397 } 398 399 /* 如果可读,__read_frame()函数,并跳出循环 */ 400 return __read_frame(camera_fd, p_buffers, filename); 401 } 402 403 return 0; 404 } 405 406 #if 1 407 /* 将摄像头拍到的照片保存成pic.jpg文件 */ 408 int main(int argc, const char *argv[]) 409 { 410 int camera_fd = 0; /* 摄像头设备的文件描述符 */ 411 struct buffer * p_buffers = NULL; /* 用于保存缓冲帧地址和长度 */ 412 413 if (0 != v4l2_init(CAMERA_DEV, &camera_fd, &p_buffers)) { 414 perror("v4l2_init failed\n"); 415 return 0; 416 } 417 camera_jpg(camera_fd, p_buffers, "pic.jpg"); 418 419 v4l2_deinit(camera_fd, p_buffers); 420 421 return 0; 422 } 423 424 #else 425 /* 将摄像头拍摄的照片实时显示出来,需要将jpg.c一同编译,要加-ljpeg选项 */ 426 extern int framebuffer_init (void); 427 extern int show_jpg(unsigned int x, unsigned int y, const char *name); 428 429 int main(int argc, const char *argv[]) 430 { 431 int camera_fd = 0; /* 摄像头设备的文件描述符 */ 432 struct buffer * p_buffers = NULL; /* 用于保存缓冲帧地址和长度 */ 433 434 framebuffer_init(); 435 if (0 != v4l2_init(CAMERA_DEV, &camera_fd, &p_buffers)) { 436 perror("v4l2_init failed\n"); 437 return 0; 438 } 439 440 while (1) { 441 camera_jpg(camera_fd, p_buffers, "pic.jpg"); 442 show_jpg(0, 0, "pic.jpg"); 443 } 444 445 v4l2_deinit(camera_fd, p_buffers); 446 447 return 0; 448 } 449 #endif
程序运行后,在当前路径下会出现名为pic.jpg的文件,这就是用摄像头拍摄到的照片。如果使用代码最后面的main函数,可以将拍摄到的照片直接在屏幕上显示出来,但是要支持jpg图片的显示。通过framebuffer显示jpg图片可以参考我这篇博客:framebuffer显示jpg图片。但是用这种方式显示出来的图片是一卡一卡的,应该是处理器性能不够强大,再者,摄像头本来就支持视频显示的,本文中的例程是取出摄像头拍摄到的画面,保存成jpg文件,再将图片显示出来,这个过程做了很多多余的事情,因此显示出来是一卡一卡的。
三、问题解答
如果使用nfs挂载运行该程序,那么很容易出现下面两个问题。
1. 运行程序时,如果pic.jpg不存在,那么在调用fopen打开pic.jpg时会打开失败,并且报错“Permission denied”。
既然报“Permission denied”,那显然就是权限问题。(有些人可能会觉得是文件不存在导致的,但是本程序使用fopen打开文件,传入的标志是“w+”,这个标志在文件不存在时会自己创建文件,而且如果是文件不存在,那么会直接报“No such file or directory”,而不是“Permission denied”。)
要解决这个问题,有2种方法
1)在Ubuntu上用touch命令手动创建pic.jpg文件。
(因为nfs目录下的文件是Ubuntu的,因此只能在Ubutnu上创建,在开发板终端没有权限创建文件)。创建完成之后可以用ls -l命令查看文件权限,此时pic.jpg的权限为“rw-rw-r--”,为什么是这个权限呢,首先,文件权限和umask有关,可以在终端上直接输入umask命令,可以看到终端打印出来“0002”,这表示其他用户是没有写权限的。另外使用touch命令创建的文件都是没有执行权限的,因此创建出来的文件权限为“rw-rw-r--”。所以创建出来的文件还需要使用chmod 0666 pic.jpg命令,让其他用户有写权限。这样再在开发板端运行程序就没问题了。但是这种方法缺点很明显,就是如果改变了文件名,那么又要进行一遍这样的操作,或者如果需要一次性拍很多张照片,这种方法就不适用了。
2)通过程序自动创建文件。
前面已经说了,如果pic.jpg文件是不存在的,那么程序运行时报没有权限的错误,既然是创建文件时需要权限,就应该看看希望被创建的文件所处的文件夹有没有写权限,如果没有的话,使用chmod加入写权限。之后运行程序,发现文件可以自动创建。
但是这又带来一个新的问题,就是文件的所有者变成了nobody,组变成了nogroup,如下图。
暂时不知道这种情况会带来什么后果,但是这个是可以解决的,解决办法如下:
在Ubuntu终端中,输入sudu vi /etc/exports
文件的最后一行写的是nfs共享目录的路径也权限等信息,在其中加入一项:no_root_squash,修改过后如下图:
注意各项中间用逗号分隔,且没有空格。保存文件,使用命令:sudo /etc/init.d/nfs-kernel-server restart重启nfs服务。
之后再在开发板上运行程序,这时创建出来的文件的用户和组都变成了root。
2. 程序运行一段时间就死了,经过统计,问题会有下面几种:
1)程序运行时报错:
然后程序自动退出,但系统没有死掉,并且可以再次用a.out运行程序,但程序运行一段时间后还是会死掉。
2)程序运行时报错,如下图:
并且系统死掉,需要重启,但问题又来了,重启也不一定成功,可能需要多次断电上电才能成功重启。
最气的是,重烧系统之后这个问题就没再出现了。
虽然问题没再出现了,但是没有找到出现问题的根本原因和100%能解决问题的方法。在这里先作几点猜测:
1. 网络连接问题。既然在开发板上运行系统不会死机,而使用nfs挂载就会死机,那有理由怀疑和网络连接有关,但是这肯定不是问题发生的根本原因,只是由某个问题导致了两种不同的现象;
2. 存储器问题。可能是Nand Flash出问题,导致内核下载进去时出现了某些错误内容。这样推测的理由是出现问题之后系统变得难以启动,而且重烧系统之后这个问题就不再出现了。