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

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

[复制链接]

该用户从未签到

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

EDA365欢迎您登录!

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

x
在此文章之前,我们讲解的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux中断、poll机制、异步通知、同步互斥、非阻塞、定时器去抖动。0 l) v- _! C) h, D

) D, g$ B5 G/ q3 d3 D, d上一节文章:8 E/ u+ S( |1 ?9 R3 o, l
1 P3 b" e4 P7 j# z0 y, V

- f9 H0 S& A- x9 \' i4 j+ k在这一节里,我们要引入linux的分离分层的概念,linux输入子系统是一个很好的代表,在讲解如何编写input子系统的驱动之前,我们理所当然的要先好好认识一下input子系统的框架。
) \/ i  O! Q% b( r+ k8 q0 k0 Z- p: L( u9 @( z7 N' H
一、linux输入子系统的框架+ j7 a( K" h& o$ J

( Z0 ?- G; B# T下图是input输入子系统框架,输入子系统由输入子系统核心层(Input Core),驱动层和事件处理层(Event Handler)6 z8 {! p# O  L; i4 m) T2 k
5 l7 Z1 y3 Z) F/ s( S' e- _
三部份组成。一个输入事件,如鼠标移动,键盘按键按下,joystick的移动等等通过
, R0 `+ x  l: L1 U
6 W2 F6 Y5 s1 n) D) Iinput driver -> Input core -> Event handler -> userspace 到达用户空间传给应用程序。% _: T1 z. ~$ }: ~4 M

/ f; t( v/ O+ ?% l2 Z# z9 h+ k * L8 m3 p7 o) k, M( B) a" V
# _) B+ W. ^+ P* W, n' W; [
二、drivers/input/input.c:$ J( \& h6 w, ~- O

! M5 `8 [( ]) P入口函数input_init > err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
, S  b+ A( Y& K' _& k
3 d0 ?% J5 ]7 d1 {4 bstatic int __init input_init(void)
# k0 S$ `- l, p' m4 B% A5 g{7 z$ u& N5 h7 D$ |4 @& l
        int err;2 g& Y  y0 ~( Q$ }! ^( Z
        ...
9 C8 M# {- B, g5 D+ d( L1 n        /* 创建类 */- U" ^$ {1 p0 ~5 i$ U0 Z5 S
        err = class_register(&input_class);: n8 c$ V3 r0 S/ v% N
        .../ N0 F# Y2 W0 {1 h! [* O. @
        /* 注册一个字符驱动,主设备号为13 */
2 ^; o! \; ^3 ^0 l        err = register_chrdev(INPUT_MAJOR, "input", &input_fops);0 i. A! f' P0 c; c: m
        ...
3 e/ d3 I# i4 A. U        return 0;% E% R& k7 h3 ^4 G8 e; N
}
/ \5 C; z- K0 U4 t, n只有一个open函数,其他read,write函数呢?
4 J' _& H9 i) {/ g" sstatic const struct file_operations input_fops = {
# N4 t: j/ X' u        .owner = THIS_MODULE,
$ ~' L2 K, N3 a( p3 Y; q& l) p        .open = input_open_file,4 c9 y- v/ Z- ^0 Y
};
/ R2 p# F4 _( E7 X7 X" A6 j) einput_open_file函数2 ]: a- S) t! X, X
+ }9 e$ Y4 l3 B3 V2 J9 ~
static int input_open_file(struct inode *inode, struct file *file)3 b* `; l4 Y1 G, o
{( s9 b, L8 c! s$ M6 |0 Q
        struct input_handler *handler;
" M6 V  i* Y8 G9 G' A7 A5 J        const struct file_operations *old_fops, *new_fops = NULL;
6 K) X! j: \! K3 F        int err;
1 U- E( ~6 P; m# [) v, ~- |        ...
2 ?% N) \" p  }8 W        /* 以次设备号为下标,在input_table数组找到一项handler */+ ^; Z: V; `' P
        handler = input_table[iminor(inode) >> 5];! t6 S) B% t+ A- M0 y* _
       
" U  d4 y; l7 S- Z# E# r0 B! B        /* 通过handler找到一个新的fops */6 k8 \3 D, i4 {  [* l+ {7 y
        new_fops = fops_get(handler->fops);/ [* m. h0 w. m& B  a7 c
        ...
$ S1 R$ E# s8 o0 S+ s        old_fops = file->f_op;
8 P/ `0 M. m' k9 t% H1 I& v1 |. J" t        /* 从此file->f_op = new_fops */
* U8 W! [" M2 \4 b1 F8 s3 H7 Z/ V0 Y3 P        file->f_op = new_fops;
) @8 ]. T% p. c9 u( D8 t        ...9 K# }4 C1 f7 x4 ~; L5 b! ~
        /* 用新的new_fops的打开函数 */' F4 U0 R% n+ F1 R7 ]/ f1 e
        err = new_fops->open(inode, file);/ ?) W/ i% R% }- {$ I8 o
        .../ V% @: A5 P1 V9 A9 j: `1 r
        return err;
4 T5 P7 T" _5 e  ^( R- c7 X}5 m7 {: P, Z9 p: O
input_handlerj结构体成员
' C" O2 B0 E  U- p1 O4 {' J+ xstruct input_handler {
# c; C, c4 b6 F1 ]
2 C1 {8 a( f% ?; l% K        void *private;# z- [  S  m, _- l# l! Z
' t" o  H$ i% `9 ?
        void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);- o+ U$ L1 M7 L
        int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);6 j" h0 w: Q0 X
        void (*disconnect)(struct input_handle *handle);
3 q, V" q  J3 U3 c1 U" \* ]        void (*start)(struct input_handle *handle);
+ m! f; E2 E6 z$ J* o$ g: h
8 s/ e* _; j2 S7 K4 C+ K8 ]        const struct file_operations *fops;& n7 h" N. d" W9 O4 g" d; D: c' X7 ^) v
        int minor;" K5 u! o& r. s  ~
        const char *name;  }) h* p. J, G) `, g) B
) H4 J! L; t3 E/ n2 M
        const struct input_device_id *id_table;
  F. O1 D, Y  Q5 R2 M        const struct input_device_id *blacklist;
$ {- Q. I" p( ~0 v! @' g4 S; B4 ^; `  W9 k/ h5 b, s6 e7 n; J
        struct list_head        h_list;0 ]) Q: b6 w. e. G7 m
        struct list_head        node;% n* U3 n$ E5 t: v5 Y5 L. C
};' s8 M/ V, M. g& n+ |
问:怎么读按键?
4 v# t5 c* e9 `APP:read > ... > file->f_op->read
! d4 v" A$ e& x5 M5 s8 J! g4 U
" l; f" m0 ^' C0 U" Z问:input_table数组由谁构造?
" x0 j' E7 F$ g+ c2 g4 |  Y2 b- E
答:input_register_handler- b7 s0 [  K; U) d4 }
" w% l; X8 _5 r8 O( x) R- o
三、input_register_handler函数(注册input_handler)
4 [1 f. w2 f7 _- e& R- z
! A3 z+ K- Q+ _8 ]
# m- ^: s1 q( H' T2 {! s. N8 Jint input_register_handler(struct input_handler *handler)/ m' l" I) ~$ S" c/ x
{
; _: B% _- S# S        struct input_dev *dev;
: W& _/ W8 S# ^" `        ..., f$ s0 X! |$ F& t0 L; R  D
        INIT_LIST_HEAD(&handler->h_list);! `( w. \4 ?: H8 ?5 s8 I
        ...* V9 o( E) {, _8 u% E- |
        /* 将handler放入input_table数组 */
/ d# t# ~% ]1 M+ w( U6 _        input_table[handler->minor >> 5] = handler;
/ Z! d/ e% S! Y' F5 y/ S( q) P        ...7 F+ b3 Y# C( q
        /* 将handler放入input_handler_list链表 */
7 G0 E/ H) @/ k8 Z8 ~; d; j        list_add_tail(&handler->node, &input_handler_list);
% }4 n+ \( q6 L. ]$ F( }        ...
; P2 M# J$ x  S# A5 l: l        /* 对于每个input_dev,调用input_attach_handler  _/ H. s' ~7 G& {+ D
         * 根据input_handler的id_table判断能否支持这个input_dev  w7 M+ L9 z( h3 {+ A' s
         */' I1 U0 g7 K' `/ A9 g4 m
        list_for_each_entry(dev, &input_dev_list, node)
. W2 @+ W3 F& R                input_attach_handler(dev, handler);
4 K& v% E" _* c6 H" Z3 r, Y9 z* l" U        ...
6 m$ m  X+ E& r" ]$ ~( Z! C}
0 R8 B; d8 O" @) y6 |- c) t* J6 W9 n- ^( T; E2 y
四、input_register_device函数(注册inout_dev)
/ f1 \3 O. L8 V8 n% N( Q
+ \0 H' l/ A( ^9 ?3 S% Jint input_register_device(struct input_dev *dev). b" Z6 h, V% e/ n# l  C( s
{
7 J9 G& c# M! v+ j! a: z0 K+ w        ...( O6 C* X% n5 ~' |& ?
        struct input_handler *handler;
1 `. |. n* X2 I# \        ...( ]: A5 h& F; \" t5 Z( W
        device_add(&dev->dev);& o) U( k7 p& L' \$ q" E0 c2 t
        ...
  y2 y( P2 U% {/ r# u- q        /* 把input_dev放入input_dev_list链表 */
; U* }0 n6 w2 k7 l        list_add_tail(&dev->node, &input_dev_list);  l4 `+ G- Y! n- A8 V& {
        ...
2 b4 n, z+ T5 `! l        /* 对于每一个input_handler,都调用input_attach_handler
+ T# L/ l) C" E8 R9 t: `         * 根据input_handler的id_table判断能否支持这个input_dev
2 f" `- U! `' P$ x         */
! x6 l  S$ y- s2 b) s        list_for_each_entry(handler, &input_handler_list, node)
( k4 s! v6 m6 y7 ~) R                input_attach_handler(dev, handler);
* b: c! K' m5 s& b% u! a9 o        ...1 @; l" W/ F5 z: X
}* F8 z$ V: n# W2 D
% j* _* e0 v7 B6 U# X5 A) `' x
五、input_attach_handler函数
( t7 K+ f6 ?! Q: ^* J* Q7 Pstatic int input_attach_handler(struct input_dev *dev, struct input_handler *handler)# W( U" p# T! s# z  \0 Q
{% D# V4 V* G8 D0 C* ?" }
        const struct input_device_id *id;3 P& C8 g- ], X0 }8 ?8 P$ U
        ...- v/ a9 h  x- y
        /* 根据input_handler的id_table判断能否支持这个input_dev */
* i* O; r' b! `; ?3 C; l7 h  f        input_match_device(handler->id_table, dev);
& m% p  C3 ^( f' J5 X6 C        ...  O0 D5 L; j9 B3 J% `/ x
        /* 若支持,则调用handler的connect函数,建立连接 */4 h) U' R$ g" U* W
        handler->connect(handler, dev, id);/ o' e2 W2 I6 _/ K: W0 C: D3 Q
        ...
* J6 Z2 C5 U* s, E}
% l8 s$ P8 V2 r$ C/ A0 ]& R! S" B% B
小总结:6 Z9 K* n" O' Z, q6 W3 {
注册input_dev或input_handler时,会两两比较左边的input_dev和右边的input_handler,根据input_handler的id_table判断这个input_handler能否支持这个input_dev,如果能支持,则调用input_handler的connect函数建立"连接"。
' E( ~7 G& w1 m9 }' x7 o7 m+ g7 N0 i
问:如何建立连接connect?
! o1 v5 E& T! \0 _( ?, C
- d9 [+ B6 j9 {8 w. f- E% ], X答:举例,evdev_connect函数4 I! @: I' a: a
4 o, N! N+ N0 N$ `# b( P* c& i

! M* w: i" [* V& pstatic int evdev_connect(struct input_handler *handler, struct input_dev *dev,# J( ~- L$ Q2 H1 u9 {) N! i
                         const struct input_device_id *id)8 N8 Z: v, B/ d; g
{5 y6 k, w4 t9 F
        struct evdev *evdev;8 R; Q/ F6 M( a
        .... K8 Z* _, {6 `+ r8 {5 R+ x2 c2 [# f

) c% x  |% w) C% k3 W3 O        /* 分配一个input_handle */
. T7 E: L% z" ^% g0 F3 W/ X        evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL);
% {, }- M# f* |* {        ...( V; V1 K; }1 o" B9 X: m
        snprintf(evdev->name, sizeof(evdev->name), "event%d", minor);
3 k( G" I; U+ F. q$ d/ C9 g        evdev->exist = 1;
3 w: m) P/ c8 o9 u, }) ?4 a        evdev->minor = minor;
3 J0 q( T+ }# z4 @" z% X1 A% m# g% L, _1 P  i; S% o
        evdev->handle.dev = input_get_device(dev); // 指向左边的input_dev
  Y4 j& p0 V0 |2 _1 U; v+ z        evdev->handle.name = evdev->name;
