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

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

. Q) s( M! ]3 a9 c, [) e# i5 b+ C
微信图片_20230701185542.png
) {* G6 {* m! A  K% n2 U/ e% [% S
从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。
SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下:

    0 l; p9 X/ m6 ?" ~9 @& G
  • MISO:主设备数据输入,从设备数据输出
  • MOSI:主设备数据输出,从设备数据输入
  • SCLK:时钟信号,由主设备产生

    ; \1 V" I9 ?  t4 ~
  • CS:从设备片选信号,由主设备控制
    + u1 ^7 P* `+ `9 b: a
    1 Q; F4 S# W8 n' c7 k  T( C9 D
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。5 B0 M4 g$ u( H* Y  F
当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。( A1 @4 l7 o, J% Q" E
SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式:
    5 X+ O+ [9 g2 d7 U4 E1 Q
  • CPOL=0:串行同步时钟的空闲状态为低电平
  • CPOL=1:串行同步时钟的空闲状态为高电平
  • CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样

    0 B4 F$ f1 F' K2 B. U! z
  • CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
    ! f) C6 F: F3 E4 a$ a

    / d7 `* r" B9 V% Q5 C/ q' r: p
微信图片_20230701185539.png
. _- c- @5 n4 Y; f: Q
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的常用命令表
# V/ A3 g) S8 f- u% P
微信图片_20230701185536.png
1 E+ h4 d; b- f
2.硬件设计
D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息

    8 [+ M/ t/ a( r5 q! y, z2 q) c
  • 指示灯D1
  • USART1串口
  • W25Q64

    ' n& W6 r- \0 p, o
  • K_UP和K_DOWN按键
    ! W" F0 o1 @$ @- }% w
    4 N8 }, I! H  i2 k
    ! g& L' v5 T# j  N5 b
微信图片_20230701185531.png
, b8 d5 p5 ?7 O. g% g- S
; c6 Z% [8 |# d6 P
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
& p; \  L6 E& F8 ?) u

1 j) I" v% ]) k1 _' u& y$ L
➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码
8 d. s' j1 e. [
3.2 MDK-ARM软件编程
➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中

3 Z3 U$ }1 i* C  h9 Q" v
  1. void MX_SPI2_Init(void){& }$ `2 y$ ?7 b4 A! A( s2 h
  2.   hspi2.Instance = SPI2;
    ( C; i) K1 A4 [
  3.   hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式) t3 r$ O8 d8 S7 h% B5 V6 r9 r$ H
  4.   hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式
    3 h7 }# s. v& ?+ d- b
  5.   hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度, Q: d; O% n! h" A% @; E
  6.   hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平) u% z- X1 ~; T/ h. j! J- l
  7.   hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样
    9 A  Q% ~9 e: c5 J5 `6 G
  8.   hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制
    & t: I$ [$ l0 r
  9.   hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256
    : `) d, B& q. N; `+ `
  10.   hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行
    $ X7 \( J. b' F6 x( e$ _2 S8 a7 |
  11.   hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式; v2 H8 [, G2 V! d1 \, B& ^1 r2 `# @
  12.   hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验( a% {  H, N. P5 P4 k
  13.   hspi2.Init.CRCPolynomial = 10;
    , w3 b/ a' d* k8 q6 p
  14.   if (HAL_SPI_Init(&hspi2) != HAL_OK){0 F2 g" V7 @! i) _& B3 X+ T$ K  v& G
  15.     Error_Handler();+ x' G+ r( E, K" z5 S5 Z
  16.   }
    % n. a8 v7 X* I9 M
  17. }% J4 e% q% ~& D2 R8 g9 `7 j6 R

  18. - X  W2 u: ]2 p8 b) ^
  19. void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){0 @4 N% X! b7 c6 g7 i" a
  20.   GPIO_InitTypeDef GPIO_InitStruct = {0};5 L( {0 C* }  M
  21.   if(spiHandle->Instance==SPI2){
    ( w% G' c: ]" h$ ]
  22.   __HAL_RCC_SPI2_CLK_ENABLE();  9 r) x' }- U2 g. W# K) o8 D+ j+ \
  23.   __HAL_RCC_GPIOB_CLK_ENABLE();7 D5 }( s+ L1 b, y* e
  24.   /**SPI2 GPIO Configuration   
    0 o3 b: |0 \9 f9 e
  25.   PB13     ------> SPI2_SCK
    & R0 }. G' ?0 b5 M2 o1 A3 @
  26.   PB14     ------> SPI2_MISO
    0 S3 Q" a. _, R: C
  27.   PB15     ------> SPI2_MOSI */
    $ V+ Z1 U' S& ]7 O5 {3 l& G* ]  ?
  28.   GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;) ]7 ]( b7 J' j1 {8 k+ C! p$ I& R( B
  29.   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;; l* h0 O, }) c1 K; T6 t
  30.   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    ! C; ]# v& y: _$ s* K3 |
  31.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    5 m  Y9 b9 T7 z
  32.   GPIO_InitStruct.Pin = GPIO_PIN_14;
    7 f. b- a# M* L4 S+ m
  33.   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;! N/ }' p4 U# x- Q; M5 k9 ^% Q
  34.   GPIO_InitStruct.Pull = GPIO_NOPULL;) o7 q, J" ?: A4 s
  35.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);& a+ T, M. X% y1 q
  36.   }! B& M% J3 l& U- ]' J' D- {
  37. }
