认识FreeRTOS
from https://www.programminghunter.com/article/6450751464/
认识FreeRTOS
● FreeRTOS开源免费
●FreeRTOS已经被越来越多的使用
● 操作系统,开发时方便实现多任务调度
● FreeRTOS的内核支持抢占式,合作式和时间片调度
● 提供了一个用于低功耗的Tickless模式
● 高效的软件定时器
● 强大的跟踪执行功能
●堆栈溢出检测功能
● 任务数量不限
●FreeRTOS系统简单、小巧、易用,通常情况下内核占用4k-9k字节的空间
●高可移植性,代码主要C语言编写
●任务与任务、任务与中断之间可以使用任务通知、消息队列、二值信号量、数值型信号量、递归互斥信号量和互斥信号量进行通信和同步
Freerots是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需求。FreeRTOS操作系统是完全免费的操作系统,具有源码公开、可移植、可裁剪、调度策略灵活的特点。
FreeRTOS,可以分为两部分Free和RTOS, Free就是免费的、自由的、不受约束的意思, RTOS全称是Real Time Operating System,中文名就是实时操作系统。可以看出FreeROTS就是一个免费的RTOS类系统。这里要注意, RTOS不是指某一个确定的系统,而是指一类系统。比如uC/os, FreeRTOS, RTX, RT-Thread等这些都是RTOS类操作系统。操作系统允许多个任务同时运行,这个叫做多任务,实际上,一个处理器核心在某一时刻只能运行一个任务。操作系统中任务调度器的责任就是决定在某一时刻究竟运行哪个任务,任务调度在各个任务之间的切换非常快!这就给人们造成了同一时刻有多个任务同时运行的错觉。操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分配同样的运行时间,时间到了就轮到下一个任务, Unix操作系统就是这样的。RTOS的任务调度器被设计为可预测的,而这正是嵌入式实时操作系统所需要的,实时环境中要求操作系统必须对某一个事件做出实时的响应,因此系统任务调度器的行为必须是可预测的。像FreeRTOS这种传统的RTOS类操作系统是由用户给每个任务分配一个任务优先级,任务调度器就可以根据此优先级来决定下一刻应该运行哪个任务。FreeRTOS是RTOS系统的一种, FreeRTOS十分的小巧,可以在资源有限的微控制器中运行,当然了, FreeRTOS不仅局限于在微控制器中使用。但从文件数量上来看FreeRTOS要比uC/OSII和uC/OSII小的多。
嵌入式系统比较
前后台系统
我们经常在嵌入式开发是都是在main函数里一个while(1)循环,再加上一些中断函数,可以认为其是一个单任务系统,也称之为前后台系统,其前台的意思是中断,后台的意思是main函数里的while(1)循环。
有以下特点:
l 简单,消耗资源少
l 任务排队执行,无优先级区分
RTOS系统
多任务实现将一个大的功能分成每一小块,每一小块分别由每一个任务进行管理,多任务并不是同时执行多个任务,本质上CPU在某一时间段只能被一个任务占用,但因为每个任务所占用的时间很短,所以看上去就像同一时间段执行了多个任务。
有以下特点:
l 消耗资源较大
l 通过任务优先级管控,优先级高的任务可以随时打断低任务,功能实时性高
ESP32中的FreeRTOS
本章将围绕ESP32中的FreeRTOS实现展开
原始的FreeRTOS设计为在单个内核上运行。但是ESP32是双核,包含协议CPU(称为CPU 0或PRO_CPU)和应用程序CPU(称为CPU 1或APP_CPU)。这两个内核实际上是相同的,并且共享相同的内存。这允许两个内核在它们之间交替运行任务。
ESP32中关于任务大小使用的是字节为单位。但标准的FreeRTOS使用的是字,在标准中创建任务时如果堆栈为16位宽,而usStackDepth为100,则将分配200字节(16位=2个字节,2*100=200个字节)用作任务的堆栈。再举一个例子,如果堆栈为32位宽,而usStackDepth为400,则将分配1600个字节(32位=4个字节,4*400=1600个字节)用作任务的堆栈。
外链接
字:在计算机中,一串数码作为一个整体来处理或运算的,称为一个计算机字,简称字。
字节:是指一小组相邻的二进制数码。通常是8位作为一个字节。它是构成信息的一个小单位,并作为一个整体来参加操作,比字小,是构成字的单位。
2、所代表的含义不同:
计算机内存中,最小的存储单位是“位(bit)”,8个“位”构成一个“字节(byte)”.
通常若干个字节组成一个“字”。
任务
l 任务使用无限制,可创建任务数量无最大值
l 任务支持优先级,一个优先级下可以有多个任务,取值0到(configMAX_PRIORITIES – 1),其中configMAX_PRIORITIES在FreeRTOSConfig.h中定义,ESP32中为25, 数字越高优先级越高。
l 每个任务维护自己的堆栈(用于任务被抢占后存储上下文),从而导致更高的RAM使用率
l 任务实现函数必须是无返回值void类型的,其内部是通常是一个无限循环,如while(1)
l 任务实现循环中需要有能引起任务调度的内容,通常是延时函数,如vTaskDelay(),也可以是其他只要能让FreeRTOS发生任务切换的API函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。只不过最常用的就是FreeRTOS的延时函数。
l 任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用函数vTaskDelete(NULL);删除此任务以释放内存
l 任务状态:
运行态
当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
就绪态
处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
阻塞态
如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数vTaskDela()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
挂起态
暂停任务,像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数vTaskSuspend()和xTaskResume()。
l 任务堆栈用于存储任务被打断时的上下文,以便任务再次被运行时恢复现场
l 任务创建成功将返回pdPASS,一般创建失败的原因是系统堆内存不足
l ISR是中断可调用函数的标识
l 任务调度:
FreeRTOS如果在单核CPU上运行的话,那就决定了运行时只能有一个任务获得执行。任务调度其实就是任务切换的意思,能引起任务调度的函数有:
vTaskDelay()延时,任务的延时就是让任务进入阻塞状态,交出cpu的使用权。
创建任务
在FreeRTOS实现内部,任务使用两个内存块。第一个块用于保存任务的数据结构。任务将第二个块用作其堆栈。如果使用xTaskCreate()创建任务,则两个内存块将自动在xTaskCreate()函数内部动态分配。
参数:
pvTaskCode:指向任务的实现方法,该方法通常是一个无限循环,如果要退出的话必须使用vTaskDelete(NULL);删除此任务以释放内存
constpcName:任务名称,该名称方便输出一下调试信息,configMAX_TASK_NAME_LEN(ESP32中是16)定义的最大长度-默认为16。
usStackDepth:指定为字节数的任务堆栈的大小。请注意,这与原始FreeRTOS不同。
constpvParameters:任务参数
uxPriority:任务优先级,0到(configMAX_PRIORITIES – 1),其中configMAX_PRIORITIES在FreeRTOSConfig.h中定义, 当前ESP32SDK中该值为( #define configMAX_PRIORITIES ( 25 ) )低优先级数字表示低优先级任务,同一优先级下将进行轮询执行,可以使用taskYIELD()尽快让出CPU使用权。
constpvCreatedTask: 用于传回可引用创建的任务的句柄,方便后续操控任务,可以设置为NULL
返回值
如果成功创建任务并将其添加到就绪列表,则为pdPASS,否则,在文件projdefs.h中定义的错误代码
static BaseType_t xTaskCreate(TaskFunction_t pvTaskCode, const char *constpcName, const uint32_t usStackDepth, void *constpvParameters, UBaseType_t uxPriority, TaskHandle_t *constpvCreatedTask)
小试牛刀
程序总共有三个循环输出随机数:
1.主循环,可获取的资源大
2.任务0循环,可获取的资源被创建任务函数时限制
3.任务1循环,可获取的资源被创建任务函数时限制
1 #include <stdio.h> 2 #include "bootloader_random.h"//随机数相关 3 #include "freertos/FreeRTOS.h"//freertos相关 4 #include "freertos/task.h" 5 6 //任务0处理函数 7 void Task_Run_0(){ 8 uint32_t ranv=0; 9 while(1){ 10 ranv=esp_random();//获取一个随机值,正负数 11 printf("【%s】随机数输出:%d\r\n","任务0",ranv); 12 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 13 } 14 vTaskDelete(NULL); 15 } 16 17 //任务1处理函数 18 void Task_Run_1(void *datas){ 19 uint32_t ranv=0; 20 char *strdata=(char *)datas; 21 while(1){ 22 ranv=esp_random();//获取一个随机值,正负数 23 printf("【%s】随机数输出:%d\r\n",strdata,ranv); 24 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 25 } 26 vTaskDelete(NULL); 27 } 28 29 //主函数 30 void app_main() 31 { 32 printf("\r\n--------------DONGIXAODONG FreeRTOS-----------------\r\n"); 33 //未启用RF获取随机值 34 bootloader_random_enable();//开启随机值获取 35 36 //启动任务0,简化 37 //函数,名字,字节大小,参数,优先级[0,25-1](最高优先为configMAX_PRIORITIES – 1),任务句柄 38 BaseType_t t0res=xTaskCreate(Task_Run_0,"DONG Task_Run_0",1024*2,NULL,16,NULL); 39 if(t0res==pdPASS){ 40 printf("任务0启动成功....\r\n"); 41 } 42 43 //启动任务1,标准 44 TaskHandle_t xHandle1 = NULL; 45 //函数,名字,字节大小,参数,优先级[0,25-1](最高优先为configMAX_PRIORITIES – 1),任务句柄 46 BaseType_t t1res=xTaskCreate(Task_Run_1,"DONG Task_Run_1",1024*2,(void *)"任务1",16,&xHandle1); 47 if(t1res==pdPASS){ 48 printf("任务1启动成功....\r\n"); 49 } 50 51 uint32_t ranv=0; 52 while(1){ 53 ranv=esp_random();//获取一个随机值,正负数 54 printf("【main】随机数输出:%d\r\n",ranv); 55 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 56 } 57 }
获取当前任务的句柄
返回:
调用该函数的任务的任务句柄
TaskHandle_t xTaskGetCurrentTaskHandle()
挂起(暂停)任务调度器:
FreeRTOS如果在单核CPU上运行的话,那就决定了运行时只能有一个任务获得执行。任务调度其实就是任务切换的意思
关闭和开启任务调度器是为了某一个任务在操作过程中不被其它任务打断效果
任务挂起函数被调用几次便要恢复几次,因为其内部有一个挂起计数,支持嵌套操作
在不禁用中断的情况下挂起调度程序。
调度程序挂起时不会发生上下文切换。
调用vTaskSuspendAll()之后,调用任务将继续执行,而不会被交换出来,直到对xTaskResumeAll()进行了调用。
挂起调度程序时,不得调用可能导致上下文切换的API函数(例如vTaskDelayUntil(),xQueueSend()等)
void vTaskSuspendAll( )
恢复挂起(暂停)的任务调度器
关闭和开启任务调度器是为了某一个任务在操作过程中不被其它任务打断效果
通过调用vTaskSuspendAll()挂起后恢复调度程序活动。
xTaskResumeAll()仅恢复调度程序。它不会取消暂停先前通过调用vTaskSuspend()而暂停的任务。
BaseType_t xTaskResumeAll( )
小试牛刀(挂起和恢复任务调度器)
1 void vTask1(void * pvParameters) 2 { 3 while(1){ 4 //任务代码在这里。 5 //在某一时刻,任务需要执行长时间的操作 6 //它不想换出来,并不希望被高优先级的任务打断 7 /*它不能使用: 8 taskENTER_CRITICAL ()/taskEXIT_CRITICAL()的长度 9 因为操作可能会导致中断被错过 10 */ 11 //防止实时内核交换任务。 12 vTaskSuspendAll (); 13 14 //在这里开始做你想做的不会被打断的工作 15 //在这段时间里,中断仍然会发生 16 //时间内核滴答计数将被维护。 17 //工作完成后 18 //重新启动内核,我们想要强制 19 //上下文切换——但是如果恢复调度器是没有意义的 20 //已经导致了上下文切换。 21 if(!xTaskResumeAll ()) 22 { 23 taskYIELD ();//强制切换一次上下文,让高优先级的抢占 24 } 25 } 26 }
删除任务
必须将INCLUDE_vTaskDelete定义为1才能使用此功能
从RTOS实时内核的管理中删除任务。
要删除的任务将从所有准备就绪,阻止,暂停和事件列表中删除
参数:
任务句柄,将删除指定任务,如果传递值为NULL,则删除调用任务删除的当前任务
void vTaskDelete(TaskHandle_txTaskToDelete)
延时
将任务延迟给定的滴答数。
任务保持阻塞的实际时间取决于滴答率。常数portTICK_PERIOD_MS可用于根据滴答速率计算实时时间-分辨率为一个滴答周期。
vTaskDelay()指定相对于调用vTaskDelay()的时间,任务希望解除阻塞的时间。例如,将阻止时间段指定为100个滴答声将导致任务在调用vTaskDelay()之后取消阻止100个滴答声。因此,vTaskDelay()不能提供一种控制周期性任务频率的好方法,因为通过代码的路径以及其他任务和中断活动将影响vTaskDelay()的调用频率,从而影响时间接下来执行任务的位置。请参阅vTaskDelayUntil(),了解旨在简化固定频率执行的替代API函数。它通过指定调用任务应解除阻止的绝对时间(而不是相对时间)来实现。
方法1相对延时函数 vTaskDelay:
必须将INCLUDE_vTaskDelay定义为1,此功能才可用。
参数:xTicksToDelay:调用任务应阻塞的时间(以滴答周期为单位)
vTaskDelay函数传递的参数是延时几个息屏节拍,查看系统时钟,系统时钟是1KHZ,那么系统延时一个节拍就是1MS
vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1000ms=1S
方法2绝对延时函数vTaskDelayUntil:
必须将INCLUDE_vTaskDelayUntil定义为1,此功能才可用。
TickType_t xLastWakeTime是类似用来记录开始时间的句柄
TickType_t xLastWakeTime = xTaskGetTickCount(); vTaskDelayUntil( &xLastWakeTime, (1000 / portTICK_PERIOD_MS); //延时1000ms=1S
两个比较
vTaskDelayUntil与vTaskDelay()不同:vTaskDelay()将导致任务从调用vTaskDelay()时起以指定的滴答数阻塞。因此,很难单独使用vTaskDelay()来生成固定的执行频率,因为任务开始执行与调用vTaskDelay()的任务之间的时间可能不固定[该任务可能采用不同的路径,尽管调用之间的代码不同,或者每次执行时可能被打断或抢占不同的次数]。vTaskDelay()指定相对于调用该函数的时间的唤醒时间,而vTaskDelayUntil()指定其希望解除阻止的绝对(精确)时间。
xTicksToDelay延时只是交出CPU时间比如说20MS,但是它没计算这个任务本身运行消耗的时间和其它中间环节耗用的时间,获取在执行时被高优先级打断可能,因此它的延时是个大概值,具有不确定性;第2个就不一样了,你可以把它想像成一个时钟,比方说它记录了下上次延时的时候是9.30分钟,你再次延时30分钟,那么它在10:00就是准时切换成本次任务,所以说它是比较精准的延时。https://bbs.21ic.com/icview-412527-1-1.html
设置任务优先级
必须将INCLUDE_vTaskPrioritySet定义为1才能使用此功能
参数1:任务句柄
参数2:新的优先级,与创建任务时的【uxPriority】类似
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
获取任务优先级
必须将INCLUDE_uxTaskPriorityGet定义为1才能使用此功能
参数:
任务句柄,将指定获取某个任务的优先级,如果传递值为NULL,则获取调用该函数的当前任务的优先级值
任务中使用函数
UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask )
中断服务中使用函数
UBaseType_t uxTaskPriorityGetFromISR(TaskHandle_t xTask )
小试牛刀
1 //主函数 2 void app_main() 3 { 4 //------ 5 //启动任务2,标准 6 TaskHandle_t xHandle2 = NULL; 7 //函数,名字,字节大小,参数,优先级[0,25-1](最高优先为configMAX_PRIORITIES – 1),任务句柄 8 BaseType_t t2res=xTaskCreate(Task_Run_2,"DONG Task_Run_2",1024*2,(void *)"任务2",5,&xHandle2); 9 if(t2res==pdPASS){ 10 printf("任务2启动成功....\r\n"); 11 } 12 13 UBaseType_t t2pri=uxTaskPriorityGet(xHandle2);//获取任务2的优先级,输出5 14 UBaseType_t thispri=uxTaskPriorityGet(NULL);//获取main函数的优先级,输出1 15 printf("t2任务的优先级为:%d,当前任务(main)的优先级为:%d\r\n",t2pri,thispri); 16 while(1){ 17 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 18 } 19 }
查询任务状态
必须将INCLUDE_eTaskGetState定义为1才能使用此功能。
返回值: enum 枚举类型
eNoAction= 0,/*运行态,任务正在查询自身的状态,所以必须运行。* /
eReady, / * 就绪态,被查询的任务在已读或暂挂的就绪列表中。* /
eBlocked, / * 阻塞态,被查询的任务处于阻塞状态。* /
eSuspended, / *挂起态,被查询的任务处于挂起状态,或者处于阻塞状态,超时时间为无限。* /
edeleted / * !<正在查询的任务已被删除,但其TCB尚未被释放。* /
1 //主函数 2 void app_main() 3 { 4 //----------- 5 //启动任务2,标准 6 TaskHandle_t xHandle2 = NULL; 7 //函数,名字,字节大小,参数,优先级[0,25-1](最高优先为configMAX_PRIORITIES – 1),任务句柄 8 BaseType_t t2res=xTaskCreate(Task_Run_2,"DONG Task_Run_2",1024*2,(void *)"任务2",5,&xHandle2); 9 if(t2res==pdPASS){ 10 printf("任务2启动成功....\r\n"); 11 } 12 eTaskState t2_sta=eTaskGetState(xHandle2); 13 printf("t2任务的运行状态:%d\r\n",t2_sta);//输出 2 14 while(1){ 15 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 16 } 17 }
暂停(挂起)任务
必须将INCLUDE_vTaskSuspend定义为1,此功能才可用。
然后暂停后如同播放音乐是按下了暂停键,当恢复时将会从暂停处再次开始执行,并不会重新开始于while循环外,所以暂停是会保留上下文的。
暂停任务后,无论其优先级如何,任务将永远不会获得任何微控制器处理时间。
对vTaskSuspend的调用不是累积性的-即,在同一任务上两次或两次以上调用vTaskSuspend()仍然只需要对vTaskResume()进行一次调用即可准备挂起的任务。
参数
任务句柄,如果是NULL,则表示挂起调用该函数的任务
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
恢复暂停(挂起)的任务
必须将INCLUDE_vTaskSuspend定义为1,此功能才可用。
通过一次调用vTaskResume(),已被一个或多个vTaskSuspend()调用暂停的任务将可以再次运行
参数
任务句柄,如果是NULL,则表示挂起调用该函数的任务
void vTaskResume( TaskHandle_t xTaskToResume )
中断服务中恢复暂停(挂起)的任务
必须将INCLUDE_xTaskResumeFromISR定义为1,此功能才可用。
通过一次调用xTaskResumeFromISR(),已被一个或多个vTaskSuspend()调用暂停的任务将可以再次运行。
如果在挂起任务之前中断可能到达,则xTaskResumeFromISR()不应用于将任务与中断同步-因为这可能导致中断丢失。使用信号量作为同步机制可以避免这种情况的发生。
返回:
如果继续执行任务,则为pdTRUE,这将导致上下文切换,否则为pdFALSE。ISR使用它来确定在ISR之后是否可能需要上下文切换。
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume )
获取任务系统总数量
实时内核当前正在管理的任务数。这包括所有准备就绪,已阻止和已暂停的任务。空闲任务已删除但尚未释放的任务也将包括在计数中。
UBaseType_t tasknum=uxTaskGetNumberOfTasks(); printf("系统任务总数量:%d\r\n",tasknum);
获取任务的名称
必须将FreeRTOSConfig.h中的INCLUDE_pcTaskGetTaskName设置为1,pcTaskGetTaskName()才可用
返回任务创建时给的的任务名称
参数:
任务句柄,传递NULL表示当前调用函数任务
//主函数 void app_main() { char *main_task= pcTaskGetTaskName(NULL); printf("当前任务的名称是:%s",main_task);//当前任务的名称是:main }
获取系统时间计数器值
返回调用调用vTaskStartScheduler()以来的滴答计数
//任务中使用 TickType_t xTaskGetTickCount( void ) //中断服务函数使用 TickType_t xTaskGetTickCountFromISR( void )
小试牛刀
1 #include <stdio.h> 2 #include "freertos/FreeRTOS.h"//freertos相关 3 #include "freertos/task.h" 4 //任务0处理函数 5 void Task_Run_0(){ 6 TickType_t counts=0; 7 while(1){ 8 counts=xTaskGetTickCount(); 9 printf("【%s】系统任务计数器值:%d\r\n","任务0",counts); 10 vTaskDelay(3000 / portTICK_PERIOD_MS);//延时1S 11 } 12 } 13 //主函数 14 void app_main() 15 { 16 printf("\r\n--------------DONGIXAODONG FreeRTOS-----------------\r\n"); 17 18 //启动任务0,简化 19 //函数,名字,字节大小,参数,优先级[0,25-1](最高优先为configMAX_PRIORITIES – 1),任务句柄 20 BaseType_t t0res=xTaskCreate(Task_Run_0,"DONG Task_Run_0",1024*2,NULL,7,NULL); 21 if(t0res==pdPASS){ 22 printf("任务0启动成功....\r\n"); 23 } 24 TickType_t counts=0; 25 while(1){ 26 counts=xTaskGetTickCount(); 27 printf("【%s】系统任务计数器值:%d\r\n","Main",counts); 28 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 29 } 30 }
任务堆栈使用的高水位线中断服务
要使此功能可用,必须在FreeRTOSConfig.h中将INCLUDE_uxTaskGetStackHighWaterMark设置为1。
自任务开始以来,高水位标记是已存在的最小可用堆栈空间,后面将会保留最高使用率(以字节为单位,而不是原始FreeRTOS中的单词)。返回的数字越小,任务越接近其堆栈溢出。
参数:
与要检查的堆栈关联的任务的句柄。将xTask设置为NULL可检查调用任务的堆栈。
返回:
自创建xTask引用的任务以来,可用堆栈空间最小值(以字节为单位,而不是原始FreeRTOS中的字数)。
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask )
获取系统中的所有任务信息
必须在FreeRTOSConfig.h中将configUSE_TRACE_FACILITY定义为1,才能使uxTaskGetSystemState()可用。
uxTaskGetSystemState()为系统中的每个任务填充TaskStatus_t结构。TaskStatus_t结构包含任务句柄的成员,任务名称,任务优先级,任务状态以及任务消耗的运行时间总量。有关完整的成员列表,请参见此文件中的TaskStatus_t结构定义。
注意:此功能仅用于调试用途,因为其使用会导致调度程序长时间处于挂起状态。
参数:
pxTaskStatusArray:指向TaskStatus_t结构数组的指针。对于受RTOS控制的每个任务,该数组必须至少包含一个TaskStatus_t结构。可以使用uxTaskGetNumberOfTasks()API函数来确定RTOS控制下的任务数。
uxArraySize:pxTaskStatusArray参数指向的数组的大小。该大小指定为数组中的索引数,或者数组中包含的TaskStatus_t结构数,而不是数组中的字节数。
pulTotalRunTime:如果在FreeRTOSConfig.h中将configGENERATE_RUN_TIME_STATS设置为1,则* pulTotalRunTime由uxTaskGetSystemState()设置为总运行时间(由运行时间统计时钟定义,请参见http://www.freertos.org/rtos-run-自目标启动以来。time-stats.html)。可以将pulTotalRunTime设置为NULL以省略总运行时间信息。
返回:
uxTaskGetSystemState()填充的TaskStatus_t结构的数量。该值应等于uxTaskGetNumberOfTasks()API函数返回的数字,但如果uxArraySize参数中传递的值太小,则该数字将为零。
任务信息结构体TaskStatus_t:
TaskHandle_t xHandle;/ * !<结构中其余信息所涉及的任务的句柄。* /
const char * pcTaskName;/ * !<指向任务名称的指针。如果任务被删除,则此值无效,因为该结构已被填充!*/ /*lint !e971不合格的字符类型只允许用于字符串和单个字符。* /
UBaseType_t xTaskNumber;/ * !<任务编号,越小表示越早被创建* /
eTaskState eCurrentState;/ * !<结构被填充时任务存在的状态。* /
UBaseType_t uxCurrentPriority;/ * !<结构被填充时任务运行的优先级(可能是继承的)。* /
UBaseType_t uxBasePriority;/ * !<如果继承了任务当前的优先级,任务将返回的优先级,以避免在获取互斥锁时发生无界的优先级反转。只有在FreeRTOSConfig.h中将configUSE_MUTEXES定义为1时才有效。* /
uint32_t ulRunTimeCounter;/ * !<到目前为止分配给任务的总运行时间,由运行时统计时钟定义。见http://www.freertos.org/rtos-run-time-stats.html。只有在FreeRTOSConfig.h中将configGENERATE_RUN_TIME_STATS定义为1时才有效。* /
StackType_t * pxStackBase;/ * !<指向任务堆栈区域的最低地址。* /
uint32_t usStackHighWaterMark;/ * !<自任务创建以来为该任务保留的最小堆栈空间量。该值越接近于零,任务就越接近溢出其堆栈。
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray, const UBaseType_t uxArraySize, uint32_t * const pulTotalRunTime )
小试牛刀
configUSE_TRACE_FACILITY定义为1
configUSE_MUTEXES定义为1
configGENERATE_RUN_TIME_STATS定义为0,所以下面输出的时间是无效的
//输出值:任务名称,优先级,运行时间,系统运行时间
DONG Task_Run_0我创建的任务
Main主函数任务
Tmr Svc 定时器任务
IDLE1 空闲任务
IDLE0 空闲任务
1 #include <stdio.h> 2 #include "freertos/FreeRTOS.h"//freertos相关 3 #include "freertos/task.h" 4 5 //获取所有任务并显示 6 //configUSE_TRACE_FACILITY 设置为1 7 void show_task() 8 { 9 volatile UBaseType_t uxArraySize, x;//存储任务数量,x为for循环所使用的变量 10 uint32_t ulTotalRunTime;//运行时间存储 11 //获取系统的任务数量 12 uxArraySize = uxTaskGetNumberOfTasks(); 13 //申请内存空间以存储用户信息 14 TaskStatus_t * pxTaskStatusArray = pvPortMalloc( uxArraySize * sizeof( TaskStatus_t ) ); 15 //如果申请成功 16 if( pxTaskStatusArray != NULL ) 17 { 18 // 开始获取所有任务信息,参数(任务信息存储空间,获取的任务数量,存储系统运行时间),返回值为数量 19 uxArraySize = uxTaskGetSystemState( pxTaskStatusArray, uxArraySize, &ulTotalRunTime ); 20 21 // 循环获取的任务数量输出 22 for( x = 0; x < uxArraySize; x++ ) 23 { 24 //输出值:任务名称,优先级,运行时间,系统运行时间 25 printf("%s\t\t%d\t\t%d\t\t%d\r\n", pxTaskStatusArray[ x ].pcTaskName, pxTaskStatusArray[ x ].uxCurrentPriority, pxTaskStatusArray[ x ].ulRunTimeCounter, ulTotalRunTime ); 26 } 27 //不再需要该数组,释放它所消耗的内存。 28 vPortFree( pxTaskStatusArray ); 29 } 30 } 31 32 //任务0处理函数 33 void Task_Run_0(){ 34 show_task();////任务查询 35 while(1){ 36 printf("【%s】创建任务\r\n","任务0"); 37 vTaskDelay(3000 / portTICK_PERIOD_MS);//延时1S 38 } 39 } 40 41 //主函数 42 void app_main() 43 { 44 printf("\r\n--------------DONGIXAODONG FreeRTOS-----------------\r\n"); 45 46 //启动任务0,简化 47 //函数,名字,字节大小,参数,优先级[0,25-1](最高优先为configMAX_PRIORITIES – 1),任务句柄 48 BaseType_t t0res=xTaskCreate(Task_Run_0,"DONG Task_Run_0",1024*2,NULL,7,NULL); 49 if(t0res==pdPASS){ 50 printf("任务0启动成功....\r\n"); 51 } 52 53 while(1){ 54 printf("【%s】主任务\r\n","main"); 55 vTaskDelay(1000 / portTICK_PERIOD_MS);//延时1S 56 } 57 }
列出所有任务的一些信息
为了使此功能可用,必须将configUSE_TRACE_FACILITY和configUSE_STATS_FORMATTING_FUNCTIONS都定义为1
列出所有当前任务,以及它们的当前状态和堆栈使用率高水位标记。
注意:
此功能将在其持续时间内禁用中断。它不适合正常的应用程序运行时使用,而是作为调试辅助。
任务报告为已阻止('B'),就绪('R'),已删除('D')或已暂停('S')。
vTaskList()调用uxTaskGetSystemState(),然后将uxTaskGetSystemState()输出的一部分格式化为人类可读的表,以显示任务名称,状态和堆栈使用情况。
建议生产系统直接调用uxTaskGetSystemState()以获得对原始统计数据的访问,而不是通过调用vTaskList()间接进行。
参数:
pcWriteBuffer:一个缓冲区,上面提到的详细信息将以ASCII形式写入其中。假定此缓冲区足够大以包含生成的报告。每个任务大约40个字节就足够了。
输出:
任务名称、任务状态、任务优先级、任务堆栈历史最小剩余量、任务编号(越先开启值越低)
void vTaskList( char * pcWriteBuffer )
小试牛刀
输出:名称、状态、优先级、历史最小剩余堆栈、任务编号
char reslist[1000]; vTaskList(reslist); printf("获取任务详情:\r\n%s\r\n",reslist);
获取任务运行时间百分比
configGENERATE_RUN_TIME_STATS和configUSE_STATS_FORMATTING_FUNCTIONS需要设置为1
void vTaskGetRunTimeStats( char *pcWriteBuffer )
小试牛刀
//输出任务名称,运行时间,所占系统运行时间比例
IDLE1 空闲任务
IDLE0 空闲任务
空闲任务运行时间占比越大越好,如果有某个任务所暂用运行时间百分比大的话,需要考虑将任务拆分
char reslist[1000]; vTaskGetRunTimeStats(reslist); printf("获取任务运行时间:\r\n%s\r\n",reslist);
消息队列
头文件:freertos / include / freertos / queue.h
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列用来传递消息的,所以也称为消息队列。FreeRTOS中的信号量的也是依据队列实现的!所以有必要深入的了解FreeRTOS的队列。
队列通常时先进先出的,也可以设置为先进后出,FreeRTOS中队列通常传递的是内容,而不是指针
入队:
队列中没有消息,可以设置为不等待立即返回,等待指定时间节拍、一直等待有空位
出队:
队列满了会阻塞,可以设置为不等待立即返回、等待指定的时间节拍、一直等待有消息为止
队列创建
创建一个新的队列实例。这将分配新队列所需的存储,并返回该队列的句柄。
参数:
uxQueueLength 队列可以存储的条数,队列可以包含的最大项目数。
unsigned int uxItemSize 队列中每条的最大存储字节,队列中每个项目所需的字节数。项目按副本而不是引用排队,因此这是将为每个过账项目复制的字节数。队列中的每个项目都必须具有相同的大小。
返回:
如果创建成功则返回队列句柄,无法创建则返回0
QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize )
发送消息到队列中
将项目发布到队列中。该项目按副本而不是参考排队。不得从中断服务程序中调用此函数。有关在ISR中可以使用的替代方法,请参见xQueueSendFromISR()
参数:
xQueue:要发布项目的队列的句柄。
pvItemToQueue:内容,指向要放在队列中的项目的指针。创建队列时已定义了队列将要容纳的项目大小,因此,这许多字节将从pvItemToQueue复制到队列存储区域。
xTicksToWait:如果任务已满,则该任务应阻止等待队列上的可用空间的最长时间。如果将其设置为0并且队列已满,则呼叫将立即返回。时间以滴答周期定义,因此如果需要,应使用常数portTICK_PERIOD_MS转换为实时。portMAX_DELAY表示一直等待
返回:
如果项目已成功发布,则为pdTRUE,否则为errQUEUE_FULL。
官方手册建议使用函数如下:
发送消息到队列头:
BaseType_t xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait )
发送消息到队列尾
BaseType_t xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait )
发送到消息队列尾
BaseType_t xQueueSend( xQueue, pvItemToQueue, xTicksToWait )
上面三个函数本质上调用了xQueueGenericSend
其最后一个参数:
xCopyPosition:可以使用值queueSEND_TO_BACK将项目放置在队列的后面,或将queueSEND_TO_FRONT放置在队列的前面(对于高优先级消息)
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT ) #define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK ) #define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
发送消息到队列中(复写)
仅用于长度为1的队列-因此队列为空或已满。
将项目发布到队列中。如果队列已满,则覆盖队列中保存的值。该项目按副本而不是参考排队。
不得从中断服务程序中调用此函数。有关可以在ISR中使用的替代方法,请参见xQueueOverwriteFromISR()。
xQueueOverwrite()是一个宏,它调用xQueueGenericSend(),因此具有与xQueueSendToFront()相同的返回值。但是,pdPASS是唯一可以返回的值,因为即使队列已满,xQueueOverwrite()也会写入队列
参数
xQueue:将数据发送到的队列的句柄。
pvItemToQueue:指向要放在队列中的项目的指针。创建队列时已定义了队列将要容纳的项目大小,因此,这许多字节将从pvItemToQueue复制到队列存储区域。
BaseType_t xQueueOverwrite( xQueue,pvItemToQueue )
其本质调用的函数为
#define xQueueOverwrite( xQueue, pvItemToQueue ) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
中断服务函数中发消息到队列
将项目发布到队列中。在中断服务程序中可以安全地使用此功能。
项目通过复制而不是引用进行排队,因此最好仅将小项目排队,尤其是从ISR调用时。在大多数情况下,最好存储一个指向正在排队的项目的指针。
参数:
xQueue:要发布项目的队列的句柄
pvItemToQueue:内容,指向要放在队列中的项目的指针。创建队列时已定义了队列将要容纳的项目大小,因此,这许多字节将从pvItemToQueue复制到队列存储区域。
pxHigherPriorityTaskWoken:判断是否需要手动切换上下文,如果发送到队列导致任务取消阻止,并且未阻止的任务的优先级高于当前运行的任务,则xQueueGenericSendFromISR()会将* pxHigherPriorityTaskWoken设置为pdTRUE。如果xQueueGenericSendFromISR()将此值设置为pdTRUE,则应在退出中断之前请求上下文切换taskYIELD ();。
发送消息到队列头
BaseType_t xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
发送消息到队列尾
BaseType_t xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
发送到消息队列尾
BaseType_t xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
上面三个函数本质上调用了xQueueGenericSendFromISR
其最后一个参数
xCopyPosition:可以使用值queueSEND_TO_BACK将项目放置在队列的后面,或将queueSEND_TO_FRONT放置在队列的前面(对于高优先级消息)
#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT ) #define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK ) #define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
中断服务函数中发消息到队列(复写)
可以在中断服务程序(ISR)中使用的xQueueOverwrite()版本。
仅用于可容纳单个项目的队列-因此队列为空或已满。
将项目发布到队列中。如果队列已满,则覆盖队列中保存的值。该项目按副本而不是参考排队。
参数:
xQueue:要发布项目的队列的句柄
pvItemToQueue:指向要放在队列中的项目的指针。创建队列时已定义了队列将要容纳的项目大小,因此,这许多字节将从pvItemToQueue复制到队列存储区域。
pxHigherPriorityTaskWoken:如果发送到队列导致任务取消阻止,并且未阻止的任务的优先级高于当前运行的任务,则xQueueOverwriteFromISR()会将* pxHigherPriorityTaskWoken设置为pdTRUE。如果xQueueOverwriteFromISR()将此值设置为pdTRUE,则应在退出中断之前请求上下文切换。
返回
QueueOverwriteFromISR()是一个调用xQueueGenericSendFromISR()的宏,因此其返回值与xQueueSendToFrontFromISR()相同。但是,pdPASS是唯一可以返回的值,因为即使队列已满,xQueueOverwriteFromISR()也会写入队列。
BaseType_t xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
本质调用函数
#define xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueOVERWRITE )
获取消息队列消息
从队列中接收项目。该项目以副本形式接收,因此必须提供足够大小的缓冲区。创建队列时定义了复制到缓冲区中的字节数。
不得在中断服务程序中使用此功能。另请参见xQueueReceiveFromISR。
参数:
xQueue:要从中接收项目的队列的句柄。
pvBuffer:指向将接收到的项目复制到的缓冲区的指针。
xTicksToWait:如果队列在调用时为空,则任务应等待等待接收项目的最长时间。时间以滴答周期定义,因此如果需要,应使用常数portTICK_PERIOD_MS转换为实时。如果队列为空并且xTicksToWait为0,则xQueueGenericReceive()将立即返回。portMAX_DELAY表示一直等待
返回:
如果从队列成功接收到项目,则为pdTRUE,否则为pdFALSE。
仅仅取出消息队列中的内容,不删除已经取出的消息
BaseType_t xQueuePeek( xQueue, pvBuffer, xTicksToWait )
取出并删除接收的内容
BaseType_t xQueueReceive( xQueue, pvBuffer, xTicksToWait )
上面两个函数本质上调用了xQueueReceiveFromISR
其最后一个参数
xJustPeek:当设置为true时,从队列接收的项目实际上并未从队列中删除-意味着对xQueueReceive()的后续调用将返回相同的项目。设置为false时,将从队列中接收的项目也将从队列中删除。
#define xQueuePeek( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdTRUE ) #define xQueueReceive( xQueue, pvBuffer, xTicksToWait ) xQueueGenericReceive( ( xQueue ), ( pvBuffer ), ( xTicksToWait ), pdFALSE )
中断服务函数中获取队列消息
xQueuePeekFromISR
可以从中断服务程序(ISR)
仅仅取出消息队列中的内容,不删除已经取出的内容(项目)
xQueue:要从中接收项目的队列的句柄。
pvBuffer:指向将接收到的项目复制到的缓冲区的指针
返回:
如果从队列成功接收到项目,则为pdTRUE,否则为pdFALSE。
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,void * const pvBuffer )
xQueueReceiveFromISR
可以从中断服务程序(ISR)取出并删除接收的内容(项目)
参数
xQueue:要从中接收项目的队列的句柄。
pvBuffer:指向将接收到的项目复制到的缓冲区的指针。
pxHigherPriorityTaskWoken:任务可能等待队列中的可用空间被阻塞。如果xQueueReceiveFromISR使此类任务解除阻止,则* pxTaskWoken将设置为pdTRUE,否则* pxTaskWoken将保持不变。如果值为pdTRUE,则应在退出中断之前请求上下文切换taskYIELD ();。
注意:
此函数比xQueuePeekFromISR多一个参数,是因为如果取出队列数据后,删除该项目可能会使得触发优先级高的任务停止阻塞,所以要查看返回值启动任务切换
返回:
如果从队列成功接收到项目,则为pdTRUE,否则为pdFALSE。
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue,void * const pvBuffer,BaseType_t * const pxHigherPriorityTaskWoken )
获取存储在队列中的消息数
参数:
xQueue:查询队列的句柄
返回:
队列中可用的消息数,未取出的消息数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue )
获取队列的剩余可用条数
返回队列中可用的可用空间数。这等于如果没有删除任何项目,则在队列变满之前可以发送到队列的项目数
参数:
xQueue:查询队列的句柄
返回:
队列中剩余可用条数(最大为创建任务时的uxQueueLength 值,其表示队列可以存储的条数,队列可以包含的最大项目数。)
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue )
队列删除
删除队列-释放分配用于存储放置在队列中的项目的所有内存。
参数:要删除的队列的句柄
void vQueueDelete( QueueHandle_t xQueue )
重置队列
将队列重置回其原始的空状态。如果成功重置队列,则返回pdPASS。如果无法重置队列,则返回pdFAIL,因为队列上有阻塞的任务正在等待从队列接收或发送到队列,因此无法重置队列。
参数:xQueue:要重置的队列
BaseType_t xQueueReset( xQueue )
小试牛刀(队列)
1 #include <stdio.h> 2 #include "freertos/FreeRTOS.h"//freertos相关 3 #include "freertos/task.h" 4 #include "freertos/queue.h" 5 6 //自定义队列消息,不一定是结构体 7 struct myMsg 8 { 9 uint32_t d_id; 10 char d_msg[50]; 11 }; 12 //存储消息队列句柄 13 QueueHandle_t Dong_uint32_Queue, Dong_myMsg_Queue; 14 15 //创建消息队列 16 void dong_creat_queue() 17 { 18 //创建能够存储10个uint32_t的队列 19 Dong_uint32_Queue = xQueueCreate( 10, sizeof( uint32_t ) ); 20 if( Dong_uint32_Queue == 0 ) 21 { 22 printf("Dong_uint32_Queue 队列创建失败"); 23 } 24 //创建一个能够包含10个指向myMsg结构的队列。 25 //此处传递的是结构体,并不是结构体指针 26 Dong_myMsg_Queue = xQueueCreate( 10, sizeof(struct myMsg) ); 27 if( Dong_myMsg_Queue == 0 ) 28 { 29 printf("Dong_myMsg_Queue 队列创建失败"); 30 } 31 } 32 33 //任务0处理函数 34 void Task_Run_0(){ 35 uint32_t resi=0; 36 struct myMsg resmymsg; 37 while(1){ 38 printf("\r\n【%s】////////开始接收/////////\r\n","任务0"); 39 40 xQueueReceive(Dong_uint32_Queue,&resi,portMAX_DELAY); 41 printf("【%s】获取到Dong_uint32_Queue内容:%d\r\n","任务0",resi); 42 xQueueReceive(Dong_myMsg_Queue,&resmymsg,portMAX_DELAY); 43 printf("【%s】获取到Dong_myMsg_Queue内容:%d(%s)\r\n","任务0",resmymsg.d_id,resmymsg.d_msg); 44 45 printf("\r\n【%s】////////完成接收/////////\r\n","任务0"); 46 } 47 } 48 49 //主函数,优先级为1 50 void app_main() 51 { 52 printf("\r\n--------------DONGIXAODONG FreeRTOS-----------------\r\n"); 53 54 //创建消息队列 55 dong_creat_queue(); 56 57 //启动任务0,简化 58 //函数,名字,字节大小,参数,优先级[0,16](16最优先),任务句柄 59 BaseType_t t0res=xTaskCreate(Task_Run_0,"DONG Task_Run_0",1024*2,NULL,7,NULL); 60 if(t0res==pdPASS){ 61 printf("任务0启动成功....\r\n"); 62 } 63 64 BaseType_t res=0; 65 uint32_t i=0; 66 struct myMsg mymsg; 67 while(1){ 68 //赋值 69 i++; 70 mymsg.d_id=i; 71 sprintf(mymsg.d_msg,"dongxiaodong%d",i); 72 73 //输出发送标志 74 printf("\r\n【%s】*****开始发送*******\r\n","main"); 75 76 //发送队列1 77 res= xQueueGenericSend( Dong_uint32_Queue, ( void * ) &i,( TickType_t ) 10,queueSEND_TO_BACK ); 78 if(res == pdPASS ) 79 { 80 printf("【%s】Dong_uint32_Queue 发送成功\r\n","main"); 81 } 82 //发送队列2 83 res= xQueueGenericSend( Dong_myMsg_Queue, ( void * ) &mymsg, ( TickType_t ) 0, queueSEND_TO_BACK ); 84 if(res == pdPASS ) 85 { 86 printf("【%s】Dong_myMsg_Queue 发送成功\r\n","main"); 87 } 88 89 printf("\r\n【%s】*****结束发送*******\r\n","main"); 90 91 vTaskDelay(3000 / portTICK_PERIOD_MS);//延时3S 92 } 93 }
因篇幅问题,剩下相关笔记将于下一篇文章进行总结,剩下部分包括:
信号量
计时器
事件组
任务通知
参考:
https://zhidao.baidu.com/question/7412988.html
ESP32文档:https://docs.espressif.com/projects/esp-idf/en/v4.0/api-reference/system/freertos.html
正点原子
留言
張貼留言