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

简单介绍一下linux输入子系统的概念

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2020-6-30 15:15 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您登录!

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

x
在此文章之前,我们讲解的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux中断、poll机制、异步通知、同步互斥、非阻塞、定时器去抖动。
2 o' d' f0 x8 \1 u, k0 p: X! W" P, l4 z' J+ i0 M$ @, q
上一节文章:( N: y3 F% s% d) E( }! }1 i) v
) N  o& o$ P- b- W7 a
5 W& s$ u+ q  G8 g. M! M
在这一节里,我们要引入linux的分离分层的概念,linux输入子系统是一个很好的代表,在讲解如何编写input子系统的驱动之前,我们理所当然的要先好好认识一下input子系统的框架。
/ Q& k8 ^$ a5 p0 U% c* m' d% u) P
1 c5 ?2 G( E4 y$ K+ @/ F" o1 V一、linux输入子系统的框架
7 N  q& q2 _+ e2 h8 @) R& n* b6 [( q* z) F: U! x
下图是input输入子系统框架,输入子系统由输入子系统核心层(Input Core),驱动层和事件处理层(Event Handler)
+ J' R5 Z( I" G, `2 g& h
6 i9 }* y3 J" P) `) Q! K三部份组成。一个输入事件,如鼠标移动,键盘按键按下,joystick的移动等等通过
8 E6 G% \0 C# B2 ~' ~& d! N; y+ n* y8 Y6 \' [: p% o
input driver -> Input core -> Event handler -> userspace 到达用户空间传给应用程序。" q, r9 }2 j& `- C/ I) d) k+ n
$ Z1 H( J- U6 p+ G) o/ y; s
" K% X7 Y- ~) B
9 ?& ~% g* \4 X2 U# h5 C, w
二、drivers/input/input.c:
& N) s- U7 K& b5 i* j6 {3 L3 D2 T# g# p
入口函数input_init > err = register_chrdev(INPUT_MAJOR, "input", &input_fops);. o3 c. D1 w3 Q, h; M1 h
$ g( X: ?: M7 \
static int __init input_init(void)( ]% U* J! U% S# y1 }) i
{* e- `* p' w6 U6 T7 k2 T
        int err;
' j" o3 n; R3 _        ...
  i3 z" n8 M1 Y1 d+ n+ Y        /* 创建类 */
6 C, ~" Q& h. m2 a        err = class_register(&input_class);
9 D" ?. h1 M4 f+ j% e6 v: Q        ...+ N$ B% w; G* g3 V% B
        /* 注册一个字符驱动,主设备号为13 */
1 K2 i  q8 n. T1 Q/ }' b        err = register_chrdev(INPUT_MAJOR, "input", &input_fops);  v% T  a# \" ^' B. R* B1 C' n
        ..." ]) ]- a; r! W+ A# }
        return 0;1 @  ~4 J( R, p6 |1 X+ a9 \
}
7 s& C8 d% ]- I' W只有一个open函数,其他read,write函数呢?
8 X: L1 p  {1 c6 S6 I+ Y( v8 Ystatic const struct file_operations input_fops = {
* \% h8 n- U/ p, I9 S- D        .owner = THIS_MODULE,
4 E8 i$ A: q5 L; d1 Y# Z        .open = input_open_file,
9 q- k! o. T9 t% x  z};9 n% z, n: G/ J5 ?9 e. K' `
input_open_file函数
' ]+ P4 b; w) }- x8 i% _' y  k7 r
static int input_open_file(struct inode *inode, struct file *file)# P5 @, j0 Q7 ]) ?( G
{! f4 t: _. g# M7 D% T7 K
        struct input_handler *handler;
4 I6 A4 e2 I& s4 @5 k8 B; K        const struct file_operations *old_fops, *new_fops = NULL;
9 V' P; @1 }+ n! \: s        int err;
& Y7 H9 @7 `) ?4 t" u        ...
1 n* V- y) j; V( j( Y" W        /* 以次设备号为下标,在input_table数组找到一项handler */8 h, h" f! l8 X2 [7 _3 y
        handler = input_table[iminor(inode) >> 5];5 p& x' ]2 A/ Y& K5 W7 J; x
       
/ _& _$ O' G7 J* x: K0 D        /* 通过handler找到一个新的fops */) [+ [7 z7 [8 L6 H. P) A8 |% `
        new_fops = fops_get(handler->fops);; r8 o  G& G6 ?" x
        .../ a( h) }% W* U6 Z& y; X
        old_fops = file->f_op;
7 v% f# ^" n2 z: Y# ]% K- ~        /* 从此file->f_op = new_fops */$ S7 y  ~) a- v( H) ]0 Y
        file->f_op = new_fops;
0 m  Y) p; p6 |& W! r) O        ...
# R8 k# l% _" R) `/ o        /* 用新的new_fops的打开函数 */3 }7 f5 g& u3 N. ]( {9 k' ]
        err = new_fops->open(inode, file);
5 V8 G: o. x1 X+ Z9 v        ...
. I% E/ q5 J% s' J$ t( ]        return err;. r& o& _- h" l; U+ u0 c
}* w# w5 A3 F0 h0 l5 M% k  ]
input_handlerj结构体成员
7 p% t' f9 l" a$ Ystruct input_handler {
/ J+ x5 {; m) l7 T5 z1 t& y. }: a6 A  |) X
        void *private;
) r: x5 H) c# @+ N  K9 h0 ?
' ?; |! o% I9 _0 j2 {0 l% a        void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
) m9 b% c/ x3 S' b        int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);$ e7 A& ]9 M  p( g/ C& p
        void (*disconnect)(struct input_handle *handle);6 ^9 N$ n8 X+ \3 ~/ m3 _5 j
        void (*start)(struct input_handle *handle);( F& z0 q0 W2 t* H% Q. d2 V  t; L
8 \8 h; r" Y1 s4 F: T/ P( W
        const struct file_operations *fops;. F3 h  M. M) ^% U
        int minor;
