极海APM32F407 uc/os3学习笔记之任务管理介绍

描述

来源:转载自21ic论坛极海半导体专区

最近了解了uc/os3这个操作系统,上篇介绍了uc/os3如何正确移植到APM32F407开发板上,根据我最近学到的一些知识,这篇文章主要介绍一下uc/os3中的一些简单的任务管理API以及如何使用。

1、任务管理介绍:

uc/os3支持单核cpu,不支持多核cpu,这样在某一时刻只有一个任务会获得CPU使用权进入运行态,其他的任务就会进入其他的状态,uc/os3中的任务有多个状态,如下所示。

任务状态 描述
休眠态 休眠态就是任务只是以任务函数的方式存在,只是存储区中的一段代码, 并未用 OSTaskCreate()函数创建这个任务,不受 UCOSIII 管理的。
就绪态 任务在就绪表中已经登记,等待获取 CPU 使用权。
运行态 正在运行的任务就处于运行态。
等待态 正在运行的任务需要等待某一个事件,比如信号量、消息、事件标志组等, 就会暂时让出 CPU 使用权,进入等待事件状态。
中断服务态 一个正在执行的任务被中断打断,CPU 转而执行中断服务程序,这时这个 任务就会被挂起,进入中断服务态。


在uc/os3中任务可以在这5个状态中转换,转换关系如下图。

开发板

2、任务控制块介绍

我们需要知道uc/os3有一个重要的数据结构:任务控制块OS_TCB。任务控制块用来保存任务的信息,我们使用OSTaskCreate() 函数来创建任务的时候就会给任务分配一个任务控制块。任务控制块是一个结构体。

struct os_tcb {

    CPU_STK             *StkPtr;                            /* Pointer to current top of stack                        */

    void                *ExtPtr;                            /* Pointer to user definable data for TCB extension       */

    CPU_STK             *StkLimitPtr;                       /* Pointer used to set stack 'watermark' limit            */

#if (OS_CFG_DBG_EN > 0u)

    CPU_CHAR            *NamePtr;                           /* Pointer to task name                                   */

#endif

    OS_TCB              *NextPtr;                           /* Pointer to next     TCB in the TCB list                */

    OS_TCB              *PrevPtr;                           /* Pointer to previous TCB in the TCB list                */

#if (OS_CFG_TICK_EN > 0u)

    OS_TCB              *TickNextPtr;

    OS_TCB              *TickPrevPtr;

#endif

#if ((OS_CFG_DBG_EN > 0u) || (OS_CFG_STAT_TASK_STK_CHK_EN > 0u) || (OS_CFG_TASK_STK_REDZONE_EN > 0u))

    CPU_STK             *StkBasePtr;                        /* Pointer to base address of stack                       */

#endif

#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)

    OS_TLS               TLS_Tbl[OS_CFG_TLS_TBL_SIZE];

#endif

#if (OS_CFG_DBG_EN > 0u)

    OS_TASK_PTR          TaskEntryAddr;                     /* Pointer to task entry point address                    */

    void                *TaskEntryArg;                      /* Argument passed to task when it was created            */

#endif

    OS_TCB              *PendNextPtr;                       /* Pointer to next     TCB in pend list.                  */

    OS_TCB              *PendPrevPtr;                       /* Pointer to previous TCB in pend list.                  */

    OS_PEND_OBJ         *PendObjPtr;                        /* Pointer to object pended on.                           */

    OS_STATE             PendOn;                            /* Indicates what task is pending on                      */

