Scheduling

目录

启动任务调度器

CMSIS级别操作

osStatus_t osKernelStart (void) {
  osStatus_t stat;

  if (IS_IRQ()) {
    stat = osErrorISR;
  }
  else {
    if (KernelState == osKernelReady) {
      /* Ensure SVC priority is at the reset value */
      SVC_Setup();
      /* Change state to enable IRQ masking check */
      KernelState = osKernelRunning;
      /* Start the kernel scheduler */
      vTaskStartScheduler();
      stat = osOK;
    } else {
      stat = osError;
    }
  }

  return (stat);
}

SVC_Setup()

这一句 SVC_Setup(); 在 CMSIS-RTOS2 的 osKernelStart() 函数中,主要目的是确保 SVC(Supervisor Call)异常的优先级被设置为复位后的默认值。 关于异常可看Interrupt_Mgmt

详细解释:

1. SVC 异常的作用
  • SVC 是 ARM Cortex-M 处理器的一个系统异常
  • RTOS 使用 SVC 异常来实现从用户模式(线程模式)到特权模式(处理器模式)的安全切换
  • 系统调用(如创建线程、信号量操作等)通常通过 SVC 指令触发
2. 为什么要设置 SVC 优先级
  • 在系统启动过程中,某些初始化代码或第三方库可能会修改 SVC 的优先级
  • 如果 SVC 优先级设置不当,可能导致:
  • 系统调用无法正确执行
  • 任务调度出现问题
  • 系统稳定性降低
3. SVC_Setup() 的具体工作
static void SVC_Setup (void) {
  #if   (defined(__ARM_ARCH_7M__) && (__ARM_ARCH_7M__ != 0)) || \
        (defined(__ARM_ARCH_7EM__) && (__ARM_ARCH_7EM__ != 0))
  SCB->SHCSR |= SCB_SHCSR_SVCALLUSED_Msk;  // 启用 SVC 异常
  NVIC_SetPriority(SVCall_IRQn, 0xFE);     // 设置较低的优先级
  #endif
}
4. 在 osKernelStart() 中的意义
  • 确保内核启动前 SVC 异常处于已知的、正确的状态
  • 为后续的系统调用和任务调度提供可靠的基础
  • 这是 RTOS 启动过程中的重要安全措施

FreeRTOS级别操作

vTaskStartScheduler()

