07. µCOS-Ⅲ的信号量

一、µC/OS-Ⅲ的信号量简介

  信号量 是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后,才继续执行;而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源。

  信号量用于管理共享资源的场景相当于对共享资源上了个锁,只有任务成功获取到了锁的要是,才能够访问这个共享资源,访问完共享资源后还得归还要是,当然钥匙可以不只一把,即信号量可以有多个资源。

二、二值信号量

2.1、二值信号量简介

  二值信号量 实际上就是只有两种情况的信号量,二值信号量只有两种情况,分别为有资源和无资源,这也就意味着,如果把二值信号量比喻成一把锁,那么这把锁只有一把钥匙,当一个任务获取了这个二值信号量之后,在这个任务释放这个二值信号量值之前,其它任务都是获取不到这个二值信号量的。二值信号量通常用于互斥访问或任务同步。

  二值信号量有可能会导致严重的 优先级翻转问题优先级翻转问题 指的是,当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放这个二值信号量,而在这之前,如果有一个优先级介于高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行,这么一来,这三个任务中优先级最高的任务反而要最后才能运行。

  信号量的组成可以简单地理解为是一个资源计数器和一个任务等待挂起链表组成的,二值信号量与下文要讲解的计数型信号量的最大的不同之处就在于,组成二值信号量的资源计数器只会用来表示 01,一次来区分无资源和有资源的两种状态,而计数型信号量的资源计数器就能够表示 0 ~ 计数型信号量 最大资源数的多种情况,组成信号量的结构体(二值信号量和计数型信号量共用一个结构体),该结构体定在文件 os.h 中,具体的代码如下所示:

struct  os_sem {
#if (OS_OBJ_TYPE_REQ > 0u)                                                      // 此宏用于使能检查内核对象的类型
    // 对象类型,在信号量初始化时,信号量的类型会被初始化为OS_OBJ_TYPE_SEM
    OS_OBJ_TYPE          Type;
#endif
#if (OS_CFG_DBG_EN > 0u)                                                        // 此宏用于使能代码调试功能
    // 如果使能了代码调试功能,那么就会为每一个信号量赋一个名称,方便调试
    CPU_CHAR            *NamePtr;
#endif
    // 挂起等待任务链表当任务因获取了无资源的信号量时,就可以选择被挂起到该链表,等待信号量的资源
    OS_PEND_LIST         PendList;                                              // 此宏用于使能代码调试功能
#if (OS_CFG_DBG_EN > 0u)
    // 一些与代码调试相关的成员变量
    OS_SEM              *DbgPrevPtr;
    OS_SEM              *DbgNextPtr;
    CPU_CHAR            *DbgNamePtr;
#endif
    // 信号量的资源计数器,通过该成员变量判断信号量是否有资源
    OS_SEM_CTR           Ctr;
#if (OS_CFG_TS_EN > 0u)                                                         // 以下的成员变量用于第三方的调试工具
    CPU_TS               TS;
#endif
#if (defined(OS_CFG_TRACE_EN) && (OS_CFG_TRACE_EN > 0u))
    CPU_ADDR             SemID;
#endif
};

  从上面的代码中可以看出,信号量的结构体中,有两个最重要的成员变量,分别为 PendList(任务等待挂起链表)和 Ctr(信号量资源计数器)。

2.2、二值信号量相关的API函数

  µC/OS-Ⅲ 提供了信号量的一些相关操作 API 函数,前面也说到了,二值信号量与计数型信号量是很相似的,因此二值信号量与计数型信号量是共用一套 API 函数的,这些操作函数如下所示。

2.2.1、创建一个信号量

void  OSSemCreate (OS_SEM      *p_sem,      // 指向信号量结构体的指针
                   CPU_CHAR    *p_name,     // 指向作为信号量名的 ASCII 字符串的指针
                   OS_SEM_CTR   cnt,        // 信号量资源数的初始值
                   OS_ERR      *p_err);     // 指向接收错误代码变量的指针

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

