【正点原子探索者STM32F407开发板例程连载+教学】第55章 串口IAP实验

2019-07-20 12:55发布

第五十五章 串口IAP实验

 1.硬件平台:正点原子探索者STM32F407开发板  2.软件平台:MDK5.1  3.固件库版本:V1.4.0
IAP,即在应用编程。很多单片机都支持这个功能,STM32F4也不例外。在之前的FLASH模拟EEPROM实验里面,我们学习了STM32F4FLASH自编程,本章我们将结合FLASH自编程的知识,通过STM32F4的串口实现一个简单的IAP功能本章分为如下几个部: 55.1 IAP简介 55.2 硬件设计 55.3 软件设计 55.4 下载验证  

55.1 IAP简介   

IAPIn Application Programming)即在应用编程,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。 通常实现IAP功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(USBUSART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在User Flash中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作: 1)检查是否需要对第二部分代码进行更新 2)如果不需要更新则转到4 3)执行更新操作 4)跳转到第二部分代码执行 第一部分代码必须通过其它手段,如JTAGISP烧入;第二部分代码可以使用第一部分代码IAP功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分IAP代码更新。 我们将第一个项目代码称之为Bootloader程序,第二个项目代码称之为APP程序,他们存放在STM32F4 FLASH的不同地址范围,一般从最低地址区开始存放Bootloader,紧跟其后的就是APP程序(注意,如果FLASH容量足够,是可以设计很多APP程序的,本章我们只讨论一个APP程序的情况)。这样我们就是要实现2个程序:BootloaderAPP STM32F4APP程序不仅可以放到FLASH里面运行,也可以放到SRAM里面运行,本章,我们将制作两个APP,一个用于FLASH运行,一个用于SRAM运行。 我们先来看看STM32F4正常的程序运行流程,如图55.1.1所示:
 55.1.1 STM32F4正常运行流程图        STM32F4的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序文件就从此地址开始写入。此外STM32F4是基于Cortex-M4内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是0x08000004,当中断来临,STM32F4的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。        在图55.1.1中,STM32F4在复位后,先从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main函数,如图标号②所示;而我们的main函数一般都是一个死循环,在main函数执行过程中,如果收到中断请求(发生重中断),此时STM32F4强制将PC指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回main函数执行,如图标号⑤所示。        当加入IAP程序之后,程序运行流程如图55.1.2所示:  55.1.2 加入IAP之后程序运行流程图        在图55.1.2所示流程中,STM32F4复位后,还是从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAPmain函数,如图标号①所示,此部分同图55.1.1一样;在执行完IAP以后(即将新的APP代码写入STM32F4FLASH,灰底部分。新程序的复位中断向量起始地址为0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数,如图标号②和③所示,同样main函数为一个死循环,并且注意到此时STM32F4FLASH,在不同位置上,共有两个中断向量表。        main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0X08000004中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回main函数继续运行,如图标号⑥所示。        通过以上两个过程的分析,我们知道IAP程序必须满足两个要求: 1)  新程序必须在IAP程序之后的某个偏移量为x的地址开始; 2)  必须将新程序的中断向量表相应的移动,移动的偏移量为x 本章,我们有2APP程序,一个为FLASHAPP,另外一个位SRAMAPP,图55.1.2虽然是针对FLASH APP来说的,但是在SRAM里面运行的过程和FLASH基本一致,只是需要设置向量表的地址为SRAM的地址。 1.APP程序起始地址设置方法 随便打开一个之前的实例工程,点击Options for TargetàTarget选项卡,如图55.1.3所示:  55.1.3 FLASH APP Target选项卡设置        默认的条件下,图中IROM1的起始地址(Start)一般为0X08000000,大小(Size)为0X100000,即从0X08000000开始的1024K空间为我们的程序存储区。而图中,我们设置起始地址(Start)为0X08010000,即偏移量为0X1000064K字节),因而,留给APP用的FLASH空间(Size)只有0X100000-0X10000=0XF0000960K字节)大小了。设置好StartSzie,就完成APP程序的起始地址设置。        这里的64K字节,需要大家根据Bootloader程序大小进行选择,比如我们本章的Bootloader程序为36K左右,理论上我们只需要确保APP起始地址在Bootloader之后,并且偏移量为0X200的倍数即可(相关知识,请参考:http://www.openedv.com/posts/list/392.htm)。这里我们选择64K0X10000)字节,留了一些余量,方便Bootloader以后的升级修改。        这是针对FLASH APP的起始地址设置,如果是SRAM APP,那么起始地址设置如图55.1.4所示:  55.1.4 SRAM APP Target选项卡设置        这里我们将IROM1的起始地址(Start)定义为:0X20001000,大小为0X19000100K字节),即从地址0X20000000偏移0X1000开始,存放APP代码。因为整个STM32F407ZGT6SRAM大小(不算CCM)为128K字节,所以IRAM1SRAM)的起始地址变为0X2001A000,大小只有0X600024K字节)。这样,整个STM32F407ZGT6SRAM(不含CCM)分配情况为:最开始的4KBootloader程序使用,随后的100K存放APP程序,最后24K,用作APP程序的内存。这个分配关系大家可以根据自己的实际情况修改,不一定和我们这里的设置一模一样,不过也需要注意,保证偏移量为0X200的倍数(我们这里为0X1000)。        2.中断向量表的偏移量设置方法        之前我们讲解过,在系统启动的时候,会首先调用SystemInit函数初始化时钟系统,同时SystemInit还完成了中断向量表的设置,我们可以打开SystemInit函数,看看函数体的结尾处有这样几行代码: #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;  /* Vector Table Relocation in Internal SRAM. */ #else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */ #endif 从代码可以理解,VTOR寄存器存放的是中断向量表的起始地址。默认的情况VECT_TAB_SRAM是没有定义,所以执行SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;  对于FLASH APP,我们设置为FLASH_BASE+偏移量0x10000,所以我们可以在SystemInit函数里面修改SCB->VTOR的值。当然为了尽可能不修改系统级别文件,我们可以也可以在FLASH APPmain函数最开头处添加如下代码实现中断向量表的起始地址的重设: SCB->VTOR = FLASH_BASE | 0x10000;        以上是FLASH APP的情况,当使用SRAM APP的时候,我们设置起始地址为:SRAM_BASE+0x1000,同样的方法,我们在SRAM APPmain函数最开始处,添加下面代码: SCB->VTOR = SRAM_BASE | 0x1000;        这样,我们就完成了中断向量表偏移量的设置。 通过以上两个步骤的设置,我们就可以生成APP程序了,只要APP程序的FLASHSRAM大小不超过我们的设置即可。不过MDK默认生成的文件是.hex文件,并不方便我们用作IAP更新,我们希望生成的文件是.bin文件,这样可以方便进行IAP升级(至于为什么,请大家自行百度HEXBIN文件的区别!)。这里我们通过MDK自带的格式转换工具fromelf.exe,来实现.axf文件到.bin文件的转换。该工具在MDK的安装目录ARMBIN40文件夹里面。 fromelf.exe转换工具的语法格式为:fromelf [options] input_file。其中options有很多选项可以设置,详细使用请参考光盘《mdk如何生成bin文件.doc. 本章,我们通过在MDK点击Options for TargetàUser选项卡,在Run User Programs After Build/Rebuild 栏,勾选Run #1,并写入:D:MDK5.11AARMARMCCinfromelf.exe  --bin -o .. OBJTEST.bin ..OBJTEST.axf ,如图55.1.5所示:  55.1.5 MDK生成.bin文件设置方法        通过这一步设置,我们就可以在MDK编译成功之后,调用fromelf.exe(注意,我的MDK是安装在D:MDK5.11A文件夹下,如果你是安装在其他目录,请根据你自己的目录修改fromelf.exe的路径),根据当前工程的TEST.axf,生成一个TEST.bin的文件。并存放在axf文件相同的目录下,即工程的OBJ文件夹里面。在得到.bin文件之后,我们只需要将这个bin文件传送给单片机,即可执行IAP升级。        最后再来看看APP程序的生成步骤: 1) 设置APP程序的起始地址和存储空间大小 对于在FLASH里面运行的APP程序,我们只需要设置APP程序的起始地址,和存储空间大小即可。而对于在SRAM里面运行的APP程序,我们还需要设置SRAM的起始地址和大小。无论哪种APP程序,都需要确保APP程序的大小和所占SRAM大小不超过我们的设置范围。 2) 设置中断向量表偏移量 这一步按照上面讲解,重新设置SCB->VTOR的值即可。 3) 设置编译后运行fromelf.exe,生成.bin文件. 通过在User选项卡,设置编译后调用fromelf.exe,根据.axf文件生成.bin文件,用于IAP更新。  以上3个步骤,我们就可以得到一个.binAPP程序,通过Bootlader程序即可实现更新。

55.2 硬件设计

本章实验(Bootloader部分)功能简介:开机的时候先显示提示信息,然后等待串口输入接收APP程序(无校验,一次性接收),在串口接收到APP程序之后,即可执行IAP。如果是SRAM APP,通过按下KEY0即可执行这个收到的SRAM APP程序。如果是FLASH APP,则需要先按下KEY_UP按键,将串口接收到的APP程序存放到STM32F4的FLASH,之后再按KEY2既可以执行这个FLASH APP程序。通过KEY1按键,可以手动清除串口接收到的APP程序。DS0用于指示程序运行状态。 本实验用到的资源如下: 1)  指示灯DS0 2)  四个按键(KEY0/KEY1/KEY2/KEY_UP 3)  串口 4)  TFTLCD模块 这些用到的硬件,我们在之前都已经介绍过,这里就不再介绍了。

