我们的ESP-IDF已经将FreeRTOS 作为组件集成到 ESP-IDF 中了。
并且因为原生FreeRTOS是单核的RTOS,而我们的ESP32是有双核的,因此乐鑫为了支持多核,将FreeRTOS 内核移植到 ESP 芯片的所有可用架构中了。移植改过之后的叫ESP-IDF FreeRTOS,ESP-IDF FreeRTOS 是基于原生 FreeRTOS v10.5.1 的 FreeRTOS 实现,其中包含支持 SMP 的大量更新。ESP-IDF FreeRTOS 最多支持两个核(即双核 SMP)。
所以我们可以通过ESP-IDF FreeRTOS来学习FreeRTOS。
这边说一下,一般来说我们说ESP32,是指代ESP32这一大类芯片,而我上面说“ESP32是有双核的”意思就是有部分型号的ESP32是没有双核的。
比如说ESP32-S2,ESP32-H2,ESP32-C等就是单核的,如果想使用ESP-IDF FreeRTOS的双核功能的话(单核芯片可以使用单核功能),在购买前就需要确认好芯片是否为双核。
我们一般买的开发板是ESP32型号的(没错,有个型号就叫ESP32,所以我在上一段强调了一下ESP32的这种说法,大家根据语境自行判断ESP32是指特定型号还是泛指这类芯片),基本是支持双核的,但还是有个版本是单核的。
单核双核其实对于我们学习FreeRTOS是无所谓的,只不过ESP-IDF FreeRTOS中有些函数是双核才能使用的,使用的时候注意一下就好了。
其实我们之前一直都在使用ESP-IDF FreeRTOS,因为我们的主函数是app_main,这其实就是ESP-IDF FreeRTOS的一个任务。
我们的app_main是由一个叫main的任务调用的。
除了main任务,还有一些任务是ESP-IDF FreeRTOS自动创建的。
这些任务做什么的我们可以不用知道,我们只需要保证我们后续创建的任务不重名即可。另外如果使用了蓝牙或是WiFi,那么它们还会另外创建一些后台任务。
这边推荐三个网站来学习,第一个是ESP-IDF的官方文档,有对应的ESP-IDF FreeRTOS的章节。
第二个是FreeRTOS的官网。
FreeRTOS™ - FreeRTOS™https://www.freertos.org/
第三个是百问网的FreeRTOS相关内容。
先包含下头文件。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
接下来开始介绍一下关于延时的API。
在介绍API之前我们先简单聊聊FreeRTOS的命名规则。
如果我们大致的翻翻FreeRTOS的API就会看到有很多奇奇怪怪的前缀。
常见的有v,p,x之类的。
v表示的是void,用在函数前缀则表示这个函数的返回值是void。
比如说我们下面这个之前常用的延时函数。
p表示的是pointer,也就是指针。
x表示的是非标准类型,也就是自定义类型。
e表示的是enum,也就是枚举类型。
但也不是一定的,比如说pdTRUE和pdFALSE就貌似和pointer没什么关系。
我们先来看看延时函数。
void vTaskDelay(const TickType_t xTicksToDelay)
这也是我们常用的延时函数。上面介绍一大堆啰啰嗦嗦的,实际上我们就传入一个数,然后这个函数就会延时这么多个时钟周期。
那么一个时钟周期是多久呢?默认应该是10ms(应该哈,我用的ESP32S3,其他型号的没玩过)。
具体怎么查看呢?
我们打开VSCode的下面一个任务栏,点击这个齿轮,等待加载片刻就会有个页面出现在VSCode里面。
然后我们搜索Tick就可以看到频率是100,那么周期自然就是1s/100 = 10ms了。
可以修改吗,当然可以。修改完记得保存。
然后就会发现编译失败。
实际上你打开设置之后,就算什么都不做,编译也是失败的。
这时候我们只需要先clean一下再编译就行了。
clean之后设置界面就消失了,但如果设置完之后保存了,那么修改的内容还是有效的。
一般有什么需要改的配置,最好是在一开始就改好,因为每次设置完需要clean,clean之后重新编译就会像第一次编译那样比较久了。
那我们每次写程序之前都需要打开设置看看时钟周期是多少吗?
当然不用,我们不需要查看时钟周期也可以指定时间。
那就是使用宏。下面介绍两种。portTICK_PERIOD_MS和pdMS_TO_TICKS。
portTICK_PERIOD_MS的使用方法就是像上面这样拿一个数去除它,得到的数就是我们要延时的毫秒数对应的时钟周期的数量了。像上面这样1000/portTICK_PERIOD_MS得到的数就是我们延时1000ms所需的时钟周期数。
我们再进一步看看portTICK_PERIOD_MS内部是什么。
它里面是1000/另一个宏,注意这边的1000和我上面例子中的1000没有关系,纯属巧合。
而里面这个宏又是另一个宏。
这时我们看到这另外一个宏的名字开头是config,就是设置的意思。
最终我们可以看到,它的值就是我们一开始设置的值。所以理论上说我们修改这个值的效果跟打开设置修改的效果是一样的。
最后我们合起来看,portTICK_PERIOD_MS的值实际上就是10。而我们传给vTaskDelay的值起码得是1吧,所以我们至少得延时10ms(1个时钟周期),但这个我们可以通过设置去修改时钟周期,不过不建议修改,如果需要延时更小单位的时间,建议是使用usleep(#include <unistd.h>)
另一个pdMS_TO_TICKS顾名思义 就是将我们传入的ms转为对应的tick。
其实本质上和上面的portTICK_PERIOD_MS是一样的,所以用哪个都可以。
另外还有个宏是pdTICKS_TO_MS,是把时钟周期数转换成毫秒数的,不要搞混了。
除了vTaskDelay之外,还有另一个延时函数。
BaseType_t xTaskDelayUntil(TickType_t *const pxPreviousWakeTime, const TickType_t xTimeIncrement)
它需要俩参数,第一个是上一次唤醒时间,第二个是时钟周期数。
看不懂对吧,一开始我也看不懂。
举个例子吧,比如说我第一个参数给它传入8点,第二个参数给它传入俩小时,那么不管我在几点的时候调用它,我都会延时到8+2 = 10点,如果我八点调用,那么就是延时俩小时,如果我九点调用,那么我还是延时到10点,也就是延时一小时。
vTaskDelay是不管什么时候调用,都延时固定的时间。而xTaskDelayUntil则是指定一个时间点,以及延时的时间,不管什么什么时候调用,都只延时到一个时间点。
再举个例子,如果我要ESP32每隔10s执行一个任务,那么我可以在一个死循环里先执行任务再用vTaskDelay延时10s,也可以先执行任务,然后使用xTaskDelayUntil传入任务执行前的时间点以及10s对应的时钟周期数。
如果任务很简单,那么二者几乎没差别。
可如果任务很复杂,执行一个任务就需要花费5s,那么使用vTaskDelay的方案中每次循环的时间就会是5(执行任务)+10(延时),也就是15s。
但使用xTaskDelayUntil的方案则是5(执行任务)+5(延时),正好10s,因为xTaskDelayUntil是按照时间点延时的。
并且xTaskDelayUntil调用结束之前还会自动将参数一给加上参数二的值,也就是可以循环使用。
那么就剩最后一个问题了,我们需要获取到时间点,也就是我程序运行到此刻所花费的时钟周期数。
我们用下面两个函数即可获得。
两个函数差别在于一个是普通使用的,另一个是中断函数内使用的。
接下来给个示例,大家就知道怎么使用了。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void app_main(void){
TickType_t lastTime = xTaskGetTickCount();
while(1){
printf("hello world\r\n");
xTaskDelayUntil(&lastTime,pdMS_TO_TICKS(1000));
}
}
这样就会以一秒为周期打印一次hello world了。
另外这里的延时并不是真正的延时,而是将任务先挂起,等到延时的时间到了再接着执行任务。这里有个好处,那就是不会像裸机延时那样空循环干耗时间,因为FreeRTOS是多任务的,因此当前任务挂起,就可以让CPU去执行其他任务,等到延时时间结束,再接着执行之前的任务,这样可以充分利用CPU的性能。