FreeRTOS简单内核实现3 任务管理

news2024/10/6 12:27:51

文章目录

  • 0、思考与回答
    • 0.1、思考一
    • 0.2、思考二
    • 0.3、思考三
  • 1、任务控制块
  • 2、创建任务
    • 2.1、xTaskCreateStatic( )
    • 2.2、prvInitialiseNewTask( )
    • 2.3、pxPortInitialiseStack( )
    • 2.4、任务内存详解
  • 3、就绪链表
    • 3.1、定义
    • 3.2、prvInitialiseTaskLists( )
  • 4、任务调度器
    • 4.1、vTaskStartScheduler( )
    • 4.2、xPortStartScheduler( )
    • 4.3、prvStartFirstTask( )
    • 4.4、vPortSVCHandler( )
    • 4.5、xPortPendSVHandler( )
    • 4.6、vTaskSwitchContext( )
    • 4.7、taskYIELD( )
  • 5、两个重要问题
    • 5.1、启动第一个任务流程
      • 5.1.1、问题 1 解答
      • 5.1.2、问题 2 解答
    • 5.2、任务调度原理
      • 5.2.1、问题 1 解答
      • 5.2.2、问题 2 解答
  • 6、实验
    • 6.1、测试
    • 6.2、待改进


0、思考与回答

0.1、思考一

对于 Cortex-M4 内核的 MCU 在发生异常/中断时,哪些寄存器会自动入栈,哪些需要手动入栈?

会自动入栈的寄存器如下

  1. R0 - R3:通用寄存器
  2. R12:通用寄存器
  3. LR (Link Register):链接寄存器,保存返回地址
  4. PC (Program Counter):程序计数器,保存当前执行指令的地址
  5. xPSR (Program Status Register):程序状态寄存器

需要手动入栈的寄存器如下

  1. R4 - R11:其他通用寄存器

0.2、思考二

这些入栈的寄存器是怎么找到要入栈的地址的呢?

依靠堆栈指针来确定入栈的地址,Cortex-M4 的堆栈指针寄存器 SP 在同一物理位置上有 MSP 和 PSP 两个堆栈指针,默认情况下会使用主堆栈指针 MSP

0.3、思考三

自动入栈的寄存器什么时候入栈,什么时候出栈恢复原来的处理器状态?

进入异常处理时会发生自动入栈,当异常处理完成并执行异常返回指令(bx r14)时,处理器会自动从堆栈中弹出这些寄存器的值

1、任务控制块

通常使用名为任务控制块(TCB)的结构体来对每个任务进行管理,该结构体中包含了任务管理所需要的一些重要成员,其中栈顶指针 pxTopOfStack 作为结构体的第一个成员,其地址也即任务控制块的地址,具体如下所示

/* task.h */
// 任务控制块
typedef struct tskTaskControlBlock
{
    volatile StackType_t  *pxTopOfStack;                        // 栈顶
    ListItem_t            xStateListItem;                       // 任务节点
    StackType_t           *pxStack;                             // 任务栈起始地址
    char                  pcTaskName[configMAX_TASK_NAME_LEN];  // 任务名称
}tskTCB;
typedef tskTCB TCB_t;

configMAX_TASK_NAME_LEN 是一个宏,用于设置任务名称长度,具体定义如下

/* FreeRTOSConfig.h */
// 任务名称字符串长度
#define configMAX_TASK_NAME_LEN                 24

2、创建任务

2.1、xTaskCreateStatic( )

静态创建任务时需要指定任务栈(地址和大小)、任务函数、任务控制块等参数,该函数就是负责将这些分散的变量联系在一起,方便后续对任务进行管理,但是其并不是真正的创建任务的函数,具体如下所示

/* task.c */
// 静态创建任务函数
#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 puxTaskBuffer, // 任务栈起始指针
                            TCB_t* const pxTaskBuffer)        // 任务栈控制指针
{
	TCB_t* pxNewTCB;
	TaskHandle_t xReturn;
	// 任务栈控制指针和任务栈起始指针不为空
	if((pxTaskBuffer != NULL) && (puxTaskBuffer != NULL))
	{
		pxNewTCB = (TCB_t*)pxTaskBuffer;
		// 将任务控制块的 pxStack 指针指向任务栈起始地址
		pxNewTCB->pxStack = (StackType_t*)puxTaskBuffer;
		
		// 真正的创建任务函数
		prvInitialiseNewTask(pxTaskCode,
							 pcName,
							 ulStackDepth,
							 pvParameters,
							 &xReturn,
							 pxNewTCB);
	}
	else
	{
		xReturn = NULL;
	}
	// 任务创建成功后应该返回任务句柄,否则返回 NULL
	return xReturn;
}
#endif

/* task.h */
// 函数声明
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,
                            const char* const pcName,
                            const uint32_t ulStackDepth,
                            void* const pvParameters,
                            StackType_t* const puxTaskBuffer,
                            TCB_t* const pxTaskBuffer);

configSUPPORT_STATIC_ALLOCATION 是一个宏,用于配置和裁剪 RTOS 静态创建任务的功能,具体定义如下所示

/* FreeRTOSConfig.h */
// 是否支持静态方式创建任务
#define configSUPPORT_STATIC_ALLOCATION         1

