嵌入式 - 图文

更新时间:2024-04-24 14:24:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

光电与计算机工程学院 形 式 开 卷 闭 卷 级 研究生 密 编 号: 考试日期: 月 日

印刷份数: 份

上 海 理 工 大 学

研 究 生 试 题

20 14 /20 15 学年第 一学期

课程名称: 嵌入式系统

教 师 签 章: 年 月 日

教研室主任审查意见:

签 章: 年 月 日 1.试题原稿请于考试前2周送研究生部。 2.编号栏由研究生部填写。

上海理工大学研究生课程试题

*

共 页 第 1 页 2014/2014 学年第 1学期 考试课程 嵌入式系统 学 号 姓 名 得 分

一:固件及软件描述题(20分)

1.1、阅读STM32启动代码,阅读STM(10X)固件标准,描述其主要观点。

1.2、描述MDK软件使用基本步骤。 二:程序设计题(无操作系统)(20分)

2.1、根据固件建立工程,以流水灯为例说明固件建立工程和程序设计的方法(需结合硬件叙述)

2.2、以按键中断实现灯的闪烁为例,描述设计其实现原理和主要程序设计含流程图。(结合硬件) 三.操作系统题(60分)

3.1、阐述裸μc/osII操作系统的基本原理(10分)。

3.2、如何在实现裸μc/osII系统的移植STM32(编译系统选MDK)(20分)。

3.3、在μc/osII系统中以流水灯闪烁为例阐述程序设计的基本方法(需结合硬件)(10分)。

3.4、在μc/osII系统中,中断实现的基本原理,并以按键中断实现灯的闪烁为例,阐述程序设计的基本方法。(需结合硬件)(20分)

*

注:考题全部写在框内,不要超出边界。内容一律用黑色墨水书写或计算机打印,以便复印。

一:固件及软件描述题(20分)

1.1、阅读STM32启动代码,阅读STM(10X)固件标准,描述其主要观点。

答:打开keil4新建一个工程,选择stm32f107vc芯片(这是我本次实验所用的板子上的芯片型号),启动代码就会自动添加进来,文件名是startup_stm32f10x_cl.s,双击就可以看到启动代码,是用汇编语言编写的底层文件。从网上下载跟我这块板子配套的资料可以找到固件库,原理图等各种各样的资料,对固件标准来说有寄存器版本和库函数版本,像我这样的初学者直接去翻看是很难理解的,而且很容易厌倦,直到我在寄存器版本的例程中修改程序的时候,用库函数版本的文件替换,发现根本行不通,编译产生一大堆的错误,这时候我才耐下心来去细细观看其中的一些东西,比如例程中是stm32f103的程序,而我要在我的板子上跑就要修改其GPIO引脚,我用库函数去修改就产生一大堆的错误,然后不得不去看寄存器的标准,然后修改。作为一个初学者,给我的感觉是寄存器比较难以理解,而且用起来比较难,库函数相当方便,上手也简单,但是我觉得寄存器是非常非常基础的东西,能够帮助我们更好的理解单片机,理解程序运行的机制,总之,库函数学起来相当简单,而且功能强大,但是并不是万能的,有些时候实现一些功能还是得靠寄存器,所以学好寄存器编程是必要的。

1.2、描述MDK软件使用基本步骤。

答:首先我从下载下来的资料中找到keil4,然后安装,然后以管理员身份运行程序,打开之后用软件生产序列号激活一下,之后将资料包中的Jlink驱动安装一下,因为我要用Jlink连接板子调试。然后新建工程,project--new uvision project,新建一个文件夹,取任意工程名字,然后保存,之后再文件夹中新建USER和SYSTEM文件夹,之后将其他标标准工程文件中的SYSTEM文件夹拷贝过来,然后选择芯片,填出对话框点击是,加入启动代码,然后右键target1--manage components,出现下图对话框,

在groups中添加组,USER, SYSTEM等,然后将SYSTEM中的源码.c文件都添加进STSTEM中来,然后单击target options,出现以下对话框,

然后C/C++选项中include paths点击将USER和SYSTEM添加进路径,有时还有一些其他的将.c文件添加进组中的文件夹也要加进路径中,这样才能保证其中

