04. µCOS-Ⅲ的任务调度

一、µC/OS-Ⅲ的初始化

  µC/OS-Ⅲ 内核的初始化是通过调用函数 OSInit() 完成的,函数 OSInit() 必须在函数 OSStart() 调用之前被调用,函数 OSInit() 会初始化 µC/OS-Ⅲ 内核并检测是否存在错误,如果检测到错误,就会立即返回。

  函数 OSInit() 的函数原型如下所示:

void OSInit(OS_ERR *p_err);

  函数 OSInit() 有一个形参 p_err,是一个指向接收错误代码变量的指针,如果函数 OSInit()在初始化 µC/OS-III 内核的时候,检测到错误,就会立即返回,并将错误代码通过形参 p_err 返回。

  该函数内部实现,如下:

/*
************************************************************************************************************************
*                                                    INITIALIZATION
*
* Description: This function is used to initialize the internals of uC/OS-III and MUST be called prior to
*              creating any uC/OS-III object and, prior to calling OSStart().
*
* Arguments  : p_err         is a pointer to a variable that will contain an error code returned by this function.
*
*                                OS_ERR_NONE    Initialization was successful
*                                Other          Other OS_ERR_xxx depending on the sub-functions called by OSInit().
* Returns    : none
************************************************************************************************************************
*/
void  OSInit (OS_ERR  *p_err)
{
#if (OS_CFG_ISR_STK_SIZE > 0u)                                                  // 此宏用于定义异常栈的大小
    CPU_STK      *p_stk;
    CPU_STK_SIZE  size;
#endif

#ifdef OS_SAFETY_CRITICAL                                                       // 此宏用于定义异常栈的大小
    if (p_err == (OS_ERR *)0) {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

    /**
     * μC/OS-Ⅲ内核初始化钩子函数,用于完成一些特定于硬件架构的初始化,如下:
     *  1、将异常栈的栈底(OS_CPU_ExceptStkBase)按 8 字节对齐
     *  2、检查 FPCCR 寄存器是否为复位值
     *  3、根据配置项 CPU_CFG_NVIC_PRIO_BITS 和配置项 CPU_CFG_KA_IPL_BOUNDARY,初始化全局变量 OS_KA_BASEPRI_Boundary 的值,
     *      该全局变量用于设置 BASEPRI 寄存器,实现只屏蔽受 μC/OS-Ⅲ 管理的中断
     */
    OSInitHook();

    OSIntNestingCtr       =           0u;                                       // 中断嵌套计数器
    OSRunning             =  OS_STATE_OS_STOPPED;                               // μC/OS-Ⅲ 内核运行状态
    OSSchedLockNestingCtr =           0u                                        // 任务调度器锁定嵌套计数器
    OSTCBCurPtr           = (OS_TCB *)0;                                        // 指向当前任务的任务控制块的指针
    OSTCBHighRdyPtr       = (OS_TCB *)0;                                        // 指向优先级最高的任务的任务控制块的指针
    OSPrioCur             =           0u;                                       // 前任务的任务优先级
    OSPrioHighRdy         =           0u;                                       // 优先级最高的任务的任务优先级

#if (OS_CFG_SCHED_LOCK_TIME_MEAS_EN > 0u)                                       // 此宏用于使能测量任务调度器的锁定时长
    OSSchedLockTimeBegin  =           0u;                                       // 任务调度器被锁定的开始时间戳
    OSSchedLockTimeMax    =           0u;                                       // 任务调度器被锁定的历史最长时长
    OSSchedLockTimeMaxCur =           0u;                                       // 任务调度器被当前任务锁定的最长时长
#endif

#ifdef OS_SAFETY_CRITICAL_IEC61508                                              // 此宏用于使能 μC/OS-Ⅲ 对 IEC61508 国际标准的支持
    // 此全局变量用于标志用户是否调用过函数 OsSafetyCriticalStart(),
    // 当函数 OsSafetyCriticalStart() 被调用过后,表示已经完成了所有的初始化工作,在此之后将不允许再创建内核对象
    OSSafetyCriticalStartFlag = OS_FALSE;
#endif

#if (OS_CFG_SCHED_ROUND_ROBIN_EN > 0u)                                          // 此宏用于使能 μC/OS-Ⅲ 对时间片调度的支持
    OSSchedRoundRobinEn             = OS_FALSE;                                 // 时间片调度使能标志
    OSSchedRoundRobinDfltTimeQuanta = OSCfg_TickRate_Hz / 10u;                  // 时间片长度的默认值
#endif

#if (OS_CFG_ISR_STK_SIZE > 0u)                                                  // 此宏用于定义异常栈的大小
    // 检查异常栈的配置是否无误(异常栈的起始地址不为空),检查异常栈的配置是否无误(异常栈的起始地址不为空)
    p_stk = OSCfg_ISRStkBasePtr;              
    if (p_stk != (CPU_STK *)0) {
        size  = OSCfg_ISRStkSize;
        while (size > 0u) {
            size--;
           *p_stk = 0u;
            p_stk++;
        }
    }
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)                                           // 此宏用于使能 Redzone 栈检查功能
    OS_TaskStkRedzoneInit(OSCfg_ISRStkBasePtr, OSCfg_ISRStkSize);               // 初始化异常栈的 RedZone 栈检查功能
#endif
#endif

#if (OS_CFG_APP_HOOKS_EN > 0u)                                                  // 此宏用于使能应用钩子函数
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)                                           // 此宏用于使能 RedZone 栈检查功能
    OS_AppRedzoneHitHookPtr = (OS_APP_HOOK_TCB )0;                              // 指向栈溢出钩子函数的指针
#endif
    OS_AppTaskCreateHookPtr = (OS_APP_HOOK_TCB )0;                              // 指向任务创建钩子函数的指针
    OS_AppTaskDelHookPtr    = (OS_APP_HOOK_TCB )0;                              // 指向任务删除钩子函数的指针
    OS_AppTaskReturnHookPtr = (OS_APP_HOOK_TCB )0;                              // 指向任务非法返回钩子函数的指针
    OS_AppIdleTaskHookPtr   = (OS_APP_HOOK_VOID)0;                              // 指向空闲任务钩子函数的指针
    OS_AppStatTaskHookPtr   = (OS_APP_HOOK_VOID)0;                              // 指向统计任务钩子函数的指针
    OS_AppTaskSwHookPtr     = (OS_APP_HOOK_VOID)0;                              // 指向任务切换钩子函数的指针
    OS_AppTimeTickHookPtr   = (OS_APP_HOOK_VOID)0;                              // 指向时钟节拍钩子函数的指针
#endif

#if (OS_CFG_TASK_REG_TBL_SIZE > 0u)                                             // 此宏用于定义特定于任务的寄存器数量
    OSTaskRegNextAvailID = 0u;                                                  // 任务特定寄存器组读取索引
#endif

    /**
     * 初始化就绪态任务任务优先级位图表,μC/OS-Ⅲ 将所有就绪态任务的任务优先级以位图的方式记录在 OSPrioTb1[]中,
     * 这里将配置项 OS_CFG_PRIO_MAX 配置为 32,因此数组 OSPrioTb1 的元素个数为 1,即使用 OSPrioTbl[0] 的 bit0 ~ bit31 记录就绪态任务任务优先级
     * 就绪态任务任务优先级位图表的初始化过程,如下:
     *  1、将数组 OSPrioTb1 中的所有元素清零
     *  2、如果未使能配置项 OS_CFG_TASK_IDLE_EN(未使能空闲任务),则强制将空闲任务的优先级(OS_CFG_PRIO_MAX-1)记录到就绪态务任务优先级位图表中,这样是为了兼容使能了空闲任务的情况
     */
    OS_PrioInit();

    /**
     * 初始化就绪态任务链表,在 µC/OS-Ⅲ 中定义了 OS_CFG_PRIO_MAX 个就绪态任务链表(一个任务优先级对应一个就绪态任务链表),
     * 这 OS_CFG_PRIO_MAX 个就绪态任务链表被存放在数组 OSRdyList[OS_CFG_PRIO_MAX]中,数组 OSRdyList 的一个元素就代表一个就绪态任务链表
     * 就绪态任务链表的初始化过程,如下:
     *  1、如果使能了配置项 OS_CFG_DBG_EN(使能代码调试功能),则数组 OSRdyList 的每一个元素中都包含一个用于记录该就绪态任务链表中任务数量的变量,初始化时,会将该变量清零
     *  2、将每一个就绪态任务链表中指向上一个就绪态任务任务控制块的指针和指向下一个就绪态任务任务控制块的指针清空。
     */
    OS_RdyListInit();

