Chapter 172: System Timer (SYSTIMER) Usage

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the architecture and purpose of the SYSTIMER peripheral.
  • Utilize the esp_timer API for high-precision timekeeping.
  • Obtain the current system time with microsecond resolution using esp_timer_get_time().
  • Implement high-precision interval measurements.
  • Create and manage one-shot and periodic software timers using the esp_timer API, which often leverages SYSTIMER.
  • Recognize which ESP32 variants include the SYSTIMER peripheral and how esp_timer behaves on others.
  • Troubleshoot common issues related to high-resolution timers.

Introduction

In the realm of embedded systems, while general-purpose timers (like those discussed in Chapter 171) offer flexibility for a wide array of tasks, there’s often a need for a dedicated, high-resolution, continuously running system clock. This clock serves as a reliable time base for the operating system, precise event timestamping, and fine-grained scheduling. On newer ESP32 variants, this role is fulfilled by the System Timer (SYSTIMER) peripheral.

The SYSTIMER is designed to provide a monotonic, 64-bit counter that typically runs at a fixed, high frequency (e.g., 16 MHz or 20 MHz, derived from the crystal oscillator). Unlike general-purpose timers that might be stopped, started, or reconfigured by different parts of an application, the SYSTIMER is generally intended to run continuously from boot, providing a stable time reference.

In ESP-IDF, the primary way to interact with this high-resolution timing capability is through the esp_timer API. This API provides an abstraction layer, using SYSTIMER as its backend on supported chips, and falling back to other timer sources (like the LACT/FRC2 timer or RTC timer) on older variants. This chapter will delve into the SYSTIMER itself and, more practically, how to use the esp_timer API for precision timing applications.

Understanding SYSTIMER and the esp_timer API is crucial for tasks requiring microsecond-level accuracy, such as profiling code, synchronizing events, or implementing custom low-level schedulers.

Theory

SYSTIMER Peripheral Architecture

The SYSTIMER peripheral, where available, is a hardware module designed for system-level timekeeping. Its key characteristics typically include:

  • Core Counter: A 64-bit main up-counter. This large width ensures an extremely long overflow period (many thousands of years), making it practically non-overflowing for typical application lifetimes.
  • Clock Source: It’s driven by a fixed, high-frequency clock, usually derived directly from the main crystal oscillator (XTAL_CLK). A common configuration is XTAL_CLK / 2. For a 40 MHz XTAL, this results in a 20 MHz SYSTIMER clock (50 ns resolution). For a 32 MHz XTAL, it would be 16 MHz (62.5 ns resolution). This fixed frequency provides consistent timing.
  • Always-On (Typically): Once started (which is usually done by the system early in the boot process), the SYSTIMER runs continuously.
  • Comparators (Alarms): SYSTIMER modules usually contain a few hardware comparators (often 3). When the main counter’s value matches a comparator’s target value, an alarm event (interrupt) can be triggered.
    • One comparator is typically reserved for the operating system’s tick (e.g., FreeRTOS tick interrupt).
    • The remaining comparators can be used for other system-level purposes or by the esp_timer API for its software timers.
  • Low Power Operation: SYSTIMER is often designed to continue running even when the CPU cores are in light sleep mode, allowing for accurate timekeeping across sleep cycles.

The esp_timer API: The Recommended Interface

While the SYSTIMER is a hardware peripheral, direct register manipulation is generally discouraged for application developers in ESP-IDF. Instead, Espressif provides the esp_timer API (driver/include/esp_timer.h) as a high-level abstraction for high-resolution wall-clock time and software timers.

%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%%
graph TD
    subgraph "Your Application Code"
        A("<b>esp_timer_get_time()</b><br><b>esp_timer_create()</b>");
    end

    subgraph "ESP-IDF Abstraction Layer"
        B("esp_timer API");
    end

    subgraph "Hardware Backend (Chip Dependent)"
        C{"If (ESP32-S3, C3, C6, ...)"};
        D["<b>SYSTIMER</b><br>(Hardware 64-bit counter)"];
        E["<b>LACT / FRC2 or RTC Timer</b><br>(Software 64-bit extension)"];
    end

    A --> B;
    B --> C;
    C -- "True" --> D;
    C -- "False (ESP32, S2, ...)" --> E;

    %% Styling
    classDef app fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef api fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef hardware fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;

    class A app;
    class B api;
    class C decision;
    class D,E hardware;

