文章目录
- 0x01 临界段
- Cortex-M内核快速关中断指令
- 关中断
- 开中断
- 进入和退出临界段的宏
- 进入临界段
- 退出临界段
- 临界段代码应用
- 0x02 空闲任务与阻塞延时的实现
-
- 0x03 多优先级
- 通用方法:
- taskRECORD_READY_PRIORITY()
- taskSELECT_HIGHEST_PRIORITY_TASK()
- 优化方法:
- taskRECORD_READY_PRIORITY()
- taskRESET_READY_PRIORITY()
- taskSELECT_HIGHEST_PRIORITY_TASK()
- prvAddTaskToReadyList()
- 0x04 任务延时列表
- 任务延时列表初始化
- 定义xNextTaskUnblockTime
- prvAddCurrentTaskToDelayedList()
- xTaskIncrementTick()
- taskSWITCH_DELAYED_LISTS()
- taskRESET_READY_PRIORITY()
- 0x05 时间片
- 0x06 总结
0x01 临界段
临界段用一句话概括就是一段在执行的时候不能被中断的代码段。临界段会被打断是在以下两种情况下:一个是系统调度,还有一个就是外部中断。在FreeRTOS,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。既然这样,FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制。
Cortex-M内核快速关中断指令
三个中断屏蔽寄存器有:PRIMASK
、FAULTMAST
、BASEPRI
。
在FreeRTOS上对中断的开和关使用的寄存器是BASEPRI
,在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理。用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。
关中断
FreeRTOS 关中断的函数在 portmacro.h
中定义,分不带返回值和带返回值两种:
对于不带返回值的关中断函数,不可以进行嵌套,不能在中断中进行使用:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
- 不带返回值的关中断函数,不能嵌套,不能在中断里面使用。不带返回值的意思是:在往 BASEPRI 写入新的值的时候,不用先将 BASEPRI 的值保存起来,即不用管当前的中断状态是怎么样的,既然不用管当前的中断状态,也就意味着这样的函数不能在中断里面调用。
- configMAX_SYSCALL_INTERRUPT_PRIORITY 是 一 个 在FreeRTOSConfig.h 中定义的宏,即要写入到 BASEPRI 寄存器的值。存入后高四位有效,优先级大于此值会被屏蔽,小于此值的中断不受RTOS管理。
- 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值写入BASEPRI 寄存器,实现关中断(准确来说是关部分中断)。
对于带返回值的关中断函数,可以进行嵌套,可以在中断中进行使用:
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
-
带返回值的关中断函数,可以嵌套,可以在中断里面使用。带返回值的意思是:在往 BASEPRI 写入新的值的时候,先将 BASEPRI 的值保存起来,在更新完BASEPRI 的值的时候,将之前保存好的 BASEPRI 的值返回,返回的值作为形参传入开中断函数。
-
其configMAX_SYSCALL_INTERRUPT_PRIORITY的作用同上。
-
保存 BASEPRI 的值,记录当前哪些中断被关闭。
-
更新 BASEPRI 的值。
-
返回原来 BASEPRI 的值。
开中断
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
- 开中断函数,具体是将传进来的形参更新到 BASEPRI 寄存器。根据传进来形参的不同,分为中断保护版本与非中断保护版本。
- 不带中断保护的开中断函数,直接将 BASEPRI 的值设置为 0,与portDISABLE_INTERRUPTS()成对使用。
- 带中断保护的开中断函数,将上一次关中断时保存的 BASEPRI 的值作为形参 ,与 portSET_INTERRUPT_MASK_FROM_ISR()成对使用。
进入和退出临界段的宏
进入和退出临界段的宏分中断保护版本和非中断版本,但最终都是通过开/关中断来实现,主要是由如下的宏配置:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
进入临界段
不带中断保护,不可以进行嵌套:
#define portENTER_CRITICAL() vPortEnterCritical()
#define portENTER_CRITICAL() vPortEnterCritical()
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
带中断保护保本,可以进行嵌套:
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
退出临界段
不带中断保护版本,不能嵌套:
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
带中断的保护版本,可以嵌套:
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
临界段代码应用
进程进入临界区的调度原则是:
- 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入。
- 任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待。
- 进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
- 如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象
访问一个被多任务共享,或是被多任务和中断共享的资源时,需要采用**“互斥”技术**以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直到这个资源又恢复到完整状态。
可以使用在中断场合场合,可以使用如下代码进行操作:
uint32_t ulReturn;
ulReturn = taskENTER_CRITICAL_FROM_ISR();
taskEXIT_CRITICAL_FROM_ISR( ulReturn );
对于在非中断场合,临界段不可以进行嵌套:
taskENTER_CRITICAL();
taskEXIT_CRITICAL();
0x02 空闲任务与阻塞延时的实现
在实现打印效果的时候,可以发现其打印时间并不是“一起发生的”,而是间隔着一秒一秒的时间发生的,类似于裸机系统的处理:
这里使用的延时效果是使用软件延时,也就是还让CPU空着等来达到延时的效果,这样其实也是一种浪费资源的行为。RTOS中的延时称为阻塞延时,即任务需要延时的时候会放弃CPU的使用权,CPU可以去干其他事情。当任务进行延时时,如果CPU没有其他任务需要运行,RTOS会给CPU创建一个空闲任务给CPU执行。
空闲任务为FreeRTOS中启动调度器的时候创建的优先级最低的任务。空闲任务主要是做一些系统内存的清理工作。在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或者低功耗等操作。
空闲任务的创建
-
定义空闲任务的栈
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
-
定义空闲任务的任务控制块
TCB_t IdleTaskTCB;
-
创建空闲任务
extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize );
void vTaskStartScheduler( void )
{
TCB_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
&pxIdleTaskStackBuffer,
&ulIdleTaskStackSize );
xIdleTaskHandle =
xTaskCreateStatic( (TaskFunction_t)prvIdleTask,
(char *)"IDLE",
(uint32_t)ulIdleTaskStackSize ,
(void *) NULL,
(StackType_t *)pxIdleTaskStackBuffer,
(TCB_t *)pxIdleTaskTCBBuffer );
vListInsertEnd( &( pxReadyTasksLists[0] ),
&( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
...
}
函数vApplicationGetIdleTaskMemory
用于获取空闲任务的内存,即将pxIdleTaskTCBBuffer
和pxIdleTaskStackBuffer
这两个接下来要作为形参传到xTaskCreateStatic
函数中的指针分别指向空闲任务的TCB和栈的起始地址,这个操作由函数vApplicationGetIdleTaskMemory()
来实现,该函数需要用户自定义。
void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer = &xIdleTaskTCBBuffer;
*ppxIdleTaskStackBuffer = &xIdleStack[0];
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
实现阻塞延时
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。
#if ( INCLUDE_vTaskDelay == 1 )
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
if( xTicksToDelay > ( TickType_t ) 0U )
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll();
{
traceTASK_DELAY();
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
xTicksToDelay 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。
-
修改**vTaskSwitchContext()**函数,在这里多了个空闲任务,在这里可以通过判断当前任务是否为空闲任务,之后执行对应的程序,去判断延时时间是否已经到达,若到达则执行,若没有到达则切换
-
SysTick中断服务函数xPortSysTickHandler(),在FreeRTOS中,xTicksToDelay是以SysTick中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期。
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI();
{
if( xTaskIncrementTick() != pdFALSE )
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR();
}
-
**xTaskIncrementTick()**更新系统时基函数,这个函数主要用于xTicksToDelay变量的递减,更新系统时基计数器 xTickCount,加一操作。xTickCount 是一个在port.c 中定义的全局变量,在函数 vTaskStartScheduler()中调用 xPortStartScheduler()函数前初始化。
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
}
}
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
#if ( configUSE_TICK_HOOK == 1 )
{
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
else
{
++uxPendedTicks;
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
return xSwitchRequired;
}
在这里执行完任务切换后,则会退出临界段,然后开中断。
-
若需要将SysTick的中断服务函数顺利执行,这个时候需要将Systick进行初始化,使用函数vPortSetupTimerInterrupt()
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
#else
#define portNVIC_SYSTICK_CLK_BIT ( 0 )
#endif
#define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
SysTick 初始化函 数 vPortSetupTimerInterrupt() , 在**xPortStartScheduler()**中被调用。
设置重装载寄存器的值,决定 SysTick 的中断周期。同时 configTICK_RATE_HZ 也在 FreeRTOSConfig.h 中定义:
#define configCPU_CLOCK_HZ (( unsigned long ) 25000000)
#define configTICK_RATE_HZ (( TickType_t ) 100)
在函数vPortSetupTimerInterrupt()中,配置SysTick 每秒中断多少次,目前配置为100,即每10ms中断一次,最后设置系统定时器的时钟等于内核时钟,使能 SysTick 定时器中断,使能 SysTick 定时器。
阻塞等待总结
在这里总结一下如何实现阻塞等待:
-
在前面我们在配置时钟的时候得到了我们当前系统时钟与RTOS的时钟并非同一个时钟,RTOS使用的时钟为SysTick,后续跟rtos相关的API都是用改时钟源,例vtaskdelay。它会在每次计数的时候判断此时 就绪队列中是否有delay为0的任务如果有则进行调度。
-
单片机系统的时钟源可以自己设置,比如TIMx,跟单片机相关的API都用该时钟源,例如hal delay,里面就是用TIMx来计数从而实现延时。
最后实现的效果伪代码如下:
vtaskdelay的流程:
{
装入延时的值进行任务调度(假设目前只有task1和task2,此次发起延时的任务是task1,则调用YIELD之后会进行task2)
}
运行task2过程中:
{
systick不断计数,每次计数都会判断task1的delay是否到期,到期则重新切换回task1
}
在这个过程中使用的一直为RTOS的时钟源。
0x03 多优先级
在就绪列表pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组,数组里面存的是就绪任务的 TCB(准确来说是 TCB 里面的 xStateListItem 节点),数组的下标对应任务的优先级,优先级越低对应的数组下标越小。空闲任务的优先级最低,对应的是下标为 0 的链表。任务在创建的时候,会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面的同一条链表中,这个时候就需要用到时间片。
pxCurrenTCB 是一个全局的 TCB 指针,用于指向优先级最高的就绪任务的 TCB,即当前正在运行的 TCB 。要做到多优先级只需要在任务切换的时候将指针指向最高优先级的就绪任务TCB即可。
如果找到最高优先级的就绪任务的 TCB,在FreeRTOS上具有两套方法,于task.c
中定义,查找最高优先级的就绪任务有两种方法 , 具 体 由configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏控制,定义为 0 选择通用方法,定义为 1 选择根据处理器优化的方法,该宏默认在 portmacro.h 中定义为 1,即使用优化过的方法:
#ifndef configUSE_PORT_OPTIMISED_TASK_SELECTION
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
#endif
通用方法:
taskRECORD_READY_PRIORITY()
此函数用于更新uxTopReadyPriority的值。这个是一个在task.c
定义的静态变量,用于创建任务的最高优先级,默认初始化为0,即空闲任务的优先级:
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )
PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
通用方法:
#define taskRECORD_READY_PRIORITY( uxPriority ) \
{ \
if( ( uxPriority ) > uxTopReadyPriority ) \
{ \
uxTopReadyPriority = ( uxPriority ); \
} \
}
taskSELECT_HIGHEST_PRIORITY_TASK()
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority = uxTopReadyPriority; \
\
\
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
{ \
configASSERT( uxTopPriority ); \
--uxTopPriority; \
} \
\
\
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
uxTopReadyPriority = uxTopPriority; \
}
此函数用于查找优先级最高的就绪任务,更新uxTopReadyPriority
和pxCurrentTCB
的值。
优化方法:
Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32位)从高位开始第一次出现 1 的位的前面的零的个数。
如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应的位置 1,反之则清零。那么可以利用前导零指令计算出当前就绪任务的最高优先级,将高优先级的放在高位。
taskRECORD_READY_PRIORITY()
taskRECORD_READY_PRIORITY()用于根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority 的某个位置 1。与通用方法中用来表示创建的任务的最高优先级不一样,它在优化方法中担任的是一个优先级位图表的角色,即该变量的每个位对应任务的优先级,如果任务就绪,则将对应的位置 1,反之清零。
其主要实现是靠如下的宏:
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
#define taskRESET_READY_PRIORITY( uxPriority ) \
{ \
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
{ \
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
} \
}
taskRESET_READY_PRIORITY()
#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )
#define taskRESET_READY_PRIORITY( uxPriority ) \
{ \
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
{ \
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
} \
}
此函数是将变量uxTopReadyPriority
的某个位清零。实际上根据优先级调用 taskRESET_READY_PRIORITY()函数复位 uxTopReadyPriority 变量中对应的位时,要先确保就绪列表中对应该优先级下的链表没有任务才行,如果此时列表中具有延时任务,不能将一个非就绪任务从就绪列表中移除,所以只能通过优先级的变量在对应的位中进行清零。
taskSELECT_HIGHEST_PRIORITY_TASK()
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
\
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
根据 uxTopReadyPriority 的值,找到最高优先级,然后更新到uxTopPriority 这个局部变量中。之后根据 uxTopPriority 的值,从就绪列表中找到就绪的最高优先级的任务的 TCB,然后将 TCB 更新到 pxCurrentTCB。
prvAddTaskToReadyList()
根据优先级将位图uxTopReadyPriority
中的对应位置位,并且根据优先级将任务插入就绪列表pxReadyTasksLists[]
。
#define prvAddTaskToReadyList( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
0x04 任务延时列表
工作原理:在FreeRTOS中,当任务需要延时的时候,先将任务进行挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量:xNextTaskUnblockTime
的值。
xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。
任务延时列表表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。当每次时基中断(SysTick 中断)来临时,就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较,如果相等,则表示有任务延时到期,需要将该任务就绪,否则只是单纯地更新系统时基计数器xTickCount 的值,然后进行任务切换。FreeRTOS定义了如下的延时列表:
PRIVILEGED_DATA static List_t xDelayedTaskList1 = {0};
PRIVILEGED_DATA static List_t xDelayedTaskList2 = {0};
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList = NULL;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList = NULL;
前面为两个任务延时列表,当系统时基计数器xTickCount没有溢出时,只用一条列表,当xTickCount溢出后,用另外一条列表。
后面为指向这两个列表的指针。
任务延时列表初始化
使用函数vListInitialise
进行初始化。
定义xNextTaskUnblockTime
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = ( TickType_t ) 0U;
xNextTaskUnblockTime 是一个在 task.c 中定义的静态变量,用于表示下一个任务的解锁时刻。xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。
初始化xNextTaskUnblockTime位于函数vTaskStartScheduler()
中,初始化为portMAX_DELAY
,位于portmacro.h
。
并且在延时函数vTaskDelay()
中加入延时列表prvAddCurrentTaskToDelayedList
:
prvAddCurrentTaskToDelayedList()
通过判断当前时间是否已经满足等待的时间,如果是则加入溢出队列,否则时间还没到,继续加入等待队列。只需要让系统时基计数器 xTickCount 与 xNextTaskUnblockTime 的值先比较就知道延时最快结束的任务是否到期。
xTaskIncrementTick()
对于此函数,主要是在判断变量xConstTickCount
是否溢出,若溢出则切换延时列表。如果延时任务列表为空,则设置xNextTaskUnblockTime为可能的最大值。如果不为空,则要知道将延时任务中所有延时到期的任务移除才能跳出for循环。若找到已经延时结束的任务,则消除等待状态,将解除等待的任务添加到就绪列表。
taskSWITCH_DELAYED_LISTS()
#define taskSWITCH_DELAYED_LISTS() \
{ \
List_t *pxTemp; \
\
\
configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) ); \
\
pxTemp = pxDelayedTaskList; \
pxDelayedTaskList = pxOverflowDelayedTaskList; \
pxOverflowDelayedTaskList = pxTemp; \
xNumOfOverflows++; \
prvResetNextTaskUnblockTime(); \
}
在这切换了延时列表,实际就是更换pxDelayedTaskList
和pxOverflowDelayedTaskList
这两个指针的指向。之后复位xNextTaskUnblockTime
的值。
static void prvResetNextTaskUnblockTime( void )
{
TCB_t *pxTCB;
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
}
else
{
( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
}
}
taskRESET_READY_PRIORITY()
#define taskRESET_READY_PRIORITY( uxPriority ) \
{ \
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
{ \
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \
} \
}
需要判断就绪列表 pxReadyTasksLists[]在当前优先级下对应的链表的节点是否为 0,只有当该链表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。
0x05 时间片
在同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间叫时间片。
在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,RT-Thread 和 μC/OS 可以指定时间片的大小为多个 tick,但是 FreeRTOS 不一样,时间片只能是一个 tick。
如何实现一个优先级下多个任务的时间片处理,主要由函数taskRESET_READY_PRIORITY()和taskSELECT_HIGHEST_PRIORITY_TASK()这两个函数的实现方法。
系统在任务系统切换时,总会从就绪列表中寻找优先级最高的任务来执行,寻找优先级最高的任务使用函数taskSELECT_HIGHEST_PRIORITY_TASK
:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
\
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
如果当前优先级上具有任务1和任务2,那么如何进行选择和切换其任务,具体在函数listGET_OWNER_OF_NEXT_ENTRY
:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \
{ \
List_t * const pxConstList = ( pxList ); \
\ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \
}
在这个函数中不断执行链表的遍历操作,每个任务都轮流执行,享有相同的CPU时间,即所谓的时间片。
其实 FreeRTOS 的这种时间片功能不能说是真正意义的时间片,因为它不能随意的设置时间为多少个 tick,而是默认一个 tick,然后默认在每个 tick 中断周期中进行任务切换而已。
0x06 总结
对于具有多个优先级的任务,他们是通过设置优先级进行抢占式运行,但是对于同个优先级下的任务,则是通过时间片分配来执行的。但如果你的同优先级的任务中加入了死循环,那么在这个优先级后的任务则无法执行,所以这个时候需要函数vTaskDelay()来将任务进行挂起,之后执行低优先级的任务,才可以顺序执行。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)