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

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

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑 7 E* q6 G$ h. H3 d

0 v7 g0 W9 t& t  Z0 c6 w  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。
4 e6 @9 v* V! K$ ]8 @9 i$ w+ l5 z$ E8 P3 d0 P4 Q# q/ @0 W; V
使用ringbuffer
! e' m' h/ E' W8 h' F; L这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。
; Y! J! d# [5 x8 A2 estruct ringbuffer
0 N; I) e" r' H6 l; V{
4 i5 R; p- t9 g0 ~# V    uint8_t *bf;
5 Y6 x6 b0 q& P& [0 Y    int len;+ m: n; |. r8 z% x6 Y$ k) N: s
   
$ T6 w8 [3 M8 l# J    int count;
' X- U8 \! @) Q* F; q    int putidx; /* read index and put index */4 P1 F/ ~& @9 f, i0 g
    int getidx;
. x8 [. @7 C& I+ c6 D* S};
7 M" `, t7 R5 q* n
struct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。/ w! J# P1 D: E5 T- e( g
#define DECLARE_RB(name,len) \6 B) {' P- ?3 S! U
    uint8_t name##_buff[len]; \
8 F4 ~8 N: `% v; s8 g    struct ringbuffer name = { \  o6 V9 e- x6 v& @) M$ n
        name##_buff,len,0,0,0 }
# i# e! w7 E1 {9 q+ C
提供的接口函数:
" m! i2 C* t4 n1 ]; R#define rbcount(prb) ((prb)->count) ' _2 g; H3 q4 @0 j
#define rbfull(prb) ( (prb)->count==(rb)->len )
: v) M! T! U7 q/ H+ V#define rbempty(prb) ( (prb)->count==0 )
9 F. }, T' l. ^/ g9 ]$ [/ t; t# \" M( }9 j/ Y
/* if ringbuffer is full,return 0.else return 1 */1 f' C- t, a+ a
int rbput(struct ringbuffer *prb,uint8_t add);& ]( M' X/ x. E5 b4 H
/* if ringbuffer is empty,return -1.else return bf[tail] */
- Q- d; f2 L5 @1 T) S" [int rbget(struct ringbuffer *prb);

& u+ L2 D! L% T8 [3 u
/ T1 r' F8 I  ]9 p" T" i5 ?+ vrbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
% J" d9 \% _/ w. a; |#include "ringbuffer.h"
, n8 w' u# l! W7 q* r
  `/ ?! y! B% a2 L  G1 f* |6 ~8 m3 `int rbput(struct ringbuffer *rb,uint8_t add)9 f% {: B/ ^" |; y# M6 `- \! x
{
! R8 \9 P6 V4 \7 d8 D    int curidx=rb->putidx;0 f; J1 h- B; s8 X
    if(rbfull(rb)) return 0;
5 k/ F4 U( F, h9 I: L# {% a    rb->bf[curidx]=add;) A# q( O; w  M" T/ ]+ o0 `
    rb->putidx=(curidx+1)%rb->len;) S( X+ J0 s7 m. F6 C' F8 h6 n
    rb->count++;
$ {7 x4 U, Q0 L# c! A2 O- c    return 1;
# k: P$ q4 o/ e& A  R* |}  W' N' x- Z5 d4 M6 h# g
7 P3 |3 m. O% Y$ W( c# [/ w% n* L
int rbget(struct ringbuffer *rb)( c, q3 [% Q* o" e
{
$ }# {1 y- H3 x. }    int result;
4 @4 J% N& I8 _5 t# p8 j* t; m    int curidx=rb->getidx;* C4 l/ t+ i% _# _# D/ E
    if(rbempty(rb)) return -1;
. X& G4 p0 ?  L    result=rb->bf[curidx];) o/ M" N" k1 ?& |! D4 G/ A
    rb->getidx=(curidx+1)%rb->len;6 u# X( T1 n8 {9 t) d
    rb->count--;+ H7 S! g& A1 x1 z' `' S( e
    return result;
* ^+ o/ U+ C5 c  @  k) I# _}: E! G+ X4 k( _2 J3 L
6 f6 q) L/ w, j+ X7 @; ^: Z4 p4 t3 z
串口中断+ringbuffer+ I5 h! B( c: b: _% P2 c
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。
0 _8 l& {3 [& R! A9 }+ B& W/* USART interrupt handler */
# t. m( k: L6 W9 \0 QUSART_Handler()& S! `6 G: R  ]& ?
{- ~0 `. |) |: @* e/ G5 y
    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){
3 E/ V6 t3 y$ Y7 h        rbput(&rb_usart,USART_ReceiveData(USART));  g/ Y2 K4 s, V5 }0 m, s
    }
! ^2 x2 }9 t: L$ y: W}
3 u& l* [8 l6 c, c& R! d( @! F
编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。
9 `# q* s: f5 K# l9 a# X" m- u6 yDECLARE_RB(rb_usart,USART_RD_BUF_LEN);% l4 ~, n9 Z* x8 j4 A
& Y5 m6 a6 ?5 N2 j& i$ C4 k
int __io_putchar(int ch)* q2 b* p5 ~. k, d: U5 p
{+ C8 {' H' O& d0 j
    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
! k& @  q5 r& K5 I4 [    USART_SendData(USART,ch);
% p3 `" {9 s0 \6 \! `$ f    return ch;4 U1 E- q3 v; i6 I$ b
}
( v0 m& H2 Y) }0 B
  Z0 X, c( x) D: p& S( Wint __io_getchar(void)9 c* i0 R2 E7 \8 o0 x0 \. h
{. ]7 z: E) S: x* x+ V
    signed char ch;& u- e' g6 I& n, B5 x9 |
    while(rb_usart.count==0) continue;% g1 L3 ?! P9 W( ]6 ~, o
    ch=rbget(&rb_usart);4 p/ d" s. [) F2 K. }9 ^
    switch(ch)  p9 B$ m, A) _
    {
8 v1 D' F6 N$ m" b        case '\r':ch='\n';break;3 \( {1 F) t. y( p  p( c
        case 0x1b:ch=' ';break;, m* t7 {/ H7 B; z# t
        default: break;' }2 ^& E9 G/ ~, R6 ]5 ^7 C- ^' G
    }
& x( ?8 C- e2 z: g5 [' @! d' _3 z7 `    % Y5 G* R4 R0 h
    /* echo it */$ U. K, Y+ H9 d3 i  \  r: Q
    return __io_putchar(ch);' a: @5 I$ D1 @* ]9 T
}
4 x, N3 X5 j4 `, N
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。
' p( G" K! x3 ?由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。
$ X0 x# Y0 n/ d6 b
! Q$ N" k0 Q) Y" t$ P3 l在主函数中一直读取字符; T6 T! ]2 z% r# L
我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。. O: a& E0 H' ?3 t4 |5 J% y' }6 N
int main(void)
- @  Z8 H# C: l2 B; d6 J- M. h' ]0 s4 S{% D, X1 N0 e, t# u+ T1 i/ w
    const char *str="Try to enter something...\n";; u0 }  Y$ C+ o- r
    # d3 K! s0 O) o. I4 s3 t* ]
    const char *tmp=str;
; s* c/ J5 x' W! s% B0 k4 q    for(;*tmp!='\0';tmp++)
- c2 d+ _& q/ w0 i9 }        __io_putchar(*tmp);
2 X9 s3 k/ K( Y, w+ L/ J        # B! G# n  q4 v
    while(1) __io_getchar();
* U) p! S1 Q% J1 v, e}
2 G, `) A) I6 O6 b: `( \# O# ?: G/ [( ~% j! N  y. v
void _exit(int status)
/ j2 w% k2 w9 j) @% u, S) g8 B{/ \" m. \3 K! F( Z1 Y" ^
    const char *exit_str="GoodBye!\n";
- m5 `+ x: Q+ `6 R: \( G- u6 R0 E! L8 E    const char *tmp=exit_str;
% |& M9 e) m* N% |0 B    for(;*tmp!='\0';tmp++)
1 p# c# N; }  L: I2 _        __io_putchar(*tmp);( M) n$ r8 x0 Y# d2 @& A
    + a5 h/ C. Z2 M: F1 ?. A
    for(;;) continue;
9 d' l3 F/ `# f2 ]" P. |/ d, L: e  s) O}
0 k% N2 B1 k5 V0 S
) h( e$ L5 ]* O- s( Y6 J, O
下载到FLASH中运行2 N, |. w" ~( u! {3 A
目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。: x' k, I5 l$ K
试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .
! U0 E6 b2 t' U7 p4 l' O不管什么原因,至少能够显示也是好的。$ ]% Z6 o2 Z6 \8 e+ `* w- r

; u8 e  G/ y0 V0 {: e/ A使用gdb调试代码  C+ V6 X9 @5 ^
新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。
" ?! X) t$ M: r, |7 b) l9 w 2015-10-28 21:47:50屏幕截图.png   R( w" h3 r) m2 q8 v' z7 h
在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。4 V. n/ D) K2 ^" e' }; z2 [% a
在gdb串口使用tar连接远程终端。如下图所示:' C- C% Q, \; w, ]9 Y
2015-10-28 21:03:36屏幕截图.png
4 h! S4 Q9 s2 L+ F5 o6 star连接完毕之后,使用load命令将代码下载至单片机内。. _6 d9 ~3 S1 P9 W) ^

3 X- S. \. S# k. t5 D至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。0 {9 F  m. x# `6 o5 S
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
    1 J. V! x5 A  a" a; F& b/ y# y
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。
+ Q% U" A) s  m& J, {9 ?: `  L 2015-10-28 21:11:53屏幕截图.png 8 E) G5 r1 M( G0 |; W6 W
在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。: |# J; s% b* G5 d# {

1 E# ^$ Z( k6 ~& o$ x% [ 2015-10-28 21:15:25屏幕截图.png
2 P3 H5 z* ^8 a) g3 o, A% B$ L在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。
  q+ C* M6 A8 d 2015-10-28 21:16:33屏幕截图.png
2 V  F: \; R, v( t9 J3 c/ k 2015-10-28 21:17:02屏幕截图.png / J% H: o& s; v7 L' t7 t
在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。8 H: P5 {' d0 {( P- K, D$ b
2015-10-28 21:18:21屏幕截图.png   \9 Q! Z' t9 @/ I1 ~
在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)5 f: q  H2 \  E' u% k
当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)
3 ^" J% U# z3 S9 e; U- y, L- h' Y- v, g: O0 }8 Q# [" H8 t
2015-10-28 21:22:46屏幕截图.png
+ |% ^5 U6 S  `; z+ C2 u我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。) f* y3 ]0 K* v9 N2 D
2015-10-28 21:26:09屏幕截图.png 1 x# l+ `8 Y* E2 T7 ?8 n
使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。
- [" K' ~; Y8 A/ l/ d  \
! L) U& z, u$ E3 [如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。- L$ J/ g' h/ V/ h

0 a+ o9 n1 k. W6 K& v) s. M; _
7 K+ e) `$ X3 R

" H1 Z" c" M6 M3 |bug19 q+ ?$ m5 @! T
如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。
8 J) ]) `% k7 U# ~- O0 b重新make all,make burn测试。
) g9 N8 d7 R, E5 \3 c 2015-10-28 21:32:23屏幕截图.png 7 O& Y2 J$ E0 \" h' a
问题已经成功解决。更正之后的代码在serial_v2.zip
) N( C2 I2 j# r; h" Y( N$ ?
6 W% f! t7 Y$ i# [! A- Ibug2
: }/ }( n, M+ }9 k5 X  e" X
bug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。
2 d1 ?! y7 g. a一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。
- {+ e: d$ H2 P- R  f3 X! G4 x2 k2 i在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:
" j9 O; j7 _; i0 D#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )
) V6 ]; T- y$ D2 }/ B% v* W% P4 c, p#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )
( ?7 y9 H# s  B: N5 R! \1 w
我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。
/ m0 W- W" h+ s% `更新之后的两个实现代码:
) ]  M# g5 h  w2 D) E. v3 \" H% Pint rbput(struct ringbuffer *rb,uint8_t add)- \" y- X' l6 z# B7 B4 u
{  t4 ], s' D1 N2 A& v. \
    if(rbfull(rb)) return 0;) g+ K/ c3 a) u- b/ f& V. T* W
    rb->bf[rb->putidx]=add;