; M( |' X- V" J% q* s) I4 t8 V/ [        const char *name;
% J" n+ `& X7 [* b0 s* v! V
: D( |- l# Q0 o        const struct input_device_id *id_table;
  Y$ Q( n' u9 d3 R$ J3 @        const struct input_device_id *blacklist;  Z6 ^- s: x0 m7 B2 K& T
2 z" v6 G) l; `4 H
        struct list_head        h_list;$ T# H/ \& W3 w+ d" C4 L5 z% o
        struct list_head        node;
0 A" o2 V% m6 i# y+ n};# V! @- U; M$ w: m7 o6 v
问:怎么读按键?3 S5 I, y- G) Z) L5 M
APP:read > ... > file->f_op->read ; U# z; N  I+ n7 M# W, x

8 t, A, p& K: k0 p& F* w问:input_table数组由谁构造?; \4 a8 r2 f: w: ]) L" F7 x, Y
, @$ R" Y# _; `0 P) {
答:input_register_handler: j: I5 c7 _  k! n4 R! W0 h( Z: m
0 u& W& F0 v, o: j5 ?( ~3 f
三、input_register_handler函数(注册input_handler)
- M+ Z  t" D- `2 M
/ V: Y9 D' m( q8 n
3 s% V: ^3 O! iint input_register_handler(struct input_handler *handler)
4 q) q3 K1 E! n" c{
& `3 P" l$ D, J        struct input_dev *dev;
. ?' H" J4 S  W2 e( S( g6 f        ...0 u. c% x, q7 l( m
        INIT_LIST_HEAD(&handler->h_list);$ D9 K/ ]9 k$ m, b
        ...; \# o4 |6 p, q8 T
        /* 将handler放入input_table数组 */
9 x/ V7 I8 z, h0 s% \        input_table[handler->minor >> 5] = handler;- |, ?- L5 f+ T4 W
        ...
+ _) ^9 J; [$ ?# r        /* 将handler放入input_handler_list链表 */* G# B7 m, w+ E& T( j# x8 M
        list_add_tail(&handler->node, &input_handler_list);
4 ~5 Y" t# T' h& M- x, u( M: L        .... [) N% J3 |: B' ]
        /* 对于每个input_dev,调用input_attach_handler
1 k" e$ M; I4 {6 x( V         * 根据input_handler的id_table判断能否支持这个input_dev$ {( I) }  ]0 m  Q" M
         */
. Q- b2 j; @7 O# a# o0 g' |        list_for_each_entry(dev, &input_dev_list, node)5 j; x- M- p! C" w5 e
                input_attach_handler(dev, handler);
. a( ~, [& s+ Z. {        ...4 y: |& w0 D& A) W) l7 B# w: ^
}* h. ~0 i9 V' U. ?) r' ^
' K. a8 C  F1 P  S! X
四、input_register_device函数(注册inout_dev)9 X( }/ `9 e: L7 `, Z% ^+ S

) H4 d1 [: P1 |, U5 O+ Qint input_register_device(struct input_dev *dev)2 y" ]4 `. H. q
{7 p8 H* ^4 n0 I6 X
        ...
9 o" u3 L& P7 b        struct input_handler *handler;9 E3 {3 i2 f" U& X. d; y$ L0 x1 S
        ...
" o+ e: B$ ^3 _) L        device_add(&dev->dev);" M* M) l0 a4 o; F% G, p: [% ^! u/ o  B
        ...
% i, I) Y5 Y; Z# ^, D/ N        /* 把input_dev放入input_dev_list链表 */
8 H8 a& f+ u- ]0 @        list_add_tail(&dev->node, &input_dev_list);* K) i2 R6 Y4 C
        ...