#if (OS_CFG_FLAG_EN > 0u)                                                       // 此宏用于使能事件标志功能
#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    OSFlagDbgListPtr = (OS_FLAG_GRP *)0;                                        // 此宏用于使能代码调试功能
    OSFlagQty        =                0u;
#endif
#endif

#if (OS_CFG_MEM_EN > 0u)                                                        // 此宏用于使能代码调试功能
    // 初始化内存管理功能,主要用于初始化用于调试内存管理的变量(使能了代码调试功能)
    OS_MemInit(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

#if (OS_MSG_EN > 0u)                                                            // 此宏用于使能消息队列功能或任务内嵌消息队列功能
    /**
     * 初始化消息池初始化消息池,实际上就是初始化消息池中的消息控制块
     * 消息控制块的初始化过程,如下:
     *  1、消息控制块中指向下一个消息控制块的指针指向下一个消息控制块(将消息池中的消息控制块链接成一个单向链表)
     *  2、消息控制块中指向消息的指针清空
     *  3、消息控制块中记录消息大小的变量清零
     *  4、消息控制块中记录消息发送时时间戳的变量清零
     */
    OS_MsgPoolInit(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

#if (OS_CFG_MUTEX_EN > 0u)                                                      // 此宏用于使能互斥信号量功能
#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    OSMutexDbgListPtr = (OS_MUTEX *)0;                                          // 用于调式互斥信号量的变量
    OSMutexQty        =             0u;
#endif
#endif

#if (OS_CFG_Q_EN > 0u)                                                          // 此宏用于使能消息队列功能
#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    OSQDbgListPtr = (OS_Q *)0;                                                  // 此宏用于使能代码调试功能
    OSQQty        =         0u;
#endif
#endif

#if (OS_CFG_SEM_EN > 0u)                                                        // 此宏用于使能信号量功能
#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能信号量功能
    OSSemDbgListPtr = (OS_SEM *)0;                                              // 此宏用于使能信号量功能
    OSSemQty        =           0u;
#endif
#endif

#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)                  // 此宏用于定义任务本地存储寄存器的数量
    OS_TLS_Init(p_err);                                                         // 此宏用于定义任务本地存储寄存器的数量
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

    /**
     * 初始化任务管理,任务管理的初始化过程,如下:
     *  1、将任务数量计数器清零
     *  2、将任务切换次数计数器清零
     */
    OS_TaskInit(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }

#if (OS_CFG_TASK_IDLE_EN > 0u)                                                  // 此宏用于使能空闲任务
    /**
     * 初始化空闲任务,空闲任务的初始化过程,如下:
     *  1、将空闲任务运行次数计数器清零
     *  2、根据配置文件中空闲任务的相关配置项创建空闲任务
     */
    OS_IdleTaskInit(p_err);               */
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

#if (OS_CFG_TICK_EN > 0u)                                                       // 此宏用于使能系统时钟节拍
    /**
     * 初始化系统时钟节拍,系统时钟节拍初始化过程,如下:
     *  1、将系统时钟节拍计数器(OsTickCtr)清零
     *  2、将 Tick 任务链表(OsTickLi st)中指向下一个Tick任务控制块的指针清空
     */
    OS_TickInit(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

#if (OS_CFG_STAT_TASK_EN > 0u)                                                  // 此宏用于使能任务统计功能
    /**
     * 初始化任务统计任务,任务统计任务的初始化过程,如下:
     *  1、初始化任务统计功能相关的全局变量
     *  2、根据配置文件中任务统计任务相关的配置项创建任务统计任务
     */
    OS_StatTaskInit(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

#if (OS_CFG_TMR_EN > 0u)                                                        // 此宏用于使能软件定时器功能
    /**
     * 始化软件定时器功能,软件定时器功能的初始化过程,如下:
     *  1、将软件定时器链表的链表头清空
     *  2、根据配置项 OSCfg_TickRate_Hz 和配置项 OSCfg_TmrTaskRate_Hz计算软件定时器时间到Tick时间二点转换系数(OSTmrToTicksMult)
     *  3、创建用于保护软件定时器的互斥信号量
     *  4、初始化用于软件定时器的内部变量(仅 μC/OS-III 内核使用) 
     *  5、根据配置文件中软件定时器相关的配置项创建软件定时器任务
     */
    OS_TmrInit(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }
#endif

#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    OS_Dbg_Init();                                                              // 初始化代码调试功能
#endif

    // µC/OS-Ⅲ 配置初始化,主要用于保证一些未使用到的变量不会被编译器优化掉
    OSCfg_Init();

    OSInitialized = OS_TRUE;                                                    //  µC/OS-Ⅲ 内核初始化状态
}

  该函数对一些全局变量赋初始值,然后初始化就绪列表以及 Tick 列表等,接着创建三个任务:空闲任务(必须创建),统计任务(条件创建),软件定时器任务(条件创建)。

  • 空闲任务:任务优先级最低 31,当系统无其他的就绪任务,那么空闲任务将会执行,注意空闲任务不能被阻塞。
  • 统计任务:任务优先级为 30,用来统计 CPU 使用率和各个任务的堆栈使用量。
  • 软件定时器任务:任务优先级为 29,主要用于在特定的时间段内处理单次或周期性的软件定时器。

二、创建应用任务

  完成 µC/OS-Ⅲ 内核初始化后,在开启 µC/OS-Ⅲ 任务调度之前,还必须至少创建一个应用任务,因为,虽然在函数 OSInit() 初始化 µC/OS-Ⅲ 内核的过程中,创建了空闲任务、任务统计任务和软件定时器任务(其中空闲任务是一定会被创建的,而任务统计任务和软件定时器任务会根据配置文件的配置,进行创建或不创建),但是以上这三个任务都属于内核任务,应用程序是无法访问的,并且在 µC/OS-Ⅲ 内核开始任务调度之后,就不会返回,CPU 只会执行已创建的任务,因此在开始任务调度之前,还需要至少创建一个应用任务,这个应用任务来负责初始化应用程序。

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_ERR        *p_err            // 指向接收错误代码变量的指针
);

  此函数用于创建任务,任务的任务控制块以及任务栈空间所需的内存,需要由用户手动分配并提供。当任务被创建好后,就会立马处于 就绪态,此时只要任务调度器处于正常工作状态,那么创建好的任务就会由任务调度器调度。要注意的是,不能在中断服务函数中创建任务。

  任务控制块是一个 OS_TCB 类型的结构体,它的主要成员如下:

struct os_tcb {
    CPU_STK *StkPtr;                // 任务栈栈顶,必须为TCB的第一个成员
    CPU_STK *StkLimitPtr;           // 指向任务栈警戒线指针
    OS_TCB *NextPtr;                // 指向任务列表中下一个任务控制块指针
    OS_TCB *PrevPtr;                // 指向任务列表中前一个任务控制块指针
    OS_TCB *TickNextPtr;            // 指向任务列表中下一个任务控制块指针
    OS_TCB *TickPrevPtr;            // 指向任务列表中前一个任务控制块指针
    OS_PRIO Prio;                   // 任务优先级,数值越小,优先级越大
    OS_TICK TickRemain;             // 任务延迟的剩余时钟节拍数
    OS_TICK TimeQuanta;             // 任务时间片
    OS_TICK TimeQuantaCtr;          // 任务剩余时间片
    ...
};

  任务选项共有 5 个值,它们的描述如下:

#define  OS_OPT_TASK_NONE       (OS_OPT)(0x0000u)   // 没有选项
#define  OS_OPT_TASK_STK_CHK    (OS_OPT)(0x0001u)   // 是否允许对任务进行堆栈检查
#define  OS_OPT_TASK_STK_CLR    (OS_OPT)(0x0002u)   // 是否需要清除任务堆栈
#define  OS_OPT_TASK_SAVE_FP    (OS_OPT)(0x0004u)   // 是否保存浮点寄存器
#define  OS_OPT_TASK_NO_TLS     (OS_OPT)(0x0008u)   // 不需要对正在创建的任务提供LTS(线程本都存储)支持

  函数 OSTaskCreate() 的错误代码描述,如下表所示:

OS_ERR_NONE                     // 任务创建成功
OS_ERR_ILLEGAL_CREATE_RUN_TIME  // 定义了OS_SAFETY_CRITICAL_IEC61508,且在OSStart()之后非法地创建内核对象
OS_ERR_PRIO_INVALID             // 非法的任务优先级数值
OS_ERR_STAT_STK _SIZE_INVALID   // 任务栈在初始化期间溢出
OS_ERR_STK_INVALID              // 指向任务栈起始地址的指针为空
OS_ERR_STK_SIZE_INVALID         // 任务栈小于配置项OS_CFG_STK_SIZE_MIN
OS_ERR_STK_LIMIT_INVALID        // 任务栈“水位”限制大小大于或等于任务栈大小
OS_ERR_TASK_CREATE_ISR          // 在中断中非法地创建任务
OS_ERR_TASK_INVALID             // 指向任务函数的指针为空
OS_ERR_TCB_INVALID              // 指向任务控制块的指针为空

  应用程序中创建的任务不能够使用数值为 01OS_CFG_PRIO_MAX-2OS_CFG_PRIO_MAX-1 的任务优先级,因此数值为 2 的任务优先级是应用程序中能够使用的最高任务优先级。

  通常,我们将开始任务(start_task)的任务优先级设置为 2 。这样做的目的是,在 start_task 任务完成应用程序初始化的过程中,会创建其他应用任务,其他应用任务的任务优先级应按照实际的应用需求进行设置,但只要其他应用任务的任务优先级小于 start_task 任务的任务优先级,那么 start_task 任务的一整个应用程序初始化流程就不会被其他应用程序打断。

  函数 OSTaskCreate() 的代码如下所示:

/*
************************************************************************************************************************
*                                                    CREATE A TASK
*
* Description: This function is used to have uC/OS-III manage the execution of a task.  Tasks can either be created
*              prior to the start of multitasking or by a running task.  A task cannot be created by an ISR.
*
* Arguments  : p_tcb          is a pointer to the task's TCB
*
*              p_name         is a pointer to an ASCII string to provide a name to the task.
*
*              p_task         is a pointer to the task's code
*
*              p_arg          is a pointer to an optional data area which can be used to pass parameters to
*                             the task when the task first executes.  Where the task is concerned it thinks
*                             it was invoked and passed the argument 'p_arg' as follows:
*
*                                 void Task (void *p_arg)
*                                 {
*                                     for (;;) {
*                                         Task code;
*                                     }
*                                 }
*
*              prio           is the task's priority.  A unique priority MUST be assigned to each task and the
*                             lower the number, the higher the priority.
*
*              p_stk_base     is a pointer to the base address of the stack (i.e. low address).
*
*              stk_limit      is the number of stack elements to set as 'watermark' limit for the stack.  This value
*                             represents the number of CPU_STK entries left before the stack is full.  For example,
*                             specifying 10% of the 'stk_size' value indicates that the stack limit will be reached
*                             when the stack reaches 90% full.
*
*              stk_size       is the size of the stack in number of elements.  If CPU_STK is set to CPU_INT08U,
*                             'stk_size' corresponds to the number of bytes available.  If CPU_STK is set to
*                             CPU_INT16U, 'stk_size' contains the number of 16-bit entries available.  Finally, if
*                             CPU_STK is set to CPU_INT32U, 'stk_size' contains the number of 32-bit entries
*                             available on the stack.
*
*              q_size         is the maximum number of messages that can be sent to the task
*
*              time_quanta    amount of time (in ticks) for time slice when round-robin between tasks.  Specify 0 to use
*                             the default.
*
*              p_ext          is a pointer to a user supplied memory location which is used as a TCB extension.
*                             For example, this user memory can hold the contents of floating-point registers
*                             during a context switch, the time each task takes to execute, the number of times
*                             the task has been switched-in, etc.
*
*              opt            contains additional information (or options) about the behavior of the task.
*                             See OS_OPT_TASK_xxx in OS.H.  Current choices are:
*
*                                 OS_OPT_TASK_NONE            No option selected
*                                 OS_OPT_TASK_STK_CHK         Stack checking to be allowed for the task
*                                 OS_OPT_TASK_STK_CLR         Clear the stack when the task is created
*                                 OS_OPT_TASK_SAVE_FP         If the CPU has floating-point registers, save them
*                                                             during a context switch.
*                                 OS_OPT_TASK_NO_TLS          If the caller doesn't want or need TLS (Thread Local
*                                                             Storage) support for the task.  If you do not include this
*                                                             option, TLS will be supported by default.
*
*              p_err          is a pointer to an error code that will be set during this call.  The value pointer
*                             to by 'p_err' can be:
*
*                                 OS_ERR_NONE                    If the function was successful
*                                 OS_ERR_ILLEGAL_CREATE_RUN_TIME If you are trying to create the task after you called
*                                                                   OSSafetyCriticalStart()
*                                 OS_ERR_PRIO_INVALID            If the priority you specify is higher that the maximum
*                                                                   allowed (i.e. >= OS_CFG_PRIO_MAX-1) or,
*                                 OS_ERR_STK_OVF                 If the stack was overflowed during stack init
*                                 OS_ERR_STK_INVALID             If you specified a NULL pointer for 'p_stk_base'
*                                 OS_ERR_STK_SIZE_INVALID        If you specified zero for the 'stk_size'
*                                 OS_ERR_STK_LIMIT_INVALID       If you specified a 'stk_limit' greater than or equal
*                                                                   to 'stk_size'
*                                 OS_ERR_TASK_CREATE_ISR         If you tried to create a task from an ISR
*                                 OS_ERR_TASK_INVALID            If you specified a NULL pointer for 'p_task'
*                                 OS_ERR_TCB_INVALID             If you specified a NULL pointer for 'p_tcb'
*
* Returns    : none
*
* Note(s)    : 1) OSTaskCreate() will return with the error OS_ERR_STK_OVF when a stack overflow is detected
*                 during stack initialization. In that specific case some memory may have been corrupted. It is
*                 therefore recommended to treat OS_ERR_STK_OVF as a fatal error.
************************************************************************************************************************
*/
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_ERR        *p_err)
{
    CPU_STK_SIZE   i;
#if (OS_CFG_TASK_REG_TBL_SIZE > 0u)
    OS_REG_ID      reg_nbr;
#endif
#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)
    OS_TLS_ID      id;
#endif

    CPU_STK       *p_sp;
    CPU_STK       *p_stk_limit;
    CPU_SR_ALLOC();

#ifdef OS_SAFETY_CRITICAL                                                       // 此宏通常用于使能检查 p_érr 指针的合法性
    if (p_err == (OS_ERR *)0) {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

#ifdef OS_SAFETY_CRITICAL_IEC61508                                              // 此宏用于使能 μC/OS-Ⅲ 对 IEC61508 国际标准的支持
    if (OSSafetyCriticalStartFlag == OS_TRUE) {
       *p_err = OS_ERR_ILLEGAL_CREATE_RUN_TIME;
        return;
    }
#endif

#if (OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u)                                        // 此宏用于使能检查是否在中断中非法调用相关函数
    // 全局变量 OSIntNestingCtr 为中断嵌套计数器,当OSIntNestingCtr 大于O时,说明当前处于中断中
    // 因此在中断中不能调用创建任务的函数,因此在中断中不能调用创建任务的函数,
    if (OSIntNestingCtr > 0u) {
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_TASK_CREATE_ISR;
        return;
    }
#endif

#if (OS_CFG_ARG_CHK_EN > 0u)                                                    // 此宏用于使能参数检查
    if (p_tcb == (OS_TCB *)0) {                                                 // 指向任务控制块的指针不能为空
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_TCB_INVALID;
        return;
    }
    if (p_task == (OS_TASK_PTR)0u) {                                            // 指向任务函数的指针不能为空
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_TASK_INVALID;
        return;
    }
    if (p_stk_base == (CPU_STK *)0) {                                           // 指向任务栈的指针不能为空
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_STK_INVALID;
        return;
    }
    if (stk_size < OSCfg_StkSizeMin) {                                          // 任务栈的大小不能小于配置项OsCfg_StkSizeMin 的值
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_STK_SIZE_INVALID;
        return;
    }
    if (stk_limit >= stk_size) {                                                // “水位"限制值不能超过任务栈的大小
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_STK_LIMIT_INVALID;
        return;
    }
    if ((prio  > (OS_CFG_PRIO_MAX - 2u)) && 
        (prio != (OS_CFG_PRIO_MAX - 1u))) {                                     // 任务优先级数值不能大于OS_CFG_PRIO_MAX-1
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_PRIO_INVALID;
        return;
    }
#endif

    // 判断任务优先级是否为最低的任务优先级(OS_CFG_PRIO_MAX-1)
    // 此任务优先级只能用于创建空闲任务,如果使用该任务优先级创建的任务不是空闲任务,则返回错误
    if (prio == (OS_CFG_PRIO_MAX - 1u)) {
#if (OS_CFG_TASK_IDLE_EN > 0u)
        if (p_tcb != &OSIdleTaskTCB) {
            OS_TRACE_TASK_CREATE_FAILED(p_tcb);
           *p_err = OS_ERR_PRIO_INVALID;
            return;
        }
#else
        OS_TRACE_TASK_CREATE_FAILED(p_tcb);
       *p_err = OS_ERR_PRIO_INVALID;
        return;
#endif
    }

    /**
     * 初始化任务控制块,初始化任务控制块实际上就是初始化任务控制块中成员变量的值,初始化后任务控制块中成员变量的值,如下:
     *  1、任务名(NamePtr):“?Task”
     *  2、任务启动时的时间戳(CyclesStart):OS_TS_GET()
     *  3、用于代码调式的任务名(DbgNamePtr):“ ”
     *  4、其余的成员变量全部初始化为 0 或空
     */
    OS_TaskInitTCB(p_tcb);

   *p_err = OS_ERR_NONE;                                                        //  初始化错误代码为“无错误”

    // 如果形参任务选项(opt)包含 OS_OPT_TASK_STK_CLR 则将任务栈清 0
    if (((opt & OS_OPT_TASK_STK_CHK) != 0u) ||
        ((opt & OS_OPT_TASK_STK_CLR) != 0u)) {
        if ((opt & OS_OPT_TASK_STK_CLR) != 0u) {
            p_sp = p_stk_base;
            for (i = 0u; i < stk_size; i++) {
               *p_sp = 0u;
                p_sp++;
            }
        }
    }

// 宏 CPU_CFG_STK_GROWTH 用于定义栈的生长方向,栈的生长方向是与硬件有关的,
// 当宏 CPU_CFG_STK_GROWTH 被定义为 CPU_STK_GROWTH_LO_TO_HI 时,表示栈的生长方向为向上生长(数据入栈后,CPU 的栈指针指向下一个更高的栈地址)
// 当宏 CPU_CFG_STK_GROWTH 被定义为 CPU_STK_GROWTH_HI_TO_LO 时,表示栈的生长方向为向下生长(数据入栈后,CPU 的栈指针指向下一个更低的栈地址)
#if (CPU_CFG_STK_GROWTH == CPU_STK_GROWTH_HI_TO_LO)
    // p_stk_limit 用于指向任务栈“水位”限制的地址
    // 当栈为向下生长时(STM32 就是这种情况),p_stk_limit 就应该为栈的起始地址 p_stk_base(也就是栈满时栈指针指向的地址)加上(与栈的生长方向相反)“水位”限制的大小
    p_stk_limit = p_stk_base + stk_limit;
#else
    //  当栈为向上生长时,p_stk_limit 就应该为栈的起始地址 p_stk_base(也就是栈空时栈指针指向的地址)加上栈的大小(计算出栈满时栈指针指向的地址)减去(与栈的生长方向相反)“水位”限制的大小
    p_stk_limit = p_stk_base + (stk_size - 1u) - stk_limit;
#endif

    /**
     * 初始化任务栈,在初始化任务栈的时候,会模拟 CPU 寄存器入栈的操作,将初始数据压入到任务栈中,并且每入栈一个数据,都会更新一次栈指针,
     * 一开始的栈指针是指向栈底(任务栈的高地址,因为 STM32 的栈是向下生长的)的,初始化后的栈指针将指向比栈底地址底的某个地址(具体由入栈数据的数量决定),
     * 任务栈初始化后,会返回此时的栈顶指针(p_sp),此时任务栈的初始化结果,从栈底开始,从高地址到低地址数据的含义及初始值,如下所示:
     * 初始化后的任务栈中,保存了任务的入口函数和函数的参数,以及任务意外返回后 CPU 执行的函数等信息
     */
    p_sp = OSTaskStkInit(p_task,
                         p_arg,
                         p_stk_base,
                         p_stk_limit,
                         stk_size,
                         opt);

#if (CPU_CFG_STK_GROWTH == CPU_STK_GROWTH_HI_TO_LO)                             // 检查任务栈初始化后是否溢出
    if (p_sp < p_stk_base) {
       *p_err = OS_ERR_STK_OVF;
        return;
    }
#else
    if (p_sp > (p_stk_base + stk_size)) {
       *p_err = OS_ERR_STK_OVF;
        return;
    }
#endif

#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)                                           // 此宏用于使能 RedZone 栈检查功能
    // 初始化任务的 RedZone 栈检查功能,实际上就是在任务栈的末端填充 OS_CFG_TASK_STK_REDZONE_DEPTH 个数据,所填充的数据值为 OS_STACK_CHECK_VAL,
    // 之后只要检查任务栈末端的 OS_CFG_TASK_STK_REDZONE_DEPTH 个数据是否被修改,就能够简单判断任务栈是否溢出
    OS_TaskStkRedzoneInit(p_stk_base, stk_size);
#endif

#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    p_tcb->TaskEntryAddr = p_task;                                              // 指向任务函数的指针
    p_tcb->TaskEntryArg  = p_arg;                                               // 指向任务函数的指针
#endif

#if (OS_CFG_DBG_EN > 0u)                                                        // 指向任务函数的指针
    p_tcb->NamePtr       = p_name;                                              // 指向任务名的指针
#else
    (void)p_name;
#endif

    p_tcb->Prio          = prio;                                                // 任务优先级

#if (OS_CFG_MUTEX_EN > 0u)                                                      // 此宏用于使能互斥信号量
    p_tcb->BasePrio      = prio;                                                // 任务原始优先级
#endif

    p_tcb->StkPtr        = p_sp;                                                // 更新任务的栈顶指针
    p_tcb->StkLimitPtr   = p_stk_limit;                                         // 指向任务栈“水位”限制的指针

#if (OS_CFG_SCHED_ROUND_ROBIN_EN > 0u)                                          // 此宏用于使能 µC/OS-Ⅲ 对时间片调度的支持
    // 设置任务时间片和任务默认时间片,每个任务的任务时间片是可以单独设置的
    p_tcb->TimeQuanta    = time_quanta;
    if (time_quanta == 0u) {
        p_tcb->TimeQuantaCtr = OSSchedRoundRobinDfltTimeQuanta;
    } else {
        p_tcb->TimeQuantaCtr = time_quanta;
    }
#else
    (void)time_quanta;
#endif

    p_tcb->ExtPtr        = p_ext;                                               // 指向用户自定义数据的指针

// OS_CFG_DBG_EN:此宏用于使能代码调试功能
// OS_CFG_STAT_TASK_STK_CHK_EN:此宏用于使能在进行任务统计时,检查任务栈的使用情况
// OS_CFG_TASK_STK_REDZONE_EN:此宏用于使能 RedZone 栈检查功能
#if ((OS_CFG_DBG_EN > 0u) || (OS_CFG_STAT_TASK_STK_CHK_EN > 0u) || (OS_CFG_TASK_STK_REDZONE_EN > 0u))
    p_tcb->StkBasePtr    = p_stk_base;                                          // 指向任务栈起始地址的指针
    p_tcb->StkSize       = stk_size;                                            // 任务栈大小
#endif
    p_tcb->Opt           = opt;                                                 // 任务操作选项

#if (OS_CFG_TASK_REG_TBL_SIZE > 0u)                                             // 此宏用于定义特定于任务的寄存器数量
    for (reg_nbr = 0u; reg_nbr < OS_CFG_TASK_REG_TBL_SIZE; reg_nbr++) {         // 将任务中所有特定寄存器清零
        p_tcb->RegTbl[reg_nbr] = 0u;
    }
#endif

#if (OS_CFG_TASK_Q_EN > 0u)                                                     // 此宏用于使能任务内嵌消息队列功能
    OS_MsgQInit(&p_tcb->MsgQ, q_size);                                          // 初始化任务内嵌消息队列
#else
    (void)q_size;
#endif

    OSTaskCreateHook(p_tcb);                                                    // 调用任务创建钩子函数

    OS_TRACE_TASK_CREATE(p_tcb);                                                // 用于调试
    OS_TRACE_TASK_SEM_CREATE(p_tcb, p_name);
#if (OS_CFG_TASK_Q_EN > 0u)
    OS_TRACE_TASK_MSG_Q_CREATE(&p_tcb->MsgQ, p_name);
#endif

#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)                  // 此宏用于定义任务本地存储寄存器的数量
    for (id = 0u; id < OS_CFG_TLS_TBL_SIZE; id++) {                             // 将所有任务本地存储寄存器清零
        p_tcb->TLS_Tbl[id] = 0u;
    }
    OS_TLS_TaskCreate(p_tcb);                                                   // 调用任务本地存储寄存器创建钩子函数
#endif

    CPU_CRITICAL_ENTER();                                                       // 保存中断状态,并进入临界区
    OS_PrioInsert(p_tcb->Prio);                                                 // 任务优先级插入就绪态任务优先级位图表
    OS_RdyListInsertTail(p_tcb);                                                // 任务插入就绪态任务链表

#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    OS_TaskDbgListAdd(p_tcb);                                                   // 将任务添加到调试链表中
#endif

    OSTaskQty++;                                                                // 更新系统中已创建任务的数量

    // 判断系统是否还未运行,如果系统未运行,则在退出临界区,并恢复中断状态后直接返回,否则触发任务调度
    if (OSRunning != OS_STATE_OS_RUNNING) {
        CPU_CRITICAL_EXIT();
        return;
    }

    CPU_CRITICAL_EXIT();

    OSSched();
}

  函数 OSTaskCreate()在创建一个任务的时候,会初始化任务控制块中的成员变量,但最重要的还是初始化的任务的任务栈,任务栈中就包含了任务在获得 CPU 使用权时,应该从哪里开始执行、任务函数意外返回,要返回到那里去、任务函数的参数以及任务执行过程中的 CPU 寄存器的值等信息。

三、开启任务调度器

  开始 µC/OS-Ⅲ 任务调度是 µC/OS-Ⅲ 内核开始运行前的最后一步,在此之前需要先调用函数 OSInit()对 µC/OS-Ⅲ 内核进行初始化,并至少创建一个应用任务。通过调用函数 OSStart() 就能够开始 µC/OS-Ⅲ 的任务调度。

  函数 OSStart() 的函数原型如下所示:

void  OSStart (OS_ERR  *p_err);

  函数 OSStart() 有一个形参 p_err,是一个指向接收错误代码变量的指针,如果函数 OSStart() 在开启 µC/OS-Ⅲ 任务调度的过程中,检测到错误,就会返回,并将错误代码通过形参 p_err 返回。如果函数 OSStart() 成功开启了 µC/OS-Ⅲ 的任务调度,那么函数 OSStart() 是不会返回的,会直接开始进行任务调度,并执行任务。

  函数 OSStart() 的代码如下所示:

/*
************************************************************************************************************************
*                                                 START MULTITASKING
*
* Description: This function is used to start the multitasking process which lets uC/OS-III manage the task that you
*              created.  Before you can call OSStart(), you MUST have called OSInit() and you MUST have created at least
*              one application task.
*
* Argument(s): p_err      is a pointer to a variable that will contain an error code returned by this function.
*
*                             OS_ERR_FATAL_RETURN    OS was running and OSStart() returned
*                             OS_ERR_OS_NOT_INIT     OS is not initialized, OSStart() has no effect
*                             OS_ERR_OS_NO_APP_TASK  No application task created, OSStart() has no effect
*                             OS_ERR_OS_RUNNING      OS is already running, OSStart() has no effect
*
* Returns    : none
*
* Note(s)    : 1) OSStartHighRdy() MUST:
*                 a) Call OSTaskSwHook() then,
*                 b) Load the context of the task pointed to by OSTCBHighRdyPtr.
*                 c) Execute the task.
*
*              2) OSStart() is not supposed to return.  If it does, that would be considered a fatal error.
************************************************************************************************************************
*/
void  OSStart (OS_ERR  *p_err)
{
    OS_OBJ_QTY  kernel_task_cnt;


#ifdef OS_SAFETY_CRITICAL                                                       // 此宏通常用于使能检查 p_érr 指针的合法性
    if (p_err == (OS_ERR *)0) {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

    /**
     * 在开始任务调度之前,必须先完成 μC/OS-Ⅲ 内核的初始化(函数 OSInit()),在函数 OSInit() 初始化完成后会将全局变量 OSInitialized 赋值为 OS_TRUE,
     * 因此判断全局变量 OSInitialized 的值,来判断用户是否在 μC/OS-Ⅲ 内核完成初始化之前,非法调用函数 OSStart() 开启 μC/OS-Ⅲ 任务调度
     */
    if (OSInitialized != OS_TRUE) {
       *p_err = OS_ERR_OS_NOT_INIT;
        return;
    }

    // 计算已创建的内核任务的数量
    // 内核任务一共有三个,分别为空闲任务、任务统计任务、软件定时器任务
    // 根据配置文件配置的不同,内核任务的数量可能不同
    kernel_task_cnt = 0u;
#if (OS_CFG_STAT_TASK_EN > 0u)
    kernel_task_cnt++;
#endif
#if (OS_CFG_TMR_EN > 0u)
    kernel_task_cnt++;
#endif
#if (OS_CFG_TASK_IDLE_EN > 0u)
    kernel_task_cnt++;
#endif

    //  在 µC/OS-Ⅲ 开始任务调度之前,用户必须要至少创建一个应用任务,
    // 因此通过已创建任务的数量是否不大于内核任务的数量,来判断用户在此之前是否没有创建应用任务
    if (OSTaskQty <= kernel_task_cnt) {
        *p_err = OS_ERR_OS_NO_APP_TASK;
         return;
    }

    //  判断系统是否已经开始运行,目的是为了方式重复启动 µC/OS-Ⅲ 内核
    if (OSRunning == OS_STATE_OS_STOPPED) {
        // 获取就绪态任务中最高的任务优先级,就绪态任务的任务优先级以位图的当时记录在数组 OSPrioTbl 中 
        // 通过计算前导零的方式就能够非常方便地获取就绪态任务中最高的任务优先级
        OSPrioHighRdy   = OS_PrioGetHighest();
        // 当前任务的任务优先级设置为最高的就绪态任务优先级,因为接下来就要运行这个任务优先级最高的就绪态任务了
        OSPrioCur       = OSPrioHighRdy;
  
        OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;                     // 根据任务优先级从就绪态任务链表中获取任务控制块

        // 将指向当前任务控制块的指针指向上面获取到的指针,因为接下来就要运行这个任务优先级最高的就绪态任务了
        OSTCBCurPtr     = OSTCBHighRdyPtr;
  
        OSRunning       = OS_STATE_OS_RUNNING;                                  // 标记系统已经启动,防止之后误操作,重复启动系统
  
        OSStartHighRdy();                                                       // 启动第一个任务
       *p_err           = OS_ERR_FATAL_RETURN;                                  // 成功启动第一个任务后,是不会返回的
    } else {
       *p_err           = OS_ERR_OS_RUNNING;
    }
}

  函数 OSStart() 进行完准备工作后,如果不存在错误,最后会通过调用函数 OSStartHighRdy() 进入到第一个任务中去,从此之后就不会在返回了,那么接下来看看函数 OSStartHighRdy() 是如何实现的。

四、任务切换

  任务切换的本质就是 CPU 寄存器的切换。假设当由任务 A 切换到任务 B 时,主要分为两步:

  1. 需暂停任务A的执行,并将此时任务 A 的寄存器保存到任务堆栈,这个过程叫做 保存现场
  2. 将任务 B 的各个寄存器值(被存于任务堆栈中)恢复到 CPU 寄存器中,这个过程叫做 恢复现场

  对任务 A 保存现场,对任务 B 恢复现场,这个整体的过程称之为:上下文切换

任务切换

  PendSV 异常是一个可以挂起的异常,这主要体现在 PendSV 异常能够被挂起而延迟执行,那么 RTOS 就能够利用 PendSV 异常的这一特点,等待 CPU 处理完其他重要的事务后,再处理 PendSV 异常。

  利用 PendSV 异常的可挂起特点,µC/OS-Ⅲ 将 PendSV 的中断优先级配置为最低的中断优先级,这么一来,PendSV 异常的中断服务函数就会在其他所有中断处理完成后才被执行。µC/OS-Ⅲ 就是将任务切换的过程放到 PendSV 异常的中断服务函数中处理的。

  要挂起 PendSV 异常(触发 PendSV 异常)也非常简单,只需将 ICSR 寄存器(中断控制状态寄存器)中断的位 28 PENDSVSET 位置 1,即可挂起 PendSV 异常,再挂起 PendSV 异常后,PendSV 的中断服务函数并不会立马被执行,但也不会被忽略,而是会等 CPU 处理完所有中断优先级不小于 PendSV 异常中断优先级的中断后,在再处理 PendSV 异常的中断处理函数。

  µC/OS-Ⅲ 要触发任务切换无需在应用程序代码中做任务特殊的处理,因为 µC/OS-Ⅲ 是一个抢占式的内核,系统会保证当前执行的任务,一定是系统中任务优先级最高的就绪态任务,因此 µC/OS-Ⅲ 会在一定的条件下自动触发任务切换,µC/OS-Ⅲ 自动触发任务切换的条件,如下表所示:

µCOS-Ⅲ自动触发任务切换条件表

  当上表中的条件触发时,µC/OS-Ⅲ 就会自动地触发任务切换。当然,要注意的是,触发任务切换并不一定会切换任务,而仅仅是保证系统运行的任务为系统中任务优先级最高的就绪态任务。在 µC/OS-Ⅲ 中有两个用于触发任务切换的函数,分别为函数 OSSched() 和函数 OSIntExit(),这两个函数的不同在于,函数 OSSched() 是在任务中使用的,而函数 OSIntExit() 是用于中断中的使用的。

void OSSched(void);
void OSIntExit(void);

  函数 OSSched() 用于在任务中触发任务切换,一般情况下,该函数会在满足上表中的条件(除中断嵌套结束和在应用程序中调用函数 OSSched())时,被 µC/OS-Ⅲ 自动调用。当然,应用程序也可以在任务中调用 OSSched(),手动触发任务切换。

/*
************************************************************************************************************************
*                                                      SCHEDULER
*
* Description: This function is called by other uC/OS-III services to determine whether a new, high priority task has
*              been made ready to run.  This function is invoked by TASK level code and is not used to reschedule tasks
*              from ISRs (see OSIntExit() for ISR rescheduling).
*
* Arguments  : none
*
* Returns    : none
*
* Note(s)    : 1) Rescheduling is prevented when the scheduler is locked (see OSSchedLock())
************************************************************************************************************************
*/
void  OSSched (void)
{
    CPU_SR_ALLOC();


#if (OS_CFG_INVALID_OS_CALLS_CHK_EN > 0u)                                       // 此宏用于使能检查μC/OS-III系统是否运行
    // 如果 μC/OS-Ⅲ 系统还未运行,那么是不能进行任务切换的
    if (OSRunning != OS_STATE_OS_RUNNING) {
        return;
    }
#endif

    // 通过中断嵌套计数器判断是否在中断中调用函数 OsSched(),函数 OSSched() 只能在任务中被调用
    if (OSIntNestingCtr > 0u) {
        return; 
    }

    // 通过任务调度器锁定嵌套计数器判断任务调度器是否被锁定,如果任务调度器被锁定,那么是不能进行任务切换的
    if (OSSchedLockNestingCtr > 0u) {
        return;
    }

    CPU_INT_DIS();                                                              // 屏蔽中断,相当于进入临界区

    // 取就绪态任务中最高的任务优先级
    // 就绪态任务的任务优先级以位图的当时记录在数组 OSPrioTb1 中
    // 通过计算前导零的方式就能够非常方便地获取就绪态任务中最高的任务优先级
    OSPrioHighRdy   = OS_PrioGetHighest();

#if (OS_CFG_TASK_IDLE_EN > 0u)                                                  // 此宏用于使能空闲任务
    OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;                         // 据任务优先级从就绪态任务链表中获取任务控制块
    // 如果获取到的任务任务控制块就是当前任务的任务控制块,那么就无需进行任务切换,于是直接恢复中断状态,并返回
    if (OSTCBHighRdyPtr == OSTCBCurPtr) { 
        CPU_INT_EN();
        return;
    }
#else
    //  如果没有使能空闲任务,但是获取到的任务优先级为 OS_CFG_PRIO_MAX-1,此任务优先级是分配给空闲任务的,其他任务是不能使用该任务优先级的,
    // 因为空闲任务没有启用,因此该任务优先级就不应该有对应的就绪态任务,因此就无需进行任务切换
    if (OSPrioHighRdy != (OS_CFG_PRIO_MAX - 1u)) {
        OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
        if (OSTCBHighRdyPtr == OSTCBCurPtr) {
            CPU_INT_EN();
            return;
        }
    }
#endif

    OS_TRACE_TASK_PREEMPT(OSTCBCurPtr);                                         // 用于调试

#if (OS_CFG_TASK_PROFILE_EN > 0u)                                               // 此宏用于使能任务分析功能
    OSTCBHighRdyPtr->CtxSwCtr++;                                                // *更新任务的执行次数,
#endif

// OS_CFG_TASK_PROFILE_EN:用于使能任务分析功能
// OS_CFG_DBG_EN:用于使能代码调试功能
#if ((OS_CFG_TASK_PROFILE_EN > 0u) || (OS_CFG_DBG_EN > 0u))
    // 更新系统执行任务切换的次数
    OSTaskCtxSwCtr++;
#endif

#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)                  // 此宏用于定义任务本地存储寄存器的数量
    // 更新系统中使用的 TLS指针,切换任务后,TLS 指针应指向任务切换后的任务的任务本地存储寄存器
    OS_TLS_TaskSw();
#endif

#if (OS_CFG_TASK_IDLE_EN > 0u)                                                  // 此宏用于使能空闲任务
    // 执行任务切换实际上仅仅是挂起 PendsV 异常,当处理完其他中断后,会自动进入 PendsV 异常的中断服务函数,进行任务切换
    OS_TASK_SW();
    // 恢复中断状态,相当于对出临界区
    CPU_INT_EN();
#else
    //  如果未使能空闲任务,那就需要判断任务优先级是否为 OS_CFG_PRIO_MAX-1,该任务优先级使能由空闲任务使用,
    // 因为未使能空闲任务,因此该任务优先级就不应该有对应的空闲任务因此就无需进行任务切换
    if ((OSPrioHighRdy != (OS_CFG_PRIO_MAX - 1u))) {
        OS_TASK_SW();                                           /* Perform a task level context switch                  */
        CPU_INT_EN();
    } else {
        // 无需进行任务切换,因此重新赋值 OSTCBHighRdyPtr
        OSTCBHighRdyPtr = OSTCBCurPtr;
        CPU_INT_EN();
        // 下面这个 for 循环实际上就是在未使能空闲任务的情况下,完成空闲任务应该完成的事务
        for (;;) {
#if ((OS_CFG_DBG_EN > 0u) || (OS_CFG_STAT_TASK_EN > 0u))
            CPU_CRITICAL_ENTER();
#if (OS_CFG_DBG_EN > 0u)
            OSIdleTaskCtr++;
#endif
#if (OS_CFG_STAT_TASK_EN > 0u)
            OSStatTaskCtr++;
#endif
            CPU_CRITICAL_EXIT();
#endif

#if (OS_CFG_APP_HOOKS_EN > 0u)
            OSIdleTaskHook();                                   /* Call user definable HOOK                             */
#endif
            // 任务会一直替空闲任务完成事务,直到系统中有其他就绪态任务使得 CPU 不再“空闲”
            if ((*((volatile OS_PRIO *)&OSPrioHighRdy) != (OS_CFG_PRIO_MAX - 1u))) {
                break;
            }
        }
    }
#endif

// 进行指令同步
#ifdef OS_TASK_SW_SYNC
    OS_TASK_SW_SYNC();
#endif
}

  函数 OSIntExit() 用于告诉 µC/OS-Ⅲ 一个中断的中断服务函数执行完毕,并在需要的时候进行任务切换。

/*
************************************************************************************************************************
*                                                       EXIT ISR
*
* Description: This function is used to notify uC/OS-III that you have completed servicing an ISR.  When the last nested
*              ISR has completed, uC/OS-III will call the scheduler to determine whether a new, high-priority task, is
*              ready to run.
*
* Arguments  : none
*
* Returns    : none
*
* Note(s)    : 1) You MUST invoke OSIntEnter() and OSIntExit() in pair.  In other words, for every call to OSIntEnter()
*                 (or direct increment to OSIntNestingCtr) at the beginning of the ISR you MUST have a call to OSIntExit()
*                 at the end of the ISR.
*
*              2) Rescheduling is prevented when the scheduler is locked (see OSSchedLock())
************************************************************************************************************************
*/
void  OSIntExit (void)
{
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)
    CPU_BOOLEAN  stk_status;
#endif
    CPU_SR_ALLOC();


    // 如果 µC/OS-III 系统还未运行,那么是不需要进行中断嵌套计数和任务切换的
    if (OSRunning != OS_STATE_OS_RUNNING) {
        OS_TRACE_ISR_EXIT();
        return;
    }

    CPU_INT_DIS();                                                              // 屏蔽中断,相当于进入临界区

    //  在调用函数 OSIntExit()前,应调用过函数 OSIntEnter()因此如果此时中断嵌套计数器为 0 的话,则返回
    if (OSIntNestingCtr == 0u) {
        OS_TRACE_ISR_EXIT();
        CPU_INT_EN();
        return;
    }

    OSIntNestingCtr--;                                                          // 更新中断嵌套计数器

    // 如果中断嵌套计数器还是大于 0 的话,说明当前还处于中断状态,因此直接返回,无需进行任务切换
    if (OSIntNestingCtr > 0u) {
        OS_TRACE_ISR_EXIT();
        CPU_INT_EN();
        return;
    }

    //  如果任务调度器锁定嵌套计数器大于 0 的话,说明当前任务调度器处于锁定状态,因此直接返回,无需进行任务切换
    if (OSSchedLockNestingCtr > 0u) {
        OS_TRACE_ISR_EXIT();
        CPU_INT_EN();
        return;
    }

//  对异常栈进行 RedZone 检查,主要判断异常栈是否溢出
#if (OS_CFG_ISR_STK_SIZE > 0u)
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)
    stk_status = OS_TaskStkRedzoneChk(OSCfg_ISRStkBasePtr, OSCfg_ISRStkSize);
    if (stk_status != OS_TRUE) {
        OSRedzoneHitHook((OS_TCB *)0);
    }
#endif
#endif

    // 获取就绪态任务中最高的任务优先级就绪态任务的任务优先级以位图的当时记录在数组 OSPrioTbl 中
    // 通过计算前导零的方式就能够非常方便地获取就绪态任务中最高的任务优先级
    OSPrioHighRdy   = OS_PrioGetHighest();

#if (OS_CFG_TASK_IDLE_EN > 0u)                                                  // 此宏用于使能空闲任务
    OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;                         // 根据任务优先级从就绪态任务链表中获取任务控制块
  
    // 如果获取到的任务任务控制块就是当前任务的任务控制块,那么就无需进行任务切换,于是在检查完任务栈后,恢复中断状态,并返回
    if (OSTCBHighRdyPtr == OSTCBCurPtr) {

//  对当前任务的任务栈进行 RedZone 检查,主要判断任务栈是否溢出
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)
        stk_status = OSTaskStkRedzoneChk((OS_TCB *)0);
        if (stk_status != OS_TRUE) {
            OSRedzoneHitHook(OSTCBCurPtr);
        }
#endif
        OS_TRACE_ISR_EXIT();
        CPU_INT_EN();
        OS_TRACE_TASK_SWITCHED_IN(OSTCBHighRdyPtr);             /* Do this here because we don't execute OSIntCtxSw().  */
        return;
    }
#else
    //  如果没有使能空闲任务,但是获取到的任务优先级为 OS_CFG_PRIO_MAX-1,此任务优先级是分配给空闲任务的,其他任务是不能使用该任务优先级的,
    // 因为空闲任务没有启用,因此该任务优先级就不应该有对应的就绪态任务,因此就无需进行任务切换
    if (OSPrioHighRdy != (OS_CFG_PRIO_MAX - 1u)) {
        OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
        if (OSTCBHighRdyPtr == OSTCBCurPtr) {
            OS_TRACE_ISR_EXIT();
            CPU_INT_EN();
            OS_TRACE_TASK_SWITCHED_IN(OSTCBHighRdyPtr);
            return;
        }
    }
#endif

#if (OS_CFG_TASK_PROFILE_EN > 0u)                                               // 此宏用于使能任务分析功能
    OSTCBHighRdyPtr->CtxSwCtr++;                                                // 更新任务的执行次数
#endif

// OS_CFG_TASK_PROFILE_EN:用于使能任务分析功能
// OS_CFG_DBG_EN:用于使能代码调试功能
#if ((OS_CFG_TASK_PROFILE_EN > 0u) || (OS_CFG_DBG_EN > 0u))
    OSTaskCtxSwCtr++;                                                           // 更新系统执行任务切换的次数
#endif

#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)                  // 此宏用于定义任务本地存储寄存器的数量
    // 更新系统中使用的 TLS 指针,切换任务后,TLS 指针应指向任务切换后的任务的任务本地存储寄存器
    OS_TLS_TaskSw();
#endif

    OS_TRACE_ISR_EXIT_TO_SCHEDULER();                                           // 用于调试

    // 执行任务切换,实际上仅仅是挂起 PendSV 异常,当处理完其他中断后,会自动进入 PendSV 异常的中断服务函数,进行任务切换
    OSIntCtxSw();

    CPU_INT_EN();                                                               // 恢复中断状态,相当于退出临界区
}

