2023年薪火培训电控第六讲 —— RTOS

发布时间 2023-07-04 14:50:39作者: 北京理工大学机器人队

什么是RTOS

RTOS 是实时操作系统(Real-Time Operating System)的缩写。它是一种专门用于实时任务处理的操作系统,用于管理和调度实时任务,并提供与硬件和外部设备的交互接口。

实时操作系统可以根据任务的时间要求和优先级,对任务进行调度和执行,以满足实时性的需求。它提供了任务管理、任务调度、中断处理、资源管理、通信机制等功能,使开发者能够方便地开发和管理实时应用程序。

操作系统

操作系统是一个控制程序,作为硬件和应用程序之间的桥梁,主要是和硬件打交道,负责协调分配计算资源和内存资源给不同的应用程序使用,并防止系统出现故障。面对来自不同应用程序的大量且互相竞争的资源请求,操作系统通过一个调度算法和内存管理算法尽可能把资源公平,有效率地分配给不同的程序。

实时???

实时性主要分为:硬实时和软实时

  • 硬实时:要求任务在严格的时间限制内完成,并绝对不能错过截止时间。即使有一次时间违规,系统的正确性和可靠性也可能受到严重威胁。
  • 软实时:任务有时间约束,但允许偶尔的时间违规。软实时任务的主要目标是在大部分情况下满足时间约束,但偶尔的延迟可能会被接受。

实时性的要求高主要是为了确保系统能够快速、准确地感知和响应各种情况,以提供安全性、稳定性和高效性能。

比如汽车的安全气囊系统,一旦检测到碰撞,系统必须在几毫秒的时间范围内完成气囊的充气,以在乘客撞击前提供必要的保护。任何延迟或错误可能导致安全气囊无法正常充气,从而无法起到保护作用,造成灾难性的后果。

和裸机开发的不同

裸机开发 基于RTOS的开发
在没有操作系统支持的情况下直接编写代码来控制STM32。需要手动编写任务调度、同步和通信机制,以及处理中断和定时器等底层硬件操作。 更高级别的抽象和便利,使开发过程更加简化和高效。适用于需要处理多任务和并发操作的应用程序,同时提供了丰富的功能和组件来管理任务和资源。

现在市面上常见的RTOS有freeRTOS、RT-Thread、µC/OS等等,本节课主要介绍freeRTOS。

freeRTOS

相关背景:
FreeRTOS是一个热门的嵌入式设备用即时操作系统核心,它最初由Richard Barry于2003年左右开发,并由Barry的公司Real Time Engineers Ltd进行后续的开发和维护。2017年,该公司将FreeRTOS项目的管理权交给了亚马逊网络服务(Amazon Web Services,AWS)。Barry继续作为AWS团队的一员继续开发FreeRTOS。

优势

  • FreeRTOS的设计小巧且简易,整个核心代码只有3到4个C文件,为了让代码容易阅读、移植和维护,大部分的代码都是以C语言编写,只有一些函数采用汇编语言编写。
  • 开源,可免费使用

任务的配置和创建

CUBE MX的配置

  1. 根据自己的开发板选择型号
    • 注意自己手上的是C6还是C8,可以给芯片打个光看一下
  2. RCC
  3. SYS,(这里有一点点不同,需要选择一个定时器作为时基
    以选TIM1为例
  4. 在middleware中选择FREERTOS在下拉菜单中选择CMSIS_V1
  5. 配置时钟频率72MHz
  6. 在Project Manager中进行最后的配置(这部分和以前一样),生成代码,打开project

两种任务配置的方法

直接敲代码

  • Task永远不会返回(return),实现需要套在一个死循环内,如果循环结束了需要调用vTaskDelete()进行删除
void myTask(void *pvParameters){
    
    for(;;){
        /* 任务实现的代码 */

        vTaskDelay(/*下一次执行的最短时间间隔*/);
        /* 让任务进入阻塞状态 */
        /* 也可以用osDelay()函数来实现 */
    }
    vTaskDelete( NULL );
    // NULL表示删除当前的任务,也可通过传入其他任务句柄来删除其他任务
}

我们以翻转一个标志位为例为例:

int flag_a = 0;
/* 在程序前面定义 */

void flip_flag(void* parameter) {
   int* p = parameter;
   for (;;) {
      *p = 1;
      *p = 0;
      vTaskDelay(1);
   }
}

完成函数的定义之后,就可以创建任务

创建任务需要调用如下函数:

BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
                        const char * const pcName,
                        uint16_t usStackDepth,
                        void *pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t *pxCreatedTask);