' E  q5 Q, z6 s        evdev->handle.handler = handler; // 指向右边的input_handler
8 w7 B4 _! X5 v3 v5 C        evdev->handle.private = evdev;
$ ^: @& v. \8 p
! b; a6 |- y8 a7 a/ J( f        /* 设置dev结构体成员 *// K. H- S  C7 a7 [7 z7 F+ v  q
        dev_set_name(&evdev->dev, evdev->name);
4 u+ J, v8 s- N; k! e. b+ t( M        evdev->dev.devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor);
& \6 p" m+ N8 r* `) h$ }        evdev->dev.class = &input_class;
$ U0 ?9 a, I, {1 Q- b0 I) o4 s        evdev->dev.parent = &dev->dev;
' K6 d. o1 K$ X        evdev->dev.release = evdev_free;
+ d' |% \7 z, i2 `. a        device_initialize(&evdev->dev);
1 b+ n6 R; S& }) Q
8 i4 @8 j3 |4 x0 H. c& ?        /* 注册 */; B' h2 ]) h, _
        input_register_handle(&evdev->handle);
; s" A: A1 X- W4 G& i% a- v# A* U        ...
( ?7 N8 V4 c9 k- a7 D}
/ I" P  ~7 S# G4 Q% \! c& h5 ]input_handle结构体成员' q0 r* Z* K4 U  N/ E0 r

