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

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

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x
在此文章之前,我们讲解的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux中断、poll机制、异步通知、同步互斥、非阻塞、定时器去抖动。
5 B, T! G6 j& B" f
$ m! I/ i8 e# m9 w: ~0 A) f7 ^: d上一节文章:
! x: b% Y1 w$ }" ~5 U
! G% F: c) _, m( _9 W+ K3 ~
. }+ V% J8 x# E1 f4 n
在这一节里,我们要引入linux的分离分层的概念,linux输入子系统是一个很好的代表,在讲解如何编写input子系统的驱动之前,我们理所当然的要先好好认识一下input子系统的框架。
; x: g. j- \% V/ r/ p6 y( W* H* u$ d- p- d7 M5 f  f
一、linux输入子系统的框架
. ^! e. N2 O0 j( a) c7 E  N
0 w/ |7 \7 G+ j2 e下图是input输入子系统框架,输入子系统由输入子系统核心层(Input Core),驱动层和事件处理层(Event Handler)% s; Z6 X, W$ f  w; ^; `3 D" A3 |

3 w2 m9 Z. G  a, e4 |# Y% Z三部份组成。一个输入事件,如鼠标移动,键盘按键按下,joystick的移动等等通过
' o- E6 c/ R8 z" [$ u" R2 h: F, K  Z( }- ?5 I+ I
input driver -> Input core -> Event handler -> userspace 到达用户空间传给应用程序。$ t. R5 W- o% Q4 c8 J- j) U

) f9 ^3 ^+ K& T6 K9 a8 d
8 `1 Q) c+ K8 _3 Y8 Y8 P0 S6 l! Z& J
二、drivers/input/input.c:% z4 z& u$ ?2 h2 Z, t8 ^

/ X9 i8 C9 h+ G: m% a2 T: z入口函数input_init > err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
% W# R$ w- t9 b( T0 X0 N
# \  n& v7 u5 K. [0 l3 c, hstatic int __init input_init(void)
* O$ J0 C: B+ ^9 w{
! ?) x1 B1 b7 m# `/ S        int err;
4 [/ |% J2 @% o        ...+ `' t' I& z) I" B3 F! L+ ^
        /* 创建类 */5 N  ^& r. H# {9 ]! |' e% r3 U
        err = class_register(&input_class);- F4 O3 O* e) j
        ...& i: d$ |2 @0 E! P- w
        /* 注册一个字符驱动,主设备号为13 */
- r! l; W( R" [        err = register_chrdev(INPUT_MAJOR, "input", &input_fops);  X: |% |" z/ X/ n& ]
        .... [0 k7 Z1 n: J& m: D5 t3 b
        return 0;4 B( h6 Q/ E8 R* |  a6 U" o
}
1 I9 S+ {9 x$ S8 E) V/ U只有一个open函数,其他read,write函数呢?9 I# d9 s6 T8 ?5 X6 ?. B* l" w) B2 e
static const struct file_operations input_fops = {! c9 W6 P1 }' _# r7 i/ I
        .owner = THIS_MODULE,2 \/ G/ ?. K: X( ]* D7 r
        .open = input_open_file,
$ `$ \- h) R: ^};; ?  @# s& w. b5 s, G  w" h
input_open_file函数; t+ Z# t  g0 h0 h2 a0 \

3 e* h2 f# O- e6 y* B' Pstatic int input_open_file(struct inode *inode, struct file *file)) i" G" G( a- s. R& M& g& r' m
{
& r% B3 A* `2 B& |) b. c        struct input_handler *handler;
% V# Q  a) |( m2 P! C        const struct file_operations *old_fops, *new_fops = NULL;
$ q6 H6 w6 K+ q& h, F! B/ D% Y# s        int err;
) q" m. e! ^. `5 U/ b0 Q& H4 X/ h        ...
/ O% q0 K3 G/ X! }) u        /* 以次设备号为下标,在input_table数组找到一项handler */
0 n" I( {) M, F$ _, l8 }        handler = input_table[iminor(inode) >> 5];
1 h8 F* v# r; g' b( F- Z6 C        5 S& _: I# L+ s& s" F% O& s$ L  W
        /* 通过handler找到一个新的fops */
