freertos实时操作系统空闲任务阻塞延时示例解析

阻塞态:如果一个任务当前正在等待某个外部事件,则称它处于阻塞态。

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; i<configMAX_PRIORITIES; i++)
	{
        pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
		if(pxTCB->xTicksToDelay > 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,跑完了才进行任务切换,如下图所示,一个任务高低电平全搞完,才切到下一个任务。

以上就是freertos实时操作系统空闲任务阻塞延时示例解析的详细内容,更多关于freertos空闲任务阻塞延时的资料请关注我们其它相关文章!

(0)

相关推荐

  • C语言 Freertos的递归锁详解

    目录 1.死锁的概念 2.自我死锁 3.递归锁 4.代码 5.运行流程分析 6.运行结果 总结 1.死锁的概念 假设有 2 个互斥量 M1. M2, 2 个任务 A. B: A 获得了互斥量 M1 B 获得了互斥量 M2 A 还要获得互斥量 M2 才能运行,结果 A 阻塞 B 还要获得互斥量 M1 才能运行,结果 B 阻塞 A. B 都阻塞,再无法释放它们持有的互斥量 死锁发生! 2.自我死锁 任务 A 获得了互斥锁 M 它调用一个函数 函数要去获取同一个互斥锁 M,于是它阻塞:任务 A 休眠,

  • FreeRTOS实时操作系统特点介绍

    目录 1.什么是FreeRTOS? 2.为什么择 选择 FreeRTOS ? 3.FreeRTOS 特点 FreeRTOS资料与源码下载 FreeRTOS源码文件介绍 1.什么是FreeRTOS? Free 即免费的,RTOS 全称是 Real Time Operating System,中文就是实时操作系统.注意,RTOS 不是指某一个确定的系统,而是指一类系统.比如 uC/OS,FreeRTOS,RTX,RT-Thread 等这些都是 RTOS 类操作系统. 操作系统允许多个任务同时运行,这

  • FreeRTOS实时操作系统结构示例

    目录 1.查找相关文档页 2.获取RTOS源代码 3.FreeRTOS源码目录结构 移植层目录举例: 演示例程目录举例: 4.编译工程 5.运行演示例程 FreeRTOS可以被移植到很多不同架构的处理器和编译器.每一个RTOS移植都附带一个已经配置好的演示例程,可以方便快速启动开发.更好的是,每个演示例程都附带一个说明网页,提供如何定位RTOS演示工程源代码.如何编译演示例程.如何配置硬件平台的全部信息. 演示例程说明网页还提供基本的RTOS移植细节信息,包括如何编写FreeRTOS兼容的中断服

  • freertos实时操作系统空闲任务阻塞延时示例解析

    阻塞态:如果一个任务当前正在等待某个外部事件,则称它处于阻塞态. rtos中的延时叫阻塞延时,即任务需要延时的时候,会放弃CPU的使用权,进入阻塞状态.在任务阻塞的这段时间,CPU可以去执行其它的任务(如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务),当任务延时时间到,重新获取 CPU 使用权,任务继续运行. 空闲任务:处理器空闲的时候,运行的任务.当系统中没有其他就绪任务时,空闲任务开始运行,空闲任务的优先级是最低的. 空闲任务 定义空闲任务: #define portSTACK_

  • FreeRTOS实时操作系统空闲任务的阻塞延时实现

    目录 什么是阻塞延时.为什么需要空闲任务 空闲任务的实现 阻塞延时的实现 xTicksToDelay 递减 SysTick初始化 仿真 什么是阻塞延时.为什么需要空闲任务 RTOS中的延时叫阻塞延时,即任务需要延时时,任务会放弃cpu使用权,cpu转而去做其他的事,当任务延时时间到后,任务重新请求获得cpu使用权.但当所有的任务都处于阻塞后,为了不让cpu空闲没事干就需要一个空闲任务让cpu干活. 空闲任务的实现 空闲任务实现和创建普通任务没区别,空闲任务在调用vTaskStartSchedul

  • FreeRTOS实时操作系统临界段保护场合示例

    目录 临界段保护场合 非中断场合 中断场合 临界段保护场合 FreeRTOS中临界段保护有2种场合,中断和非中断,通过关中断(或者关部分中断)来实现临界保护. 非中断场合 task.h 中 #define taskENTER_CRITICAL()portENTER_CRITICAL() #define taskEXIT_CRITICAL()portEXIT_CRITICAL() portmacro.h中 #define portENTER_CRITICAL()vPortEnterCritical

  • FreeRTOS实时操作系统多任务管理基础知识

    目录 什么是多任务系统? FreeRTOS  任务与协程 1.任务(Task) 的特性 2.协程(Co-routine)的特性 任务状态 运行态 就绪态 阻塞态 挂起态 任务优先级 任务实现 任务控制块 任务堆栈 RTOS 系统的核心就是任务管理,FreeRTOS 也不例外,而且大多数学习 RTOS 系统的工程师或者学生主要就是为了使用 RTOS 的多任务处理功能,初步上手 RTOS 系统首先必须掌握的也是任务的创建.删除.挂起和恢复等操作,由此可见任务管理的重要性. 什么是多任务系统? 回想一

  • FreeRTOS实时操作系统队列的API函数讲解

    目录 FreeRTOS为操作队列提供了非常丰富的API函数,包括队列的创建.删除,灵活的入队和出队方式.带中断保护的入队和出队等等.下面就来详细讲述这些API函数. 1.获取队列入队信息数目1.1函数描述 UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue ); 返回队列中存储的信息数目.具有中断保护的版本为uxQueueMessagesWaitingFromISR(),原型为:UBaseType_t uxQueueMessagesW

  • freertos实时操作系统临界段保护开关中断及进入退出

    目录 中断的基础知识 嵌套: 优先级: 中断的悬起与解悬: 咬尾中断Tail‐Chaining: 晚到的高优先级异常: 进入临界段和退出临界段 中断的基础知识 嵌套: 嵌套向量中断控制器 NVIC(Nested Vectored Interrupt Controller与内核是紧耦合的.提供如下的功能:可嵌套中断支持.向量中断支持.动态优先级调整支持.中断延迟大大缩短. 中断可屏蔽. 所有的外部中断和绝大多数系统异常均支持可嵌套中断.异常都可以被赋予不同的优先级,当前优先级被存储在 xPSR的专

  • FreeRTOS实时操作系统信号量基础

    目录 前言 1.信号量简介 2.二进制信号量 3.计数信号量 4.互斥量 5.递归互斥量 前言 本文介绍信号量的基础知识,详细源码分析见<FreeRTOS进阶FreeRTOS信号量分析> 1.信号量简介 FreeRTOS的信号量包括二进制信号量.计数信号量.互斥信号量(以后简称互斥量)和递归互斥信号量(以后简称递归互斥量). 我们可以把互斥量和递归互斥量看成特殊的信号量.互斥量和信号量在用法上不同: 信号量用于同步,任务间或者任务和中断间同步:互斥量用于互锁,用于保护同时只能有一个任务访问的资

  • FreeRTOS实时操作系统队列基础

    目录 本文介绍队列的基本知识,详细源码分析见<FreeRTOS高级篇5---FreeRTOS队列分析> 1.FreeRTOS队列 队列是主要的任务间通讯方式.可以在任务与任务间.中断和任务间传送信息.大多数情况下,队列用于具有线程保护的FIFO(先进先出)缓冲区:新数据放在队列的后面.当然,数据也可以放在队列的前面,在下一篇讲队列API函数时,会涉及到数据的存放位置. 图1-1:读写队列 图1-1所示的队列中,最多能保存5个项目,并且假设队列永远不会满.任务A使用API函数xQueueSend

  • FreeRTOS实时操作系统Cortex-M内核使用注意事项

    前言 在阅读本文之前,有两个定义在FreeRTOSConfig.h中的宏,你必须先明白它们是什么意思,<FreeRTOS内核配置说明>一文中,讲解了这两个宏: configKERNEL_INTERRUPT_PRIORITY configMAX_SYSCALL_INTERRUPT_PRIORITY FreeRTOS与Cortex-M内核可谓是绝配,以至于让移植和使用FreeRTOS都变得更简单起来.根据FreeRTOS官方反馈,在Cortex-M内核上使用FreeRTOS大多数的问题点是由不正确

随机推荐