日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

freertos實時操作系統空閑任務阻塞延時示例解析_操作系統

作者:jym蒟蒻 ? 更新時間: 2022-06-07 編程語言

前言

阻塞態:如果一個任務當前正在等待某個外部事件,則稱它處于阻塞態。

rtos中的延時叫阻塞延時,即任務需要延時的時候,會放棄CPU的使用權,進入阻塞狀態。在任務阻塞的這段時間,CPU可以去執行其它的任務(如果其它的任務也在延時狀態,那么 CPU 就將運行空閑任務),當任務延時時間到,重新獲取 CPU 使用權,任務繼續運行。

空閑任務:處理器空閑的時候,運行的任務。當系統中沒有其他就緒任務時,空閑任務開始運行,空閑任務的優先級是最低的。

空閑任務

定義空閑任務:

#define portSTACK_TYPE	uint32_t
typedef portSTACK_TYPE StackType_t;
/*定義空閑任務的棧*/
#define configMINIMAL_STACK_SIZE	( ( unsigned short ) 128 )
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
/*定義空閑任務的任務控制塊*/
TCB_t IdleTaskTCB;

創建空閑任務:在vTaskStartScheduler調度器啟動函數中創建。

/*任務控制塊的結構體 */
typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 棧頂 */

	ListItem_t			    xStateListItem;   /* 任務節點 */
    
    StackType_t             *pxStack; /* 任務棧起始地址 */
	                                          
	char     pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任務名稱,字符串形式 */

    TickType_t xTicksToDelay; /* 用于延時 */
} tskTCB;
typedef tskTCB TCB_t;

/*獲取獲取空閑任務的內存:任務控制塊、任務棧起始地址、任務棧大小*/
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize )
{
		*ppxIdleTaskTCBBuffer=&IdleTaskTCB;//空閑任務的任務控制塊
		*ppxIdleTaskStackBuffer=IdleTaskStack; //空閑任務的任務棧
		*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;//棧的大小
}
void vTaskStartScheduler( void )
{
/*創建空閑任務start*/     

    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 ) );
    
/*創建空閑任務end*/
                                         
    /* 手動指定第一個運行的任務 */
    pxCurrentTCB = &Task1TCB;
    /* 初始化系統時基計數器 */
    xTickCount = ( TickType_t ) 0U;
    /* 啟動調度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 調度器啟動成功,則不會返回,即不會來到這里 */
    }
}
//下面是空閑任務的任務入口,看到,里面什么都沒做
//這個我用debug發現一直卡到這個for不動了。
//通過單步運行,發生了中斷,程序也無法進入中斷。
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
	/* 防止編譯器的警告 */
	( void ) pvParameters;
    for(;;)
    {
        /* 空閑任務暫時什么都不做 */
    }
}

阻塞延時

任務函數如下:延時函數由軟件延時替代為阻塞延時。

void Task1_Entry( void *p_arg )
{
	for( ;; )
	{
#if 0        
		flag1 = 1;
		delay( 100 );/*軟件延時*/		
		flag1 = 0;
		delay( 100 );
		/* 線程切換,這里是手動切換 */
        portYIELD();
#else
		flag1 = 1;
        vTaskDelay( 2 );/*阻塞延時*/		
		flag1 = 0;
        vTaskDelay( 2 );
#endif        
	}
}

任務函數里面調用了vTaskDelay阻塞延時函數,如下。

/*阻塞延時函數的定義 */
void vTaskDelay( const TickType_t xTicksToDelay )
{
    TCB_t *pxTCB = NULL;
    /* 獲取當前任務的任務控制塊 */
    pxTCB = pxCurrentTCB;
    /* 設置延時時間:xTicksToDelay個SysTick延時周期 */
    pxTCB->xTicksToDelay = xTicksToDelay;
    /* 任務切換 */
    taskYIELD();
}

然后vTaskDelay里面調用了taskYIELD函數,如下。目的是產生PendSV中斷,進入PendSV中斷服務函數。

/* Interrupt control and state register (SCB_ICSR):0xe000ed04
 * Bit 28 PENDSVSET: PendSV set-pending bit
 */