OS_ERR_NONE                         // 信号量创建成功
OS_ERR_CREATE_ISR                   // 在中断中非法调用该函数
OS_ERR_ILLEGAL_CREATE_RUN_TIME      // 在系统运行过程中非法创建内核对象
OS_ERR_OBJ_PTR_NULL                 // 指向信号量结构体的指针为空

2.2.2、删除一个信号量

// 返回值:删除信号量时,被终止挂起任务的数量
OS_OBJ_QTY  OSSemDel (OS_SEM  *p_sem,           // 指向信号量结构体的指针
                      OS_OPT   opt,             // 函数操作选项
                      OS_ERR  *p_err);          // 指向接收错误代码变量的指针

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

OS_ERR_NONE                     // 信号量删除成功
OS_ERR_DEL_ISR                  // 在中断中非法调用该函数
OS_ERR_ILLEGAL_DEL_RUN_TIME     // 定义了 OS_SAFETY_CRITICAL_IEC61508,且在 OSStart() 之后非法地删除内核对象
OS_ERR_OBJ_PTR_NULL             // 指向信号量结构体的指针为空
OS_ERR_OBJ_TYPE                 // 待被删除内核对象的类型不是信号量
OS_ERR_OPT_INVALID              // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING            // μC/OS-IⅢ 内核还未运行
OS_ERR_TASK_WAITING             // 有任务挂起等待待被删除的信号量

2.2.3、尝试获取信号量资源

// 返回值:信号量资源数更新后的值
OS_SEM_CTR  OSSemPend (OS_SEM   *p_sem,         // 指向信号量结构体的指针
                       OS_TICK   timeout,       // 任务挂起等待信号量的最大允许时间
                       OS_OPT    opt,           // 函数操作选项
                       CPU_TS   *p_ts,          // 指向接收信号量接收时的时间戳的变量的指针
                       OS_ERR   *p_err);        // 指向接收错误代码变量的指针

  函数 OSSemPend() 的函数操作选项描述,如下所示:

OS_OPT_PEND_BLOCKING            // 如果信号量没有资源的话就阻塞任务
OS_OPT_PEND_NON_BLOCKING        // 如果信号量没有资源任务就直接返回

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

OS_ERR_NONE                     // 成功获取信号量
OS_ERR_OBJ_DEL                  // 待获取的信号量已经被删除
OS ERR_OBJ_PTR_NULL             // 指向信号量结构体的指针为空
OS_ERR_OBJ_TYPE                 // 待被删除内核对象的类型不是信号量
OS_ERR_OPT_INVALID              // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING            // μC/OS-IⅢ 内核还未运行
OS_ERR_PEND_ABORT               // 任务挂起等待信号量时被终止 (还未超时)
OS_ERR_PEND_ISR                 // 在中断中非法调用该函数
OS_ERR_PEND_WOULD_BLOCK         // 获取的信号量没有资源,并且不挂起任务等待信号量
OS_ERR_SCHED_LOCKED             // 任务调度器已锁定
OS_ERR_STATUS_INVALID           // 无效的任务挂起结果
OS_ERR_TIMEOUT                  // 任务挂起等待信号量超时

2.2.4、释放信号量资源

// 返回值:信号量资源数更新后的值
OS_SEM_CTR  OSSemPost (OS_SEM  *p_sem,      // 指向信号量结构体的指针
                       OS_OPT   opt,        // 函数操作选项
                       OS_ERR  *p_err);     // 指向接收错误代码变量的指针

  函数 OSSemPost() 的函数操作选项描述,如下所示:

OS_OPT_POST_1               // 只给一个任务(最高优先级的)发信号
OS_OPT_POST_ALL             // 给所有等待该信号量的任务发信号
OS_OPT_POST_NO_SCHED        // 禁止在本函数内执行任务调度

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

OS_ERR_NONE             // 释放信号量成功
OS_ERR_OBJ_PTR_NULL     // 指向信号量结构体的指针为空
OS_ERR_OBJ_TYPE         // 操作的内核对象的类型不是信号量
OS_ERR_OPT_INVALID      // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING    // µC/OS-Ⅲ 内核还未运行
OS_ERR_SEM_OVF          // 信号量资源数溢出

2.2.5、终止任务挂起等待信号量资源

