当前位置:   article > 正文

C语言-嵌入式-STM32:FreeRTOS说明和详解

C语言-嵌入式-STM32:FreeRTOS说明和详解

        Free即免费的,RTOS的全称是Real time operating system,中文就是实时操作系统。

         注意:RTOS不是指某一个确定的系统,而是指一类操作系统。比如:uc/OS,FreeRTOS,RTX,RT-Thread 等这些都是RTOS类操作系统。

        FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间 管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需 要。 由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少 数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系 统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植 到各种单片机上运行,其最新版本为10.4.4版。

        1、任务的创建与删除

        1.1、 什么是任务?

        任务可以理解为进程/线程,创建一个任务,就会在内存开辟一个空间。

       1. 2、 任务创建与删除相关函数

        任务创建与删除相关函数有如下三个:

函数名称函数作用
xTaskCreate()动态方式创建任务
xTaskCreateStatic()静态方式创建任务
vTaskDelete()删除任务
        1.2.1、任务动态创建与静态创建的区别:

         动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递。 通常情况下使用动态方式创建任务。

        1.2.2、vTaskDelete 删除任务函数原型
void vTaskDelete(TaskHandle_t xTaskToDelete);

        只需将待删除的任务句柄传入该函数,即可将该任务删除。 当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)。

        总结:在FreeRTOS中,任务的概念有些类似于Linux中的多线程操作,因为在一个main()函数中,只要进入了while或者for循环,那么将会不断地去重复里面的代码块内容,如果此时我们想要“同时”去做另一件事,那就分身乏术了。而多任务(多线程/进程)很好的帮助我们去“一心二用”甚至“一心多用”。

