本专栏将对FreeRTOS进行快速讲解,带你了解并使用FreeRTOS的各部分内容。适用于快速了解FreeRTOS并进行开发、突击面试、对新手小白非常友好。期待您的后续关注和订阅!
目录
任务调度与时间片任务调度
任务调度详解需要大家了解一下,时间片任务调度需要大家进行掌握。
1 任务调度(了解)
FreeRTOS任务调度器的启动和任务切换过程是嵌入式系统中实时操作系统的重要组成部分。理解启动任务调度器、启动第一个任务以及任务切换的实现细节,可以更好地帮助我们掌握FreeRTOS的工作机制,并应用于实际的嵌入式开发中。
1.1 启动调度器
首先为vTaskStartScheduler()函数
用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度。
void vTaskStartScheduler(void)
{
// 创建空闲任务
prvIdleTask();
// 如果使能软件定时器,则创建定时器任务
xTimerCreateTimerTask();
// 关闭中断,防止调度器开启之前或过程中,受中断干扰
portDISABLE_INTERRUPTS();
// 初始化全局变量
// 设置任务调度器的运行标志
xSchedulerRunning = pdTRUE;
// 初始化任务运行时间统计功能的时基定时器
vInitTaskRunTimeStats();
// 启动任务调度器
xPortStartScheduler();
}
- 创建空闲任务:空闲任务是系统在无其他任务可运行时所运行的任务。
- 创建软件定时器任务:如果启用了软件定时器,需创建相应的任务来处理定时器事件。
- 关闭中断:在启动调度器前关闭中断,以防止中断干扰。
- 初始化全局变量:包括调度器运行标志等。
- 初始化时基定时器:用于统计任务的运行时间。
- 启动调度器:调用
xPortStartScheduler()
完成启动。
1.2 启动第一个任务
xPortStartScheduler()
函数负责启动第一个任务,并进行与硬件架构相关的配置。
BaseType_t xPortStartScheduler(void)
{
// 检测用户在 FreeRTOSConfig.h 文件中的配置
configASSERT( portCHECK_INTERRUPT_PRIORITY );
// 配置 PendSV 和 SysTick 的中断优先级
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
// 配置 SysTick
vPortSetupTimerInterrupt();
// 初始化临界区嵌套计数器
uxCriticalNesting = 0;
// 启用 FPU
prvEnableVFP();
// 启动第一个任务
prvStartFirstTask();
return 0;
}
- 配置中断优先级:将PendSV和SysTick的中断优先级设置为最低。
- 配置SysTick:用于系统时钟滴答定时。
- 初始化临界区嵌套计数器:设为0。
- 启用FPU:使能浮点运算单元(如果有)。
- 启动第一个任务:通过
prvStartFirstTask()
函数。
1.3 启动第一个任务的实现
prvStartFirstTask()
函数用于初始化启动第一个任务前的环境,主要是重新设置MSP指针,并使能全局中断。
void prvStartFirstTask(void)
{
// 获取向量表的偏移地址
__asm volatile(
"LDR R0, =0xE000ED08 \n" /* 获取 VTOR 寄存器的地址 */
"LDR R0, [R0] \n" /* 获取向量表存储的地址 */
"LDR R0, [R0] \n" /* 获取 MSP 的初始值 */
"MSR MSP, R0 \n" /* 设置 MSP */
"CPSIE I \n" /* 使能中断 */
"SVC 0 \n" /* 触发 SVC 中断 */
"NOP \n"
"NOP \n"
);
}
- 获取MSP初始值:从向量表中获取MSP的初始值,并设置MSP。
- 使能中断:启用全局中断。
- 触发SVC中断:进入SVC中断服务函数
vPortSVCHandler()
。
1.4 任务切换
任务切换是FreeRTOS的重要机制之一,主要在PendSV中断中完成。
void vPortSVCHandler(void)
{
__asm volatile (
"LDR R3, =pxCurrentTCB \n" /* 获取当前任务的TCB指针 */
"LDR R1, [R3] \n" /* 获取当前任务的栈顶地址 */
"LDR R0, [R1] \n" /* 获取栈顶地址 */
"LDMIA R0!, {R4-R11} \n" /* 恢复R4-R11寄存器 */
"MSR PSP, R0 \n" /* 设置PSP指针 */
"MOV R0, #0 \n"
"MSR BASEPRI, R0 \n" /* 允许中断 */
"ORR LR, LR, #0x04 \n"
"BX LR \n" /* 返回到任务 */
);
}
1.5 任务切换过程
任务切换的过程可以分为保存当前任务上下文和恢复下一个任务上下文。
void PendSV_Handler(void)
{
// 保存当前任务上下文
__asm volatile(
"MRS R0, PSP \n" /* 获取PSP指针 */
"STMDB R0!, {R4-R11, LR} \n" /* 保存R4-R11和LR寄存器 */
"LDR R1, =pxCurrentTCB \n"
"LDR R1, [R1] \n"
"STR R0, [R1] \n" /* 保存当前任务的栈顶地址 */
// 切换到下一个任务
"BL vTaskSwitchContext \n"
"LDR R1, =pxCurrentTCB \n"
"LDR R1, [R1] \n"
"LDR R0, [R1] \n" /* 获取下一个任务的栈顶地址 */
"LDMIA R0!, {R4-R11, LR} \n" /* 恢复R4-R11和LR寄存器 */
"MSR PSP, R0 \n" /* 设置PSP指针 */
"BX LR \n" /* 返回到任务 */
);
}
- 保存上下文:将当前任务的R4-R11和LR寄存器保存到任务堆栈中。
- 切换任务:调用
vTaskSwitchContext()
查找下一个最高优先级的任务。 - 恢复上下文:从下一个任务的堆栈中恢复R4-R11和LR寄存器。
- 返回任务:通过
BX LR
返回到新任务。
2 任务调度实验
2.1 简介
在之前的文章里我们已经对其进行过介绍,一共有三种调度方式。但是我们一般只使用两种调度方式:
- 抢占式调度:占式调度依赖任务的抢占优先级别,在FreeRTOS中抢占优先级数值越大,任务优先级越高。
- 时间片调度:同等优先级任务它们轮流享有相同的运行时间(时间片),在该系统中一个时间片就相当于系统抵达定时器的一个中断周期。
2.2 实验展示
2.2.1 实验内容
下面将以一个例程代码进行运用,本次实验将创建四个优先级不同的任务,每个任务将通过串口打印其运行次数和优先级。
(1)配置FreeRTOS:
- 确保
configUSE_TIME_SLICING
和configUSE_PREEMPTION
设置为1。 - 设置滴答定时器的中断频率为50ms。
(2)任务设计:
- 创建四个任务:
start_task
、task1
、task2
、task3
和task4
。 start_task
负责创建其他四个任务。task1
、task2
、task3
和task4
分别通过串口打印其运行次数和优先级。
(3)任务优先级:
task1
优先级设为1。task2
优先级设为2。task3
优先级设为2。task4
优先级设为3。
2.2.2 实验现象
由于时间片调度的作用,四个任务会按优先级顺序抢占CPU时间,优先级高的任务会更频繁地获得CPU时间片。特别是两个优先级相同的任务task2
和task3
将会公平地分享同一优先级的时间片。
2.2.3 实验代码
#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
// 任务句柄
TaskHandle_t StartTask_Handler;
TaskHandle_t Task1_Handler;
TaskHandle_t Task2_Handler;
TaskHandle_t Task3_Handler;
TaskHandle_t Task4_Handler;
// 函数声明
void start_task(void *pvParameters);
void task1(void *pvParameters);
void task2(void *pvParameters);
void task3(void *pvParameters);
void task4(void *pvParameters);
int main(void)
{
// 初始化硬件(例如串口)在此处添加
// 创建StartTask
xTaskCreate((TaskFunction_t)start_task,
(const char *)"start_task",
(uint16_t)128,
(void *)NULL,
(UBaseType_t)2,
(TaskHandle_t *)&StartTask_Handler);
// 启动调度器
vTaskStartScheduler();
// 循环执行永不返回
while(1);
}
// start_task函数实现
void start_task(void *pvParameters)
{
taskENTER_CRITICAL();
// 创建task1
xTaskCreate((TaskFunction_t)task1,
(const char *)"task1",
(uint16_t)128,
(void *)NULL,
(UBaseType_t)1,
(TaskHandle_t *)&Task1_Handler);
// 创建task2
xTaskCreate((TaskFunction_t)task2,
(const char *)"task2",
(uint16_t)128,
(void *)NULL,
(UBaseType_t)2,
(TaskHandle_t *)&Task2_Handler);
// 创建task3
xTaskCreate((TaskFunction_t)task3,
(const char *)"task3",
(uint16_t)128,
(void *)NULL,
(UBaseType_t)2,
(TaskHandle_t *)&Task3_Handler);
// 创建task4
xTaskCreate((TaskFunction_t)task4,
(const char *)"task4",
(uint16_t)128,
(void *)NULL,
(UBaseType_t)3,
(TaskHandle_t *)&Task4_Handler);
// 删除start_task任务
vTaskDelete(StartTask_Handler);
taskEXIT_CRITICAL();
}
// task1函数实现
void task1(void *pvParameters)
{
int count = 0;
while(1)
{
printf("Task1 running count: %d, priority: %d\n", count++, uxTaskPriorityGet(NULL));
vTaskDelay(50); // 延时50ms
}
}
// task2函数实现
void task2(void *pvParameters)
{
int count = 0;
while(1)
{
printf("Task2 running count: %d, priority: %d\n", count++, uxTaskPriorityGet(NULL));
vTaskDelay(50); // 延时50ms
}
}
// task3函数实现
void task3(void *pvParameters)
{
int count = 0;
while(1)
{
printf("Task3 running count: %d, priority: %d\n", count++, uxTaskPriorityGet(NULL));
vTaskDelay(50); // 延时50ms
}
}
// task4函数实现
void task4(void *pvParameters)
{
int count = 0;
while(1)
{
printf("Task4 running count: %d, priority: %d\n", count++, uxTaskPriorityGet(NULL));
vTaskDelay(50); // 延时50ms
}
}
2.2.4 现象分析
在时间片调度的机制下,不同优先级的任务会按优先级顺序抢占CPU时间。具体表现为:
task4
由于优先级最高,会比其他任务更频繁地运行。task2
和task3
优先级相同,将会公平地分享同一优先级的时间片。task1
优先级最低,运行的频率最低。
通过观察串口输出,可以清晰地看到不同优先级任务的运行次数,验证时间片调度的效果,尤其是相同优先级的任务之间的时间片共享情况。
本专栏将对FreeRTOS进行快速讲解,带你了解并使用FreeRTOS的各部分内容。期待诸君的关注点赞!