TaskHandle_t 是一个 void * 类型的变量,用于表示任务句柄,TaskFunction_t 是一个函数指针,用于表示任务函数,具体定义如下所示

/* task.h */
// 任务句柄指针
typedef void* TaskHandle_t;
// 任务函数指针
typedef void (*TaskFunction_t)(void *);

2.2、prvInitialiseNewTask( )

函数 xTaskCreateStatic() 最终调用了真正的创建任务函数 prvInitialiseNewTask() ,该函数主要是对任务栈内存、任务控制块成员等进行初始化,具体如下所示

/* task.c */
// 使用的外部函数声明
extern StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, 
                                          TaskFunction_t pxCode, 
                                          void* pvParameters);
// 真正的创建任务函数																 
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;
	}
	pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN-1] = '\0';
	
	// 初始化链表项
	vListInitialiseItem(&(pxNewTCB->xStateListItem));
	
	// 设置该链表项的拥有者为 pxNewTCB
	listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
	
	// 初始化任务栈
	pxNewTCB->pxTopOfStack = 
	          pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
	
	if((void*)pxCreatedTask != NULL)
	{
	    *pxCreatedTask = (TaskHandle_t)pxNewTCB;
	}
}

2.3、pxPortInitialiseStack( )

初始化任务栈函数,内存具体被初始化为什么样可以阅读 “2.4 任务内存详解” 小节

/* port.c */
// 错误出口
static void prvTaskExitError(void)
{
    for(;;);
}
// 初始化栈内存
StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, 
								   TaskFunction_t pxCode, 
								   void* pvParameters)
{
    // 异常发生时,自动加载到CPU的内容
    pxTopOfStack --;
    *pxTopOfStack = portINITIAL_XPSR;
    pxTopOfStack --;
    *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK;
    pxTopOfStack --;
    *pxTopOfStack = (StackType_t)prvTaskExitError;
	
    // r12、r3、r2 和 r1 默认初始化为 0
    pxTopOfStack -= 5;
    *pxTopOfStack = (StackType_t)pvParameters;
	
    // 异常发生时,手动加载到 CPU 的内容
    pxTopOfStack -= 8;
	
    // 返回栈顶指针,此时 pxTopOfStack 指向空闲栈
    return pxTopOfStack;
}
/* portMacro.h */
#define portINITIAL_XPSR            (0x01000000)
#define portSTART_ADDRESS_MASK      ((StackType_t) 0xFFFFFFFEUL)

2.4、任务内存详解

使用静态方式创建任务之前需要提前定义好任务句柄、任务栈空间、任务控制块和任务函数,具体如下程序所示

// 任务句柄
TaskHandle_t Task_Handle;
// 任务栈大小
#define TASK_STACK_SIZE                    128
// 任务栈空间
StackType_t TaskStack[TASK_STACK_SIZE];
// 任务控制块
TCB_t TaskTCB;
// 任务函数
void TaskFunction(void *parg)
{
	for(;;)
	{
	}
}

定义好这些变量之后都会在 RAM 中占用一定的空间,其中任务控制块 TaskTCB 的地址和结构体第一个成员 pxTopOfStack 是同一个地址,假设这些变量在空间中的占用情况如下图所示

![[Kernel_3.1.png]]

将这些定义好的变量作为参数传递给静态创建任务函数 xTaskCreateStatic() ,具体如下程序所示

// 静态方式创建任务
Task_Handle = xTaskCreateStatic(TaskFunction,
							   "Task",
							   128,
							   TaskParameters,
							   TaskStack,
							   TaskTCB);

该函数执行完毕之后,最终这些变量在空间中的占用情况如下图所示

![[Kernel_3.2.png]]
为什么要按照顺序这么存放?

因为在 ARM Cortex-M 处理器中,当异常发生时硬件会自动按照上图的顺序将寄存器推入当前的栈中,同时退出异常时也会按照其相反的顺序从当前栈中取值加载到 MCU 寄存器中

为什么任务栈顶 PSR 的值为 0x01000000 ?

PSR 是程序状态寄存器,对于异常来说具体的寄存器为 EPSR ,其第 24 位为 Thumb 状态位(T位),在 ARM Cortex-M 架构中,所有代码都必须在 Thumb 模式下执行,寄存器定义可以在 Cortex-M4 Devices Generic User Guide 手册中找到,具体如下图所示

![[Kernel_3.2_add.png]]

可以发现当执行 BX 指令退出异常时(后面启动任务 / 任务调度会频繁使用到异常退出指令),会将 T 位清零,而一旦清零并尝试执行指令就会导致 MCU 故障或锁定,因此必须在异常退出自动恢复 MCU 寄存器时将 T 位重新置 1

为什么任务栈存放任务函数入口地址的内容为 pxCode & portSTART_ADDRESS_MASK?

简单来说这步操作其实是为了内存对齐portSTART_ADDRESS_MASK 是一个宏,pxCode & portSTART_ADDRESS_MASK 只是为了确保 pxCode 任务函数入口地址最低位置为 0

3、就绪链表

3.1、定义

为什么要定义链表?

使用链表可以将各个独立的任务链接起来,对于多任务时会方便任务管理

/* task.c */
// 就绪链表
List_t pxReadyTasksLists;

3.2、prvInitialiseTaskLists( )

