FreeRTOS实时操作系统与STM32的爱恨情仇

931844854操作系统 2024-01-01 19:38:48 6023阅读 举报

前言

emmm,这里主要简单讲了关于FreeRTOS系统框架的一些知识,和怎么用微处理器跑FreeRTOS的一个学习过程

一、实时操作系统是什么?

实时操作系统(Real-time operating system, RTOS),又称即时操作系统,它会按照排序运行、管理系统资源,并为开发应用程序提供一致的基础。
实时操作系统与一般的操作系统相比,最大的特色就是“实时性”,如果有一个任务需要执行,实时操作系统会马上(在较短时间内)执行该任务,不会有较长的延时。这种特性保证了各个任务的及时执行。我们刚刚开始记住“实时”就可以了,其他的后面再学。

了解完实时操作系统的定义,就来数一数目前主流的实时操作系统了。常见的实时操作系统有FreeRTOS、uCOS 等,国内比较著名的实时操作系统有RT-thread 和Huawei LiteOS 等。这次说的主要是FreeRTOS系统,因为它是世界上主流的实时操作系统之一,并且开源,小巧,免费,经过多年的风风雨雨,它已经有充足的资料提供给他人查阅,不用担心学习的过程中找不到资料。

二、stm32和FreeRTOS之间的点点滴滴

1.FreeRTOS介绍
FreeRTOS是当下热门的操作系统之一,并且开源免费,相较于ucos这个系统来说代码量比较小,能够移植到大部分微处理器上,特别适合新入门的学习。

FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。任务调度机制是嵌入式实时操作系统的一个重要概念,也是其核心技术。对于可剥夺型内核,优先级高的任务一旦就绪就能剥夺优先级较低任务的CPU使用权,提高了系统的实时响应能力。不同于μC/OS-II,FreeRTOS对系统任务的数量没有限制,既支持优先级调度算法也支持轮换调度算法,因此FreeRTOS采用双向链表而不是采用查任务就绪表的方法来进行任务调度。(任务调度这些有兴趣可以看看计算机操作系统那本书了解了解。)

2.CMSIS RTOS的介绍
与操作系统不同,CMSIS-RTOS是ARM公司为统一操作系统、降低嵌入式门槛而发布的操作系统标准软件接口(API)。通俗讲,CMSIS-RTOS将操作系统(不管是FREE-RTOS还是RTX等)屏蔽起来,然后提供CMSIS-RTOS接口函数给最终使用者调用。如此以来,最终使用者只需要学习CMSIS-ROTS即可,从而降低学习门槛。(不过,目前只有FREE-RTOS和RTX能够支持CMSIS-RTOS)。

CMSIS-RTOS 是实时操作系统的通用 API。它提供了标准化的编程接口,它只是封装了RTX/embos,以后还可能封装freeRTOS(已经封装了~~~),uc/os(好像也已经封装了)等等第三方OS。总的来说吧,用CMSIS提供的接口而不用FreeRTOS的接口的原因是CMSIS可以减少在不同实时操作系统开发之间的工作量,使得多平台之间的移植变得高效便捷。

目前的CMSIS-RTOS V2 版本的接口有如下特性:

支持Cortex-M 内存保护单元(MPU);
支持多处理器系统;
支持DMA 功能;
能够避免死锁,支持优先级反转。
支持多种操作系统。
3.关于开发环境STM32CubeIDE
STM32CubeIDE是ST公司推出的一个多功能的集成开发工具,集成了TrueSTUDIO和STM32CubeMX,它是STM32Cube软件生态系统的一部分。
利用这个开发环境我们只需要简单的图形界面配置就可以快速生成一些初始化代码,不需要重头写初始化代码了。(具体一些的STM32CubeIDE就不在这里细说了,大家可以在各种论坛找到stm322cube ide的安装方法和使用方法。)(其实最重要的是免费!免费!免费!懂了吧?)

先行必备知识

