STM32F1+HAL库+FreeTOTS学习8——第一个任务,启动!
上一期我们学习了列表和列表项的相关内容和API函数实验,接下来我们来学习FreeRTOS是如何启动第一个任务开启任务调度的,以及期间发生了什么
开启任务调度器
1. 函数 vTaskStartScheduler()
void freertos_demo(void)
{
taskENTER_CRITICAL(); /* 进入临界区,关闭中断,此时停止任务调度*/
/* 创建任务1 */
xTaskCreate((TaskFunction_t )task1,
(const char* )"task1",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
/* 创建任务2 */
xTaskCreate((TaskFunction_t )task2,
(const char* )"task2",
(uint16_t )TASK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_PRIO,
(TaskHandle_t* )&Task2Task_Handler);
taskEXIT_CRITICAL(); /* 退出临界区,重新开启中断,开启任务调度 */
/*这里是开启任务调度*/
vTaskStartScheduler(); //开启任务调度
}
在前面,我们已经使用过函数 vTaskStartScheduler(),作用就是开启FreeRTOS的任务调度,下面我们来具体的看一下内部实现:
/*开启任务调度器函数*/
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
// 1. 创建空闲函数
/* 如果使用的是静态内存管理,则使用静态的方式创建空闲函数 */
#if ( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t * pxIdleTaskTCBBuffer = NULL;
StackType_t * pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
/* 空闲任务的创建是使用用户提供的RAM,获取到相应的地址后才会进行创建。*/
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL, /*强制类型转换对于所有编译器都是多余的. */
portPRIVILEGE_BIT, /* 实际上这里应该是是 ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), 但是 tskIDLE_PRIORITY 为0. */
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 /* if ( configSUPPORT_STATIC_ALLOCATION == 1 ) */
{
/* 空闲函数动态分配RAM空间. */
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
#endif /* 结束空闲任务创建*/
// 2. 创建软件定时器任务
/*如果使能软件定时器,则需要创建定时器服务任务*/
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
/*函数内部会完成定时器服务任务的创建,创建方式参照空闲任务*/
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* 结束软件定时器配置*/
if( xReturn == pdPASS )
{
/* 此函数用于添加一些附加初始化,不用理会*/
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif
// 3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
portDISABLE_INTERRUPTS();
/* Newlib 相关的一些东西,这里我也看不懂 */
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
/* Switch Newlib's _impure_ptr variable to point to the _reent
* structure specific to the task that will run first.
* See the third party link http://www.nadler.com/embedded/newlibAndFreeRTOS.html
* for additional information. */
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
// 4. 初始化一些全局变量
xNextTaskUnblockTime = portMAX_DELAY; // 下一个距离取消任务阻塞的时间,设置为最大,因为此时还没有运行任务
// 避免阻塞超时导致任务切换
xSchedulerRunning = pdTRUE; //设置任务调度标志为开启
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; //系统节拍计数器,初始化为0
// 5. 为任务运行时间统计功能初始化功能时基定时器,是否使用该功能可在 FreeRTOSConfig.h 文件中进行配置
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
/* 调试使用 */
traceTASK_SWITCHED_IN();
/* xPortStartScheduler() 设置用于系统时钟节拍的硬件定时器(SysTick) 会在这个函数中进入第一个任务,并开始任务调度
* 任务调度开启后,便不会再返回 */
if( xPortStartScheduler() != pdFALSE )
{
/* 代码不会运行到这里 */
}
else
{
/* 当调用关闭任务调度器函数 xTaskEndScheduler()时会运行到这里. */
}
}
else
{
/*动态方式创建空闲任务和定时器服务任务时,堆栈空间不足,会导致无法创建,进入这里 */
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
/* 防止编译器警告,不用管*/
( void ) xIdleTaskHandle;
/* 调试使用,不用管*/
( void ) uxTopUsedPriority;
}
上述代码来自FreeRTOS官方提供源码,为了跟方便看懂,去除了大部分的英文注释,该为中文注释,更适合中国宝宝体质!!!
结合上述的注释,我们可以大概明白 vTaskStartScheduler() 完成了如下内容:
- 创建空闲任务
- 创建软件定时器任务
- 关闭中断(确切的说是关闭FreeRTOS能够控制的中断),防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断
- 初始化全局变量,并将任务调度器的运行标志设置为已运行
- 初始化任务运行时间统计功能的时基定时器,任务运行时间统计功能需要一个硬件定时器提供高精度的计数,这个硬件定时器就在这里进行配置,如果配置不启用任务运行时间统计功能的,就无需进行这项硬件定时器的配置。
- 最后就是调用函数 xPortStartScheduler(),由于里面的内容比较多,所以我们下面再起一部分讲解。
需要注意以下几点:
- 如果使用的是静态创建任务的方式,则空闲任务和定时器任务需要用户自行定义任务堆栈和TCB(任务控制块)
- 软件定时器人的优先级为31(最高),空闲任务的优先级为0(最低)
2. 函数xPortStartScheduler()
函数 xPortStartScheduler()完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务,具体的代码如下所示:
BaseType_t xPortStartScheduler( void )
{
/*1. 检测用户在 FreeRTOSConfig.h 文件中对中断相关部分的配置是否有误*/
#if ( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
/* 确定可以从哪个最高优先级调用ISR安全的FreeRTOS API函数
ISR安全函数是以“FromISR”结尾的函数。FreeRTOS维护了单独的线程和ISR API函数,以确保中断进入尽可能快速和简单。
保存即将被覆盖的中断优先级值。 */
ulOriginalPriority = *pucFirstUserPriorityRegister;
/* 确定可用的优先级位数。首先写入所有可能的位。. */
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
/* Read the value back to see how many bits stuck. */
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
/* The kernel interrupt priority should be set to the lowest
* priority. */
configASSERT( ucMaxPriorityValue == ( configKERNEL_INTERRUPT_PRIORITY & ucMaxPriorityValue ) );
/* Use the same mask on the maximum system call priority. */
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
/* Calculate the maximum acceptable priority group value for the number
* of bits read back. */
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
/* Check the CMSIS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif
#ifdef configPRIO_BITS
{
/* Check the FreeRTOS configuration that defines the number of
* priority bits matches the number of priority bits actually queried
* from the hardware. */
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif
/* Shift the priority group value back to its position within the AIRCR
* register. */
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
/* Restore the clobbered interrupt priority register to its original
* value. */
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif /* configASSERT_DEFINED */
/* 2. 设置 PendSV 和 SysTick 的中断优先级为最低优先级 */
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
/* 3. 开启并配置SYSTick,设置系统节拍、时钟源、然后开启SysTick和中断 */
vPortSetupTimerInterrupt();
/* 4. 初始化临界区嵌套计数器为 0 */
uxCriticalNesting = 0;
/* 5. 开启FPU,但是在Crotex_M3的内核里面,没有FPU,所以这里没有相关代码*/
/* 6. 开启第一个任务:第一个任务,启动! */
prvStartFirstTask();
/* Should not get here! */
return 0;
}
上述代码来自FreeRTOS官方提供源码,为了跟方便看懂,去除了大部分的英文注释,该为中文注释,更适合中国宝宝体质!!!
结合上述的注释,我们可以大概明白 xPortStartScheduler() 完成了如下内容:
- 在启用断言的情况下,函数 xPortStartScheduler()会检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误
- 配置 PendSV 和 SysTick 的中断优先级为最低优先级
- 调用函数 vPortSetupTimerInterrupt()配置 SysTick,函数 vPortSetupTimerInterrupt()首先会将 SysTick 当 前 计 数 值 清 空 , 并 根 据 FreeRTOSConfig.h 文件中配置的configSYSTICK_CLOCK_HZ(SysTick 时钟源频率)和 configTICK_RATE_HZ(系统时钟节拍频率)计算并设置 SysTick 的重装载值,然后启动 SysTick 计数和中断。
- 初始化临界区嵌套计数器,将其置为0
- 调用函数 prvEnableVFP()使能 FPU,因为 ARM Cortex-M3 内核 MCU 无 FPU,此函数仅在 ARM Cortex-M4/M7 内核 MCU 平台上被调用,执行改函数后 FPU 被开启。(这里因为我们使用的是STM32F1系列,使用的Crotex_M3内核,所以没有FPU)
- 调用prvStartFirstTask() 函数,第一个任务启动!
启动第一个任务
1. 函数 prvStartFirstTask()
prvStartFirstTask() 函数的调用,标志着我们正式迈步FreeRTOS的领域,该函数用于初始化启动第一个任务的环境:重新设置MSP指针和使能全局中断,最后使用SVC指令,触发SVC中断。
我们来看一下是如何实现的:
__asm void prvStartFirstTask( void )
{
/* 8 字节对齐 */
PRESERVE8
ldr r0, =0xE000ED08 /* 0xE000ED08 为 VTOR 地址 */
ldr r0, [ r0 ] /* 获取 VTOR 的值 */
ldr r0, [ r0 ] /* 获取 MSP 的初始值 */
/* 初始化 MSP */
msr msp, r0
/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用 SVC 启动第一个任务 */
svc 0
nop
nop
}
咋一看,这一段代码有点让人看不懂,因为是汇编,但是请你放心,它一点都不简单,我们这里先来补充几个重要的知识:
- 0xE000ED08 是什么东西?
事实上,0xE000ED08 是向量表偏移量寄存器的地址,“ ldr r0, =0xE000ED08 ” 这一步的目的就是将向量表偏移量寄存器的地址读取到" r0 "寄存器,紧接着 “ ldr r0, [ r0 ] ” 则是获取向量表偏移量寄存器中所指向的内容,也就是我们的中断向量表,但是我们知道,中断向量表,里面存放着各个中断服务函数的地址,“ ldr r0, [ r0 ] ” 就是获取第一个中断服务函数的地址 ,查阅start_stm32xxxxxx.s文件,就可以知道,这里的目的就是获取MSP指针的初始值。
2. MSP指针是干嘛的?
Cortex‐M3 处理器拥有 R0‐R15 的寄存器组。其中 R13 作为堆栈指针 SP。SP 有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。
程序在运行过程中需要一定的栈空间来保存局部变量等一些信息。对应任务的当有信息保存到栈中时,MCU 会自动更新 SP 指针,ARM Cortex-M 内核提供了两个栈空间,
- 主堆栈指针(MSP):复位后默认使用的堆栈指针,用于操作系统内核以及异常处理例程(包
括中断服务例程) - 进程堆栈指针(PSP):由用户的应用程序代码使用。
看到这里,相信你就一目了然了,复位后默认使用的是MSP指针,所以我们先需要查找中断向量表获取MSP指针初始值,将其赋值给msp寄存器,让MSP指针回到原点,紧接着,开启全局中断,进入SVC服务函数,由操作系统进行接管。
我们来总结一下prvStartFirstTask() 里面到底干了什么:
- 首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,栈在任何时候都是需要 4 字节对齐的,而在调用入口得 8 字节对齐,在进行 C 编程的时候,编译器会自动完成的对齐的操作,而对于汇编,就需要开发者手动进行对齐。
- 了获得 MSP 指针的初始值
- 对MSP指针进行初始化,这个操作相当于丢弃了程序之前保存在栈中的数据,因为FreeRTOS从开启任务调度器到启动第一个任务都是不会返回的,是一条不归路,因此将栈中的数据丢弃,也不会有影响。
- 使能全局中断,因为之前关闭了FreeRTOS管理的中断
- 最后使用 SVC 指令,并传入系统调用号 0,触发 SVC 中断。
2. 函数 vPortSVCHandler()
__asm void vPortSVCHandler( void )
{
/* 8 字节对齐 */
PRESERVE8
/* 获取任务栈地址 */
ldr r3, = pxCurrentTCB /* r3 指向优先级最高的就绪态任务的任务控制块 */
ldr r1, [ r3 ] /* r1 为任务控制块地址 */
ldr r0, [ r1 ] /* r0 为任务控制块的第一个元素(栈顶) */
/* 模拟出栈,并设置 PSP */
ldmia r0 !, { r4 - r11 } /* 任务栈r0地址从低到高,将r0存储地址里面的内容手动加载到 CPU寄存器r4到r11 */
msr psp, r0 /* 设置 PSP 为任务栈指针 */
isb
/* 使能所有中断 */
mov r0, # 0
msr basepri,
/* 使用 PSP 指针,并跳转到任务函数 */
orr r14, # 0xd
bx r14
}
同样的我们来分析一些,这个函数里做了什么:
- 同样进行了字节对齐,因为这里是汇编的世界!
- 获取任务栈的地址:pxCurrentTCB 是一个全局变量,用于指向系统中优先级最高的就绪态任务的任务控制块,之前我们创建了两个任务,一个空闲任务,优先级为0,一个软件定时器服务函数,优先级为31(这部没有包含用户自己创建的任务), 这里pxCurrentTCB 是软件定时器任务的任务控制块。那么对应的就是获取软件定时器任务的栈顶地址
- 将软件定时器任务栈的内容出栈到CPU寄存器组内,如何设置PSP指针。
- 使能所有中断
- 对CPU寄存器里面的r14(连接寄存器)或上0x0d ,使得r14的值为 0xFFFFFFFD
R14 是链接寄存器 LR,在 ISR 中(此刻我们在 SVC 的 ISR 中),它记录了异常返回值 EXC_RETURN,而EXC_RETURN 只有 6 个合法的值(M4、M7),如下表所示:
描述 | 使用浮点单元(M4、M7的内核) | 未使用浮点单元(M3的内核) |
---|---|---|
中断返回后进入Hamdler模式,并使用MSP | 0xFFFFFFE1 | 0xFFFFFFF1 |
中断返回后进入线程模式,并使用 MSP | 0xFFFFFFE9 | 0xFFFFFFF9 |
中断返回后进入线程模式,并使用 PSP | 0xFFFFFFED | 0xFFFFFFFD |
经过以上步骤,最终进入线程模式,使用PSP指针,开始运行第一个任务,软件定时器任务,至此第一个任务正式启动!!!