    OS_

以下是对给定结构体 os_tcb 中每个成员的解释:

1. StkPtr: 指向当前堆栈顶部的指针,用于跟踪任务的堆栈。

2. ExtPtr: 指向任务的用户可定义数据的指针,用于扩展任务控制块(TCB)。

3. StkLimitPtr: 用于设置堆栈限制的指针。

4. NamePtr (如果启用了调试功能): 指向任务名称的指针。

5. NextPtr 和 PrevPtr: 分别指向任务列表中下一个和前一个任务的指针。

6. TickNextPtr 和 TickPrevPtr (如果启用了时钟节拍功能): 分别指向时钟节拍列表中下一个和前一个任务的指针。

7. StkBasePtr (如果启用了调试功能或堆栈检查功能): 指向任务堆栈基地址的指针。

8. TLS_Tbl (如果定义了OS_CFG_TLS_TBL_SIZE): 任务线程局部存储表。

9. TaskEntryAddr 和 TaskEntryArg(如果启用了调试功能): 分别为任务入口点地址和任务创建时传递的参数。

10. PendNextPtr、PendPrevPtr、PendObjPtr、PendOn 和 PendStatus: 用于处理任务挂起的指针和状态信息。

11. TaskState 和 Prio: 任务状态和优先级。

12. BasePrio、MutexGrpHeadPtr (如果启用了互斥锁功能): 基本优先级和拥有的互斥锁组的头指针。

13. StkSize (如果启用了调试功能、堆栈检查功能或堆栈红区功能):任务堆栈大小。

14. Opt: 由 OSTaskCreate() 传递的任务选项。

15. TS :(如果启用了时间戳功能): 时间戳。

16. SemID (如果定义了OS_CFG_TRACE_EN): 用于跟踪和调试的唯一 ID。

17. SemCtr: 任务特定的信号量计数器。

18. TickRemain 和 TickCtrPrev (如果启用了时钟节拍功能): 用于任务延迟和定时器的计数器。

19. TimeQuanta 和 TimeQuantaCtr (如果启用了循环调度功能): 时间量和计数器。

20. MsgPtr 和 MsgSize (如果启用了消息队列功能): 接收到的消息和消息大小。

21. MsgQ (如果启用了任务消息队列功能): 与任务相关联的消息队列。

22. RegTbl (如果定义了OS_CFG_TASK_REG_TBL_SIZE): 任务特定的寄存器。

23. FlagsPend、FlagsRdy 和 FlagsOpt (如果启用了事件标志功能): 分别为等待的事件标志、使任务准备好的事件标志和选项。

24. SuspendCtr (如果启用了任务挂起功能): 挂起计数器。

25. CPUUsage、CPUUsageMax、CtxSwCtr、CyclesDelta、CyclesStart、CyclesTotal 和 CyclesTotalPrev (如果启用了任务性能分析功能):CPU 使用率、上下文切换计数器和周期计数器等信息。

26. StkUsed 和 StkFree (如果启用了堆栈检查功能): 堆栈使用量和剩余量。

27. IntDisTimeMax 和 SchedLockTimeMax (如果启用了中断禁止时间测量功能或调度锁定时间测量功能): 中断禁止时间和调度锁定时间的最大值。

28. DbgPrevPtr、DbgNextPtr 和 DbgNamePtr (如果启用了调试功能): 调试相关的指针和名称。

29. TaskID (如果定义了OS_CFG_TRACE_EN): 任务的唯一 ID。

3、任务堆栈介绍

在uc/os3中,任务堆栈用来切换和调用其他函数的时候保存现场,因此每个任务都应该有自己的堆栈。我们可以使用CPU_STK定义一个任务堆栈,CPU_STK就是CPU_INT32U,一个CPU_STK变量为4字节,因此任务的实际堆栈大小应该为我们定义的4倍。

CPU_STK ledTaskStk[64]; //定义一个任务堆栈,堆栈大小为64*4=256字节

我们使用OSTaskCreate()函数创建任务的时候就可以把创建的堆栈传递给任务,将堆栈的及地址传递给OSTaskCreate()函数的参数p_stk_base,将堆栈深度传递给stk_limit,堆栈深度通常为堆栈大小的十分之一,主要是用来检测堆栈是否为空,将堆栈大小传递给参数stk_size。

OSTaskCreate(&ledTaskTCB, 

        "LED Task", 

        ledTask, 

        NULL, 

        LED_TASK_PRIO, 

        ledTaskStk, 

        0, 

        STACK_SIZE, 

        0, 

        0, 

        NULL, 

        (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), 

        &err);

创建任务的时候会初始化任务的堆栈,我们需要提前将CPU的寄存器保存在任务堆栈中,完成这个任务的是OSTaskStkInit()函数,用户不能调用这个函数,这个函数是被OSTaskCreate()函数在创建任务的时候调用的。

4、任务就绪表介绍

在 uC/OS-III 中,任务就绪表(ReadyList)是一个重要的数据结构,用于管理处于就绪状态的任务。任务就绪表是一个数组,其中每个元素都对应一个优先级。每个优先级的元素是一个链表,存储了具有相同优先级的就绪任务的任务控制块(TaskControl Block,TCB)。

任务就绪表的基本结构如下:

Ready List:

Priority 0: -> TCB1 -> TCB2 -> ... -> TCBn -> NULL

Priority 1: -> TCB1 -> TCB2 -> ... -> TCBn -> NULL

...

Priority N: -> TCB1 -> TCB2 -> ... -> TCBn -> NULL

在 uC/OS-III 中,任务按照它们的优先级排列在任务就绪表中。每个优先级链表中的任务控制块按照某种顺序(例如先进先出)存储,以确保公平地分配 CPU 时间片。任务就绪表使得任务调度器能够快速地找到下一个要执行的任务。当一个任务被创建并准备好执行时,它的任务控制块将被添加到适当优先级的就绪链表中。当任务调度器需要选择下一个要执行的任务时,它会遍历任务就绪表,从具有最高优先级的就绪链表中选择一个任务执行。

因此,任务就绪表是uC/OS-III 中实现任务调度的重要数据结构之一,它提供了对任务就绪状态的高效管理。

针对任务就绪表的操作有以下几个函数,这些函数都是uc/os3内部使用的,用户程序不能使用。

函数 描述
OS_RdyListInit() 由 OSInit()调用用来初始化并清空任务就绪列表
OS_RdyListInsertHead() 向某一优先级下的任务双向链表头部添加一个任务控制块 TCB
OS_RdyListInsertTail() 向某一优先级下的任务双向链表尾部添加一个任务控制块 TCB
OS_RdyListRemove() 将任务控制块 TCB 从任务就绪列表中删除
OS_RdyListInsertTail() 将一个任务控制块 TCB 从双向链表的头部移到尾部
OS_RdyListInsert() 在就绪表中添加一个任务控制块 TCB

5、任务调度和切换介绍

我们如果知道一些简单的操作系统的进程调度的话,这一块是比较简单的。在 uC/OS-III 中,任务调度是指操作系统决定下一个要执行的任务的过程。这个过程通常由一个专门的部分,即任务调度器,来负责。任务调度器根据一定的策略从就绪任务中选择一个来执行。这个策略可能是基于任务的优先级,也可能是基于其他的调度算法。

在uc/os3中,主要分为任务级调度器、中断级调度器以及时间片轮转调度器。

任务级调度器:我们在创建一个任务时,会给每个任务一个优先级,而任务执行的先后顺序就是根据我们事先指定的任务优先级运行的,优先级数值越小的任务拥有更高的优先级

中断级调度器:在 uC/OS-III 中, 中断级调度器的作用是确保即使在中断服务程序执行期间,也能够及时地响应高优先级任务的需求,从而保证系统的实时性和响应性。

需要注意的是,中断级调度器是针对中断服务程序中的任务切换而设计的,而普通任务的切换则由任务级调度器负责。

时间片轮转调度:时间片轮转调度器通过周期性地切换任务执行,确保每个任务都有机会执行,并且可以平衡系统中不同任务的执行时间,提高系统的响应性和公平性。

在 uC/OS-III 中,时间片轮转调度器的运行过程如下:

初始化设置:在系统启动时,可以通过配置选项来启用或禁用时间片轮转调度器,并设置每个任务的时间片大小。

2. 任务就绪队列:所有就绪状态的任务都会被放置在任务就绪队列中,等待调度器选择执行。

3. 选择任务:调度器会选择一个就绪状态的任务来执行。如果启用了时间片轮转调度器,调度器会按照一定的策略选择一个任务。

4. 执行任务:选定的任务开始执行,并且会被分配一个时间片来执行。任务会持续执行直到它完成了它的时间片,或者被其他更高优先级的任务抢占。

5. 时间片用完:当任务的时间片用完时,调度器会暂停当前任务的执行,并选择下一个就绪状态的任务来执行。

6. 任务切换:如果有其他任务需要执行,调度器会进行任务切换。这个过程包括保存当前任务的上下文信息,恢复下一个任务的上下文信息,并开始执行下一个任务。

7. 重复执行:这个过程会一直重复进行,直到系统关闭或者没有就绪状态的任务为止。

开发板

6、函数介绍

1、OSTaskCreate()函数

uc/os3是多任务系统,那么肯定要能创建任务,创建任务就是将任务控制块、任务堆栈、任务代码等联系在一起,并且初始化控制块的相应字段。在uc/os3中,我们使用OSTaskCreate()来创建任务。调用OSCreateTask()创建一个任务以后,刚创建的任务就会进入就绪态,注意,不能在中断服务程序中调用OSTaskCreate()函数来创建任务

void  OSTaskCreate (OS_TCB        *p_tcb, //指向任务的控制任务块

                    CPU_CHAR      *p_name, //指向任务的名字

                    OS_TASK_PTR    p_task, //执行任务代码,即任务函数名

                    void          *p_arg, //传递给任务的参数

                    OS_PRIO        prio, //任务优先级

                    CPU_STK       *p_stk_base, //任务堆栈的基地址

                    CPU_STK_SIZE   stk_limit, //堆栈深度

                    CPU_STK_SIZE   stk_size, //堆栈大小

                    OS_MSG_QTY     q_size, //任务的内部消息队列

                    OS_TICK        time_quanta, //时间片轮转调度时,用来设置任务的时间片长度

                    void          *p_ext,//指向用户补充的存储区

                    OS_OPT         opt, //任务待定选项,可选项如下

/*

OS_OPT_TASK_NONE 表示没有任何选项 

OS_OPT_TASK_STK_CHK 指定是否允许检测该任务的堆栈 

OS_OPT_TASK_STK_CLR 指定是否清除该任务的堆栈 

OS_OPT_TASK_SAVE_FP 指定是否存储浮点寄存器,CPU 需要有浮点

*/

                    OS_ERR        *p_err)  //用来保存调用该函数后返回的错误码。

为什么不能在中断服务程序中调用OSTaskCreate()函数来创建任务?

这是因为在 uC/OS-III 中,任务控制块(TCB)和堆栈空间是预先分配的,而 OSTaskCreate() 函数需要使用系统堆栈来创建任务。而中断服务程序中的堆栈通常较小且固定,不适合用于创建任务。举个例子,假设我们有一个需要在外部中断触发时执行的任务,我们希望在中断处理程序中创建一个新的任务来处理该中断触发的事件。

#include "includes.h"

// 外部中断触发时的中断处理程序

void ExternalInterruptHandler(void) {

    // 在这里调用 OSTaskCreate() 来创建新任务

    OSTaskCreate(NewTask, /* arguments */);

}

// 新任务的入口函数

void NewTask(void *p_arg) {

    // 处理中断触发的事件

    // ...

}

int main(void) {

    // 初始化 uC/OS-III 内核

    OSInit();

    // 创建其他任务

    // ...

    // 启动 uC/OS-III 内核

    OSStart();

    return 0;

}

上面的代码试图在外部中断触发时,通过调用 OSTaskCreate() 函数来创建一个新的任务来处理中断触发的事件。然而,这种做法是错误的。原因如下:

1. 中断服务程序中的堆栈通常较小且固定,不适合用于创建任务。OSTaskCreate() 函数需要使用系统堆栈来创建任务,而不是中断服务程序的堆栈。 2. 在中断服务程序中创建任务可能会引入不可预测的延迟,影响系统的实时性能。中断服务程序应该尽量保持简洁和高效,以便尽快退出中断处理,从而允许系统继续响应其他中断或任务。   正确的做法是,将中断服务程序中需要处理的任务标记为标志、消息或信号量等形式,并在任务上下文中通过检测这些标记来执行相应的操作。例如,中断服务程序可以向任务发送消息或释放信号量,任务在接收到这些消息或信号量后执行相应的操作。

2、OSTaskDel()函数

OSTaskDel()函数用来删除任务,当一个任务不需要运行时,我们就可以将其删除,删除任务不是说删除任务代码,而是uc/os3不在管理这个任务,再有些应用中我们只需要某个任务只运行一次,与逆行完成就将其删除掉,比如初始化任务。

void OSTaskDel (OS_TCB *p_tcb, // 指向要删除任务的TCB

OS_ERR *p_err)  //用来保存调用该函数后返回的错误码。

3、OSTaskSuspend()函数