#define portNVIC_INT_CTRL_REG		( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
#define portNVIC_PENDSVSET_BIT		( 1UL << 28UL )
#define portSY_FULL_READ_WRITE		( 15 )
/* Scheduler utilities. */
#define portYIELD()																\
{																				\
	/* 設置 PendSV 的中斷掛起位,產生上下文切換 */								\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\																				\
	/* Barriers are normally not required but do ensure the code is completely	\
	within the specified behaviour for the architecture. */						\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

PendSV中斷服務函數如下,里面調用了vTaskSwitchContext上下文切換函數,目的是尋找最高優先級的就緒任務,然后更新pxCurrentTCB。

__asm void xPortPendSVHandler( void )
{
//	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
	PRESERVE8
    /* 當進入PendSVC Handler時,上一個任務運行的環境即:
       xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0(任務的形參)
       這些CPU寄存器的值會自動保存到任務的棧中,剩下的r4~r11需要手動保存 */
    /* 獲取任務棧指針到r0 */
	mrs r0, psp
	isb
	ldr	r3, =pxCurrentTCB		/* 加載pxCurrentTCB的地址到r3 */
	ldr	r2, [r3]                /* 加載pxCurrentTCB到r2 */

	stmdb r0!, {r4-r11}			/* 將CPU寄存器r4~r11的值存儲到r0指向的地址 */
	str r0, [r2]                /* 將任務棧的新的棧頂指針存儲到當前任務TCB的第一個成員,即棧頂指針 */				
                               	stmdb sp!, {r3, r14}        
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 進入臨界段 */
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext       /* 調用函數vTaskSwitchContext,尋找新的任務運行,通過使變量pxCurrentTCB指向新的任務來實現任務切換 */ 
	mov r0, #0                  /* 退出臨界段 */
	msr basepri, r0
	ldmia sp!, {r3, r14}        /* 恢復r3和r14 */
	ldr r1, [r3]
	ldr r0, [r1] 				/* 當前激活的任務TCB第一項保存了任務堆棧的棧頂,現在棧頂值存入R0*/
	ldmia r0!, {r4-r11}			/* 出棧 */
	msr psp, r0
	isb
	bx r14                      
	nop
}

vTaskSwitchContext上下文切換函數如下。

任務需要延時的時候,會放棄CPU的使用權,進入阻塞狀態。在任務阻塞的這段時間,CPU可以去執行其它的任務(如果其它的任務也在延時狀態,那么 CPU 就將運行空閑任務),當任務延時時間到,重新獲取 CPU 使用權,任務繼續運行。

void vTaskSwitchContext( void )
{
	if( pxCurrentTCB == &IdleTaskTCB )//如果當前線程是空閑線程
	{
		if(Task1TCB.xTicksToDelay == 0)//如果線程1延時時間結束
		{            
            pxCurrentTCB =&Task1TCB;//切換到線程1
		}
		else if(Task2TCB.xTicksToDelay == 0)//如果線程2延時時間結束(線程1在延時中)
		{
            pxCurrentTCB =&Task2TCB;//切換到線程2
		}
		else
		{
			return;		/* 線程延時均沒有到期則返回,繼續執行空閑線程 */
		} 
	}
	else//當前任務不是空閑任務
	{
		if(pxCurrentTCB == &Task1TCB)//如果當前線程是線程1
		{
			if(Task2TCB.xTicksToDelay == 0)//如果線程2不在延時中
			{
                pxCurrentTCB =&Task2TCB;//切換到線程2
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)//如果線程1進入延時狀態(線程2也在延時中)
			{
                pxCurrentTCB = &IdleTaskTCB;//切換到空閑線程
			}
			else 
			{
				return;		/* 返回,不進行切換 */
			}
		}
		else if(pxCurrentTCB == &Task2TCB)//如果當前線程是線程2
		{
			if(Task1TCB.xTicksToDelay == 0)//如果線程1不在延時中
			{
                pxCurrentTCB =&Task1TCB;//切換到線程1
			}
			else if(pxCurrentTCB->xTicksToDelay != 0)//如果線程2進入延時狀態(線程1也在延時中)
			{
                pxCurrentTCB = &IdleTaskTCB;//切換到空閑線程
			}
			else 
			{
				return;		/* 返回,不進行切換*/
			}
		}
	}
}

由上面代碼可知,vTaskSwitchContext上下文切換函數通過看xTicksToDelay是否為零,來判斷任務已經就緒or繼續延時。

xTicksToDelay以什么周期遞減,在哪遞減。這個周期由SysTick中斷提供。

SysTick

SysTick是系統定時器,重裝載數值寄存器的值遞減到0的時候,系統定時器就產生一次中斷,以此循環往復。

下面是SysTick的初始化。

//main函數里面
    /* 啟動調度器,開始多任務調度,啟動成功則不返回 */
    vTaskStartScheduler();  
//task.c里面調用了xPortStartScheduler函數
void vTaskStartScheduler( void )
{
    //.....省略部分代碼
    /* 啟動調度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 調度器啟動成功,則不會返回,即不會來到這里 */
    }
}
//port.c里面
//xPortStartScheduler調度器啟動函數,里面調用了vPortSetupTimerInterrupt函數初始化SysTick
BaseType_t xPortStartScheduler( void )
{
    /* 配置PendSV 和 SysTick 的中斷優先級為最低 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    /* 初始化SysTick */
    vPortSetupTimerInterrupt();
	/* 啟動第一個任務,不再返回 */
	prvStartFirstTask();
	/* 不應該運行到這里 */
	return 0;
}
//system_ARMCM4.c文件
#define  XTAL            (50000000UL)     /* Oscillator frequency */
#define  SYSTEM_CLOCK    (XTAL / 2U)
//FreeRTOSConfig.h文件
//系統時鐘大小
#define configCPU_CLOCK_HZ			( ( unsigned long ) 25000000 )	
//SysTick每秒中斷多少次,配置成100,10ms中斷一次
#define configTICK_RATE_HZ			( ( TickType_t ) 100 )
//下面初始化SysTick
/* SysTick 控制寄存器 */
#define portNVIC_SYSTICK_CTRL_REG			( * ( ( volatile uint32_t * ) 0xe000e010 ) )
/*SysTick 重裝載寄存器*/
#define portNVIC_SYSTICK_LOAD_REG			( * ( ( volatile uint32_t * ) 0xe000e014 ) )
/*SysTick時鐘源的選擇*/
#ifndef configSYSTICK_CLOCK_HZ
	#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ//configSYSTICK_CLOCK_HZ=configCPU_CLOCK_HZ
	/* 確保SysTick的時鐘與內核時鐘一致 */
	#define portNVIC_SYSTICK_CLK_BIT	( 1UL << 2UL )//無符號長整形32位二進制,左移兩位
