はじめに
NTPで同期した時刻を電波時計向けの電波として発信するNTPリピーターを自作してみました。
電子回路とか電気的な知識は殆どなく試行錯誤の末にできたものなので、内容については参考程度に・・・(本職はソフト屋です)
かなり長くなってしまったので、最終的に作ったものを速やかに知りたい方はこの辺りからどうぞ。
きっかけ
ずっとデスクの上に置いてある貰い物の電波時計の温湿度表示がイマイチ不正確な気がして、もっと精度が高そうなやつに買い換えたい気持ちが・・・
しかし、それよりもまず電波を受信できておらず時刻がズレまくりという致命的な問題があったことを思い出し、買い換えるにしても先にこれを何とかしたかった。
調べてみる
市販のNTPリピーター
高い。
JJYシミュレータなる変態染みたものを見つけた
PCのイヤホンやスピーカーから最大音量を鳴らせば何故か電波時計が同期される摩訶不思議なシミュレータ。
13.333kHzを音割れさせると波形が潰れて矩形波に近付き、高調波の39.999kHzの成分が出てくるとか何とか・・・
すごい発想(゜Д゜)
しかも試してみたら本当に同期できた。
ピー音を鳴らすだけならラズパイあたりで自作できそう!
全く同じ発想の人がいた(笑)
さらに調べてる中、ふと、あることに気づく・・・
ボードで自作するなら、音じゃなくて電波そのものを出せば良いのでは?
それはそう。
というわけで、そっちの方向で更に調べる・・・
色々と制作記録を見つけた中で、今回はこちらの記事をとても参考にさせてもらった。
決め手は、
Raspberry Pi のみで 40kHz を生成することもできますが,必要以上に消費電力が増加してしまうため,実用性を考えると 40kHz はこのように外部で生成した方がおすすめです.
この仕組みがとても効率良さそうに感じたから。
あと、この仕組みだと、制御側は電波時計の仕様(以下JJY)に従ってGPIOをon/off制御するだけだから、ラズパイの必要すら無さそう。
ラズパイ以外のボードを調べてみたところ、全部入りマイコンボード『ESP32』がWi-Fiとかネットワーク接続に対応していて、自力で実装するのが面倒なntp同期周りも関数1つでやってくれそうなことが分かった。
なによりボードがとてつもなく安い!(2024年2月時点で800円)
ひとまず使用するボードは決まったので、次は回路側を考える・・・
回路側を少しでも理解してみる(沼のはじまり)
ボードを変更するにあたって、回路側がそのままで大丈夫か分からなかったので、少しでも理解して問題ないか考えてみようと試みる・・・
回路シミュレータすごい
回路の動作を知る上で、このブラウザ上で動作する回路シミュレータが非常に役に立った。
好きに回路を組んで波形とか電流の流れが視覚的に分かる・・・学生時代に欲しかった・・・
(学生のころ電子工作に手を出したものの、電気について何一つ理解できず半田付けの技術だけ身に付けて挫折したタイプ)
回路の基本動作
常に40kHzで信号を発信し続ける回路(LTC1799)と、JJYの仕様でon/offするGPIOのANDを取ることで、GPIOがonの間だけ40kHzでon/offを繰り返す信号が出力される。(回路図中のNANDゲート8番ピン)
その信号(3.3V)をMOSFET1のゲート端子に入れることで、同様の動きを5Vラインで再現し、アンテナから電波を発信する・・・という認識で落ち着いた。
合ってるはず・・・
調べる中で気になった点
パルス波をそのまま発信してるから高調波が多め?
前述した『JJYシミュレータ』で学んだ高調波。
実際にどの程度の問題があるかは分からないけど、できれば不要な周波数の電波は減らしておきたい。
4V駆動のMOSFETに対して3.3Vしか入ってない
これについては参考元の記事のコメントで著者さんが言及されていて、
こちらは問題ないです.3.3V でも 10A 程度は流せるので,今回の目的では 3.3V ドライブでも目的は果たせます.
とのことで、問題はなさそう。
でもちょっと気持ち悪いから5Vを入れてあげたい・・・
瞬間的な高電圧が気になる
シミュレータで参考元の回路を簡略化したものを動かしてみると、信号がon/offする度に一瞬だけ160V程度の電圧が発生していて少し怖い。
どうやら「フライバック電圧」と言うらしい。
改造してみる
前述した気になった点を踏まえてシミュレータ上で改造してみた。
とりあえず波形の見た目は正弦波になったが・・・
高調波を取り除いて正弦波にすべくLPFを仕込む
この記事を参考にLPFを入れてみた。
・・・しかし、RC回路で挟んだつもりが、実際にはアンテナのコイルも作用してRLC回路の構成になっていて、意図したものになってなかった。
しかも、GPIOをoffにしてもコイルとキャパシタ間でずっと電荷が行ったり来たりしていて、on/off制御ができてない致命的な不具合あり。
5VでMOSFETを駆動させる
この記事を参考にNANDから出てきた3.3Vを5Vにレベルシフトさせてみた。
・・・あれ、そもそも3.3Vの信号で5Vを制御するためのMOSFETだったような・・・
もしかしてMOSFETいらなくね・・・?
電気や電磁波の勉強を始める・・・
さらに改良すべく、電圧に電流、電界や磁界、電磁波がどういう仕組みで発生するのか・・・などなど気が付くとどんどん沼にハマっていき・・・
よく分からないことがよく分かった。
うまくいきそうな回路を発見する
沼にハマっていた間のことは割愛。
試行錯誤の末に意図した波形で出ていそうな回路を編み出した。
参考元の回路では3.3Vで駆動していた発振モジュールを5Vで駆動させておき、GPIOの出力もすぐに5Vへシフトすることで、電波を発信する回路は全て5Vで動作するようにしてみた。
ANDゲートより右側は、LCの共振回路になっていて40kHzで共振してる・・・のか?
信号がONの間はLC回路で発信して、OFFになったら溜まった電荷が抵抗で消費されているような挙動に見えた。
抵抗値が大きすぎると波形のon/offの遅延が大きくなり、抵抗値が小さすぎると正弦波が歪んでいき、1.3k~1.4kΩくらいがちょうど良さそう。
原理をイマイチ理解できておらず自信なし・・・
とりあえずGPIOに見立てたスイッチのon/offに呼応して、正弦波が出たり消えたり正しいように見えるのでヨシ?
もう少し精度の高そうなシミュレータでも試してみる
「LTspice」という操作感が独特で使いにくい回路シミュレータをインストールして、実際のパーツモデルを使ってシミュレートしてみたところ、こちらでも意図した通りの正弦波が出力されていた。
スペクトルを見ても40kHzがしっかり立っていて気持ち良い。
しかし、どれだけシミュレーションしても机上の空論に過ぎず本当に上手くいくのか確証が得られない・・・(圧倒的な知識不足)
ただ、電圧と電流お互いの変動が作用し合うことで電磁波が伝播するのであれば、この波形でいけるはず・・・
ってことで、失敗を前提にこの方向で進めてみることに。
とりあえず燃えることは無さそうだからヨシ!(重要)
試作してみる
回路図(試作ver.)
慣れない回路図エディタで慣れない回路図づくり。たのしい。
ひとまずこんな感じで。自分が分かればヨシ!の精神。
コイル手前の抵抗は、ちょうど良い抵抗値のものが売ってなかったので、330Ωを4本直列で代用することにした。
秋月電子で必要そうなパーツを色々と購入。
アンテナは、参考元の記事ではコイルを自作されていたけど、自分で作るのは信頼性に欠ける(なにより面倒くさい)ってことで、探したら40kHzに調整済みらしいコイルが売っていたので、これを使うことに。
シミュレータの中にも度々登場した「7.2mH」の値は、このコイルのデータシートに記載されていたインダクタンス値。
ESP32を触るのも初めてなので、ブレッドボードにプスプスと刺して、いわゆる「Lチカ」から・・・
なんの問題もなく、とても簡単だった。
JJY周りを実装する
とにかく、まず自作の回路で本当に動作するのか確かめたかったので、嘘の時刻情報を発信するだけの仮のプログラムをこさえてみた。(ようやく本職の領域に・・・)
内部時計に嘘の時刻を設定しておいて、GPIO32番をJJYに従ってon/offし続けるだけの内容。
電波の発信内容は、JJY公式の情報を参考にした。
これも意外と簡単だった。
組み込みを意識した実装
今回はマイコン相手ってことで、組み込みっぽさを意識して実装してみた。
除算を使わずに数値をBCDへ変換する
「出来る限り除算は使わないのがセオリー」というイメージがあるので、よくある除算・剰余を使ったものではなく『Double dabble』というアルゴリズムで実装してみた。
unsigned short bin2bcd3(unsigned int c) {
// 3シフト目までは必ず条件に一致しないので飛ばす
c = c << 3;
for (int i = 3; i < 12; ++i) {
if ((c & 0xF00000) > 0x400000)
c = c + 0x300000;
if ((c & 0xF0000) > 0x40000)
c = c + 0x30000;
if ((c & 0xF000) > 0x4000)
c = c + 0x3000;
c = c << 1;
}
return c >> 12;
}
この計算法を編み出した人は天才に違いない・・・
偶数パリティを効率よく求める
半分ずつ折り畳むように計算していく、とても賢い手法でちょっと感動。
unsigned char calcParity(unsigned char c) {
c ^= c >> 4;
c ^= c >> 2;
c ^= c >> 1;
return c & 0x01;
}
この計算法を編み出した人は天才に違いない・・・
動かしてみる
呆気なく動いた。
受信完了のマークを灯しつつも頓珍漢な時刻を表す電波時計。
オシロスコープとかサーモグラフィといった高価な計測器を持っていないので詳しくは分からないものの、マルチメーターで周波数を図ったところ「40.000kHz」と表示され、触った感じ特に異常な発熱も感じられないので、大きな問題はなさそう。
ここで全く動作せず、送信しているデータが間違ってるのか、回路が間違ってるのか、電波は出てるのか出てないのか・・・調べるのも難しくて途方に暮れることをイメージしていたので、すんなり動いてくれて本当に良かった・・・
完成を目指して・・・
改造した回路で問題なく動作することは分かったので、本格的に製作開始。
制御プログラム
まず、嘘っぱちの時刻ではなく正しい時刻を発信する制御プログラムを作った。
電子回路はよく分からなかったので、本職の部分くらいは頑張りたい。
こだわりポイント
スケジュール機能
24時間ずっと電波を出し続けても無駄なので、電波時計が同期する時間だけ動作するようスケジュール機能を設けてみた。
// 発信スケジュール(開始時間が早い順に時分を設定)
const struct scheInfo {
int m_hour;
int m_minute;
} schedules[] = {
{ 3, 57 }, { 7, 57 }, { 11, 57 }, { 15, 57 }, { 19, 57 }, { 23, 57 }
};
#define SCHE_TIMES_MINITES 10 // 1回に発信する時間(分)
この設定の場合、4時・8時・12時・16時・20時・24時の3分前から10分間だけ稼働する。
DeepSleep
スケジュールの期間外は、なるべく消費電力を落とすべくESP32の「DeepSleep」機能を使って眠らせることにした。
// NOTE: ディープスリープ後は指定した時間より少し早く起きてしまう特性を考慮して1%長めに設定
sleepTime *= 1.01;
log_i("Out of term. Wakeup: %u/%u/%u %u:%u (Sleep: %llu us)",
scheTime.tm_year + 1900, scheTime.tm_mon + 1, scheTime.tm_mday,
scheTime.tm_hour, scheTime.tm_min, sleepTime);
esp_sleep_enable_timer_wakeup(sleepTime);
esp_sleep_enable_ext0_wakeup(GPIO_SW1, HIGH);
// ディープスリープへ移行
esp_deep_sleep_start();
就寝中はタイマの精度があまり良くないようで、設定した時間よりも早く起きてしまう傾向にあるらしく、1%長めに設定することで少し補正を試みる。
5Vラインの制御
DeepSleep中でもUSBから直接入ってくる5Vは給電されっぱなしで、試作の回路だと発振モジュールが動作したままになっていたので、JJYの制御とは別にGPIOで5Vのon/offをできるようにしておいた。
// 5VラインをON
digitalWrite(GPIO_5V, HIGH);
DeepSleep時には、勝手にGPIOがLOWに落ちて発振回路が停止することに期待。
また、このとき「そもそも発振モジュール自体をJJYに合わせてon/offすれば良くないか?」と思ったりもしたが、参考元の回路がこの仕組みで作られていて、きっと自分の知らないメリットがあるに違いない...ということで、そのままにしておくことに。
外のNTPサーバには優しく
気にするほどの頻度では無いかもしれないけど、起床の度に時刻同期を行ってしまうので、外部サーバではなく家の中で常時動いているNAS(TrueNAS)を1番目に設定しておいた。
// NTPサーバ設定
#define NTP_HOST1 "(NTP HOST)" // NTPサーバを設定(要設定!)
#define NTP_HOST2 "ntp.jst.mfeed.ad.jp"
#define NTP_HOST3 "ntp.nict.jp"
もしソースを使い回す際は、適宜接続先を変更してください。
電波のon/offのタイミングをなるべく正確に・・・
最初に試作した嘘っぱちの時刻を発信するプログラムでは、雑にdelayで待機しながら送っていたけど、本番ではタイマの割り込みを使って少しでも正確になるよう工夫してみた。
電波をon/offするタイミングで割り込みが発生するようにタイマ起動
↓
タイマ作動中にJJYのタイムコード生成やスケジュール関連の処理を済ませておく
↓
タイマの割り込みまで待機
↓
割り込みに合わせてGPIOの切り替え
という処理構造にすることで、他の処理に影響をなるべく受けずに正確なタイミングで電波のon/offができるようになった(はず)。
また、電波の発信は、なるべく秒の頭に合うようets_delay_us()
で更に微調整してみたりもした。
/*
* 電波ONタイマ割り込み
*/
void IRAM_ATTR onSignalOnTimer() {
struct timespec nowTime;
if (clock_gettime(CLOCK_REALTIME, &nowTime) != -1) {
// 残り時間を待機
//ets_delay_us((1000000000 - nowTime.tv_nsec) / 1000);
// 残り時間を待機(発信までの遅延を考慮して僅かに短め)
ets_delay_us((999999000 - nowTime.tv_nsec) / 1000);
} else {
log_w("error clock_gettime");
}
// 出力ON
digitalWrite(GPIO_JJY, HIGH);
// 通知
xSemaphoreGiveFromISR(g_hSemaphore, nullptr);
}
効果のほどは、よく分からず。
少しだけユーザフレンドリーに
LEDで初期化中・動作中やエラーを表したり、スイッチでスケジュールを無視した連続動作モードに切り替えられるようにして、少し使い勝手を良くした。
回路図(最終ver.)
プログラムに合わせて回路の方も修正。最終的にはこんな感じに。
※有線LAN化も視野に回路図上はLANモジュール(LAN8720)を含めてましたが、結果的に実装しませんでした。
試作からの更新点は制御プログラムに合わせて、
・動作中、エラーを知らせるLEDとモード切替用のスイッチを追加
・ボード側から5Vラインを制御できるように改良
の主に2点。
DeepSleep中は、NANDゲート以外は給電されないようになったはず。
パーツリスト
アンテナコイル以外は、全て秋月電子で購入できた。
部品の良し悪しはよく分からず、それっぽいものを直感で選択。
- ESP32-WROOM-32Eマイコンボード
- FT-232RQ USBシリアル変換モジュール
- 電源用マイクロUSBコネクタDIP化キット
- 1kHz~30MHzオシレータ LTC1799モジュール
- 丸ピンICソケット ( 8P)
- 多回転半固定ボリューム たて型 3296W 100kΩ [104]
- 2入力NANDゲート TC74HC00AP
- 丸ピンICソケット (14P)
- トランジスタ 2SC1815L-GR-T92-K 60V150mA
- 積層セラミックコンデンサー 2200pF50V C0G 5mm
- 小型 金属皮膜抵抗 1/4W4.7kΩ
- 小型 金属皮膜抵抗 1/4W10kΩ
- 小型 金属皮膜抵抗 1/4W330Ω
- 3mm青色LED 470nm 70° OSB5YU3Z74A
- 3mm赤色LED 625nm 70度 OSR5JA3Z74A
- タクトスイッチ(水色)
- 片面ガラスコンポジット・ユニバーサル基板 Bタイプ
- アンテナ ACL27NP-40KHz
ブレッドボードで試験
実装する前にブレッドボードで最終試験。
とてもスッキリ配線できて渾身の一作だっただけに、試験だったのが惜しい・・・
動作試験も問題無く成功。
試作成功に合わせて当初の目的だった精度が高そうな「電波時計付デジタル温・湿度計」なるものも買った。(電波時計はあくまでオマケ)
あと、電波を制御する部分を工夫したおかげか、電波時計、パソコンの時計、ブラウザで表示させたNICTの標準時は、見た目で分からない程度には綺麗に一致していた。
配線図的なもの
あとはユニバーサル基板に実装するだけ!
実装するにあたり事前に配線を整理しておきたくて、ユニバーサル基板に特化したソフトを探したりしてみたものの、VB5製とかHSP製のとても古いソフトしか見つけられず、結果的に回路図エディタを工夫して使うことで何とかなった。
具体的には、エディタ上の点をユニバーサル基板の穴に見立てて、LCoV.exeで実物とサイズを合わせた部品をチマチマ作って並べた。
レイヤー機能を使って、表面、裏面、ジャンパ線をそれぞれ分離して配置することで分かりやすく描けた。
あと、左右反転したものを並べて配置しておくことで、基板を裏返したときも分かりやすくなったのもGOOD。
表面
裏面
ジャンパ線
できればジャンパ線は避けたかったけど、どうしても1本だけ無くす配線を見つけられなかった・・・
ようやく実装
できた。
余りまくる細々とした部品や予備で買った部品を使って2つ作った。
1つ目(下側)は、四隅ギリギリまで使いすぎたため脚のネジが止められなくなってしまった。
配線は、色々と苦戦したものの過去一の出来映えかもしれない・・・(当社比)
反省
無事に完成したので最後に反省など。
送信出力が少し弱すぎたかも
受信可能距離が思いのほか短く、時計側の受信アンテナの位置も大きく影響して、お互いの配置が結構シビアっぽい。
時計側も向きを調整した上で、おおよそ30~50cmが限界だった。時計を反対に向けただけで至近距離でもダメ。
ただ、この範囲しか飛んでないということは、電波法関係は問題ないと思われる。
【考えられる理由】
- アンテナコイルが小さすぎる上に、基板上に固定しちゃった
購入したコイルが思いのほか小さく、他の人が製作した写真と見比べても小さすぎるように見える。
やっぱり大きいコイルを自作した方が良かったかもしれない。
もしくは、アンテナコイルを基板上ではなく時計側に貼り付けておくとかでも良かったかも。
- オペアンプを用いた増幅などを行っていない
一般的にはオペアンプで増幅させるっぽいことは調べてる中で分かっていたけど、回路が難しくなりすぎるのと、電波法の心配もあってやらなかった。
もし次に作るなら、出力を調整できるようにして部屋の中くらいは届くようにしてみたい。(次・・・?)
厳密にはJJYの仕様と合ってない
今回の制御プログラムでは、Offのとき完全に電波の出力を切っているけど、本来はOnのときの10%の出力で発信し続けるのが正しいらしい。
ただ、完全に切ってしまっても電波時計は問題なく同期してくれる。
できれば正しい仕様で実装してみたかったけど、回路側をこれ以上改造するのは避けたかったので諦めた。
もし次に作るなら、ちゃんと10%を出力できるような仕組みにしてみたい。(次・・・?)
配線作業が面倒すぎた
最近は、個人でも手が届く金額で基板の発注できる業者の存在は知りつつも、回路設計CADまで一から覚えるのは流石にしんどかったので見なかったことにした。
しかし、いざ配線作業をやると面倒すぎて、CADを覚えて基板を発注した方が今後の役に立つし、仕上がりはキレイし、圧倒的に良かったと後悔。。
ユニバーサル基板は時代遅れだった・・・
もし次に作るなら、基板を外注してみたい。(次・・・?)
このあたりの記事を参考にすれば、ESP32のボードからカスタマイズして作れそう・・・
本物の電波と比較すると250msくらい遅い?
もともと持ってた時計を窓の外に置いて本物の電波に同期させた上で、NTPに同期した電波時計と見比べてみると、16分音符1つ分(♩=60)くらい遅いように見えた。
NTPに同期した電波時計・パソコンの時計・ブラウザで表示させたNICTの標準時は一致していたから、同期してる時間が遅いような・・・
家庭環境レベルだとNTPで同期させても結構な誤差があるのだろうか?
とりあえず実運用上は問題ないので気にしないことにした。
もし次に作るなら・・・どうしようもないか・・・
おわりに
最初は「ラズパイでピー音を鳴らせばできる!」程度のイメージだったのに、どうしてこうなったんだろう・・・
ただ、ネットで拾った回路図を何も考えずに使うのではなく、回路の改造から取り組んだのは初めてだったのに、ちゃんと動作するものが作れて良かった。
結果的に、今まで苦手意識が強かった電子回路の理解が少し深まったのも嬉しい。
ちなみに、余った部品とか余計に買ったものとか、実現するまでに掛かった労力は抜きにして、純粋に基板の上に乗っかってる部品だけで費用を計算してみたら2,000円弱と市販品の1/10程度で済んだ。(全てトータルしても1万円程度のはず)
「普通に買った方が安かった・・・」というオチにならなかったのが何より上出来だと思う。
せっかく勉強したし、反省点を踏まえて更に改良してみたい欲求と、今回の物でも目的は十分達成できていて改良する必要が全くない現実・・・悩ましい。
これ以上やると「普通に買った方が安かった・・・」というオチになってしまうぞ・・・!
余談
もともと家にあった時計の温湿度計もそんなに精度悪くなかった!!!
1番精度が悪かったのは自身の体内センサだった。
ESP32 3.0.x対応(2024/6/19追記)
コメントにてESP32のライブラリが更新されて色々と変わったことを教えていただきました。
とりあえずライブラリをアップデートしてみたところ、無事にエラーまみれです。
どうしてそんなことするの・・・
と言うわけで、教えていただいたサイトを参考にしつつ修正して上げておきました。
【主な変更点】
-
#include <rom/ets_sys.h>
を追加(ets_delay_us()
がこのヘッダに移動した模様) -
timerBegin()
の引数を周波数(1MHz)に変更 -
timerAttachInterrupt()
の第3引数を削除 -
timerAlarmWrite()
をtimerAlarm()
に変更して、第4引数に0
を追加(autoreloadの回数を指定できるようになったっぽい?) -
timerAlarmEnable()
をtimerStart()
に変更・・・ではなく削除
どうやらtimerAlarm()
を呼び出すと自動でタイマが開始するようになったようで、さらにtimerStart()
まで呼び出すと
gptimer: gptimer_start(348): timer is not enabled yet
というエラーメッセージが出力される。
timerAlarmWrite()
でタイマの時間を設定してから timerAlarmEnable()
で開始するまでのラグを少しでも削るための変更かな?
ひとまずこれでコンパイルが通って、これまで通り電波時計の同期が取れるところまでは確認できました。
めでたしめでたし・・・