FreeRTOS知识
在操作系统中,任务是竞争系统资源的最小运行单元。FreeRTOS 是一个支持多任务的操作系统。在 FreeRTOS 中,任务可以使用或等待CPU、使用内存空间等系统资源,并独立于其它任务运行,任何数量的任务可以共享同一个优先级。
FreeRTOS可以创建多个任务,但是对于单核cpu来说,在任意给定时间,实际上只有一个任务被执行,这样就可以把任务分成2个状态,即运行状态和非运行状态。当任务处于运行状态时,处理器就执行该任务的代码。处于非运行态的任务,它的所有寄存器状态都保存在自己的任务堆栈中,当调度器将其恢复到运行态时,会从上一次离开运行态时正准备执行的那条指令开始执行。

在FreeRTOS中任务的特性
1.简单易用
2.没有使用限制
3.支持抢占、支持优先级
4.每个任务都有自己的堆栈,对RAM消耗较多

FreeRTOS的任务函数

void StartTask(void *argument)	//StartTask is the name of task
{
	//user's codes begin
	...
	//user's codes end
	for(;;)
	{
		//user's codes begin
		...
		//user's codes end
		osDelay(1);
	}
}


任务中包含了一个for 循环,也可以使用while循环,但是请注意FreeRTOS任务函数一般不允随意跳出

循环,如果要关闭任务要使用VTaskDelete()来删除任务。


关于任务的状态

在FreeRTOS 中,任务有五种状态:运行态、就绪态、阻塞态、挂起态和删除态。


运行态(Running):说明当前CPU 正在执行该任务,单核处理器任何时刻只有一个任务处于运行态。处于就绪态就说明该任务正在占用CPU。

就绪态(Ready):说明任务已经就绪,可以被任务调度器调用去执行,但是调度只会去执行目前优先级最高的任务。

阻塞态(Blocked):阻塞态的任务不会被任务调度器的调度,并且任务阻塞是有一定时间的,当超过时间结束会退出阻塞状态。处于阻塞状态的作用除了可以使任务处于暂停状态,还有一个作用是灵活调度,当任务处于阻塞状态,调度器就会去执行其它任务,提高效率。比如当某个任务调用TaskDelay 毫秒延时函数时,该任务就会阻塞,同时调度器自动运行其它处于就绪态的任务,等待延时结束时重新进入就绪态。

挂起态(Suspend):处于挂起态的任务同样不会被任务调度器调度,但是对比阻塞态它没有超时时间,当调用当明确的分别调用vTaskSuspend()和xTaskResume() 函数后,任务才会进入或退出挂起状态。

删除态:任务删除之后任务控制块TCB 会保留一段时间,等待内核检查和回收资源,任务也不能再被调度,此时任务处于删除态,由于任务也实际不存在,所以官方的文档中只描述了运行态、就绪态、阻塞态和挂起态这四个状态,并没有把删除态列入任务的状态中。


FreeRTOS任务管理调度的方法

为了满足处理器多任务并发进行的需求,需要通过系统调度来合理安排各个任务占有CPU的时间。任务管理和调度是RTOS的核心功能。

在一般的系统中,任务可以分为Running态和非Running态,而非Running态还可以再细分。(不懂请看任务的状态这部分)


FreeRTOS系统中,任务主要有以下两种调度方法:

