|
我是老温,一名热爱学习的嵌入式工程师; l! W- p/ g- {0 O2 b
关注我,一起变得更加优秀!说起通信,首先想到的肯定是串口,日常中232和485的使用比比皆是,数据的发送、接收是串口通信最基础的内容。这篇文章主要讨论串口接收数据的断帧操作。空闲中断断帧一些mcu(如:stm32f103)在出厂时就已经在串口中封装好了一种中断——空闲帧中断,用户可以通过获取该中断标志位来判断数据是否接收完成,中断标志在中断服务函数中获取,使用起来相对简单。void UART4_IRQHandler(void)
J$ [* } p6 v{+ f; j1 t1 P0 g5 A" \
uint8_t data = 0;
5 }8 s9 y- N/ p8 T, \0 r8 M/ o data = data;
% T8 d& p: t; Y% h% x% v, U& z if(USART_GetITStatus(LoraUSARTx, USART_IT_RXNE) == SET)- ]4 a* `# h; M
{
. n: K+ ?" u0 ?0 p USART_ClearITPendingBit(LoraUSARTx, USART_IT_RXNE);! J% c0 d3 N f2 J
if(Lora_RecvData.Rx_over == 0)- ]; w k5 w" u( M! m7 i
Lora_RecvData.RxBuf[Lora_RecvData.Rx_count++] = LoraUSARTx->DR;
* @2 U# H# D( f" \% L }- n- U% K8 V2 f- S, |4 Q( ]% d! l
if(USART_GetITStatus(LoraUSARTx, USART_IT_IDLE) == SET). |4 k0 x+ x, k" I. V
{# Z) ~" @ a7 G& m
data = LoraUSARTx->SR;
6 U; x3 K2 K' [3 q8 g$ I data = LoraUSARTx->DR;' q. v% T6 b: I9 L2 e2 |
7 r0 z, ^. }9 E, f
Lora_RecvData.Rx_over = 1; //接收完成, V+ w: T6 l+ w$ e8 H
}( s1 x1 ?( \. l V
}例程中,当接收完成标志 Lora_RecvData.Rx_over 为1时,就可以获取 uart4 接收到的一帧数据,该数据存放在 Lora_RecvData.RxBuf 中。8 Z6 h2 J' U1 g" f1 [% J- ~2 u: u
超时断帧空闲帧中断的使用固然方便,但是并不是每个mcu都有这种中断存在(只有个别高端mcu才有),那么这个时候就可以考虑使用超时断帧了。! l4 A5 `; u1 {# O- h5 q' p
Modbus协议中规定一帧数据的结束标志为3.5个字符时长,那么同样的可以把这种断帧方式类比到串口的接收上,这种方法需要搭配定时器使用。0 L+ H( I$ _) i+ o6 U* K
其作用原理就是:串口进一次接收中断,就打开定时器超时中断,同时装载值清零(具体的装载值可以自行定义),只要触发了定时器的超时中断,说明在用户规定的时间间隔内串口接收中断里没有新的数据进来,可以认为数据接收完成。
- V5 @- e* M: m8 q. E* J" Cuint16_t Time3_CntValue = 0;//计数器初值
) S% v( }) `! d) B N
- d1 _4 W/ `7 M; u; A- A# U/*******************************************************************************
- D3 q" x1 U( I+ ` `! h# A5 Y. A3 F * TIM3中断服务函数- u8 B0 q9 v1 k
******************************************************************************/! A8 i# k2 U4 j" U
void Tim3_IRQHandler(void): k f! x: U. \) R# L5 U
{
2 ]; R8 ?/ Z* E( n: X if(TRUE == Tim3_GetIntFlag(Tim3UevIrq)) P) l6 L Y' @+ A! O. A" e9 d
{' ]% X( M& C/ E4 A; R: U1 V
Tim3_M0_Stop(); //关闭定时器3
/ R) h: A" r4 S Uart0_Rec_Count = 0;//接收计数清零% D0 a! b1 r& T$ {) ~! |
Uart0_Rec_Flag = 1; //接收完成标志
' ~8 K: ^( r/ v, d) N8 {. a Tim3_ClearIntFlag(Tim3UevIrq); //清除定时器中断
% Y- {- L$ m1 Q8 C }
\2 @: k2 \* x4 C3 K}5 o; b4 n; e5 z2 ?: V
! t ]4 @1 }9 Y' _3 T! ?/ w
void Time3_Init(uint16_t Frame_Spacing)
; e9 T' _; Y5 r8 C* ?{8 A* C7 A( u# P9 x1 @
uint16_t u16ArrValue;//自动重载值
# I1 o1 {7 e: W2 f uint32_t u32PclkValue;//PCLK频率4 S* L+ A; T* f7 l
{7 S5 e; n& t) Y: p! { stc_tim3_mode0_cfg_t stcTim3BaseCfg;
8 K/ [) I/ {# p7 [* M1 L M; U + j/ F- x) s [( B8 A' n( v/ u
//结构体初始化清零
) J' O% K& a1 w* k. k+ B6 R DDL_ZERO_STRUCT(stcTim3BaseCfg);, U8 N. {- u( r. o" \" N
1 K& E' d& l5 D Sysctrl_SetPeripheralGate(SysctrlPeripheralTim3, TRUE); //Base Timer外设时钟使能* I6 z* _1 X; ]. P( J
9 u, w9 Q" n! Z! P& g- e2 i2 J: m- z
stcTim3BaseCfg.enWorkMode = Tim3WorkMode0; //定时器模式! V i+ J: D/ v E6 M
stcTim3BaseCfg.enCT = Tim3Timer; //定时器功能,计数时钟为内部PCLK5 e! s* C0 X9 s
stcTim3BaseCfg.enPRS = Tim3PCLKDiv1; //不分频
F' [+ q, f" \' }& Q% R& a: L+ q @- d stcTim3BaseCfg.enCntMode = Tim316bitArrMode; //自动重载16位计数器/定时器
8 A y7 U; {& B( G" t( n; T stcTim3BaseCfg.bEnTog = FALSE;
9 Z5 t9 [! i. N7 v$ x" [ stcTim3BaseCfg.bEnGate = FALSE;
$ s' _" T/ G5 n# n3 u6 H stcTim3BaseCfg.enGateP = Tim3GatePositive;3 d4 ?4 v! t4 z9 W! v4 D: W
5 @' f p' N R$ _% [ Tim3_Mode0_Init(&stcTim3BaseCfg); //TIM3 的模式0功能初始化+ M5 P1 Z% C' r7 V: F
* M5 h9 o9 _) w' O$ V3 W( o8 Q
u32PclkValue = Sysctrl_GetPClkFreq(); //获取Pclk的值
% M" H" [& S- k; f' D( s //u16ArrValue = 65535-(u32PclkValue/1000); //1ms测试7 ]. I* @' i1 C3 q5 c8 f0 i
u16ArrValue = 65536 - (uint16_t)((float)(Frame_Spacing*10)/RS485_BAUDRATE*u32PclkValue);//根据帧间隔计算超时时间
( z5 i9 ?. l6 V9 k- R6 r$ A Time3_CntValue = u16ArrValue; //计数初值" F+ M9 T8 v9 r/ p6 z4 V/ Z( \
Tim3_M0_ARRSet(u16ArrValue); //设置重载值
$ C' w" y6 h. n5 a8 m' Q& f! k Tim3_M0_Cnt16Set(u16ArrValue); //设置计数初值( L8 o# \% ~- B+ `4 p
$ m0 o" [2 `) R( O0 G( n Tim3_ClearIntFlag(Tim3UevIrq); //清中断标志 d# J0 p# T; @% @0 E! I8 n
Tim3_Mode0_EnableIrq(); //使能TIM3中断(模式0时只有一个中断)( L) k$ [( H4 R! E, T Q3 U
EnableNvic(TIM3_IRQn, IrqLevel3, TRUE); //TIM3 开中断 4 a6 R. S2 X7 F" U3 A
} / p* i( h7 F( P* p. c h
3 a" Q4 K4 D) y5 O4 Z. e" @4 [ y/**************************此处省略串口初始化部分************************/
O+ Z( R& |. ?//串口0中断服务函数
& w8 ?* U, k+ Y* f3 m7 b7 m4 `void Uart0_IRQHandler(void)) C3 ~! v" F' ~2 K( u8 t. M1 \ E8 P$ C
{1 s0 V0 g6 B3 ^$ K# a1 n
uint8_t rec_data=0;. ~, ]$ c$ L( ^+ `9 \+ o2 u9 i+ |
& y; p( ^5 W! M# W. h9 q* j( B1 P if(Uart_GetStatus(M0P_UART0, UartRC))
. ~. f0 f2 W' a1 F5 [1 F {
, T( a( t! X7 J& r" N4 O1 F Uart_ClrStatus(M0P_UART0, UartRC); * H' |6 P! `3 U
rec_data = Uart_ReceiveData(M0P_UART0);
, x6 R$ b. s4 Q' s L3 X$ R if(Uart0_Rec_Count[U]//帧长度
" x6 W: Z" c$ M# E% s {. |1 V3 |1 d# H0 o3 L1 M
Uart0_Rec_Buffer[Uart0_Rec_Count++] = rec_data;
* \, N' d ~8 |: p. F0 Q: l }
; o6 o$ F6 _0 G6 k6 l Tim3_M0_Cnt16Set(Time3_CntValue);//设置计数初值 4 k! h4 S4 q; E8 r
Tim3_M0_Run(); //开启定时器3 超时即认为一帧接收完成, x* C2 k; f" p0 B7 h" ^7 b' ?
}
0 |0 c; y& M- r+ b}例程所用的是华大的hc32l130系列mcu,其它类型的mcu也可以参考这种写法。其中超时时间的计算尤其要注意数据类型的问题,u16ArrValue = 65536 - (uint16_t)((float)(Frame_Spacing * 10)/RS485_BAUDRATE * u32PclkValue);其中Frame_Spacing为用户设置的字符个数,uart模式为一个“1+8+1”共10bits。
6 Z0 O4 L4 f% J状态机断帧状态机,状态机,又是状态机,没办法!谁让它使用起来方便呢?其实这种方法我用的也不多,但是状态机的思想还是要有的,很多逻辑用状态机梳理起来会更加的清晰。" m" g z* X! E$ s) ?4 W% W7 W @, R
相对于超时断帧,状态机断帧的方法节约了一个定时器资源,一般的mcu外设资源是足够的,但是做一些资源冗余也未尝不是一件好事,万一呢?对吧。1 v% `, D' Q: R+ @. E/ ^
//状态机断帧) L" Y! S) c$ F* b" O
void UART_IRQHandler(void) //作为485的接收中断9 i X' e7 F/ T }
{
$ H( s. i$ a( {" q w/ o% O uint8_t count = 0;
& ~0 s7 [% @! @" x unsigned char lRecDat = 0; 6 K6 M* j) W. M
0 R a" [' ]( k1 o
if(/*触发接收中断标志*/)
; y1 L3 s4 f( }. D {1 \$ Y3 F0 }4 J [) ^( E: t# @
//清中断状态位- g4 x! e6 Q3 F/ g3 I2 K b
rec_timeout = 5;6 R8 @5 J, g! L: M. n
if((count == 0)) //接收数据头,长度可以自定义. X9 ^$ M ~5 H `1 u3 h$ o5 k
{- g9 }' |) ~. i6 @3 ]( O
RUart0485_DataC[count++] = /*串口接收到的数据*/;7 v; t3 ` X& M9 j; {3 O. J# L
gRecStartFlag = 1;
! k4 G0 R" Y- K; M return;5 o' I9 y" A z5 z; d. i$ c( W6 C2 p3 U
}8 A# v- x" j1 C: Y
if(gRecStartFlag == 1): }7 r" Z; N7 t" g+ Z: n4 B) f9 D- I
{! k0 y# H( v& B% U% J! T+ B2 g
RUart0485_DataC[count++] = /*串口接收到的数据*/;
" \. ~# f* {# i8 j
# z1 b: `$ e) b% r0 |! a+ I if(count > MAXLEN) //一帧数据接收完成
) M) V o- F# X3 s# a) v5 h {
8 O! m' E, ~! C7 J. [& J count=0;
! g! \) z1 J) E* c4 K% ~% C gRecStartFlag = 0;9 n2 i- U0 X* W
1 Y: B) c9 e c5 {( Z if(RUart0485_DataC[MAXLEN]==CRC16(RUart0485_DataC,MAXLEN))# ` _% D; b# A4 r: a
{
$ l. _) D6 N' f2 E0 k4 l- |: a. K memcpy(&gRecFinshData,RUart0485_DataC,13);) Q7 ]: L4 W: r
gRcvFlag = 1; //接收完成标志位
/ Z8 J% C% n$ Q. ]+ S7 B }
, X% W1 L1 t1 h }
/ F# L6 W. Q# a }6 T0 K u7 C3 I: ~- C7 Q
return; 3 s; k# X# w4 h3 u6 p( P# T, t
}
! a" V1 S% F) S' B# Z/ y5 c return ;
. ]0 S: g; ?! k2 b6 v}这种做法适合用在一直有数据接收的场合,每次接收完一帧有效数据后就把数据放到缓冲区中去解析,同时还不影响下一帧数据的接收。
$ L" P5 C/ u8 ?: o k. O6 j; ?整个接收状态分为两个状态——接收数据头和接收数据块,如果一帧数据存在多个部分的话还可以在此基础上再增加几种状态,这样不仅可以提高数据接收的实时性,还能够随时看到数据接收到哪一部分,还是比较实用的。- q# l! `; }5 j. ?5 p+ Y0 w; A( r
"状态机+FIFO"断帧记得刚毕业面试的时候,面试官还问过我一个问题:如果串口有大量数据要接收,同时又没有空闲帧中断你会怎么做?
# _8 x; ]3 Q8 S4 J0 [5 ^( Z. Q没错,就是FIFO(当时并没有回答上来,因为没用过),说白了就是开辟一个缓冲区,每次接收到的数据都放到这个缓冲区里,同时记录数据在缓冲区中的位置,当数据到达要求的长度的时候再把数据取出来,然后放到状态机中去解析。当然FIFO的使用场合有很多,很多数据处理都可以用FIFO去做,有兴趣的可以多去了解一下。/********************串口初始化省略,华大mcu hc32l130******************/
/ K) M# S# [/ N0 S, y& x1 gvoid Uart1_IRQHandler(void)
( X" R' a- w$ `6 K+ V% m4 [{
6 Y/ q! K0 d! l8 c uint8_t data;
! l$ Q. j+ |- |6 Q2 M if(Uart_GetStatus(M0P_UART1, UartRC)) //UART0数据接收
3 W* R/ {7 W4 n" \1 \! z5 O {' u2 Q& @/ q& j. U
Uart_ClrStatus(M0P_UART1, UartRC); //清中断状态位
8 ]0 {7 s5 r0 n$ M data = Uart_ReceiveData(M0P_UART1); //接收数据字节
4 E; R- e6 g) {7 ~ comFIFO(&data,1);
- j: E) j' r! Z, B% Q8 T0 z } / s1 F$ B9 ~$ Q
}
' H& j* ?4 w9 Y$ T: a" h
D' ^" C; X3 N( |, C' T6 N! u2 F8 J/******************************FIFO*******************************/8 B$ F- F: x4 k# H6 l8 i
volatile uint8_t fifodata[FIFOLEN],fifoempty,fifofull;% _( i! Y+ B! c" _
volatile uint8_t uart_datatemp=0;
& ~" V3 c- J" H8 D! Z / q8 q4 ], ~. t' D: L, f8 e s
uint8_t comFIFO(uint8_t *data,uint8_t cmd)
, G7 r3 n1 G2 \3 ^3 Y{
9 x* r) J8 `" \3 g. C static uint8_t rpos=0; //当前写的位置 position 0--99
3 i/ y! d2 @, C: M7 Y# I9 C- S static uint8_t wpos=0; //当前读的位置/ I- r% p% z0 Q
$ t% T- B; ?4 c# R3 N if(cmd==0) //写数据
! I' b' V# O B" q9 s* n0 e1 v {
+ t& n4 j: O( ^( m0 P. g if(fifoempty!=0) //1 表示有数据 不为空,0表示空
. g' i: }6 s0 o3 `2 _% i {
; H; K5 B! I) J1 P3 w4 M/ p *data=fifodata[rpos];* @9 Y! }4 q1 h; _; N, V& g
fifofull=0;
+ V3 Q5 O6 C2 C U: r' \. d' x5 ^4 i% _ rpos++;5 j2 p; b* p: C6 g, F$ ~
if(rpos==FIFOLEN)
2 Y% t* x2 r+ N4 C1 U( O. ] rpos=0;8 L, B7 S7 ]2 B5 S! z5 E4 m
if(rpos==wpos)
, ~" H3 }0 \9 B) F7 M% w fifoempty=0;" a" d0 K) y* u1 }1 c" _
return 0x01;
' W: C1 n; F2 d6 b( ] } ) X* v& g0 \- H& I! y% }
else
: N8 h! g/ L) L" F2 W return 0x00;
7 R8 V- ? W( w$ Q8 n4 y 0 ?, D6 `3 E) V8 x8 f: p S9 _% R
}
+ F$ c8 _' }- p$ U else if(cmd==1) //读数据7 y' P' ?# |$ t
{( @ @" \9 u) G R3 D9 g- @( s
if(fifofull==0)
q6 l: \$ l( h8 ^5 L6 \0 w9 M {5 b: U7 v$ i$ B5 y5 m
fifodata[wpos]=*data;
- O3 X' P2 }3 C, C- ]2 n fifoempty=1;( r! i% T+ ?* g2 A! X& e
wpos++; k( B* t7 W7 o7 g- V
if(wpos==FIFOLEN)
# O s' {7 G$ ?( [9 h wpos=0;
0 i7 x: y6 A( D5 ~- E0 Z4 P9 A, x+ X if(wpos==rpos) 3 q7 y2 j. x) z0 `% z
fifofull=1;5 d: e" J& t% e
return 0x01;& E. u3 T' A) o
} else# q# I& I- d% _0 d: c3 l N
return 0x00;
H& b4 X0 r; q& X }
; t5 H4 Y8 p) @/ M6 @ return 0x02;& k( L+ P8 L. Q8 N, ^9 Q, ?9 ~0 k
}& Q1 u2 I3 A: c8 G& }* `
, B0 \4 M% s4 L" U
/********************************状态机处理*******************************/, [! X$ I$ T1 Y0 v- y, N
void LoopFor485ReadCom(void)
0 z/ {7 ]0 P: z% S! w& V{8 Y. G! M, C3 C. ]0 X, M
uint8_t data;5 _7 S( Z, B' `. L3 x0 L+ G6 ]# W
5 Y% c; ^4 S' n$ A3 r) Y F while(comFIFO(&data,0)==0x01)' g) j) Y7 p, j. L( g: |
{
$ k/ u! K0 j$ U p6 j# W if(rEadFlag==SAVE_HEADER_STATUS) //读取头
% g( q, M5 ]; ?& G {
" h1 d# ^% D2 j% Z4 P( ] if(data==Header_H)
3 N( ?7 c, A0 a% U8 p" _' C/ ` c+ a {
' I' q; R4 ]4 ~# `5 a buffread[0]=data;3 a6 s7 P$ M9 ~$ ~0 a2 S# R
continue;
# X4 i: ]" v# L+ N }
8 T0 i7 U/ b9 b5 q5 ?& A; ~# i+ {1 H if(data==Header_L)
/ H5 K0 B# u; T {! Z+ Y. y$ @$ @) e& @5 j: L
buffread[1]=data;% L \" D: l2 i% x
if(buffread[0]==Header_H)6 g! f1 O; B7 H+ l
{$ J% s& v: n7 m6 ]
rEadFlag=SAVE_DATA_STATUS;1 Y% G8 f, s" m/ l- B4 i6 R
}# H4 [ B& S6 K) l7 m2 e/ z
} $ ^: `6 f7 `, R7 H8 o7 c7 J" v( N% D
else
) H+ ?2 p0 G& y {
, g; W5 n, A4 O3 b7 W memset(buffread,0,Length_Data);) F* M) C7 l1 q- {+ |. n7 k0 r* ~6 k1 T" ]
}4 v5 T4 V$ Y7 E8 o9 z, z0 w f
}
* Y7 u! O* X2 @ else if(rEadFlag==SAVE_DATA_STATUS) //读取数据
4 t* o" S7 p# F. }- e { w! k& { x0 L, v7 [: {% g. Q' P
buffread[i485+2]=data;
. q/ A+ {$ O9 W5 D i485++;
9 l6 G) |, j6 |$ h+ i( y3 ` if(i485==(Length_Data-2)) //数据帧除去头/ [5 r/ a# i8 v; g7 o6 t1 a4 R
{
6 E6 r$ k6 c$ M! W8 g unsigned short crc16=CRC16_MODBUS(buffread,Length_Data-2);" n9 n% Y0 o' B, B
if((buffread[Length_Data-2]==(crc16>>8))&&(buffread[Length_Data-1]==(crc16&0xff)))' F E* F+ ~9 Q% h- {- J; l' `
{; W# X; d& D! x0 H2 n8 s
rEadFlag=SAVE_OVER_STATUS;
( K- y+ W$ u, Q# ^- T2 ? memcpy(&cmddata,buffread,Length_Data); //拷贝Length_Struct个字节,完整的结构体
2 |% k9 m F& q8 R" x5 a }
' O6 U' s* ~* P) ` T3 V+ y else- o4 Z2 w% q$ e" C/ d7 p a, ~' R9 q
{, p4 ?) X' @) m# [* h- j" N
rEadFlag=SAVE_HEADER_STATUS;
9 Y M5 l3 c2 {! F9 R& } }
/ F+ B) v6 b" O D' a9 q% K 8 j+ {; t8 _4 l9 B
memset(buffread,0,Length_Data);1 ^# y7 b, b3 U: h- k
i485=0;
2 P* B0 {' `& S/ f7 K/ j8 F+ e( p break;0 P2 ^! C/ ^# Z1 q) s5 X
}; S/ K) W% c8 P5 L; @) Q7 w. i% B
}
( q7 c9 v& h/ f0 s0 p }
# W) W2 C1 v3 K# T}原文:https://blog.csdn.net/qq_56527127/article/details/1230185153 y0 @/ L$ i0 K7 Q* v
ohb1aidbpas64054373034.png
# j! \- v* I" s
-END-3 U# R' j: Z% E4 a( n$ u& A
往期推荐:点击图片即可跳转阅读
1 }+ f$ P) e9 s" T: p( b* c
/ d' n4 ?1 ]7 m" i# f ) {, `: V9 ?9 Q- R" v0 r0 O
) s9 d9 s g+ w' z! \/ m
8 F! T3 L4 d7 _4 M8 D j: j- f! A! a5 [1 v$ V4 q. t
ptzib2nqqkv64054373134.jpg
' @4 z. G: j' u2 G
4 q; l4 N7 ?: d8 B! D* s u 嵌入式软件调试,如何计算任务的运行周期?
6 Q5 o; c4 _6 [- d; \) n
* |. ?4 X) r) [) k5 Y' J 1 u9 _! [: b/ R4 y; |
% x7 i+ [0 k7 r- U% A# N # P, w, A9 ?% M0 o3 c
: q8 T9 h) p+ w9 F3 i
. U3 p8 q# t* ^0 U
/ l! _5 m9 x. z* X W 2 C) w; E( [$ t' m8 Y; L
2 L# f4 v6 I* m* l# i / ? \" U# {- a& B* z
iarkvaxoix564054373235.jpg
: O& U, p/ b. f# d# E1 s 0 V% D, T: i1 |3 [9 n# @; o
嵌入式软件,如何把编译时间加入到bin文件,进行版本管理?& h0 l; H+ t* X" w- F* x
5 `; c1 i% N% Z, G5 U, K% A1 b9 P0 l4 w
- z( u/ [3 R- p* |0 M* d/ n ; i& X! h* `/ J, U+ T& X
4 Y" B2 s" ^3 C# d4 R6 l
4 E @( G7 @7 A. Y: R3 e9 F* p) }- `( b3 h9 Z
/ e( S# v: r) f/ X5 B& r: G2 m
6 F8 K" L/ S- C" q( y5 j8 N, z
; V' ^) p" k$ a0 E P( g* ~8 ^
* m# v1 I/ c- n! K, L" p, D
e31rvw53ost64054373335.jpg
9 D# W' e/ S5 C, T0 H! i5 w) y0 d, `
- e( v0 P7 V: C$ Q( W1 X3 }: S
嵌入式初学者入门后,应该如何开始进阶学习?
8 m6 R3 z! z2 d+ S
0 W3 q, ?) e% F
" a% J1 n) m3 m) m: T! W
" C3 J0 n6 E1 r; ~& Z; z8 g
! a( z( P, k4 p) o: P" h0 L 8 ]& q1 n) l( w( x2 _
我是老温,一名热爱学习的嵌入式工程师
) g. S: O! b5 I/ Q5 e- [( f关注我,一起变得更加优秀! |
|