常规单核处理器一次只能执行一个任务,但多任务操作系统通过将处理器时间进行分片,并在时间片上运行不同任务, 使所有任务看起来像是同时在执行。下图展示了 三个任务相对于时间的执行模式。任务名称用不同颜色标示,并写在左侧。时间从左向右移动, 彩色线条显示在特定时间执行的任务。上方展示了所感知的并发执行模式, 下方展示了实际的多任务执行模式。

使用多任务操作系统可以简化原本复杂的软件应用程序的设计:
xTaskCreate 函数是用来创建一个新的任务,原型如下:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, /* 任务函数 */
const char * const pcName, /* 任务名称 */
const uint32_t uxStackDepth, /* 任务堆栈字数 */
void *pvParameters, /* 自定义参数指针 */
UBaseType_t uxPriority, /* 任务优先级 */
TaskHandle_t *pxCreatedTask /* 任务句柄指针 */
);
xTaskCreatePinnedToCore() 函数是是 ESP-IDF 对 FreeRTOS 的扩展函数,可创建一个在指定处理器核心上执行的任务,原型如下:
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode, /* 任务函数 */
const char * const pcName, /* 任务名称 */
const uint32_t usStackDepth, /* 任务堆栈字数 */
void *pvParameters, /* 自定义参数指针 */
UBaseType_t uxPriority, /* 任务优先级 */
TaskHandle_t *pxCreatedTask, /* 任务句柄指针 */
const BaseType_t xCoreID /* 内核ID,对于ESP32通常为0或1 */
);
任务可以存在于以下状态中:
运行:当任务实际执行时,它被称为处于运行状态。任务当前正在使用处理器。 如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。
准备就绪:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
阻塞:如果任务当前正在等待时间或外部事件,则该任务被认为处于阻塞状态。 例如,如果一个任务调用vTaskDelay(),它将被阻塞(被置于阻塞状态), 直到延迟结束——一个时间事件。 任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量事件。处于阻塞状态的任务通常有一个"超时"期, 超时后任务将被超时,并被解除阻塞, 即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间,不能被选择进入运行状态。
挂起:与“阻塞”状态下的任务一样, “挂起”状态下的任务不能被选择进入运行状态,但处于挂起状态的任务没有超时。相反,任务只有在分别通过 vTaskSuspend() 和 xTaskResume() API 调用明确命令时才会进入或退出挂起状态。

Task是开发框架中封装FreeRTOS任务相关函数的类,方便使用者操作任务,如创建、挂起、恢复、改变优先级等。
在Task类的实现上,通过定义初始化函数(执行一次)和循环体函数(重复执行)来简化使用。
任务启动,代码如下(src/framework/src/task.h):
int Start(uint32_t stack_depth, uint8_t priority) {
return xTaskCreate(
[](void *parameter) {
Task* task = (Task *)parameter;
if (task->init_function_!=nullptr) {
task->init_function_();
}
if (task->loop_function_!=nullptr) {
while (1) {
task->loop_function_();
}
}
vTaskDelete(NULL);
}, /* 任务函数 TaskFunction_t */
name_.c_str(), /* 任务名称 const char* */
stack_depth, /* 堆栈字数 */
this, /* 自定义参数指针 void* */
priority, /* 任务优先级 UBaseType_t */
&task_handle_ /* 任务句柄指针 TaskHandle_t* */
);
}
Start方法接收两个参数,其中stack_depth为堆栈字数,priority为优先级,
xTaskCreate创建任务,任务函数为lambda表达式函数,若设置了初始化函数,则执行它,若设置了循环体函数,则在循环内执行它,若未设置循环体函数,则删除任务,相当于只执行一次的任务。
从 https://gitee.com/billyzh/esp32-cpp-lesson 下载本教程的源码到本地硬盘文件夹,如d:\esp32-cpp-lesson
在VSCode中,选择【文件】->【打开文件夹...】选择上一步保存的文件夹打开
打开项目后,选择config.h文件,修改第10行为
#define APP_LESSON62 1
打开unit6-lesson62/board_config.h文件,设置LED使用的引脚,
#define BUILTIN_LED_PIN GPIO_NUM_4
配置启用GpioLed
#define CONFIG_USE_LED_GPIO 1
创建LED实例,代码如下(unit6-lesson62/my_board.cpp):
MyBoard::MyBoard() : Board() {
Log::Info(TAG, "===== Create Board ...... =====");
Log::Info(TAG, "initial led.");
led_ = new GpioLed(BUILTIN_LED_PIN, false);
Log::Info( TAG, "===== Board config completed. =====");
}
程序很简单,就是创建一个GpioLed实例。
多任务应用
代码如下(unit6-lesson62/my_application.cpp):
void MyApplication::OnInit() {
task1_ = new Task(std::string("task1"));
task1_->OnLoop([this](){
Task1Loop();
});
task1_->Start(4096, tskIDLE_PRIORITY+1);
task2_ = new Task(std::string("Task2"));
task2_->OnLoop([this](){
Task2Loop();
});
task2_->Start(4096, tskIDLE_PRIORITY+1);
}
/**
* 任务1的循环执行体
*/
void MyApplication::Task1Loop() {
Led *led = Board::GetInstance().GetLed();
if (state_==0) {
led->TurnOn();
state_ = 1;
} else {
led->TurnOff();
state_ = 0;
}
delay(1000);
}
/**
* 任务2的循环执行体
*/
void MyApplication::Task2Loop() {
long long n = 0;
for (int i=1; i<10000000; i++) {
n += i;
}
Log::Info(TAG, "1...10000000 = %lld", n);
delay(2000);
}
程序解读
1. 在OnInit方法内,创建两个Task实例,然后启动任务,Task是封装FreeRTOS任务相关函数的类;
2. Task1Loop方法是任务1的循环体方法,具体为控制Led的打开和关闭;
3. Task2Loop方法是任务2的循环体方法,具体为计算从1到10000000的累加值;
编译项目并上传开发板检验
步进电机是将电脉冲信号,转变为角位移或线位移的开环控制电机,又称为脉冲电机。
本节主要讲解Timer类,FreeRTOS定时器的使用。
本节主要讲解舵机驱动类和用按键控制舵机。
本节主要讲解执行器件类型和用按键控制继电器。
本小节主要讲解红外接收和遥控器件,以及遥控操作LED。
本小节讲解模拟量传感器使用,旋转电位器,DHT11温湿度传感器和实现自定义传感器类。
本小节讲解Sensor类及派生类、数字量传感器使用和传感器的推荐交互流程。
本小节讲解ESP32内置触摸引脚的用法,
本小节主要介绍按键信号转换、Button类及派生类、和Button交互推荐流程。
本小节主要介绍Ws2812灯珠的使用、对父类进行扩展实现自定义功能,和指针向下强制转换的使用。
本小节主要介绍RGB三色LED的使用,以及多态的具体实现。