的.h文件在build的时候可以正常加进来,然后选中debug选项,选择合适的调试方式,我用的是Jlink,之后点击最右边的选项Utilities,同样选择Jlink。这样基本上我们就可以开始设计新的程序了。

然后就是设计程序,添加和修改所用到的引脚,编译,如有错误就按照提示修改,直到build成功,就可以点击download烧进板子中,然后不断的调试,直到达到我们满意的功能为止。

二:程序设计题(无操作系统)(20分)

2.1、根据固件建立工程,以流水灯为例说明固件建立工程和程序设计的方法(需结合硬件叙述)

答:首先需要看一下灯的原理图,

可以看出有四个灯PC0,PC6,PC7,PC8,然后就清楚了需要用到的引脚了。主要代码如下。 int main(void) {

System_Setup(); //系统初始化 GPIO_SetBits(GPIOC , GPIO_Pin_0); GPIO_SetBits(GPIOC , GPIO_Pin_6); GPIO_SetBits(GPIOC , GPIO_Pin_7);

GPIO_SetBits(GPIOC , GPIO_Pin_8);//熄灭所有的LED灯 while (1) {

GPIO_SetBits(GPIOC , GPIO_Pin_6); GPIO_ResetBits(GPIOC , GPIO_Pin_7);//熄灭PC6,点亮PC7 Delay(0xfffff);

Delay(0xfffff); Delay(0xfffff); Delay(0xfffff);//延时 GPIO_SetBits(GPIOC , GPIO_Pin_7); GPIO_ResetBits(GPIOC , GPIO_Pin_0);//熄灭PC7,点亮PC0 Delay(0xfffff); Delay(0xfffff); Delay(0xfffff); Delay(0xfffff); GPIO_SetBits(GPIOC , GPIO_Pin_0); GPIO_ResetBits(GPIOC , GPIO_Pin_8);//熄灭PC0,点亮PC8 Delay(0xfffff); Delay(0xfffff); Delay(0xfffff); Delay(0xfffff); GPIO_SetBits(GPIOC , GPIO_Pin_8); GPIO_ResetBits(GPIOC , GPIO_Pin_6);//熄灭PC8,点亮PC6 Delay(0xfffff); Delay(0xfffff); Delay(0xfffff); Delay(0xfffff); } }

这样就实现了流水灯,GPIO引脚配置程序如下,

这些都是库函数固定的格式,没什么好说的。

GPIO_SetBits和GPIO_ResetBits都是stm32f10x_gpio.c中已经定义好了的库函数,可以直接用。但是代码非常冗长,由于是第一次写程序,经验不足,不知道在.h文件中定义简单的LEDx,用LEDx=0或1来表示亮灭。

而程序设计的一般方法就是需要用到的一些功能函数比如延时函数等,先在外部定义好然后在主程序中声明一下,程序开头先声明包含哪些.h文件和用到的自定义函数,然后定义所用到的变量,之后进入main函数,开始就是一些系统初始化,时钟初始化,IO口初始化之类的库函数,然后就是上面提到的设计代码,实现主要功能。而所用到的引脚要定义一下就可以了。下图所示就是程序烧进板子中跑的图。(见电子版)

2.2、以按键中断实现灯的闪烁为例,描述设计其实现原理和主要程序设计含流程图。(结合硬件)

答:按键中断首先要用到按键,可以看一下原理图

可以看到有PC10,PC11,PC12和PD3四个按键,我们不妨使用其中的三个PC10,PC11和PC12。板子上四个灯和四个按键是平行的而且挨得很近,因此我就喜欢将其按照从左到右一一对应的顺序进行了如下定义。从左到右的四个灯分别是PC7,PC0,PC8,PC6,按键对应关系是PC12,PC11,PD3,PC10,由于不使用PD3,因此将灯用如下的程序定义顺序。 #define LED0 PCout(7)// PC7 #define LED1 PCout(0)// PC0 #define LED3 PCout(6)// PC6

而按键的顺序用如下的程序定义。 #define KEY0 PCin(12) //PC12 #define KEY1 PCin(11) //PC11 #define KEY3 PCin(10) //PC10

由于都是接地的,因此按键配置模式都选择下拉输入。

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOC, &GPIO_InitStructure);