2、任务调度

        2.1、什么是任务调度?

        在单片机中,任务的执行其实是以先后顺序来编译运行的,只不过计算机运行的速度非常快,例如:每一个任务分配1ms就能够完成,那么3个同时进行的任务,按先后顺序执行1次也就只要3ms,我们肉眼根本分不清它们的先后顺序,这样我们就会认为他们是在同时执行的。但是,既然讲到了先后顺序,那就有优先级之分了,也就是接下来要讲的任务调度:到底谁先执行?

         而调度器就是使用相关的调度算法来决定当前需要执行的哪个任务。

        FreeRTOS中开启任务调度的函数是 vTaskStartScheduler() 

        2.2、FreeRTOS的任务调度规则是怎样的?

        FreeRTOS 是一个实时操作系统,它所奉行的调度规则:

        1. 高优先级抢占低优先级任务,系统永远执行最高优先级的任务(即抢占式调度);

        总结: 1. 高优先级任务,优先执行;

                    2. 高优先级任务不停止,低优先级任务无法执行;

                    3. 被抢占的任务将会进入就绪态

        2. 同等优先级的任务轮转调度(即时间片调度);

        总结: 1. 同等优先级任务,轮流执行,时间片流转;

                    2. 一个时间片大小,取决为滴答定时器中断周期;

                    3. 注意没有用完的时间片不会再使用,下次任务 Task3 得到执行,还是按照一个时间片的时钟节拍运行

       

         2.3、任务的状态

        FreeRTOS中任务共存在4种状态:

        Running 运行态 当任务处于实际运行状态称之为运行态,即CPU的使用权被这个任务占用(同一时间仅一个任务处于运行 态)。

        Ready 就绪态 处于就绪态的任务是指那些能够运行(没有被阻塞和挂起),但是当前没有运行的任务,因为同优先级或更 高优先级的任务正在运行。

        Blocked 阻塞态 如果一个任务因延时,或等待信号量、消息队列、事件标志组等而处于的状态被称之为阻塞态。

        Suspended 挂起态 类似暂停,通过调用函数 vTaskSuspend() 对指定任务进行挂起,挂起后这个任务将不被执行,只有调用 函数 xTaskResume() 才可以将这个任务从挂起态恢复。

        总结: 1. 仅就绪态可转变成运行态 2. 其他状态的任务想运行,必须先转变成就绪态。

        4、队列 

        4.1、什么是队列?

        队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息。

        为什么不使用全局变量?

        如果使用全局变量,在任务1中修改了变量 a ,等待任务3处理该变量,但任务3处理速度很慢(比如调用了延时函数),那么在任务3处理数据的过程中,如果有中断或任务2修改了变量 a ,那么将会导致任务3会得到一个不正确的变量a。

        关于队列的几个名词:

        队列项目:队列中的每一个数据;

        队列长度:队列能够存储队列项目的最大数量;

        创建队列时,需要指定队列长度及队列项目大小

        4.2、队列特点

        4.2.1. 数据入队出队方式

                通常采用先进先出(FIFO:first in first out)的数据存储缓冲机制,即先入队的数据会先从队列中被读取。 也可以配置为后进先出(LIFO)方式,但用得比较少。

        4.2. 2. 数据传递方式

                采用实际值传递,即将数据拷贝到队列中进行传递,也可以传递指针,在传递较大的数据的时候采用指针传递。

         4.2.3. 多任务访问

                队列不属于某个任务,任何任务和中断都可以向队列发送/读取消息

         4.2.4. 出队、入队阻塞

                当任务向一个队列发送消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队。

        阻塞时间如果设置为:

                 0:直接返回不会等待;

                 0~port_MAX_DELAY:等待设定的阻塞时间,若在该时间内还无法入队,超时后直接返回不再等待;

                 port_MAX_DELAY:死等,一直等到可以入队为止。出队阻塞与入队阻塞类似;

       

         4.3、队列相关 API 函

        4.3.1、创建队列
  1. QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
  2. UBaseType_t uxItemSize );

        参数:

                 uxQueueLength:队列可同时容纳的最大项目数

                uxItemSize:存储队列中的每个数据项所需的大小(以字节为单位)。

                返回值: 如果队列创建成功,则返回所创建队列的句柄 。 如果创建队列所需的内存无法分配 ,则返回 NULL。

         4.3.2、写队列

  1. BaseType_t xQueueSend(//向尾部发送数据
  2. QueueHandle_t xQueue, //队列的句柄,数据项将发送到此队列。
  3. const void * pvItemToQueue, //待写入数据
  4. TickType_t xTicksToWait //阻塞超时时间
  5. ); //返回值:如果成功写入数据,返回 pdTRUE,否则返回 errQUEUE_FULL。
        4.3.3、读队列

  1. BaseType_t xQueueReceive(//从队列头部读取消息,并删除消息
  2. QueueHandle_t xQueue,//待读取的队列
  3. void *pvBuffer, //数据读取缓冲区
  4. TickType_t xTicksToWait//阻塞超时时间
  5. ); //成功返回 pdTRUE,否则返回 pdFALSE。

        5、信号量

        5.1、什么是信号量?

        信号量(Semaphore),是在多任务环境下使用的一种机制,是可以用来保证两个或多个关键代码段不被并 发调用。

        信号量这个名字,我们可以把它拆分来看,信号可以起到通知信号的作用,然后我们的量还可以用来表示资 源的数量,当我们的量只有0和1的时候,它就可以被称作二值信号量,只有两个状态,当我们的那个量没有 限制的时候,它就可以被称作为计数型信号量。

         信号量也是队列的一种。

        5.2、什么是二值信号量?

        二值信号量其实就是一个长度为1,大小为零的队列,只有0和1两种状态,通常情况下,我们用它来进行互 斥访问或任务同步。

        互斥访问:比如门钥匙,只有获取到钥匙才可以开门

        任务同步:比如我录完视频你才可以看视频

        5.3、二值信号量相关 API 函数

        5.3.1、创建二值信号量
  1. SemaphoreHandle_t xSemaphoreCreateBinary( void )
  2. //参数:
  3. // 无
  4. //返回值:
  5. // 成功,返回对应二值信号量的句柄;
  6. // 失败,返回 NULL 。
        5.3.2、释放二值信号量
  1. BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
  2. /*参数:
  3. xSemaphore:要释放的信号量句柄
  4. 返回值:
  5. 成功,返回 pdPASS ;
  6. 失败,返回 errQUEUE_FULL 。
  7. */
        5.3.3、获取二值信号量
  1. BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
  2. TickType_t xTicksToWait );
  3. /*
  4. 参数:
  5. xSemaphore:要获取的信号量句柄
  6. xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;
  7. 返回值:
  8. 成功,返回 pdPASS ;
  9. 失败,返回 errQUEUE_FULL 。
  10. */

        5.4、什么是计数信号量?

        计数型信号量相当于队列长度大于1 的队列,因此计数型信号量能够容纳多个资源,这在计数型信号量被创 建的时候确定的。

        5.5、计数型信号量相关 API 函数

         5.5.1、使用动态方法创建计数型信号量
  1. SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
  2. UBaseType_t uxInitialCount);
  3. /*
  4. 参数:
  5. uxMaxCount:可以达到的最大计数值
  6. uxInitialCount:创建信号量时分配给信号量的计数值
  7. 返回值:
  8. 成功,返回对应计数型信号量的句柄;
  9. 失败,返回 NULL 。
  10. */

        计数型信号量的释放和获取与二值信号量完全相同 !

         5.5.2、释放计数信号量
  1. BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )
  2. /*参数:
  3. xSemaphore:要释放的信号量句柄
  4. 返回值:
  5. 成功,返回 pdPASS ;
  6. 失败,返回 errQUEUE_FULL 。
  7. */
        5.5.3、获取计数信号量
  1. BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
  2. TickType_t xTicksToWait );
  3. /*
  4. 参数:
  5. xSemaphore:要获取的信号量句柄
  6. xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;
  7. 返回值:
  8. 成功,返回 pdPASS ;
  9. 失败,返回 errQUEUE_FULL 。
  10. */

        5.6、什么是互斥量?

        在多数情况下,互斥型信号量和二值型信号量非常相似,但是从功能上二值型信号量用于同步,而互斥型信 号量用于资源保护。

         互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现象。

        5.7、什么是优先级翻转?

        以上图为例,在正常情况下(没有使用互斥信号量),优先级最低的任务(没有人权)因为任务调度权限的问题,非常容易被打断,尤其是在数据操作过程中非常容易造成数据的不准确。为了解决这个问题,引入了互斥信号量。

        假设3个任务的优先级TaskH>TaskM>TaskL,正常情况下,TaskL会被TaskH和TaskM所打断,TaskM会被TaskH所打断。但是当TaskH和TaskL使用了互斥信号量后,假设任务是上厕所:

        当L在上厕所时,此时H也想上厕所,但是因为加入了互斥信号量,L的优先级会发生翻转,它的优先级会被临时提升到H的优先级,直到释放互斥信号量。

        在H等待过程中,M想要打断L施法,但是在H面前它级别不够,所以只能老老实实等在H的后面。

        当L释放互斥量,优先级高的H则会抢占厕所,直到H也释放互斥量,M才有机会抢占厕所资源。

        优先级继承:当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获 取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优 先级提升到与自己相同的优先级。 优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。

        5.8、互斥量相关 API 函数

        互斥信号量不能用于中断服务函数中!

  1. SemaphoreHandle_t xSemaphoreCreateMutex( void )
  2. /*
  3. 参数:
  4. 返回值:
  5. 成功,返回对应互斥量的句柄;
  6. 失败,返回 NULL 。
  7. */

        6、事件标志组

        6.1、什么是事件标志组?

        事件标志位:表明某个事件是否发生,联想:全局变量 flag。通常按位表示,每一个位表示一个事件(高8位 不算)

        事件标志组是一组事件标志位的集合, 可以简单的理解事件标志组,就是一个整数。

        事件标志组本质是一个 16 位或 32 位无符号的数据类型 EventBits_t ,由 configUSE_16_BIT_TICKS 决定。

        虽然使用了 32 位无符号的数据类型变量来存储事件标志, 但其中的高8位用作存储事件标志组的控制信息, 低 24 位用作存储事件标志 ,所以说一个事件组最多可以存储 24 个事件标志!

        6.2、事件标志组相关 API 函数

