你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

Ubuntu下开发STM32---5.使用串口Part1 精华

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑
( W" i2 q. N: Q* ~  S0 E
3 g$ U& p" o: T- U  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。
* n/ f/ j/ E0 }3 l8 y" ~: E
; a' w! j- M3 V使用ringbuffer6 u- U0 w) a, A+ o1 C4 }8 A' ~
这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。) `" r6 A2 F# g4 D0 f0 R- P( F
struct ringbuffer
1 R2 x; ?# i# T  l2 \: B; e2 X{
0 B' Z1 \7 s0 ^& j    uint8_t *bf;
! ~6 v  O$ A5 Q3 p8 {5 H    int len;
. X' Y+ D, S; s) `1 z    ' Y6 L4 y& e% ^9 r
    int count;; R- l1 S) H9 a5 C% p1 c
    int putidx; /* read index and put index */
, u- k; E+ B, `& v2 Z    int getidx;
2 g7 D4 d) f/ ^; `! L; X4 W};

* m4 C7 d7 U  @struct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。9 W: {/ q% [: O
#define DECLARE_RB(name,len) \: Y  Z/ l+ y% O  ]( Q# i
    uint8_t name##_buff[len]; \
& F5 k6 R, n' i; K) R; `4 J4 K    struct ringbuffer name = { \3 V5 o, J8 _2 ~# R6 {
        name##_buff,len,0,0,0 }
% O# V+ n( B& N, W, J/ V/ J
提供的接口函数:, R7 k: g- B8 b! h) D& ?
#define rbcount(prb) ((prb)->count) 3 |* s  w: v( k7 y! p* O% b0 S  S
#define rbfull(prb) ( (prb)->count==(rb)->len )- p& I: U; F# |: T1 m
#define rbempty(prb) ( (prb)->count==0 )
4 u. U  K7 J0 r6 @/ X- `( [5 D
) N$ e" `9 |( f0 ~- ?/* if ringbuffer is full,return 0.else return 1 */
6 y9 [4 }$ N8 l$ o5 i, d* Y/ @6 Qint rbput(struct ringbuffer *prb,uint8_t add);
$ ~* r( h) M( C: Z1 j7 T- J/* if ringbuffer is empty,return -1.else return bf[tail] */
' d3 j- y# ^, D- oint rbget(struct ringbuffer *prb);
% c0 B- Y: l" ]/ T

: ~" Q- ]8 b4 Prbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
# M+ W7 Q7 r  n- ?" x4 t# u* _#include "ringbuffer.h"  B# {. N1 s- O' G4 ^1 ?

7 P4 V! y: ?- D4 |% g% U, f' Mint rbput(struct ringbuffer *rb,uint8_t add)
5 U7 `. h# E. _1 K5 G# I{
, x) x$ E9 E) O9 f( S7 o& ^9 w    int curidx=rb->putidx;
8 \4 L- k' W" q7 M    if(rbfull(rb)) return 0;$ c3 l& ^+ X' F: Z& I
    rb->bf[curidx]=add;& o; C, |+ J4 A
    rb->putidx=(curidx+1)%rb->len;
" ]# l7 J5 P( A% P- z    rb->count++;
; W+ b6 C: y& E2 f( g! M+ C    return 1;3 \" s7 H# @) u4 |3 b0 B) R
}( x1 P5 o7 L; K. Z/ e% W0 e
: U! O! X% [9 R4 A- S) F
int rbget(struct ringbuffer *rb)" D7 b. J0 D4 h9 I! R- ]
{
7 P* @7 a6 a' n5 Y, t7 _: w* N) s! `    int result;2 d/ M. K1 z& A$ o. b/ Q
    int curidx=rb->getidx;
8 s3 H$ e6 d% s    if(rbempty(rb)) return -1;! {* h. L1 u- b- r
    result=rb->bf[curidx];
; ^& y; y1 v: V0 A, t    rb->getidx=(curidx+1)%rb->len;
6 s  |$ L! @* N7 w    rb->count--;
/ q9 f- O# F# N/ W* e    return result;
% X3 M' s. p% C& e; q* W. k: t9 T}
/ j4 }0 B) a' m  @$ h) m% K

/ A' I5 @: v  q! F6 [" \串口中断+ringbuffer) j2 i& ]% b5 A* v2 U! `: t8 S- W
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。& J0 I6 i4 y% T( u
/* USART interrupt handler */
" S  ?9 P3 c8 J2 q. G/ u; U: TUSART_Handler()
, T5 j- E$ b# e; z: t3 H{
" I# j) v4 H8 o2 a$ @9 K( D4 H9 }    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){* F, t/ t4 b; v. C  O: Z
        rbput(&rb_usart,USART_ReceiveData(USART));& i% H: e  |" Y' |
    }
. ~: X* \5 ]$ ^8 o. O}
. v& G3 v" l! e( M
编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。
% B5 x6 \! m6 ^& ~( `DECLARE_RB(rb_usart,USART_RD_BUF_LEN);
' X/ Z, O+ X) ^
& k; f% d% b! v/ e' Dint __io_putchar(int ch)
& N( B" E8 J( s% v, R8 Y{% z8 C) P+ N, N: J, Y* v- B$ w
    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
- s/ e% t; u: T9 U0 G  z7 _    USART_SendData(USART,ch);# w$ c  d6 v% Y! @
    return ch;
: p# C9 J/ z2 N}7 l. t4 x; ]0 h) c  a
% w: I6 L) ]0 {" d! K
int __io_getchar(void)
: Z# C) w8 D. @* ~8 r5 _{" W9 M) Q1 [$ ]9 A* g9 D( o
    signed char ch;/ l- ?# k( O. V" }6 [
    while(rb_usart.count==0) continue;
1 Z5 _6 f, H$ f0 v( c, q    ch=rbget(&rb_usart);: l) l4 v# f+ I9 Q# D6 ]
    switch(ch)" `1 G( v! Y1 c
    {
+ z, _1 C9 }' D2 B        case '\r':ch='\n';break;
5 {% G0 ^1 H& x! m/ o6 l. w" P        case 0x1b:ch=' ';break;3 C# ~% z0 l# f8 g% y. J9 ~  z
        default: break;5 D; c' o# G: Q: g( J( S: @
    }
; l- x" f. z& j" A   
% l! g+ x! @3 C/ U) o    /* echo it */6 I! O% t' V1 X- P- q
    return __io_putchar(ch);# F" i) r! g# R6 y) r0 y
}
9 D; u% j; |' S& ?9 b
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。
/ l4 \* }/ U! j) \6 l由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。6 h* ~& c1 I; B: [+ S: @! |0 D! B
1 j4 Y8 {6 |$ d+ K
在主函数中一直读取字符
/ q9 h5 P6 q# e) W+ A我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。4 N- f1 u. o4 C. N
int main(void): b% U* U! c9 ]+ c& v, ^: G$ G
{# x% I9 o; v' ~- b7 f
    const char *str="Try to enter something...\n";/ j- L9 M( r! {% f( r
   
6 @. i  S, J  s* M    const char *tmp=str;. M+ ^% @- F  O; }9 a; n
    for(;*tmp!='\0';tmp++)
, ^0 O+ X' z+ G+ o. k) A4 ~: {        __io_putchar(*tmp);9 y2 _2 ^  A2 a- B7 N) D/ x
        , e. f$ J) z; U) T2 [# S) h1 w
    while(1) __io_getchar();* S0 [( u" }" L( S5 l3 t
}/ x5 Y/ Y1 Y' J5 q4 ?3 @5 ?' D
8 H( L- H. a2 \6 ~& c: J2 D  H
void _exit(int status)
9 Z" c% q$ A. z+ i3 t{" s+ x% H0 H# ~& ]) j
    const char *exit_str="GoodBye!\n";" }$ Q9 b3 f8 L7 {2 K
    const char *tmp=exit_str;
4 W! p6 e6 h5 ~, V    for(;*tmp!='\0';tmp++)
9 g* J6 I; W" s% S+ z3 ~& m" @        __io_putchar(*tmp);5 ^: c( G' T' }( _: N
   
: }. p* w0 I* R. A/ [    for(;;) continue;
% y7 N/ r5 ]- C, G( Q. r2 M; i}

  K, E7 p7 W, P! b( M/ |* x+ s$ x6 @- D7 d" J% h7 }
下载到FLASH中运行/ V: O) h0 t/ N% M% H7 A6 U4 ?- w, K6 ?- U
目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。
1 `% A" G2 b/ D! z! T' v: F试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .
' k! w, n2 W' X, X* y5 q; i不管什么原因,至少能够显示也是好的。9 Q- [" K, U. X1 D7 ?  ~( v3 d6 b

2 {4 v9 `  b# E/ c使用gdb调试代码, D6 B* Q( N4 A6 V) P5 t. y- |: u. N
新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。" E: e) H9 B- A  H3 C' Y
2015-10-28 21:47:50屏幕截图.png - ~" X2 R) y9 A
在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
0 u+ E  w, ~7 `2 s+ Y: q# D  d6 K在gdb串口使用tar连接远程终端。如下图所示:6 a2 x; h5 w8 N$ B. u
2015-10-28 21:03:36屏幕截图.png
4 Z1 u" l9 L' x$ i, `tar连接完毕之后,使用load命令将代码下载至单片机内。, o/ h8 Q+ j: z

) y* ?5 Z1 k9 _: K9 ^) K至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。
; ~% n% s9 Z" o- B. @
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
      u6 Y, |* \4 B6 k
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。* X$ T% I, \8 X
2015-10-28 21:11:53屏幕截图.png / l) N2 O- Z6 g+ A, e, w
在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。
3 f3 _- C( w3 @/ t, H$ r" _
; [# G4 ?$ X1 p5 m' l 2015-10-28 21:15:25屏幕截图.png $ `/ J7 w: |  j8 D
在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。
6 M8 y- z* y; j3 V0 W, W8 ?0 X7 L 2015-10-28 21:16:33屏幕截图.png - i, g. w- k, t, r( H) k
2015-10-28 21:17:02屏幕截图.png
! A' r4 j4 A2 H$ L+ h在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。
( {$ d! _2 B" z+ M, t 2015-10-28 21:18:21屏幕截图.png 4 k2 i: `! D! D3 q7 J% Q: b
在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)1 P2 e7 A9 M0 F; p  n
当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)8 r0 M. {5 X6 p
% u( X* v* v* d! Y' {
2015-10-28 21:22:46屏幕截图.png
; f6 t/ C$ r* t5 w' U; }我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。; ?( y( j8 H9 E: ~
2015-10-28 21:26:09屏幕截图.png $ ?) }7 h6 ?; B5 @1 R% J
使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。
3 o' q# @1 f9 w
. O* Q" f0 B* }; u6 Q7 P如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。
9 ^: f, s+ A% b: m
( h' N' o9 ^: t  X4 c% R& O, j) a  A! a
( K" L7 f; t, O  o$ E# g
bug1) B. _4 |* x7 c$ e! ~
如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。8 h8 A3 R0 n8 y! \$ Q
重新make all,make burn测试。
8 x' t/ j( m3 I# G, V' R  T  [ 2015-10-28 21:32:23屏幕截图.png / y! P. y7 U# b8 }
问题已经成功解决。更正之后的代码在serial_v2.zip! o9 ~- i0 f9 ^/ A
( y& Z% e! D& s5 x5 N
bug2

9 Q$ e, g- c5 |6 Q( G& k4 {0 Ibug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。* Z/ n$ |9 k* g0 b, M
一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。
: S" `9 O- ?% T6 [( i在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:
' S1 _: V; _2 j+ J( i+ }! P#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )$ t, x" r3 W3 Z: a- e9 M
#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )

3 @4 b/ Y+ H' T% W# j/ T我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。
0 ?7 O8 N7 p7 ?# [2 x更新之后的两个实现代码:
& {4 D( H# V, _1 dint rbput(struct ringbuffer *rb,uint8_t add)
; F/ F; u' R- L2 h8 q, P{
5 L& M- S6 z! e2 h4 {& y    if(rbfull(rb)) return 0;
; g  [1 J8 C. ?- N; S! Z1 Q3 C    rb->bf[rb->putidx]=add;) [, s6 ~  W# p' w- r! ~- B) j
    rb->putidx=(rb->putidx+1)%rb->len;
' @, v) j! U8 V( C, o2 }) ?    return 1;
6 m9 j# `/ X$ b) w: {2 o3 [4 ~9 m}( I' Q$ j) Y, S- Q  C2 I

: R' b! D8 @, n, |int rbget(struct ringbuffer *rb)5 ~  E( E4 |4 b7 h  L0 H
{
+ y6 V% T9 q. y6 @& r8 `- g    int result;
$ Q* z( t6 U2 _) y: V9 }    if(rbempty(rb)) return -1;6 n# q' l9 S0 s  H1 I
    result=rb->bf[rb->getidx];
, u2 S7 n3 M8 o3 L/ w    rb->getidx=(rb->getidx+1)%rb->len;( X& _( O# K% v% U4 X  N
    return result;
! E$ w' v* D. H! `* Z$ E7 u) J}
! _0 m) `0 W  z
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断。9 L3 a5 z! \' U4 {
int __io_getchar(void)
( F$ z% d0 d# W* D3 j) i: _{
6 j" K* |6 u4 `& n    signed char ch;( Y) ~  l6 b/ m( u
    while(rbempty(&rb_usart)) continue;
' m1 `) r* C! l6 T9 |1 \" r    ch=rbget(&rb_usart);
0 a+ P, r4 F( m1 G$ b/ u" P    switch(ch)* L  m% A! ?" h
    {- W: M; [$ Z, V- b$ h
        case '\r':ch='\n';break;
! i* a* H5 f. p8 S: w1 ~# ^9 F        case 0x1b:ch=' ';break;4 i' H7 J2 m0 M, \$ M
        default: break;( M  B  C5 K* z3 n
    }
& D6 o) l! N* {- w2 @    - A& Z4 O  `5 s9 y
    /* echo it */% z' c( _' [, T$ L8 d7 Z3 U
    return __io_putchar(ch);
# z/ n' s8 Z7 c" u3 e2 t+ w}
  X/ a0 r1 J4 f8 s/ M
更新之后的代码在附件serial_v3.zip中。
6 o' a) _$ f6 i2 ?* o
  q" u6 G( d6 @! n
7 K/ i$ g" ~& Y6 ~  m) N, B! [7 R更多
+ Y/ a. n/ Y- a; P在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题
。
0 Q" [9 O4 ?0 j2 h: L4 S  G$ P* h/ k

- V/ |8 k7 a7 G1 c$ w7 g' N
; I" k9 `! a2 X- P- S

serial _v1.zip

下载

395.61 KB, 下载次数: 55

serial _v2.zip

下载

395.62 KB, 下载次数: 50

serial _v3.zip

下载

395.61 KB, 下载次数: 75

评分

参与人数 1 ST金币 +30 收起 理由
zero99 + 30

查看全部评分

赞 收藏 3 评论20 发布时间:2015-10-28 21:36

举报

20个回答
Mandelbrot_Set 最优答案 回答时间:2015-10-29 09:08:51
感觉楼主的 rb->count 可能不是安全的.- b4 S- [. n3 k2 k' @
考虑:
0 L, x3 t5 `0 {9 f5 Jrb->count--执行到一半,
% E, f* H  Z* I9 @此时进入中断rbput里rb->count++1 d& s$ O, v* @- ^7 `
可能会导致rb->count计算不对
: w; |# Z: b5 R' N1 y
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:082 V6 O8 v% i" G, Q6 Z" N2 }
感觉楼主的 rb->count 可能不是安全的.
. X, _  J* x3 t  p考虑:
# g9 O* v7 G* T; M6 t( r# J' rrb->count--执行到一半,

) L7 H, m. z1 E. h确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。0 R" w/ j7 G5 A1 `
还需要改改。
党国特派员 回答时间:2015-10-29 09:12:20
学习了。。。 blank.png blank1.png blank2.png blank3.png blank4.png
khadgar 回答时间:2015-10-29 14:32:30
QianFan 发表于 2015-10-29 13:46
5 t/ Y3 V$ H. H, k0 c5 b把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
) n$ S" t# S5 L9 ?1 r2 ]- W
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?
Paderboy 回答时间:2015-10-28 22:58:39
沙发啊。。。这个很有arduino的味道啊。。。
yanhaijian 回答时间:2015-10-29 08:36:02
环形队列很有用。
aabird 回答时间:2015-10-29 08:40:17
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
埃斯提爱慕 回答时间:2015-10-29 10:20:44
提示: 作者被禁止或删除 内容自动屏蔽
khadgar 回答时间:2015-10-29 10:49:01
Mandelbrot_Set 发表于 2015-10-29 09:08
1 G. U, K/ ^! ^7 Z. N感觉楼主的 rb->count 可能不是安全的.
) X1 y' ~0 ]) c考虑:
1 k1 _- U: J6 x! h- V: srb->count--执行到一半,

( S. s4 ?- h% n全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:40! P/ A' L  R# {3 r
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
8 ^0 ?- e% a/ s, k
用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:58
8 G: h# F5 I' M  l( K+ k0 R沙发啊。。。这个很有arduino的味道啊。。。

3 P2 M1 q+ }( B. o' R一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:58
* O/ P( W1 X5 @0 f5 }. F" ^8 G. n沙发啊。。。这个很有arduino的味道啊。。。
6 A: Y( c% E' u1 V$ _
只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:49
* _7 F  Y* E3 H  F2 O全局量要加互斥锁的吧
$ Y" ~# J8 F" G# x% O$ |6 L$ r, s4 r
把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:32: K) Z2 _7 Y* ~5 S9 W5 i- Z" \( D
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?

( f' O) C+ |* thttp://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版