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

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

[复制链接]
qianfan 发布时间:2015-10-28 21:36
本帖最后由 QianFan 于 2015-10-29 15:30 编辑
* @  |1 h3 v+ w) k" c4 R
+ x* I, B2 I. c* c) U  可能觉得使用串口很简单,无非就是初始化GPIO,初始化串口。接着发送---检测是否成功。表面上看来是很简单的问题。然而,我要说的并不是这些。我要说的是volatile和中断向量表的问题。在其中配合一点gdb调试。( P( V3 l; |$ R. C2 p! E+ _

- K6 u% E' y+ ?" h. ~使用ringbuffer" T- p; o) [* v1 q
这里的串口使用非缓冲发送,ringbuffer中断缓冲接收的方式。先来看看ringbuffer。ringbuffer是一个特殊的队列,FIFO。4 Q0 H0 ?/ a1 r% U! n
struct ringbuffer
5 ?' b3 U/ K1 `; A( H' v( S{
; L0 C: {- n) H# G. v, J* G, T0 {' `% U    uint8_t *bf;# c; `+ Y' F  T: U& a* o+ U$ N
    int len;
' I7 F: E% ^; n    7 ^+ f/ n$ `3 U* N; U7 a9 {& ~! y
    int count;+ n) a4 M3 z8 m' D/ c
    int putidx; /* read index and put index */0 G1 `, t1 N! |* P, \2 `
    int getidx;! K  ~- I2 B# g( n
};

. l2 |8 r: o+ p# O6 x4 o! a* l; sstruct ringbuffer对一个数组中的数据进行管理。数组的字节个数是len。更加详细的细节请大家尽情Google。写了一个宏,用来声明一个ringbuffer用来管理内存,准确的说叫做定义更好一些。
! R. F( C. G' Q#define DECLARE_RB(name,len) \7 w8 ]/ w) X% y1 z* k: \' T
    uint8_t name##_buff[len]; \
  i! S. K! e  o* L) |- k# }9 P  S    struct ringbuffer name = { \( S$ z2 f3 S& e
        name##_buff,len,0,0,0 }

: c5 [: E' v4 z. U- z提供的接口函数:. D( M( D: |/ l$ Y( X4 p2 t7 ^3 y
#define rbcount(prb) ((prb)->count) 2 `+ {& ?. m( p
#define rbfull(prb) ( (prb)->count==(rb)->len )' X" O$ a# @# ^, n2 J
#define rbempty(prb) ( (prb)->count==0 )
. G( x) H/ e. B% ^0 v- T! F* h
; u) m( X9 ~: w" Q8 L& z- {/* if ringbuffer is full,return 0.else return 1 */, L  q; A, H( s+ H0 |
int rbput(struct ringbuffer *prb,uint8_t add);
$ v0 _1 W2 B: \& r+ Z6 f5 j  e/* if ringbuffer is empty,return -1.else return bf[tail] */
& k5 n: }' k# V4 E$ p/ k- ^int rbget(struct ringbuffer *prb);

" i  S2 y# m+ B+ e$ e
. m, e; o9 I9 @% k: M( B3 r: Lrbput是往ringbuffer中添加一个数据。rbget是从ringbuffer中读取一个数据。相关的源代码:
; z& c# v( C/ f6 o4 Y- i#include "ringbuffer.h"9 a9 n6 P3 h5 q3 ], S
+ U& F) A9 M. |/ r
int rbput(struct ringbuffer *rb,uint8_t add)
! [! b- t9 t  W{
7 B; U6 V' W4 C& w" ]% [    int curidx=rb->putidx;
8 q2 G0 k! K/ m/ G( I9 H$ K    if(rbfull(rb)) return 0;
6 Q9 M/ v( F7 d( i1 m6 P. e    rb->bf[curidx]=add;/ s2 E$ s1 c/ M/ g% C+ U5 {- S5 v
    rb->putidx=(curidx+1)%rb->len;* M$ m0 ?/ s$ O, {+ Y
    rb->count++;$ {' m& N& G5 \: U* c" k+ l% E/ J6 @
    return 1;
( e" A2 l4 }( V5 i}
7 F0 K& b4 j, n1 J8 m' K% _
) g" e+ B8 I6 ~3 |int rbget(struct ringbuffer *rb)6 Y5 I- e) F1 ]3 D- o0 c* Q
{! h3 A; q+ V' S. r% u* w* P
    int result;9 P+ O, J/ n* \# @1 g
    int curidx=rb->getidx;
9 u6 L7 k2 u% R% v/ L    if(rbempty(rb)) return -1;# v3 A, y: _% e; f8 G- z
    result=rb->bf[curidx];5 k. P' l) }  d  c. \
    rb->getidx=(curidx+1)%rb->len;
) g; Q& |& Z) U* h# f3 j0 P9 |    rb->count--;
. a( f/ e- \' J. m; }& D& O' Q    return result;
* m) ~4 p6 Q' m; ]* L}
0 l* }6 Z9 p5 H: _& ^$ l# C3 u4 H
5 F0 }6 m4 F  g  p/ j7 [4 m  Z
串口中断+ringbuffer
: Y0 `: `/ o* T: f7 f) U
在串口的初始化的时候,打开串口中断以及RXNE中断。在中断代码中将接受到的字符rbput到ringbuffer中。2 D+ ?+ @3 ?. A, r# Q, A+ m
/* USART interrupt handler */. ?, z- Z. t$ m: k' f/ @4 O
USART_Handler(): `9 f: p9 s# o3 m) u
{5 @9 W2 S( K$ [
    if(USART_GetITStatus(USART,USART_IT_RXNE)==SET){) W; o# [" \( s) W6 p" u8 f
        rbput(&rb_usart,USART_ReceiveData(USART));
; S5 q. K. r9 d2 ^8 x4 y6 W# X    }
. q* H: \1 S$ ]* a5 b/ L}

, _& }9 Q8 B8 j1 X2 O6 K1 P编写__io_getchar() __io_putchar()函数。用于发送和读取一个数据。
% S/ B# i6 c7 A. s7 oDECLARE_RB(rb_usart,USART_RD_BUF_LEN);
* y- w/ q5 R$ |7 H9 B
) s4 g9 ^; @3 m/ Xint __io_putchar(int ch)8 y/ b9 y* f: G
{
, u+ i, }0 F/ X/ n* v; l    while(USART_GetFlagStatus(USART,USART_FLAG_TXE)!=SET);
7 ]5 M* n0 L7 G, A    USART_SendData(USART,ch);! f% U: n6 C' m" }: {
    return ch;
, o2 N4 ]* W- c8 `( I6 C}
4 K- ]* A7 U9 C
7 Q0 H; y2 H7 {9 h" t. U( v' D1 ~8 qint __io_getchar(void)
: B0 p4 K# G  r/ C$ n# U{, |( h9 x3 {- g9 p( m  i
    signed char ch;' L$ T, b6 D" u" W5 @7 P. z
    while(rb_usart.count==0) continue;
% {5 S7 V4 C: H3 i    ch=rbget(&rb_usart);7 I- {+ y3 S! g, c: s) ~& e
    switch(ch)
6 A+ P$ ~# k  n' x    {" V+ V  l: y4 r# n; A
        case '\r':ch='\n';break;
2 c8 g$ p7 ]/ j$ [8 n        case 0x1b:ch=' ';break;
+ a8 K& g2 I! k        default: break;
( m6 N8 T; l* k# K4 d0 O4 O3 l    }
- i& H, p1 t0 N/ }& ^; ^: n   
, z) {0 s2 B8 v) C5 G, ?    /* echo it */% u- t( h, u' J8 A
    return __io_putchar(ch);
  z  K5 O2 Z+ a* w5 }& x3 h' f- I}
$ g# e9 t  q* F% E- V1 [9 K' h
当在使用minicom发送特殊的按键,比如Up,Down,Left,Right按键的时候,会发送以0x18开始的三个或者若干字符。对于这些,我们只显示0x18之后的字符。5 J( V$ d% ^" Y" H
由于使用minicom,putty一类的串口,发送的数据并不能回显。因此,要想有回显,只能在读取的时候进行回显。在中断中回显并不是一个好的方法,会占用一定的中断时间。因此我将回显放在__io_getchar函数中。  K* e% s5 {" a3 `

6 u1 j" P) x2 Z: S2 X在主函数中一直读取字符
8 u: ?5 D4 W; L; R$ `# ~, N我们的主函数中什么也不做,一直在读取字符。由于读取的时候会有回显,所以不论我们按下什么按键,都会实时回显。$ v+ L( A* z* E$ o5 J
int main(void)4 A5 b; M' y: g
{
/ F( G. r  d# p( z) x& v    const char *str="Try to enter something...\n";
; I  O2 p- n: k3 D% I0 O8 n   
& R" ^- I: R2 C4 U    const char *tmp=str;. S) }7 f" v4 }8 M/ F& _
    for(;*tmp!='\0';tmp++)* Q; T; T3 i9 \1 I: p% i( d
        __io_putchar(*tmp);
& o. A; p" z0 K1 e6 [% I' W        7 w! L& c: ~' B
    while(1) __io_getchar();. i( c/ I' v% P' Q
}
9 G! j0 l- v, S, Y0 G3 Y
7 Y, Q0 ^$ w( `) ~" ]8 @void _exit(int status)$ p7 m2 y+ l# c7 o3 P- G) j6 a
{* u6 x' _. r" q: L- C% Y/ O( k9 d! m
    const char *exit_str="GoodBye!\n";7 }; W1 F9 Z( M: e) b5 N2 x
    const char *tmp=exit_str;
1 i. a' J9 ]' Z4 K    for(;*tmp!='\0';tmp++)
( j2 r& v4 t' h        __io_putchar(*tmp);
# F+ h$ D! R2 }" K% P3 h+ V   
1 {( N# i4 l3 v- m9 ^8 @( `    for(;;) continue;
& R$ L; w& b3 F8 J& z2 Z}
: u+ }- B# F3 S. m

4 P: @- R4 X  }3 l% x- W下载到FLASH中运行
- |; O7 q! j; i( W# r/ K" y目前所有的代码都在serial_v1.zip中,大家可以自行下载测试。
: z0 h& P/ _$ U% ?试着使用make all,make burn将代码下载至FLASH中。发现Try to enter something...这几个字符确实能够输出,但是不管我们按下什么按键都不能回显。暂时先不要使用make sram, make burns .
: S& ]" f1 B: c: v) H( d不管什么原因,至少能够显示也是好的。, h9 b7 I( r# K% n+ K$ O! k* f

; L8 v! }) r, g! {4 C: y' W4 c使用gdb调试代码
' ]) v6 W4 X8 f8 H: [新建一个控制台窗口,输入sudo st-util。连接st-link与stm32。st-util会监听4242端口。使用这个端口可以与arm-none-eabi-gdb进行通信。' Y3 w# ?% I8 E% C' Z$ w
2015-10-28 21:47:50屏幕截图.png 5 o; Y; Z9 N# |( L0 _0 p. L
在当前目录下启动控制台窗口,输入arm-none-eabi-gdb blink.elf。由于使用了上一个例子的Makefile,所以文件名字blink.elf并没有更改。
# |# g* h' v) R5 W0 Y在gdb串口使用tar连接远程终端。如下图所示:
' Y6 N% [0 e1 _+ V5 ` 2015-10-28 21:03:36屏幕截图.png
  d; Z3 W' y/ B% Q4 Star连接完毕之后,使用load命令将代码下载至单片机内。3 n7 D! ?7 ?$ ~( d

) t3 r2 w0 p( [0 ~至于没有回显字符,我首先想到的原因是串口RXNE中断没有进去。我可以在串口中断里面设置一个断点。当进入串口中断的时候自动停止。设置断点需要使用break命令+行号。为了查看usart.c的内容,可以使用list命令。list命令(可以简写成l,小写的L,不是数字1)有几种形式。
( h* q8 e/ H' V  U+ |5 h5 Y: I2 q
  • list function_name 用于显示一个函数附近的代码。如list main.
  • list file_name.c:line_number 用于显示一个源文件的第line_number行。
  • list 在只输入list的情况下,可以从当前代码的位置继续往下显示代码。
    1 A* u; e% `: C& k* }/ J
使用list usart.c:1显示usart.c文件的代码。继续输入list显示其他代码。找到串口中断的代码。
6 O3 U: w& w1 B$ O1 [  b; x7 k 2015-10-28 21:11:53屏幕截图.png , ^6 [, h: r; i7 B8 k
在第33行,也就是进入RXNE中断处设置一个断点。如果想查看所有已经设置的断点,可以使用info break查看。想删除断点,先使用info break查看对应断点的序号,在使用delete删除即可。% z+ w% Z: D( e9 q4 N$ a
0 Y8 ?  @. V) F: f; E, C7 l& t
2015-10-28 21:15:25屏幕截图.png ; d# q9 e2 D8 {3 e) G
在设置完断点之后,可以输入continue(或者c)。继续运行。直到遇到断点停止。  A/ m& F2 j+ c5 j
2015-10-28 21:16:33屏幕截图.png
. N4 k1 _. C$ f' }: q 2015-10-28 21:17:02屏幕截图.png
5 D! @  V; q5 D2 ]/ U在continue之后,可以看见确实通过串口把数据输出了。这时候,可以试着在minicom中输入一个字符。我随便点了一个c。这时候,可以看到gdb中停在了中断的位置。
8 c$ p! k# B, ?. S 2015-10-28 21:18:21屏幕截图.png
: T. c* W0 B( v4 H# E9 h在试着随便输入几个字符。(没输入一个字符需要在gdb中输入continue。)( g& g1 q: K3 t# l  N
当gdb停在断点处的时候,使用print输出一下结构体rb_usart中的值。(或者使用print rb_usart.count查看结构体中的任意值)
7 I9 I( }  t3 H0 h- S1 j8 D5 `  t* W0 u- L! @( S3 [
2015-10-28 21:22:46屏幕截图.png 2 `- n5 x0 J- ]" d, }
我们的rb_usart结构体中确实存放了数值。但为什么不能读取呢?可以在gdb中按下Ctrl+c结束程序运行,使用frame查看当前程序死在什么地方。9 j0 v, f; w: ~3 v) ]
2015-10-28 21:26:09屏幕截图.png
+ |: v8 u# u+ ^) ^1 P- F使用frame之后,发现程序死在rb_usart.count==0这句。可是我们使用gdb查看,rb_usart.count明明是2,为什么这一句还过不去呢?初步感觉是volatile的事情。由于我们编译命令开启了-Os,使用了优化,可能这个rb_usart.count被优化到寄存器中去了。而while判断的时候,并没有实际读取内存,而是直接从寄存器中读取。所以造成数据不同步的原因。
- c, u* o4 y) W5 V, M9 b  z$ Q9 _2 u( W; U# [
如果想退出gdb的时候,先按下Ctrl+c,让程序停止运行。在输入q,并按y就能够退出了。
2 Y) |6 d! t' a1 Z# `
/ [3 z4 s+ |; ]
2 B' ^1 ]% B( F3 i
9 v0 J. l  k8 `$ l  V; v7 i
bug1& y8 J1 I+ B' }) d* D; H7 n3 y
如果只是volatile的原因的话,那么改正很简单。只要在相关变量中添加volatile就行了。. [; z/ F# k" V' ?2 N# K
重新make all,make burn测试。; ^! N. f6 B7 P3 ^  q2 y  b3 S
2015-10-28 21:32:23屏幕截图.png
) }8 k" I3 P+ x8 B) g' G9 o9 }, x问题已经成功解决。更正之后的代码在serial_v2.zip! t: Z( L0 e  @2 ]2 Q- [

! @. @; t3 h; R; S) T3 tbug2
" B% F, ^$ }! {9 Q! f' f
bug描述请参考下面回复的置顶贴。关于ringbuffer无锁的实现请参考:http://www.cnblogs.com/l00l/p/4115001.html 网址。" y3 A5 p( c- A; F& ]
一开始接触到ringbuffer的时候,是阅读Arduino源代码中Hardwareserial的实现。他最开始并没有将ringbuffer单独抽象出来。代码比较难阅读。后来,Arduino sam的源代码将ringbuffer抽象成一个类。/ b5 e3 Z) P" Z0 Q
在阅读完相关ringbuffer的代码之后,感觉ringbuffer如果只使用两个变量readidx,putidx来记录指针的变化,判断空和满的时候就会变得复杂。像这样:
: L. P6 B4 ~2 K  e0 F& {4 X2 X  N#define rbfull(prb) ( ((prb)->putidx+1)%(prb)->len == (prb)->getidx )
) O( E2 s3 v- V6 f8 P" W4 U. E& `#define rbempty(prb) ( (prb)->getidx == (prb)->putidx )

& M- b) F8 |6 B" J0 Z' z我很大意的给ringbuffer添加了一个count变量,用来记录存储的数据。但是这样的一个变量也破坏了ringbuffer的无锁结构。( Y* v* ]) m3 N2 k! j# L- d6 _
更新之后的两个实现代码:9 ~' {$ f8 G1 @$ ^+ A
int rbput(struct ringbuffer *rb,uint8_t add)
2 @1 Y- D* f6 w- a9 u) t6 y{
3 v6 D$ G" F# ?4 ^. C' f7 T& J/ t    if(rbfull(rb)) return 0;
1 M! D: `, @' j6 M    rb->bf[rb->putidx]=add;. l1 a& N. H  Q( i9 q
    rb->putidx=(rb->putidx+1)%rb->len;
2 H) \3 W! I& q, P' O9 v+ U+ r    return 1;9 k' G; r' f' Q
}
2 g' z2 D2 V8 W9 i0 E6 F1 K0 V
int rbget(struct ringbuffer *rb)
) O# n& b$ E  x5 \# T* T' W{
9 }% D  P& H+ Y    int result;( V# _6 [* L6 n
    if(rbempty(rb)) return -1;9 B9 S7 \& F8 J. G0 N7 V& Y
    result=rb->bf[rb->getidx];
: n3 h; [* A* Q: r9 K    rb->getidx=(rb->getidx+1)%rb->len;
0 \1 C  I* ?3 f0 q4 @    return result;
) d1 b% A/ G( f; }}
9 K  ~. ^: A! U+ S& a! ?8 [
由于去掉了count这个变量,在__io_getchar的时候,就不能使用count元素进行判断了。可以使用rbempty这个方法来判断
) L$ _/ }5 ^( ?1 h, uint __io_getchar(void)
/ G! i% i0 T- ]5 W7 s8 L$ Y{8 F: b; i0 W- y
    signed char ch;8 W: O1 _7 S5 [# G1 E) X
    while(rbempty(&rb_usart)) continue;: e1 d: I3 H! D8 p9 N* v
    ch=rbget(&rb_usart);! _0 L" L, M! N6 G3 V+ G( G3 o  \
    switch(ch)
/ Q, s8 G/ W# c5 g; B. t    {$ [* l$ H  C# D  e
        case '\r':ch='\n';break;/ k1 J8 j& k7 m+ l
        case 0x1b:ch=' ';break;
5 {3 k  a& c9 i3 t% q* ^5 `        default: break;
  |3 n" v0 H) v+ X    }2 ]  {' e; [- g; M! K7 A
    / E/ D# `3 Q6 R3 y/ V
    /* echo it */. L, P; B6 {3 b: r% E  c
    return __io_putchar(ch);
' _, D/ E) d# X5 w2 R/ T}- |4 a/ E0 N/ S- B
更新之后的代码在附件serial_v3.zip中。
2 j, b9 g! T1 p# c, G
! `# v- l; _, c) v  }, T) o
0 I( _" d! z& \更多
/ T8 M( o9 A/ ^: z在下一节的时候,我们将来解决在SRAM中运行,使用中断的问题
7 J+ u( U4 `  a; P# o2 F

5 E1 \; X3 L3 U) }) u0 W& L0 _( l7 n

9 o; s# W3 a% \0 k) H

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 可能不是安全的.- v% _7 M* V# W. y9 j0 R1 D
考虑:
+ P  p8 O- P( K2 V& orb->count--执行到一半,# E3 r0 d4 l7 R( U6 b: v3 Y7 c- ^
此时进入中断rbput里rb->count++
) w3 I% T" c9 j- {3 O可能会导致rb->count计算不对5 w7 V! _) N  Y- v$ f) u
qianfan 回答时间:2015-10-29 13:45:32
Mandelbrot_Set 发表于 2015-10-29 09:08
. v2 U. |/ Y5 s  x1 z9 v感觉楼主的 rb->count 可能不是安全的.+ a, B; U/ m& V) ^
考虑:) T: X) ]! F9 \, [
rb->count--执行到一半,
/ y+ z5 d( e' t) X
确实是这样的。之前阅读Arduino的源代码的时候,发现如果只使用head,tail两个变量,在判断空或者满的时候稍微有点麻烦。我索性就加上了一个count。这样实现比较简单。没想到把ringbuffer无锁编程的特性给意外的去掉了。9 Y7 v$ E+ s1 c+ w3 [/ Q8 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
( X5 O$ `+ U  {把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
+ @" c3 ^1 ]/ ~
刚开始接触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:082 _. c, Q. {3 w) q" L3 g9 h
感觉楼主的 rb->count 可能不是安全的.
3 c2 k/ C1 J1 G考虑:
7 R$ S, M% u5 a5 Q4 l& z7 Mrb->count--执行到一半,

1 n2 t7 P6 |4 V3 Z/ I全局量要加互斥锁的吧
qianfan 回答时间:2015-10-29 11:59:54
aabird 发表于 2015-10-29 08:40' I3 P4 c2 m  |. z2 S! m3 U: z
哇,真的是大神呀,这篇帖子,说实话90%没看懂呀

1 Z- F9 Q  g" i. ]2 a% i! n用过之后就好了。。。
qianfan 回答时间:2015-10-29 12:01:05
Paderboy 发表于 2015-10-28 22:58
0 `. @) p: A5 l  ^3 ~8 V6 {沙发啊。。。这个很有arduino的味道啊。。。
8 F& l  w0 U0 v6 P3 Q
一开始接触到ringbuffer的时候,也是看到了Arduino的代码才知道的。
qianfan 回答时间:2015-10-29 12:01:44
Paderboy 发表于 2015-10-28 22:58/ d( H" k1 S5 r" @' Y' R; S3 h
沙发啊。。。这个很有arduino的味道啊。。。
8 ~, x) ^( k6 O) f$ C$ m
只不过加了一个DECLARE_RB而已
qianfan 回答时间:2015-10-29 13:46:16
卡德加 发表于 2015-10-29 10:49; B& ~! G% i+ Z; x' x
全局量要加互斥锁的吧

$ p" l2 x5 v. K6 a6 H. s) a- U把count变量去掉。更改full和empty的实现方法,还是能实现无锁编程的。
qianfan 回答时间:2015-10-29 14:35:31
卡德加 发表于 2015-10-29 14:322 p9 F; Z! [( M
刚开始接触linux,能详细说说怎么实现吗?无锁比有锁有优点吗?

: C9 _  v+ ?3 \' Ihttp://www.cnblogs.com/l00l/p/4115001.html
12下一页

所属标签

相似分享

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