链表创建后不能直接使用,需要调用 vListInitialise() 函数对链表进行初始化,由于后续会创建多个链表,因此将链表初始化操作包装为一个函数,具体如下所示

/* task.c */
// 就绪列表初始化函数
void prvInitialiseTaskLists(void)
{
	vListInitialise(&pxReadyTasksLists);
}

/* task.h */
// 函数声明
void prvInitialiseTaskLists(void);

4、任务调度器

任务调度流程如下所示

  1. 启动调度器
    1. vTaskStartScheduler( )
    2. xPortStartScheduler( )
  2. 启动第一个任务
    1. prvStartFirstTask( )
    2. vPortSVCHandler( )
  3. 产生任务调度
    1. taskYIELD( )
    2. xPortPendSVHandler( )
    3. vTaskSwitchContext( )

4.1、vTaskStartScheduler( )

任务创建好了怎么执行呢?

这就是启动调度器函数的工作,选择一个任务然后启动第一个任务的执行

怎么选择任务?

通过一个名为 pxCurrentTCBTCB_t * 类型的指针选择要执行的任务,该指针始终指向需要运行的任务的任务控制块

选择哪一个?

在创建多个任务时,可以通过一些方法(比如根据任务优先级)来选择先执行的任务,但是这里我们的 RTOS 内核还不支持优先级,因此可以先手动指定要执行的第一个任务

/* task.c */
// 当前 TCB 指针
TCB_t volatile *pxCurrentTCB = NULL;
// 使用的外部函数声明
extern BaseType_t xPortStartScheduler(void);
// 在 main.c 中定义的两个任务声明
extern TCB_t Task1TCB;
extern TCB_t Task2TCB;

// 启动任务调度器
void vTaskStartScheduler(void)
{
	// 手动指定第一个运行的任务
	pxCurrentTCB = &Task1TCB;
	// 启动调度器
	if(xPortStartScheduler() != pdFALSE)
	{
		// 调度器启动成功则不会到这里
	}
}

/* task.h */
// 函数声明
void vTaskStartScheduler(void);

4.2、xPortStartScheduler( )

vTaskStartScheduler() 函数选择了要执行的任务,启动第一个任务执行的重任交给了 4.2 ~ 4.4 小节的三个函数,xPortStartScheduler() 函数主要设置 PendSV 和 SysTick 的中断优先级为最低,然后调用了 prvStartFirstTask() 函数

/* port.c */
// 启动调度器
BaseType_t xPortStartScheduler(void)
{
	// 设置 PendSV 和 SysTick 中断优先级为最低
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
	
	// 初始化滴答定时器
	
	// 启动第一个任务,不再返回
	prvStartFirstTask();
	
	// 正常不会运行到这里
	return 0;
}

为什么要设置 PendSV 和 SysTick 中断优先级最低?

在 RTOS 中,PendSV 和 SysTick 中断服务函数均与任务调度有关,而任务调度优先级需要小于外部硬件中断的优先级,所以要将其设置为最低优先级

怎么设置 PendSV 和 SysTick 中断优先级为最低?

这里是直接操作寄存器的方法来设置 Cortex-M4 的 PendSV 和 SysTick 中断优先级的,可以在 Cortex-M4 Devices Generic User Guide 手册中找到有关 SHPR3 寄存器的地址与其每一位的含义,具体如下图所示

![[Kernel_3.3.png]]

根据上图应该很好理解程序是怎么将 PendSV 和 SysTick 优先级设置为 15 的(NVIC 为 4 位抢占优先级,最低优先级就是 15)

/* portMacro.h */
#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)
/* FreeRTOSConfig.h */
// 设置内核中断优先级(最低优先级)
#define configKERNEL_INTERRUPT_PRIORITY         15

4.3、prvStartFirstTask( )

prvStartFirstTask 是使用汇编语言编写的函数,我们要实现的 RTOS 内核中一共有三个函数是由汇编语言编写的,除了这个外还有 4.4 和 4.5 两个小节的函数,该函数实际上仍然没有真正的启动第一个任务,实质上是触发了 SVC 中断

Keil 版本

/* port.c */
// 启动第一个任务,实际上是触发 SVC 中断
__asm void prvStartFirstTask(void)
{
	// 8 字节对齐
    PRESERVE8
	
	// 在 cortex-M 中,0xE000ED08 是 SCB_VTOR 这个寄存器的地址,
	// 里面放的是向量表的起始地址 ,其第一项为 __initial_sp,也即 msp 的初始地址 
    ldr r0,=0xE000ED08
    ldr r0,[r0]
    ldr r0,[r0]
	// 设置主栈指针 msp 的值
    msr msp,r0
    // 启用全局中断
    cpsie i
    cpsie f
    dsb
    isb
    // 调用 SVC 启动第一个任务
    svc 0
    nop
    nop
}

// 函数声明
void prvStartFirstTask(void);

CLion 版本

void prvStartFirstTask(void)  
{  
    __asm volatile  
    (
		".align 8             \n"  
		"ldr r0,=0xE000ED08   \n"  
		"ldr r0,[r0]          \n"  
		"ldr r0,[r0]          \n"  
		"msr msp,r0           \n"  
		"cpsie i              \n"  
		"cpsie f              \n"  
		"dsb                  \n"  
		"isb                  \n"  
		"svc 0                \n"  
		"nop                  \n"  
		"nop                  \n"  
    );
}