9 s; d3 N- n  r, g  B        /* 对于每一个input_handler,都调用input_attach_handler3 [6 O, E# \3 u+ W' u% ~% m
         * 根据input_handler的id_table判断能否支持这个input_dev
; E' x1 n+ `. C  L         */
1 E# A1 X! g: j+ D        list_for_each_entry(handler, &input_handler_list, node)& X1 w. c# e5 e( N
                input_attach_handler(dev, handler);
6 E4 F% P5 ^/ ^# {, B% j6 ^        ...& a/ U  g& U: p& ]
}
* H7 O2 m8 Y4 i* Y
/ \$ e( H. Q# P五、input_attach_handler函数
: ]' L5 i: e( G  j# q5 O' }' e# pstatic int input_attach_handler(struct input_dev *dev, struct input_handler *handler)% c. ]) j8 j8 N. J  ?3 J
{8 Z& q- x1 p. o; Z& d8 M$ H
        const struct input_device_id *id;7 w0 m+ g( O3 g7 q: t" a/ z
        ...# @" e7 s, z+ C5 Y6 e4 c5 J  D6 m
        /* 根据input_handler的id_table判断能否支持这个input_dev */. _5 a% B; w2 h3 \  \+ k: K& A2 b% _
        input_match_device(handler->id_table, dev);. ]8 H2 @9 f/ g! O" l
        ...
8 t3 F/ f8 A0 }! ~" {/ g% J        /* 若支持,则调用handler的connect函数,建立连接 */
' l$ y# B8 H: H        handler->connect(handler, dev, id);
; Z4 [# H8 K, X! F$ y7 s        ...4 v7 m' m* V  H7 v9 E
}. E0 J: q& Y& `/ y

6 d: ^& X8 K: ~( q9 w2 F小总结:
2 ~; I* S2 @' w& s* q. u5 x注册input_dev或input_handler时,会两两比较左边的input_dev和右边的input_handler,根据input_handler的id_table判断这个input_handler能否支持这个input_dev,如果能支持,则调用input_handler的connect函数建立"连接"。. ~3 n4 A* {! C" Q6 l
  U$ h. k) f. X( p
问:如何建立连接connect?
6 U0 g' b, R" X( y
1 e9 S! r' Q0 X9 `* e. D! q答:举例,evdev_connect函数6 y4 V4 z. q" b1 F9 r9 h$ J9 c