// 返回值:被终止挂起的任务数量
OS_OBJ_QTY  OSSemPendAbort (OS_SEM  *p_sem,         // 指向信号量结构体的指针
                            OS_OPT   opt,           // 函数操作选项
                            OS_ERR  *p_err);        // 指向接收错误代码变量的指针

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

OS_ERR_NONE                 // 终止任务挂起等待信号量成功
OS_ERR_OBJ_PTR_NULL         // 指向信号量结构体的指针为空
OS_ERR_OBJ_TYPE             // 操作的内核对象的类型不是信号量
OS_ERR_OPT_INVALID          // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING        // μC/OS-Ⅲ 内核还未运行
OS_ERR_PEND_ABORT_ISR       // 在中断中非法调用该函数
OS_ERR_PEND_ABORT_NONE      // 没有任务挂起等待该信号量

2.2.6、强制设置信号量的资源数

void  OSSemSet (OS_SEM      *p_sem,         // 指向信号量结构体的指针
                OS_SEM_CTR   cnt,           // 信号量资源数设置后的值
                OS_ERR      *p_err);        // 指向接收错误代码变量的指针

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

OS_ERR_NONE             // 强制设置信号量资源数成功
OS_ERR_OBJ_PTR_NULL     // 指向信号量结构体的指针为空
OS_ERR_OBJ_TYPE         // 操作的内核对象的类型不是信号量
OS_ERR_SET_ISR          // 在中断中非法调用该函数
OS_ERR_TASK_WAITING     // 有任务挂起等待信号量,不能修改该信号量的资源数

2.3、实验例程

  main() 函数内容如下:

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

    UART_Init(&g_usart1_handle, USART1, 115200);

    Key_Init();
  
    UC_OS3_Demo();

    return 0;
}

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

OS_SEM binary_sem;                                                              // 二值信号量

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

    OSInit(&error);                                                             // 初始化µC/OS-Ⅲ
  
    OSSemCreate(&binary_sem, "binary sem", 1, &error);                          // 创建二值信号量

    // 创建开始任务
    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     1
#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中断及优先级

    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);                                        // 错误码

    CPU_CRITICAL_EXIT();                                                        // 退出临界区

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

  TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK1_PRIORITY          2
