ESP32には、Wi-FiやBluetoothだけでなく、高精度な時間制御を実現する**GPTimer(General Purpose Timer)**が搭載されています。
LEDの一定周期点滅やセンサーの定期読み取り、モーター制御など、組み込みシステムでは「決まった時間ごとに処理を実行したい」という場面が数多くあります。
ESP32で開発を始めると、
- GPTimerとは何?
vTaskDelay()との違いは?- ソフトウェアタイマーと何が違うの?
- どのような用途で使うべき?
といった疑問を持つ方も多いでしょう。
この記事では、GPTimerの基本からESP32で利用するメリット、実務での活用方法まで、現役エンジニアの視点で分かりやすく解説します。
GPTimerとは?
GPTimer(General Purpose Timer)は、ESP32に搭載されているハードウェアタイマーです。
CPUとは独立して時間を計測できるため、高精度で安定した周期処理を実現できます。
ソフトウェアだけで時間を管理する場合は、CPUの負荷や他のタスクの影響を受けることがあります。
一方、GPTimerは専用ハードウェアによって時間を管理するため、Wi-Fi通信やBluetooth通信など他の処理が動作していても、一定周期で割り込みを発生させることができます。
そのため、リアルタイム性が求められる組み込み機器では非常によく利用されています。
GPTimerが使われる場面
ESP32では、次のような用途でGPTimerが利用されます。
- LEDの一定周期点滅
- センサーの定期サンプリング
- モーター制御
- PWM以外の周期処理
- 通信タイムアウトの監視
- 一定周期でのデータ送信
- ソフトウェアウォッチドッグの補助
例えば、100msごとに温度センサーを読み取る場合や、1秒ごとにクラウドへデータを送信する場合など、周期が重要になる処理ではGPTimerが適しています。
GPTimerとvTaskDelay()の違い
ESP-IDFでは、一定時間待機する方法としてvTaskDelay()もよく利用されます。
しかし、両者には大きな違いがあります。
GPTimer
- ハードウェアタイマー
- 高精度
- 割り込みを発生できる
- CPU負荷の影響を受けにくい
vTaskDelay()
- FreeRTOSのタスク待機
- タスクを一定時間停止する
- タスクスケジューラの影響を受ける
- 厳密な周期制御には向かない
単純なLED点滅程度であればvTaskDelay()でも十分ですが、周期のばらつきを抑えたい場合やリアルタイム性が重要な処理ではGPTimerを使用することをおすすめします。
ソフトウェアタイマーとの違い
ESP-IDFには、FreeRTOSソフトウェアタイマーも用意されています。
GPTimerとの違いを簡単にまとめると、
ソフトウェアタイマー
- FreeRTOSが管理
- 実装が簡単
- 一般的な周期処理に向く
GPTimer
- ハードウェアが管理
- 高精度
- 割り込み処理が可能
- リアルタイム性が高い
IoT機器のように数百ms〜数秒単位の周期処理であればソフトウェアタイマーでも十分な場合があります。
一方、モーター制御や高精度な計測など、タイミングが重要な用途ではGPTimerが適しています。
ESP32でGPTimerを使用するメリット
ESP32では、ESP-IDFがGPTimer用のドライバを提供しているため、レジスタを直接操作する必要はありません。
主なメリットは次のとおりです。
- マイクロ秒単位の時間管理が可能
- 周期タイマー・ワンショットタイマーの両方に対応
- 割り込み処理を簡単に実装できる
- ESP-IDF APIで容易に制御できる
- CPU負荷の影響を受けにくい
Wi-FiやBluetoothを使用するアプリケーションでも、高精度な時間制御を実現できることがESP32の大きな強みです。
今回使用する開発環境
この記事では、次の環境でGPTimerの動作を確認します。
- ESP-IDF:v5.5
- ターゲット:ESP32-C5
- 開発環境:Visual Studio Code
- タイマー周期:1秒
- LED接続:GPIO8
今回は、GPTimerのアラーム割り込みを利用してLEDを1秒ごとにON/OFFするプログラムを作成します。
LED接続回路
今回はLEDをGPIO8へ接続します。
ESP32-C5
GPIO8 ─── 330Ω ─── LED ─── GND
GPIO8をHighにするとLEDが点灯し、Lowにすると消灯します。
実際の製品では、LEDだけでなくセンサーの周期読み取りや通信処理の開始タイミングなどにも同じ考え方で利用できます。
CMakeLists.txt
GPTimerドライバを使用するため、mainフォルダ内のCMakeLists.txtを次のように設定します。
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES
esp_driver_gptimer
esp_driver_gpio
)
driver/gptimer.hはesp_driver_gptimerコンポーネントで提供されています。
サンプルコード
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/gptimer.h"
#include "esp_err.h"
#include "esp_log.h"
/*
* 使用するGPIO番号です。
*
* 使用するESP32-C5開発ボードに合わせて変更してください。
*/
#define LED_GPIO GPIO_NUM_8
/*
* GPTimerのカウント分解能です。
*
* 1MHzに設定すると、
* 1カウント = 1マイクロ秒になります。
*/
#define GPTIMER_RESOLUTION_HZ 1000000UL
/*
* 1秒周期でアラームを発生させます。
*
* 1カウント = 1マイクロ秒
* 1,000,000カウント = 1秒
*/
#define GPTIMER_ALARM_COUNT 1000000ULL
static const char *TAG = "GPTIMER_SAMPLE";
/*
* GPTimer割り込みから通知するタスクのハンドルです。
*
* 今回はapp_main()を実行しているタスクへ通知します。
*/
static TaskHandle_t s_main_task_handle = NULL;
/**
* GPTimerのアラームコールバックです。
*
* この関数は割り込みコンテキストで実行されます。
* そのため、LED制御やログ出力などはここで行わず、
* タスク通知だけを実行します。
*/
static bool gptimer_alarm_callback(
gptimer_handle_t timer,
const gptimer_alarm_event_data_t *event_data,
void *user_context)
{
(void)timer;
(void)event_data;
TaskHandle_t task_handle =
(TaskHandle_t)user_context;
BaseType_t higher_priority_task_woken =
pdFALSE;
if (task_handle != NULL) {
vTaskNotifyGiveFromISR(
task_handle,
&higher_priority_task_woken);
}
/*
* pdTRUEを返すと、必要に応じて割り込み終了後に
* 高優先度タスクへコンテキストスイッチします。
*/
return (higher_priority_task_woken == pdTRUE);
}
/**
* LEDを接続したGPIOを初期化します。
*/
static void led_gpio_initialize(void)
{
const gpio_config_t gpio_config = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(
gpio_config(&gpio_config));
/*
* 起動時はLEDを消灯します。
*/
ESP_ERROR_CHECK(
gpio_set_level(LED_GPIO, 0));
}
/**
* 1秒周期のGPTimerを生成・設定します。
*/
static gptimer_handle_t gptimer_initialize(
TaskHandle_t notification_task)
{
gptimer_handle_t timer_handle = NULL;
/*
* GPTimer本体の設定です。
*
* カウント方向:アップカウント
* 分解能:1MHz
*/
const gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = GPTIMER_RESOLUTION_HZ,
.intr_priority = 0,
.flags = {
.intr_shared = 0,
.allow_pd = 0,
.backup_before_sleep = 0,
},
};
ESP_ERROR_CHECK(
gptimer_new_timer(
&timer_config,
&timer_handle));
/*
* アラーム発生時に呼び出される
* コールバックを登録します。
*
* user_contextとして通知先タスクの
* ハンドルを渡します。
*/
const gptimer_event_callbacks_t callbacks = {
.on_alarm = gptimer_alarm_callback,
};
ESP_ERROR_CHECK(
gptimer_register_event_callbacks(
timer_handle,
&callbacks,
notification_task));
/*
* 1秒周期の自動リロードアラームを設定します。
*
* カウンタが1,000,000に到達するとアラームが発生し、
* 自動的に0へ戻って再びカウントを開始します。
*/
const gptimer_alarm_config_t alarm_config = {
.alarm_count = GPTIMER_ALARM_COUNT,
.reload_count = 0,
.flags = {
.auto_reload_on_alarm = true,
},
};
ESP_ERROR_CHECK(
gptimer_set_alarm_action(
timer_handle,
&alarm_config));
/*
* タイマーを有効化します。
*/
ESP_ERROR_CHECK(
gptimer_enable(timer_handle));
return timer_handle;
}
void app_main(void)
{
bool led_state = false;
/*
* 現在のapp_mainタスクのハンドルを取得します。
* GPTimerの割り込みコールバックから、
* このタスクへ通知を送ります。
*/
s_main_task_handle =
xTaskGetCurrentTaskHandle();
led_gpio_initialize();
gptimer_handle_t timer_handle =
gptimer_initialize(s_main_task_handle);
/*
* GPTimerを開始します。
*/
ESP_ERROR_CHECK(
gptimer_start(timer_handle));
ESP_LOGI(
TAG,
"GPTimer started");
ESP_LOGI(
TAG,
"LED GPIO: %d",
LED_GPIO);
ESP_LOGI(
TAG,
"Alarm period: 1 second");
while (1) {
/*
* GPTimerの割り込みコールバックから
* 通知されるまで待機します。
*
* portMAX_DELAYを指定しているため、
* 通知を受信するまでタスクはブロックされます。
*/
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY);
/*
* 1秒ごとにLED状態を反転します。
*/
led_state = !led_state;
ESP_ERROR_CHECK(
gpio_set_level(
LED_GPIO,
led_state ? 1 : 0));
ESP_LOGI(
TAG,
"LED %s",
led_state ? "ON" : "OFF");
}
}
コードのポイント
LED用GPIOを初期化する
最初に、LEDを接続したGPIO8を出力として設定します。
const gpio_config_t gpio_config = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK( gpio_config(&gpio_config));
pin_bit_maskには、使用するGPIOをビットマスク形式で指定します。
今回はGPIO8を使用するため、1ULL << LED_GPIOを設定しています。
GPIOの初期化後は、起動直後にLEDが点灯しないよう、出力をLowへ設定します。
ESP_ERROR_CHECK( gpio_set_level(LED_GPIO, 0));
通知先タスクのハンドルを取得する
今回のサンプルでは、GPTimerの割り込みコールバックからapp_main()を実行しているタスクへ通知を送ります。
そのため、最初に現在のタスクハンドルを取得します。
s_main_task_handle = xTaskGetCurrentTaskHandle();
タスクハンドルは、後からGPTimerのコールバックへuser_contextとして渡します。
これにより、割り込みコールバックは通知先のタスクを識別できます。
GPTimerを生成する
GPTimerの基本設定には、gptimer_config_tを使用します。
const gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = GPTIMER_RESOLUTION_HZ,
.intr_priority = 0,
};
今回の設定内容は次のとおりです。
- クロック源:自動選択
- カウント方向:アップカウント
- タイマー分解能:1MHz
- 割り込み優先度:ドライバによる自動設定
設定後、gptimer_new_timer()でGPTimerを生成します。
gptimer_new_timer( &timer_config, &timer_handle);
今回の分解能は1MHzなので、1カウントは1µsです。
そのため、1秒を計測するには1,000,000カウントが必要になります。
アラームコールバックを登録する
GPTimerのカウンタが設定値へ到達すると、アラームイベントが発生します。
アラーム発生時に呼び出す関数を、次のように登録します。
const gptimer_event_callbacks_t callbacks = {
.on_alarm = gptimer_alarm_callback,
};
ESP_ERROR_CHECK( gptimer_register_event_callbacks( timer_handle, &callbacks, notification_task));
第3引数には、コールバックへ渡すユーザー情報を指定できます。
今回のサンプルでは、通知先となるapp_main()タスクのハンドルを渡しています。
GPTimerのコールバックは、タイマーを有効化するgptimer_enable()より前に登録してください。
アラーム周期を設定する
1秒ごとにアラームを発生させるため、次の設定を行います。
const gptimer_alarm_config_t alarm_config = {
.alarm_count = GPTIMER_ALARM_COUNT,
.reload_count = 0,
.flags = {
.auto_reload_on_alarm = true,
},
};
各設定値の意味は次のとおりです。
alarm_count:アラームを発生させるカウント値reload_count:アラーム後に戻すカウント値auto_reload_on_alarm:アラーム後の自動リロード
今回は、次の値を使用しています。
#define GPTIMER_RESOLUTION_HZ 1000000UL
#define GPTIMER_ALARM_COUNT 1000000ULL
タイマー分解能が1MHzなので、1,000,000カウントで1秒になります。
また、自動リロードを有効にしているため、アラーム発生後はカウンタが0へ戻り、再び1秒の計測を開始します。
設定したアラーム条件は、次の関数でGPTimerへ反映します。
gptimer_set_alarm_action( timer_handle, &alarm_config);
GPTimerを有効化して開始する
GPTimerは、生成しただけではカウントを開始しません。
最初にgptimer_enable()でタイマーを有効化します。
gptimer_enable(timer_handle);
その後、gptimer_start()でカウントを開始します。
gptimer_start(timer_handle);
処理順序は次のようになります。
- GPTimerを生成する
- コールバックを登録する
- アラーム条件を設定する
- GPTimerを有効化する
- GPTimerを開始する
設定順序を間違えると、コールバックが呼ばれない原因になることがあります。
割り込みコールバックからタスクへ通知する
アラームが発生すると、次のコールバックが割り込みコンテキストで実行されます。
static bool gptimer_alarm_callback(
gptimer_handle_t timer,
const gptimer_alarm_event_data_t *event_data,
void *user_context) {
TaskHandle_t task_handle = (TaskHandle_t)user_context;
BaseType_t higher_priority_task_woken = pdFALSE;
if (task_handle != NULL) {
vTaskNotifyGiveFromISR( task_handle, &higher_priority_task_woken);
}
return (higher_priority_task_woken == pdTRUE);
}
このコールバックでは、LEDの制御やログ出力を直接実行していません。
vTaskNotifyGiveFromISR()を使い、LED制御を担当するタスクへ通知するだけにしています。
GPTimerのイベントコールバックは割り込みコンテキストで実行されるため、次のような処理は避ける必要があります。
- 長時間かかる演算
- 待ち時間が発生する処理
- 動的メモリの確保
- 通常のタスク向けAPIの呼び出し
- 大量のログ出力
割り込み処理はできるだけ短時間で終了させ、実際の処理はFreeRTOSタスク側で実行する構成が安全です。
タスク側で通知を待つ
app_main()側では、次の関数を使ってGPTimerからの通知を待ちます。
ulTaskNotifyTake( pdTRUE, portMAX_DELAY);
portMAX_DELAYを指定しているため、通知が届くまでタスクはブロック状態になります。
待機中はCPUを占有しないため、次のようなポーリング処理より効率的です。
while (通知がない) { // 繰り返し確認 }
GPTimerのアラームが発生するとタスクの待機が解除され、次の処理へ進みます。
LEDの状態を反転する
タスク通知を受け取った後、LEDの状態を反転します。
led_state = !led_state;
ESP_ERROR_CHECK( gpio_set_level( LED_GPIO, led_state ? 1 : 0));
1回目の通知でLEDが点灯し、次の通知で消灯します。
以降も1秒ごとにONとOFFが切り替わります。
ログには現在の状態を表示します。
ESP_LOGI( TAG, "LED %s", led_state ? "ON" : "OFF");
ログ出力は割り込みコールバックではなく、通常のタスク側で実行している点が重要です。
実行結果
GPIO8へLEDと330Ωの電流制限抵抗を接続し、プログラムを実行します。
シリアルモニターには、次のようなログが表示されます。
I (310) GPTIMER_SAMPLE: GPTimer started
I (310) GPTIMER_SAMPLE: LED GPIO: 8
I (310) GPTIMER_SAMPLE: Alarm period: 1 second
I (1310) GPTIMER_SAMPLE: LED ON
I (2310) GPTIMER_SAMPLE: LED OFF
I (3310) GPTIMER_SAMPLE: LED ON
I (4310) GPTIMER_SAMPLE: LED OFF
GPIO8へ接続したLEDが1秒ごとに点灯・消灯すれば、GPTimerは正常に動作しています。
実際のログ時刻には、起動処理やログ出力処理によるわずかな差が生じる場合があります。
掲載する際は、可能であれば実機で取得したログへ差し替えてください。
GPTimer利用時によくあるトラブル
1.コールバックが呼ばれない
GPTimerを生成しただけでは、アラームコールバックは実行されません。
次の処理がすべて実行されていることを確認してください。
gptimer_new_timer()gptimer_register_event_callbacks()gptimer_set_alarm_action()gptimer_enable()gptimer_start()
また、コールバックはgptimer_enable()より前に登録してください。
2.LEDが点灯しない
次の項目を確認してください。
- LEDの向きが正しいか
- 電流制限抵抗が接続されているか
- GPIO番号が実際の配線と一致しているか
- GPIO8が開発ボード上の別機能と競合していないか
- LEDのカソードがGNDへ接続されているか
GPIO8は今回のサンプルで使用する一例です。
使用するESP32-C5開発ボードによっては、他のGPIOへ変更する必要があります。
3.周期が想定と異なる
GPTimerの周期は、タイマー分解能とアラーム値によって決まります。
今回の設定では、
1,000,000Hz ÷ 1,000,000カウント = 1秒周期
となります。
例えば100ms周期に変更する場合は、次のように設定します。
#define GPTIMER_ALARM_COUNT 100000ULL
タイマー分解能が1MHzの場合、100,000カウントは100msです。
4.一度しかコールバックが呼ばれない
周期動作させる場合は、アラーム設定で自動リロードを有効にします。
.flags = { .auto_reload_on_alarm = true, },
この設定が無効の場合、アラームは基本的に一度だけ発生します。
ワンショットタイマーとして使用する場合は、自動リロードを無効にする方法もあります。
5.割り込み内の処理が重い
GPTimerのコールバック内で時間のかかる処理を行うと、他の割り込みやタスクへ影響する可能性があります。
割り込みコールバックでは、
- タスク通知
- キューへの最小限のデータ送信
- フラグの更新
などに処理を限定しましょう。
センサーの読み取り、通信、ログ出力などは、通知を受け取ったタスク側で実行するのがおすすめです。
6.CMakeLists.txtの依存関係が不足している
次のようなエラーが発生する場合は、必要なコンポーネントが指定されていない可能性があります。
driver/gptimer.h: No such file or directory
driver/gpio.h: No such file or directory
main/CMakeLists.txtに、次の依存関係があることを確認してください。
REQUIRES esp_driver_gptimer esp_driver_gpio
実務での設計ポイント
GPTimerは高精度な周期を生成できますが、コールバック内で実行する処理まで一定時間で完了するとは限りません。
実務では、GPTimerを処理そのものではなく、処理を開始するためのトリガとして使用するのがおすすめです。
例えば、次のような構成です。
- GPTimerが一定周期でアラームを発生させる
- 割り込みコールバックからタスクへ通知する
- 通知を受けたタスクがセンサーを読み取る
- 必要に応じてデータを保存・送信する
この構成にすると、タイマー割り込みを短時間で終了させながら、通常のタスク上で複雑な処理を実行できます。
また、センサー読み取りや通信処理に時間がかかる場合は、次のアラームが発生する前に処理を完了できるか確認してください。
処理時間がタイマー周期を超える場合は、
- タイマー周期を長くする
- 専用タスクを用意する
- 通知の取りこぼしや蓄積を考慮する
- 処理を複数のタスクへ分割する
といった設計が必要になります。
GPTimerと他の時間制御機能の使い分け
用途によっては、GPTimer以外の方法が適している場合があります。
vTaskDelay()
単純な待ち時間や、厳密な周期精度を必要としないタスクに適しています。
FreeRTOSソフトウェアタイマー
数十ms~数秒単位の一般的な周期処理に適しています。
GPTimer
マイクロ秒単位の時間管理や、周期のばらつきを抑えたい処理に適しています。
LEDC
PWM波形を生成し、LEDの明るさやモーター出力を制御する用途に適しています。
「時間を待つ」「周期イベントを発生させる」「PWM波形を出力する」では目的が異なるため、用途に合わせて周辺機能を選びましょう。
まとめ
GPTimerは、ESP32で高精度な時間管理を行うためのハードウェアタイマーです。
vTaskDelay()やFreeRTOSソフトウェアタイマーよりも、周期のばらつきを抑えた処理に適しています。
今回のサンプルでは、GPTimerのアラーム割り込みからタスク通知を行い、通常のタスク側でLEDを制御しました。
この構成には、次のメリットがあります。
- 割り込み処理を短くできる
- 通常のタスク上で安全に処理できる
- センサー読み取りや通信処理へ応用しやすい
- システム全体の応答性を維持しやすい
GPTimerを単なるLED点滅機能としてではなく、周期処理を開始するためのトリガとして活用することで、より安定した組み込みシステムを構築できます。
技術は、経験から価値になる。
DLROW Design|現場エンジニア