0 a7 g8 [" ?+ Z3 V1 D: o2 y        new_fops = fops_get(handler->fops);
8 Z$ t# [+ z5 d, r- r1 h' I: X        ...3 g2 F! ]/ \7 l1 |7 Q$ O
        old_fops = file->f_op;
; g' w! Q, n* C1 x% e        /* 从此file->f_op = new_fops */+ m$ g5 N6 _3 o7 ~; H4 q7 }/ O
        file->f_op = new_fops;
2 Z$ F0 V3 M* f1 x* u        ...
) _% g" T# H& r( E0 v        /* 用新的new_fops的打开函数 */
5 ?! g2 T+ }8 [$ X! w5 l) U        err = new_fops->open(inode, file);* F6 @% S5 M+ v0 B% _2 e% `
        ...
, y: ~; n, z  ^  I        return err;
+ S# }# d( V" u}
; K3 `: T. J; ~4 s5 sinput_handlerj结构体成员
* e- W$ l9 i# ^& D  c9 I: k/ estruct input_handler {' i/ a: b6 ?& h$ Z( o
- [! F3 S( P, K6 d; {
        void *private;
  |1 h  g+ E* R' Y' ]8 B1 X
: P# h* L5 A* W! A        void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);' B; ?2 q2 q6 f, d6 N3 G
        int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);% Y; r$ v) ?' @& p: T. f; b
        void (*disconnect)(struct input_handle *handle);5 L+ R5 H% @, v2 B
        void (*start)(struct input_handle *handle);
