Freertos中如何进行调度器启动过程解析?

2026-05-19 17:211阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计6807个文字,预计阅读时间需要28分钟。

Freertos中如何进行调度器启动过程解析?

前言+本节主要讲解启动调度器。+这些都与硬件相关,因此会分为两条线进行:POSIX和Cortex-M3。+原文:李志明博客:https://www.cnblogs.com/lizhuming/p/16076476.+5.1+调度器的基本概念+5.1.1+调度器

前言

本节主要讲解启动调度器。

这些都是与硬件相关,所以会分两条线走:posix和cortex m3。

原文:李柱明博客:www.cnblogs.com/lizhuming/p/16076476.html

5.1 调度器的基本概念 5.1.1 调度器

调度器就是使用相关的调度算法来决定当前需要执行的任务。

调度器特点:

  1. 调度器可以区分就绪态任务和挂起任务。
  2. 调度器可以选择就绪态中的一个任务,然后激活它。
  3. 不同调度器之间最大的区别就是如何分配就绪态任务间的完成时间。

嵌入式实时操作系统的核心就是调度器和任务切换:

  • 调度器的核心就是调度算法。
  • 任务切换是基于硬件内核架构实现。
5.1.2 抢占式调度

抢占式调度:

  • 每个任务都被分配了不同的优先级,抢占式调度器会获得就绪列表中优先级最高的任务,并运行这个任务。
  • 在FreeRTOS系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。
5.1.3 时间片调度

最常用的的时间片调度算法就是Round-robin调度算法,这种调度算法可以用于抢占式或者合作式的多任务中。

实现Round-robin调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片。

当任务就绪链表中最高优先级中存在两个以上的任务时,当前运行的任务耗尽时间片后,当前链表的下一个任务到运行态,把当前任务重新插入到当前优先级就绪链表尾部。

使用时间片调度需要在FreeRTOSConfig.h文件中使能宏定义:#defineconfigUSE_TIME_SLICING 1

需要注意的是,freertos时间片不能随意的设置时间为多少个tick,只能默认一个tick。

5.2 cortex m3架构的三个异常

在Cortex-M3架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick。

对应三个异常回调:

#define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler #define vPortSVCHandler SVC_Handler

注意:Cortex-M的优先级数值越大其优先级越低。

5.2.1 SVC

SVC(系统服务调用,亦简称系统调用)用于任务启动。所以只被调用一次。

有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个SVC异常。

在该异常回调里启动第一个任务。

5.2.2 PendSV

PendSV(可挂起系统调用)用于完成任务切换。

该异常可以像普通的中断一样被挂起的,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕,这样子产生的PendSV中断就不会打断其他中断的运行。

在该异常的回调函数里执行任务切换。

5.2.3 SysTick

SysTick用于产生系统节拍时钟。

每次systick异常产生都会检查是否需要任务调度,如果需要,则出发PendSV异常即可。

5.3 启动调度器 5.3.1 启动调度器描述

启动调度器使用API函数vTaskStartScheduler()

该函数会:

  • 创建一个空闲任务;
  • 创建软件定时器任务;
  • 初始化一些静态变量;
  • 会初始化系统节拍定时器并设置好相应的中断;
  • 启动第一个任务。

启动调度器,硬件相关是调用xPortStartScheduler()

5.3.2 创建空闲任务

空闲任务时在启动调度器时创建的,该任务不能阻塞,创建空闲任务是为了不让系统退出,因为系统一旦启动就必须占有任务。

空闲任务主体主要是做一些系统内存的清理工作、进入休眠或者低功耗操作等操作。

创建空闲任务,也分两种方式,取决于是否开启静态内存分配宏configSUPPORT_STATIC_ALLOCATION

5.3.2.1 静态内存创建

参考前面任务基础相关的文章便可知,静态内存创建任务需要用户提供任务控制块和任务栈空间。

由于空闲任务是内核API创建的,所以用户需要通过指定的函数vApplicationGetIdleTaskMemory()提供这些信息。

实现代码如下:

/* 如果开启了静态内存功能,创建空闲任务就按静态内存创建 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxIdleTaskTCBBuffer = NULL; StackType_t * pxIdleTaskStackBuffer = NULL; uint32_t ulIdleTaskStackSize; /* 获取空闲任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。 这个API是有用户实现 */ vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); /* 创建空闲任务,使用最低优先级*/ xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, configIDLE_TASK_NAME, ulIdleTaskStackSize, ( void * ) NULL, portPRIVILEGE_BIT, pxIdleTaskStackBuffer, pxIdleTaskTCBBuffer ); if( xIdleTaskHandle != NULL ) { xReturn = pdPASS; } else { xReturn = pdFAIL; } } #endif /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */ 5.3.2.2 动态内存创建

动态内存创建空闲任务,直接使用xTaskCreate()实现即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 ) { /* 动态内存方式创建空闲任务 */ xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL, portPRIVILEGE_BIT, &xIdleTaskHandle ); } #endif /* configSUPPORT_STATIC_ALLOCATION */ 5.3.3 创建软件定时器任务

软件定时器组件功能,后面会详细分析,这里只做简单说明

和创建空闲任务一个道理。

前提条件时需要配置configUSE_TIMERS开启软件定时器功能。

创建软件定时器内容集成在xTimerCreateTimerTask()API内部了,其实现和创建空闲任务一样的。

通过宏configSUPPORT_STATIC_ALLOCATION区分静态和动态内存创建。

5.3.3.1 初始化软件定时器组件内容

调用prvCheckForValidListAndQueue()API初始化定时链表和创建定时器通信服务队列。

5.3.3.2 静态内存创建

通过用户实现的vApplicationGetTimerTaskMemory()API获取软件定时器任务控制块和任务栈信息。

#if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxTimerTaskTCBBuffer = NULL; StackType_t * pxTimerTaskStackBuffer = NULL; uint32_t ulTimerTaskStackSize; /* 获取软件定时器任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。 这个API是有用户实现 */ vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize ); /* 创建软件定时器任务 */ xTimerTaskHandle = xTaskCreateStatic( prvTimerTask, configTIMER_SERVICE_TASK_NAME, ulTimerTaskStackSize, NULL, ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, pxTimerTaskStackBuffer, pxTimerTaskTCBBuffer ); if( xTimerTaskHandle != NULL ) { xReturn = pdPASS; } } #endif 5.3.3.3 动态内存创建

动态内存创建软件定时器任务,直接使用xTaskCreate()实现即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 ) { /* 动态内存方式创建软件定时器任务 */ xReturn = xTaskCreate( prvTimerTask, configTIMER_SERVICE_TASK_NAME, configTIMER_TASK_STACK_DEPTH, NULL, ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, &xTimerTaskHandle ); } #endif /* configSUPPORT_STATIC_ALLOCATION */ 5.3.4 调度器中的用户函数

在启动调度器时,内核运行用户插入一个函数调用,一般用于启动调度器标识处理。

指定函数:freertos_tasks_c_additions_init()

使能宏:FREERTOS_TASKS_C_ADDITIONS_INIT

