从0到1学习FreeRTOS:FreeRTOS 内核应用开发:(四)FreeRTOS 的启动流程
1、未到主函数之前:
我们知道,在系统上电的时候第一个执行的是启动文件里面由汇编编写的复位函数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
2、创建任务 xTaskCreate()函数:
在 main()函数中,我们直接可以对 FreeRTOS 进行创建任务操作,因为 FreeRTOS 会自动帮我们做初始化的事情,比如初始化堆内存。
这种简单的特点使得 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 )
{
if ( pxStack != NULL ) {
/* 分配任务控制块内存 */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if ( pxNewTCB != NULL ) {
/* 将堆栈位置存储在 TCB 中。 */
pxNewTCB->pxStack = pxStack;
}
}
/*
省略代码
......
*/
}
/* 分配内存函数 */
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
vTaskSuspendAll();
{
/*如果这是对 malloc 的第一次调用,那么堆将需要初始化来设置空闲块列表。 */
if ( pxEnd == NULL ) {
prvHeapInit();
} else {
mtCOVERAGE_TEST_MARKER();
}
/*
省略代码
......
*/
}
}
在未初始化内存的时候一旦调用了xTaskCreate()函数, FreeRTOS 就会帮我们自动进行内存的初始化,内存的初始化具体见下面的代码清单。注意,此函数是 FreeRTOS 内部调用的,目前我们暂时不用管这个函数的实现,在后面我们会仔细讲解 FreeRTOS 的内存管理相关知识,现在我们知道 FreeRTOS 会帮我们初始话系统要用的东西即可。
static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
uxAddress = ( size_t ) 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;
xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) *
heapBITS_PER_BYTE ) - 1 );
}
/*-----------------------------------------------------------*/
3、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 );
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 );
}
#endif
#if ( configUSE_TIMERS == 1 )
{
/* 如果使能了 configUSE_TIMERS 宏定义
表明使用定时器,需要创建定时器任务*/
if ( xReturn == pdPASS ) {
xReturn = xTimerCreateTimerTask();
} else {
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if ( xReturn == pdPASS ) {
/* 此处关闭中断,以确保不会发生中断
在调用 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;
}
/*-----------------------------------------------------------*/
动态创建空闲任务(IDLE),因为现在我们不使用静态创建,这个 configSUPPORT_STATIC_ALLOCATION 宏定义为 0,只能是动态创建空闲任务,并且空闲任务的优先级与堆栈大小都在 FreeRTOSConfig.h 中由用户定义, 空闲任务的任务句柄存放在静态变量xIdleTaskHandle 中, 用户可以调用 API 函数 xTaskGetIdleTaskHandle()获得空闲任务句柄。
如果在 FreeRTOSConfig.h 中使能了 configUSE_TIMERS 这个宏定义,那么需要创建一个定时器任务,这个定时器任务也是调用 xTaskCreate()函数完成创建,过程十分简单,这也是系统的初始化内容,在调度器启动的过程中发现必要初始化的东西,FreeRTOS 就会帮我们完成,真的对开发者太友好了, xTimerCreateTimerTask()函数具体见下面的代码清单 。
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;
}
xSchedulerRunning 等于 pdTRUE,表示调度器开始运行了,而xTickCount 初始化需要初始化为 0,这个 xTickCount 变量用于记录系统的时间,在节拍定时器(SysTick)中断服务函数中进行自加。
调用函数 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 无关,它照样在计时。
4、main.c
/**
******************************************************************************
* @file main.c
* @author Sumjess
* @version V1.0
* @date 2019-09-xx
* @brief MDK5.27
******************************************************************************
* @attention
*
* 实验平台 :STM32 F429
* 微信公众号 :Tech云
*
******************************************************************************
*/
/*
*************************************************************************
* 包含的头文件
*************************************************************************
*/
/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
/* 开发板硬件bsp头文件 */
#include "sum_common.h"
/**************************** 任务句柄 ********************************/
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
/* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED1任务句柄 */
static TaskHandle_t LED1_Task_Handle = NULL;
/* LED2任务句柄 */
static TaskHandle_t LED2_Task_Handle = NULL;
/********************************** 内核对象句柄 *********************************/
/*
* 信号量,消息队列,事件标志组,软件定时器这些都属于内核的对象,要想使用这些内核
* 对象,必须先创建,创建成功之后会返回一个相应的句柄。实际上就是一个指针,后续我
* 们就可以通过这个句柄操作这些内核对象。
*
* 内核对象说白了就是一种全局的数据结构,通过这些数据结构我们可以实现任务间的通信,
* 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数
* 来完成的
*
*/
/******************************* 全局变量声明 ************************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些全局变量。
*/
/*
*************************************************************************
* 函数声明
*************************************************************************
*/
static void AppTaskCreate(void);/* 用于创建任务 */
static void LED1_Task(void* pvParameters);/* LED1_Task任务实现 */
static void LED2_Task(void* pvParameters);/* LED2_Task任务实现 */
static void BSP_Init(void);/* 用于初始化板载相关资源 */
/*****************************************************************
* @brief 主函数
* @param 无
* @retval 无
* @note 第一步:开发板硬件初始化
第二步:创建APP应用任务
第三步:启动FreeRTOS,开始多任务调度
****************************************************************/
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
/* 开发板硬件初始化 */
BSP_Init();
printf("这是一个FreeRTOS-动态创建任务!\r\n");
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数---即任务函数的名称,需要我们自己定义并且实现。*/
(const char* )"AppTaskCreate",/* 任务名字---字符串形式, 最大长度由 FreeRTOSConfig.h 中定义的configMAX_TASK_NAME_LEN 宏指定,多余部分会被自动截掉,这里任务名字最好要与任务函数入口名字一致,方便进行调试。*/
(uint16_t )512, /* 任务栈大小---字符串形式, 最大长度由 FreeRTOSConfig.h 中定义的configMAX_TASK_NAME_LEN 宏指定,多余部分会被自动截掉,这里任务名字最好要与任务函数入口名字一致,方便进行调试。*/
(void* )NULL,/* 任务入口函数参数---字符串形式, 最大长度由 FreeRTOSConfig.h 中定义的configMAX_TASK_NAME_LEN 宏指定,多余部分会被自动截掉,这里任务名字最好要与任务函数入口名字一致,方便进行调试。*/
(UBaseType_t )1, /* 任务的优先级---优先级范围根据 FreeRTOSConfig.h 中的宏configMAX_PRIORITIES 决定, 如果使能 configUSE_PORT_OPTIMISED_TASK_SELECTION,这个宏定义,则最多支持 32 个优先级;如果不用特殊方法查找下一个运行的任务,那么则不强制要求限制最大可用优先级数目。在 FreeRTOS 中, 数值越大优先级越高, 0 代表最低优先级。*/
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针---在使用内存的时候,需要给任务初始化函数xTaskCreateStatic()传递预先定义好的任务控制块的指针。在使用动态内存的时候,任务创建函数 xTaskCreate()会返回一个指针指向任务控制块,该任务控制块是 xTaskCreate()函数里面动态分配的一块内存。*/
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
while(1); /* 正常不会执行到这里 */
}
/***********************************************************************
* @ 函数名 : AppTaskCreate
* @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
* @ 参数 : 无
* @ 返回值 : 无
**********************************************************************/
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* 任务入口函数 */
(const char* )"LED1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED1_Task任务成功!\r\n");
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
(const char* )"LED2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&LED2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED2_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
/**********************************************************************
* @ 函数名 : LED_Task
* @ 功能说明: LED_Task任务主体
* @ 参数 :
* @ 返回值 : 无
********************************************************************/
static void LED1_Task(void* parameter)
{
while (1)
{
LED1_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_ON\r\n");
LED1_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_OFF\r\n");
}
}
/**********************************************************************
* @ 函数名 : LED_Task
* @ 功能说明: LED_Task任务主体
* @ 参数 :
* @ 返回值 : 无
********************************************************************/
static void LED2_Task(void* parameter)
{
while (1)
{
LED2_ON;
vTaskDelay(1000); /* 延时1000个tick */
printf("LED2_Task Running,LED2_ON\r\n");
LED2_OFF;
vTaskDelay(1000); /* 延时1000个tick */
printf("LED2_Task Running,LED2_OFF\r\n");
}
}
/***********************************************************************
* @ 函数名 : BSP_Init
* @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
* @ 参数 :
* @ 返回值 : 无
*********************************************************************/
static void BSP_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
Debug_USART_Config();
}
/********************************END OF FILE****************************/
当创建的应用任务的优先级比 AppTaskCreate 任务的优先级高、低或者相等时候,程序是如何执行的?假如像我们代码一样在临界区创建任务,任务只能在退出临界区的时候才执行最高优先级任务。