抢占式调度:当有新的任务就绪(ready,且优先级大于等于当前任务的优先级时,当前任务就会被抢占;需要用户自己通过configUSE_PREEMPTION配置。


时间片调度:同处于ready态的最高优先级的任务会轮流运行固定的时间片;通过configUSE_TIME_SLICING配置,默认开启


关于任务优先级

FreeRTOS 中任务可以设置优先级,这种方式是全抢占式调度,同时任务优先级的个数可以由用户配置。任务的优先级由数字决定的,数字越大优先级越高,数字越小代表优先级越低。


可以通过FreeRTOSConfig.h 文件中的configMAX_PRIORITIES 宏去配置任务优先级,用户实际可以使用的优先级范围是0到configMAX_PRIORITIES–1。比如我们配置此宏定义为5,那么用户可以使用的优先级号是0,1,2,3,4。假如使能了 configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏(在 FreeRTOSConfig.h 文件定义),一般强制限定最大可用优先级数目为 32与此同时还有一个容易弄混的概念是中断优先级,中断优先级和任务优先级是没有任何关系的,中断优先级的数字越大代表优先级越低,数字越小代表优先级越高。中断的级别永远高于任务级别。当任务开始执行的时候中断发生,微处理器会立刻执行中断服务程序。


当FreeRTOSConfig.h 文件中的configUSE_TIME_SLICING 宏配置为1 或者没有配置此

宏的时候是开启时间片调度的,多个任务可以共用一个优先级,也就是处于相同优先级的任

务会轮流切换执行。当有更高的优先级处于就绪状态就会去处理更高优先级的任务。


在FreeRTOS 中,任务优先级个数可以由用户配置,可以通过FreeRTOSConfig.h文件中的configMAX_PRIORITIES 宏进行配置的,而CMSIS-RTOS 所提供的接口对freertos中的优先级个数已经定义,在CMSIS-RTOS V2 中的configMAX_PRIORITIES 为56,其中用户可用优先级个数为49个,有5个优先级保留。1 个ISR 延迟线程,该线程优先级最高,不能被用户使用。还有一个osPriorityNone优先级,该优先级是无优先级,未初始化,不能使用。

CMSIS中线程优先级的官方定义:

typedef enum {
  osPriorityNone          =  0,         ///< No priority (not initialized).
  osPriorityIdle          =  1,         ///< Reserved for Idle thread.
  										///< 2-7 Reserved								
  osPriorityLow           =  8,         ///< Priority: low
  osPriorityLow1          =  8+1,       ///< Priority: low + 1
  osPriorityLow2          =  8+2,       ///< Priority: low + 2
  osPriorityLow3          =  8+3,       ///< Priority: low + 3
  osPriorityLow4          =  8+4,       ///< Priority: low + 4
  osPriorityLow5          =  8+5,       ///< Priority: low + 5
  osPriorityLow6          =  8+6,       ///< Priority: low + 6
  osPriorityLow7          =  8+7,       ///< Priority: low + 7
  osPriorityBelowNormal   = 16,         ///< Priority: below normal
  osPriorityBelowNormal1  = 16+1,       ///< Priority: below normal + 1
  osPriorityBelowNormal2  = 16+2,       ///< Priority: below normal + 2
  osPriorityBelowNormal3  = 16+3,       ///< Priority: below normal + 3
  osPriorityBelowNormal4  = 16+4,       ///< Priority: below normal + 4
  osPriorityBelowNormal5  = 16+5,       ///< Priority: below normal + 5
  osPriorityBelowNormal6  = 16+6,       ///< Priority: below normal + 6
  osPriorityBelowNormal7  = 16+7,       ///< Priority: below normal + 7
  osPriorityNormal        = 24,         ///< Priority: normal
  osPriorityNormal1       = 24+1,       ///< Priority: normal + 1
  osPriorityNormal2       = 24+2,       ///< Priority: normal + 2
  osPriorityNormal3       = 24+3,       ///< Priority: normal + 3
  osPriorityNormal4       = 24+4,       ///< Priority: normal + 4
  osPriorityNormal5       = 24+5,       ///< Priority: normal + 5
  osPriorityNormal6       = 24+6,       ///< Priority: normal + 6
  osPriorityNormal7       = 24+7,       ///< Priority: normal + 7
  osPriorityAboveNormal   = 32,         ///< Priority: above normal
  osPriorityAboveNormal1  = 32+1,       ///< Priority: above normal + 1
  osPriorityAboveNormal2  = 32+2,       ///< Priority: above normal + 2
  osPriorityAboveNormal3  = 32+3,       ///< Priority: above normal + 3
  osPriorityAboveNormal4  = 32+4,       ///< Priority: above normal + 4
  osPriorityAboveNormal5  = 32+5,       ///< Priority: above normal + 5
  osPriorityAboveNormal6  = 32+6,       ///< Priority: above normal + 6
  osPriorityAboveNormal7  = 32+7,       ///< Priority: above normal + 7
  osPriorityHigh          = 40,         ///< Priority: high
  osPriorityHigh1         = 40+1,       ///< Priority: high + 1
  osPriorityHigh2         = 40+2,       ///< Priority: high + 2
  osPriorityHigh3         = 40+3,       ///< Priority: high + 3
  osPriorityHigh4         = 40+4,       ///< Priority: high + 4
  osPriorityHigh5         = 40+5,       ///< Priority: high + 5
  osPriorityHigh6         = 40+6,       ///< Priority: high + 6
  osPriorityHigh7         = 40+7,       ///< Priority: high + 7
  osPriorityRealtime      = 48,         ///< Priority: realtime
  osPriorityRealtime1     = 48+1,       ///< Priority: realtime + 1
  osPriorityRealtime2     = 48+2,       ///< Priority: realtime + 2
  osPriorityRealtime3     = 48+3,       ///< Priority: realtime + 3
  osPriorityRealtime4     = 48+4,       ///< Priority: realtime + 4
  osPriorityRealtime5     = 48+5,       ///< Priority: realtime + 5
  osPriorityRealtime6     = 48+6,       ///< Priority: realtime + 6
  osPriorityRealtime7     = 48+7,       ///< Priority: realtime + 7
  osPriorityISR           = 56,         ///< Reserved for ISR deferred thread.
  osPriorityError         = -1,         ///< System cannot determine priority or illegal priority.
  osPriorityReserved      = 0x7FFFFFFF  ///< Prevents enum down-size compiler optimization.
} osPriority_t;


FreeRTOS中的任务控制块

一个任务在创建的时候,会同时创建一个结构体TCB(Task Control Block),中文叫做任务控制块,这个任务控制块包含了任务的堆栈指针、任务名称、任务的形参等,任务调度器需要通过这个任务块结构体来操作该任务。任务控制块的结构体定义为tskTCB 类型,在task.c中可查找到该定义,最新的FreeRTOS 10.01 版本给这个结构体起别名为TCB_t,在新版本中两种类型别名都能用,旧版本的FreeRTOS 只能使用tskTCB。任务被创建后,任务默认状态为就绪态,在系统中等待调度。


typedef struct tskTaskControlBlock

{

volatile StackType_t *pxTopOfStack; //任务堆栈的栈顶

#if ( portUSING_MPU_WRAPPERS == 1 ) //通过宏来控制微处理器的相关设置

xMPU_SETTINGS xMPUSettings;

#endif

ListItem_t xStateListItem; //状态列表项

ListItem_t xEventListItem; //事件列表项

UBaseType_t uxPriority; //任务优先级

StackType_t *pxStack; //任务堆栈的起始地址

char pcTaskName[ configMAX_TASK_NAME_LEN ]; //任务名字

.... //部分省略,可以参考FreeRTOS 源码

}tskTCB;

typedef tskTCB TCB_t; //给tskTCB 类型起别名为TCB_t 类型


任务的堆栈空间

FreeRTOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场 (CPU寄存器值等 )保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场 ,恢复现场以后任务就会接着从上次中断的地方开始运行。FreeRTOS 中的每个任务都有一个堆栈空间,任务堆栈可以由系统提供,也可以由用户提供。当由系统提供时使用xTaskCreate()函数,此时需要将申请的任务堆栈的大小作为形参传入。当由用户自行提供的时候使用xTaskCreateStatic()函数,此时需要将用户提供的堆栈空间的地址作为形参传入。


任务函数的创建

在FreeRTOS中,任务创建可分为动态方法创建任务和静态方法创建任务。动态静态的区别主要在于堆栈由谁提供。

动态方法创建任务无需用户提供堆栈空间,系统能够自动分配,而静态方法创建用户需要用户先提供堆栈空间,而且两个函数的返回值类型并不相同。


BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,	//动态方式创建任务
						const char *const pcName,
						unsigned short usStackDepth,
						void *pvParameters,
						UBaseType_t uxPriority,
						TaskHandle_t *pxCreatedTask );