55.3 软件设计

本章,我们总共需要3个程序:1Bootloader2FLASH APP3SRAM APP;其中,我们选择之前做过的RTC实验(在第二十章介绍)来做为FLASH APP程序(起始地址为0X08010000),选择触摸屏实验(在第三十三章介绍)来做SRAM APP程序(起始地址为0X20001000)。Bootloader则是通过TFTLCD显示实验(在第十八章介绍)修改得来。本章,关于SRAM APPFLASH APP的生成比较简单,我们就不细说,请大家结合光盘源码,以及55.1节的介绍,自行理解。本章软件设计仅针对Bootloader程序。 复制第十八章的工程(即实验13),作为本章的工程模版(命名为:IAP Bootloader V1.0),并复制第三十九章实验(FLASH模拟EEPROM实验)的STMFLASH文件夹到本工程的HARDWARE文件夹下,打开本实验工程,并将STMFLASH文件夹内的stmflash.c加入到HARDWARE组下,同时将STMFLASH加入头文件包含路径。HARDWARE文件夹所在的文件夹下新建一个IAP的文件夹,并在该文件夹下新建iap.ciap.h两个文件。然后在工程里面新建一个IAP的组,将iap.c加入到该组下面。最后,将IAP文件夹加入头文件包含路径。 打开iap.c,输入如下代码: iapfun jump2app; u32 iapbuf[512];   //2K字节缓存  //appxaddr:应用程序的起始地址 //appbuf:应用程序CODE. //appsize:应用程序大小(字节). void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize) {        u32 t; u16 i=0;  u32 temp;        u32 fwaddr=appxaddr;//当前写入的地址        u8 *dfu=appbuf;        for(t=0;t<appsize;t+=4)        {                                                         temp=(u32)dfu[3]<<24;                 temp|=(u32)dfu[2]<<16;                  temp|=(u32)dfu[1]<<8;               temp|=(u32)dfu[0];                 dfu+=4;//偏移4个字节               iapbuf[i++]=temp;                    if(i==512)               {   i=0;                      STMFLASH_Write(fwaddr,iapbuf,512);                      fwaddr+=2048;//偏移2048  512*4=2048               }        }        if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去 } //跳转到应用程序段 //appxaddr:用户代码起始地址. void iap_load_app(u32 appxaddr) {        if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)//检查栈顶地址是否合法.        {               jump2app=(iapfun)*(vu32*)(appxaddr+4);  //用户代码区第二个字为程序开始地址(复位地址)                        MSR_MSP(*(vu32*)appxaddr);           //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)               jump2app();                                                            //跳转到APP.        } }        该文件总共只有2个函数,其中,iap_write_appbin函数用于将存放在串口接收buf里面的APP程序写入到FLASHiap_load_app函数,则用于跳转到APP程序运行,其参数appxaddrAPP程序的起始地址,程序先判断栈顶地址是否合法,在得到合法的栈顶地址后,通过MSR_MSP函数(该函数在sys.c文件)设置栈顶地址,最后通过一个虚拟的函数(jump2app)跳转到APP程序执行代码,实现IAPàAPP的跳转。        保存iap.c,打开iap.h输入如下代码: #ifndef __IAP_H__ #define __IAP_H__ #include "sys.h"    typedef  void (*iapfun)(void);                          //定义一个函数类型的参数.   #define FLASH_APP1_ADDR            0x08010000         //第一个应用程序起始地址(存放在FLASH) //保留0X08000000~0X0800FFFF的空间为Bootloader使用(64KB)     void iap_load_app(u32 appxaddr);       //跳转到APP程序执行 void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen);//在指定地址开始,写入bin #endif        这部分代码比较简单,保存iap.h。本章,我们是通过串口接收APP程序的,我们将usart.cusart.h做了稍微修改,在usart.h中,我们定义USART_REC_LEN120K字节,也就是串口最大一次可以接收120K字节的数据,这也是本Bootloader程序所能接收的最大APP程序大小。然后新增一个USART_RX_CNT的变量,用于记录接收到的文件大小,而USART_RX_STA不再使用。在usart.c里面,我们修改USART1_IRQHandler部分代码如下: //串口1中断服务程序 //注意,读取USARTx->SR能避免莫名其妙的错误        u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000))); //接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.    //接收状态 //bit15 接收完成标志   bit14     接收到0x0d //bit13~0,接收到的有效字节数目 u16 USART_RX_STA=0;              //接收状态标记       u32 USART_RX_CNT=0;                   //接收的字节数       void USART1_IRQHandler(void) {        u8 res;     #ifdef OS_CRITICAL_METHOD //如果OS_CRITICAL_METHOD定义了,说明使用ucosII.        OSIntEnter();    #endif        if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)        {               Res =USART_ReceiveData(USART1);//(USART1->DR);   //读取接收到的数据                                    if(USART_RX_CNT<USART_REC_LEN)               {                      USART_RX_BUF[USART_RX_CNT]=Res;                      USART_RX_CNT++;                                                                                                   }            } #ifdef OS_CRITICAL_METHOD        //如果OS_CRITICAL_METHOD定义了,说明使用ucosII.        OSIntExit();                                                                                 #endif }        这里,我们指定USART_RX_BUF的地址是从0X20001000开始,该地址也就是SRAM APP程序的起始地址!然后在USART1_IRQHandler函数里面,将串口发送过来的数据,全部接收到USART_RX_BUF,并通过USART_RX_CNT计数。代码比较简单,我们就不多说了。 改完usart.cusart.h之后,我们在main.c修改main函数如下: int main(void) {                   u8 t; u8 key; u8 clearflag=0;        u16 oldcount=0;     //老的串口接收数据值        u32 applenth=0;     //接收到的app代码长度        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2        delay_init(168);  //初始化延时函数        uart_init(460800);         //初始化串口波特率为115200        LED_Init();                         //初始化LED       LCD_Init();                         //LCD初始化        KEY_Init();                         //按键初始化       POINT_COLOR=RED;//设置字体为红 {MOD}        LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");              LCD_ShowString(30,70,200,16,16,"IAP TEST");             LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");        LCD_ShowString(30,110,200,16,16,"2014/7/21");         LCD_ShowString(30,130,200,16,16,"KEY_UP:Copy APP2FLASH");        LCD_ShowString(30,150,200,16,16,"KEY1:Erase SRAM APP");        LCD_ShowString(30,170,200,16,16,"KEY0:Run SRAM APP");        LCD_ShowString(30,190,200,16,16,"KEY2:Run FLASH APP");        POINT_COLOR=BLUE;        //显示提示信息        POINT_COLOR=BLUE;//设置字体为蓝 {MOD}          while(1)        {              if(USART_RX_CNT)               {                      if(oldcount==USART_RX_CNT)//新周期内,没收到数据,认为本次接收完成.                      {                             applenth=USART_RX_CNT;                             oldcount=0;                             USART_RX_CNT=0;                             printf("用户程序接收完成! ");                             printf("代码长度:%dBytes ",applenth);                      }else oldcount=USART_RX_CNT;                                   }               t++;               delay_ms(10);               if(t==30)               {                      LED0=!LED0;  t=0;                      if(clearflag)                      {                             clearflag--;                             if(clearflag==0)LCD_Fill(30,210,240,210+16,WHITE);//清除显示                      }               }                           key=KEY_Scan(0);               if(key==WKUP_PRES) //WK_UP按键按下               {                      if(applenth)                      {                             printf("开始更新固件... ");                                  LCD_ShowString(30,210,200,16,16,"Copying APP2FLASH...");                            if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000) //判断是否为0X08XXXXXX.                             {                                        iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth); //更新FLASH代码                                      LCD_ShowString(30,210,200,16,16,"Copy APP Successed!!");                                    printf("固件更新完成! ");                                   }else                             {                                    LCD_ShowString(30,210,200,16,16,"Illegal FLASH APP!  ");                                             printf("FLASH应用程序! ");                             }                     }else                      {                             printf("没有可以更新的固件! ");                             LCD_ShowString(30,210,200,16,16,"No APP!");                      }                      clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示                                    }               if(key==KEY1_PRES)  //KEY1按下               {                      if(applenth)                      {                                                                                                              printf("固件清除完成! ");                                LCD_ShowString(30,210,200,16,16,"APP Erase Successed!");                             applenth=0;                      }else                      {                             printf("没有可以清除的固件! ");                             LCD_ShowString(30,210,200,16,16,"No APP!");                      }                      clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示                             }               if(key==KEY2_PRES)  //KEY2按下               {                      printf("开始执行FLASH用户代码!! ");                      if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000) //判断是否为0X08XXXXXX.                      {                                 iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码                      }else                      {                             printf("FLASH应用程序,无法执行! ");                             LCD_ShowString(30,210,200,16,16,"Illegal FLASH APP!");                            }                                                                                  clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示                 }               if(key==KEY0_PRES)  //KEY0按下               {                      printf("开始执行SRAM用户代码!! ");                      if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x20000000) //判断是否为0X20XXXXXX.                      {                                 iap_load_app(0X20001000);//SRAM地址                      }else                      {                             printf("SRAM应用程序,无法执行! ");                             LCD_ShowString(30,210,200,16,16,"Illegal SRAM APP!");                             }                                                                                  clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示               }            }  }        该段代码,实现了串口数据处理,以及IAP更新和跳转等各项操作。Bootloader程序就设计完成了,但是一般要求bootloader程序越小越好(给APP省空间嘛),实际应用时,可以尽量精简代码来得到最小的IAP。本章例程我们仅作演示用,所以不对代码做任何精简,最后得到工程截图如图55.3.1所示:  55.3.1 Bootloader 工程截图        从上图可以看出,Bootloader大小为36K左右,比较大,主要原因是液晶驱动和printf占用了比较多的flash,如果大家想删减代码,可以去掉不用的LCD部分代码和printf等,不过我们在本章为了演示效果,所以保留了这些代码。至此,本实验的软件设计部分结束。        FLASH APPSRAM APP两部分代码,根据55.1节的介绍,大家自行修改都比较简单,我们这里就不介绍了,不过要提醒大家:FLASH APP的起始地址必须是0X08010000,而SRAM APP的起始地址必须是0X20001000

