Chapter 171: Advanced Timer Groups (TIMG)

Chapter Objectives

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

  • Understand the architecture and capabilities of the ESP32 Timer Group (TIMG) peripherals.
  • Configure General Purpose Timers (GPTimers) for various timing applications.
  • Implement periodic interrupt generation using GPTimers.
  • Measure code execution time or intervals with high precision.
  • Utilize one-shot timer functionalities.
  • Understand the differences in TIMG modules across various ESP32 variants.
  • Identify and troubleshoot common issues when working with GPTimers.

Introduction

In embedded systems, precise timing is paramount. Whether it’s for scheduling tasks, generating waveforms, measuring event durations, or implementing communication protocols, the ability to control and measure time accurately is a fundamental requirement. The ESP32 series of microcontrollers is equipped with versatile Timer Group (TIMG) peripherals, each containing one or more General Purpose Timers (GPTimers) and a Watchdog Timer (WDT).

While FreeRTOS provides software timer services (covered in Chapter 15), hardware timers offer higher precision, lower overhead for certain tasks, and direct interaction with hardware events. This chapter focuses on the advanced capabilities of the GPTimers within the TIMG modules, exploring how to harness them for precision timing, interval measurement, and generating periodic events. We will be using the driver/gptimer.h API, which is the standard for ESP-IDF v5.x.

Understanding and effectively utilizing these hardware timers will unlock a new level of control and responsiveness in your ESP32 applications, from simple blinking LEDs with precise cadence to complex real-time control systems.

Theory

Each ESP32 variant contains one or more Timer Group (TIMG) peripherals. Typically, a TIMG module consists of:

  1. General Purpose Timers (GPTimers): These are highly configurable timers that can be used for a wide range of timing tasks. Most ESP32 variants have two TIMGs (TIMG0 and TIMG1). The number of GPTimers per TIMG can vary (see Variant Notes).
  2. Watchdog Timers (WDT): Each TIMG also includes a WDT. These are crucial for system stability, ensuring that the system can recover from software hangs or unexpected states. While WDTs are part of the TIMG, their detailed implementation is covered in Chapter 243: Watchdog Timers Implementation. This chapter will focus on GPTimers.

GPTimer Architecture

A General Purpose Timer (GPTimer) is a sophisticated piece of hardware with several key components:

  • Counter: The core of the timer is a 64-bit up/down counter. This large width allows for very long timing intervals without overflow, even at high clock frequencies.
  • Clock Source: The counter is driven by a clock signal. Common sources include:
    • GPTIMER_CLK_SRC_APB: The APB bus clock (typically 80 MHz, but can vary). This is a common choice for many applications.
    • GPTIMER_CLK_SRC_XTAL: The main crystal oscillator clock (typically 40 MHz).
    • Other sources like GPTIMER_CLK_SRC_RTC (slow RTC clock) might be available on some variants for low-power scenarios.
  • Prescaler (Divider): The input clock can be divided by a programmable prescaler (16-bit, allowing division from 1 to 65536). This allows you to reduce the clock frequency fed to the main counter, thereby adjusting the timer’s resolution and maximum period. The actual resolution of the timer is (Source Clock Frequency) / Prescaler. The gptimer driver allows you to specify a desired resolution_hz, and it will calculate the best prescaler value.
  • Count Direction: Timers can be configured to count up or count down.
  • Alarm: GPTimers can be configured to trigger an event (typically an interrupt) when the counter reaches a specific pre-set “alarm” value.
  • Auto-Reload: When an alarm occurs, the timer can be configured to automatically reload the counter with a specific value (or 0 if counting up from 0). This is essential for generating periodic interrupts. If auto-reload is disabled, the timer might act as a one-shot timer or continue counting past the alarm value.
  • Interrupt Generation: Timers can generate CPU interrupts upon various events, most commonly the alarm event. Interrupts can be level-triggered or edge-triggered. The gptimer driver uses an alarm callback mechanism.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937"}}}%%
