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

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

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x
在此文章之前,我们讲解的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux中断、poll机制、异步通知、同步互斥、非阻塞、定时器去抖动。
/ e8 d4 w8 {8 G5 h( V, I- @- O! t5 M! p# k- D
上一节文章:; ~6 l6 o/ W6 N9 K# \7 ^  D7 a) A# S

7 f( I) P8 p6 l9 u0 W. r* m$ j6 A3 n
9 d$ {/ i/ |0 h8 _7 i% }4 i9 \
在这一节里,我们要引入linux的分离分层的概念,linux输入子系统是一个很好的代表,在讲解如何编写input子系统的驱动之前,我们理所当然的要先好好认识一下input子系统的框架。
- T/ w3 D: E  u  c
8 y5 d6 R. G/ N) M! W一、linux输入子系统的框架
1 O; v% E3 E3 g: P7 d' T$ r
6 @) [- i( S8 V2 w' o下图是input输入子系统框架,输入子系统由输入子系统核心层(Input Core),驱动层和事件处理层(Event Handler)
# i$ |$ I3 ~2 Q+ ?( c+ h. `
& _' U3 s, r) L9 {4 P; F' E: N三部份组成。一个输入事件,如鼠标移动,键盘按键按下,joystick的移动等等通过8 t" D& D$ j  D$ ~5 \0 z, L1 n2 b
: K- H6 a/ V" q( X0 a6 I
input driver -> Input core -> Event handler -> userspace 到达用户空间传给应用程序。, a2 _; D6 v, o
9 |: v7 y" M3 z- N. v

, f' `. B) [- M* ^; B. W* N( [" k7 ]' d# g+ N
二、drivers/input/input.c:5 }4 H- y2 l+ K2 {+ f# Y
* Y4 c% N: o2 _/ g
入口函数input_init > err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
; f7 ~6 ~( @- F4 l) s
1 U8 y" J3 t' m/ `& Q; N7 Istatic int __init input_init(void)+ Y: F4 Q1 r$ F, A  w* Y7 M3 y
{% N* r* C5 p% o" n
        int err;9 a. c2 h2 i2 L/ A, e
        ...
+ B9 W1 V8 ?, M3 K        /* 创建类 */- ?' Q* o7 i$ R3 ]7 d- ^; w
        err = class_register(&input_class);
6 {: X- c1 F* L9 O0 P        ...6 k0 N" S4 E6 A
        /* 注册一个字符驱动,主设备号为13 */. x" V" a% u$ R" v, I; I/ K, z
        err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
! N6 D. N- w0 N2 d        ...4 D$ a. i  }* s5 V( B
        return 0;
3 s: n, w* B$ O: |- O, ]  h5 R% U' {}
# l1 _, `( Z8 F只有一个open函数,其他read,write函数呢?
2 P1 d) ~0 i% m* m; Q( h- {static const struct file_operations input_fops = {% U# U8 z* a) y7 v: _
        .owner = THIS_MODULE,
! M$ M- ^, d6 w. W        .open = input_open_file,
. D% Y" T9 D" E7 p: ]; W2 c};
8 j, N( v; I/ m3 U' L  D# ]1 zinput_open_file函数
8 P0 A: x% Q. |5 z' _! X2 F0 O3 R. l' I
static int input_open_file(struct inode *inode, struct file *file)
7 K9 `9 ^# Z: U' F/ X: @{
- N& \3 |0 c' _( O        struct input_handler *handler;
* c6 z, h. e) U/ n$ X& L- D        const struct file_operations *old_fops, *new_fops = NULL;
1 x/ J. p+ A7 O$ k8 O& x$ ^        int err;
! t, m! c0 q6 J& u: r8 b8 I        .../ `* C6 t! w$ E" J+ y5 N
        /* 以次设备号为下标,在input_table数组找到一项handler */
- L, z# |. \% }1 u        handler = input_table[iminor(inode) >> 5];
, l( c8 X( N1 o% X: c* I# {        8 ^$ ~  J4 V! Y* R2 c
        /* 通过handler找到一个新的fops */
% ~* ]- ^2 V+ j8 H/ f        new_fops = fops_get(handler->fops);/ ]* ]2 k" A: v5 \
        ...7 z" q( p" S5 g9 x7 ~4 }' m7 D
        old_fops = file->f_op;( ~5 Q0 l# M$ a: Y3 Q' l
        /* 从此file->f_op = new_fops */
3 W! L$ f3 _, ?' m        file->f_op = new_fops;
* T! ?1 J  O  Z, H        ...
& K  I0 v0 }  P+ v" C6 t+ x$ K3 O, k        /* 用新的new_fops的打开函数 */
2 I& c0 }& o# t1 m        err = new_fops->open(inode, file);. o0 {7 r9 L9 d% O- X0 \
        ...6 \! s. S  Q0 c' n! k! L/ R
        return err;
; ?* b. V, m! L}' [5 N3 `1 C$ ?0 T
input_handlerj结构体成员! t5 R* ^) w( c7 l5 a8 I" M
struct input_handler {& S) S8 u( y& [3 {7 q
. i) @% T. W( `' F& Q: X
        void *private;# a! [8 ?& e, h7 V

9 E; R1 H8 d0 R7 R# |        void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
8 Z- [/ L& m$ v) L        int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
4 T8 [( z) w4 q; p        void (*disconnect)(struct input_handle *handle);/ F7 q* ?+ I4 L$ [
        void (*start)(struct input_handle *handle);
. @: c' p# b6 b; w7 [' c- R: E1 Z* Z1 i0 S% ?
        const struct file_operations *fops;: [8 L+ Y) y8 o' ]; Z
        int minor;
8 F) G  h+ I! {: p9 k( W        const char *name;
( F' {$ {5 p  Z% D# k7 u; f4 ^, f; G: [
        const struct input_device_id *id_table;
2 i  e) y, o* o& g) T( r# K# [        const struct input_device_id *blacklist;$ ?% F& n4 _. s' C3 B
% `+ f8 w$ c7 l
        struct list_head        h_list;: K, C& u& \6 O7 a. {) y5 B
        struct list_head        node;
3 }( q+ c0 v7 s* d% F: @+ {};9 N: L) A) J4 M1 J! p: G. _
问:怎么读按键?4 @+ J" i* q# t: f- m# i
APP:read > ... > file->f_op->read ' ]9 R% B7 n- R; i) j* i+ I5 f1 E: W
* S/ h3 e  b7 f. D- \
问:input_table数组由谁构造?
1 V) m  [2 p5 E2 B8 Q, M3 L0 I1 K) y: |- n5 ~; L1 k6 ~8 I, l# O
答:input_register_handler
: G% _) v& H5 k# |9 ^! e  e& H0 C* W7 j4 Z: s: r
三、input_register_handler函数(注册input_handler)
3 v; d6 U5 i6 t' J
1 f8 Q1 y' `# D9 Z# s' B
3 }/ h) [: I( s. q# `int input_register_handler(struct input_handler *handler)
) l! q' g# z$ r: `  ~{! x$ ^# o6 I( V- S& V' m$ Y
        struct input_dev *dev;
( g6 E" B0 A9 K: g' \' l        ...
3 a, ?( o, z9 \: W        INIT_LIST_HEAD(&handler->h_list);
; u( ?. S  z4 k1 D        ...9 a6 r( D0 z( E) j- q* l
        /* 将handler放入input_table数组 */
; w  ?% c8 O/ O0 E. W' M        input_table[handler->minor >> 5] = handler;
9 J+ z/ K8 I" @$ O6 I        ...
! f, z- P. [6 r* I8 X- M        /* 将handler放入input_handler_list链表 */
- |0 j" b; {* r' n9 z/ [" X        list_add_tail(&handler->node, &input_handler_list);# ~* m5 B+ }' T7 u& A7 o, d. u6 ]. Y; Z
        ...' X+ T+ k2 B$ k/ c" g; y
        /* 对于每个input_dev,调用input_attach_handler
' e$ y) P) g4 h" C+ K  F& r2 {         * 根据input_handler的id_table判断能否支持这个input_dev; u' a) x/ T: m9 G" T: p
         */
2 i- }3 e# S, P7 S( V& N( ^6 g        list_for_each_entry(dev, &input_dev_list, node): X$ X; ]& p! E" Q
                input_attach_handler(dev, handler);/ J7 r. o" |+ K, X* o0 c9 ^
        ...
/ X9 Z5 L8 }+ b7 i6 I" X5 P' c, t}1 M: F" ~! y# ?4 M0 [: t4 v
3 b7 h4 Z- c/ a/ b( i
四、input_register_device函数(注册inout_dev)! `! H/ H6 K  W5 d5 S! U
4 f2 r) c3 m+ ^  B  H* n' H
int input_register_device(struct input_dev *dev)$ g& |, u' S6 t7 X
{$ b7 C3 v3 O- e# [
        ...
. c- p: C) R3 `7 d! U9 I7 K        struct input_handler *handler;
$ V; s' t" h$ }/ @/ h/ I) D        ...( s8 V: [: U4 E+ Q; u/ h
        device_add(&dev->dev);4 Y# j# w/ P" Q6 d! Q! q
        ...: Q" A6 {, P) f+ ~4 o( Y  G5 m
        /* 把input_dev放入input_dev_list链表 */
- n1 c9 ?' d2 c        list_add_tail(&dev->node, &input_dev_list);
7 q  w2 C* x, C) b- j        ...# F' O. ?5 k- ]) ?. @- z& N% v- A
        /* 对于每一个input_handler,都调用input_attach_handler
. J! F- ]" [# [* F0 h8 V% u         * 根据input_handler的id_table判断能否支持这个input_dev
+ X, y  @6 x6 G# F! G         */
1 c1 i! L8 j6 g. H; m$ j        list_for_each_entry(handler, &input_handler_list, node)
/ N! S) t' {" v6 E0 k                input_attach_handler(dev, handler);
0 \( j' D0 E; l0 r  k        ...6 J5 Y/ }$ m+ A+ X
}8 E* Y4 R( C+ {3 [
  [! S, j3 M- ?; G2 T8 a$ u9 B
五、input_attach_handler函数
+ O( r# ^; |4 I- a2 g. l+ estatic int input_attach_handler(struct input_dev *dev, struct input_handler *handler)
7 I2 J0 y6 F2 X; q{
9 Q: O' ~) D: S5 n" [        const struct input_device_id *id;
& f! y0 s% ^* w) O        ...4 b$ N( }5 d% V; r8 i) }: }
        /* 根据input_handler的id_table判断能否支持这个input_dev */2 V1 \% H3 R& T% T" Q# B
        input_match_device(handler->id_table, dev);" x2 @8 B# K3 g+ p3 c, x
        ...$ q! E4 |; e4 w$ @" r0 p0 r
        /* 若支持,则调用handler的connect函数,建立连接 */5 @$ v- d, Q# T) t1 Z
        handler->connect(handler, dev, id);6 s- l; a  Z) |) J+ ^, J
        ...
2 d! L* P! n! U& q}  k+ Q- e2 N+ n5 w* s
+ s% ~, u* }% {5 y
小总结:
% L# l: f& z& p7 [( a5 D3 t注册input_dev或input_handler时,会两两比较左边的input_dev和右边的input_handler,根据input_handler的id_table判断这个input_handler能否支持这个input_dev,如果能支持,则调用input_handler的connect函数建立"连接"。
6 v4 |3 ^% x5 W1 T: L
2 @. L$ a' {+ f3 f* N6 P( C# b问:如何建立连接connect?! E% p4 ~# E$ a
$ f* Y- m* w& m. ~4 k( F
答:举例,evdev_connect函数) I! W/ A/ q4 Z' @

! S, G; d, u3 w: W& q! o* d3 r; }3 q' D2 K
static int evdev_connect(struct input_handler *handler, struct input_dev *dev,3 m! h. ^& E. ?
                         const struct input_device_id *id)) o3 R$ a# Y. `& J+ d
{
) c7 m$ O+ e  _* u$ A) `: G        struct evdev *evdev;
  i6 c( I: C  q" S6 `; S) s        ...& T3 x* g: X- q' w) g% |9 r8 ?

