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

基于STM32CubeMX的SPI总线经验分享

[复制链接]
攻城狮Melo 发布时间:2023-7-1 18:56
1.SPI总线及W25QXX芯片
1.1 SPI总线简介
SPI全称Serial Peripheral Interface,即串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的、全双工、同步通讯总线,在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局节省空间提供了方便,正是这种简单易用的特性,如今越来越多的芯片集成了这种通讯协议。下图是SPI内部结构简易图

& p& i6 t7 u$ W$ Z4 a* p2 B
微信图片_20230701185542.png

" l& f; q- ?8 p" C+ {. M
从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。
SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下:
    1 g7 J, l0 q$ ~4 \* [" d
  • MISO:主设备数据输入,从设备数据输出
  • MOSI:主设备数据输出,从设备数据输入
  • SCLK:时钟信号,由主设备产生
    7 H$ i/ u# {2 E7 S
  • CS:从设备片选信号,由主设备控制
    6 T0 }: |9 H1 U6 X+ W

    * M3 L  R# @0 Z0 ]( }
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。
. Z+ L6 r9 e2 F# K$ V: h% o当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。5 H7 q8 g0 U, C, [( i
SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式:
    2 T, C) ~: q- [: w/ g
  • CPOL=0:串行同步时钟的空闲状态为低电平
  • CPOL=1:串行同步时钟的空闲状态为高电平
  • CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样

      d$ l* f7 g# {
  • CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样

    + P! S. @! \9 O  r7 N# O% `* O
      |* D9 o) s* q' r
微信图片_20230701185539.png
1 G3 s1 u8 u- H0 O) o
1.2 W25QXX芯片简介
W25QXX芯片是华邦公司推出的大容量SPI FLASH产品,该系列有W25Q16/32/62/128等。本例程使用W25Q64,W25Q64容量为64Mbits(8M字节):8MB的容量分为128个块(Block)(块大小为64KB),每个块又分为16个扇区(Sector)(扇区大小为4KB);W25Q64的最小擦除单位为一个扇区即4KB,因此在选择芯片的时候必须要有4K以上的SRAM(可以开辟4K的缓冲区)。W25Q64的擦写周期多达10万次,具有20年的数据保存期限。下表是W25QXX的常用命令表

4 C5 G, F* j/ Y
微信图片_20230701185536.png

