STM32F103C8T6で192MHz周波数カウンタ

更新日 2025.07.01 登録日 2024.04.02

*

周波数カウンタを作るには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のホームページのダウンロード版をインストールしてください。

(2025.05.18 追記)シリアルからスケッチを書き込んだ場合PA15/PB3/PB4はJTAGデバッグ用に使われているので使えないらしい。USBコネクタ/ブートローダ経由なら使えるようです。

disableDebugPorts();
または
afio_cfg_debug_ports(AFIO_DEBUG_NONE);
を setup() 内に記述してみてください。

7セグメントLED表示版

まずはオリジナルと同じMAX7219を使った8桁7セグメント表示器を使用したものです。 プリスケーラーの切り替えを自動にしました。 100MHz以上は9桁になるので、1Hzの桁を表示しません。 PB9ピンから24MHzのテスト信号を発生します。 ソースコードではゲート時間切り替えボタンが使えますが、ボタンを付けなくてもゲート時間1秒で動作するのでシンプルな回路にしました。

*
写真右側が周波数カウンタ、左側がパルス発生に使ったRaspberry Pi Picoオシロ。125MHzを計測。

ソースコード

frequency_meter7219_101.zip (2025.04.13 更新)

回路図

*

OLED表示版

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-セグメント風フォント表示

ソースコード

frequency_meter101.zip (2025.04.13 更新)

回路図

* (2025.04.13 更新)