函数描述
xEventGroupCreate()使用动态方式创建事件标志组
xEventGroupCreateStatic()使用静态方式创建事件标志组
xEventGroupClearBits()清零事件标志位
xEventGroupClearBitsFromISR()在中断中清零事件标志位
xEventGroupSetBits()设置事件标志位
xEventGroupSetBitsFromISR()在中断中设置事件标志位
xEventGroupWaitBits()等待事件标志位
                6.2.1、创建事件标志组
  1. EventGroupHandle_t xEventGroupCreate( void );
  2. /*
  3. 参数:
  4. 返回值:
  5. 成功,返回对应事件标志组的句柄;
  6. 失败,返回 NULL 。
  7. */
                6.2.2、设置事件标志位
  1. EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
  2. const EventBits_t uxBitsToSet );
  3. /*
  4. 参数:
  5. xEventGroup:对应事件组句柄。
  6. uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值。
  7. 返回值:
  8. 设置之后事件组中的事件标志位值
  9. */
                6.2.3、清除事件标志位
  1. EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
  2. const EventBits_t uxBitsToClear );
  3. /*
  4. 参数:
  5. xEventGroup:对应事件组句柄。
  6. uxBitsToClear:指定要在事件组中清除的一个或多个位的按位值。
  7. 返回值:
  8. 清零之前事件组中事件标志位的值。
  9. */
                6.2.4、等待事件标志位
  1. EventBits_t xEventGroupWaitBits(
  2. const EventGroupHandle_t xEventGroup,
  3. const EventBits_t uxBitsToWaitFor,
  4. const BaseType_t xClearOnExit,
  5. const BaseType_t xWaitForAllBits,
  6. TickType_t xTicksToWait );
  7. /*
  8. 参数:
  9. xEventGroup:对应的事件标志组句柄
  10. uxBitsToWaitFor:指定事件组中要等待的一个或多个事件位的按位值
  11. xClearOnExit:pdTRUE——清除对应事件位,pdFALSE——不清除
  12. xWaitForAllBits:pdTRUE——所有等待事件位全为1(逻辑与),pdFALSE——等待的事件位有一个为1
  13. (逻辑或)
  14. xTicksToWait:超时
  15. 返回值:
  16. 等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位
  17. 其他值:等待事件标志位失败,返回事件组中的事件标志位
  18. */

        举例:有一二三四五六七个任务,就像集龙珠,每当一个任务被执行,对应的标志位置1,当对应的7个标志位都被置1时,则可以触发终极任务:召唤神龙。

        7、任务通知

        7.1、什么是任务通知?

         FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队 列)。 在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度为 1 的队列(可 以保存一个 32 位整数或指针值),并且任务通知速度更快、使用的RAM更少!

        7.2、任务通知值的更新方式

        FreeRTOS 提供以下几种方式发送通知给任务 :

        发送消息给任务,如果有通知未读, 不覆盖通知值

        发送消息给任务,直接覆盖通知值

        发送消息给任务,设置通知值的一个或者多个位

        发送消息给任务,递增通知值 通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等

        7.3、任务通知的优势和劣势

        7.3.1、任务通知的优势

        1. 使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。

        2. 使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。

        7.3.2、任务通知的劣势

        1. 只有任务可以等待通知,中断服务函数中不可以,因为中断没有 TCB 。

        2. 通知只能一对一,因为通知必须指定任务。

        3. 等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待。

        4. 任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数 据。

        7.4、任务通知相关 API 函数

        7.4.1、发送通知
 函数描述