+ K; z# q; s7 _0 G3 ^, m& G/ d        /* 分配一个input_handle */
' _) H0 Q+ s" w" B4 P2 K. u8 j3 s3 h        evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); 9 a6 o& a4 g1 a
        ...
7 K( F0 W9 T/ c' u) a0 Y        snprintf(evdev->name, sizeof(evdev->name), "event%d", minor);
2 u8 q# y1 `! V' k& [        evdev->exist = 1;, T: D+ q! h: C; ^6 Z4 \
        evdev->minor = minor;
) Y# E( k# \; R9 ]
. r: c& ~) S7 K: y; E9 Q        evdev->handle.dev = input_get_device(dev); // 指向左边的input_dev% B7 |0 {: H* a
        evdev->handle.name = evdev->name;
! `6 Y; d, Z% F; J% R        evdev->handle.handler = handler; // 指向右边的input_handler4 H( O+ g& T+ t# H! s0 j
        evdev->handle.private = evdev;  O6 W# f  I* _
2 S# f5 @4 d9 _" l1 \
        /* 设置dev结构体成员 */
9 G, r" ~, M) l6 g, R  W6 h, t        dev_set_name(&evdev->dev, evdev->name);
( |. t" l0 [1 j. e0 z. f        evdev->dev.devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor);
7 \. Y) w; O' u  |7 K# x        evdev->dev.class = &input_class;
' ^9 Z$ o9 [/ }: w$ G# @3 S        evdev->dev.parent = &dev->dev;0 `4 e! I! ^; O
        evdev->dev.release = evdev_free;" b" H% a/ P+ e
        device_initialize(&evdev->dev);# `4 F& x) l2 w% ]% @& t