复制代码

9 O. y: W+ U2 m9 `8 A) `$ l
➡️ 创建按键驱动文件key.c 和相关头文件key.h
➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍
  1. //这里仅介绍几个重要的函数4 C) r$ J& @9 J/ [" N. p3 k2 @
  2. void W25QXX_Init(void){
    1 q& P+ z8 ]7 a5 r6 v, K
  3.   W25Qx_Disable();* _4 C0 I1 H5 i' z4 v* Z+ V
  4.   W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID* N& \# \# O7 V$ K% `
  5.   printf("FLASH ID:%X\r\n",W25QXX_TYPE);7 a& `6 ^: G, |1 i% j
  6.   if(W25QXX_TYPE == 0xc816)4 m8 N  A! d6 p1 |$ S3 C
  7.     printf("FLASH TYPE:W25Q64\r\n");4 f* R$ C9 y6 M; ^- u/ w0 u" S
  8. }2 \# S5 W% z+ v$ n

  9. 0 U% F/ U) `& l& V# I# n+ s3 A
  10. uint16_t W25QXX_ReadID(void){3 _2 S- P1 q/ E$ l% T" U; J/ ~) H
  11.   uint16_t ID;4 E6 L4 y3 {0 }" o8 g* W0 @
  12.   uint8_t id[2]={0};
    + e: s0 B# l5 S" [0 s  G4 u  Y
  13.   uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令        1 s/ [: H5 r# Y, ]8 t
  14.   W25Qx_Enable();//使能器件( C0 S; `& f. a* C( j4 @
  15.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);
    * u+ R% z) |% `5 ]" K, }% @
  16.   HAL_SPI_Receive(&hspi2,id,2,1000);4 v8 v: N& ^& X
  17.   W25Qx_Disable();//取消片选       
    ' D( l" i. T/ a% X7 _+ \" h2 W" E
  18.   ID = (((uint16_t)id[0])<<8)|id[1];* u" r5 {" ^( S+ X( q( U8 g9 h( c
  19.   return ID;7 a: _2 r5 a9 y- D% U# y9 C
  20. }. S, B+ Z5 v6 s# O% S/ c3 Q
  21. . l. p' ~2 c/ P6 Z
  22. void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){; [9 Q2 L6 j) f1 \4 ^& A
  23.   uint8_t cmd[4] = {0};+ ?& ?  g8 _- z* h& P9 j. x
  24.   cmd[0] = W25X_ReadData;//读取命令; E2 s: r8 n/ U0 a& V
  25.   cmd[1] = ((uint8_t)(ReadAddr>>16));
    ; [. o3 J" l! B; A! B  q
  26.   cmd[2] = ((uint8_t)(ReadAddr>>8));0 f$ x1 U/ c3 V
  27.   cmd[3] = ((uint8_t)ReadAddr);
    " [( x0 X! n. ]% ?6 j$ W
  28.   @. {8 ?) _0 l  J
  29.   W25Qx_Enable();//使能器件2 Z! V' t, d6 T  A
  30.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);
    2 e& u! A" Y$ j+ ?2 ~4 D% I
  31.   HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);% e$ h/ O) B& B
  32.   W25Qx_Disable();//取消片选       
    7 T' h& Z$ L% q( e1 P
  33. }5 F6 J' n* i, z( r: z) l
  34.   B' l4 J$ M# J1 k! f4 v
  35. void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){* E0 [# R, e8 f8 L; ^8 T  _# V
  36.   uint32_t secpos;
    7 C( s0 k: v. W
  37.   uint16_t secoff;
    # x5 S7 T* u/ H
  38.   uint16_t secremain;
    # g4 ~; y- D6 n
  39.   uint16_t i;
    ! r4 _! D  ~/ }+ {; r$ v  }- H
  40.   uint8_t *W25QXX_BUF;, B6 j' V+ X( W8 V% ?1 @# ~
  41.         - H& D' U# D( a0 O" t4 A
  42.   W25QXX_BUF = W25QXX_BUFFER;
    ; D, a/ e& [: `0 h6 c+ I+ h
  43.   secpos = WriteAddr/4096;        //扇区地址& q, G9 @! ]. q
  44.   secpos = WriteAddr%4096;        //在扇区里的偏移
    0 d( M2 v/ z; J6 r& M) J
  45.   secremain = 4096-secoff;        //扇区剩余空间大小
    , R: r3 Q# K. f1 y. K% B
  46.   printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);9 V% x; K+ @. R2 R
  47.   if(NumByteToWrite <= secremain)  //不大于4K字节* d6 }# G- \# A: b. D- l
  48.     secremain = NumByteToWrite;/ z7 v0 e5 p% H
  49.   while(1){' p$ m8 p7 a# l3 R, b. v( B* o0 P
  50.     W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容
    % ^6 v, w' I3 g/ x, p
  51.     for(i=0;i<secremain;i++){//校验数据
    + K( f3 t1 O7 {, j- U3 D% H
  52.       if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除
      m9 j3 _& L5 v' N4 v% p
  53.         break;7 w2 b( t: |% b8 z' W" G
  54.     }2 d# T  B' S" I( f- D
  55.                 / p" p* j! {' O9 t+ ]. X, I5 G6 f
  56.     if(i < secremain){//需要擦除5 `; P8 {: T) L) E
  57.       W25QXX_Erase_Sector(secpos);//擦除扇区9 a+ C8 M: ~: |3 s8 w0 d
  58.       for(i=0;i<secremain;i++){) f  u+ d. p0 Z5 P3 H! [
  59.         W25QXX_BUF[i+secoff] = pBuffer[i];
    ! O. U, b# S  \5 p( U6 ]5 e
  60.       }
    # T4 u4 s: n, T  s' p& F) N
  61.       W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区* x/ J- c+ P8 G' H
  62.     }; M( B( h0 g; A
  63.     else{
    6 ]1 Q# ?/ y# {. x3 x) l: c. U
  64.       W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间
    0 T% Z% P& o: o1 i7 i' T, M7 m
  65.     }4 V/ `: [. z* W( F, ]
  66.                
    7 f- G5 Q, Q) G9 g: A8 H0 S' _
  67.     if(NumByteToWrite == secremain){//写入结束了& J: N  A( i7 e8 @1 I' u
  68.       break;
    8 s; P3 U+ w8 _# v0 n
  69.     }* O- [8 @7 D8 ?+ \; t$ [0 \+ x
  70.     else{        //写入未结束3 B6 W$ P6 b5 F' r
  71.       secpos++;        //扇区地址增1
    + f+ l/ c" v, g# Y( r8 Q
  72.       secoff = 0;                //偏移位置为0                . l( E9 \* _; t$ H
  73.       pBuffer += secremain;        //指针偏移) p$ g7 }6 d6 Y, t- B# E7 Z
  74.       WriteAddr += secremain;        //写地址偏移9 x& ]. h8 C9 k$ m# ~
  75.       NumByteToWrite -= secremain;//字节数递减
    9 N) a% b& p) `5 m7 ]
  76.       if(NumByteToWrite > 4096)               
    4 i$ `3 {. H3 P9 f- T# n
  77.         secremain = 4096;        //下个扇区还没是写不完9 E/ C2 Z: F% W5 r0 Z1 ~
  78.       else5 d) G; e: f- _5 t
  79.         secremain = NumByteToWrite;//下个扇区可以写完了1 L- t, O8 c5 w6 p& }
  80.     }
    1 O$ N  ]$ r7 f- F+ {' H0 t9 t3 }
  81.   }3 }# X- |+ y& n  ~8 T% U
  82. }
复制代码
/ U' w# K. _) m, `. @
➡️ 在main.c文件下编写SPI测试代码
  1. /* USER CODE BEGIN PV */9 g2 S' h, A3 Z: @- _
  2. uint8_t wData[0x100];8 x8 R" E9 {* U) s3 k8 T) R
  3. uint8_t rData[0x100];
    8 T- [! X  S$ \3 n: q4 a+ I- J
  4. uint32_t i;
    & W! C( c4 ^8 u5 e3 ]5 f
  5. /* USER CODE END PV */$ L$ J# N+ u. i: @, i$ c. ~
  6. int main(void){
    ) M3 {/ e/ z& k6 P/ x7 y. {5 r" u
  7.   /* USER CODE BEGIN 1 */& y! o# Q- h, I( S( W
  8.   uint8_t key;
    7 Z8 _- }  e5 I( P- u8 W; G3 b, y
  9.   /* USER CODE END 1 */
    7 S# G7 g3 S: N8 L
  10.   HAL_Init();6 k0 p% S- Y) I0 {* ?% O% z5 J
  11.   SystemClock_Config();
    ) n  L3 K; Q( t; j" n* Y' e0 y
  12.   MX_GPIO_Init();( S& U  P# x6 q( ?+ ?
  13.   MX_SPI2_Init();
    * ^$ j1 E9 L7 W" W
  14.   MX_USART1_UART_Init();
    2 m5 T  s5 g8 P9 H( |
  15.   /* USER CODE BEGIN 2 */
    , K# F9 ]3 `) E( q/ @
  16.   W25QXX_Init();
    $ F) a8 c8 }1 x" b
  17.   for(i=0;i<0x100;i++){
    3 ^) p' g: y+ ]6 S3 R" |0 N
  18.     wData[i] = i;
    7 I: t8 {& M6 y+ w5 k) A9 s
  19.     rData[i] = 0;
    + j/ `2 ?, ~+ C0 ]* W; a! [
  20.   }       
    ' |/ x" \% t( C2 @7 N1 s
  21.   /* USER CODE END 2 */
    * Y9 x& }) c% k3 X; ]8 h
  22.   while (1){7 `1 H" A( s4 H
  23.     key = KEY_Scan(0);
    ( v$ e1 q# |9 K; [6 r1 G
  24.     if(key == KEY_UP_PRES){
    : F7 l$ O2 ~$ I8 I; H
  25.       printf("KEY_UP_PRES write data...\r\n");
    5 O6 w# y- F6 D1 q
  26.       W25QXX_Erase_Sector(0);
    ) b% r/ h4 |" T, b, N
  27.       W25QXX_Write(wData,0,256);                ; D- O6 m8 A1 L
  28.     }
    # S7 {4 }. l+ ?, K( F
  29.                         8 y4 o+ E/ U0 _9 J# C' H9 y
  30.     if(key == KEY_DOWN_PRES){
    , l/ p1 K, ^' `+ O( Z6 }
  31.       printf("KEY_DOWN_PRES read data...\r\n");' [6 J& M1 s- B! a
  32.       W25QXX_Read(rData,0,256);
    ; [& H4 K: L8 ?0 _
  33.       for(i=0;i<256;i++){3 P* e4 N  g4 X! R- `
  34.         printf("0x%02X ",rData[i]);
    # l  o8 s6 s; g* m+ j9 g% ?* T
  35.       }
    ) g" @7 y5 ?9 X; c: Y5 f
  36.     }9 ]6 P7 n2 ^7 `
  37.                   a. ?( g2 _6 O; Q& e- f
  38.     HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);: ~9 R+ i+ Y8 S% w
  39.     HAL_Delay(200);$ Z( s* l! C9 A5 K2 z9 V' ^
  40.   }# ^6 |1 k/ A" {! U1 T3 W5 v
  41. }
复制代码

+ I! s  O9 V, Z! _3 j& v/ p1 ?8 K1 w/ Y
4.下载验证
编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息
* c6 p- a) W# P  U& n/ [
微信图片_20230701185521.png
/ `$ i: l4 {2 w5 U0 `
3 u1 X6 U  a3 @( ~: w1 k转载自: 嵌入式攻城狮; ^' f' I0 ?9 k% Q
如有侵权请联系删除" z; S# j0 X4 y' t- i* }8 @

/ W2 C1 k6 `* v+ \& O
6 b" R: R& y" S7 A0 I/ A$ U5 m0 z* K; A" V; M
收藏 评论0 发布时间:2023-7-1 18:56

举报

0个回答

所属标签

相似技术帖

官网相关资源

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