目录

1. 创建任务

1.1 什么是任务

1.2 定义任务栈

1.3 定义任务函数

1.4 定义任务控制块

1.5 实现任务创建函数

1.5.1 xTaskCreateStatic函数实现

1.5.2 prvInitialiseNewTask函数实现

1.5.3 pxPortInitialiseStack函数实现

2. 实现就绪列表

2.1 定义就绪列表

2.2 就绪列表初始化

3. 实现调度器

3.1 调度器的主要任务

3.2 启动调度器

3.2.1 vTaskStartScheduler函数实现

3.2.2 xPortStartScheduler函数实现

3.2.3 prvStartFirstTask函数实现

3.2.3 vPortSVCHandler函数实现

3.3 任务切换

3.3.1 taskYIELD函数实现

3.3.2 portYIELD函数实现

3.3.3 xPortPendSVHandler函数实现

3.3.4 vTaskSwitchContext函数实现

4. 实验现象


1. 创建任务

1.1 什么是任务

在实现上,任务的实体是一个永不返回的函数

void task_entry(void *parg)
{for (;;){/* 任务主体代码 */}
}

1.2 定义任务栈

① 在多任务系统中,每个任务都是独立互不干扰的,所以要为每个任务都分配独立的栈空间

② 任务栈内存有2种来源,

a. 静态分配的全局数组

b. 动态分配的内存

// 示例:使用全局数组作为任务栈
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE]; // 512B

说明:栈的数据类型为StackType_t,单位为字(4B),设置为128,即512B,这也是FreeRTOS推荐的最小任务栈

1.3 定义任务函数

portCHAR flag1;void delay(uint32_t count)
{for (;count != 0; count--);
}void Task1_Entry(void *p_arg)
{for (;;){flag1 = 1;delay(100);flag1 = 0;delay(100);/* 主动放弃CPU */taskYIELD();}
}

说明:此处调用taskYIELD是为了主动放弃CPU,实现任务的切换,具体实现见后文

1.4 定义任务控制块

① 操作系统为每个任务定义一个任务控制块(TCB),操作系统通过TCB识别并控制任务状态,即在操作系统层面任务就是TCB

② TCB中存储任务的所有信息,e.g. 任务的栈指针、任务名称等

/* 任务控制块数据结构 */
typedef struct tskTaskControlBlock
{volatile StackType_t *pxTopOfStack; /* 任务栈顶指针 */ListItem_t xStateListItem;          /* 任务状态节点 */StackType_t *pxStack;               /* 任务栈起始地址 */char pcTaskName[configMAX_TASK_NAME_LEN]; /* 任务名称 */
} tskTCB;
/* 任务控制块数据类型 */
typedef tskTCB TCB_t;

说明:任务状态节点的作用是根据需要将TCB链接到不同的队列中(e.g. 就绪队列、等待队列、延时队列等)

1.5 实现任务创建函数

上面介绍的任务的三个要素(任务栈、任务函数、TCB)最终需要关联起来才能由操作系统实现统一的调度,该工作就是由任务创建函数完成的

1.5.1 xTaskCreateStatic函数实现

#if (configSUPPORT_STATIC_ALLOCATION == 1)
/*
* file: tasks.c
* pxTaskCode: 任务入口,即任务的函数名称
* pcName: 任务名称,字符串形式
* ulStackDepth: 任务栈大小,以字为单位
* pvParameters: 任务参数
* puxStackBuffer: 任务栈起始地址
* pxTaskBuffer: TCB指针
*/
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;TaskHandle_t xReturn; // 用于返回的任务句柄if ((pxTaskBuffer != NULL) && (puxStackBuffer != NULL)){pxNewTCB = (TCB_t *)pxTaskBuffer;pxNewTCB->pxStack = (StackType_t *)puxStackBuffer;/* 初始化新任务 */prvInitialiseNewTask(pxTaskCode,pcName,ulStackDepth,pvParameters,&xReturn, // 出参,返回任务句柄pxNewTCB);}else{xReturn = NULL;}/* 返回任务句柄,如果任务创建成功,xReturn指向任务控制块 */return xReturn;
}
#endif /* configSUPPORT_STATIC_ALLOCATION *

