一、前提案例
1、单 LED 小灯控制
要求: LED灯每秒闪烁一次。
设备:Esp32开发板、面包板、led灯珠、电阻150欧
实现代码:
#include <Arduino.h>
#define LED_PIN 23 // 23号引脚
void setup()
{
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
}
void loop()
{
digitalWrite(LED_PIN, HIGH);
delay(1000);
digitalWrite(LED_PIN, LOW);
delay(1000);
}
2、双 LED 小灯控制
要求: led1 小灯 每隔1s亮1次,led2 小灯 每隔3s亮1次。
按照 单 LED 小灯控制方式实现去观察效果。
#include <Arduino.h>
#define LED_PIN1 23 // 23号引脚
#define LED_PIN2 22 // 22号引脚
void setup()
{
Serial.begin(115200);
pinMode(LED_PIN1, OUTPUT);
pinMode(LED_PIN2, OUTPUT);
}
void loop()
{
// LED1小灯 每隔1s亮一次
digitalWrite(LED_PIN1, HIGH);
delay(1000);
digitalWrite(LED_PIN1, LOW);
delay(1000);
// LED3小灯 每隔3s亮一次
digitalWrite(LED_PIN2, HIGH);
delay(3000);
digitalWrite(LED_PIN2, LOW);
delay(3000);
}
二、问题分析
1、观察现象
双 LED 小灯控制中要求: led1 小灯 每隔1s亮1次,led2 小灯 每隔3s亮1次。 通过从上面的双 LED 小灯控制实验中可以看出:
- LED1从熄灭到点亮,中间经历了7s 【1 + 3 + 3】
- LED2从熄灭到点亮,中间经历了5s 【1 + 1 + 3】
2、原因与解决方案
通过观察代码发现,原因是在代码的 loop函数中的逻辑,它是一行行按顺序往下执行的,LED1和LED2的控制逻辑会相互影响,最终造成延迟增大。
为了实现 led1 小灯 每隔1s亮1次,led2 小灯 每隔3s亮1次 的需求,我们需要引入多任务即RTOS(Real-Time Operating System, 实时操作系统),让两个任务相互不干扰,“同时”运行。
注意:RTOS的实现有很多,其中开源免费的FreeRTOS广受欢迎。
三、FreeRTOS
FreeRTOS是市场领先的面向微控制器和小型微处理器的实时操作系统(RTOS),与世界领先的芯片公司合作开发,现在每 170 秒下载一次。FreeRTOS 通过 MIT 开源许可免费分发,包括一个内核和一组不断丰富的 IoT 库,适用于所有行业领域。FreeRTOS 的构建突出可靠性和易用性。
FreeRTOS是RTOS中开源免费的。ESP32的Arduino框架里面已经内置了FreeRTOS框架,并且对ESP32的双核进行了完美的适配,所以我们在使用时,无需引入第三方库就可以直接使用。
四、FreeRTOS多任务函数
1、xTaskCreate函数
static inline IRAM_ATTR BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask
)
- pvTaskCode:任务函数(任务要执行的逻辑),返回值必须为void,只有一个参数,参数类型必须为void*
- pcName:自定义任务的名称,一个字符串,主要为了方便排查问题;
- usStackDepth:给任务分配的最大堆栈大小,比如2048,单位是字节,这个值需要根据任务复杂度来选择,一般简单的任务,2048~4096范围的值就足够,如果该任务需要处理的逻辑确实比较繁重,可以适当增大,比如8192
- pvParameters:传递给任务的参数,类型是void*;
- uxPriority:任务优先级,取值范围为[0, 24],数字越大,优先级越高
- pxCreatedTask:用于接收该任务的句柄,后续对该任务的操作,需要基于该句柄完成。
2、实现步骤
01、定义任务函数pvTaskCode
定义 pvTaskCode 任务函数handle_led1 和 handle_led2,参数都只有1个,参数类型必须为void*, 且返回值为void类型。
=================================================================
void handle_led1(void *ptr)
{
pinMode(LED1, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED1, HIGH);
vTaskDelay(1000);
digitalWrite(LED1, LOW);
vTaskDelay(1000);
}
vTaskDelete(NULL);
}================================================================
void handle_led2(void *ptr)
{
pinMode(LED2, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED2, HIGH);
vTaskDelay(3000);
digitalWrite(LED2, LOW);
vTaskDelay(3000);
}
vTaskDelete(NULL);
}=================================================================
02、定义任务名称
分别为handle_led1 和 handle_led2任务函数定义任务名称: "task1"、"task2"
03、设置堆栈大小
分别为handle_led1 和 handle_led2任务设置堆栈大小:2048、2048
04、设置任务参数
分别为handle_led1 和 handle_led2设置任务的参数,类型是void*:(void *)"led1"、(void *)"led2"
05、设置优先级
分别为handle_led1 和 handle_led2设置任务的优先级分别为:1、1
06、设置任务句柄
分别为handle_led1 和 handle_led2设置任务句柄:
TaskHandle_t task1; TaskHandle_t task2;3、完整xTaskCreate函数
xTaskCreate(handle_led1,"task1",2048,(void *)"led1",1,&task1);
xTaskCreate(handle_led2,"task2",2048,(void *)"led2",1,&task2);
4、双 LED 小灯控制多任务执行
#include <Arduino.h>
#define LED1 23 // 控制第一颗LED灯的引脚
#define LED2 22 // 控制第二颗LED灯的引脚TaskHandle_t task1;
TaskHandle_t task2;void handle_led1(void *ptr)
{
pinMode(LED1, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED1, HIGH);
vTaskDelay(1000);
digitalWrite(LED1, LOW);
vTaskDelay(1000);
}
vTaskDelete(NULL);
}void handle_led2(void *ptr)
{
pinMode(LED2, OUTPUT);
char *param = (char *)ptr;
Serial.print(param);
Serial.println(" started");
while (1)
{
digitalWrite(LED2, HIGH);
vTaskDelay(3000);
digitalWrite(LED2, LOW);
vTaskDelay(3000);
}
vTaskDelete(NULL);
}void setup()
{
Serial.begin(115200);
pinMode(LED_PIN1, OUTPUT);
pinMode(LED_PIN2, OUTPUT);
xTaskCreate(handle_led1, "task1_led", 2048, (void *)"led1", 1, &task1);
xTaskCreate(handle_led2, "task2_led", 2048, (void *)"led2", 1, &task2);
}
void loop()
{
}
五、其他函数
1、vTaskDelete函数:
- xTaskToDelete:表示要删除的任务的句柄,这个句柄就是上面xTaskCreate函数中传入的最后一个参数~,如果参数设置为NULL,表示删除当前任务。一般建议,该函数在每个任务运行结束时传入NULL调用,不建议跨任务删除,有些情况下会产生一些不可预知的问题。
2、 vTaskDelay函数
delay的底层函数也是这个函数。
void vTaskDelay( const TickType_t xTicksToDelay )
xTicksToDelay是延迟的ticks数,对于ESP32,一个tick等于1ms,所以要延迟3秒,就应该调用vTaskDelay(3000)
六、任务绑定CPU
将创建的任务绑定到固定的cpu执行。
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pvCreatedTask,
const BaseType_t xCoreID);
xCoreID 是cpu编号
完整案例:
#include <Arduino.h>
#define LED_PIN1 23 // 23号引脚
#define LED_PIN2 22 // 22号引脚
TaskHandle_t task1;
TaskHandle_t task2;
void handle_led1(void *pra)
{
char *param = (char *)pra;
Serial.print("handle_led1 传入的数据参数是:");
Serial.println(param);
while (1)
{
Serial.print("handle_led1 绑定的CPU是:");
Serial.println(xPortGetCoreID());
digitalWrite(LED_PIN1, HIGH);
vTaskDelay(1000);
digitalWrite(LED_PIN1, LOW);
vTaskDelay(1000);
}
vTaskDelete(NULL);
}
void handle_led2(void *pra)
{
char *param = (char *)pra;
Serial.print("handle_led2 传入的数据参数是:");
Serial.println(param);
while (1)
{
Serial.print("handle_led2 绑定的CPU是:");
Serial.println(xPortGetCoreID());
digitalWrite(LED_PIN2, HIGH);
vTaskDelay(3000);
digitalWrite(LED_PIN2, LOW);
vTaskDelay(3000);
}
vTaskDelete(NULL);
}
void setup()
{
Serial.begin(115200);
pinMode(LED_PIN1, OUTPUT);
pinMode(LED_PIN2, OUTPUT);
xTaskCreatePinnedToCore(handle_led1, "task1_led", 2048, (void *)"led1", 1, &task1, 0);
xTaskCreatePinnedToCore(handle_led2, "task2_led", 2048, (void *)"led2", 1, &task2, 1);
}
void loop()
{
}