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

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

( j" {8 T% j7 k6 s: ~2 D
微信图片_20230701185542.png

3 y3 l; R  U1 g2 c5 |8 H& k. r( y
从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。
SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下:

    8 i. O. I" [( y* ]7 @7 ?
  • MISO:主设备数据输入,从设备数据输出
  • MOSI:主设备数据输出,从设备数据输入
  • SCLK:时钟信号,由主设备产生

    , `5 a/ z) q, c6 Q! P( @/ @5 m
  • CS:从设备片选信号,由主设备控制

    9 f8 S; m, v% p/ k

    & m* c" A' r2 y5 m
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。, @9 n, _! o: |
当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。( A  A4 U3 Y" Q+ R* b2 ]
SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式:

    $ N: ?* U4 j  a" L, A8 `# U
  • CPOL=0:串行同步时钟的空闲状态为低电平
  • CPOL=1:串行同步时钟的空闲状态为高电平
  • CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样

    $ D4 h3 Y  b  r# i+ T/ R
  • CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
    . |$ K3 t, l3 K! |3 m- A
    8 j! C- ?* j) N8 V; F
微信图片_20230701185539.png
% B. Q, S+ F; E. i% z; U: t* S/ U! Y
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的常用命令表
- D9 E9 f7 p) m/ q0 e9 `- k+ |
微信图片_20230701185536.png

  I# A2 V' U1 v2 Z7 C5 R7 {
2.硬件设计
D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息

    + l" u( d0 m: ]( y7 u  g+ A
  • 指示灯D1
  • USART1串口
  • W25Q64
    - [8 M# a3 _; V9 d- L* I
  • K_UP和K_DOWN按键
    ) M! b1 l/ w3 A. I  |

    # m2 C$ S6 z, p- N* m9 j1 L* E
    " T( C6 a' s1 b) o
微信图片_20230701185531.png

& a+ R9 f9 u# d' k

/ d; B/ I: Q! c+ `2 z( 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
0 _& `* h# z4 e2 `5 T) D
1 i+ n' d" g8 A7 b+ e; U
➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码