说明:xTaskCreateStaic函数用于实现静态地创建任务,任务的创建有2种方式,

① 动态创建:TCB和任务栈在创建任务时动态分配,任务删除时内存可释放

② 静态创建:TCB和任务栈需要事先定义好,任务删除时内存不能释放

1.5.2 prvInitialiseNewTask函数实现

/*
* file: tasks.c
* pxTaskCode: 任务函数
* pcName: 任务名称
* ulStackDepth: 任务栈大小
* pvParameters: 任务参数
* pxCreatedTask: 任务句柄,出参
* pxNewTCB: 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)0x007)));/* 将任务名称存储在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中的任务状态节点 */vListInitializeItem(&(pxNewTCB->xStateListItem));/* 设置任务状态节点拥有者为TCB */listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);/* 初始化任务栈,体系结构相关 */pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack,pxTaskCode,pvParameters);/* 将任务句柄指向TCB */if ((void *)pxCreatedTask != NULL){*pxCreatedTask = (TaskHandle_t)pxNewTCB;}
}

说明1:栈顶地址的计算

首先此处给出的计算方式,说明使用的是递减栈,即栈的方向是从高地址向低地址生长

递减栈又分为满减栈和空减栈,其实只有空减栈才需要减1,而Cortex-M默认使用的是满减栈,FreeRTOS代码在此处均减1,以简化实现

说明2:栈顶地址8字节对齐

在Cortex-M体系结构总线宽度为32位,进行栈顶地址8字节对齐,是因为Cortex-M的浮点运算是64位的

在其他体系结构中,可根据需要设置栈顶地址的对齐方式

说明3:初始化任务栈与体系结构相关

任何操作系统在体系结构相关和体系结构无关的代码之间必须有一条清晰的界线,对任务栈的初始化就是一个体系结构相关的操作

初始化任务栈的目的是确保任务被首次调度时可以正确运行,本质上相当于一次恢复下文(任务切换分为保存上文和恢复下文),只是这里的下文是初始化任务栈时人为设定的

1.5.3 pxPortInitialiseStack函数实现

#define portINITIAL_XPSR (0x01000000) // 进入Thumb模式,bit[24]必须为1
#define portSTART_ADDRESS_MASK ((StackType_t)0xfffffffeUL)static void prvTaskExitError(void)
{for (;;);
}/*
* file: port.c
* pxTopOfStack: 任务栈顶指针
* pxCode: 任务函数
* pvParameters: 任务参数
*/
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack,TaskFunction_t pxCode,void *pvParameters)
{/* 异常发生时,硬件自动保存的内容 */pxTopOfStack--;*pxTopOfStack = portINITIAL_XPSR;pxTopOfStack--;*pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK; // PCpxTopOfStack--;*pxTopOfStack = (StackType_t)prvTaskExitError; // LRpxTopOfStack -= 5;*pxTopOfStack = (StackType_t)pvParameters; // r0/* 异常发生时,软件需要保存的内容 */pxTopOfStack -= 8;return pxTopOfStack;
}

说明:任务栈初始化规范

任务栈的初始化与体系结构高度相关,必须符合对应体系结构在异常处理时的规范

其中r0存储任务参数指针,是为了符合AAPCS(ARM架构过程调用标准)规范

2. 实现就绪列表

2.1 定义就绪列表

① 就绪列表用于组织系统中所有可供调度运行的任务

② 就绪列表实际上是一个List_t类型的数组,数组的大小由系统支持的最大优先级个数决定,即相同优先级的任务组织在一个列表中

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

2.2 就绪列表初始化

