空闲任务与阻塞延时(笔记)

news2024/10/6 1:39:04

目录

    • 前言
  • 空闲任务
    • 实现空闲任务
      • 1、定义空闲任务栈
      • 2、定义空闲任务的任务控制块
      • 4、定义空闲任务主体
  • 实现阻塞延时
    • vTaskDelay()函数
    • 任务与空闲任务切换的例子:vTaskSwitchContext()函数
    • SysTick中断服务函数
      • 更新系统时基
    • SysTick初始化函数
    • 实验
    • 仿真

前言

软件延时是让CPU等待达到延时效果。
而RTOS的优势是可以充分发挥CPU的性能,永远不会让CPU闲着。
RTOS中的延时叫做阻塞延时

空闲任务

当没有其他任务可以运行时,RTOS会为CPU创建一个空闲任务,然后CPU去执行。
在RTOS中,空闲任务是系统在调度器创建的优先级最低的任务,空闲任务主体是主要做一些系统内存的清理工作。

实现空闲任务

1、定义空闲任务栈

在min.c 中定义:

/* 获取空闲任务的内存 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;

在FeeRTOSConfig.h中定义的宏

#define configMINIMAL_STACK_SIZE	( ( unsigned short ) 128 )//(字,即512个字节)

2、定义空闲任务的任务控制块

定义空闲任务的任务控制块

TCB_t IdleTaskTCB;

3## 3、定义空闲函数主体
在mask.c 中定义:

#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )//在protmacro.h
static portTASK_FUNCTION( prvIdleTask, pvParameters ) //prvIdleTask( void *pvParameters )
{
	/* 防止编译器的警告 */
	( void ) pvParameters;
    
    for(;;)
    {
        /* 空闲任务暂时什么都不做 */
    }
}

4、定义空闲任务主体

extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize );
void vTaskStartScheduler( void )
{
/*======================================创建空闲任务start==============================================*/     
    TCB_t *pxIdleTaskTCBBuffer = NULL;               /* 用于指向空闲任务控制块 */
    StackType_t *pxIdleTaskStackBuffer = NULL;       /* 用于空闲任务栈起始地址 */
    uint32_t ulIdleTaskStackSize;
    
    /* 获取空闲任务的内存:任务栈和任务TCB */
    vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, 
                                   &pxIdleTaskStackBuffer, 
                                   &ulIdleTaskStackSize );    
    
    xIdleTaskHandle = xTaskCreateStatic( (TaskFunction_t)prvIdleTask,              /* 任务入口 */
					                     (char *)"IDLE",                           /* 任务名称,字符串形式 */
					                     (uint32_t)ulIdleTaskStackSize ,           /* 任务栈大小,单位为字 */
					                     (void *) NULL,                            /* 任务形参 */
					                     (StackType_t *)pxIdleTaskStackBuffer,     /* 任务栈起始地址 */
					                     (TCB_t *)pxIdleTaskTCBBuffer );           /* 任务控制块 */
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[0] ), &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
/*======================================创建空闲任务end================================================*/
                                         
    /* 手动指定第一个运行的任务 */
    pxCurrentTCB = &Task1TCB;
                                         
    /* 初始化系统时基计数器 */
    xTickCount = ( TickType_t ) 0U;
    
    /* 启动调度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 调度器启动成功,则不会返回,即不会来到这里 */
    }
}

vApplicationGetIdleTaskMemory()函数需要用户自己实现:
在main.c中

void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize )
{
		*ppxIdleTaskTCBBuffer=&IdleTaskTCB;//任务控制块
		*ppxIdleTaskStackBuffer=IdleTaskStack; //任务栈的起始地址
		*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;//任务栈的大小
}

其中,prvIdleTask在task.c中定义的空闲任务的任务句柄

static TaskHandle_t xIdleTaskHandle					= NULL;//实现空闲任务的任务句柄
  1. 空闲任务只有在CPU空闲时才会执行。
  2. 空闲任务优先级最低是不能被阻塞的。
  3. 空闲任务主要做内存清理等辅助性工作如检查变量等。

实现阻塞延时

