① 任务管理

② 内存管理

③ 嵌入式C编程规范——Arduino 框架下

1 freeRTOS嵌入式操作系统

当我们进入嵌入式这个领域的时候,往往首先接触的都是单片机编程,单片机编程又 首选 51 单片机来入门。这里面说的单片机编程通常都是指裸机编程,即不加入任何 RTOS (Real Time Operating System 实时操作系统)的程序。常用的 RTOS 有国外的FreeRTOS、 μC/OS、RTX 和国内的 FreeRTOS、Huawei LiteOS 和 AliOS-Things 等,其中尤以国外开源 且免费的 FreeRTOS 的市场占有率最高。

操作系统是允许多个任务“同时运行”的,操作系统的这个特性被称为多任务。然而实际上,一个 CPU 核心在某一时刻只能运行一个任务,而操作系统中任务调度器的责任就是决定在某一时刻 CPU 究竟要运行哪一个任务,任务调度器使得CPU 在各个任务之间来回切换并处理任务,由于切换处理任务的速度非常快,因此就给人造成了一种同一时刻有多个任务同时运行的错觉。

ESP32 arduino内嵌了freeRTOS操作系统,在编程时可以方便的调用freeRTOS的API实现复杂操作。

1.1 freeRTOS 任务基本概念

1.1.1 操作系统概述

(1)单任务系统

① 轮询系统

单任务系统的编程方式,即裸机的编程方式,这种编程方式的框架一般都是在 main()函数 中使用一个大循环,在循环中顺序地调用相应的函数以处理相应的事务。

轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。

② 前后台系统

相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台,main()函数里面的无限循环我们称为后台。

从上图可以看出,前后台系统的实时性很差,因为大循环中函数处理的事务没有优先级之分,必须是顺序地被执行处理的,不论待处理事务的紧急程度有多高,没轮到只能等着,虽然 中断能够处理一些紧急的事务,但是在一些大型的嵌入式应用中,这样的单任务系统就会显得力不从心。

(2)多任务操作系统

多任务系统在处理事务的实时性上比单任务系统要好得多,从宏观上来看,多任务系统的多个任务是可以“同时”运行的,因此紧急的事务就可以无需等待 CPU 处理完其他事务,在被处理。

要注意的是多任务系统的多个任务可以“同时”运行,是从宏观的角度而言的,对于单核的 CPU 而言,CPU 在同一时刻只能够处理一个任务,但是多任务系统的任务调度器会根据相关的任务调度算法,将 CPU 的使用权分配给任务,在任务获取 CPU 使用权之后的极短时间(宏观角度)后,任务调度器又会将 CPU 的使用权分配给其他任务,如此往复,在宏观的角度看来,就像是多个任务同时运行了一样。

多任务系统的运行示意图,如下图所示:

从上图可以看出,相较于单任务系统而言,多任务系统的任务也是具有优先级的,高优先级的任务可以像中断的抢占一样,抢占低优先级任务的 CPU 使用权;优先级相同的任务则各自轮流运行一段极短的时间(宏观角度),从而产生“同时”运行的错觉。以上就是抢占式调度和 时间片调度的基本原理。

在任务有了优先级的多任务系统中,用户就可以将紧急的事务放在优先级高的任务中进行 处理,那么整个系统的实时性就会大大地提高。

1.1.2 freeRTOS任务状态

FreeRTOS 中任务存在四种任务状态,分别为运行态、就绪态、阻塞态和挂起态。FreeRTOS 运行时,任务的状态一定是这四种状态中的一种,下面就分别来介绍一下这四种任务状态。

1. 运行态

如果一个任务得到 CPU 的使用权,即任务被实际执行时,那么这个任务处于运行态。如果 运行 RTOS 的 MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处理运行态。

2. 就绪态

如果一个任务已经能够被执行(不处于阻塞态后挂起态),但当前还未被执行(具有相同优先级或更高优先级的任务正持有 CPU 使用权),那么这个任务就处于就绪态。

