当前位置: 首页 > news >正文

FreeRTOS实战指南 — 5 多任务系统实现流程

目录

5.1 创建多任务系统

5.1.1 什么是任务栈

5.1.2 多任务系统实现

5.2 创建任务

5.2.1 申请任务堆栈空间

5.2.2 定义任务函数

5.2.3 定义任务控制块

5.2.4实现任务创建函数

5.3 实现就绪列表

5.3.1 定义就绪列表

5.3.2 就绪列表初始化

5.3.3 将任务插入到就绪列表

5.4 实现调度器

5.4.1 启动调度器

5.4.2 xPortStartScheduler()函数

5.4.3 prvStartFirstTask()函数

5.4.4 vPortSVCHandler()函数

5.4.5 任务切换


5.1 创建多任务系统

5.1.1 什么是任务栈

任务栈(Task Stack) 是每个任务必不可少的内存区域,其核心作用类似于通用计算机系统中的栈,主要负责存储函数调用信息、局部变量、上下文数据等。

在裸机系统中,通常使用一个单一的、全局的栈来存储中断服务例程和主程序的局部变量与返回地址。在这种模式下,系统资源和执行流程通常是线性管理的,没有复杂的任务调度和多任务并发的需求,因此不需要为每个任务维护独立的栈空间,但同时也限制了系统处理多任务的能力。

在多任务系统中,任务栈的定义是为了实现任务的独立性,每个任务都拥有自己的专属栈空间。这样做的目的是为了保证任务在被操作系统挂起或切换时能够保存其执行状态,确保任务可以在恢复执行时从中断点继续运行。这种设计提高了系统的灵活性和效率,允许更有效地管理多任务并发执行。

5.1.2 多任务系统实现

本章我们要实现两个变量按照一定的频率轮流的翻转,每个变量对应一个任务,那么就需要定义两个任务栈,在多任务系统中,有多少个任务就需要定义多少个任务栈。

  1. 初始化任务列表:调用prvInitialiseTaskLists()函数初始化任务就绪列表,这是调度器运行前的必要步骤。
  2. 创建任务:使用xTaskCreateStatic()函数创建任务(示例程序创建Task1和Task2)。这个函数需要指定任务的入口函数、任务名称、栈大小、任务参数、栈起始地址和任务控制块。
  3. 添加任务到就绪列表:使用vListInsertEnd()函数将新创建的任务添加到对应优先级的就绪列表中。
  4. 启动调度器:调用vTaskStartScheduler()函数启动调度器,将开始任务调度过程。调度器负责管理任务的执行顺序,确保按照优先级和时间分配原则,公平且有效地在多个任务之间进行CPU时间的分配和切换。
  5. 任务循环:每个任务进入一个无限循环,并使用taskYIELD()被用来手动触发任务切换,让出CPU给其他任务。

下面的示例程序展示了如何在FreeRTOS操作系统中创建和调度两个简单的任务,展示了任务创建、手动切换和调度器管理的基本流程。

