基于 Michael_ee 老师的视频做的笔记 https://space.bilibili.com/1338335828
视频深入浅出,很适合初学者以及开发者提升用。
优势:
系统免费
方便代码复用
实时性系统
代码量很小,只需要3个C文件
已经移植到很多平台
在esp-idf-components中,bootloader是第一阶段启动文件,bootloader_support是第二阶段启动文件
在espidf-components-freertos中,有freertos核心代码:
最简的RTOS只需要上面这三个文件即可。
port文件夹中是不同内核平台的接口,一般由c和asm汇编文件组成。
ESP32 系统是怎么启动到app_main函数?(可以打开components下的freertos以及esp_system的文件夹搜索)
//freertos目录下//
app_main 【main.c】
-->
main_task【port_common.c】
-->
esp_startup_start_app_common 【port_common.c】(在这里创建了main_task任务)
-->
esp_startup_start_app 【port.c】 (调用创建maintask,并开启任务调度,对应不同平台的port接口文件)
-->
//esp_system目录下//
start_cpu0_default 【startup.c】 (初始化底层函数,并用while(1)卡死)
-->
start_cpu0 【startup.c】 (通过__attribute__弱链接到start_cpu0函数)
-->
g_startup_fn 【startup.c】(数组,调用不同的启动程序)
-->
SYS_STARTUP_FN 【startup_internal.h】 (通过宏链接)
-->
call_start_cpu0 【cpu_start.c】 (cpu0任务启动函数)
-->
ENTRY(call_start_cpu0); 【sections.ld.in】 (最终的链接文件)
freertos 参考手册:
https://www.freertos.org/Documentation/RTOS_book.html
void myTask(void *pvParam){ //用空指针就可以传递任何类型,传进来后再转化
while(1){//任务函数里必须是死循环
//实际任务
vTaskDelay(1000/portTICK_PERIOD_MS);//任务函数里必须有延时,也可用pdMS_TO_TICKS(1000)代替
}
vTaskDelete(NULL);//在本函数中删除,不需要传递句柄
}
void app_main(void){
//创建任务句柄
TaskHandle_t myHandle = NULL;
//任务指针,任务标签,堆栈大小,传入参数,优先级,任务句柄
xTaskCreate(myTask,"myTask",1024,NULL,1,&myHandle);//传出任务句柄
//通过任务句柄删除
if(myHandle!=NULL){
vTaskDelete(myHandle);//在任务函数外删除任务
}
}
指针类型为 pointer to void’ ( void* ) ,可以接收任何数据类型
//1.整形数字:
int testNum = 1;
xTaskCreate(myTask,"myTask",1024,(void *) &testNum,1,&myHandle);//取地址,转空类型(main中)
void myTask(void *pvParam){
...
int *pINT; //定义整数型指针
pINT = (int *)pvParam; //取得输入参数地址给整数型指针(就是把地址对上)
printf("%d\n",*pINT); //将指针对应地址的具体数值取出来
...
}
//2.整形数组(本质上和单个数字是一样的,都是传递一个地址):
int testNum[] = {6,7,8};
xTaskCreate(myTask,"myTask",1024,(void *) testNum,1,&myHandle);//直接赋值数组地址(main中)
void myTask(void *pvParam){
...
int *pArrayAddr; //定义整形数组指针
pArrayAddr = (int *)pvParam; //取得输入参数地址给整数型指针(就是把地址对上)
printf("num1 = %d\n num2 = %d\n num3 = %d\n",*(pINT),*(pINT+1),*(pINT+1)); //将指针对应数组具体数值取出来
...
}
//3.结构体
typedef struct A_STRUCT{
int iMEN1;
int iMEN2;
} xStruct;
xStruct xStrTest = {6,9}; //初始化结构体
xTaskCreate(myTask,"myTask",1024,(void *)&xStrTest,1,&myHandle);//取得结构体数据地址(main中)
void myTask(void *pvParam){
...
xStruct *pStrTest; //定义结构体组指针
pStrTest = (xStruct *)pvParam; //取得输入参数地址给结构体指针(就是把地址对上)
printf("num1 = %d\n num2 = %d\n",pStrTest->iMEN1,pStrTest->iMEN2); //将指针对应结构体具体数值取出来
...
}
//4.字符串
static const char *pcTxt = "Hello World" //需要定义为静态
xTaskCreate(myTask,"myTask",1024,(void *)pcTxt,1,&myHandle);//将字符串地址赋值上(main中)
void myTask(void *pvParam){
...
char *pcTxtInTask; //定义字符串类型
pcTxtInTask = (char *)pvParam; //取得输入参数地址给字符串指针(就是把地址对上)
printf("%s\n",pcTxtInTask); //将指针对应字符串具体数值取出来
...
}
每个任务分配一个从 0 到 (configMAX_PRIORITIES - 1) 的优先级,其中 configMAX_PRIORITIES 在 FreeRTOSConfig.h 中定义。
(路径esp-idf\components\freertos\include\esp_additions\freertos)一般不建议修改
/* This has impact on speed of search for highest priority */
#define configMAX_PRIORITIES ( 25 )
因此我们可以定义的默认最大优先级就是(25-1)= 24
//取得优先级别:
//API : UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
UBaseType_t iPriority = 0;
TaskHandle_t pxTask = NULL;
xTaskCreate(myTask,"myTask",1024,(void *)pcTxt,26,&pxTask);//默认系统最大24,实际上系统会改成24
iPriority = uxTaskPriorityGet(pxTask);//应该返回24
print...
优先级别,数字越大越高
同一优先级:
创建后顺序运行,切换不同的时间片,给予相同的运行时间
官网介绍:具有相同优先级的就绪状态任务将使用时间切片循环调度方案共享可用处理时间
谁先创建,谁先运行
不同优先级:
先运行高优先级的task,不管谁先创建
重定义优先级:
//设置优先级别
//API : void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );
...
xTaskCreate(myTask1,"myTask1",1024,(void *)pcTxt,1,&pxTask1);
xTaskCreate(myTask2,"myTask2",1024,(void *)pcTxt,2,&pxTask2);
vTaskPrioritySet(pxTask1,3);
//运行顺序 task2 -> task1 -> task1 ->task2
重设优先级后,task优先级立即生效
任务的状态:
状态 | 意义 |
---|---|
Running | 执行态 |
Ready | 就绪态 |
Blocked | 阻塞态 |
Suspended | 挂起态 |
系统建立任务后,任务进入Ready就绪态,在任务调度器调度下会进入执行态 / 就绪态。
在被挂起函数调用后,任务进入挂起状态(在执行态、就绪态、阻塞态下都可以挂起),只有恢复状态函数被调用才可以恢复就绪态。
在被阻塞函数调用后(只能在执行态中被调用),任务进入阻塞态,当事件发生(如时间延时)后,任务可以再次进入就绪状态,也可以进去挂起状态。
阻塞态有“超时”概念,挂起状态没有。
//挂起一个任务
// API: void vTaskSuspend( TaskHandle_t pxTaskToSuspend );
...
xTaskCreate(myTask1,"myTask1",1024,(void *)pcTxt,1,&pxTask1);
xTaskCreate(myTask2,"myTask2",1024,(void *)pcTxt,2,&pxTask2);
vTaskDelay(1000/portTICK_PERIOD_MS);
vTaskSuspend(pxTask1);//这是在外部挂起,在任务中挂起他自己用 vTaskSuspend(NULL);
...
//恢复一个任务
// API : void vTaskResume( TaskHandle_t pxTaskToResume );
...
vTaskResume(pxTask1);
...
可在其他函数中,通过挂起和恢复全权控制任务的运行时间、顺序、次数等
当需要进行一些不希望被打断的运算时(需要独占CPU时),可在任务中通过全部挂起和全部恢复进行
//挂起所有任务
vTaskSuspendAll(); //在执行这个函数后,不能调用RTOS的其他API函数(在执行一些运算不想被调度器打扰时)
//恢复所有任务
xTaskResumeAll();
//实例:
void myTask1(void *pvParam){
printf("Task Begin");
vTaskSuspendAll();//任务挂起
...//重要时间相关程序(不想被其他任务打扰)
xTaskResumeAll();//任务恢复(在这之前不能再调用其他RTOS的API)
printf("Task End");
}
使用vTaskList()打印出系统的所有信息:
查看任务状态,优先级,可用堆栈(内存)等。
堆栈:显示任务栈的 “高水位”。这是在任务有效期内的最小任务的生命周期内可用的最小堆栈量。这个值越接近于零,任务就越接近于溢出。
几种状态的定义如下:
在使用list前,需要在FreeRTOSConfig.h中使能一些宏:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-390yIrcB-1652860915129)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220513151911690.png)]
可以在menuconfig中直接配置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lPxhrs3r-1652860915130)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220513151929634.png)]
//在main函数中
TaskHandle_t mytask1_handle,mytask2_handle;
xTaskCreate(mytask1,"mytask1",1024,NULL,1,&mytask1_handle);
xTaskCreate(mytask2,"mytask2",1024,NULL,1,&mytask2_handle);
static char pcWriteBuffer[512] = {0};
vTaskList(pcWriteBuffer);
printf
设置堆栈的是深度,不是Byte:
实际占用的字节数是 深度 X 堆栈宽度。
设置堆栈大小,可以先给个大致估计的数值,然后用vTaskList查看堆栈值并修改。
在嵌入式系统中,调用vTaskList将会消耗大量资源,有时候不是很方便,因此可以采用以下的方法(只观察需要关注的任务):
// UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
//main或者函数内部
UBaseType_t uxHighWaterMark;
for( ;; )
{
vTaskDelay(pdMS_TO_TICKS(1000));
uxHighWaterMark = uxTaskGetStackHighWaterMark(mytask1_handle);
printf("stacksize=%d\n",uxHighWaterMark);
}
返回的数值是剩余stack数值,越接近0越要溢出。
如果溢出系统就会一直重启:
***ERROR*** A stack overflow in task xxx has been detected.
中断看门狗
中断看门狗确保 FreeRTOS 任务切换中断不会被长时间阻塞(包括 WiFi 任务和空闲任务等潜在重要任务)
作用: 【1】 方便GDB调试
【2】 由于某种未知原因无法调用重要任务(NMI),强制重启系统
通过 CONFIG_ESP_INT_WDT 宏配置启用/关闭中断看门狗
通过 CONFIG_ESP_INT_WDT_TIMEOUT_MS 宏配置超时时间
任务看门狗
任务看门狗负责检测某任务长时间运行影响了部分关键任务的执行(如Idle Task,包含一些CPU指令),默认系统会增加 Idle 任务监控。
以下是触发Idle任务看门狗的提示:
E (10276) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (10276) task_wdt: - IDLE (CPU 0)
E (10276) task_wdt: Tasks currently running:
E (10276) task_wdt: CPU 0: mytask2
IDLE的优先级别是0,其他任务优先级比它高而且都没有调用阻塞(让低优先级的任务得以进行),就没有时间执行IDLE任务。
解决方法
增加自己的任务到监控列表中:
https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/system/wdts.html
// API: esp_err_tesp_task_wdt_add(TaskHandle_thandle); 增加任务到监控列表中
// API: esp_err_tesp_task_wdt_reset(void); 喂狗,放在任务中
void mytask2(void *pvParam){
esp_task_wdt_add(NULL);
while(1){
esp_task_wdt_reset();
vTaskDelay(pdMS_TO_TICKS(6000));
printf("Task2\n");
}
}
默认看门狗的超时时间是5s,可以在menuconfig中设置任务看门狗参数:
队列:一个先入先出(FIFO)的数组
创建队列/发送/接收数据:
//发送整形数据
#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h" //引入此头文件
void sendTask1(void *pvParam)
{
QueueHandle_t QHandle; //定义队列句柄
BaseType_t xStatus; //返回发送状态
QHandle = (QueueHandle_t)pvParam; //传递队列句柄
int i = 0; //发送的数据
while (1)
{
xStatus = xQueueSend(QHandle, &i, 0);//发送
if (xStatus != pdPASS) //判断是否发送成功
printf("SEND FAILED!\n");
else
printf("SEND SUCCESS!\n");
i++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void receiveTask1(void *pvParam)
{
QueueHandle_t QHandle; //转化为队列句柄
BaseType_t xStatus; //返回接收状态
QHandle = (QueueHandle_t)pvParam;//传递队列句柄
int j = 0; //接收的数据
while (1)
{
if (uxQueueMessagesWaiting(QHandle) != 0)
{
xStatus = xQueueReceive(QHandle,&j,0);//接收
if (xStatus != pdPASS) //判断是否接收成功
printf("RECEIVE FAILED!\n");
else
printf("RECEIVE SUCCESS!\n");
}
else{
printf("no data!\n");
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
QueueHandle_t QHandle; //创建原始队列句柄
QHandle = xQueueCreate(5, sizeof(int)); //创建队列
if (QHandle != NULL)//判断队列是否创建成功
{
printf("Success\n");
xTaskCreate(sendTask1, "sendTask1", 1024 * 5, (void *)QHandle, 1, NULL);//创建发送任务
xTaskCreate(receiveTask1, "receiveTask1", 1024 * 5, (void *)QHandle, 1, NULL);//创建接收任务
}
else
{
printf("ERROR\n");
}
}
//发送结构体(其他同上,以下修改)
...
//最上面
typedef struct A_STRUCT{
char id;
char data;
} xStruct; //定义一个结构体
//send函数中
xStruct xUSB = {1,55};
xStatus = xQueueSend(QHandle, &xUSB, 0);
xUSB.id++;
//receive函数中
xStruct xUSB = {0,0};
xStatus = xQueueReceive(QHandle, &xUSB, 0);
printf("RECEIVE:%d %d\n",xUSB.id,xUSB.data);
//main函数中
QHandle = xQueueCreate(5, sizeof(xStruct));
...
//发送指针(字符串)(其他同上,以下修改)
...
//send函数中
char *pStrToSend;
int i = 0;
pStrToSend = (char *)malloc(50);
snprintf(pStrToSend,50,"Hello World! %d",i);
i++;
//receive函数中
char *pStrToRec;
xStatus = xQueueReceive(QHandle, &pStrToRec, 0);
printf("RECEIVE:%s\n",pStrToRec);
//主函数中
QHandle = xQueueCreate(5, sizeof(char *));
...
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zlcQzOWF-1652860915131)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220513182405103.png)]
多个发送任务一个接收任务,接收方的优先级必须比发送的高,然后接收数据方用阻塞接收(max等待时间)
//发送方多设置一个,发送整形数据
//接收方如下:
void receiveTask1(void *pvParam)
{
QueueHandle_t QHandle;
BaseType_t xStatus;
QHandle = (QueueHandle_t)pvParam;
int j;
while (1)
{
xStatus = xQueueReceive(QHandle, &j, portMAX_DELAY); //阻塞接收
if (xStatus != pdPASS)
printf("RECEIVE FAILED!\n");
else
printf("RECEIVE:%d\n",j);
//这里不用delay因为是阻塞接收,有数据才进行下一轮循环
}
}
void app_main(void)
{
QueueHandle_t QHandle;
QHandle = xQueueCreate(5, sizeof(char *));
if (QHandle != NULL)
{
printf("Success\n");
xTaskCreate(sendTask1, "sendTask1", 1024 * 5, (void *)QHandle, 1, NULL);
xTaskCreate(sendTask2, "sendTask2", 1024 * 5, (void *)QHandle, 1, NULL);
xTaskCreate(receiveTask1, "receiveTask1", 1024 * 5, (void *)QHandle, 2, NULL);//接收方的优先级要比发送方的高,保证不会漏数据
}
else
{
printf("ERROR\n");
}
}
队列集合(将多个队列集合在一起,一起读)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cXfuzB9a-1652860915131)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220513230736752.png)]
创建的时候用 xQueueCreateSet xQueueAddToSet,接收解析的时候用xQueueSelectFromSet 将Set中的数据提取出来。
这样可以防止多个queue在读取时候的数据混乱。
//发送方同上
//接收方
void receiveTask1(void *pvParam)
{
QueueSetHandle_t QueueSet;
QueueSetMemberHandle_t QueueData;
QueueSet = (QueueSetHandle_t)pvParam;
BaseType_t xStatus;
int j;
while (1)
{
QueueData = xQueueSelectFromSet(QueueSet,portMAX_DELAY);//从queueset中获取数据
xStatus = xQueueReceive(QueueData, &j, portMAX_DELAY); //从收到数据中取出数据,阻塞
if (xStatus != pdPASS)
printf("RECEIVE FAILED!\n");
else
printf("RECEIVE:%d\n",j);
}
}
//主函数
void app_main(void)
{
QueueHandle_t QHandle1;
QHandle1 = xQueueCreate(5, sizeof(char *));
QueueHandle_t QHandle2;
QHandle2 = xQueueCreate(5, sizeof(char *));
QueueSetHandle_t QueueSet;
QueueSet = xQueueCreateSet(10);//定义一个集合
xQueueAddToSet(QHandle1,QueueSet);//将QHandle1加入QueueSet
xQueueAddToSet(QHandle2,QueueSet);//将QHandle2加入QueueSet
if ((QHandle1 != NULL)&&(QHandle2 != NULL)&&(QueueSet != NULL))
{
printf("Success\n");
xTaskCreate(sendTask1, "sendTask1", 1024 * 5, (void *)QHandle1, 1, NULL);//发送任务1创建
xTaskCreate(sendTask2, "sendTask2", 1024 * 5, (void *)QHandle2, 1, NULL);//发送任务2创建
xTaskCreate(receiveTask1, "receiveTask1", 1024 * 5, (void *)QueueSet, 2, NULL);//接收任务创建,传入QueueSet
}
else
{
printf("ERROR\n");
}
}
邮箱的数据不会传递出去(只有一个),给任何task读取。发送方会不断重写邮箱里的数值,接收方不能修改里面的数值。
用队列创建邮箱:
//发送(写入方)
void writeTask(void *pvParam)
{
QueueHandle_t Mailbox;
BaseType_t xStatus;
Mailbox = (QueueHandle_t)pvParam;
int i = 0;
while (1)
{
xStatus = xQueueOverwrite(Mailbox, &i); //覆盖已经保存在队列中的数据,即使队列已满,也会向队列写入
if (xStatus != pdPASS)
printf("SEND FAILED!\n");
else
printf("SEND SUCCESS!\n");
i++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
//读取方(下面通过相同的函数创建三个不同的任务)
void readTask(void *pvParam)
{
QueueHandle_t Mailbox;
BaseType_t xStatus;
Mailbox = (QueueHandle_t)pvParam;
int i;
while (1)
{
xStatus = xQueuePeek(Mailbox, &i, portMAX_DELAY); //从队列中读取一个项目,但不从队列中删除该项目,阻塞读取
if (xStatus != pdPASS)
printf("read fail!\n");
else
printf("read i=%d!\n",i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
//主函数
void app_main(void)
{
QueueHandle_t Mailbox;
Mailbox = xQueueCreate(1, sizeof(int));
if (Mailbox != NULL)
{
printf("Success\n");
xTaskCreate(writeTask, "writeTask", 1024 * 5, (void *)Mailbox, 1, NULL);//写的优先级别低
xTaskCreate(readTask, "readTask1", 1024 * 5, (void *)Mailbox, 2, NULL);//运用同一个readTask函数体,创建不同的三个读的任务
xTaskCreate(readTask, "readTask2", 1024 * 5, (void *)Mailbox, 2, NULL);
xTaskCreate(readTask, "readTask3", 1024 * 5, (void *)Mailbox, 2, NULL);
}
else
{
printf("ERROR\n");
}
}
在FreeRTOS中,软件定时器是一个基于Deamon Task的任务(与硬件/平台无关)
软件定时器数量可以自定义。
引入头文件 “freertos/timers.h”
--> 可以用来实现闪灯之类的功能
//回调函数
void Timer1CallBack( TimerHandle_t xTimer ){ //回调函数标准形式,
const char *strName;
uint32_t id;
strName = pcTimerGetName(xTimer);//获取任务名称,可以用这个区分不同的任务,进行不同任务的处理
id = (uint32_t)pvTimerGetTimerID(xTimer);//获取任务ID,可以用这个区分不同的任务,进行不同任务的处理(这里直接转uint32_t是因为传入的不是指针而是数)
printf("Timer name = %s | Timer id = %d\n",strName,id);
}
//主函数
void app_main(void)
{
TimerHandle_t xTimer1,xTimer2;
xTimer1 = xTimerCreate("Timer1",pdMS_TO_TICKS(1000),pdTRUE,(void *)0,Timer1CallBack); //定时器名称 运行周期时间 是否重复 ID 回调函数
xTimer2 = xTimerCreate("Timer2",pdMS_TO_TICKS(2000),pdTRUE,(void *)1,Timer1CallBack); //用同一个回调函数进行多个进程
xTimerStart(xTimer1,0);//启动定时器任务
xTimerStart(xTimer2,0);//启动定时器任务
//---1.使能下面,定时器2被定期复位,任务2就不会执行
//while(1){
//vTaskDelay(pdMS_TO_TICKS(1000));
//xTimerReset(xTimer2,0);//复位定时器的计数数值,使之不会执行
//}
//---2.使能下面,则在6秒时任务1被停止
//vTaskDelay(pdMS_TO_TICKS(6000));
//xTimerStop(xTimer1,0);//停止定时器任务
//---3.使能下面,则定时器1在6秒后改变运行周期
//vTaskDelay(pdMS_TO_TICKS(6000));
//xTimerChangePeriod(xTimer1,pdMS_TO_TICKS(6000),0);
}
信号量:其实上就是标志位,可以防止多个Task共用一个资源出现的竞争情况,消除竞争问题。
信号量创建后必须先被释放后才可使用
需要引入头函数 “freertos/semphr.h”
下面例程中是两个任务共用一个全局变量出现的竞争:
(可以通过注释 ”获得信号量“ ”释放信号量“ 观察现象)
int iCount = 0;
SemaphoreHandle_t semphrHandle;
//任务1
void myTask1(void* pvParam){
while(1){
xSemaphoreTake(semphrHandle,portMAX_DELAY);//获得信号量
for(int i=0;i<10;i++){
iCount++;
printf("myTask1 iCount = %d\n",iCount);
vTaskDelay(pdMS_TO_TICKS(100));
}
xSemaphoreGive(semphrHandle);//释放信号量
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
//任务1
void myTask2(void* pvParam){
while(1){
xSemaphoreTake(semphrHandle,portMAX_DELAY);//获得信号量
for(int i=0;i<10;i++){
iCount++;
printf("myTask2 iCount = %d\n",iCount);
vTaskDelay(pdMS_TO_TICKS(100));
}
xSemaphoreGive(semphrHandle);//释放信号量
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
//主函数
void app_main(void)
{
semphrHandle = xSemaphoreCreateBinary();//创建一个二进制信号量
xSemaphoreGive(semphrHandle);//释放信号量
xTaskCreate(myTask1,"myTask1",1024*5,NULL,1,NULL);
xTaskCreate(myTask2,"myTask2",1024*5,NULL,1,NULL);
}
可以发现如果没有信号互斥量,则Task1,Task2交替操作全局变量。
而如果有信号互斥量时,全局变量则会在信号量释放前一直在该Task内操作。
计数型信号量:就是非二值的标志位,有最大值限制。
可以统计数量,比如停车场空位
以下例程以一个停车场的车位管理为例:
SemaphoreHandle_t semphrHandle;
void carInTask(void* pvParam){
BaseType_t iResult;
int emptySpace = 0;
while(1){
emptySpace = uxSemaphoreGetCount(semphrHandle);//取得当前停车位的数量
printf("emptySpace = %d!\n",emptySpace);
iResult = xSemaphoreTake(semphrHandle,portMAX_DELAY);//获得一个信号量
if(iResult == pdPASS){
printf("One Car in!\n");
}
else{
printf("No Space!\n");
}
vTaskDelay(pdMS_TO_TICKS(1000));//每1秒进一个车
}
}
void carOutTask(void* pvParam){
BaseType_t iResult;
while(1){
vTaskDelay(pdMS_TO_TICKS(6000));
xSemaphoreGive(semphrHandle);//每6秒释放一个车位
printf("One Car out!\n");
}
}
计数型信号量和二进制信号量区别就是创建时的API不同,以及需要获取信号量时需要用API获取,其他API和二进制信号量基本一致。
互斥信号量和二进制信号量十分相似。
区别:拥有互斥量信号的任务可以继承企图获得同样互斥量任务的优先级别
SemaphoreHandle_t mutexHandle;
void Task1(void *pvParam){
BaseType_t iRet;
printf("task1 begin!\n");
while(1){
iRet = xSemaphoreTake(mutexHandle,1000); //试图取得获得互斥量
if(iRet == pdPASS){
printf("task1 take!\n");
for(int i = 0;i<50;i++){
printf("task1 i=%d!\n",i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(mutexHandle);
printf("task1 give!\n");
vTaskDelay(pdMS_TO_TICKS(5000)); //阻塞自身让其他任务有时间执行
}
else{
printf("task1 didn't take!\n");
}
}
}
void Task2(void *pvParam){
printf("task2 begin!\n");
vTaskDelay(pdMS_TO_TICKS(1000));//阻塞,让Task1有时间执行
while(1){
; //无阻塞,只有Task3可以剥夺运行时间
}
}
void Task3(void *pvParam){
BaseType_t iRet;
printf("task3 begin!\n");
vTaskDelay(pdMS_TO_TICKS(1000)); //阻塞,让Task2,Task1有时间执行
while (1)
{
iRet = xSemaphoreTake(mutexHandle,1000); //试图取得获得互斥量
if(iRet == pdPASS){
printf("task3 take!\n");
for(int i=0;i<10;i++){
printf("task3 i=%d",i);
vTaskDelay(pdMS_TO_TICKS(1000)); //模拟程序处理
}
xSemaphoreGive(mutexHandle); //处理完,释放互斥量
printf("task3 give!\n");
vTaskDelay(pdMS_TO_TICKS(5000)); //阻塞自身让其他任务有时间执行
}
else{
printf("task3 did't take!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}
void app_main(void)
{
//二选一观察现象:
//1-
//mutexHandle = xSemaphoreCreateBinary();//创建二进制信号量
//2-
mutexHandle = xSemaphoreCreateMutex();//创建互斥量
vTaskSuspendAll();//挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1,"Task1",1024*5,NULL,1,NULL);
xTaskCreate(Task2,"Task2",1024*5,NULL,2,NULL);
xTaskCreate(Task3,"Task3",1024*5,NULL,3,NULL);
xTaskResumeAll();//开启任务调度器
}
如果没有优先级继承的话,那Task1就无法再次被执行(当Task2执行后,剥夺Task1时间片,因为这个时候Task1的优先级要小于Task2)
当有优先级继承后(互斥量),Task1的优先级别就会变成和Task3一样,大于Task2,所以在前面一秒的阻塞时间过后,Task2任务执行剥夺时间片,但是这个时候Task1的优先级别变成3大于Task1的2,所以Task1任务得以执行。
当一个任务需要再调用另一个任务时,不需要定义多个互斥量锁住进程。
实际上就是多次加锁解锁的操作,递归加锁解锁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SpnIJ2Yl-1652860915132)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220514095523276.png)]
SemaphoreHandle_t mutexHandle;
void Task1(void *pvParam){
while(1){
printf("-----------\n");
printf("task1 begin!\n");
xSemaphoreTakeRecursive(mutexHandle,portMAX_DELAY); //获取资源A递归互斥量
printf("task1 take A!\n");
for (int i = 0; i < 10; i++)
{
printf("task1 i=%d for A!\n",i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreTakeRecursive(mutexHandle,portMAX_DELAY); //获取资源B递归互斥量
printf("task1 take B!\n");
for (int i = 0; i < 10; i++)
{
printf("task1 i=%d for B!\n",i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
printf("task1 give B!\n");
xSemaphoreGiveRecursive(mutexHandle); //释放资源B
vTaskDelay(pdMS_TO_TICKS(3000));
printf("task1 give A!\n");
xSemaphoreGiveRecursive(mutexHandle); //释放资源A
vTaskDelay(pdMS_TO_TICKS(3000)); //阻塞,让Task2有机会获得锁
}
}
void Task2(void *pvParam){
vTaskDelay(pdMS_TO_TICKS(1000));//阻塞,让Task1有时间执行先获得互斥量锁
while(1){
printf("-----------\n");
printf("task2 begin!\n");
xSemaphoreTakeRecursive(mutexHandle,portMAX_DELAY); //获取资源A递归互斥量
printf("task2 take!\n");
for (int i = 0; i < 10; i++)
{
printf("task2 i=%d for A!\n",i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
printf("task2 give A!\n");
xSemaphoreGiveRecursive(mutexHandle); //释放资源A
vTaskDelay(pdMS_TO_TICKS(3000)); //阻塞,让Task1有机会获得锁
}
}
void app_main(void)
{
mutexHandle = xSemaphoreCreateRecursiveMutex();//创建递归互斥量
vTaskSuspendAll();//挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1,"Task1",1024*5,NULL,1,NULL);
xTaskCreate(Task2,"Task2",1024*5,NULL,1,NULL);
xTaskResumeAll();//开启任务调度器
}
可以看到现象,Task1在一开始先后拿到A,B两个资源锁,而Task2在Task1释放B资源锁后才可以拿到递归互斥锁,因此Task1加锁了两次,就需要解锁两次其他线程才可以拿到资源锁。
引入头文件 “freertos/event_groups.h”
事件组创建:当 USE_16_BIT_TICKS 宏为1时,事件组为8bit,为0时则为24bit。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rm3sTQ57-1652860915132)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220514111452136.png)]
主要使用的API 函数是 xEventGroupCreate ,xEventGroupSetBits 和 xEventGroupWaitBits,最后一个API函数是用来判断当前的时间组bits位的状态是否满足设定(让任务组等待Task中的一位或或多位被触发),否则就阻塞。
注意事件组的API都不能在中断中被调用
EventGroupHandle_t xCreatedEventGroup; //定义一个事件组
#define BIT_0 ( 1 << 0 ) //标志位第0位
#define BIT_4 ( 1 << 4 ) //标志位第4位
void Task1(void *pvParam){
while(1){
printf("-----------\n");
printf("task1 begin to wait!\n");
xEventGroupWaitBits(
xCreatedEventGroup, /* 事件组 */
BIT_0 | BIT_4, /* 事件组需要等待的bits位 */
pdTRUE, /* 在函数返回时会被清除(重新等待) */
pdFALSE, /* '或'的设置(pdFALSE),只要有一个bit使能就停止阻塞 '与'的设置(pdTRUE),两个bit同时使能才能停止阻塞*/
printf("-----------\n");
printf("In task1, BIT0 or BIT4 is set!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParam){
vTaskDelay(pdMS_TO_TICKS(1000));//阻塞,让Task1有时间优先运行
while(1){
printf("-----------\n");
printf("task2 begin to set bit0!\n");
xEventGroupSetBits(xCreatedEventGroup,BIT_0);
vTaskDelay(pdMS_TO_TICKS(5000));
printf("-----------\n");
printf("task2 begin to set bit4!\n");
xEventGroupSetBits(xCreatedEventGroup,BIT_4);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void app_main(void)
{
xCreatedEventGroup = xEventGroupCreate();//创建事件组
if( xCreatedEventGroup == NULL )
{
printf("Event group create fail!\n");
}
else
{
printf("Event group create successs!\n");
vTaskSuspendAll();//挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1,"Task1",1024*5,NULL,1,NULL);
xTaskCreate(Task2,"Task2",1024*5,NULL,1,NULL);
xTaskResumeAll();//开启任务调度器
}
}
事件组等待主要是通过事件组中各种事件标志位的组合,触发事件等待,用来处理一些复杂的项目需求十分合适。
和事件组wait刚好相反,事件组同步就是等待最后一个同步事件组设置完成后,再同步执行所有任务:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgJTJKKn-1652860915133)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220514111842628.png)]
EventGroupHandle_t xCreatedEventGroup; //定义一个事件组
#define TASK_0_BIT ( 1 << 0 )
#define TASK_1_BIT ( 1 << 1 )
#define TASK_2_BIT ( 1 << 2 )
#define ALL_SYNC_BITS ( TASK_0_BIT | TASK_1_BIT | TASK_2_BIT )
void Task0(void *pvParam){
while(1){
printf("-----------\n");
printf("task0 begin!\n");
vTaskDelay(pdMS_TO_TICKS(1000));
printf("-----------\n");
printf("task0 set bit0!\n");
xEventGroupSync(xCreatedEventGroup, /* 事件组 */
TASK_0_BIT, /* 设置第0位标志位 */
ALL_SYNC_BITS, /* 等待三个标志位都设置 */
portMAX_DELAY);/* 超时时长 最大 */
printf("task0 sync!\n");
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
void Task1(void *pvParam){
while(1){
printf("-----------\n");
printf("task1 begin!\n");
vTaskDelay(pdMS_TO_TICKS(3000));
printf("-----------\n");
printf("task1 set bit1!\n");
xEventGroupSync(xCreatedEventGroup, /* 事件组 */
TASK_1_BIT, /* 设置第1位标志位 */
ALL_SYNC_BITS, /* 等待三个标志位都设置 */
portMAX_DELAY);/* 超时时长 最大 */
printf("task1 sync!\n");
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
void Task2(void *pvParam){
while(1){
printf("-----------\n");
printf("task2 begin!\n");
vTaskDelay(pdMS_TO_TICKS(6000));
printf("-----------\n");
printf("task2 set bit0!\n");
xEventGroupSync(xCreatedEventGroup, /* 事件组 */
TASK_2_BIT, /* 设置第2位标志位 */
ALL_SYNC_BITS, /* 设置等待所有标志位 */
portMAX_DELAY);/* 超时时长 最大 */
printf("task2 sync!\n");
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
void app_main(void)
{
xCreatedEventGroup = xEventGroupCreate();//创建事件组
if( xCreatedEventGroup == NULL )
{
printf("Event group create fail!\n");
}
else
{
printf("Event group create successs!\n");
vTaskSuspendAll();//挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task0,"Task0",1024*5,NULL,1,NULL);
xTaskCreate(Task1,"Task1",1024*5,NULL,1,NULL);
xTaskCreate(Task2,"Task2",1024*5,NULL,1,NULL);
xTaskResumeAll();//开启任务调度器
}
}
每个任务都有一个32位的通知值,在任务创建时被初始化为零,任务通知是一个直接发送给任务的事件,它可以解除对接收任务的封锁,并可选择更新接收任务的通知值。
static TaskHandle_t xTask1 = NULL;
void Task1(void *pvParam){
while(1){
printf("-----------\n");
printf("task1 wait notification!\n");
ulTaskNotifyTake( pdTRUE, portMAX_DELAY);//Task1阻塞,取得通知
printf("-----------\n");
printf("task1 got notification!\n");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void Task2(void *pvParam){
while(1){
vTaskDelay(pdMS_TO_TICKS(5000));
printf("-----------\n");
printf("task2 notify task1!\n");
xTaskNotifyGive(xTask1);//解除通知,解锁Task1的阻塞状态
}
}
void app_main(void)
{
vTaskSuspendAll();//挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1,"Task1",1024*5,NULL,1,&xTask1);//传出Task1的任务句柄
xTaskCreate(Task2,"Task2",1024*5,NULL,1,NULL);
xTaskResumeAll();//开启任务调度器
}
实际上就是通过任务自带的消息,进行两个任务的同步,一个任务先阻塞另一个任务开始运行前发送消息,解除先前任务的阻塞。(类似之前的二进制信号量)
通过在任务中接收不同的通知,进而触发不同的event。
xTaskNotifyWait() 在可选的超时条件下,等待调用任务收到通知。如果接收任务已经被封锁,等待通知,当通知到达时,接收任务将从封锁中移除。
接收任务将被从阻塞状态中移除,并且通知被清除。
static TaskHandle_t xTask1 = NULL;
void Task1(void *pvParam)
{
uint32_t ulNotifiedValue;
while (1)
{
printf("-----------\n");
printf("task1 wait notification!\n");
//使用wait等待通知事件
xTaskNotifyWait(0x00, /* 某一位为1表示进入该函数后对应的通知位清零,这里设置是全部不清 */
ULONG_MAX, /* 某一位为1表示退出该函数后对应的通知位清零,这里设置的是全部清除 */
&ulNotifiedValue, /* 取得通知数值 */
portMAX_DELAY); /* 阻塞嘴上事件 */
if( ( ulNotifiedValue & 0x01 ) != 0 )
{
/* Bit 0 设置后的事件 */
printf("task1 BIT0 event\n");
}
if ((ulNotifiedValue & 0x02) != 0)
{
/* Bit 1 设置后的事件 */
printf("task1 BIT1 event\n");
}
if ((ulNotifiedValue & 0x04) != 0)
{
/* Bit 2 设置后的事件 */
printf("task1 BIT2 event\n");
}
}
}
void Task2(void *pvParam)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
printf("-----------\n");
printf("task2 notify BIT0!\n");
xTaskNotify( xTask1, 0X01, eSetValueWithOverwrite );//设置task1的通知数值(覆盖)为0X01,就是第0位置成1
vTaskDelay(pdMS_TO_TICKS(5000));
printf("task2 notify BIT1!\n");
xTaskNotify( xTask1, 0X02, eSetValueWithOverwrite );//设置task1的通知数值(覆盖)为0X02,就是第1位置成1
vTaskDelay(pdMS_TO_TICKS(5000));
printf("task2 notify BIT2!\n");
xTaskNotify( xTask1, 0X04, eSetValueWithOverwrite );//设置task1的通知数值(覆盖)为0X04,就是第2位置成1
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void app_main(void)
{
vTaskSuspendAll(); //挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1, "Task1", 1024 * 5, NULL, 1, &xTask1); //传出Task1的任务句柄
xTaskCreate(Task2, "Task2", 1024 * 5, NULL, 1, NULL);
xTaskResumeAll(); //开启任务调度器
}
通知事件wait的时候,只有收到通知才会跳出这个wait,然后进而处理里面的数据。
需要引入头文件 “freertos/stream_buffer.h” “string.h”
流数据缓冲区是用来处理流数据,就是类似流水一样不断产生的数据,如音频数据等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RbbD6bD0-1652860915133)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220514131839198.png)]
流数据阻塞接收的长度,第一次若流缓冲区中有数据,会先接收下来,再阻塞:
StreamBufferHandle_t StreamBufferHandle = NULL;
void Task1(void *pvParam)
{
char txbuf[50];
int buf_len = 0;
int i =0;
int send_bytes = 0;
while (1)
{
vTaskDelay(pdMS_TO_TICKS(3000)); //阻塞先让Task2工作,进入阻塞状态
i++;
buf_len = sprintf(txbuf,"Hello World : %d",i);
//阻塞发送,全部数据发送后才停止阻塞
send_bytes = xStreamBufferSend( StreamBufferHandle,//流数据句柄
(void *)txbuf, //输入数据,需要强转空类型
buf_len, //发送数据的长度
portMAX_DELAY); //等待发送时间,设置最长
printf("-----------\n");
printf("Send:Str_len = %d,Send bytes = %d \n",buf_len,send_bytes);
}
}
void Task2(void *pvParam)
{
char rxbuf[50];
int rec_bytes = 0;
while (1)
{
memset(rxbuf,0,sizeof(rxbuf));
//阻塞接收,执行后若第一次没有数据就要等待接收数据长度达到阈值再接收
rec_bytes = xStreamBufferReceive( StreamBufferHandle, //流数据句柄
(void *)rxbuf, //输出数据,需要强转空类型
sizeof(rxbuf), //接收数据的长度
portMAX_DELAY); //等待接收时间,设置最长
printf("-----------\n");
printf("Receive: Rec_bytes = %d , data = %s \n",rec_bytes,rxbuf);
}
}
void app_main(void)
{
StreamBufferHandle = xStreamBufferCreate(1000 //设置流缓冲区能够容纳的字节数
,100); //触发任务解除阻塞需要流入的字节数
if(StreamBufferHandle != NULL){
vTaskSuspendAll(); //挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1, "Task1", 1024 * 5, NULL, 1, NULL);//发送线程
xTaskCreate(Task2, "Task2", 1024 * 5, NULL, 1, NULL);//接收线程
xTaskResumeAll(); //开启任务调度器
}
}
上一节中使用的是固定缓冲区的大小,但是在实际系统中,Buffer太大或者太小都会对系统造成影响。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x9JoRtpd-1652860915134)(https://github.com/liaozhelin/picgo/raw/master/picpath2/image-20220514132032370.png)]
因此我们可以引入一个task3,用来监控缓冲区的大小,
void Task3(void *pvParam){
int buf_space = 0;
int min_space = 1000;
while(1){
buf_space = xStreamBufferSpacesAvailable(StreamBufferHandle);
if(buf_space<min_space)
min_space = buf_space;
printf("-----------\n");
printf("Buf_space = %d | Min_space = %d\n",buf_space,min_space);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
//main中
...
xTaskCreate(Task3, "Task3", 1024 * 5, NULL, 1, NULL);//监视线程
...
用来监控可用空间的大小,然后设置初始的缓冲区的大小。
需要包含头文件 “freertos/message_buffer.h”
流数据和消息十分类似,不同之处就是消息每次只能接收一条完整的消息,而流数据可以一次性全部接收(不管条数)。
消息缓冲区中会保存每一条消息的长度。
MessageBufferHandle_t MessageBufferHandle = NULL;
void Task1(void *pvParam)
{
char txbuf[50];
int str_len = 0;
int i =0;
int send_bytes = 0;
//创建三条Message,发送
for ( i = 0; i < 3; i++)
{
str_len = sprintf(txbuf,"Hello World : %d",i);
send_bytes = xMessageBufferSend( MessageBufferHandle,//消息数据句柄
(void *)txbuf, //输入消息,需要强转空类型
str_len, //发送消息的长度
portMAX_DELAY); //等待发送时间,设置最长
printf("-----------\n");
printf("Send:i = %d,Send bytes = %d \n",i,send_bytes);
}
vTaskDelete(NULL);
}
void Task2(void *pvParam)
{
char rxbuf[200];
int rec_bytes = 0;
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
memset(rxbuf,0,sizeof(rxbuf));
rec_bytes = xMessageBufferReceive( MessageBufferHandle, //消息数据句柄
(void *)rxbuf, //输出消息,需要强转空类型
sizeof(rxbuf), //接收消息的长度
portMAX_DELAY); //等待接收时间,设置最长
printf("-----------\n");
printf("Receive: Rec_bytes = %d , data = %s \n",rec_bytes,rxbuf);
}
}
void app_main(void)
{
MessageBufferHandle = xMessageBufferCreate(1000); //消息缓冲区在任何时候能够容纳的字节总数
if(MessageBufferHandle != NULL){
vTaskSuspendAll(); //挂起所有任务,任务创建完再按照优先级依次操作
xTaskCreate(Task1, "Task1", 1024 * 5, NULL, 1, NULL);//发送线程
xTaskCreate(Task2, "Task2", 1024 * 5, NULL, 1, NULL);//接收线程
xTaskResumeAll(); //开启任务调度器
}
}
若接收缓冲区小于发送的数据,对于streambuffer,其会分段以接收缓冲区长度接收发送过来的数据,而对于messagebuffer,则接受则会失败,就不会接收任何数据。