3. 阻塞态

如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任务调用了函vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情况下,处于阻塞态的任务都有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后,即使任务等待的外部事件还没有发生,任务的阻塞态也会被解除。 要注意的是,处于阻塞态的任务是无法被运行的。

4. 挂起态

任务一般通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态与阻塞态一样,处于挂起态的任务也无法被运行。

1.1.3 freeRTOS优先级

(1)抢占式调度

抢占式调度主要是针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可 以抢占优先级低的任务,只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以 运行。

(2)时间片调度

时间片调度主要针对优先级相同的任务,当多个任务的优先级相同时,任务调度器会在每 一次系统时钟节拍到的时候切换任务,也就是说 CPU 轮流运行优先级相同的任务,每个任务运 行的时间就是一个系统时钟节拍。

1.2 freeRTOS任务管理API

1.2.1 freeRTOS任务创建

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

void task1(void *pvParameters) {
    while (1) {
        // 任务1的代码
        vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
    }
}

void task2(void *pvParameters) {
    while (1) {
        // 任务2的代码
        vTaskDelay(2000 / portTICK_PERIOD_MS);  // 延时2秒
    }
}

void setup() {
    // 初始化代码可以放在这里
    Serial.begin(115200);

    // 创建FreeRTOS任务
    xTaskCreate(task1, "task1", 4096, NULL, 1, NULL);
    xTaskCreate(task2, "task2", 4096, NULL, 2, NULL);
}

void loop() {
    // 由于FreeRTOS自行管理任务,loop()中一般不需要添加额外的代码
}

ESP32基于Arduino框架在 PlatformIO 的多任务系统,A任务每隔50ms执行一次,B任务每隔100ms执行一次,C任务每隔1000ms执行一次。

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

// 定义任务句柄
TaskHandle_t TaskAHandle = NULL;
TaskHandle_t TaskBHandle = NULL;
TaskHandle_t TaskCHandle = NULL;

// 任务A:每50ms执行一次
void TaskA(void *pvParameters) {
  
  for(;;) {
    // 任务A的工作代码
    Serial.println("Task A executed");
    
    vTaskDelay(pdMS_TO_TICKS(500)); // 延迟50ms
  }
}

// 任务B:每100ms执行一次
void TaskB(void *pvParameters) {
  
  for(;;) {
    // 任务B的工作代码
    Serial.println("Task B executed");
    
    vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟100ms
  }
}

// 任务C:每1000ms执行一次
void TaskC(void *pvParameters) {
  
  for(;;) {
    // 任务C的工作代码
    Serial.println("Task C executed");
    
    vTaskDelay(pdMS_TO_TICKS(5000)); // 延迟1000ms
  }
}

void setup() {
  Serial.begin(115200);
  
  // 创建任务A
  xTaskCreate(
    TaskA,           // 任务函数
    "TaskA",         // 任务名称
    2048,            // 堆栈大小(字节)
    NULL,            // 任务参数
    1,               // 优先级(0-24,数字越大优先级越高)
    &TaskAHandle     // 任务句柄
  );
  
  // 创建任务B
  xTaskCreate(
    TaskB,
    "TaskB",
    2048,
    NULL,
    2,
    &TaskBHandle
  );
  
  // 创建任务C
  xTaskCreate(
    TaskC,
    "TaskC",
    2048,
    NULL,
    3,
    &TaskCHandle
  );
  
  // 如果不需要在setup中执行其他操作,可以删除以下行
  // 因为FreeRTOS调度器会自动接管
}

void loop() {
  // 在基于FreeRTOS的应用中,loop()通常为空
  // 因为所有工作都在任务中完成
  vTaskDelete(NULL); // 删除loop任务(可选)
}

(1)函数 xTaskCreate()