#include "FreeRTOS.h"
#include "task.h"
#include "stm32f1xx_hal.h"  // 根据实际芯片添加,如 STM32F103// -------------------------------
// 全局标志变量(用于观察任务切换)
portBASE_TYPE flag1 = 0;  // 使用 portBASE_TYPE 替代 portCHAR(更标准)
portBASE_TYPE flag2 = 0;
// -------------------------------// -------------------------------
// 任务栈与控制块静态分配
#define TASK1_STACK_SIZE  128
#define TASK2_STACK_SIZE  128StackType_t Task1_Stack[TASK1_STACK_SIZE];
TCB_t       Task1_TCB;StackType_t Task2_Stack[TASK2_STACK_SIZE];
TCB_t       Task2_TCB;TaskHandle_t Task1_Handle;
TaskHandle_t Task2_Handle;
// -------------------------------// -------------------------------
// 延时函数(软件延时,仅用于演示)
void delay(uint32_t count)
{while (count--);
}
// -------------------------------// -------------------------------
// 任务1入口函数:设置 flag1 并翻转
void Task1_Entry(void *p_arg)
{(void)p_arg; // 避免未使用参数警告for (;;){flag1 = 1;delay(1000);  // 延时 1000 次(约几毫秒,取决于系统时钟)flag1 = 0;delay(1000);taskYIELD(); // 主动让出 CPU,触发调度器}
}
// -------------------------------// -------------------------------
// 任务2入口函数:设置 flag2 并翻转
void Task2_Entry(void *p_arg)
{(void)p_arg;for (;;){flag2 = 1;delay(1000);flag2 = 0;delay(1000);taskYIELD(); // 主动让出 CPU}
}
// -------------------------------// -------------------------------
// 主函数:初始化并启动 FreeRTOS
int main(void)
{// 初始化硬件(如 GPIO、时钟等)—— 根据你的平台添加// HAL_Init();// MX_GPIO_Init();// 1. 创建任务1(静态分配)Task1_Handle = xTaskCreateStatic(Task1_Entry,           // 任务入口函数"Task1",               // 任务名称(调试用)TASK1_STACK_SIZE,      // 栈大小(单位:字)NULL,                  // 传入参数(无)tskIDLE_PRIORITY + 1,  // 优先级(建议 > IDLE)Task1_Stack,           // 栈空间起始地址&Task1_TCB             // 任务控制块);// 2. 创建任务2(静态分配)Task2_Handle = xTaskCreateStatic(Task2_Entry,"Task2",TASK2_STACK_SIZE,NULL,tskIDLE_PRIORITY + 1,  // 与 Task1 同优先级,实现轮转Task2_Stack,&Task2_TCB);// ✅ 重要:xTaskCreateStatic() 已自动将任务加入就绪列表!// ❌ 不要手动操作 pxReadyTasksLists!这是内核私有结构!// 3. 启动调度器vTaskStartScheduler();// 此处永远不会执行到(除非调度器启动失败)for (;;);
}

程序首先初始化任务就绪列表,然后使用静态内存分配创建两个任务(Task1和Task2),每个任务在执行中通过设置标志位和延时来模拟工作,最后通过taskYIELD()手动触发任务切换。创建任务后,程序将它们添加到对应的就绪列表,然后启动调度器来开始多任务调度。主函数在调度器启动后进入无限循环,因为调度器一旦启动,通常不会返回。后续将进一步讲解程序中涉及的函数和概念。

5.2 创建任务

5.2.1 申请任务堆栈空间

上面的代码声明了Task1Stack和Task2Stack的数组,用来作为任务的堆栈空间类型为StackType_t,大小为128字,即512字节,也是FreeRTOS推荐的最小的任务栈。

// 任务栈大小定义
#define TASK1_STACK_SIZE    128
#define TASK2_STACK_SIZE    128// 任务栈空间分配
StackType_t Task1Stack[TASK1_STACK_SIZE];
StackType_t Task2Stack[TASK2_STACK_SIZE];

5.2.2 定义任务函数

下面的代码定义了两个简单的任务Task1_Entry和Task2_Entry,它们通过无限循环(for(;;))来模拟持续运行的任务,且不能返回。

/* 软件延时函数 */
void delay(uint32_t count)
{for(; count != 0; count--);
}/* 任务1入口函数 */
void Task1_Entry(void *p_arg)
{(void)p_arg;  // 未使用的参数,避免编译警告for(;;)  // 任务主循环{flag1 = 1;delay(100);flag1 = 0;delay(100);}
}/* 任务2入口函数 */
void Task2_Entry(void *p_arg)
{(void)p_arg;  // 未使用的参数,避免编译警告for(;;)  // 任务主循环{flag2 = 1;delay(100);flag2 = 0;delay(100);}
}

5.2.3 定义任务控制块

在裸机系统中,程序的主体是CPU按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。任务控制块(TCB)就是操作系统中用于管理和存储任务相关数据的数据结构,它包含了任务的栈顶指针、任务状态、任务名称等关键信息,是操作系统调度任务的核心组件。FreeRTOS中任务控制块的数据类型在task.c文件中声明,具体的声明见代码清单。

/* 任务控制块(TCB)结构体定义 */
typedef struct tskTaskControlBlock
{volatile StackType_t *pxTopOfStack;       /* 栈顶指针 */ListItem_t xStateListItem;                /* 任务状态列表项 */StackType_t *pxStack;                     /* 任务栈起始地址 */char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名称 */
} tskTCB;
typedef tskTCB TCB_t;

pxTopOfStack:指向StackType_t类型的指针,它存储了任务栈顶的地址,用于任务上下文切换时保存和恢复任务的状态。

xStateListItemListItem_t类型的成员,它将任务控制块作为列表项,用于将任务添加到就绪列表或其他任务管理列表中。

pxStackStackType_t类型的指针,它存储了任务栈的起始地址,即任务栈的底部。

pcTaskName字符数组,用于存储任务的名称。

5.2.4实现任务创建函数

FreeRTOS中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。

这个函数的作用是在系统初始化时或运行过程中创建新的任务,而不需要动态分配内存,这对于内存资源受限的系统非常有用。通过静态分配,系统可以预知所有任务的内存布局,有助于优化内存使用和提高系统的可靠性。

#if (configSUPPORT_STATIC_ALLOCATION == 1)// 定义一个函数,用于静态创建任务
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,const char *const pcName,const uint32_t ulStackDepth,void *const pvParameters,StackType_t *const puxStackBuffer,TCB_t *const pxTaskBuffer)
{TCB_t *pxNewTCB;                  // 指向新任务的TCB的指针TaskHandle_t xReturn;             // 用于返回任务句柄// 检查传入的栈缓冲区和任务控制块缓冲区是否为非NULLif ((pxTaskBuffer != NULL) && (puxStackBuffer != NULL)){// 将传入的任务控制块缓冲区转换为TCB指针pxNewTCB = (TCB_t *)pxTaskBuffer;// 设置任务的栈起始地址pxNewTCB->pxStack = (StackType_t *)puxStackBuffer;// 调用私有函数创建新的任务prvInitialiseNewTask(pxTaskCode,  /* 任务入口函数 */pcName,      /* 任务名称 */ulStackDepth,/* 任务栈的深度 */pvParameters,/* 传递给任务的参数 */&xReturn,    /* 用于返回任务句柄的变量 */pxNewTCB);   /* 新任务的TCB */}else{xReturn = NULL;                // 如果栈缓冲区或任务控制块缓冲区为NULL,则返回NULL}// 返回任务句柄,如果任务创建成功,xReturn应该指向新创建的任务的TCBreturn xReturn;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */

首先,检查是否开启了静态内存分配的支持(configSUPPORT_STATIC_ALLOCATION)。

验证传入的栈缓冲区(puxStackBuffer)和任务控制块缓冲区(pxTaskBuffer)是否有效。如果缓冲区有效,将这些缓冲区关联到新任务的TCB上。调用prvInitialiseNewTask函数初始化新任务,设置任务的入口函数、名称、栈大小、参数和返回的任务句柄。如果传入的缓冲区无效,函数返回NULL。如果任务成功创建,函数返回一个指向新任务TCB的任务句柄,否则返回NULL。

prvInitialiseNewTask函数的作用是为新创建的任务设置初始状态,包括栈的初始化、任务名称的存储、任务控制块(TCB)的设置等,以便任务可以被调度器调度执行。

static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,const char *const pcName,const uint32_t ulStackDepth,void *const pvParameters,TaskHandle_t *const pxCreatedTask,TCB_t *pxNewTCB )
{StackType_t *pxTopOfStack;UBaseType_t x;/* 获取栈顶地址 */pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );// 栈是从高地址向低地址增长的,所以栈顶是栈数组的最后一个元素。/* 向下做8字节对齐 */pxTopOfStack = ( StackType_t * ) \( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );/* 将任务的名字存储在TCB中 */for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ){pxNewTCB->pcTaskName[ x ] = pcName[ x ];if( pcName[ x ] == 0x00 ){break;}}/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';/* 初始化TCB中的xStateListItem节点 */vListInitialiseItem( &( pxNewTCB->xStateListItem ) );/* 设置xStateListItem节点的拥有者 */listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );// 设置列表项的所有者为当前的任务控制块,以便调度器可以识别任务。/* 初始化任务栈 */pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack,pxTaskCode,pvParameters );/* 让任务句柄指向任务控制块 */if( ( void * ) pxCreatedTask != NULL ){*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;}
}

