找回密码
 注册
关于网站域名变更的通知
查看: 381|回复: 1
打印 上一主题 下一主题

video4linux(v4l)使用摄像头的实例基础教程与体会

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2020-4-14 09:42 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
" R9 z- L9 `; f8 {1 U1 I! P' f
本人要做一下在linux系统中视频的相关工作比如采集和传输。由于本人是菜鸟一个,所以是需要上网搜一搜看大家都是如何做的,当然开始都是理不出一个头绪,但是很多文章都提到了video4linux(v4l),所以我觉得工作的展开可以先从这里开始,。看了网上的一些文章,其中比较重要的也是比较知名的吧,有戴小鼠写的《基于Video4Linux 的USB 摄像头图像采集实现》,有陈俊宏写的《video stream 初探》的一系列共六篇文章,也找了一些英文的资料,看到过《video4linux programming》但是这篇文章偏重于视频设备在linux中的驱动实现,所以对像我这种低端的只是使用v4l相关系统调用的人来说有些帮助但帮助不大,《Video4Linux Kernel API Reference》详细介绍了v4l中各个重要的结构体的作用。另外顺着陈俊宏的文章,找到了一个叫EffecTV的软件,其中的有关v4l的源码部分也很值得一看,在后的文章里也会介绍。翻看了网上的很多文章,多半是使用陈俊宏介绍的相关代码,或者是EffecTV中的,大家都是这么用而且也都用的不错。: w. L: D" a. m, X9 V7 B9 P6 n

# M! q" b/ C  W5 h0 |8 W2 D9 D我写这个文章一是想为自己的毕业论文积累些素材,二是我想可能会给今后想要了解v4l相关使用知识的人提供一个学习的路线,因为上一段中提到的几篇文章无论谁读起来肯定都会对他有很大的帮助,三是希望我也写篇文章给想学习的人一点帮助,哪怕只有一点点。
; C( _! m+ B0 v
% ~, {0 H9 W) T/ K      文章就分成三个大部分吧:8 o( i/ E1 {% P

( S# z% w7 N# [4 X第一个部分介绍一些v4l的基本概念和基本方法,利用系统API完成一系列函数以方便后续应用程序的开发和使用。/ q" a4 P" t( p+ o& V2 Q5 K$ ]* {

8 Y' ?  n4 S- V& c* S7 Z. {9 O3 y第二个部分一些说明如何使用v4l,用一个示例程序说明。( {/ P! J+ M" w) A5 I3 d2 e: `' u
2 O+ n+ \& P; u& L) T0 }
第三个部分想简单说一说对获取和处理图像相关问题的思路。在这一章可能会谈一谈我的一些理解和体会。其实网络上的资料很多,我只是稍微整理一下而已。
. i9 b( Q0 u" [  {; m+ U
0 P" h4 n; Q/ X2 N   # t! o7 X+ k. P) C1 q0 M- e2 \/ C3 y
我的感觉linux内核和驱动开发的那些程序员很厉害因为他们留给我们一个很容易使用的接口而使底层复杂的工作对我们很透明,读过上述我提到的文章后会觉得使用v4l是相对容易的(我希望如果有人读了我的文章也会有这种感觉),相对复杂的是采集到图像数据后我们应该怎么办,我想这也可能是很多人当然也包括我所不是特别清晰和明确的。所以我想在第三个部分里做一些对采集到图像数据后相关问题的探讨,当然我的水平有限,请您指出文中的错误方法和对概念的错误理解,我非常愿意共同学习和进步。9 o( u: @( i0 p! N- h4 \) ?8 L

8 t7 Y% e) m  F3 a " |1 v. Q) g8 W, x4 L! a# J
+ [' V) j, S/ n. d( g% Y5 J  F
1.video4linux基础相关
+ q$ k/ k7 l( X8 _2 ?6 |; v' v" y4 D( B
1.1 v4l的介绍与一些基础知识的介绍
3 c2 O5 r7 w" k# p, {  a) S8 F8 V5 }3 g/ z) N8 V2 J
I.首先说明一下video4linux(v4l)。
- p; K7 {5 W, a' M1 C6 O$ k
( K- W0 O$ j- x+ s' k) E它是一些视频系统,视频软件,音频软件的基础,经常使用在需要采集图像的场合,如视频监控,webcam,可视电话,经常应用在embedded linux中是linux嵌入式开发中经常使用的系统接口。它是linux内核提供给用户空间的编程接口,各种的视频和音频设备开发相应的驱动程序后,就可以通过v4l提供的系统API来控制视频和音频设备,也就是说v4l分为两层,底层为音视频设备在内核中的驱动,上层为系统提供的API,而对于我们来说需要的就是使用这些系统的API。
  [7 Q9 p, ]1 p! L" J7 x& j5 }4 F* ~6 k4 v( J
II.Linux系统中的文件操作  D3 P& \5 e' s' P2 ~! F
5 q& X5 ?. v' z
有关Linux系统中的文件操作不属于本文的内容。但是还是要了解相关系统调用的作用和使用方法。其中包括open(),read(),close(),ioctl(),mmap()。详细的使用不作说明。在Linux系统中各种设备(当然包括视频设备)也都是用文件的形式来使用的。他们存在与dev目录下,所以本质上说,在Linux中各种外设的使用(如果它们已经正确的被驱动),与文件操作本质上是没有什么区别的。
6 j/ E! [4 K1 Q2 [2 T/ v% t/ r
1.2 建立一套简单的v4l函数库8 e' ?/ g" ?* W, ]3 z! u" l
4 S4 e* b: _4 u) X& T3 y% g. D
       这一节将一边介绍v4l的使用方法,一边建立一套简单的函数,应该说是一套很基本的函数,它完成很基本的够能但足够展示如何使用v4l。这些函数可以用来被其他程序使用,封装基本的v4l功能。本文只介绍一些和摄像头相关的编程方法,并且是最基础和最简单的,所以一些内容并没有介绍,一些与其他视频设备(如视频采集卡)和音频设备有关的内容也没有介绍,本人也不是很理解这方面的内容。4 \8 v9 q4 @4 O& K7 _8 P

, x$ ]1 l8 W% p' C5 }1 N! v- `       这里先给出接下来将要开发出来函数的一个总览。: r) u# Q/ K6 v

; B' h8 y" G; r7 v& F5 U4 M0 h! J相关结构体和函数的定义我们就放到一个名为v4l.h的文件中,相关函数的编写就放在一个名为v4l.c的文件中把。
/ K$ K) ^4 D# y; N0 A  I4 {0 q! B1 T- M0 o" n
对于这个函数库共有如下的定义(也就是大体v4l.h中的内容):
) A9 ]) d" }! t" c& H4 P7 m: E& R& @4 _* K1 o  y

0 @- E" d. D! h) s5 r) L2 J   1: #ifndef _V4L_H_9 E$ l9 e& d2 J+ O$ F
   2: #define _V4L_H_+ q9 M) ~/ K6 {/ `# {! r& f) y
   3: #include <sys/types.h>
& F2 |& y. K8 a7 w& ?+ `& b   4: #include <linux/videodev.h> //使用v4l必须包含的头文件1 a/ E. ]: w) \2 f& I  ^7 Z
这个头文件可以在/usr/include/linux下找到,里面包含了对v4l各种结构的定义,以及各种ioctl的使用方法,所以在下文中有关v4l的相关结构体并不做详细的介绍,可以参看此文件就会得到你想要的内容。6 m+ }- m8 Z! G/ H  {3 U

& _9 `% Y% d7 p% X# y; u+ d+ p  H下面是定义的结构体,和相关函数,突然给出这么多的代码很唐突,不过随着一点点解释条理就会很清晰了。5 p& V) d7 y) Q' N

9 {6 n% r* f/ B; ~2 `8 _* t2 l- l" R# `7 y7 |8 Q
   1: struct _v4l_struct
9 y1 ]6 q  m1 [4 @   2:  
4 \. Z% `, t: M8 j; I* J6 ]) T   3:       {
; R" y) \  u* i  E% C4 w$ @   4:  9 k. B! ]1 d: o8 z6 i5 u
   5:          int fd;//保存打开视频文件的设备描述符. M4 S, ~: Z. M' `4 o! H
   6:  
7 v+ ?9 j1 @1 D% s0 R7 g* a6 a   7:          struct video_capability capability;//该结构及下面的结构为v4l所定义可在上述头文件中找到9 P% C. u+ ~, i
   8:  7 B4 U8 R; X- V9 w
   9:          struct video_picture picture;/ t8 i$ M, o8 ^. k: d, ?! h& }
  10:  
6 E. W1 P8 |0 p+ R  11:          struct video_mmap mmap;
# P+ T8 V: g- S  O- n  12:  
6 I% m0 W) h7 |9 I  13:          struct video_mbuf mbuf;
4 g6 ]9 T, p  }  F' l. I7 g  14:    g  b( {9 p, L) I
  15:          unsigned char *map;//用于指向图像数据的指针
