注:在目前的 RTOS 中,主要有两种比较流行的启动方式,暂时还没有看到第三种,接下来我将通过伪代码的方式来讲解下这两种启动方式的区别,然后再具体分析下 FreeRTOS的启动流程。
15.1 万事俱备,只欠东风
第一种我称之为万事俱备, 只欠东风法。这种方法是在
- main 函数中将硬件初始化,
- RTOS 系统初始化,所有任务的创建这些都弄好,
这个称之为万事都已经准备好。最后只欠一道东风,
即启动 RTOS 的调度器,开始多任务的调度 ```c int main (void) { / 硬件初始化 / HardWare_Init(); (1)
/ RTOS 系统初始化 / RTOS_Init(); (2)
/ 创建任务 1,但任务 1 不会执行,因为调度器还没有开启 / RTOS_TaskCreate(Task1); / 创建任务 2,但任务 2 不会执行,因为调度器还没有开启 / RTOS_TaskCreate(Task2);
/ ……继续创建各种任务 /
/ 启动 RTOS,开始调度 / RTOS_Start(); }
void Task1( void arg ) { while (1) { / 任务实体,必须有阻塞的情况出现 */ } }
void Task1( void arg ) { while (1) { / 任务实体,必须有阻塞的情况出现 */ } }
1. RTOS 系统初始化。比如 RTOS 里面的全局变量的初始化,空闲任务的创建等。不同的 RTOS,它们的初始化有细微的差别
1. 任务实体通常是一个不带返回值的无限循环的 C 函数,函数体必须有阻塞的情况出现,不然任务(如果优先权恰好是最高)会一直在 while 循环里面执行,导致其它任务没有执行的机会。
<a name="xvK4U"></a>
## 15.2 小心翼翼、十分谨慎
这种方法是在 main 函数中将硬件和 RTOS 系统先初始化好,然后创建一个启动任务后就启动调度器,然后在启动任务里面创建各种应用任务,当所有任务都创建成功后,启动任务把自己删除。
```c
int main (void)
{
/* 硬件初始化 */
HardWare_Init();
/* RTOS 系统初始化 */
RTOS_Init();
/* 创建一个任务 */
RTOS_TaskCreate(AppTaskCreate);
/* 启动 RTOS,开始调度 */
RTOS_Start();
}
/* 起始任务,在里面创建任务 */
void AppTaskCreate( void *arg )
{
/* 创建任务 1,然后执行 */
RTOS_TaskCreate(Task1);
/* 当任务 1 阻塞时,继续创建任务 2,然后执行 */
RTOS_TaskCreate(Task2);
/* ......继续创建各种任务 */
/* 当任务创建完成, 删除起始任务 */
RTOS_TaskDelete(AppTaskCreate);
}
void Task1( void *arg )
{
while (1)
{
/* 任务实体,必须有阻塞的情况出现 */
}
}
void Task2( void *arg )
{
while (1)
{
/* 任务实体,必须有阻塞的情况出现 */
}
}
15.3 优劣
好像没有区别:
LiteOS 和 ucos 第一种和第二种都可以使用,由用户选择, RT-Thread 和 FreeRTOS 则默认使用第二种。
15.4 FreeRTOS的启动流程
在系统上电的时候第一个执行的是启动文件里面由汇编编写的复位函数Reset_Handler。
复位函数的最后会调用 C 库函数main
main 函数的主要工作是初始化系统的堆和栈,最后调用 C 中的 main 函数,从而去到 C 的世界
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
15.4.1 创建xTaskCreate函数
在 main()函数中,我们直接可以对 FreeRTOS 进行创建任务操作,因为 FreeRTOS 会自动帮我们做初始化的事情,比如初始化堆内存。 FreeRTOS 的简单方便是在别的实时操作系统上都没有的,像 RT-Tharead,需要做很多事情。
这种简单的特点使得 FreeRTOS 在初学的时候变得很简单,我们自己在 main()函数中直接初始化我们的板级外设——BSP_Init(),然后进行任务的创建即可——xTaskCreate(),在任务创建中, FreeRTOS 会帮我们进行一系列的系统初始化,在创建任务的时候,会帮我们初始化堆内存。
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
{
/* 分配任务控制块内存 */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if( pxNewTCB != NULL )
{
pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxNewTCB->pxStack == NULL )
{
/* Could not allocate the stack. Delete the allocated TCB. */
vPortFree( pxNewTCB );
pxNewTCB = NULL;
}
}
}
/* 省略代码 */
}
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/* If this is the first call to malloc then the heap will require
initialisation to setup the list of free blocks. */
if( pxEnd == NULL )
{
prvHeapInit(); //核心
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 省略代码 */
}
在未初始化内存的时候一旦调用了xTaskCreate()函数, FreeRTOS 就会帮我们自动进行内存的初始化,内存的初始化具体见代码清单 15-5。注意,此函数是 FreeRTOS 内部调用的。
xTaskCreate->pvPortMalloc->prvHeapInit
prvHeapInit()函数定义
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
/*确保堆在正确对齐的边界上启动. */
uxAddress = ( size_t ) ucHeap; //uxAddress存的是ucHeap的地址
if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
{
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
}
pucAlignedHeap = ( uint8_t * ) uxAddress;
/* xStart 用于保存指向空闲块列表中第一个项目的指针。
void 用于防止编译器警告*/
xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
xStart.xBlockSize = ( size_t ) 0;
/* pxEnd 用于标记空闲块列表的末尾,并插入堆空间的末尾。 */
uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
uxAddress -= xHeapStructSize;
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
pxEnd = ( void * ) uxAddress;
pxEnd->xBlockSize = 0;
pxEnd->pxNextFreeBlock = NULL;
/* 首先,有一个空闲块,其大小可以占用整个堆空间,减去 pxEnd 占用的空间。 */
pxFirstFreeBlock = ( void * ) pucAlignedHeap;
pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
/* 只存在一个块 - 它覆盖整个可用堆空间。 因为是刚初始化的堆内存*/
xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
/* Work out the position of the top bit in a size_t variable. */
/* 计算出变量中最高位的值 */
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}
上面取地址的方法:
#include <stdio.h>
char a[10] = {0};
int main(int argc, char const *argv[])
{
size_t b;
b = (size_t)a;
printf("b = %p.\n", b);
printf("a = %p.\n", &a);
//b = 0000000000407030.
、、a = 0000000000407030
return 0;
}
15.4.2 vTaskStartScheduler()函数
在创建完任务的时候,我们需要开启调度器,因为创建仅仅是把任务添加到系统中,还没真正调度,并且空闲任务也没实现,定时器任务也没实现,这些都是在开启调度函数vTaskStartScheduler()中实现的。为什么要空闲任务?因为 FreeRTOS 一旦启动,就必须要保证系统中每时每刻都有一个任务处于运行态(Runing),并且空闲任务不可以被挂起与删除, 空闲任务的优先级是最低的,以便系统中其他任务能随时抢占空闲任务的 CPU 使用权。这些都是系统必要的东西,也无需用户自己实现, FreeRTOS 全部帮我们搞定了。处理完这些必要的东西之后,系统才真正开始启动。
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/*添加空闲任务、这个是用静态方法创建的*/
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* 空闲任务是使用用户提供的 RAM 创建的 - 获取
然后 RAM 的地址创建空闲任务。这是静态创建任务,我们不用管*/
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
"IDLE",
ulIdleTaskStackSize,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else /* 这里才是动态创建 idle 任务 */
{
/* 使用动态分配的 RAM 创建空闲任务。 */
xReturn = xTaskCreate( prvIdleTask,
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
/* 如果使能了 configUSE_TIMERS 宏定义,表明使用定时器,需要创建定时器任务*/
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS )
{
/* 此处关闭中断,以确保不会发生tick中断
在调用 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 ) 0U;
/* 如果定义了 configGENERATE_RUN_TIME_STATS,则以下内容
必须定义宏以配置用于生成的计时器/计数器
运行时计数器时基。目前没启用该宏定义 */
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* 调用 xPortStartScheduler 函数配置相关硬件 如滴答定时器、 FPU、 pendsv 等*/
if( xPortStartScheduler() != pdFALSE )
{
/* 如果 xPortStartScheduler 函数启动成功,则不会运行到这里 */
}
else
{
/* 不会运行到这里,除非调用 xTaskEndScheduler() 函数 */
}
}
else
{
/* 只有在内核无法启动时才会到达此行,因为没有足够的堆内存来创建空闲任务或计时器任务。
此处使用了断言,会输出错误信息,方便错误定位 */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* 如果 INCLUDE_xTaskGetIdleTaskHandle 设置为 0,则防止编译器警告,
这意味着在其他任何地方都不使用 xIdleTaskHandle。暂时不用理会 */
( void ) xIdleTaskHandle;
}
如果在 FreeRTOSConfig.h 中使能了 configUSE_TIMERS 这个宏定义,那么需要创建一个定时器任务,这个定时器任务也是调用 xTaskCreate()函数完成创建,过程十分简单,这也是系统的初始化内容,在调度器启动的过程中发现必要初始化的东西,FreeRTOS 就会帮我们完成,对开发人员友好!!!
BaseType_t xTimerCreateTimerTask( void )
{
BaseType_t xReturn = pdFAIL;
/* 检查使用了哪些活动计时器的列表,以及
用于与计时器服务通信的队列,已经
初始化。 */
prvCheckForValidListAndQueue();
if( xTimerQueue != NULL )
{
/* 这是静态创建的,无需理会 */
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t *pxTimerTaskTCBBuffer = NULL;
StackType_t *pxTimerTaskStackBuffer = NULL;
uint32_t ulTimerTaskStackSize;
vApplicationGetTimerTaskMemory( &pxTimerTaskTCBBuffer, &pxTimerTaskStackBuffer, &ulTimerTaskStackSize );
xTimerTaskHandle = xTaskCreateStatic( prvTimerTask,
"Tmr Svc",
ulTimerTaskStackSize,
NULL,
( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
pxTimerTaskStackBuffer,
pxTimerTaskTCBBuffer );
if( xTimerTaskHandle != NULL )
{
xReturn = pdPASS;
}
}
#else
{ /* 这是才是动态创建定时器任务 */
xReturn = xTaskCreate( prvTimerTask,
"Tmr Svc",
configTIMER_TASK_STACK_DEPTH,
NULL,
( ( UBaseType_t ) configTIMER_TASK_PRIORITY ) | portPRIVILEGE_BIT,
&xTimerTaskHandle );
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
}
else
{
mtCOVERAGE_TEST_MARKER();
}
configASSERT( xReturn );
return xReturn;
}
调用函数 xPortStartScheduler()来启动系统节拍定时器(一般都是使用 SysTick) 并启动第一个任务。因为设置系统节拍定时器涉及到硬件特性,因此函数xPortStartScheduler()由移植层提供(在 port.c 文件实现) ,不同的硬件架构,这个函数的代码也不相同,在 ARM_CM3 中,使用 SysTick 作为系统节拍定时器。 有兴趣可以看看xPortStartScheduler()的源码内容,下面我只是简单介绍一下相关知识。
在 Cortex-M3 架构中, FreeRTOS 为了任务启动和任务切换使用了三个异常: SVC、PendSV 和 SysTick:
- SVC(系统服务调用, 亦简称系统调用)用于任务启动,有些操作系统不允许应用程序直接访问硬件, 而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件, 它就会产生一个SVC 异常。
- PendSV(可挂起系统调用)用于完成任务切换,它是可以像普通的中断一样被挂起的,它的最大特性是如果当前有优先级比它高的中断在运行, PendSV 会延迟执行,直到高优先级中断执行完毕,这样子产生的 PendSV 中断就不会打断其他中断的运行。
- SysTick 用于产生系统节拍时钟,提供一个时间片,如果多个任务共享同一个优先级,则每次 SysTick 中断,下一个任务将获得一个时间片。关于详细的 SVC、 PendSV异常描述,推荐《Cortex-M3 权威指南》一书的“异常”部分。
这里将 PendSV 和 SysTick 异常优先级设置为最低,这样任务切换不会打断某个中断服务程序,中断服务程序也不会被延迟,这样简化了设计,有利于系统稳定。有人可能会问,那 SysTick 的优先级配置为最低,那延迟的话系统时间会不会有偏差?答案是不会的,因为 SysTick 只是当次响应中断被延迟了,而 SysTick 是硬件定时器,它一直在计时,这一次的溢出产生中断与下一次的溢出产生中断的时间间隔是一样的,至于系统是否响应
还是延迟响应,这个与 SysTick 无关,它照样在计时。
查看pendsv:https://blog.csdn.net/xiaohua0877/article/details/89290172
这个方法是:有中断在进行任务,可以由systick进行中断,然后挂起SVC,返回执行中断程序。
上面的是,必须在没有中断程序执行的时候,才能执行systick中断,挂起SVC。也就是说优先处理其他的中断。
15.4 其他
在启动任务调度器的时候,假如启动成功的话,任务就不会有返回了,假如启动没成功,则通过 LR 寄存器指定的地址退出,在创建 AppTaskCreate 任务的时候,任务栈对应 LR 寄存器指向是任务退出函数 prvTaskExitError(),该函数里面是一个死循环, 这代表着假如创建任务没成功的话,就会进入死循环,该任务也不会运行