    OSTaskSuspend()函数用于任务挂起,有时有些任务因为某些原因需要暂停运行,但是以后还要运行,因此我们就不能删除这些任务。

void   OSTaskSuspend (OS_TCB  *p_tcb, //需要挂起的任务TCB

                      OS_ERR  *p_err) //指向一个变量,用来保存该函数的错误码

4、OSTaskResume()函数

OSTaskResume()函数用来恢复被挂起的任务。

void  OSTaskResume (OS_TCB  *p_tcb, //需要恢复的任务TCB

                    OS_ERR  *p_err) //指向一个变量,用来保存该函数的错误码

5、OSSchedRoundRobinCfg()函数

OSSchedRoundRobinCfg()函数用来使能或失能UCOSIII 的时间片轮转调度功能,如果我们 要使用时间片轮转调度功能的话不仅要将宏OS_CFG_SCHED_ROUND_ROBIN_EN 定义为 1, 还需要调用OSSchedRoundRobinCfg()函数来使能 UCOSIII。

void  OSSchedRoundRobinCfg (CPU_BOOLEAN   en, //用于设置打开或关闭时间片轮转调度机制,如果为 DEF_ENABLED 表 示打开时间片轮转调度,为 DEF_DISABLED 表示关闭时间片轮转调度。

                            OS_TICK       dflt_time_quanta,// 设置默认的时间片长度,就是系统时钟节拍个数,默认的时间片长度:OSCfg_TickRate_Hz / 10

                            OS_ERR       *p_err) //保存调用此函数后返回的错误码

这里提一下怎么计算得到系统时钟节拍。我们可以在os_cfg_app.h中查找OS_CFG_TICK_RATE_HZ变量,这个变量就是系统时钟频率。例如我们设置系统时钟频率OS_CFG_TICK_RATE_HZ为1000Hz,那么每个时钟节拍就是5ms。当我们设置dflt_time_quanta为n时,时间片长度就是(n*1)ms,如果我们设置dflt_time_quanta为0时,uc/os3就会使用默认的时间片长度OSCfg_TickRate_Hz/ 10,例如我们设置OS_CFG_TICK_RATE_HZ为1000Hz,那么时间片长度就是1000/10*1=100ms。

6、OSSchedRoundRobinYield()函数

当一个任务想要放弃本次时间片,把CPU的使用全让给同优先级下的另外一个任务就可以使用这个函数。

void  OSSchedRoundRobinYield (OS_ERR  *p_err) //保存调用此函数后返回的错误码

它有以下几个错误的返回值。