/* freertos_tasks_c_additions_init 函数由用户定义,用于启动调度器时调用一次 */ #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT { freertos_tasks_c_additions_init(); } #endif 5.3.5 CPU利用率统计配置

如果用户配置了portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()宏函数,在调度器启动时需要调用。

该函数一般是重置定时器起始值,搭配portGET_RUN_TIME_COUNTER_VALUE()宏函数实现运行时间统计功能。

可以参考李柱明博客:cpu利用率统计。后面可能会有独立章节描述该功能的实现

在启动调度器中的代码:

/* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/ portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); 5.3.6 posix启动调度器分析

源码分析:

  • 启动调度:xPortStartScheduler()
  • 利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()
  • 利用线程通信实现启动第一个任务:vPortStartFirstTask()
  • 在第一次初始化任务栈时会跑该函数(只跑一次):prvSetupSignalsAndSchedulerPolicy()
5.3.6.1 启动调度器

在接口层,启动调度调用xPortStartScheduler()

  • 获取线程ID;
  • 配置系统滴答时钟;
  • 启动第一个任务;
  • 等待用户调用vPortEndScheduler()关闭调度。
  • 系统调度求关闭后需要删除和释放启动调度器时创建的空闲任务和软件定时器任务。
  • 恢复主线程型号掩码。

portBASE_TYPE xPortStartScheduler( void ) { int iSignal; sigset_t xSignals; /* 获取当前线程ID */ hMainThread = pthread_self(); /* 设置系统计时器以按要求的频率生成滴答中断 */ prvSetupTimerInterrupt(); /* 开启第一个任务. */ vPortStartFirstTask(); /* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); /* 等待关闭调度器的信号 */ while ( !xSchedulerEnd ) { sigwait( &xSignals, &iSignal ); } /* 删除Idle任务并释放其资源 */ #if ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) vPortCancelThread( xTaskGetIdleTaskHandle() ); #endif #if ( configUSE_TIMERS == 1 ) /* 删除软件定时器任务并释放其资源 */ vPortCancelThread( xTimerGetTimerDaemonTaskHandle() ); #endif /* configUSE_TIMERS */ /* 恢复原始信号掩模 */ (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask, NULL ); return 0; 5.3.6.2 实现滴答时钟

利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()

采用posix标准下的getitimer()setitimer()API去实现。

在进程里使用ITIMER_REAL计数器实现系统滴答时钟。

  • posix标准下,每个进程都会维护三个域的定时器,当前使用的ITIMER_REAL是进程实时定时器。

void prvSetupTimerInterrupt( void ) { struct itimerval itimer; int iRet; /* 用当前的定时器信息初始化结构 */ iRet = getitimer( ITIMER_REAL, &itimer ); if ( iRet ) { prvFatalError( "getitimer", errno ); } /* 设置定时器事件之间的时间间隔. */ itimer.it_interval.tv_sec = 0; itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS; /* 设计初始值 */ itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS; /* 重置定时器. */ iRet = setitimer( ITIMER_REAL, &itimer, NULL ); if ( iRet ) { prvFatalError( "setitimer", errno ); } /* 获取纳秒值 */ prvStartTimeNs = prvGetTimeNs(); } 5.3.6.3 启动第一个任务

利用线程通信实现启动第一个任务:vPortStartFirstTask()

原理在前面posix模拟器设计说过。

利用线程型号实现线程的启停从而实现任务切换。

先获取线程句柄:

void vPortStartFirstTask( void ) { /* 获取当前任务的线程句柄 */ Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); /* 启动第一个任务. */ prvResumeThread( pxFirstThread ); }

发信号给下一个需要跑的线程,让其启动,这样就进入了freertos世界嘞:

static void prvResumeThread( Thread_t *xThreadId ) { /* 如果当前线程不是接下来要跑的线程 */ if ( pthread_self() != xThreadId->pthread ) { /* 发送事件启动新的线程 */ event_signal(xThreadId->ev); } } void event_signal( struct event * ev ) { pthread_mutex_lock( &ev->mutex ); ev->event_triggered = true; // 解除阻塞的标记 pthread_cond_signal( &ev->cond ); // 发送信号给需要启动的线程,让其解除阻塞 pthread_mutex_unlock( &ev->mutex ); }

那还需要停止当前线程嘞,完成这些时后,回进入等待结束调度器事件而阻塞:(代码在xPortStartScheduler()中)

/* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); /* 等待关闭调度器的信号 */ while ( !xSchedulerEnd ) { sigwait( &xSignals, &iSignal ); } 5.3.7 cortex m3启动调度器分析

启动调度器:xPortStartScheduler()

SVC异常启动第一个任务:vPortSVCHandler()

Freertos中如何进行调度器启动过程解析?

5.3.7.1 基本知识
  1. cortex m的双堆栈指针MSP和PSP的切换。

  2. 硬件出入栈和软件出入栈。

    1. 硬件出入栈:异常时,硬件会完成部分必要寄存器的出入栈。
    2. 软件出入栈:由于硬件压栈信息对保护上下文不够,需要软件出入栈完成其它CPU寄存器的出入栈。
5.3.7.2 cortex m3的启动调度器的基本内容
  1. 把PendSV和SysTick设置为最低优先级的中断。

  2. 启动滴答定时器。

  3. 启动第一个任务。通过SVC异常方式。

    1. 重置MSP堆栈指针。

    2. 使能全局中断。

    3. 触发SVC异常。进入SVC异常。

      1. 获取pxCurrentTCB值,即是当前需要跑的任务句柄。
      2. 通过任务句柄获取任务控制块,通过任务控制块获取任务栈顶。
      3. 软件出栈。
      4. 更新栈顶指针到PSP。
      5. 修改R14寄存器,使异常退出时,进入线程模式,使用PSP栈指针。
      6. 退出异常。硬件自动使用PSP出栈。

至此,系统已经启动,进入freertos世界。

5.3.7.3 FromISR中断保护配置

在freertos中会看到FromISR后缀的API,这些API执行环境不一样,一般用于中断回调中使用,要求不能阻塞,快进快出。

这些API不能在中断保护外的中断回调中使用,取决于宏configMAX_SYSCALL_INTERRUPT_PRIORITY

所以需要配置进出临界能屏蔽中断的优先级级别,优先级等于或低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断能被临界API屏蔽,可调用FromISR后缀的API。

先了解下几个宏:(数值越小,中断优先级越高)

  • configLIBRARY_LOWEST_INTERRUPT_PRIORITY:定义SysTick与PendSV的中断优先级。
  • configKERNEL_INTERRUPT_PRIORITY:配置SysTick与PendSV的中断优先级到寄存器。
  • configMAX_SYSCALL_INTERRUPT_PRIORITY:定义freertos系统可控最大中断优先级。
  • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:用于配置basepri寄存器的,当 basepri 设置为某个值的时候,会让系统不响应比该优先级低的中断,而优先级比之更高
    的中断则不受影响。这样,freertos可以通过控制basepri值来控制部分中断,实现中断保护。

#if ( configASSERT_DEFINED == 1 ) { volatile uint32_t ulOriginalPriority; volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER ); volatile uint8_t ucMaxPriorityValue; /* 确定可以调用ISR安全FreeRTOS API函数的最大优先级。 ISR安全函数是以“FromISR”结尾的。 FreeRTOS维护独立的线程和ISR API函数,以确保进入中断尽可能快和简单。 保存将要被破坏的中断优先级值。 */ ulOriginalPriority = *pucFirstUserPriorityRegister; /* 确定可用的优先级位数。首先写所有可能的位。 */ *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE; /* 把值读回来看看,因为无效的优先级位读出位0,读出有多少个1就知道有多少位优先级。 */ ucMaxPriorityValue = *pucFirstUserPriorityRegister; /* 内核中断优先级应该设置为最低优先级。 */ configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) ); /* 对最大系统调用优先级使用相同的掩码。 */ ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue; /* 为回读的位数计算可接受的最大优先级组值。 */ ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS; while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ) { ulMaxPRIGROUPValue--; ucMaxPriorityValue <<= ( uint8_t ) 0x01; } #ifdef __NVIC_PRIO_BITS { /* 检查定义优先级位数的CMSIS配置,该配置与实际从硬件查询的优先级位数相匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS ); } #endif #ifdef configPRIO_BITS { /* 检查定义优先级位数的FreeRTOS配置,该配置与从硬件实际查询的优先级位数匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS ); } #endif /* 将优先级组的值移回它在AIRCR寄存器中的位置 */ ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT; ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK; /* 将中断的中断优先级寄存器恢复到原来的值 */ *pucFirstUserPriorityRegister = ulOriginalPriority; } #endif /* configASSERT_DEFINED */ 5.3.7.4 配置PendSV和SysTick中断优先级

PendSV用于切换任务;

SysTick用于系统节拍。

这两个都配置为最低优先级。

这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,有利于系统稳定。

而且SysTick是硬件定时器,响应可能会延迟,都是系统事件不会有偏差。

/* 将PendSV和SysTick设置为最低优先级的中断 */ portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; 5.3.7.5 启动滴答定时器

调用vPortSetupTimerInterrupt()实现。

5.3.7.6 启动第一个任务

调用prvStartFirstTask()实现。

启动第一个任务:

  • 先使能全局中断;
  • 触发进入SVC异常回调;
  • 在SVC回调切入第一个任务。

__asm void prvStartFirstTask( void ) { PRESERVE8 /* 当前栈需按照 8 字节对齐 */ /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */ ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */ ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */ ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */ /* 将msp设置回堆栈的开始 */ msr msp, r0 /* 使能全局中断 */ cpsie i cpsie f dsb isb /* 触发SVC异常开启动第一个任务. */ svc 0 nop nop /* *INDENT-ON* */ }

SVC回调:

  • 通过pxCurrentTCB获取当前需要跑的第一个任务控制块;
  • 获取该任务栈顶地址;
  • 从栈顶地址软件出栈;(下文恢复)
  • 更新栈顶地址到PSP;
  • 双堆栈指针从MSP转用PSP;
  • 异常返回,硬件会根据PSP栈出栈,完成下文恢复,进入freertos第一个任务。

__asm void vPortSVCHandler( void ) { /* *INDENT-OFF* */ PRESERVE8 ldr r3, = pxCurrentTCB /* 加载 pxCurrentTCB 的地址到 r3. */ ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */ ldr r0, [ r1 ] /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */ ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */ msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */ isb mov r0, # 0 /* 将寄存器 r0 清 0 */ msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */ orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */ bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */ /* *INDENT-ON* */ } 5.3.7.7 启动第一个任务后的任务栈情况

该图片源自野火

附件 vTaskStartScheduler()

void vTaskStartScheduler( void ) { BaseType_t xReturn; /* 如果开启了静态内存功能,创建空闲任务就按静态内存创建 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxIdleTaskTCBBuffer = NULL; StackType_t * pxIdleTaskStackBuffer = NULL; uint32_t ulIdleTaskStackSize; /* 获取空闲任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。 这个API是有用户实现 */ vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); /* 创建空闲任务,使用最低优先级*/ xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, configIDLE_TASK_NAME, ulIdleTaskStackSize, ( void * ) NULL, portPRIVILEGE_BIT, pxIdleTaskStackBuffer, pxIdleTaskTCBBuffer ); if( xIdleTaskHandle != NULL ) { xReturn = pdPASS; } else { xReturn = pdFAIL; } } #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */ { /* 动态内存方式创建空闲任务 */ xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL, portPRIVILEGE_BIT, &xIdleTaskHandle ); } #endif /* configSUPPORT_STATIC_ALLOCATION */ #if ( configUSE_TIMERS == 1 ) { if( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_TIMERS */ if( xReturn == pdPASS ) { /* freertos_tasks_c_additions_init 函数由用户定义,用于启动调度器时调用一次 */ #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT { freertos_tasks_c_additions_init(); } #endif /* 先关闭中断,确保节拍定时器中断不会在调用xPortStartScheduler()时或之前发生。当第一个任务启动时,会重新启动中断*/ portDISABLE_INTERRUPTS(); #if ( configUSE_NEWLIB_REENTRANT == 1 ) { /* 略 */ _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif /* configUSE_NEWLIB_REENTRANT */ /* 初始化静态变量 */ xNextTaskUnblockTime = portMAX_DELAY; xSchedulerRunning = pdTRUE; xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; /* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/ portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); traceTASK_SWITCHED_IN(); /* 设置系统节拍定时器,这与硬件特性相关,因此被放在了移植层.*/ if( xPortStartScheduler() != pdFALSE ) { /* 如果调度器正确运行,则不会执行到这里,函数也不会返回*/ } else { /* 仅当任务调用API函数xTaskEndScheduler()后,会执行到这里.*/ } } else { /* 执行到这里表示内核没有启动,可能因为堆栈空间不够 */ configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY ); } /* 预防编译器警告*/ ( void ) xIdleTaskHandle; ( void ) uxTopUsedPriority; } posix:xPortStartScheduler()