我这里设计的程序实现的主要功能是按键中断KEY0控制LED0的亮灭,KEY1控制LED1的亮灭,按下KEY3则三个灯同时闪烁,再按一下就不闪,保持状态,而在灯闪烁的同时按下KEY1,KEY0照样可以控制两灯的亮灭。因为在这里我将这三个中断键设置了相同的优先级。 流程图如下。

其中,t值是由KEY3的按下而翻转的,KEY0和KEY1在中断服务程序中直接定义的就是控制LED0和LED1的亮灭。主要代码如下。 int main(void) {

SystemInit();

delay_init(72); NVIC_Configuration(); uart_init(9600); LED_Init(); EXTIX_Init(); while(1) { printf(\ switch(t) {case 1: LED0=!LED0; LED1=!LED1; LED3=!LED3; delay_ms(100); break; default:break; }

delay_ms(200); } }

中断线及中断初始化配置见附件,中断服务程序如下。 void EXTI15_10_IRQHandler(void) {

delay_ms(10);

if(EXTI_GetITStatus(EXTI_Line10) != RESET) { t=!t; }

else if (EXTI_GetITStatus(EXTI_Line11) != RESET) { LED1=!LED1; }

else if (EXTI_GetITStatus(EXTI_Line12) != RESET) { LED0=!LED0; }

EXTI_ClearITPendingBit(EXTI_Line10); EXTI_ClearITPendingBit(EXTI_Line11); EXTI_ClearITPendingBit(EXTI_Line12); }

程序在板子跑的图片如下(见电子版),第二张照片不是很清楚,因为如果开闪光灯的话灯的闪烁就看不出来了。

三.操作系统题(60分)

3.1、阐述裸μc/osII操作系统的基本原理(10分)。

答:UCOSII体系结构如下。

用户应用程序UCOSII与处理器无关的代码ucos_ii.h ucos_ii.cos_tmr.c os_time.cos_task.c os_sem.cos_q.c os_mutex.cos_mem.c os_mbox.cos_flag.c os_core.c UCOSII与应用程序相关代码os_cfg.hincludes.hUCOSIIUCOSII与处理器相关的代码(移植时需要修改)os_cpu.h os_cpu_a.asm os_cpu_c.cCPU定时器

UCOSII基本概念: 任务:将一个大任务分解为多个小任务,然后通过运行小任务达到解决大任务的目的。这个小任务对应的程序实体就叫“任务”。

使操作系统并发的运行多个任务,从而提高处理器的利用率,加快程序执行速度。

UCOS通过对小任务的运行进行管理和调度。

用户任务:应用程序设计者编写的任务,解决应用问题。

系统任务:系统提供的任务,为应用程序提供服务或为系统本身服务。

一个完整的任务应该有如下三部分: 任务代码(程序)

任务的私有堆栈(用以保护运行环境)

任务控制块(关联任务代码的程序控制块) 并发:由同一个处理器轮换地运行多个程序。或者说是由多个程序轮班地占用处理器这个资源。且在占用这个资源期间,并不一定能够把程序运行完毕。

外部中断相信大家都比较熟悉了。CPU在执行一段用户代码的时候,如果此时发生了外部中断,那么先进行现场保护,之后转向中断服务程序执行,执行完成后恢复现场,从中断处开始执行原来的用户代码。Ucos的原理本质上也是这样的,当一个任务A正在执行的时候,如果他释放了cpu控制权,先对任务A进行现场保护,然后从任务就绪表中查找其他就绪任务去执行,等到任务A的等待时间到了,它可能重新获得cpu控制权,这个时候恢复任务A的现场,从而继续执行任务A,这样看起来就好像两个任务同时执行了。实际上,任何时候,只有一个任务可以获得cpu控制权。这个过程很负责,场景也多样,这里只是举个简单的例子说明。

任务切换:中止当前正在运行任务,转而去运行另外一个任务就绪任务。

任务无缝切换的关键是断点数据的保护。也就是必须任务在终止时把该任务的断点数据保存到堆栈,而在被重新运行时,则把堆栈中的这些断点数据重新恢复到CPU的各寄存器,实现程序的无缝切换。

uC/OS-II只是一个实时操作系统内核,它包含了任务调度,任务管理,时间管理,内存管理和任务间的通信和同步等基本功能。

uC/OS-II的任务调度是完全基于任务优先级的抢占式调度,也就是最高优先级的任务一旦处于就绪状态,则立即抢占正在运行的低优先级任务的处理器资源。

所谓的任务,其实就是一个死循环函数,该函数实现一定的功能,一个工程可以有很多这样的任务(最多255个),UCOSII对这些任务进行调度管理,让这些任务可以并发工作(注意不是同时工作!!,并发只是各任务轮流占用CPU,而不是同时占用,任何时候还是只有1个任务能够占用CPU),这就是UCOSII最基本的功能。

假如我们新建了2个任务为MyTask和YourTask,这里我们先忽略任务优先级的概念,两个任务死循环中延时时间为1s。如果某个时刻,任务MyTask在执行中,当它执行到延时函数OSTimeDlyHMSM的时候,它释放cpu控制权,这个时候,任务YourTask获得cpu控制权开始执行,任务YourTask执行过程中,也会调用延时函数延时1s释放CPU控制权,这个过程中任务A延时1s到达,重新获得CPU控制权,重新开始执行死循环中的任务实体代码。如此循环,现象就是两个任务交替运行,就好像CPU在同时做两件事情一样。

任务优先级:这个概念比较好理解,ucos中,每个任务都有唯一的一个优先级。优先级是任务的唯一标识。在UCOSII中,使用CPU的时候,优先级高(数值小)的任务比优先级低的任务具有优先使用权,即任务就绪表中总是优先级最高的任务获得CPU使用权,只有高优先级的任务让出CPU使用权(比如延时)时,低优先级的任务才能获得CPU使用权。UCOSII不支持多个任务优先级相同,也就是每个任务的优先级必须不一样。

任务堆栈:就是存储器中的连续存储空间。为了满足任务切换和响应中断时保存CPU寄存器中的内容以及任务调用其他函数时的需要,每个任务都有自己的堆栈。在创建任务的时候,任务堆栈是任务创建的一个重要入口参数。

任务控制块OS_TCB:用来记录任务堆栈指针,任务当前状态以及任务优先级等任务属性。UCOSII的任何任务都是通过任务控制块(TCB)的东西来控制的,一旦任务创建了,任务控制块OS_TCB就会被赋值。每个任务管理块有3个最重要的参数:1,任务函数指针;2,任务堆栈指针;3,任务优先级;任务控制块就是任务在系统里面的身份证(UCOSII通过优先级识别任务)。 任务就绪表:简而言之就是用来记录系统中所有处于就绪状态的任务。它是一个位图,系统中每个任务都在这个位图中占据一个进制位,该位置的状态(1或者0)就表示任务是否处于就绪状态。

任务调度:作用一是在任务就绪表中查找优先级最高的就绪任务,二是实现任务的切换。比如说,当一个任务释放cpu控制权后,进行一次任务调度,这个时候任务调度器首先要去任务就绪表查询优先级最高的就绪任务,查到之后,进行一次任务切换,转而去执行下一个任务。 任务状态:

睡眠状态,任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。 就绪状态,系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准备好了,但由于该任务的优先级比正在运行的任务的优先级低, 还暂时不能运行,这时任务的状态叫做就绪状态。

运行状态,该任务获得CPU使用权,并正在运行中,此时的任务状态叫做运行状态。 等待状态,正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把CPU的使用权让给别的任务而使任务进入等待状态。 中断服务状态,一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态。

3.2、如何在实现裸μc/osII系统的移植STM32(编译系统选MDK)(20分)。

答:首先下载源代码http://micrium.com/page/downloads/ports/st/stm32。 然后我们要修改代码,配置UCOS。

首先,os_cfg.h是用来配置系统功能的,我们需要通过修改它来达到剪裁系统功能的目的。在做实际项目时,我们通常不会用完全部的ucos功能,我们需要通过裁剪内核以避免浪费系统的宝贵资源。

1.首先肯定要禁用信号量、互斥信号量、邮箱、列队、信号量集、定时器、内存管理、关闭调试模式。

用不着的钩子函数也禁掉,多重事件控制也禁掉

2.修改os_cpu.h,将下面三个文件注释掉就可以了。 void OS_CPU_SysTickHandler(void) void OS_CPU_SysTickInit(void); INT32U OS_CPU_SysTickClkFreq(void);

为什么要注释掉这三个函数,因为在os_cpu_c.c中定义,是SYSTICK中断的中断处理函数,而在stm32f10x_it.c中已经有该中断函数的定义,所以就不需要了,OS_CPU_SysTickInit(void);我们会定义在BSP中,所以也注释掉。OS_CPU_SysTickClkFreq(void);定义在os_cpu_c.c中用于初始化systick定时器,它依赖于OS_CPU_SysTickClkFreq(void),也要注释。 3.修改os_cpu_c.c

将systick defines、OS_CPU_SysTickHandler、OS_CPU_SysTickInit注释掉,因为它们是为systick定时器服务的,需把所有与这个相关的代码删掉。 4.修改os_cpu_a.asm

由于编译器原因将下面的内容替换一下

还有将

前面的export改成public。

5.修改os_dbj.c

将里面的#define OS_COMPILER_OPT __root改为. #define OS_COMPILER_OPT 也是因为编译器不同产生的原因。 6.修改startup_stm32f10x_hd.s

因为我们移植使用CMSIS中的startup_stm32f10x_hd.s作为启动文件,还没有设置OS_CPU_SysTickHandler。所以我们需要将启动代码中PendSV中断向量名改为OS_CPU_PendSVHandler。 7.在include中编写以下头文件. #include \#include \#include \#include \#include \#include \8.编写BSP

在前面我们说定时器我们自己定义,因此BSP.c中我们加入我们自己定义,这个函数需要添加一个头文件stm32f10x_rcc.h。另外哈有要编写初始化启动函数BSP_Init(),包括设置系统时钟,初始化硬件。 9.编写stm32f10x_it.c 添加SysTick中断的处理代码 void SysTick_Handler(void) {

OSIntEnter(); OSTimeTick(); OSIntExit(); }

因为调用uc/os函数,所以时钟也配置好了。至此移植过程就结束了。

3.3、在μc/osII系统中以流水灯闪烁为例阐述程序设计的基本方法(需结合硬件)(10分)。

答:在3.2已经移植好了的模板中直接编写。 定义栈

OS_STK task_led1_stk[TASK_LED1_STK_SIZE]; OS_STK task_led2_stk[TASK_LED2_STK_SIZE];

OS_STK startup_task_stk[STARTUP_TASK_STK_SIZE]; 设置任务优先级

#define STARTUP_TASK_PRIO 8 #define TASK_LED1_PRIO 10 #define TASK_LED2_PRIO 13 设置栈的大小

#define STARTUP_TASK_STK_SIZE 128 #define TASK_LED2_STK_SIZE 64 #define TASK_LED3_STK_SIZE 64 主要程序如下。 int main(void) {

BSP_Init(); OSInit();

OSTaskCreate(Task_Start,(void *)0,

&startup_task_stk[STARTUP_TASK_STK_SIZE-1], STARTUP_TASK_PRIO); OSStart(); return 0; }

