硬汉嵌入式论坛

 找回密码
 立即注册
查看: 5597|回复: 1
收起左侧

[μCOS-III] 【uCOS-III教程】第3章  基于时间触发的混合式调度器

[复制链接]

740

主题

1326

回帖

3546

积分

管理员

春暖花开

Rank: 9Rank: 9Rank: 9

积分
3546
QQ
发表于 2014-12-10 14:13:17 | 显示全部楼层 |阅读模式
第3章  基于时间触发的混合式调度器

         本期教程带领大家学习混合式调度器的设计和使用方法,有了第2期合作式调度器的基础,本期教程的混合式调度器也相对比较容易。
        3.1 抢占式调度器介绍
        3.2混合式调度器介绍
        3.3混合式调度器设计
        3.4实验说明
        3.5实验总结
3.1  抢占式调度器介绍
        在上期教程,我们简单的介绍了抢占式调度器,本期教程再做进一步的讨论,主要集中于抢占式调度器的优缺点的讨论,让大家对抢占式调度器有一个更加深入的了解。
        先来说一下抢占式调度器的特性(来自时间触发嵌入式模式这本书,这里说的特性和现在常用的小型RTOS有些区别),进而引出我们要讨论的问题:

抢占式调度器
  l  抢占式调度器提供了一种多任务的系统结构
操作:
  l  任务在特定的时刻被调度运行(以周期性或者单次方式)
  l  当任务需要运行的时候,被添加到等待队列。
  l  等待的任务(如果有的话)运行一段固定的时间,如果没有完成,将被放回到等待队列。然后下一个等待任务将会运行一段固定的时间,以此类推。
实现:
  l  这种调度器相对复杂,访问共享资源时,要防止冲突。
  l  该调度器必须为强占任务的所有中间状态分配存储器。
  l  该调度器通常将(至少部分)由汇编语言编写。
  l  该调度器通常作为一个独立的系统被创建。
性能:
  l  对外部事件的响应速度快
可靠性和安全性:
  l  与合作式任务相比,通常认为更不可预测,并且可靠性低。
        使用抢占式调度器最大的好处是对外部事件的快速响应,提高系统的实时性。带来好处的同时也是要付出代价的,下面就举一个例子,这个例子比较典型。
        假设系统中的一个任务要从ADC端口读取模拟信号,这时发生了上下文切换,另一个任务也要访问这个端口,在这种情况下如果不采取措施阻止这种情况,数据将可能丢失或者破坏。这种问题往往出现在多任务平台中所谓的“临界段”,这些代码段一旦开始就必须不中断地运行,直到跑完这段代码。临界段还有很多,比如:
  l  读取或者修改变量(特别是任务间通信的全局变量)的代码,一般来说这时最常见的关键代码。
  l  如果读过μCOS-II或μCOS-III源代码的话,会发现里面很多地方都做了开关中断的处理,就是为了防止多任务造成错误。
  l  调用公共函数的代码,特别是不可重入的函数,如果多个任务都访问这个函数,结果是可想而知的。
解决这种情况最常用的的办法就是在临界段做开关中断处理,这种情况是最影响实时性的,如果关中断期间发生了外部中断事件,那么这个中断事件只有临界段执行完之后才能得到相应。还有一种解决办法就是使用调度锁,这样在一定程度上能够提供任务的实时性,在发生外部中断的时候会得到相应,但是不会发生任务切换,必须等到调度锁被解开。
        关于抢占式调度器和合作式调度器到底谁好,在嵌入式领域没有一个定论,要根据自己的时间应用选择,很多嵌入式领域的专家都发表过这方面的文章,有兴趣的可以找找深入研究下,下面是Michael J. Pont的观点,原话如下:
