type
status
password
date
slug
summary
category
URL
tags
icon
1. 任务定义与切换原理
1.1 任务是什么
1.1.1 任务的外观
任务的外观:一个永不返回的函数
说明:使用void *类型形参,确保可以传入任意类型的参数
1.1.2 任务的内在
任务的内在:一个函数的执行
- 代码段和数据区由编译器在编译代码时自动分配与控制
- 堆的分配和使用由程序员控制
- C代码中一般不会显式使用栈,将由编译器完成;在汇编代码中,程序员可以设置栈的位置并使用
- C代码也不会显示使用寄存器,也是由编译器完成
个人:代码区 + 数据区 + 栈 + 堆可以理解为任务的实体 + 运行环境
1.2 任务切换原理
1.2.1 任务切换的本质
任务切换的本质:保存前一任务(prev)的当前运行状态,恢复后一任务(next)之前的运行状态,并切换到该任务运行
1.2.2 要保存哪些任务运行状态
- 代码区 & 数据区:由编译器自动分配,各个任务相互独立,并不冲突
- 堆:由程序员定义并使用,对堆操作的句柄保存在栈中。不使用
- 栈:内核硬件只支持两个栈空间(MSP & PSP),而不同任务的栈不能共用,所以需要给每个任务设置自己的栈空间
- 寄存器:编译器会在某些时刻将寄存器保存到栈中(e.g. 函数调用,异常处理),如果其他寄存器也会被破坏,则需要程序员自己保存
- 其他需要保存的状态数据
1.2.3 任务运行状态保存方案
解决方案:为每个任务分配独立的栈,用于保存该任务的所有状态数据
在进行任务切换时,
- 将前一(prev)任务的当前状态保存在该任务的栈中
- 找到下一(next)任务的栈,然后从该栈中恢复任务状态
1.3 设计实现
1.3.1 类型定义
- 栈类型:Cortex-M中对栈操作的单位为4B,所以使用uint32_t类型
- 任务类型:每个任务的栈地址,均保存在任务结构体中,即TCB中
1.3.2 任务定义与初始化
2. 任务切换的实现
2.1 设计目标
说明:在本节实现中,任务切换由任务主动调用
tTaskSched
函数实现2.2 任务切换原理
2.2.1 如何启动初始任务
- 上电启动后,系统进入特权级线程模式(线程模式 + MSP),而任务一般运行在用户级线程模式(线程模式 + PSP)
- 设置任务初始栈:也就是设置初始任务的内核寄存器值、其他状态数据等。这些初始栈代表了程序运行的初始状态。
- 触发任务切换,使用任务初始栈内容填充任务运行状态。
说明2:在设置任务初始栈时,会将任务函数入口地址(task entry)填入PC寄存器对应的位置,这样在执行任务切换后,CPU即可运行任务函数
2.2.2 如何实现任务切换
- 将当前任务运行状态保存到当前任务栈中。说明:此处的任务运行状态保存分为2部分,
- 硬件自动保存部分(进入pendSV异常时硬件自动保存),硬件保存的数据也是保存在系统当前使用的栈中,也就是当前任务的栈中。
- 程序员自行保存部分。需要通过软件保存R4~R11寄存器
M3处理器会自动保存xPSR、PC、LR、R12、R0~R3寄存器
- 找到下一任务,并用下一任务的任务栈恢复该任务运行环境。与任务状态的保存类似,任务状态的恢复也分为
- 硬件自动恢复部分
- 程序员自行恢复部分
在任务切换函数中,只需要完成程序员自行恢复部分即可
2.3 设计实现
2.3.1 设置任务初始栈
说明1:任务初始化栈,是任务首次运行时的环境。由于是任务首次运行,软硬件均为设置过该任务的栈,所以此时同时初始化了由硬件保存的部分和由程序员保存的部分
说明2:任务初始化栈设置注意事项
- 硬件自动保存状态部分必须与硬件操作顺序一致
- 程序员自行保存状态部分要遵循高编号寄存器对应高地址的规则(STRM & LDRM指令要求的规则)
2.3.2 启动初始任务
说明:触发pendSV异常后,系统进入pendSV ISR运行,由于此处将PSP寄存器置为0,在进行任务切换时据此可识别出此时为启动初始任务,无需保存前一任务的运行状态(因为此时还没有这个"前一任务")
2.3.3 申请任务调度
说明:此处调度实现非常简单,就是在2个任务之间相互切换
2.3.4 任务切换
说明1:任务切换流程图如下,
3. 双任务时间片运行原理
3.1 设计目标
说明:在上节实现中,任务切换由任务主动调用tTaskSched函数实现;在本节实现中,将在SysTick ISR中调用tTaskSched函数进行任务切换,进而实现基于时间片的任务调度
3.2 时间片切换原理
3.2.1 时间片实现
核心:利用SysTick的周期性中断,在该中断的ISR中选择下一任务,并触发pendSV异常进行任务切换
3.2.2 SytTick定时器简介
SysTick定时器是Cortex-M3内核内置的24位递减定时器,当递减到0时,将从RELEAD寄存器中自动重载定时初值到CURRENT寄存器,如此反复。
3.3 设计实现
3.3.1 SysTick设置
3.3.2 SysTick_Handler
3.3.3 任务函数
说明:由于在SysTick_Handler中进行任务切换,所以在任务函数中无需再调用tTaskSched函数
4. 双任务延时原理与空闲任务
4.1 设计目标
说明:delay函数的延时通过在循环中递减计数值实现,属于忙等操作,在延时过程中持续占用CPU,因此降低了CPU使用率。本节设计的任务延时接口的要点就是在延时过程中释放CPU
4.2 任务延时原理
4.2.1 任务延时
- 目标:在任务延时过程中,暂停当前任务运行,释放CPU控制权给其他任务
- 步骤:
- 启动任务计时器时,通过调用任务切换函数释放CPU
- 结束任务计时器时,也会调用任务切换函数,恢复之前暂停的任务(当然,此处还涉及多任务优先级的调度问题)
4.2.2 软件计时器
- 说明1:由于任务数量不限,而硬件计时器资源有限,所以任务计时器一般使用软件计时器实现
- 说明2:软件计时器是基于硬件计时器工作的,从本质上说,软件计时器是一个计数值,每当硬件SysTick触发时,在SysTick ISR中维护软件计时器计数值
- 说明3:由于软件计时器基于硬件计时器工作,所以软件定时时间必须是硬件定时器周期的倍数。相当于硬件计时器提供了最小的计时分辨率,如果需要更高精度的延时则只能使用硬件定时器
4.2.3 延时精度问题
说明:由于使用软件计时器,加之延时处理需要时间,所以延时精度有限,使用时需要注意场景。
4.2.4 空闲任务
如果所有任务都进入延时(或者当前没有任务可运行),那么RTOS应该运行什么 ?(毕竟CPU不能闲着呀~)。所以我们添加空闲任务,在空闲任务中添加低功耗运行指令或者进行CPU统计任务
4.3 设计实现
4.3.1 增设软件计时器
说明:在任务初始化函数tTaskInit函数中需要对应地将delayTick字段初始化为0
4.3.2 SysTick_Handler
说明:在SysTick ISR中,首先遍历所有任务的延时计数值,如果非零则递减,说明完成一个SysTick的延时;然后调用tTaskSched函数触发任务切换,这是原先基于时间片调度就需要完成的操作
4.3.3 tTaskDelay函数
说明1:tTaskDelay函数的参数为延时时间对应的SysTick个数
说明2:任务调用tTaskDelay函数设置延时后,需要释放CPU控制权,该动作通过调用tTaskSched函数实现