" V4 D0 X) E" z6 P, z
2.硬件设计
D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息
    & b+ f5 g" {0 T2 [! d
  • 指示灯D1
  • USART1串口
  • W25Q64
    6 H% N1 H7 ]: \+ E9 z  o& \4 C
  • K_UP和K_DOWN按键" g5 A7 }! O  h/ [( D4 L/ J

    6 }2 R8 L5 z  ?4 D6 F5 V4 x

      k; Q/ b1 [8 K- B9 J+ [1 c
微信图片_20230701185531.png

$ ?- M3 j. ^2 E- a8 l: N
1 O; w" C8 d& b6 F2 S
3.软件设计
3.1 STM32CubeMX设置
➡️ RCC设置外接HSE,时钟设置为72M
➡️ PC0设置为GPIO推挽输出模式、上拉、高速、默认输出电平为高电平
➡️ USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位
➡️ PA0设置为GPIO输入模式、下拉模式;PE3设置为GPIO输入模式、上拉模式
➡️ PG13设置为GPIO推挽输出模式、上拉、高速(片选引脚)
➡️ 激活SPI2,不开启NSS,数据长度8位,MSB先输出,分频因子256,CPOL为HIGH,CPHA为第二个边沿,不开启CRC检验,NSS为软件控制
微信图片_20230701185526.png
, W- ~1 c' |: u* m1 |; e: O

3 J/ m6 z0 o4 f4 q- p. U+ V+ c
➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码

/ b% P% J4 g& d, H1 t1 z& W
3.2 MDK-ARM软件编程
➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中

0 s5 W& Q9 i) [1 u7 ~6 c
  1. void MX_SPI2_Init(void){
    1 N6 Q/ N1 s) a% q' M/ v, {0 ]; B' [
  2.   hspi2.Instance = SPI2;$ t7 C  \  `7 b/ p. c: K
  3.   hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式& p; |6 H2 F5 E
  4.   hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式
    4 `4 I# I" @6 z
  5.   hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度
    * n. T5 Y) D3 c' m
  6.   hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平5 y/ u  F4 T+ o% ]. W" t- `
  7.   hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样# \. S3 D/ Q4 t1 g$ A
  8.   hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制( r- G. E# T) {! t, x% Z9 j# K
  9.   hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256, T  k3 o# X% \: Z
  10.   hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行6 Y/ V% n: _0 J+ \: @; j
  11.   hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式4 \  |3 g- V- j! }9 L
  12.   hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验9 n2 S; r$ r$ E( Y/ E# J
  13.   hspi2.Init.CRCPolynomial = 10;9 V( ]( [0 p- f1 x2 _
  14.   if (HAL_SPI_Init(&hspi2) != HAL_OK){8 N2 ]; s" w0 k
  15.     Error_Handler();6 ~5 p( v7 g( E% ~! B
  16.   }
    7 c8 }. I' U5 r
  17. }
    - m5 p6 e4 b" a+ n2 t
  18. - _, O% u7 I) |5 O  ~& U0 h
  19. void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){  L1 e8 F5 L2 }2 D
  20.   GPIO_InitTypeDef GPIO_InitStruct = {0};2 m2 Z0 {5 O! B4 ]/ g8 v5 n3 x, K
  21.   if(spiHandle->Instance==SPI2){1 C1 F- P$ T% O: H
  22.   __HAL_RCC_SPI2_CLK_ENABLE();  3 m; R3 v6 x% j, {8 o  G( ~0 L5 K
  23.   __HAL_RCC_GPIOB_CLK_ENABLE();- U8 V7 w+ g2 H0 ?) F  Q! }  o' E
  24.   /**SPI2 GPIO Configuration    + {6 g6 t! D. W6 }% s
  25.   PB13     ------> SPI2_SCK
    " h9 y, S; R( B) |' F
  26.   PB14     ------> SPI2_MISO; Z. f! A4 R' A. ~7 k3 P) w5 ~1 A
  27.   PB15     ------> SPI2_MOSI */9 {8 C& R/ |8 m' p. @
  28.   GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;3 B) S3 Y0 ^/ d1 T5 ^5 x( b
  29.   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    " b+ o2 `% x6 M
  30.   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;2 E! Q# _( r4 {. b- \+ w. A4 M
  31.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    . ~7 c- W! X+ J
  32.   GPIO_InitStruct.Pin = GPIO_PIN_14;+ E- ~2 p6 s0 Y4 @' {
  33.   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    2 f2 h  Z) ]2 Q6 V- h; C, Q
  34.   GPIO_InitStruct.Pull = GPIO_NOPULL;7 b& ]/ G& ~; R
  35.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);* {! I, d4 q* c$ V% Y- n
  36.   }
    # M$ U, H1 f, m* o; |2 G6 y
  37. }
