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

详解 stm32 在线 IAP 升级

[复制链接]
STMCU小助手 发布时间:2022-12-14 14:24
本文主要讲解在线升级IAP的基础知识, 主要是针对IAP从原理分析, 分区划分, 到代码编写和实验验证等过程阐述这一过程. 帮助大家加深对在线升级的认识。2 Q2 T' T9 T7 N
+ P# O/ b! G4 g- C- B+ i5 q
1.在线升级知识) u$ k2 \" h# [( k, G$ x! S( r% P( y3 O
什么是BootLoader?
. {# u) @$ X2 Y5 s, I" NBootLoader可以理解成是引导程序, 它的作用是启动正式的App应用程序. 换言之, BootLoader是一个程序,App也是一个程序,BootLoader程序是用于启动App程序的.
0 a& e9 X9 l5 ]8 P9 C
- w% r( }/ ~3 h, {% l# ]5 USTM32中的程序在哪儿?
# i5 n: Y, L9 L* w  P正常情况下, 我们写的程序都是放在STM32片内Flash中(暂不考虑外扩Flash). 我们写的代码最终会变成二进制文件, 放进Flash中 感兴趣的话可以在Keil>>>Debug>>>Memory中查看, 右边Memory窗口存储的就是代码, s' j# w5 z! E; X  P: G1 e' U! L
1 j; d" G7 _  m* P8 }7 n
微信图片_20221214142442.png
9 S5 x2 ~! [" j$ ^" t  D+ J' b* @3 c. V
接下来就可以进入正题了.( @; e2 y. }4 q$ _" J6 M8 S
1 c4 n4 E0 A7 ]5 a# f$ p: s
进行分区

3 F% L' A; ~. f7 W, l既然我们写的程序都会变成二进制文件存放到Flash中, 那么我们就可以进一步对我们程序进行分区. 我使用的是F103RB-NUCLEO开发板,他的Flash一共128页, 每页1K.见下图:. @, b4 Y1 G0 i4 V8 S

! H- X: C+ y' V9 e, m$ p 微信图片_20221214142437.png
8 a  h) _5 g$ W, a* }

' y3 K6 A4 T* U以它为例, 我将它分为三个区.BootLoader区、 App1区、App2区(备份区)具体划分如下图:% b: g: s7 J' E3 f9 I6 U
BootLoader区存放启动代码
, E# U. ?- V% ^- ^App1区存放应用代码
% H+ Z  H' `7 z+ A; YApp2区存放暂存的升级代码
! {( L; V, B1 b6 E2 Z9 r$ b% {; a7 y+ O( p3 O9 ]
微信图片_20221214142434.png
: ~  r8 y; |+ D# o9 w+ ]" i' M

. P3 V6 m" T* I$ n" }总体流程图
: W9 d9 e# h; c
先执行BootLoader程序, 先去检查APP2区有没有程序, 如果有就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序.
2 D# q6 @3 K5 g% c然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表. 然后再去执行其他的应用程序.
4 ]% B8 ^3 }* y+ m$ L! n; h在应用程序里面会加入程序升级的部分, 这部分主要工作是拿到升级程序, 然后将他们放到App2区(备份区), 以便下次启动的时候通过BootLoader更新App1的程序. 流程图如下图所示:! I. W+ b" B/ |4 S; ~

  K, t# d. Z& e8 c/ o: C
微信图片_20221214142430.png

! ?2 g# L6 [9 G) d2 T0 @$ l4 q; E4 Q7 Z- x8 u5 u

! S1 i  {5 L7 H) V& z2. BootLoader的编写
% p: n! c9 W" [+ d4 P本节主要讲解在线升级的BooLoader的编写,我将以我例程的BootLoader为例, 讲解BootLoader(文末会提供免费的代码下载链接),其他的大体上原理都差不多。4 |! f5 t3 j( N" T) ~; |1 l1 v0 o
' F& c1 Q7 U8 T! L# C- f
流程图分析+ a' `) {5 u4 U
以我例程的BootLoader为例:
* C0 C7 N  l5 A1 {

% g) h: Z; g5 W2 ]& g& |我将App2区的最后一个字节(0x0801FFFC)用来表示App2区是否有升级程序, STM32在擦除之后Flash的数据存放的都是0xFFFFFFFF, 如果有, 我们将这个地址存放0xAAAAAAAA. 具体的流程图见下图所示& s9 x9 b8 Z# c4 E& ~

