FreeRTOS系列|多任务调度

本文详细解析了FreeRTOS系统中多任务的启动流程,包括内核启动、任务调度器、调度器启动及第一个任务的执行。在启动过程中,涉及到的任务创建、中断配置、上下文切换等关键步骤通过源码分析进行了深入阐述。此外,还介绍了任务切换的场景,如系统滴答定时器中断和系统调用,并展示了PendSV中断服务函数在任务切换中的核心作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 多任务启动流程

多任务启动流程如下表所示

启动后以下各函数由上至下依次执行含义
osKernelStart()启动内核
vTaskStartScheduler()启动任务调度器
xPortStartScheduler()启动调度器
prvStartFirstTask()启动第一个任务
SVC调用SVC中断
2. 源码分析
  • 启动任务调度器
void vTaskStartScheduler( void ){
  BaseType_t xReturn;
  /* Add the idle task at the lowest priority. */
  #if(configSUPPORT_STATIC_ALLOCATION == 1){
  }
  #else{
	/* 动态创建空闲任务 */
	xReturn = xTaskCreate(prvIdleTask,
						  "IDLE", configMINIMAL_STACK_SIZE,
						  (void *) NULL,
						  (tskIDLE_PRIORITY|portPRIVILEGE_BIT),
						  &xIdleTaskHandle); 
  }
  #endif /* configSUPPORT_STATIC_ALLOCATION */

  if(xReturn == pdPASS){
	/* 关闭中断 */
	portDISABLE_INTERRUPTS();  		
	/* 下一个任务锁定时间赋值为最大,其实就是时间片调度,不让其进行调度 */
	#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
	xNextTaskUnblockTime = portMAX_DELAY;
	/* 调度器的运行状态置位,标记开始运行了 */
	xSchedulerRunning = pdTRUE;
	/* 初始化系统的节拍值为0 */
	xTickCount = ( TickType_t ) 0U;
	/* 启动调度器 */
	if(xPortStartScheduler() != pdFALSE){
	  //如果调度器启动成功就不会执行到这里,所以没有代码
	}
	else{
	  //不会执行到这里,所以没有代码
	}
  }
  else{
	//运行到这里说明系统内核没有启动成功,空闲任务创建失败	
  }
}

  • 启动调度器:FreeRTOS系统时钟是由滴答定时器来提供,任务切换也会用到PendSV中断,这些硬件的初始化在这里完成
BaseType_t xPortStartScheduler( void ){
  /* 为了保证系统的实时性,配置systick和pendsv为最低的优先级 */
  portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
  portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
  /* 配置滴答定时器systick的定时周期,并开启systick中断 */
  vPortSetupTimerInterrupt();
  /* 初始化临界段嵌套计数器 */
  uxCriticalNesting = 0;
  /* 启动第一个任务 */
  prvStartFirstTask();
  /* 代码正常执行的话不会到这里! */
  return 0;
}

  • 启动第一个任务:用于启动第一个任务,是一个汇编函数
__asm void prvStartFirstTask( void ){
  PRESERVE8	//8字节对齐,AAPCS的标准,ARM特有
  /* 将0xE000ED08保存在寄存器R0中;它是中断向量表的一个地址,
     存储的是MSP的指针,最终获取到MSP的RAM的地址 */
  ldr r0, =0xE000ED08
  ldr r0, [r0]	//取R0保存的地址处的值赋给R0
  ldr r0, [r0]	//获取MSP初始值
  /* 重新把MSP的地址,赋值为MSP,相当于复位MSP	*/
  msr msp, r0
  /* 开启全局中断 */
  cpsie i	//使能中断
  cpsie f	//使能中断
  dsb		//数据同步屏障
  isb		//指令同步屏障
  /* 调用SVC  */
  svc 0
  nop
  nop
}


  • 调用SVC中断:在prvStartFirstTask()中通过调用SVC指令触发了SVC中断,而第一个任务的启动就是在SVC中断服务函数中完成的
__asm void vPortSVCHandler(void){
  PRESERVE8//8字节对齐

  /* 获取当前任务控制块 */
  ldr	r3, =pxCurrentTCB
  ldr r1, [r3]	//
  ldr r0, [r1]	//
  /* 出栈内核寄存器,R14其实就是异常返回值 */
  ldmia r0!, {r4-r11, r14}
  /* 进程栈指针PSP设置为任务的堆栈 */
  msr psp, r0
  isb	//指令同步屏障
  /* 把basepri赋值为0,即打开屏蔽中断 */
  mov r0, #0
  msr basepri, r0
  /* 异常退出 */
  bx r14
}

3. 任务切换
3.1 任务切换场合

RTOS系统的核心是任务管理,而任务管理的核心是任务切换,任务切换决定了任务的执行顺序。上下文(任务)切换被触发的场合可以是

  • 系统滴答定时器(SysTick)中断
  • 执行一个系统调用

典型的嵌入式OS系统中,处理器被划分为多个时间片。若系统中只有两个任务,这两个任务会交替执行,任务切换都是在SysTick中断中执行,如下图示:
在这里插入图片描述
在一些OS设计中,为了解决SysTick和IRQ的冲突问题,PendSV异常将上下文切换请求延迟到所有其他IRQ处理都已经完成后,在PendSV异常内执行上下文切换。如下图示:

在这里插入图片描述
PendSV(可挂起的系统调用)异常对OS操作非常重要,其优先级可通过编程设置。可通过将中断控制和状态寄存器ICSR的bit28(挂起位)置1来触发PendSV中断。上面提到过上下文切换被触发的两个场合:SysTick中断和执行一个系统调用,其源码分析如下:

  • SysTick中断
