前言

此篇文章为自己学习FreeRTOS过程中,写下的笔记,学习的是韦东山老师的FreeRTOS快速入门,感觉还行,自己对FreeRTOS也有了一个深刻的理解,并且在学习完课程后,成功把FreeRTOS移植到Stm32F103C8T6上 ,期间还是踩了很多坑的




FreeRTOS主要包括任务的创建,全部都在围绕同步和互斥通信,主要包括队列、信号量、互斥量、事件组、任务通知、定时器、中断管理




和裸机比起来,操作系统还是很高效的,对于刚开始,可能需要浅浅理解一下


目前也只是对FreeRTOS有了一个整体的认识,还是有很多不足




下面是对FreeRTOS,记下的笔记,总字数约23000字 ,在此分享,希望对大家有帮助




往期学习笔记链接


第一弹FreeRTOS学习笔记(1、FreeRTOS初识、任务的创建以及任务状态理论、调度算法等)
第二弹: FreeRTOS学习笔记(2、同步与互斥通信、队列、队列集的使用)
第三弹: FreeRTOS学习笔记(3、信号量、互斥量的使用)
第四弹: FreeRTOS学习笔记(4、事件组、任务通知)
第五弹: FreeRTOS学习笔记(5、定时器、中断管理、调试与优化)


学习工程


所有学习工程
oufen / FreeRTOS学习
都在我的Gitee工程当中,大家可以参考学习


RTOS

实时操作系统,简称为RTOS,是指当外界时间或数据产生时,能够接收并以足够快的速度做出相应处理,其处理的结果又能在规定时间内来控制生产过程或对处理系统快速做出相应

调度一切可以利用的资源完成实时任务,并且控制所有实时性任务协调一致的操作系统

提供及时响应和高可靠性是其主要特点

大体上,实时操作系统要求:

  • 多任务
  • 处理能被区分优先次序的进程栈
  • 一个中断水平的充分数量

由于RTOS需要占用一定的系统资源(尤其是ARM资源),只有uc/os-II、embos、salvo、FreeRtos能够在小RAM单片机上运行

FreeRtos是完全免费的操作系统,具有源码公开,可移植,可裁剪,调度灵活的特点,可以方便的移植到单片机上运行


FreeRtos

FreeRtos是一个迷你的实时操作系统内核,作为一个轻量的操作系统,功能包括:任务管理,时间管理,消息队列,内存管理,记录功能,软件定时器,协程等,可以基本满足较小系统的需要



多任务操作系统和裸机开发的区别





FreeRTOS

可以一边喂饭,一边打字,交替执行
如果发生了某些紧急的事情,将会立马停下所有工作,去灭火,这就是程序的实时性

优先级高的事情可以先处理,优先级一样的东西可以交叉处理



cpu的类别,架构,CPU的内部结构,深入理解RTOS,就要深入了解CPu的架构

双架构和双系统



RTOS的意思是:Real-time operating system,实时操作系统。
我们使用的Windows也是操作系统,被称为通用操作系统。使用Windows时,我们经常碰到程序卡死、停顿的现象,日常生活中这可以忍受。
但是在电梯系统中,你按住开门键时如果没有即刻反应,即使只是慢个1秒,也会夹住人。
在专用的电子设备中,“实时性”很重要。


我们只需要多任务,并不需要文件系统等等,所以FreeRTOS应用最为广泛

而RT_Thread,生态非常完善,从底层的文件系统,网络协议到上层的各种组件都非常丰富




如果只是使用别人移植好的RTOS来写程序,当然不需要了解CPU架构。

甚至编写驱动程序时,也不需要了解CPU架构:因为我们操作的是CPU之外的设备,不是操作CPU。

  • 深入理解RTOS的内部实现
  • 移植RTOS
  • 解决疑难问题

堆和栈的区别


FreeRTOS的核心文件

核心文件

task.c和list.c

task.c 里面是任务的相关函数,包括任务操作
list.c 里面是列表的操作


头文件相关



关于Keil仿真的问题

关于Keil仿真中无法执行的问题,
keil 5的软件仿真遇到的问题:error 65: access violation at 0x40021000 : no ‘read’ permission的解决办法


