初写FreeRTOS 从入门到精通系列文章之初,笔者只是当作可以随时回顾的学习笔记来写,并没有想到这些偏技术的文章收获了意料之外的阅读量和关注。首先当然很欣喜自己的文章能够得到了读者们的认可,但同时也有种使命感,既期望启迪并与大家共同提高嵌入式开发的技术,因此我会根据个人的体会和读者的反馈不时添加和补充理论篇文章的内容。相信读者通过系列文章只是了解理论的话都会有种意犹未尽的感觉吧。现在,笔者将结合自己的经验和所阅读的书籍内容,通过一些典型的代码来充分展示FreeRTOS操作系统的特性和能力。本人才疏学浅,这个实战篇系列也只是抛砖引玉。
读者须知
在实战篇系列的文章中,笔者的开发平台将采用ESP32平台。ESP32单片机相较传统的单片机多了WI-FI无线网和蓝牙的功能支持,同时具有更高的CPU处理速度和更大的SRAM存储空间,使之更适用于物联网(IoT),移动设备和可穿戴设备的应用场景。ESP32开发板可以使用Arduino IDE简单上手的开发环境,敏捷快速地用于产品原型机开发和功能验证,方便读者快速复现实战篇的内容。并且,ESP32是双核心芯片,可以用于测试验证FreeRTOS在多核心下的支持程度和性能表现。所有ESP32的代码经过简单修改适配都能运用在任意单片机平台上。
关于ESP32更多的内容以及如何搭建开发平台可以参考笔者的文章
奔腾的心:ESP32 IoT学习实战笔记1- 初识ESP32与搭建Arduino IDE开发环境3 赞同 · 0 评论文章编辑
任务调度的概念
FreeRTOS对任务的调度采用基于时间片(time slicing)的方式。时间片,顾名思义,把一段时间等分成了很多个时间段,在每一个时间段保证优先级最高的任务能执行,同时如果几个任务拥有相等的优先级,则它们会轮流使用每个时间段占用CPU资源。调度器会在每个时间片结束的时候通过周期中断(tick interrupt)执行一次,调度器根据设置的抢占式还是合作式模式选择哪个任务在下一个时间片会运行。
时间片的大小由configTICK_RATE_HZ这个参数设置。如果configTICK_RATE_HZ设置为10HZ,则时间片的大小为100ms。configTICK_RATE_HZ的值由应用需求决定,通常设为100HZ(时间片大小相应为10ms)。
任务调度的演示
在上图任务调度的演示中,Kernel表示系统内核即调度程序,Task1和Task2是两个优先级相同的任务。t1到t2是一个时间片,t2到t3是另一个时间片。在每一个时间片快结束的时候,调度程序通过周期中断(tick interrupt)被调用并选择在下一个时间片要执行的任务(红色部分代表调度程序Kernel在运行)。此时因为两个任务的优先级相同,调度程序会让两个任务轮流占用时间片进行运行(蓝色部分代表Task1在运行,绿色部分代表Task2在运行)。
案例一
第一个案例将用于测量时间片(time slicing)的长度和看相同优先级的轮流调度
代码如下,可以直接复制到Arduino中运行
TaskHandle_t Task1;
TaskHandle_t Task2;
#define GPIO 16
#define PRIORITY 1
#define APP_CPU 1static void gpio_on(void *argp) {for (;;) {digitalWrite(GPIO,HIGH);}
}static void gpio_off(void *argp) {for (;;) {digitalWrite(GPIO,LOW);}
}void setup() {pinMode(GPIO,OUTPUT);delay(100);xTaskCreatePinnedToCore(gpio_on,"gpio_on",1024,NULL,PRIORITY,&Task1,APP_CPU);xTaskCreatePinnedToCore(gpio_off,"gpio_off",1024,NULL,PRIORITY,&Task2,APP_CPU);
}void loop() {vTaskDelete(xTaskGetCurrentTaskHandle());
}
本例程创建了两个任务,gpio_on任务用于置IO16口为高,gpio_off任务用于置IO16口为低。ESP32有两个核心CPU0和CPU1, xTaskCreatePinnedToCore函数用把任务指定绑定某个核心运行。最后一个参数APP_CPU为1指定两个任务都在CPU1中运行。vTaskDelete函数用于删除任务,loop()循环中删除自身循环函数用于确保CPU1中只有gpio_on和gpio_off两个任务。因为两个任务的优先级PRIORITY都设置为1,所以调度器会轮流调度两个任务,调度的间隔便是时间片(time slicing)。
我们把IO16口接在示波器,在程序运行时可以关注到如下的波形图
案例一波形图
波形图中间隔为500微秒。可以观察到gpio_on的任务执行时间为1毫秒,gpio_off的任务执行时间为1毫秒,两个任务交替执行,调度器的执行时间可以忽略不计。由此可以推断FreeRTOS在ESP32的Arduino开发环境下时间片为1毫秒,这个又称为滴答周期(tick period)。理论上调度器在1秒至少可以执行1000次任务调度(如果任务在一个时间片内提前结束的话任务调度次数还会更多)
案例二
案例二相较于案例一稍微修改了gpio_on的任务内容,允许在任务内调用调度器直接进行任务切换
TaskHandle_t Task1;
TaskHandle_t Task2;
#define GPIO 16
#define PRIORITY 1
#define APP_CPU 1static void gpio_on(void *argp) {for ( short x=0; x<1000; ++x ){digitalWrite(GPIO,HIGH);}taskYIELD();
}static void gpio_off(void *argp) {for (;;) {digitalWrite(GPIO,LOW);}
}void setup() {pinMode(GPIO,OUTPUT);delay(100);xTaskCreatePinnedToCore(gpio_on,"gpio_on",1024,NULL,PRIORITY,&Task1,APP_CPU);xTaskCreatePinnedToCore(gpio_off,"gpio_off",1024,NULL,PRIORITY,&Task2,APP_CPU);
}void loop() {vTaskDelete(xTaskGetCurrentTaskHandle());
}
案例二中gpio_on在执行1000次循环后通过taskYIELD函数调用调度器直接进行任务切换到gpio_off任务。
观察到的波形图如下
案例二波形图
波形图中间隔为200微秒,可以看到IO16口下降沿到上升沿之间的间隔小于时间片1毫秒。下降沿发生的时刻在gpio_on通过taskYIELD函数切换到gpio_off任务,然后gpio_off会在gpio_on所在时间片的剩余时间内进行执行直到下次时间片开始调度器重新进行调度。由此可以看出来,每个任务无法保证一定拥有一个完整的时间片。当一个任务在时间片内进行任务调度时,剩余相同优先级的任务会通过“Round Robin”轮流调度,直到下一个时间片开始重新调度。这是应该值得关注的一个现象。