3 d3 U$ m5 l. q3 o( O
3.2 MDK-ARM软件编程
➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中
! q( d* W8 @' Y
  1. void MX_SPI2_Init(void){. [% C0 b4 x6 t, K
  2.   hspi2.Instance = SPI2;
    . G7 T, D. V& O1 G, o' U
  3.   hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式
    / P9 W4 }2 b  l/ W: N
  4.   hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式( k, G( H: r0 ^9 T7 f3 F
  5.   hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度  W: V9 L2 ^( A9 `3 y* u
  6.   hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平+ N+ v4 P: D" _. L
  7.   hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样
    6 ^! U; D) v6 ~+ a# Q
  8.   hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制
    6 b1 `2 \: ~* y
  9.   hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256( F$ Z8 U; \& W& \  j( G
  10.   hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行  z1 W0 ]6 [4 Y% }
  11.   hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式
      n3 e7 L  R; X3 r5 I
  12.   hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验) R  |: n) p" ]# s' B
  13.   hspi2.Init.CRCPolynomial = 10;: u# |" i- _5 q9 Y+ `
  14.   if (HAL_SPI_Init(&hspi2) != HAL_OK){. U" C8 F* L3 }. K, V9 P5 P- u
  15.     Error_Handler();
    0 F6 {9 K0 `0 b6 t" P9 P& B  v
  16.   }) [) d/ P6 G6 k. C8 [" g2 M7 t' b% v  M
  17. }3 m- J5 h9 Y9 |2 p# Q. H! X. `

  18. 4 R/ d) Q& F( ^  \  e
  19. void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){
    ) V) D  G2 `3 I& W( A8 V
  20.   GPIO_InitTypeDef GPIO_InitStruct = {0};* L, x- w7 h- I* t. D, \
  21.   if(spiHandle->Instance==SPI2){
    : ~* S. J1 {. f  o& q
  22.   __HAL_RCC_SPI2_CLK_ENABLE();  : l  E. B: y: L
  23.   __HAL_RCC_GPIOB_CLK_ENABLE();
    " [% F/ M( S2 R: ~7 i0 d
  24.   /**SPI2 GPIO Configuration    / R2 ~9 k. G, f" q, l0 s
  25.   PB13     ------> SPI2_SCK
      K! @2 S, T, ]- Y7 T8 o$ i; l
  26.   PB14     ------> SPI2_MISO
    ; F8 L5 i6 b: w# p9 H% M  ~
  27.   PB15     ------> SPI2_MOSI */
    * t5 ~& L$ B! |: ]- C
  28.   GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;
    * S7 }, `; o2 y) x: R2 z8 ]5 X
  29.   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;" t4 Q" M2 F8 w/ F
  30.   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;5 |' y* F* j0 j% K/ b
  31.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    ( A5 C: e- d! i1 U% ~- ~' a
  32.   GPIO_InitStruct.Pin = GPIO_PIN_14;4 G' j7 Q6 c+ n4 u; S/ R
  33.   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    $ `0 i' o) H2 {0 W1 |
  34.   GPIO_InitStruct.Pull = GPIO_NOPULL;
    8 `7 r8 F! r5 P& \& ~7 w
  35.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);% c+ Q+ v: `0 f. y  S8 r
  36.   }$ i8 x) B7 ?" d) t) D/ f, B
  37. }
复制代码
+ `7 t/ A  W# S$ u0 l( a, L! _
➡️ 创建按键驱动文件key.c 和相关头文件key.h
➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍
  1. //这里仅介绍几个重要的函数* R4 v8 [  o0 ~; S9 T
  2. void W25QXX_Init(void){
    3 q. k) r" X4 [, r( S" i, u- p2 I
  3.   W25Qx_Disable();
    . r  E8 }7 y7 s; m
  4.   W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID
    $ k) J* N, M, x0 d! d
  5.   printf("FLASH ID:%X\r\n",W25QXX_TYPE);
    : Q$ R7 W4 m* O9 n
  6.   if(W25QXX_TYPE == 0xc816)! M) x: o/ Y( _+ q
  7.     printf("FLASH TYPE:W25Q64\r\n");/ l) H  E6 O2 x
  8. }
    ; s. r3 Y% j9 w" j  g- C- P
  9. . Q8 o/ ~$ f3 a. I. K
  10. uint16_t W25QXX_ReadID(void){
    3 {5 U3 |8 |0 K
  11.   uint16_t ID;
    ' O" u3 Q) v$ q/ T; {3 p, U
  12.   uint8_t id[2]={0};& G, c0 G2 J& @# C( y
  13.   uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令        : I, F+ F: W4 m' @7 I
  14.   W25Qx_Enable();//使能器件
    7 ?/ u, ^5 _* s5 @
  15.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);6 M- C* c! _6 b, o9 H. `- k( T
  16.   HAL_SPI_Receive(&hspi2,id,2,1000);/ ]. Q7 q; k6 D% m7 t
  17.   W25Qx_Disable();//取消片选        , _9 O4 ~2 U6 T2 `, f3 f" Y  J  \
  18.   ID = (((uint16_t)id[0])<<8)|id[1];) W' \$ H% s( e, h$ L, X5 I. ?
  19.   return ID;
    - ^: U' l1 z. o" [
  20. }
    & i" l; l, u: a  H0 d1 I- A

  21. 8 U8 b1 Y& o& n6 D) q5 C" N4 q
  22. void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){; x: _. s* W0 {1 v% E+ S- g
  23.   uint8_t cmd[4] = {0};, k9 B. U4 _9 E, i
  24.   cmd[0] = W25X_ReadData;//读取命令
    9 G+ }! |: d, h( T
  25.   cmd[1] = ((uint8_t)(ReadAddr>>16));
    ' g- P1 h1 V) s' t4 B
  26.   cmd[2] = ((uint8_t)(ReadAddr>>8));& Q9 w4 s" Y0 F1 }. o7 ^1 `% I2 [
  27.   cmd[3] = ((uint8_t)ReadAddr);
    ( J% W% M( F$ g' R9 B3 }. W
  28. ; v! A5 n, }& N& i0 a3 I# u1 b
  29.   W25Qx_Enable();//使能器件2 w2 h. @* f0 h
  30.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);$ n) H6 V4 `. T% [
  31.   HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);
      |2 y  X0 _6 z/ W5 a5 T" T" E
  32.   W25Qx_Disable();//取消片选        # R7 [7 n0 n$ b& |) D% {9 N
  33. }4 l  a/ y+ d! a# C
  34. . {) q. g" x4 |$ |
  35. void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){4 ~) @/ ^. a. y+ a  H* h
  36.   uint32_t secpos;; S( B* n2 |0 I' W5 L3 x
  37.   uint16_t secoff;
    - s* G1 O* j4 f- Z. J( i7 d
  38.   uint16_t secremain;3 S: t; b" \. ^. d
  39.   uint16_t i;- j% J0 \: {2 W7 H0 a3 N
  40.   uint8_t *W25QXX_BUF;
    2 {+ |' p3 }- L8 a1 ?
  41.        
    9 z) d) e6 a- h! e( {
  42.   W25QXX_BUF = W25QXX_BUFFER;
    ' i9 ^/ w% d, a" |) x& U! I" r% C
  43.   secpos = WriteAddr/4096;        //扇区地址. g8 c3 z' @5 P" g, Y
  44.   secpos = WriteAddr%4096;        //在扇区里的偏移
    % L4 F2 h0 [# K! O
  45.   secremain = 4096-secoff;        //扇区剩余空间大小; |* ~5 w! o% D4 \) p
  46.   printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);, _% i& e$ X! j5 E. r% ]# [; _
  47.   if(NumByteToWrite <= secremain)  //不大于4K字节/ T: _" b- C% c: Q; k9 E( r0 o
  48.     secremain = NumByteToWrite;
    ! W- x8 O( R, ^9 ~
  49.   while(1){* A8 k+ s' w1 _" L! s; |/ u" K
  50.     W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容
    9 p/ G2 X+ C  b9 u
  51.     for(i=0;i<secremain;i++){//校验数据6 d3 e! `) W. n! n$ |, v. S
  52.       if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除
    3 h+ Z* j7 B/ B
  53.         break;+ p' `0 O5 A0 ?! G& ^* I
  54.     }3 t" n5 o, a( @' O
  55.                 0 S& q8 F0 ^8 U9 e
  56.     if(i < secremain){//需要擦除
    ) c1 ~/ M  {9 N* g
  57.       W25QXX_Erase_Sector(secpos);//擦除扇区
    ' W' H* R- _! y( s) Z
  58.       for(i=0;i<secremain;i++){
    $ N2 {- E! W. J6 h; U7 ^
  59.         W25QXX_BUF[i+secoff] = pBuffer[i];/ h5 y8 X& ?$ [
  60.       }' M" f# b$ F' c4 f, h* E- H6 W
  61.       W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区: c2 k5 m  ~2 `$ V" }+ e. {
  62.     }
    6 d0 t7 `* e/ V5 m; I
  63.     else{4 j+ s$ F9 h# @+ `8 G+ y. I. J+ U
  64.       W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间
    3 q" v& q4 y) u% P* y) c+ V
  65.     }
    4 b! U6 \, t. ?0 X  b1 q
  66.                
    0 P, I# S+ N) V
  67.     if(NumByteToWrite == secremain){//写入结束了
    " ~3 f( _) J+ r2 _  s- V4 q
  68.       break;
      _; J0 {' j+ v6 @( N" r2 L$ U
  69.     }
    4 f  N6 U/ i! `" m0 k* L; F6 v
  70.     else{        //写入未结束
    6 m/ x8 b, |7 f* X' b9 o
  71.       secpos++;        //扇区地址增1# B( Z5 W) t( ^% M* g- T
  72.       secoff = 0;                //偏移位置为0                : L2 v0 d& E. l9 Y4 l9 d
  73.       pBuffer += secremain;        //指针偏移
    4 y8 l! M$ ?4 p4 P; G
  74.       WriteAddr += secremain;        //写地址偏移7 K8 F9 u& |: v) f, n3 o2 J
  75.       NumByteToWrite -= secremain;//字节数递减
    5 u+ I+ ]0 I. V* m6 F5 j. o% i" W
  76.       if(NumByteToWrite > 4096)               
    6 s$ q7 P1 ^9 F, A0 g; V$ c
  77.         secremain = 4096;        //下个扇区还没是写不完. ?9 p" ~/ r! R4 Y7 q
  78.       else; v5 r0 C/ |5 N- F
  79.         secremain = NumByteToWrite;//下个扇区可以写完了
    & a2 ~; o  @3 A% Q5 v
  80.     }
    % M3 {- l9 y+ R: O6 Y# r
  81.   }) X2 K6 s# c7 R/ L' c$ j  s+ m! q9 ^( \
  82. }
复制代码
+ v" j* a; ~9 s: ]+ @$ q
➡️ 在main.c文件下编写SPI测试代码
  1. /* USER CODE BEGIN PV */3 q3 _- L7 W4 F
  2. uint8_t wData[0x100];" L+ s: O5 D  D. }  \
  3. uint8_t rData[0x100];
    * t, `: {6 V* n- _' \
  4. uint32_t i;
    ( P; [9 Q" a" s1 O0 j8 s
  5. /* USER CODE END PV */. D- ?0 \6 J# X$ ]: Y
  6. int main(void){
    * V$ Z/ `" t4 R8 B
  7.   /* USER CODE BEGIN 1 */
    ! |! b. j1 j0 H2 X5 H
  8.   uint8_t key;
    ; i7 a' K$ y* R8 _. E: {$ i' v( X
  9.   /* USER CODE END 1 */
    ) _9 y6 o# g1 F! ]
  10.   HAL_Init();
    6 |/ I, q8 c5 R5 C
  11.   SystemClock_Config();
    ( w) H+ h* U$ x/ L% ?, n
  12.   MX_GPIO_Init();
    ' C: _& Z) s- y" x0 T9 C7 X! m
  13.   MX_SPI2_Init();
    $ b0 e. e+ I( h* _
  14.   MX_USART1_UART_Init();
    , I$ L$ l: T( J1 t, |1 r% U
  15.   /* USER CODE BEGIN 2 */
    . u, `6 k! ?' R6 t3 @
  16.   W25QXX_Init();
    # N9 v7 b4 d+ d" ~$ U! }9 Y3 e
  17.   for(i=0;i<0x100;i++){
    / c, R$ y8 Z2 w' ~1 P1 i/ K2 g5 V
  18.     wData[i] = i;0 D6 I$ ~7 k" S* Q, ~7 F8 e
  19.     rData[i] = 0;1 p8 \' ~* {) X0 ~: w+ h
  20.   }       
    : a# ?) ~+ p# R2 h8 q! q# N; u. H
  21.   /* USER CODE END 2 */+ p2 U! n; d, M$ O# L- }
  22.   while (1){) e: P* K* ~( M  `, x
  23.     key = KEY_Scan(0);' K* @  V1 j) e" R. o, @1 x; Z$ x
  24.     if(key == KEY_UP_PRES){& O; h- t6 a* A: C# J! q* {
  25.       printf("KEY_UP_PRES write data...\r\n");
    $ [; W# O6 C3 L$ y3 Q
  26.       W25QXX_Erase_Sector(0);& f1 Y2 m1 p% e
  27.       W25QXX_Write(wData,0,256);               
    " u7 k6 [. z' o# L0 G: E6 q- j
  28.     }) S) H/ v8 @  U% E$ B: u! V
  29.                        
    * M7 h0 |7 w: [+ b) D
  30.     if(key == KEY_DOWN_PRES){
    , E# v- R5 o$ Q- f0 J5 j7 D
  31.       printf("KEY_DOWN_PRES read data...\r\n");
    , B! O0 b3 W" ]# y# F
  32.       W25QXX_Read(rData,0,256);9 i! L7 ]. ]% S
  33.       for(i=0;i<256;i++){2 G  z; Z2 L0 l) d0 I+ n1 }" m# |
  34.         printf("0x%02X ",rData[i]);! ]" T) @, @! {3 P3 M4 Z
  35.       }2 h" {. p: a/ f; ]5 R
  36.     }
    1 @& D% g! @. i; _6 v
  37.                 * N* b  I% |7 Q  ^+ b- T. S$ s. X
  38.     HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);
    4 U% o1 ^: f8 c6 b) q+ }
  39.     HAL_Delay(200);
    - K/ F$ z) ?7 Y# P* d
  40.   }0 n9 N. p; B3 N, o( p
  41. }
复制代码
3 }, T7 B) `5 b1 Z$ l. E* J& ^& F1 y1 v
4.下载验证
编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息
% b' f" B# K5 E" k$ }8 c! U
微信图片_20230701185521.png
" B7 a0 s) w5 G& T9 A# T' ]' f* T, q$ Z$ d) R& r2 F4 y: U& F
转载自: 嵌入式攻城狮6 r8 |+ O8 h) Q8 [- d
如有侵权请联系删除* p$ U( f' h0 Y
3 d( a& l+ h$ g) c/ z2 u
8 l" G5 U1 o& U7 m& Z

- [( W/ R! z# E* C$ [4 k
收藏 评论0 发布时间:2023-7-1 18:56

举报

0个回答

所属标签

相似分享

官网相关资源

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