裸机特点: 前后台系统, 前台主循环, 后台中断服务函数. 无法做到实时性高, CPU的工作被大量的浪费在延时中.?
FreeRTOS特点: 简单, 免费, 开源, 可裁剪. 实时性高, 充分利用CPU资源.
FreeRTOS任务调度器, 并不是硬件层面, 而是软件层面实现的, 它的工作内容是: 使用相关的调度算法来决定当前需要执行哪个任务. FreeRTOS支持三种调度方式: 抢占式调度, 时间片调度, 和协程式调度.
任务的创建的时候定义任务的优先级, 优先级高的任务将会抢占优先级低的任务的运行状态, 实现了快速响应的效果. 抢的任务进入运行态, 被抢的任务进入就绪态, 等待高优先级任务完成.
抢占式调度和协程式调度的关系是互斥的, 可以在配置文件FreeRTOSConfig.h文件中进行配置.
#define configUSE_PREEMPTION 1 //支持优先级抢占
时间片调度, 实现了单核多线程的效果, 它和其他两种调度方式共存, 没有冲突关系, 快速的切换任务, 每一个任务都会执行一个时间切片, 快速的切换, 给人一种任务同时进行的感受. 保证了实时性的数据处理, 但是速度会慢一些.
工作对象: 同优先级的任务, 会在一个时间片的时间来回切换.
实现的原理: 使用芯片的系统滴答定时器中断, 每一次中断都是一个时间片, 再调用任务调度器来切换任务, 所以需要配置系统时钟保持一致, 否则会出现时钟不准确的效果.
#define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) //系统时钟频率
#define configUSE_16_BIT_TICKS 0 //0使用32位定时器,1使用16位定时器
#define configSYSTICK_CLOCK_HZ ( configCPU_CLOCK_HZ / 8) //配置SysTick时钟频率
不允许任务发生抢占, 但是支持时间片轮转, 官方已经不再更新了. 任务的切换可以从以下两个方面来看.
由此可以看出来, 优先级不能很好的起作用了, 无法做到紧急事件紧急处理, 实时操作系统形同虚设.?
#define configUSE_PREEMPTION 0 //关闭优先级抢占
列表是FreeRTOS非常重要的数据结构, 内部逻辑就是: 双向循环带头单链表, 头结点始终指向最后一个节点, 每一个节点又全都指向头节点.?
在源码中 List_t 类型结构体为头结点, 被称为列表, ListItem_t 类型结构体为节点, 被称为列表项.
源码结构如下所示.?
//列表项
struct xLIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue; //设置节点参考值,用于排序
struct xLIST_ITEM * configLIST_VOLATILE pxNext; //指向下一个节点
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; //指向前一个节点
void * pvOwner; //指向TCB
struct xLIST * configLIST_VOLATILE pxContainer; //指向自己所在的列表
};
typedef struct xLIST_ITEM ListItem_t;
//列表
typedef struct xLIST
{
volatile UBaseType_t uxNumberOfItems; //列表项的个数
ListItem_t * configLIST_VOLATILE pxIndex; //始终指向最后一个列表项, 没有的话指向自己的最小项列表
MiniListItem_t xListEnd; //最小列表项, 用于开头, 也用于结束
} List_t;
//最小列表项的选择, 默认开启
#if ( configUSE_MINI_LIST_ITEM == 1 )
struct xMINI_LIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue;
struct xLIST_ITEM * configLIST_VOLATILE pxNext;
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
#else
typedef struct xLIST_ITEM MiniListItem_t;
#endif
列表被广泛应用在任务中, 任务的状态列表, 等等, 都是基于这个数据结构实现的, 非常具有参考意义.
分配空闲的内存, 取出来使用, 由程序员申请和释放.
栈区是编译器分配的, 由编译器自动完成分配和释放的操作.?RTOS中每一个任务都有自己的栈.
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) //最小任务栈的大小
在随机存储器区划出一块区域作为堆栈区,数据可以一个个顺序地存入(压入)到这个区域之中,这个过程称为‘压栈’(push )。通常用一个指针(堆栈指针?SP---StackPointer)实现做一次调整,SP总指向最后一个压入堆栈的数据所在的数据单元(栈顶)。从堆栈中读取数据时,按照堆栈 指针指向的堆栈单元读取堆栈数据,这个过程叫做 ‘弹出’(pop ),每弹出一个数据,SP 即向相反方向做一次调整,如此就实现了后进先出的原则。
不像大多数其它处理器, ARM 为了减少访问内存的次数(访问内存的操作往往要 3 个以上指令周期,带 MMU 和cache 的就更加不确定了),把返回地址直接存储在寄存器中。这样足以使很多只有 1 级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于 1 级,则需要把前一级的 R14 值压到堆栈里。在 ARM上编程时,应尽量只使用寄存器保存中间果,迫不得以时才访问内存。在 RISC 处理器中,为了强调访内操作越过了处理器的界线,并且带来了对性能的不利影响,给它取了一个专业的术语:溅出。
?
指向当前的程序地址。如果修改它的值,就能改变程序的执行流(很多高级技巧就在这里面—
—译注)。
在stm32中会先执行startup汇编文件, 在文件中会指定SP = 0x20000000+0x100. 然后跳转到main函数. 假设main函数中出现了如下所示的调用关系, 划分栈的示意图如下.
int main(void)
{
aFun();
return 0;
}
void aFun(void)
{
bFun();
cFun();
}
void bFun(void)
{
cFun();
}
?
中断是可以打断任务的, 任务就是一个线程, 每一个任务都相当于逻辑里的大循环程序, 虽然也有优先级, 抢占等, 但是不属于中断, 是操作系统层面的应用, 而中断属于芯片硬件层面的, 不可混为一谈.?
?四种状态转换图:
四种状态, 除了运行态, 其他三种状态都有各自的列表, 阻塞态会将列表排好顺序, ?就绪态会有很多列表, 默认是5个, 与优先级个数相等.
如果就绪任务进入了运行态, 导致某一位没有了就绪任务, 该位将会清零.?
高优先级任务会抢占CPU运行, 使得低优先级任务无法运行. 解决方法: 让其进入阻塞态, 或者手动切换任务.
同优先级: 时间片调度.
操作的对象: 就绪列表的任务.
TCB: Task Control Block, 相当于任务的身份证, 里面具有任务的基本信息. 基础代码如下.
typedef struct tskTaskControlBlock
{
volatile StackType_t * pxTopOfStack; //栈顶指针
ListItem_t xStateListItem; //任务状态列表项
ListItem_t xEventListItem; //任务事件列表项
UBaseType_t uxPriority; //任务优先级
StackType_t * pxStack; //起始栈地址
char pcTaskName[ configMAX_TASK_NAME_LEN ]; //任务名称
} tskTCB;
?任务切换函数, 切换之后会通过任务调度器在就绪列表中寻找高优先级任务, 或者同优先级任务按照时间片调度.
taskYIELD();
?任务切换函数的实质就是把PendSV里的悬起位置1, 当没有其它中断运行的时候, 响应PendSV中断, 去执行写好的中断服务函数, 在里面实现任务切换.
系统保持必须每时每刻都有一个可以运行的任务, 没有的话, 运行空闲任务. 空闲任务的优先级为0, 是最低的. 是在启动Task Scheduler时创建的, 函数主体主要是: 系统内存的清理工作, 钩子函数(用户调用做想做的).?
空闲任务只能在就绪状态和运行状态, 因为这有这样才能保证系统每时每刻都有一个可以运行的任务.
?vTaskDelay(); 软件延时-->CPU空等待, 无意义, 阻塞延时任务会放弃CPU的使用权, CPU可以去干其他的事情, 充分地利用了CPU资源, 阻塞延时时间到, 进入就绪状态.
?FreeRTOS具有两个任务延时列表, 当任务需要延时的时候, 则先将任务挂起, 从就绪列表中删除, 插入到任务延时列表, 同时更新下一个任务解锁时刻变量
xNextTaskUnblockTime = xTickCount + xTicksToDelay.
当xTickCount ==?xNextTaskUnblockTime, 该任务转入就绪列表
?任务延时列表会按照延时时间大小, 做升序排列, 这样就不用每次都扫描所有任务了.
第二个列表, 用于xTickCount溢出时, 没有溢出用一个列表.
static List_t xDelayedTaskList1;? ? ? ? ? ? ? ? ? ? ? ?
static List_t xDelayedTaskList2;? ? ? ? ? ? ? ? ? ? ? ?
static List_t * volatile pxDelayedTaskList;? ? ? ? ? ? ??
static List_t * volatile pxOverflowDelayedTaskList;? ? ??
概念: 一段在执行的时候不能被中断的代码段. 在FreeRTOS中, 临界段最常用于全局变量的操作. 防止中断修改全局变量, 导致程序乱套.
任务可以被打断的两种方式:?
?Cortex -M内核, 有特殊功能寄存器, 其中有中断屏蔽寄存器.
?汇编指令:
CPSID? ?I? ? ;PRIMASK=1,关中断,只剩下NMI和硬FAULT可以响应
CPSIE? ?I? ? ;? ? ? ? ? ? ? ? ?=0, 开中断
CPSID? ?F? ?;FAULTMASK=1, 关异常?
CPSIE? ?F? ?;? ? ? ? ? ? ? ? ? ? ?=0, 开异常
?BASEPRI: 寄存器最多有9位, 由表达优先级的位数决定. 它定义了被屏蔽优先级的阈值, 当它被设置为某个值时, 所有优先级号大于等于此值的中断都被关闭, (优先级号越大, 优先级越低), 若被设置为0, 则不关闭任何中断, 0也是缺省值.
PRIMASK和FAULTMASK都只有1位.
FreeRTOS使用BASEPRI来进行关中断.
?在portmacro.h中定义.
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
//无返回值, 不能嵌套, 不能在中断里面使用
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR( x ) vPortSetBASEPRI( x )
//有返回值, 可以嵌套, 可以在中断里面使用
?FROM_ISR结尾的函数/宏定义, 都是在中断中使用的, FreeRTOS命名规范.
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191
?上面的配置代码为: 配置屏蔽的优先级, 191 = 0xbf, 高四位有效, 所以等于0xb0 = 11, 含义是, 默认中断优先级号高于等于11的会被屏蔽, 小于11的会被响应(这里优先级号越大, 优先级越高)
在FreeRTOS中, 一个时间片就等于SysTick中断周期, 每个任务只运行一个时间片, 同等优先级任务来回切换运行. 其他RTOS如uC-OS可以有多个并且每个任务可以时间片数量不同.
?如果一个时间片没有结束, 任务进入了阻塞态或者挂起等需要切换任务的操作, 后续的任务不会接上那半个时间片, 而是重新开启一个时间片, 继续执行, 不会等待.
#define xPortSysTickHandler SysTick_Handler
时间片节拍: SysTick中断, 同优先级按时间片节拍, 交替切换Task.
?可以参考官方文档自己编写, 也可以复制demo工程里面的.
?官方文档