. u; x# c3 l8 u: b: H0 h
        /* 注册 */- t/ k1 h$ p' ]6 f
        input_register_handle(&evdev->handle);
+ t% r, f) O9 h$ L0 E, j2 }+ B        ...9 a$ g5 z8 q1 n% t  J" Q
}9 e) U) x/ h/ j! o5 e! _
input_handle结构体成员
2 H. A  Y+ A8 @
( y6 h) b9 c1 m2 n/ k" B* x1 Wstruct input_handle {7 }" G3 s& w! z; l4 j

, ?- ?, |; j: M( J0 R6 n0 x        void *private;
8 s0 Z+ p$ I& h# p( a" H5 N2 x( G1 D# W5 c
        int open;& `" q1 `% J' r5 w/ ?
        const char *name;
: t0 K) `+ m% U1 Y" }1 X& l& R  o) t$ O0 N! a
        struct input_dev *dev;
1 `/ s* U" W: n) H7 [        struct input_handler *handler;9 N, J( U( `" i# H" \" q' }# z8 v

( X  z! v/ J7 ~5 y/ W; L# F        struct list_head        d_node;
! `; ^1 j9 r1 v+ g) }        struct list_head        h_node;
( T- q/ D( M  [8 l; J$ ~$ G};2 k0 o8 z# C) D9 j  {1 ~" N
问:input_register_handle如何注册?1 p8 D( h% d0 d8 w2 ?& `1 _, H

0 T  t1 e5 e9 l2 [) o' t0 sint input_register_handle(struct input_handle *handle)
/ W" z' \) f" \! d5 D{4 i5 @- M" @7 }
        struct input_handler *handler = handle->handler;# r+ r3 c5 M: o7 ]
        struct input_dev *dev = handle->dev;3 U* C  p2 Z6 ~
        ...
7 c% w5 O" o/ J$ [7 N        * O" r+ ]7 X- u! ]) G/ T1 z. ~
        /* 把handle->d_node添加到dev->h_list
3 J  E: d# O% E: F( h9 [0 H; n         * 这样,就可以从dev->h_list找到handle,进而找到handler# i- Q+ O6 f5 G
         */