/*
* 初始化任务相关列表
*/
void prvInitialiseTaskLists(void)
{UBaseType_t uxPriority;for (uxPriority = (UBaseType_t)0U;uxPriority < (UBaseType_t)configMAX_PRIORITIES;uxPriority++){vListInitializeList(&(pxReadyTasksLists[uxPriority]));}
}

说明:目前与任务相关的只有就绪列表,后续会逐渐增加

3. 实现调度器

3.1 调度器的主要任务

调度器的主要任务有2个,

① 调度运行第1个任务

② 实现任务切换

3.2 启动调度器

3.2.1 vTaskStartScheduler函数实现

// file: tasks.c
/* 启动调度器 */
void vTaskStartScheduler(void)
{/* 指定第一个运行的任务 */pxCurrentTCB = &Task1TCB;/* 启动调度器中体系结构相关的操作 */if (xPortStartScheduler() != pdFALSE){/* 调度器启动成功,则不会运行到此处 */}
}

3.2.2 xPortStartScheduler函数实现

// file: 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中断都会涉及到任务调度,所以将其优先级设为最低,以便优先响应系统中的外部硬件中断

3.2.3 prvStartFirstTask函数实现

// file: port.c
__asm void prvStartFirstTask(void)
{PRESERVE8/* 设置MSP寄存器 */ldr r0, =0xE000ED08ldr r0, [r0] // r0 = 0x00000000ldr r0, [r0] // r0 = 0x200008D8, MSP初始值msr msp, r0/* 使能全局中断 */cpsie icpsie fdsbisb/* 触发SVC异常,在SVC handler中启动第一个任务 */svc 0nopnop
}

说明1:Cortex-M体系结构中通过触发SVC中断实现第一个任务的调度

说明2:Cortex-M中断屏蔽寄存器

说明3:Cortex-M开关中断指令

3.2.3 vPortSVCHandler函数实现

__asm void vPortSVCHandler(void)
{extern pxCurrentTCB;PRESERVE8ldr r3, =pxCurrentTCBldr r1, [r3]        // 获得TCB地址ldr r0, [r1]        // 获得任务栈顶指针ldmia r0!, {r4-r11} // 加载软件保存的寄存器msr psp, r0         // 设置线程栈指针isbmov r0, #0msr basepri, r0 // 打开所有中断orr r14, #0xd   // 异常返回线程模式,使用PSPbx r14
}

说明1:从vPortSVCHandler的实现中可见,启动第一个任务就是一次恢复下文的操作(正常的任务切换则是保存上文 + 恢复下文),进入SVC handler时,第一个任务的任务栈布局如下图所示,

其中需要手动恢复的寄存器在SVC Handler中处理,需要硬件恢复的寄存器在异常返回时由硬件自动完成

说明2:修改r14返回线程模式

Cortex-M体系结构在启动时为线程模式 + MSP栈(也称作特权级线程模式),我们希望FreeRTOS运行在线程模式 + PSP栈(也称作用户级线程模式),这点可以通过修改进入异常后的r14(LR)寄存器实现。具体含义如下图所示,

补充:关于r14(LR)的使用

在Cortex-A体系结构中,进入中断时,会使用相应模式的LR寄存器保存中断返回地址;在Cortex-M体系结构中,中断返回地址被压入栈中,LR寄存器可用来实现模式切换

说明3:值得注意的是,在SVC Handler中启动第1个任务时,并没有修改TCB中的栈顶指针,这是因为没有必要

在TCB中保存栈顶地址的目的是为了恢复下文,所以对应地,仅需要在保存上文的时候记录保存上文之后的栈顶地址即可

而在任务的运行过程中,任务栈指针由PSP寄存器保存,由编译器编译出的代码自动维护(如果是写汇编代码,则是由汇编代码自己维护)

3.3 任务切换

FreeRTOS中任务切换的目标就是在就绪列表中寻找优先级最高的任务,然后调度该任务运行

由于目前尚未实现任务优先级,此处先实现任务主动放弃CPU

3.3.1 taskYIELD函数实现