graph TD
    subgraph TIMG Module
        subgraph GPTimer
        direction TB
            A[Clock Source <br> e.g., APB, XTAL] --> B{Prescaler / Divider};
            B --> C[64-bit Up/Down Counter];
            C --> D{Comparator};
            E[Alarm Value] --> D;
            D -- Alarm Match --> F[Interrupt Logic];
            F --> G[CPU Interrupt];
            C -- Reload Path --> H((Auto-Reload));
            H -- "On Alarm" --> C;
        end
    end

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

    class A,E primary;
    class B,C,F,H process;
    class D decision;
    class G success;

Key Concepts for GPTimer Configuration

  1. Resolution (resolution_hz): This is the effective frequency at which the timer’s main counter ticks. It’s determined by the chosen clock source and the internal divider (prescaler) value. For example, if the APB clock is 80 MHz and you desire a 1 MHz resolution (1-microsecond tick), the driver will attempt to set a prescaler of 80. The achievable resolution depends on the source clock and the 16-bit range of the prescaler.
  2. Alarm Value (alarm_count): This is the value the counter must reach to trigger an alarm event. If the timer resolution is 1 MHz, an alarm_count of 1,000,000 would correspond to 1 second.
  3. Reload Value (reload_count): When auto_reload_on_alarm is enabled, the counter will be set to this value after an alarm event. For a periodic timer counting up from 0, reload_count is typically set to 0.
  4. Callbacks (gptimer_event_callbacks_t): The modern gptimer driver uses a callback system to handle events, primarily the alarm event (on_alarm). You register a function that will be executed when the timer alarm occurs.
Parameter Struct Description
resolution_hz gptimer_config_t The desired ticking frequency of the timer’s counter in Hz. The driver calculates the best prescaler to achieve this.
clk_src gptimer_config_t The clock source for the timer (e.g., GPTIMER_CLK_SRC_DEFAULT). Determines the base frequency before the prescaler.
direction gptimer_config_t The counting direction, either GPTIMER_COUNT_UP or GPTIMER_COUNT_DOWN.
alarm_count gptimer_alarm_config_t The counter value at which the alarm event will be triggered.
reload_count gptimer_alarm_config_t The value to which the counter is reset after an alarm. Usually 0 for periodic timers counting up.
auto_reload_on_alarm gptimer_alarm_config_t If true, the timer automatically reloads its value (from reload_count) and continues, enabling periodic behavior. If false, it’s a one-shot alarm.
on_alarm gptimer_event_callbacks_t A pointer to the callback function (ISR) that will be executed when the alarm event occurs.

Precision Timing and Interval Measurement

  • Precision Timing: By carefully selecting the clock source, configuring the prescaler (via resolution_hz), and setting the alarm value, you can generate events (like interrupts) with high temporal precision. This is fundamental for tasks like controlling motors, sampling sensors at exact intervals, or managing communication protocol timings.
  • Interval Measurement: GPTimers can be used to measure the duration of events or code execution.
    • Code Execution Time: Read the timer’s raw count value (gptimer_get_raw_count()) before and after a block of code. The difference in counts, multiplied by the timer’s tick period (1 / resolution_hz), gives the execution time. For this, the timer should be configured to free-run without alarms or auto-reloads affecting the measurement window.
    • External Event Interval: While PCNT (Pulse Counter) or GPIO-triggered captures are often more specialized for external pulse width/period measurement, you can use GPTimers in conjunction with GPIO interrupts. For instance, a GPIO ISR for a rising edge could record the timer’s value, and another for a falling edge could record it again, allowing calculation of pulse width.

What is a Semaphore? (Brief Refresher)

While not directly part of the TIMG peripheral, understanding semaphores is crucial when dealing with interrupts and shared resources, which often arise with timer usage. As covered in Chapter 13, a semaphore is a synchronization primitive used to control access to a shared resource or to signal between tasks. In the context of timer ISRs, a semaphore might be “given” or “signaled” from the ISR to wake up a higher-priority task that was “taking” or “waiting” on it. This allows minimal processing in the ISR itself, deferring longer work to a task context.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937"}}}%%
sequenceDiagram
    participant ISR as Timer ISR
    participant RTOS as FreeRTOS Task
    
    Note over ISR, RTOS: Task is pending on a semaphore
    RTOS->>RTOS: xSemaphoreTake(semaphore, portMAX_DELAY)
    
    loop Timer Fires
        ISR->>ISR: Alarm Event Occurs
        Note over ISR: Minimal Processing Only!
        ISR->>RTOS: xSemaphoreGiveFromISR(semaphore)
        Note right of RTOS: Task is unblocked
        RTOS->>RTOS: Perform heavy lifting<br>(e.g., logging, I/O, calculations)
    end