Main就是创建一个起始任务。 void Task_Start(void *p_arg) {

(void)p_arg; SysTick_init();

OSTaskCreate(Task_LED1,(void *)0,

&task_led1_stk[TASK_LED1_STK_SIZE-1], TASK_LED1_PRIO); OSTaskCreate(Task_LED2,(void *)0,

&task_led2_stk[TASK_LED2_STK_SIZE-1], TASK_LED2_PRIO); while (1) {

LED0( ON );

OSTimeDlyHMSM(0, 0,0,400); LED0( OFF); OSTimeDlyHMSM(0, 0,0,400); } }

Task_led1和task_led2就是控制LED1和LED2闪烁的程序,就不贴了。

原理就是进入main函数,然后创建起始任务,同时控制LED0的闪烁,在LED0进入灭的期间,去做其他两个任务中优先级高得那个,然后进入延时再次释放

CPU使用权,去寻找另一个就绪的任务,不断重复。在板子上跑的图就不贴了,因为貌似图片看不出闪烁的效果,程序见附件。

3.4、在μc/osII系统中,中断实现的基本原理,并以按键中断实现灯的闪烁为例,阐述程序设计的基本方法。(需结合硬件)(20分)

答:首先介绍一下uCOSii处理中断的原理。

系统接收到中断请求后,如果CPU处于开中断状态,系统就会中止正在运行的当前任务,而按中断向量的指向去运行中断服务子程序,当中断服务子程序运行完成后,系统会根据具体情况返回到被中止的任务继续运行,或转向另一个中断优先级别更高的就绪任务。