Dialog DLL改为 DARMSTM.DLL
Parameter改为 -pSTM32F103C8(此项根据具体型号而定)


三个任务交替执行

优先级相同的任务,后创建的先执行


volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改


volatile用来避免被优化掉


FreeRTOS任务的创建

RTOS是多任务系统

优先级相同的情况下要后面的任务要等前面的任务释放串口1才能去使用

多任务在我们的感觉中是同时运行的,但是任务之间是交叉执行的



在FreeRTOS中,任务就是一个函数

void ATaskFunction( void _pvParameters );

  • 这个函数不能有返回值
  • 同一个函数,可以创建多个任务,换句话来说,多个任务可以运行同一个函数
  • 函数内部,尽量使用局部变量
    • 每个任务都有自己的栈
    • 每个任务运行这个函数时
      • taskA的局部变量存放在taskA的栈中,taskB的局部变量存放在taskB的栈中
      • 不同的局部变量,有自己的副本
    • 函数使用全局变量、静态变量的话
      • 多个任务使用的是同一个副本
      • 要防止冲突


创建一个任务函数



在main函数中进行创建task



更高优先级的或是后面创建的task先执行


编程规范和源码结构

1、数据类型

在FreeRTOS中定义了两个基本的数据类型
TickType_t 和 BaseType_t

中断次数累加 tick_count 的数据类型为TickType_t,有可能是16位也有可能是32位,根据处理器来进行选择

BaseType_t 也是基于效率来考虑的,对于32位处理器,BaseType_t 就是32位的

编程时就可以返回这个BaseType_t,是最高效的返回值类型
通常作为简单返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE



2、变量名



根据以上规则来命名

前缀表示类型,后一部分表示含义

比如


const char _ const pcName;

这里的pc 就是 指向char类型的指针,Name是这个变量名的名字

3、函数名



就比如这个函数,返回值类型位BaseType_t ,在Task中定义,Create就代表者这个函数的含义
prvSetupHardware();

static void prvSetupHardware(void)

这里prv就代表这是一个私有函数,即static函数

4、宏定义


动态分配task和静态分配task

任务控制块结构体




对于每一个task,都有一个TCB结构体,TCB_t,这个叫做任务控制块

动态分配,就是使用xTaskCreate,创建任务时,函数内部会使用malloc,从动态内存,即堆中分配出结构体

也可以使用其他函数,使用其他函数时,可以事先分配好这个结构体

一个任务可以简单任务就是一个函数,函数中可能存在各种调用,各种局部变量

局部变量,函数的调用关系存放在栈中,每个任务的栈都不一样,否则栈会互相冲突,乱套

在任务中指定了栈的大小

handle句柄就指向了TCB_t结构体,handle就是一个任务控制块

创建任务所返回的handle就是指向TCB_t的指针写程序时一般使用句柄来表示某一个对象,handle是同种结构体类型的另外的一种名称

静态和动态创建task本质就是

创建任务时,TCB任务块是动态分配的,任务堆栈的大小也是动态分配的,即我们手动给的

静态task,TCB结构体需要我们事先分配好,栈也需要我们事先分配好




任务优先级

FreeRTOS的任务优先级是,数值越小,优先级越低

同优先级的task是交叉执行的



高优先级的task,优先执行
如果高优先级的task没有主动放弃执行的话,其他低优先级的task无法正常执行



高优先级的task优先执行,同等优先级task交替执行

这是一种调度机制



删除task

创建了一个task,如何使用task
传入一个句柄handle,使用handle来引用这个task,想要删除task,就必须要使用handle来删除



使用vTaskDelete();函数可以删除使用xTaskCreate()函数创建的Task

也可以删除xTaskCreateStatic()函数创建的Task,使用一个变量接收句柄,并且记录这个函数的返回值,使用vTaskDelete()函数删除


一个函数创建多个任务




栈的大小要考虑清楚

在创建Task时,要对栈的大小仔细考虑,防止栈溢出,造成程序崩溃



任务状态理论

RTOS多个任务可以同时运行,实际上是多任务交叉执行