void vTaskStartScheduler( void )
{
    /* Add the idle task at the lowest priority. */
    //1.创建空闲任务
    xTaskCreate() or xTaskCreateStatic()

    //2.创建软件定时器任务(如果使能)
    xTimerCreateTimerTask()

    //3.关中断,防止调度器开启之前或之中受中断干扰
    portDISABLE_INTERRUPTS();

    xNextTaskUnblockTime = portMAX_DELAY; //设置第一个任务的阻塞超时时间为很大0xffffffffUL
    xSchedulerRunning = pdTRUE; //标记任务调度器开始运行
    xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; //初始化心跳计数器为0

    portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();//统计任务运行时间的实现接口(用户实现)

    traceTASK_SWITCHED_IN()//调试接口,需要实现

xPortStartScheduler()

xPortStartScheduler() 是 FreeRTOS 调度器的核心启动函数,负责初始化硬件资源并启动多任务调度环境。

函数概述

xPortStartScheduler() 位于 FreeRTOS 的移植层(通常是 port.c 文件),主要职责包括:

  • 配置系统关键硬件(如 SysTick 定时器)
  • 设置异常优先级
  • 启动第一个任务
  • 永不返回(除非发生严重错误)

代码分析

1. 异常优先级配置
BaseType_t xPortStartScheduler( void )
{
    /* 配置 PendSV 和 SysTick 异常的优先级 */
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

关键设计要点

  • PendSV 优先级设置为最低,确保高优先级中断能够立即响应
  • 这种设计避免了任务切换阻塞时间敏感的中断处理
2. 系统定时器初始化
    /* 启动 SysTick 定时器 - 提供任务调度的时间基准 */
    vPortSetupTimerInterrupt();

SysTick 定时器的作用: - 产生固定的时间节拍(tick) - 驱动时间片轮转调度 - 管理阻塞任务的超时机制

3. 调度器状态初始化
    /* 初始化调度器关键变量 */
    xNextTaskUnblockTime = portMAX_DELAY;
    xSchedulerRunning = pdTRUE;
    xTickCount = 0;
4. 启动第一个任务
    /* 启动第一个任务 */
    if( xPortStartFirstTask() != pdFALSE )
    {
        /* 如果执行到这里,说明任务启动失败 */
        return pdFALSE;
    }

    /* 正常情况下不会到达这里 */
    return pdFALSE;
}

启动第一个任务的核心机制

xPortStartFirstTask() 通常用汇编语言实现,主要步骤包括:

1. 堆栈指针初始化
xPortStartFirstTask:
    /* 从向量表初始化主堆栈指针 */
    ldr r0, =0xE000ED08    /* VTOR 寄存器地址 */
    ldr r0, [r0]
    ldr r0, [r0]           /* 获取初始 MSP 值 */
    msr msp, r0            /* 设置主堆栈指针 */
2. 中断使能
    /* 使能全局中断 */
    cpsie i                /* 使能 IRQ 中断 */
    cpsie f                /* 使能 Fault 中断 */
    dsb
    isb
3. 触发 SVC 异常启动任务
    /* 通过 SVC 异常启动第一个任务 */
    svc 0                  /* 触发 SVC 异常 */

    /* SVC 异常处理程序会配置任务环境并开始执行 */

SVC 异常处理程序的关键操作

在 SVC 异常处理程序中完成以下关键操作:

1. 任务上下文恢复
void vPortSVCHandler( void )
{
    /* 从当前任务的 TCB 中恢复堆栈指针 */
    pxCurrentTCB = 获取最高优先级任务的 TCB;
    __asm volatile (
        "ldr r3, [%0]          \n" /* 从 TCB 获取堆栈指针 */
        "ldmdb r3!, {r0-r2}    \n" /* 恢复部分寄存器 */
        "msr control, r0       \n" /* 设置控制寄存器 */
        "msr psp, r3           \n" /* 设置进程堆栈指针 */
        "isb                   \n"
        :: "r" (&pxCurrentTCB)
    );
}
2. 切换到任务模式
    /* 使用进程堆栈执行任务 */
    __asm volatile (
        "mov r0, #2            \n" /* 切换到线程模式并使用 PSP */
        "msr control, r0       \n"
        "isb                   \n"
        "bx lr                 \n" /* 返回到任务代码 */
    );

设计原理分析

1. 为什么使用 SVC 异常启动任务?
  • 权限切换:从 Handler 模式(特权级)切换到 Thread 模式(可能为非特权级)
  • 环境隔离:确保任务在受控的环境中开始执行
  • 堆栈分离:实现主堆栈(MSP)和进程堆栈(PSP)的分离
2. 优先级配置的重要性
  • SysTick:需要适当的优先级来保证时间基准的准确性
  • PendSV:最低优先级确保不会延迟高优先级中断的处理
  • SVC:用于系统调用,需要合理的优先级设置
3. 状态机转换

调度器启动过程的状态转换: 1. 初始化状态:配置硬件,设置优先级 2. 准备状态:初始化变量,启动定时器 3. 启动状态:通过 SVC 异常切换到第一个任务 4. 运行状态:多任务环境正式运行

错误处理机制

启动失败的可能原因
  • 堆栈指针初始化失败
  • 向量表配置错误
  • 优先级配置冲突
  • 内存分配问题
返回值语义
  • pdTRUE:启动成功(实际上不会返回)
  • pdFALSE:启动失败,需要错误处理

任务切换

任务切换的本质CPU寄存器的切换

基本概念

任务切换是多任务操作系统的核心机制,指 CPU 从一个正在运行的任务转移到另一个任务的过程。在单核处理器系统中,多个任务通过快速切换创造"同时运行"的假象。

类比理解

想象一名厨师同时处理多道菜品: - 每道菜就是一个任务 - 厨师在不同菜品间快速切换注意力 - 每次切换时需要记住当前菜品的进度 - 切换到另一道菜时需要回忆该菜品的上次状态

上下文切换的深入理解

什么是上下文

上下文是指任务在某个时间点的完整执行状态,包括:

硬件上下文: - 所有寄存器值(R0-R15) - 程序状态寄存器(xPSR) - 堆栈指针(SP) - 程序计数器(PC)

软件上下文: - 任务控制块(TCB)中的状态信息 - 堆栈中的局部变量和返回地址 - 任务特有的配置和数据

上下文切换的过程

// 伪代码表示上下文切换流程
void context_switch(Task* current, Task* next) {
    // 1. 保存当前任务上下文
    save_registers(current->stack_pointer);
    save_processor_state(current->tcb);

    // 2. 更新调度器状态
    scheduler->current_task = next;

    // 3. 恢复新任务上下文  
    restore_processor_state(next->tcb);
    restore_registers(next->stack_pointer);

    // 4. 跳转到新任务
    jump_to_task(next->program_counter);
}

FreeRTOS 任务切换的触发条件

主动触发方式

任务主动让出 CPU portYIELD()/taskYIELD()

通过挂起PendSV启用PendSv异常,如何挂起见KernelPend

void vTaskA( void *pvParameters )
{
    for( ;; )
    {
        // 执行一些工作
        perform_work();

        // 主动让出 CPU 给其他任务
        taskYIELD();
    }
}

等待资源时自动切换:

void vTaskB( void *pvParameters )
{
    for( ;; )
    {
        // 等待信号量,期间会自动切换任务
        xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );

        // 获取信号量后继续执行
        process_data();
    }
}