//滴答定时器中断服务函数
void SysTick_Handler(void){
  if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED){ //系统已经运行
	xPortSysTickHandler();
  }
}

void xPortSysTickHandler( void ){
  vPortRaiseBASEPRI();	//关闭中断
  {
	if( xTaskIncrementTick() != pdFALSE ){ //增加时钟计数器xTickCount的值						
	  /* 通过向中断控制和状态寄存器的bit28位写入1挂起PendSV来启动PendSV中断 */
	  portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 
	}
  }
  vPortClearBASEPRIFromISR();	//打开中断
}

  • 执行一个系统调用
//以任务切换函数taskYIELD()为例
#define taskYIELD()  portYIELD()
#define portYIELD() 
{ 
  /* 通过向中断控制和状态寄存器的bit28位写入1挂起PendSV来启动PendSV中断 */
  portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 
  __dsb( portSY_FULL_READ_WRITE ); 
  __isb( portSY_FULL_READ_WRITE ); 
}

3.2 PendSV中断服务函数

FreeRTOS任务切换的具体过程是在PendSV中断服务函数中完成的,下面分析PendSV中断服务函数源码,看看切换过程是如何进行的

__asm void xPortPendSVHandler( void ){
  extern uxCriticalNesting;
  extern pxCurrentTCB;
  extern vTaskSwitchContext;

  PRESERVE8

  mrs r0, psp
  isb
  /* 获取当前任务控制块,其实就获取任务栈顶 */
  ldr	r3, =pxCurrentTCB
  ldr	r2, [r3]
  /* 浮点数处理,如果使能浮点数,就需要入栈 */
  tst r14, #0x10
  it eq
  vstmdbeq r0!, {s16-s31}
  /* 保存内核寄存器---调用者需要做的 */
  stmdb r0!, {r4-r11, r14}
  /* 保存当前任务栈顶,把栈顶指针入栈 */
  str r0, [r2]
  stmdb sp!, {r3}
  /* 使能可屏蔽的中断-----临界段 */
  mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
  msr basepri, r0
  dsb
  isb
  /* 执行上线文切换 */
  bl vTaskSwitchContext
  /* 使能可屏蔽的中断 */
  mov r0, #0
  msr basepri, r0
  /* 恢复任务控制块指向的栈顶 */
  ldmia sp!, {r3}
  /* 获取当前栈顶 */
  ldr r1, [r3]
  ldr r0, [r1]
  /* 出栈*/
  ldmia r0!, {r4-r11, r14}
  /* 出栈*/
  tst r14, #0x10
  it eq
  vldmiaeq r0!, {s16-s31}
  /* 更新PSP指针 */
  msr psp, r0
  isb
  /* 异常返回,下面要执行的代码,就是要切换的任务代码了 */
  bx r14
  nop
  nop
}

在PendSV中断服务函数中有调用函数vTaskSwitchContext来获取下一个要运行的任务,其源码如下

void vTaskSwitchContext( void ){
  if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ){
	/* 标记调度器状态*/
	xYieldPending = pdTRUE;
  }
  else{
	/* 标记调度器状态*/
	xYieldPending = pdFALSE;
	/* 检查任务栈是否溢出 */
	taskCHECK_FOR_STACK_OVERFLOW();
	/* 选择优先级最高的任务,把当前的任务控制块进行赋值 */
	taskSELECT_HIGHEST_PRIORITY_TASK();
  }
}

### FreeRTOS 中微秒级别任务调度机制 FreeRTOS 是一种实时操作系统内核,专为嵌入式设备设计。为了实现微秒级别的任务调度FreeRTOS 提供了高分辨率定时器支持和精确的时间管理。 #### 高分辨率定时器配置 要启用微秒级精度的任务调度,通常需要配置系统的滴答频率(tick rate)。默认情况下,FreeRTOS 的滴答率设置较低,一般为 1kHz 或更低。对于更高精度的需求,可以增加此频率: ```c #define configTICK_RATE_HZ ((TickType_t)10000) ``` 上述定义将滴答周期设为每秒钟发生 10,000 次中断,即每次中断间隔约为 100 微秒[^1]。 #### 时间片轮转与上下文切换 当多个同优先级任务竞争 CPU 资源时,FreeRTOS 使用时间片轮转策略来公平分配执行时间给这些任务。通过调整 `configUSE_TIME_SLICING` 宏,可以选择开启或关闭该特性。如果启用了时间切片,则每当一次滴答中断到来时就会触发潜在的上下文切换操作。 此外,在某些应用场景下可能还需要考虑减少上下文切换开销的方法,比如优化堆栈大小、限制全局变量访问范围等措施以提高效率并保持响应速度。 #### 实现细节 为了让开发者能够更方便地处理亚毫秒量级延迟需求,FreeRTOS 提供了一些辅助函数用于获取当前系统时间和延时指定时间段的功能: - **vTaskDelay()**: 基于 tick 计数进行阻塞等待; - **pdMS_TO_TICKS(), pdUS_TO_TICKS()**: 将毫秒/微秒转换成相应的 ticks 数值以便传参给 vTaskDelay 函数调用; 需要注意的是,由于硬件平台差异较大,具体实现可能会有所不同。例如 ARM Cortex-M 架构处理器上可以通过 SysTick 定时器配合 systick_handler 来完成精准计时功能[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值