实现多任务交替执行的基础是tick中断,滴答中断,周期性的定时器中断

两次中断之间的时间被称为时间片



这个tick大概一次是1ms进行一次

发生中断时,会把中断次数记录下来,发生第一次中断时为1,发生第二次中断时为2,这个称之为tick count ,RTOS的时钟基准



每发生中断时,tick的函数将会被调用,判断是否要切换任务

执行完中断处理函数后,切换到新的task

在FreeRTOS中,每个基本任务的基本时间是1ms,这个基本时间可以由我们自己配置,配置滴答定时器产生中断的周期



这个1000,就代表者是1ms,产生一次中断

FreeRtos的基准tick是1ms

而其他RTOS,可以指定Task执行的tick,比如任务1可以执行10个tick,任务2可以执行5个tick等





任务状态

  1. Running状态 运行

正在运行的Task,成为Running状态,即运行状态,即任务3正在运行

  1. Ready状态 就绪

任务3正在运行,任务1和任务2 处于Ready状态

我可以随时运行,但是还轮不到我

  1. blocked状态 阻塞

等待某事完成后,再运行,妈妈喂饭,等孩子吃完后再喂

而孩子一直在吃,就进入了阻塞状态

  1. suspended 暂停状态 挂起

主动休息,被命令去休息

FreeRTOS中的Task只能有这四种状态之一



就绪(等待高优先级任务结束),运行(当前运行的任务),阻塞(当前任务释放CPU使用权),挂起(任务退出就绪列表)



阻塞状态中,是等待某些事情,而暂停状态中,存粹的是休息

不是因为等待某些事情而进入阻塞状态

在暂停状态中,不是因为要等待某些事情,而是自己主动休息或者被动休息


如何从暂停状态切换成READY状态呢?
必须由别的Task,调用vTaskResume(),传入句柄,从而回到Ready状态

如何从blocked状态切换成READY状态呢?
当这件事情Event发生后,就可以从Blocked状态切换成READY状态

  1. 事件源可以是中断,比如按下按键后,触发中断,发生了某些事情,可以把某些Task唤醒,使其从blocked状态切换成Ready状态
  2. 事件源也可以是其他Task,其他Task完成后,来告诉正在阻塞的事件源,等待的事情已经发生,可以由blocked状态切换成Ready状态

如何管理各个Task
最简单的就是链表

当发生Tick中断时,将会从链表中查找任务,进行任务的切换






任务状态实验

创建三个任务 task1 task2 task3

对于已经进入到暂停状态的task,必须由别人来调用Resume来使其退出暂停状态

task1 调用vTaskSuspend 去命令task3 进入暂停状态
task3 如何转换成Ready状态,再由task1来调用vTaskResume ,使task3进入Ready状态

task2 主动进入阻塞状态 vTaskDelay 等待延时结束,退出阻塞状态,进入Ready状态




task3正在运行running,task1和task2处于就绪Ready状态
task2 延时某段时间,处于阻塞blocked状态
task3被task1命令休息,处于suspended状态



vTaskDelay和vTaskDelayUntil

基于tick实现的延时并不精确,在使用延时时

使用pdMS_TO_TICKS把时间转换为tick




vTaskDelay:指的是阻塞的时间
vTaskDelayUntil:指的是任务执行的间隔、周期等

  • vTaskDelayUntil


使用这个vTaskDelayUntil函数,可以让任务周期性的执行


task刚开始的时间是t1,
vTaskDelayUntil(t1,▲t); 延时到t1+▲t,即t2
指定了终点,延时的时间是不固定的,通过传入的参数来控制delay的延时时间
t1、t2、t3时刻他们之间的间隔是一样的



  • vTaskDelay

延时的时间是固定的,Task延时的时间是固定的




把task1的任务优先级设置为最高
只有他delay的时候,其他任务才可以执行


打印1的时间不同,但是延时的时间是相同的,休眠的时间是一样的,但不能保证每次高电平的时间一样



而vTaskDelayUntil可以解决这个问题,让函数周期执行



