裸机系统的软件延时通过让CPU等待来达到延时效果;而RTOS的优点就是完全发挥CPU的性能,即使延时也不会让CPU空等。操作系统的延时叫做阻塞延时,当任务需要延时的时候,任务会放弃CPU的使用权,直到延时结束,重新获得CPU进行运行。在延时期间,CPU可以处理其他的事情。
在当前任务进入阻塞状态时,CPU可以去处理别的任务。假如当前没有别的任务可运行,CPU就会进入系统创建的空闲任务。空闲任务在调度器里是优先级最低的任务,其主体主要是做一些系统内存的清理工作。在实际应用中,当系统进入空闲任务后,可以让单片机进入休眠或者低功耗等操作。
这里野火让空闲任务实现对一个全局变量进行计数的功能。
目前创建栈和任务控制块都是静态创建,使用静态内存。因此,需要预先定义内存空间。
栈和TCB的内存空间均在main.c中定义。
任务栈是一个定义好的数组,大小由 FreeRTOSConfig.h 中定义的宏 控制,默认为 128, 单位为字(byte),即 512 个字节(bit)。
/* 定义空闲任务的栈 */
/* 在FreeRTOSConfig.h中定义 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
/* 获取空闲任务的内存 */
/* 在main.c中定义 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB; //定义空闲任务发任务控制块
在定义好空闲任务的栈和任务控制块后,才可以创建空闲任务。空闲任务在调度器启动函数 vTaskStartScheduler()中创建。
该部分在task.c中实现。
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;
/* 获取空闲任务的内存:任务栈和任务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 ) );
}
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
阻塞,就是在任务调用这个延时函数后,任务会失去CPU使用权,并进入阻塞状态。在延时结束后,重新得到CPU使用权,继续运行。在该任务在阻塞状态时,菜CPU可以去执行其他任务。如果当前没有可运行的任务,CPU就会进入空闲任务。
阻塞延时函数在task.c中实现。
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
xTicksToDelay 是任务控制块的成员,用于记录延时时间,单位为SysTick的中断周期。野火的中断周期为10ms,这里调用xTicksToDelay完成20ms的延时。
带xTicksToDelay的任务控制块定义FreeRTOS.h下,具体如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; /* 栈顶 */
ListItem_t xStateListItem; /* 任务节点 */
StackType_t *pxStack; /* 任务栈起始地址 */
/* 任务名称,字符串形式 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
TickType_t xTicksToDelay; /* 用于延时 */
} tskTCB;
这里需要把 task.c中的定义注释掉,之后再在FreeRTOS.h中定义。
调用 tashYIELD()(在portmacro.h定义)会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),其作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。 因为增加了一个空闲任务,所以需要让pxCurrentTCB 在这三个任务中切换,因此我们需要修改一下切换函数。
void vTaskSwitchContext( void )
{
if( pxCurrentTCB == &IdleTaskTCB )
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return; /* 线程延时均没有到期则返回,继续执行空闲线程 */
}
}
else
{
if(pxCurrentTCB == &Task1TCB)
{
if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
else if(pxCurrentTCB == &Task2TCB)
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(pxCurrentTCB->xTicksToDelay != 0)
{
pxCurrentTCB = &IdleTaskTCB;
}
else
{
return; /* 返回,不进行切换,因为两个线程都处于延时中 */
}
}
}
}
如果当前任务是空闲任务的话,如果检测到另外两个任务的延时结束,那么就执行他们;延时没有结束则返回空闲任务。
如果当前有一个任务在执行,那么就检查另外一个任务是否在延时。,如果另一个不在延时中,就切换到那任务;如果当前任务需要进行延时,那么就切换到空闲任务,否则继续运行当前任务。
SysTick 必须先初始化,然后中断服务函数才能被调用。 SysTick 初始化函数在 port.c 中定义。
/* SysTick 配置寄存器 */
#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
/* 确保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 )
void vPortSetupTimerInterrupt( void );
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配置为100ms中断一次。
SysTick的初始化函数将在xPortStartScheduler()中被调用。
重装载寄存器的值决定着SysTick的中断周期。如果这个宏没有定义,就会默认等于cpu的时钟频率。configSYSTICK_CLOCK_HZ和configTICK_RATE_HZ都在FreeRTOSConfig.h中定义。
BaseType_t xPortStartScheduler( void )
{
/* 配置 PendSV 和 SysTick 的中断优先级为最低 */
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
/* 初始化 SysTick */
vPortSetupTimerInterrupt();
/* 启动第一个任务,不再返回 */
prvStartFirstTask();
/* 不应该运行到这里 */
return 0;
}
在实现任务切换的过程中,需要判断每个任务的延时是否为0。当任务需要延时的时候,xTicksToDelay会进行递减至0。而延时的周期递减由SysTick中断提供,同时操作系统的最小时间单位就是SysTick的中断周期,一个周期被称为一个tick。中断函数在port.c中实现。
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 退出临界段,开中断 */
vPortClearBASEPRIFromISR();
}
更新系统时基函数在 task.c 中定义。
void xTaskIncrementTick( void )
{
TCB_t *pxTCB = NULL;
BaseType_t i = 0;
/* 更新系统时基计数器xTickCount,xTickCount是一个在port.c中定义的全局变量 */
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();
}
时基即周期,更新系统时基计数器 xTickCount, 加一操作。xTickCount 是在port.c 中定义的全局变量,在函数 vTaskStartScheduler()中调用 xPortStartScheduler()函数前初始化。
因篇幅问题不能全部显示,请点此查看更多更全内容
怀疑对方AI换脸可以让对方摁鼻子 真人摁下去鼻子会变形
女子野生动物园下车狼悄悄靠近 后车司机按喇叭提醒
睡前玩8分钟手机身体兴奋1小时 还可能让你“变丑”
惊蛰为啥吃梨?倒春寒来不来就看惊蛰
男子高速犯困开智能驾驶出事故 60万刚买的奔驰严重损毁