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

基于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内部结构简易图

5 V! W8 ^- O# t/ a6 o5 `
微信图片_20230701185542.png

% M: W7 H' C) o' k! V4 Z# j5 \! n
从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。
SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下:
    0 u! p" \9 S" |! Z1 s
  • MISO:主设备数据输入,从设备数据输出
  • MOSI:主设备数据输出,从设备数据输入
  • SCLK:时钟信号,由主设备产生

    5 S, h! {5 Q$ R3 u! b8 i5 z
  • CS:从设备片选信号,由主设备控制
    . _3 i& G9 I2 r
    + o+ l. F# w8 [
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。
& |# j' w' V% i* c2 b) D当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。- D1 ^' Y0 q; p( O; x/ `
SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式:

    0 \7 s7 ]# |# l3 G% X
  • CPOL=0:串行同步时钟的空闲状态为低电平
  • CPOL=1:串行同步时钟的空闲状态为高电平
  • CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样
    7 H% @6 m* u+ ?+ y
  • CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
    ) X& s* W5 J) G( {% }8 ^+ p

    $ ?% b; b& w. |) C0 w
微信图片_20230701185539.png

, }4 P: {- Y# n" t; n
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的常用命令表

& c5 i( f$ [8 b* a  O* x+ k8 W/ r; ^
微信图片_20230701185536.png
: _( P- F% s* p0 ~) Q0 x
2.硬件设计
D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息
    7 a& |6 f9 b& D: C8 D! V% N1 D
  • 指示灯D1
  • USART1串口
  • W25Q64
    & W' P' s; ?  }! Q& M# c0 [' r: K
  • K_UP和K_DOWN按键
    + M$ _; V3 l* `9 o+ Q8 H. |

    7 n! d4 a. }1 S

    , R* q# U) Y, F6 ]
微信图片_20230701185531.png

2 i6 y: ?. `4 W1 B7 R
4 n  v* ~, ~3 ?3 Z9 Q
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
7 x# X7 q9 g% D# A
8 |+ P) N, p7 D/ s! |
➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码

7 i' Q/ F2 M& }" w7 S
3.2 MDK-ARM软件编程
➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中

: H& V, J# n9 h0 @9 E
  1. void MX_SPI2_Init(void){* @. z0 s( q3 Z; l
  2.   hspi2.Instance = SPI2;$ l6 y8 ]( M4 P
  3.   hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式) O; J  b9 a3 [
  4.   hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式' B" ?  a1 i  G
  5.   hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度6 u; f$ N9 R$ i. G, e
  6.   hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平& z) M  e, ?& D
  7.   hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样5 b8 b- O& w( o; r2 I! V- c3 [
  8.   hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制3 S" w& _; [5 g6 e! }  a* @
  9.   hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子2563 j, ?$ r) j3 N7 y
  10.   hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行% v. a! c5 R+ u/ `8 E8 D& |
  11.   hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式7 G1 D$ \7 t8 ?% [5 y1 e' g
  12.   hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验
    ; J, d$ x0 Z3 C
  13.   hspi2.Init.CRCPolynomial = 10;
    1 h4 ?% B  K  B7 i+ m
  14.   if (HAL_SPI_Init(&hspi2) != HAL_OK){2 W9 p  u- J( p8 V; B2 C: A: I
  15.     Error_Handler();
    8 s9 C) P( {5 Y5 A* H$ ^- {
  16.   }
    / f: H, R$ {& ?, e) J
  17. }
    9 n, H8 D# F3 Y# \
  18. 7 v. d* `% Z  V- w$ c- g
  19. void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){2 @& Q7 t9 S+ \% h1 y% B2 C! s2 G' f
  20.   GPIO_InitTypeDef GPIO_InitStruct = {0};
    : H3 j7 z, r; W
  21.   if(spiHandle->Instance==SPI2){
    ) W1 u6 T1 @+ p' q  K. E9 Y  `0 g
  22.   __HAL_RCC_SPI2_CLK_ENABLE();  
    . P2 P' u, J) B  k$ Q: \
  23.   __HAL_RCC_GPIOB_CLK_ENABLE();5 x  u& Y  u( m- W) m! o
  24.   /**SPI2 GPIO Configuration    6 Q3 B  Q0 J- g. y  y
  25.   PB13     ------> SPI2_SCK5 H$ b% l- u$ X" @. t, S
  26.   PB14     ------> SPI2_MISO
    1 {9 V; f/ P6 s, Q
  27.   PB15     ------> SPI2_MOSI */4 e( n7 Z" ~- X
  28.   GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;) r- N; z5 {7 D; _0 J
  29.   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;# K" X. W+ j8 ~7 i/ j7 d
  30.   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;& h) a% M5 u" Q/ r5 X, ]
  31.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);% A) h; s' @3 z* P( M% q6 v) m
  32.   GPIO_InitStruct.Pin = GPIO_PIN_14;1 y: t$ j" @- j8 v: T4 t, {# c) b- f
  33.   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    * Z3 W9 i/ b$ g; k0 }
  34.   GPIO_InitStruct.Pull = GPIO_NOPULL;( v+ U  A( `% m) K7 S. p
  35.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    " q. u# Q( _( o
  36.   }+ v6 }# P8 l3 `9 b
  37. }
复制代码

+ t, ?' K6 G/ q- l0 {  h1 b
➡️ 创建按键驱动文件key.c 和相关头文件key.h
➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍
  1. //这里仅介绍几个重要的函数
    ' u1 J8 w% U* A- ?* P9 ^& J
  2. void W25QXX_Init(void){3 n4 Y' F6 S6 A* F2 u. f8 D
  3.   W25Qx_Disable();# x- J+ J7 L$ k7 ~2 `& D2 e
  4.   W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID
    % u) z+ ?$ K. q6 F9 q1 |
  5.   printf("FLASH ID:%X\r\n",W25QXX_TYPE);. @7 y3 w1 [- Z1 @' ^. ?
  6.   if(W25QXX_TYPE == 0xc816)
    % }: E" j5 O5 q/ V% A- Y, P
  7.     printf("FLASH TYPE:W25Q64\r\n");0 C* b' E8 b2 G- f
  8. }& ~7 `1 G4 a) O# d. v+ Z  w5 t/ R" w

  9. ' u( {2 |: I5 U/ l% q( `
  10. uint16_t W25QXX_ReadID(void){
    # |5 p* W) `" @; F- i& G
  11.   uint16_t ID;
    $ _9 {2 Q1 u- \$ m9 L4 O, W1 W
  12.   uint8_t id[2]={0};
    0 A' ^* P0 {$ f% _. P  \$ P( g
  13.   uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令        2 k- V- h- p/ Y& P# I
  14.   W25Qx_Enable();//使能器件3 t) `8 u$ E2 @, s1 p. e5 v# S
  15.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);1 V, C" g/ H; z3 n
  16.   HAL_SPI_Receive(&hspi2,id,2,1000);
    # l. F+ l/ c2 u: e) T9 u% |
  17.   W25Qx_Disable();//取消片选       
    % y" j1 S5 B1 I6 H9 w
  18.   ID = (((uint16_t)id[0])<<8)|id[1];
    : e- P# r2 G6 T$ U8 n
  19.   return ID;
    $ C: @7 r& t: x
  20. }* f1 i5 _$ i/ H7 J! S

  21. # A9 T( p8 {; H" k  @) q# K
  22. void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){
    & g! J& t3 `" U
  23.   uint8_t cmd[4] = {0};
    ; N* F0 \# z8 [6 N4 ^
  24.   cmd[0] = W25X_ReadData;//读取命令! G5 `3 P* }3 g
  25.   cmd[1] = ((uint8_t)(ReadAddr>>16));
    8 P1 a4 K5 z) Z, {$ l
  26.   cmd[2] = ((uint8_t)(ReadAddr>>8));2 g- V% x8 V; X
  27.   cmd[3] = ((uint8_t)ReadAddr);/ j! K0 J1 s4 S( T" q

  28. 3 `; S' Q. w1 R  M3 c- g8 c" b
  29.   W25Qx_Enable();//使能器件$ z4 p, X9 s5 \7 F( B  h
  30.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);; s" O$ a6 z- E& e* `
  31.   HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);
    7 o( U6 r2 e3 e# r1 p5 V3 o  p' U
  32.   W25Qx_Disable();//取消片选       
    9 Z, f0 S' a8 `" R( w3 A4 q
  33. }. l9 Q- q8 a9 w; |5 J
  34. " |8 o( ^# C1 C& d# ~8 Q5 l- [: [
  35. void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){( E9 Y0 z$ A4 K
  36.   uint32_t secpos;  ]4 \4 y) ^6 O: A
  37.   uint16_t secoff;+ c7 Z  B% O. ]" d
  38.   uint16_t secremain;
    # s% R1 w3 m% B/ W3 _* H. @
  39.   uint16_t i;! o' p# |" {* [+ Z5 o# S& F
  40.   uint8_t *W25QXX_BUF;4 W, h  h( ^( c/ `
  41.         # x: @! j7 Q. F7 y
  42.   W25QXX_BUF = W25QXX_BUFFER;
    & l8 s7 [) o2 Q7 ]" h
  43.   secpos = WriteAddr/4096;        //扇区地址
    0 N5 m1 a% `2 A4 I: z6 J8 x
  44.   secpos = WriteAddr%4096;        //在扇区里的偏移) ^* W" W% Y0 C' l5 \' d5 F' r( C
  45.   secremain = 4096-secoff;        //扇区剩余空间大小
    - R- @! \* K; h1 _. R# i2 U' H
  46.   printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);
    # j& b4 j8 _7 q. x( c: U4 s
  47.   if(NumByteToWrite <= secremain)  //不大于4K字节3 E. }- m, Y) F
  48.     secremain = NumByteToWrite;
    ; y# \! P2 m  c% M* b: r, I4 J
  49.   while(1){# a5 p& @6 s; s3 H  N, [& F
  50.     W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容5 o# Q* `5 J3 {& T, p) X
  51.     for(i=0;i<secremain;i++){//校验数据
    + o6 v/ m7 F# e9 A$ _
  52.       if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除
    + f0 W" L3 b2 Q9 K9 i! h& u
  53.         break;) j, n8 X3 @( i% }" C0 \, e/ J4 x) m
  54.     }6 a! Q4 r4 u; H0 r( B
  55.                 2 e. f- I5 a1 P
  56.     if(i < secremain){//需要擦除8 B# ^5 s* g. g, \  b: x; _/ j
  57.       W25QXX_Erase_Sector(secpos);//擦除扇区
    9 M# |6 z3 S; [( n- V
  58.       for(i=0;i<secremain;i++){
    - G6 v/ F& ^2 r5 \- v- v
  59.         W25QXX_BUF[i+secoff] = pBuffer[i];
      i& A9 h( {2 _* s7 P4 l3 ~6 F/ s
  60.       }) U. R# p4 e6 R9 d+ O6 o2 c# B, M% I
  61.       W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区  s" W4 {; A3 O& E
  62.     }
    ; i. J4 p8 G; o
  63.     else{
    . E9 O+ ?. X+ l7 a# U) V
  64.       W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间
    3 K5 F7 d/ C; B
  65.     }, h, ?& h) `6 j3 ]1 V* j
  66.                
    7 W1 l1 {( O8 d5 Q7 J+ }3 p) u% V
  67.     if(NumByteToWrite == secremain){//写入结束了& `& c$ d, G. t$ Q/ H
  68.       break;
    3 i3 b( l5 a7 i$ l/ W! p0 `6 j
  69.     }
    1 E0 N/ W8 p7 T% }9 t9 A7 {$ K; h% M
  70.     else{        //写入未结束  h  e" V! M2 |8 Z1 P8 i
  71.       secpos++;        //扇区地址增18 r. t' B% {6 R! ^
  72.       secoff = 0;                //偏移位置为0                7 G2 s! F; O& v( T: h  g2 I0 ]; _
  73.       pBuffer += secremain;        //指针偏移9 ?8 ~2 u+ f( P% m& H
  74.       WriteAddr += secremain;        //写地址偏移
    8 u8 Y! V  [0 s. X
  75.       NumByteToWrite -= secremain;//字节数递减, C$ {! i0 M+ x5 }) ?7 h
  76.       if(NumByteToWrite > 4096)                * w( Y6 k  k+ O
  77.         secremain = 4096;        //下个扇区还没是写不完. o7 T2 ?" t1 N# \2 R9 d' v, `
  78.       else
    ! a2 u( p1 y9 r% e! {) N
  79.         secremain = NumByteToWrite;//下个扇区可以写完了
    # i% k* q: G0 ?2 H4 t
  80.     }( X+ i8 ]( J9 h5 d% m+ R2 }. N6 R
  81.   }; Z  @' \2 H  X8 L6 l9 M, H1 r
  82. }
复制代码
# y' H" R1 B% H9 y7 Y
➡️ 在main.c文件下编写SPI测试代码
  1. /* USER CODE BEGIN PV */
    / M# r; O( W+ w. P# I. r! u
  2. uint8_t wData[0x100];4 h- j% W4 d: J, A
  3. uint8_t rData[0x100];6 l) A+ L* [! x: c/ {) G# A- o
  4. uint32_t i;
    $ {  u3 z7 Y3 ~! P' S% Z& e& K* y
  5. /* USER CODE END PV */$ j! f- p) g6 ^  r( G+ y+ k
  6. int main(void){
    5 f5 r5 V! ]# s6 S# z3 Q$ }
  7.   /* USER CODE BEGIN 1 */
    % `3 k0 |4 ^7 |- [; g
  8.   uint8_t key;  B& Z6 `3 A1 U4 q' i
  9.   /* USER CODE END 1 */' l6 k: E9 t7 n3 l( e" o" @% A3 F
  10.   HAL_Init();3 t0 I$ C0 T. Z! v/ E1 P
  11.   SystemClock_Config();# x. c7 T* A& E/ ~: ^# ]
  12.   MX_GPIO_Init();
    2 v2 n( u/ ~+ c$ O7 z" ?% V
  13.   MX_SPI2_Init();
    . ~# Q' S* e5 X- @$ E. J
  14.   MX_USART1_UART_Init();
    ) g1 g0 d& T1 A; h" A0 K( g3 B
  15.   /* USER CODE BEGIN 2 */
    ' ?$ Y, _$ B! Q( e
  16.   W25QXX_Init();
    7 q1 j5 ^/ s8 |0 j: c3 A) ~
  17.   for(i=0;i<0x100;i++){
    # i" A8 ^4 W. D
  18.     wData[i] = i;
    6 p3 z; Z( `: @8 b, x+ P
  19.     rData[i] = 0;, k' l8 k# V  _* U+ o; M. x. C
  20.   }        ' o+ T0 @0 A7 D. ?+ K
  21.   /* USER CODE END 2 */
    ; A0 l8 I; K  P  {8 j) A$ [
  22.   while (1){! o8 ]- F! N- A: t& [
  23.     key = KEY_Scan(0);
    + M4 l, R+ j5 g# g; \' K
  24.     if(key == KEY_UP_PRES){
    - @9 g* D" J5 O! {2 |5 }
  25.       printf("KEY_UP_PRES write data...\r\n");
    ' l1 d) a% R7 P' [
  26.       W25QXX_Erase_Sector(0);* `: k' Y! m& G3 p$ w- {* ~* O" T9 F
  27.       W25QXX_Write(wData,0,256);               
    2 T* |1 a8 p; ?) J; F1 H
  28.     }- v5 F( {- p- P6 `9 a: p, [
  29.                        
    7 N9 j6 f8 F  A& ~0 D4 r+ A' E: c
  30.     if(key == KEY_DOWN_PRES){% l, C: O+ n; u9 p9 u8 g
  31.       printf("KEY_DOWN_PRES read data...\r\n");. w* Z3 i0 m9 Z* I( t9 d- j1 k/ o. ?* e
  32.       W25QXX_Read(rData,0,256);1 t( ]7 g  Q( y
  33.       for(i=0;i<256;i++){: X/ T, r3 R% W) |
  34.         printf("0x%02X ",rData[i]);! B3 k1 Y% Y9 ?6 W
  35.       }
    " ^1 i4 C: {6 T3 S+ C# F
  36.     }7 o$ H  e3 k- H& M
  37.                 / d7 T: @# s8 x# V( e0 r* w
  38.     HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);, q3 [- D8 r5 z" f
  39.     HAL_Delay(200);4 V3 G" N+ Y2 {* e- A
  40.   }3 ~. `  \: m! a2 k1 y' `- `  r
  41. }
复制代码

0 T3 r$ `" ]9 U; _5 ?, p' w9 C
4.下载验证
编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息

; X" d$ ^0 A' D6 @% O7 k6 O; g: h# ] 微信图片_20230701185521.png ( R- \3 ~" F, ~; T+ o& D
) P5 ?6 Z9 p& L. |( E
转载自: 嵌入式攻城狮! c3 |3 P! m8 J( |- J. p7 C
如有侵权请联系删除6 ?3 @4 V4 J* m7 X, s: _3 N

+ n6 F4 {) v# v+ s' z  P: }' z* D! L  ~

+ @2 {! V: M9 ^8 d1 j
收藏 评论0 发布时间:2023-7-1 18:56

举报

0个回答

所属标签

相似技术帖

官网相关资源

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