tStart 是任务task启动时间,使vTaskDelayUntil将会进入,延时t1+20后,才会退出进入running状态,并且tStart=t2,继续进入下一段,以保证每一次的running后延时的时间是相同的,从而保证任务周期执行





所以

  • vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态
  • vTaskDelayUntil
    • 老版本,没有返回值
    • 等待到指定的绝对时刻,才能变为就绪态。
  • xTaskDelayUntil
    • 新版本,返回pdTRUE表示确实延迟了,返回pdFALSE表示没有发生延迟(因为延迟的时间点早就过了)
    • 等待到指定的绝对时刻,才能变为就绪态。



空闲任务以及钩子函数

在task1中创建task2,并且在task1中把task2删除掉


如果task创建成功,将会返回pdPass





vTaskDelete

  • 自杀 vTaskDelete(NULL)
  • 被杀 别的任务执行,但是vTaskDelate(pvTaskCode) pvTaskCode是自己的句柄
  • 杀人 vTaskDelete(pvTaskCode) pvTaskCode是别人的句柄


对于task的清理,内存的回收工作是放在空闲任务里的

系统有三个任务 ,空闲task的优先级是0,task1的优先级是1,task2的优先级是2

由于task1和task2的优先级都比task的优先级要高,空闲task没有机会执行,无法进行清理

这样就意味者,在task1中不断创建task2,但是清理工作没办法执行,于是堆将会逐渐消耗光,最终导致创建task2失败




空闲任务

空闲任务(Idle任务)的作用:释放被删除的任务的内存。

对于自杀的任务,由空闲task进行清理
对于被杀的任务,由vTaskDelete这个函数内部清理(凶手调用这个函数,由凶手清理)




在启动任务调度器时,会帮助我们创建空闲task

  • 空闲任务的任务优先级为0,它不能阻碍用户的任务运行
  • 空闲任务要么处于就绪Ready状态,要么处于运行状态,永远不会阻塞


创建task1,优先级为1,task1运行时,创建task2,task2的优先级是2
task2优先级最高,优先执行,task2打印一句话然后,就自杀了
task2删除后,task1的优先级最高,task1继续执行,调用delay函数进入阻塞状态
阻塞状态下,idle 空闲函数执行,释放task2的内存和TCB块,延时时间到后,task1继续执行


如果不调用delay函数,则idle函数就没办法执行,无法释放task2的内存
task1就在不断的创建任务,不断的消耗内存,最终内存耗尽,创建task失败




钩子函数

可以让空闲task去执行一些低优先级、后台的、需要连续执行的函数

这个如何实现呢,需要修改空闲任务的task函数

但是最好不要修改FreeRTOS的核心文件,所以给我们提供了一个函数,这个函数就叫做钩子函数



使用钩子函数的话,就要定义宏并且实现钩子函数
且钩子函数有限制

  • 不能导致空闲任务进入阻塞状态、暂停状态 因为空闲任务要执行一些清理工作,阻塞的话就没法清理
  • 空闲任务要么处于Ready状态,要么处于Running状态
  • 钩子任务要执行的越快越好,不要占据太多时间


实现钩子函数

  • 首先定义宏
  • 然后实现钩子函数




设置task1的优先级和idle task的优先级相同 ,这样task1 和 idle就可以交替执行了

可以看到程序并未崩溃,因为idle task和task1交替执行,idle task 执行的话,就会去清理工作,内存就不会被耗尽



  1. 主函数中创建了task1 ,task1创建后立刻执行
  2. 而在task1中创建了task2 ,task2的优先级最高,优先执行,执行完成后,杀掉自己,即删除task2
  3. task2自杀后,task1和idle task交替执行
  4. idle task函数在执行时,会调用我们的钩子函数,钩子函数不能做死循环,否则将会阻塞,idle task 阻塞状态,无法进行内存清理工作



任务调度算法

在单处理系统中,任何时间里只能有一个任务处于运行状态。

优先级高的task优先执行,优先级相同的task交替执行

这是RTOS调度策略中的一种,还有其他调度策略

任务的状态有四种状态

  • Ready 就绪状态
  • Running 运行状态
  • Suspended 暂停状态
  • Blocked 阻塞状态