// file: task.h
#define taskYIELD() portYIELD()

说明:taskYIELD只是简单调用了体系结构相关的portYIELD函数,放弃CPU需要触发系统调度,而不同的体系结构触发的方式不同(e.g. Cortex-M是触发PendSV中断,而Cortex-A是触发SVC中断)

3.3.2 portYIELD函数实现

// file: portmacro.h
#define portYIELD() \
{ \/* 触发PendSV中断,产生上下文切换 */ \portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \__dsb(portSY_FULL_READ_WRITE); \__isb(portSY_FULL_READ_WRITE); \
}

3.3.3 xPortPendSVHandler函数实现

PendSV Handler是Cortex-M体系结构中实现任务切换最主要的函数

// file: port.c
__asm void xPortPendSVHandler(void)
{extern pxCurrentTCB;extern vTaskSwitchContext;PRESERVE8/* 保存上文 */mrs r0, pspisbldr r3, =pxCurrentTCBldr r2, [r3]        // 获得任务栈顶指针地址,即TCB地址stmdb r0!, {r4-r11} // 软件保存的寄存器str r0, [r2]        // 更新任务栈指针// 寻找要执行的任务// 此处保存r3 & r14,是因为恢复下文时还要使用这2个寄存器的值// 同时需要注意的是,r3 & r14此时是被保存在了MSP指向的栈中// 因为我们目前处于中断处理函数中stmdb sp!, {r3, r14}mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITYmsr basepri, r0dsbisbbl vTaskSwitchContextmov r0, #0msr basepri, r0ldmia sp!, {r3, r14}// 恢复下文ldr r1, [r3]ldr r0, [r1] // 获得要运行任务的栈顶指针ldmia r0!, {r4-r11}msr psp, r0isbbx r14nop
}

说明1:在调用vTaskSwitchContext函数寻找要执行的任务前后,进行了临界区保护,保护的方式是关闭优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断

Cortex-M的中断优先级如下图所示,

代码中configMAX_SYSCALL_INTERRUPT_PRIORITY为191,由于只有最高4位有效,所以中断优先级阈值为11,也就是中断优先级编号大于等于11的中断均被关闭

说明2:进入SVC Handler时上文的任务栈布局如下图所示,硬件已经完成了他所要完成的环境保存任务

3.3.4 vTaskSwitchContext函数实现

PendSV Handler中关中断要保护的就是vTaskSwitchContext函数,该函数用于寻找下一个要执行的任务

// file: tasks.c
/* 选择下一个运行的任务,并更新pxCurrentTCB */
void vTaskSwitchContext(void)
{if (pxCurrentTCB == &Task1TCB){pxCurrentTCB = &Task2TCB;}else{pxCurrentTCB = &Task1TCB;}
}

说明:此处实现很简单,就是Task1 & Task2交替运行

4. 实验现象

从标志位的波形可见,Task1 & Task2可以交替运行