    - OS_ERR_NONE 调用成功

    - OS_ERR_ROUND_ROBIN_1 当前优先级下没有其他就绪任务

    - OS_ERR_ROUND_ROBIN_DISABLED 未使能时间片轮转调度功能

    - OS_ERR_SCHED_LOCKED      调度器已被锁定

    - OS_ERR_YIELD_ISR 在中断调用了本函数。

我们在调用这个后函数遇到最多的错误就是OS_ERR_ROUND_ROBIN_1,也就是当前优先级下没有就绪任务了。

7、相关示例代码

/* Includes */

#include "main.h"

#include "Board.h"

#include

#include

#include

#define DEBUG_USART  USART1

/** @addtogroup Examples

  @{

  */

/** @addtogroup Template

  @{

  */

/** @defgroup Template_Functions Functions

  @{

  */

#define STACK_SIZE 128      // Stack size for LED task

#define LED2_TASK_PRIO 5     // Priority for LED2 task

#define LED3_TASK_PRIO 5        // Priority for LED2 task

#define SERIAL_TASK_PRIO 6  // Priority for serial print task

OS_TCB startTaskTCB;                // Task Control Block for start task

OS_TCB led2TaskTCB;         // Task Control Block for LED task

OS_TCB led3TaskTCB;         // Task Control Block for LED task

OS_TCB serialTaskTCB;      // Task Control Block for serial print task

CPU_STK startTaskStk[STACK_SIZE];    // Stack for START task

CPU_STK led2TaskStk[STACK_SIZE];    // Stack for LED task

CPU_STK led3TaskStk[STACK_SIZE];    // Stack for LED task

CPU_STK serialTaskStk[STACK_SIZE]; // Stack for serial print task

void startTask(void *p_arg);

void led2Task(void *p_arg);

void led3Task(void *p_arg);

void serialPrintTask(void *p_arg);

int num = 0;

/*!

 * [url=home.php?mod=space&uid=247401]@brief[/url]       Main program

 *

 * @param       None

 *

 * @retval      None

 */

int main(void)

{

    USART_Config_T usartConfig;

    /* Configure USART */

    usartConfig.baudRate = 115200;

    usartConfig.wordLength = USART_WORD_LEN_8B;

    usartConfig.stopBits = USART_STOP_BIT_1;

    usartConfig.parity = USART_PARITY_NONE ;

    usartConfig.mode = USART_MODE_TX_RX;

    usartConfig.hardwareFlow = USART_HARDWARE_FLOW_NONE;

    SysTick_Config(RCM_ReadSYSCLKFreq()/1000);

    APM_MINI_COMInit(COM1,&usartConfig);

    APM_MINI_LEDInit(LED2);

    APM_MINI_LEDInit(LED3);

    OS_ERR err;

    // Initialize uC/OS-III

    OSInit(&err);

    OSTaskCreate(&startTaskTCB, "START Task", startTask, NULL, LED2_TASK_PRIO, startTaskStk, STACK_S

上面是我编写的一个简单的示例代码,主要是这些函数的简单应用,这里为了体现时间片轮转调度功能,我把两个LEDTask的优先级设置的一样,当分配给LED2Task的时间片用完后,LED3Task就会抢占LED2Task对CPU的使用权,然后LED2Task进入就绪队列,如此反复,直到这两个任务都被执行完。这段代码主要实现的功能如下。

1. 任务堆栈和控制块定义:

   - 定义了几个任务所需的堆栈和控制块,包括启动任务(startTask)、LED任务(led2Task和led3Task)以及串口打印任务(serialPrintTask)。

2. main函数:

   - 配置USART串口通信的参数,初始化系统时钟。

   - 初始化uC/OS-III操作系统。

   - 创建启动任务(startTask)。

   - 启动uC/OS-III操作系统。

3. startTask函数:

   - 启动任务中进行了时间片轮转调度的配置,使用了OSSchedRoundRobinCfg()函数启用了时间片轮转调度,并设置了时间片长度为5ms。

   - 创建LED任务(led2Task和led3Task)和串口打印任务(serialPrintTask),设置了led2Task和led3Task的时间片为5*3=15ms,设置了serialTask的时间片为100ms。

   - 最后删除启动任务自身。

4. LED任务:

   - led2Task和led3Task分别控制LED2和LED3的闪烁。

   - 每个LED任务在循环中先打印五次特定的消息,然后延时1秒。

   - LED3任务在执行到第3次循环时,暂停串口打印任务(serialPrintTask)。

   - 当LED3任务执行到第6次循环时,恢复串口打印任务。

5. 串口打印任务:

   - serialPrintTask任务每500ms向串口打印一条消息。

由于我接触这个操作系统也不是特别久,也是最近开始了解并学习,这篇文档更偏向于API应用,也就是直接讲了一下我们经常可能会使用的API,而有关uc/os3具体的启动流程目前还没有去深究,后续有时间的话我会去了解学习一下,附件是我编写的示例代码工程,有需要的可以下载,同时欢迎各位大佬指导与交流。

注:文章作者在原帖中提供了例程文件,有需要请至原文21ic论坛下载

原文地址:https://bbs.21ic.com/icview-3369956-1-1.html

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分