周波数カウンタを作るにはPICプロセッサを使うのが順当なようで各種の実例が見られます。
しかしながら、Arduino IDEにどっぷり浸かったズボラな私にはとっつきにくいです。
Arduino UNO R3を使った周波数カウンタは6.5MHzが上限でした。
Raspberry Pi Pico WとOLEDで2CH WEBオシロスコープ・Pulse generator・Function Generator・周波数カウンタ
に実装したものは内部クロック周波数計測機能を使って分解能1kHzで133MHz程度は測れるものでした。
もう少し高い周波数まで分解能が良くカウント出来るものが手近な部品で作れないか探してみたところ、STM32F103C8T6で24MHzまで測れるというものを見つけました。ロシアのアマチュア無線のWEBサイトrcl-radio.ruにありました。 Частотомер STM32 + индикатор на MAX7219 (Arduino)。 ロシア語のサイトですが日本語表示が選択できるので読みやすいです。
早速試してみましたが、動きません。何をつないでも72MHzが表示されます -_-。これを参照している英語のサイトでも日本語のサイトでもそのまま動いたように書かれています。自分のSTM32F103C8T6がコピー品なのかもしれない。何とかならないかとSMCRレジスタで外部入力を指定したら動きました。なんで他所では動いているのだろう???
ところで、この周波数カウンタを利用している信号発生器のフォーラム https://arduino.ru/forum/proekty/generator-s-reguliruemoei-chastotoi-na-arduino#comment-296530 ではgen_v3_6から同様の設定が入っています。また、内部の8倍までのプリスケーラーを有効にすれば192MHzまで測れるという指摘がありました。試してみたら使えそうです。 Raspberry Pi Pico WとOLEDで2CH WEBオシロスコープ・Pulse generator・Function Generator・周波数カウンタ で250MHzにオーバークロックして125MHzのパルスを出して確認できました。Si5351が手に入ったらもっと上まで確認してみます。
測定周波数の上限ですが一般的にはクロック周波数72MHzの1/3の24MHzまでが安全ですが、30MHzまで何となく測れます。CPUクロックに同期した信号なら1/2の36MHzでもきっちり測れています。プリスケーラーを有効にすれば分解能は下がりますが192MHzから240MHz程度まで行けそうです。ただし、こんなに高い周波数まで3.3Vppの信号を与えるプリアンプを作れるかどうかが問題になってきます。デジタル信号を見るだけならプリアンプ不要なのでシンプルになります。
(2024.04.18 追記) Si5351でパルスを発生して、192MHzまで測れることを確認しました。使用したライブラリで設定できる上限の225MHzでもどうにか測れることを確認しました。ただしブレッドボード上の配線には注意が必要で、GNDと撚り線にしたり終端抵抗を付ける等の工夫が必要でした。
開発環境は
Arduino IDE 1.8.19 + STM32F1xx/GD32F1xx boards by stm32duino version 2022.9.26です。
Arduino IDEの環境設定で追加のボードマネージャのURLに
http://dan.drown.org/stm32duino/package_STM32duino_index.json
を追加すると、ボードマネージャで"stm"を検索すれば出てきます。
bootloaderが使えるようにするには、Microsoft StoreからインストールしたArduino IDEではだめなので、Arduino IDEのホームページのダウンロード版をインストールしてください。
まずはオリジナルと同じMAX7219を使った8桁7セグメント表示器を使用したものです。 プリスケーラーの切り替えを自動にしました。 100MHz以上は9桁になるので、1Hzの桁を表示しません。 PB9ピンから24MHzのテスト信号を発生します。 ソースコードではゲート時間切り替えボタンが使えますが、ボタンを付けなくてもゲート時間1秒で動作するのでシンプルな回路にしました。
写真右側が周波数カウンタ、左側がパルス発生に使ったRaspberry Pi Picoオシロ。125MHzを計測。
7セグメントの数字だけでは味気ないので、0.96インチの128x32のOLEDに表示するようにしてみました。 プリスケーラーの切り替えを自動にしました。プリスケーラー無効の時は"max 24MHz"、有効の時は"max 192MHz"と表示します。 9桁表示出来ます。 PB9ピンから24MHzのテスト信号を発生します。 ゲート時間切り替えボタンで1秒と0.1秒が選べます。 12x16フォントと14x24の7-セグメント風フォントが選べます。7-セグメント風フォントは少しでも大きく表示したいと思ってビットマップで作成しました。 電池駆動に備えて3V3端子の電源電圧を測定して表示します。電源電圧は外付け部品なしで測れる便利なバグ付きAPIがあります。バグは簡単に補正出来ます。
写真左側が周波数カウンタ、右側がパルス発生に使ったRaspberry Pi Picoオシロ。125MHzを計測。
7-セグメント風フォント表示
まず、プリスケーラーを有効にしてゲート時間1msで計測します。24MHzを超えていなければプリスケーラーを無効にします。カウント値は24000と比較します。 その後、目的のゲート時間の1秒または0.1秒で計測して表示します。
Raspberry Pi Picoのオシロスコープから125MHzを出して測定した場合、124.995144MHzでした。39ppmの誤差です。Raspberry Pi PicoもSTM32F103C8T6も安い水晶を使っているだろうから50ppm程度の誤差があっても普通だと思います。1MHzで50Hz、100MHzで5000Hzの誤差があるのは覚悟しなければいけません。
周波数を補正するにはSTM32F103C8T6の水晶発振回路のコンデンサを調整するとか、外部クロック入力にするとかの方法がありますが、計算で補正してしまった方が楽です。例えば正確な125MHzを測定して124.995144MHzになったら、
freq = (double)freq * 125.0e6 / 124.995144e6;
と補正してしまえばとりあえず補正出来ます。それでも温度変動には無力で、STM32F103C8T6の水晶発振回路の性能次第になります。結局手軽な周波数カウンタとして数10ppmの誤差は覚悟しましょう。
(2024.05.28 追記) OLED表示版の周波数計測部分をライブラリ風にまとめました。ArduinoのFreqCountライブラリと同じ使い方ができるようにしました。また、ETRプリスケーラーの1倍、2倍、4倍、8倍を必要に応じて細かく切り替えるようにしました。
(2024.05.28 追記) ここまでのカウンタは一定時間のパルス数を単純にカウントするものでしたが、ゲート時間が1秒だと1Hz単位でしか周波数がわかりません。それとは違って、入力パルスの周期をカウントして周波数を求めるレシプロカル方式の周波数カウンタを試してみました。基本的には入力をカウントして約1秒のゲート信号を作り、システムクロックをカウントすることで周期を求めることが出来ます。1秒のゲート時間に対する分解能はシステムクロック分の1すなわち1/72,000,000Hz = 1.3e-8秒になるので、全計測範囲で0.013ppmの精度が期待できます。
具体的にはTimer2をワンパルスモードにしてTimer1のゲート信号を作って、システムクロックをTimer1の16ビットカウンタでカウントするとともに、そのオーバーフローをTimer3でカウントして実質32ビットの周期カウンタにしています。よく使われるキャプチャー機能やインタラプトは使っていません。
実際に計測してみると何故か2ppmから5ppmの誤差が出ています。ゲート時間0.1秒では20ppmから50ppmの誤差になります。何か設定に不足があるのでしょうか、原因はわかっていません。それでも低い周波数で小数点以下まで分かるのが特徴です。
回路図はOLED表示版と同じです。使用上の注意として、ゲート時間を作るためのカウント数を自動的に設定するために数回のリトライが必要で、安定するまで変な値を表示してしまいます。ETRプリスケーラーの選択は手動で、8倍にすれば192MHzまで測れます。ETRプリスケーラーの選択は分周比に含めて自動で、4Hzから192MHzまで、実力でほぼ240MHzまで測れます。
今回も周期計測部分をライブラリ風にまとめました。ArduinoのFreqMeasureライブラリと同じような使い方ができるようにしました。
frequency_reciprocal101.zip (2024.07.05 更新)
(2024.06.25 追記) 入力パルスの周期をカウントして周波数を求めるレシプロカル方式の周波数カウンタの前作では変な誤差が出てしまったので、キャプチャーとインタラプトを使う方式を試しました。基本的には入力を分周して約1秒毎のキャプチャー用のトリガー信号を作り、システムクロックをカウントしているカウンターをキャプチャーし、前回のキャプチャー値との差を計算することで周期を求めることが出来ます。1秒のゲート時間に対する分解能はシステムクロック分の1すなわち1/72,000,000Hz = 1.3e-8秒になるので、全計測範囲で0.013ppmの精度が期待できます。
キャプチャーするカウンタが32bitであれば簡単なのですが、STM32F103C8T6には16bitカウンタしかありません。16bitカウンタを32bit化するには以下の3つの方法が考えられます。
1) オーバーフローのインタラプトをソフトでカウントして上位16bitにする方法。これではキャプチャーしたタイミングと上位16bitが変化するタイミングのずれを解消するのが困難です。
2) システムクロックをカウントするものと、システムクロックの1/4096をカウントするものの値を合成して28bit相当にする方法。これは分周器の位相の不確定さを除去できるかが不安です。
3) 周期が2^16のカウンタと周期が2^16-1のカウンタを同時にキャプチャーしてその値の違いからカウント値を求める方法。中国人の剰余定理によって答えは0から(2^16)*(2^16-1)の間に一つだけであることが保証されます。タイミングや位相の問題はありません。
今回は3番目の方法を採用しました。
具体的にはTimer2を入力信号の分周器にしてTimer1とTimer3のキャプチャー信号を作って、キャプチャーで発生したインタラプトでTimer1とTimer3のカウント値を読み取り、前回のキャプチャーでの読み取り値との差を求めます。
システムクロックのカウント数をn、Timer1の周期が65536、Timer3の周期が65535とし、それぞれの前回のキャプチャーとの差をa, bとすると、
n = a + 65536 * x
n = b + 65535 * y
x, yは未知の整数。
正しい解法はともかく、直感的にbがオーバーフローして折り返す度にbがaよりも一つ大きくなることが分かります。ただし、aが遅れてオーバーフローするまではbがaよりも小さくなります。したがって、
b >= a の場合は n = (b - a) * 65536 + a
a > b の場合は n = (b - a - 1) * 65536 + a
となります。(b - a - 1)の演算は32bit符号なし整数で行うことで上位16bitに1が立ちますが65536を掛けることによって範囲外に押し出されるので無視して構いません。
入力周波数 / 分周数 = FCPU / n
なので
入力周波数 = 分周数 * FCPU / n
となります。
分周数を決めるには入力周波数がどの位であるかを調べなければならないので、Timer2だけを使った簡易式の周波数カウンタで測定します。まず、ETRプリスケーラを8倍にして1msのゲート時間で360MHzから8kHzの範囲で求め、500kHz以下だったらETRプリスケーラを1倍にして100msのゲート時間で655.350kHzから10Hzの範囲で再計測します。その結果を使って分周数を決めています。
入力周波数が急に高くなった場合に備えて、キャプチャーの間隔が10msを切ったら360MHzでも計測できる分周数に設定変更します。入力周波数が急に低くなってタイムアウトした場合は測定を中止して再度簡易式の周波数カウンタを使って分周数を設定し直します。
回路図はOLED表示版と同じです。使用上の注意として、ゲート時間を作るためのカウント数を自動的に設定するために数回のリトライが必要で、安定するまで変な値を表示してしまいます。ETRプリスケーラーの選択は分周比に含めて自動で、4Hzから192MHzまで、実力でほぼ240MHzまで測れます。計測時間が1秒を切る場合もあるので原理的な誤差は0.02ppm程度で全計測範囲で有効数字8桁弱を確保できると思います。ただしシステムクロックの精度が50ppmあるので小まめにキャリブレーションする必要があります。 不安要素として、ETRプリスケーラ1倍で計測中に36MHz以上に変化した場合にETRプリスケーラを切り替えられない恐れがあります。都合により7-セグメント風フォント表示はありません。
frequency_capture101.zip (2024.07.05 更新)