五、任务调用锁

  任务调度锁,用于对调度器上锁以及解锁的。当调度器上锁时则禁止任务调度,当解锁时则允许任务调度。我们可以多次调用 OSSchedLock() 对调度器上锁,解锁时需调用同样次数的 OSSchedUnlock() 才可解锁。

void OSSchedLock(OS_ERR *p_err);        // 调度器加锁
void OSSchedUnlock(OS_ERR *p_err);      // 调度器解锁

调度锁只是将调度器关闭,并不影响中断的执行,中断依旧正常触发,只是不会执行任务切换,它仅仅放防止了任务之间的资源争夺。

挂起调度器的方式,适用于临界区位于任务与任务之间,这样既不用去延迟中断,又可以做到临界区安全。

调度器加锁和调度器解锁必须成对使用,且支持嵌套使用。

六、任务调度

  调度器就是使用相关的调度算法来决定当前需要执行哪个任务,µC/OS-Ⅲ 支持以下任务调度方式:抢占式调度时间片调度

  • 抢占式调度:主要针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占优先级低的任务。
  • 时间片调度:主要针对优先级相同的任务,当多个任务的优先级相同且就绪时,任务调度器会根据用户所设置的时间片轮流的运行这些任务。

时间片是以一次系统节拍位单位。如 µC/OS-Ⅲ 默认设置的任务片为 100,则 µC/OS-Ⅲ 会在当前任务运行 100 次系统时钟节拍时的时间后,切换到另一个相同任务优先级的任务中运行。