由于UCOSii是可剥夺型的内核,所以中断服务程序结束后,系统会根据实际情况进行一次任务调度,如果有优先级更高的任务,就去执行优先级更高的任务,而不一定要返回被中断了的任务。

Ucosii的中断过程示意图

具体中断过程:

1.中断到来,如果被CPU识别,CPU将查中断向量表,根据中断向量表,获得中断服务子程序的入口地址。

2.将CPU寄存器的内容压入当前任务的任务堆栈中(依处理器的而定,也可能压入被压入被中断了的任务堆栈中)。

3.通知操作系统将进入中断服务子程序。即:调用OSIntEnter()或OSIntNesting直接加1。

4.If(OSIntNesting==1){OSTCBCur->OSTCBStrPtr=SP;} //如果是第一层中断,则将堆栈指针保存到被中断任务的任务控制块中。

5.清中断源,否则在开中断后,这类中断将反复的打入,导致系统崩溃。 6.执行用户ISR。

7.中断服务完成后,调用OSIntExit().如果没有高优先级的任务被中断服务子程序激活而进入就绪态,那么就执行被中断了的任务,且只占用很短的时间。 8.恢复所有CPU寄存器的值. 9.执行中断返回指令。

本题就是在上一题的基础上,将exti.c文件直接加进来,然后在stm32f10x_conf.h中把#include \和#include \这两句话去掉注释,就可以了。Main函数仍然和上一题一样,很多地方都不用变,只有少数几个地方。