此函数用于使用动态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存, 均由 FreeRTOS 从 FreeRTOS 管理的堆中分配,若使用此函数,需要在 FreeRTOSConfig.h 文件中将宏configSUPPORT_DYNAMIC_ALLOCATION 配置为 1。此函数创建的任务会立刻进入就 绪态,由任务调度器调度运行。函数原型如下所示:

BaseType_t xTaskCreate( 
    TaskFunction_t                   pxTaskCode, 
    const char * const               pcName, 
    const configSTACK_DEPTH_TYPE     usStackDepth, 
    void * const                     vParameters, 
    UBaseType_t                      uxPriority, 
    TaskHandle_t * const             pxCreatedTask);

函数 xTaskCreate()的形参描述,如下表所示:

(2) 函数 xTaskCreateStatic()

此函数用于使用静态的方式创建任务,任务的任务控制块以及任务的栈空间所需的内存, 需要由用户分配提供, 若使用此函数,需 要 在 FreeRTOSConfig.h 文件中将宏configSUPPORT_STATIC_ALLOCATION 配置为 1。此函数创建的任务会立刻进入就绪态,由任 务调度器调度运行。函数原型如下所示:

TaskHandle_t xTaskCreateStatic( 
    TaskFunction_t        pxTaskCode, 
    const char * const    pcName, 
    const uint32_t        ulStackDepth, 
    void * const          pvParameters, 
    UBaseType_t           uxPriority, 
    StackType_t * const   puxStackBuffer, 
    StaticTask_t * const  pxTaskBuffer);

函数 xTaskCreateStatic()的形参描述,如下表所示:

函数 xTaskCreateStatic()的返回值,如下表所示:

(3)函数 vTaskDelete()

此函数用于删除已被创建的任务,被删除的任务将被从就绪态任务列表、阻塞态任务列表、 挂起态任务列表和事件列表中移除,要注意的是,空闲任务会负责释放被删除任务中由系统分 配的内存,但是由用户在任务删除前申请的内存,则需要由用户在任务被删除前提前释放,否 则将导致内存泄露。若使用此函数,需要在FreeRTOSConfig.h文件中将宏INCLUDE_vTaskDelete 配置为 1。函数原型如下所示:

void vTaskDelete(TaskHandle_t xTaskToDelete);

任务创建及删除实验:

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

// 定义任务句柄
TaskHandle_t TaskAHandle = NULL;
TaskHandle_t TaskBHandle = NULL;
TaskHandle_t TaskCHandle = NULL;

// 任务A:每50ms执行一次
void TaskA(void *pvParameters) {
  
  for(;;) {
    // 任务A的工作代码
    Serial.println("Task A executed");
    
    vTaskDelay(pdMS_TO_TICKS(500)); // 延迟50ms
  }
}

// 任务B:每100ms执行一次
void TaskB(void *pvParameters) {
  
  for(;;) {
    // 任务B的工作代码
    Serial.println("Task B executed");
    
    vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟100ms
  }
}

// 任务C:每1000ms执行一次
void TaskC(void *pvParameters) {
  u32_t times;
  for(;;) {
    // 任务C的工作代码
    Serial.println("Task C executed");
    times ++;
    if (times> 1 && times/2 == 1)
    {
      Serial.println("delete Task A");
      vTaskDelete(TaskAHandle);
    }
    
    vTaskDelay(pdMS_TO_TICKS(5000)); // 延迟1000ms
  }
}

void setup() {
  Serial.begin(115200);
  
  // 创建任务A
  xTaskCreate(
    TaskA,           // 任务函数
    "TaskA",         // 任务名称
    2048,            // 堆栈大小(字节)
    NULL,            // 任务参数
    1,               // 优先级(0-24,数字越大优先级越高)
    &TaskAHandle     // 任务句柄
  );

  // 静态任务堆栈和TCB的分配
    StaticTask_t xTaskBuffer;
    StackType_t xStack[4096];
    BaseType_t xReturned;
  
  xTaskCreateStatic(
        TaskB,       // 任务函数
        "TaskB",            // 任务名
        4096, // 堆栈大小(以sizeof(StackType_t)为单位)
        NULL,                // 传递给任务的参数
        2,                   // 任务优先级(数字越小优先级越高)
        xStack,             // 指向堆栈的指针
        &xTaskBuffer        // 指向TCB的指针
    );
  
  // 创建任务C
  xTaskCreate(
    TaskC,
    "TaskC",
    2048,
    NULL,
    3,
    &TaskCHandle
  );
  
  // 如果不需要在setup中执行其他操作,可以删除以下行
  // 因为FreeRTOS调度器会自动接管
}