FreeRTOS内核实现02:任务的定义与任务切换相关推荐

  1. freertos内核 任务定义与切换 原理分析

    freertos内核 任务定义与切换 原理分析 主程序 任务控制块 任务创建函数 任务栈初始化 就绪列表 调度器 总结任务切换 主程序 这个程序目的就是,使用freertos让两个任务不断切换.看两个 ...

  2. FreeRTOS内核笔记(一):基本知识和命名规则

    FreeRTOS内核笔记(一):基本知识和命名规则 FreeRTOS内核笔记 命名规则: 常用宏定义 Thread运行状态: RTOS Tick Context切换: 实时调度器Scheduler F ...

  3. 任务的定义、任务切换的原理及实现

    文章目录 1 任务的定义 2 任务切换的原理 3 任务切换的实现 3.1 设计目标 3.2 设计实现 3.3 代码实现 3.4 PendSV_Handler的另一种实现 1 任务的定义 任务的外观:一 ...

  4. Linux内核分析---进程的创建,执行与切换

    学号:210 "原创作品转载请注明出处 + https://github.com/mengning/linuxkernel/ " 一.实验要求 从整理上理解进程创建.可执行文件的加 ...

  5. Mac上refind定义macos+uos切换

    Mac上refind定义macos+uos切换 一.Mac上安装rEFInd第三方驱动 二.UOS上去除多余的项目 2.1编辑refind.conf文件 2.2 编辑删除 grub.efi文件 2.3 ...

  6. FreeRTOS内核详解(1) —— 临界段保护原理

    什么是临界段 临界段用一句话概括就是一段在执行的时候不能被中断的代码段. 在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作,由于不同任务间可以切换运行,当一个任务在访问某个全局变量 ...

  7. FreeRTOS内核实现01:列表与列表项实现

    目录 1. FreeRTOS编程风格 1.1 数据类型 1.2 变量名 1.3 函数名 1.4 宏 2. FreeRTOS列表实现分析 2.1 列表项数据类型 2.2 迷你列表项数据类型 2.3 列表 ...

  8. FreeRTOS内核——任务与任务切换

    2. 任务 相关函数 1. xTaskCreateStatic() 2. prvInitialiseNewTask() 3. prvInitialiseTaskLists() 4. vTaskStar ...

  9. freertos内核走读2——task任务调度机制(三)

    本文为jorhai原创,转载请注明,谢谢! 继续任务操作相关函数走读. vTaskDelayUntil, vTaskDelay的可以实现当前任务阻塞一定的tick时间,然后唤醒任务.任务从vTaskD ...

最新文章

  1. 【数据挖掘】数据挖掘总结 ( 拉普拉斯修正 | 贝叶斯分类器示例2 ) ★
  2. 【DBMS 数据库管理系统】OLTP 联机事务处理 与 OLAP 联机分析处理 ( 数据仓库 与 OLAP | OLAP 联机分析处理 | OLTP 与 OLAP 区别 )
  3. 闭包函数 装饰器 迭代器
  4. JQuery实现滚动广告(转)
  5. 【Python进阶】你真的明白NumPy中的ndarray吗?
  6. linux学习-简单命令介绍及安装VMware Tools
  7. oracle计算1到一百偶数的和,Perl 打印在1..100内所有偶数和奇数
  8. 因为不想「被绿」,美国年轻人只想和 iPhone 聊天
  9. java实验的总结_Java实验总结——初学(上)
  10. 麦肯锡方法:解决问题的七个步骤
  11. 疫情肆虐下,程序员们都在哪里?
  12. 什么流读取MultipartFile_IO流 - ShelterY
  13. GROMACS .mdp 选项翻译及笔记
  14. QT:主页面全屏显示(根据显示屏分辨率调整主页面尺寸)
  15. 中国十六烷基磷酸钾行业市场供需与战略研究报告
  16. document image inpaint
  17. easyUI form
  18. 在线 xml转java对象_XML转Java实体对象
  19. python中元组拆包_Python 元组拆包示例(Tuple Unpacking)
  20. [HCTF 2018] WarmUp

热门文章

  1. python信息传送管道_python – 获取返回管道输入的命令
  2. ps4pro服务器维护,PS4 | PS4 Pro 常见问题 | PlayStation
  3. 微金融php源码下载,ThinkPHP金融微盘微交易系统平台源码
  4. IDEA导入Gradle项目后,重现构建项目并导入jar包后但是External Libraries目录中无任何引入的jar包
  5. 湖南师范大学数学与计算机学院郭水霞,湖南师范大学数学与计算机科学学院2013备考手册...
  6. php文件便利,PHP便利文件夹下所有文件,创建压缩包
  7. uniapp 表单提交图片跟其他填写数据_记录第一次实现表单数据提交到数据库
  8. springboot 事务_第六章:springboot开启声明式事务
  9. windows上查看和设置weblogic的编码格式
  10. mysql如何将多条返回结果的一个字段合并成一条