portBASE_TYPE xPortStartScheduler( void ) { int iSignal; sigset_t xSignals; /* 获取当前线程ID */ hMainThread = pthread_self(); /* 设置系统计时器以按要求的频率生成滴答中断 */ prvSetupTimerInterrupt(); /* 开启第一个任务. */ vPortStartFirstTask(); /* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); /* 等待关闭调度器的信号 */ while ( !xSchedulerEnd ) { sigwait( &xSignals, &iSignal ); } /* Cancel the Idle task and free its resources */ #if ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) vPortCancelThread( xTaskGetIdleTaskHandle() ); #endif #if ( configUSE_TIMERS == 1 ) /* Cancel the Timer task and free its resources */ vPortCancelThread( xTimerGetTimerDaemonTaskHandle() ); #endif /* configUSE_TIMERS */ /* Restore original signal mask. */ (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask, NULL ); return 0; } posix:prvSetupTimerInterrupt()

void prvSetupTimerInterrupt( void ) { struct itimerval itimer; int iRet; /* 用当前的定时器信息初始化结构 */ iRet = getitimer( ITIMER_REAL, &itimer ); if ( iRet ) { prvFatalError( "getitimer", errno ); } /* 设置定时器事件之间的时间间隔. */ itimer.it_interval.tv_sec = 0; itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS; /* 设计初始值 */ itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS; /* 重置定时器. */ iRet = setitimer( ITIMER_REAL, &itimer, NULL ); if ( iRet ) { prvFatalError( "setitimer", errno ); } /* 获取纳秒值 */ prvStartTimeNs = prvGetTimeNs(); } posix:vPortStartFirstTask()

void vPortStartFirstTask( void ) { /* 获取当前任务的线程句柄 */ Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); /* 启动第一个任务. */ prvResumeThread( pxFirstThread ); } static void prvResumeThread( Thread_t *xThreadId ) { /* 如果当前线程不是接下来要跑的线程 */ if ( pthread_self() != xThreadId->pthread ) { /* 发送事件启动新的线程 */ event_signal(xThreadId->ev); } } void event_signal( struct event * ev ) { pthread_mutex_lock( &ev->mutex ); ev->event_triggered = true; // 解除阻塞的标记 pthread_cond_signal( &ev->cond ); // 发送信号给需要启动的线程,让其解除阻塞 pthread_mutex_unlock( &ev->mutex ); } cortex m3:xPortStartScheduler()

BaseType_t xPortStartScheduler( void ) { #if ( configASSERT_DEFINED == 1 ) { volatile uint32_t ulOriginalPriority; volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER ); volatile uint8_t ucMaxPriorityValue; /* 确定可以调用ISR安全FreeRTOS API函数的最大优先级。 ISR安全函数是以“FromISR”结尾的。 FreeRTOS维护独立的线程和ISR API函数,以确保进入中断尽可能快和简单。 保存将要被破坏的中断优先级值。 */ ulOriginalPriority = *pucFirstUserPriorityRegister; /* 确定可用的优先级位数。首先写所有可能的位。 */ *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE; /* 把值读回来看看,因为无效的优先级位读出位0,读出有多少个1就知道有多少位优先级。 */ ucMaxPriorityValue = *pucFirstUserPriorityRegister; /* 内核中断优先级应该设置为最低优先级。 */ configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) ); /* 对最大系统调用优先级使用相同的掩码。 */ ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue; /* 为回读的位数计算可接受的最大优先级组值。 */ ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS; while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ) { ulMaxPRIGROUPValue--; ucMaxPriorityValue <<= ( uint8_t ) 0x01; } #ifdef __NVIC_PRIO_BITS { /* 检查定义优先级位数的CMSIS配置,该配置与实际从硬件查询的优先级位数相匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS ); } #endif #ifdef configPRIO_BITS { /* 检查定义优先级位数的FreeRTOS配置,该配置与从硬件实际查询的优先级位数匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS ); } #endif /* 将优先级组的值移回它在AIRCR寄存器中的位置 */ ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT; ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK; /* 将中断的中断优先级寄存器恢复到原来的值 */ *pucFirstUserPriorityRegister = ulOriginalPriority; } #endif /* configASSERT_DEFINED */ /* 将PendSV和SysTick设置为最低优先级的中断 */ portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; /* 启动滴答定时器。注意,当前全局中断是关闭的,在启动第一个任务时会开启。 */ vPortSetupTimerInterrupt(); /* 初始化为第一个任务准备的关键嵌套计数。 */ uxCriticalNesting = 0; /* 启动第一个任务。 */ prvStartFirstTask(); /* 启动调度器后时不会跑到这里的 */ return 0; } cortex m3:prvStartFirstTask()

__asm void prvStartFirstTask( void ) { /* *INDENT-OFF* */ PRESERVE8 /* 当前栈需按照 8 字节对齐 */ /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */ ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */ ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */ ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */ /* 将msp设置回堆栈的开始 */ msr msp, r0 /* 使能全局中断 */ cpsie i cpsie f dsb isb /* 触发SVC异常开启动第一个任务. */ svc 0 nop nop /* *INDENT-ON* */ } cortex m3:vPortSVCHandler()

__asm void vPortSVCHandler( void ) { /* *INDENT-OFF* */ PRESERVE8 ldr r3, = pxCurrentTCB /* 加载 pxCurrentTCB 的地址到 r3. */ ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */ ldr r0, [ r1 ] /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */ ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */ msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */ isb mov r0, # 0 /* 将寄存器 r0 清 0 */ msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */ orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */ bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */ /* *INDENT-ON* */ } cortex m3:xPortPendSVHandler()

__asm void xPortPendSVHandler(void) { extern uxCriticalNesting; extern pxCurrentTCB; /* 指向当前激活的任务 */ extern vTaskSwitchContext; PRESERVE8 mrs r0, psp /* PSP内容存入R0 */ isb /* 指令同步隔离,清流水线 */ ldr r3, = pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */ ldr r2,[r3] stmdb r0 !,{r4 - r11} /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */ str r0,[r2] /* 将新的栈顶保存到任务TCB的第一个成员中 */ stmdb sp !,{r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/ mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界区 */ msr basepri,r0 dsb /* 数据和指令同步隔离 */ isb bl vTaskSwitchContext /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ mov r0,#0 /* 退出临界区*/ msr basepri,r0 ldmia sp !, {r3, r14} /* 恢复R3和R14*/ ldr r1,[r3] ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/ ldmia r0 !,{r4 - r11} /* 出栈*/ msr psp,r0 isb bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/ nop }

本文共计6807个文字,预计阅读时间需要28分钟。

Freertos中如何进行调度器启动过程解析?

前言+本节主要讲解启动调度器。+这些都与硬件相关,因此会分为两条线进行:POSIX和Cortex-M3。+原文:李志明博客:https://www.cnblogs.com/lizhuming/p/16076476.+5.1+调度器的基本概念+5.1.1+调度器

前言

本节主要讲解启动调度器。

这些都是与硬件相关,所以会分两条线走:posix和cortex m3。

原文:李柱明博客:www.cnblogs.com/lizhuming/p/16076476.html

5.1 调度器的基本概念 5.1.1 调度器

调度器就是使用相关的调度算法来决定当前需要执行的任务。

调度器特点:

  1. 调度器可以区分就绪态任务和挂起任务。
  2. 调度器可以选择就绪态中的一个任务,然后激活它。
  3. 不同调度器之间最大的区别就是如何分配就绪态任务间的完成时间。

嵌入式实时操作系统的核心就是调度器和任务切换:

  • 调度器的核心就是调度算法。
  • 任务切换是基于硬件内核架构实现。
5.1.2 抢占式调度

抢占式调度:

  • 每个任务都被分配了不同的优先级,抢占式调度器会获得就绪列表中优先级最高的任务,并运行这个任务。
  • 在FreeRTOS系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。
5.1.3 时间片调度

最常用的的时间片调度算法就是Round-robin调度算法,这种调度算法可以用于抢占式或者合作式的多任务中。

实现Round-robin调度算法需要给同优先级的任务分配一个专门的列表,用于记录当前就绪的任务,并为每个任务分配一个时间片。

当任务就绪链表中最高优先级中存在两个以上的任务时,当前运行的任务耗尽时间片后,当前链表的下一个任务到运行态,把当前任务重新插入到当前优先级就绪链表尾部。

使用时间片调度需要在FreeRTOSConfig.h文件中使能宏定义:#defineconfigUSE_TIME_SLICING 1

需要注意的是,freertos时间片不能随意的设置时间为多少个tick,只能默认一个tick。

5.2 cortex m3架构的三个异常

在Cortex-M3架构中,FreeRTOS为了任务启动和任务切换使用了三个异常:SVC、PendSV和SysTick。

对应三个异常回调:

#define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler #define vPortSVCHandler SVC_Handler

注意:Cortex-M的优先级数值越大其优先级越低。

5.2.1 SVC

SVC(系统服务调用,亦简称系统调用)用于任务启动。所以只被调用一次。

有些操作系统不允许应用程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用SVC发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件,它就会产生一个SVC异常。

在该异常回调里启动第一个任务。

5.2.2 PendSV

PendSV(可挂起系统调用)用于完成任务切换。

该异常可以像普通的中断一样被挂起的,它的最大特性是如果当前有优先级比它高的中断在运行,PendSV会延迟执行,直到高优先级中断执行完毕,这样子产生的PendSV中断就不会打断其他中断的运行。

在该异常的回调函数里执行任务切换。

5.2.3 SysTick

SysTick用于产生系统节拍时钟。

每次systick异常产生都会检查是否需要任务调度,如果需要,则出发PendSV异常即可。

5.3 启动调度器 5.3.1 启动调度器描述

启动调度器使用API函数vTaskStartScheduler()

该函数会:

  • 创建一个空闲任务;
  • 创建软件定时器任务;
  • 初始化一些静态变量;
  • 会初始化系统节拍定时器并设置好相应的中断;
  • 启动第一个任务。

启动调度器,硬件相关是调用xPortStartScheduler()

5.3.2 创建空闲任务

空闲任务时在启动调度器时创建的,该任务不能阻塞,创建空闲任务是为了不让系统退出,因为系统一旦启动就必须占有任务。

空闲任务主体主要是做一些系统内存的清理工作、进入休眠或者低功耗操作等操作。

创建空闲任务,也分两种方式,取决于是否开启静态内存分配宏configSUPPORT_STATIC_ALLOCATION

5.3.2.1 静态内存创建

参考前面任务基础相关的文章便可知,静态内存创建任务需要用户提供任务控制块和任务栈空间。

由于空闲任务是内核API创建的,所以用户需要通过指定的函数vApplicationGetIdleTaskMemory()提供这些信息。

实现代码如下:

/* 如果开启了静态内存功能,创建空闲任务就按静态内存创建 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxIdleTaskTCBBuffer = NULL; StackType_t * pxIdleTaskStackBuffer = NULL; uint32_t ulIdleTaskStackSize; /* 获取空闲任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。 这个API是有用户实现 */ vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); /* 创建空闲任务,使用最低优先级*/ xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, configIDLE_TASK_NAME, ulIdleTaskStackSize, ( void * ) NULL, portPRIVILEGE_BIT, pxIdleTaskStackBuffer, pxIdleTaskTCBBuffer ); if( xIdleTaskHandle != NULL ) { xReturn = pdPASS; } else { xReturn = pdFAIL; } } #endif /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */ 5.3.2.2 动态内存创建

动态内存创建空闲任务,直接使用xTaskCreate()实现即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 ) { /* 动态内存方式创建空闲任务 */ xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL, portPRIVILEGE_BIT, &xIdleTaskHandle ); } #endif /* configSUPPORT_STATIC_ALLOCATION */ 5.3.3 创建软件定时器任务