xTaskNotify()发送通知,带有通知值
xTaskNotifyAndQuery()发送通知,带有通知值并且保留接收任务的原通知值
xTaskNotifyGive()发送通知,不带通知值
xTaskNotifyFromISR()在中断中发送任务通知
xTaskNotifyAndQueryFromISR()在中断中发送任务通知
vTaskNotifyGiveFromISR()在中断中发送任务通知
  1. //发送通知,带有通知值
  2. BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
  3. uint32_t ulValue,
  4. eNotifyAction eAction );
  5. /*
  6. 参数:
  7. xTaskToNotify:需要接收通知的任务句柄;
  8. ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定;
  9. eAction:一个枚举,代表如何使用任务通知的值;
  10. 返回值:
  11. 如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE, 而其他情况均返回pdPASS。
  12. -----------------------------------------------------------------------------------------
  13. 枚举值 描述
  14. eNoAction 发送通知,但不更新值(参数ulValue未使用)
  15. eSetBits 被通知任务的通知值按位或ulValue。(某些场景下可代替事件组,
  16. 效率更高)
  17. eIncrement 被通知任务的通知值增加1(参数ulValue未使用),相当于
  18. xTaskNotifyGive
  19. eSetValueWithOverWrite 被通知任务的通知值设置为 ulValue。(某些场景下可代替
  20. xQueueOverwrite ,效率更高)
  21. eSetValueWithoutOverwrite 如果被通知的任务当前没有通知,则被通知的任务的通知值设为
  22. ulValue。
  23. 如果被通知任务没有取走上一个通知,又接收到了一个通知,则这次
  24. 通知值丢弃,在这种情况下视为调用失败并返回 pdFALSE
  25. (某些场景下可代替 xQueueSend ,效率更高)
  26. ----------------------------------------------------------------------------------------
  27. */
  1. //发送通知,带有通知值并且保留接收任务的原通知值
  2. BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify,
  3. uint32_t ulValue,
  4. eNotifyAction eAction,
  5. uint32_t *pulPreviousNotifyValue );
  6. /*
  7. 参数:
  8. xTaskToNotify:需要接收通知的任务句柄;
  9. ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定;
  10. eAction:一个枚举,代表如何使用任务通知的值;
  11. pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为 NULL, 则不需要回传, 这个时候就等价于函数 xTaskNotify()。
  12. 返回值:
  13. 如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE, 而其他
  14. 情况均返回pdPASS。
  15. */
  1. //发送通知,不带通知值
  2. BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
  3. /*
  4. 参数:
  5. xTaskToNotify:接收通知的任务句柄, 并让其自身的任务通知值加 1。
  6. 返回值:
  7. 总是返回 pdPASS
  8. */
        7.4.2、等待通知

        等待通知API函数只能用在任务,不可应用于中断中!

  1. //
  2. uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
  3. TickType_t xTicksToWait );
  4. /*
  5. 参数:
  6. xClearCountOnExit:指定在成功接收通知后,将通知值清零或减 1,pdTRUE:把通知值清零(二值信号
  7. 量);pdFALSE:把通知值减一(计数型信号量);
  8. xTicksToWait:阻塞等待任务通知值的最大时间;
  9. 返回值:
  10. 0:接收失败
  11. 非0:接收成功,返回任务通知的通知值
  12. */
  1. BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
  2. uint32_t ulBitsToClearOnExit,
  3. uint32_t *pulNotificationValue,
  4. TickType_t xTicksToWait );
  5. /*
  6. ulBitsToClearOnEntry:函数执行前清零任务通知值那些位 。
  7. ulBitsToClearOnExit:表示在函数退出前,清零任务通知值那些位,在清 0 前,接收到的任务通知值会先被保存到形参*pulNotificationValue 中。
  8. pulNotificationValue:用于保存接收到的任务通知值。 如果 不需要使用,则设置为 NULL 即可 。
  9. xTicksToWait:等待消息通知的最大等待时间。
  10. */

        8、延时函数

        延时函数分类

        相对延时:vTaskDelay

        绝对延时:vTaskDelayUntil

        vTaskDelay 与 HAL_Delay 的区别:

        vTaskDelay 作用是让任务阻塞,任务阻塞后,RTOS系统调用其它处于就绪状态的优先级最高的任务来执 行。

        HAL_Delay 一直不停的调用获取系统时间的函数,直到指定的时间流逝然后退出,故其占用了全部CPU时 间。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/593958
推荐阅读
相关标签
  

闽ICP备14008679号