pxPortInitialiseStack函数的作用是为新创建的任务设置初始栈帧,包括程序计数器(PC)、链接寄存器(LR)、参数、以及用于异常处理的寄存器值。

StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack,TaskFunction_t pxCode,void *pvParameters)
{pxTopOfStack--;                             // 栈顶指针向下移动,为xPSR寄存器值预留空间。*pxTopOfStack = portINITIAL_XPSR;           // 设置xPSR寄存器的初始值。pxTopOfStack--;                             // 栈顶指针向下移动,为程序计数器(PC)预留空间。*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;  // 设置程序计数器的初始值,即任务的入口函数地址。pxTopOfStack--;                             // 栈顶指针向下移动,为链接寄存器(LR)预留空间。*pxTopOfStack = (StackType_t)prvTaskExitError;  // 设置链接寄存器的初始值,即任务退出时调用的函数地址。pxTopOfStack -= 5;                          // 栈顶指针向下移动,为R12,R3,R2和R1寄存器预留空间,这些寄存器默认初始化为0。*pxTopOfStack = (StackType_t)pvParameters;  // 设置参数寄存器的值,即传递给任务的参数。pxTopOfStack -= 8;                          // 栈顶指针向下移动,为R0-R7寄存器预留空间,这些寄存器用于传递参数。return pxTopOfStack;                        // 返回调整后的栈顶指针,此时新任务的栈已经初始化完成。
}