#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)
{
    OS_ERR error = {0};
    OS_SEM_CTR ctr = 0;

    while (1)
    {
        ctr = OSSemPend(&binary_sem, 0, OS_OPT_PEND_BLOCKING, 0, &error);       // 获取信号量
        printf("任务1获取信号量成功\r\n");
        printf("任务1获取信号量成功后的资源数的计数值:%d\r\n", ctr);

        ctr = OSSemPost(&binary_sem, OS_OPT_POST_1,&error);                     // 释放信号量
        printf("任务1释放信号量成功\r\n");
        printf("任务1释放信号量成功后的资源数的计数值:%d\r\n", ctr);

        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}

三、计数型信号量

  计数型信号量 与二值信号量是很相似的 ,但是计数型信号量的资源数大于1,也就是它的资源不止 0 和 1。计数型信号量适用以下场合:

  • 事件计数:当每次事件发生后,在事件处理函数中释放计数型信号量(计数值 + 1),其他任务会获取计数型信号量(计数值 - 1) ,这种场合一般在创建时将初始计数值设置为 0。
  • 资源管理:信号量表示有效的资源数目。任务必须先获取信号量(信号量计数值 - 1)才能获取资源控制权。当计数值减为零时表示没有的资源。当任务使用完资源后,必须释放信号量(信号量计数值 + 1)。信号量创建时计数值应等于最大资源数目。

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

OS_SEM count_sem;                                                               // 计数型信号量

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

    OSInit(&error);                                                             // 初始化µC/OS-Ⅲ
  
    OSSemCreate(&count_sem, "count sem", 0, &error);                            // 创建计数型信号量

    // 创建开始任务
    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     1
#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中断及优先级

    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     )  0,                                              // 时间片长度,设置为0,则默认时间片长度
                (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          2
#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)
{
    OS_ERR error = {0};
    OS_SEM_CTR ctr = 0;

    while (1)
    {
        switch (Key_Scan(0))
        {
        case KEY1_PRESS:
            ctr = OSSemPost(&count_sem, OS_OPT_POST_1,&error);                  // 释放信号量
            printf("任务1释放信号量成功\r\n");
            printf("任务1释放信号量成功后的资源数的计数值:%d\r\n", ctr);
            break;
  
        default:
            break;
        }

        OSTimeDly(10, OS_OPT_TIME_DLY, &error);
    }
}

  新建 TASK2 任务配置:

/**
 * TASK2 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK2_PRIORITY          2
#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)
{
    OS_ERR error = {0};
    OS_SEM_CTR ctr = 0;
  
    while (1)
    {
        ctr = OSSemPend(&count_sem, 0, OS_OPT_PEND_BLOCKING, 0, &error);        // 获取信号量
        printf("任务2获取信号量成功\r\n");
        printf("任务2获取信号量成功后的资源数的计数值:%d\r\n", ctr);
        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}

计数型信号量和二值信号量的 API 函数是公用的。

四、优先级翻转

  优先级翻转 是指高优先级的任务反而慢执行,低优先级的任务反而优先执行。在使用二值信号量和计数型信号量的时候,会经常地遇到优先级翻转的问题,优先级翻转的问题在抢占式内核中是很常见的,但是在实时操作系统中是不允许出现优先级翻转的现象的,因为优先级翻转会破坏任务执行的预期顺序,可能会导致未知的严重后果。

优先级翻转

  如上图所示,定义:任务 H 为优先级最高的任务,任务 L 为优先级最低的任务,任务 M 为优先级介于任务 H 与任务 L 之间的任务。

  1. 任务 H 和任务 M 为挂起状态,等待某一事件发生,此时任务 L 正在运行。
  2. 此时任务 L 要访问共享资源,因此需要获取信号量。
  3. 任务 L 成功获取信号量,并且此时信号量已无资源,任务 L 开始访问共享资源。
  4. 此时任务 H 就绪,抢占任务 L 运行。
  5. 任务 H 开始运行。
  6. 此时任务 H 要访问共享资源,因此需要获取信号量,但信号量已无资源,因此任务 H 挂起等待信号量资源。
  7. 任务 L 继续运行。
  8. 此时任务 M 就绪,抢占任务 L 运行。
  9. 任务 M 正在运行。
  10. 任务 M 运行完毕,继续挂起。
  11. 任务 L 继续运行。
  12. 此时任务 L 对共享资源的访问操作完成,释放信号量,虽有任务 H 因成功获取信号量,解除挂起状态并抢占任务 L 运行。
  13. 任务 H 得以运行。

  从上面优先级翻转的示例中,可以看出,任务 H 为最高优先级的任务,因此任务 H 执行的操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务 H 需要等到任务 L 释放信号量才能够运行,并且,任务 L 还会被其他介于任务 H 与任务 L 任务优先级之间的任务 M 抢占,因此任务 H 还需等待任务 M 运行完毕,这显然不符合任务 H 需要的高实时性要求。

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

OS_SEM binary_sem;                                                              // 二值信号量

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

    OSInit(&error);                                                             // 初始化µC/OS-Ⅲ
  
    OSSemCreate(&binary_sem, "binary sem", 1, &error);                          // 创建二值信号量

    // 创建开始任务
    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     1
#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中断及优先级

    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     )  0,                                              // 时间片长度,设置为0,则默认时间片长度
                (void *      )  0,                                              // 扩展内存
                (OS_OPT      )  (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),    // 任务选项
                (OS_ERR *    )  &error);                                        // 错误码

    // 创建任务3
    OSTaskCreate((OS_TCB *   )  &task3_tcb,                                     // 任务控制块
                (CPU_CHAR *  )  "task3",                                        // 任务名
                (OS_TASK_PTR )  Task3,                                          // 任务函数
                (void *      )  0,                                              // 任务参数
                (OS_PRIO     )  TASK3_PRIORITY,                                 // 任务优先级
                (CPU_STK *   )  task3_stack,                                    // 任务堆栈
                (CPU_STK_SIZE)  TASK3_STACK_SIZE / 10,                          // 任务栈的使用警戒线
                (CPU_STK_SIZE)  TASK3_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);                                        // 错误码

    CPU_CRITICAL_EXIT();                                                        // 退出临界区

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

  修改 TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK1_PRIORITY          2
#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)
{
    OS_ERR error = {0};

    while (1)
    {
        printf("高优先级的任务正在获取二值信号量\r\n");
        OSSemPend(&binary_sem, 0, OS_OPT_PEND_BLOCKING, 0, &error);             // 获取二值信号量
        printf("高优先级的任务获取成功二值信号量\r\n");

        printf("高优先级的任务正在运行\r\n");
        Delay_ms(1000);

        printf("高优先级的任务正在释放二值信号量\r\n");
        OSSemPost(&binary_sem, OS_OPT_POST_1, &error);                          // 释放二值信号量
        printf("高优先级的任务释放成功二值信号量\r\n");

        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}

  修改 TASK2 任务配置:

/**
 * TASK2 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK2_PRIORITY          3
#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)
{
    OS_ERR error = {0};
  
    while (1)
    {
        printf("中等优先级的任务正在运行\r\n");
        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}

  新建 TASK3 任务配置:

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

OS_TCB  task3_tcb;
CPU_STK task3_stack[TASK3_STACK_SIZE];

void Task3(void *p_arg);

/**
 * @brief 任务3的任务函数
 * 
 * @param p_arg 任务参数
 */
void Task3(void *p_arg)
{
    OS_ERR error = {0};
  
    while (1)
    {
        printf("低优先级的任务正在获取二值信号量\r\n");
        OSSemPend(&binary_sem, 0, OS_OPT_PEND_BLOCKING, 0, &error);             // 获取二值信号量
        printf("低优先级的任务获取成功二值信号量\r\n");

        printf("低优先级的任务正在运行\r\n");
        Delay_ms(3000);

        printf("低优先级的任务正在释放二值信号量\r\n");
        OSSemPost(&binary_sem, OS_OPT_POST_1, &error);                          // 释放二值信号量
        printf("低优先级的任务释放成功二值信号量\r\n");

        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}

五、互斥信号量

5.1、互斥信号量简介

  互斥信号量 也叫 互斥锁,可以理解为是一种特殊的二值信号量,互斥信号量拥有优先级继承的机制使得互斥信号量能够在一定的程度上解决优先级翻转的问题。互斥信号量一般用于那些需要互斥访问的应用中。在互斥访问的应用中,互斥信号量就相当于是一把钥匙,当任务想要访问共享资源的时候,就必须先获取到这把钥匙,当任务访问完共享资源后,就必须归还这把钥匙,这样其他任务就可以拿着这把钥匙再去访问这个共享资源。

  互斥信号量的优先级继承机制体现在,当一个互斥信号量正被一个低优先级的任务持有时,如果此时有一个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会因获取不到互斥锁而被挂起,不过接下来,高优先级的任务会将持有互斥信号量的低优先级任务的任务优先级提成到与高优先级任务的任务优先级相同的任务优先级,这个过程就是 优先级继承。优先级继承可以尽可能地减少高优先级任务挂起等待互斥锁的时间,并且将优先级翻转问题带来的影响降到最低。

  但是优先级继承并不是能完全解决优先级翻转带来的问题,因为优先级继承仅仅是将持有互斥信号量的低优先级任务的任务优先级提高的与高优先级任务相同的任务优先级,而非直接将互斥信号量直接从低优先级的任务手上 “抢” 过来,因此高优先级的任务还是需要等待低优先级的任务释放互斥信号量,高优先级的任务才能够获取到互斥信号量。

优先级继承

优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。

互斥信号量不能用于中断服务函数中,原因如下:

  • 互斥信号量有任务优先级继承的机制, 但是中断不是任务,没有任务优先级, 所以互斥信号量只能用于任务中,不能用于中断服务函数。
  • 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

5.2、互斥信号量相关 API 函数

5.2.1、创建一个互斥信号量

void  OSMutexCreate (OS_MUTEX  *p_mutex,            // 指向互斥信号量结构体的指针
                     CPU_CHAR  *p_name,             // 指向作为互斥信号量名的 ASCII 字符串的指针
                     OS_ERR    *p_err);             // 指向接收错误代码变量的指针

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

OS_ERR_NONE                         // 互斥信号量创建成功
OS_ERR_CREATE_ISR                   // 在中断中非法调用该函数
OS_ERR_ILLEGAL_CREATE_RUN_TIME      // 在系统运行过程中非法创建内核对象
OS_ERR_OBJ_PTR_NULL                 // 指向互斥信号量结构体的指针为空

创建互斥信号量时,默认信号量有效。

5.2.2、删除一个互斥信号量

// 返回值:删除互斥信号量时,被终止挂起任务的数量
OS_OBJ_QTY  OSMutexDel (OS_MUTEX  *p_mutex,     // 指向互斥信号量结构体的指针
                        OS_OPT     opt,         // 函数操作选项
                        OS_ERR    *p_err);      // 指向接收错误代码变量的指针

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

OS_ERR_NONE                         // 互斥信号量删除成功
OS_ERR_DEL_ISR                      // 在中断中非法调用该函数
OS_ERR_ILLEGAL_DEL_RUN_TIME         // 在系统运行过程中非法创建内核对象
OS_ERR_OBJ_PTR_NULL                 // 指向互斥信号量结构体的指针为空
OS_ERR_OBJ_TYPE                     // 操作的内核对象的类型不是互斥信号量
OS_ERR_OPT_INVALID                  // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING                // µC/OS-Ⅲ 内核还未运行
S_ERR_TASK_WAITING                  // 有任务挂起等待互斥信号量,不能删除互斥信号量

5.2.3、尝试获取互斥信号量

void  OSMutexPend (OS_MUTEX  *p_mutex,      // 指向互斥信号量结构体的指针
                   OS_TICK    timeout,      // 任务挂起等待互斥信号量的最大允许时间
                   OS_OPT     opt,          // 函数操作选项
                   CPU_TS    *p_ts,         // 指向接收互斥信号量接收时的时间戳的变量的指针
                   OS_ERR    *p_err);       // 指向接收错误代码变量的指针

  函数 OSMutexPend() 的函数操作选项描述,如下所示:

OS_OPT_PEND_BLOCKING        // 如果信号量没有资源的话就阻塞任务
OS_OPT_PEND_BLOCKING        // 如果信号量没有资源的话就阻塞任务

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

OS_ERR_NONE                 // 成功获取互斥信号量
OS_ERR_MUTEX_OWNER          // 任务重复获取互斥信号量
OS_ERR_MUTEX_OVF            // 互斥信号量持有递归计数器计溢出
OS_ERR_OBJ_DEL              // 待获取的互斥信号量已经被删除
OS_ERR_OBJ_PTR_NULL         // 指向互斥信号量结构体的指针为空
OS_ERR_OBJ_TYPE             // 待获取内核对象的类型不是互斥信号量
OS_ERR_OPT_INVALID          // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING        // μC/OS-Ⅲ 内核还未运行
OS_ERR_PEND_ABORT           // 任务挂起等待互斥信号量时被终止(还未超时)
OS_ERR_PEND_ISR             // 在中断中非法调用该函数
OS_ERR_PEND_WOULD_BLOCK     // 获取互斥信号量失败,并且不挂起任务等待互斥信号量
OS_ERR_SCHED_LOCKED         // 任务调度器已锁定
OS_ERR_STATUS_INVALID       // 无效的任务挂起结果
OS_ERR_TIMEOUT              // 无效的任务挂起结果

5.2.4、释放互斥信号量

void  OSMutexPost (OS_MUTEX  *p_mutex,          // 指向互斥信号量结构体的指针
                   OS_OPT     opt,              // 函数操作选项
                   OS_ERR    *p_err);           // 指向接收错误代码变量的指针

  函数 OSMutexPost() 的函数操作选项描述,如下所示:

OS_OPT_POST_NONE            // 不指定特定的选项
OS_OPT_POST_NO_SCHED        // 禁止在本函数内执行任务调度

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

OS_ERR_NONE                     // 释放互斥信号量成功
OS_ERR_MUTEX_NESTING            // 还未完全释放互斥信号量(持有递归计数器不为0)
OS_ERR_MUTEX_NOT_OWNER          // 释放互斥信号量的任务不是互斥信号量的持有者
OS_ERR_OBJ_PTR_NULL             // 指向互斥信号量结构体的指针为空
OS_ERR_OBJ_TYPE                 // 操作的内核对象的类型不是互斥信号量
OS_ERR_OPT_INVALID              // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING            // μC/OS-Ⅲ 内核还未运行
OS_ERR_POST_ISR                 // 在中断中非法调用该函数

5.2.5、终止任务挂起等待互斥信号量

// 返回值:被终止挂起任务的数量
OS_OBJ_QTY  OSMutexPendAbort (OS_MUTEX  *p_mutex,       // 指向互斥信号量结构体的指针
                              OS_OPT     opt,           // 函数操作选项
                              OS_ERR    *p_err);        // 指向接收错误代码变量的指针

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

OS_ERR_NONE                     // 终止任务挂起等待互斥信号量成功
OS_ERR_OBJ_PTR_NULL             // 指向互斥信号量结构体的指针为空
OS_ERR_OBJ_TYPE                 // 操作的内核对象的类型不是互斥信号量
OS_ERR_OPT_INVALID              // 无效的函数操作选项
OS_ERR_OS_NOT_RUNING            // μC/OS-Ⅲ 内核还未运行
OS_ERR_PEND_ABORT_ISR           // 在中断中非法调用该函数
OS_ERR_PEND_ABORT_NONE          // 没有任务挂起等待该互斥信号量

5.3、实验例程

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

OS_MUTEX mutex_sem;                                                             // 互斥信号量

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

    OSInit(&error);                                                             // 初始化µC/OS-Ⅲ
  
    OSMutexCreate(&mutex_sem, "mutex sem", &error);                             // 创建互斥信号量

    // 创建开始任务
    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);                                                            // 开始任务调度
}

  修改 TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括:任务优先级 任务栈大小 任务控制块 任务栈 任务函数
 */