Key features of esp_timer:

  • Microsecond Resolution: esp_timer_get_time() returns the time since esp_timer initialization (typically close to boot time) as a 64-bit integer in microseconds.
  • Backend Agnostic (to an extent):
    • On ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2, and other newer variants with SYSTIMER, esp_timer uses SYSTIMER as its default backend (CONFIG_ESP_TIMER_IMPL_SYSTIMER).
    • On older variants like the original ESP32 and ESP32-S2 (which lack SYSTIMER), esp_timer uses the LACT (Low Accuracy Timer, also known as FRC2) or RTC timer as a backend (CONFIG_ESP_TIMER_IMPL_LACT or CONFIG_ESP_TIMER_IMPL_RTC). The precision and behavior (especially during sleep) can differ.
  • Software Timers: The esp_timer API allows creation of multiple software timers (one-shot or periodic) that are multiplexed onto the underlying hardware comparators (if SYSTIMER is the backend) or managed in software. These are distinct from FreeRTOS timers (Chapter 15) and offer higher precision. Callbacks from esp_timer run from a high-priority esp_timer task.

Key esp_timer Functions

  • esp_err_t esp_timer_init():
    • Initializes the esp_timer system. This is usually called automatically by the startup code (esp_system_init()). Applications typically don’t need to call this directly unless they have very specific custom startup sequences.
  • int64_t esp_timer_get_time():
    • This is the most commonly used function. It returns the current time as a uint64_t value representing the number of microseconds since esp_timer was initialized.
    • It’s designed to be a low-overhead call, suitable for frequent polling or timestamping.
  • esp_err_t esp_timer_create(const esp_timer_create_args_t* create_args, esp_timer_handle_t* out_handle):
    • Creates a software timer.
    • esp_timer_create_args_t includes:
      • callback: The function to call when the timer expires.
      • arg: An argument to pass to the callback.
      • dispatch_method: Specifies how the callback is dispatched. ESP_TIMER_TASK (default) dispatches via the esp_timer task. ESP_TIMER_ISR attempts to dispatch from ISR context (use with extreme care, only for very short, critical callbacks, and subject to backend capabilities).
      • name: A descriptive name for the timer (useful for debugging).
      • skip_unhandled_events: If true, skip events if the timer task is busy.
    • out_handle: Returns a handle to the created timer.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%%
graph TD
    A(Timer Interrupt Fires) --> B{Dispatch Method?};
    B -- "ESP_TIMER_TASK (Default)" --> C[Event sent to<br><b>esp_timer</b> FreeRTOS task];
    C --> D["Callback runs in a<br>high-priority task context."];
    D --> E["<b>Pros:</b> Safer, can use<br>some FreeRTOS APIs.<br><b>Cons:</b> Higher latency."];
    
    B -- "ESP_TIMER_ISR" --> F[Callback executed<br>directly from ISR];
    F --> G["Callback runs in<br><b>Interrupt Context</b>."];
    G --> H["<b>Pros:</b> Very low latency.<br><b>Cons:</b> Highly restricted.<br>Cannot block or use<br>most FreeRTOS APIs."];

    %% Styling
    classDef startNode fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef goodNode fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;
    classDef badNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;

    class A startNode;
    class B decisionNode;
    class C,D,F,G processNode;
    class E goodNode;
    class H badNode;
  • esp_err_t esp_timer_start_once(esp_timer_handle_t timer, uint64_t timeout_us):
    • Starts a previously created timer to fire once after timeout_us microseconds.
  • esp_err_t esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period_us):
    • Starts a previously created timer to fire periodically, with an interval of period_us microseconds.
  • esp_err_t esp_timer_stop(esp_timer_handle_t timer):
    • Stops a running timer. The timer can be restarted later.
  • esp_err_t esp_timer_delete(esp_timer_handle_t timer):
    • Stops and deletes a timer, freeing its resources.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%%
