请选择 进入手机版 | 继续访问电脑版

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

基于STM32的语经验分享—SPI读写FLASH

[复制链接]
攻城狮Melo 发布时间:2023-3-18 15:00
FLASH简介6 b9 a0 r5 Z0 W' s9 }) }2 `1 M7 p
FLASH俗称闪存,和EEPROM一样,都是掉电数据不丢失的非易失行存储器,但FLASH的存储容量普遍大于EEPROM,现在像如U盘、SD卡、SSD固态硬盘以及STM32芯片内部存储程序的设备都是FLASH类型的存储器。由此可见FLASH对于我们学习和工作的重要性,EEPROM可以实现单字节的擦写,而FLASH都是一大片的擦写,就像是大规模杀伤性武器,其最小擦除单:扇区的大小也是4KB。
" H2 ^$ w1 [. P0 Q我们此次通过SPI对FLASH存储芯片W25Q64进行读写擦除的操作。
2 c! s# \, @0 `$ j5 S
6 X6 B, c4 V. L0 h) Y# ?/ g5 c对于FLASH内部结构的详细说明博主会专门整理一篇博客来说明,所以关于FLASH芯片的相关原理,本文中只做简单说明,侧重代码部分。
: ~0 T6 i$ P7 h' V- |2 IFLASH详细说明的博客链接:(没有链接就说明还没有整理出)
/ K  g& k2 q' t; d+ a& F4 u
# `  n$ U# O$ G; c7 c& rW25Q64
: H8 {( J8 Z# zW25Q64简介
4 p* Z/ [2 _- x5 I- B8 A4 Z! x
就长这么个样子! }* [6 }: V. Y) s' T% A$ L
! @4 Y5 V0 J3 x- {/ k
20190814195953540.png $ O$ g0 N1 {2 Q1 @5 f* t

4 E/ r% t& p5 s) f/ b$ t- _STM32内部原理图如下:
5 P5 I% _9 H/ J1 y" z& E# K8 V- A8 R  F
20190814200413370.png
( d" q0 b7 |5 n0 J9 u4 c8 @8 {) F% x7 Z! g9 @) ?

) D! `$ F% m" h5 E; WW25Q64是一种使用SPI通信协议的NOR FLASH存储器,它 的CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。FLASH 芯片中还有 WP 和 HOLD 引脚。 WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。 HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。! a: p0 e7 `2 m) M& [7 V8 Y/ A

- p* r$ ?. g# ~+ _& q- U2 {
W25Q64支持SPI通讯的模式0和模式3- L+ J2 d  G+ W( K$ t1 h2 O9 q% c

5 C$ N9 o2 ?- a1 y* D3 c2 I
+ Z6 T5 m: B5 {
FLASH控制指令
4 t$ J8 M; }9 {3 S5 y) r( y* Q4 IFLASH芯片中规定了许多指令,只要SPI向FLASH发送相应的指令,FLASH就会执行相应的操作,所以我们对FLASH的一切操作都是基于这个指令集的,接下来介绍一下FLASH的控制指令:4 j7 ?6 ?! _7 z% \- u( n/ C3 a

, E6 g, h. L7 c+ ~. P% n
20190814200912265.png
- x( t2 e# V  S. f/ a) {' @; X

6 n( k5 g, z  ^表中第一列为指令名,第二列为相应的指令代码,第三列及后面的内容根据指令的不同而意义不同,其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。2 T. o1 q4 q9 M
看起来很复杂的样子,其实只要在需要执行相应操作时来查这个表,只要能够理解这些指令的使用方法,FLASH就算学会了。! K5 a0 I, T& E" g8 O

/ `) r4 w! g2 ^- Q8 x* E
例如:要知道FLASH的ID,那就在指令中找对应的取ID指令“JEDEC ID”,仔细解读这个指令
, `$ ]/ s6 P3 q1 K  V
20190814201942797.png
2 x' k5 y& C% C* ]# i4 @. w
, b( W, O& u& r0 A& P) `
可以看出对应的指令代码为“9F”,后面的三个字节带括号,代表这三个字节就是FLASH向STM32发送的数据,即这三个字节就是FLASH的ID,然后使用SPI进行读取就可以了。
' K, I1 s: C# W, `: t* Y& w
& r/ i: d( ~; y1 T* c
我们一般是将这些指令宏定义在头文件中,便于使用:
9 C) p- e# [& H/ W
  1. #define W25X_WriteEnable                      0x06 ) E% @8 s4 z4 L: b
  2. #define W25X_WriteDisable                      0x04
    ( B7 \7 R& x7 U- f4 Z+ T0 @
  3. #define W25X_ReadStatusReg                      0x05 1 J5 E* t( {& [3 p2 v! u+ ~
  4. #define W25X_WriteStatusReg                      0x01
    5 ~6 I8 O  P" b. Z/ i$ E
  5. #define W25X_ReadData                              0x03
    ; N3 |- d9 a( Y  c" I, B
  6. #define W25X_FastReadData                      0x0B
    - x, k9 _+ O+ H6 {2 `8 O! W
  7. #define W25X_FastReadDual                      0x3B $ |9 X. n1 l" S' B1 R, Z' B5 `
  8. #define W25X_PageProgram                      0x02 - `8 e4 i8 Q9 J' _
  9. #define W25X_BlockErase                              0xD8   ~! t% y* C9 P: ^/ s7 `
  10. #define W25X_SectorErase                      0x20
    / z; u8 E; u: A1 E
  11. #define W25X_ChipErase                              0xC7
    9 r2 g/ f  u- V) R# C1 J
  12. #define W25X_PowerDown                              0xB9 , s4 Z' K( w. D
  13. #define W25X_ReleasePowerDown              0xAB % U0 @' S$ w- T2 S5 ]1 |3 F
  14. #define W25X_DeviceID                              0xAB 9 b1 f% x" }: W
  15. #define W25X_ManufactDeviceID         0x90 0 ^  _6 f+ {) L1 O+ E# h
  16. #define W25X_JedecDeviceID                      0x9F
    % Y4 Z) T; J9 M- c
  17. 6 a; f& s& v1 S: A3 {
复制代码
# F4 h- r6 ~/ A/ _* {
FLASH内部存储结构! T: O4 b' v7 `, r# U3 a' K
FLASH的存储矩阵如图:其内存分为128块,每一块都有16个扇区,每个扇区大小为4KB,擦除数据的时候是以扇区为基本单位的。1 b+ p- F4 I4 n$ T
, V) h) s2 t% U
20190814203311343.png * @! W* [; K3 c6 n

" A2 Z7 E: R/ c' k代码讲解
- c. @6 o( H9 Y# v+ x代码都是博主亲手写出来的,可以运行。% B6 K3 d8 K0 y; R$ ?4 @) X
代码部分会用到SPI的代码,关于SPI的说明之前整理过:SPI详解
) x9 M% I8 ~: V7 ?
# O& G$ A% u" j- [! D
读取芯片ID
) m' ]9 g  n, {3 ?0 u- M9 T7 d1 Z
  1. /************************读取芯片ID*************************************/9 s( b& x$ w. A3 J  q8 Z7 u3 P/ J
  2. uint32_t FLASH_ReadID(void)
    " _. U, @& A$ `- G1 d+ j  f
  3. {
    4 x  k$ K4 Z+ d1 y7 q& i0 @9 h
  4.         uint32_t temp,temp1,temp2,temp3;
    ) G8 t- L3 a" t% n5 _
  5.         SPI_NSS_Begin();8 @7 A# M5 l# B$ G
  6.         /* 发送取ID指令 */9 j3 b/ l  O1 o3 R/ s
  7.         SPI_SendData( W25X_JedecDeviceID);
    7 d9 a3 n3 C$ K# |: V7 @$ Y
  8.         /* FLASH会连续发送三个字节数据 */
    0 G' u/ b  Y. R+ w: U; r9 n2 {: o2 {' B
  9.         temp1=SPI_SendData(Dummy_Byte);
    - w, p, K+ P5 L
  10.         temp2=SPI_SendData(Dummy_Byte);2 J- ?# u( N; }1 H! O1 V
  11.         temp3=SPI_SendData(Dummy_Byte);
    ) N5 ?- u' ]8 p/ n: u( e* G) f: i
  12.         SPI_NSS_Stop();
      N2 S! h6 `; b! h) v/ W1 W4 F
  13.         /* 高为先行,将三个字节整理在一起 */# {. v% d  `7 R& y
  14.         temp=temp1<<16 | temp2<<8 | temp3;
    / a6 h" v9 U$ Y+ d# ~9 z
  15.         return temp;
    7 r7 p+ t! u8 J7 v
  16. }
    # X6 ]6 \- M4 f3 h$ m9 A% a
复制代码
/ G- `! J* X- V# C
由于SPI是全双工通信,所以接收数据和发送数据用的是同一个函数) _' f5 s8 v$ r3 Y8 c
" V, y2 c& u. k; a
发送写使能信号
% I0 q5 e. `( e+ M. c2 p
  1. /****************发送写使能信号****************************/
    5 U- @: X6 O% D. q  x# O2 T7 c
  2. void FLASH_WriteEnable(void)
    & F1 a7 i2 W! e- j: w& E
  3. {
    $ {7 c' J/ s. }( d$ {: M
  4.         SPI_NSS_Begin();  X8 ~$ Z" ~5 y% e; Q
  5.         /* 发送相应的指令代码 */
    & h7 t3 G1 f5 a5 @/ W+ Q- }. G& T; W
  6.         SPI_SendData(W25X_WriteEnable);
    & ]% L5 W( ]+ D. t1 Q$ I" a
  7.         SPI_NSS_Stop();7 ^" n# D- O2 x6 y4 \
  8. }
    ! X- {# u; S( j8 N% t# N$ y( M
复制代码
! |) S; y% i2 E4 Q' f6 w! \
在向FLASH中执行写操作之前,都要进行写使能操作,通过发送写使能指令到FLASH来实现0 S/ r$ C# E! o& j/ b

) K9 `$ t- C) i5 Z等待FLASH不忙
' V; C4 x, L( o, A. @# S
  1. /******************等待,直到不忙***********************************/3 t) x; u7 _& @0 [* i
  2. void FLASH_WaitBusy(void)! \( W5 u4 a) G
  3. {
      O# K) w- T: ?( ^9 J/ l- E
  4.         uint8_t StatusReg=0x01;+ z8 b$ a# X, V
  5.         SPI_NSS_Begin();
    / z- Y5 P8 ~0 @. o8 d
  6.    /* 读取状态寄存器中的数据,判断忙标志0x01位 置位代表忙 */1 B0 O! y5 s3 E/ l  \$ _0 d
  7.         SPI_SendData(W25X_ReadStatusReg);      " [9 y2 y; Y& F3 x' H1 M' p
  8.    /* 只读取状态寄存器的BUSY位,即第一位 */
    3 l1 M; g( C" P. t7 V" B
  9.         while((StatusReg & 0x01) == 1)
    7 y3 n) m% v7 b8 D: S
  10.                 StatusReg=SPI_SendData(Dummy_Byte);
    ( W) g9 X9 z0 y0 J# ?
  11.         SPI_NSS_Stop();
    ' |6 X, i' y' b; t7 Z) t. m2 ^
  12. }
    9 t% U/ Q  }- J  g7 h' ?

  13. 1 `+ t( ?9 m2 ~- A1 a

  14. ( b& ?# @% `2 L5 X. u
复制代码

' y. R3 ~+ K/ Z- fFLASH在通讯的过程中需要一定的时间来执行操作,在这期间,传输数据是无效的,因为FLASH忙着呢,所以我们就要有一个函数来专门等!等到FLASH不忙了,再进行通讯,那怎么等呢?FLASH不忙了会给出一个信号——将状态寄存器的BUSY位重置(也就是0),所以我们需要不断的来检测状态寄存器中的BUSY位是否置位,利用读取寄存器状态的指令来获取状态寄存器当下的状态,然后根据寄存器的BUSY位(第1位)来判断FLASH是否处于忙碌状态。% [- Z$ V+ a# m, `( e
简单来说,这就是个延时函数,延时直到FLASH空闲,可以进行下一步传输。# U) d( K: h) \7 f, {3 g
* c- u1 [/ a  y/ x1 Z
. C3 I+ n/ A5 z
擦除扇区
( S" F# y$ D4 T
  1. /******擦除扇区的内容,切记地址要对其到4kB,每个扇区的大小都是4KB********/
    % Z# w5 h# t9 N
  2. void FLASH_SectorErase(uint32_t addr)
    # K* Y# U  e0 {- W. l2 j. t
  3. {8 q- a9 b: G9 I8 N# d# y) i
  4.         /* 开始的时候要发送写使能信号*/
    , j/ q3 V/ M4 U0 V. C. A& X6 B$ W
  5.         FLASH_WriteEnable();! k$ c, ?& G9 V- _3 N+ N! H
  6.         SPI_NSS_Begin();
    * q, A3 }; @+ ]# p! \) B
  7.         /* 发送扇区擦除命令 */
    - V& E& p$ C) J: }& K5 w/ c
  8.         SPI_SendData(W25X_SectorErase);
    ; y6 `/ k+ q4 c% k7 Y' n. e
  9.         /* 发送扇区的地址,高位先行 */4 z, |+ @3 b) n
  10.         SPI_SendData((addr & 0xff0000) >> 16);3 @3 ^# Y; r! B& ^' l/ o
  11.         SPI_SendData((addr & 0xff00) >> 8);5 y# {5 U+ B. U* r
  12.         SPI_SendData(addr & 0xff);
    9 A9 N# ^. M( i" g+ m* N( t# J
  13.         SPI_NSS_Stop();9 G2 y0 `( s% k$ r7 L
  14.         /* 最后也要等待FLASH处理完这次的信号再退出 */" K" H$ z" T1 |+ b$ y! f6 E
  15.         FLASH_WaitBusy();3 U" G  N$ l0 Y6 _  n2 _; |9 X
  16. }0 o% z% r. w" Z# _
  17. 7 i+ W* F/ ^6 ?% I! r
复制代码
; O3 @2 f1 S3 S
扇区的擦除之前要发送一个写使能信号,先发送擦除指令,然后发送要擦除扇区的地址(分三个字节发出去),高位先行。. l) B5 z( z9 L* l9 Q
扇区上的内容不是1就是0,擦除的过程就是写1的过程(将一个扇区全部写1),因为在写入数据的时候,可以将1写为0,但不能将0写为1.
0 M6 ~2 n( J" ~2 @) s- q! h8 x9 Z0 C/ n# |, ^$ l  Y

/ n. h- s* t  q' g写入数据
& X( ]5 R: T. |- y: |2 ~: M
  1. /************按页写入数据,但写入之前要进行擦除***********/9 j5 n3 e3 Q! }  }
  2. void FLASH_PageWrite(uint32_t addr , uint8_t* pBuffer ,uint8_t size), R7 t. p9 [& u$ ?
  3. {
      l3 M1 b9 u# z  M
  4.         /* 开始的时候要发送写使能信号 */
    8 w- K, s2 Y) j. X
  5.         FLASH_WriteEnable();, B/ x, H& w( O+ D
  6.         SPI_NSS_Begin();+ ?0 i; r) P: n3 a/ p2 L! r
  7.         /* 发送页写入命令 */
    3 U0 E/ V" K9 O: z7 K/ ^
  8.         SPI_SendData(W25X_PageProgram);4 x/ J6 R- U0 d
  9.         /* 发送写入的地址,高位先行 */
    0 {& O/ A- S1 j) f
  10.         SPI_SendData((addr & 0xff0000) >> 16);5 r6 k, Z7 j* w  P3 b2 }
  11.         SPI_SendData((addr & 0xff00) >> 8);5 Z5 I6 Q7 m  a
  12.         SPI_SendData(addr & 0xff);
    - W6 h0 V8 [; N0 `
  13.         /* 逐位发送数据 */
    & S8 Z$ S2 m% q( Q
  14.         while(size--)
    " f# d) J2 u" }! z* B+ h, D
  15.         {
      C& L+ b! R  G; n
  16.                 SPI_SendData(*pBuffer);
    4 v: o5 \- }/ L2 D! {( d3 d5 a; E
  17.                 pBuffer++;. B) E+ T2 U' e; N) L* [4 E8 b
  18.         }# a8 f& L4 D& E& q' `
  19.         SPI_NSS_Stop();
    , Z9 @2 R9 k: r* z9 J5 ]* l1 r
  20.         /* 最后也要等待FLASH处理完这次的信号再退出 */
    7 _0 N- j( l+ p3 ~6 d3 q
  21.         FLASH_WaitBusy();8 d/ v& b' S- B6 \7 P1 y
  22. }6 ^9 h' m+ K2 y$ j
  23.   V7 Z6 p7 T8 ~% ]3 v
复制代码

0 @3 N. @8 L6 ]6 w6 P: O* C# B在执行写入数据的时候函数的参数有三部分:
6 R4 c4 G1 Z( u$ Q  I& E6 _' A' K1.要写入的地址
) L2 }: e2 s3 i/ \: u' C4 Z5 s- [2 v2.要写入数据的首地址2 V$ Q; s9 [1 e0 U/ O( t% r
3.要写入数据的大小, D) c3 H, ]% w# @: Y$ P0 O
函数在执行的过程中,首先发送一个写使能信号,然后发送写数据指令,紧接着发送数据要写入的地址,然后就是逐位发送数据了,函数最后等FLASH处理完这次操作再退出。
3 t+ D- B6 t, L8 u+ R: `
( s/ F$ ]5 ~7 @/ a6 b# Z5 k

( ?. e0 q, ~0 ]$ g; \读取数据9 O  F9 `( z' a" v/ \3 u  ^
  1. /**********************读取指定地址、指定长度的数据******************/
    ( V' H5 v3 i. o  N
  2. /* 因为读取在了指针中,所以不需要返回值 */
    ; M3 E! J/ F6 n! N+ Y4 Y6 q1 |
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)
    8 D# }" o4 R" S1 q# i
  4. {) Y0 {6 f, ~% o: q% o8 S8 V% K
  5.         SPI_NSS_Begin();: p& V/ _7 r: D7 N: q# w2 K/ }
  6.         /* 发送读取命令 */
    * ^- |% o8 }" A
  7.         SPI_SendData(W25X_ReadData);
    0 w& `' _- Z" D+ N/ a! k
  8.         /* 发送读取数据的地址,高位先行 */
    # F3 a/ T( [3 M/ m; s- c$ i
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    8 t9 U- l! V# a7 G& ]% J
  10.         SPI_SendData((addr & 0xff00) >> 8);
    8 Y! i0 f! s# u
  11.         SPI_SendData(addr & 0xff);
    0 h9 i6 ?1 r+ M5 c6 _: v  `
  12.         /* 逐位读取数据到指针上 */& l! p  R) i9 ^. p8 [2 P. L
  13.         while(size--)1 l, e4 W7 Z2 P1 `
  14.         {  p8 Y) p6 f+ C- @
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);# n2 }+ c$ H& T& J
  16.                 pBuffer++;+ Z& O7 K% k! `% T; t
  17.         }0 @2 [8 s' F$ m* M# h# D5 }+ J
  18.         SPI_NSS_Stop();: J5 `1 ?$ X  [7 [
  19. }% C8 M- R8 ~9 R- s. }

  20. , m4 n1 X) c% p! J; k: {
复制代码
  I7 z* M- u0 D( A% j
在执行读出数据的时候函数的参数也有三部分:( ^+ L  U# T4 z' I- ?% w
1.要读出的地址
7 I: v; X6 y" ~) x: z6 X) `3 }2.读出到指定地址
+ b6 h+ E) ]5 |0 A3.读出数据的大小$ m- k& c! o+ f1 ^  Z) }
函数执行过程,首先发送读取指令(这时就不用发送写使能了),然后读取数据的地址,然后将数据逐位读取在固定地址中(地址最好是全局变量),使用时再从全局变量地址中获取数据。) {% v6 q$ Y; h# ^
这里涉及到函数的返回值问题,具体分析链接:返回多个变量怎么办
. l1 V( E1 N1 g0 K! X; K1 N5 N( e
) _3 @$ }2 q, a1 ~  W/ e! q! i
+ z0 r2 V# m9 \! E7 ]有一个问题当时困扰了博主一天,那就是发送和读取数据时,怎么把数据返回到主函数中,解决方法是,创建俩个全局变量数组,一个负责发送数据、另一个负责接收数据,这样就ok了' p: f& `% K$ Z, E! r9 T5 v
附上主函数
8 i" u# x9 N* a9 K  E  ?* z
  1. /**********************读取指定地址、指定长度的数据******************/
    # F8 t( u8 A  P" t. s. f0 N5 _+ q
  2. /* 因为读取在了指针中,所以不需要返回值 */
    4 z% Q- E; C! W3 z; @' i
  3. void FLASH_BufferRead(uint32_t addr , uint8_t* pBuffer ,uint16_t size)0 x3 Z$ H: H* o  t6 E& `# S: B3 p
  4. {
    8 E$ d7 g& J0 k4 L2 \% ~
  5.         SPI_NSS_Begin();3 j0 e: Q- _* l+ Z8 M6 P1 {
  6.         /* 发送读取命令 */
    9 {; ^; L0 a: C& x! E0 t
  7.         SPI_SendData(W25X_ReadData);& }  C6 V: j1 P3 A+ |0 E
  8.         /* 发送读取数据的地址,高位先行 */
    ) Y' [. S% [( X( y! i9 k+ d  ~8 W
  9.         SPI_SendData((addr & 0xff0000) >> 16);
    9 [; v5 O0 x4 ~" {
  10.         SPI_SendData((addr & 0xff00) >> 8);
    , G& l9 \3 l: p% L# b& q
  11.         SPI_SendData(addr & 0xff);
    ; [+ O' v8 q8 B, `* b9 V
  12.         /* 逐位读取数据到指针上 */
    ; ?# j# `$ G; B6 z! `
  13.         while(size--), i1 J" R  d- f: f: D2 ], I
  14.         {
    9 a9 y5 A$ T/ X7 z
  15.                 *pBuffer=SPI_SendData(Dummy_Byte);  m& ^1 [5 M3 @6 h, \
  16.                 pBuffer++;
    * Y3 {0 u' S3 }. a* V: ?# |/ Z
  17.         }0 g8 R2 {/ Y( o
  18.         SPI_NSS_Stop();
    9 F0 A6 F& o& B/ O' p) a
  19. }! G4 L4 ]' A' \0 T6 c0 M' ~

  20. % ~' d/ K% [4 j: V7 \- c5 w: ~
复制代码
9 j7 ~# u, e9 k9 U
————————————————
' B8 a4 K4 J4 E  i  g版权声明:Aspirant-GQ
0 K& q: ~% s# F; ]5 J* V
5 E6 P; f/ V) F( n0 ~5 \  l4 E3 H如有侵权请联系删除. I; @& N; V# M# w! _& d
& o* l2 c. t+ h+ W: q2 F" ^) W
8 `( @/ I, b  ]) V  n4 e* E
2 z7 P3 @- Z( R
收藏 评论0 发布时间:2023-3-18 15:00

举报

0个回答

所属标签

相似分享

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32Cube扩展软件包
意法半导体边缘AI套件
ST - 理想汽车豪华SUV案例
ST意法半导体智能家居案例
STM32 ARM Cortex 32位微控制器
关注我们
st-img 微信公众号
st-img 手机版