void loop() {
  // 在基于FreeRTOS的应用中,loop()通常为空
  // 因为所有工作都在任务中完成
  vTaskDelete(NULL); // 删除loop任务(可选)
}

1.3 freeRTOS内存管理

ESP32S3N16R8有内部的512K SRAM和外部的8M PSRAM.

#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

void setup() {
  Serial.begin(115200);
  sys_delay_ms(5000);
  Serial.printf("ESP32 total Heap size :%d bytes\n",ESP.getHeapSize());
  //查看ESP32堆的可用大小
  Serial.printf("ESP32 free  Heap size :%d bytes\n",ESP.getFreeHeap());
  //查看ESP32的Flash的大小
  Serial.printf("Flash size: %d bytes\n", ESP.getFlashChipSize());
 
  //查看ESP32的内部和外部RAM的总共大小
  Serial.printf("Deafult total size: %d bytes\n", heap_caps_get_total_size(MALLOC_CAP_DEFAULT));
  //查看ESP32的内部和外部RAM的可用大小
  Serial.printf("Deafult free size: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_DEFAULT));
 
  //查看ESP32的内部RAM总共大小
  Serial.printf("Internal total size: %d bytes\n", heap_caps_get_total_size(MALLOC_CAP_INTERNAL));
  //查看ESP32的内部RAM可用大小
  Serial.printf("Internal free size: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
 
  //查看ESP32的外部RAM的可用大小
  Serial.printf("PSRAM total size: %d bytes\n", heap_caps_get_total_size(MALLOC_CAP_SPIRAM));
  //查看ESP32的外部RAM的可用大小
  Serial.printf("PSRAM free size: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

  
  // 如果不需要在setup中执行其他操作,可以删除以下行
  // 因为FreeRTOS调度器会自动接管
}

void loop() {
  delay(10);
}

内存申请与释放

 //从芯片内部申请PSARAM,大小为1000字节,数据类型为char型
 char* str1=(char *)heap_caps_malloc(1000,MALLOC_CAP_INTERNAL);  
 
 //从芯片外部申请PSARAM,大小为1000字节,数据类型为char型
 char* str1=(char *)heap_caps_malloc(1000,MALLOC_CAP_SPIRAM);  
 
 //内存释放
 heap_caps_free(str); 
 
#include <Arduino.h>
#include <esp_heap_caps.h>
 
 
void setup() {
  Serial.begin(115200);
 
  Serial.printf("内部RAM可使用大小: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
  char* str=(char *)heap_caps_malloc(1000,MALLOC_CAP_INTERNAL);
  sprintf(str,"hello world! I am a handsome boy!");
  Serial.println(str);
  Serial.printf("内部RAM可使用大小: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
 
  Serial.printf("外部RAM可使用大小: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
  char* str1=(char *)heap_caps_malloc(1000,MALLOC_CAP_SPIRAM);
  sprintf(str1,"hello world! I am a pretty girl!");
  Serial.println(str1);
  Serial.printf("外部RAM可使用大小: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
 
  Serial.println("释放内存!");
  heap_caps_free(str); 
  heap_caps_free(str1); 
  Serial.printf("内部RAM可使用大小: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
  Serial.printf("外部RAM可使用大小: %d bytes\n", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
  
}
 
void loop() {
 
  delay(10); 
}

Avatar photo

作者 skyate

发表回复