Finally,it should be noted that the reasons why pre-emptive schedulershave been more widely discussed and used may not be for technical reasons atall: in fact, the use of pre-emptive environments can be seen to have clearcommercial advantages for some companies.For example, a co-operative schedulermay be easily constructed, entirely in a high-level programming language,inaround 300 lines of ‘C’ code, as we demonstrate in Chapter 9. The code ishighly portable, easy to understand and to use and is, in effect, freelyavailable. By contrast, the increased complexity of a preemptive operatingenvironment results in a much larger code framework (some ten times the size,even in a simple implementation: Labrosse, 1998). The size and complexity ofthis code makes it unsuitable for ‘in-house’ construction in most situations andtherefore provides the basis for a commercial ‘RTOS’ products to be sold,generally at high prices and often with expensive run-time royalties to bepaid. The continued promotion and sale of such environments has, in turn,prompted further academic interest in this area.
       上面这段话的意思是说,抢占式调度器被广泛的讨论和使用的原因可能并不完全是技术上的问题,实际上对于一些公司来说,使用抢占式平台有明显的商业上的好处,这个该怎么理解呢?比如我们做了一款产品,产品的系统是用的μCOS-II。我们肯定会在说明书中宣称我们使用的是μCOS-II系统,因为这个系统已经得到了业界的认可,而不会使用我们自己的设计的系统,用户在购买产品的时候肯定会首选用μCOS-II设计的系统。如果使用合作式调度器,只需要300行左右的C代码就可以实现一个合作式调度器,这些代码的可移植性非常好,易于理解和使用,并且实际上是免费使用的。相比而言,抢占式运行环境增加了复杂性,从而导致更复杂的代码结构。在大多数情况下,这些代码的复杂性和长度使其不适合于普通开发人员自己构建,因此提供了商业实时操作系统的产品的销售基础。抢占式调度器通常价格比较贵,而且还需占用很大的实时开销。这些平台持续不断的宣称和销售反过来又促进了这个领域的更进一步的学术研究。
        关于抢占式调度器就跟大家讲这么多,更深入的东西我们会在讲解μCOS-III源码的时候再跟大家分析。
3.2  混合式调度器介绍
        合作式调度器为各种嵌入式系统提供了一个可预测的平台。在一些场合可能需要在合作式调度器结构中加入抢占式调度器的一些特性,并小心的加以控制,混合式调度器应运而生。
        混合式调度器综合了合作式调度器和抢占式调度器的特性,而且这种方式始终是受控的,不依赖复杂的上下文切换程序,以及任务间复杂的通信机制。下面是混合式调度器的特性:

混合式调度器
  l  混合式调度器提供了有限的多任务处理能力。
操作:
  l  支持多个合作式调度的任务。
  l  支持一个抢占式任务(可以中断合作式任务)。
实现:
  l  这种调度器很简单,用少量代码即可实现。
  l  该调度器必须同时为两个任务分配存储器。
  l  该调度器通常完全由高级语言(比如”C”实现)。
  l  该调度器不是一种独立的系统,它成为开发人员的代码的一部分。
性能:
  l  对外部事件的响应速度快
可靠性和安全性:
  l  只要小心设计可以和纯碎的合作式调度器一样可靠。

   这里所描述的混合式调度器和合作式调度器是有区别的,区别如下:
  l  不再要求所有任务都在时标间隔之间完成任务。一个(或者更多)的合作式任务的运行时间可以大于时标间隔。
  l  与前面讨论的合作式调度器一样,可以调度任意个合作式任务。然而,还可以同时调度一个抢占式任务。
  l  抢占式任务可以抢先(中断)合作式任务。
  l  一旦抢占式任务开始运行,将一直运行到完成。
  同时注意:
  l  与完全的抢占式解决方案相比,只有一个抢占式任务,而且该任务连续运行直到完成,这将及大地简化系统的结构。尤其是不需要实现上下文切换的机制。这意味着:1. 该结构仍然十分简单。2. 运行环境可以全部由C实现。
  l  和完全的抢占式调度器相比,简化了任务间的通信。
  l  应该只有一个短任务(最长的运行时间大于为时标间隔的%50,尽可能的短)可以强占运行,否则将削弱系统的整体性能。
      
         下面详细讲一下混合式调度器的设计,这样大家会有一个更好的认识。
3.3  混合式调度器设计
        上期教程我们讲解了合作式调度器设计,只要稍作修改就可以改为混合式调度器。可以看到,很多设计人员使用混合式设计仅仅是因为它很容易实现,这通常是错误的。因为在混合式调度器中已经没有了合作式的调度特性,这可能对设计过程和以及最终系统的可靠性带来深远的影响,在使用混合式调度器的时候,大家要特别注意这点。
