FreeRTOS系统启动过程主要分为三部分:汇编部分、main函数初始化部分、开启任务调度部分。
对于汇编部分主要是设置一些中断向量表、设置堆和栈等一些C语言运行需要的条件,当这些部分设置完成时候,就会跳转到main函数运行。对于main函数初始化部分,主要是做一些必要的硬件外设初始化、板级初始化、还有就是任务的创建。任务创建完成之后,就会开启调度器,FreeRTOS开始运行。
下面就讲一下FreeRTOS是怎么开始运行的:
由于之前讲过一篇关于apollo2 MCU的汇编启动,关于Cortex-M4的汇编启动部分基本一致。大家可以看之前博客。直接从main函数开始,上代码:
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口
LED_Init(); //初始化LED端口
KEY_Init(); //初始化按键
LCD_Init(); //初始化LCD
my_mem_init(SRAMIN); //初始化内部内存池
POINT_COLOR = RED;
LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 20-1");
LCD_ShowString(30,50,200,16,16,"Mem Manage");
LCD_ShowString(30,70,200,16,16,"KEY_UP:Malloc,KEY1:Free");
LCD_ShowString(30,90,200,16,16,"KEY0:Use Mem");
LCD_ShowString(30,110,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,130,200,16,16,"2016/11/14");
LCD_ShowString(30,170,200,16,16,"Total Mem: Bytes");
LCD_ShowString(30,190,200,16,16,"Free Mem: Bytes");
LCD_ShowString(30,210,200,16,16,"Message: ");
POINT_COLOR = BLUE;
//创建开始任务
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(); //开启任务调度
}
main函数第一部分会进行一些硬件初始化(串口、定时器、LED、LCD显示器),之后会调用任务创建函数xTaskCreate,此函数会创建一个start任务。关于start_task函数代码如下:
//开始任务任务函数
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区
//创建TASK1任务
xTaskCreate((TaskFunction_t )malloc_task,
(const char* )"malloc_task",
(uint16_t )MALLOC_STK_SIZE,
(void* )NULL,
(UBaseType_t )MALLOC_TASK_PRIO,
(TaskHandle_t* )&MallocTask_Handler);
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
关于start任务会进行一系列的任务创建,当这些任务创建完成之后,start会将自己删除。从上面代码可以看出,在start任务中会创建一个malloc任务。具体的malloc任务的代码如下:
//MALLOC任务函数
void malloc_task(void *pvParameters)
{
u8 *buffer;
u8 times,i,key=0;
u32 freemem;
LCD_ShowxNum(110,170,configTOTAL_HEAP_SIZE,5,16,0);//显示内存总容量
while(1)
{
key=KEY_Scan(0);
switch(key)
{
case WKUP_PRES:
buffer=pvPortMalloc(30); //申请内存,30个字节
printf("申请到的内存地址为:%#x\r\n",(int)buffer);
break;
case KEY1_PRES:
if(buffer!=NULL)vPortFree(buffer); //释放内存
buffer=NULL;
break;
case KEY0_PRES:
if(buffer!=NULL) //buffer可用,使用buffer
{
times++;
sprintf((char*)buffer,"User %d Times",times);//向buffer中填写一些数据
LCD_ShowString(94,210,200,16,16,buffer);
}
break;
}
freemem=xPortGetFreeHeapSize(); //获取剩余内存大小
LCD_ShowxNum(110,190,freemem,5,16,0);//显示内存总容量
i++;
if(i==50)
{
i=0;
LED0=~LED0;
}
vTaskDelay(10);
}
}
回到main函数,当创建完成任务之后,FreeRTOS会调用vTaskStartScheduler(); 开启任务调度器。
整体步骤:main函数硬件初始化->start任务创建->start创建各种任务->start任务自杀->开启任务调度器。
下面就开始讲解调度器的实现
函数void vTaskStartScheduler( void )代码如下:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////// //////
////// 调度器 //////
////// //////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
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,
"IDLE",
ulIdleTaskStackSize,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else
{//动态方法创建任务
xReturn = xTaskCreate( prvIdleTask,//创建空闲任务,空闲任务是必须要有的,当没有任务需要运行时,给FreeRTOS找点事干
"IDLE", configMINIMAL_STACK_SIZE,//空闲任务的堆栈大小
( void * ) NULL,//空闲任务的参数
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),//设置空闲任务的优先级,为系统的最低优先级
&xIdleTaskHandle ); //空闲任务的任务句柄
}
#endif
#if ( configUSE_TIMERS == 1 )//启用软件定时器
{
if( xReturn == pdPASS )
{//空闲任务创建成功
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
if( xReturn == pdPASS )
{
portDISABLE_INTERRUPTS();//屏蔽中断,屏蔽掉FreeRTOS能够管理的所有中断
#if ( configUSE_NEWLIB_REENTRANT == 1 )//没有使用嵌入式C语言专用库
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
xNextTaskUnblockTime = portMAX_DELAY;//下一个需要解除阻塞的任务的时间
xSchedulerRunning = pdTRUE;//标识任务调度器开始工作
xTickCount = ( TickType_t ) 0U;//用于记录系统运行时间,记录的是运行的节拍数
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();//关于代码运行时间和任务状态收集相关功能函数。此时必须借助MCU的系统定时器之外的一个定时器
if( xPortStartScheduler() != pdFALSE )
{
}
else
{
}
}
else
{
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
( void ) xIdleTaskHandle;
}
该函数主要是首先创建空闲任务,在FreeRTOS中空闲任务是必须要有的。之后根据配置决定是否开启软件定时器。再之后屏蔽FreeRTOS的能够管理的中断,设置相应的全局变量。这里要注意一点,空闲任务是FreeRTOS必须要创建的,而且任务的优先级是最低的。全局变量xNextTaskUnblockTime表示下一个解除阻塞的任务;全局变量xSchedulerRunning标识任务调度器开启;xTickCount表示记录FreeRTOS运行时间。该函数最终会调用xPortStartScheduler()函数,具体代码如下:
BaseType_t xPortStartScheduler( void )
{
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
#if( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );//0xE000E400是外部中断0#优先级设置寄存器
ulOriginalPriority = *pucFirstUserPriorityRegister;//保存外部中断0#原始优先级
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;//得到MCU支持的可编程优先级数量
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 )//ucMaxSysCallPriority此时存储的系统可以支持的最高优先级,如果我们设置最高优先级为5,那么此处变量ucMaxSysCallPriority数值为0b01010000
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;//设置PENDSV中断优先级,一般会设置MCU所支持的最低优先级
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;//设置系统滴答时钟中断优先级,一般设置MCU所支持的最低优先级
vPortSetupTimerInterrupt();//开启滴答定时器
uxCriticalNesting = 0;//用于临界区保护嵌套作用
prvEnableVFP();//使能FPU
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;//设置浮点上下文控制寄存器
prvStartFirstTask();//启动第一个任务
return 0;
}
关于函数xPortStartScheduler主要是这是pendsv和滴答定时器的中断优先级,一般会设置为整个系统所支持的最低中断优先级。开启滴答定时器,滴答定时器的作用是产生系统节拍,进行任务切换。最后调用prvStartFirstTask()函数启动第一个任务。
函数vPortSetupTimerInterrupt( void )代码如下:
void vPortSetupTimerInterrupt( void )
{
#if configUSE_TICKLESS_IDLE == 1
{//使能低功耗模式
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );//滴答定时器时间
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;//
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;//portNVIC_SYSTICK_LOAD_REG是Systick重装载寄存器,设置系统节拍
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );//portNVIC_SYSTICK_CTRL_REG是Systick控制和状态寄存器,使用外部时钟,定时器减到0产生中断,定时器使能
}
上述函数主要是设置滴答定时器的装载寄存器数值,配置滴答定时器的时钟来源,定时器开启等。
函数prvStartFirstTask( void )如下:
__asm void prvStartFirstTask( void )
{
PRESERVE8
ldr r0, =0xE000ED08 //向量表偏移寄存器地址
ldr r0, [r0] //取向量表地址
ldr r0, [r0] //取MSP初始值
msr msp, r0 //重置MSP指针
cpsie i //开中断
cpsie f //开异常
dsb //数据同步隔离
isb //指令同步隔离
svc 0 //异常触发,启动第一个任务,SVC异常
nop
nop
}
该函数最主要的就是产生一个SVC中断,以此启动第一个任务。
函数vPortSVCHandler( void )代码如下:
__asm void vPortSVCHandler( void )
{
PRESERVE8
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
mov r0, #0
msr basepri, r0
bx r14
}
关于上面函数重点说明以下几点:
1)pxCurrentTCB存储的是当前任务的控制块,任务控制块中的第一个成员变量就是任务堆栈的指针。
2)ldmia r0!, {r4-r11, r14},通过上面代码的处理,R0寄存器保存着当前任务的栈顶,r0!表示自增的意思,因此这步操作的内容是将栈顶的数据手动加载到寄存器r4~r11,以及r14寄存器中;msr psp, r0将PSP寄存器重新指向当前任务的新的栈顶。
3)msr basepri, r0,basepri寄存器是屏蔽优先级寄存器,当我们向该寄存器写入某一个数值n之后,就会屏蔽优先级数值高于n的所有中断和异常,这里要注意数值越大优先级越低。当我们写入0时,表示不屏蔽任何中断和异常。
4)bx r14,跳转到用户写的任务函数中运行,这时候FreeRTOS开始真正运行。
关于FreeRTOS的启动过程大致总结如下:
首先就是创建一个start任务,在start任务中会创建一堆任务;当这些任务创建完成之后,start任务会进行自杀;
接着会启动任务调度器,在任务调度器中会进行滴答定时器设置、开启滴答定时器、产生SVC中断、跳转到用户编写的任务函数。
可能大家会有很多疑问,比如在《FreeRTOS内核源码解读之-------系统启动(一)》这篇文章中提到的MSP和PSP之间是怎么进行切换的?为什么通过bx r14指令就能够跳转到用户任务函数?内部各个寄存器保存的的数值有什么作用?这些问题我会在下一篇文章中进行介绍。