实现的功能就是按下中断键key1实现LED1的闪烁,按下中断键key2使得LED2也开始闪烁,并且LED1保持闪烁,也就是两灯同时以不同频率闪烁。 首先是exti.c中要把中断服务程序变掉, void EXTI15_10_IRQHandler(void) {

OSTimeDlyHMSM(0,0,0,10);

if(EXTI_GetITStatus(EXTI_Line10) != RESET) { f1=!f1; }

else if (EXTI_GetITStatus(EXTI_Line11) != RESET) { //LED2=!LED2; f2=!f2; }

EXTI_ClearITPendingBit(EXTI_Line10); EXTI_ClearITPendingBit(EXTI_Line11); }

然后在app.c中变化如下。

void Task_Start(void *p_arg) {

(void)p_arg; SysTick_init();

OS_ENTER_CRITICAL();

OSTaskCreate(Task_LED2,(void *)0,

&task_led2_stk[TASK_LED2_STK_SIZE-1], TASK_LED2_PRIO); OSTaskCreate(Task_LED3,(void *)0,

&task_led3_stk[TASK_LED3_STK_SIZE-1], TASK_LED3_PRIO); OSTaskSuspend(STARTUP_TASK_PRIO ); OS_EXIT_CRITICAL(); }

void Task_LED2(void *p_arg) {

(void)p_arg; SysTick_init(); while (1) { switch(f1) {case 1: LED2( ON );

OSTimeDlyHMSM(0, 0,0,700); LED2( OFF); OSTimeDlyHMSM(0, 0,0,700); break; default:break; } } }

void Task_LED3(void *p_arg) {

(void)p_arg; SysTick_init(); while (1) {

switch(f2) {case 1: LED1( ON );

OSTimeDlyHMSM(0, 0,0,400); LED1( OFF); OSTimeDlyHMSM(0, 0,0,400); break; default:break; }

} }

然后就可以编译实现本题中所叙述的功能了。

本文来源:https://www.bwwdw.com/article/eipp.html

Top