/**
*	pvTaskCode:任务入口函数
*	pcName:指向任务名字字符串的指针
*	usStackDepth:任务堆栈大小
*	pvParameters:任务函数的参数
*	uxPriority:任务的优先级
*	pxCreatedTask:任务控制块指针
**/
/**
返回值(return):
	pdPASS:任务创建成功。
	errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:创建任务
	失败,一般是系统空间没有足够的内存来分配任务的堆栈空间。
**/
TaskHandle_t xTaskCreateStatic( TaskFunction_t pvTaskCode,	//静态方式创建任务
								const char * const pcName,
								uint32_t ulStackDepth,
								void *pvParameters,
								UBaseType_t uxPriority,
								StackType_t * const puxStackBuffer,
								StaticTask_t * const pxTaskBuffer );
/**
*	pvTaskCode:任务入口函数
*	pcName:指向任务名字字符串的指针
*	usStackDepth:任务堆栈大小
*	pvParameters:任务函数的参数
*	uxPriority:任务的优先级
*	puxStackBuffer:指向任务堆栈的指针
**/
/**
返回值(return):
	创建失败:返回NULL
	创建成功:返回任务句柄。
/**


线程的创建

上面的函数是FreeRTOS自带的函数接口,但是之前说过为了减少不同实时操作系统之间移植的方便性我使用的是CMSIS上的软件接口创建任务。其实在CMSIS上,任务的创建更准确来说是线程的创建。
下面是有关线程管理的函数:

osThreadDetach();//分离线程(线程终止时可以回收线程存储
osThreadEnumerate();//枚举活动线程
osThreadExit();//终止当前正在运行的线程的执行
osThreadGetCount();//获取活动线程的数量
osThreadGetId();//返回当前正在运行的线程的线程ID
osThreadGetName();//获取线程的名称
osThreadGetPriority();//获取线程的当前优先级
osThreadGetStackSize();//获取线程的堆栈大小
osThreadGetStackSpace();//根据执行期间的堆栈水印记录获取线程的可用堆栈空间
osThreadGetState();//获取线程的当前线程状态
osThreadJoin();//等待指定线程终止
osThreadNew();//创建一个线程并将其添加到活动线程中。
osThreadResume();//恢复线程的执行。
osThreadSetPriority();//更改线程的优先级。
osThreadSuspend();//暂停执行线程。
osThreadTerminate();//终止线程的执行。
osThreadYield();//将控制权传递给处于就绪状态的下一个线程


CMSIS-RTOS 拥有四个状态:就绪态、运行态、阻塞态和终止态,它和FreeRTOS中的状态有所区别,CMSIS-RTOS 的阻塞态合并了FreeRTOS 中的挂起态和阻塞态,终止态对应的是FreeRTOS 中的删除态。注意在终止态时调用osThreadTerminate()函数,线程被终止,处于终止态的时候如果线程支持可连接属性资源并未被释放,当线程处于终止态的时候线程不仅会被终止,资源也会被释放。FreeRTOS 的CMSIS-RTOS 目前暂不支持可连接线程,所以当调用osThreadTerminate()时,如果线程是动态方式创建的,线程的资源同时也会被释放,使得线程处于终止态。


osThreadId_t osThreadNew(osThreadFunc_t func, void * argument, const osThreadAttr_t * attr );//线程创建函数
/**
功能:	该功能为创建一个新的的线程,并将该线程设置为就绪。使用参
		数指针* argument 传递线程函数的参数。当创建的线程函数的
		优先级高于当前运行的线程时,创建的线程立即执行。线程属性
		通过attr 控制,属性包括线程优先级,堆栈大小或内存分配的设置。
**/
/**
*	func:线程函数,即线程要实现的功能函数。
*	argument:线程函数的参数,以指针形式传入。
*	attr:控制线程的参数,为一个结构体指针
**/
/**
返回值(return):
	线程的ID,之后线程的其它操作都通过该ID来控制。
/**


CMSIS-RTOS 的接口把FreeRTOS 的任务创建函数xTaskCreate()和xTaskCreateStatic()封装成一个osThreadNew()函数,堆栈空间是否由用户自行提供可以通过控制osThreadNew()的第三个参数attr来实现(即是动态创建线程还是静态创建线程)。

attr结构体:

typedef struct {
	const char *name; //线程的名称
	uint32_t attr_bits; //线程的属性位,可以设置为以分离形式创建线程和可连接
						//方式创建线程,分别是osThreadDetached和osThreadJoinable
	void *cb_mem; //线程控制块指针,如果设置为NULL将从系统空间自动分配空间用于线程控制块
	uint32_t cb_size;//线程控制块所占内存的大小,注意仅在用户自行分配线程堆栈时配置,
					 //如果采用从系统空间也就是内存池分配线程堆栈时设置为0,默认值也是0
	void *stack_mem; //指向线程堆栈的指针,注意以64 位对齐,注意尽在用户自行分配线程堆栈
					 //时配置,如果采用从系统空间也就是内存池分配线程堆栈时设置为NULL,默
					 //认值也是NULL
	uint32_t stack_size;//线程堆栈的大小,以字节为单位,最好是4 的倍数
	osPriority_t priority; //线程的优先级
	TZ_ModuleId_t tz_module; //TrustZone 模块标识符
	uint32_t reserved; //保留的变量以备之后使用
}osThreadAttr_t;

在FreeRTOS中有两种创建线程的方法,一是动态创建线程,二是静态创建线程。


在使用静态方法创建线程时,必须先定义静态的线程控制块,并且定义好堆栈空间。采用这种方式,线程控制块和堆栈占用的内存会放在 RW/ZI段(详细请看ARM架构中RO段,RW段,ZI段的区别),这段空间在编译时就已经确定,它不是动态分配的,所以不能简单用osThreadTerminate()释放空间,只能使用它将该线程控制块从任务管理器中脱离。


使用动态方法创建线程时, 线程会动态申请线程控制块和堆栈空间。在编译时, 编译器是不会感知到这段空间的,只有在程序运行时, 线程才会从系统堆中申请分配这段内存空间,当不需要使用该线程时,在CMSIS提供的接口中,调用osThreadTerminate()函数就会将这段申请的内存空间重新释放到内存堆中。


这两种方式各有利弊,静态定义方式会占用 RW/ZI空间,但是不需要动态分配内存,运行时效率较高,实时性较好。 动态方式不会占用额外的 RW/ZI空间,占用空间小,但是运行时需要动态分配内存,效率没有静态方式高。 总的来说,这两种方式就是空间和时间效率的平衡,可以根据实际环境需求选择采用具体的分配方式。

动态创建线程:

int main(void)
{
	osKernelInitialize(); //初始化内核
	//结构体变量Task_attributes 存放线程相关配置信息
	osThreadAttr_t Task_attributes =
	{
		.name = "task_name", //线程的名称,注意并非是线程函数的名称
		.priority = (osPriority_t)osPriorityNormal, // 设置线程优先级为osPriorityNormal
		.stack_size = 128 //设置堆栈大小为128 字,即512 字节
	};
	osThreadNew(Task_name,NULL,&Task_attributes); //创建一个新的线程
	osKernelStart(); //开启内核调度
}
/*线程函数的实现*/
void Task_name(void *argument)
{
	//user's codes begin
	...
	//user's codes end
	for(;;)
	{
			//user's codes begin
			...
			//user's codes end
	}
}