软件定时器组件功能,后面会详细分析,这里只做简单说明

和创建空闲任务一个道理。

前提条件时需要配置configUSE_TIMERS开启软件定时器功能。

创建软件定时器内容集成在xTimerCreateTimerTask()API内部了,其实现和创建空闲任务一样的。

通过宏configSUPPORT_STATIC_ALLOCATION区分静态和动态内存创建。

5.3.3.1 初始化软件定时器组件内容

调用prvCheckForValidListAndQueue()API初始化定时链表和创建定时器通信服务队列。

5.3.3.2 静态内存创建

通过用户实现的vApplicationGetTimerTaskMemory()API获取软件定时器任务控制块和任务栈信息。

#if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxTimerTaskTCBBuffer = NULL; StackType_t * pxTimerTaskStackBuffer = NULL; uint32_t ulTimerTaskStackSize; /* 获取软件定时器任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。 这个API是有用户实现 */ vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize ); /* 创建软件定时器任务 */ xTimerTaskHandle = xTaskCreateStatic( prvTimerTask, configTIMER_SERVICE_TASK_NAME, ulTimerTaskStackSize, NULL, ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, pxTimerTaskStackBuffer, pxTimerTaskTCBBuffer ); if( xTimerTaskHandle != NULL ) { xReturn = pdPASS; } } #endif 5.3.3.3 动态内存创建

动态内存创建软件定时器任务,直接使用xTaskCreate()实现即可。

#if ( configSUPPORT_STATIC_ALLOCATION != 1 ) { /* 动态内存方式创建软件定时器任务 */ xReturn = xTaskCreate( prvTimerTask, configTIMER_SERVICE_TASK_NAME, configTIMER_TASK_STACK_DEPTH, NULL, ( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT, &xTimerTaskHandle ); } #endif /* configSUPPORT_STATIC_ALLOCATION */ 5.3.4 调度器中的用户函数

在启动调度器时,内核运行用户插入一个函数调用,一般用于启动调度器标识处理。

指定函数:freertos_tasks_c_additions_init()

使能宏:FREERTOS_TASKS_C_ADDITIONS_INIT

/* freertos_tasks_c_additions_init 函数由用户定义,用于启动调度器时调用一次 */ #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT { freertos_tasks_c_additions_init(); } #endif 5.3.5 CPU利用率统计配置

如果用户配置了portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()宏函数,在调度器启动时需要调用。

该函数一般是重置定时器起始值,搭配portGET_RUN_TIME_COUNTER_VALUE()宏函数实现运行时间统计功能。

可以参考李柱明博客:cpu利用率统计。后面可能会有独立章节描述该功能的实现

在启动调度器中的代码:

/* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/ portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); 5.3.6 posix启动调度器分析

源码分析:

  • 启动调度:xPortStartScheduler()
  • 利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()
  • 利用线程通信实现启动第一个任务:vPortStartFirstTask()
  • 在第一次初始化任务栈时会跑该函数(只跑一次):prvSetupSignalsAndSchedulerPolicy()
5.3.6.1 启动调度器

在接口层,启动调度调用xPortStartScheduler()

  • 获取线程ID;
  • 配置系统滴答时钟;
  • 启动第一个任务;
  • 等待用户调用vPortEndScheduler()关闭调度。
  • 系统调度求关闭后需要删除和释放启动调度器时创建的空闲任务和软件定时器任务。
  • 恢复主线程型号掩码。

portBASE_TYPE xPortStartScheduler( void ) { int iSignal; sigset_t xSignals; /* 获取当前线程ID */ hMainThread = pthread_self(); /* 设置系统计时器以按要求的频率生成滴答中断 */ prvSetupTimerInterrupt(); /* 开启第一个任务. */ vPortStartFirstTask(); /* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); /* 等待关闭调度器的信号 */ while ( !xSchedulerEnd ) { sigwait( &xSignals, &iSignal ); } /* 删除Idle任务并释放其资源 */ #if ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) vPortCancelThread( xTaskGetIdleTaskHandle() ); #endif #if ( configUSE_TIMERS == 1 ) /* 删除软件定时器任务并释放其资源 */ vPortCancelThread( xTimerGetTimerDaemonTaskHandle() ); #endif /* configUSE_TIMERS */ /* 恢复原始信号掩模 */ (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask, NULL ); return 0; 5.3.6.2 实现滴答时钟

利用进程实时定时器实现系统滴答:prvSetupTimerInterrupt()

采用posix标准下的getitimer()setitimer()API去实现。

在进程里使用ITIMER_REAL计数器实现系统滴答时钟。

  • posix标准下,每个进程都会维护三个域的定时器,当前使用的ITIMER_REAL是进程实时定时器。

void prvSetupTimerInterrupt( void ) { struct itimerval itimer; int iRet; /* 用当前的定时器信息初始化结构 */ iRet = getitimer( ITIMER_REAL, &itimer ); if ( iRet ) { prvFatalError( "getitimer", errno ); } /* 设置定时器事件之间的时间间隔. */ itimer.it_interval.tv_sec = 0; itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS; /* 设计初始值 */ itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS; /* 重置定时器. */ iRet = setitimer( ITIMER_REAL, &itimer, NULL ); if ( iRet ) { prvFatalError( "setitimer", errno ); } /* 获取纳秒值 */ prvStartTimeNs = prvGetTimeNs(); } 5.3.6.3 启动第一个任务

利用线程通信实现启动第一个任务:vPortStartFirstTask()

原理在前面posix模拟器设计说过。

利用线程型号实现线程的启停从而实现任务切换。

先获取线程句柄:

void vPortStartFirstTask( void ) { /* 获取当前任务的线程句柄 */ Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); /* 启动第一个任务. */ prvResumeThread( pxFirstThread ); }

发信号给下一个需要跑的线程,让其启动,这样就进入了freertos世界嘞:

static void prvResumeThread( Thread_t *xThreadId ) { /* 如果当前线程不是接下来要跑的线程 */ if ( pthread_self() != xThreadId->pthread ) { /* 发送事件启动新的线程 */ event_signal(xThreadId->ev); } } void event_signal( struct event * ev ) { pthread_mutex_lock( &ev->mutex ); ev->event_triggered = true; // 解除阻塞的标记 pthread_cond_signal( &ev->cond ); // 发送信号给需要启动的线程,让其解除阻塞 pthread_mutex_unlock( &ev->mutex ); }

那还需要停止当前线程嘞,完成这些时后,回进入等待结束调度器事件而阻塞:(代码在xPortStartScheduler()中)

/* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); /* 等待关闭调度器的信号 */ while ( !xSchedulerEnd ) { sigwait( &xSignals, &iSignal ); } 5.3.7 cortex m3启动调度器分析

启动调度器:xPortStartScheduler()

SVC异常启动第一个任务:vPortSVCHandler()

Freertos中如何进行调度器启动过程解析?

5.3.7.1 基本知识
  1. cortex m的双堆栈指针MSP和PSP的切换。

  2. 硬件出入栈和软件出入栈。

    1. 硬件出入栈:异常时,硬件会完成部分必要寄存器的出入栈。
    2. 软件出入栈:由于硬件压栈信息对保护上下文不够,需要软件出入栈完成其它CPU寄存器的出入栈。
5.3.7.2 cortex m3的启动调度器的基本内容
  1. 把PendSV和SysTick设置为最低优先级的中断。

  2. 启动滴答定时器。

  3. 启动第一个任务。通过SVC异常方式。

    1. 重置MSP堆栈指针。

    2. 使能全局中断。

    3. 触发SVC异常。进入SVC异常。

      1. 获取pxCurrentTCB值,即是当前需要跑的任务句柄。
      2. 通过任务句柄获取任务控制块,通过任务控制块获取任务栈顶。
      3. 软件出栈。
      4. 更新栈顶指针到PSP。
      5. 修改R14寄存器,使异常退出时,进入线程模式,使用PSP栈指针。
      6. 退出异常。硬件自动使用PSP出栈。

至此,系统已经启动,进入freertos世界。

5.3.7.3 FromISR中断保护配置

在freertos中会看到FromISR后缀的API,这些API执行环境不一样,一般用于中断回调中使用,要求不能阻塞,快进快出。

这些API不能在中断保护外的中断回调中使用,取决于宏configMAX_SYSCALL_INTERRUPT_PRIORITY

所以需要配置进出临界能屏蔽中断的优先级级别,优先级等于或低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断能被临界API屏蔽,可调用FromISR后缀的API。

先了解下几个宏:(数值越小,中断优先级越高)

  • configLIBRARY_LOWEST_INTERRUPT_PRIORITY:定义SysTick与PendSV的中断优先级。
  • configKERNEL_INTERRUPT_PRIORITY:配置SysTick与PendSV的中断优先级到寄存器。
  • configMAX_SYSCALL_INTERRUPT_PRIORITY:定义freertos系统可控最大中断优先级。
  • configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY:用于配置basepri寄存器的,当 basepri 设置为某个值的时候,会让系统不响应比该优先级低的中断,而优先级比之更高
    的中断则不受影响。这样,freertos可以通过控制basepri值来控制部分中断,实现中断保护。

#if ( configASSERT_DEFINED == 1 ) { volatile uint32_t ulOriginalPriority; volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER ); volatile uint8_t ucMaxPriorityValue; /* 确定可以调用ISR安全FreeRTOS API函数的最大优先级。 ISR安全函数是以“FromISR”结尾的。 FreeRTOS维护独立的线程和ISR API函数,以确保进入中断尽可能快和简单。 保存将要被破坏的中断优先级值。 */ ulOriginalPriority = *pucFirstUserPriorityRegister; /* 确定可用的优先级位数。首先写所有可能的位。 */ *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE; /* 把值读回来看看,因为无效的优先级位读出位0,读出有多少个1就知道有多少位优先级。 */ ucMaxPriorityValue = *pucFirstUserPriorityRegister; /* 内核中断优先级应该设置为最低优先级。 */ configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) ); /* 对最大系统调用优先级使用相同的掩码。 */ ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue; /* 为回读的位数计算可接受的最大优先级组值。 */ ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS; while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ) { ulMaxPRIGROUPValue--; ucMaxPriorityValue <<= ( uint8_t ) 0x01; } #ifdef __NVIC_PRIO_BITS { /* 检查定义优先级位数的CMSIS配置,该配置与实际从硬件查询的优先级位数相匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS ); } #endif #ifdef configPRIO_BITS { /* 检查定义优先级位数的FreeRTOS配置,该配置与从硬件实际查询的优先级位数匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS ); } #endif /* 将优先级组的值移回它在AIRCR寄存器中的位置 */ ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT; ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK; /* 将中断的中断优先级寄存器恢复到原来的值 */ *pucFirstUserPriorityRegister = ulOriginalPriority; } #endif /* configASSERT_DEFINED */ 5.3.7.4 配置PendSV和SysTick中断优先级

PendSV用于切换任务;

SysTick用于系统节拍。

这两个都配置为最低优先级。

这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,有利于系统稳定。

而且SysTick是硬件定时器,响应可能会延迟,都是系统事件不会有偏差。

/* 将PendSV和SysTick设置为最低优先级的中断 */ portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; 5.3.7.5 启动滴答定时器

调用vPortSetupTimerInterrupt()实现。

5.3.7.6 启动第一个任务

调用prvStartFirstTask()实现。

启动第一个任务:

  • 先使能全局中断;
  • 触发进入SVC异常回调;
  • 在SVC回调切入第一个任务。

__asm void prvStartFirstTask( void ) { PRESERVE8 /* 当前栈需按照 8 字节对齐 */ /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */ ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */ ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */ ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */ /* 将msp设置回堆栈的开始 */ msr msp, r0 /* 使能全局中断 */ cpsie i cpsie f dsb isb /* 触发SVC异常开启动第一个任务. */ svc 0 nop nop /* *INDENT-ON* */ }

SVC回调:

  • 通过pxCurrentTCB获取当前需要跑的第一个任务控制块;
  • 获取该任务栈顶地址;
  • 从栈顶地址软件出栈;(下文恢复)
  • 更新栈顶地址到PSP;
  • 双堆栈指针从MSP转用PSP;
  • 异常返回,硬件会根据PSP栈出栈,完成下文恢复,进入freertos第一个任务。

__asm void vPortSVCHandler( void ) { /* *INDENT-OFF* */ PRESERVE8 ldr r3, = pxCurrentTCB /* 加载 pxCurrentTCB 的地址到 r3. */ ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */ ldr r0, [ r1 ] /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */ ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */ msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */ isb mov r0, # 0 /* 将寄存器 r0 清 0 */ msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */ orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */ bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */ /* *INDENT-ON* */ } 5.3.7.7 启动第一个任务后的任务栈情况

该图片源自野火

附件 vTaskStartScheduler()

void vTaskStartScheduler( void ) { BaseType_t xReturn; /* 如果开启了静态内存功能,创建空闲任务就按静态内存创建 */ #if ( configSUPPORT_STATIC_ALLOCATION == 1 ) { StaticTask_t * pxIdleTaskTCBBuffer = NULL; StackType_t * pxIdleTaskStackBuffer = NULL; uint32_t ulIdleTaskStackSize; /* 获取空闲任务的任务控制块地址、任务栈地址、任务栈大小这三个参数。 这个API是有用户实现 */ vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize ); /* 创建空闲任务,使用最低优先级*/ xIdleTaskHandle = xTaskCreateStatic( prvIdleTask, configIDLE_TASK_NAME, ulIdleTaskStackSize, ( void * ) NULL, portPRIVILEGE_BIT, pxIdleTaskStackBuffer, pxIdleTaskTCBBuffer ); if( xIdleTaskHandle != NULL ) { xReturn = pdPASS; } else { xReturn = pdFAIL; } } #else /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */ { /* 动态内存方式创建空闲任务 */ xReturn = xTaskCreate( prvIdleTask, configIDLE_TASK_NAME, configMINIMAL_STACK_SIZE, ( void * ) NULL, portPRIVILEGE_BIT, &xIdleTaskHandle ); } #endif /* configSUPPORT_STATIC_ALLOCATION */ #if ( configUSE_TIMERS == 1 ) { if( xReturn == pdPASS ) { xReturn = xTimerCreateTimerTask(); } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_TIMERS */ if( xReturn == pdPASS ) { /* freertos_tasks_c_additions_init 函数由用户定义,用于启动调度器时调用一次 */ #ifdef FREERTOS_TASKS_C_ADDITIONS_INIT { freertos_tasks_c_additions_init(); } #endif /* 先关闭中断,确保节拍定时器中断不会在调用xPortStartScheduler()时或之前发生。当第一个任务启动时,会重新启动中断*/ portDISABLE_INTERRUPTS(); #if ( configUSE_NEWLIB_REENTRANT == 1 ) { /* 略 */ _impure_ptr = &( pxCurrentTCB->xNewLib_reent ); } #endif /* configUSE_NEWLIB_REENTRANT */ /* 初始化静态变量 */ xNextTaskUnblockTime = portMAX_DELAY; xSchedulerRunning = pdTRUE; xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; /* 如果宏configGENERATE_RUN_TIME_STATS被定义,表示使用运行时间统计功能,则下面这个宏必须被定义,用于初始化一个基础定时器/计数器.*/ portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); traceTASK_SWITCHED_IN(); /* 设置系统节拍定时器,这与硬件特性相关,因此被放在了移植层.*/ if( xPortStartScheduler() != pdFALSE ) { /* 如果调度器正确运行,则不会执行到这里,函数也不会返回*/ } else { /* 仅当任务调用API函数xTaskEndScheduler()后,会执行到这里.*/ } } else { /* 执行到这里表示内核没有启动,可能因为堆栈空间不够 */ configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY ); } /* 预防编译器警告*/ ( void ) xIdleTaskHandle; ( void ) uxTopUsedPriority; } posix:xPortStartScheduler()

portBASE_TYPE xPortStartScheduler( void ) { int iSignal; sigset_t xSignals; /* 获取当前线程ID */ hMainThread = pthread_self(); /* 设置系统计时器以按要求的频率生成滴答中断 */ prvSetupTimerInterrupt(); /* 开启第一个任务. */ vPortStartFirstTask(); /* 等待用户调用关闭调度器 vPortEndScheduler() 这个API发出的信号 */ sigemptyset( &xSignals ); sigaddset( &xSignals, SIG_RESUME ); /* 等待关闭调度器的信号 */ while ( !xSchedulerEnd ) { sigwait( &xSignals, &iSignal ); } /* Cancel the Idle task and free its resources */ #if ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) vPortCancelThread( xTaskGetIdleTaskHandle() ); #endif #if ( configUSE_TIMERS == 1 ) /* Cancel the Timer task and free its resources */ vPortCancelThread( xTimerGetTimerDaemonTaskHandle() ); #endif /* configUSE_TIMERS */ /* Restore original signal mask. */ (void)pthread_sigmask( SIG_SETMASK, &xSchedulerOriginalSignalMask, NULL ); return 0; } posix:prvSetupTimerInterrupt()

void prvSetupTimerInterrupt( void ) { struct itimerval itimer; int iRet; /* 用当前的定时器信息初始化结构 */ iRet = getitimer( ITIMER_REAL, &itimer ); if ( iRet ) { prvFatalError( "getitimer", errno ); } /* 设置定时器事件之间的时间间隔. */ itimer.it_interval.tv_sec = 0; itimer.it_interval.tv_usec = portTICK_RATE_MICROSECONDS; /* 设计初始值 */ itimer.it_value.tv_sec = 0; itimer.it_value.tv_usec = portTICK_RATE_MICROSECONDS; /* 重置定时器. */ iRet = setitimer( ITIMER_REAL, &itimer, NULL ); if ( iRet ) { prvFatalError( "setitimer", errno ); } /* 获取纳秒值 */ prvStartTimeNs = prvGetTimeNs(); } posix:vPortStartFirstTask()

void vPortStartFirstTask( void ) { /* 获取当前任务的线程句柄 */ Thread_t *pxFirstThread = prvGetThreadFromTask( xTaskGetCurrentTaskHandle() ); /* 启动第一个任务. */ prvResumeThread( pxFirstThread ); } static void prvResumeThread( Thread_t *xThreadId ) { /* 如果当前线程不是接下来要跑的线程 */ if ( pthread_self() != xThreadId->pthread ) { /* 发送事件启动新的线程 */ event_signal(xThreadId->ev); } } void event_signal( struct event * ev ) { pthread_mutex_lock( &ev->mutex ); ev->event_triggered = true; // 解除阻塞的标记 pthread_cond_signal( &ev->cond ); // 发送信号给需要启动的线程,让其解除阻塞 pthread_mutex_unlock( &ev->mutex ); } cortex m3:xPortStartScheduler()

BaseType_t xPortStartScheduler( void ) { #if ( configASSERT_DEFINED == 1 ) { volatile uint32_t ulOriginalPriority; volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER ); volatile uint8_t ucMaxPriorityValue; /* 确定可以调用ISR安全FreeRTOS API函数的最大优先级。 ISR安全函数是以“FromISR”结尾的。 FreeRTOS维护独立的线程和ISR API函数,以确保进入中断尽可能快和简单。 保存将要被破坏的中断优先级值。 */ ulOriginalPriority = *pucFirstUserPriorityRegister; /* 确定可用的优先级位数。首先写所有可能的位。 */ *pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE; /* 把值读回来看看,因为无效的优先级位读出位0,读出有多少个1就知道有多少位优先级。 */ ucMaxPriorityValue = *pucFirstUserPriorityRegister; /* 内核中断优先级应该设置为最低优先级。 */ configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) ); /* 对最大系统调用优先级使用相同的掩码。 */ ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue; /* 为回读的位数计算可接受的最大优先级组值。 */ ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS; while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE ) { ulMaxPRIGROUPValue--; ucMaxPriorityValue <<= ( uint8_t ) 0x01; } #ifdef __NVIC_PRIO_BITS { /* 检查定义优先级位数的CMSIS配置,该配置与实际从硬件查询的优先级位数相匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS ); } #endif #ifdef configPRIO_BITS { /* 检查定义优先级位数的FreeRTOS配置,该配置与从硬件实际查询的优先级位数匹配。 */ configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS ); } #endif /* 将优先级组的值移回它在AIRCR寄存器中的位置 */ ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT; ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK; /* 将中断的中断优先级寄存器恢复到原来的值 */ *pucFirstUserPriorityRegister = ulOriginalPriority; } #endif /* configASSERT_DEFINED */ /* 将PendSV和SysTick设置为最低优先级的中断 */ portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; /* 启动滴答定时器。注意,当前全局中断是关闭的,在启动第一个任务时会开启。 */ vPortSetupTimerInterrupt(); /* 初始化为第一个任务准备的关键嵌套计数。 */ uxCriticalNesting = 0; /* 启动第一个任务。 */ prvStartFirstTask(); /* 启动调度器后时不会跑到这里的 */ return 0; } cortex m3:prvStartFirstTask()

__asm void prvStartFirstTask( void ) { /* *INDENT-OFF* */ PRESERVE8 /* 当前栈需按照 8 字节对齐 */ /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */ ldr r0, =0xE000ED08 /* 将 0xE000ED08 这个立即数加载到寄存器 R0 */ ldr r0, [ r0 ] /* 将 0xE000ED08 地址中的值,也就是向量表的实际地址加载到 R0 */ ldr r0, [ r0 ] /* 根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针 MSP 的初始值 */ /* 将msp设置回堆栈的开始 */ msr msp, r0 /* 使能全局中断 */ cpsie i cpsie f dsb isb /* 触发SVC异常开启动第一个任务. */ svc 0 nop nop /* *INDENT-ON* */ } cortex m3:vPortSVCHandler()

__asm void vPortSVCHandler( void ) { /* *INDENT-OFF* */ PRESERVE8 ldr r3, = pxCurrentTCB /* 加载 pxCurrentTCB 的地址到 r3. */ ldr r1, [ r3 ] /* 加载 pxCurrentTCB 到 r3. 而任务控制块的第一个成员就是任务栈顶指针。 */ ldr r0, [ r1 ] /* 任务控制块的第一个成员就是栈顶指针,所以此时 r0 等于栈顶指针 */ ldmia r0 !, { r4 - r11 } /* 软件出栈部分,r4-r11寄存器出栈 */ msr psp, r0 /* 将新的栈顶指针 r0 更新到 psp,任务执行的时候使用的堆栈指针是psp. */ isb mov r0, # 0 /* 将寄存器 r0 清 0 */ msr basepri, r0 /* 设置 basepri 寄存器的值为 0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。Cortex-M的优先级数值越大其优先级越低。 */ orr r14, # 0xd /* 向 r14 寄存器最后 4 位按位或上0x0D。退出异常时使用进程堆栈指针 PSP 完成出栈操作并返回后进入任务模式、返回 Thumb 状态 */ bx r14 /* 异常返回,这个时候出栈使用的是 PSP 指针,自动将栈中的剩下内容加载到 CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0。PSP 的值也将更新,即指向任务栈的栈顶 */ /* *INDENT-ON* */ } cortex m3:xPortPendSVHandler()

__asm void xPortPendSVHandler(void) { extern uxCriticalNesting; extern pxCurrentTCB; /* 指向当前激活的任务 */ extern vTaskSwitchContext; PRESERVE8 mrs r0, psp /* PSP内容存入R0 */ isb /* 指令同步隔离,清流水线 */ ldr r3, = pxCurrentTCB /* 当前激活的任务TCB指针存入R2 */ ldr r2,[r3] stmdb r0 !,{r4 - r11} /* 保存剩余的寄存器,异常处理程序执行前,硬件自动将xPSR、PC、LR、R12、R0-R3入栈 */ str r0,[r2] /* 将新的栈顶保存到任务TCB的第一个成员中 */ stmdb sp !,{r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护; R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护*/ mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界区 */ msr basepri,r0 dsb /* 数据和指令同步隔离 */ isb bl vTaskSwitchContext /* 调用函数,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */ mov r0,#0 /* 退出临界区*/ msr basepri,r0 ldmia sp !, {r3, r14} /* 恢复R3和R14*/ ldr r1,[r3] ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/ ldmia r0 !,{r4 - r11} /* 出栈*/ msr psp,r0 isb bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/ nop }