graph TD
    subgraph "Setup Phase"
        A(Start) --> B{Create Timer};
        B -- esp_timer_create() --> C[Get Timer Handle];
    end

    subgraph "Operational Phase"
        C --> D{Choose Mode};
        D -- "One-Shot" --> E["esp_timer_start_once()"];
        D -- "Periodic" --> F["esp_timer_start_periodic()"];
        E --> G{Wait for Timeout};
        F --> H{Wait for Period};
        H --> I[Callback Fires];
        I --> H;
        G --> J[Callback Fires];
    end

    subgraph "Teardown Phase"
        K(Stop?) --> L{"esp_timer_stop()"};
        L --> M{"esp_timer_delete()"};
        M --> N(End);
    end

    J --> K;
    I --> K;


    %% Styling
    classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;

    class A,N startNode;
    class B,C,E,F,G,H,I,J,L,M processNode;
    class D,K decisionNode;

SYSTIMER vs. GPTimers (Chapter 171)

Feature SYSTIMER (via esp_timer) GPTimers (General Purpose Timers)
Primary Use System-wide high-resolution timekeeping, precise timestamps, esp_timer software timers. Application-specific timing, PWM, event counting, custom periodic interrupts.
Counter 64-bit, typically always running from boot. 64-bit (typically), with configurable start, stop, and direction control.
Resolution Fixed and high (e.g., 50 ns raw). esp_timer_get_time() provides microsecond resolution. Configurable via a prescaler; can range from very high to lower resolutions.
Clock Source Fixed, high-frequency source (e.g., XTAL/2). Selectable clock sources (e.g., APB, XTAL, RTC).
Configuration Minimal direct user configuration; managed by the esp_timer API. Highly configurable: prescaler, alarm, auto-reload, count direction, etc.
Always On Yes, on supported chips after initialization. No, it is controlled by the application (start/stop).
API Focus esp_timer_get_time(), esp_timer_create(), etc. gptimer_new_timer(), gptimer_set_alarm_action(), etc.
Availability Newer ESP32 variants (S3, C3, C6, H2). Available on most or all ESP32 variants.
Sleep Behavior Designed to continue running during light sleep. Behavior varies; timers clocked by APB may stop during sleep.

In essence, use esp_timer_get_time() for getting the current “wall-clock” time with microsecond precision. Use esp_timer_create() for general-purpose software timers when you need callbacks at specific intervals with higher precision than FreeRTOS timers might offer. Use GPTimers when you need direct hardware control over counting, specific alarm values tied to raw counts, complex reload behaviors, or interaction with other peripherals that might use GPTimer events.

Practical Examples

The following examples assume you are working with an ESP32 variant that supports SYSTIMER as the backend for esp_timer (e.g., ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2). If you are on an older variant, the esp_timer API will still work, but its underlying source will be different.

Example 1: Getting Current Time and Basic Timestamping

This example demonstrates how to get the current system time using esp_timer_get_time() and use it for basic timestamping.

1. Project Setup:

Create a new ESP-IDF project or use an existing one.

2. Code (main/main.c):

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_rom_sys.h" // For esp_rom_delay_us

static const char *TAG = "systimer_example_1";

void app_main(void)
{
    // esp_timer_init() is called by system startup code.

    int64_t time_now_us = esp_timer_get_time();
    ESP_LOGI(TAG, "Time since boot: %lld microseconds", time_now_us);

    ESP_LOGI(TAG, "Performing a short delay...");
    int64_t start_time_us = esp_timer_get_time();

    // Simulate some work or delay
    esp_rom_delay_us(500 * 1000); // Delay for 500 milliseconds (500,000 microseconds)

    int64_t end_time_us = esp_timer_get_time();
    ESP_LOGI(TAG, "Delay finished.");

    int64_t duration_us = end_time_us - start_time_us;
    ESP_LOGI(TAG, "Start time: %lld us", start_time_us);
    ESP_LOGI(TAG, "End time:   %lld us", end_time_us);
    ESP_LOGI(TAG, "Measured duration: %lld microseconds (expected ~500000 us)", duration_us);

    // Loop to show time progression
    for (int i = 0; i < 5; i++) {
        vTaskDelay(pdMS_TO_TICKS(1000)); // Delay 1 second using FreeRTOS ticks
        time_now_us = esp_timer_get_time();
        ESP_LOGI(TAG, "Current time: %lld us", time_now_us);
    }
}

3. CMakeLists.txt (main/CMakeLists.txt):

Ensure your CMakeLists.txt includes esp_timer (usually pulled in by esp_system or freertos). Explicitly:

Plaintext
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "."
                    REQUIRES esp_timer esp_log freertos)

4. Build Instructions:

  1. Open VS Code.
  2. Ensure your ESP-IDF project is selected and configured for your target ESP32 variant.
  3. Build the project (Espressif IDF: Build your project).

5. Run/Flash/Observe Steps:

  1. Flash the built firmware to your ESP32 (Espressif IDF: Flash your project (UART)).
  2. Open the ESP-IDF Monitor (Espressif IDF: Monitor your device).
  3. Observe the logged timestamps and the measured duration, which should be close to 500,000 µs. You’ll also see the current time printed every second.

Example 2: One-Shot esp_timer

This example demonstrates creating and using a one-shot timer with esp_timer.

%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%%
stateDiagram-v2
    direction TB
    [*] --> Idle: esp_timer_create()
    
    Idle --> Running: esp_timer_start_once()
    Running --> Fired: Timeout expires
    Fired --> Idle: Callback completes
    
    Running --> Idle: esp_timer_stop()
    Idle --> [*]: esp_timer_delete()
    Running --> [*]: esp_timer_delete()
    Fired --> [*]: esp_timer_delete()

    note right of Fired : The timer's callback function\nis executed at this point.

1. Code (main/main.c):

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_log.h"

static const char *TAG = "systimer_example_2";

static void oneshot_timer_callback(void* arg)
{
    int64_t time_now_us = esp_timer_get_time();
    char* timer_name = (char*) arg;
    ESP_LOGI(TAG, "One-shot timer '%s' fired at %lld us", timer_name, time_now_us);
}

void app_main(void)
{
    ESP_LOGI(TAG, "Creating and starting a one-shot esp_timer...");

    esp_timer_handle_t oneshot_timer_handle;
    esp_timer_create_args_t oneshot_timer_args = {
            .callback = &oneshot_timer_callback,
            .arg = "MyOneShotTimer", // Argument for the callback
            .name = "one-shot"       // Descriptive name for debugging
            // .dispatch_method = ESP_TIMER_TASK, // Default
            // .skip_unhandled_events = false,    // Default
    };

    ESP_ERROR_CHECK(esp_timer_create(&oneshot_timer_args, &oneshot_timer_handle));

    // Start the timer to fire once after 2.5 seconds (2,500,000 microseconds)
    uint64_t timeout_us = 2500 * 1000;
    ESP_ERROR_CHECK(esp_timer_start_once(oneshot_timer_handle, timeout_us));
    ESP_LOGI(TAG, "One-shot timer started. Will fire in %.2f seconds.", (double)timeout_us / 1000000.0);

    // Keep main task alive to allow timer to fire
    // In a real app, other tasks would be running.
    vTaskDelay(pdMS_TO_TICKS(5000)); // Wait for 5 seconds

    ESP_LOGI(TAG, "Deleting the one-shot timer.");
    ESP_ERROR_CHECK(esp_timer_stop(oneshot_timer_handle)); // Good practice to stop before delete, though delete might imply stop.
    ESP_ERROR_CHECK(esp_timer_delete(oneshot_timer_handle));

    ESP_LOGI(TAG, "Example finished.");
}

2. Build, Flash, and Observe:

Follow standard build/flash steps. You should see the log indicating the timer has started, and then approximately 2.5 seconds later, the “One-shot timer ‘MyOneShotTimer’ fired” message.

Example 3: Periodic esp_timer

This example demonstrates creating and using a periodic timer with esp_timer.

1. Code (main/main.c):

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_log.h"

static const char *TAG = "systimer_example_3";
static int periodic_fire_count = 0;