阻塞延时是指任务调用该延时函数后,任务会被剥夺CPU的使用权,然后进入阻塞状态,直到延时结束,任务重新获得CPU使用权才继续执行任务。

在FreeRTOS.h中的任务控制块定义中,增加了一个成员,用于记录需要延时的时间

typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 栈顶 */

	ListItem_t			    xStateListItem;   /* 任务节点 */
    
    StackType_t             *pxStack;         /* 任务栈起始地址 */
	                                          /* 任务名称,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];

    TickType_t xTicksToDelay; /* 用于延时 */    
} tskTCB;
typedef tskTCB TCB_t;

vTaskDelay()函数

void vTaskDelay( const TickType_t xTicksToDelay )
{
    TCB_t *pxTCB = NULL;
    
    /* 获取当前任务的TCB */
    pxTCB = pxCurrentTCB;
    
    /* 设置延时时间 */
    pxTCB->xTicksToDelay = xTicksToDelay;
    
    /* 任务切换 */
    taskYIELD();
}

任务与空闲任务切换的例子:vTaskSwitchContext()函数

思路:
如果当前是执行的任务是空闲任务就去尝试执行任务1和任务2,看看这两个任务的延时时间是否结束,如果没有到期,返回继续执行空闲函数。
如果当前是执行的任务是任务1或者任务2,检查一下下一个任务,如果另一个任务不在延时中,就切换到该任务。否则,判断当前任务是否应该进入延时状态,如果是就切换到空闲任务,否则就不做任务切换。

void vTaskSwitchContext( void )
{
	/* 如果当前线程是空闲线程,那么就去尝试执行线程1或者线程2,
       看看他们的延时时间是否结束,如果线程的延时时间均没有到期,
       那就返回继续执行空闲线程 */
	if( pxCurrentTCB == &IdleTaskTCB )
	{
		if(Task1TCB.xTicksToDelay == 0)
		{            
            pxCurrentTCB =&Task1TCB;
		}
		else if(Task2TCB.xTicksToDelay == 0)
		{
            pxCurrentTCB =&Task2TCB;
		}
		else
		{
			return;		/* 线程延时均没有到期则返回,继续执行空闲线程 */
		} 
	}
	else
	{
		/*如果当前线程是线程1或者线程2的话,检查下另外一个线程,如果另外的线程不在延时中,就切换到该线程
        否则,判断下当前线程是否应该进入延时状态,如果是的话,就切换到空闲线程。否则就不进行任何切换 */
		if(pxCurrentTCB == &Task1TCB)
		{
			if(Task2TCB.xTicksToDelay == 0)
			{
                pxCurrentTCB =&Task2TCB;
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)
			{
                pxCurrentTCB = &IdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
		else if(pxCurrentTCB == &Task2TCB)
		{
			if(Task1TCB.xTicksToDelay == 0)
			{
                pxCurrentTCB =&Task1TCB;
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)
			{
                pxCurrentTCB = &IdleTaskTCB;
			}
			else 
			{
				return;		/* 返回,不进行切换,因为两个线程都处于延时中 */
			}
		}
	}
}

SysTick中断服务函数

如果一个任务需要延时,一开始xTicksToDelay肯定不为0,当xTicksToDelay变为0时,表示延时结束。
但是,存在一个问题,xTicksToDelay是以什么周期递减,在哪里递减的。

在FreeRTOS中,这个周期由SysTick中断提供,操作系统中最小的时间单位就是SysTick的中断周期,我们称之为一个tick。
SysTick中断服务函数在port.c中实现。

void xPortSysTickHandler( void )
{
	/* 关中断 */
    vPortRaiseBASEPRI();
    
    /* 更新系统时基 */
    xTaskIncrementTick();

	/* 开中断 */
    vPortClearBASEPRIFromISR();
}

更新系统时基

void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;
    
    /* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
    const TickType_t xConstTickCount = xTickCount + 1;
    xTickCount = xConstTickCount;

    
    /* 扫描就绪列表中所有线程的xTicksToDelay,如果不为0,则减1 */
	for(i=0; i<configMAX_PRIORITIES; i++)
	{
        pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
		if(pxTCB->xTicksToDelay > 0)
		{
			pxTCB->xTicksToDelay --;
		}
	}
    
    /* 任务切换 */
    portYIELD();
}

SysTick初始化函数

SysTick的中断服务函数想要被顺利执行,则SysTick必须先初始化。SysTick的初始化函数vPortSetupTimerInterrupt()在port.c中定义

void vPortSetupTimerInterrupt( void )
{
     /* 设置重装载寄存器的值 */
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    
    /* 设置系统定时器的时钟等于内核时钟
       使能SysTick 定时器中断
       使能SysTick 定时器 */
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | 
                                  portNVIC_SYSTICK_INT_BIT |
                                  portNVIC_SYSTICK_ENABLE_BIT ); 
}

