根据个人的学习方向,学习FreeRTOS。由于野火小哥把FreeRTOS讲得比较含蓄,打算在本专栏尽量细化一点。作为个人笔记,仅供参考或查阅。
配套资料:FreeRTOS内核实现与应用开发实战指南、野火FreeRTOS配套视频源码、b站野火FreeRTOS视频。搭配来看更佳哟!!!
空闲任务与阻塞延时
FreeRTOS中的延时叫阻塞延时,即任务需要延时的时候,任务会放弃CPU的使用权,CPU转而去干其他的事情。当任务延时时间到,重新获取CPU使用权,任务继续运行。
细里说,当任务性需要延时时,进入阻塞状态。如果没有其他任务可以运行,FreeRTOS都会为CPU创建一个空闲任务,这个时候CPU就运行空闲任务。
在FreeRTOS中,空闲任务是系统在“启动任务调度器”的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。
在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或低功耗等操作。在本节中,简易操作,在空闲任务中对一个全局变量进行计数。
实现空闲任务
定义空闲任务的栈和任务控制块
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
创建空闲任务
空闲任务在任务调度器启动函数vTaskStartScheduler()中创建。
void vTaskStartScheduler( void )
{
/*创建空闲任务start*/
TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */
StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
uint32_t ulIdleTaskStackSize;
/* 获取空闲任务的内存:任务栈和任务TCB */
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;
/* 初始化系统时基计数器,与阻塞延时、Systick有关,下文会讲 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回 */
}
}
上面函数用到了vApplicationGetIdleTaskMemory函数。如下
/* 获取空闲任务的内存 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
空闲任务创建完毕。
阻塞延时
阻塞延时函数vTaskDelay()在task.c中定义
在实现阻塞延时函数之前,看一下TCB的结构体定义
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;
可以看出,TCB的结构体定义比以前多了一个参数,TCB的结构体定义逐步完善。
xTicksToDelay用于延时,单位为Systick的中断周期,本节下面会讲,这里不展开。比如Systick的中断周期为10ms,调用vTaskDelay(2)则完成2*10ms的延时。
阻塞延时函数如下
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
上面函数中,任务切换时,调用taskYIELD()会产生PendSV中断,在PendSV中断服务函数中会调用上下文切换函数vTaskSwitchContext()。
该函数的作用是寻找最高优先级的就绪任务,然后更新pxCurrentTCB。
上下文切换函数如下。实现三个任务间的转换。
void vTaskSwitchContext( void )
{
/* 如果当前线程是空闲线程,那么就去尝试执行线程1或者线程2,看看他们的延时时间是否结束,如果线程的延时时间均没有到期,那就返回继续执行空闲线程 */
if( pxCurrentTCB == &IdleTaskTCB ) //当前TCB是空闲TCB,那么Task1和Task2哪个的延时结束就执行哪个Task,都没有延时结束,则继续执行空闲Task
{
if(Task1TCB.xTicksToDelay == 0) //Task1TCB的延时结束
{
pxCurrentTCB =&Task1TCB; //执行Task1
}
else if(Task2TCB.xTicksToDelay == 0) //Task2TCB的延时结束
{
pxCurrentTCB =&Task2TCB; //执行Task2
}
else
{
return; /* 线程延时均没有到期则返回,继续执行空闲线程 */
}
}
else //当前TCB不是空闲任务
{
/*如果当前线程是线程1或者线程2的话,检查下另外一个线程,如果另外的线程不在延时中,就切换到该线程
否则,判断下当前线程是否应该进入延时状态,如果是的话,就切换到空闲线程。否则就不进行任何切换 */
if(pxCurrentTCB == &Task1TCB) //当前TCB是Task1TCB
{
if(Task2TCB.xTicksToDelay == 0) //Task2TCB的延时结束
{
pxCurrentTCB =&Task2TCB; //执行Task2
}
else if(pxCurrentTCB->xTicksToDelay != 0) //Task1TCB和Task2TCB延时还没到零
{
pxCurrentTCB = &IdleTaskTCB; //进入空闲任务TCB
}
else
{
return; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
else if(pxCurrentTCB == &Task2TCB) //当前TCB是Task2TCB
{
if(Task1TCB.xTicksToDelay == 0) //Task1TCB的延时结束
{
pxCurrentTCB =&Task1TCB; //执行Task1
}
else if(pxCurrentTCB->xTicksToDelay != 0) //Task1TCB和Task2TCB延时还没到零
{
pxCurrentTCB = &IdleTaskTCB; //进入空闲任务TCB
}
else
{
return; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
}
}
SysTick
SysTick中断服务函数
在本节中,我们得知xTickToDelay与SysTick相关。在操作系统中,最小的时间单位就是SysTick的中断周期,我们称为一个Tick。
SysTick中断服务函数在port.c实现。
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
//进入临界段保护
/* 更新系统时基 */
xTaskIncrementTick();
//退出临界段保护
/* 开中断 */
vPortClearBASEPRIFromISR();
}
上面函数使用到xTaskIncrementTick()函数,用来更新系统时基。如下
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
/* xTickCount在vTaskStartScheduler()中初始化为0 */
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
/* 扫描就绪列表中所有线程的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();
}
SysTick初始化
SysTick的中断服务函数要想被顺利执行,则SysTick必须先初始化。初始化函数定义在port.c。
一些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
/* 确保SysTick的时钟与内核时钟一致 */
#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初始化函数如下
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初始化函数在xPortStartScheduler()中被调用。
BaseType_t xPortStartScheduler( void )
{
/* 配置PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化SysTick */
vPortSetupTimerInterrupt();
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
main()函数
无多大变动,主要的还是任务入口函数由原来的delay(100)变成vTaskDelay(2)这样。
主要函数:
空闲任务在任务调度器启动函数vTaskStartScheduler()中创建。
阻塞延时函数vTaskDelay()。
上下文切换函数vTaskSwitchContext()实现三个任务间的转换。