混合式调度器主要由以下几部分组成:
  l  调度器数据结构
  l  初始化函数
  l  嘀嗒定时器中断,用来以一定的时间间隔刷新调度器
  l  向调度器增加任务函数
  l  使任务在应当运行的时候被执行的调度函数
  l  从调度器删除任务的函数(此功能未做)
下面就把这几方面的设计详细讲述下。

3.3.1      调度器数据结构
调度器的数据结构如下:
typedef unsigned char    tByte;
typedef unsigned int     tWord;
typedef  struct
{
         void (*pTask)();                   /*指向任务的指针必须是一个*void(void)*函数;*/
         tWord Delay;                    /*延时(时标)知道下一个函数的运行*/
         tWord Period;                               /*连续运行之间的间隔*/
         tByte RunMe;                               /*当任务需要运行的时候由调度器加1*/
     tByte Co_op;                                 /*混合式调度和合作式调度的区分 合作式1 抢占式0*/
}sTask;

任务队列的大小通过下面进行定义:
#define        SCH_MAX_TASKS     5
sTask SCH_task_G[SCH_MAX_TASKS];  /*建立的任务数*/

建立任务的时候要注意不能超过这个最大任务数。
3.3.2      初始化函数
这里的初始化函数主要是指滴答定时器的初始化,以此来产生调度器所需要的时标。一般情况下把时
标间隔都设置为1ms。关于嘀嗒定时器方面的知识已经在前面第10章: SysTick实验有详细的讲解,这里就不再赘述了,只把初始化函数贴出来:
/*
*******************************************************************************************
*       : bsp_InitTimer
*       功能说明: 配置systick中断,并初始化软件定时器变量
*           :  
*       :
*******************************************************************************************
*/
void bsp_InitTimer(void)
{
         uint8_t i;
         /* 清零所有的软件定时器 */
         for (i = 0; i <TMR_COUNT; i++)
         {
                   s_tTmr.Count =0;
                   s_tTmr.PreLoad= 0;
                   s_tTmr.Flag =0;
                   s_tTmr.Mode =TMR_ONCE_MODE;        /* 缺省是1次性工作模式 */
         }
         /*
                   配置systic中断周期为1ms,并启动systick中断。
     SystemCoreClock是固件中定义的系统内核时钟,对于STM32F4XX,一般为 168MHz
     SysTick_Config()函数的形参表示内核时钟多少个周期后触发一次Systick定时中断.
              --SystemCoreClock / 1000  表示定时频率为 1000Hz 也就是定时周期为  1ms
              --SystemCoreClock / 500   表示定时频率为 500Hz  也就是定时周期为  2ms
              --SystemCoreClock / 2000  表示定时频率为 2000Hz 也就是定时周期为  500us
    对于常规的应用,我们一般取定时周期1ms。对于低速CPU或者低功耗应用,可以设置定时周期为 10ms
    */
         SysTick_Config(SystemCoreClock/ 1000);
}     


3.3.3      刷新函数
刷新函数的主要功能是:每个时标中断执行一次,在嘀嗒定时器中断里面执行。当刷新函数确定某个合作式任务要执行的时候,将这个任务结构体的成员RunMe加1,要注意的是刷新任务不执行任何合作式任务,这里只是设置 一下RunMe标志,由调度函数根据此标志执行相应任务。抢占式任务需要执行的话,立即就会得到执行。
        下面是具体的函数代码:
/*
*********************************************************************************************************
*       :hSCH_Update(void)
*       功能说明: 调度器的刷新函数,每个时标中断执行一次。在嘀嗒定时器中断里面执行。
*                           当刷新函数确定某个任务要执行的时候,将RunMe1,要注意的是刷新任务
*                           不执行任何函数,需要运行的任务有调度函数激活。
*                           抢占式任务需要执行的话,立即就会得到执行。
*           参:无
*       :
*********************************************************************************************************
*/
void hSCH_Update(void)
{
         tByte index;
         /*注意计数单位是时标,不是毫秒*/
         for(index = 0; index <hSCH_MAX_TASKS; index++)
         {
                   if(hSCH_task_G[index].pTask)                                     /*检测这里是否有任务*/
                   {
                            if(hSCH_task_G[index].Delay== 0)
                            {
                                     if(hSCH_task_G[index].Co_op)
                                     {
                                               hSCH_task_G[index].RunMe+= 1;  /*合作式任务需要运行 RunMe1*/         
                                     }      
                                     else
                                     {
                                               (*hSCH_task_G[index].pTask)();   /*抢占式任务需要立即运行 RunMe1*/   
                                               //hSCH_task_G[index].RunMe-= 1;
                                               if(hSCH_task_G[index].Period== 0)
                                               {                                                             /*单次执行的任务 则将其清除*/
                                                        hSCH_task_G[index].pTask= 0;                                                                              
                                               }
                                     }
                                     if(hSCH_task_G[index].Period)
                                     {                                                                 /*调度周期性的任务再次执行*/
                                               hSCH_task_G[index].Delay= hSCH_task_G[index].Period;                                                                                 
                                     }
                            }
                            else
                            {                                                                             /*还有准备好运行*/
                                     hSCH_task_G[index].Delay-= 1;
                                                                                                                                                                                 
                            }
                   }                 
         }
}


3.3.4      添加任务函数
添加任务函数的主要功能是将任务添加到任务队列上,下面主要是说一下这个函数中参数的功能:
tByte hSCH_Add_Task(void (*pFuntion)(void),
                                                          tWord DELAY,
                                                          tWord PERIOD,
                                                          tByte Co_op)
1.       void (*pFuntion)(void) :表示函数的地址,也就是将函数名填进去就行了。
2.       DELAY :表示函数第一次运行需要等待的时间。
3.       PERIOD:表示函数周期性执行的时间间隔
4.       Co_op:混合式调度和合作式调度的区分 合作式用1表示 抢占式用0表示

举四个例子说明一下:
  l  hSCH_Add_Task(DOTASK,1000, 0, 0)

              抢占式任务,DOTASK是函数的运行地址,1000是1000个时标以后开始运行,只运行一次。
  l  hSCH_Add_Task(DOTASK,0, 1000,0)
              抢占式任务,DOTASK是函数的运行地址,每个1000个时标周期性的运行一次。
  l  hSCH_Add_Task(DOTASK,300, 1000, 1)
               合作式任务,DOTASK是函数的运行地址,300是300个时标以后开始运行,以后就是每1000个时标周期运行一次,也就是说运行时刻是300,1300,2300,3300,4300等等。
  l  Task_ID= hSCH_Add_Task(DOTASK,1000,0);
             Task_ID是任务标示符,将任务标示符保存以便以后删除任务。
下面是函数的源代码:
/*
*********************************************************************************************************
*       : hSCH_Add_Task
*       功能说明: 添加任务。
*           参:void(*pFuntion)(void) tWord DELAY tWord PERIOD tByte Co_op)
*       : 返回任务的ID
*********************************************************************************************************
*/
tByte hSCH_Add_Task(void (*pFuntion)(void),
                                                                 tWordDELAY,
                                                                 tWordPERIOD,
                                                                 tByteCo_op)         /*任务合作式和抢占式的区分*/
{
         tByte index = 0; /*首先在队列中找到一个空隙,(如果有的话)*/
         
         while((hSCH_task_G[index].pTask!= 0) && (index <hSCH_MAX_TASKS))
         {
                   index ++;            
         }
         if(index ==hSCH_MAX_TASKS)/*超过最大的任务数目 则返错误信息*/
         {
                   Error_code_G =ERROR_SCH_TOO_MANY_TASKS;/*设置全局错误变量*/
                   returnhSCH_MAX_TASKS;         
         }
         
         hSCH_task_G[index].pTask =pFuntion; /*运行到这里说明申请的任务块成功*/
         hSCH_task_G[index].Delay =DELAY;
         hSCH_task_G[index].Period =PERIOD;
         hSCH_task_G[index].RunMe =0;
         hSCH_task_G[index].Co_op =Co_op;
         
         return index;                                                /*返回任务的位置,以便于以后删除*/
}


3.3.5      调度函数
         在前面已经说过,刷新函数不执行任何合作式任务,需要运行的任务由调度函数激活。下面是调度函数的源码:
/*
*********************************************************************************************************
*       :hSCH_Dispatch_Tasks
*       功能说明: 在主任务里面执行的调度函数。
*           参:无
*       :
*********************************************************************************************************
*/
void hSCH_Dispatch_Tasks(void)
{
         tByte index;
         /*运行下一个任务,如果下一个任务准备就绪的话*/
         for(index = 0; index <hSCH_MAX_TASKS; index++)
         {
                   if((hSCH_task_G[index].RunMe>0) && (hSCH_task_G[index].Co_op))    /*只调度合作式任务*/
                   {
                            (*hSCH_task_G[index].pTask)();     /*执行任务 */
                            hSCH_task_G[index].RunMe-= 1;            /*执行任务完成后,将RunMe减一 */
                            if(hSCH_task_G[index].Period== 0) /*如果是单次任务的话,则将任务删除 */
                            {
                                     hSCH_Task_Delete(index);
                            }
                   }
         }                          
}

有上面的五步,一个简单的合作式调度器就算设计完成了。
努力打造安富莱高质量微信公众号:点击扫描图片关注
回复

使用道具 举报

740

主题

1326

回帖

3546

积分

管理员

春暖花开

Rank: 9Rank: 9Rank: 9

积分
3546
QQ
 楼主| 发表于 2014-12-10 14:28:05 | 显示全部楼层
3.4  实验说明
本期教程配套的实验是:基于时间触发的合作式调度器,工程中需要添加如下驱动:
3.1.jpg

3.4.1      实验一:基于时间触发的混合式调度器
实验目的:
        1. 学习基于时间触发的混合式调度器。
实验内容:
        1.    通过函数hSCH_Add_Task添加三个任务
                 hSCH_Add_Task(bsp_KeyScan,0, 10, 1);          /* 按键扫描合作式任务,  周期10ms */
                 hSCH_Add_Task(AppTask_KeyScan, 0, 4,1);      /* 执行按键打印合作式任务  周期4ms */
                 hSCH_Add_Task(AppTask_LedToggle,0, 500, 0);  /* 四个LED闪烁抢占式任务 周期500ms */
        2. 主程序不断执行hSCH_Dispatch_Tasks();刷新函数。
实验现象:
        四个LED灯实现每500ms闪烁一次,按下3个按键中的某一个,或者五向摇杆会打印如下信息:
3.2.jpg

程序设计:
本程序主要分为四个部分:
  Ø  混合式调度器的设计
  Ø  混合式调度器的初始化
  Ø  任务API
  Ø  主程序
下面将这四个部分讲述一下:
        1.    混合式调度器的设计
            这个已经在前面的1.2小节详细进行了说明,这里就不再赘述了。
        2.    混合式调度器的初始化
             这里主要是时标间隔的初始化(这个放在了主函数里面实现,也就是嘀嗒定时器的初始化)和任务函数的添加,源代码如下:
/*
*********************************************************************************************************
*       : bsp_Init
*       功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次
*           参:无
*       :
*********************************************************************************************************
*/
void bsp_Init(void)
{
         /*
                   由于ST固件库的启动文件已经执行了CPU系统时钟的初始化,所以不必再次重复配置系统时钟。
                   启动文件配置了CPU主时钟频率、内部Flash访问速度和可选的外部SRAM FSMC初始化。
                   系统时钟缺省配置为168MHz,如果需要更改,可以修改system_stm32f4xx.c 文件
         */
         
         bsp_InitLed();     /* 初始LED指示灯端口 */
     bsp_InitUart();    /* 初始化串口 */
         bsp_InitKey();      /* 初始化按键 */
         
         
         /* 添加三个任务 */
         hSCH_Add_Task(bsp_KeyScan,0, 10, 1);           /* 合作式 周期10ms */
         hSCH_Add_Task(AppTask_KeyScan,0, 4, 1);        /* 合作式 周期4ms */
         hSCH_Add_Task(AppTask_LedToggle,0, 500, 0);    /* 抢占式 周期500ms */
         
}