Timer Handles (gptimer_handle_t)

In ESP-IDF v5.x, when you create a new timer instance using gptimer_new_timer(), you receive a gptimer_handle_t. This handle is an opaque pointer that uniquely identifies that timer instance. You must pass this handle to all subsequent functions that operate on that specific timer (e.g., gptimer_set_alarm_action()gptimer_start()gptimer_stop()).

Practical Examples

Let’s explore some practical applications of GPTimers. Ensure you have your ESP-IDF v5.x environment set up with VS Code and the Espressif IDF extension.

Example 1: Periodic Interrupt Generation

This example demonstrates how to configure a GPTimer to generate an interrupt periodically, for instance, every 1 second. We’ll log a message from the ISR callback.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937"}}}%%
graph TD
    subgraph "Periodic Timer Setup"
        A(Start) --> B["Define <b>gptimer_config_t</b><br>Set resolution, direction, etc."];
        B --> C["<b>gptimer_new_timer()</b><br>Create timer instance, get handle"];
        C --> D["Define <b>on_alarm</b> callback<br><i>IRAM_ATTR bool my_cb(...)</i>"];
        D --> E["<b>gptimer_register_event_callbacks()</b><br>Link callback to timer"];
        E --> F["Define <b>gptimer_alarm_config_t</b><br>Set alarm_count, reload_count,<br><b>auto_reload_on_alarm = true</b>"];
        F --> G["<b>gptimer_set_alarm_action()</b><br>Configure the alarm"];
        G --> H["<b>gptimer_enable()</b>"];
        H --> I["<b>gptimer_start()</b>"];
        I --> J{Timer Running...};
        J -- "Counter == alarm_count" --> K["<b>on_alarm</b> callback runs<br>Counter reloads to 0"];
        K --> J;
    end

    %% Styling
    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    
    class A primary;
    class B,C,D,E,F,G,H,I process;
    class J decision;
    class K success;

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 "driver/gptimer.h"
#include "esp_log.h"

static const char *TAG = "periodic_timer_example";

// ISR Callback function
// This function will be called from an ISR context when the timer alarm fires.
// It should return true if a high-priority task has been woken by this interrupt.
static bool IRAM_ATTR timer_on_alarm_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
    // A high priority task has been woken up by this timer event
    BaseType_t high_task_awoken = pdFALSE;

    // For demonstration, just log. In a real application, you might signal a task.
    ESP_DRAM_LOGI(TAG, "Timer Alarm! Count: %lld", edata->count_value);

    // You could increment a counter or signal a task here
    // For example: xSemaphoreGiveFromISR(my_semaphore_handle, &high_task_awoken);

    // Return whether a task was woken. This is important for FreeRTOS scheduling.
    return high_task_awoken == pdTRUE;
}

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing GPTimer for periodic interrupt...");

    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, // Use default clock source (usually APB clock)
        .direction = GPTIMER_COUNT_UP,
        .resolution_hz = 1 * 1000 * 1000, // 1 MHz resolution (1 tick = 1 microsecond)
        .intr_shared = false, // Set to true if you want to share the interrupt source (not common for simple timers)
    };
    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));

    ESP_LOGI(TAG, "Timer created with resolution %lu Hz", timer_config.resolution_hz);

    gptimer_event_callbacks_t cbs = {
        .on_alarm = timer_on_alarm_cb, // Register the alarm callback
    };
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, NULL)); // No user context passed for this example

    ESP_LOGI(TAG, "Enabling timer interrupt...");
    ESP_ERROR_CHECK(gptimer_enable(gptimer));

    gptimer_alarm_config_t alarm_config = {
        .alarm_count = 1 * 1000 * 1000, // Alarm target: 1,000,000 ticks (1 second at 1MHz resolution)
        .reload_count = 0,              // Counter will reload to 0 on alarm
        .auto_reload_on_alarm = true,   // Enable auto-reload for periodic behavior
    };
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config));

    ESP_LOGI(TAG, "Starting timer...");
    ESP_ERROR_CHECK(gptimer_start(gptimer));

    // Keep the main task running or do other things
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(10000)); // Keep main alive
    }

    // Cleanup (though this part won't be reached in this example)
    ESP_LOGI(TAG, "Stopping timer...");
    ESP_ERROR_CHECK(gptimer_stop(gptimer));
    ESP_LOGI(TAG, "Disabling timer...");
    ESP_ERROR_CHECK(gptimer_disable(gptimer));
    ESP_LOGI(TAG, "Deleting timer...");
    ESP_ERROR_CHECK(gptimer_del_timer(gptimer));
}

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