参数 含义
pvTaskCode 任务函数指针
pcName 任务名称,只在debug的时候有用,需要具有描述性。但这个参数不会被freeRTOS使用
usStackDepth 栈深度
pvParameters 参数指针,可以在传入任务后转换为需要的数据类型
uxPriority 任务优先级
pxCreatedTask 传出任务句柄的位置,用于在系统运行的时候改变任务的优先级或者删除

RTOS的标识符命名具有一定的规律,可以在下面这篇博客中进一步了解:
freeRTOS名称规范

MX_FREERTOS_Init()函数内创建任务:

   xTaskCreate(flip_flag, "task_0", 128, &flag_a, 0, NULL);

使用Keil进行仿真

在使用之前,需要修改一下keil的配置

  1. 关闭代码优化
    • 将优化级别改为-O0
  2. 配置仿真参数
    • 将debug页面左边的Use StimulatorLimit Speed to Real Time选上
    • 在下面的Dialog DLL中填入DARMSTM.DLL,Parameter中填入-pSTM32F103C6(如果手上的板子是C8就填C8)

配置完成后,就可以进入debug模式

打开逻辑分析器后,将变量添加到里面,并将我们需要查看的标志位添加到逻辑分析器中



运行后即可看到标志位flag_a的变化,说明任务可以正常执行。

同一个函数也可以用来创建多个任务,

   int flag_b = 0;
   xTaskCreate(flip_flag, "task_1", 128, &flag_b, 0, NULL);


两个任务都可以被执行

CUBE MX同样为我们提供了比较简便的配置方式

  • 在CUBE MX中进行配置

    任务需要填入的参数与之前的相同,优先级改为Normal

再次生成工程后,在freertos.c可以找到cube自动生成的代码,我们在StartTask02()的循环中写程序即可,内容与前面的flip_flag()函数类似。

且在MX_FREERTOS_Init()中,cube已经帮我们添加了创建任务的语句,只需要修改传入的参数

注意:在用cube重新生成工程之后,需要再次修改debug的配置

多任务调度

在实际开发的过程中,系统需要执行多个任务来维持自身的稳定,并对外界变化或指令进行相应。不同任务的执行时间和容忍的延迟各有不同,但我们使用的单片机是单核心的,在任意时刻只会有一个任务被执行,因此我们需要对任务进行合理的调度,来满足任务对于实时性的要求。

三种调度算法

  1. 先来先服务(FCFS)调度算法
    按照任务到达的先后顺序进行调度,先到达的任务先执行。这种算法简单直观,但可能导致长任务优先的问题。

  2. 时间片轮转算法
    将任务按照轮询顺序进行调度,每个任务执行一定的时间片(时间片轮转),然后切换到下一个任务。这种算法能够公平地分配CPU时间,但可能导致上下文切换开销较大。

    可以试着将flip_flag()函数中的延迟注释掉,观察逻辑分析器的波形有什么变化

  3. 优先级调度算法
    为每个任务分配优先级,并按照优先级进行调度。具有较高优先级的任务优先执行,但可能导致低优先级任务的饥饿问题。
    可以通过下面的图了解一下

    用Cubemx生成的FreeRTOS默认有七个优先级,这七个优先级定义在了cmsis_os.h中。与中断优先级不同,任务的优先数越大,则优先级越高。
    • 举个?
      假设一个系统有三个任务
    1. 在分别进入准备状态时,三个任务都可以立即运行,此时能够满足硬实时的要求。
    2. 如果三个任务同时进入准备状态,则会按照优先级排队运行,此时假设前两个任务的运行时间大于第三个任务的容忍延迟,就会导致在第三个任务无法满足硬实时的要求。
    3. 其实只需要合理设置优先级,在三个任务同时进入准备时,也能够满足硬实时