5.3 实现就绪列表

5.3.1 定义就绪列表

任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。就绪列表在task.c中定义,具体见代码清单。

/*任务就绪列表*/
List_t pxReadyTasksLists[configMAX_PRIORITIES];

就绪列表实际上就是一个List_t类型的数组,数组的大小由决定最大任务优先级的宏configMAX_PRIORITIES决定,configMAX_PRIORITIES在FreeRTOSConfig.h中默认定义为5,最大支持256个优先级。数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。一个空的就绪列表具体见图。

5.3.2 就绪列表初始化

就绪列表在使用前需要先初始化,就绪列表初始化的工作在函数prvInitialiseTaskLists()里面实现,函数的主要作用是为每个可能的优先级创建一个空的任务就绪列表。在FreeRTOS中,每个任务根据其优先级被放置在不同的就绪列表中,以便操作系统能够根据优先级进行任务调度。具体见代码清单。

// 初始化任务就绪列表函数
void prvInitialiseTaskLists(void)
{// 定义一个无符号基数类型的变量,用于表示优先级UBaseType_t uxPriority;// 使用for循环遍历所有可能的优先级for (uxPriority = (UBaseType_t)0U;                // 从最低优先级(0)开始uxPriority < (UBaseType_t)configMAX_PRIORITIES;  // 直到最大优先级uxPriority++)                                // 优先级递增{// 对每个优先级的就绪列表进行初始化// pxReadyTasksLists是一个数组,每个元素代表一个优先级的就绪列表// uxPriority是当前正在初始化的优先级索引// vListInitialise是FreeRTOS提供的函数,用于初始化列表vListInitialise(&(pxReadyTasksLists[uxPriority]));}
}

5.3.3 将任务插入到就绪列表

任务控制块里面有一个xStateListItem成员,数据类型为ListItem_t,我们将任务插入到就绪列表里面,就是通过将任务控制块的xStateListItem这个节点插入到就绪列表中来实现的。如果把就绪列表比作是晾衣架,任务是衣服,那xStateListItem就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。

