目录
- 前言
- 空闲任务
- 实现空闲任务
- 1、定义空闲任务栈
- 2、定义空闲任务的任务控制块
- 4、定义空闲任务主体
- 实现阻塞延时
- vTaskDelay()函数
- 任务与空闲任务切换的例子:vTaskSwitchContext()函数
- SysTick中断服务函数
- 更新系统时基
- SysTick初始化函数
- 实验
- 仿真
前言
软件延时是让CPU等待达到延时效果。
而RTOS的优势是可以充分发挥CPU的性能,永远不会让CPU闲着。
RTOS中的延时叫做阻塞延时
空闲任务
当没有其他任务可以运行时,RTOS会为CPU创建一个空闲任务,然后CPU去执行。
在RTOS中,空闲任务是系统在调度器创建的优先级最低的任务,空闲任务主体是主要做一些系统内存的清理工作。
实现空闲任务
1、定义空闲任务栈
在min.c 中定义:
/* 获取空闲任务的内存 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
在FeeRTOSConfig.h中定义的宏
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )//(字,即512个字节)
2、定义空闲任务的任务控制块
定义空闲任务的任务控制块
TCB_t IdleTaskTCB;
3## 3、定义空闲函数主体
在mask.c 中定义:
#define portTASK_FUNCTION( vFunction, pvParameters ) void vFunction( void *pvParameters )//在protmacro.h
static portTASK_FUNCTION( prvIdleTask, pvParameters ) //prvIdleTask( void *pvParameters )
{
/* 防止编译器的警告 */
( void ) pvParameters;
for(;;)
{
/* 空闲任务暂时什么都不做 */
}
}
4、定义空闲任务主体
extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize );
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;
/* 初始化系统时基计数器 */
xTickCount = ( TickType_t ) 0U;
/* 启动调度器 */
if( xPortStartScheduler() != pdFALSE )
{
/* 调度器启动成功,则不会返回,即不会来到这里 */
}
}
vApplicationGetIdleTaskMemory()函数需要用户自己实现:
在main.c中
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;//任务控制块
*ppxIdleTaskStackBuffer=IdleTaskStack; //任务栈的起始地址
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;//任务栈的大小
}
其中,prvIdleTask在task.c中定义的空闲任务的任务句柄
static TaskHandle_t xIdleTaskHandle = NULL;//实现空闲任务的任务句柄
- 空闲任务只有在CPU空闲时才会执行。
- 空闲任务优先级最低是不能被阻塞的。
- 空闲任务主要做内存清理等辅助性工作如检查变量等。
实现阻塞延时
阻塞延时是指任务调用该延时函数后,任务会被剥夺CPU的使用权,然后进入阻塞状态,直到延时结束,任务重新获得CPU使用权才继续执行任务。
在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;
typedef tskTCB TCB_t;
vTaskDelay()函数
void vTaskDelay( const TickType_t xTicksToDelay )
{
TCB_t *pxTCB = NULL;
/* 获取当前任务的TCB */
pxTCB = pxCurrentTCB;
/* 设置延时时间 */
pxTCB->xTicksToDelay = xTicksToDelay;
/* 任务切换 */
taskYIELD();
}
任务与空闲任务切换的例子:vTaskSwitchContext()函数
思路:
如果当前是执行的任务是空闲任务就去尝试执行任务1和任务2,看看这两个任务的延时时间是否结束,如果没有到期,返回继续执行空闲函数。
如果当前是执行的任务是任务1或者任务2,检查一下下一个任务,如果另一个任务不在延时中,就切换到该任务。否则,判断当前任务是否应该进入延时状态,如果是就切换到空闲任务,否则就不做任务切换。
void vTaskSwitchContext( void )
{
/* 如果当前线程是空闲线程,那么就去尝试执行线程1或者线程2,
看看他们的延时时间是否结束,如果线程的延时时间均没有到期,
那就返回继续执行空闲线程 */
if( pxCurrentTCB == &IdleTaskTCB )
{
if(Task1TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task1TCB;
}
else if(Task2TCB.xTicksToDelay == 0)
{
pxCurrentTCB =&Task2TCB;
}
else
{
return; /* 线程延时均没有到期则返回,继续执行空闲线程 */
}
}
else
{
/*如果当前线程是线程1或者线程2的话,检查下另外一个线程,如果另外的线程不在延时中,就切换到该线程
否则,判断下当前线程是否应该进入延时状态,如果是的话,就切换到空闲线程。否则就不进行任何切换 */
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中断服务函数
如果一个任务需要延时,一开始xTicksToDelay肯定不为0,当xTicksToDelay变为0时,表示延时结束。
但是,存在一个问题,xTicksToDelay是以什么周期递减,在哪里递减的。
在FreeRTOS中,这个周期由SysTick中断提供,操作系统中最小的时间单位就是SysTick的中断周期,我们称之为一个tick。
SysTick中断服务函数在port.c中实现。
void xPortSysTickHandler( void )
{
/* 关中断 */
vPortRaiseBASEPRI();
/* 更新系统时基 */
xTaskIncrementTick();
/* 开中断 */
vPortClearBASEPRIFromISR();
}
更新系统时基
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();
}
SysTick初始化函数
SysTick的中断服务函数想要被顺利执行,则SysTick必须先初始化。SysTick的初始化函数vPortSetupTimerInterrupt()在port.c中定义
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 );
}
configSYSTICK_CLOCK_HZ 为系统时钟,port.c中被重定义
#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
其中,configCPU_CLOCK_HZ的原始定义在FreeRTOSconfig.h中定义
#define configCPU_CLOCK_HZ ( ( unsigned long ) 25000000 )
configTICK_RATE_HZ 为系统时基,在FreeRTOSconfig.h中定义
#define configTICK_RATE_HZ ( ( TickType_t ) 100 )
这里系统时钟频率为25Mhz,即1s有25000000Tick,那么1ms就有25000Tick。而每个周期中断100次,中断周期为25Mhz/100=250000Tick,因此中断一次为250000Tick/25000Tick=10ms.
可以参考《STM32F10xxx Cortex-M3 programming manual》手册,在port.c中定义为偏移:
#define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
实验
把软件延时替换时钟延时
#include "FreeRTOS.h"
#include "task.h"
extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );
int main(void)
{
/* 硬件初始化 */
/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
/* 初始化与任务相关的列表,如就绪列表 */
prvInitialiseTaskLists();
/* 创建任务 */
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
(char *)"Task1", /* 任务名称,字符串形式 */
(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task1Stack, /* 任务栈起始地址 */
(TCB_t *)&Task1TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
(char *)"Task2", /* 任务名称,字符串形式 */
(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
(void *) NULL, /* 任务形参 */
(StackType_t *)Task2Stack, /* 任务栈起始地址 */
(TCB_t *)&Task2TCB ); /* 任务控制块 */
/* 将任务添加到就绪列表 */
vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
/* 启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler();
for(;;)
{
/* 系统启动成功不会到达这里 */
}
}
/* 软件延时 */
void delay (uint32_t count)
{
for(; count!=0; count--);
}
/* 任务1 */
void Task1_Entry( void *p_arg )
{
for( ;; )
{
#if 0
flag1 = 1;
delay( 100 );
flag1 = 0;
delay( 100 );
/* 线程切换,这里是手动切换 */
portYIELD();
#else
flag1 = 1;
vTaskDelay( 2 ); //延时20ms
flag1 = 0;
vTaskDelay( 2 );
#endif
}
}
/* 任务2 */
void Task2_Entry( void *p_arg )
{
for( ;; )
{
#if 0
flag2 = 1;
delay( 100 );
flag2 = 0;
delay( 100 );
/* 线程切换,这里是手动切换 */
portYIELD();
#else
flag2 = 1;
vTaskDelay( 2 );
flag2 = 0;
vTaskDelay( 2 );
#endif
}
}
/* 获取空闲任务的内存 */
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;
}
仿真
宏观上看几乎是同时运行的
从微观上看
高低电平是几近20ms的,实验结果与代码相符。
学习于《FreeRTOS内核实现与应用开发实战指南–基于stm32》
b站视频地址:https://www.bilibili.com/video/BV1Jx411X7NS/