4.4、vPortSVCHandler( )

SVC 中断服务函数,真正执行第一个任务的函数

Keil 版本

/* port.c */
// SVC 中断服务函数
__asm void vPortSVCHandler(void)
{
    extern pxCurrentTCB;
	// 8 字节对齐
    PRESERVE8
    // 将 pxCurrentTCB 指针的地址加载到寄存器 r3
    ldr r3,=pxCurrentTCB
    // 将 pxCurrentTCB 指针指向的地址(存储 TCB 的地址)加载到寄存器 r1
    ldr r1,[r3]
    // 将 pxCurrentTCB 指针指向的地址里的值(当前运行的 TCB 结构体)加载到寄存器 r0
    // 也即寄存器 r0 中存储了当前运行的任务的栈顶指针地址
    ldr r0,[r1]
    // 以寄存器 r0 为基地址,将任务栈中存储的值加载到寄存器 r4 ~ r11 中
    // 同时寄存器 r0 的值会自动更新
    ldmia r0!,{r4-r11}
    // 将更新后的寄存器 r0 的值加载给 psp
    msr psp,r0
    isb
    // 开中断
    mov r0,#0
    msr basepri,r0
    // 指示处理器在异常返回时使用 psp 作为堆栈指针
    orr r14,#0xd
	// 异常返回,自动加载剩下的 xPSR、PC、R14、R12 和 R3 ~ R0 寄存器
    bx r14
}

CLion 版本

void vPortSVCHandler(void)  
{  
    extern uint32_t pxCurrentTCB;  
  
    __asm volatile  
    (  
		".align 8                    \n"  
		"ldr r3, =pxCurrentTCB       \n"
		"ldr r1, [r3]                \n"
		"ldr r0, [r1]                \n"
		"ldmia r0!, {r4-r11}         \n"
		"msr psp, r0                 \n"
		"isb                         \n"
		"mov r0, #0                  \n"
		"msr basepri, r0             \n"
		"orr lr, lr, #0x0d           \n"     // 笔者未验证,可能有误,请注意分辨
		"bx lr                       \n"
		:
		: 
		: "r0", "r1", "r3", "memory"
    );  
}

熟悉 STM32 的读者应该了解,NVIC 中所有中断服务函数都有一个固定的函数名,该函数名可以在 startup_stm32f407xx.s 文件中找到,这里 vPortSVCHandler 其实是 SVC_Handler 的别名,具体宏定义如下所示

/* portMacro.h */
#define vPortSVCHandler             SVC_Handler

注意:由于我们重新实现了 SVC 中断服务函数,因此在 stm32f4xx_it.c 中自动生成的 SVC_Handler 函数需要注释或者直接删除

4.5、xPortPendSVHandler( )

PendSV 中断服务函数,实现任务切换的函数

Keil 版本

/* port.c */
// PendSV 中断服务函数,真正实现任务切换的函数
__asm void xPortPendSVHandler(void)
{
	// 只要触发 PendSV 中断,寄存器 xPSR、PC、R14、R12 和 R3 ~ R0,
	// 这 8 个寄存器的值会自动从 psp 指针指向的地址开始加载到任务栈中
	// 并且 psp 指针也会自动更新
    extern pxCurrentTCB;
    extern vTaskSwitchContext;
	
    PRESERVE8
	// 将 psp 的值存储到寄存器 r0 中 
    mrs r0,psp
    isb
	// 将 pxCurrentTCB 的地址存储到寄存器 r3 中
    ldr r3,=pxCurrentTCB
    // 将寄存器 r3 存储的地址里的内容存储到寄存器 r2 中
    // 此时寄存器 r2 中存储了当前执行任务的 TCB 地址
    ldr r2,[r3]
	// 以寄存器 r0 为基地址(r0 中现在存储了 psp 指针的值)
	// 将寄存器 r11 ~ r4 中存储的值存储到任务栈
	// 并且 r0 中存储的地址指针也会自动更新
    stmdb r0!,{r4-r11}
    // 将寄存器 r0 中的内容存储到上一个执行任务的 TCB 地址
    // 也即上一个任务的栈顶指针 pxTopOfStack 指向寄存器 r0 现在存储的地址
    str r0,[r2]
	// 上文保存完成
	// 开始加载下文
	// 将寄存器 r3 和 r14 压入栈
    stmdb sp!,{r3,r14}
    // 屏蔽 优先级值 高于或者等于 11 的中断
    mov r0,#configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri,r0
    dsb
    isb
    // 跳转到 vTaskSwitchContext 函数,得到下一个要执行任务的指针 pxCurrentTCB
    bl vTaskSwitchContext
    // 开中断
    mov r0,#0
    msr basepri,r0
    // 恢复寄存器 r3 和 r14
    ldmia sp!,{r3,r14}
	
	// 将 pxCurrentTCB 加载到寄存器 r1 
    ldr r1,[r3]
    // 将下一个要执行任务的栈顶指针加载到寄存器 r0 
    ldr r0,[r1]
    // 以寄存器 r0 为基地址(r0 中现在存储了任务的栈顶指针)
    // 将任务栈中存储的值加载到寄存器 r4 ~ r11 中
    // 同时 r0 中的值会自动更新
    ldmia r0!,{r4-r11}
    // 将更新后的 r0 的值加载到 psp
    msr psp,r0
    isb
    // 异常退出,自动加载剩下的 xPSR、PC、R14、R12 和 R3 ~ R0 寄存器
    bx r14
    nop
}

