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

【经验分享】[stm32] NRF24L01+USART搞定有线和无线通信

[复制链接]
STMCU小助手 发布时间:2022-1-13 19:48
前言
一般进行远程监控时,2.4G无线通信是充当远程数据传输的一种方法。这时就需要在现场部分具备无线数据发送装置,而在上位机部分由于一般只有串口,所以将采集到的数据送到电脑里又要在上位机端设计一个数据接收的适配器。这里基于stm32分别设计了现场部分和适配器部分,这里只是基本通信功能实现的讲解,一些复杂的技术比如加密、可靠等要根据具体的应用来设计~
2 x, e* m5 m9 T1 f9 `
222027025943411.png

! D# _/ E' C3 d% ~- ]0 e
总体说明
这里采用stm32作为MCU,采用nRF24L01作为2.4G通信模块。其中适配器中仅仅采用了USART和NRF24L01两个主要部分,负责将下位机通过2.4G发送过来的数据通过串口发送给上位机,或者将上位机的通过串口传来的数据通过2.4G发送给下位机来实现远程监控(没有采用uc-os操作系统,也没有界面,要用串口和上位机相连);其中下位机比较复杂,因为一般下位机是一个集成的系统,包括从各种传感器收集数据、向各种类型的驱动电路发送控制命令、将数据输给打印机或显示器、和无线通信或有线通信设备进行互相通信来实现数据传输等,这里的下位机比较简单:采用uc-os实时操作系统+uc-gui负责界面显示,外接7寸TFT液晶显示屏,和适配器类似也包括USART和NRF24L01通信部分,但是因为有了操作系统和可视化交互界面,所以也有点不同,接下来开始介绍。
适配器部分
这里介绍的流程是以main函数为基准,广度拓宽知识点,最后main函数说完,整个工程的细节也就大致能了解了~
  1. 1 int main(void){
    + A( b# U# n& ?/ I! e0 f2 W
  2. 2   uint8_t a=0;//LED高低电压控制
    6 u4 S% @1 a' E9 c- D! A+ ]
  3. 3   /* System Clocks Configuration */. l0 f" i- Z" ~# J* T
  4. 4   RCC_Configuration();                                              //系统时钟设置   
    1 c9 `$ }3 y2 ^$ w" `' _
  5. 5   /*嵌套向量中断控制器 $ D; j( c, ~) `  I  L' O
  6. 6       说明了USART1抢占优先级级别0(最多1位) ,和子优先级级别0(最多7位) */
    , P4 t  S: p. H. b
  7. 7   NVIC_Configuration();                                              //中断源配置1 x1 d7 U, |5 d# P
  8. 8   /*对控制LED指示灯的IO口进行了初始化,将端口配置为推挽上拉输出,口线速度为50Mhz。PA9,PA10端口复用为串口1的TX,RX。
    . Z+ ]6 p: @# R5 `- ~# S
  9. 9   在配置某个口线时,首先应对它所在的端口的时钟进行使能。否则无法配置成功,由于用到了端口B, 因此要对这个端口的时钟4 d' e: d( T& v; L( q0 l
  10. 10   进行使能,同时由于用到复用IO口功能用于配置串口。因此还要使能AFIO(复用功能IO)时钟。*/7 g  L1 ?* h$ L" D; u) j
  11. 11   GPIO_Configuration();                                              //端口初始化' b+ q: E5 ^8 y; g% Y
  12. 12   SPI2_NRF24L01_Init();                                           //SPI2及NRF24L01接口初始化  / C  s. {1 E1 A- Q/ L2 b
  13. 13   USART_Config(USART1);                                              //串口1初始化; U4 i7 h. ^2 j4 J
  14. 14   /*NRF24L01设置为接收模式*/6 }; y; Z! X- x* s3 Z
  15. 15   RX_Mode();
    # j* N. q6 U/ N
  16. 16
    3 l9 t; W7 r2 i$ v% p4 E
  17. 17    while (1)) I+ u4 A" ]9 j! I) l+ z) q% a" {' w
  18. 18   {
    " u4 |9 D, ^3 J; F3 a+ D/ w: V) |
  19. 19     if(usart_rec_flag==1) //判断是否收到一帧有效数据
    0 N0 j* O8 f+ [' o5 L* ]3 a
  20. 20     {                                                  
    8 g+ e9 H* i' X9 M7 m' F
  21. 21         usart_rec_flag=0;
    % `$ {4 o4 W# i7 _) u* m
  22. 22         NRF_Send_Data(TxBufferRF,sizeof(TxBufferRF));5 D8 O1 h  c, ~
  23. 23         if(a==0){GPIO_SetBits(GPIOB, GPIO_Pin_5);a=1;}          //LED1  明暗闪烁                # ?1 i/ |2 D1 D4 o! Q* A
  24. 24         else{GPIO_ResetBits(GPIOB, GPIO_Pin_5);a=0;}
    # D, ^+ z0 m: f' R
  25. 25     }
    & u1 K& G. t! L7 E. _
  26. 26     if(rf_rec_flag==1)
    ; r( S+ m4 ]+ V* U" y6 Q" E
  27. 27     {
    ' `* K1 z7 Y8 K. F
  28. 28           rf_rec_flag=0;
    ( F$ y, y& J9 b( X. }
  29. 29            for(i=0;i<32;i++)//发送字符串
    & u% e, |! W( N2 B  @8 m
  30. 30         {
    : K0 F; \. c* y0 }: d/ }/ P4 K
  31. 31             USART_SendChar(USART1,TxBufferUSART[i]);1 H# I! a  o* A- s  |6 G
  32. 32         //    Delay(0x0000ff00);
    % x5 @6 r; g0 u5 D$ J1 U5 T" r
  33. 33         }' d' J" M; h3 j; i; ^: m
  34. 34     }$ j" q" q3 n( K" {3 R: ]
  35. 35   }7 a) g( n% \9 h- x, i0 v9 s
  36. 36 }
复制代码
& ~3 L8 S: N! ^; N
第4行RCC初始化主要是系统时钟和外设时钟配置,这里注意要使能RCC_APB2Periph_USART1,当时忘了使能这个结果串口出现异常,我还以为是初始化和中断向量什么的弄错了呢,浪费了很长时间。
  1. 1 /*--------------------------------------------------------------------------------------8 x, G3 a0 A7 N) S) K
  2. 2 系统时钟配置为72MHZ+外设时钟配置*/ + a6 o. {, N; D  U. [! _
  3. 3 void RCC_Configuration(void){
    $ ?1 [* }' [) i, W: x& X$ c$ X
  4. 4    SystemInit();
    ' ?3 Q5 j1 v! S. I: d
  5. 5    RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |RCC_APB2Periph_AFIO  , ENABLE);  
    # E1 i5 q  h' o, E  r: S
  6. 6 }
复制代码

8 r6 v* [0 [$ |# U) S* F4 g  _! A" t  B$ O0 r/ R! m  z
第7行中断向量初始化设置,主要是设置串口接收中断和NRF24L01中断的,这样设置好了之后当串口中断被触发时其对应的中断子程序将被执行(这个科班的大概都知道这里就不多说了),所以我们就要在stm32f10x_it.c里实现他们各自的中断子程序了(这个一会再详细介绍,咱们先把整个框架了解下)。另外说一句,这里的的优先级组将影响主优先级和子优先级数量具体请参考stm32f10X_的固件库的NVIC.
  1. 1 void NVIC_Configuration(void){$ g. i4 i* E4 A) B
  2. 2  /*  结构声明*/
    # Z" T+ J# j# W* o  }& P4 i
  3. 3   NVIC_InitTypeDef NVIC_InitStructure;
    ; F" ?. g! Q- k" \! t
  4. 4   EXTI_InitTypeDef EXTI_InitStructure;        
    ; C7 f; t0 K1 A; R
  5. 5   / w5 T! y0 d5 \
  6. 6   /* 优先级组 1  */    / z! R5 j- T. X$ Q8 ]
  7. 7   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);           : G+ P' ?6 v9 s/ }
  8. 8   7 Y& }# x0 v- B- L7 N
  9. 9   /* Enable the USART1 Interrupt */+ z( G' @9 M5 I! p" }6 o
  10. 10   NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;                     //设置串口1中断
      I& D# B1 p! b' ~
  11. 11   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;             //抢占优先级 0
    ; u. z1 G5 ^& i2 t; r, b
  12. 12   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;                //子优先级为0  t, Q* s/ E* x* X  |
  13. 13   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                    //使能
    ) ?% J: T( R( L2 f6 F. K. N
  14. 14   NVIC_Init(&NVIC_InitStructure);                                             
    , x* @, ]5 Z4 @3 x+ Y3 G7 L
  15. 15
    2 j- ?1 e( {5 j- m
  16. 16                                                                     ) H$ f, V3 ~/ h0 S
  17. 17   NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;                    //NRF24L01 中断响应
    ! M$ Q5 h7 p2 P' k$ p) v7 u% L# C: \9 o
  18. 18   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;            //抢占优先级 0( i( S0 {& [1 Y6 n
  19. 19   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;                //子优先级为1' Y$ j/ ^2 @/ H! s) O! c
  20. 20   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                    //使能7 U% H% U7 v( }
  21. 21   NVIC_Init(&NVIC_InitStructure);                                                * ]' N% Z- K4 o5 R  P8 ]! S$ R
  22. 22 7 o3 P5 k) N4 j' d$ }7 Z
  23. 23   GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);       //NRF24L01 IRQ  PA0
    6 Q; H3 e1 k4 Z. I* W
  24. 24   + h: i) }2 x0 n
  25. 25   EXTI_InitStructure.EXTI_Line = EXTI_Line0;                       //NRF24L01 IRQ PA0
    8 q# J  ^9 k( _; P
  26. 26   EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;               //EXTI中断4 S: D) M( w8 j; `
  27. 27   EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;           //下降沿触发" z1 U2 L5 @1 v# M
  28. 28   EXTI_InitStructure.EXTI_LineCmd = ENABLE;                           //使能
    / V# n3 ^' X0 @' ]. R' h& A
  29. 29   EXTI_Init(&EXTI_InitStructure);    . z, k0 r, d: q
  30. 30 }