' t/ ]* C, c9 J8 J( b, L  ]struct input_handle {: U+ l+ X8 N# H8 B" y

4 ?) }" H+ w/ q        void *private;
& x6 Z0 @5 b/ q* Y4 i6 ^- M$ I2 t0 O
# D" _, F1 h& R& N% J7 R' r, q0 O        int open;
/ |; A" I9 o( G; R+ V/ y        const char *name;
+ G" j5 f% ^+ j% t8 V' T" h
, N0 ?8 [2 c8 V& j3 ]- {5 R# f! }        struct input_dev *dev;' M8 ~2 p( o5 E! E0 g5 n
        struct input_handler *handler;3 U5 n" L! f" ]; S) }6 l

8 E$ b. u9 ?6 w; Q8 V% c/ S* h        struct list_head        d_node;9 k" b6 X! A8 q& }# ^3 y
        struct list_head        h_node;
7 z% a# I# G- d};
+ S" s5 e/ X& _' g$ {. `( E问:input_register_handle如何注册?, J# y# m; M: s- b) B6 x" H
. h4 {9 I: C( q' x* g3 S, m, |
int input_register_handle(struct input_handle *handle)
/ F# b0 a2 K- I! o0 ~{$ g6 i, e8 X" S/ R
        struct input_handler *handler = handle->handler;) }2 x' K5 n" {* Q. v
        struct input_dev *dev = handle->dev;