#else
	#define portNVIC_SYSTICK_CLK_BIT	( 0 )
#endif
#define portNVIC_SYSTICK_INT_BIT			( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT			( 1UL << 0UL )
//初始化SysTick的函數如下
void vPortSetupTimerInterrupt( void )
{
     /* 設置重裝載寄存器的值 */
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
    /* 設置系統定時器的時鐘等于內核時鐘
       使能SysTick 定時器中斷
       使能SysTick 定時器 */
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | 
                                  portNVIC_SYSTICK_INT_BIT |
                                  portNVIC_SYSTICK_ENABLE_BIT ); 
}

初始化好SysTick,下面看看SysTick的中斷服務函數。

現在就明白了,xTicksToDelay是以SysTick的中斷周期遞減的。

// port.c文件,SysTick中斷服務函數
//里面調用了xTaskIncrementTick函數更新系統時基
void xPortSysTickHandler( void )
{
	/* 關中斷 進入臨界段*/
    vPortRaiseBASEPRI();
    /* 更新系統時基 */
    xTaskIncrementTick();
	/* 開中斷 退出臨界段*/
    vPortClearBASEPRIFromISR();
}
//task.c文件,
static volatile TickType_t xTickCount 				= ( TickType_t ) 0U;
void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;
    /* 更新系統時基計數器xTickCount,xTickCount是一個在port.c中定義的全局變量 */
    const TickType_t xConstTickCount = xTickCount + 1;
    xTickCount = xConstTickCount;//把xTickCount加1
    
    /* 掃描就緒列表中所有線程的xTicksToDelay,如果不為0,則減1 */
	for(i=0; ixTicksToDelay > 0)
		{
			pxTCB->xTicksToDelay --;
		}
	}
    /* 任務切換 */
    portYIELD();
}

實驗現象

這個里面就可以看到,高電平時間是20ms,剛好是阻塞延時的20ms。而且兩個任務波形相同,好像是CPU在同時做兩件事。這就是阻塞延時的好處。

為什么呢,

一開始,所有任務都沒有進入延時。

當一個任務放棄CPU后(進入延時),這一瞬間,CPU立即轉向運行另一個任務(另一個任務也立即進入延時)。這是因為uvTaskDelay阻塞延時函數里面調用了taskYIELD()任務切換函數。所以產生PendSV中斷,進入PendSV中斷服務函數xPortPendSVHandler。

在那個PendSV中斷服務函數里面,調用vTaskSwitchContext上下文切換函數,由于現在兩個任務都在延時過程中,就開始切到空閑任務。

等到重裝載數值寄存器的值遞減到0的時候,系統定時器就產生一次中斷,進入系統定時器的中斷函數中,改變xTicksToDelay,然后再次調用任務切換函數portYIELD()。目的是產生PendSV中斷,進入PendSV中斷服務函數。

然后再次調用vTaskSwitchContext上下文切換函數,判斷現在兩個任務是否還在延時,如果任務1不在延時,那么立即切到任務1,任務1里面又調用uvTaskDelay阻塞延時函數,再次套娃重復上面的活動。

所以波形上幾乎同步。

之前用軟件延時在任務函數里面寫delay(100),這就屬于cpu一直跑這個delay,跑完了才進行任務切換,如下圖所示,一個任務高低電平全搞完,才切到下一個任務。

原文鏈接:https://www.cnblogs.com/jiangyiming/p/16102499.html

欄目分類
最近更新