1 前言
本文主要是对野火的《FreeRTOS内核实现与应用开发实战指南》中任务调度的学习与总结,梳理出任务创建成功后,启动调度器开启第一个任务的流程,因为本人使用的是Linux环境的GCC模拟器,与设备中实际使用场景存在差异,因此以此文为准。
示例源码基于FreeRTOS V202212.01
2 任务调度器vTaskStartScheduler
任务调度器vTaskStartScheduler用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度。
其主要完成以下工作:
1、创建空闲任务
2、创建定时任务
3、关中断(防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断)
4、开启任务调度xPortStartScheduler
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
/*创建空闲任务,优先级为最低*/
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT,
&xIdleTaskHandle );
/*当启用定时器时,则开启定时器*/
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( xReturn == pdPASS )
{
/*关闭中断*/
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
/*在FreeRTOS中,xNextTaskUnblockTime变量用于记录下一个需要从阻塞状态唤醒的任务的唤醒时间。这个时间是基于系统的滴答计数(tick count)来计算的。当任务因为某些原因(如等待时间延迟、等待信号量或消息队列)进入阻塞状态时,它们会被指定一个唤醒时间。调度器会检查xNextTaskUnblockTime来确定是否有任务需要被唤醒。*/
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE; (4)
xTickCount = ( TickType_t ) 0U;
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)
if( xPortStartScheduler() != pdFALSE ) (6)
{
//如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
}
else
{
//不会运行到这里,除非调用函数 xTaskEndScheduler()。
}
}
else
{
//程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
//空闲任务或者定时器任务的时候没有足够的内存。
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
//防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提示 xIdleTaskHandle 未使用。
( void ) xIdleTaskHandle;
}
2.1 空闲任务
空闲任务的优先级为0,所以空闲任务的优先级最低。在操作系统(尤其是实时操作系统,RTOS)中,空闲任务(Idle Task)扮演着一个特别的角色。它是系统中优先级最低的任务,当没有其他用户任务(或者说,高优先级任务)需要运行时,空闲任务就会执行。它的主要作用包括但不限于以下几点:
资源回收:空闲任务可以用来执行内存回收或者资源清理工作。在一些系统中,比如FreeRTOS,内存的释放操作会被推迟到空闲任务中执行,这样做可以避免在高优先级任务中执行可能耗时的内存释放操作。
系统监控:通过监测空闲任务的运行时间,可以评估系统的负载情况。例如,可以通过计算空闲任务占用的CPU时间比例来估算系统的空闲时间比例,这对于分析和优化系统性能非常有用。
省电和功耗管理:在空闲任务中,可以执行一些省电操作,比如将CPU置于低功耗模式。对于电池供电的设备,这是非常重要的功能,有助于延长设备的续航时间。
保活(Keep-alive)操作或心跳信号:在一些系统中,空闲任务也可以用来维持与外部系统的通信,比如定期发送心跳信号,确保系统仍然在线。
执行低优先级的后台任务:虽然空闲任务的主要目的不是执行实际的应用逻辑,但它可以被用来执行一些不紧急的后台任务,比如日志记录、状态更新等。
调试和测试:在开发和调试阶段,空闲任务可以用来执行一些诊断操作,例如内存检查、系统健康状况报告等。
2.2 定时器任务
使能软件定时器的宏为1,则创建定时器任务,定时器任务在操作系统中,尤其是实时操作系统(RTOS)中,定时器任务(Timer Task)发挥着重要的作用。定时器任务允许系统或应用程序在指定的时间点或经过指定的时间间隔执行特定的操作。这些操作可以是一次性的,也可以是周期性的。定时器任务的主要作用包括:
任务调度:定时器可以用来触发或调度任务的执行。例如,一个任务可能需要每隔一定时间周期运行,定时器就可以用来实现这种周期性调度。
时间管理:在需要精确控制操作执行时间的场景中,定时器提供了一种有效的时间管理机制。例如,在通信协议中,定时器可以用来管理超时重传、心跳包发送等。
资源释放:定时器可以用来监控系统资源的使用情况,并在资源使用超时时自动释放资源。这对于防止资源泄漏非常有用。
性能监控:通过定时器,系统可以定期收集和记录性能数据,如CPU使用率、内存使用情况等,有助于系统性能的监控和优化。
用户界面更新:在图形用户界面(GUI)应用中,定时器常用于定期更新用户界面,如动画效果的实现、状态信息的刷新等。
省电和功耗管理:在嵌入式系统和移动设备中,定时器可以用来实现省电策略,比如在设备空闲时自动进入低功耗模式,或者定期唤醒设备执行必要的更新操作。
事件触发:定时器可以用来在特定时间点触发事件,这对于实现基于时间的事件处理逻辑非常有用。
2.3 xPortStartScheduler
BaseType_t xPortStartScheduler( void )
{
/*设置 PendSV 的中断优先级,为最低优先级*/
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
/*设置滴答定时器SysTick的中断优先级,为最低优先级*/
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/*调用函数 vPortSetupTimerInterrupt()来设置滴答定时器的定时周期,并且使能滴答定时器的中断*/
vPortSetupTimerInterrupt();
/*初始化临界区嵌套计数器*/
uxCriticalNesting = 0;
/*开启第一个任务*/
prvStartFirstTask();
//代码正常执行的话是不会到这里的!
return 0;
}
xPortStartScheduler函数的主要任务是将PendSV与滴答定时器SysTick的中断优先级,为最低优先级,这样做的原因在于让系统优先响应外部硬件中断。虽然开启第一个任务prvStartFirstTask。
2.4 开启第一个任务
prvStartFirstTask函数用于开启第一个任务,主要进行两个操作:一是更新MSP的值,二是产生SVC系统调用,然后到SVC的中断服务函数中真正切换到第一个任务。
__asm void prvStartFirstTask( void )
{
/* 八字节对齐*/
PRESERVE8
/* 获取 MSP 的初始值(栈顶指针) */
ldr r0, =0xE000ED08
ldr r0, [ r0 ]
ldr r0, [ r0 ]
/* 设置主栈指针 MSP 的值*/
msr msp, r0
/* 使能全局中断 */
cpsie i
cpsie f
dsb
isb
/* 调用 SVC 启动第一个任务 */
svc 0
nop
nop
}
在Cortex-M中,0xE000ED08是SCB_VTOR寄存器的地址,里面存放的是向量表的起始地址,及msp的地址。
SVC中断要想被成功响应,其函数名必须与向量表注册的名称一致,在启动文件的向量表中,SVC的中断服务函数注册的名称是SVC_Handler,所以SVC中断服务函数的名称应写成SVC_Handler。
#define vPortSVCHandler SVC_Handler
vPortSVCHandler函数开启真正的第一个任务后,不再返回。
__asm void vPortSVCHandler( void )
{
/* 声明外部变量pxCurrentTCB任务控制块 */
extern pxCurrentTCB;
PRESERVE8
/* 加载pxCurrentTCB任务控制块的栈顶指针到r0. */
ldr r3, =pxCurrentTCB
ldr r1, [ r3 ]
ldr r0, [ r1 ]
/*以r0为基地址,将pxCurrentTCB栈中的8个字节数据出栈加载到CPU寄存器r4~r11*/
ldmia r0!,[r4-r11]
/* 将新的栈顶指针更新到psp中,任务执行时使用的栈指针是psp. */
msr psp, r0
isb
/*清空r0*/
mov r0, #0
/* basepri 中断屏蔽寄存器设置为0,打开所有中断*/
msr basepri, r0
orr r14, #0xd
bx r14
}