/* 初始化与任务相关的列表(如就绪列表) */
prvInitialiseTaskLists();// 静态创建任务1
Task1_Handle = xTaskCreateStatic((TaskFunction_t)Task1_Entry,    /* 任务入口函数 */(char*)"Task1",                 /* 任务名称(字符串形式) */(uint32_t)TASK1_STACK_SIZE,     /* 任务栈大小(单位:字) */(void*)NULL,                    /* 传递给任务的参数(无参数) */(StackType_t*)Task1Stack,       /* 任务栈起始地址 */(TCB_t*)&Task1TCB               /* 任务控制块(TCB)地址 */
);/* 将任务1添加到优先级为1的就绪列表 */
vListInsertEnd(&(pxReadyTasksLists[1]),&( ((TCB_t*)(&Task1TCB))->xStateListItem )
);// 静态创建任务2
Task2_Handle = xTaskCreateStatic((TaskFunction_t)Task2_Entry,    /* 任务入口函数 */(char*)"Task2",                 /* 任务名称(字符串形式) */(uint32_t)TASK2_STACK_SIZE,     /* 任务栈大小(单位:字) */(void*)NULL,                    /* 传递给任务的参数(无参数) */(StackType_t*)Task2Stack,       /* 任务栈起始地址 */(TCB_t*)&Task2TCB               /* 任务控制块(TCB)地址 */
);/* 将任务2添加到优先级为2的就绪列表 */
vListInsertEnd(&(pxReadyTasksLists[2]),&( ((TCB_t*)(&Task2TCB))->xStateListItem )
);

这段代码的作用是初始化任务相关的列表,创建两个静态任务(Task1和Task2),并将它们添加到就绪列表中。首先,调用prvInitialiseTaskLists()函数来初始化任务就绪列表。然后,使用xTaskCreateStatic函数创建两个任务Task1和Task2,指定它们的入口函数、名称、栈大小、传入参数、栈的起始地址和任务控制块(TCB)。最后,使用vListInsertEnd函数将这两个任务的控制块(TCB)插入到对应的优先级就绪列表的末尾,使它们成为可调度的任务。

5.4 实现调度器

FreeRTOS调度器负责管理任务的执行顺序和时间分配,调度器基于优先级和时间片轮转算法,确保高优先级的任务能够优先执行,同时通过时间片确保低优先级任务也能得到处理。它支持多任务处理,允许任务在后台运行,并且可以响应外部事件和中断。调度器还负责任务的创建、删除、挂起和唤醒,以及管理任务的就绪、阻塞和延迟等状态。

5.4.1 启动调度器

调度器的启动由vTaskStartScheduler()函数来完成,该函数在task.c中定义,具体实现见代码清单。

void vTaskStartScheduler(void)
{/*手动指定第一个运行的任务*/pxCurrentTCB=&Task1TCB;/*启动调度器*/if(xPortStartScheduler()!=pdFALSE){}
}

5.4.2 xPortStartScheduler()函数

xPortStartScheduler()函数的作用是初始化并启动FreeRTOS调度器。首先,通过定义宏来获取PendSV和SysTick中断的优先级寄存器地址,并设置这两个中断的优先级为最低,这是通过将配置的内核中断优先级左移相应的位数来实现的。然后,调用prvStartFirstTask()函数来启动第一个任务,这个函数通常负责设置初始的任务堆栈和执行第一个任务。一旦调度器启动,它将不再返回,因为调度器接管了CPU的控制权,开始执行任务调度。

#define portNVIC_SYSPRI2_REG    (*((volatile uint32_t *)0xe000ed20))
#define portNVIC_PENDSV_PRI     (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 16UL)
#define portNVIC_SYSTICK_PRI    (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 24UL)BaseType_t xPortStartScheduler( void )
{/* 配置PendSV和SysTick的中断优先级为最低 */portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;/* 启动第一个任务,不再返回 */prvStartFirstTask();/* 正常情况下不会执行到这里,返回0仅为满足函数声明 */return 0;
}

5.4.3 prvStartFirstTask()函数

prvStartFirstTask()函数通过汇编语言操作,配置处理器的堆栈指针并使能中断,最终触发SVC指令以启动FreeRTOS操作系统的第一个任务。