2 j# g2 J' f' r. s  j6 N        list_add_tail_rcu(&handle->d_node, &dev->h_list);
- e# V8 R5 B0 l+ u2 v3 e        ...
( x" a0 {% o/ G* d' o# d( k; P5 T  Z2 x* B# ?& R/ k* m
        /* 把handle->h_node添加到handler->h_list % u* ?9 Y, d: ~2 C, T
         * 这样,就可以从handler->h_list找到handle,进而找到dev' N1 Y8 X" ]- _! C
         */
) h0 N7 F: A! Y2 X" \, V        list_add_tail(&handle->h_node, &handler->h_list);
3 W; j* p% ?, x2 V5 J        ...! h0 v& F! W  n& F+ M8 r
        return 0;+ x/ |1 x6 ^+ l
}
6 ^' P+ T' p" Y  \1 r小总结:
! }3 A5 y- H  }6 v8 Z' X! ~, n怎么建立连接connect?
4 C) e. k. R% `4 e. C& y$ M1. 分配一个input_handle结构体: d6 M! g7 \4 @
2.
* l. `2 s& J. t* i0 sinput_handle.dev = input_dev;  // 指向左边的input_dev7 l- Z& F+ ^" c/ Y. u: q8 ~3 Q
input_handle.handler = input_handler;  // 指向右边的input_handler8 h' I1 H1 i0 @( v
3. 注册:
+ j8 _2 _( G! }; t, M   input_handler->h_list = &input_handle;; l9 ^4 g& k- l9 O+ V* K* \
   inpu_dev->h_list      = &input_handle;