. }; B. R  A( @3 y. s4 Z
微信图片_20221214142425.png
/ q- C; k( U( c+ \. B) u
& G8 c6 B; B! t! ~$ r
4 p/ _! `# G6 }* M; E/ i
程序编写和分析
1 }; y5 [6 v7 W) Q  _所需STM32的资源有:
' @$ S! Z" Z2 X发送USART数据和printf重定向) ~6 J4 f/ I% s0 @7 [
Flash的读写
0 k0 |) n% _: M; s5 }程序跳转指令,可以参考如下代码:
( E* M. P! [! U- }: D8 a6 c8 Y6 V; q& j& Q. b7 |4 h. T
  1. /* 采用汇编设置栈的值 */
    $ b: N7 E5 ?# M; F! T6 z1 n3 A5 o
  2. __asm void MSR_MSP (uint32_t ulAddr)
    ) v- N% m+ q" h2 L/ A
  3. {' E0 o  |+ M4 Z
  4.     MSR MSP, r0   //设置Main Stack的值
    ! q& U+ [( T: [+ w
  5.     BX r14) c! s: M2 ]: [" m+ e9 O
  6. }
    & P% E0 {6 y. c) T

  7. - p' }  R3 v" c& B6 V
  8. /* 程序跳转函数 */
    . @8 V7 N2 M7 q
  9. typedef void (*Jump_Fun)(void);
    3 }4 L! u4 W  v( o# H
  10. void IAP_ExecuteApp (uint32_t App_Addr)
    # f* H( [. }4 \  |' v) M3 J
  11. {) w  K7 Y# j, T& e3 H7 P
  12.     Jump_Fun JumpToApp;
    $ e+ z7 p7 p! a# u% C4 f

  13. + C. B+ m  p, r  g, }% Z( v/ D+ M
  14.     if ( ( ( * ( __IO uint32_t * ) App_Addr ) & 0x2FFE0000 ) == 0x20000000 )  //检查栈顶地址是否合法.' W  D" y# ]" b( E  p" h& l
  15.     {% N1 U7 h- |9 @" Q9 V2 e
  16.         JumpToApp = (Jump_Fun) * ( __IO uint32_t *)(App_Addr + 4);  //用户代码区第二个字为程序开始地址(复位地址)
    3 ?$ `& J4 b5 [* P. q. i4 w
  17.         MSR_MSP( * ( __IO uint32_t * ) App_Addr );                  //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
    " J2 p- B6 O- w9 Q; y
  18.         JumpToApp();                                                //跳转到APP.
    3 O- e% i. ?0 V& x1 r
  19.     }
    4 D* I3 I% y* B/ @( K
  20. }
复制代码
, a" q4 x; @- g% M# l4 P
在需要跳转的地方执行这个函数就可以了IAP_ExecuteApp(Application_1_Addr);& S) r0 \; g' p, p, N- g
其他的代码请参考BootLoader源代码5 t/ g3 P/ T* ~
3. APP的编写

/ F6 W) R  n1 f7 C. r2 d本节主要讲解在线升级(OTA)的App1的编写以及整个流程的说明,我将以我例程的App为例, 采用Ymodem协议进行串口传输,讲解App的编写(后面会提供免费的代码下载链接), 其他的协议原理大体上都差不多, 都是通过某种协议拿到升级的代码。
4 M, v5 S3 D" n% C2 R$ `- e+ Z/ g" o# e0 B0 x" r, i% J
流程图分析
* [) I2 i  p1 Q以我例程的App1为例:6 u- ^9 H0 D& M( O

: f, O( C" M5 K5 J  X- r
微信图片_20221214142419.png 3 x- d8 M3 M: u! t$ ?
$ I2 t5 j& o' y$ g1 E: a9 q
先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;# G) f# z- K/ K% f+ `
打印版本信息, 方便查看不同的App版本;: V9 k: C) y' Y$ ]
本例程的升级程序采用串口的Ymoderm协议进行传输bin文件. 具体的流程图见下图所示:
. o1 F0 z& J4 V! _: v8 b  A1 u7 @
3 L8 F: u3 O- G- G- d程序编写和分析" ?8 P  c( p- D/ q# V1 j
所需STM32的资源有:3 N! s$ |0 l  d& r9 G
发送USART数据和printf重定向+ ]2 A# d( }' q. H$ ?! {
Flash的读写
: W! y" Y. [: w9 T8 c# j串口的DMA收发! W% v- q  Z  v
YModem协议相关
, o; i, k8 I5 ~% w, FYmodem协议1 A6 K' [/ O# S- Y
百度百科[Ymodem协议]% _# N' y  P) a' g+ B
具体流程可自行查找相关文档, 这儿提供一个我找到的 XYmodem.pdf(文末和源码一起提供).
1 }0 [* s3 M" ]Ymodem协议相关介绍可参考我的这篇教程 YModem介绍
  z* n; L/ f, k8 T1 Y8 U7 p, s5 F
8 L4 K2 g8 ]! k2 s* V* I7 ~
代码分析9 }$ I+ f. g  h7 a, X
代码大多数都是通过串口实现Ymodem协议的接收, 这儿就不详细说明/ B2 Z/ ~- O- D) q8 b9 L
# }& K# G$ O3 {8 }1 F, n
后面放了我的源代码, 详情请参考我的源代码.
- y/ R' r( Z" s
, V5 h: i0 [8 T$ \4 O7 x
主函数添加修改向量表的指令
0 a* l- P! _  z  n3 j0 V; M) _" m7 N( a