静态创建线程:

void Task_name(void *argument); //声明线程函数
uint32_t myTaskBuffer[ 128 ]; //定义由用户提供的线程堆栈空间
int main(void)
{
osKernelInitialize(); //初始化内核
//结构体变量Task_attributes 存放线程配置信息
osThreadAttr_t Task_attributes =
{
.name = "task_name", //线程的名称,注意并非是线程函数的名称
.stack_mem = &myTaskBuffer[0], //用户提供堆栈空间的地址
.stack_size = sizeof(myTaskBuffer), //用户提供堆栈空间的大小,此处为
512 字节
.priority = (osPriority_t) osPriorityNormal, // 设置线程优先级为
osPriorityNormal
};
osThreadNew(Task_name,NULL,&Task_attributes); //创建一个新的线程
osKernelStart(); //开启内核调度
}
/*线程函数的实现*/
void Task_name(void *argument)
{
	//user's codes begin
	...
	//user's codes end
	for(;;)
	{
		//user's codes begin
		...
		//user's codes end
	}
}


总结

这次主要先行了解学习FreeRTOS的简单的知识点,FreeRTOS虽然小,但是内容挺丰富的,学无止境,一起加油。还有借鉴了许多大佬的文章,在此感谢各位大佬,就不一一列出来了,敬礼!(小白选手,有错的话各位大佬请见谅,麻烦各位告知一下这位可怜的小白ღ( ´・ᴗ・` )比心)





标签: #FreeRTOS# #STM32#

版权声明:
作者:931844854
链接:https://www.dianziwang.net/p/1e8fd26d60b319.html
来源:操作系统
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以点击 “举报”


登录 后发表评论
0条评论
还没有人评论过~