ESP32で周波数カウンター

更新日 2026.09.18 登録日 2026.08.16

ESP32の周波数カウンターにはkapraranさんの A frequency counter library for esp32 が有るので期待していましたが、esp32 by Espressif Systems 2.X用で3.0ではビルド出来なくなっていました。仕方がないので自分で3.0用に修正したものがsiliconvalley4066の A frequency counter library for esp32 です。 試しに使ってみると、やはりインタラプト処理によるソフトウェアでタイミングを取っているので、30MHz近辺では±9Hz程の変動が有ります。また、たまに±32768Hzの変な値が出てしまいます。たまにしか起こらないので気にしなければ良いのですがやっぱり気になります。

なんとかしてハードウェアだけでタイミングを取れないかといろいろと試した結果、そこそこ信頼できそうなものが出来ました。それにしてもESP32では思いもよらないことが色々と発生して手こずりました。

測定原理

PCNTでパルスをカウントします。PCNTは16ビットカウンタですが、正負のプラス側しか使わないので実質15ビットです。上限を32767に設定すると32767になった時点でオーバーフローが発生して0にリセットされるようです。

PCNTのオーバーフローインタラプトでソフトウェアのカウンタで回数ovfを数えます。測定期間終了後にPCNTとオーバーフローカウントを合成してトータルのカウント数を求めます。ここで(ovf * 32768)ではなく(ovf * 32767)になるのが曲者です

count = PCNT + ovf * 32767;

PCNTはコントロール端子がHighの期間だけカウントするように設定します。コントロール端子にはLEDCからのパルスを与えます。LEDCのパルスがHighの期間を適正に設定すれば正確なカウントが出来ます。

LEDCからのパルスがLowになった時点でLEDCを止められれば良いのですが現時点でのAPIでは出来なさそうです。次にHighになる前にカウントの読み込みと計算を終了して、PCNTとオーバーフローカウントをゼロにリセットする必要があります。このLowの期間は31.25msから62.5msとしました。

測定期間終了時にはLEDCからのパルスがLowになった時点のインタラプト処理でPCNTを読み込むことが出来ますが、オーバーフローインタラプト処理との競合でオーバーフローカウントとの計算結果が保証できません。そこで、esp_timerを使ってその100us後にはオーバーフローインタラプト処理カウントが終わっているはずなのでカウントの合成をするようにしました。

測定期間は1秒と0.1秒の2種類だけです。細かく設定しようとしても煩雑で正確に設定するのは難しそうなので諦めました。目標の測定時間ピッタリではありませんが周波数への換算は簡単に出来るし一番の目的は変動をなくすことなのでこれで良しとします。ESP32以外のESP32-S3等ではLEDC分解能が1-14なので測定期間1秒は使えません。

目標時間 LEDC周波数 LEDC分解能 測定期間 休止期間
1000ms 1Hz 17bit 937.5ms (15/16) 62.5ms (1/16)
100ms 8Hz 14bit 93.75ms (3/4) 31.25ms (1/4)

LEDCは原理的には1Hz以下でも発生できそうですが現状のAPIでは1Hz以下は指定できなさそうです。

測定周波数の上限ですが一般的には周辺クロック周波数80MHzの1/3の26.7MHzまでが安全ですが、40MHzまで何となく測れます。

ゲート時間はFreqCount.begin(msec)のmsecで設定できます。msecは500以上は1000ms、未満は100msとして扱います。

出来上がった後で見直してみると基本的な測定原理はTj LabさんのESP32-S3 を使った周波数カウンタと同じだということが分かりました。2.X用であって3.X非対応で周波数への換算の考え方が少し違います。オーバーフローカウントの処理対策がありません。

ESP-IDF用ですがLEDCではなくRMTを使った物もあるようです。 ESP32-freqcount (追記 2026.08.19)

測定期間の修正 (追記 2026.09.18)

その後、測定期間が937.5msや97.35msであることがすっきりしないので丁度になるように工夫してみました。
ledcAttach()とledcWrite()では使用するタイマーを選べないので ESP‑IDFのledc_timer_config()とledc_channel_config()を使って設定し、それでも整数でしか周波数を設定できないのでledc_hal_set_clock_divider()でdividerを直接設定し直すようにしました。
APB clockの80,000,000を因数分解すると割り切れる数として625が有ります。 divider=625 に設定することによって1秒は128000カウントになります。
duty=128000として周期をそれより少し大きい2のべき乗 2^17=131072 にすれば幅1秒のパルスを作れます。
duty=12800として周期をそれより少し大きい2のべき乗 2^14=16384 にすれば幅0.1秒のパルスを作れます。

目標時間 divider LEDC周波数 LEDC分解能 duty 測定期間 休止期間
1000ms 625 0.9765625Hz 17bit 128000 1000ms 24ms
100ms 625 7.8125Hz 14bit 12800 100ms 28ms

ここで注意が必要なのは、他のパルス発生にledcAttach()やledcWrite()を使ってしまうと、この周波数カウンタの設定が壊されてしまうので、ledc_timer_config()とledc_channel_config()を使う必要があることです。またLEDC_CHANNELやLEDC_TIMERの組み合わせによっては互いに干渉することもあるようなので干渉の無い組み合わせを探る必要があります。 それが嫌なら旧版のv1.02を使ってください。
また、ESP32 Core 3.3.11の時点ではLEDC関係でGuru Meditation Errorのリブートが発生しやすくなっています。