$ ]2 n: y9 q/ g2 h: ^3 { 微信图片_20221214142329.png ! X  J8 ~1 _% P  U; p2 f- r8 G

3 x1 d0 z) V4 b: R打印版本信息以及跳转指令' d0 m' b' |# d7 @, K0 d
' s" `  E! G% c$ m( Q. ^
微信图片_20221214142322.png
" A5 C/ @& A1 P9 o$ R) a  W% e. J, e1 l- \! f7 W0 U- G
YModem相关的文件接收部分, a1 p% Q7 b6 k
  1. /**
    ( D: B& P( u& {; N
  2. * @bieaf YModem升级; r# i( n* Z+ u8 ]; Y  ]
  3. *
    / x/ P4 b* d- m7 N$ ?( M' P
  4. * @param none) _" e4 ^' h& f# B' a' a+ Q% V
  5. * @return none
    1 P& V, Z9 {# q* r
  6. */! P) C% l1 _  Y2 I1 O3 V2 G
  7. void ymodem_fun(void)
    1 m" Y- r- b# n( A2 \# V
  8. {5 `% O  i3 K& D. s. c3 l6 i
  9.     int i;# H* U! o- x1 {9 S, z& ?
  10.     if(Get_state()==TO_START)7 O! {/ k$ d$ h: u5 R: o! J/ f
  11.     {
    0 ?1 P5 T6 f+ X6 E, \5 @- b
  12.         send_command(CCC);
    2 {/ ~' h3 w# C! K, i0 b9 z6 o
  13.         HAL_Delay(1000);. w$ x" K) ~% M$ q% t
  14.     }
    2 W/ Z: u7 l9 [! A3 I  Q
  15.     if(Rx_Flag)        // Receive flag
    : P6 q8 c: y$ y0 n& c  B4 \. x+ |
  16.     {
    6 L9 @- R: @9 R3 n9 C9 n
  17.         Rx_Flag=0;    // clean flag
    4 k7 A" w5 o3 N3 a. K

  18. 3 ^8 R, J1 F' a3 D- N4 U
  19.         /* 拷贝 */
    % T  I/ \, t0 a
  20.         temp_len = Rx_Len;. Q1 x! s$ n$ i2 `$ {# X
  21.         for(i = 0; i < temp_len; i++)2 Y9 k: w" Y  _/ E- h
  22.         {
    9 }. ^2 U' R, y% B9 H7 \6 }* s
  23.             temp_buf[i] = Rx_Buf[i];
    " i: p. `2 ^8 `. p/ {% w8 K4 _8 R
  24.         }# ~' ?8 u" I3 l+ \' }* C: R

  25. 0 O" h4 S9 O5 E( M4 n
  26.         switch(temp_buf[0])- \" i5 X5 ^2 E- u* f
  27.         {' S. t% l& o0 N- `; V6 @3 x
  28.             case SOH:///<数据包开始
      ]) C% C& z( N
  29.             {3 C: ^, {. [- Y
  30.                 static unsigned char data_state = 0;
    2 I% u" v( V% z
  31.                 static unsigned int app2_size = 0;! `- Z' w; Q7 G9 W1 t
  32.                 if(Check_CRC(temp_buf, temp_len)==1)///< 通过CRC16校验4 f  ]. Q( x3 p$ v
  33.                 {
    - P  F- m1 p4 H: y
  34.                     if((Get_state()==TO_START)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 开始
    # X$ ~! P8 @& m5 y: E1 L: A
  35.                     {2 q% |) d% |: d6 `
  36.                         printf("> Receive start...\r\n");
    % P2 [! ~1 p' {* C
  37. : a/ j# I' z1 k6 |2 I# S6 ^
  38.                         Set_state(TO_RECEIVE_DATA);/ b" m# ]! b  M$ t6 N2 H
  39.                         data_state = 0x01;
    - t5 p: X3 X: \% N
  40.                         send_command(ACK);8 g) g8 w, d, D0 j' D/ _! d! k
  41.                         send_command(CCC);. a' t# r% G" ]2 \
  42. 9 m9 e8 C: H9 e8 o, n) H
  43.                         /* 擦除App2 */  q4 E8 @. i9 P) i; ]
  44.                         Erase_page(Application_2_Addr, 40);+ B- m7 g  s8 L% w1 U
  45.                     }6 n: a6 U  r: j) F' e+ Z
  46.                     else if((Get_state()==TO_RECEIVE_END)&&(temp_buf[1] == 0x00)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 结束
    8 G- u6 @9 \" L3 v
  47.                     {# G0 x/ L% p( j6 l" H
  48.                         printf("> Receive end...\r\n");/ H& E! Y. q3 K' k0 o5 q1 M* s- Y
  49. . e: x3 q5 g% K' A9 c# h8 ~
  50.                         Set_Update_Down();3 E8 P  p3 o+ u  \3 O1 a4 |
  51.                         Set_state(TO_START);
    3 T% d* _% |9 h+ Z; i6 C- I
  52.                         send_command(ACK);9 N' d3 D  |% r
  53.                         HAL_NVIC_SystemReset();
    8 w- R' @2 Z0 p& p- N6 J* b5 P
  54.                     }
    / E2 B- H' ~) d. N1 C. O( g
  55.                     else if((Get_state()==TO_RECEIVE_DATA)&&(temp_buf[1] == data_state)&&(temp_buf[2] == (unsigned char)(~temp_buf[1])))///< 接收数据6 Q' S. x7 \2 W. N& E0 ^
  56.                     {! p% ]( [. |0 q: T# s8 Q
  57.                         printf("> Receive data bag:%d byte\r\n",data_state * 128);
    , T' P  b1 T. ~9 t1 h) t

  58. " c4 W9 O# B- O6 }5 M
  59.                         /* 烧录程序 */
    5 i9 F* a$ X9 M6 M/ n. j' S
  60.                         WriteFlash((Application_2_Addr + (data_state-1) * 128), (uint32_t *)(&temp_buf[3]), 32);4 X9 i- l- W* h5 |
  61.                         data_state++;
    * ^7 X& p, F5 a
  62. - i) K" A6 L) @( t( ^$ G7 g
  63.                         send_command(ACK);* d$ n7 ~% n  K* N# m3 {
  64.                     }5 L1 L, D+ l% i8 B$ a  [
  65.                 }
    + A2 b+ l5 Q0 ?0 m9 g' W
  66.                 else) V; N( f4 {- w3 A
  67.                 {& e5 k2 b$ M" N
  68.                     printf("> Notpass crc\r\n");  u6 }# s; n2 s9 v
  69.                 }
    . m( e8 f! H! e" o8 @, _! g

  70. & O: {, u. [! F1 S# J
  71.             }break;3 W9 h7 T4 I9 r% V$ D) }/ G
  72.             case EOT://数据包开始5 M6 ^9 L! L) F6 P8 L1 I; v  N3 Q( E
  73.             {
    ) D( P1 ~. C; y& N+ d# ~
  74.                 if(Get_state()==TO_RECEIVE_DATA)+ H( f, O1 {' D0 ]
  75.                 {
    7 K8 D3 N4 z- Y4 g
  76.                     printf("> Receive EOT1...\r\n");, D2 l. J( r3 f3 E/ ^

  77. 0 r; S+ C7 Q" L2 R
  78.                     Set_state(TO_RECEIVE_EOT2);
    * Y  \. C9 r5 Z* C5 b; ^$ v
  79.                     send_command(NACK);
    4 H# C$ F2 O$ [* i; a, H
  80.                 }
    / i- W/ l3 n3 i7 p0 v6 }2 h
  81.                 else if(Get_state()==TO_RECEIVE_EOT2)* @# n1 v' p4 O. [
  82.                 {# ?3 s# x' T- R6 I: C" @
  83.                     printf("> Receive EOT2...\r\n");
    + g6 z8 P! O) Z6 \3 c, m' ?' I
  84. ) P2 f" L% U+ m* v$ W/ I
  85.                     Set_state(TO_RECEIVE_END);9 _9 c+ {& Z  s  W8 Y6 V+ c, G0 _" Z
  86.                     send_command(ACK);
    : c( u8 l4 C4 ^5 _9 [% R0 M
  87.                     send_command(CCC);
    7 }$ H9 D. F& r& s. F
  88.                 }: M* F  O4 {6 S& z
  89.                 else) B" Y. O0 T' O' r2 Y
  90.                 {
    1 g1 `+ M. J; I3 f$ m- N2 I
  91.                     printf("> Receive EOT, But error...\r\n");
    - i8 ~3 x2 o: y2 i
  92.                 }
    $ c' H9 [2 m9 @8 N/ |7 ^
  93.             }break;$ R: l) f$ R) F
  94.         }/ M) s" [1 x* X1 j3 b% n
  95.     }4 i3 }. N1 Q1 |  {) f' L# |( T, T- o% r
  96. }
复制代码
, ]0 E8 t) `6 M* S" C
其中部分函数未在以上代码中展现, 详情请参看文末给出的源码链接.
! F% p: _; |- O7 t% k9 y4 M7 @5 @$ O6 e& S3 d
4. 整体测试9 j* X6 l2 ^/ N0 [- ?4 k8 f
本节主要对前三节的教程做测试验证 BootLoader + App的升级功能。
5 e; d& z2 [% M1 E/ J. k# S" t5 d$ P6 {3 s) {/ y2 O- ?/ y
代码的下载
# ~6 c, Y! ~, j7 }: l9 ?由下图可知两份代码的下载区域是不一样的,所以他们「下载的区域也不一样」。0 K, x- A' k8 E* i  B4 X! H6 |

. g* j# ?7 @3 n) V- V8 L( ]
微信图片_20221214142309.png
2 d1 C: r* R( v6 T2 i
9 a* D% ], e. m" w# A( e
BootLoader的下载' O% R" ?. _1 f' s
BootLoader的代码默认是最开始的所以不需要特别设置代码的下载位置" B, E7 `, {: G! b1 F
按照下图, 修改擦除方式为Erase Sectors, 大小限制在0X5000(20K)
8 ^2 [( [& G9 d+ v& D; O: G) b  B5 V; c( m$ v, i* n
微信图片_20221214142249.png
6 D" F( s8 r9 K' l4 L0 _) ^* `  P- g& q9 O0 B9 K
烧录代码( B  e6 }) g4 g' y6 K
运行, 通过串口1打印输出, 会看到以下打印消息$ b8 U0 q  f( E" B. x
说明BootLoader已经成功运行" |: S  o& ~7 h6 Y
, M0 c6 q% B& z
微信图片_20221214142234.png
) O' U) S( S! w  M) w& ~8 A8 A3 A# T
App1的下载# [/ J3 n: m  [; D' S9 R
App1稍微复杂一点, 需要将代码的起始位置设置为0x08005000
3 _0 F( W) Q7 h0 T+ D# |0 ~8 i4 ?同时也要修改擦除方式为Erase Sectors, 见下图
5 @* [3 j! o9 e2 m" R
( W; V4 ?4 W& u5 f! Z3 Z
微信图片_20221214142224.png
  L4 r% R  Y4 q, \1 m

% Z) G7 `* s2 R 微信图片_20221214142220.png
; @7 \- a$ \5 K* ^0 Q2 c" H4 K2 i* ?' R* N3 U' i
烧录代码6 I; \' u# N2 G7 l1 N$ E/ G, ^
运行, 通过串口1打印输出, 会看到以下打印消息
/ Q+ }- y& w& u说明BootLoader已经成功跳转到版本号为0.0.1的App1
  u5 X# P6 ~% L8 T/ t. f5 I2 M4 u+ G+ q' u8 o  ]6 D% {' L
微信图片_20221214142153.png
, E" i" k7 \7 v8 G" L
! {' ?( q$ A& j
生成App2的.bin文件
  V& i$ l3 Y% A修改代码, 把版本号改为0.0.2, 并且编译并且生成.bin文件
4 x4 g% e) d* Y; m
) O/ v( \) p; `8 N" K生成好之后你会得到一个.bin结尾的文件, 这就是我们待会儿YModem要传输的文件
9 B( ^; F6 m) h
* ]3 p- P. R: S% O4 Y+ h 微信图片_20221214142148.png
! _! M" U: P* T( i. F3 q
8 R5 E* W. d% N+ ^: h- y使用Xshell进行文件传输
. K  ?7 J' n/ ]; J2 Y2 D9 b打开Xshell4 Q6 D, y) u" D/ A6 ?$ G9 Y+ d
代码中, 串口1进行调试信息的打印, 串口2进行YModem升级的1 p& O+ f# l+ r% W9 k" p: E& E
所以使用Xshell打开串口2进行文件传输, 串口1则可以通过串口调试助手查看调试消息. N- j+ R% ?- M" l5 i. J1 i# q

8 t0 x) {2 U1 i* n6 X, M 微信图片_20221214142114.png
) I' c# B6 |$ z0 ~2 N+ O# P1 B
$ m. A# z$ x3 I你会看到App的版本成功升级到0.0.2了.  @0 j4 \+ ?$ ^
如果你到了这一步.  t! f/ K5 H9 z. B3 U, |
那么恭喜你! 你已经能够使用在线升级了!
6 F2 S% \. U8 w' i9 [
5 @! f- e0 Y& M+ ~0 A5. 总结( p, l0 s/ H% @' x( M. }! ^
通过本几节的教程,想必你已经会使用在线升级了,只要原理知道了其他的问题都可以迎刃而解了,除了使用YModem协议传输.bin文件,你还可以通过蓝牙、WIFI等其他协议传输,只要能够将.bin文件传输过去,那其他的部分原理都差不多。, B3 @; Z# O9 h  Q! ^
% s  H. i* @" [
& L- D/ j" @3 M7 P+ D
& h4 W  m0 t* }3 ]9 {9 M* x# R
转载自:混说Linux
  @) F0 O* q0 S3 Q0 u
( U; t. R: ^2 e8 t8 _( R
2 G; J: I. O: t; V
收藏 评论1 发布时间:2022-12-14 14:24

举报

1个回答
1+1=2 回答时间:2022-12-14 21:07:32
帖子分析的挺好的,但是有些地方好像没有说,比如从boot跳转到APP1的时候是否需要关中断?在APP1初始化的时候,除了修改栈地址,还需要做其他的吗?" V; Y( {5 L: [# s4 U

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版