2 L/ k; J* \1 V& u7 N1 J+ z8 Y  16:  ' t; f; q, _  r) g8 q1 `
  17:                int frame_current;
0 l: H8 W( }6 ^) l5 d  }7 _  18:  
) h: _+ o( f" N/ y  19:          int frame_using[VIDEO_MAXFRAME];//这两个变量用于双缓冲在后面介绍。
( `" j/ T/ O+ w2 x. J' B' H* S  20:  5 `2 u# J$ s0 d& A; |7 m
  21:       };
" q' x/ I" ~1 ^" u  22:  
, f7 l1 m& O6 b$ p  23: typedef struct _v4l_struct v4l_device;. e" Z( ~! \" u2 @) N6 D, K2 l$ D3 s
//上面的定义的结构体,有的文中章有定义channel的变量,但对于摄像头来说设置这个变量意义不大通常只有一个channel,本文不是为了写出一个大而全且成熟的函数库,只是为了介绍如何使用v4l,再加上本人水平也有限,能够给读者一个路线我就很知足了,所以并没有设置这个变量同时与channel相关的函数也没有给出。
- \  T" U. U: _
* H: h4 l' r+ e, f! u2 E   1: extern int v4l_open(char *, v4l_device *);
: R) B/ q( Z; C: ]5 O2 N   2:  7 D. s, w# [8 @  B
   3: extern int v4l_close(v4l_device *);
. Z, z% B  `9 ]; ~' Z   4:  
$ M, t6 N% X, G  a   5: extern int v4l_get_capability(v4l_device *);2 g+ t2 p2 c' Y% L! V% f
   6:  ' R1 y# u0 F* t
   7: extern int v4l_get_picture(v4l_device *);
* G% V' ]( \/ F) g' x5 L2 Y6 _   8:  5 _1 J$ t1 T+ J9 n/ ?  D9 [
   9: extern int v4l_get_mbuf(v4l_device *);
' S& f; u" W7 y! z  10:  ' [5 M3 ]/ p1 ~  B
  11: extern int v4l_set_picture(v4l_device *, int, int, int, int, int,);
$ n+ a) |, H9 z% x& H- e  12:  
; V+ e& K  |8 }  13: extern int v4l_grab_picture(v4l_device *, unsigned int);
' ^; m* w* l5 J' P6 n; g3 N4 m  14:  
. n8 R& Y0 \, |& L9 G( @3 z9 m  15: extern int v4l_mmap_init(v4l_device *);/ g* w. t6 V# a6 f' u- r
  16:  
$ S7 R7 W% a8 y* Z: w  17: extern int v4l_grab_init(v4l_device *, int, int);
1 G8 Y0 K7 S( X; U  Y6 a/ n. c+ E& `  18:  3 I9 i) F6 B1 k+ f
  19: extern int v4l_grab_frame(v4l_device *, int);& G9 i7 `+ Z9 v4 Q
  20:  
0 l: w6 a: F, Y- C2 t* V1 ?, x  21: extern int v4l_grab_sync(v4l_device *);1 @  }0 C7 u8 i: t8 e
1 Y; ~9 H( B; C5 j' q' s
上述函数会在下文中逐渐完成,功能也会逐渐介绍,虽然现在看起来没什么感觉只能从函数名上依稀体会它的功能,或许看起来很烦,不过看完下文就会好了。% I7 B, V% z+ G# }3 ^4 F

/ s% A& _$ V/ n% M1 R: }
/ W. h% z* T( `* d9 K前面已经说过使用v4l视频编程的流程和对文件操作并没有什么本质的不同,大概的流程如下:
  V" h: c0 S$ `, I  [+ ^
  |6 u7 ?/ N) o- ~' ^) T       1.打开视频设备(通常是/dev/video0)4 J) a+ `( m2 i' `0 j& e

) {, C' e" \! i8 O/ v" Y, K7 o       2.获得设备信息。
' p) K0 \' h7 r4 `: r
  ~# [; V: B) H       3.根据需要更改设备的相关设置。# D# `$ Q7 ]' s5 o4 ?