static void periodic_timer_callback(void* arg)
{
    periodic_fire_count++;
    int64_t time_now_us = esp_timer_get_time();
    ESP_LOGI(TAG, "Periodic timer fired! Count: %d, Time: %lld us", periodic_fire_count, time_now_us);

    esp_timer_handle_t* timer_handle_ptr = (esp_timer_handle_t*) arg;
    if (periodic_fire_count >= 5) {
        ESP_LOGI(TAG, "Periodic timer fired 5 times. Stopping and deleting it.");
        ESP_ERROR_CHECK(esp_timer_stop(*timer_handle_ptr));
        ESP_ERROR_CHECK(esp_timer_delete(*timer_handle_ptr));
        ESP_LOGI(TAG, "Periodic timer deleted.");
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Creating and starting a periodic esp_timer...");

    // Note: We pass the address of the handle to the callback so it can delete itself.
    // This is one way; another is to use a global or signal another task to delete it.
    static esp_timer_handle_t periodic_timer_handle; // Static or global if passed to callback for deletion

    esp_timer_create_args_t periodic_timer_args = {
            .callback = &periodic_timer_callback,
            .arg = &periodic_timer_handle, // Pass timer handle address as argument
            .name = "periodic"
    };

    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer_handle));

    // Start the timer to fire every 750 milliseconds (750,000 microseconds)
    uint64_t period_us = 750 * 1000;
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer_handle, period_us));
    ESP_LOGI(TAG, "Periodic timer started. Will fire every %.3f seconds.", (double)period_us / 1000000.0);

    // Keep main task alive. The timer callback will eventually stop and delete the timer.
    // If the timer ran indefinitely, this task would need to run indefinitely too.
    while(periodic_fire_count < 5) {
        vTaskDelay(pdMS_TO_TICKS(100));
    }
    ESP_LOGI(TAG, "Main task observed 5 firings. Example finished.");
}

2. Build, Flash, and Observe:

Build and flash. You will see the “Periodic timer fired!” message approximately every 750 milliseconds. After 5 firings, the timer callback will stop and delete the timer, and the program will conclude.

Variant Notes

The availability and underlying implementation of high-resolution timing via esp_timer vary significantly across ESP32 variants. This is critical to understand for portability and performance expectations.

ESP32 Variant Family SYSTIMER Peripheral Default esp_timer Backend Key Characteristics
ESP32-S3, C3, C6, H2 Yes SYSTIMER Provides true hardware 64-bit microsecond timing. Runs from XTAL clock (e.g., 20 MHz from 40 MHz XTAL). Resilient across light sleep modes.
ESP32, ESP32-S2 No LACT or RTC Lacks the dedicated SYSTIMER hardware. esp_timer relies on other timers like LACT (FRC2) or RTC, which have different precision and sleep behavior.
  • ESP32, ESP32-S2:
    • Do NOT have the SYSTIMER peripheral.
    • On these chips, esp_timer typically uses the LACT (Low Accuracy Timer / FRC2) or RTC timer as its backend.
    • The LACT is a 32-bit timer, and esp_timer extends it to 64-bit in software. Its resolution is typically based on the APB clock (e.g., 80 MHz), so it can still offer microsecond-level timing, but its characteristics (e.g., behavior in deep sleep) are different from SYSTIMER.
    • RTC timer has lower resolution but can run during deep sleep.
    • The exact precision and behavior during sleep modes might differ compared to SYSTIMER-backed implementations.
  • ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2:
    • HAVE the SYSTIMER peripheral.
    • esp_timer uses SYSTIMER as its default high-resolution backend.
    • SYSTIMER typically runs from XTAL_CLK / 2. For example:
      • If XTAL is 40 MHz, SYSTIMER clock is 20 MHz (50 ns per tick).
      • If XTAL is 32 MHz (e.g., some ESP32-C3 modules), SYSTIMER clock is 16 MHz (62.5 ns per tick).
    • This provides true hardware-based 64-bit microsecond timing and is designed to work reliably across light sleep modes.

Key Implications:

  • Code Portability: The esp_timer API itself is portable. Code written using esp_timer_get_time()esp_timer_create(), etc., will compile and run on all variants.
  • Performance & Precision: The actual precision and overhead of esp_timer_get_time() might differ slightly. SYSTIMER is generally optimized for this.
  • Sleep Mode Behavior: Timers based on SYSTIMER are often more resilient across light sleep states. If using esp_timer on older chips, check documentation for behavior if deep sleep or extensive light sleep is involved.
  • Kconfig: The CONFIG_ESP_TIMER_IMPL option in sdkconfig shows which backend is active. For newer chips, it defaults to SYSTIMER.