CLion 版本

void xPortPendSVHandler(void)  
{  
    extern uint32_t pxCurrentTCB;  
    extern void vTaskSwitchContext(void);  
  
    uint32_t temp;  
  
    __asm volatile  
    (  
		".align 8                     \n"  
		"mrs r0, psp                  \n"  
		"isb                          \n"  
		"ldr r3, =pxCurrentTCB        \n"  
		"ldr r2, [r3]                 \n"  
		"stmdb r0!, {r4-r11}          \n"  
		"str r0, [r2]                 \n"  
		"stmdb sp!, {r3, lr}          \n"  
		"mov r0, %[max_syscall_prio]  \n"  
		"msr basepri, r0              \n"  
		"dsb                          \n"  
		"isb                          \n"  
		"bl vTaskSwitchContext        \n"  
		"mov r0, #0                   \n"  
		"msr basepri, r0              \n"  
		"ldmia sp!, {r3, lr}          \n"  
		"ldr r1, [r3]                 \n"  
		"ldr r0, [r1]                 \n"  
		"ldmia r0!, {r4-r11}          \n"  
		"msr psp, r0                  \n"  
		"isb                          \n"  
		"bx lr                        \n"  
		"nop                          \n"  
		:  
		: [max_syscall_prio] "i" (configMAX_SYSCALL_INTERRUPT_PRIORITY)  
		: "r0", "r1", "r2", "r3", "memory"  
    );  
}

xPortPendSVHandlerPendSV_Handler 的别名,用于表示 PendSV 中断服务函数入口地址

/* portMacro.h */
#define xPortPendSVHandler          PendSV_Handler

关于如何屏蔽中断的请读者阅读后续 FreeRTOS简单内核实现4 临界段 文章中 ”思考三“ ,FreeRTOS 的中断管理策略是将所有的抢占优先级分为两组,一组可以通过 RTOS 的临界区进行屏蔽,另外一组不受 RTOS 的影响,这两组的边界则由 configMAX_SYSCALL_INTERRUPT_PRIORITY 宏确定,如下所示

/* FreeRTOSConfig.h */
// 设置 RTOS 可管理的最大中断优先级
#define configMAX_SYSCALL_INTERRUPT_PRIORITY    11<<4

注意:由于我们重新实现了 PendSV 中断服务函数,因此在 stm32f4xx_it.c 中自动生成的 PendSV_Handler 函数需要注释或者直接删除

4.6、vTaskSwitchContext( )

任务调度函数中通常应该使用某一调度策略来选择下一个要执行的任务(比如始终保证最高优先级的任务得到执行),但这里我们只简单的进行两个任务的交替,后面会对该函数完善,具体如下所示

/* task.c */
// 任务调度函数
void vTaskSwitchContext(void)
{
	// 两个任务轮流切换
	if(pxCurrentTCB == &Task1TCB)
	{
		pxCurrentTCB = &Task2TCB;
	}
	else
	{
		pxCurrentTCB = &Task1TCB;
	}
}

/* task.h */
// 函数声明
void vTaskSwitchContext(void);

4.7、taskYIELD( )

taskYIELD() 是主动产生任务调度的宏函数,其实质上是触发了 PendSV 中断,然后在 PendSV 中断服务函数中产生了任务调度,具体如下所示

/* task.h */
// 主动产生任务调度
#define taskYIELD() portYIELD()
/* portMacro.h */
#define portNVIC_INT_CTRL_REG       (*((volatile uint32_t*)0xE000ED04))
#define portNVIC_PENDSVSET_BIT      (1UL << 28UL)
// 触发 PendSV,产生上下文切换
#define portYIELD() \
{ \
    portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
    __DSB(); \
    __ISB(); \
};

同样这里是直接操作 Cortex-M4 内核 ICSR 寄存器的 PENDSVSET 位置 1 来产生 PendSV 中断的,关于该寄存器的相关内容仍然可以在 Cortex-M4 Devices Generic User Guide 手册中找到,具体的原理如下图所示

![[Kernel_3.4.png]]

5、两个重要问题

5.1、启动第一个任务流程

通过上面的学习读者应初步了解到启动第一个任务的函数调用流程如下

  1. vTaskStartScheduler()
  2. -> xPortStartScheduler()
  3. -> prvStartFirstTask()
  4. -> vPortSVCHandler()

已知 prvStartFirstTask() 函数实质是触发了 SVC 中断,然后在 vPortSVCHandler() 这个 SVC 中断服务函数中启动了第一个任务,于是有如下问题

问题 1 :为什么在触发 SVC 中断前进行了设置主堆栈指针 msp 值的操作?

问题 2 :vPortSVCHandler() 函数究竟是怎么把第一个任务加载到 MCU 的寄存器中运行的?

5.1.1、问题 1 解答

这个问题的答案很简单,就是因为之后的程序将不再使用主堆栈指针 msp,因此将其初始化为默认状态,设置主栈指针的程序如下所示

ldr r0,=0xE000ED08
ldr r0,[r0]
ldr r0,[r0]
msr msp,r0