6.1、抢占式调度

  在 µC/OS-Ⅲ 中,高优先级任务,优先执行,并且高优先级任务不停止,低优先级任务无法执行。被抢占的任务将会进入就绪态。

抢占式调度

  首先,这里创建三个任务:Task1、Task2、Task3。Task1、Task2、Task3 的优先级分别为 3、2、1;在 µC/OS-Ⅲ 中任务设置的数值越小,优先级越高,所以 Task3 的优先级最高。

  该程序在运行时,首先 Task1 在运行中,在这个过程中 Task2 就绪了,在抢占式调度器的作用下 Task2 会抢占 Task1 的运行。然后,在 Task2 运行过程中,Task3 就绪了,在抢占式调度器的作用下 Task3 会抢占 Task2 的运行。接着,Task3 运行过程中,Task3 阻塞了(系统延时或等待信号量等),此时就绪态中,优先级最高的任务 Task2 执行。随后,Task3 阻塞解除了(延时到了或者接收到信号量),此时 Task3 恢复到就绪态中,抢占 TasK2 的运行。

6.2、时间片调度

  同等优先级任务轮流地享有相同的 CPU 时间(可设置), 叫时间片,在 µC/OS-Ⅲ 中,一个时间片就等于 SysTick 中断周期。没有用完的时间片不会再使用,下次任务得到执行还是按照一个时间片的时钟节拍运行。