被动触发方式

时间片到期:

// 在 SysTick 中断服务程序中
void xPortSysTickHandler( void )
{
    // 更新时间计数
    if( xTaskIncrementTick() != pdFALSE )
    {
        // 触发任务切换
        portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    }
}

中断服务程序中唤醒高优先级任务:

void vAnInterruptHandler( void )
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    // 释放信号量,可能唤醒高优先级任务
    xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );

    // 如果需要立即切换
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

FreeRTOS 任务切换的硬件机制

ARM Cortex-M 异常系统

FreeRTOS 充分利用 ARM Cortex-M 的异常机制来实现高效任务切换:

SysTick 异常: - 用途:系统时钟节拍,提供时间基准 - 优先级:配置为较低优先级 - 功能:驱动时间片轮转和任务超时管理

PendSV 异常:

  • 用途:可挂起的系统服务,执行实际上下文切换
  • 优先级:配置为最低优先级
  • 特点:可延迟执行,不阻塞高优先级中断

SVC 异常: - 用途:系统调用接口 - 功能:启动调度器和执行特权操作

优先级配置策略

// FreeRTOSConfig.h 中的典型配置
#define configKERNEL_INTERRUPT_PRIORITY    255
#define configMAX_SYSCALL_INTERRUPT_PRIORITY  191

// 实际优先级设置
#define portNVIC_PENDSV_PRI          ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI         ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

任务切换的详细执行流程

完整的切换序列

阶段一:触发切换

// 从任务中触发切换
taskYIELD();
    
// 设置 PendSV 挂起位
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    
// CPU 在适当时候进入 PendSV 异常

阶段二:保存当前任务上下文

vPortPendSVHandler:
    /* 保存当前任务状态 */
    mrs r0, psp                 // 获取当前任务的堆栈指针
    isb                         // 指令同步屏障

    // 将寄存器 r4-r11 保存到任务堆栈手动压栈
    stmdb r0!, {r4-r11}

    // 更新任务控制块中的堆栈指针
    ldr r2, =pxCurrentTCB
    ldr r1, [r2]
    str r0, [r1]

阶段三:选择下一个任务

// 调用调度器选择新任务
vTaskSwitchContext();
    
// 调度器决策过程
void vTaskSwitchContext( void )
{
    // 检查调度器是否挂起
    if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
    {
        xYieldPending = pdTRUE;
        return;
    }

    // 寻找最高优先级的就绪任务
    #if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 1 )
        // 使用位图算法快速查找
        uxTopReadyPriority = portGET_HIGHEST_PRIORITY();
    #else
        // 遍历就绪列表查找
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, 
                                   &( pxReadyTasksLists[ uxTopReadyPriority ] ) );
    #endif
}

阶段四:恢复新任务上下文

    /* 恢复新任务状态 */
    ldr r1, =pxCurrentTCB      // 获取新任务的 TCB
    ldr r0, [r1]
    ldr r0, [r0]               // 获取新任务的堆栈指针

    // 从堆栈恢复寄存器 r4-r11
    ldmia r0!, {r4-r11}

    // 更新进程堆栈指针
    msr psp, r0
    isb

    // 返回到新任务继续执行
    bx r14