3 M9 E% f) h: a       4.获得采集到的图像数据(在这里v4l提供了两种方式,直接通过打开的设备读取数据,使用mmap内存映射的方式获取数据)。! P) l1 [' X. I) \
8 U- h* u8 [7 m# j: z: ^8 w
       5.对采集到的数据进行操作(如显示到屏幕,图像处理,存储成图片文件)。
) p  S8 z4 c! }6 {  o. a% p  w
/ V/ l" Y0 k& L2 G6 e$ i: G       6.关闭视频设备。
0 c( Y8 U% P5 J( y
& x, }; V* f+ ]: D7 R知道了流程之后,我们就需要根据流程完成相应的函数。) a' J) H3 U7 t; w8 |
0 F8 B* t' u2 k' J  T, D
6 A: e* b( V, o. `& h" i
. {3 U9 G' n7 o9 ]
那么我们首先完成第1步打开视频设备,需要完成int v4l_open(char *, v4l_device *);
4 C& W, Y6 J+ E3 U6 y% k( }% v- p9 Q9 t! n/ D1 N9 \: v5 m) o
具体的函数如下! Z- C! X& A2 \
: M4 m7 ^# h, E3 B2 T+ s
' I5 m: k2 d' D7 \3 ?! |# P. r
   1: #define DEFAULT_DEVICE “/dev/video0”8 j$ \# Z( A( N: j* I
   2:  # A, F3 c; Q7 C% D4 A% g) C
   3: int v4l_open(char *dev , v4l_device *vd)  w$ {& y$ M- L, b( K0 n9 q
   4:  5 m8 T) j; C9 n- {0 t
   5: {
$ }/ O% O9 n1 i& a+ q   6:  
- k1 h/ K" P1 Y/ m6 r7 h/ T' W' g4 r   7:        if(!dev)dev= DEFAULT_DEVICE;
6 [# v" s' ?- i. V4 h* [: B   8:  
* Z4 C# Y& Y$ b+ e# y" x" [   9:        if((vd-fd=open(dev,O_RDWR))<0){perror(“v4l_open:”);return -1;}
% P! g1 {+ _5 Y  n) ]) l  10:  
. O1 ^% g, q& m  11:        if(v4l_get_capability(vd))return -1;
6 [7 a9 l) l+ o- n  12:  
( J* g* p7 F. N0 n" v. O  13:        if(v4l_get_picture(vd))return -1;//这两个函数就是即将要完成的获取设备信息的函数* p7 f$ A2 b& z
  14:  
6 d+ q- F  L% D4 p) t& a% B- B, @  15:        return 02 R, E6 n8 J, p. p$ D; f) N8 p
  16:  
" [# ?- ?5 U4 H+ w# y  17: }0 l  [1 M6 Q8 C! f, r- k9 g. _! v
同样对于第6步也十分简单,就是int v4l_close(v4l_device *);的作用。* K5 d5 k6 e1 O  {: p/ [
6 i3 M& U; e- M8 L# a, \# b! I
函数如下:
1 x) e9 |/ L& l
% u  W$ \) K) O  a2 O
0 [/ d$ q7 T( ^. B7 Z# f   1: int v4l_close(v4l_device *vd); S9 \" k- t) y2 s
   2:  ( H# L0 M0 L* W2 N- Y7 U
   3: {close(vd->fd);return 0;}5 F4 i! C9 _( g& B+ k# A5 D- n
现在我们完成第2步中获得设备信息的任务,下面先给出函数在对函数作出相应的说明。: Y3 A/ w  O  ^0 ~# p( ~8 j

* h! F  I9 j) J2 p; R3 {- l6 w
" C! _  ]  ^* D* L   1: int v4l_get_capability(v4l_device *vd)
* a* n9 Y2 T  u. P; ?  o" k$ D   2:  2 Y% `- l7 b5 u' t; l
   3: {   ! y. d* C& q- t' z1 P2 S
   4:  
" F- L8 L9 F. X# X# ]! b   5:    if (ioctl(vd->fd, VIDIOCGCAP, &(vd->capability)) < 0) {   * N/ _; ?2 D5 j+ [1 o$ g
   6:  
! V0 S- y8 y3 L9 y   7:       perror("v4l_get_capability:");   - N! I1 D( }# X+ _
   8:  
  b; x' K# g5 H( f   9:       return -1;   
$ L9 X2 a5 o1 X+ e+ e  10:  
1 v' o3 _+ w9 _6 `# T4 o  11:    }   
+ u5 q3 |& c* y6 e( d  12:  
' u' E7 W, O3 W  13:    return 0;   
( B* e3 J# K& o1 {/ {. g  14:    {: A$ P& j' J. @
  15: }
! {5 B3 Q/ B) t9 s8 Q# ]  16:  6 j* M, n. M; t+ \2 G3 g+ c
  17:  
9 u! ~6 N  m4 h+ U  18:  + k. ]: E: f' U0 O# L. Q
  19: int v4l_get_picture(v4l_device *vd)   ) R9 n1 `! o# k0 e& L% \, P
  20:  # [/ z8 N3 Z3 S5 [( N
  21: {   
  Z6 D4 o& `7 @  22:  
  Q; _: N; W7 O% q+ P; C) e1 X  23:    if (ioctl(vd->fd, VIDIOCGPICT, &(vd->picture)) < 0) {   ( \6 O  M5 ?; M
  24:  
+ Z& k  w2 V  j: p* ^  25:       perror("v4l_get_picture:");   & W8 ?4 }2 f$ G/ n. Y3 x
  26:  + t; {0 `# B# o. x: w
  27:       return -1;   
+ I+ N) b" s4 V! I0 b& J  28:  
9 d6 @2 ~8 Q7 J9 X& V  29:    }   : X2 G& a3 d# V0 F
  30:  8 J# F! i8 w: Z# ]
  31:    return 0;   
- V* J( ^' i6 R; B  32:  1 w( H% {, s0 }1 E6 x0 f1 N
  33: }# G& r% a9 A8 {% c. G) B7 {' g
对于以上两个函数我们不熟悉的地方可有vd->capability和vd->picture两个结构体,和这两个函数中最主要的语句ioctl。对于ioctl的行为它是由驱动程序提供和定义的,在这里当然是由v4l所定义的,其中宏VIDIOCGCAP和VIDIOCGPICT的分别表示获得视频设备的capability和picture。对于其他的宏功能定义可以在你的Linux系统中的/usr/include/linux/videodev.h中找到,这个头文件也包含了capability和picture的定义。例如:
2 _$ \& M& ]" [( _" k+ {" v7 I7 \5 F  N9 x
struct video_capability( ^6 b3 q; ~0 o1 R+ E$ t

4 [& u5 l8 q6 W4 s$ X/ Q: y{. Z7 s/ I2 A" g

& F5 l& W1 b( f% q" p2 r' \6 U       char name[32];
" V2 G9 S5 @' M1 t# t! _+ j+ ^4 C( e3 n, ?' o/ P
       int type;  s* g/ r2 K" g5 d2 g* P$ Z8 ^# ~

' \) p  F. ^- C6 F       int channels;   /* Num channels */3 z0 b$ c  V$ r; ]9 ~

2 C+ H6 c8 T5 M       int audios;      /* Num audio devices */
+ i& w. q% R% v8 D2 C
" @  P. Q8 v1 N: R8 ~       int maxwidth; /* Supported width */6 R, _% A6 j& P: _1 u2 d

4 T" F7 ~  ]; }  v% }/ ]5 |, Q8 @       int maxheight; /* And height */+ H* k) k9 N  l4 c  _) c6 m

/ ?3 B. ?, {+ H0 d5 ^5 b       int minwidth;  /* Supported width */
8 s; ?+ ]0 u  H, Y1 ?4 N7 R
8 S9 y8 s+ A  |6 S$ _       int minheight; /* And height */7 q( K# D6 _, t2 J* X7 h) N
5 n2 j' W2 ]( i, S2 W: _5 f. u. [
};capability结构它包括了视频设备的名称,频道数,音频设备数,支持的最大最小宽度和高度等信息。
3 h" }$ Z  s" D* c2 [$ }8 A0 A- R9 T4 e
struct video_picture1 U! M  n' j: U5 V

6 W0 o+ N4 x. {% s1 ~" P" i{/ C, ]+ n) N/ g1 P3 b! b
; N, g/ ~. z  M6 r! Y' \
       __u16     brightness;- W0 P4 k3 L) r7 ?* h+ \
& u0 F! d) W) e& t) [& v
       __u16     hue;
* |4 B: m* L2 A$ m% `6 g  O! `6 |  V
       __u16     colour;3 o1 G$ j! ?( g9 C- m" b& j

8 g2 `& _- [7 N1 g& a, k2 D       __u16     contrast;; K7 E; n+ i, ?% f

! K+ O  [: {& z& o8 h* g* W       __u16     whiteness;       /* Black and white only */( Z3 B: V" l; V6 \/ K9 v1 h& J, j

0 t( Z/ g) ~) h: b  O4 }       __u16     depth;            /* Capture depth */; G9 u1 T0 j( J' u2 r' T

9 ~5 @/ K9 M# C3 G: L) M  f9 b       __u16   palette;    /* Palette in use */
. ?+ }/ W; |1 ^. b7 m4 o/ I) L) q& q( e9 Z
}picture结构包括了亮度,对比度,色深,调色板等等信息。头文件里还列出了palette相关的值,这里并没有给出。
& M2 i8 C( h% C: R2 d" h- H$ R4 t9 \, ?
       了解了以上也就了解了这两个简单函数的作用,现在我们已经获取到了相关视频设备的capabilty和picture属性。
5 y6 G/ ?! ~9 a: i
, ?0 \- [5 f4 U& N' P8 k; r  n5 K. |5 z这里直接给出另外一个函数( |* s6 S. H5 H* H
3 P- Y. g0 U* z2 w

, P/ d  O% F! I; ?* h3 V; w8 y0 h   1: int v4l_get_mbuf(v4l_device *vd)   
- G. }4 B# N# j0 s" c   2:  ' Z  X1 s3 A' k& _5 F. @  [
   3: {   : J) A" O8 a1 i% V/ C
   4:  
" V4 F) a; f9 c, a, q0 z/ ^1 G+ f   5:    if (ioctl(vd->fd, VIDIOCGMBUG ,&(vd->mbuf)) < 0) {   4 g7 E/ E" A2 J+ o
   6:  
- h" s7 ~& n1 b7 Y7 \# s! I   7:       perror("v4l_get_mbuf:");   " O, e  U0 [+ g% c) a$ L
   8:  
$ z3 k1 U& i4 p( e3 i   9:       return -1;   
! g9 X: G% t  e' J9 P2 e  10:  ) L; U5 Q% w. G! _6 o
  11:    }   & g6 l1 U- o6 ~+ V8 l
  12:  
+ `$ f% T( a  H- H  13:    return 0;   
; L& K: D- h- f% p5 T  14:  
# c1 Z1 T  ^6 D% ~8 w$ x$ N  15: }
+ C+ b; H: Q* @* V对于结构体video_mbuf在v4l中的定义如下,video_mbuf结构体是为了服务使用mmap内存映射来获取图像的方法而设置的结构体,通过这个结构体可以获得摄像头设备存储图像的内存大小。具体的定义如下,各变量的使用也会在下文详细说明。
  L. t' W3 Y+ U% {& v! [' F" G; S2 Y; U' S+ @
struct video_mbuf- {2 g% l2 T/ y8 x; O2 f! c
7 B) ?: I) C; H8 M
{3 ?* k" q, v/ p3 L7 `6 \3 p- J

2 J. R* A; `; {, |       int   size;        可映射的摄像头内存大小
+ q/ @6 C: ?  K4 r
% R; c! `3 }8 g  Y4 z9 W3 f       int   frames;    摄像头可同时存储的帧数
! P4 W0 z4 `8 L3 e
: X2 b4 i, T' x6 J( M       int   offsets[VIDEO_MAX_FRAME];每一帧图像的偏移量# W  ?* R: W- Z. i( l' o6 f
% Z$ c" Y0 }( ]. p
};; {3 f- X7 w1 u8 y' R
9 `8 i0 E; s% i* {8 s
       下面完成第3步按照需要更改设备的相应设置,事实上可以更改的设置很多,本文以更改picture属性为例说明更改属性的一般方法。& ?& m* F! G& D0 b. U* t  N/ k

! E5 i$ N. J$ M! O5 C, u8 f: w) h       那么我们就完成extern int v4l_set_picture(v4l_device *, int, int, int, int, int,);这个函数吧7 v9 ?" G. [: y0 Y  K

5 t. i6 n( a$ l
& ~, P& M" M8 V* U: H   1: int v4l_set_picture(v4l_device *vd,int br,int hue,int col,int cont,int white)$ V) b! n) X4 N+ ~$ c5 I$ C% z  i
   2:  
4 g8 r) |/ h( }% o- y   3: {
3 ~1 U) F$ t9 R   4:  1 U: \) J6 A2 U& E9 X. I9 }' C
   5:    if(br) vd->picture.brightnesss=br;