Ensure your CMakeLists.txt includes the driver component:

Plaintext
idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "."
                    REQUIRES driver 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. You should see the log message “Timer Alarm! Count: ” printed approximately every second. The count value will be the alarm count.

Tip: The IRAM_ATTR attribute for the callback function timer_on_alarm_cb ensures that the function’s code is placed in IRAM. This is crucial because ISRs must not cause page faults, which can happen if the code resides in flash memory and the flash cache is disabled (e.g., during flash write operations). ESP_DRAM_LOGI is used for logging from ISRs that might run when flash cache is disabled.

Example 2: Measuring Code Execution Time

This example shows how to use a GPTimer to measure the execution time of a small code block.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937"}}}%%
sequenceDiagram
    actor User as app_main
    participant Timer as GPTimer
    
    User->>Timer: Configure and create timer (gptimer_new_timer)
    User->>Timer: Enable and start timer (gptimer_enable, gptimer_start)
    Note over Timer: Counter is now free-running...
    
    User->>Timer: gptimer_get_raw_count()
    activate Timer
    Timer-->>User: Returns start_time
    deactivate Timer

    User->>User: Execute function_to_measure()
    
    User->>Timer: gptimer_get_raw_count()
    activate Timer
    Timer-->>User: Returns end_time
    deactivate Timer
    
    User->>User: Calculate duration:<br>exec_time = end_time - start_time

1. Code (main/main.c):

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

static const char *TAG = "exec_time_example";

// Dummy function whose execution time we want to measure
static void function_to_measure(void)
{
    // Simulate some work
    esp_rom_delay_us(1500); // Delay for 1500 microseconds
}

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing GPTimer for execution time measurement...");

    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT,
        .direction = GPTIMER_COUNT_UP,
        .resolution_hz = 1 * 1000 * 1000, // 1 MHz resolution (1 tick = 1 microsecond)
        .intr_shared = false,
    };
    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));

    // No alarm or callbacks needed for this use case, just enable and start
    ESP_ERROR_CHECK(gptimer_enable(gptimer));
    ESP_ERROR_CHECK(gptimer_start(gptimer));

    uint64_t start_time, end_time, execution_time_us;

    // --- Measurement Start ---
    ESP_ERROR_CHECK(gptimer_get_raw_count(gptimer, &start_time));

    function_to_measure(); // Call the function

    ESP_ERROR_CHECK(gptimer_get_raw_count(gptimer, &end_time));
    // --- Measurement End ---

    if (end_time >= start_time) {
        execution_time_us = end_time - start_time;
    } else {
        // Handle counter overflow if the measurement spans across it
        // (less likely with 64-bit counter and short measurements)
        // For a 1MHz timer, 64-bit counter overflows in ~584,942 years.
        // This case is mostly for completeness if a smaller counter or very long measurement was used.
        uint64_t max_count; // This would be 2^counter_width - 1
        // For simplicity, we assume no overflow for short execution times with 64-bit counter.
        // If you need to get max_count, it's related to the timer's actual bit width,
        // but gptimer_get_raw_count returns uint64_t.
        // The driver internally manages the 64-bit value even if hardware counter is smaller and overflows.
        // So, direct subtraction should generally work unless the measurement is extremely long.
        execution_time_us = (UINT64_MAX - start_time) + end_time + 1;
    }

    ESP_LOGI(TAG, "Measured execution time: %llu microseconds", execution_time_us);

    // Cleanup
    ESP_LOGI(TAG, "Stopping timer...");
    ESP_ERROR_CHECK(gptimer_stop(gptimer));
    ESP_LOGI(TAG, "Disabling timer...");
    ESP_ERROR_CHECK(gptimer_disable(gptimer));
    ESP_LOGI(TAG, "Deleting timer...");
    ESP_ERROR_CHECK(gptimer_del_timer(gptimer));
}