* i/ j, L3 Z. j- k3 y. J" L        ...
3 E/ ^0 @4 i9 N% t) b6 f0 A4 l7 V( R        6 J% O2 B7 \; Z$ U! V; y" C: j
        /* 把handle->d_node添加到dev->h_list
) I( K  ^; y0 t  n, Y3 ^         * 这样,就可以从dev->h_list找到handle,进而找到handler1 d( F6 B* |4 s; V
         */( |$ v; S. @) d; N9 D! i+ }9 u2 @
        list_add_tail_rcu(&handle->d_node, &dev->h_list);
+ i8 e$ |; }: }' [        ...
& c& u1 K2 a4 n5 e/ j$ }; R& A$ x' m4 U6 j; G: h# j
        /* 把handle->h_node添加到handler->h_list + N0 m9 T( J$ H& }* R* Z
         * 这样,就可以从handler->h_list找到handle,进而找到dev
. y. c  ?: Q9 Z, E$ |         */3 }+ D3 h) w6 X- e  j( e- k0 x
        list_add_tail(&handle->h_node, &handler->h_list);* B7 ?; d' J5 h  e" L
        ...: C1 x9 A" D$ i6 M
        return 0;8 G- t$ n' z# t4 {; `
}
% I6 V* v' G$ S( c* D  H小总结:
1 A% F  x9 S0 B* _  y& R怎么建立连接connect?1 a& C$ o+ H, J1 k
1. 分配一个input_handle结构体
# D6 _, b1 [0 l2. 6 P& n) _% N& h# K- ]; O
input_handle.dev = input_dev;  // 指向左边的input_dev; y/ Z* M  U  x  g+ Z
input_handle.handler = input_handler;  // 指向右边的input_handler2 \) {) j! d6 N- ]! Y
3. 注册:
8 q  G4 @/ n$ v' A2 z$ u1 n   input_handler->h_list = &input_handle;
; n/ ]) V; B) f9 M' A   inpu_dev->h_list      = &input_handle;# {# D& j$ G0 S
+ _- _6 M& \4 \2 B& M( S: F
六、怎么读按键?
9 Y! y/ j1 K7 B7 ~3 n; A# x! X1 o8 s9 w* @+ B2 D
答:举例,evdev_read
, I+ u, X( \: i" @3 D6 `  s& i5 h1 L5 N& ?/ m
static ssize_t evdev_read(struct file *file, char __user *buffer,: N0 m, e2 ~- C: v3 X; L
                          size_t count, loff_t *ppos)
8 ?: E- t  k1 ^4 k{
' f5 @; ?. C- l        struct evdev_client *client = file->private_data;" o3 ~" g3 D( H" Q: y& ~
        struct evdev *evdev = client->evdev;4 h8 l% ~, G) Z% x& V. T% ]- m
        struct input_event event;* z$ c# M. R' R
        ...
9 |/ h; O$ y2 T  H  Y* m2 X' W" v6 @. P4 ]& J! r' y! x
        /* 无数据并且是非阻塞方式打开,则立刻返回 */1 Q- O7 R9 ]! Z
        if (client->head == client->tail && evdev->exist &&
. T5 k* b$ ]) [1 Z            (file->f_flags & O_NONBLOCK))' y9 ?7 e/ A( e4 q
                return -EAGAIN;1 r+ w& g; i+ T) e
