文章目录
- 2 FreeRTOS的任务理论
- 2.1 任务及任务优先级
- 2.2 任务状态理论
- 2.2.1 任务状态的转换
- 2.2.2 任务状态改变相关函数
- 2.2.3 调度器相关函数
- 2.3 FreeRTOS延时
- 2.3.1 vTaskDelay延时
- 2.3.2 vTaskDelayUntil延时
- 2.3.3 pdMS_TO_TICKS(x)宏
- 2.4 TCB任务控制块
- 2.5 任务调度宏与任务调度算法
2 FreeRTOS的任务理论
2.1 任务及任务优先级
简单来说,任务是指可独立运行的基本执行单元。任务是并发执行的最小单位,每个任务都有自己的代码逻辑和资源。FreeRTOS多任务执行其实是多任务交替执行实现的。实现多任务交替执行的基础是tick中断,滴答中断,周期性的定时器中断。类比Linux,我们可以类比的认为一个任务相当于一个线程,同时,任务也有不同的种类和实现方式,比如说定时器任务等等。
在FreeRTOS中,任务具有不同的优先级,不过FreeRTOS中,优先级是与大多数操作系统相反的,其值越小,优先级越低,**反之越大,优先级越高。**和CubeMX中的配置也是相反的。关于优先级我们需要注意以下几点:
- 优先级相同时,任务是可以交替进行的,但是需要同优先级正在运行的任务释放自己的资源,例如使用vTaskDelete(NULL)退出,taskYIELD()释放等。或使用vTaskDelay()等函数暂时的释放了CPU资源时,才会轮到另一个任务执行;
- 优先级不同时,高优先级的任务先执行,执行结束后才轮到低优先级的任务。这里需要注意的是,高优先级的任务中如果也有vTaskDelete(NULL),或vTaskDelay()等暂时的释放CPU资源的函数存在,这时候调度器会自动执行低优先级的的任务。而不是等着高优先级的delay延迟结束。
- 任务的优先级不可以无限制的高,在FreeRTOSConfig.h中有一个宏configMAX_PRIORITIES规定了能到达的最高的优先级,其优先级的取值范围是( 0 ~ configMAX_PRIORITIES-1 )。 ps:注意是最大值是 configMAX_PRIORITIES-1而不是 configMAX_PRIORITIES-1
- 这里我要再次强调,任务的优先级不是中断的优先级,任务也不是中断,任务优先级的高低仅仅决定了在就绪态的队伍中的排队次序,若低优先级的任务一直不释放CPU资源,那么再高的优先级也无法执行!!!。
/*
@brief 设置任务的优先级
@retval None
@param pxTask:要修改优先级的任务句柄,通过NULL改变任务自身优先级
uxNewPriority:要修改的任务优先级
*/
void vTaskPrioritySet(TaskHandle_t pxTask, UBaseType_t uxNewPriority);
/*
@brief 获取任务优先级
@retval 任务优先级
@param pxTask:要获取任务优先级的句柄,通过NULL获取任务自身优先级
*/
UBaseType_t uxTaskPriorityGet(TaskHandle_t pxTask);
2.2 任务状态理论
2.2.1 任务状态的转换
在通用操作系统中有5态(或3态),而在FreeRTOS中稍有不同,他拥有4个状态。同时要注意,FreeRTOS中的任务函数,是一个永远不会退出的C函数,是无法使用return来退出的,要彻底清除他我们必须使用vTaskDelete(NULL)删除它。
- (RUNNING)运行态:正在运行的任务,只能有一个。
- (Ready)就绪态:条件Event都满足了,只等待正在运行的任务释放CPU后就轮到他来执行了。
- (Block)阻塞态:相当于等待态,在等待IO或者条件。
- (Suspended)挂起态:将运行到一半的程序挂起(主动暂停),暂时脱离调度器的调度,但是不可以直接回到运行态,只能回到就绪态。
他们的转换关系如下:
2.2.2 任务状态改变相关函数
/*
@brief 查询一个任务当前处于什么状态
@retval 任务状态的枚举类型
@param pxTask:要查询任务状态的任务句柄,NULL查询自己
*/
eTaskState eTaskGetState(TaskHandle_t pxTask);
/*任务状态枚举类型返回值*/
typedef enum
{
eRunning = 0, /* 任务正在查询自身的状态,因此肯定是运行状态 */
eReady, /* 就绪状态 */
eBlocked, /* 阻塞状态 */
eSuspended, /* 挂起状态 */
eDeleted, /* 正在查询的任务已被删除,但其 TCB 尚未释放 */
eInvalid /* 无效状态 */
} eTaskState;
-
(RUNNING)运行态:
没有主动使得任务进入运行态的函数,同时要注意,单核处理器,那么同一时间只可能有一个任务再执行
-
(Ready)就绪态:
-
(Block)阻塞态:
//vTaskDelay()和vTaskDelayUntil()这两个函数,都会使得任务进入阻塞状态。 //当一个任务因为延时函数或者其他同步事件进入阻塞状态后,可以通过 xTaskAbortDelay() API 函数终止任务的阻塞状态,即使事件任务等待尚未发生,或者任务进入时指定的超时时间阻塞状态尚未过去,都会使其进入就绪状态,具体函数描述如下所述 /* @brief 终止任务延时,退出阻塞状态 @retval pdPASS:任务成功从阻塞状态中删除,pdFALSE:任务不属于阻塞状态导致删除失败 @param xTask:操作的任务句柄 */ BaseType_t xTaskAbortDelay(TaskHandle_t xTask);
-
(Suspended)挂起态:
/* @brief 将某个任务挂起 @retval None @param pxTaskToSuspend:被挂起的任务的句柄,通过传入NULL来挂起自身 */ void vTaskSuspend(TaskHandle_t pxTaskToSuspend); /* @brief 将某个任务从挂起状态恢复 @retval None @param pxTaskToResume:正在恢复的任务的句柄 */ void vTaskResume(TaskHandle_t pxTaskToResume); /* @brief vTaskResume的中断安全版本 @retval 返回退出中断之前是否需要进行上下文切换(pdTRUE/pdFALSE) @param pxTaskToResume:正在恢复的任务的句柄 */ BaseType_t xTaskResumeFromISR(TaskHandle_t pxTaskToResume);
2.2.3 调度器相关函数
/*
@brief 启动调度器
@retval None
*/
void vTaskStartScheduler(void);
/*
@brief 停止调度器
@retval None
*/
void vTaskEndScheduler(void);
/*
@brief 挂起调度器
@retval None
*/
void vTaskSuspendAll(void);
/*
@brief 恢复调度器
@retval 返回是否会导致发生挂起的上下文切换(pdTRUE/pdFALSE)
*/
BaseType_t xTaskResumeAll(void);
/*
@brief 让位于另一项同等优先级的任务
@retval None
*/
void taskYIELD(void);
/*
@brief ISR 退出时是否执行上下文切换(汇编)
@retval None
@param xHigherPriorityTaskWoken:pdFASLE不请求上下文切换,反之请求上下文切换
*/
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
/**
@brief ISR 退出时是否执行上下文切换(C语言)
@retval None
@param xHigherPriorityTaskWoken:pdFASLE不请求上下文切换,反之请求上下文切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
2.3 FreeRTOS延时
在裸机开发中,我们常常使用HAL_Delay()来实现延时功能,但FreeRTOS开发中,此时系统滴答是FreeRTOS主导的,所以我们应使用FreeRTOS中的延时函数来实现延迟。这两个函数都会使得当前的任务进入阻塞状态,使其他的任务有机会运行。
2.3.1 vTaskDelay延时
/*
@brief:相对延时函数,宏定义INCLUDE_vTaskDelay必须定义为1,此函数才可用。相对延时,指的是从调用函数后开始计时,直到延时 指定的时间结束。观察源代码发现,原来他只是把任务挂起(挂起态),等延时时间到,再把任务恢复(注意此时不是直接进入到 运行态,而是就绪态)。
@retval:None
@param:const TickType_t xTicksToDelay:表示延时的时钟周期数(tick),configTICK_RATE_HZ配置FreeRTOS的内核时钟周期。 例如,如果内核时钟为1kHz,那么1tick对应的时间为1ms。 这个函数是仅仅通过tick数来计算延时持续的时间。
*/
void vTaskDelay(const TickType_t xTicksToDelay);
2.3.2 vTaskDelayUntil延时
/*
@brief:绝对延时函数,允许任务在指定的绝对时间再次激活。
@retval:None
@param:TickType_t *pxPreviousWakeTime:一个指针,指向一个变量,这个变量存储着上次任务唤醒的时刻。
TickType_t xTimeIncrement:每次唤醒后,需要延时的时间。
*/
void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);
2.3.3 pdMS_TO_TICKS(x)宏
//是一个宏而不是函数,前面所使用的Delay函数是使用tick来计时的,但是我们人更喜欢用ms,s来作为延时的单位,而这个宏就是用来将毫秒数转化为相应的时钟滴答数(tick次数)的。
#ifndef pdMS_TO_TICKS
#define pdMS_TO_TICKS( xTimeInMs ) \
( (TickType_t) ( ( (TickType_t) (xTimeInMs) * (TickType_t) configTICK_RATE_HZ) / (TickType_t)1000U ) )
#endif
2.4 TCB任务控制块
对于每一个task任务来讲,都有一个TCB_t结构体。这个结构体就叫做任务控制块。我们创建任务时最后的句柄handle就是指向的这个结构体。这个控制块里面包含的是任务的一些基本信息,例如任务状态,任务的优先级,任务栈的头指针,尾指针,任务的标识符等。(下面我们只看一些最重要的变量,重要的变量我将用中文进行标注)
typedef struct tskTaskControlBlock //TCB_T的旧名字
{
volatile StackType_t * pxTopOfStack; //一个指针,指向当前栈的栈顶,必须位于控制块的第一项
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings;//MPU设置,必须位于结构体的第二项
#endif
ListItem_t xStateListItem; //该任务的任务状态列表项,该任务是运行态,还是就绪态,阻塞态,挂起态,就存在这里。
ListItem_t xEventListItem; //事件列项表,将任务以引用方式挂到事件列表中
UBaseType_t uxPriority; //任务的优先级
StackType_t * pxStack; //栈的起始位置
char pcTaskName[ configMAX_TASK_NAME_LEN ]; //任务的名字
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t * pxEndOfStack; //指向栈最深的有效地址,也就是栈底。
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; //存储一个特定的值来标识这个TCB任务块
UBaseType_t uxTaskNumber; //对于任务,存储了一个特殊的值来表示任务,方便我们进行追踪,错误的定位等。
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if ( configGENERATE_RUN_TIME_STATS == 1 )
configRUN_TIME_COUNTER_TYPE ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
#if ( ( configUSE_NEWLIB_REENTRANT == 1 ) || ( configUSE_C_RUNTIME_TLS_SUPPORT == 1 ) )
configTLS_BLOCK_TYPE xTLSBlock; /*< Memory block used as Thread Local Storage (TLS) Block for the task. */
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif
/* See the comments in FreeRTOS.h with the definition of
* tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE. */
#if ( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 ) /*lint !e731 !e9029 Macro has been consolidated for readability reasons. */
uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the task is a statically allocated to ensure no attempt is made to free the memory. */
#endif
#if ( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
#if ( configUSE_POSIX_ERRNO == 1 )
int iTaskErrno;
#endif
} tskTCB;
typedef tskTCB TCB_t;
2.5 任务调度宏与任务调度算法
FreeRTOS的调度算法一般有三种,是否启用的宏定义在FreeRTOSConfig.h文件夹下可以看到,他们分别是:
#define configUSE_PREEMPTION //是否允许高优先级抢占 1允许 0不允许
#define configUSE_TIME_SLICING //是否允许时间片轮转 1允许 0不允许
#define configIDLE_SHOULD_YIELD //是否允许空闲任务让步 1允许 0不允许
接下来我们来介绍这三种调度方式:
- 优先级抢占:即高优先级的任务是否可以优先抢占,其实对应的就是在就绪态任务中任务排队的次序,优先级越高,排队越靠前,越优先抢占。
- 时间片轮转:即同优先级的任务是否可以轮流运转,当宏定义为1时,可以轮流运行。
- 空闲任务让步:如果配置了让步的宏,空闲函数可以执行时,空闲函数只触发一次调度,调度后,又主动让出CPU资源让用户task执行。如果未配置的话,将会在空闲函数的while循环里多次循环,也就是说一直处于空闲任务执行的状态。
PS:我们一般使用的是111,也就是全使能的模式,允许高优先级抢占,允许时间片轮转,允许空闲任务让步。