Tip: Always verify the SYSTIMER clock frequency for your specific variant and XTAL frequency by checking its Technical Reference Manual (TRM). The esp_timer API abstracts this into microseconds, but understanding the underlying hardware tick rate can be useful for advanced debugging or performance analysis.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Blocking in a callback Other timers are delayed or don’t fire. System seems unresponsive. Watchdog timer might be triggered. Solution: Keep callbacks extremely short. Offload heavy work to a dedicated FreeRTOS task using a queue or semaphore to signal it.
Forgetting to delete timers Memory leak over time. Application may eventually crash from resource exhaustion. Solution: Ensure every call to esp_timer_create() has a matching esp_timer_delete() when the timer is no longer needed.
Using vTaskDelay in a callback The entire esp_timer task is blocked, delaying all other software timers. This is a severe form of blocking. Solution: Never use FreeRTOS blocking delays inside a timer callback. Use one-shot timers to schedule future actions if needed.
Confusing µs and ms Timer fires 1000x too fast or 1000x too slow. A 1-second timer set to 1000 will fire in 1ms. Solution: Remember all esp_timer API calls use microseconds. Use 1000 * 1000 for one second.
Modifying a timer from another task without a handle Unable to stop or delete a timer created in a different scope or task. Solution: Store the esp_timer_handle_t in a location accessible to the tasks that need to control the timer (e.g., a global variable or passed in a struct).
Assuming timer portability without checking backend Code relies on SYSTIMER’s light sleep resilience but is run on an ESP32 where the timer stops in sleep. Solution: Abstract behavior with the esp_timer API, but if sleep is critical, check the docs for the specific backend (SYSTIMER vs LACT) on your target chip.

Exercises

  1. High-Frequency Event Timestamping:
    • Write an application that simulates generating 10 “events” in rapid succession within a loop (e.g., by calling esp_rom_delay_us() for a very short, slightly varying period like 50-100µs between them).
    • For each event, get a timestamp using esp_timer_get_time().
    • After all events, print each timestamp and the delta time (duration in µs) between consecutive events. This will highlight the microsecond precision.
  2. esp_timer vs. GPTimer for Short Delays (on compatible chip):
    • Requires a chip with both SYSTIMER (for esp_timer) and GPTimers (e.g., ESP32-S3).
    • Objective: Compare the practical precision/overhead of creating a very short delay using esp_timer_start_once() versus configuring a GPTimer for a one-shot alarm.
    • Part A: Implement a 200 µs delay using esp_timer_start_once(). The callback should log the actual time elapsed using esp_timer_get_time().
    • Part B: Implement a 200 µs delay using a GPTimer configured for a one-shot alarm. The GPTimer ISR should log the actual time elapsed (perhaps by reading another free-running GPTimer or esp_timer_get_time() if careful about ISR context).
    • Compare the setup complexity and observed accuracy. (Note: This is an advanced exercise).
  3. Microsecond Accurate LED Blink Scheduler:
    • Create two esp_timer instances.
    • Timer 1: Periodic, fires every 150.250 milliseconds (150250 µs). Its callback toggles LED1.
    • Timer 2: Periodic, fires every 300.500 milliseconds (300500 µs). Its callback toggles LED2.
    • Observe the LEDs. They should blink at precise, slightly offset rates, demonstrating the microsecond-level scheduling capability of esp_timer. This is more precise than what vTaskDelay (which is tick-based) can typically achieve for such fractional millisecond timings.

Summary

  • The SYSTIMER peripheral (on newer ESP32 variants like S3, C3, C6, H2) provides a high-resolution, 64-bit, always-on counter, typically clocked from XTAL_CLK / 2.
  • The esp_timer API is the recommended way to access high-resolution time and create software timers in ESP-IDF. It uses SYSTIMER as a backend on supported chips.
  • esp_timer_get_time() returns the time in microseconds since initialization as a uint64_t, offering a precise and non-overflowing timestamp.
  • esp_timer_create()esp_timer_start_once()esp_timer_start_periodic()esp_timer_stop(), and esp_timer_delete() are used to manage software timers with microsecond-level precision.
  • Older ESP32 variants (original ESP32, ESP32-S2) lack SYSTIMER; esp_timer on these chips uses LACT or RTC timers as backends, which may have different characteristics.
  • esp_timer callbacks should be kept short and non-blocking; defer lengthy operations to dedicated tasks.
  • SYSTIMER/esp_timer is ideal for precise timestamping, profiling, and scheduling tasks with finer granularity than standard FreeRTOS ticks.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top