#define portNVIC_SYSPRI2_REG    (*((volatile uint32_t *)0xe000ed20))
#define portNVIC_PENDSV_PRI     (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 16UL)
#define portNVIC_SYSTICK_PRI    (((uint32_t)configKERNEL_INTERRUPT_PRIORITY) << 24UL)BaseType_t xPortStartScheduler( void )
{/* 配置PendSV和SysTick的中断优先级为最低 */portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;/* 启动第一个任务,不再返回 */prvStartFirstTask();/* 正常情况下不会执行到这里,返回0仅为满足函数声明 */return 0;
}

首先通过读取Cortex-M处理器的SCB_VTOR寄存器来获取向量表的起始地址,这个地址通常指向主堆栈指针(MSP)的初始值。然后,将这个地址加载到MSP寄存器中,从而设置主堆栈指针。接着使能全局中断,包括IRQ(普通中断)和FIQ(快速中断)。之后通过执行SVC(SupervisorCall)指令来触发一个异常,这将导致处理器跳转到SVC处理程序,从而开始执行第一个任务。最后,通过执行两个NOP(无操作)指令来确保在跳转之前所有指令都已正确完成。

5.4.4 vPortSVCHandler()函数

vPortSVCHandler()函数的作用是在SVC异常发生时,从当前任务的堆栈中恢复寄存器r4到r11的值,并将程序状态寄存器PSP设置为当前任务的堆栈指针。然后,它禁用所有中断,并设置返回地址以便返回到SVC调用之后的代码。这个过程是FreeRTOS进行任务切换的关键步骤之一,确保任务能够安全地从用户态切换到内核态。

__asm void vPortSVCHandler(void)
{extern pxCurrentTCB;;声明外部变量pxCurrentTCB,指向当前任务的控制块PRESERVE8;宏,用于保存寄存器ldr r3, =pxCurrentTCB;将当前任务控制块的地址加载到r3寄存器ldr r1, [r3];将当前任务控制块的内容加载到r1寄存器ldr r0, [r1];将任务控制块中的堆栈指针(PSP)的值加载到r0寄存器ldmia r0!, {r4-r11};从当前任务的堆栈中加载寄存器r4-r11的值msr psp, r0;将r0(堆栈指针)的值设置为程序状态寄存器PSP的值isb;指令同步屏障,确保之前的存储操作完成mov r0, #0;将0移动到r0寄存器msr basepri, r0;将r0(值为0)设置为基础优先级寄存器的值,禁用中断orr r14, #0xd;将0xd(SVC模式的返回地址)或到r14寄存器,设置返回地址bx r14;跳转到r14指向的地址,返回到SVC调用之后的代码
}

5.4.5 任务切换

这段代码的作用是在任务中主动放弃CPU,触发PendSV中断,从而实现任务的上下文切换。

#define taskYIELD() portYIELD()//用于访问NVIC中断控制状态寄存器(0xe000ed04)
#define portNVIC_INT_CTRL_REG (*((volatile uint32_t *)0xe000ed04))
//用于设置PendSV中断
#define portNVIC_PENDSVSET_BIT (1UL << 28UL)
//用于数据同步屏障的参数
#define portSY_FULL_READ_WRITE//用于实现任务切换
#define portYIELD() \
{ \//向NVIC中断控制状态寄存器写入PendSV中断设置位,触发PendSV中断 \portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \\//数据同步屏障,确保之前的写入操作完成 \__dsb(portSY_FULL_READ_WRITE); \\//指令同步屏障,确保之后的读取操作完成 \__isb(portSY_FULL_READ_WRITE); \
}

当调用taskYIELD()宏时,它会向NVIC中断控制状态寄存器写入PendSV中断设置位,这将导致PendSV中断被触发。PendSV中断处理程序会执行任务切换,选择一个新的任务来执行。在写入PendSV设置位后,使用数据同步屏障(DSB)和指令同步屏障(ISB)确保写入操作的完成和后续指令的正确执行。

PendSV中断服务函数是真正实现任务切换的地方,确保了任务能够安全、高效地在多任务环境中运行。