0xE000ED08 是 Cortex-M4 内核 VTOR (中断向量偏移寄存器)的地址,这个寄存器是不是听起来很熟悉?

读者可以在 STM32 工程的启动文件中找到其中断向量偏移表,可以发现中断向量偏移表中第一项为 __initial_sp ,也即主堆栈指针 msp 的初始值,以 startup_stm32f407xx.s 为例,具体如下图所示

![[Kernel_3.5.png]]

5.1.2、问题 2 解答

当一个任务创建好之后,应该满足 &TaskTCB == &TaskTCB.pxTopOfStack ,并且他们都指向了任务栈中用于存放寄存器 r4 内存的地址,具体可以阅读 “2.4、任务内存详解” 小节内容

同时由于我们手动指定了 pxCurrentTCB 指针指向任务控制块,因此经过下面几句程序后,寄存器 r0 中值实际上已经是任务栈中用于存放寄存器 r4 内存的地址

ldr r3,=pxCurrentTCB
ldr r1,[r3]
ldr r0,[r1]

然后经过下面的程序,此时保存在任务栈中的 r4 ~ r11 的值就会加载到 MCU 的寄存器中,同时 r0 中的值也会自动更新,最终指向任务栈中用于存放寄存器 r0 内存的地址

ldmia r0!,{r4-r11}

接着将 r0 的值加载给 psp 指针,这时候 psp 指针指向了任务栈中用于存放寄存器 r0 内存的地址

msr psp,r0
isb

下一步在打开中断后执行了很重要的一步程序

orr r14,#0xd

执行完这条语句后,SVC 中断服务函数返回时就不再使用 msp 作为堆栈指针了,而是切换到了 psp 堆栈指针,这样 MCU 会从 psp 堆栈指针处将可以自动加载的寄存器进行加载,同时 psp 指针也会更新,最终 psp 指针会指向任务栈的栈顶

值得提醒的是由于任务创建时已经提前在任务栈用于存放寄存器 PC 内存处存储了任务函数的入口地址,因此第一个任务函数便会开始运行

5.2、任务调度原理

已知 RTOS 中发生任务调度其实质为触发 PendSV 中断,而 xPortPendSVHandler() 是 PendSV 中断服务函数,因此任务调度时上文的保存和下文的加载都是在该函数中发生的,于是有如下问题

问题 1 :上文是如何保存的?

问题 2 :下文是如何加载到 MCU 寄存器中的?

5.2.1、问题 1 解答

首先需要知道的是启动第一个任务后 psp 堆栈指针在哪里?它现在正指向任务栈的栈顶

其次需要知道当发生任务调度产生 PendSV 中断时,MCU 会从 psp 堆栈指针处自动保存那些可以自动保存的寄存器,并且 psp 也会跟着更新

于是当进入 PendSV 中断服务函数时,psp 指针已经指向了任务栈中用于存放寄存器 r0 内存的地址,然后将 psp 指针的值加载到 r0 寄存器中,并执行以下程序从 MCU 保存 r4 ~ r11 寄存器的值到任务栈对应位置,同时 r0 中的值也会更新,执行完之后 r0 此时应该指向任务栈中用于存放寄存器 r4 内存的地址

mrs r0,psp
isb
stmdb r0!,{r4-r11}

如下所示的几条程序则是将当前任务的任务控制块指向了 r0 中的值描述的地址处,也即当前任务的任务控制块指向了任务栈中用于存放寄存器 r4 内存的地址,可以发现和刚创建完成任务时一样,这样当前任务的上文就保存好了

ldr r3,=pxCurrentTCB
ldr r2,[r3]
str r0,[r2]

5.2.2、问题 2 解答

在保存好上文后加载下文前,中间的程序是为了找到下一个需要执行的任务,当找到下一个要执行的任务之后,加载下文的过程其实和启动第一个任务时大致相同,不再详细分析

6、实验

6.1、测试

使用 xTaskCreateStatic() 函数创建两个任务

  1. Task1 负责每隔 100ms 翻转一次 GREEN_LED 灯引脚的电平
  2. Task2 负责每隔 500ms 翻转一次 ORANGE_LED 灯引脚的电平

任务创建好之后将其添加到就绪链表中,然后启动任务调度器即可,具体代码如下所示

/* main.c */
/* USER CODE BEGIN Includes */
#include "FreeRTOS.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
extern List_t pxReadyTasksLists;

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;

// 任务 1 入口函数
void Task1_Entry(void *parg)
{
	for(;;)
	{
		HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
		HAL_Delay(100);
		taskYIELD();
	}
}
// 任务 2 入口函数
void Task2_Entry(void *parg)
{
	for(;;)
	{
		HAL_GPIO_TogglePin(ORANGE_LED_GPIO_Port, ORANGE_LED_Pin);
		HAL_Delay(500);
		taskYIELD();
	}
}
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
// 使用链表前手动初始化
prvInitialiseTaskLists();
// 创建任务 1 和 2
Task1_Handle = xTaskCreateStatic((TaskFunction_t)Task1_Entry,
								 (char *)"Task1",
								 (uint32_t)TASK1_STACK_SIZE,
								 (void *)NULL,
								 (StackType_t *)Task1Stack,
								 (TCB_t *)&Task1TCB);
														