复制代码

  U* B" ]( E# u1 y# i% r
' r( E$ ^6 `. n! N
第11行的GPIO初始化,主要是对通用IO口的属性设置和初始化,这里一定要对串口所需的A9和A10配置好!
  1. 1 void GPIO_Configuration(void){5 V) X, O; H5 s. G& J
  2. 2   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;                     //LED1控制--PB5
    ; ~& N8 H; Z( _3 {5 l. \1 I6 ?
  3. 3   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;             //推挽输出
    9 f* d9 X: A" @% e4 P
  4. 4   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;* B# @8 V/ Y. Q* D8 K5 H
  5. 5   GPIO_Init(GPIOB, &GPIO_InitStructure);                     - Y$ m% z9 _" K* u3 T, `  y1 o
  6. 6
    6 r( U. ^! Z% U8 K
  7. 7   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;                      //USART1 TX" `1 s; W) A3 x& Z, I
  8. 8   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;             //复用推挽输出
      f  r* n% x! L4 z: n
  9. 9   GPIO_Init(GPIOA, &GPIO_InitStructure);                     //A端口 * ]6 i1 G  W$ H! ^/ B/ b+ z3 R( v
  10. 10   y; V0 D  l' I9 j; s! b( @$ \9 V% ]
  11. 11   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;                  //USART1 RX: B% x$ c5 \" {1 d7 h" u2 y) g
  12. 12   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;        //复用开漏输入
    ( i: b3 i7 m, Q: U  X
  13. 13   GPIO_Init(GPIOA, &GPIO_InitStructure);                      //A端口
    & h2 j# ]; [' l3 [  E
  14. 14 }
复制代码

7 f' X$ x; `  s
% r7 L3 \0 n! E- D3 T
第12行的SPI2_NRF24L01_Init();主要是驱动NRF24L01的接口初始化,因为NRF24L01采用的是SPI通信,所以这里免不了SPI的设置和相关操作了,不过幸好都封装好了~像以前在51上做SPI就得自己模拟SPI,没有示波器调试起来甚是坑~此外这里我已经把NRF24L01的整个驱动都封装在NRF24L01.c这个文件里了,当想用的时候只要在中断向量里设置其中断接收函数,并在it.c里实现其接收函数;一般主函数里用到的是其初始化函数SPI2_NRF24L01_Init();和 RX_Mode();,当在过程中想利用NRF24L01向外发数据时只要调用函数void NRF_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes):
  1. 1 /****************************************************************************: A) L+ s1 }4 p$ I, t
  2. 2 * 名    称:NRF_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes)
    8 J) D, ~% e: L2 ^6 }/ f4 p+ Z1 \
  3. 3 * 功    能:将保存在接收缓存区的32字节的数据通过NRF24L01+发送出去1 O% ?; o& B6 _, Z
  4. 4 * 入口参数:data_buffer   待发送数据  s" V6 Y7 W; _  e, O
  5. 5             Nb_bytes      待发送数据长度
    ; g$ L& b. c* ]6 J* c0 l+ L1 m2 O" q
  6. 6 * 出口参数:无
    ) O* x2 S* S  N7 P( y- r+ q' l
  7. 7 * 说    明:数据小于32,把有效数据外的空间用0填满% E" \7 @9 y7 S8 w. H' e
  8. 8 * 调用方法:RX_Mode();. F1 [, I  U' h/ C3 A
  9. 9 ****************************************************************************/
    " Y/ e/ C9 S: y
  10. 10 void NRF_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes)
    * T8 O( s7 Y4 y0 e1 Y
  11. 11 {  
    2 `# n0 X9 U8 E+ s1 k" S
  12. 12     uchar i=0;  
    * a7 w+ E! E, S1 e. D
  13. 13     MODE_CE(0);                                 //NRF 模式控制     - n% k, [$ Q* s( R3 }
  14. 14
    - j5 Y1 z' n4 l, }" w8 _
  15. 15     SPI_RW_Reg(WRITE_REG1+STATUS,0xff);         //设置状态寄存器初始化
    0 B0 L; v, ~) t2 R
  16. 16     SPI_RW_Reg(0xe1,0);                         //清除TX FIFO寄存器# F+ b) a1 |9 z/ }; s) q0 x+ w+ F
  17. 17     SPI_RW_Reg(0xe2,0);                         //清除RX FIFO寄存器
    % V8 g) A. A! V8 ^; d
  18. 18     TX_Mode();                                 //设置为发送模式
    / ~- A& i' h6 n9 W* R& S0 j& c- A
  19. 19     delay_ms(1);/ h. T6 @5 U9 r5 K! I
  20. 20     if(Nb_bytes<32){                         //当接收到的USB虚拟串口数据小于32,把有效数据外的空间用0填满. m" x2 z' ^" A3 _9 w: b
  21. 21         for(i=Nb_bytes;i<32;i++) data_buffer[i]=0;
    % l: g5 y* B, q
  22. 22     }( d, r. o. ~8 q9 _
  23. 23     MODE_CE(0);
    . X& E3 g2 F& h8 s" [
  24. 24       SPI_Write_Buf(WR_TX_PLOAD, data_buffer, TX_PLOAD_WIDTH);        //发送32字节的缓存区数据到NRF24L01- c; c8 v6 H% z1 p. }6 M. _
  25. 25     MODE_CE(1);                                                        //保持10us以上,将数据发送出去        $ f: W# k" x5 _2 w' C& F
  26. 26 }
复制代码
* W2 X1 Y* {1 l2 k8 J# E" u
6 c( G) p2 C% n
第13行是USART初始化,包括波特率、数据位、停止位等~
  1. 1 void USART_Config(USART_TypeDef* USARTx){
    7 d6 v' s" J/ Y0 q
  2. 2   USART_InitStructure.USART_BaudRate = 9600;                        //速率9600bps
      s) r- \" f- L0 ~6 j( U
  3. 3   USART_InitStructure.USART_WordLength = USART_WordLength_8b;        //数据位8位  |; G! N$ S  q- h* W, o
  4. 4   USART_InitStructure.USART_StopBits = USART_StopBits_1;            //停止位1位
    / _! V7 y% b. _. w" V
  5. 5   USART_InitStructure.USART_Parity = USART_Parity_No;                //无校验位
    - T4 Y; P3 j/ a& B
  6. 6   USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;   //无硬件流控9 U1 n. _$ z( A8 `+ `
  7. 7   USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                    //收发模式( g2 w2 p) x, q1 b0 c, U+ V4 e
  8. 8 - b# s! _6 |: v  D1 h
  9. 9   /* Configure USART1 */- y; {. A: Y% Q3 H( T3 B1 `
  10. 10   USART_Init(USARTx, &USART_InitStructure);                            //配置串口参数函数
    % b4 A& W+ u' E4 ~& Y
  11. 11  
    " o. J' e# u3 N3 ?# b
  12. 12   & o" S8 ^  g* y% X; O& C
  13. 13   /* Enable USART1 Receive and Transmit interrupts *// ^+ L9 P6 _, z% @% _
  14. 14   USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);                    //使能接收中断: m3 R, R: X2 ]5 m/ k" c' l
  15. 15   USART_ITConfig(USART1, USART_IT_TXE, ENABLE);                        //使能发送缓冲空中断   
    + Z* u2 Q( B8 L* l, [" V
  16. 16
    ( c0 f8 n" b( j5 F( x4 `% s
  17. 17   /* Enable the USART1 */
    ! g9 P: Q( n' o; W$ P  ^1 n0 I
  18. 18   USART_Cmd(USART1, ENABLE);   
    , J6 _6 y) `4 J) a7 V1 m1 P8 z
  19. 19 }
复制代码
# h6 g% ^: N) H1 q; o

1 S' F# w- `5 d3 K2 K
同样的类似于NRF24L01一旦初始化之后,其数据接收一般采用中断方式、数据发送一般采用直接发送的方式。所以在中断向量里也要设置,也要在it.c中实现其接收中断子函数。其发送直接调用stm32f10的固件库函数(这里我稍加封装了下):其实就是发送一个data之后要监听是否发送完成才能进行下次发送~
  1. 1 void USART_SendChar(USART_TypeDef* USARTx,uint8_t data){
    # Q1 g! ^& t, u: j1 z4 W
  2. 2     USART_SendData(USARTx,data);; q. L; x) ^. f3 Q. ^- Z  ~
  3. 3     while(USART_GetFlagStatus(USARTx,USART_FLAG_TC)==RESET);
    ; c6 c' g$ J& k/ G5 j! C" r
  4. 4 }
复制代码

+ D! N8 n1 w( ?
接下来进入while循环,不断进行监听看是否有串口接收标志位置1或者无线模块接收标志位置1,如果有表明相应的有数据从该通道传送过来。当是从串口传来的数据表明数据是从上位机发送来的数据,并且想把该数据通过2.4G发送出去,所以这里调用:NRF_Send_Data(TxBufferRF,sizeof(TxBufferRF));将数据发送出去;当数据是从2.4G通道中传过来的,表明数据是从下位机传送过来的想给上位机,于是调用串口发送函数将数据发送给上位机:USART_SendChar(USART1,TxBufferUSART);
5 q8 B* Q$ Q5 q
222138483757782.png
& e# s  I& i! W
看了上图适配器端的数据交换过程就明白了串口中断和无线中断大致要干的事了,这里我就不多介绍,看看下面的代码就明白了(在stm32f10x_it.c中),要再次提醒的是无论是串口还是无线其接收都是采用中断,而发送采用循环直接发送,他们的中断和中断向量有关并要在stm32f10x_it.c里实现相应的中断子程序~
  1. 1 /******************************************************************************/
    - n( A9 [; c0 u% N
  2. 2 /*            STM32F10x Peripherals Interrupt Handlers                        */
    7 J/ W* g% {0 n; c; c9 R4 f# W4 o' E
  3. 3 /******************************************************************************/# L5 H4 R- J: z* B" G9 o. z
  4. 4
    8 |4 K. F) b7 X5 V$ S
  5. 5 /**% a* b, D3 N( }3 k3 [5 g; g
  6. 6   * @brief  This function handles USART1 global interrupt request.
      z& }1 B3 k8 d2 O1 N, ^
  7. 7   * @param  None* B; u1 Z+ i- z
  8. 8   * @retval : None
    ( s1 r" Z, ~% ?# _& ^7 H' g
  9. 9   */  P0 Y6 L  |+ |' q
  10. 10 void USART1_IRQHandler(void)      //串口1 中断服务程序
    0 k! e1 b1 V/ o! q( o# b9 F* B& u! ~
  11. 11 {8 @1 [8 E' C! z9 `; `! \
  12. 12   unsigned int i;
    ) s1 Z* I/ B) f8 B, O9 b
  13. 13   if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)       //判断读寄存器是否非空
    , X: h' m# a- r& i1 N2 N
  14. 14   {    3 b& v2 \6 i# x2 i1 b
  15. 15       {+ q4 q7 H3 V4 S& |9 s  Q
  16. 16     RxBufferUSART[RxCounter1++] = USART_ReceiveData(USART1);   //将读寄存器的数据缓存到接收缓冲区里
    3 O8 ?! T* s0 z
  17. 17     ' L! K; E. F' d
  18. 18     if(RxBufferUSART[RxCounter1-2]==0x0d&&RxBufferUSART[RxCounter1-1]==0x0a)     //判断结束标志是否是0x0d 0x0a
    - G( n/ Z% x* H/ b( _% R5 i
  19. 19     {
    ; z7 k1 E1 e) E# @
  20. 20       for(i=0; i< RxCounter1; i++) TxBufferRF[i] = RxBufferUSART[i];          //将接收缓冲器的数据转到发送缓冲区,准备转发
    3 H" p. _% g1 @9 v% R( A
  21. 21       usart_rec_flag=1;                                                             //接收成功标志
    # `' Q/ n' [, ?4 }% s9 c
  22. 22       TxBufferRF[RxCounter1]=0;                                             //发送缓冲区结束符    $ h  ]% F0 R. H% k: I" C: [
  23. 23       TxCounter1=RxCounter1;" _# `- B, u4 U  d+ l
  24. 24       RxCounter1=0;
    # a' o8 G" Y7 c* u
  25. 25     }5 `0 r8 q7 v3 l+ L$ m$ d! |1 q
  26. 26   }
    + _' `2 |# M& v6 c5 k9 w0 b
  27. 27   
    ' _% ]7 H& O5 {3 Y
  28. 28   if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET)                   //这段是为了避免STM32 USART 第一个字节发不出去的BUG % s- ^* Y6 Y( b- T8 l) n/ b
  29. 29   { : p; t: D+ M/ {/ Q1 d* b5 N/ D
  30. 30      USART_ITConfig(USART1, USART_IT_TXE, DISABLE);                         //禁止发缓冲器空中断,
    " L5 T3 d2 \+ z0 g
  31. 31   }    * i. W4 R  r' E3 m+ ~0 E, y
  32. 32 }  
    8 t3 M  M+ j! D" v' Z* O7 b
  33. 33 /*******************************************************************************+ P6 \; O# x1 V
  34. 34 * Function Name  : EXTI0 中断函数
    7 U( a7 }1 g! ^/ d2 Q: y; m5 I; F  r
  35. 35 * Description    : NRF24L01中断服务程序
    " V5 S/ V. V$ e, l; j' S5 Z
  36. 36 * Input          : None- _1 a5 m8 L3 S3 n1 A# U
  37. 37 * Output         : None
    * R3 P* @6 Q% w$ }
  38. 38 * Return         : None) Z& N9 y! f. E4 V2 ^5 s
  39. 39 *******************************************************************************/
    ' b/ J9 M5 Y7 a2 A, }" p, H7 `
  40. 40 void EXTI0_IRQHandler(void){
    4 a, f0 G! ]- x$ O
  41. 41     u8 i=0;
    7 N9 N, f4 w8 o* `+ T( C
  42. 42      u8 status;    9 m. ~" n2 r, l$ A; j  {" ]2 \
  43. 43     if(EXTI_GetITStatus(EXTI_Line0) != RESET)            //判断是否产生了EXTI0中断8 Y: O, z$ W* A+ Y3 v# n( E
  44. 44       {$ _7 m7 a: Z& C7 \
  45. 45         if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==0){ //判断是否是PA0线变低            ; o( q( a  l# q" C4 E
  46. 46             status=SPI_Read(READ_REG1+STATUS);            // 读取状态寄存其来判断数据接收状况   
    ( a7 [0 R! y2 M' Y  I% N
  47. 47             if(status & 0x40)                            // 判断是否接收到数据                  
    / M0 T6 e7 N, }4 J) @5 R. A1 b! ?5 `
  48. 48             {            
    & v1 G0 B+ L: G4 b5 E9 Y- ~
  49. 49                 //GPIO_ResetBits(GPIOB, GPIO_Pin_5);   
    ( L( x' g; R  {* B0 J
  50. 50                  SPI_Read_Buf(RD_RX_PLOAD,rx_buf,TX_PLOAD_WIDTH);  //从接收缓冲区里读出数据, E3 g2 i7 P' g0 w
  51. 51                 for(i=0; i<32; i++)TxBufferUSART[i] = rx_buf[i];  //向USB 端点1的缓冲区里放置数据      2 [7 o" N5 f- a9 d! V  s" [# B" N9 F
  52. 52                 rf_rec_flag=1;1 Q: ~& Z" g- a8 c  X  n. @. f
  53. 53             }
    & N7 S* k# B# ~2 G8 G' x, o
  54. 54             else if((status &0x10)>0){                     //发射达到最大复发次数               
    $ |3 q5 I3 J" d2 s  e
  55. 55                 SPI_RW_Reg(0xe1,0);                          //清除发送缓冲区                  ( Z! v; V% [4 R, Z+ D( ^! F
  56. 56                 RX_Mode();                                 //进入接收模式                  
    ) Q1 Y4 v4 w& U# _$ h  E" v0 D( |* y, u
  57. 57             }
    $ [) g+ y* p# o+ X& a$ O+ J6 d
  58. 58             else if((status &0x20)>0){                     //发射后收到应答
    3 r2 z2 L' S; K- G$ N6 H! n5 ?
  59. 59                 GPIO_SetBits(GPIOB, GPIO_Pin_5);   
    * T- Y: ~1 Q& U5 M. y
  60. 60                 SPI_RW_Reg(0xe1,0);                         //清除发送缓冲区              # U" I6 v! e" y- K: c% @% _* j: J
  61. 61                 RX_Mode();                                 //进入接收模式                   ( a: l) z* ]5 i7 J) u# h
  62. 62             }4 y+ X: M9 {1 z+ q& x
  63. 63             SPI_RW_Reg(WRITE_REG1+STATUS, status);         //清除07寄存器标志
    3 Y6 t) O3 K7 H6 W9 B8 J. J! v
  64. 64         }        
    3 y9 V7 l1 y( v
  65. 65         EXTI_ClearITPendingBit(EXTI_Line0);             //清除EXTI0上的中断标志              
    . ^0 |  a5 _4 N* |4 G, k$ J2 H
  66. 66     }
    9 z7 x2 z- z1 \! ?
  67. 67 }
复制代码

$ F0 u+ j; I- j0 a! u3 @: v
下位机部分
上面说过一般具有远程通信能力的嵌入式系统其下位机部分往往要干很多事,这里我们采用stm32作为MCU并搭载uc-OS实时操作系统负责任务调度,同时采用7寸TFT彩屏和uc-GUI设计可视化人机交互界面。其中任务包括主任务、界面任务和触摸任务,主任务负责建立其他任务,界面任务中将包含整个人机交互界面的界面刷新逻辑,触摸任务负责获取触摸位置数据获取~
222211477037312.png

8 |+ d8 r; G0 B% \
这里我们还是得从main函数先说起:首先在main函数中进行相关初始化,然后建立主任务并启动uc-OS内核;接着在主任务中调用App_TaskCreate(); 分别建立界面任务和触摸任务(如下每个任务的建立类似,要给出指向任务代码的指针、任务执行时传递给任务的参数的指针,分配给这个任务的栈信息,任务优先级等)。这样当任务建立好之后,其执行权就由操作系统调度了~
  1. 1 static  void App_TaskCreate(void)
    8 ~2 |' F3 Z! P
  2. 2 {
    3 W# c$ }$ K! n! _
  3. 3    /*  建立用户界面任务 */
      C3 Z2 j+ |2 ?1 f& ~. E
  4. 4    OSTaskCreateExt(AppTaskUserIF,                                               //指向任务代码的指针
    1 k2 X5 N1 A) a7 _6 J, C; N
  5. 5                        (void *)0,                                                   //任务开始执行时,传递给任务的参数的指针  ]% l- U4 Z8 t+ b( u
  6. 6                        (OS_STK *)&AppTaskUserIFStk[APP_TASK_USER_IF_STK_SIZE-1],  //分配给任务的堆栈的栈顶指针   从顶向下递减$ \0 J* h1 h  q1 ]9 H4 t+ o
  7. 7                     APP_TASK_USER_IF_PRIO,                                       //分配给任务的优先级
    # ]5 w' }, @/ J! n) u/ M2 c
  8. 8                     APP_TASK_USER_IF_PRIO,                                       //预备给以后版本的特殊标识符,在现行版本同任务优先级
    7 J' M- l( N$ \# e/ ~1 v( Z
  9. 9                     (OS_STK *)&AppTaskUserIFStk[0],                               //指向任务堆栈栈底的指针,用于堆栈的检验2 m& L# q9 E; R8 n! F# f
  10. 10                     APP_TASK_USER_IF_STK_SIZE,                                    //指定堆栈的容量,用于堆栈的检验
    % `2 q5 J2 o; N( d
  11. 11                     (void *)0,                                                    //指向用户附加的数据域的指针,用来扩展任务的任务控制块( J+ ^. V7 G  {. X' O1 }3 I
  12. 12                     OS_TASK_OPT_STK_CHK|OS_TASK_OPT_STK_CLR);                    //选项,指定是否允许堆栈检验,是否将堆栈清0,任务是否要
    # c2 L* e0 Z% O1 b) B# p
  13. 13                                                                                 //进行浮点运算等等。
    ; v& G' @0 O$ f2 }
  14. 14                     
    % e) h2 I. {* z* e
  15. 15    /* 建立触摸驱动任务 */
    2 r  Z* F, w- G( ?; c; }- E
  16. 16    OSTaskCreateExt(AppTaskKbd,
    % M  }, F: E# j' _
  17. 17                        (void *)0,
    3 l9 W# e# s! l6 }
  18. 18                     (OS_STK *)&AppTaskKbdStk[APP_TASK_KBD_STK_SIZE-1],. `" u$ @8 K) O; \0 ^& k4 B
  19. 19                     APP_TASK_KBD_PRIO,. I6 F) Z. e5 I8 {' `
  20. 20                     APP_TASK_KBD_PRIO,+ c8 {, n+ i0 I2 w& u
  21. 21                     (OS_STK *)&AppTaskKbdStk[0],& d- K/ r. M7 E& d% [' i- N0 ^
  22. 22                     APP_TASK_KBD_STK_SIZE,
    0 c# Y2 r* l4 s' h* Q. u, s3 H
  23. 23                     (void *)0,
    3 o) z2 \. g' L- o/ A, x" A
  24. 24                     OS_TASK_OPT_STK_CHK|OS_TASK_OPT_STK_CLR);   
    " r( m# h; G0 t7 p# @7 G" ~8 X$ I( U, n
  25. 25
    # A( `/ J0 r3 L0 w  v/ j' V
  26. 26 }
复制代码

' @8 p2 G( ]8 H+ A. ^  Z7 q3 P( c9 Q1 V. ^3 x$ C# r
这里以界面任务为例:因为我们在建立界面任务时已经指定其任务代码指针AppTaskUserIF,所以这里来写其对应的函数(也就是说这里是界面任务的入口)。从下面的代码可以看出进入界面任务时首先对uc-GUI进行初始化,然后进入死循环不断执行Fun()函数(有人会疑惑:这里while死循环不就只能死在这里吗?怎么执行其他任务呢?哈哈,这就是具有操作系统和不具有操作系统的不同啦~虽然这里是while死循环,但是当OS要把CPU占有权分给其他任务时就会把当前执行的任务的信息压入其对应的栈空间,当再次要把CPU分配给该任务时,则把栈里保存的上次执行的情况拿出来继续执行,从而实现抢占与多任务的效果!)
  1. 1 static  void  AppTaskUserIF (void *p_arg)
    & e* {) p" c9 ~
  2. 2 {                                               
    - Z; u+ Y$ l) Y0 S- `3 n' r
  3. 3  (void)p_arg;                                    % d9 W' T6 B5 R5 f  h) j
  4. 4   GUI_Init();                    //ucgui初始化
    ' k5 S4 w0 q) \3 {4 |
  5. 5   while(1)
    9 K: V8 b3 A, [
  6. 6   {   
    8 V/ }: k9 h8 n2 p0 |7 Q
  7. 7      Fun();                     //界面主程序8 I8 r% J' g8 P9 {
  8. 8   }% p+ U) S2 j$ M, q" G$ D9 u- i
  9. 9 }
复制代码

( |9 B! k' t, E, `4 g, m9 }# _6 T
5 ?/ M# U' W+ z& F& K9 l* f
所以接下来我们主要看Fun.c里的Fun函数:虽然代码有点长,但是很好理解,其核心思路就是建立整个界面并对界面中的每个控件进行相关设置同时获得其句柄,在最后又进入了while死循环,在循环中不断检测2.4G是否接受到数据(和适配器端类似也是中断子程序中收数据然后置接收标志为1的),然后根据从2.4G收到的数据来刷新文本显示区;下面一个if判断speed_change_flag是否有效来向串口发送相应的数据。那么我们的问题又来了:这个speed_change_flag是在哪里被改变的呢?这个我们就要参看窗口回调函数了!这里的窗口回调函数是窗口动作响应函数(就像安卓开发里的按钮监听或MFC里的按钮点击事件等),一旦窗口里的控件有相应的触发动作就会调用该函数,并把事件类型封装在WM_MESSAGE里传过来,在该函数里对该消息进行解析并作出相应的动作即可(非常像Win32!!!我怀疑做这个uc-GUI的人有copy微软的嫌疑,♪(^∇^*)随便猜测,如有雷同,纯属巧合)。这样我们就很容易找到send按钮的监听用于将数据通过NRF24L01发送出去的相关操作,也就明白了滑动条监听用来改变speed1~5.上面说了这么多,少了介绍整个界面是怎么建立的了~其实整个窗体的布局都要放在一个结构体里,然后在fun()函数里调用hWin = GUI_CreateDialogBox(aDialogCreate, GUI_COUNTOF(aDialogCreate), _cbCallback, 0, 0, 0);根据定义的窗口资源和回调函数进行窗体的建立~这样我们就圆满地理解了stm32基于uc-OS并搭载uc-GUI的运行逻辑啦!
  1.   1 void Fun(void) {   / _0 m$ S8 n! }, S  Q
  2.   2   GUI_CURSOR_Show();                                        //打开鼠标图形显示  # v8 }3 M/ Z) N% S
  3.   3   % B) q8 |2 X  e
  4.   4   /* 建立对话框时,包含了资源列表,资源数目, 并且指定了用于动作响应的回调函数  */3 h% g+ H3 a, j
  5.   5   hWin = GUI_CreateDialogBox(aDialogCreate, GUI_COUNTOF(aDialogCreate), _cbCallback, 0, 0, 0);! K5 p+ [0 a/ f9 F
  6.   6 7 b' b$ q/ v) [5 T8 c
  7.   7   FRAMEWIN_SetFont(hWin, &GUI_FontComic18B_1);                  //对话框字体设置 ! y# O" ~7 W$ U% b9 i" `
  8.   8   FRAMEWIN_SetClientColor(hWin, GUI_BLACK);                      //对话框的窗体颜色是黑色
    7 [$ s  D1 p( `; o. [: Y7 N
  9.   9   memcpy(tx_buf, "1234567890abcdefghij!@#$%^&*()-=", 32);   //将长度为32字节的发送字符串拷贝到发送缓冲区,- ^8 g! z9 i7 v2 ^
  10. 10   memcpy(rx_buf, "", 32);                                    //将接收缓存区清空2 i/ d/ H) f/ U  A
  11. 11  
    " O, T3 O' |/ |& o
  12. 12   /* 获得文本框句柄 */        
    ! x* q2 C5 {9 @
  13. 13   text1 = WM_GetDialogItem(hWin, GUI_ID_TEXT0);                //获得对话框里GUI_ID_TEXT0项目(文本框Send Text Area)的句柄
    : _; L  G" c. o( M0 d
  14. 14   text2 = WM_GetDialogItem(hWin, GUI_ID_TEXT1);                //获得对话框里GUI_ID_TEXT1项目(文本框Receive Text Area)的句柄) z! I* \( x: v9 F; l
  15. 15   text3 = WM_GetDialogItem(hWin, GUI_ID_TEXT2);                //获得对话框里GUI_ID_TEXT2项目(文本框2M BPS)的句柄, `( X# G$ J6 c3 }( T
  16. 16   text4 = WM_GetDialogItem(hWin, GUI_ID_TEXT3);                //获得对话框里GUI_ID_TEXT3项目(文本框1M BPS)的句柄" [5 D8 o# l  t6 j
  17. 17   text6 = WM_GetDialogItem(hWin, GUI_ID_TEXT5);                //获得对话框里GUI_ID_TEXT5项目(文本框250K BPS)的句柄; E  x9 i3 ^2 ?8 O3 d
  18. 18   text5 = WM_GetDialogItem(hWin, GUI_ID_TEXT4);                //获得对话框里GUI_ID_TEXT4项目(状态字符文本框)的句柄  
      E+ r# u  B3 V' C' C
  19. 19   /* 设置文本框字体 */( }* M: y+ \: h( G8 ?6 f0 Z4 [
  20. 20   TEXT_SetFont(text1,pFont);                                //设置对话框里文本框Send Text Area的字体
    % ?$ F' _' {3 w6 ~4 J7 V9 k
  21. 21   TEXT_SetFont(text2,pFont);                                //设置对话框里文本框Receive Text Area的字体& ]7 y: w* R# ]' |
  22. 22   TEXT_SetFont(text3,pFont18);                                //设置对话框里文本框2M BPS的字体7 P8 u. U0 C$ ~% F* D
  23. 23   TEXT_SetFont(text4,pFont18);                                //设置对话框里文本框1M BPS的字体. r5 H  y( m, F% f' w' D* @8 j
  24. 24   TEXT_SetFont(text6,pFont18);                                //设置对话框里文本框250K BPS的字体% O3 m$ a; |  M# `9 \
  25. 25   TEXT_SetFont(text5,pFont);                                //设置对话框里状态字符文本框的字体
    ( k3 @; X+ U8 W2 J/ b1 C
  26. 26   /* 设置文本框颜色 */0 U0 B# g+ E: d3 e1 {0 k
  27. 27   TEXT_SetTextColor(text1,GUI_GREEN);                        //设置对话框里文本框Send Text Area的字体颜色% o7 s- a% j( p' i! @! |
  28. 28   TEXT_SetTextColor(text2,GUI_GREEN );                        //设置对话框里文本框Receive Text Area的字体颜色+ L/ P) I+ A$ G
  29. 29   TEXT_SetTextColor(text3,GUI_YELLOW);                        //设置对话框里文本框2M BPS的字体颜色/ p  V/ k5 v6 _! R
  30. 30   TEXT_SetTextColor(text4,GUI_YELLOW);                        //设置对话框里文本框1M BPS的字体颜色! J* U0 y& p2 K7 T
  31. 31   TEXT_SetTextColor(text6,GUI_YELLOW);                       //设置对话框里文本框250K BPS的字体颜色
    % b4 {. n2 N" C' C7 x% h* {) Y
  32. 32   TEXT_SetTextColor(text5,GUI_YELLOW);                        //设置对话框里状态字符文本框的字体颜色) Y0 t& z# ~3 o7 y5 U0 p
  33. 33   TEXT_SetBkColor(text5,GUI_BLUE);                            //设置对话框里状态字符文本框的背景颜色
    9 _2 ^4 Z. u( ?3 }
  34. 34 " h1 p* U) M4 V+ b; ]
  35. 35   /* 编辑框相关 */
    4 e) q% g* j& w; N
  36. 36   edit1 = WM_GetDialogItem(hWin, GUI_ID_EDIT1);                //获得对话框里GUI_ID_EDIT1项目(编辑框 发送字符串显示区)的句柄$ D( E! Q4 K/ S: a, `, B
  37. 37   EDIT_SetFont(edit1,pFont18);                                //设置对话框里编辑框 发送字符串显示区的字体
      ?9 _% u; [" s, c  x& b
  38. 38   EDIT_SetText(edit1,(const char *)tx_buf);                    //设置对话框里编辑框 发送字符串显示区的字符串% R8 m6 O- c! j6 Y) R
  39. 39   edit2 = WM_GetDialogItem(hWin, GUI_ID_EDIT2);                //获得对话框里GUI_ID_EDIT2项目(编辑框 接收字符串显示区)的句柄
    % E% x' i# ]6 y0 \5 ^
  40. 40   EDIT_SetFont(edit2,pFont18);                                //设置对话框里编辑框 接收字符串显示区的字体
    ( t9 ^% a- |. {1 m; D5 j
  41. 41   EDIT_SetText(edit2,(const char *)rx_buf);                    //设置对话框里编辑框 接收字符串显示区的字符串4 v9 {/ z8 W7 J, q) t- X
  42. 42
    : s3 ~, j* M$ f  y
  43. 43   /* 按钮相关 */' K) W- `# p1 r8 ~" x- r2 F7 }% R
  44. 44   bt[0]=WM_GetDialogItem(hWin,GUI_ID_BUTTON0);                //获得对话框里GUI_ID_BUTTON0项目(按键SEND)的句柄# s! f: b3 r# Q" P+ _3 E
  45. 45   bt[1]=WM_GetDialogItem(hWin, GUI_ID_BUTTON2);                //获得对话框里GUI_ID_BUTTON2项目(按键CLEAR)的句柄
    , W3 E: {& R: P9 \- ~* w
  46. 46   BUTTON_SetFont(bt[0],pFont);                                //设置对话框里按键SEND的字体
    - w' ]( X' l  x! N% R. U; G2 i
  47. 47   BUTTON_SetFont(bt[1],pFont);                                //设置对话框里按键CLEAR的字体
    : x7 }. i3 [" t+ @- U: q8 A( d
  48. 48   BUTTON_SetTextColor(bt[0],0,GUI_WHITE);                     //设置对话框里按键SEND未被按下的字体颜色
    # ]$ z* _& n7 `% }! v3 C- T
  49. 49   BUTTON_SetTextColor(bt[1],0,GUI_WHITE);                    //设置对话框里按键CLEAR未被按下的字体颜色
    ! h) e% z- r/ w  [. D& n, S7 Z, m
  50. 50
    ! Y  {& o; I. ]: q
  51. 51   /* List相关 */            ! i3 f1 ]. E( J( ?6 v. T
  52. 52   nrf_Pipe=0;                                                //NRF24L01初始发射通道设置为0
    * \1 L0 I3 D  S1 n. L1 {8 n9 Y
  53. 53   list1 = WM_GetDialogItem(hWin, GUI_ID_LISTBOX0);            //获得对话框里GUI_ID_LISTBOX0项目(列表框-通道选择)的句柄     
    & J& @+ |8 F9 ?+ b$ g, p
  54. 54   LISTBOX_SetText(list1, _apListBox);                        //设置对话框里列表框-通道选择里的条目3 z- w+ T. H  @$ P- o6 h9 M
  55. 55   LISTBOX_SetFont(list1,pFont18);                            //设置对话框里列表框-通道选择的字体8 `  a. U8 u; J
  56. 56   LISTBOX_SetSel(list1,nrf_Pipe);                            //设置对话框里列表框-通道选择的焦点选择
    7 \6 B- V: h% b  r
  57. 57   SCROLLBAR_CreateAttached(list1, SCROLLBAR_CF_VERTICAL);    //设置对话框里列表框-通道选择的卷动方向为下拉        
    1 @8 Z+ O8 ~1 S/ S$ q- N
  58. 58 ) g  {0 t: T) J: Q% V& k5 L
  59. 59   /* Radio按钮相关 */        
    8 ]4 c$ H, ]$ {- \( S/ h; [! j6 \) t' ?
  60. 60   rd0 = WM_GetDialogItem(hWin, GUI_ID_RADIO0);                //获得对话框里GUI_ID_RADIO0项目(点选框-速率选择)的句柄
    . q9 ^* N$ |$ y
  61. 61   nrf_baud=0;                                                //NRF24L01速率 初始为2MPS  H9 \  n# k% k! n5 k- U
  62. 62   RADIO_SetValue(rd0,0);                                    //设置对话框里点选框-速率选择的焦点选择0 \4 i0 K8 u  h) n" `. j& |
  63. 63   RX_Mode();                                                //NRF24L01进入接收模式 7 }: p" p: K3 q2 S( o8 r: g
  64. 64  
    8 X1 E: I. r' t
  65. 65   /* 获得slider部件的句柄 */    ; c# f8 y! M' z0 f, t3 t" c7 ?
  66. 66   slider1 = WM_GetDialogItem(hWin, GUI_ID_SLIDER1);
    ) X1 ~! I& s$ N, ~1 p9 x& S0 `
  67. 67   slider2 = WM_GetDialogItem(hWin, GUI_ID_SLIDER2);8 O+ r8 E6 ?8 M4 Y" [, D: H( {7 \1 d
  68. 68   slider3 = WM_GetDialogItem(hWin, GUI_ID_SLIDER3);8 Y. ?* e/ ^5 v/ ?" a
  69. 69   slider4 = WM_GetDialogItem(hWin, GUI_ID_SLIDER4);  |1 V) w3 R* ?9 w7 ?' o5 g( W
  70. 70   slider5 = WM_GetDialogItem(hWin, GUI_ID_SLIDER5);
    ( o- a. z* l, I4 N' }4 E& f* J
  71. 71   /* 设置slider部件的取值范围-8-8*/  4 B( ]2 `/ G% Z/ p+ e* {+ z
  72. 72   SLIDER_SetRange(slider1,-8,8);   
    $ U: X, t$ L2 K4 y9 J- g$ o0 u- `
  73. 73   SLIDER_SetRange(slider2,-8,8);
    , B% r- y: s1 q. l# r3 C: ^
  74. 74   SLIDER_SetRange(slider3,-8,8);9 D- e4 L' O6 V6 D; B2 I% z
  75. 75   SLIDER_SetRange(slider4,-8,8);
    ' p( V9 g1 v8 p4 X  h2 g! Y
  76. 76   SLIDER_SetRange(slider5,-8,8);
    ) Y2 u' e" y( ]3 O/ |4 V
  77. 77   /* 设置slider部件的值*/      
    ' _) V: v! u) u8 l; W1 b
  78. 78   SLIDER_SetValue(slider1,0);  
    $ z  b6 U3 o: k' b& B
  79. 79   SLIDER_SetValue(slider2,0);   
    / F4 |" e5 k+ J* D) C  R
  80. 80   SLIDER_SetValue(slider3,0);  
    3 Q1 L- x  w5 A2 s" w
  81. 81   SLIDER_SetValue(slider4,0);  
    0 w1 }/ H3 d1 U6 w* n
  82. 82   SLIDER_SetValue(slider5,0);  
    7 a" M1 m' k0 \+ X4 m& l
  83. 83   /* 获取文本框句柄 */% x  c+ A2 \6 y# X+ {% |& y, J
  84. 84   text_speed1 = WM_GetDialogItem(hWin, GUI_ID_TEXT_SPEED1);    ) C6 R, [0 S( J
  85. 85   text_speed2 = WM_GetDialogItem(hWin, GUI_ID_TEXT_SPEED2);    ) x& W! O" F% q- x
  86. 86   text_speed3 = WM_GetDialogItem(hWin, GUI_ID_TEXT_SPEED3);    2 y2 ^( J' C; U# d; q
  87. 87   text_speed4 = WM_GetDialogItem(hWin, GUI_ID_TEXT_SPEED4);   
    . M5 K3 G1 t. ~2 U/ J
  88. 88   text_speed5 = WM_GetDialogItem(hWin, GUI_ID_TEXT_SPEED5);      ~+ ~2 ]3 y- n* G4 R* e- d
  89. 89   /* 设置文本框字体 */3 B" Y; C4 t2 o8 v
  90. 90   TEXT_SetFont(text_speed1,pFont18);
    " p8 I" B, S# I3 O- c' M# q
  91. 91   TEXT_SetFont(text_speed2,pFont18);
    $ k- @6 [) [& X+ _% x5 k6 {, a
  92. 92   TEXT_SetFont(text_speed3,pFont18);7 J' x4 n8 @. V2 L$ Q
  93. 93   TEXT_SetFont(text_speed4,pFont18);
    ! \' {7 v0 p: y3 L
  94. 94   TEXT_SetFont(text_speed5,pFont18);                                    6 Q4 N9 p& u! _
  95. 95   /* 设置文本框颜色 */
    1 M/ j4 e( s" Z
  96. 96   TEXT_SetTextColor(text_speed1,GUI_YELLOW);        # r4 j! s( J# Q3 f0 _
  97. 97   TEXT_SetTextColor(text_speed2,GUI_YELLOW);        
    1 }) o" c! Y+ \1 g/ K
  98. 98   TEXT_SetTextColor(text_speed3,GUI_YELLOW);        
    ; ^1 j4 P6 S) z, J% c
  99. 99   TEXT_SetTextColor(text_speed4,GUI_YELLOW);        1 p- G1 s! Q3 Z4 [4 y1 r( S2 |
  100. 100   TEXT_SetTextColor(text_speed5,GUI_YELLOW);        + Z0 U+ }; G: [5 ]
  101. 101 + u. m& s/ U$ D' a3 Z8 m1 [0 L
  102. 102   speed_change_flag=0;! H. J$ U8 c+ _$ Q) @" @/ k2 u
  103. 103
    / n( W9 p- w& @+ H$ h5 h/ \
  104. 104   while (1)
    * v* b; |; n- A
  105. 105   {               8 H0 F& G6 o, b% ?5 ^6 K5 }7 u# H
  106. 106     if(Rx_Succ==1){                                            //当NRF24L01接收到有效数据0 m" Y  u7 w7 v+ S% C% h  A
  107. 107         EDIT_SetText(edit2,(const char *)rx_buf);            //将接收缓冲区的字符写入到接收字符编辑框内
    2 v$ z9 B/ U" q5 K) k
  108. 108         TEXT_SetText(text5,(const char *)status_buf);        //将状态文本缓冲区的字符写入到状态文本框内
    1 ~5 @8 l4 r1 u; g4 b
  109. 109         Rx_Succ=0; - A5 t: g+ w1 g2 ^- k* w2 r' q& \
  110. 110 //        for(i=0;i<sizeof(rx_buf);i++)        ( s0 x# |2 H6 L+ O/ E. N/ c' X+ F
  111. 111 //            USART_SendChar(USART1,rx_buf[i]);
    . X& q1 _+ j0 t$ Q3 o3 M
  112. 112     }2 F8 Q" \# |8 ^, y
  113. 113     if(speed_change_flag!=0)
    ; p) Y+ d( H9 Q2 z# F% g4 E) U
  114. 114     {: G4 l/ ?6 t% `/ X/ U% T7 O/ f  T
  115. 115        speed_change_flag=0;% S# r2 C2 R" }$ S8 A
  116. 116        USART_SendChar(USART1,control_data);
    7 r$ B( o3 _1 l0 K5 w8 F. z
  117. 117     }' f7 d! a, \4 g2 n; r
  118. 118     WM_Exec();                                                //刷新屏幕
    . M' V$ I0 f& _) W
  119. 119   }      9 D1 D9 s5 R+ y7 }
  120. 120 }
复制代码
  1. 1 /* 定义了对话框资源列表 */7 H5 ^# r1 `7 m% R, a
  2. 2 static const GUI_WIDGET_CREATE_INFO aDialogCreate[] = {
    $ L9 V- n- g, z% u& ^. p: D
  3. 3   //建立窗体, 大小是800X480  原点在0,00 ?$ b& l. T' E0 S  e0 d5 g* q
  4. 4   { FRAMEWIN_CreateIndirect, "http://beautifulzzzz", 0,0,0, 800, 480, FRAMEWIN_CF_ACTIVE },
    , j2 v+ t0 y, P' n" K2 X+ Y( j
  5. 5   { BUTTON_CreateIndirect,   "SEND",    GUI_ID_BUTTON0,       0,    395,  200,  55 },
    ) J! u6 c+ y$ z0 E( K& t7 l
  6. 6  + ~, d' s3 V  J+ i+ K4 U% k- \
  7. 7   { BUTTON_CreateIndirect,   "CLEAR", GUI_ID_BUTTON2,   200,    395, 200,  55 },
    - g# V% t0 S, `! Z: P+ n$ u
  8. 8   { EDIT_CreateIndirect,     "",       GUI_ID_EDIT1,    0,   190, 400,  65, EDIT_CF_LEFT, 50 },
    + ]& K3 f9 w! |1 r
  9. 9   { EDIT_CreateIndirect,     "",       GUI_ID_EDIT2,    0,   290, 400,  65, EDIT_CF_LEFT, 50 },
    . T8 c* a; C2 _, O; j
  10. 10   $ O6 p5 A5 H+ q' S
  11. 11   //建立TEXT控件,起点是窗体的X,X,大小XXY  文字左对齐+ _8 k8 l. Q. O( e$ b; T
  12. 12   { TEXT_CreateIndirect,     "Send Text Area",  GUI_ID_TEXT0,   1,   160,  400,  25, TEXT_CF_LEFT },& p% V) v9 w$ }' ?
  13. 13   { TEXT_CreateIndirect,     "Receive Text Area ",  GUI_ID_TEXT1,     1,   263,  400, 25, TEXT_CF_LEFT },
    * [3 c7 `$ F  ?5 e) |3 n
  14. 14  
    & p; t5 h7 i0 s9 J1 L
  15. 15   { TEXT_CreateIndirect,     "2M bps",  GUI_ID_TEXT2,   23,   22,  140,  25, TEXT_CF_LEFT },# }- e1 g9 \) N7 F& L
  16. 16   { TEXT_CreateIndirect,     "1M bps",  GUI_ID_TEXT3,     23,   42,  140,  25, TEXT_CF_LEFT },- w& h2 C/ h( L& V% D: r
  17. 17   { TEXT_CreateIndirect,     "250K bps",  GUI_ID_TEXT5,     23,   62,  140,  25, TEXT_CF_LEFT },
    ; H( A* P5 S8 d! H0 R/ ~0 ]8 k
  18. 18   
    % u: n! }3 [) g6 h. q
  19. 19   { TEXT_CreateIndirect,     "",  GUI_ID_TEXT4,     0,   120,  400,  25, TEXT_CF_LEFT },
    ! p2 Z6 S3 T: l/ W" N
  20. 20
    1 t/ ~6 E: I9 ]# Q3 Y1 M7 l0 j
  21. 21   { RADIO_CreateIndirect,     "Receive Mode",  GUI_ID_RADIO0,     3,   33,  40,  52, RADIO_TEXTPOS_LEFT,3},
    " M+ ^/ V, ^; H7 J  Q
  22. 22
    $ ~% V- A9 d$ I: K( b# L
  23. 23   { LISTBOX_CreateIndirect,  "",       GUI_ID_LISTBOX0,  134,    13,  130,  90, 0, 0 },+ t1 f) F8 w* Q$ c) ]" v* T6 A
  24. 24
    - @) ^+ d% I( ^  U9 ]% R5 E$ D, L
  25. 25    //建立滑块: r" J- |. f4 e
  26. 26   { SLIDER_CreateIndirect,   NULL,     GUI_ID_SLIDER1,  440,  60, 320, 25, 0, 0 },
    5 {9 L" x! y+ Y
  27. 27   { SLIDER_CreateIndirect,   NULL,     GUI_ID_SLIDER2,  440,  120, 320, 25, 0, 0 },; o5 V  z9 |( L# I
  28. 28   { SLIDER_CreateIndirect,   NULL,     GUI_ID_SLIDER3,  440,  180, 320, 25, 0, 0 },
    # s/ @* Q- e: M6 h% P3 {5 u( {
  29. 29   { SLIDER_CreateIndirect,   NULL,     GUI_ID_SLIDER4,  440,  240, 320, 25, 0, 0 },
    8 {+ o# R" ]4 v. V5 F
  30. 30   { SLIDER_CreateIndirect,   NULL,     GUI_ID_SLIDER5,  440,  300, 320, 25, 0, 0 },
    6 \. e+ k' w* y& }+ l4 y9 ?
  31. 31   //建立滑块对应的text
    / E+ @2 `5 @9 Y* |% ^
  32. 32   { TEXT_CreateIndirect,     "0",  GUI_ID_TEXT_SPEED1,   770,   60,   25,  25, TEXT_CF_LEFT },
    8 U4 {" h' f9 C* x$ K# c5 ]
  33. 33   { TEXT_CreateIndirect,     "0",  GUI_ID_TEXT_SPEED2,   770,   120,  25,  25, TEXT_CF_LEFT },
    ) j, s( j9 C5 K5 g0 `2 S2 K
  34. 34   { TEXT_CreateIndirect,     "0",  GUI_ID_TEXT_SPEED3,   770,   180,  25,  25, TEXT_CF_LEFT },+ i4 f: H/ `/ N( q* Q. w- a# q" f; \
  35. 35   { TEXT_CreateIndirect,     "0",  GUI_ID_TEXT_SPEED4,   770,   240,  25,  25, TEXT_CF_LEFT },
    3 ]$ F) H1 @) K- J* t& P$ \
  36. 36   { TEXT_CreateIndirect,     "0",  GUI_ID_TEXT_SPEED5,   770,   300,  25,  25, TEXT_CF_LEFT },
    ' E: f; j2 A: G2 l- w5 _
  37. 37 };
复制代码

( w! B, W' x8 y/ x* e) O; E. R3 N8 {
还要回过头说说我们的USART和NRF24L01,他们的初始化要看main函数中的BSP_Init();函数,该函数负相关硬件的初始化设置(中文意思是板级支持包初始化函数,因为uc-OS可以并不只限于stm32单片机,所以这里要根据不同平台进行相应的设置)。该函数位于bsp.c函数中,其作用相当于将以前我们在main函数中进行的相关硬件初始化单独拿出来封装成一个函数而已~但是,串口和无线对应的中断接收程序却有点不一样,因为这里是操作系统,所以在每个中断子程序前要调用OS_ENTER_CRITICAL();保存当前的全局中断标志,然后OSIntNesting++;中断嵌套深度加1,最后调用OS_EXIT_CRITICAL();恢复全局中断标志进入正常的中断处理,此外在中断响应函数最后要调用OSIntExit(); 检测如果有更高优先级的任务就绪了,则执行一次任务切换。  
  1. 1 /*******************************************************************************+ T$ y* i: K+ z. {: w2 C$ Q
  2. 2 * Function Name  : USART1_IRQHandler+ o! F3 H7 t/ `' L" J5 E
  3. 3 * Description    : This function handles USART1 global interrupt request.1 h; t% F( @* D3 U- v( h
  4. 4 * Input          : None3 u1 L9 N! \1 A- E+ ~8 c
  5. 5 * Output         : None" s" m& [! R4 Y# G5 W& H
  6. 6 * Return         : None
    % ]* \/ M( h: D1 {# `
  7. 7 *******************************************************************************/9 z3 p1 |, x; C+ V7 t0 s/ Z) X
  8. 8 void USART1_IRQHandler(void)2 p( B* H8 Q  j0 u* T$ E
  9. 9 { $ i# }2 F/ a4 r" M( t$ y
  10. 10     unsigned int i;, ^0 k6 \& Q* X, ?- A
  11. 11     OS_CPU_SR  cpu_sr;
    0 n  L$ G0 K* v; N
  12. 12     OS_ENTER_CRITICAL();  //保存全局中断标志,关总中断 Tell uC/OS-II that we are starting an ISR1 H+ H& f% J/ c3 j& Y$ Z: N7 E& o% [& Y2 l
  13. 13     OSIntNesting++;
    8 H+ ~9 \) ^; K/ j3 o. V
  14. 14     OS_EXIT_CRITICAL();      //恢复全局中断曛?        ! Y9 Z. _2 S/ b: r+ i, x
  15. 15 ( B/ H6 `; b0 O3 |
  16. 16     if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)       //判断读寄存器是否非空
      \  |, S- @& d+ Y9 Y2 k0 C1 q( |
  17. 17     {   
    $ C' E: I1 u" e+ X& W7 V
  18. 18         RxBufferUSART[RxCounter1++] = USART_ReceiveData(USART1);   //将读寄存器的数据缓存到接收缓冲区里/ z. g3 H/ I; R$ ]5 X3 ^" L
  19. 19         if(RxBufferUSART[RxCounter1-2]==0x0d&&RxBufferUSART[RxCounter1-1]==0x0a)     //判断结束标志是否是0x0d 0x0a6 _; X, a, m$ F, R& R1 O" l
  20. 20         {
    & }: W+ l. p* j) y. a
  21. 21             for(i=0; i< RxCounter1; i++) TxBufferRF[i] = RxBufferUSART[i];          //将接收缓冲器的数据转到发送缓冲区,准备转发! w% }. G8 A' `$ a5 v# \
  22. 22             usart_rec_flag=1;                                                             //接收成功标志
    3 |+ t* j9 ]! l9 D) b; e
  23. 23             TxBufferRF[RxCounter1]=0;                                             //发送缓冲区结束符   
    $ D2 k+ l4 `" }7 ~0 z
  24. 24             TxCounter1=RxCounter1;
    - H% w0 C  ?& o7 u3 ^
  25. 25             RxCounter1=0;9 D2 O, c- O9 F' Q# W, r+ U" d
  26. 26         }/ i8 o8 U" g$ F3 `& o$ y. L
  27. 27     }
    # ^/ c+ _/ [' T3 _  l7 J
  28. 28     if(USART_GetITStatus(USART1, USART_IT_TXE) != RESET)                   //这段是为了避免STM32 USART 第一个字节发不出去的BUG / ^9 j4 b" V, [# D+ f6 z' c6 y- s
  29. 29     { 0 N. B& n* w: f! I9 C
  30. 30         USART_ITConfig(USART1, USART_IT_TXE, DISABLE);                         //禁止发缓冲器空中断, 0 Y2 _1 w% c  z! }7 B
  31. 31     }   
    - ^# d; M1 K4 b- d. I1 Z
  32. 32     OSIntExit();           //在os_core.c文件里定义,如果有更高优先级的任务就绪了,则执行一次任务切换   
    + I* {! T, x$ {5 s' q
  33. 33 }
    8 W. r, b0 T% ?8 V) Y  L# Y5 F% d
  34. 34 /////////////////////////////
    1 _. S' q4 G9 Y
  35. 35 void EXTI0_IRQHandler(void)1 v: k, _+ _' J: z& _. K0 h2 B
  36. 36 {
    : Z% _7 W; h) j9 p$ S% s
  37. 37   unsigned char status,i;
    5 n# n2 d! d( A
  38. 38   OS_CPU_SR  cpu_sr;7 \2 }1 z4 G, C# u) G. `
  39. 39   OS_ENTER_CRITICAL();  //保存全局中断标志,关总中断 Tell uC/OS-II that we are starting an ISR9 g" ~3 H# Y$ x: r1 r
  40. 40   OSIntNesting++;
    % @+ e* A' l4 s$ p& Q5 I; E, n3 W
  41. 41   OS_EXIT_CRITICAL();      //恢复全局中断标志             % K4 e. v8 n$ P5 X, o  d
  42. 42   
    : E' b; s6 U$ s# a; G
  43. 43   if(EXTI_GetITStatus(EXTI_Line0) != RESET)            //判断是否产生了EXTI0中断6 P' Y8 w; G& h, r. s- I, r- X/ `  U
  44. 44   {1 R7 Q9 o7 o) ^$ a- \, e0 O+ B
  45. 45       if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==0){ //判断是否是PA0线变低            
    6 H3 X' K; H0 L/ R+ j, t: |
  46. 46         status=SPI_Read(READ_REG1+STATUS);            // 读取状态寄存其来判断数据接收状况   
    ( E9 V; [( b& d% L: q0 T" z" ?3 |
  47. 47         if(status & 0x40)                            // 判断是否接收到数据                   7 b2 F) `! \1 x% L
  48. 48         {                                                         & ], y6 V1 t' S" x1 K. v& j
  49. 49              SPI_Read_Buf(RD_RX_PLOAD,rx_buf,TX_PLOAD_WIDTH);  //从接收缓冲区里读出数据 , B4 j' R, ~& z8 ]9 w
  50. 50             for(i=0; i<32; i++){                              //向USB 端点1的缓冲区里放置数据
    . j9 K4 L  I0 C- H! I5 j$ i( f
  51. 51                 TxBufferUSART[i] = rx_buf[i];     
    ' H  H$ ]; X) d- N) v
  52. 52             }
    9 d$ p  v3 Y2 z% y  v( R
  53. 53             rf_rec_flag=1;         
    - W5 T! ^$ N) F4 G" @
  54. 54             if((status&0x0e)<=0x0a){                           
    " \1 x& K7 X$ ^9 i  \( g- S) @2 U
  55. 55                nrf_Pipe_r=(status&0x0e)>>1;                      //读出是在哪个通道接收的
    8 J5 \; |6 c- n- \! u: C% n
  56. 56             }
    1 z$ @( R4 ?9 y: o) }3 y; z
  57. 57             else nrf_Pipe_r=0;                     ! j. u  ^1 H3 _% \
  58. 58             Rx_Succ=1;            //读取数据完成标志+ W. l) E& x2 x
  59. 59             /* 根据读出的接收通道号,将相应信息写入状态文本缓冲区 */- j2 k; {4 U/ [6 B) y
  60. 60             if(nrf_Pipe_r==0) memcpy(status_buf, "Pipe 0 Recive OK!   ", 20);           ! \1 z6 Q# A) j  B: y1 O
  61. 61             else if(nrf_Pipe_r==1) memcpy(status_buf, "Pipe 1 Recive OK!   ", 20);; B% k! X1 C& K$ z, E3 a) B
  62. 62             else if(nrf_Pipe_r==2) memcpy(status_buf, "Pipe 2 Recive OK!   ", 20);) L0 u( U# h: y: @: ^1 p- B. p
  63. 63             else if(nrf_Pipe_r==3) memcpy(status_buf, "Pipe 3 Recive OK!   ", 20);
    $ R" R3 h" @/ q. t7 a
  64. 64             else if(nrf_Pipe_r==4) memcpy(status_buf, "Pipe 4 Recive OK!   ", 20);
    & k8 }! {2 C# T- r- x6 m
  65. 65             else if(nrf_Pipe_r==5) memcpy(status_buf, "Pipe 5 Recive OK!   ", 20);8 d3 r! m4 d3 l' U* a
  66. 66         }
    - `- |: j- h, L2 ^! _
  67. 67         else if((status &0x10)>0){                     //发射达到最大复发次数               
    8 D* i# C" o. B
  68. 68             SPI_RW_Reg(0xe1,0);                          //清除发送缓冲区                  
    % l1 k* L( }# p! ^5 a
  69. 69             RX_Mode();                                 //进入接收模式                                 
    & w  E1 X4 c- [9 ]# q
  70. 70             Rx_Succ=1;                                                                                    
    . q1 h" g) Z' I
  71. 71             /* 根据发送通道,将相应信息写入状态文本缓冲区 */
    & n6 y  w# o' Q& p6 Q) u& Z
  72. 72             if(nrf_Pipe==0) memcpy(status_buf, "Pipe 0 NO ACK!      ", 20);. A% Z3 p. U: ^( U( @7 i& \; j
  73. 73             else if(nrf_Pipe==1) memcpy(status_buf, "Pipe 1 NO ACK!      ", 20);" e2 q- g% I+ g# N* ^
  74. 74             else if(nrf_Pipe==2) memcpy(status_buf, "Pipe 2 NO ACK!      ", 20);* ], D" Y3 Q5 `
  75. 75             else if(nrf_Pipe==3) memcpy(status_buf, "Pipe 3 NO ACK!      ", 20);  
    + ]( `/ C0 v5 M& ]
  76. 76             else if(nrf_Pipe==4) memcpy(status_buf, "Pipe 4 NO ACK!      ", 20);
    " Y0 f6 u- w  Z8 m& `
  77. 77             else if(nrf_Pipe==5) memcpy(status_buf, "Pipe 5 NO ACK!      ", 20);            
    ( j3 _$ Y  q5 w& H+ J
  78. 78         }
    $ v! g( P& a* f& A1 c8 g
  79. 79         else if((status &0x20)>0){                     //发射后收到应答             . t8 V/ Z4 I* {) U, Y2 Y4 A
  80. 80             SPI_RW_Reg(0xe1,0);                         //清除发送缓冲区              
    $ B* K/ @/ t' A0 Q' N
  81. 81             RX_Mode();                                 //进入接收模式
    $ w% {0 k8 _4 Y8 r7 {+ k$ T  d, a
  82. 82             Rx_Succ=1;
    5 V3 A7 k# g1 [* B5 ?
  83. 83             /* 根据发送通道,将相应信息写入状态文本缓冲区 */
    & a% P( p  F6 j; u0 q
  84. 84             if(nrf_Pipe==0) memcpy(status_buf, "Pipe 0 Send OK!     ", 20);6 a: ~7 j5 e/ d
  85. 85             else if(nrf_Pipe==1) memcpy(status_buf, "Pipe 1 Send OK!     ", 20);
    ( @# |+ s9 P  s# c5 J& b4 D
  86. 86             else if(nrf_Pipe==2) memcpy(status_buf, "Pipe 2 Send OK!     ", 20);& Z$ ^/ ?7 x; X' b
  87. 87             else if(nrf_Pipe==3) memcpy(status_buf, "Pipe 3 Send OK!     ", 20);9 j/ q  X- I  q, A3 J3 K& \
  88. 88             else if(nrf_Pipe==4) memcpy(status_buf, "Pipe 4 Send OK!     ", 20);1 S; H4 K, T' a) }
  89. 89             else if(nrf_Pipe==5) memcpy(status_buf, "Pipe 5 Send OK!     ", 20);               
    ( p+ J& b, M6 `1 j# D6 o* E. v3 H$ L
  90. 90         }2 l/ ^  Y' C2 `3 k1 a
  91. 91         1 u  H1 d$ r/ l) B2 x0 ?! Z
  92. 92         SPI_RW_Reg(WRITE_REG1+STATUS, status);         //清除07寄存器标志              6 ?7 @5 f; w  T4 n( T
  93. 93       }        
    * q- D! Z' m. S5 {3 G+ H
  94. 94       EXTI_ClearITPendingBit(EXTI_Line0);             //清除EXTI0上的中断标志              
    * }! Q3 ?! [. x1 Z4 O$ C
  95. 95   }   
    5 o6 t- r* G( v/ v$ {! O0 H2 i% q
  96. 96   OSIntExit();           //在os_core.c文件里定义,如果有更高优先级的任务就绪了,则执行一次任务切换     
    4 i0 j  Q2 I8 c: |
  97. 97 }
复制代码

5 G1 i/ q# g% q+ E1 O9 Z
最后说明
对于纯玩软件的小伙伴,这里涉及的东西有点多,不必细究,看看了解即可。但是对于初学stm32,尤其是还在为stm32控制NRF24L01不通的同学,这个还是挺有用滴~下面有工程的链接,里面有些注释不规范,一切以我博客里说的为准哦~

, u9 e$ @% S  b1 A" l3 J) f
收藏 评论0 发布时间:2022-1-13 19:48

举报

0个回答
关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版