55.4 下载验证

在代码编译成功之后,我们下载代码到ALIENTEK探索者STM32F4开发板上,得到,如图55.4.1所示:  55.4.1 IAP程序界面 此时,我们可以通过串口,发送FLASH APP或者SRAM APP到探索者STM32F4开发板,如图55.4.2所示:  55.4.2 串口发送APP程序界面        首先找到开发板USB转串口的串口号,打开串口(我电脑是COM3),然后设置波特率为460800(图中标号1所示),然后,点击打开文件按钮(如图标号2所示),找到APP程序生成的.bin文件(注意:文件类型得选择所有文件!!默认是只打开txt文件的),最后点击发送文件(图中标号3所示),将.bin文件发送给探索者STM32F4开发板,发送完成后,XCOM会提示文件发送完毕。 开发板在收到APP程序之后,我们就可以通过KEY0/KEY2运行这个APP程序了(如果是FLASH APP,则先需要通过KEY_UP将其存入对应FLASH区域)。      实验详细手册和源码下载地址:http://www.openedv.com/posts/list/41586.htm  正点原子探索者STM32F407开发板购买地址http://item.taobao.com/item.htm?id=41855882779
  

友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
26条回答
阿拓
2019-07-20 18:43
原子哥,我有一个问题,你的文章中有“也就是串口最大一次可以接收120K字节的数据,这也是本Bootloader程序所能接收的最大APP程序大小。”这么一句话,那如果我的代码大于120K字节该怎么办呢,我仔细看了一下,应该是USART_REC_LEN就可以了,但是USART接收的数据是放在SRAM中的,f407的sRAM好像只有256KB,那如果我的代码有700KB,该怎么处理呢,现在项目遇到了这样的问题,请原子哥指点一二~

一周热门 更多>