開発環境

開発環境は Arduino IDE 2.3.10 + esp32 by Espressif Systems 3.3.11です。
ボード設定は"DOIT ESP32 DEVKIT V1"と"NodeMCU-32S"が確認済みです。

ソースコード

最新版はGitHubに置いてあります
https://github.com/siliconvalley4066/FreqCountESPgate

Arduino IDEの「スケッチ=>ライブラリをインクルード=>.ZIP形式のライブラリをインストール」からダウンロードしたZIPファイルを選べばライブラリをインストールできます。

FreqCountESPgate.zip

インストール後にはArduino IDEの「ファイル=>スケッチ例=>FreqCountESPgate」からサンプルプログラムを開くことができます。

起動方法

  • FreqCount.begin(uint16_t msec);
  • 計測時間をmsecで指定できます。1秒の場合は1000を指定します。0.1秒の場合は100を指定します。周波数入力ピンはGPIO4、ゲートとLEDCに対応するピン番号はGPIO5になります。

  • FreqCount.begin(uint16_t msec, uint8_t fpin);
  • 計測時間をmsecで、周波数入力ピンをfpinで指定できます。ゲートとLEDCに対応するピン番号はデフォルトのGPIO5になります。

  • FreqCount.begin(uint16_t msec, uint8_t fpin, uint8_t gpin);
  • 計測時間をmsecで、周波数入力ピンをfpinで、ゲートとLEDCに対応するピン番号をgpinで指定できます。gpinをLED_BUILTIN (DOIT ESP32 DEVKIT V1では2)にするとオンボードのLEDがゲート時間に合わせて点滅します。

    読み取り値

    FreqCount.read();

    FreqCountライブラリは単純にカウント数を返していましたが、FreqCountESPgateではゲート時間が100msの場合でも周波数換算値を返します。

    サンプルプログラム

    serial

    Arduino IDEのシリアルモニターに結果を表示します。 LEDCを使ってGPIO16に32MHzを出してテストできるようにしました。 周波数入力ピンはGPIO4、ゲート時間を発生するLEDCに対応するピンはGPIO5になります。

    *

    oled

    SSD1306を使用した128x64のOLEDに結果を表示します。大きい数字フォントで表示します。 LEDCを使ってGPIO16に32MHzを出してテストできるようにしました。 周波数入力ピンはGPIO4、ゲート時間を発生するLEDCに対応するピンはGPIO5になります。 ゲート時間が100msの時に10サンプルの移動平均が取れるようにしています。
    Tiny4kOLEDライブラリをインストールする必要があります。

    *

    周波数誤差について

    ESP32のシステムクロックは水晶発振器が使われていて50ppm程度以内の精度に収まるようです。

    計算で周波数を補正するには、例えば正確な16MHzを測定して16.000123MHzになったら、
    freq = (long long)freq * 16000000 / 16000123;
    と補正してしまえばとりあえず補正出来ます。それでも温度変動には無力で、水晶発振回路の性能次第になります。

    周波数偏差をソースコードで修正するのは面倒なので、例えば基準周波数の16MHz, 12MHz, 10MHz, 8MHz, 1MHzと100ppm以内の誤差ならばCalibrationボタンを押して自動的に補正して補正値を保存するようにすることも考えられます。これは STM32F103C8T6で192MHz周波数カウンタ を参照してください。

    ESP32での問題点

    PCNTの割り込み処理でpcnt_get_event_status()を呼び出せば自動的に割り込みをクリアできるはずですが実際にはできていません。 幾つかの実装例では PCNT.int_clr.val = BIT(PCNT_UNIT) で割り込みをクリアしていますが、実際に試してみると時々クリアし損なうようです。PCNTの値に関係なく時々オーバーフローカウントが余分に増えていました。今回は念押しにもう一回割り込みをクリアすることで対策としました。

    LEDCのパルスには結構ジッターが発生する場合が有ります。幸い今回使っている1Hzと8Hzにはジッターが無いようです。テスト信号を発生できるようにしていますが、29MHzや31MHzにはジッターがありそうです。プリスケーラーやデバイダーやカウンタ周期を自由に設定出来れば良いのですが現在のAPIでは出来なさそうです。

    今のところESP32で高周波数側でも同等の精度でレシプロカルカウンタを構成する方法が見つかりません。

    References

  • Nikos Kapraras A frequency counter library for esp32
  • siliconvalley4066 A frequency counter library for esp32
  • Tj Lab ESP32-S3 を使った周波数カウンタ
  • siliconvalley4066 STM32F103C8T6で192MHz周波数カウンタ
  • siliconvalley4066 Raspberry Pi Picoで周波数カウンタ
  • siliconvalley4066 STM32F411CEU6で周波数カウンター
  • siliconvalley4066 Arduino UNO R4で周波数カウンタ
  • siliconvalley4066 ATtiny412で100MHz周波数カウンター

  • <=Arduinoで電子工作に戻る
    <=Siliconvalley4066番地に戻る
    ゲストブックへ
    siliconvalley4066@yahoo.co.jp