configSYSTICK_CLOCK_HZ 为系统时钟,port.c中被重定义

#ifndef configSYSTICK_CLOCK_HZ
	#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
	/* 确保SysTick的时钟与内核时钟一致 */
	#define portNVIC_SYSTICK_CLK_BIT	( 1UL << 2UL )
#else
	#define portNVIC_SYSTICK_CLK_BIT	( 0 )
#endif

其中,configCPU_CLOCK_HZ的原始定义在FreeRTOSconfig.h中定义

#define configCPU_CLOCK_HZ			( ( unsigned long ) 25000000 )	

configTICK_RATE_HZ 为系统时基,在FreeRTOSconfig.h中定义

#define configTICK_RATE_HZ			( ( TickType_t ) 100 )

这里系统时钟频率为25Mhz,即1s有25000000Tick,那么1ms就有25000Tick。而每个周期中断100次,中断周期为25Mhz/100=250000Tick,因此中断一次为250000Tick/25000Tick=10ms.

可以参考《STM32F10xxx Cortex-M3 programming manual》手册,在port.c中定义为偏移:

#define portNVIC_SYSTICK_INT_BIT			( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT			( 1UL << 0UL )

实验

把软件延时替换时钟延时

#include "FreeRTOS.h"
#include "task.h"

extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE                    128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;

TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE                    128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;

void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );

int main(void)
{	
    /* 硬件初始化 */
	/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
    
    /* 初始化与任务相关的列表,如就绪列表 */
    prvInitialiseTaskLists();
    
    /* 创建任务 */
    Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry,   /* 任务入口 */
					                  (char *)"Task1",               /* 任务名称,字符串形式 */
					                  (uint32_t)TASK1_STACK_SIZE ,   /* 任务栈大小,单位为字 */
					                  (void *) NULL,                 /* 任务形参 */
					                  (StackType_t *)Task1Stack,     /* 任务栈起始地址 */
					                  (TCB_t *)&Task1TCB );   /* 任务控制块 */
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
                                
    Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry,   /* 任务入口 */
					                  (char *)"Task2",               /* 任务名称,字符串形式 */
					                  (uint32_t)TASK2_STACK_SIZE ,   /* 任务栈大小,单位为字 */
					                  (void *) NULL,                 /* 任务形参 */
					                  (StackType_t *)Task2Stack,     /* 任务栈起始地址 */
					                  (TCB_t *)&Task2TCB );   /* 任务控制块 */
    /* 将任务添加到就绪列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
                                      
    /* 启动调度器,开始多任务调度,启动成功则不返回 */
    vTaskStartScheduler();                                      
    
    for(;;)
	{
		/* 系统启动成功不会到达这里 */
	}
}

/* 软件延时 */
void delay (uint32_t count)
{
	for(; count!=0; count--);
}
/* 任务1 */
void Task1_Entry( void *p_arg )
{
	for( ;; )
	{
#if 0        
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
		
		/* 线程切换,这里是手动切换 */
        portYIELD();
#else
		flag1 = 1;
        vTaskDelay( 2 ); //延时20ms
		flag1 = 0;
        vTaskDelay( 2 );
#endif        
	}
}

/* 任务2 */
void Task2_Entry( void *p_arg )
{
	for( ;; )
	{
#if 0        
		flag2 = 1;
		delay( 100 );		
		flag2 = 0;
		delay( 100 );
		
		/* 线程切换,这里是手动切换 */
        portYIELD();
#else
		flag2 = 1;
        vTaskDelay( 2 );		
		flag2 = 0;
        vTaskDelay( 2 );
#endif        
	}
}