5 d, j9 S  _. j& ^1 X, m. _6 B, {  l" q" H
        const struct file_operations *fops;
# }: Y" F) z( r; Y0 l        int minor;
% H; s; F! S4 z! v6 f; R( e        const char *name;
' _3 |& C% i# G+ q& C8 x! W( b* F$ M$ D, u" `/ n
        const struct input_device_id *id_table;
4 S+ |' k  L* B        const struct input_device_id *blacklist;0 Q3 y5 W2 ]* ^% F. G& I

* P: P, T8 ~! O        struct list_head        h_list;) j( T! o, K4 R  e0 w; Y" o
        struct list_head        node;
1 q; J) z# q5 `0 F4 H};4 X* G9 {3 k; g
问:怎么读按键?
5 q7 P2 u% m( Z* kAPP:read > ... > file->f_op->read 8 b+ G/ k! r8 X% t8 x$ V+ K4 W7 c" p
' p7 f, E7 h' |, M0 \
问:input_table数组由谁构造?  r$ n- [. M2 B4 j/ O# M
1 [/ l% C5 K. f. T; w, w
答:input_register_handler. J" }# b0 l, }  x, D
% Y8 g) I3 B- s& z
三、input_register_handler函数(注册input_handler)# {8 ?9 U8 B2 o: Q
, N: t! {( {& b( }8 b7 s5 Q* u/ r
* K9 {. }% t8 }% G5 U1 C. D. k
int input_register_handler(struct input_handler *handler)
- f, Y) R) b/ p$ f{( y3 ?$ d. k2 @
        struct input_dev *dev;& R% ?1 u- j% x
        ...5 d0 I- L3 K7 S1 P/ y" L
        INIT_LIST_HEAD(&handler->h_list);
6 u) B) Y# l" W4 b) V3 q  W0 c        ...
4 J8 \$ o5 K. @9 h        /* 将handler放入input_table数组 */
% i' i' G8 O5 t& Y( h# ]        input_table[handler->minor >> 5] = handler;
! a* @+ F# @& r' Y) W( @; a4 j        ...  x3 [/ ?$ [0 _
        /* 将handler放入input_handler_list链表 */
) W" B% H) D; \9 j        list_add_tail(&handler->node, &input_handler_list);
+ X% [7 q6 P- j9 i  [/ H' Z( b$ N        ...# J' W) v; K" a- a8 p; r# V% q
        /* 对于每个input_dev,调用input_attach_handler" q2 f# E" l' ^6 X6 H( |2 {
         * 根据input_handler的id_table判断能否支持这个input_dev' Y8 I* \0 B5 a! ?/ X" E3 }
         */2 _6 ]) x& B% U2 W
        list_for_each_entry(dev, &input_dev_list, node)' M( b- p: }# R
                input_attach_handler(dev, handler);+ B0 c- G8 w5 Q0 N  W8 D  k
        ...
0 z( d$ v; X7 B! w1 B1 F}
$ Z, T7 e+ v2 i' g% f0 @+ H3 J; o5 ?  R
四、input_register_device函数(注册inout_dev)3 u9 m9 ~2 n* ?3 m! P

9 ?) s  G9 W& j) M5 wint input_register_device(struct input_dev *dev)  D. ]4 c9 C! K. f; T  w( i
{
9 {% h1 W# R* z4 Q/ R' `        ...
/ v) \. X# L3 A4 F6 i" _2 C$ t        struct input_handler *handler;
( e  ^3 u( ?8 I$ Q4 C        ...
$ x8 G" Q- q+ ]+ {$ c$ T        device_add(&dev->dev);
' k8 P* x! S4 w9 Q- l        ...
+ `/ A8 v  P7 J& F: V# Z        /* 把input_dev放入input_dev_list链表 */
* J7 p# e" }" F  \        list_add_tail(&dev->node, &input_dev_list);
5 A8 c! K! `" @        ...
% y5 T# C! j, o. _        /* 对于每一个input_handler,都调用input_attach_handler# c$ F, A3 G+ M9 F% ^) p
         * 根据input_handler的id_table判断能否支持这个input_dev
0 I: v7 d7 Z- r4 |, x: t         */
" a3 g  k- y& B- z7 k: R        list_for_each_entry(handler, &input_handler_list, node)
* K& b9 Y( z. |, V; |" ~, ]7 Q+ i                input_attach_handler(dev, handler);
: n: Y4 o5 ?8 Y8 z        ...
; ]* P) u/ x! p( U4 l}3 B8 K/ K8 T: E( k4 {
5 ]' [  x+ [9 J" r- S
五、input_attach_handler函数
& z# H" z* W- n- M' sstatic int input_attach_handler(struct input_dev *dev, struct input_handler *handler)' {$ y: Y# V- }- q* U
{
# O) W- w0 ^3 H9 h/ f        const struct input_device_id *id;
  f& d6 x. h! P0 J5 h& l3 V% e6 w        ...
; r& ]# }7 J- }; d        /* 根据input_handler的id_table判断能否支持这个input_dev */
# T( h4 j1 o- T        input_match_device(handler->id_table, dev);
" A- C5 N5 c6 H, V        ...
& c# z) a& r" d5 |; [4 {4 Q/ {" a- v& ^        /* 若支持,则调用handler的connect函数,建立连接 */
; [5 h1 Q" K  |7 ]: {2 R        handler->connect(handler, dev, id);
* R& f" Y. E+ f7 {$ }        ...8 x- D/ d$ r) |' N  G
}  g. D; r6 E0 g# r2 k5 X- {

7 R. h. ^" s  w) j小总结:
# i1 V3 M+ H7 c* k注册input_dev或input_handler时,会两两比较左边的input_dev和右边的input_handler,根据input_handler的id_table判断这个input_handler能否支持这个input_dev,如果能支持,则调用input_handler的connect函数建立"连接"。0 g6 N6 H) [' W. c

, \1 ]' M2 y7 {( y: f. `# o问:如何建立连接connect?
& G" |3 Y( N: O- O3 j
( r5 y4 @# g' a/ t" @' Z  e! [% q" f答:举例,evdev_connect函数& o, e- Y: I6 q+ J" }
2 E+ ~1 L! l$ G7 C

' K+ i. y1 {7 J9 ?3 n, astatic int evdev_connect(struct input_handler *handler, struct input_dev *dev,4 r. {& r; [7 a7 k, _6 M6 s
                         const struct input_device_id *id)
% S. b& F. w, D! k4 o9 G{* s9 D: k0 p# a! G$ @2 p  E
        struct evdev *evdev;! b! `0 c! S6 Q- r# x1 E7 W) ~. b5 d# {
        ...
" ^6 I( o2 ]) r( a2 r" e
2 U1 ], q: Q, b7 e        /* 分配一个input_handle */
4 y" E9 H5 A5 o- C0 c4 s        evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);
2 z) Y8 p4 c/ I# f( G        ...! V) a% m  q& ~- I0 L+ g# L
        snprintf(evdev->name, sizeof(evdev->name), "event%d", minor);
3 ]$ x+ c3 G  Q2 d: W        evdev->exist = 1;
( _* \& a5 n; a3 j        evdev->minor = minor;* I) u+ k2 @: u4 L2 b
6 T9 K. y, {+ K+ d; X( f
        evdev->handle.dev = input_get_device(dev); // 指向左边的input_dev
) `" j/ c. c. e1 _        evdev->handle.name = evdev->name;
7 j. T# z% N1 P6 H2 E7 Q        evdev->handle.handler = handler; // 指向右边的input_handler
$ T: v: m# n6 S4 }3 v        evdev->handle.private = evdev;
% I2 B/ X/ V+ P; |7 i  [9 {" v
6 G# H$ a+ X4 N& m$ f. F        /* 设置dev结构体成员 */+ E2 M8 L1 ^! Q
        dev_set_name(&evdev->dev, evdev->name);7 S# ^" K  f( C& Z3 C
        evdev->dev.devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor);
3 Z4 u& S# p1 E        evdev->dev.class = &input_class;% m8 q6 ]& ~, n
        evdev->dev.parent = &dev->dev;
8 c- s: ^. J6 K% A* y6 o5 e6 J  f        evdev->dev.release = evdev_free;3 s; ?6 z: J3 d5 \" D+ X5 X4 Q' T
        device_initialize(&evdev->dev);4 b- `8 L) g4 |3 Y$ D
' Y, ~. E  d+ |9 I0 K1 P
        /* 注册 */7 l4 e+ W3 \2 v- {
        input_register_handle(&evdev->handle);
8 T& \: _% t' N& q1 e        ...
) R$ k, ]( P4 o# X4 Y! n( t% u: G}6 t3 S: }  H9 W& D2 ^
input_handle结构体成员0 ^2 n! F1 o2 t" w/ P

+ j$ C2 E4 \8 S6 i- E9 Xstruct input_handle {' Z9 H8 C5 f, a5 ?* ^' p+ ?- X

" M; h: ]1 J) N7 j- O: i        void *private;
. c  B4 C8 V( L" z1 o4 `2 z! b, d% C. _# b% Q6 m* \# K  p
        int open;
5 ~  z, N. S& G, {( E- [5 n' M        const char *name;- y) u$ t6 e$ \+ D+ m

6 Y. V( |3 G1 g# H        struct input_dev *dev;
; W9 i7 S& c, o$ C        struct input_handler *handler;
( v2 A$ d7 I8 D0 [& p/ `* a  W! T# K2 l6 s* a+ S
        struct list_head        d_node;
& O, V6 d. j4 k        struct list_head        h_node;
' f. \# O  s; ^1 O% `! C};* s/ N# o* `8 w9 F1 u+ l1 _% j
问:input_register_handle如何注册?
" \) Y. l7 T2 g+ v6 E& m9 {; w" Z; g9 S" t* P5 e2 T1 I
int input_register_handle(struct input_handle *handle)+ Y% S6 }  U9 n
{* U: U" y# H. n% D/ `- Z
        struct input_handler *handler = handle->handler;
2 t& V' g) w) @- l- Z1 l! u        struct input_dev *dev = handle->dev;
1 c7 x9 W2 h$ o+ ]9 v8 V7 k        ...+ `' L& s& }7 F( M* ^) p
        2 b" b% T3 b7 h" L  h$ q. ?% n* `+ h
        /* 把handle->d_node添加到dev->h_list* c  c$ g+ `# k1 e
         * 这样,就可以从dev->h_list找到handle,进而找到handler
) N2 j2 x8 D- ]* n9 C         */2 P2 ^6 ]9 @" a$ F
        list_add_tail_rcu(&handle->d_node, &dev->h_list);
& O8 s, V: j0 \! N9 U) Y2 s        ...4 X( M( D' E8 t/ z

+ [$ J- M9 A! ^! b" C        /* 把handle->h_node添加到handler->h_list
% h; v. O% k: T7 J. g1 T8 G1 R         * 这样,就可以从handler->h_list找到handle,进而找到dev4 Z' U" b) `2 P, p! P. M3 t
         */
  y  h& Y) z1 s  {0 z" B        list_add_tail(&handle->h_node, &handler->h_list);, B% J, r: }$ x" b7 l
        ...  V4 {/ V. ]" c  U1 F- M* s) s
        return 0;; Q9 r  b$ M1 F  }% n' C3 b" E( @
}' [2 X& S+ x" \, p' J' t
小总结:1 r# O2 X7 t5 c. P5 ?3 c# j
怎么建立连接connect?: X: p/ W& U# E* p2 B+ B. J  s
1. 分配一个input_handle结构体
. t+ o* Y6 z0 T* ^$ {+ ^$ j2.
; i: D: c9 }+ a# Y2 }& Sinput_handle.dev = input_dev;  // 指向左边的input_dev
0 A" G) j* g. ]; pinput_handle.handler = input_handler;  // 指向右边的input_handler
1 q7 p6 o) ?9 p3 v2 v+ N; H3. 注册:; z$ q  `, o. ]2 @2 ~
   input_handler->h_list = &input_handle;/ k: I" ?& A" s- e8 }9 E0 d9 Y6 D
   inpu_dev->h_list      = &input_handle;0 d, h: o3 z4 x6 Q4 P1 F" J! g& B
- N. b8 T3 D1 r" K6 ~0 ^0 `
六、怎么读按键?0 ^, {( j% H- V, t  Q, M' B
+ u3 Y. w9 e9 T# X4 T1 o
答:举例,evdev_read
) S! g: u" [1 W. U) c* m. F9 l+ p. n! o, Y0 U  J7 \
static ssize_t evdev_read(struct file *file, char __user *buffer,
, @9 t  A) Z) ~( ]                          size_t count, loff_t *ppos)! X! N( y# K, \$ O
{3 f- b: r6 _7 h. G# S$ {$ [! F
        struct evdev_client *client = file->private_data;) v* d) P7 M) J: B5 L; c
        struct evdev *evdev = client->evdev;
/ y; O! M! u5 |* S( V        struct input_event event;* ?+ q9 f. h3 M. c" m
        ...
; v6 \+ L" x' p7 h  }" E2 }; p$ z; ^) S- M) ~4 }; ^+ U4 U
        /* 无数据并且是非阻塞方式打开,则立刻返回 */! x" X$ W. M4 w
        if (client->head == client->tail && evdev->exist &&
2 W1 }7 |! B& x! m/ y            (file->f_flags & O_NONBLOCK)): g$ X: q, ]7 w
                return -EAGAIN;+ U- _8 b8 K5 ~3 j, T5 X- h9 q