Task2_Handle = xTaskCreateStatic((TaskFunction_t)Task2_Entry,
								 (char *)"Task2",
								 (uint32_t)TASK2_STACK_SIZE,
								 (void *) NULL,
								 (StackType_t *)Task2Stack,
								 (TCB_t *)&Task2TCB );
// 将两个任务插入到就绪链表中
vListInsertEnd(&(pxReadyTasksLists),&(((TCB_t *)(&Task1TCB))->xStateListItem));
vListInsertEnd(&(pxReadyTasksLists),&(((TCB_t *)(&Task2TCB))->xStateListItem));
// 启动任务调度器,永不返回
vTaskStartScheduler();
/* USER CODE END 2 */

启动任务调度器后的程序执行流程如下所示,第一次执行完步骤 6 后会不断重复步骤 3 ~ 6

  1. vTaskStartScheduler() -> pxCurrentTCB = &Task1TCB; -> xPortStartScheduler()
  2. -> prvStartFirstTask() -> vPortSVCHandler() -> Task1_Entry()
  3. -> taskYIELD() -> xPortPendSVHandler() -> vTaskSwitchContext()
  4. -> Task2_Entry()
  5. -> taskYIELD() -> xPortPendSVHandler() -> vTaskSwitchContext()
  6. -> Task1_Entry()

使用逻辑分析仪捕获 GREEN_LED 和 ORANGE_LED 两个引脚的电平变化,具体如下图所示

![[Kernel_3.6.png]]

可以发现两个任务不是并行运行的,而是一个任务执行完,第二个任务才会得到执行,所以两个 LED 灯引脚电平都是每隔 600ms 翻转一次,不是我们期待的 Task1 引脚电平每隔 100 ms 翻转一次,Task2 引脚电平每隔 500ms 翻转一次,这样的效果其实可被如下简单代码取代

while(1)
{
	HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
	HAL_Delay(100);
	HAL_GPIO_TogglePin(ORANGE_LED_GPIO_Port, ORANGE_LED_Pin);
	HAL_Delay(500);
}

6.2、待改进

当前 RTOS 简单内核已实现的功能有

  1. 静态方式创建任务
  2. 手动切换任务

当前 RTOS 简单内核存在的缺点有

  1. 不支持任务优先级
  2. 任务不能并行运行
  3. 无中断临界段保护

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1826918.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云系列产品免费用,不香吗?

阿里云系列产品免费用&#xff0c;不香吗&#xff1f; 什么是无影云电脑开启无影云下载安装客户端登录无影云桌面应用场景 开篇先发布一下阿里云产品免费体验地址&#xff1a;https://free.aliyun.com/?utm_contentg_1000370296 下面开始我的无影云电脑或者叫做无影云桌面的体…

LLM大语言模型算法特训,带你转型AI大语言模型算法工程师(完结)

LLM大语言模型算法 与AI大语言模型算法工程师的联系 LLM&#xff08;Large Language Model&#xff09;大语言模型是指像GPT这样的大型自然语言处理模型&#xff0c;而AI大语言模型算法工程师则是负责开发和优化这些模型的专业人士。它们之间的联系可以从以下几个方面来理解&a…

linux驱动学习(十二)之看门狗

一、看门狗定时器功能 1、产生复位信号&#xff1a;当系统受到由于噪声或者干扰而造成系统死机&#xff0c;看门狗产生一个复位信号。 2、普通定时器&#xff1a;16bits定时器&#xff0c;产生周期性的中断信号 二、看门狗系统框图 设置计数值以每隔10S就会产生一个复位信号&…

springboot依赖管理和自动配置

依赖管理和自动配置 依赖管理和自动配置依赖管理什么是依赖管理修改自动仲裁/默认版本号 starter场景启动器starter场景启动器基本介绍官方提供的starter第三方starter 自动配置自动配置基本介绍SpringBoot自动配置了哪些?如何修改默认配置如何修改默认扫描包结构resources\ap…

STM32学习笔记(一)--时钟树详解

&#xff08;1&#xff09;时钟概述&#xff1b;时钟是具有周期性的脉冲信号&#xff0c;最常用的是占空比50%的方波。&#xff08;时钟相当于单片机的脉搏&#xff1b;STM32本身非常复杂&#xff0c;外设非常的多&#xff0c;为了保持低功耗工作&#xff0c;STM32 的主控默认不…

亿达中国武汉园区入选“武汉市科技金融工作站”及“武汉市线下首贷服务站”

近日&#xff0c;武汉市2024科技金融早春行活动在深交所湖北资本市场培育基地举行。会上&#xff0c;第四批武汉市科技金融工作站试点单位名单及第五批武汉地区金融系统线下首贷服务站名单正式公布&#xff0c;武汉软件新城成功入选上述两个名单。 为缓解科技型企业融资难题&a…

远程问诊软件哪款好?选欣九康诊疗系统

近几年国家相继推出了支持发展“互联网医疗”的政策&#xff0c;如今随着相关政策的不断落实推进&#xff0c;市场上涌现出了一大批在线咨询、电子处方和远程问诊的医疗平台&#xff0c;而在面对种类如此繁多的医疗平台究竟选择哪款更好便成了医疗机构非常头疼的事情&#xff0…