__asm void xPortPendSVHandler(void)
{extern pxCurrentTCB;extern vTaskSwitchContext;PRESERVE8mrs r0, psp                     // 将程序状态寄存器PSP的值读取到r0寄存器isb                             // 指令同步屏障,确保之前的存储操作完成ldr r3, =pxCurrentTCB           // 将当前任务控制块的地址加载到r3寄存器ldr r2, [r3]                    // 将当前任务控制块的内容加载到r2寄存器stmdb r0!, {r4-r11}             // 将r4-r11寄存器的值存储到r0指向的堆栈位置,并更新堆栈指针str r0, [r2]                    // 将更新后的堆栈指针存储到当前任务控制块中stmdb sp!, {r3, r14}            // 将r3和r14寄存器的值存储到主堆栈位置,并更新堆栈指针mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY  // 将配置的最大系统调用中断优先级加载到r0寄存器msr basepri, r0                 // 设置基础优先级寄存器,禁用中断dsb                             // 数据同步屏障,确保之前的存储操作完成isb                             // 指令同步屏障,确保之后的读取操作完成bl vTaskSwitchContext           // 调用vTaskSwitchContext函数,进行任务切换mov r0, #0                      // 将0加载到r0寄存器msr basepri, r0                 // 清除基础优先级寄存器,使能中断ldmia sp!, {r3, r14}            // 从主堆栈中恢复r3和r14寄存器的值ldr r1, [r3]                    // 将当前任务控制块的内容加载到r1寄存器ldr r0, [r1]                    // 将任务控制块中的堆栈指针加载到r0寄存器ldmia r0!, {r4-r11}             // 从任务的堆栈中恢复r4-r11寄存器的值,并更新堆栈指针msr psp, r0                     // 将恢复的堆栈指针设置为程序状态寄存器PSP的值isb                             // 指令同步屏障,确保之前的存储操作完成bx r14                          // 跳转到r14指向的地址,返回到任务代码nop                             // 无操作指令,用于占位
}

这段代码的作用是在PendSV中断发生时,保存当前任务的上下文(寄存器r4-r11和堆栈指针PSP),然后调用vTaskSwitchContext函数来选择下一个要执行的任务。之后,它恢复下一个任务的上下文,并使用bxr14指令跳转到该任务的入口点,从而完成任务切换。

voidvTaskSwitchContext(void)
{/*两个任务轮流切换*/if(pxCurrentTCB==&Task1TCB){pxCurrentTCB=&Task2TCB;}else{pxCurrentTCB=&Task1TCB;}
}

http://www.dtcms.com/a/393046.html

相关文章:

  • `css`使单词保持连贯的两种方法
  • 【Vue3 ✨】Vue3 入门之旅 · 第三篇:模板语法与数据绑定
  • 分类预测 | Matlab实现PCA-BP主成分分析结合BP神经网络多特征分类预测
  • 【Linux】进程优先级切换调度
  • Ubuntu24上安装Scrapy框架实战
  • 正向shell,反弹shell学习
  • 一维数组原地更新——力扣119.杨辉三角形II
  • Python语法学习-1
  • Linux基础命令大全
  • 9.21 快速选择
  • 【常见集合】HashMap
  • Docker安装小白教程(阿里yum)
  • MySQL表结构变更详解:ALTER TABLE ADD COLUMN语法、最佳实践与避坑指南
  • 【LeetCode - 每日1题】设计电子表格
  • Spring 中 REQUIRED 事务的回滚机制详解
  • C++框架中基类修改导致兼容性问题的深度分析与总结
  • 学习笔记-SpringBoot项目配置
  • Java数据结构——时间和空间复杂度
  • 如何在接手新项目时快速上手?
  • Zynq开发实践(SDK之自定义IP2)
  • 数据库相关锻炼
  • PostgreSQL 入门与实践
  • pytorch基本运算-PyTorch.Tensor张量数据类型
  • 数据结构与算法 第三章 栈
  • Spring Boot 整合 MyBatis:从入门到企业级实践
  • FHook Java 层全函数 HOOK 框架
  • TDengine 聚合函数 STDDEV_POP 用户手册
  • 【 嵌入式Linux应用开发项目 | Rockit + FFmpeg+ Nginx】基于泰山派的IPC网络摄像头
  • 机器学习中的高准确、低召回
  • Go基础:Go基本数据类型详解