[FreeRTOS] 初识FreeRTOS

一、介绍

裸机特点: 前后台系统, 前台主循环, 后台中断服务函数. 无法做到实时性高, CPU的工作被大量的浪费在延时中. 

  • 必须等待前面的操作完成才能干做前台想要做的工作, 实时性差.
  • 如果使用后台中断完成, CPU被大量工作于中断状态, 导致其他系统出现问题, 最终程序崩溃.
  • delay等延时函数, CPU用来做无意义的事情, 等待着一个数一个数的累加, 浪费CPU资源.
  • 程序全部都在前台大循环中, 导致结构臃肿, 容易出错.

FreeRTOS特点: 简单, 免费, 开源, 可裁剪. 实时性高, 充分利用CPU资源.

  • 划分为多个任务, 类似于多线程, 由任务调度器来合理调度任务, 达到实时性的效果.
  • 延时函数, 使用SysTick中断触发, 不占用CPU资源, 任务在受到延时的时候会进入阻塞状态, 任务调度器会分配其他就绪态任务让给CPU, 充分利用CPU资源.
  • 抢占式优先级, 利用PendSV中断服务函数, 实现切换更高优先级的任务. 
  • 优先级个数不限: 软件层面不限, 硬件层面限制, stm32最多32个0-31, 数字越大优先级越高.
  • 任务数量不限: 软件层面不限制, 硬件层面要看自己的RAM大小.

二、任务调度器

FreeRTOS任务调度器, 并不是硬件层面, 而是软件层面实现的, 它的工作内容是: 使用相关的调度算法来决定当前需要执行哪个任务. FreeRTOS支持三种调度方式: 抢占式调度, 时间片调度, 和协程式调度.

2.1 抢占式调度

任务的创建的时候定义任务的优先级, 优先级高的任务将会抢占优先级低的任务的运行状态, 实现了快速响应的效果. 抢的任务进入运行态, 被抢的任务进入就绪态, 等待高优先级任务完成.

抢占式调度和协程式调度的关系是互斥的, 可以在配置文件FreeRTOSConfig.h文件中进行配置.

#define configUSE_PREEMPTION		1  //支持优先级抢占

2.2 时间片调度

时间片调度, 实现了单核多线程的效果, 它和其他两种调度方式共存, 没有冲突关系, 快速的切换任务, 每一个任务都会执行一个时间切片, 快速的切换, 给人一种任务同时进行的感受. 保证了实时性的数据处理, 但是速度会慢一些.

工作对象: 同优先级的任务, 会在一个时间片的时间来回切换.

实现的原理: 使用芯片的系统滴答定时器中断, 每一次中断都是一个时间片, 再调用任务调度器来切换任务, 所以需要配置系统时钟保持一致, 否则会出现时钟不准确的效果.

#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时钟频率

2.3 协程式调度(基本弃用)

不允许任务发生抢占, 但是支持时间片轮转, 官方已经不再更新了. 任务的切换可以从以下两个方面来看.

  • 高优先级和低优先级任务: 高优先级任务不能抢占低优先级任务, 所以需要等到时间片轮转到高优先级任务的时候, 才可以执行, 丧失了实时性. 
  • 同等优先级任务: 没有其他缺点了, 因为不存在抢占关系.

由此可以看出来, 优先级不能很好的起作用了, 无法做到紧急事件紧急处理, 实时操作系统形同虚设. 

#define configUSE_PREEMPTION		0  //关闭优先级抢占

三、列表(List)

内部逻辑

列表是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 ) //最小任务栈的大小

三个寄存器的介绍 

  • SP: 堆栈指针R13.

随机存储器区划出一块区域作为堆栈区,数据可以一个个顺序地存入(压入)到这个区域之中,这个过程称为‘压栈’(push )。通常用一个指针(堆栈指针 SP---StackPointer)实现做一次调整,SP总指向最后一个压入堆栈的数据所在的数据单元(栈顶)。从堆栈中读取数据时,按照堆栈 指针指向的堆栈单元读取堆栈数据,这个过程叫做 ‘弹出’(pop ),每弹出一个数据,SP 即向相反方向做一次调整,如此就实现了后进先出的原则。

  • LR: 链接寄存器R14. 当调用子函数时候, 由R14存储返回值地址. 