【源码】综合股票币币合约交易所源码/etf交易所源码/美股港股台股交易所源码

支持多国语言 全开源可二开的一个版本&#xff01;支持虚拟货币 ETF 外汇 美股 A股 港股 台股。 前端是VUE开发&#xff08;带vue工程源码&#xff09;后端JAVA开发&#xff01;搭建也相对简单。 总的来说功能非常强大&#xff0c;适合线上运营的一个版本&#xff0c;有兴趣的可…

RabbitMQ无法删除unsynchronized队列及解决办法

一、故障环境 操作系统:CentOS7 RabbitMQ:3 nodes Cluster RabbitMQ version: 3.8.12 Erlang Version:22.3 Queue Type:Mirror,with polices 二、故障表现: 2.1 管理界面队列列表中存在部分队列镜像同步状态标红: 2.2 TPS为0,无消费者,其他节点镜像未同步且无法手动…

【SpringBoot】Spring Boot 中高级特性详解

文章目录 1. 异步处理1.1 什么是异步处理&#xff1f;1.2 实现异步处理1.2.1 启用异步支持1.2.2 使用 Async 注解1.2.3 调用异步方法 2. 安全管理2.1 Spring Security 集成2.2 基础安全配置2.2.1 添加依赖2.2.2 默认配置2.2.3 自定义用户认证 3. 监控和调试3.1 Spring Boot Act…

结构体对齐,与 触发 segment fault 为什么是 1024*132 ,而不是1024*128

1, 简单的小示例代码 按理说 malloc 的size 是 1024*128&#xff0c;这里却需要 1024*132才能及时触发 segmentation fault #include <stdlib.h> #include <stdio.h> #define SIZE 1024*131int main() {char *p 0;p malloc(SIZE);p[SIZE -1] a;free(p);printf(…

WWDC 2024 回顾:Apple Intelligence 的发布与解析

一年一度的苹果全球开发者大会&#xff08;WWDC&#xff09;如期而至&#xff0c;2024 年的 WWDC 再次成为科技界的焦点。本次发布会中&#xff0c;苹果正式推出了他们在 AI 领域的全新战略——Apple Intelligence。这一全新概念旨在为用户打造“强大、易用、全面、个性化、注重…

正运动邀您共聚2024深圳激光展,助力激光加工与智能制造!

■展会名称 2024深圳激光展 ■展会日期 2024年6月19日 - 21日 ■展馆地点 深圳国际会展中心&#xff08;新馆&#xff09; ■展位号 9H - D101 6月19至21日&#xff0c;深圳激光展将在中国深圳国际会展中心(新馆)举办。 激光加工在消费电子、光伏锂电新能源、半导体等行…

展厅设计要关注的基本点

1、设计方案 每个企业都会有不同的风格特色&#xff0c;找到一个合适企业的设计方案才是重点&#xff0c;所以在策划设计上要有一套个性化的方案。大到展厅内的结构&#xff0c;小到单个的展陈框架摆放&#xff0c;都要有详细的规划&#xff0c;这样才能够打造出一个效果突出的…

深度学习笔记: 最详尽估算送达时间系统设计

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; 估算送达时间 1. 问题陈述 构建一个模型来估算在给定订单详情、市场条件和交通状况下的总送达时间。 为…

探索Docker容器网络

Docker容器已经成为现代应用部署的核心工具。理解Docker的网络模型对于实现高效、安全的容器化应用至关重要。在这篇博客中&#xff0c;我们将深入探讨Docker的网络架构&#xff0c;并通过一些代码例子来揭示其底层实现。 Docker网络模式 Docker提供了多种网络模式&#xff0c…

大模型系列:C-Eval中文大模型评测数据集介绍和实践

前言 C-Eval是目前权威的中文AI大模型评测数据集之一&#xff0c;用于考察大模型的知识和推理能力&#xff0c;本篇对C-Eval数据集做简要介绍&#xff0c;并演示如何使用C-Eval对大模型进行评测。 内容摘要 C-Eval整体结构概述C-Eval数据预览C-Eval的Prompt范式Python脚本实现…

第九十一周周报

学习目标&#xff1a; 论文 学习时间&#xff1a; 2024.6.8-2024.6.14 学习产出&#xff1a; 一、Consistency Models 扩散模型依赖于迭代采样过程导致生成缓慢&#xff0c;由此提出一致性模型&#xff0c;其可以直接将噪声映射至数据从而生成高质量样别。 根据给定的OD…

Kotlin编程实践-【Java如何调用Kotlin中带默认值参数的函数】

问题 如果你有一个带有默认参数值的 Kotlin 函数&#xff0c;如何从 Java 调用它而无须为每个参数显式指定值&#xff1f; 方案 为函数添加注解JvmOverloads。 也就是为Java添加重载方法&#xff0c;这样Java调用Kotlin的方法时就不用传递全部的参数了。 示例 在 Kotlin …

【数学】什么是傅里叶级数与傅里叶变换?

傅里叶级数与傅里叶变换 背景 傅里叶级数和傅里叶变换是数学和工程领域中的重要工具&#xff0c;特别是在信号处理、图像处理和物理学中。傅里叶级数用于将周期函数表示为正弦和余弦函数的和&#xff0c;而傅里叶变换用于将任意函数表示为频率的函数。 公式 傅里叶级数&…