操作方法

  • Gate Timeボタンを押す度に1秒と0.1秒を切り替え出来ます。
  • Calibrationボタンを押すと基準周波数と50ppm以内なら周波数偏差を補正します。 (2025.04.13 更新)
  • Gate TimeボタンとCalibrationボタンを同時押しすると12x16フォントと14x24の7-セグメント風フォントの切り替えができます。 (2025.04.13 更新)
  • プリスケーラーの自動切換えについて

    まず、プリスケーラーを有効にしてゲート時間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の誤差は覚悟しましょう。

    (2025.04.13 追記) 周波数偏差をソースコードで修正するのは面倒なので、基準周波数の25MHz, 24MHz, 20MHz, 16MHz, 12MHz, 10MHz, 8MHz, 6MHz, 5MHz, 4MHz, 3MHz, 2MHz, 1MHz, 100kHz, 32768Hzと50ppm以内の誤差ならばCalibrationボタンを押すと自動的に補正して補正値を保存するようにしました。

    References

  • Частотомер STM32 + индикатор на MAX7219 (Arduino) http://rcl-radio.ru/?p=76772
  • Генератор с регулируемоей частотой на ардуино. https://arduino.ru/forum/proekty/generator-s-reguliruemoei-chastotoi-na-arduino#comment-296530
  • Simple STM32 Frequency meter from 1Hz to 30 MHz-Arduino IDE https://www.hackster.io/mircemk/simple-stm32-frequency-meter-from-1hz-to-30-mhz-arduino-ide-ee77ce
  • STM32 マイコンを使って周波数カウンターを作ってみました https://www.jh1lhv.tokyo/entry/2023/10/09/123524


  • ライブラリ版

    (2024.05.28 追記) OLED表示版の周波数計測部分をライブラリ風にまとめました。ArduinoのFreqCountライブラリと同じ使い方ができるようにしました。また、ETRプリスケーラーの1倍、2倍、4倍、8倍を必要に応じて細かく切り替えるようにしました。

    (2025.04.13 追記) 周波数偏差をソースコードで修正するのは面倒なので、基準周波数の25MHz, 24MHz, 20MHz, 16MHz, 12MHz, 10MHz, 8MHz, 6MHz, 5MHz, 4MHz, 3MHz, 2MHz, 1MHz, 100kHz, 32768Hzと50ppm以内の誤差ならばCalibrationボタンを押すと自動的に補正して補正値を保存するようにしました。

    ソースコード

    frequency_meter111.zip (2025.04.13 更新)



    周期カウンタ版

    (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ライブラリと同じような使い方ができるようにしました。

    (2025.04.13 追記) 周波数偏差をソースコードで修正するのは面倒なので、基準周波数の25MHz, 24MHz, 20MHz, 16MHz, 12MHz, 10MHz, 8MHz, 6MHz, 5MHz, 4MHz, 3MHz, 2MHz, 1MHz, 100kHz, 32768Hz, 10kHz, 1kHz, 100Hz, 10Hz, 1Hzと50ppm以内の誤差ならばCalibrationボタンを押すと自動的に補正して補正値を保存するようにしました。

    (2026.06.19 追記) 3ギガ10センチさんのブログ STM32でレシプロカル周波数カウンター(2) でこの方式で発生する誤差を検討して解決されているので、それを参考にしてTimer2の設定を変更して変な誤差は解消しました。3ギガ10センチさんありがとうございます。 これで後記の「周期カウンタ版 その2」との違いがほぼ無くなりました。大きな違いは、こちらは計測完了から再計測開始までに若干の時間的ブランクがあることです。「周期カウンタ版 その2」の方は途切れなく計測を継続できます。また、こちらは全てハードウェアで計測するので余分なインタラプトが発生しないことでしょうか。

    ソースコード

    frequency_reciprocal104.zip (2026.06.19 更新)

    References

  • 3ギガ10センチ https://norichiwo.hatenablog.com/entry/2025/06/18/232123


  • 周期カウンタ版 その2

    (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-セグメント風フォント表示はありません。

    (2025.04.13 追記) 周波数偏差をソースコードで修正するのは面倒なので、基準周波数の25MHz, 24MHz, 20MHz, 16MHz, 12MHz, 10MHz, 8MHz, 6MHz, 5MHz, 4MHz, 3MHz, 2MHz, 1MHz, 100kHz, 32768Hz, 10kHz, 1kHz, 100Hz, 10Hz, 1Hzと50ppm以内の誤差ならばCalibrationボタンを押すと自動的に補正して補正値を保存するようにしました。

    (2025.04.17 追記) とある掲示板で、この安定するまでに変な値を表示してしまうのは致命的と書かれていたので、改善しました。また、測定できる周波数の下限が2.0Hzだったのを1.0Hzまで広げました。4Hz以下の測定は4周期で測定するので1Hzの測定には4秒かかります。

    ソースコード

    frequency_capture106.zip (2025.05.23 更新)

    References

  • シンセ・アンプラグド https://pcm1723.hateblo.jp/entry/20150127/1422374004


  • STM32F103C8T6 Miniで実装

    (2025.05.27 追記) 別ページで紹介したSTM32F103C8T6 Mini基板を使って「周期カウンタ版その2」のレシプロカル方式の周波数カウンタを組みました。ブレッドボードに空きが出来たのでトランジスタのプリアンプも付けました。ユニバーサル基板に組めばもっとコンパクトにできそうです。

    PB14とPB15は2列の同じ列に並んでいるのでPB14をPB13に移動。その他のピンは2列に並んでいないのでブレッドボードに組むことが出来ます。写真のように使う所だけにピンを半田付けしてブレッドボードに挿しました。トランジスタ2SC3355はベースとコレクタ間を1列開けるようにしています。ベースのバイアス抵抗R1はコレクタ電圧が1.65Vに近くなるように調整する必要があります。この時コレクタ電流は5mAになります。

    自作のディップメータから約200MHzをワンターンコイルで接続して何とか測定出来ました。 ディップメータはボール紙の筐体なので恥ずかしくてお見せできません。 プリアンプのゲインが不足気味で、ディップメータの出力を最大にしてギリギリ測定できる程度です。この時のプリアンプの入力振幅がどの程度なのかわかりませんが、おそらく数100mVppだと思われます。

    * *

    回路図

    *

    ソースコード

    frequency_capture107.zip



    大きいフォントで表示

    (2025.06.30 追記) やはり文字が小さいのが不満なので大きいフォントでの表示にしました。最初の方でやったビットマップデータでも良いんですが、コンパクトなライブラリであるOLED_I2Cを使ってみました。このライブラリには14x24の7セグメントフォント BigNumbers が付属しているので、とりあえず簡単に大きく表示することが出来ました。
    でもちょっと読みにくいので14x24の普通のフォント BigNum を作ってみました。結構読みやすくなったと思います。

    このOLED_I2Cライブラリはそのままでは動かず、128x64の画面にしか対応していないので修正が必要です。 STM32F103C8T6でOLED_I2C Libraryを使うに修正方法を書いておきました。ライブラリを丸ごと新しいものに置き換えます。

    * BigNumbers フォントで7セグメント表示

    * 新たに作った BigNum フォントで表示

    ソースコード

    frequency_capture108.zip

    OLED_I2C_STM.zip 修正した OLED_I2C ライブラリ
    置き場所は C:\Users\user\AppData\Local\Arduino15\packages\stm32duino\hardware\STM32F1\2022.9.26\libraries\OLED_I2C です。

    u8g2 ライブラリに u8g2_font_logisoso24_tn フォントがありました。こちらは15x25のサイズで文字自体は13x25なので一文字ずつ詰めて表示すれば何とかなりますが1ドット幅が狭く1ドット縦長の細身のフォントになります。ただ規模が大きいライブラリなのでコンパイル時間がかかってあまり使う気がしません。
    frequency_capture108u8g2.zip

    あれこれ調べているうちに Tiny4kOLED ライブラリでも Arduino_STM32 の Wire ライブラリに手を入れることで使えるようになることが分かりました。STM32F103C8T6でTiny4kOLED Libraryを使うに修正方法を書きました。
    frequency_capture108Tiny4k.zip Wireライブラリの修正が必要です。

    References

  • siliconvalley4066 STM32F103C8T6でOLED_I2C Libraryを使う
  • siliconvalley4066 STM32F103C8T6でTiny4kOLED Libraryを使う


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