时间片调度

  首先,这里创建三个任务:Task1、Task2、Task3。Task1、Task2、Task3 的优先级均为 1,即 3 个任务同等优先级。然后,我们将三个任务的时间片都设置为 100。

  该程序在运行时,首先 Task1 运行完 100 个时间片后,切换至 Task2 运行。然后,Task2 运行完 100 个时间片后,切换至 Task3 运行。如果在 Task3 运行过程中(还不到 100 个时间片),Task3 阻塞了(系统延时或等待信号量等),此时直接切换到下一个任务 Task1。接着,Task1 运行完 100 个时间片后,切换至 Task2 运行,依次循环运行下去。

注意没有用完的时间片不会再使用,下次任务 Task3 得到执行还是按照 100 个时间片的时钟节拍运行。

  我们可以使用下面的函数开启时间片调度,并设置时间片的默认值。

void  OSSchedRoundRobinCfg (CPU_BOOLEAN en, OS_TICK dflt_time_quanta, OS_ERR *p_err);

  参数 en 是否使能时间片调度,OS_TRUE 使能,OS_FALSE 失能。

#define  OS_FALSE                       0u
#define  OS_TRUE                        1u

  参数 dflt_time_quanta 用来指定默认的时间片长度。

  参数 p_err 用来指向接收错误代码变量的指针。

  如果我们想要使用时间片调度,还需要把宏 OS_CFG_SCHED_ROUND_ROBIN_EN 置 1。

