早期的嵌入式开发没有操作系统的概念,只有裸机程序,比如在最简单的51上面基本就没有操作系统的说法。
对于裸机程序,可以看作两部分:前台系统与后台系统。形象的理解为:
一般的裸机程序都是一个大的无限循环(while和for),这种一直在运行的程序就称为“后台”;
中断(包括中断服务程序),可以打断后台程序继而自己执行,所以称之为“前台”
下面来介绍实时操作系统。
实时:在一定时间内完成某一项任务,实时里面又分硬实时和软实时。硬实时要求这项操作必须在时间内完成,不能超时,软实时要求没有那么严格,主要体现在软实时系统超时后对整个系统造成的后果不会很严重,相反,如果硬实时操作系统中的实时任务超时,会带来灾难性的后果。所以,实时程度也是衡量一个RTOS性能的指标。
目前市面上的实时操作系统有:ucos,FreeRTOS,RT-Thread等。
RTOS的核心是实时内核,内核管理着所有的任务,决定运行哪个任务与任务的切换时机,这叫做多任务管理。
可剥夺指的就是CPU的使用权是可以被剥夺的,从优先级低的任务那里剥夺,被优先级高的任务剥夺,所以,CPU总是运行就绪任务中优先级最高的那个。而这其中,中断的优先级又是最高的,如下图所示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VR8ZOdPl-1615271769636)(C:\Users\li wenbo\AppData\Roaming\Typora\typora-user-images\image-20210111101247472.png)]
FreeRTOS是一个可剪裁的,可剥夺的多任务实时内核的操作系统,他管理的任务数没有上限,它提供了所有实时操作系统的功能,包括资源管理,同步,任务通信。而且,FreeRTOS是用C语言和少量汇编编写的,结构整洁,可读性强,最重要的一点,如其名,它是开源的,免费的,所以它的应用非常广泛。
注:ESP32在环境搭建时就自动移植了FreeRTOS
任务函数由C语言实现。
唯一特别的只是任务的函数原型,其必须返回void,而且带有一个void指针函数。
任务函数原型:
void ATaskFunction(void *pvParameters)
(1)每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一个死循环中,也不会退出。FreeRTOS任务不允许以任何方式从实现函数中返回,即不允许有“return”语句,也不能执行到函数末尾。
(2)如果一个任务不再需要,可以显式地将其删除。
(3)一个任务函数可以用来创建若干任务,创建出的任务均是独立的执行实例,拥有属于自己的栈空间以及栈变量(自动变量,即任务函数本身定义的变量)。
典型的任务函数结构:
void ATaskFunction( void *pvParameters )
{
/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务实例将会共享这个变量。 */
int iVariableExample = 0;
/* 任务通常实现在一个死循环中。 */
for( ;; )
{
/* 完成任务功能的代码将放在这里。 */
}
/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除的是当前任务 */
vTaskDelete( NULL );
}
任务从非运行态转移到运行态被称为“切换入或切入”或“交换入”;任务从运行态转移到非运行态被称为“切换出或切出”或“交换出”。FreeRTOS 的调度器是能让任务切入切出的唯一实体。
创建任务使用 FreeRTOS 的 API 函数 xTaskCreate()。
xTaskCreate()API函数原型
(描述用到的数据类型和命名约定)
xTaskCreate(
pdTASK_CODE pvTaskCode, //任务只是永不退出的C函数,效果上仅仅是函数名
const signed portCHAR * const pcName, //具有描述性的任务名,用于辅助调试
unsigned portSHORT usStackDepth, //当任务创建时,内核会为每个任务分配属于任务自己的唯一状态。usStackDepth值用于告诉内核为它分配多大的栈空间(这里指可以保存多少个字)。
void *pvParameters, //任务函数接受一个指向 void 的指针(void*)。pvParameters的值即传递到任务中的值。
unsigned portBASE_TYPE uxPriority, //指定任务执行的优先级。优先级的取值范围可以从最低优先级0到最高优先级(configMAX_PRIORITIES<一个由用户定义的常量> – 1)。
xTaskHandle *pxCreatedTask pxCreatedTask //用于传出任务的句柄。这个句柄将在API调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。如果应用程序中不会用到这个任务的句柄,则 pxCreatedTask 可以被设为 NULL。
);
//有两个可能的返回值
//1. pdTURE 表明任务创建成功。
//2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 由于内存堆空间不足,无法创建任务。
示例
int 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( ;; );
}
xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这个优先级可以在调度器启动后调用 vTaskPrioritySet() API 函数进行修改。
如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己的本身。一个称为心跳(tick,有的地方称为时钟滴答)中断周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由FreeRTOSConfig.h 中的编译时配置常量 configTICK_RATE_HZ 进行配置。比如说 configTICK_RATE_HZ 设为100(HZ),则时间长度为10 ms。
需要说明的是,FreeRTOS API 函数调用中指定的时间总是以心跳中断为单位(通常的提法为心跳”ticks”)。常量 portTICK_RATE_MS 用于将以心跳为单位的时间值转化为以毫秒为单位的时间值。有效精度依赖于系统心跳频率。
心跳计数(tick count)值表示的是从调度器启动开始,心跳中断的总数,并假定心跳计数器不会溢出。
为了使我们的任务切实有用,我们需要通过某种方式来进行事件驱动。一个事件驱动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级上,并且最高优先级任务不会把所有的低优先级任务饿死。
如果一个任务正在等待某个事件,则称这个任务处于”阻塞态(blocked)”。
等待的事件:(1)定时事件 (2)同步事件
处于挂起状态的任务对调度器而言是不可见的。让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() API 函数;而把一个挂起状态的任务唤 醒的唯一途径就是调用 vTaskResume() 或
vTaskResumeFromISR() API 函数。大多数应用程序中都不会用到挂起状态。
如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready,准备或就绪)状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwVnTtU4-1615271769641)(C:\Users\li wenbo\AppData\Roaming\Typora\typora-user-images\image-20210111163946091.png)]
调用 vTaskDelay() API 函数来代替空循环,函数原型:
void vTaskDelay( portTickType xTicksToDelay );
/*延迟多少个心跳周期。调用该延迟函数的任务将进入阻塞态,经延迟指定的心跳周期数后,再转移到就绪态。
举个例子,当某个任务调用 vTaskDelay( 100 )时,心跳计数值为 10,000,则该任务将保持在阻塞态,直到心跳计数计到10,100。
常数 portTICK_RATE_MS 可以用来将以毫秒为单位的时间值转换为以心跳周期为单位的时间值。
程序如下
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
/* The string to print out is passed in via the parameter. Cast this to a
character pointer. */
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 延迟一个循环周期。调用vTaskDelay()以让任务在延迟期间保持在阻塞态。延迟时间以心跳周期为
单位,常量portTICK_RATE_MS可以用来在毫秒和心跳周期之间相换转换。本例设定250毫秒的循环周
期。 */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
vTaskDelayUntil() API 函数
vTaskDelayUntil()的参数就是用来指定任务离开阻塞态进入就绪态那一刻的精确心跳计数值。
函数原型:
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
处理器总是需要代码来执行——所以至少要有一个任务处于运行态。为了保证这一点,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务。空闲任务是一个非常短小的循环,拥有最低的优先级。
通过空闲任务钩子函数(或称回调,hook, or call-back),可以直接在空闲任务中添加应用程序相关的功能。
void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPEuxNewPriority );
//pxTask 被修改优先级的任务句柄(即目标任务)——参考 xTaskCreate() API函数的参数 pxCreatedTask 以了解如何得到任务句柄方面的信息。
//uxNewPriority 目标任务将被设置到哪个优先级上。如果设置的值超过了最大可用优先级(configMAX_PRIORITIES – 1),则会被自动封顶为最大值。
查询一个任务的优先级
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );
//pxTask 被查询任务的句柄(目标任务)
//返回值 被查询任务的当前优先级。
任务可以使用 API 函数 vTaskDelete()删除自己或其它任务。空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就是使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。
需要说明一点,只有内核为任务分配的内存空间才会在任务被删除后自动回收。任务自己占用的内存或资源需要由应用程序自己显式地释放。
void vTaskDelete( xTaskHandle pxTaskToDelete );
//pxTaskToDelete 被删除任务的句柄(目标任务) —— 参考 xTaskCreate() API 函数的参数 pxCreatedTask 以了解如何得到任务句柄方面的信息。
//任务可以通过传入 NULL 值来删除自己。
”抢占式”是指当任务进入就绪态或是优先级被改变时,如果处于运行态的任务优先级更低,则该任务总是抢占当前运行的任务。
单调速率调度(Rate Monotonic Scheduling, RMS)是一种常用的优先级分配技术。其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。
采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显式调用 taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可能会导致系统响应不够快。