blocked 状态 在等待某些事情发生
suspended 状态 只是存粹的休息


等待某些事情的发生,又可以分为两类

  1. 时间相关的事件
  2. 同步事件

  • 时间相关的事件
    • 所谓时间相关的事件,就是设置超时时间:在指定时间内阻塞,时间到了就进入就绪状态。 就比如delay
    • 使用时间相关的事件,可以实现周期性的功能、可以实现超时功能。
  • 同步事件
    • 同步事件就是:某个任务在等待某些信息,别的任务或者中断服务程序会给它发送信息。 等待某个数据才能做某些事情,比如某些task或中断给我发来数据,等待别人发送信息
    • 怎么”发送信息”?方法很多
      • 任务通知(task notification)
      • 队列(queue)
      • 事件组(event group)
      • 信号量(semaphoe)
      • 互斥量(mutex)等
      • 这些方法用来发送同步信息,比如表示某个外设得到了数据。

一个任务可以可以因为等待某些事件,而进入阻塞状态



配置调度算法,是否抢占

所谓调度算法,就是怎么确定哪个就绪态的任务可以切换为运行状态。

通过配置文件FreeRTOSConfig.h的两个配置项来配置调度算法:configUSE_PREEMPTION、configUSE_TIME_SLICING。

可否抢占?高优先级的任务能否优先执行(配置项:configUSE_PREEMPTION)
让此宏为1,就会让高优先级任务优先执行,抢占策略



  1. 抢占模式,可抢占调度

配置项:configUSE_PREEMPTION 默认为1
task3的优先级最高,优先执行,delay后阻塞
其他task 抢占cpu资源,进行交替执行
当延时结束,task3由阻塞状态转换为运行状态,再次优先执行


  1. 合作调度模式

不能抢占就只能协商,

task3优先级最高,优先执行,delay后阻塞
然后task1执行,一直占据CPU资源,这种调度策略就是不支持抢占


修改配置项(:configUSE_PREEMPTION) 为0


如果想使用此种策略,并且不允许抢占的话,每次task中做完一些事情后,马上主动放弃CPU资源



时间片轮转,同优先级task是否轮流执行

对于同优先级的task,交替执行,这也是一个可以选择的配置项

可以通过此配置项来决定同种优先级的task是否交替执行

将此配置项修改为0 同优先级task无法交替执行,不支持时间片轮转

先到先得CPU资源,谁先执行,就一直占用CPU资源,task1执行了很久很久,应该是task1,idle task,task2,交替轮流执行一个tick,何时放弃的CPU资源,被task3抢占后放弃CPU资源

引起任务调度器进行调度,从而task2执行




默认情况下,都应该去支持时间片轮转


空闲task是否让步于用户task



空闲task,任务创建后
如果配置了让步的宏,在while循环里只会循环一次,循环一次后就触发一次调度,主动让出CPU资源
如果未配置的话,将会在while循环里多次循环


下方为空闲task的实现函数



  • 在空闲task循环中,主动触发调度器,让出CPU资源
  • 空闲task每循环一次,就调用taskYIELD()函数一次,让出CPU资源

空闲task,在while循环中循环一次后,就让出了CPU资源



将配置项修改为0,设置为不礼让用户task
也抢占CPU资源



总结

  • 是否允许抢占

    • 允许,高优先级task,优先执行
    • 不允许,谁先执行,就一直占用CPU资源,除非主动放弃CPU资源或者进入阻塞状态,高优先级的task也无法继续抢占
  • 允许抢占的情况下,是否允许时间片轮转


    • 允许,同优先级task交替执行
    • 不允许,先到先得CPU资源,谁先执行,就一直占用CPU资源,task1执行了很久很久,应该是task1,idle task,task2,交替轮流执行一个tick,何时放弃的CPU资源,被task3抢占后放弃,被任务调度器调度后task2继续执行
  • 允许抢占,允许时间片轮转的情况下,空闲task是否让步用户task


    • 让步,每次运行空闲task时,将会调用yield函数,让出CPU资源
    • 不让步,将会抢占CPU资源,和用户task抢占,波形宽度差不多