使能时间片调度

七、实验例程

  main() 函数:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
  
    UC_OS3_Demo();
  
    return 0;
}

  µC/OS-Ⅲ 例程入口函数:

/**
 * @brief µC/OS-Ⅲ例程入口函数
 * 
 */
void UC_OS3_Demo(void)
{
    OS_ERR error = {0};

    OSInit(&error);                                                             // 初始化µC/OS-Ⅲ

    // 创建开始任务
    OSTaskCreate((OS_TCB *   )  &start_task_tcb,                                // 任务控制块
                (CPU_CHAR *  )  "start_task",                                   // 任务名
                (OS_TASK_PTR )  Start_Task,                                     // 任务函数
                (void *      )  0,                                              // 任务参数
                (OS_PRIO     )  START_TASK_PRIORITY,                            // 任务优先级
                (CPU_STK *   )  start_task_stack,                               // 任务堆栈
                (CPU_STK_SIZE)  START_TASK_STACK_SIZE / 10,                     // 任务栈的使用警戒线
                (CPU_STK_SIZE)  START_TASK_STACK_SIZE,                          // 任务栈大小
                (OS_MSG_QTY  )  0,                                              // 消息队列长度
                (OS_TICK     )  0,                                              // 时间片长度
                (void *      )  0,                                              // 扩展内存
                (OS_OPT      )  (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),    // 任务选项
                (OS_ERR *    )  &error);                                        // 错误码

    OSStart(&error);                                                            // 开始任务调度
}

  START_TASK 任务配置:

/**
 * START_TASK 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define START_TASK_PRIORITY     5
#define START_TASK_STACK_SIZE   256

OS_TCB  start_task_tcb;
CPU_STK start_task_stack[START_TASK_STACK_SIZE];

void Start_Task(void *p_arg);

/**
 * @brief 开始任务的任务函数
 * 
 * @param p_arg 任务参数
 */
void Start_Task(void *p_arg)
{
    OS_ERR error = {0};
    CPU_INT32U cnts = 0;

    CPU_Init();                                                                 // 初始化CPU库

    CPU_SR_ALLOC();                                                             // 临界区保护

    cnts = HAL_RCC_GetSysClockFreq() / OS_CFG_TICK_RATE_HZ;
    OS_CPU_SysTickInit(cnts);                                                   // 根据配置的节拍频率配置SysTick中断及优先级

    OSSchedRoundRobinCfg(OS_TRUE, 10, &error);                                  // 设置时间片默认长度

    CPU_CRITICAL_ENTER();                                                       // 进入临界区

    // 创建任务1
    OSTaskCreate((OS_TCB *   )  &task1_tcb,                                     // 任务控制块
                (CPU_CHAR *  )  "task1",                                        // 任务名
                (OS_TASK_PTR )  Task1,                                          // 任务函数
                (void *      )  0,                                              // 任务参数
                (OS_PRIO     )  TASK1_PRIORITY,                                 // 任务优先级
                (CPU_STK *   )  task1_stack,                                    // 任务堆栈
                (CPU_STK_SIZE)  TASK1_STACK_SIZE / 10,                          // 任务栈的使用警戒线
                (CPU_STK_SIZE)  TASK1_STACK_SIZE,                               // 任务栈大小
                (OS_MSG_QTY  )  0,                                              // 消息队列长度
                (OS_TICK     )  0,                                              // 时间片长度,设置为0,则默认时间片长度
                (void *      )  0,                                              // 扩展内存
                (OS_OPT      )  (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),    // 任务选项
                (OS_ERR *    )  &error);                                        // 错误码

    // 创建任务2
    OSTaskCreate((OS_TCB *   )  &task2_tcb,                                     // 任务控制块
                (CPU_CHAR *  )  "task2",                                        // 任务名
                (OS_TASK_PTR )  Task2,                                          // 任务函数
                (void *      )  0,                                              // 任务参数
                (OS_PRIO     )  TASK2_PRIORITY,                                 // 任务优先级
                (CPU_STK *   )  task2_stack,                                    // 任务堆栈
                (CPU_STK_SIZE)  TASK2_STACK_SIZE / 10,                          // 任务栈的使用警戒线
                (CPU_STK_SIZE)  TASK2_STACK_SIZE,                               // 任务栈大小
                (OS_MSG_QTY  )  0,                                              // 消息队列长度
                (OS_TICK     )  5,                                              // 时间片长度
                (void *      )  0,                                              // 扩展内存
                (OS_OPT      )  (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),    // 任务选项
                (OS_ERR *    )  &error);                                        // 错误码

    CPU_CRITICAL_EXIT();                                                        // 退出临界区

    OSTaskDel(NULL, &error);                                                    // 删除任务
}

  TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK1_PRIORITY          4