4 A9 R  E" a: v
        /* 否则休眠 */
' G' E! Q8 m" a/ V4 |) T        retval = wait_event_interruptible(evdev->wait,: j5 [5 f: V2 v+ l3 Q6 e
                client->head != client->tail || !evdev->exist);% e: v* X! O2 J4 I. y7 F! _
        ...
5 p# {5 y; r4 H( h. F0 f. a' f}+ v$ {% X" A: I+ N" Z
问:谁来唤醒?4 [$ V( p3 O! R5 s% S! e
搜索evdev->wait发现是evdev_event唤醒的( V: T, c% M" O9 |

3 @! F& P. S$ I8 Cstatic void evdev_event(struct input_handle *handle,
2 ?+ {$ O. C5 i8 X. x7 f3 a: h3 c4 N                        unsigned int type, unsigned int code, int value). L0 t9 y! n9 Q/ ?0 N1 v9 |
{) v% x$ p4 M, l
        struct evdev *evdev = handle->private;
, d) r* A' I6 u        struct evdev_client *client;8 y. ?8 J$ Q$ {5 V
        struct input_event event;
' L7 D6 u3 m9 n8 ]- z2 f6 q" Q9 {0 n        ...
' u* h& V; n# B        /* 唤醒 */9 |2 z# a6 p/ t2 i# |% w
        wake_up_interruptible(&evdev->wait);
1 b, d8 y6 p3 {, l}
0 }; S) @( M/ [  V问:evdev_event被谁调用?; x$ B$ M) u6 c6 U( g: }" b
答:应该是硬件相关的代码,input_dev那层调用的在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数。/ N5 l8 l. q3 }

3 D2 W0 a9 z* }3 Y$ \举例,在drivers/input/keyboard/gpio_keys.c里的gpio_keys_isr函数
; @! _. A) t* Y: ]3 B5 [" H' o7 a% o: @) G0 [3 d; d+ q
static irqreturn_t gpio_keys_isr(int irq, void *dev_id)
9 |% Z6 D# J- C8 v{
, B- H  k" U5 ?        struct gpio_button_data *bdata = dev_id;& n! R3 S* s3 ]& w/ M) O
        struct gpio_keys_button *button = bdata->button;
4 R- n5 m" v1 F# T+ v; {        ...
4 h8 }( c7 y4 w5 K0 S0 _! z( e; L        /* 上报事件 */9 n! x( \( n, U0 @0 N
        gpio_keys_report_event(bdata);4 a( P4 h; n: }  A
        return IRQ_HANDLED;
- @3 ?9 d5 u0 V8 j) D! ?& S}/ k: F& ?1 N- n) T3 J: t3 o3 ?
gpio_keys_report_event函数
  I( }6 F" {( ~- w) ~* r7 i
; q" F" b' a$ x! J; |* {static void gpio_keys_report_event(struct gpio_button_data *bdata)2 M2 M9 Q/ D9 D3 Z7 L
{
3 R: M+ V, _; G2 g% C9 ~& t2 z& Q        struct gpio_keys_button *button = bdata->button;
' k  c% M8 X( F, a- }; V& q# l        struct input_dev *input = bdata->input;6 [  ]2 G, x$ e0 X9 T* _
        unsigned int type = button->type ?: EV_KEY;
+ X) F; t7 A; ?5 c        int state = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;3 b2 I% [, Q7 S1 Q

: `  W+ `5 T1 p/ p        /* 上报事件 */
) n, N7 |9 o: ?+ B; [& c' J& D7 F        input_event(input, type, button->code, !!state);* Y0 }) Q: ?2 _( e' o& Z3 n! G
        input_sync(input);
, w& p( u! |3 i9 X) z: _}
9 I) H+ a7 t) I; h. S" M  D  Z问:input_event函数如何上报事件2 D( L$ t6 }+ l* D, p6 h, ^