不像大多数其它处理器, ARM 为了减少访问内存的次数(访问内存的操作往往要 3 个以上指令周期,带 MMU 和cache 的就更加不确定了),把返回地址直接存储在寄存器中。这样足以使很多只有 1 级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于 1 级,则需要把前一级的 R14 值压到堆栈里。在 ARM上编程时,应尽量只使用寄存器保存中间果,迫不得以时才访问内存。在 RISC 处理器中,为了强调访内操作越过了处理器的界线,并且带来了对性能的不利影响,给它取了一个专业的术语:溅出。
 

  • PC: 程序计数器R15.

指向当前的程序地址。如果修改它的值,就能改变程序的执行流(很多高级技巧就在这里面—
—译注)。

划分栈

在stm32中会先执行startup汇编文件, 在文件中会指定SP = 0x20000000+0x100. 然后跳转到main函数. 假设main函数中出现了如下所示的调用关系, 划分栈的示意图如下.

int main(void)
{
    aFun();
    return 0;
}

void aFun(void)
{
    bFun();
    cFun();
}

void bFun(void)
{
    cFun();
}

 [FreeRTOS] 初识FreeRTOS_第1张图片

六、任务(Task)

中断是可以打断任务的, 任务就是一个线程, 每一个任务都相当于逻辑里的大循环程序, 虽然也有优先级, 抢占等, 但是不属于中断, 是操作系统层面的应用, 而中断属于芯片硬件层面的, 不可混为一谈. 

任务的四种状态

  • 运行态: 在FreeRTOS中, 同一时间只能有一个.
  • 就绪态: 能进入运行态, 但没进入的排队状态.
  • 阻塞态: 等待外部事件发生, 如: vTaskDelay()延时进入阻塞态, 延时结束进入就绪态.
  • 挂起态(暂停态): 需要主动挂起或进入就绪态, vTaskSuspend()挂起, vTaskResume()就绪.

 四种状态转换图:

[FreeRTOS] 初识FreeRTOS_第2张图片

四种状态, 除了运行态, 其他三种状态都有各自的列表, 阻塞态会将列表排好顺序,  就绪态会有很多列表, 默认是5个, 与优先级个数相等.

[FreeRTOS] 初识FreeRTOS_第3张图片

如果就绪任务进入了运行态, 导致某一位没有了就绪任务, 该位将会清零. 

任务优先级

高优先级任务会抢占CPU运行, 使得低优先级任务无法运行. 解决方法: 让其进入阻塞态, 或者手动切换任务.

同优先级: 时间片调度.

操作的对象: 就绪列表的任务.

任务控制块(TCB)

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中断, 去执行写好的中断服务函数, 在里面实现任务切换.

空闲任务(Idle Task)

系统保持必须每时每刻都有一个可以运行的任务, 没有的话, 运行空闲任务. 空闲任务的优先级为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中, 临界段最常用于全局变量的操作. 防止中断修改全局变量, 导致程序乱套.

任务可以被打断的两种方式: 

  • 中断.
  • 系统调度: 系统调度也是通过PendSV中断来实现的任务切换, 也属于中断.

中断屏蔽寄存器

 Cortex -M内核, 有特殊功能寄存器, 其中有中断屏蔽寄存器.

[FreeRTOS] 初识FreeRTOS_第4张图片

[FreeRTOS] 初识FreeRTOS_第5张图片 汇编指令:

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来进行关中断.

FreeRTOS中实现

 在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.

九、配置文件(FreeRTOSConfig.h)

 可以参考官方文档自己编写, 也可以复制demo工程里面的.

 官方文档

FreeRTOS - The Free RTOS configuration constants and configuration options - FREE Open Source RTOS for small real time embedded systemsFreeRTOS is a portable, open source, mini Real Time kernel. A free RTOS for small embedded systems. This page describes and explains the constants used to configure FreeRTOS.icon-default.png?t=N7T8https://www.freertos.org/a00110.html

你可能感兴趣的:(FreeRTOS,单片机,嵌入式硬件,FreeRTOS)