本章基于FreeRTOS的启动任务调度器源码分析,后续将会上传其它我对FreeRTOS的源码分析过程及理解,首先来认识一下任务调度器。
任务调度器:
任务调度器主要用于实现任务的切换,任务并不是我们所熟知的函数,而是指一段占有独立内存空间,专门用于处理一组逻辑事件的任务块。最直观的对比理解就像是可以有多个while函数,以前我们写代码都是一个main函数一个while循环,我们会在while循环里面做完所有的事,当任务大起来以后,我们会将各个功能分为多个函数来写,使用状态机的方式来实现逻辑操作,有时我们还需要借助各种中断来置位标志位,然后等待main里面的while轮询去执行。这个就是单任务系统,又称为前后台系统,中断服务函数为前台,main的操作为后台程序。这种操作方式的实时性非常差,由于是while轮询,没轮到就只能一直等着,对于简单的应用程序是够用的,但是一旦应用大起来就力不从心了。因此这里引入多任务的实现方式,相当于可以写多个while来处理我们的代码,显然这更符合我们的处理事件的逻辑,但是CPU只有一个,又如何做到一心多用呢,实际上操作系统是通过定时器中断来隔一阵子切换一个while,并不是真的是并行处理着两个while,这种处理方式我们叫做并发,各个while各占用一段时间,由任务调度器来决定下一个while是谁,具体怎么切换的就是任务调度器的工作了。
说到任务调度一定离不开堆栈,堆栈也是OS的基础,堆栈可以引起很多话题,下面只做个简单的介绍,后面代码会具体分析。
堆栈:笼统地讲,堆栈操作就是对内存的读写操作,在我们执行main函数的时候,我们在里面定义变量,修改变量实际上都是在操作堆栈,堆栈这里理解为内存即可,每定义一个变量,都会占用一定的内存空间,比如int类型变量就是4字节的内存空间,也就是占用4字节的堆栈空间,main这个进程的堆栈大小我们可以自行定义,但不能超过CPU的最大内存。如果将一块大的内存分为多个小的堆栈内存,不就可以实现类似多个main进程一样的效果了吗,每个进程使用自己的那块堆栈,大家并行不悖,当需要跳转的时候使用任务调度器进行一下堆栈切换就好了。
FreeRTOS能实现效果上多个任务的同时运行,由于任务调度器在各个任务之间的切换非常快,所以效果上看起来就像是所有的任务同时运行一样,这章主要分析FreeRTOS是如何启动任务调度器的(包括启动调度器前的准备工作和堆栈切换),至于调度器如何根据时间片和优先级来实现任务间的切换后面会继续分析源码的实现。
以一段初始化开始,一般我们在使用FreeRTOS之前都会先定义一个启动任务,然后启动任务调度器,如下:
int main()
{
/* 初始化外设 */
init_periph();
/* 创建开始任务 */
xTaskCreate((TaskFunction_t)start_task, /* 任务函数 */
(const char* )"start_task", /* 任务名称 */
(uint16_t )START_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传递给任务函数 */
(UBaseType_t )START_TASK_PRIO, /* 任务优先级 */
(TaskHandle_t* )&StartTask_Handler);/* 任务句柄 */
/* 开启任务调度 */
vTaskStartScheduler();
}
上面先创建了一个任务,任务的创建先不管,后面会单独进行分析,这章主要分析启动调度器的过程,即“vTaskStartScheduler();”这个函数的分析。
这里先简单总结下本章讲解的整个过程:
创建开始任务 -> 创建空闲任务 -> 关闭中断 -> 根据配置文件初始化系统滴答(作为系统时钟 )-> 获取并复位MSP(主堆栈) -> 调用SVC中断
-> 找到当前控制块 -> 打开中断 -> 任务出栈 -> 执行开始任务函数(为什么不是执行空闲任务呢?留在文末说明)
下列源码省略了部分宏定义的回调函数。
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/* 静态方式创建空闲任务(关于任务的创建这里先不分析,知道这里会创建这个任务即可) */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* 由于是静态方式创建所以需要获取存储空间 */
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
/* 静态内存方式创建空闲任务 */
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL,
portPRIVILEGE_BIT,
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
/* 创建成功返回PASS */
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else
{
/* 动态方式创建空闲任务 */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT,
&xIdleTaskHandle );
}
#endif
/* 为1即使用配置了软件定时器 */
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();/* /创建定时器任务 */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
/* 创建空闲任务成功 */
if( xReturn == pdPASS )
{
/* 关闭中断,这句会在下面进行解析*/
portDISABLE_INTERRUPTS();
/* 配置使用newlib, Newlib是一个C库函数,并非FreeRTOS维护,FreeRTOS也不对使用结果负责。*/
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
xNextTaskUnblockTime = portMAX_DELAY;/* 这是个全局变量,保存着下一个要取消阻塞的任务的最小时间 */
xSchedulerRunning = pdTRUE;/* 调度器开始运行标志 */
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;/* 时钟计数器,每个定时器周期会递增,这里初始化为0 */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();/* 时间统计功能函数,是个回调,需要我们自己定义一个定时器 */
/* 启动调度器,一去不复返 */
if( xPortStartScheduler() != pdFALSE )
{
/* 如果调度器打开成功不会运行到这里 */
}
else
{
/* 不会运行到这里除非调用xTaskEndScheduler */
}
}
else
{
/* 如果运行到了这里可能由于创建空闲任务或定时器任务申请内存时内存不够 */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* 如果定义宏INCLUDE_xTaskGetIdleTaskHandle为0,就会提示xIdleTaskHandle未使用 */
( void ) xIdleTaskHandle;
}
整个过程下来除了任务创建这个函数,我们先分析一下其余的几个函数:
portDISABLE_INTERRUPTS 关闭中断函数
xPortStartScheduler 启动调度器函数
portDISABLE_INTERRUPTS 是个宏定义,实际是调用vPortRaiseBASEPRI
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
/* 定义BASEPRI这个特殊功能寄存器的值,BASEPRI即除能所有优先级不高于某个具体数值的中断*/
/* #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 */
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* (msr:写通用寄存器的值到特殊功能寄存器)中断优先级号大于191的都会被关闭 */
msr basepri, ulNewBASEPRI
dsb (数据同步隔离(与流水线、 MPU 和 cache 等有关))
isb (指令同步隔离(与流水线和 MPU 等有关))
}
}
<<Cortex-M3权威指南(中文)>>中的解释如下:
xPortStartScheduler 启动调度器函数
BaseType_t xPortStartScheduler( void )
{
#if( configASSERT_DEFINED == 1 )
{
/* 有关断言机制的操作,调试代码的时候可以检查传入的参数是否合理,FreeRTOS中
* 关键点都会调用configASSERT(x),当x为0时说明有错误发生,使用断言的话会导致
* 开销加大,一般只在调试阶段使用,正式发布代码是这个宏都要去掉
*/
}
#endif /* conifgASSERT_DEFINED */
/* 设置pendsv和systick为最低优先级中断,这两个中断在后面章节解析 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化及设置滴答定时器定时周期,并使能定时器中断,后面看滴答定时器中断函数再做分析 */
vPortSetupTimerInterrupt();
/* 用于记录临界区的嵌套次数,这里做初始化 */
uxCriticalNesting = 0;
/* 启动第一个任务,一去不复返 */
prvStartFirstTask();
/* 不会执行到这里 */
return 0;
}
__asm void prvStartFirstTask( void )
{
PRESERVE8 /* 8字节对齐 */
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08 /* VTOR寄存器,存着向量表地址 */
ldr r0, [r0] /* 得到0x8000000这个地址 */
ldr r0, [r0] /* 从0x8000000获取MSP */
msr msp, r0 /* 复位MSP */
cpsie i /* 开中断 */
cpsie f /* 开异常 */
dsb /* 数据同步屏障 */
isb /* 指令同步屏障 */
svc 0 /* 触发SVC中断,只在启动调度器时使用一次 */
nop
nop
}
__asm void vPortSVCHandler( void )
{
PRESERVE8 //8字节对齐
/* 获取任务控制块,pxCurrentTCB 为全局变量,保存着当前需要执行任务的堆栈地址 */
ldr r3, =pxCurrentTCB
/* 获取任务控制块地址,[]为取指 */
ldr r1, [r3]
/* 从任务控制块获取任务栈顶地址 */
ldr r0, [r1]
/* 将获取到的栈的r4-r11出栈,其它寄存器由退出中断时自动出栈 */
ldmia r0!, {r4-r11}
/* 更新进程栈(PSP)顶指针 */
msr psp, r0
/* 指令同步 */
isb
/* 设置寄存器basepri=0,开启中断(前面通过basepri关了所有中断) */
mov r0, #0
msr basepri, r0
/* /r14和0xd进行或运算,得到的值给r14,进去线程模式并使用进程栈 */
orr r14, #0xd
/* 执行这句后会恢复R0~R3、R12、LR、PC和xPSR,执行PC中保存的任务函数 */
bx r14
/* 由于pxCurrentTCB里面定义了PC的值,所以到这里就会跳到最开始创建的那个启动任务的函数执行 */
}
上面这句“ orr r14, #0xd ”在权威手册中有提到,这个值的定义如下:
什么是主堆栈和进程栈呢,这是Cortex-M3 采用的双堆栈机制,在复位后缺省使用的主堆栈(MSP),一些简单的应用程序只使用MSP就足够了,以往我们的程序main和中断都共用MSP,但是当应用一旦大起来,使用OS的话使用双堆栈就更加合适,在用户应用进程使用PSP,在异常处理时使用MSP,主要为了避免系统堆栈(MSP)因应用程序的错误使用而毁坏,给应用程序专门配一个堆栈,不让它共享操作系统内核的堆栈。具体切换过程核心手册中有提到,如下:
上面我们使用的SVC中断,为什么要在SVC中断中去执行堆栈的切换呢,直接在用户进程中切换行不行呢,这里不得不提到Cortex的操作模式了,Cortex‐M3 支持 2 个模式(Handler模式线程模式)和两个特权等级(特权级和用户级)。在复位缺省状态下,处理器进入线程模式+特权级。此时允许写特殊功能寄存器和 NVIC中寄存器,这种操作方式存在风险,所以在OS中会让用户进程处在用户级,而异常处理则只能处在特权级,上面的SVC中断就处于特权级,在这里面去操作CONTROL寄存器来实现主堆栈和进程堆栈的切换是比较合适的。Cortex‐M3权威手册中的也有说明,如下:
关于SVC中断的更具体的内容可以参考<<Cortex‐M3权威手册>>
整个过程创建了两个任务,一个启动任务一个空闲任务,但在进行任务堆栈切换的时候,调用了pxCurrentTCB,当前任务控制块是用的启动任务的还是空闲任务的呢?这里pxCurrentTCB使用的是启动任务的堆栈,因为空闲任务的优先级已经被设置为了最低,而启动任务的优先级高于它,所以当前会先获取启动任务的堆栈。这个启动任务是自己创建的,可有可无,只是为了方便用于管理任务创建,如果没创建任何任务,调度器启动后就会执行空闲任务。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)