7 W3 j% R' Q& F  }+ D  S6 G        /* 否则休眠 */
( m! c; L2 |* R- y# x        retval = wait_event_interruptible(evdev->wait,, T- C- U2 p+ y! F1 J
                client->head != client->tail || !evdev->exist);
* K3 c! l+ C, U        ...
9 x, _' W1 m' U! d8 f3 ^: X}& n, C  i" @9 E' ~8 |
问:谁来唤醒?- Y: h4 U8 e6 B. o3 i
搜索evdev->wait发现是evdev_event唤醒的
% f: d3 H2 C3 w5 ]. I- _* W4 }8 p
1 Z1 z: Y/ m* mstatic void evdev_event(struct input_handle *handle,% t$ E% \9 j- ]- j# ^7 }
                        unsigned int type, unsigned int code, int value)
: \) Z  {- n6 q, J{4 h# h9 ?, u/ z3 U* S/ d' Y: X
        struct evdev *evdev = handle->private;3 x+ Q* s/ F1 ?5 `+ I
        struct evdev_client *client;' Y* f( O/ F4 e$ g& s2 j4 n
        struct input_event event;9 ^1 F& x) E# Z( d
        ...
( v) `, A8 m( g1 [0 b) J        /* 唤醒 */
1 z( C& D. Z2 a  O5 ]6 ?* F        wake_up_interruptible(&evdev->wait);! @0 z3 _. U: ~9 g6 S
}7 h1 o3 F$ [9 M, m* x
问:evdev_event被谁调用?3 _/ D, \4 D/ f9 N
答:应该是硬件相关的代码,input_dev那层调用的在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数。
* F5 R, N5 t, ?6 D! n% R7 ]8 ], H/ S& q+ o2 i
举例,在drivers/input/keyboard/gpio_keys.c里的gpio_keys_isr函数
( L1 Y9 m0 [5 i4 o
( r& m9 d5 y0 Q& h' E* U' ystatic irqreturn_t gpio_keys_isr(int irq, void *dev_id)1 J9 ]( u: j( J. L
{* |# m) T1 D3 U. _
        struct gpio_button_data *bdata = dev_id;: t4 ~  {; [5 t' G
        struct gpio_keys_button *button = bdata->button;
9 m9 r7 N6 }8 q$ u# A# y        ...* |- v, \5 l( d- K6 c
        /* 上报事件 */4 u  P# Q# I! I. o
        gpio_keys_report_event(bdata);
7 O0 a! Y/ {/ |' `' `% j  E        return IRQ_HANDLED;
# @/ p8 B. I' m; T, p2 l9 v% g}' L1 L4 ?0 e: M8 F: z% q
gpio_keys_report_event函数; A! m, Q# v" Y$ f8 G  ?* e) G

4 ?  L; @! e' E/ J. \4 Nstatic void gpio_keys_report_event(struct gpio_button_data *bdata), S1 b" a% N" |( M# \
{
+ U4 C6 i6 p) |2 N        struct gpio_keys_button *button = bdata->button;
' |. c8 M4 l! Y" B# F        struct input_dev *input = bdata->input;+ ]/ ^6 A/ @) g+ P
        unsigned int type = button->type ?: EV_KEY;8 [0 Z* j; a* M1 Y/ E
        int state = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;
5 ]& P4 B/ |6 R! l2 f1 {* S! w- _" Z, p8 V: x3 L  e  o' w
        /* 上报事件 */
2 g5 J  N* _$ Y  Q# u        input_event(input, type, button->code, !!state);
) |1 `/ [2 [& {- u3 v% G8 \4 N        input_sync(input);
4 E' ~& }$ w1 y: M% [}9 M' T/ j- ^, F3 H; I( H
问:input_event函数如何上报事件* M' C8 c+ \; |9 [  F9 X  N) b5 b
6 l; r7 p3 O3 N" h
答:
- R* ^9 g/ ?$ q+ h: v( _- g2 F( m9 G1 {" B" E+ _
input_event-->input_handle_event-->input_pass_event& J) K' U7 J; W+ d7 d" Q
list_for_each_entry_rcu(handle, &dev->h_list, d_node)1 x. B, ]  o; C
if (handle->open)! @8 W3 K0 j) Q8 |) @
handle->handler->event(handle,! e0 y: E+ \! w
type, code, value);0 F/ O( @7 X( f9 |3 P$ V
怎么写符合输入子系统框架的驱动程序?0 m9 ?. t% O: E1 D5 u/ d
" {5 X6 w+ ^4 f" Y
1. 分配一个input_dev结构体
8 b; y7 e$ J6 m+ M, }2. 设置
1 {: x" ^! s! R9 n! {# \( K" p3. 注册8 Y8 y% I" y. d' B" p
4. 硬件相关的代码,比如在中断服务程序里上报事件9 @- ?/ ]/ c: y/ K

4 Q* ^/ D' q; n+ V3 r" L3 x" x; @4 q$ b* j4 ~/ s/ v4 [! q

$ g0 x" h# Y; [2 l
8 o' |$ F* {8 w$ v, V5 U: q5 @6 x0 ~' I4 E3 V

该用户从未签到

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

本版积分规则

关闭

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

EDA365公众号

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

GMT+8, 2025-11-25 12:48 , Processed in 0.156250 second(s), 26 queries , Gzip On.

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

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

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