& k0 ]0 C; }# @2 F; C0 \) R5 q答:
+ n* ^" L% a3 x% ~
7 }: _% X7 H! ]' F& ^8 cinput_event-->input_handle_event-->input_pass_event
: S" ~$ ]% Y- {0 m# B* ^/ Klist_for_each_entry_rcu(handle, &dev->h_list, d_node): G6 a0 ^  E1 a& i  [& g
if (handle->open)
/ m  r) X& [$ {5 I  A! Jhandle->handler->event(handle,! |8 ^6 `( d% C" |  X
type, code, value);2 h) W- H, {! ~
怎么写符合输入子系统框架的驱动程序?
+ I/ R& W/ s, c& B0 F+ j  D# i# n( ~4 e- w4 ^3 ]3 U
1. 分配一个input_dev结构体
5 f+ H! s/ x- ?: |0 H6 S% i4 j0 m2. 设置
& s8 h% o  M2 |) S! F6 m. c0 G) J3. 注册
6 _) l* Q' R. H& K! X4. 硬件相关的代码,比如在中断服务程序里上报事件
$ K% a9 ]* q/ e, n$ K- G+ P; P2 ^+ X( H

5 @6 z" z; m4 g% k  P' v. X5 b7 s4 N9 c# Y) [1 K6 Z

* {- w& C. Y) j4 g0 Y! h, [; D1 l; |9 k3 X( p; b; D4 F( T* d

该用户从未签到

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

本版积分规则

关闭

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

EDA365公众号

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

GMT+8, 2025-10-26 21:10 , Processed in 0.171875 second(s), 26 queries , Gzip On.

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

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

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