& E; v* m9 h4 V4 L, n! u: W* r1 h" e
& a, c/ Z5 Q7 X5 Z  q) z六、怎么读按键?! i. o  X5 ?3 i: U6 M  ]8 S
5 O9 o9 r& q2 G: O4 m
答:举例,evdev_read1 n9 ?" G% m# b3 v3 k

, a7 w- |) P9 L  O0 _# ?static ssize_t evdev_read(struct file *file, char __user *buffer,
! ^# k* B. D9 @                          size_t count, loff_t *ppos)
. X1 s& J5 e! ^2 V{
1 w: j; C* m7 ^# h        struct evdev_client *client = file->private_data;
' \. I/ C- P& L8 W1 J) ]        struct evdev *evdev = client->evdev;
3 D! n2 r1 P6 [$ c        struct input_event event;
+ ?" {" v  A" k        ...
" d7 P( ~% s+ G6 g; _$ _' c; r  D' d; s1 ^4 D. J. N
        /* 无数据并且是非阻塞方式打开,则立刻返回 */' B0 V3 g' v% Q; d
        if (client->head == client->tail && evdev->exist &&
: ?6 l' r& A: ^1 x! ^            (file->f_flags & O_NONBLOCK))
, ~% s& H- y. d                return -EAGAIN;) f9 u" e8 g& o* s) B) W

! s. ~& z: r- h$ ~6 O! J        /* 否则休眠 */3 Y3 M+ f- |. W4 V/ f; V0 l0 }
        retval = wait_event_interruptible(evdev->wait,
1 i( r) ~7 L0 S, F                client->head != client->tail || !evdev->exist);
1 v  i. a! p# @  H, n        .../ }. C( ?' h( P' d
}) E6 C! F. t/ A1 g
问:谁来唤醒?
4 s8 ~% L1 Z9 Y7 d" I7 r7 G: M搜索evdev->wait发现是evdev_event唤醒的
- _6 I0 i' t: C) z4 S
- }* n, Y) f" Z: b- O1 Rstatic void evdev_event(struct input_handle *handle,6 K0 n, w, ?" {
                        unsigned int type, unsigned int code, int value)0 g: ]" z2 u2 H
{
( v: z& F  u0 Q        struct evdev *evdev = handle->private;
% b+ G+ l4 p( I7 p6 v; f        struct evdev_client *client;4 k2 V; C/ J+ B1 q: y
        struct input_event event;* ~; ]+ ^$ X$ k' T3 B
        ...: n+ p' [" \9 \7 x+ X- }
        /* 唤醒 */
; z1 y' I7 w' a- H6 M+ M        wake_up_interruptible(&evdev->wait);7 g; M. f% Y( m# }" ?: p" ^; Z
}
7 r+ \! j1 P' @$ ^$ Z. w, ]问:evdev_event被谁调用?
5 _2 S) L9 r9 A# h) o1 x答:应该是硬件相关的代码,input_dev那层调用的在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数。
. y$ x+ U  B9 ?% ?3 Q9 \* Z$ ]5 R- O$ q  z8 B5 N5 u
举例,在drivers/input/keyboard/gpio_keys.c里的gpio_keys_isr函数
8 m! i7 I" A% n9 `& ~9 B3 i. v: }2 }/ H
static irqreturn_t gpio_keys_isr(int irq, void *dev_id)2 m5 b! e/ w3 T; r. U+ Z
{
: h! n8 }6 Q- L6 C; [, c        struct gpio_button_data *bdata = dev_id;, M4 x9 E2 l/ K
        struct gpio_keys_button *button = bdata->button;6 k: }/ `8 H5 e' `' {* H6 f6 E
        ...: d. j( k) w7 h+ z3 B  ]
        /* 上报事件 */