#define TASK1_STACK_SIZE        256

OS_TCB  task1_tcb;
CPU_STK task1_stack[TASK1_STACK_SIZE];

void Task1(void *p_arg);

/**
 * @brief 任务1的任务函数
 * 
 * @param p_arg 任务参数
 */
void Task1(void *p_arg)
{
    uint32_t count = 0;

    CPU_SR_ALLOC();                                                             // 临界区保护

    while (1)
    {
        CPU_CRITICAL_ENTER();                                                   // 进入临界区
        printf("Task1运行次数:%ld\r\n", ++count);
        CPU_CRITICAL_EXIT();                                                    // 退出临界区
    }
}

  TASK2 任务配置:

/**
 * TASK2 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK2_PRIORITY          4
#define TASK2_STACK_SIZE        256

OS_TCB  task2_tcb;
CPU_STK task2_stack[TASK2_STACK_SIZE];

void Task2(void *p_arg);

/**
 * @brief 任务2的任务函数
 * 
 * @param p_arg 任务参数
 */
void Task2(void *p_arg)
{
    uint32_t count = 0;

    CPU_SR_ALLOC();                                                             // 临界区保护

    while (1)
    {
        CPU_CRITICAL_ENTER();                                                   // 进入临界区
        printf("Task2运行次数:%ld\r\n", ++count);
        CPU_CRITICAL_EXIT();                                                    // 退出临界区
    }
}
posted @ 2024-02-12 16:25  星光映梦  阅读(35)  评论(0编辑  收藏  举报