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

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

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑 $ u! J* E  Q, K0 p$ I  H

' b8 Q% W+ a' M6 i1 u  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。
( a! \+ @( ]2 D4 s+ s( @$ I, ?6 I+ ?8 R% a
使用ringbuffer
& H2 j5 }7 l* A+ _8 F# }2 D. u这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。
: o& N% |. @7 ?7 gstruct ringbuffer
# h% q1 X( x+ A- X8 p# r0 p{
+ i1 [: K8 m3 W    uint8_t *bf;, P! f# R% D+ F) G) S
    int len;/ ^5 C2 T' a0 w3 \0 k
    ( G# n6 e5 r- ]& p
    int count;
- e! m" E3 O* c7 V6 N    int putidx; /* read index and put index */4 U$ b  S3 e6 e
    int getidx;
' N- d9 }( `0 H: g6 N2 A5 w};
$ u1 q3 ~3 F8 O, ], K  G3 j
struct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。
6 i% D2 t' Z) ^#define DECLARE_RB(name,len) \, N5 Q% N" L/ l- P8 a0 @
    uint8_t name##_buff[len]; \
6 [% e# j, t. Q7 D6 L: G9 C    struct ringbuffer name = { \0 A. h: t' B8 I9 f
        name##_buff,len,0,0,0 }
: O/ D& }- x) B4 E8 {5 I6 }! l
提供的接口函数:
7 m5 E4 v0 `3 m1 P2 w( S#define rbcount(prb) ((prb)->count)
' m. s$ z3 B  c2 I- l* ^* z#define rbfull(prb) ( (prb)->count==(rb)->len )
) [4 P3 A" H4 |/ j4 t#define rbempty(prb) ( (prb)->count==0 )1 J" s+ N* Z3 K8 f
) {6 I; D/ ]) G) @9 _
/* if ringbuffer is full,return 0.else return 1 */1 d; I! D9 G" G, Q
int rbput(struct ringbuffer *prb,uint8_t add);0 a3 z& b/ a; w, T
/* if ringbuffer is empty,return -1.else return bf[tail] */
/ P3 X" ^/ V% a* \7 ^int rbget(struct ringbuffer *prb);
, f9 n. O5 d0 b, J

5 Y# i* q' r5 }: crbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
0 v7 V( j" Q- C' ^" @( j( E; ?#include "ringbuffer.h"
& s" \9 o: `8 D
* |3 s/ _+ M- ^: e! j7 ?' Xint rbput(struct ringbuffer *rb,uint8_t add)9 w- C3 U/ c6 m, J4 I
{
) l! t. Q. J* I/ P$ i    int curidx=rb->putidx;6 T& ], q/ m' E. }
    if(rbfull(rb)) return 0;5 L4 T1 S- O, Y& y& p
    rb->bf[curidx]=add;% E$ \4 Q1 I5 a% ?- `6 M" V  a
    rb->putidx=(curidx+1)%rb->len;+ B/ W% {' {! a/ j* z
    rb->count++;
% U5 x( Q. e3 p& ~- v7 w    return 1;
2 h( p, e8 N( `1 x}* `; r- E1 @3 q9 c
, \  l5 X5 g# ~% C
int rbget(struct ringbuffer *rb)
2 C  Y8 A" f: `/ i5 w{
1 V, [* ?# q* M, {3 I    int result;
" E2 R4 A2 J8 L, U2 Q5 @    int curidx=rb->getidx;
, o' Z; U: H: r  Y$ _" d6 \; ?8 I    if(rbempty(rb)) return -1;
/ a& e2 N7 M2 o3 \    result=rb->bf[curidx];
; l6 B' [2 Y% t+ q5 N+ a    rb->getidx=(curidx+1)%rb->len;
( y' u6 w' x5 c1 A5 v2 G    rb->count--;$ X  @5 p0 U0 g2 u2 c0 E
    return result;
0 O5 |; p7 p! k) O7 D" m}
. b$ n% t( F( O# u

# w: I% ]6 ^' t* X串口中断+ringbuffer
7 ]6 k9 l7 c+ i
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。# L% x" m% e+ X% o7 K" n
/* USART interrupt handler */
7 ?& X7 ]8 M' n7 L- s' u7 f/ P3 mUSART_Handler(), h2 a* P: a/ ], B( e7 z* |8 R
{
8 r2 Z# _0 R/ f3 T8 K/ H* ?    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){
( B% J: A# p. b1 I        rbput(&rb_usart,USART_ReceiveData(USART));
% a" q' _* `, ^  j, n* `    }1 S, T% A& H) H4 o5 V% X7 K4 p
}
5 d, ?% K  P- u  E. J3 b6 ~
编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。
+ ~: m3 l3 _3 H$ mDECLARE_RB(rb_usart,USART_RD_BUF_LEN);
, h9 L7 E# a4 G( r# d+ [% E+ J. _
  v0 W& s' K9 I% x0 I( `3 I( ~" D* p( ?, g4 vint __io_putchar(int ch)$ A* }2 B% J+ t9 \) Y( l. L
{
6 o2 h* W: s5 X& P9 n, I( O0 w    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
( E; X, V' \+ H+ s' D    USART_SendData(USART,ch);- @  \$ l" K4 ]5 ~; P# z
    return ch;
. z0 n3 _7 c/ l; h3 u* Y- G4 U% m" b8 ]' c4 ~}
+ ?2 |# M) r1 I3 a- a$ P7 D; o4 W
int __io_getchar(void)7 H: X: p. p8 E! x: L6 c
{
1 M/ q' L% R5 f& C: b. `    signed char ch;' P. r& ?6 s" x1 _# y! N7 t
    while(rb_usart.count==0) continue;7 A& j5 D" m1 d9 d7 s" S
    ch=rbget(&rb_usart);
, R3 e0 s" p4 P( P" {    switch(ch)
" c$ g1 r$ _/ Q& j  q0 D    {/ A# T  Q+ i9 l
        case '\r':ch='\n';break;+ ^( Y* w: m/ {1 k. E# ?; c
        case 0x1b:ch=' ';break;
- F' U+ ^4 b7 M# P        default: break;
. k7 @7 X7 c, a3 }* l8 K    }
$ ^, g! f. ]6 _4 ]6 x   
( H, B! y3 }) I    /* echo it */+ A' u/ b% M8 P
    return __io_putchar(ch);
  O4 o7 F( P! p}
7 |9 K$ _$ Z7 g/ |0 N3 N' g
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。& a, T& o! E9 g$ P
由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。4 {# o- H1 l7 }! }8 Z, Y
) ^4 x5 H3 ~, b, _; Q: @8 j7 E
在主函数中一直读取字符
; W3 _6 r# K: p* z' [! f4 n我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。" X* K- q( K5 d6 i* }
int main(void)
, z+ E9 j( R" R' x& S{. ?) `: H. W/ _+ k
    const char *str="Try to enter something...\n";* B/ k, L9 K2 r) k
   
. d$ p- t3 L, Q9 Q8 j1 R$ L    const char *tmp=str;; m$ e$ l8 Q! ?2 r( g
    for(;*tmp!='\0';tmp++)* H  o$ e0 ]) S! H, G
        __io_putchar(*tmp);7 Q) C9 }- @+ p/ C7 [# i- Y' _
        : t3 G4 C& H4 g- a3 J0 P6 g
    while(1) __io_getchar();
5 f" \, {6 H# g' R}; B8 Z, E* l, E7 D( Y" O' A
8 g! y7 u4 Z8 e) A
void _exit(int status)) {- I0 P% L3 J* ^8 w
{
8 B9 E" x# ^; ]; ^+ w" m    const char *exit_str="GoodBye!\n";+ R+ i' M* j$ w- W$ k4 J7 o
    const char *tmp=exit_str;
3 Q; W. ?  Q% L* ]8 v" W    for(;*tmp!='\0';tmp++)& t8 z+ o7 F% u  F
        __io_putchar(*tmp);3 a8 V% a8 L/ J6 @
   
" K& Z5 K4 ]- \! l& T8 }$ |4 Z    for(;;) continue;
$ L  z1 x6 p, w}
. ]3 u5 ~9 x1 {& s& o

2 c3 j; m1 r+ }+ A% n下载到FLASH中运行+ }4 U% c+ k, @
目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。
( P+ H8 G: f- K3 j* K& Q0 o9 a& C% e试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .. y" n/ T4 t( E8 P( T& I
不管什么原因,至少能够显示也是好的。6 \. ^# |" d8 C9 R: m5 W

; P6 m- H8 M7 S: W0 u使用gdb调试代码
1 r2 n; o" {( `/ w7 _, p/ u新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。) h3 u9 a( ]+ o6 j) g5 P7 X
2015-10-28 21:47:50屏幕截图.png
" o8 I  T3 H+ c" A  E; s在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
/ {2 m/ {7 J; \3 C& ^2 v在gdb串口使用tar连接远程终端。如下图所示:
7 R9 O( q: d% [  p2 ?" ^, S3 H 2015-10-28 21:03:36屏幕截图.png
. I( ]. f2 z8 G# }# q8 U* mtar连接完毕之后,使用load命令将代码下载至单片机内。# Z% `- u; s: J: X) d9 @4 h, i4 ]

! R' l% I: Y8 c. l7 `  s; s至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。
+ Q: o, x6 i7 S$ ~( I8 g
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
    + Z* E& p( V, g" v9 v
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。
1 d4 b* K) Z; o! \( \ 2015-10-28 21:11:53屏幕截图.png : }  ?& t* J' m
在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。
; s+ I+ R8 E/ r5 t6 Y0 R# d& I& J' B0 w" M6 y$ `* f: l9 z  t
2015-10-28 21:15:25屏幕截图.png ) q) i1 ?: I/ `8 _4 [
在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。+ W# C4 p2 e) J% |
2015-10-28 21:16:33屏幕截图.png % J" |4 h+ ~5 a2 U6 p0 n/ I
2015-10-28 21:17:02屏幕截图.png , P% F- ]+ k% j  L+ s9 o+ Y% `9 H: J5 w
在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。. [- H4 {  h+ R! r  |! K% p7 U
2015-10-28 21:18:21屏幕截图.png
( H) B, k3 h/ m( g( @在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)
# @( N% \6 j# X6 m" o* x当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)
) p" l0 u, m# i! K6 \3 E
+ G4 ]" u0 L3 H- t/ ~1 h 2015-10-28 21:22:46屏幕截图.png 6 C7 T6 k. {1 A& Q' d
我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。% b6 f  Y0 Z5 [1 b* D
2015-10-28 21:26:09屏幕截图.png
' `1 s$ H  W; m使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。
5 u" u7 U, l! m& ]) }0 C; ]
5 C9 ^$ A1 f4 d, [7 D' g2 q) c如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。
# n/ w8 D/ y$ j/ r  L9 Y' O) q3 @6 v
6 L8 D: o% }+ s6 g# Q0 M4 z
0 g% V# A/ r& _0 R* R2 J* o: T
bug1
, F2 w  D! G# ?6 T, p, K如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。
$ w# H: M7 f) w重新make all,make burn测试。
$ E/ ~9 I# q  ^ 2015-10-28 21:32:23屏幕截图.png
8 E7 g( G7 c4 B0 O0 X6 G, I! O问题已经成功解决。更正之后的代码在serial_v2.zip
7 Q* v7 B5 r3 y+ F. @6 @$ b- V' Q$ P6 R$ k5 I* f* |7 N2 ^
bug2
2 d* z6 G6 j- f8 X2 Q3 D
bug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。
9 p1 g1 d: s9 S# ]( U一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。
6 D2 I6 Q# G) Q4 b在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:0 J" Q5 K! Y0 J8 G
#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )
* l: h6 z" W" b  g1 d' f" r#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )
3 E/ e, p  U0 H$ v# {0 D; b
我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。4 F" `- s  k* C' |* s% f
更新之后的两个实现代码:5 _/ l7 Z, A) q
int rbput(struct ringbuffer *rb,uint8_t add). f) x$ V) g" q* m
{
& M$ U8 g) n3 c/ s! m) p1 b. p    if(rbfull(rb)) return 0;
& c6 T8 Z9 i7 b8 s! C- O9 \" u    rb->bf[rb->putidx]=add;
' W% n6 m! a3 _6 v; q    rb->putidx=(rb->putidx+1)%rb->len;
6 {9 [* h4 ^7 q6 G    return 1;
2 E  ]: A% X7 Q- C+ I7 ~" z}; _& A! H! b. J7 y7 @

& Q) W# e3 L' s& ]int rbget(struct ringbuffer *rb)
7 Y1 B7 v( X6 e3 O0 u8 ?{! s+ ?! ~/ D9 h" g, c
    int result;
  R5 B& g& a0 d/ ]+ O9 r    if(rbempty(rb)) return -1;: P/ G9 U/ ^2 u' P! N( e
    result=rb->bf[rb->getidx];* x: z: B. g" j$ B* x6 E+ e
    rb->getidx=(rb->getidx+1)%rb->len;
7 B) l0 ~3 E5 @" s6 }8 D5 i    return result;
7 d' ]" c* J/ G5 P}
. B0 A- g- A) C: V4 x2 n: N' q
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断
4 n% w. n  q: T& q1 M. m* Pint __io_getchar(void)4 T& R6 C3 N( v, L/ G
{. i8 F9 |/ d* H5 A
    signed char ch;
, `+ c5 L/ r* Y    while(rbempty(&rb_usart)) continue;
3 w' T( |& |0 k, g) x: F# C/ D    ch=rbget(&rb_usart);
; [, S3 l, ~" M$ y! V    switch(ch)
/ f2 |/ A8 Y  w5 w# U9 }    {# d+ K9 Y- {: U0 [/ e( W
        case '\r':ch='\n';break;" N5 C9 _. y# V% u, C" {0 K
        case 0x1b:ch=' ';break;' o9 G9 _% Z4 C7 q
        default: break;# P6 C* S7 p! `1 i
    }
2 M- \8 C$ B* a) o3 H6 M1 Q; s( B' A    8 w: G3 \! T' J2 Q. r$ }2 f
    /* echo it */9 b- n5 r* d0 O& s1 E
    return __io_putchar(ch);
% G9 v5 g8 J3 x1 b7 W3 W% ]}: x2 H  J& d5 `! p
更新之后的代码在附件serial_v3.zip中。" Y6 x0 q1 s/ c" i5 u# H. q# x

0 n3 v0 x" g/ S4 Z0 j
& \8 E; x* g( O更多: ~3 U& Q( e3 M1 E# _, C
在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题
% ]% d; ~7 C& |6 j2 M

. h% I( o! {7 ?9 ]* f3 i5 q0 X4 v! M# G$ A1 h% ?
- t- h6 j3 q$ }, f$ ?5 Q

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 可能不是安全的./ P" m& }) W) A
考虑:- t2 [( ~# f1 |7 {) S
rb->count--执行到一半,4 W4 Y! r, K0 v# A
此时进入中断rbput里rb->count++3 w6 ?+ n& [) b; `  Z
可能会导致rb->count计算不对0 L! Y* B. m3 ]; Q4 ]$ G
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:08  P" g6 b. l1 F  h
感觉楼主的 rb->count 可能不是安全的.
. Z9 A4 @) v9 ^1 F考虑:. @6 |- b0 x9 l. D, Y
rb->count--执行到一半,