" ^6 f# B7 D- O8 B        gpio_keys_report_event(bdata);+ v5 S. f. r4 u9 V6 V4 |
        return IRQ_HANDLED;
/ f% i4 ]* x: _5 w! {* ?}
* K7 V7 t+ X3 m5 t& {# fgpio_keys_report_event函数
8 ~- q; }0 L6 s' G8 ~
0 M3 t3 S/ Y# _+ mstatic void gpio_keys_report_event(struct gpio_button_data *bdata)
, y9 I" B; m% R( `' J8 ~0 r{
0 F; X4 J3 A# B: F0 L        struct gpio_keys_button *button = bdata->button;3 b2 P. A4 \- Y
        struct input_dev *input = bdata->input;
7 o0 x. X9 p  _( [% m4 Z        unsigned int type = button->type ?: EV_KEY;" @! E& z. T# q/ A: r
        int state = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;
" P2 s' N9 ~. _/ a6 j% L; f
/ G5 S6 \* q/ j0 ^4 k        /* 上报事件 */- H. o. H7 n  l& T' {+ _5 W" w
        input_event(input, type, button->code, !!state);& A0 R- n" s9 y: M! ?1 N- C
        input_sync(input);
* G7 Z3 r1 F/ w, t5 l+ {5 t; ?7 K}- Y- @- X# B: L7 R# h
问:input_event函数如何上报事件
3 k% @5 E& O. J6 ]. ~  G
8 ^3 I4 Q, x. J2 C) y答:
7 B$ ]9 R; O) m/ }' |  u  a, b0 l! a, T' P: L: O/ }& M
input_event-->input_handle_event-->input_pass_event1 L6 [0 i" v/ f1 t1 W7 F
list_for_each_entry_rcu(handle, &dev->h_list, d_node)  m+ Y2 K8 w4 P, S( w% W
if (handle->open)8 Y6 O) o2 }9 a' J; B* P4 [
handle->handler->event(handle,
9 b$ U  ~7 }6 o% \& a# s2 g& |type, code, value);1 _& |1 \. |" w
怎么写符合输入子系统框架的驱动程序?4 ~/ x, A9 d4 ?  Y' F1 }
9 x0 M8 l. c" H
1. 分配一个input_dev结构体5 v3 {+ ~! }( h
2. 设置2 v2 V: N" b3 g* u# h/ R
3. 注册
* |/ `! H/ e+ @3 ~4. 硬件相关的代码,比如在中断服务程序里上报事件
" I& w7 ]5 c& }/ Y3 h+ [/ d
- |* r% W" a$ d6 x+ e9 D" u* l: l. W! W0 I( {

7 O9 C. ~: ]0 }- A: J* W( B
5 T  X: U' L0 ~: S( @3 _3 s
! P* B2 m/ R- h

该用户从未签到

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

本版积分规则

关闭

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

EDA365公众号

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

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

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

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

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