任务状态

在RTOS中,一个任务有四种基本的工作状态:

  1. 运行态(Running)
  2. 阻塞态(Blocked):需要等待以下两种类型的事件才可恢复到就绪态。
    • 时间相关事件:可以是延迟到期或者绝对时间到点。
    • 同步事件:来自其他任务或者中断的事件。
  3. 挂起态(Suspended):对任务调度器不可见的状态,相当于从任务列表移出,但任务仍然存在,没有被删除。
  4. 就绪态(Ready):任务处于非运行状态,既没有阻塞也没有挂起,可以被运行,但尚未被运行。

状态量控制

阻塞态

  • 时间相关阻塞,可通过vTaskDelay()或者vTaskDelayUntil()函数是任务进入阻塞态

  • vTaskDelay()

    参数 含义
    xTicksToDelay 延迟的节拍(或称心跳)数
    表示让任务在多少个心跳后会恢复到准备状态
  • vTaskDelayUntil()

    参数 含义
    pxPreviousWakeTime 上一次任务被唤醒的时刻
    xTimeIncrement 到下一次唤醒的时间差
    • 单位都是心跳时间

    表示让任务在多少个心跳后恢复到准备状态,可以用来让任务周期性执行

心跳的频率为配置中的TICK_RATE_HZ

挂起态:

  • 只能通过vTaskSuspend()函数将任务挂起

    参数 含义
    xTaskToSuspend 需要挂起任务的句柄指针,如果传入NULL则表示挂起当前任务
  • 通过vTaskResume()函数将任务唤醒

    参数 含义
    xTaskToResume 需要恢复任务的句柄指针

用官方文档的一张图总结

再加一点中断

我们可以添加一个定时器中断,在中断中反转一个新的标志位,查看标志位的变化和其他任务的行为


可以看到在正常任务被执行的过程中,定时器发生的中断也能够被处理
放大查看,可以发现在进入中断的时候,实际上也只有中断回调函数在执行

如果逻辑分析器看不到波形

  • 检查一下keil的配置,代码优化是否为O0
  • 在cube中配置定时器中断后,需要启用中断

延迟中断

如果在执行及时性要求非常高的任务时,发生了中断,我们又不希望中断处理耽误现有任务太多的时间,这个时候便需要进行延迟中断的操作
一般需要把中断处理程序的最小化,只执行与中断请求的最小处理相关的操作,例如清除中断标志位、保存关键状态、设状态位等,在完成原有任务后,再根据标志位来判定是否需要进一步处理中断。
如果中断触发不太频繁,也可以中断处理不使用的时候将处理任务挂起,中断发生的时候只将处理任务恢复,在处理完原任务之后,在通过调度器进入中断的后续处理程序,不过这种方法需要对任务的优先级进行合理设置。

作业

freeRTOS和硬件包器件同时实现如下功能:

  1. 舵机控制
    • 舵机位置初始化为90度;
    • 通过蓝牙向单片机发送一个正整数,如果整数在0~180之间,让舵机调节至对应角度;如果大于180,则将舵机角度设置为90度
    • 如果超过5秒没有收到新的位置数据,将舵机角度置为90度
  2. 呼吸灯控制
    • 实现一个呼吸灯
    • 呼吸灯受到一个按键控制,按键按下时,呼吸灯保持当前亮度不变;再次按下按键时,呼吸灯亮度继续变化

REFERENCE

从0开始的FreeRTOS

【嵌入式系统放牛班】RTOS-01

【嵌入式系统放牛班】RTOS-02

【嵌入式系统放牛班】RTOS-03