3.    任务API
主要是按键任务和LED闪烁任务,按键扫描任务就不说了,这个在前面按键FIFO里面有详细的讲解。
/*
*********************************************************************************************************
*       :AppTask_KeyScan
*       功能说明: 打印按键扫描结果
*           参:无
*       :
*********************************************************************************************************
*/
void AppTask_KeyScan(void)
{
    uint8_t ucKeyCode;
         
         ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */
         if (ucKeyCode != KEY_NONE)
         {
                  
                   switch (ucKeyCode)
                   {
                            caseKEY_DOWN_K1:                           /*K1键按下 */
                                     printf("K1键按下, LED1点亮\\r\\n");
                                     break;
                            caseKEY_UP_K1:                                   /*K1键弹起 */
                                    
                                     printf("K1键弹起, LED1熄灭\\r\\n");
                                     break;
                            caseKEY_DOWN_K2:                           /*K2键按下 */
                                    
                                     printf("K2键按下, LED2点亮\\r\\n");
                                     break;
                            caseKEY_UP_K2:                                   /*K2键弹起 */
                                     printf("K2键弹起, LED2熄灭\\r\\n");
                                     break;
                            caseKEY_DOWN_K3:                           /*K3键按下 */
                                     printf("K3键按下, LED3点亮\\r\\n");
                                     break;
                            caseKEY_UP_K3:                                   /*K3键弹起 */
                                     printf("K3键弹起, LED3熄灭\\r\\n");
                                     break;
                            caseJOY_DOWN_U:                    /* 摇杆UP键按下 */
                                     printf("摇杆上键按下\\r\\n");
                                     break;
                            caseJOY_DOWN_D:                    /* 摇杆DOWN键按下 */
                                     printf("摇杆下键按下\\r\\n");
                                     break;
                            caseJOY_DOWN_L:                     /* 摇杆LEFT键按下 */
                                     printf("摇杆左键按下\\r\\n");
                                     break;
                            caseJOY_DOWN_R:                    /* 摇杆RIGHT键按下 */
                                     printf("摇杆右键按下\\r\\n");
                                     break;
                            caseJOY_DOWN_OK:                           /* 摇杆OK键按下 */
                                     printf("摇杆OK键按下\\r\\n");
                                     break;
                           
                            caseJOY_UP_OK:                                  /*摇杆OK键弹起 */
                                     printf("摇杆OK键弹起\\r\\n");
                                     break;
                            default:
                                     /*其他的键值不处理 */
                                     break;
                   }
         }
}
/*
*********************************************************************************************************
*       :AppTask_LedToggle
*       功能说明: 四个LED闪烁的程序
*           参:无
*       :
*********************************************************************************************************
*/
void AppTask_LedToggle(void)
{
         bsp_LedToggle(1);
         bsp_LedToggle(2);
         bsp_LedToggle(3);
         bsp_LedToggle(4);
}

4.    主函数
主函数比较简单,主要是在大循环里面调用调度函数。
/*
*********************************************************************************************************
*       : main
*       功能说明: c程序入口
*           参:无
*       : 错误代码(无需处理)
*********************************************************************************************************
*/
int main(void)
{
         bsp_Init();           /* 硬件初始化 */
         PrintfLogo();           /* 打印例程信息到串口1 */
         bsp_InitTimer();  /* 初始化系统滴答定时器 */
         /* 进入主程序循环体 */
         while (1)
         {
                   /* 任务执行函数 */
                   hSCH_Dispatch_Tasks();
         }
}

3.5  实验总结
       为了更好的使用混合式式调度器,这里简单的总结写使用注意事项:
  l  建立数量满足要求的混合式任务,很可能因为一个或者多个任务的运行时间大于时标间隔,所以需要使用混合式调度器。混合式调度器的使用是安全的,然而必须的保存任务不重叠。
  l  实现一个抢占式任务,这种任务一般在每个时标间隔调用,这种任务常用来检测错误或者紧急事件。
  l  抢占式任务可以中断合作式任务。
  l  抢占式任务必须的简短,运行时间最多不能超多时间间隔%50,否则将极大的削弱系统的性能。
  l  在所有的状态下仔细测试该系统,检测错误。
关于时间触发方式的调度器设计,就跟大家讲这么多,有兴趣的可以查阅相关资料做深入的了解。

参考资料:
1.    Patterns fortime-triggered embedded systems英文版和中文版
努力打造安富莱高质量微信公众号:点击扫描图片关注
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2024-5-29 07:25 , Processed in 0.181928 second(s), 28 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表