8 F% \8 ]9 H' q5 h, {! N/ l确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。
% m* X. B5 w6 b" b% A' g还需要改改。
党国特派员 回答时间: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
: \8 t# X/ G/ ?把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
' y& U& w3 e7 A8 a" y  X: f0 U" n: y
刚开始接触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) e! |7 O& J2 _) [% v! a  o( ^  b
感觉楼主的 rb->count 可能不是安全的.. \/ H; V: k7 n# P8 C3 r3 U2 l) p0 `
考虑:9 a( W* S( C% ^8 E! M
rb->count--执行到一半,

5 s7 W0 s. Y, _) Z全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:40& ^+ f( n! `  e
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀
  L# \0 R$ y5 f6 W% _: ?
用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:58
* `2 ?1 p% W& l, A沙发啊。。。这个很有arduino的味道啊。。。
4 \; b) W( S, \* G9 O0 F9 x: S
一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:58
0 S0 [; S7 ~, W& V沙发啊。。。这个很有arduino的味道啊。。。

- K& v$ T" \0 w$ G5 u只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:49% c# \) c' n/ \; \/ j
全局量要加互斥锁的吧

% E* u: }9 \4 @9 u5 o/ @把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:328 a3 ~* h8 }7 `1 `: X9 [1 g1 r
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?

3 ]6 u* a/ M; f( J8 |http://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

相似分享

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