ESP32 FreeRTOS学习总结
2023.5.11
FreeRTOS中文数据手册:https://www.freertos.org/zh-cn-cmn-s/RTOS.html
感谢以下两位B站UP主的教程:孤独的二进制、Michael_ee
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,worldvTaskDelete(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-PQOP5J54-1684032842836)(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-qgLZATfT-1684032842837)(images/2.png)]
多个队列,但是每个队列只有一个写操作,一个读操作(读取所有队列)
实现步骤:
- 创建队列集合的句柄:同时指定队列集合的总长度
- 将已创建的队列添加到集合中
- 创建一个句柄:从队列集合中获取有数据的队列
QueueHandle_t Qhandle1 = xQueueCreate(5, sizeof(int)); // 队列1
QueueHandle_t Qhandle2 = xQueueCreate(5, sizeof(int)); // 队列2QueueSetHandle_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)); // 队列2QueueSetHandle_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-CUCtItFd-1684032842837)(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()
{
}
3.信号量
信号量分类:二进制信号量、计数信号量、互斥信号量。
信号量就像红绿灯一样,控制车辆的通行。
信号量常用于控制对共享资源的访问和任务同步。信号量对于控制共享资源访问的场景相当于一个上锁机制,代码只有获得这个锁的钥匙才能执行。
- 使用队列、信号量,都可以实现互斥访问。
二进制信号量(常用)
二值信号量常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。
- 二进制信号量可以用于一个任务控制另一个任务的运行与堵塞。
- 二进制信号量表示只有两个值:0和1
- 二值信号量相当于长度为1的队列
- 二进制信号量只有两种状态:已触发和未触发,类似于一个开关。当一个任务等待一个已经触发的二进制信号量是,它会立即获得信号量,如果信号量未被触发,任务将被堵塞直到信号量被触发。
- 可以避免资源冲突和死锁问题,提高系统的可靠性和效率
// API
SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量
xSemaphoreGive(xHandler); // 释放信号量
xSemaphoreTake(xHanlder, timeout); // 在指定时间内获取信号量,返回值为pdPASS, 或者pdFAIL
示例程序1:按键控制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()
{
}
示例2:采用二进制信号量对任务进行管理,对全局变量进行读写
- 该示例验证了,使用二进制信号量可以很好的控制任务的执行顺序。
#include <Arduino.h>
int a = 0;
SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量void task1(void *pt)
{while (1){xSemaphoreTake(xHandler, portMAX_DELAY); // 无限等待,直到获取信号量for (int i = 0; i < 10; i++){a++;printf("mytask1 a = %d\n", a);}xSemaphoreGive(xHandler); // 执行完之后,需要再次释放信号量vTaskDelay(1000);}
}void task2(void *pt)
{while (1){xSemaphoreTake(xHandler, portMAX_DELAY); // 无限等待,直到获取信号量for (int i = 0; i < 10; i++){a++;printf("mytask2 a = %d\n", a);}xSemaphoreGive(xHandler); // 执行完之后,需要再次释放信号量vTaskDelay(1000);}
}void setup()
{Serial.begin(115200);xSemaphoreGive(xHandler); // 首先释放一次信号量,不然运行不了xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); // task1先获取信号量,执行一次xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, NULL, 1); // 然后task2获取信号量,执行一次,task11再执行
}void loop()
{
}
运行结果:
- 采用二进制信号量:
mytask1 a = 1
mytask1 a = 2
mytask1 a = 3
mytask1 a = 4
mytask1 a = 5
mytask1 a = 6
mytask1 a = 7
mytask1 a = 8
mytask1 a = 9
mytask1 a = 10
mytask2 a = 11
mytask2 a = 12
mytask2 a = 13
mytask2 a = 14
mytask2 a = 15
mytask2 a = 16
mytask2 a = 17
mytask2 a = 18
mytask2 a = 19
mytask2 a = 20
mytask1 a = 21
- 不采用二进制信号量运行结果:
ytask1 a = 1
mytask1 a = 2
mytask1 a = 3
mytask1 a = 4
mytask1 a = 6
mytask1 a = 7
mytask1 a = 8
mytask1 a = 9
**mytask1 a = 10**
**mytask1 a = 11**
**mytask2 a = 5**
**mytask2 a = 12**
mytask2 a = 13
计数信号量
与二进制不同的是,计数信号量可以有更多的状态。
- 计数信号量相当于长度大于1的队列,同二值信号量一样,不需要关系队列中存储了什么数据,只需要关心队列是否为空即可。
- 例如在一个停车场中有10个车位,车辆进入时,车位被占用(计数器减1),车开出去后(计数器加1),为0时表示没有可用的车位。
// API
uxSemaphoreGetCount( semphrHandle); // 获得计数型信号量的值
SemaphoreHandle_t semphrHandle = xSemaphoreCreateCounting(10,0);// 创建计数型信号量,参数1:最大值,参数2:初始值
xSemaphoreGive(semphrHandle); // 释放信号量
xSemaphoreTake(semphrHandle); // 获取信号量
使用场合:事件计数、资源管理
- 共享资源的访问控制:当多个任务需要共享同一个资源时,可以使用计数信号量来控制资源的访问。每个任务需要访问资源时,都需要获取一个计数信号量,如果计数信号量的值为0,则任务会被堵塞,直到其他任务释放资源并增加计数信号量的值。这种方式可以避免资源冲突和死锁等问题。
- 控制任务的执行顺序:有些情况下,需要控制任务的执行顺序,例如任务A必须在任务B执行完成之后才能执行。可以使用计数信号量来实现这种控制。任务B执行完成后,可以增加计数信号量的值,任务A等待计数信号量的值为1时,可以获取信号量并开始执行。
示例1:模拟停车场的停车位
#include <Arduino.h>// 创建计数型信号量,参数1:最大值,参数2:初始值
SemaphoreHandle_t semphrHandle = xSemaphoreCreateCounting(5, 5); // 初值为5,代表初始有5个空车位void carintask(void *pt)
{int emptySpace = 0; // 空的停车位BaseType_t iResult;while (1){emptySpace = uxSemaphoreGetCount(semphrHandle);printf("emptySpace = %d\n", emptySpace);iResult = xSemaphoreTake(semphrHandle, 0); // 获取信号量if (iResult == pdPASS)printf("One car in\n");elseprintf("No Space\n");vTaskDelay(1000);}
}void caroutTask(void *pt)
{while (1){vTaskDelay(6000);xSemaphoreGive(semphrHandle); // 释放信号量printf("One car out\n");}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(carintask, "", 1024 * 5, NULL, 1, NULL, 1);xTaskCreatePinnedToCore(caroutTask, "", 1024 * 5, NULL, 1, NULL, 1);
}void loop()
{
}
互斥信号量(常用)
与二进制信号量十分相似。Mutex的工作原理可以想象成共享的资源被锁在一个箱子里,只有一把钥匙,有钥匙的任务才能对共享资源进行访问。
- 互斥量与二进制信号量的区别:优先级继承,在FreeRTOS中,当一个任务持有一个互斥量时,该任务对共享资源的访问是独占的,其他试图获取该互斥量的任务将被堵塞。当一个优先级更高的任务试图获取已经被持有的互斥量时,FreeRTOS会自动暂时提高持有互斥量任务的优先级别,使其具有与试图获取互斥量的任务相同的优先级别。这样可以确保高优先级别的任务在获取共享资源时能够及时执行,并避免低优先级别任务长时间持有共享资源。当持有互斥量的任务释放互斥量时,其优先级别将恢复到原始值,而不是保持被继承的优先级。这样可以确保任务在不需要共享资源时恢复其原始优先级,以避免低优先级任务一直持有高优先级任务的优先级,导致高优先级任务无法及时执行其他任务。
- 也就是对于不同优先级别的任务,采用
mutex
对共享资源进行保护,如果任务均为同优先级别,可以采用二进制信号量
进行共享资源保护 - 可以理解为
互斥量
是二进制信号量
的升级版 - 注意:使用完立即释放钥匙
SemaphoreHandle_t mutexHandler = xSemaphoreCreateMutex(); // 创建一个Mutex互斥量
xSemaphoreGive(mutexHandler); // 释放信号量
xSemaphoreTake(mutexHandler, timeout); // 在指定时间内获取信号量,返回值为pdPASS, 或者pdFAIL
示例程序:
// 对于互斥量通常创建3个任务
#include <Arduino.h>SemaphoreHandle_t mutexHandler = xSemaphoreCreateMutex(); // 创建mutex句柄void task1(void *pt)
{printf("task1 begin\n");while (1){xSemaphoreTake(mutexHandler, portMAX_DELAY);printf("tsak1 take\n");for (size_t i = 0; i < 15; i++){printf("task1 i = %d\n", i);vTaskDelay(1000);}xSemaphoreGive(mutexHandler);printf("tsak1 give\n");}
}
void task2(void *pt)
{printf("task2 begin\n");vTaskDelay(1000); // 让低优先级别的任务有机会执行while (1){;}
}
void task3(void *pt)
{printf("task3 begin\n");vTaskDelay(1000); // 让低优先级别的任务有机会执行while (1){xSemaphoreTake(mutexHandler, portMAX_DELAY); // 获取信号量printf("tsak3 take\n");for (size_t i = 0; i < 10; i++){printf("task3 i = %d\n", i);vTaskDelay(1000);}xSemaphoreGive(mutexHandler); // 释放信号量printf("tsak3 give\n");}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); // task1获取到信号量时,如果task3高优先级的任务也尝试获取该信号量,会将task1的优先级暂时升级为3xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 2, NULL, 1);xTaskCreatePinnedToCore(task3, "", 1024 * 5, NULL, 3, NULL, 1); // 优先级别最高,最先执行vTaskDelete(NULL); // 删除当前任务
}void loop()
{
}
递归互斥量
允许同一任务在持有互斥量的情况下再次获取该互斥量,而不会导致死锁。
递归互斥量可以用于需要对同一资源进行多层保护的情况,例如嵌套调用的函数。
SemaphoreHandle_t mutexHandler = xSemaphoreCreateRecursiveMutex(); // 创建递归互斥量
xSemaphoreTakeRecursive(mutexHandler); // 获取信号量
xSemaphoreGiveRecursive(mutexHandler); // 释放信号量
- 注意:使用递归互斥量时,获取和释放的次数要相等,以避免死锁的情况。
#include <Arduino.h>SemaphoreHandle_t mutexHandler = xSemaphoreCreateRecursiveMutex(); // 创建mutex句柄void task1(void *pt)
{while (1){printf("task1 begin\n");xSemaphoreTakeRecursive(mutexHandler, portMAX_DELAY); // 第一次取得信号量printf("tsak1 take\n");for (size_t i = 0; i < 5; i++){printf("task1 i = %d for A\n", i);vTaskDelay(1000);}xSemaphoreTakeRecursive(mutexHandler, portMAX_DELAY); // 第二次取得信号量for (size_t i = 0; i < 5; i++){printf("task1 i = %d for B\n", i);vTaskDelay(1000);}xSemaphoreGiveRecursive(mutexHandler);xSemaphoreGiveRecursive(mutexHandler);printf("tsak1 give\n");taskYIELD();}
}void task2(void *pt)
{vTaskDelay(1000);while (1){printf("task2 begin\n");xSemaphoreTakeRecursive(mutexHandler, portMAX_DELAY);printf("tsak2 take\n");xSemaphoreGiveRecursive(mutexHandler);printf("tsak2 give\n");taskYIELD();}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1);xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, NULL, 1);vTaskDelete(NULL); // 删除当前任务
}void loop()
{
}
运行结果:
task1 begin
tsak1 take
task1 i = 0 for A
task2 begin
task1 i = 1 for A
task1 i = 2 for A
task1 i = 3 for A
task1 i = 4 for A
task1 i = 0 for B
task1 i = 1 for B
task1 i = 2 for B
task1 i = 3 for B
task1 i = 4 for B
tsak1 give
tsak2 take
tsak2 give
task1 begin
task2 begin
tsak2 take
tsak2 give
tsak1 take
4.任务通知(重要)
从FreeRTOS V8.2.0版本,新增了任务通知(task notify)这个功能,可用使用任务通知来代替信号量、消息队列、事件标志组这些东西。使用任务通知可用提高系统的工作效率。
FreeRTOS的每个任务都有一个32位的通知值,任务控制块中的成员变量ulNotifiedValue就是这个通知值。
- 使用任务通知,可以控制任务的流向,执行顺序。
任务通知虽然可用提高速度,并且减少RAM的使用,但是任务通知也是有使用限制的:
- FreeRTOS的任务通知只能有一个接收任务,大多数的应用都是这种情况
- 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。
通知同步
示例程序:任务2通知任务1执行,如果任务1没有接收到任务通知,就一直处于阻塞状态。更详细的内容,参考FreRTOS中文数据手册:2.18 xTaskNotifyGive()
#include <Arduino.h>static TaskHandle_t xTask1 = NULL, xTask2 = NULL; // 创建任务的句柄void task1(void *pt)
{while (1){printf("task1 wait notification!\n");ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 阻塞自身,等待通知执行下面的程序printf("task1 got notification\n");}
}void task2(void *pt)
{while (1){vTaskDelay(1000); // 1s发送1次通知printf("task2 notify task1!\n");xTaskNotifyGive(xTask1); // 通知任务1解锁阻塞状态}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, &xTask1, 1);xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, &xTask2, 1);vTaskDelete(NULL); // 删除当前任务
}void loop() {}
运行结果:
task1 wait notification!
task2 notify task1!
task1 got notification
task1 wait notification!
任务通知值
通过通知不同的值,可以控制任务进入不同的处理流程。
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, // 在进入函数时,清除所有函数的通知值uint32_t ulBitsToClearOnExit, // 在退出的时候清楚uint32_t *pulNotificationValue, // 取得当前任务通知的值TickType_t xTicksToWait); // 等待时间BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, // 任务通知的句柄uint32_t ulValue, // 需要发送的任务通知值eNotifyAction eAction); // 常用eSetValueWithOverwrite
示例程序:通过设置不同的通知值,执行不同的事件,参考数据手册:2.14 xTaskNotify()
#include <Arduino.h>static TaskHandle_t xTask1 = NULL, xTask2 = NULL; // 创建任务的句柄void task1(void *pt)
{uint32_t ulNotifiedValue;while (1){printf("task1 wait notification!\n");xTaskNotifyWait(0x00, /* Don't clear any notification bits on entry. */ULONG_MAX, /* Reset the notification value to 0 on exit. */&ulNotifiedValue, /* Notified value pass out in ulNotifiedValue. */portMAX_DELAY); /* Block indefinitely. *//* Process any events that have been latched in the notified value. */if ((ulNotifiedValue & 0x01) != 0){/* Bit 0 was set - process whichever event is represented by bit 0. */printf("task1 process bit0 event!\n");}if ((ulNotifiedValue & 0x02) != 0){/* Bit 1 was set - process whichever event is represented by bit 1. */printf("task1 process bit1 event!\n");}if ((ulNotifiedValue & 0x04) != 0){/* Bit 2 was set - process whichever event is represented by bit 2. */printf("task1 process bit2 event!\n");}/* Etc. */}
}void task2(void *pt)
{while (1){vTaskDelay(1000); // 1s发送1次通知printf("task2 notify bit0!\n");xTaskNotify(xTask1, 0x01, eSetValueWithOverwrite);vTaskDelay(1000);printf("task2 notify bit1!\n");xTaskNotify(xTask1, 0x02, eSetValueWithOverwrite);vTaskDelay(1000);printf("task2 notify bit2!\n");xTaskNotify(xTask1, 0x04, eSetValueWithOverwrite);}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, &xTask1, 1);xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, &xTask2, 1);vTaskDelete(NULL); // 删除当前任务
}void loop() {}
运行结果:
task1 wait notification!
task2 notify bit0!
task1 process bit0 event!
task1 wait notification!
task2 notify bit1!
task1 process bit1 event!
task1 wait notification!
task2 notify bit2!
task1 process bit2 event!
任务通知取代信号量
使用直接任务通知取代二进制信号量,由于没有了二进制信号量这个中间媒介,不仅节省了内存,而且速度也会快45%。
- 注意:设置任务的阻塞和通知顺序要注意先后顺序。
直接任务通知不能取代二进制信号量的场景:
- 直接任务通知相当于严格指定任务的执行顺序,而采用二进制信号量可以做到在等待事件内各任务随机抢占CPU执行权
- 因此,当有2个及以上需要接收信号量时,最好采用二进制信号量,而不是直接任务通知
#include <Arduino.h>static TaskHandle_t xTask1 = NULL, xTask2 = NULL; // 创建任务的句柄void task1(void *pt)
{while (1){xTaskNotifyGive(xTask2); // 通知任务2执行/* Block to wait for prvTask2() to notify this task. */ulTaskNotifyTake(pdTRUE, portMAX_DELAY);}
}void task2(void *pt)
{while (1){/* Block to wait for prvTask1() to notify this task. */ulTaskNotifyTake(pdTRUE, portMAX_DELAY);xTaskNotifyGive(xTask1); // 通知任务1执行}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, &xTask1, 1);xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, &xTask2, 1);vTaskDelete(NULL); // 删除当前任务
}void loop() {}
直接任务当作邮箱
可以通过设置任务通知值的方式达到想要的效果。
5.流媒体缓存
流媒体:音频、视频
- 适合一个任务读,一个任务写,不适合多任务读写
- 与队列的不同:stream buffer读写的大小没有限制,而队列是预先设置好的固定值
#include <freertos/stream_buffer.h> // 首先添加流媒体相关的头文件// 创建streambuffer
StreamBufferHandle_t xStreamBufferCreate(size_t xBufferSizeBytes, // 参数1:buffer的大小size_t xTriggerLevelBytes); // 参数2:最小一帧数据的大小,例如一个声音最少8个字节,则设置为8,// Stream Buffer内数据超过这个数值,才会被读取,否则一直接收进行存储,达到这个值进行一次读取// 发送流媒体数据
size_t xStreamBufferSend(StreamBufferHandle_t xStreamBuffer, // 句柄const void *pvTxData, // 需要发送数据的指针,需要进行强制类型转换size_t xDataLengthBytes, // 需要发送数据的长度,可以用sizeof()计算TickType_t xTicksToWait); // 堵塞时间// 接收流媒体数据
size_t xStreamBufferReceive(StreamBufferHandle_t xStreamBuffer,void *pvRxData, // 接收数据,需要进行强制类型转换size_t xBufferLengthBytes, // 存储接收数据的buffer长度TickType_t xTicksToWait);
- 注意:创建stream buffer时,
xTriggerLevelBytes
的设置非常重要。在接收函数中,如果buffer中有数据,首先会将能够接收的数据接收下来,然后堵塞当前的任务,直到buffer中的数据大于xTriggerLevelBytes
确定stream buffer的大小
在创建stream buffer时,如果创建的buffer太大,会造成资源浪费,太小系统工作会非常不稳定。
- 使用stream buffer时,通常创建3个任务
- 任务1发送数据,任务2接收数据,任务3监控stream buffer的空间大小
// API
size_t xStreamBufferBytesAvailable( StreamBufferHandle_t xStreamBuffer ); // stream buffer已使用字节
size_t xStreamBufferSpacesAvailable( StreamBufferHandle_t xStreamBuffer ); // sream buffer可用空间字节
示例:
#include <Arduino.h>
#include <string>
#include <freertos/stream_buffer.h>// 参数1:stream buffer的总大小,参数2:每帧数据的大小
// 参数2作用:接收数据时,如果小于这个值将处于堵塞状态,直到接收buffer里存储的字节大于这个值,才会接收一次
StreamBufferHandle_t streamHandler = xStreamBufferCreate(200, 50);void task1(void *pt)
{char tx_buffer[50];int str_len = 0; // 字符串长度int i = 0;int send_bytes = 0; // 实际发送的数据while (1){i++;str_len = sprintf(tx_buffer, "hello send i= %d ", i); // 要发送的数据send_bytes = xStreamBufferSend(streamHandler, (void *)tx_buffer, str_len, portMAX_DELAY); // 没有发送成功就一直处于堵塞状态printf("--------------\n");printf("Send: str_len =%d, send_bytes= %d\n", str_len, send_bytes);vTaskDelay(3000);// taskYIELD();}
}void task2(void *pt)
{char rx_buffer[50]; // 存储接收的数据int rec_bytes = 0; // 接收到多少数据while (1){memset(rx_buffer, 0, sizeof(rx_buffer)); // 初始化buffer为0rec_bytes = xStreamBufferReceive(streamHandler, (void *)rx_buffer, sizeof(rx_buffer), portMAX_DELAY);printf("--------------\n");printf("Receive: rec_bytes=%d, rec_data: %s\n", rec_bytes, rx_buffer);}
}void task3(void *pt)
{size_t buf_space = 0; // stream buffer可用空间int min_space = 1000; // buffer的初始值while (1){buf_space = xStreamBufferSpacesAvailable(streamHandler);if (buf_space < min_space){min_space = buf_space;}// 通过观察min_space的输出值,当接收数据可用正常接收时,min_space的值 ,采用1000-min_space得到的结果就是需要设置的buffer空间大小printf("buf_space = %d, min_space = %d\n", buf_space, min_space);vTaskDelay(3000);}
}void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "发送", 1024 * 5, NULL, 1, NULL, 1);xTaskCreatePinnedToCore(task2, "接收", 1024 * 5, NULL, 1, NULL, 1);xTaskCreatePinnedToCore(task3, "监控", 1024 * 5, NULL, 1, NULL, 1);vTaskDelete(NULL); // 删除当前任务
}void loop()
{
}
运行结果:
--------------
Send: str_len =16, send_bytes= 16
--------------
buf_space = 200, min_space = 200
Receive: rec_bytes=16, rec_data: hello send i= 1
--------------
Send: str_len =16, send_bytes= 16
buf_space = 184, min_space = 184
--------------
Send: str_len =16, send_bytes= 16
buf_space = 168, min_space = 168
--------------
Send: str_len =16, send_bytes= 16
buf_space = 152, min_space = 152
--------------
Send: str_len =16, send_bytes= 16
--------------
Receive: rec_bytes=50, rec_data: hello send i= 2 hello send i= 3 hello send i= 4 he6 9ô
--------------
Receive: rec_bytes=14, rec_data: llo send i= 5
buf_space = 186, min_space = 152
6.消息缓存
消息缓存与流媒体缓存的区别:
- 消息缓存一次只能接收一条完整的消息
message buffer
在接收buffer信息时,如果定义的buffer空间大小,小于一条消息的长度,则无法正常接收一条完整的消息,返回值为0。而对于stream buffer
,只要buffer中有数据,就可以获取对应长度的数据。
// API
#include <freertos/message_buffer.h> // 添加相关库文件
MessageBufferHandle_t xMessageBufferCreate( size_t xBufferSizeBytes ); // 创建,参数:buffer的大小// 接收信息
size_t xMessageBufferReceive(MessageBufferHandle_t xMessageBuffer,void *pvRxData,size_t xBufferLengthBytes, TickType_t xTicksToWait);// 发送信息
size_t xMessageBufferSend(MessageBufferHandle_t xMessageBuffer,const void *pvTxData,size_t xDataLengthBytes,TickType_t xTicksToWait);
示例程序:发送和接收三条消息
#include <Arduino.h>
#include <string>
#include <freertos/message_buffer.h> // 添加相关库文件MessageBufferHandle_t messageHandler = xMessageBufferCreate(1000); // 创建消息缓存buffervoid task1(void *pt)
{char tx_buffer[50];int str_len = 0; // 字符串长度int i = 0;int send_bytes = 0; // 实际发送的数据// 创建三条消息for (int i = 0; i < 3; i++){str_len = sprintf(tx_buffer, "hello, nomber %d\n", i);send_bytes = xMessageBufferSend(messageHandler, (void *)tx_buffer, str_len, portMAX_DELAY);printf("--------------\n");printf("Send:i=%d, send_bytes = %d\n", i, send_bytes);}vTaskDelete(NULL);
}void task2(void *pt)
{char rx_buffer[200]; // 存储接收的数据int rec_bytes = 0; // 接收到多少数据vTaskDelay(3000); // 先延时3s,让消息发送到缓存区while (1){memset(rx_buffer, 0, sizeof(rx_buffer)); // 初始化buffer为0rec_bytes = xMessageBufferReceive(messageHandler, (void *)rx_buffer, sizeof(rx_buffer), portMAX_DELAY);printf("--------------\n");printf("Receive: rec_bytes=%d, rec_data: %s\n", rec_bytes, rx_buffer);}
}
void setup()
{Serial.begin(115200);xTaskCreatePinnedToCore(task1, "发送", 1024 * 5, NULL, 1, NULL, 1);xTaskCreatePinnedToCore(task2, "接收", 1024 * 5, NULL, 1, NULL, 1);vTaskDelete(NULL); // 删除当前任务
}void loop() {}
运行结果:
--------------
Send:i=0, send_bytes = 16
--------------
Send:i=1, send_bytes = 16
--------------
Send:i=2, send_bytes = 16
--------------
Receive: rec_bytes=16, rec_data: hello, nomber 0--------------
Receive: rec_bytes=16, rec_data: hello, nomber 1--------------
Receive: rec_bytes=16, rec_data: hello, nomber 2
相关文章:

ESP32 FreeRTOS学习总结
2023.5.11 FreeRTOS中文数据手册:https://www.freertos.org/zh-cn-cmn-s/RTOS.html 感谢以下两位B站UP主的教程:孤独的二进制、Michael_ee 1.Task 创建任务常用API: 任务函数描述xTaskCreate()使用动态的方法创建一个任务xTaskCreatePinne…...

uniapp打包ios保姆式教程【最新】
uniapp打包 打包方式ios打包一、前往官网登录二、添加证书 三、添加标识符(Identifiers)四、添加安装ios测试机(Devices)五、获取证书profile文件六、生成并下载p12文件七、开始打包 打包方式 安卓打包直接使用公共测试证书即可打包成功,简单方便,这里我…...

Thread线程学习(2) Linux线程的创建、终止和回收
目录 1.首先要了解什么是线程ID,以及它的作用是什么 2.创建线程 3.终止线程 4.回收线程 5.总结 在Linux系统中,线程是轻量级的执行单元,能够在同一个进程中并发执行。本文将介绍如何在Linux环境下创建、终止和回收线程,并提供…...

linux-项目部署软件安装
安装jdk 操作步骤: 1、使用FinalShell自带的上传工具将jdk的二进制发布包上传到Linux jdk-8u171-linux-x64.tar.gz 2、解压安装包,命令为tar -zxvf jdk-8u171-linux-x64.tar.gz -C /usr/local 3、配置环境变量,使用vim命令修改/etc/profile文…...

Vue3-黑马(三)
目录: (1)vue3-基础-计算属性 (2) vue3-基础-xhr-基本使用 (3)vue3-基础-xhr-promise改造 (1)vue3-基础-计算属性 上面有重复的代码,用计算属性࿰…...

标准C库函数fprintf(),sprintf(),snprintf()的函数使用方法(往文件中写入数据,将变量的值转换成字符串输出)
前言 如果,想要深入的学习标准C库中函数fprintf(),sprintf(),snprintf(),还是需要去自己阅读Linux系统中的帮助文档。 具体输入命令: man 3 fprintf/sprintf/snprintf即可查阅到完整的资料信息。 fprintf 函数 fprin…...

不到1分钟,帮你剪完旅行vlog,火山引擎全新 AI「神器」真的这么绝?
旅行时,想在社交平台发布一支精美的旅行 vlog,拍摄剪辑需要花费多长时间? 20 分钟?一小时?半天? 在火山引擎算法工程师眼里,可能 1 分钟都用不了,因为会有 AI 替你完成。 没错&#…...

MySQL的概念、编译安装,以及自动补全
一.数据库的基本概念 1、数据(Data) • 描述事物的符号记录 • 包括数字,文字,图形,图像,声音,档案记录等 • 以“记录”形式按统一的格式进行存储 2、表 • 将不同的记录组织在一起 • …...

Jmeter常见问题和工作中遇到的问题解决方法汇总
一、标题Jmeter常见问题解决 1.1 Jmeter如何针对https协议进行接口测试? 解决方法: 协议更改为:https,端口号更改为443;Jmeter默认的是:http协议,端口号是:80 1.2 Jmeter如何解决默…...

蓝牙单连接和多连接知识
蓝牙单连接和多连接知识 生活中蓝牙连接多路蓝牙的情况越来越多,下面介绍下单连接和多连接的知识,供大家参考 一:蓝牙单连接 蓝牙单连接也称为Point-to-Point 点对点连接,是最常见的蓝牙连接场景,如手机<–>车载蓝牙。&…...

Mongodb—查询数据报错 Sort operation used more than the maximum 33554432 bytes of RAM
线上服务的MongoDB中有一个很大的表数据有十几万条。然后报了下面这个错误: “exception”:”org.springframework.data.mongodb.UncategorizedMongoDbException”, “message”:”Query failed with error code 96 and error message ‘Executor error during find command:…...

Java内存异常和垃圾回收机制
内存溢出异常 Java会存在内存泄漏吗?请简单描述 内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说, Java是有GC垃圾回收机制 的,也就是说,不再被使用的对象,会被GC自动回收 掉,自动…...

linux系统挂载逻辑卷和扩展逻辑卷组
fdisk /dev/vdb fdisk /dev/vdc在分区后需要修改分区类型为 8e,操作过程类似: [rootlocal ~] $ fdisk /dev/xvdb #### 选择磁盘 Command (m for help): m #### 帮助 Command actiona tog…...

WPF:WPF原生布局说明
前言 WPF在国内讨论度很小,我在这里记录一下WPF简单的原生控件是如何使用的,顺便回忆一下WPF的基础知识,有些忘记的比较厉害了 WPF简介 WPF是微软推出的桌面UI软件,是我觉得最早实现MVVM(数据驱动事务)&…...

SpringMVC常用注解用法
Spring MVC是基于Servlet API构建的原始Web框架。 MVC是Model View Controller的缩写即视图模型控制器,是一种思想,而Spring MVC是对该思想的具体实现。关于SpringMVC的学习我们需要掌握用户和程序的连接、获取参数以及返回数据三大部分。而这三大功能的…...

Liunx find locate 命令详解
文章目录 find补充说明语法选项参数实例根据文件或者正则表达式进行匹配否定参数根据文件类型进行搜索基于目录深度搜索根据文件时间戳进行搜索根据文件大小进行匹配删除匹配文件根据文件权限/所有权进行匹配借助-exec选项与其他命令结合使用搜索但跳过指定的目录find其他技巧收…...

JAVA并发专题(1)之操作系统底层工作的整体认识
一、分诺依曼计算机模型 现代计算机模型是基于-冯诺依曼计算机模型,计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按…...

WiFi(Wireless Fidelity)基础(七)
目录 一、基本介绍(Introduction) 二、进化发展(Evolution) 三、PHY帧((PHY Frame ) 四、MAC帧(MAC Frame ) 五、协议(Protocol) 六、安全&#x…...

Agilent安捷伦33522B任意波形发生器
Agilent安捷伦33522B任意波形发生器30兆赫 2通道 为您最苛刻的测量生成全方位信号的无与伦比的能力 具有 5 倍低谐波失真的正弦波,可提供更纯净的信号 脉冲频率高达 30 MHz,抖动减少 10 倍,可实现更精确的计时 具有排序功能的逐点任意波形功能…...

PostgreSQL-如何创建并发索引
索引简介 索引是数据库中一种快速查询数据的方法。索引中记录了表中的一列或多列值与其物理位置之间的对应关系,就好比一本书前面的目录,通过目录中页码就能快速定位到我们需要查询的内容。 建立索引的好处是加快对表中记录的查找或排序,但…...

【大数据模型】使用Claude浅试一下
汝之观览,吾之幸也!本文主要聊聊Claude使用的流程,在最后对国内外做了一个简单问题的对比,希望国内的大数据模型更快的发展。 一、产品介绍 claude官网 Claude是一款由前OpenAI的研究员和工程师开发的新型聊天机器人,…...

鼎盛合——国产电量计芯片的分类与发展
电池技术在 200 余年的时间里不断演进,并在近 30 年的时间里取得了飞速发展,从最早期的铜-锌电池、铅酸电池,到目前的锂电池、钠电池,电池能量密度从早期的~10Wh/kg 飞速攀升至 200Wh/kg。回顾历史上来看,电池管理系统…...

交叉验证之KFold和StratifiedKFold的使用(附案例实战)
🤵♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞Ǵ…...

Cloud Kernel SIG月度动态:发布ANCK 5.10、4.19新版本,ABS新增仓库构建功能
Cloud Kernel SIG(Special Interest Group):支撑龙蜥内核版本的研发、发布和服务,提供生产可用的高性价比内核产品。 01 SIG 整体进展 发布 ANCK 5.10-014 版本。 发布 ANCK 4.19-027.2 版本。 ABS 平台新增 OOT 仓库临时构建功…...

JavaScript:new操作符
一、new操作符的作用 用于创建一个给定构造函数的实例对象 new操作符创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例。二、new一个构造函数的执行过程 2.1、创建一个空对象obj 2.2、将空对象的原型与构造函数的原型连接起来 2.3、将构造函数中的this绑定…...

XShell配置以及使用教程
目录 1、XShell介绍 2、安装XShell 1. 双击运行XShell安装文件,并点击“下一步” 2. 点击“我接受许可证协议中的条款”,点击“下一步” 3. 点击“浏览”更改默认安装路径,点击“下一步” 4. 直接点击“安装” 5. 安装完成࿰…...

Vue3 基础语法
文章目录 1.创建Vue项目1.1创建项目1.2 初始项目 2.vue3 语法2.1 复杂写法2.2 简易写法2.3 reactive(对象类型)2.4 ref(简单类型)2.5 computed(计算属性)2.6 watch(监听) 3.vue3 生命周期4.vue3 组件通信4.…...

【开源项目】Disruptor框架介绍及快速入门
Disruptor框架简介 Disruptor框架内部核心的数据结构是Ring Buffer,Ring Buffer是一个环形的数组,Disruptor框架以Ring Buffer为核心实现了异步事件处理的高性能架构;JDK的BlockingQueue相信大家都用过,其是一个阻塞队列…...

双向链表实现约瑟夫问题
title: 双向链表实现约瑟夫问题 date: 2023-05-16 11:42:26 tags: **问题:**知n个人围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去&…...

日心说为人类正确认识宇宙打下了基础(善用工具的重要性)
文章目录 引言I 伽利略1.1 借助天文望远镜获得了比别人更多的信息。1.2 确定了科学研究方法:实验和观测 II 开普勒三定律 引言 享有科学史上崇高地位的人,都需要在构建科学体系上有重大贡献。 日心说在哥白尼那里还是一个假说,伽利略拿事实…...