目录
FreeRTOS的内核函数
函数 | 功能 |
taskYIELD() | 进行任务切换 |
taskENTER_CRITICAL() | 进入临界段,关中断 |
taskEXIT_CRITICAL() | 退出临界段,开中断 |
taskENTER_CRITICAL_FROM_ISR() | 进入临界段中断版本 |
taskEXIT_CRITICAL_FROM_ISR() | 退出临界段中断版本 |
taskDISABLE_INTERRUPTS() | 关中断 |
taskENABLE_INTERRUPTS() | 开中断 |
vTaskStartScheduler() | 开启调度器 |
vTaskEndScheduler() | 关闭调度器 |
vTaskSuspendAll() | 挂起调度器 |
xTaskResumeAll() | 恢复调度器 |
vTaskStepTick() | 设置系统时钟节拍追加值 |
1,临界区操作函数
在程序运行过程中,一些关键代码的执行不能被打断,这一段不能被打断的程序被称为临界段或临界区。能打断程序执行的往往是中断,所以进入、退出临界段都要进行开、关中断操作。在STM32微控制器上,FreeRTOS进入和退出临界段主要是通过操作寄存器basepri实现的。在进入临界段时,操作寄存器basepri关闭所有低于宏configMAX_SYSCALL_INTERRUPT_PRIORITY所设定的中断优先级的中断,也就是FreeRTOS能管理的所有中断,不由FreeRTOS管理的中断不会被关闭。在退出临界段时,操作寄存器basepri打开所有中断。
进入和退出临界段函数有4个:taskENTER_CRITICAL()、taskEXIT_CRITICAL()、taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()。后两个函数在中断服务函数中使用。进入和退出临界段函数必须成对使用,而且要求临界段代码尽量短,以免破坏实时性。
1,任务函数中使用
进入和退出临界段函数其实是两个宏,最终实现进入临界段功能的是vPortEnterCritical()函数(不用自己写,port.c中有)。进入该函数后首先用portDISABLE_INTERRUPTS()操作寄存器basepri关闭FreeRTOS管理的所有中断,全局变量uxCriticalNesting加1,用来记录临界段的嵌套次数。
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
/* This is not the interrupt safe version of the enter critical function so
assert() if it is being called from an interrupt context. Only API
functions that end in "FromISR" can be used in an interrupt. Only assert if
the critical nesting count is 1 to protect against recursive calls if the
assert function also uses a critical section. */
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
与进入临界段类似,真正实现退出临界段功能的是vPortExitCritical()函数。该函数操作全局变量uxCriticalNesting减1,只有当uxCriticalNesting为0时,也就是所有临界段代码都退出后,才打开中断。这样保证了在有多个临界段时,不会因某个临界段代码执行完成退出而打乱其他临界段的保护。
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
2,中断函数中使用
taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR()是进入和退出临界段函数的中断版本,在中断服务函数中使用。这个中断必须是FreeRTOS能管理的中断,即中断优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY所设置的值的中断。
2,挂起和恢复调度器函数
vTaskSuspendAll()函数用于挂起调度器,xTaskResumeAll()函数用于恢复调度器。调度器挂起后,介于vTaskSuspendAll()和xTaskResumeAll()之间的代码不会被更高优先级的任务抢占,即任务调度被禁止。挂起调度器不用关闭中断,这一点与进入临界段不一样。挂起调度器的代码如下(使用时直接调用即可)。
void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
调度器挂起支持嵌套。在调度器挂起函数中,将挂起嵌套计数器uxSchedulerSuspended加1,这是一个静态全局变量,在使用调度器恢复函数时,此计数器会减1,当这个值减到0时真正恢复调度器。调用几次vTaskSuspendAll()函数,就要调用几次xTaskResumeAll()函数。
3,任务切换函数
taskYIELD()是任务切换函数,该函数其实是一个宏,最终由宏portYIELD()实现。通过向ICSR的bit28位写入1,启动一个PendSV中断,在PendSV中断中完成任务切换。
#define taskYIELD() portYIELD()
#define portYIELD()
{
/* 设置PendSV挂起位以切换任务 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
}
4 ,系统时钟节拍追加
vTaskStepTick()函数用于设置系统时钟节拍的追加值,在低功耗tickless模式下使用。当宏configUSE_TICKLESS_IDLE为1时,使能低功耗tickless模式。一般情况下,会在空闲任务中让系统时钟节拍停止运行,恢复系统时钟节拍后,系统时钟节拍停止运行的节拍数就可用vTaskStepTick()函数补上。
void vTaskStepTick( const TickType_t xTicksToJump )
{
configASSERT( ( xTickCount + xTicksToJump ) <= xNextTaskUnblockTime );
xTickCount += xTicksToJump;
traceINCREASE_TICK_COUNT( xTicksToJump );
}
vTaskStepTick()函数有一个参数xTicksToJump,为要追加的系统时钟节拍值。
5 ,内核函数使用示例
串口打印函数printf()在任一时刻只允许一个任务访问,当多个任务同时向串口输出字符时,将造成输出的混乱。在时间片调度示例中,是通过临界段代码保护函数taskENTER_CRITICAL()和taskEXIT_CRITICAL()来避免多个任务同时向串口输出字符的。除此之外,也可用挂起调度器的方式达到同样的目的。
任务描述:任务1的任务函数为Led0Task(),优先级为4,其功能是使LED0每秒闪烁1次,并将任务运行次数通过串口发送。
任务2的任务函数是Led1Task(),优先级为3,其功能是使LED1每秒闪烁2次,并将任务运行次数通过串口发送。
1,任务函数
/******************************************
- 函数名:Led0Task
- 功能说明:LED0每秒闪烁1次任务函数,统计闪烁次数并通过串口发送
- 形 参:pvParameters是在创建该任务时传递的参数
- 返回值:无
- 优先级:4
******************************************/
static void Led0Task(void *pvParameters)
{
uint16_t cnt=0; //用于统计闪烁次数的局部变量
while(1)
{
HAL_GPIO_TogglePin(GPIOB,LED0_Pin); //LED0闪烁
cnt++;
taskENTER_CRITICAL(); //进入临界段,关中断
printf("任务1: LED0闪烁,任务1已运行 %d 次\r\n",cnt);
taskEXIT_CRITICAL(); //退出临界段,开中断
vTaskDelay(pdMS_TO_TICKS(500)); //每秒闪烁1次
}
}
/******************************************
- 函数名:Led1Task
- 功能说明:LED1闪烁任务函数,统计闪烁次数并通过串口发送
- 形 参:pvParameters是在创建该任务时传递的参数
- 返回值:无
- 优先级:3
******************************************/
static void Led1Task(void *pvParameters)
{
uint16_t cnt=0; //用于统计闪烁次数的局部变
while(1)
{
HAL_GPIO_TogglePin(GPIOB,LED1_Pin); //LED1闪烁
cnt++;
vTaskSuspendAll(); //挂起调度器
printf("任务2: LED1闪烁,运行 %d 次\r\n",cnt);
if(xTaskResumeAll() == pdTRUE) //如果有任务需要切换,则函数返回pdTRUE
{
taskYIELD(); //进行一次任务切换
}
vTaskDelay(pdMS_TO_TICKS(250)); //每秒闪烁2次
}
}
2,任务创建
static TaskHandle_t Led0TaskHandle = NULL; // LED0任务句柄
static TaskHandle_t Led1TaskHandle = NULL; // LED1任务句柄
/******************************************
- 函数名:appStartTask
- 功能说明:创建任务函数,用于创建其他任务并开启调度器
- 形 参:无
- 返回值:无
******************************************/
void appStartTask(void)
{
taskENTER_CRITICAL(); // 进入临界段,关中断
xTaskCreate(Led0Task, // 任务函数
"Led0Task", // 任务名
128, // 任务堆栈大小,单位为word,也就是4B
NULL, // 任务参数
4, // 任务优先级
&Led0TaskHandle ); // 任务句柄
xTaskCreate(Led1Task, // 任务函数
"Led1Task", // 任务名
128, // 任务堆栈大小,单位为word,也就是4B
NULL, // 任务参数
3, // 任务优先级
&Led1TaskHandle ); // 任务句柄
taskEXIT_CRITICAL(); // 退出临界段,开中断
vTaskStartScheduler(); // 开启调度器
}