, P" C" [5 V' ?, v$ p5 R  K. |. X   6:  
2 g8 ?- f5 V0 g5 h- E7 K   7:    if(hue) vd->picture.hue=hue;. H* s9 h2 Q/ v- ~9 q- M6 ^
   8:  & F) C+ W( C8 T. ~* J
   9:    if(col) vd->picture.color=col;
! G6 [/ X& I( N$ G: A  10:    u/ S; f0 \% j2 l/ Y  p4 H
  11:    if(cont) vd->picture.contrast=cont;% T/ R& \; t; A0 ~
  12:  + P6 S0 ~: X, r0 [
  13:    if(white) vd->picture.whiteness=white;; ?" z: [' M" [6 ~+ R! y
  14:  
; q* B3 }# J) {2 L  15:    if(ioctl(vd->fd,VIDIOCSPICT,&(vd->picture))<0)) }' P) c4 g/ l- E1 o& C0 ?
  16:  
: Y: k" n# P6 f% ^) r  17:    {perror("v4l_set_picture: ");return -1;}   
) E: R/ Z+ H" m3 Z# x  18:  5 X& p: |0 l( l$ L* \
  19:    return 0;
( {$ a! {/ ~. o! @) d  20:  8 O; `6 f0 k. W
  21: }9 A1 ?3 `+ c1 A5 S% \' C; H0 o
上述函数就是更改picture相关属性的例子,其核心还是v4l给我们提供的ioctl的相关调用,通过这个函数可以修改如亮度,对比度等相关的值。
8 [/ e4 a  n, [8 u0 p
0 I- n2 B1 ]6 P9 P* _       第4步获得采集到的图像数据。& K- A$ P8 e+ a" i4 G

7 a. D3 W2 Q- }: @8 Q+ ~# ^       这一步是使用v4l比较重要的一步,涉及到几个函数的编写。当然使用v4l就是为了要获得图像,所以这一步很关键,但是当你获得了图像数据后,还需要根据你想要达到的目的和具体情况做进一步的处理,也就是第5步所做的事情,这些内容将在后面第三部分提到。这里讲如何获得采集到的数据。
8 _& O$ ]! n# Q3 G/ q8 Y; m) j/ v
2 N8 V& {3 e4 M0 v       如前所述获得图像的方式有两种,分别是直接读取设备和使用mmap内存映射,而通常大家使用的方法都是后者。
7 r9 x7 }2 a2 f% I1 U2 _
, n* T% V  g1 l# u& O1).直接读取设备
: D3 i4 n( i1 w! i( e
. _9 J' b3 ~& H; Y- e' g直接读设备的方式就是使用read()函数,我们先前定义的
% t5 z& @2 |7 ~) `' ]( i- t) \% j* O" P
extern int v4l_grab_picture(v4l_device *, unsigned int);函数就是完成这个工作的,它的实现也很简单。- L, r) j2 G' @: {: U

% G# ~. |0 F( O4 n! O   1: int v4l_grab_picture(v4l_device *vd, unsighed int size)
7 v, Q% O9 R( Q   2:  
  j# x, ]3 C' ?# x/ L% R   3: {4 @% j6 X* k3 k7 S" F: L1 |; n
   4:  
8 \% j5 u2 L! q1 `& V) v   5:    if(read(vd-fd,&(vd->map),size)==0)return -1;
- {! _# e! t% a4 w/ r( j" x   6:  
6 X- |+ P# X5 M   7:    return 0;
7 a, e/ [) O+ C- C7 T5 I   8:  9 c9 K4 g. D( n
   9: }
7 x! F' i: S1 t" ^+ [* j8 y# z# q8 ^+ I" E/ M9 J
该函数的使用也很简单,就是给出图像数据的大小,vd->map所指向的数据就是图像数据。而图像数据的大小你要根据设备的属性自己计算获得。
0 V1 R) D4 c; j$ d3 d2 S  a4 m   @" w  |# W& ]& h+ M3 `
  o$ H7 u8 Q# \; B, G
2).使用mmap内存映射来获取图像
) e" Q6 a2 K; T) A2 D: g3 t( U. l+ z9 q1 Q. B% ~" S& X
       在这部分涉及到下面几个函数,它们配合来完成最终图像采集的功能。# O- X. `( F5 T6 r2 h! @) {
$ H4 I) X. B( U+ p
       extern int v4l_mmap_init(v4l_device *);该函数把摄像头图像数据映射到进程内存中,也就是只要使用vd->map指针就可以使用采集到的图像数据(下文详细说明)
2 P7 w# {8 E1 l4 P1 I: y
, z: Z+ r3 Y7 t$ k7 ~6 u- P7 qextern int v4l_grab_init(v4l_device *, int, int);该函数完成图像采集前的初始化工作。& N# |4 M# U+ S) u. T
  n. {/ J! e5 t2 m
extern int v4l_grab_frame(v4l_device *, int);该函数是真正完成图像采集的一步,在本文使用了一个通常都会使用的一个小技巧,可以在处理一帧数据时同时采集下一帧的数据,因为通常我们使用的摄像头都可以至少存储两帧的数据。$ l9 \1 W9 b0 F' j
! g4 H- O- a( D: W) x' Q" K
extern int v4l_grab_sync(v4l_device *);该函数用来完成截取图像的同步工作,在截取一帧图像后调用,返回表明一帧截取结束。
; k- d( Z, U% b# ~( A+ ?5 b: H$ W! ^
0 g$ j9 w7 R" G0 T% V
( @, H' s+ C, T; ?) h7 }
       下面分别介绍这几个函数。
0 J9 W- X: \+ _* ?7 j9 {
( ~1 l4 E5 l2 V3 x; f, F. n - P6 t- a1 v' e& ~1 ?5 g) k

6 D1 g/ J& c7 }: |5 B3 t  L       mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必在调用read(),write()等操作。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时访问进程B对共享内存中数据的更新,反之亦然。
5 Q: x+ X# F# v$ c' y& e$ o+ _5 ^% R0 g. G7 m8 w* k- y
       采用共享内存通信的一个显而易见的好处是减少I/O操作提高读取效率,因为使用mmap后进程可以直接读取内存而不需要任何数据的拷贝。/ S: ]" ?3 l2 m- [8 \
; g; J- g. N4 x& N& j8 J: w/ Q( _
mmap的函数原型如下
6 Q. t: _. {' |) d* L! V, J
& [% U9 k% f  C* v4 Q# Nvoid* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
+ O6 b  y% z, W0 B$ ?
* `# p" G- F3 x/ `. uaddr:共享内存的起始地址,一般设为0,表示由系统分配。1 L. z: ~; C0 ?

; t  g  h, j/ d' mlen:指定映射内存的大小。在我们这里,该值为摄像头mbuf结构体的size值,即图像数据的总大小。5 a* I" Y# ~/ a, \! `4 F

& ]( ^: r: a8 s% c) O6 S& c, tport:指定共享内存的访问权限 PROT_READ(可读),PROT_WRITE(可写)
7 D/ D; ^) l9 q2 q: k1 b2 a9 Q7 D7 Q# p7 N
flags:一般设置为MAP_SHARED
$ j& r* X- S& m4 [3 I% z) x+ l& _
fd:同享文件的文件描述符。
" w0 f2 R  j$ k$ h& d& [* R# C/ \) y

" N: L# C# \9 g2 R+ _. m1 \- d0 ^# `# [0 T3 _
介绍完了mmap的使用,就可以介绍上文中定义的函数extern int v4l_mmap_init(v4l_device *);了。先给出这个函数的代码,再做说明。
5 W7 b& e- n+ O$ b: _) ?2 n5 R, C$ ]% g, b2 f$ Q6 I! }
# _" G' F2 ~" V+ b) M
   1: int v4l_mmap_init(v4l_device *vd)   
( H' k; F, D6 v$ Y6 Q3 d8 p" v4 @   2:  
; j$ w; x7 ?$ r# i/ p   3: {   
$ S$ {% g! H" d$ S, H6 b6 F: W   4:  
( b2 N$ ?3 Q, x% \   5:    if (v4l_get_mbuf(vd) < 0)   . R0 V0 u' p" T1 W+ X  E: \
   6:  ) j: b% F$ c# s1 h7 w( `% V  g7 J
   7:    return -1;   + N0 I# a/ u7 B3 A. ?
   8:  : ^# @; p% P- ?" q7 h+ O3 Y
   9:    if ((vd->map = mmap(0, vd->mbuf.size, PROT_READ|PROT_WRITE, MAP_SHARED, vd->fd, 0)) < 0) {   ; s- F5 \. ?& ?, T! r; E" B4 r: Y
  10:  0 ?) A  c: Q8 i) @
  11:       perror("v4l_mmap_init:mmap");   . n7 W0 L5 c& `' q4 ^6 P* o$ D+ r
  12:  
1 a4 P, z& Y8 g2 h- Z  13:       return -1;   3 i# }8 B$ ]  y8 ]2 Z! Y2 A, Z4 [
  14:  
( h" y( B; @' r. R  15:    }   * `" L3 v1 z% J7 ?- p0 U4 {) \
  16:  
" r  _. z) n. U) `' }  17:    return 0;   
* j4 D6 N; n% |1 w0 H! V1 }  18:  0 m" V$ z! f# S
  19: }
0 a% ^* Z' j. u" J& }这个函数首先使用v4l_get_mbuf(vd)获得一个摄像头重要的参数,就是需要映射内存的大小,即vd->mbuf.size,然后调用mmap,当我们在编程是调用v4l_mmap_init后,vd.map指针所指向的内存空间即为我们将要采集的图像数据。
% G0 I) U3 \5 \2 k8 m8 Q0 ^4 R/ f# K/ H' g- m/ H1 X2 [' V" R* r
      
' b' M8 `# W8 U" C# b- e. F
* L# Y9 J+ Z7 t  z% m9 x; g! M      获得图像前的初始化工作v4l_grab_init();该函数十分简单直接粘上去,其中将。vd->frame_using[0]和vd->frame_using[1]都设为FALSE,表示两帧的截取都没有开始。
7 b$ V' C/ |9 i. @4 J/ L  t0 t, b7 F, y# f% Q% w) f
   1:  + o6 P0 A+ k; O; U; m
   2:  
; G% M- u* {9 H7 z( o; d   3: int v4l_grab_init(v4l_device *vd, int width, int height)   ! }: ]0 u+ H3 V: J  {4 l# V  a7 N
   4:  
7 W$ r: V, }  w8 A2 h+ w: V   5: {   . o6 o( [1 }+ U( M- G
   6:  ) D+ y4 B. z2 {6 F
   7:    vd->mmap.width = width;    ! Q. A/ N, Z& U- o8 z
   8:  
. {) b- r/ x5 ^/ ]' d   9:    vd->mmap.height = height;   
  V6 G) K% @/ i: ?  10:  
: Q: B' m8 A6 H4 ]  11:    vd->mmap.format = vd->picture.palette;    4 _$ C, b' d8 w& y, \
  12:  
+ w* m: e6 U& p1 V. m/ E& L5 D  13:    vd->frame_current = 0;   * n% |& |1 N- d# g
  14:  / _. U% \, j0 P! S
  15:    vd->frame_using[0] = FALSE;   
1 s2 b2 O1 y3 r6 V$ l  16:  : c; h" M0 q7 l2 G
  17:    vd->frame_using[1] = FALSE;   
$ V. T$ I9 U6 H: P' g- u  18:  % ~! r% B1 d8 ^8 x1 d
  19:   
1 f* b) y$ \2 s; w) _- x, W  20:  . w0 w8 C' j0 T' h4 I
  21:    return v4l_grab_frame(vd, 0);   
& |9 J. _9 X" O) |* N& B3 F  22:  
( k6 u. E1 c$ G/ o$ l; o  K3 U  23: }  
+ ~0 Q2 A! E) w1 L( Z1 U  24:  2 S7 x" _6 \# k% @* X* n2 Q
  25:        
0 |1 D( ]/ R+ b0 g) g8 I7 C真正获得图像的函数extern int v4l_grab_frame(v4l_device *, int);
9 y, W; e, Z2 d5 Q2 T
; `! |" j/ |7 x/ D  G% C
/ G7 p/ N* [) i" P   1: int v4l_grab_frame(v4l_device *vd, int frame)   
5 P% p* m! e! D, i, @. W   2:  
+ s, {2 f4 L# V$ y# i9 k/ ~: [0 B   3: {   
+ u2 Z' y9 ~+ X  G   4:  
  g9 c; R  `% K6 z$ m* j0 W   5:    if (vd->frame_using[frame]) {   . A  m9 i0 ?2 ?5 i: M/ c
   6:  
$ t4 Y9 G* W( ~! y9 w) P0 }   7:       fprintf(stderr, "v4l_grab_frame: frame %d is already used.\n", frame);   
) {- r) p8 V2 X( {! M   8:  ! O8 N# V, V; v) V: R
   9:       return -1;   , y3 H* x# ~, K" A, C% Y+ `4 h
  10:  5 h, T( E5 A: V
  11:    }   / |  c8 {! F# s( H3 T
  12:  , J) E! k: `+ `2 _5 A$ j. }' G- z2 r
  13:   
0 x3 O. h  d3 t. L7 F1 N2 }  14:  
2 A9 `' r) Q$ r+ R! W, o  15:    vd->mmap.frame = frame;   
4 m' `4 d; O4 h  16:  " W, X4 k- @% q8 f* _. Q) K& Z
  17:    if (ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)) < 0) {   
, M$ {$ G8 {/ V9 N$ ]* K  18:  4 v& z3 d% Y0 t# Z
  19:       perror("v4l_grab_frame");   - l4 j, U+ C$ u, R6 Z: M
  20:  7 ], y! p" t/ ~  r
  21:       return -1;   
- y% J# ?4 C! U$ p' B  22:  8 a) f& e# c5 ]2 Z2 d
  23:    }   