/ p$ U6 z( d  ?5 ~* J
( D  [* j: _7 O# m. bstatic int evdev_connect(struct input_handler *handler, struct input_dev *dev,
0 k8 @! p9 z( |( u                         const struct input_device_id *id)
( H, c' e! ?* k' f+ O{2 g& [4 b; ~. J0 e. n+ E; i
        struct evdev *evdev;  T, [7 F: C4 p( z5 F- f) M
        ...; b* x7 e2 A( k+ @# k2 {0 f

! Z1 x4 `+ p: N6 V& S2 }        /* 分配一个input_handle */
' p, M6 k8 B  h        evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); 0 Y0 P9 l  _5 z' O' j$ `* b8 n
        ...- q4 [1 }. v3 a1 F/ O7 D# i
        snprintf(evdev->name, sizeof(evdev->name), "event%d", minor);
$ r- Y* l; t% _1 `2 X: @0 \        evdev->exist = 1;. C. u. }; p% ?
        evdev->minor = minor;; ~4 v+ @1 H4 Q. C+ @( f! m
, B- j7 I+ |6 @# r5 A0 ]5 {
        evdev->handle.dev = input_get_device(dev); // 指向左边的input_dev$ r) O  h, P. l, P% N5 ~
        evdev->handle.name = evdev->name;$ Q, _0 r5 e" E- d" O+ `
        evdev->handle.handler = handler; // 指向右边的input_handler
2 u+ @  U; O8 X! b9 }3 o        evdev->handle.private = evdev;: z3 z' V; Y# w. I- ^0 ^

) ~  t; O; }. ~7 N2 S* X        /* 设置dev结构体成员 */9 G' r+ f) b  R  ^: A
        dev_set_name(&evdev->dev, evdev->name);7 B' ^% B7 o" p! X7 e
        evdev->dev.devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor);: K/ o: D0 V! q+ Z3 o1 t
        evdev->dev.class = &input_class;' t& N2 r, b) ]+ S
        evdev->dev.parent = &dev->dev;, }. O6 V* S7 q8 R
        evdev->dev.release = evdev_free;/ b5 p, p3 o6 J" C/ Y; \
        device_initialize(&evdev->dev);2 b; v) @6 q. g

& s, d  h" |3 I' t' R5 l) ^        /* 注册 */
* h/ t* q! E- y8 A2 m        input_register_handle(&evdev->handle);. ~9 y- Z+ b. e  W1 n# x
        ...8 M0 n+ K3 k6 g/ d  Y- d
}+ Y0 W+ j: y7 c( v2 q. d
input_handle结构体成员
. M, e, j7 T) Y) ^
% F6 Q$ {6 X9 X: e: W( h3 Tstruct input_handle {2 {0 c6 f. Q4 n
4 w/ S9 f- ]+ R$ h6 G. a# v8 n, N7 j
        void *private;* L, v; ~  w2 B/ |3 c; m6 I' {

. L8 u: V6 Y7 n5 ]. R+ w        int open;0 r# R1 b7 D" w
        const char *name;' D2 m# _: c9 `: X

; y/ Y, e  b2 Y+ _- p1 V' V7 ]        struct input_dev *dev;. r* W$ P% P! N& l2 o
        struct input_handler *handler;) A* b8 i2 C, Q$ k$ p9 o- z0 G% V! y

# E% Z* L5 ~5 t) @        struct list_head        d_node;" v" b- `. ^  f* h0 I+ }' e) h
        struct list_head        h_node;0 U, j% K* h3 R$ [( k2 z
};
& n0 V! \/ G# R% e2 W) y+ e+ k问:input_register_handle如何注册?9 J, k9 J' z9 b% u

) r, V8 x3 o( P8 Oint input_register_handle(struct input_handle *handle)
) Q5 e* |0 d1 K{
$ |5 _. a2 c. B! v        struct input_handler *handler = handle->handler;( U) I' g( v) H+ y* g9 ^/ @
        struct input_dev *dev = handle->dev;" h5 [4 V7 u2 ^. n. n9 P2 b
        ...+ x( F. m0 M9 L
        " G. T1 H9 u+ e- u7 ^) l# C, S. [+ u
        /* 把handle->d_node添加到dev->h_list
5 w) o  d3 ^5 H( r5 @! m         * 这样,就可以从dev->h_list找到handle,进而找到handler' G6 Y1 b, M- u
         */* ]0 L9 s( {( q* z$ L. Q/ {
        list_add_tail_rcu(&handle->d_node, &dev->h_list);
$ a$ d& K. ~' l2 E* M        ...
4 A6 A) N) G6 k& W5 U
  M( M: `( D0 f: \& L        /* 把handle->h_node添加到handler->h_list
: v, }* t6 C# C/ ]' N         * 这样,就可以从handler->h_list找到handle,进而找到dev
1 ?' t$ r; ^, R' v         */
- y4 V/ Z" j/ I        list_add_tail(&handle->h_node, &handler->h_list);
" r! o; J& T1 n! a        ...
8 s6 q4 H- |1 i        return 0;
/ P1 S5 i* S& J) O}
3 [, m* K! g0 j! T2 O( S! U7 u& A- y小总结:
; f* ?9 _  x0 {怎么建立连接connect?% Z5 O, [5 _" y# C) b; V3 ~
1. 分配一个input_handle结构体
; n( |4 i$ Q. r7 h4 K2.
/ u/ A$ v! O; o/ h1 {input_handle.dev = input_dev;  // 指向左边的input_dev1 \2 C% C) X2 _4 N2 B, N
input_handle.handler = input_handler;  // 指向右边的input_handler0 S' [; \' E5 r+ O1 l- C0 {; b
3. 注册:
* z4 _5 M2 r* B0 p; @/ Y   input_handler->h_list = &input_handle;% I% y3 _2 Y: t2 N* |+ K
   inpu_dev->h_list      = &input_handle;
# F7 |3 ^/ p) ]/ Z: K' N. p& t  p9 h! {5 t5 G6 ~" U
六、怎么读按键?4 ^" r4 E3 \# ]. O3 i" G
) F! c( I0 J2 @/ z& A
答:举例,evdev_read
5 H" z% |- S% J$ D. \" O+ V) y' d" V: I# I" Y7 ~$ Q* K3 p
static ssize_t evdev_read(struct file *file, char __user *buffer,
$ l; L+ b% I( q" [- G& d                          size_t count, loff_t *ppos)+ Q: m% f2 @! g+ z
{* x6 [5 n8 R7 X* f  d9 @
        struct evdev_client *client = file->private_data;' I- v4 Z" ~6 C8 ~7 m
        struct evdev *evdev = client->evdev;
! s) j+ ?! i1 W) `        struct input_event event;. z: C$ ^. P4 a9 ]
        ...
- a- j" C4 ?; a9 d
8 z5 ?7 G0 \( p" s0 b  o% W! I        /* 无数据并且是非阻塞方式打开,则立刻返回 */7 x2 p, v2 i! g, ?0 m+ \
        if (client->head == client->tail && evdev->exist &&
0 W: \/ f3 r* x            (file->f_flags & O_NONBLOCK))
7 a6 I  a4 x4 ?% N. Q5 N                return -EAGAIN;
' X/ N2 Z8 K- J2 J  H. \. ]* A; ]) |
        /* 否则休眠 */
$ G2 `( P( s' K$ \$ R        retval = wait_event_interruptible(evdev->wait,
; Z. I+ @* H! T5 F, f; P6 ~( G! t                client->head != client->tail || !evdev->exist);) E. \# g8 Q( a# h* O0 U9 l; O$ q
        ...
2 q* u. @  c( \1 Y% K}
  ~. l% f# ]' X  ]9 s问:谁来唤醒?% n& c" Y. [, b) _4 F4 p
搜索evdev->wait发现是evdev_event唤醒的% h9 ~2 q. Q7 m$ M$ _: j# ?# m
" W: Y/ k- E/ j8 l' r
static void evdev_event(struct input_handle *handle,
7 X. b( D6 Y# b" b5 a  u3 F                        unsigned int type, unsigned int code, int value)
; {/ M% b2 P7 S$ o% ~6 S{! @# f6 u) S. {( k9 `& \0 K
        struct evdev *evdev = handle->private;
$ m# k) a' M7 E& m; Q/ }. j8 I        struct evdev_client *client;' w4 {( ?) E$ W) ~$ O) K: S
        struct input_event event;
2 j1 ^5 [2 E5 _2 {        ...# ^$ J2 d: C# b- |9 N3 Q4 R% Z
        /* 唤醒 */
+ s5 \* s% Y# \8 D0 ~) f        wake_up_interruptible(&evdev->wait);
6 e1 Q/ J- d4 w- _/ ]2 j}5 F0 g8 m( F" Y  p% ^
问:evdev_event被谁调用?* v! F4 L. Z7 Z2 L# C% c- S
答:应该是硬件相关的代码,input_dev那层调用的在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数。: J5 R5 k5 R- C5 Q  Z3 ~0 y: q! p8 _
3 Q! q% ]% s! @- y2 |
举例,在drivers/input/keyboard/gpio_keys.c里的gpio_keys_isr函数% y( l4 G9 S4 _4 X( q% x1 L  M; D
4 T7 q$ g' a7 L1 T  g2 x# y0 L/ ^
static irqreturn_t gpio_keys_isr(int irq, void *dev_id)
, q( }1 F9 L) w{2 j0 B0 \5 z. c. q/ s
        struct gpio_button_data *bdata = dev_id;
% s- X7 R; U4 B8 ?        struct gpio_keys_button *button = bdata->button;9 ^& {& P3 v+ F9 d
        ..." |5 t" T6 ?5 _
        /* 上报事件 */, y/ L( |" K! R& l4 H3 S
        gpio_keys_report_event(bdata);) J7 s1 Q; ^; B6 h5 ?
        return IRQ_HANDLED;% e7 v$ M- b! X* R1 E' m# T6 ~
}0 F" ^" P# r2 B$ B4 Y
gpio_keys_report_event函数
" q; v9 |+ r* R# T4 S; C$ K: m! X% I5 G$ s
static void gpio_keys_report_event(struct gpio_button_data *bdata)
% W, d3 n+ X9 O* K( n{
0 N+ X1 c+ F& b# C" h1 Z" b4 k        struct gpio_keys_button *button = bdata->button;3 O' H% O( n- E4 I% B
        struct input_dev *input = bdata->input;0 `; ]) J6 d: q$ G7 Y% G
        unsigned int type = button->type ?: EV_KEY;( @! P, M6 w' P4 f0 z- D
        int state = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;$ B) N/ V9 K. `  O8 j# `! l

/ l& Z1 m6 L. ~& H( u        /* 上报事件 */. m* c5 C1 P- `9 l
        input_event(input, type, button->code, !!state);4 M8 `3 K. W) I7 w
        input_sync(input);
* M1 C  \+ Q( k* \* `) b}: o" ^! v0 v- T
问:input_event函数如何上报事件
+ M* S" N! w$ q( h$ N$ w
0 w( k- X- O2 l- {  W- w答:
5 N% p8 r2 q7 |" w
( ?: C" N6 O9 p# ^7 `! oinput_event-->input_handle_event-->input_pass_event
- x$ |' D* S  i" e$ Plist_for_each_entry_rcu(handle, &dev->h_list, d_node)* e4 P+ \) i8 ~! }7 _4 y
if (handle->open)5 _7 M) [1 f$ e( s4 O
handle->handler->event(handle,
( u/ O5 g0 `6 Z4 C1 j3 otype, code, value);8 M8 y6 W8 n- B- i/ b. O7 O
怎么写符合输入子系统框架的驱动程序?. @- @! R8 U3 K$ `# b$ }
$ |3 D' @) `2 L
1. 分配一个input_dev结构体
( L; q6 j1 G- [- s& F: K# S2. 设置! _- `! e: f* u; c' H  X4 A
3. 注册
' M* C$ s+ f, I, R+ Q8 g4 m7 p( }4. 硬件相关的代码,比如在中断服务程序里上报事件7 K; N: {1 i1 u2 Q- H$ @# x
# h  d5 [( Z( p3 f

5 f4 ~( y# |$ y7 }  P; V7 O& E+ V
( |# m0 W4 Q( N, c3 m/ F+ B$ F0 c* r5 u* ?
9 p9 N7 N. ~& R# H6 u6 o: j

该用户从未签到

2#
发表于 2020-6-30 16:33 | 只看该作者
linux输入子系统
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

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

EDA365公众号

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

GMT+8, 2025-7-2 20:21 , Processed in 0.093750 second(s), 26 queries , Gzip On.

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

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

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