#define TASK1_PRIORITY          2
#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)
{
    OS_ERR error = {0};

    while (1)
    {
        printf("高优先级的任务正在获取互斥信号量\r\n");
        OSMutexPend(&mutex_sem, 0, OS_OPT_PEND_BLOCKING, 0, &error);           // 获取互斥信号量
        printf("高优先级的任务获取成功互斥信号量\r\n");

        printf("高优先级的任务正在运行\r\n");
        Delay_ms(1000);

        printf("高优先级的任务正在释放互斥信号量\r\n");
        OSMutexPost(&mutex_sem, OS_OPT_POST_1, &error);                         // 释放互斥信号量
        printf("高优先级的任务释放成功互斥信号量\r\n");

        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}

  修改 TASK3 任务配置:

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

OS_TCB  task3_tcb;
CPU_STK task3_stack[TASK3_STACK_SIZE];

void Task3(void *p_arg);

/**
 * @brief 任务3的任务函数
 * 
 * @param p_arg 任务参数
 */
void Task3(void *p_arg)
{
    OS_ERR error = {0};
  
    while (1)
    {
        printf("低优先级的任务正在获取互斥信号量\r\n");
        OSMutexPend(&mutex_sem, 0, OS_OPT_PEND_BLOCKING, 0, &error);            // 获取互斥信号量
        printf("低优先级的任务获取成功互斥信号量\r\n");

        printf("低优先级的任务正在运行\r\n");
        Delay_ms(3000);

        printf("低优先级的任务正在释放互斥信号量\r\n");
        OSMutexPost(&mutex_sem, OS_OPT_POST_1, &error);                          // 释放互斥信号量
        printf("低优先级的任务释放成功互斥信号量\r\n");

        OSTimeDly(1000, OS_OPT_TIME_DLY, &error);
    }
}
posted @ 2024-02-18 19:20  星光映梦  阅读(40)  评论(0编辑  收藏  举报