/ h. A: V9 X1 G( T  24:  
9 Q, V4 Q' g4 o) z( v+ K* J0 l1 t& e  25:    vd->frame_using[frame] = TRUE;   / \" I2 n( i9 g  T# f% N  l
  26:  
4 {2 C% q5 k! ~: ?! H" F  27:    vd->frame_current = frame;   
; f0 D; q' m. `, R1 }1 E  28:  
3 P; ?4 S) D6 j8 M  b% j3 _8 F  29:    return 0;   
! S5 d2 H2 }4 G) q) {  30:  " l- o0 X% u5 s
  31: }   1 i  j3 o# r8 m2 E  B1 ?; P
读到这里,应该觉得这个函数也是相当的简单。最关键的一步即为调用ioctl(vd->fd, VIDIOCMCAPTURE, &(vd->mmap)),调用后相应的图像就已经获取完毕。其他的代码是为了完成双缓冲就是截取两帧图像用的,可以自己理解下。5 z' w" r' L$ v

2 i& p( N1 j7 R. @' J: M       在截取图像后还要进行同步操作,就是调用extern int v4l_grab_sync(v4l_device *);函数,该函数如下5 t5 c+ K9 V, e) E1 ~
) F( v: ?( i0 p
- X3 u! C3 c, o' Y1 ]
   1: int v4l_grab_sync(v4l_device *vd)   ' L! e8 _$ F$ k$ `
   2:  4 |: C1 a6 a+ {
   3: {   - s) \% W- n8 }9 @
   4:  
! x9 {4 T1 |) D% H4 ^- m   5:    if (ioctl(vd->fd, VIDIOCSYNC, &(vd->frame_current)) < 0) {   * {( {9 ?3 Z$ c# o( b( W8 V
   6:  4 M: J5 T* A! f1 }  |
   7:       perror("v4l_grab_sync");   
1 u- c4 S. r& L6 t, t   8:  & Y4 F9 k7 [6 o) p4 |8 n
   9:    }   
5 F4 U* O* X/ N  10:  ! Z- X# b$ E; d0 O% W3 n
  11:    vd->frame_using[vd->frame_current] = FALSE;   : N" y" u& F8 v* Q6 w7 v
  12:  
: c/ r) M/ `1 g1 o% r7 Z6 p  13:    return 0;   / `1 u  n; _2 D. I/ E
  14:  5 W0 b& l! o5 v% Q: u5 b, f
  15: }   
2 i( Z, ]' V& G4 n" J& i: U- V5 s该函数返回0说明你想要获取的图像帧已经获取完毕。5 [8 z9 u0 r2 M6 D6 ~

8 p; W) u/ p5 e6 d/ b 7 y  G6 e+ f$ P; F" `& E2 g5 ]0 u4 K5 S

- `: Z7 q  @8 M4 W8 t图像存在了哪里?
* p! p8 g/ G/ j& w" P, l3 w1 N
' a- v( c8 q3 ?8 W% Q# K! s       最终我们使用v4l的目的是为了获取设备中的图像,那么图像存在哪里?从上面的文章可以知道,vd.map指针所指就是你要获得的第一帧图像。图像的位置,存在vd.map+vd.mbuf.offsets[vd.frame_current]处。其中vd.frame_current=0,即为第一帧的位置,vd.frame_current=1,为第二帧的位置。: ?; U5 R) N8 X; @2 Q& x( p

9 X" o/ t; }- @7 d1 `: t 3 ]+ A3 ~8 r/ t: C* O
8 B4 B1 K) G8 P7 Y4 ~  f
2 上述v4l库使用的方法
6 \) G( k  ?2 m' F; D9 K7 H6 h2 i
给出了上述的一些代码,这里用一些简单的代码表明如何来使用它。上文中已经说过将相关结构体和函数的定义放到一个名为v4l.h的文件中,相关函数的编写放在一个名为v4l.c的文件。
2 f' |: p4 @! q0 @, b% }: Z
; p; l' C5 a0 @1 X7 K" a' V9 t现在我们要使用它们。/ O/ D/ e5 R( H! j7 w

& O5 n# y! X6 _4 Q" S使用的方法很简单,你创建一个.c文件,假设叫test.c吧,那么test.c如下
) a( M; e, B4 W6 n! ]6 d5 g+ L$ m" n4 Y+ m& `+ C. h7 ?6 I

( c5 M. q, ~7 B   1: //test.c, b1 B8 @4 r, O! `
   2:  * v5 g* U4 h" A& h* a' H9 Z5 o% F% J. K
   3: include “v4l.h”* M8 G, y8 K: Q/ N6 l( A
   4:  
$ \. u0 `# r( d* S   5: ...
0 a1 n: O  K* L9 y6 c   6:  / p& a6 c4 P5 E' G& j
   7: v4l_device vd;& p9 w$ o  v- ]
   8:  4 g9 M. q  A) T. |* ~
   9:  
' W$ i) g8 J8 c% Q5 {  10:  
8 D: q* O8 q) D" I! l* g  11: void main()/ D# P( N9 P( C" R0 n
  12:  ) |( v( n+ p+ B7 S5 f) C! v
  13: {$ o% w& h. D1 j* m# T( e2 G
  14:  
1 _  P! L3 Y& ^6 t2 o5 a  15:        v4l_open(DEFAULT_DEVICE,&vd);  J: y6 R8 c! W! L2 U' K! m* [2 \
  16:  
5 T0 @+ T2 C& B8 D  17:        v4l_mmap_init(&vd);% N& ?8 M3 ]& o1 n1 l2 n0 |
  18:  ! D; S( J/ @6 \, f, s, R
  19:        v4l_grab_init(&vd,320,240);" |8 Z5 S' I3 a
  20:  
1 x2 i6 @) }# i, k: l; G  21:        v4l_grab_sync(&vd);//此时就已经获得了一帧的图像,存在vd.map中( K6 w  U- x8 @) K) F% T7 ]
  22:  " z* ^( i/ L3 E4 m( b
  23:        while(1)
3 S' t# b- Q1 R' R$ b* B( E  24:  
/ F- k2 Q3 {) f6 r9 k* \  25:        {
2 y) W) O4 k# \$ Z8 z  26:  * C. n* G3 m( S: Z
  27:               vd.frame_current ^= 1;   
2 T6 K# Y2 s! E" n$ [  28:  
, ^9 {, O$ ^# x  29:               v4l_grab_frame(&vd, vd.frame_current);) ?$ y% }$ I2 K) M" T/ F$ e' D
  30:  
4 J6 L1 Q8 N( d+ Z" C  31:               v4l_grab_sync(&vd);+ r1 h) t6 H8 v
  32:  ( a9 [5 W, Q- w/ W2 Z' {1 H
  33:               图像处理函数(vd.map+vd. vd.map+vd.mbuf.offsets[vd.frame_current]);
! h+ V& a  t4 a; W- ]8 r1 F  34:  
8 s% s% G6 |9 O( ~) p/ I( @  35:               //循环采集,调用你设计的图像处理函数来处理图像* X0 Q" Q0 M) i5 G" r9 y
  36:  . V- F% t2 c3 V( R0 n
  37: //其中vd.map+vd. vd.map+vd.mbuf.offsets[vd.frame_current]就是图像所在位置。. I+ t: i+ i6 p; g
  38:  
. a5 E; n0 o: B  I9 b  39: }
3 n4 s9 f+ b8 d, Y0 X  40:  
1 v* q" u2 G" x" q; k  41: }& R% _. U, k+ P; a% s
  42:  ! L' H* {; g2 k/ i' A- }3 L
  43:  6 d3 o# k# A, _. v- O' k. X
6 `9 x" C$ I. B/ w
! v3 x. t/ ]6 p" P3 ?) B
3 有关获取的图像的一些问题
7 U5 b6 x" e$ e; M- C# {4 f6 `% E7 \. y1 g8 l( Y5 m$ M' R4 g
问:我获取到的图像究竟长什么样?/ c7 _, }+ d5 j5 m3 L/ x0 o
9 d7 A" Q2 x8 N( l  g  B& G$ o7 |
答:每个摄像头获取的图像数据的格式可能都不尽相同,可以通过picture. palette获得。获得的图像有黑白的,有yuv格式的,RGB格式的,也有直接为jpeg格式的。你要根据实际情况,和你的需要对图像进行处理。比如常见的,如果你要在嵌入式的LCD上显示假设LCD是RGB24的,但是你获得图像是YUV格式的那么你就将他转换为RGB24的。具体的转换方法可以上网查找,也可参考前面提到过的effectTV中的相关代码。
% Y$ I( ]/ W; V# T: [$ ]' ~! w4 {& Z; D$ P( `
1 ?, ~, O. Z, u, D4 I1 j) s
& g6 y# w7 Y+ ^7 y6 Q, k$ R
问:如何显示图像或将图像保存?$ f9 k% p; W. A: Z) N
/ N& N9 z6 K5 P+ E$ E+ B0 y# }4 b( }' H, L
答:假设你采集到的图像为RGB24格式的,我接触过的可以使用SDL库显示(网络上很流行的叫spcaview的软件就是这样的,不过它将图像数据压缩为jpeg的格式后显示,这个软件也被经常的移植到一些嵌入式平台使用,如ARM的)。当然也可以使用嵌入式linux的Framebuffer直接写屏显示。将图像保存可以用libjpeg将其保存为jpeg图片直接存储,相关的使用方法可以上网查找。也可以使用一些视频编码,将其编码保存(我希望学习一下相关的技术因为我对这方面一点不懂,如果你有一些资料可以推荐给我看,我十分想看一看)。# v2 S  D4 q. y5 \7 ^, d; a0 j
# y) m  j' N) N" J$ r

; v) [  Z7 K, i, w, I+ Y0 }4 f9 M- Y$ b5 r
       一边写文章一边才发现自己很菜,因为很多都是参考别人的文章,而自己想写出来去一落键盘就写不出什么。就写这么多,因为我只会这么多。高手见笑,新手和我一样我们互相讨论

该用户从未签到

2#
发表于 2020-4-14 18:26 | 只看该作者
实例分享,都是大神啊
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-7-2 03:42 , Processed in 0.093750 second(s), 23 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表