2023.5.11
1.Task
创建任务常用API:
任务函数 | 描述 |
---|---|
xTaskCreate() | 使用动态的方法创建一个任务 |
xTaskCreatePinnedToCore | 指定任务的运行核心(最后一个参数) |
vTaskDelete(NULL) | 删除当前任务 |
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, // 任务函数名
const char *const pcName, // 任务备注
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小
void *const pvParameters, // 传入的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *const pxCreatedTask); // 任务句柄
任务间传参
任务间传参可以使用多种方式,常见的为:
- 使用全局变量:需要注意并发读写的问题,当有两个任务及以上对全局变量进行读写时,需要使用信号量或互斥量进行保护。
- 使用队列:需要注意队列的大小和数据类型的一致性,不需要使用信号量或互斥量进行保护。
队列的读写效率相比全局变量慢一些
使用全局变量进行传参时:
- 传入参数:传递的为指针,且必须进行强制类型转换为空指针
(void *)pt
- 接收参数:把传递过来的空指针进行强制类型转换,转换为对应传输的类型指针
传递整数
#include <Arduino.h>
int a = 1;
void mytask(void *pt)
{
int *b = (int *)pt;
Serial.println(*b);
while (1)
{
}
}
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)&a, 1, NULL, 1);
}
void loop() {}
输出结果为1
传递数组
#include <Arduino.h>
int arr[] = {1, 2, 3};
void mytask(void *pt)
{
int *b = (int *)pt;
int len = sizeof(arr) / sizeof(int); // 数组的长度,注意这里指针占4个字节,要用原数组名
Serial.println(len);
for (int i = 0; i < len; i++)
{
Serial.print(*(b + i)); // 输出数组元素
Serial.print(",");
}
while (1)
{
}
}
void setup()
{
Serial.begin(115200);
// 数组名代表数组元素的首地址,所以不需要&
xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)arr, 1, NULL, 1);
vTaskDelete(NULL);
}
void loop() {}
传递结构体
#include <Arduino.h>
typedef struct
{
int a;
int b;
} Mystruct;
Mystruct test1 = {1, 2};
void mytask(void *pt)
{
Mystruct *test2 = (Mystruct *)pt; // 强制类型转换为结构体指针
Serial.println(test2->a);
Serial.println(test2->b);
while (1) {
}
}
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)&test1, 1, NULL, 1);
vTaskDelete(NULL);
}
void loop() {}
传递字符串
#include <Arduino.h>
const char *str = "hello,world!";
void mytask(void *pt)
{
char *pstr = (char *)pt;
Serial.println(pstr); // 输出hello,world
vTaskDelete(NULL);
}
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)str, 1, NULL, 1);
vTaskDelete(NULL);
}
void loop() {}
任务的优先级
注意:中断任务的优先级永远高于任何任务的优先级。
在ESP32中,默认一共有25个优先级别,最低为0,最高为24。(可修改相关的配置函数进行修改优先级的数目超过25,但是不建议,级别越高,越占内存)。
- 同优先级的任务:FreeRTOS将采用循环调度算法来运行他们,也就是交替执行同优先级的任务。每个任务执行一个时间片,然后将CPU时间片分配给另一个任务。
- 优先级别高的任务先被创建和运行。
任务的调度: - 在FreeRTOS中,
vTaskDelay()
和vTaskDelayUntil()
函数可以暂停当前任务的执行,等待一段时间后再继续执行。(让其他任务有机会执行) taskYIELD()
函数:立即将CPU时间片退让给同等级或更高优先级的任务,如果没有其他任务等待执行,则当前任务会立即继续执行。(简单的说,就是让其他任务执行)
任务的挂起和恢复
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSkjjy2F-1683896646565)(images/1.png)]
任务的状态:running、ready、blocked、suspended(挂起,暂停)
- running:运行状态,如果MCU只有一个内核,那么在任何给定时间内只能有一个任务处于运行状态。
- ready:准备状态(任务刚被创建时,准备执行),不处于堵塞或挂起状态(没有获得CPU执行权限,等待执行状态),因为同等级或更高优先级的任务正在执行
- blocked:使用了
vTaskDelay()或delay()
函数 - suspended:挂起状态,挂起之后,任务被恢复才能继续执行
// API:
TaskHandle_t pxtask = NULL; // 创建任务的句柄
xTaskCreatePinnedToCore(task1, "", 1024 * 2, NULL, 1, &pxtask, 1);
vTaskSuspend(pxtask); // 挂起任务,任务不再执行
vTaskResume(pxtask); // 恢复被挂起的任务,继续执行
vTaskSuspendAll(); // 挂起所有函数,挂起后不可以执行
vTaskResumeAll(); // 恢复所有挂起函数
任务的堆栈设置和调试
创建任务时,如果给任务分配的内存空间过小,会导致程序不断重启。如果分配的内存空间过多,会造成资源浪费。
// API:
ESP.getHeapSize() // 本程序Heap最大尺寸(空间总大小)
ESP.getFreeHeap() // 当前Free Heap最大尺寸(当前可用剩余空间大小)
uxTaskGetStackHighWaterMark(taskHandle) // 计算当前任务剩余多少内存
示例程序:
TaskHandle_t taskHandle; // 创建任务的句柄
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(mytask, "", 1024*3 , NULL, 1, &taskHandle, 1);
int waterMark = uxTaskGetStackHighWaterMark(taskHandle);
Serial.print("Task Free Memory: "); // 任务剩余空间
Serial.print(waterMark);
vTaskDelete(NULL);
}
vTaskDelay()和delay()
一个tick的时间是由FreeRTOS的时钟节拍周期和时钟频率决定的,可以通过配置文件进行设置。默认情况下1 tick = 1ms
vTaskDelay()
函数:以系统时钟节拍(tick)为单位进行延时,例如vTaskDelay(100)表示让任务暂停100个系统时钟节拍的时间。delay()
函数:是一个简单的延时函数,它通常在不需要多任务处理和系统保护的应用中使用。使用后会后边的程序都会被延迟执行。
vTaskDelayUntil()
vTaskDelayUntil
函数比vTaskDelay
函数定时精准。
// API
TickType_t xLastWakeTime = xTaskGetTickCount(); // 获取当前时间
const TickType_t xFrequency = 3000; // 需要的时间间隔
vTaskDelayUntil(&xLastWakeTime, xFrequency);
while(1){
vTaskDelayUntil(&xLastWakeTime, xFrequency);
// 下边为需要运行的函数
}
//
示例程序:
#include <Arduino.h>
void mytask(void *pt)
{
TickType_t xLastWakeTime = xTaskGetTickCount(); // 获取当前时间
const TickType_t xFrequency = 1000; // 需要的时间间隔
while (1)
{
vTaskDelayUntil(&xLastWakeTime, xFrequency);
Serial.println(xTaskGetTickCount()); // 输出当前时间进行验证
}
}
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(mytask, "", 1024 * 3, NULL, 1, NULL, 1);
vTaskDelete(NULL);
}
void loop() {}
2.Queue
2023.5.12
队列:先入先出(FIFO,first in first out)
使用方法:
- 创建队列:长度,尺寸(每个内存空间存储的数据大小)
- 发送数据到队列中
- 从队列中取数据
// portMAX_DELAY - 无限Block
// TickType_t timeOut = portMAX_DELAY; // 无限等待,直到队列中有数据,或者等待数据有空位置可以存储新数据
TickType_t timeOut = 10;
xStatus = xQueueSend(Qhandle, &i, timeOut); // 往队列里发送数据,如果队列里内容是满的就等待10ms再次尝试发送
API | 描述 |
---|---|
xQueueCreate() | 创建一个队列 |
xQueueSend() | 往队列里写数据 |
xQueueReceive | 从队列里读数据 |
uxQueueMessagesWaiting(队列句柄) | 返回值为队列中参数的个数,可用于接收数据时,先判断一下队列里是否有数据 |
// 创建一个队列
QueueHandle_t Qhandle = xQueueCreate(5, sizeof(int)); // 创建一个队列,长度为5,每个空间的大小为int
队列存储int数据
#include <Arduino.h>
// 创建队列的句柄
QueueHandle_t Qhandle = xQueueCreate(5, sizeof(int));
void send(void *pt)
{
int i = 0;
while (1)
{
if (xQueueSend(Qhandle, &i, portMAX_DELAY) != pdPASS)
{
Serial.println(F("队列数据发送失败"));
}
else
{
Serial.print(F("发送成功:"));
Serial.println(i);
}
i++;
if (i == 8)
i = 0;
vTaskDelay(1000);
}
}
void receive(void *pt)
{
int j = 0; // 存储接收的队列数据
while (1)
{
if (xQueueReceive(Qhandle, &j, portMAX_DELAY) != pdPASS)
{
Serial.println(F("接收失败"));
}
else
{
Serial.print(F("接收成功:"));
Serial.println(j);
}
}
}
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(send, "", 1024 * 5, NULL, 1, NULL, 1); // 发送数据
xTaskCreatePinnedToCore(receive, "", 1024 * 5, NULL, 1, NULL, 1); // 接收数据
vTaskDelete(NULL);
}
void loop() {}
运行结果:
发送成功:0
接收成功:0
发送成功:1
接收成功:1
发送成功:2
接收成功:2
发送成功:3
接收成功:3
队列传递结构体(重点)
跟上面的案例类似,只是队列中每个元素类型为struct
,并且发送和接收的数据存储也要设置为struct
类型
#include <Arduino.h>
// 创建一个结构体
typedef struct
{
int a;
int b;
} Mystruct;
// 创建队列的句柄
QueueHandle_t Qhandle = xQueueCreate(5, sizeof(Mystruct));
void send(void *pt)
{
Mystruct struct1 = {1, 2};
while (1)
{
if (xQueueSend(Qhandle, &struct1, portMAX_DELAY) != pdPASS)
{
Serial.println(F("队列数据发送失败"));
}
else
{
Serial.print(F("发送成功:"));
struct1.a++;
Serial.println(struct1.a);
}
vTaskDelay(1000);
}
}
void receive(void *pt)
{
Mystruct struct2; // 接收结构体数据
while (1)
{
if (xQueueReceive(Qhandle, &struct2, portMAX_DELAY) != pdPASS)
{
Serial.println(F("接收失败"));
}
else
{
Serial.print(F("接收成功:"));
Serial.println(struct2.a);
}
}
}
void setup()
{
Serial.begin(115200);
xTaskCreatePinnedToCore(send, "", 1024 * 5, NULL, 1, NULL, 1); // 发送数据
xTaskCreatePinnedToCore(receive, "", 1024 * 5, NULL, 1, NULL, 1); // 接收数据
vTaskDelete(NULL);
}
void loop() {}
运行结果:按照FIFO的规则进行数据的发送和接收
发送成功:2
接收成功:1
发送成功:3
接收成功:2
发送成功:4
接收成功:3
队列传递大型数据时
例如传递字符串。传递大型数据时,把指针对应的数据进行传递。
malloc()
函数:在使用malloc开辟空间时,使用完一定要释放空间,如果不释放会造成内存泄漏。malloc()函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用。指针自身 = (指针类型*)malloc(sizeof(指针类型)*数据数量)
int *p = NULL;
p = (int *)malloc(sizeof(int)*10);
// 使用完之后采用free()进行释放
free(p);
p = NULL; // 让其重新指向NULL
队列的多进单出:多个任务写,一个任务读
多个任务把数据写入一个队列,一个任务进行读。设置写入的任务级别为同级别,读任务的优先级别要比写任务高一级别。
- 不推荐这种方式:容易造成系统工作混乱。最好的工作方式是一个队列只有一个写操作,可以有多个读操作,但是写操作只能有一个。
队列集合(常用):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YDqTMbew-1683896646567)(images/2.png)]
多个队列,但是每个队列只有一个写操作,一个读操作(读取所有队列)
实现步骤:
- 创建队列集合的句柄:同时指定队列集合的总长度
- 将已创建的队列添加到集合中
- 创建一个句柄:从队列集合中获取有数据的队列
QueueHandle_t Qhandle1 = xQueueCreate(5, sizeof(int)); // 队列1
QueueHandle_t Qhandle2 = xQueueCreate(5, sizeof(int)); // 队列2
QueueSetHandle_t QueueSet = xQueueCreateSet(10); // 队列集合句柄,10为队列的总长度
xQueueAddToSet(Qhandle1, QueueSet); // 把队列1加入到队列集合中
xQueueAddToSet(Qhandle2, QueueSet); // 把队列2加入到队列集合中
QueueSetMemberHandle_t QueueData = xQueueSelectFromSet(QueueSet, portMAX_DELAY); // 从队列集合中获取有数据的队列, QueueData为句柄
示例程序:这个程序编译不成功,还没有解决
#include <Arduino.h>
QueueHandle_t Qhandle1 = xQueueCreate(5, sizeof(int)); // 队列1
QueueHandle_t Qhandle2 = xQueueCreate(5, sizeof(int)); // 队列2
QueueSetHandle_t QueueSet = xQueueCreateSet(10); // 队列集合句柄
xQueueAddToSet(Qhandle1, QueueSet); // 把队列1加入到队列集合中
xQueueAddToSet(Qhandle2, QueueSet); // 把队列2加入到队列集合中
QueueSetMemberHandle_t QueueData = xQueueSelectFromSet(QueueSet, portMAX_DELAY); // 从队列集合中获取有数据的队列
void send1(void *pt)
{
int i = 1; // 任务1要发送的数据
while (1)
{
if (xQueueSend(Qhandle1, &i, portMAX_DELAY) != pdPASS)
{
Serial.println("发送失败");
}
else
{
Serial.println("发送成功");
}
vTaskDelay(1000);
}
}
void send2(void *pt)
{
int i = 2; // 任务2要发送的数据
while (1)
{
if (xQueueSend(Qhandle2, &i, portMAX_DELAY) != pdPASS)
{
Serial.println("发送失败");
}
else
{
Serial.println("发送成功");
}
vTaskDelay(1000);
}
}
void receive(void *pt)
{
int i; // 存储接收数据
while (1)
{
if (xQueueReceive(QueueData, &i, portMAX_DELAY) != pdPASS) // portMAX_DELAY,一直等待,直到队列中有数据
{
Serial.println("接收失败");
}
else
{
Serial.print("接收成功:");
Serial.println(i);
}
// vTaskDelay(1000); // 采用了portMAX_DELAY,这里就不需要delay了
}
}
void setup()
{
Serial.begin(9600);
Serial.println("队列创建成功");
xTaskCreatePinnedToCore(send1, "", 1024 * 5, NULL, 1, NULL, 1); // 两个相同的优先级别,轮流发送数据
xTaskCreatePinnedToCore(send2, "", 1024 * 5, NULL, 1, NULL, 1);
xTaskCreatePinnedToCore(receive, "", 1024 * 5, NULL, 2, NULL, 1); // 优先级别2,只要队列中有数据,就读
}
void loop()
{
}
队列邮箱(常用):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yOpA8zwk-1683896646567)(images/3.png)]
只有一个队列,一个任务写,多个任务读
// API
QueueHandle_t Mailbox = xQueueCreate(5, sizeof(int)); // 创建一个队列邮箱
xQueueOverwrite(); // 往队列中写数据
xQueuePeek(); // 从队列中读数据
示例程序:运行不成功
#include <Arduino.h>
QueueHandle_t Mailbox = xQueueCreate(5, sizeof(int));
void send(void *pt)
{
int i = 1; // 任务1要发送的数据
while (1)
{
if (xQueueOverwrite(Mailbox, &i) != pdPASS)
{
Serial.println("发送失败");
}
else
{
Serial.println("发送成功");
i++;
}
vTaskDelay(1000);
}
}
void receive1(void *pt)
{
int i; // 存储接收数据
while (1)
{
if (xQueuePeek(Mailbox, &i, 1000) != pdPASS) // portMAX_DELAY,一直等待,直到队列中有数据
{
Serial.println("接收失败");
}
else
{
Serial.print("接收成功:");
Serial.println(i);
}
}
}
void receive2(void *pt)
{
int i; // 存储接收数据
while (1)
{
if (xQueuePeek(Mailbox, &i,1000) != pdPASS) // portMAX_DELAY,一直等待,直到队列中有数据
{
Serial.println("接收失败");
}
else
{
Serial.print("接收成功:");
Serial.println(i);
}
}
}
void setup()
{
Serial.begin(9600);
Serial.println("队列创建成功");
xTaskCreatePinnedToCore(send, "", 1024 * 5, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(receive1, "", 1024 * 5, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(receive2, "", 1024 * 5, NULL, 2, NULL, 1);
}
void loop()
{
}
信号量
信号量分类:二进制信号量、计数信号量、互斥信号量。
信号量常用于控制对共享资源的访问和任务同步。信号量对于控制共享资源访问的场景相当于一个上锁机制,代码只有获得这个锁的钥匙才能执行。
二进制信号量(常用)
二值信号量常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。
- 二进制信号量可以用于一个任务控制另一个任务的运行与堵塞。
- 二进制信号量只有两种状态:已触发和未触发,类似于一个开关。当一个任务等待一个已经触发的二进制信号量是,它会立即获得信号量,如果信号量未被触发,任务将被堵塞直到信号量被触发。
- 可以避免资源冲突和死锁问题,提高系统的可靠性和效率
// API
SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量
xSemaphoreGive(xHandler); // 获取二进制信号量
xSemaphoreTake(xHanlder, timeout); // 释放二进制信号量
示例程序:按键控制LED的亮灭(已验证)
#include <Arduino.h>
SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量
TickType_t timeOut = 1000;
void task1(void *pt)
{
pinMode(23, OUTPUT);
while (1)
{
if (xSemaphoreTake(xHandler, timeOut) == pdTRUE)
{
digitalWrite(23, !digitalRead(23));
}
}
}
void task2(void *pt)
{
pinMode(22, INPUT_PULLUP);
while (1)
{
if (digitalRead(22) == LOW)
{
xSemaphoreGive(xHandler);
vTaskDelay(120); // button debounce
}
}
}
void setup()
{
Serial.begin(9600);
xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); // 两个相同的优先级别,轮流发送数据
xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, NULL, 1);
}
void loop()
{
}