" J- G. ^( F9 S- l    rb->putidx=(rb->putidx+1)%rb->len;! g9 a" f9 {% `0 R: ]1 M% R) q
    return 1;
$ Y! q, Q( d. a6 `# X5 F}
/ A: R2 \9 v' b' }& C9 O  ]" |( U( C8 j; K
int rbget(struct ringbuffer *rb)
' \  Z# L; h4 |( O{
4 P  @( Z3 {# g0 D    int result;
) V& d# G( Y. A; V  d4 A5 w    if(rbempty(rb)) return -1;
  @3 Y8 {6 d9 G    result=rb->bf[rb->getidx];
* J6 Q4 J7 h# B# t: W1 G0 p* V    rb->getidx=(rb->getidx+1)%rb->len;
; d( t( W0 p% J* x/ F# L9 y0 `7 {# C    return result;
' c4 X* s6 w; Z* U}) S% F9 w* _+ S5 _1 }2 u& l
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断
9 K: S4 b& ?2 g7 L$ B& m3 rint __io_getchar(void)# i8 j5 Y) j( H4 m( e& _
{( E8 T: }& d- c. q; t
    signed char ch;
) C2 ?' Y4 `  C    while(rbempty(&rb_usart)) continue;
8 `( {; k) w4 [    ch=rbget(&rb_usart);; @- k7 k/ R! e, W
    switch(ch)
; r; t( ]1 I; B* v- _    {6 h7 d5 M# q8 [3 J
        case '\r':ch='\n';break;: r( E9 h+ F3 X  O
        case 0x1b:ch=' ';break;$ u$ R( K) M4 H9 d" s
        default: break;
' E7 n0 Z6 R. \8 Q5 h2 U/ m7 ]    }
8 o, ]! o$ I( Y" r9 u* e/ M  n    0 D" x7 e  d" O6 T, T/ t
    /* echo it */4 |8 R3 b+ u/ e
    return __io_putchar(ch);6 k' W5 K) D9 w* L. O
}
9 h. ^( T  P7 M! A' p
更新之后的代码在附件serial_v3.zip中。
5 z: u$ `1 t& @% y4 R& ^+ ~  U
! R! k4 K$ H( J0 l) y5 F& S- b% u1 ~$ |, \* @
更多6 y) B1 A5 ~5 t! K0 h. R
在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题

: ]+ q. G5 S0 D( O7 v# x
# t! w4 `1 p; m+ g  G2 K1 _8 K+ {& [

2 B) E5 j* E+ e( C" O

serial _v1.zip

下载

395.61 KB, 下载次数: 54

serial _v2.zip

下载

395.62 KB, 下载次数: 49

serial _v3.zip

下载

395.61 KB, 下载次数: 74

评分

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

查看全部评分

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

举报

20个回答
Mandelbrot_Set 最优答案 回答时间:2015-10-29 09:08:51
感觉楼主的 rb->count 可能不是安全的.9 W$ r3 H: P+ P
考虑:1 g1 {- c$ ?- `$ S
rb->count--执行到一半,4 G& ~" L4 n, |8 {: Z- _) b6 s
此时进入中断rbput里rb->count++
# j4 G4 p$ i# Y6 Q) ]. n% r# _- k/ y可能会导致rb->count计算不对
7 R+ V4 R$ P1 ?
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:08+ N/ U' ?8 k! k: H, V0 Q  O
感觉楼主的 rb->count 可能不是安全的.
8 J" ?* K$ U8 ]; T3 ^6 u考虑:
0 N  P# l6 {) o# a9 P1 d* Lrb->count--执行到一半,
; C) R4 f9 L7 Q2 w5 ~+ b8 _8 v/ X, N
确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。
( ]% N9 k' [# ~4 Q还需要改改。
党国特派员 回答时间: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
% [6 r3 d5 w: m3 @3 }把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
" Y7 L' V5 J3 @# {
刚开始接触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:083 }" [2 H+ O* }7 d6 f# R/ K$ o& A
感觉楼主的 rb->count 可能不是安全的.: o% D& ?+ o- U/ W; N/ o
考虑:+ R& s0 ^& r5 \! g; t
rb->count--执行到一半,

5 j' i1 v3 ^) t, }全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:40
" C( D' L" Y( v7 B. p) B4 z( v8 \8 K哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
0 O$ a0 N4 S9 s% H. m  c" ]5 Q9 q( ^0 }
用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:586 {( R4 _& \. G1 D8 w0 m1 F
沙发啊。。。这个很有arduino的味道啊。。。
# `5 S% N$ }; s4 T3 K) p
一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:581 W$ z- D. m+ J+ A; G& b0 X% K
沙发啊。。。这个很有arduino的味道啊。。。

, j1 k/ T2 t0 S* s% n" d只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:492 d4 R$ ?. X( |) a" |
全局量要加互斥锁的吧

$ E6 ^. b$ p% K把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:32* V  U( M8 I- y4 n- k/ g
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?
( A- g* s, K: f2 P0 }/ d( ^( p0 ^; k
http://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

相似分享

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版