2. Build, Flash, and Observe:

Follow the same build and flash steps as Example 1. In the monitor, you should see the measured execution time, which should be slightly above 1500 microseconds due to function call overhead and reading the timer.

Warning: When measuring very short execution times, the overhead of calling gptimer_get_raw_count() itself can become significant. For extremely fine-grained profiling, other techniques or specialized hardware (like logic analyzers toggling GPIOs) might be necessary. However, for many application-level code blocks, this method is quite effective.

Example 3: One-Shot Timer

This example configures a timer to trigger an alarm only once after a specified delay.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#DBEAFE", "primaryTextColor": "#1E40AF", "primaryBorderColor": "#2563EB", "lineColor": "#6B7280", "textColor": "#1F2937"}}}%%
graph TD
    subgraph "One-Shot Timer Setup"
        A(Start) --> B["Configure & Create Timer<br><i>gptimer_new_timer()</i>"];
        B --> C["Register <b>on_alarm</b> Callback<br><i>gptimer_register_event_callbacks()</i>"];
        C --> D["Define <b>gptimer_alarm_config_t</b><br>Set alarm_count<br><b>auto_reload_on_alarm = false</b>"];
        D --> E["<b>gptimer_set_alarm_action()</b>"];
        E --> F["<b>gptimer_enable()</b> & <b>gptimer_start()</b>"];
        F --> G{Timer Running...};
        G -- "Counter == alarm_count" --> H["<b>on_alarm</b> callback runs once"];
        H --> I(Timer Stops/Halts at Alarm);
    end

    %% Styling
    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class A primary;
    class B,C,E,F process;
    class D check;
    class G decision;
    class H success;
    class I success;

1. Code (main/main.c):

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

static const char *TAG = "one_shot_timer_example";

static bool IRAM_ATTR one_shot_alarm_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
    ESP_DRAM_LOGI(TAG, "One-shot Timer Alarm Fired! Count: %lld", edata->count_value);
    
    // Optionally, stop or disable the timer from ISR if it's truly one-shot and no longer needed
    // gptimer_stop(timer); // Be careful with API calls from ISR, check documentation.
    // It's often better to signal a task to do this.
    // Since auto-reload is false, it won't fire again unless reconfigured and restarted or alarm value is set again.

    return pdFALSE; // No high-priority task woken
}

void app_main(void)
{
    ESP_LOGI(TAG, "Initializing GPTimer for one-shot alarm...");

    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT,
        .direction = GPTIMER_COUNT_UP,
        .resolution_hz = 1 * 1000 * 1000, // 1 MHz resolution
        .intr_shared = false,
    };
    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));

    gptimer_event_callbacks_t cbs = {
        .on_alarm = one_shot_alarm_cb,
    };
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, NULL));
    ESP_ERROR_CHECK(gptimer_enable(gptimer));

    gptimer_alarm_config_t alarm_config = {
        .alarm_count = 2 * 1000 * 1000, // Alarm after 2 seconds
        .reload_count = 0,              // Not used as auto_reload is false
        .auto_reload_on_alarm = false,  // IMPORTANT: Disable auto-reload for one-shot
    };
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config));

    ESP_LOGI(TAG, "Starting one-shot timer (2 seconds)...");
    ESP_ERROR_CHECK(gptimer_start(gptimer));

    // Wait for a bit longer than the alarm to see it fire
    vTaskDelay(pdMS_TO_TICKS(5000));

    ESP_LOGI(TAG, "Timer should have fired. Cleaning up.");
    // Cleanup
    ESP_ERROR_CHECK(gptimer_stop(gptimer));
    ESP_ERROR_CHECK(gptimer_disable(gptimer));
    ESP_ERROR_CHECK(gptimer_del_timer(gptimer));
    ESP_LOGI(TAG, "One-shot timer example finished.");
}

2. Build, Flash, and Observe:

Build and flash. In the monitor, you should see the “One-shot Timer Alarm Fired!” message once, approximately 2 seconds after the timer starts.

Variant Notes