任务控制块(TCB)的关键作用

TCB 数据结构

typedef struct tskTaskControlBlock
{
    // 堆栈管理
    volatile StackType_t *pxTopOfStack;    // 当前堆栈顶
    StackType_t *pxStack;                  // 堆栈起始地址

    // 任务状态管理
    ListItem_t xStateListItem;             // 状态列表项
    UBaseType_t uxPriority;                // 任务优先级
    StackType_t *pxEndOfStack;             // 堆栈结束地址

    // 任务标识
    char pcTaskName[ configMAX_TASK_NAME_LEN ];

    // 调试和跟踪信息
    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxTCBNumber;
    #endif

    // 互斥量继承相关
    #if ( configUSE_MUTEXES == 1 )
        UBaseType_t uxBasePriority;
        UBaseType_t uxMutexesHeld;
    #endif
} tskTCB;

堆栈布局设计

任务堆栈在上下文保存后的布局:
高地址 -> | 异常自动保存的寄存器(自动压栈,自动出栈) |
         | R0-R3, R12, LR, PC, xPSR |
         |---------------------------|
         | 手动保存的寄存器 R4-R11 (手动压栈,手动出栈)  | <- PSP 指向这里
         |---------------------------|
         | 任务局部变量和调用栈      |
低地址 -> | 堆栈起始位置            |

时间片轮转调度原理

同优先级任务调度

