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)
その後、測定期間が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ファイルを選べばライブラリをインストールできます。
インストール後にはArduino IDEの「ファイル=>スケッチ例=>FreqCountESPgate」からサンプルプログラムを開くことができます。
FreqCount.read();
FreqCountライブラリは単純にカウント数を返していましたが、FreqCountESPgateではゲート時間が100msの場合でも周波数換算値を返します。
Arduino IDEのシリアルモニターに結果を表示します。 LEDCを使ってGPIO16に32MHzを出してテストできるようにしました。 周波数入力ピンはGPIO4、ゲート時間を発生するLEDCに対応するピンはGPIO5になります。

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周波数カウンタ を参照してください。
PCNTの割り込み処理でpcnt_get_event_status()を呼び出せば自動的に割り込みをクリアできるはずですが実際にはできていません。 幾つかの実装例では PCNT.int_clr.val = BIT(PCNT_UNIT) で割り込みをクリアしていますが、実際に試してみると時々クリアし損なうようです。PCNTの値に関係なく時々オーバーフローカウントが余分に増えていました。今回は念押しにもう一回割り込みをクリアすることで対策としました。
LEDCのパルスには結構ジッターが発生する場合が有ります。幸い今回使っている1Hzと8Hzにはジッターが無いようです。テスト信号を発生できるようにしていますが、29MHzや31MHzにはジッターがありそうです。プリスケーラーやデバイダーやカウンタ周期を自由に設定出来れば良いのですが現在のAPIでは出来なさそうです。
今のところESP32で高周波数側でも同等の精度でレシプロカルカウンタを構成する方法が見つかりません。
Arduinoで電子工作に戻る
Siliconvalley4066番地に戻る