复制代码
6 u2 ~# `$ U# f" `* k
➡️ 创建按键驱动文件key.c 和相关头文件key.h
➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍
  1. //这里仅介绍几个重要的函数0 n- N4 O& g7 g% g4 p' ^* R
  2. void W25QXX_Init(void){
    ) ?  G7 c, o) U! S& p* N9 @) B3 k
  3.   W25Qx_Disable();3 W) D! h8 u5 D3 |. ]
  4.   W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID. |+ H7 u# T) G/ V9 b0 p" Q
  5.   printf("FLASH ID:%X\r\n",W25QXX_TYPE);
    ; }" w3 P) I, `& \, k& l
  6.   if(W25QXX_TYPE == 0xc816)
    + D9 q5 ^# f3 u2 d
  7.     printf("FLASH TYPE:W25Q64\r\n");
    1 `+ P- V. [* h$ F$ G5 z
  8. }7 i; I  G7 j' J8 A; ]4 U0 R

  9. ( j- `' A  d. k; b: T: Z
  10. uint16_t W25QXX_ReadID(void){
    ! Q+ i4 h" X5 r4 [
  11.   uint16_t ID;
    , D8 U5 K) F/ L+ r5 s) h0 U+ K2 F" X
  12.   uint8_t id[2]={0};" n7 h0 P- C* ~, D# P) s
  13.   uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令        : c5 n" T- E) _( J
  14.   W25Qx_Enable();//使能器件8 i$ E" N% ~- R' {% D* j
  15.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);9 a- P; L9 ]" Z+ \) ~
  16.   HAL_SPI_Receive(&hspi2,id,2,1000);
    : E! U$ ?% _' V4 g
  17.   W25Qx_Disable();//取消片选          J- g/ C4 r" ]' }9 ~
  18.   ID = (((uint16_t)id[0])<<8)|id[1];" Y9 h; t8 Z9 y9 N. {
  19.   return ID;+ \% @' G' G' \* |
  20. }
    / e$ P: I7 w6 L
  21. 7 ?1 j3 o7 X; Q) [( K* T1 I0 _4 ?! w
  22. void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){
    6 h9 F' W2 r! g% I6 Q6 h
  23.   uint8_t cmd[4] = {0};# \$ M- j( `3 K; J5 w: o) `% W
  24.   cmd[0] = W25X_ReadData;//读取命令2 |# Q- T7 d* z
  25.   cmd[1] = ((uint8_t)(ReadAddr>>16));
    8 t6 @* ~: j* {5 r
  26.   cmd[2] = ((uint8_t)(ReadAddr>>8));2 j3 m( k+ h( z
  27.   cmd[3] = ((uint8_t)ReadAddr);: j, {  c* X. e5 E) k' l" ^9 i
  28. * L& u0 @; M, E4 z; B% D4 R
  29.   W25Qx_Enable();//使能器件& q4 ?  i5 t% S5 `. p. y
  30.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);' D" Z* Q: j7 P. M0 P: S
  31.   HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);2 w! k' E, ~* o, o( y1 M% _9 f
  32.   W25Qx_Disable();//取消片选        2 m6 A# ]9 u5 T  i7 n3 A
  33. }
    8 T% }1 x; R( p7 a+ l! n* ~! ~+ w
  34. 3 u1 d) r# B; K+ L; s
  35. void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){5 k1 A. h' l5 D* }# `3 y
  36.   uint32_t secpos;
      M) m* J) m: Z& ?$ r! s; k% j- Y1 n
  37.   uint16_t secoff;0 }' I% D% U; \% t. d# \
  38.   uint16_t secremain;
      D9 B- n# o1 @( N& @$ [1 ~
  39.   uint16_t i;# T% m* S- u' f6 j3 z. G
  40.   uint8_t *W25QXX_BUF;
    / E, M- f/ W6 P! T
  41.        
    " K  m' A1 p# v% r3 N) _
  42.   W25QXX_BUF = W25QXX_BUFFER;
    - G3 P" v$ `* Y
  43.   secpos = WriteAddr/4096;        //扇区地址
      j+ f, s; r4 F. z# ~0 w% R) s) f
  44.   secpos = WriteAddr%4096;        //在扇区里的偏移1 I2 }7 [% j, t9 Y+ R% [! r  i
  45.   secremain = 4096-secoff;        //扇区剩余空间大小
    8 S% m# P6 W! {" \$ D) W
  46.   printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);
    8 z' {2 i# h. D) W% i  W/ K' A# Z
  47.   if(NumByteToWrite <= secremain)  //不大于4K字节
    5 u+ W3 w9 }8 K2 F$ V
  48.     secremain = NumByteToWrite;; R  d2 |' J8 I+ k& E9 p. l% i
  49.   while(1){
    9 y1 @! b& h& I1 l' w  _
  50.     W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容& h! U$ I! [4 o, q2 n1 X
  51.     for(i=0;i<secremain;i++){//校验数据
    3 l5 S9 A1 P2 @. `4 q
  52.       if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除/ g) G! L# |  k$ y! P
  53.         break;
    3 v6 g) ]8 k/ w3 d  |* H7 M+ v
  54.     }
    $ M; E; I; W  }* a2 i0 G, y, j) O" E
  55.                
    3 x" L2 c3 X6 s9 d0 b8 V
  56.     if(i < secremain){//需要擦除! u& Q+ g3 E& T: ^- R5 i" C5 d2 m$ e
  57.       W25QXX_Erase_Sector(secpos);//擦除扇区
    5 z1 b$ s. q, d* m' u
  58.       for(i=0;i<secremain;i++){
    0 S5 ?: \: y* b7 X' z
  59.         W25QXX_BUF[i+secoff] = pBuffer[i];
    # C, C8 E* \% _8 q
  60.       }
    ! ?' F$ _" D. N+ S9 l( Z
  61.       W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
    6 a( S7 |3 F4 ~* K" K# T( a
  62.     }2 i4 D1 m( R7 p
  63.     else{: g/ R3 }2 c% G: o
  64.       W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间3 X8 y* j8 o& r0 t6 p; u" P: K
  65.     }
    $ i7 l, k/ v3 l: d5 l6 Z
  66.                
    9 _" n$ k7 A! E; F3 D7 x/ @
  67.     if(NumByteToWrite == secremain){//写入结束了
    , x2 d+ v4 u/ q  F; [. e* @* q
  68.       break;8 e$ W5 q6 b, d; ?9 l0 w: n
  69.     }
    ; F' ^5 s# J4 U3 \- C& Z. |
  70.     else{        //写入未结束
    1 p- W+ X3 U+ h5 b" K
  71.       secpos++;        //扇区地址增1
    ( L/ J* P6 x0 n9 g& o7 z% R: l
  72.       secoff = 0;                //偏移位置为0                " q& m# v8 I: x# {( F
  73.       pBuffer += secremain;        //指针偏移4 r  a$ z. j; }( v  o  }
  74.       WriteAddr += secremain;        //写地址偏移: F( {" Q" r- l% O
  75.       NumByteToWrite -= secremain;//字节数递减4 T) `/ k" P3 l6 ]8 d, k
  76.       if(NumByteToWrite > 4096)                  G( b. |& i! K$ O5 ?
  77.         secremain = 4096;        //下个扇区还没是写不完, g3 K1 [( ^0 l" x
  78.       else
    . U: S& L  Q: ?" U, Q
  79.         secremain = NumByteToWrite;//下个扇区可以写完了
    9 t* a$ h& I& t* n
  80.     }" v* \# @8 Q& Y$ m
  81.   }: Y, F# S/ Q" D
  82. }
复制代码

/ O9 g" `7 N  i- L/ J+ S) W; O  ~
➡️ 在main.c文件下编写SPI测试代码
  1. /* USER CODE BEGIN PV */
    4 d% }# T) q$ q! w
  2. uint8_t wData[0x100];# U; z8 {: Q8 \+ W( C& }
  3. uint8_t rData[0x100];
    / R8 J+ R: N" X: Q3 E8 @0 R% Q
  4. uint32_t i;2 g4 j6 T! d6 y2 S7 t" u4 Q+ U
  5. /* USER CODE END PV */! l7 w& \% ^. n4 q+ P
  6. int main(void){6 G) [& d: V2 V6 e  O3 n
  7.   /* USER CODE BEGIN 1 */) x+ ^) @/ \: A7 k  ~7 j. i
  8.   uint8_t key;
    + ~6 k+ y( M6 P8 P/ [
  9.   /* USER CODE END 1 */+ v3 S3 H0 _2 L2 N- M
  10.   HAL_Init();
    - L' w9 \! L! I2 |# R7 d
  11.   SystemClock_Config();6 E2 g6 t; L5 c% x+ ]- c
  12.   MX_GPIO_Init();! V" K6 G' x8 i. E1 _/ v8 J$ F" Y; ~* Y: B
  13.   MX_SPI2_Init();. v- G, F5 W8 W8 u/ b. d
  14.   MX_USART1_UART_Init();
    - f: o( V0 a) E! a
  15.   /* USER CODE BEGIN 2 */9 D7 w" {7 ~% a; l' ^/ S
  16.   W25QXX_Init();
    1 j3 c1 ?" h1 `8 n% K7 }4 i
  17.   for(i=0;i<0x100;i++){
    1 @; c1 i9 K- u+ Y4 h& l
  18.     wData[i] = i;* r4 X  K# V$ I6 o
  19.     rData[i] = 0;
    " p' \8 \0 F( L- b8 X
  20.   }       
    ( [& E0 X. O. _: v1 y+ g7 \
  21.   /* USER CODE END 2 */
    3 V0 o- Z" b% i2 d" m, w
  22.   while (1){
    * W: `% c9 K  s  f* ?2 c
  23.     key = KEY_Scan(0);; [+ s1 D7 N# ~; e* ^6 X
  24.     if(key == KEY_UP_PRES){1 R4 d' ^  h- X& p- Q' r- t( ^8 p& U" Q
  25.       printf("KEY_UP_PRES write data...\r\n");( f4 l, X9 \4 o5 C- w+ l7 h+ }
  26.       W25QXX_Erase_Sector(0);
    ! D2 p; n/ T/ F5 h2 M4 A
  27.       W25QXX_Write(wData,0,256);               
    5 o5 a' V2 u/ \7 s, b
  28.     }
    3 `$ J( E: C' C) \4 @$ l' |
  29.                        
    6 |) o# D, c0 B- e. E
  30.     if(key == KEY_DOWN_PRES){
    # ^' m+ z: [' s3 t; t7 F
  31.       printf("KEY_DOWN_PRES read data...\r\n");
    0 K* h+ t9 r& V, }" x
  32.       W25QXX_Read(rData,0,256);
    . [% A; E. [$ J& I* ^3 W# Z( f
  33.       for(i=0;i<256;i++){
    $ z" b2 r. j/ Y  Z" Q- ~
  34.         printf("0x%02X ",rData[i]);
    - h1 Y4 c# Y/ L$ `* M
  35.       }
    : k& K" B- o* L' U- c0 p1 z
  36.     }
    6 |' e6 [' J! K. O: T6 U
  37.                
    8 D3 l4 }  J/ T1 f
  38.     HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);9 M5 q7 b6 w; m2 q  C& B
  39.     HAL_Delay(200);
    : U1 u* [1 I  G+ o6 [, X! E
  40.   }
    & a/ _+ ?5 l# T" t" k4 M
  41. }
复制代码
, K- i% j' J0 K( T+ u3 v" {
4.下载验证
编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息
; q! O$ C5 r! f6 [
微信图片_20230701185521.png . }5 H6 v  ]- }& l& K- G8 C

8 E- n" [/ Z/ B1 x# R- E转载自: 嵌入式攻城狮
- ^) y8 Q$ `' d  l% L- x如有侵权请联系删除( j, u! w& D. y9 O
# h7 z! W% k# E6 q
( j: H, a- @9 J" `+ @& U. a

8 ^4 [4 @& k+ S$ [, y( R
收藏 评论0 发布时间:2023-7-1 18:56

举报

0个回答

所属标签

相似技术帖

官网相关资源

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