The Timer Group (TIMG) peripherals and their General Purpose Timers (GPTimers) are present across most ESP32 variants, but there can be differences in the number of TIMGs and GPTimers per TIMG.

ESP32 Variant Timer Groups (TIMG) General Purpose Timers (GPTimers) Watchdog Timers (WDT)
ESP32 2 4 (2 per group) 2 (1 per group)
ESP32-S2 2 2 (1 per group) 2 (1 per group)
ESP32-S3 2 4 (2 per group) 2 (1 per group)
ESP32-C3 2 2 (1 per group) 2 (1 per group)
ESP32-C6 2 2 (1 per group) 2 (1 per group)
ESP32-H2 2 2 (1 per group) 2 (1 per group)
  • ESP32:
    • 2 Timer Groups (TIMG0, TIMG1).
    • Each TIMG has 2 x 64-bit general-purpose timers and 1 Watchdog Timer.
    • Total: 4 GPTimers, 2 WDTs.
  • ESP32-S2:
    • 2 Timer Groups (TIMG0, TIMG1).
    • Each TIMG has 1 x 64-bit general-purpose timer and 1 Watchdog Timer.
    • Total: 2 GPTimers, 2 WDTs.
  • ESP32-S3:
    • 2 Timer Groups (TIMG0, TIMG1).
    • Each TIMG has 2 x 64-bit general-purpose timers and 1 Watchdog Timer.
    • Total: 4 GPTimers, 2 WDTs.
  • ESP32-C3 (RISC-V):
    • 2 Timer Groups (TIMG0, TIMG1).
    • Each TIMG has 1 x 64-bit general-purpose timer (referred to as TIMG_T0_OUT_INT for TIMG0 and TIMG_T1_OUT_INT for TIMG1 in some docs, but conceptually one GPTimer per group for driver purposes) and 1 Watchdog Timer.
    • Total: 2 GPTimers, 2 WDTs. The gptimer driver abstracts these.
  • ESP32-C6 (RISC-V):
    • 2 Timer Groups (TIMG0, TIMG1).
    • Each TIMG has 1 x 64-bit general-purpose timer and 1 Watchdog Timer.
    • Total: 2 GPTimers, 2 WDTs.
  • ESP32-H2 (RISC-V):
    • 2 Timer Groups (TIMG0, TIMG1).
    • Each TIMG has 1 x 64-bit general-purpose timer and 1 Watchdog Timer.
    • Total: 2 GPTimers, 2 WDTs.

Key Considerations:

  • Timer Identification: When using the gptimer driver, you don’t usually specify TIMG0_T0 directly. The driver allocates an available timer. If you need a specific hardware timer, you might need to delve into lower-level configurations or ensure other components are not using it. However, for most applications, letting the driver manage this is fine.
  • Clock Sources: While GPTIMER_CLK_SRC_DEFAULT (often APB clock) and GPTIMER_CLK_SRC_XTAL are common, the exact frequencies or availability of other sources (like GPTIMER_CLK_SRC_RTC) might vary slightly. Always refer to the Technical Reference Manual (TRM) for your specific ESP32 variant for the most accurate details on clocking. The gptimer_config_t::resolution_hz field in the driver helps abstract some of this by allowing you to request a target resolution.
  • API Consistency: The driver/gptimer.h API aims to provide a consistent interface across variants. However, underlying hardware capabilities (like the number of available timers) will differ.