// 在 SysTick 中断中处理时间片
BaseType_t xTaskIncrementTick( void )
{
    TickType_t xSwitchRequired = pdFALSE;

    if( xSchedulerRunning != pdFALSE )
    {
        // 更新系统节拍计数
        const TickType_t xConstTickCount = xTickCount + 1;
        xTickCount = xConstTickCount;

        // 检查是否有任务超时
        if( xConstTickCount == 0 )
        {
            taskSWITCH_DELAYED_LISTS();
        }

        // 时间片轮转决策
        #if ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 )
        {
            // 如果当前优先级有多个就绪任务,触发切换
            if( listCURRENT_LIST_LENGTH( 
                &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
            {
                xSwitchRequired = pdTRUE;
            }
        }
        #endif
    }

    return xSwitchRequired;
}

性能优化技术

快速任务选择算法

位图调度算法:

#if configUSE_PORT_OPTIMISED_TASK_SELECTION == 1

// 使用前导零计数指令快速找到最高优先级
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
    __asm volatile( "clz %0, %1" : "=r" ( uxTopPriority ) : "r" ( uxReadyPriorities ) )

#endif

惰性堆栈保存: - 只保存被调用者保存的寄存器(R4-R11) - 调用者保存的寄存器(R0-R3, R12)由 C-ABI 保证 - 硬件自动保存异常帧(R0-R3, R12, LR, PC, PSR)

中断延迟优化

PendSV 的低优先级设计:

// PendSV 设置为最低优先级,确保:
// 1. 高优先级中断可以立即响应
// 2. 多个中断可以合并处理
// 3. 减少不必要的上下文切换

#define portPendSVHandler_PRIORITY ( 255UL << 16UL )

实际应用场景分析

场景一:高优先级任务抢占

void vHighPriorityTask( void *pvParameters )
{
    for( ;; )
    {
        // 等待事件
        xQueueReceive( xHighPriorityQueue, ... );

        // 立即抢占当前运行的低优先级任务
        process_critical_work();
    }
}

void vLowPriorityTask( void *pvParameters )
{
    for( ;; )
    {
        // 执行非关键工作
        background_processing();

        // 可能在任何时刻被高优先级任务抢占
        taskYIELD();
    }
}

场景二:资源共享与同步

void vProducerTask( void *pvParameters )
{
    for( ;; )
    {
        // 生产数据
        generate_data();

        // 发送数据到队列,可能唤醒消费者任务
        xQueueSend( xDataQueue, &data, portMAX_DELAY );

        // 如果消费者优先级更高,会发生任务切换
    }
}

void vConsumerTask( void *pvParameters )
{
    for( ;; )
    {
        // 等待数据,如果没有数据会阻塞
        xQueueReceive( xDataQueue, &data, portMAX_DELAY );

        // 被生产者唤醒后继续执行
        process_data( data );
    }
}

调试与性能分析

上下文切换开销测量

// 测量切换时间的简单方法
uint32_t measure_switch_time( void )
{
    uint32_t start_time, end_time;

    // 获取开始时间
    start_time = DWT->CYCCNT;

    // 触发任务切换
    taskYIELD();

    // 获取结束时间
    end_time = DWT->CYCCNT;

    return end_time - start_time;
}

常见问题与解决方案

问题一:过多的上下文切换 - 症状:CPU 大部分时间在切换任务而非执行任务 - 解决方案:调整时间片大小,合并小任务

问题二:优先级反转 - 症状:高优先级任务被低优先级任务阻塞 - 解决方案:使用优先级继承协议或优先级天花板

问题三:堆栈溢出 - 症状:任务崩溃或系统不稳定 - 解决方案:增加堆栈大小,使用堆栈检查功能

其他API函数

FreeRTOS

这些函数以 vTaskxTask 等为前缀,是 FreeRTOS 内核的直接接口。

分类 函数名 功能描述 参数说明 返回值/注意事项
taskYIELD() 主动请求任务切换(强制调度)。 是一个宏,通常触发 PendSV 异常。
优先级管理 vTaskPrioritySet() 设置任务的优先级。 xTask: 任务句柄
uxNewPriority: 新的优先级
优先级必须在 0configMAX_PRIORITIES-1 范围内。
uxTaskPriorityGet() 获取任务的当前优先级。 xTask: 任务句柄 返回任务的当前优先级。
状态查询 eTaskGetState() 获取任务的状态(运行、就绪、阻塞、挂起、删除)。 xTask: 任务句柄 返回一个 eTaskState 枚举值。
uxTaskGetStackHighWaterMark() 获取任务堆栈的历史最小剩余空间(高水位线)。 xTask: 任务句柄 返回值越小,说明堆栈使用率越高。0 表示堆栈已溢出。用于评估堆栈大小是否合理。
其他 pcTaskGetName() 获取任务的名称字符串。 xTaskToQuery: 任务句柄 返回任务名称字符串的指针。
xTaskGetTickCount() 获取系统节拍计数器自启动以来的值。 在任务中使用。注意节拍计数器溢出。
xTaskGetTickCountFromISR() 在中断服务程序中获取系统节拍计数器。 在 ISR 中使用。

CMSIS-RTOS v2

这些函数以 osThread 为前缀,是 FreeRTOS 原生 API 的封装,提供了标准化的接口。

分类 函数名 功能描述 参数说明 返回值/注意事项
osThreadGetId() 获取当前正在运行的线程的 ID。 返回当前线程的 ID。
osThreadGetName() 获取指定线程的名称字符串。 thread_id: 线程 ID 返回线程名称字符串的指针。
任务控制 osDelay() 将当前线程延迟(阻塞)指定的时间。 ticks: 延迟的时间节拍数 相当于 FreeRTOS 的 vTaskDelay
osDelayUntil() 精确的周期性延迟。 ticks: 下一次唤醒的绝对节拍时间 相当于 FreeRTOS 的 vTaskDelayUntil
osThreadYield() 将 CPU 控制权交给另一个就绪态的同优先级线程。 相当于 FreeRTOS 的 taskYIELD()
状态与优先级 osThreadGetState() 获取当前线程的状态。 thread_id: 线程 ID 返回一个 osThreadState_t 枚举值(就绪、运行、阻塞、终止等)。
osThreadSetPriority() 设置指定线程的优先级。 thread_id: 线程 ID
priority: 新的优先级
优先级在 osPriority_t 枚举中定义。
osThreadGetPriority() 获取指定线程的当前优先级。 thread_id: 线程 ID 返回线程的当前优先级。
系统信息 osThreadGetStackSpace() (可选)获取线程的剩余堆栈空间。 thread_id: 线程 ID 返回值依赖于具体实现,可能返回字节数或字数的剩余空间。
osKernelGetTickCount() 获取内核节拍计数器。 相当于 FreeRTOS 的 xTaskGetTickCount,可在任务和中断中使用。
osKernelGetSysTimerCount() 获取系统计时器的精确计数值。 用于高精度时间测量。