/* 获取空闲任务的内存 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize )
{
		*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
		*ppxIdleTaskStackBuffer=IdleTaskStack; 
		*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}

仿真

在这里插入图片描述
宏观上看几乎是同时运行的

从微观上看
在这里插入图片描述
在这里插入图片描述
高低电平是几近20ms的,实验结果与代码相符。


学习于《FreeRTOS内核实现与应用开发实战指南–基于stm32》
b站视频地址:https://www.bilibili.com/video/BV1Jx411X7NS/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/494822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

牛客网专项练习Pytnon分析库(二)

1.Z-score标准化公式&#xff0c;,中的σ表示的是什么&#xff08;C&#xff09;。 A.总体平均值 B.数据的方差 C.数据的标准差 D.数据的众数 解析&#xff1a; Z-score标准化也叫标准差标准化法&#xff0c;其中X表示数据样本值&#xff0c;μ表示数据样本的平均值&#x…

婚姻的本质,不是爱情

婚姻的本质&#xff0c;不是爱情 结婚是为了爱情么&#xff1f;普通人或许以为是&#xff0c;但实际并不是。如果你是为了爱情&#xff0c;那你不需要结婚。什么叫爱情。所谓爱情&#xff0c;就是你对她朝思暮想&#xff0c;时时刻刻都想和她在一起。而她也对你朝思暮想&#…

Vue学习笔记1 - Vue是什么?

1&#xff0c;Vue概念 官网上&#xff08;简介 | Vue.js&#xff09; 上说&#xff0c; Vue (发音为 /vjuː/&#xff0c;类似 view) 是一款用于构建用户界面的 JavaScript 框架。 这个还好理解&#xff0c;就是说它是一款前端框架&#xff0c;用于构建 前端界面的。 但是它…

NewBing 还无法访问的几个问题

大部分的AI自媒体都在说&#xff0c;Bing new已经向全世界开放了&#xff0c;我也凑一下这个热闹&#xff0c;用Edge浏览器打开&#xff0c;访问https://www.bing.com/new?ccus 想体验一下Bing new的效果&#xff0c;结果如下&#xff1a; 相信很多人都碰到了这个问题 此体验…

Windows上使用CLion配置OpenCV环境,CMake下载,OpenCV的编译,亲测可用的方法(一)

一、Windows上使用CLion配置OpenCV环境,亲测可用的方法: Windows上使用CLion配置OpenCV环境 教程里的配置: widnows 10 clion 2022.1.1 mingw 8.1.0 opencv 4.5.5 Cmake3.21.1 我自己的配置: widnows 10 clion 2022.2.5 mingw 8.1.0 https://sourceforge.net/projects/min…

二十三种设计模式第三篇--抽象工厂模式

介绍 抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是围绕一个超级工厂创建其他工厂&#xff0c;该超级工厂又称为其他工厂的工厂。 这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 在抽象工厂模式中&#xff0c;接口是负责…

对标世界一流|亚马逊供应链管理经验借鉴

当前电商零售行业竞争日趋激烈&#xff0c;服务标准的提升、产品价格的竞争力等因素&#xff0c;导致企业经营成本持续上升&#xff0c;供应链的管理水平已经成为零售行业成败的关键。然而在电商零售行业的红海竞争中&#xff0c;亚马逊却始终保持着高速增长的态势&#xff0c;…

港联证券|4连板的AI+传媒概念股火了,近5亿资金抢筹

今天&#xff0c;沪深两市共51股涨停&#xff0c;除掉10只ST股&#xff0c;合计41股涨停。别的&#xff0c;11股封板未遂&#xff0c;全体封板率为81%。 涨停战场&#xff1a;长江传媒封单量最高 从收盘涨停板封单量来看&#xff0c;长江传媒封单量最高&#xff0c;有39.96万手…

STL初识

什么是STL? 菜鸟教程的解释是&#xff1a;C STL&#xff08;标准模板库&#xff09;是一套功能强大的 C 模板类&#xff0c;提供了通用的模板类和函数&#xff0c;这些模板类和函数可以实现多种流行和常用的算法和数据结构&#xff0c;如向量、链表、队列、栈。 也就是说&am…

4.1 数据结构引入

目录 什么是数据结构 语言出生顺序 基本概念 数据的逻辑结构 数据的存储结构 顺序储存 链式存储 索引存储 散列存储 基本概念 第一卷《基本算法》 第二卷《半数字化算法》 第三卷《排序与搜索》 第四卷《组合算法》 什么是数据结构 数据结构研究计算机数据间关系…

每日学术速递5.5

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CL 1.ResiDual: Transformer with Dual Residual Connections 标题&#xff1a;ResiDual&#xff1a;具有双剩余连接的Transformer 作者&#xff1a;Shufang Xie, Huishuai Zhang, Jun…

制造企业选择库存管理条码工具需要关注哪些点?

Dynamsoft Barcode Reader SDK 一款多功能的条码读取控件&#xff0c;只需要几行代码就可以将条码读取功能嵌入到Web或桌面应用程序。这可以节省数月的开发时间和成本。能支持多种图像文件格式以及从摄像机或扫描仪获取的DIB格式。使用Dynamsoft Barcode Reader SDK&#xff0c…

OpenCV实战(22)——单应性及其应用

OpenCV实战&#xff08;22&#xff09;——单应性及其应用 0. 前言1. 单应性1.1 单应性基础1.2 计算两个图像之间的单应性1.3 完整代码 2. 检测图像中的平面目标2.1 特征匹配2.2 完整代码 小结系列链接 0. 前言 我们已经学习了如何从一组匹配项中计算图像对的基本矩阵。在射影…

读论文《大气压等离子体电离波沿介质管传输特性研究》

文章目录 一、研究背景和意义二、研究目的与内容三、电离波概述3.1 电离波与传统的流注放电3.2 电离波传输速度的计算方法 四、放电参数对电离波传输特性的影响4.1 施加电压与电压波形对电离波传输的影响4.1.1 交流高压对电离波的影响4.1.2 脉冲高压对电离波的影响![在这里插入…

《编程思维与实践》1047.Base64编码

《编程思维与实践》1047.Base64编码 题目 思路 直接模拟:将每个Base64编码值都分为两部分:前半部分由上一个字符求得,后半部分由下一个字符求得. 特别地,如果字符为第一个或最后一个,则直接可以求得Base64编码. 如下图: 其中,% 2 n 2^n 2n表示取出后n位的二进制位, 这是因…

专业游戏录屏软件Camtasia 2023强悍来袭,Camtasia Studio 2023的新增功能!

Camtasia Studio 2023是一款专门录制屏幕动作的工具&#xff0c;它能在任何颜色模式下轻松地记录 屏幕动作&#xff0c;包括影像、音效、鼠标移动轨迹、解说声音等等&#xff0c;另外&#xff0c;它还具有即时播放和编 辑压缩的功能&#xff0c;可对视频片段进行剪接、添加转场…

又一起数据泄露事件五个月内的第二次

据报道&#xff0c;T-Mobile 在发现攻击者从 2023 年 2 月下旬开始的一个多月内访问了数百名客户的个人信息后&#xff0c;披露了 2023 年的第二次数据泄露事件。 与之前报告的数据泄露事件&#xff08;最近一次影响了 3700 万人&#xff09;相比&#xff0c;此次事件仅影响了…

Linux一学就会——编写自己的shell

编写自己的shell 进程程序替换 替换原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行…

Node.js 是什么?

简介 Node.js入门指南&#xff0c;服务器端JavaScript运行时环境。Node.js是在Google Chrome V8 JavaScript引擎的基础上构建的&#xff0c;它主要用于创建web服务器&#xff0c;但并不局限于此。 实际上Node.js 是把运行在浏览器中的js引擎抽离处理&#xff0c;进行再次封装…

MagicaCloth2安装教程

您可访问官网查看详情&#xff1b; MagicaSoft Unity Assets – Magica Soft 也可通过我的资源文件获得此插件的详细教程&#xff1a; (19条消息) UnityMagicaCloth2插件中文文档&#xff08;机翻/部分&#xff09;-Unity3D文档类资源-CSDN文库 MagicaCloth2是基于ECS开发的…