Tip: Always consult the official Espressif Technical Reference Manual (TRM) for your specific ESP32 variant to understand the precise hardware capabilities and register details of the TIMG modules. The ESP-IDF documentation for the GPTimer driver is also an essential resource.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Periodic timer fires only once. The timer interrupt is seen in the logs once, then never again. Ensure auto_reload_on_alarm is set to true in your gptimer_alarm_config_t struct.
Guru Meditation Error (Crash) when interrupt fires. The device panics and reboots when the timer alarm is triggered. Often related to an IllegalInstruction or LoadProhibited error. The ISR callback function is likely not in IRAM. Add the IRAM_ATTR attribute before the function definition. Example: static bool IRAM_ATTR timer_on_alarm_cb(…)
Timer runs at the wrong speed. Interrupts occur much faster or slower than the intended period (e.g., expecting 1s, getting 12.5ms). Your resolution_hz might be incorrect. The default clock source is often 80MHz (APB_CLK). If you set resolution_hz to 1, you’re asking for a 1Hz tick, not a 1µs tick. For a 1µs tick, set it to 1 * 1000 * 1000.
Long operations or blocking calls in an ISR. System becomes unresponsive, other interrupts are missed, or a watchdog timer (WDT) fires and reboots the device. Keep ISRs minimal. Do not use standard printf, vTaskDelay, or other blocking calls. Use ISR-safe functions like xSemaphoreGiveFromISR to signal a high-priority task to perform the work. Use ESP_DRAM_LOGI for logging from an ISR.
Timer doesn’t start. No interrupts ever fire. No errors are logged during setup. You may have forgotten to enable or start the timer. Ensure you call both gptimer_enable() and gptimer_start() after configuration.
gptimer_new_timer() fails. The function returns ESP_ERR_NO_MEM or ESP_ERR_NOT_FOUND. All available hardware timers on the specific ESP32 variant are already in use by other components (e.g., LEDC, I2S, or other timer instances). Check the Variant Notes table for the number of available timers and review your code to see what other peripherals might be consuming them.

Exercises

  1. LED Toggle with GPTimer:
    • Write an application that uses TIMG0_T0 (or any available GPTimer) to toggle an LED connected to a GPIO pin every 250 milliseconds. The ISR callback should be responsible for toggling the GPIO state.
  2. Dynamic Frequency Control:
    • Extend Exercise 1. Implement UART command reception. Allow a user to send a new frequency (e.g., “freq 5” for 5 Hz, meaning a period of 200ms) via UART. Your application should then reconfigure the GPTimer’s alarm settings to match the new frequency for the LED toggle. Be mindful of stopping, reconfiguring, and restarting the timer safely.
  3. Nested Timing Measurement:
    • Configure one GPTimer (Timer A) to generate a periodic interrupt every 500ms.
    • Inside Timer A’s ISR callback, start another GPTimer (Timer B, ensure it’s a different hardware timer or reconfigure carefully if resources are scarce) to measure the execution time of a esp_rom_delay_us(50 * 1000); (50ms delay).
    • Stop Timer B after the delay, read its count, and log the measured duration.
    • This exercise explores using timers for measuring tasks triggered by other timer events.
  4. Simple Task “Watchdog” using GPTimer:
    • Create a FreeRTOS task that performs some simulated work in a loop.
    • Configure a GPTimer with an alarm set to, for example, 2 seconds, and auto_reload_on_alarm = false.
    • In the working task’s loop, periodically “feed” the timer. This “feeding” action should re-arm the timer’s alarm for another 2 seconds (e.g., by calling gptimer_set_alarm_action() again with the same alarm value or by stopping, setting counter to 0, and restarting).
    • If the working task gets stuck or fails to feed the timer within the 2-second window, the timer’s alarm ISR should fire. In the ISR, log an “ERROR: Task watchdog expired!” message.
    • This simulates a basic watchdog mechanism for a specific task.

Summary

  • ESP32 microcontrollers feature Timer Groups (TIMG), each typically containing General Purpose Timers (GPTimers) and a Watchdog Timer.
  • GPTimers are 64-bit up/down counters with configurable clock sources, prescalers (managed via resolution_hz), alarm capabilities, and auto-reload features.
  • The driver/gptimer.h API in ESP-IDF v5.x is used for configuring and controlling GPTimers.
  • Key configuration steps involve gptimer_new_timer()gptimer_register_event_callbacks()gptimer_set_alarm_action()gptimer_enable(), and gptimer_start().
  • GPTimers are essential for precision periodic interrupt generation, one-shot delays, and measuring code execution intervals.
  • ISR callbacks for timers should be IRAM_ATTR and kept short, deferring lengthy processing to tasks.
  • The number of available GPTimers varies across ESP32 variants (e.g., ESP32/S3 have 4, while S2/C3/C6/H2 have 2). Always check the TRM for your specific chip.
  • Common pitfalls include incorrect resolution settings, ISR bugs, improper periodic/one-shot configuration, and forgetting to enable/start the timer.

Further Reading

Leave a Comment

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

Scroll to Top