深入解析FreeRTOS SysTick和任务延时机制

浅析 FreeRTOS SysTick 和任务延时

概述

FreeRTOS 提供的最小时间单元为一个 SysTick,举例:
假设配置 RTOS 的 SysTick 为 100Hz,则 RTOS 能提供的最小时间单位为 1/100 s,即 10ms. 即一个 RTOS 的系统时钟为 10ms.
FreeRTOS 自带了一个 SysTick 计数器,任务调度器启用后,每个 SysTick 发生,该计数器就加一。可以通过下述函数获取任务调度器当前运行了几个 SysTick:

TickType_t xTaskGetTickCount(void)

通常,这个 API 可以用来测试 TaskCode 中的一段代码的运行时间:

TickType_t xLastWakeTime;
xLastWakeTime = xTaskGetTickCount ();
// dosomethings
code();
xLastWakeTime = xTaskGetTickCount () - xLastWakeTime;

上述示例计算 code()共消耗了多少个 SysTick,假设当前的 SysTick 为 上述的 100Hz,当前消耗了 10个 SysTick,则实际消耗时间为 10* 10ms = 100ms.
FreeRTOS 中提供了一个宏来完成 SysTick 到 毫秒单位的转换:

#define pdMS_TO_TICKS( xTimeInMs )    ( ( TickType_t ) ( ( ( TickType_t ) ( xTimeInMs ) * ( TickType_t ) configTICK_RATE_HZ ) / ( TickType_t ) 1000U ) )

将 10 SysTick 转为 ms 单位,使用 pdMS_TO_TICKS(10)即可。

要改变RTOS 的 SysTick 的大小,可以改变宏 configTICK_RATE_HZ的值,在 ESP32 的开发环境中,使用 idf.py menuconfig命令在下述图形界面改变 SysTick 的值即可。

注意,SysTick 决定了 RTOS 的最小时间单位,因此,当 SysTick 为 100Hz 时,最小时间单位为 10ms,比这更小的时间需求,如延时,定时需求将不容易通过 RTOS 的系统接口实现。增大 SysTick 提高时间的精确度诚然可行,但当 SysTick 过大,系统的运算需求和负载(中断切换时间)也将变大,可能导致设备变热和运行问题。

在了解 SysTick 的基础上,我们可以研究 RTOS 两种延时函数的使用和异同:
1)相对延时:

void vTaskDelay(const TickType_t xTicksToDelay) // 参数为要延时的 SysTick 数目

2)绝对延时:

BaseType_t vTaskDelayUntil(TickType_t *const pxPreviousWakeTime, const TickType_t xTimeIncrement) // 参数为要延时起始的 SysTick 时刻,和相对起始时刻的时间间隔

两者都可以实现延时,进入延时后的函数将进入休眠状态,等指定的几个 SysTick 后,任务会被唤醒,重新进入可执行的就绪状态。注意,唤醒后的任务并不一定是立即运行的,只是进入一个就绪状态,只有其优先级足够高,才能被任务调度器赋予其 CPU 使用权,进入运行状态。

两者的不同:

1)传的参数不同。

2)进入休眠状态的时机不一样:

vTaskDelay(2) 在执行时,假设期望在 Tick2 得到执行,但此时 CPU 被其他任务抢占,则实际 vTaskDelay(2) 并没有在 Tick2 得到执行,而是在该任务重庆获取 CPU 后的 Tick4 成功调用 vTaskDelay(2) ,之后延时两个 SysTick,在Tick6 被唤醒。

vTaskDelayUntil(2,2) 则不同,它可以指定计算 SysTick 的起始时刻,指定其开始的 SysTick 为 Tick2 后,其一定在 Tick4 得到唤醒。 因此,后者延时的准确性、可控制性更好。

需求及功能解析

示例延时了两种延时的区别:

1)vTaskDelay():

static void task1_process(void *arg)
{
    static const char *TASK1_TAG = "TASK1";
    TickType_t last_wake_time = 0;
    TickType_t ticks_before_delay = 0;
    ticks_before_delay = xTaskGetTickCount();
    while (1) {
        esp_rom_delay_us(100*1000); // 模拟有其他任务或者系统中断抢占 CPU,导致延迟进入 vtaskdelay()
        task1_flag++;
        vTaskDelay(pdMS_TO_TICKS(1000));
        last_wake_time = xTaskGetTickCount();
        printf("%s: tick used=%dms\r\n", TASK1_TAG, TICKS_TO_MS(last_wake_time-ticks_before_delay));
        ticks_before_delay = last_wake_time;
    }
}

2)vTaskDelayUntil():

static void task2_process(void *arg)
{
    static const char *TASK2_TAG = "TASK2";
    TickType_t ticks_before_delay;;
    TickType_t last_wake_time = 0;
    last_wake_time = xTaskGetTickCount();
    ticks_before_delay = last_wake_time;

    while (1) {
        esp_rom_delay_us(100*1000); // 模拟有其他任务或者系统中断抢占 CPU
        task2_flag++;
        vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(1000)); // 注意,使用后,last_wake_time 的值会被更新为当下的系统 systick.
        printf("%s: tick used=%dms\r\n", TASK2_TAG, TICKS_TO_MS(xTaskGetTickCount() - ticks_before_delay));
        ticks_before_delay = last_wake_time;
    }
    vTaskDelete(NULL);
}

示例解析

示例输出:

This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 294424 bytes
TASK2: tick used=1000ms
TASK1: tick used=1100ms
TASK3: use time=1095764us
TASK2: tick used=1000ms
TASK1: tick used=1100ms
TASK3: use time=1099870us

示例中使用 esp_rom_delay_us(100*1000) 模拟有其他任务或者系统中断抢占 CPU的情况,该函数实际是一个 while 实现的让 CPU 空转指定时间的函数。

当希望实现延时为 1000ms 的延时时。与上述的延时原理一致,使用vTaskDelayUntil() 的 Task2 每次运行的 tick used = 1000ms,即实现了较为准确的周期为 1000ms 的延时执行。而使用 vTaskDelay() 的 Task1 则因为进入睡眠的延时,导致实际延时为1100ms。

最后,Task3 演示了vTaskDelay() 中使用的参数的单位为几个 SysTick,并不是实际的 ms 单位。使用 esp_timer_get_time()可以获得较为准确的系统时间。

讨论

尝试改变 ESP32 的 SysTick,观察系统延时精度及上诉函数返回值的变化。

总结

1)FreeRTOS 提供的最小时间单元为一个 SysTick,使用 xTaskGetTickCount() 可以获得自任务调度器启用后,系统一共运行了几个 SysTick 了。

2)FreeRTOS 提供了两种系统延时函数 vTaskDelay()、vTaskDelayUntil(),两者都可以实现延时,进入延时后的函数将进入休眠状态,等指定的几个 SysTick 后,任务会被唤醒,重新进入可执行的就绪状态。注意,唤醒后的任务并不一定是立即运行的,只是进入一个就绪状态,只有其优先级足够高,才能被任务调度器赋予其 CPU 使用权,进入运行状态。特别注意,两者进入睡眠的时机和真正延时时间上的区别。

3)SysTick 与 时间单位的转换可以使用宏 pdMS_TO_TICKS()TICKS_TO_MS()

资源链接

1)Learning-FreeRTOS-with-esp32 系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)

3)下一篇:FreeRTOS 任务通知浅析

物联沃分享整理
物联沃-IOTWORD物联网 » 深入解析FreeRTOS SysTick和任务延时机制

发表评论