Main Takeaway
Following 哈工深上传到B站的电控组培训来入门robomaster电控组,我购买了普中科技玄武套餐开发板作为硬件。
本篇介绍我学习FreeRTOS的见闻,自己看FreeRTOS中文参考手册进行琢磨。
FreeRTOS(1)——任务管理
Tips:抱歉托更,最近有点懒,检讨!
操作系统简介
operating system
操作系统 (Operating System) 的本质是一个帮助用户进行功能管理的软件。操作系统运行在硬件之上,为其他工作的软件执行资源分配等管理工作
一般称呼不使用操作系统的单片机开发方式为“裸机开发”,当进行裸机开发时,需要自己设计循环,中断,定时等功能来控制各个任务的执行顺序。
而使用操作系统进行开发时,只需要创建任务,操作系统会自动按照一些特定的机制自动进行任务的运行和切换。
除了任务管理之外,操作系统还可以提供许多功能,比如各个任务之间的通信,同步,任务的堆栈管理,控制任务对重要资源的互斥访问等。
Tips:一般在单片机上运行的是经过专门设计的嵌入式实时操作系统 (RTOS)
freeRTOS 操作系统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行,并且有着庞大的社区和生态。
任务管理
任务函数
任务是由 C 语言函数实现的。唯一特别的只是任务的函数原型,其必须返回 void,而且带有一个 void 指针参数。
任务函数原型:1
void ATaskFunction( void *pvParametera);
典型的任务函数结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void ATaskFunction( void *pvParametera)
{
/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变
量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务实
例将会共享这个变量。 */
int iVariableExample = 0;
/* 任务通常实现在一个死循环中。 */
for( ;; )
{
/* 完成任务功能的代码将放在这里。 */
}
/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除
的是当前任务 */
vTaskDelete( NULL );
}
每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一个死循环中,也不会退出。
FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条 ”return” 语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删除。
顶层任务状态
运行状态+非运行状态(有很多子状态,后面会介绍)
如果运行应用程序的微控制器只有一个核(core),那么在任意给定时间,实际上只会有一个任务被执行。
任务从非运行态转移到运行态被称为”切换入或切入(switched in)”或”交换入(swapped in)”。
任务从运行态转移到非运行态被称为”切换出或切出(switched out)”或”交换出(swapped out)”。
Notes:FreeRTOS 的调度器是能让任务切入切出的唯一实体。
创建任务
API:xTaskCreate();
很复杂,参数很多
任务的实现放在任务函数中,main()函数只是简单地创建这两个任务,然后启动调度器执行任务。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int main( void )
{
/* 创建第一个任务。 需要说明的是一个实用的应用程序中应当检测函数xTaskCreate()的返回值,以确保任
务创建成功。 */
xTaskCreate( vTask1, /* 指向任务函数的指针 */
"Task 1", /* 任务的文本名字,只会在调试中用到 */
1000, /* 栈深度 – 大多数小型微控制器会使用的值会比此值小得多 */
NULL, /* 没有任务参数 */
1, /* 此任务运行在优先级1上. */
NULL ); /* 不会用到任务句柄 */
/* Create the other task in exactly the same way and at the same priority. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 启动调度器,任务开始执行 */
vTaskStartScheduler();
/* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
for( ;; );
}
Tips:两个任务在同时运行,但实际上这两个任务运行在同一个处理器上,所以不可能会同时运行。事实上这两个任务都迅速地进入与退出运行态。由于这两个任务运行在同一个处理器上,所以会平等共享处理器时间(如果只有一个core的话)
Note:也可以在main中创建task1,在task1中创建task21
2
3
4
5
6
7void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
/* 如果已经执行到本任务的代码,表明调度器已经启动。在进入死循环之前创建另一个任务。 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
//后面代码省略
Notes:也可以用相同任务函数,创建任务时不同的任务传递不同的参数即可
任务优先级
xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这个侁先级可以在调度器启动后调用 vTaskPrioritySet() API 函数进行修改。
优先级数目由文件 FreeRTOSConfig.h中的常量configMAX_PRIORITIES 配置
Tips:值越大占用内存越多,所以常设为满足需要的最小值。
保证最大设计弹性:让任务共享同一个优先级
低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范围从 0 到(configMAX_PRIORITES – 1)。
时间片:
时间片由文件 FreeRTOSConfig.h中的常量configTICK_RATE_HZ配置
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本身。一个称为心跳(tick,or时钟滴答)中断的周期性中断用于此目的。
Tips:FreeRTOS API 函数调用中指定的时间总是以ticks中断为单位。常量 portTICK_RATE_MS 用于将以心跳为单位的时间值转化为以毫秒为单位的时间值。有效精度依赖于系统心跳频率。
扩充“非运行态”
事件驱动任务
- 阻塞状态:一个任务正在等待某个事件——blocked阻塞态
- 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如说某个任务可以进入阻塞态以延迟10ms。(利用blocked进行延时)
- 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞态以等待队列中有数据到来。同步事件囊括了所有板级范围内的事件类型。
Tips:任务可以在进入阻塞态以等待同步事件时指定一个等待超时时间,这样可以有效地实现阻塞状态下同时等待两种类型的事件。
- 挂起状态:suspended,处于挂起状态的任务对调度器而言是不可见的。
- vTaskSuspend() 进入挂起状态
- vTaskResume() 或vTaskResumeFromISR() 唤醒挂起状态任务
- 就绪状态:ready

延时:
vTaskDelay():
使用空循环进行延时非常不好,可能将其他任务starved,也会造成MCU的浪费,我们用vTaskDelay() API 函数来代替空循环。
调用该延迟函数的任务将进入阻塞态,经延迟指定的心跳周期数后,再转移到就绪态。(即调用改函数延时期间会执行其他任务)
Tips:接收参数是ticks,我们用常数 portTICK_RATE_MS 将以毫秒为单位的时间值转换为以心跳周期为单位的时间值
实现250ms的延时:1
vTaskDelay( 250 / portTICK_RATE_MS );
vTaskDelayUntil:
(假定用于实现某个任务以固定频率周期性执行)比调用 vTaskDelay()可以实现更精确的周期性,保证它们具有固定的执行频率1
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
- pxPreviousWakeTime:保存了任务上一次离开阻塞态(被唤醒)的时刻。这个时刻被用作一个参考点来计算该任务下一次离开阻塞态的时刻。
Tips:只用一次初始化,该参数会在函数调用过程中自动更新。
- xTimeIncrement:假定用于实现某个任务以固定频率周期性执行,该频率则由xTimeIncrement指定
Tips:接收参数是ticks,我们用常数 portTICK_RATE_MS 将以毫秒为单位的时间值转换为以心跳周期为单位的时间值

空闲任务
处理器总是需要代码来执行——所以至少要有一个任务处于运行态。为了保证这一点,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务(优先级0,最低)。 空闲任务是一个非常短小的循环,总是可以运行的。
空闲任务钩子函数1
void vApplicationIdleHook( void );
空闲任务钩子函数(或称回调, hook, or call-back),可以直接在空闲任务中添加应用程序相关的功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。
FreeRTOSConfig.h 中的配置常量 configUSE_IDLE_HOOK 必须定义为 1,这样空闲任务钩子函数才会被调用。
常被用于:
执行低优先级,后台或需要不停处理的功能代码。
测试处系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式。
hook实现限制:
绝不能阻塞或挂起。
Tips:空闲任务只会在其它任务都不运行时才会被执行(除非有应用任务共享空闲任务优先级)。以任何方式阻塞空闲任务都可能导致没有任务能够进入运行态!
如果应用程序用到了 vTaskDelete() API 函数,则空闲钩子函数必须能够尽快返回。因为在任务被删除后,空闲任务负责回收内核资源。如果空闲任务一直运行在钩子函数中,则无法进行回收工作
改变任务优先级
vTaskPrioritySet()可以用于在调度器启动后改变任何任务的优先级1
2
3void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );
//pxTask是任务句柄,Tips:任务中可以传入Null来修改自己的优先级
//uxNewPriority设置新的优先级
uxTaskPriorityGet() API 函数用于查询一个任务的优先级
Tips:也可以在函数中传入null来查询自己的优先级
删除任务
任务可以使用API函数 vTaskDelete()删除自己或其它任务。空闲任务的责任是要将分配给已删除任务的内存释放掉。
Tips:所以使用该API的任务不能让空闲任务starved
Notes:只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显式地释放(有点不太理解)
调度算法
Notes:<通用规则>完成硬实时功能的任务优先级会高于完成软件时功能任务的优先级
固定优先级抢占式调度:之前介绍的即为这种调度方法
选择任务优先级
单调速率调度(Rate Monotonic Scheduling, RMS)是一种常用的优先级分配技术。其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。这种优先级分配方式被证明了可以最大化整个应用程序的可调度性(schedulability)
TIps:但是运行时间不定以及并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。
协作式调度
采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显式调用 taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。
Tips:simple but slow
混合调度
这需要在中断服务例程中显式地进行上下文切换,从而允许同步事件产生抢占行为,但时间事件却不行。这样做的结果是得到了一个没有时间片机制的抢占式系统。或许这正是所期望的,因为获得了效率,并且这也是一种常用的调度器配置。
References
- 哈工深电控内训
- FreeRTOS中文参考手册