ESP32 で 市販の 電波時計 を合わせてみた

ESP32 ( ESP-WROOM-32 )

こんばんは。

前回の記事に引き続き、ESP-WROOM-32 ( ESP32 )のGPIO から疑似的に日本標準電波 JJY を出す実験の第2弾です。

今回は、いよいよ 日本標準電波 JJY のタイムコードを出力して、市販の電波時計を合わせてみようと思います。

スポンサーリンク

ただ、ひとつ大きな疑問が出てきます。

NeoCat さんのこちらの記事では、Arduino と Ethernet Shield を使って、有線LAN でNTPサーバーから時刻を補正して標準電波 JJY を出していましたが、ESP-WROOM-32 ( ESP32 )はそれ自体が 2.4GHz 帯の Wi-Fi 電波を出しています。
それって、Wi-Fi電波に影響が出たり、標準電波 JJY のノイズになって、電波時計が合わせられないのではないかと素人的に考えてしまいます。

でも、実際にやってみたら全く問題無く合わせることができました。
ただ、電波は30㎝も飛ばないので、アンテナ線に殆ど接触させる感じになってしまいます。
3.3V のパルス波(方形波)なので仕方ありません。

では、以下の動画をご覧ください。

いかがでしょうか。
最初と最後の方で3つの異なる電波時計が同期していますね。
一番同期が遅かった電波時計は13分ほどかかりました。
これは実際の時間とは異なって、私が適当な時間を設定しました。
市販の電波時計を自動で自由に合わせることができて、イイ感じです。

電波時計というものは正確にキッチリ時刻を合わせてくれますが、仕事をしている人にとっては5分早めたかったりしたい場合ありますよね。
そういう場合にはこの自作標準電波 JJY を使うと自由な時刻を正確に合わせることができて便利です。

これができれば、バッテリーなどを使って壁掛けの電波時計に一緒に吊るしておけば、電波の届かないところでも WiFiで正しい時刻に修正してくれますね。

それに、常にESP-WROOM-32 ( ESP32 )から Wi-Fi電波が出ているはずですが、これだけ近い位置に電波時計を置いてもしっかり同期してくれました。
これはなかなかスバラシイ!!!

ただ、市販の電波時計の仕様によって、取扱説明書に書いてある時刻誤差の範囲を超えてしまうと、いくら正確な標準電波 JJY が出ていても合わせてくれない事がありますので気を付けてください。
時計の取扱説明書を予めよく読んでおいてくださいね。

では、ESP-WROOM-32 ( ESP32 )で市販の電波時計を合わせる方法を紹介したいと思います。

※ここで紹介した標準電波発信は、とても微弱で1mも飛ばないので、電波法違反ではないと思います。
また、以下に紹介する方法は個人の趣味の自己満足で実験した方法により、動作保証はしません。
電波時計によっては受信できない場合もあります。

※ここで紹介する方法はあくまで個人的趣味の実験です。
基本的には無線工学についてはド素人ですので、あくまで自己満足報告です。
大げさな測定器も使っていますが、自分が使ってみたかっただけです。

また、この記事を書いた当初は LC共振回路というものを良く知りませんでした。
最新記事では、LC共振回路の理解を深めて、LC共振回路を使ってガッツリ電波時計を合わせる実験もして見ましたので、合わせて以下の記事もご覧ください。
(2019/01/17)
https://www.mgo-tec.com/blog-entry-esp32-lc-resonance-radio-watch.html

前回の記事を参照して事前設定を済ませておく

前回の記事を参照して、ESP-WROOM-32 ( ESP32 )開発ボードの接続や設定、アンテナ作り等を事前に済ませておいてください。

waves ESP32 DevKitC V4 ESP-WROOM-32 ESP-32 WiFi BLE
waves
¥1,170(2024/11/21 21:33時点)

Time ライブラリのインストール

前回の記事に付け加えて、Arduino 標準の Time ライブラリをインストールしておきます。
これのインストール方法は以下のページを参照してください。

https://www.mgo-tec.com/blog-entry-1616shinonome-ws-oled-news.html

事前にWi-Fi環境を整えておく

今回はインターネットに接続できる Wi-Fi 環境が必要です。
NTPサーバーにアクセスして、ESP-WROOM-32 ( ESP32 )の内蔵時計を補正します。

事前にESP-WROOM-32 ( ESP32 )がご自分の Wi-Fi ルーターに接続できるようにしておいてください。
ファイアウォールやMACアドレスフィルタリングなどは設定し直すか、解除しておいてください。

スケッチの入力

では、いよいよ Arduino IDE に標準電波 JJY タイムコードを出すプログラムを組んでみます。
殆どが独自に組んだものですが、通算日を計算するTimeライブラリ計算では、NeoCatさんの記事を拝借させていただきました。

また、標準電波 JJY タイムコードについては、前回の記事を参照しながらスケッチを見てください。

こちらの記事のコメント欄で、Kat-Kai さんからとても有効な情報をいただきました。
ESP32 のクロックカウントから正確な時間経過をμセコンドオーダーで微調整ができるとのことです。
パルス長が格段に安定しますので、是非試してみて下さい。
また、コンパイルする時に、Arduino IDE の Core Debug Level を "なし" に選択してください。
Debugモードにしてしまうと、100ms毎にウォッチドッグタイマが作動して、パルス周波数が不安定になりますのでご注意ください。
スケッチ10行目の T_day が uint8_t型になっていたため、1月1日からの起算日が256以上になると0日になってしまいました。
よって、11行目に新たに uint16_t型に変更しました。
失礼しました。m(_ _)m
2017/9/13

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <WiFi.h>
#include <WiFiUdp.h>
#include "TimeLib.h" //Arduino time library ver1.5
 
const char* ssid = "xxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxx"; //ご自分のルーターのパスワードに書き換えてください
 
const uint8_t Denpa_pin = 17;
 
uint8_t Min, Hour, Year, Weekday;
uint16_t T_day;
uint8_t PA1, PA2; //パリティ定義
//-----------NTP server Get 関連初期化 --------------------------
unsigned int localPort = 2390;
IPAddress timeServer;
const char* ntpServerName = "time.windows.com";
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
byte timeZone = 9; //Tokyo
WiFiUDP Udp;
uint32_t Ntp_Get_LastTime = 0;
 
//*****************セットアップ******************************
void setup() {
  Serial.begin(115200);
  delay(10);
 
  Serial.println();
  Serial.print(F("Connecting to "));
  Serial.println(ssid);
 
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
 
  Serial.println();
  Serial.println(F("WiFi connected"));
  delay(1000);
 
  Serial.println(WiFi.localIP());
  delay(10);
 
  pinMode(Denpa_pin, OUTPUT);
 
  WiFi.hostByName(ntpServerName, timeServer); //サーバーネームをIPアドレスに変換
 
  Udp.begin(localPort);
 
  setTime( Get_Ntp_Time() ); //NTPサーバーから時刻を取得し、ESP32内蔵時計にセットする
  delay(2000); //安定するまで待つ
 
  setTime( now() + 1 ); //標準時とキッチリ合わせるため、1秒早める。
 
  Ntp_Get_LastTime = millis();
 
  TaskHandle_t th; //マルチタスクハンドル定義
  //マルチタスク実行
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th, 0);
}
//************* メインループ ****************************************
void loop() {
  if((millis() - Ntp_Get_LastTime) > 1800000){ //30分毎にNTPサーバーから時刻取得
    setTime( Get_Ntp_Time() );
    Now_Time(); //現在時刻表示
    Ntp_Get_LastTime = millis();
  }
}
//************* マルチタスク ****************************************
void Task1(void *pvParameters){
  uint32_t LastTime = 0;
 
  if(second() == 0){
    delay(1500); //ゼロ秒の場合は次のゼロ秒までやり過ごすために、1.5秒待つ。
  }
  while(second() != 0){ //ゼロ秒になるまで待つ
    delay(1); //ウォッチドッグタイマを動作させるために必要
  }
  Serial.println();
 
  while(1){
    LastTime = millis();
    Now_Time();                   //現在時刻初期化
    Marker();                     //M マーカー送信
    Tcode_Min_Send();             //タイムコード(分)送信
    Marker();                     //P1 ポジションマーカー送信
    Tcode_Hour_Send();            //タイムコード(時)送信
    Marker();                     //P2 ポジションマーカー送信
    Tcode_TotalDay_Parity_Send(); //タイムコード(通算日)送信
    Marker();                     //P4 ポジションマーカー送信
    Tcode_Year_Send();            //タイムコード(年)送信
    Marker();                     //P5 ポジションマーカー送信
    Tcode_Weekday_Uruu_Send();    //タイムコード(曜日、うるう秒)送信
    Marker();                     //P0 ポジションマーカー送信
 
    Serial.printf("\r\n[Total = %d ms]\r\n", (int)(millis() - LastTime));
 
    while(second() != 0){ //もし、時間がズレた場合、ゼロ秒になるまで待つ
      Serial.print('-');
      delay(1); //ウォッチドッグタイマを動作させるために必要
    }
  }
}
//******** マーカーおよびポジションマーカー送信 ************************
void Marker(){
  Serial.print(".M.");
  Generator_High(200);
  Generator_Low(800);
}
//******** タイムコード(分)送信 **************
void Tcode_Min_Send(){
  uint8_t Min10 = floor(Min / 10);
  uint8_t Min01 = Min % 10;
  uint8_t m[7] = {0}; //分のパリティ PA2 計算用配列初期化
 
  //---------10の位-------------
  m[6] = bitRead( Min10, 2 );
  Serial.print( m[6] );
  Time_Code_Bit_Generate( m[6] );
  m[5] = bitRead( Min10, 1 );
  Serial.print( m[5] );
  Time_Code_Bit_Generate( m[5] );
  m[4] = bitRead( Min10, 0 );
  Serial.print( m[4] );
  Time_Code_Bit_Generate( m[4] );
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  //---------1の位--------------
  m[3] = bitRead( Min01, 3 );
  Serial.print( m[3] );
  Time_Code_Bit_Generate( m[3] );
  m[2] = bitRead( Min01, 2 );
  Serial.print( m[2] );
  Time_Code_Bit_Generate( m[2] );
  m[1] = bitRead( Min01, 1 );
  Serial.print( m[1] );
  Time_Code_Bit_Generate( m[1] );
  m[0] = bitRead( Min01, 0 );
  Serial.print( m[0] );
  Time_Code_Bit_Generate( m[0] );
 
  //-------分パリティ計算-------
  PA2 = (m[6] + m[5] + m[4] + m[3] + m[2] + m[1] + m[0]) % 2;
}
//******** タイムコード(時)送信 *************
void Tcode_Hour_Send(){
  uint8_t Hour10 = floor(Hour / 10);
  uint8_t Hour01 = Hour % 10;
  uint8_t h[6] = {0}; //時のパリティ PA1 計算用配列初期化
 
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  Serial.print('0');
  Time_Code_Bit_Generate(0);
 
  //---------10の位-------------
  h[5] = bitRead( Hour10, 1 );
  Serial.print( h[5] );
  Time_Code_Bit_Generate( h[5] );
  h[4] = bitRead( Hour10, 0 );
  Serial.print( h[4] );
  Time_Code_Bit_Generate( h[4] );
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  //---------1の位-------------
  h[3] = bitRead( Hour01, 3 );
  Serial.print( h[3] );
  Time_Code_Bit_Generate( h[3] );
  h[2] = bitRead( Hour01, 2 );
  Serial.print( h[2] );
  Time_Code_Bit_Generate( h[2] );
  h[1] = bitRead( Hour01, 1 );
  Serial.print( h[1] );
  Time_Code_Bit_Generate( h[1] );
  h[0] = bitRead( Hour01, 0 );
  Serial.print( h[0] );
  Time_Code_Bit_Generate( h[0] );
 
  //-------分パリティ計算-------
  PA1 = (h[5] + h[4] + h[3] + h[2] + h[1] + h[0]) % 2;
}
//******** タイムコード(その年の1月1日からの通算日)送信 ***********
void Tcode_TotalDay_Parity_Send(){
  uint8_t T_day100 = floor(T_day / 100);
  uint8_t T_day10 = ((uint8_t)(floor(T_day / 10))) % 10;
  uint8_t T_day01 = T_day % 10;
  uint8_t b;
 
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  Serial.print('0');
  Time_Code_Bit_Generate(0);
 
  //---------100の位-------------
  b = bitRead( T_day100, 1 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day100, 0 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  //---------10の位-------------
  b = bitRead( T_day10, 3 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day10, 2 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day10, 1 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day10, 0 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
 
  Marker(); //P3 ポジションマーカー
 
  //---------1の位-------------
  b = bitRead( T_day01, 3 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day01, 2 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day01, 1 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( T_day01, 0 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
 
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  Serial.print('0');
  Time_Code_Bit_Generate(0);
 
  //---------パリティビット---------
  Serial.print(PA1);
  Time_Code_Bit_Generate(PA1);
  Serial.print(PA2);
  Time_Code_Bit_Generate(PA2);
  //---------予備ビット-------------
  Serial.print('0');
  Time_Code_Bit_Generate(0); //SU1 予備ビット
}
//**********タイムコード(年)下2桁送信**********************
void Tcode_Year_Send(){
  uint8_t Y10 = (uint8_t)(floor( Year / 10 ));
  uint8_t Y01 = Year % 10;
  uint8_t b;
 
  //---------予備ビット---------
  Serial.print('0');
  Time_Code_Bit_Generate(0); //SU2 予備ビット
  //---------10の位-------------
  b = bitRead( Y10, 3 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Y10, 2 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Y10, 1 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Y10, 0 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  //---------1の位-------------
  b = bitRead( Y01, 3 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Y01, 2 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Y01, 1 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Y01, 0 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
}
//********** タイムコード(曜日、うるう秒)送信 ************
void Tcode_Weekday_Uruu_Send(){
  uint8_t b;
 
  //---------曜日-------------
  b = bitRead( Weekday, 2 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Weekday, 1 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  b = bitRead( Weekday, 0 );
  Serial.print( b );
  Time_Code_Bit_Generate( b );
  //---------うるう秒---------
  Serial.print('0');
  Time_Code_Bit_Generate(0); //LS1:うるう秒
  Serial.print('0');
  Time_Code_Bit_Generate(0); //LS2:うるう秒
  //--------------------------
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  Serial.print('0');
  Time_Code_Bit_Generate(0);
  Serial.print('0');
  Time_Code_Bit_Generate(0);
}
//********** タイムコードビット送信 *****************
void Time_Code_Bit_Generate(uint8_t b){
  if( b ){ //2進数の1
    Generator_High(500);
    Generator_Low(500);
  }else{ //2進数の0
    Generator_High(800);
    Generator_Low(200);
  }
}
//*********** 40kHz High レベル発信******************
void Generator_High(uint16_t H_time){
  int i;
  uint16_t H_cnt = 493; //Highレベルが12.5μs になるようにこの値を微調整
  uint16_t L_cnt = 422; //LOWレベルが12.5μs になるようにこの値を微調整
 
  uint32_t LastTime1 = millis();
  while(1){
    digitalWrite(Denpa_pin, HIGH);
    for(i=0; i<H_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    digitalWrite(Denpa_pin, LOW);
    for(i=0; i<L_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    if(millis() - LastTime1 == H_time) break;
  }
}
//*********** 40kHz LOW レベル ******************
void Generator_Low(uint32_t H_time){
  digitalWrite(Denpa_pin, LOW);
  delay(H_time);
}
//*********** その年の1月1日からの通算日計算 ******
uint16_t TotalDay(){
  //Arduino Timeライブラリ関数を使う
  tmElements_t tm = {0, 0, 0, 0, 1, 1, (uint8_t)(year()-1970)};
  time_t t = makeTime(tm);
  return (now() - t) / SECS_PER_DAY + 1;
}
//*********** 現在時刻代入 ************************
void Now_Time(){
  Min = minute();
  Hour = hour();
  Year = year() - 2000;
  if(Year > 100) Year = 0; //有り得ない数値は0にする
  Weekday = weekday() - 1;
  T_day = TotalDay();
  Serial.printf("Year=%d / TotalDay=%d / Weekday=%d / %02d:%02d\r\n", Year, T_day, Weekday, Hour, Min);
}
//********* NTP time GET ***************************
time_t Get_Ntp_Time(){
  while (Udp.parsePacket() > 0) ;
  Serial.println("Transmit NTP Request");
  Send_NTP_Packet(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * 3600UL;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}
//******** Send NTP Packet *************************
void Send_NTP_Packet(IPAddress &address){
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;         
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

【解説】

前回記事と重複しているところは省略します。

●1-2行:
Arduino core for ESP32 標準のライブラリインクルードです。
WiFi を使ってインターネットに接続するために使います。

●3行目:
Arduino 標準の Time ライブラリのインクルードです。

●10-11行:
時刻変数やパリティビットについては、関数を跨いで使用するので、グローバル変数領域で宣言しておきます。

●14-20行:
NTPサーバーClient のサンプルスケッチの初期化をそのまま流用しています。
16行目ではNTPタイムサーバーのサーバーネームを定義します。
私が一番取得し易いサーバーが “time.windows.com” でした。
ご自分の都合の良いサーバーに指定してください。

●28-44行:
ここでWi-Fiルーター(アクセスポイント)接続します。

●46行:
digitalWrite 関数でGPIO の High-Low を出力する場合、忘れずに pinMode 関数を使って出力設定にしておきます。

●48行目:
ここで、サーバーネームをIPアドレスに変換しています。

●52-55行:
Get_Ntp_Time関数は 366行で定義しています。
ここで、NTPサーバーから時刻を取得して、ESP-WROOM-32 の内蔵時計をセットしています。
55行目ではその時刻をさらに1秒進めています。
なぜかというと、このまま電波時計を合わせても、電話の時報よりどうしても1秒遅くなってしまうので、ここで1秒早めておきます。

●59-61行:
電波時計用電波発信のパルスを遅らせることが出来ないので、メインループとは別タスクで実行します。
60行で ESP32 のもう一つのCPU(コア番号0)で電波を発信させています。
それが、72-105行になります。

ESP32 ( ESP-WROOM-32 )のマルチタスク(デュアルコア)の使い方については以下の記事を参照してください。

Arduino – ESP32 のマルチタスク ( Dual Core ) を試す

●64-70行:
メインループの CPUコア番号は 1 です。
ここで30分毎に NTPサーバーから時刻を取得して、内蔵時計を補正します。

●72-105行:
マルチタスク、CPUコア番号0 のループ動作です。
74-79行では、時刻がゼロ秒の途中からこのタスクに入ってしまったら、秒を標準時とピッタリ合わせることが出来ないので、次のゼロ秒まで待ちます。
79行目のwhileループ内では最低delay(1)などの休止期間がないと、監視用のウォッチドッグタイマが動作できないので、必ず delay(1) を置きます。

83-104行で、規定に乗っ取ってタイムコードを送信します。
標準電波 JJY のタイムコード構成については、前回の記事を参照してください。

ここで重要なのが、85行目で現在時刻を各変数に代入しているということです。
時刻ゼロ秒のマーカーを発信する時点の時刻を変数に代入する必要があります。
タイムコードはトータル1分かけて出力するので、一番最初のマーカーを送信した時点の時刻でなければいけません。
1分以内だから途中で時刻が変化することはあり得ませんが、NTPサーバーから時刻取得して補正することが途中で入るとややこしくなるからです。

98行目でトータルタイムをミリセコンド単位でシリアルモニターに表示させています。
Totalがピッタリ 60000ms になっていることが重要です。
これがズレてしまうと、タイムコード発信もだんだんとズレていってしましますので要注意です。

もし、長時間運用でズレていってしまったら、100-103行で次のゼロ秒まで待ちます。
でも、今のところこれは不要かも知れません。

●107-111行:
マーカーやポジションマーカーはここで生成します。

●113-146行:
マーカーの後に分のタイムコードを送信します。
114-115行で、2桁の分表示を十の位と一の位に分割する計算です。
116行でパリティ―ビット計算の為のビットを格納しておく配列定義です。

119-142行で2桁の数値をそれぞれ BCD値に変換してタイムコード送信しています。
145行で予めパリティビット計算しておきます。

●148-183行:
時間ビット送信は分とほぼ同じ構成です。

●185-248行:
その年の1月1日からの通算日を出力します。
185-187行では3桁の10進数を各桁に分ける方法がちょっと難しくなりますね。
積算日は、349-354行にあるように、Timeライブラリの関数を使って計算します。

219行にポジションマーカー P3 を送信することを忘れないでください。

242-244行でパリティビットを送信しています。

●250-284行:
年の下2桁のみをビット送信しています。

●286-313行:
曜日と「うるう」秒ビットを送信しています。
「うるう」秒はNTPサーバーから時刻を取得しているので、ここは常にゼロで良いです。

●325-342行:
ここが心臓部です。
whileループ内で 40kHz のパルスをGPIO に出力するわけですが、1波長が 25μsという細かい時間を設定することが難しいのです。
delayMicroseconds関数がうまく機能すれば問題無かったのですが、Arduino core for ESP32 ではパルスの長さをマイクロセコンド単位で微調整できませんでした。

そこで、NOP関数を使います。
これについては前回の記事を参照してください。

ただ、オシロスコープで波形を見ると、340行の条件式があるせいか、微妙にパルス幅が変わったり、意図しない休止期間があったりして、完璧な40kHz を発信することができませんでした。
SPI通信のように、ビットを送信するところだけ安定したパルスが出ればよいという場合はこれでも良いのですが、標準電波は最大800ms もの長い間、安定した40kHz を発信し続けなければならないので、マイコンのGPIOではちょっと無理のような気がします。
搬送波の発信は別途発信機器を設けた方が良いと改めて思いました。

しかし、これでも問題無く受信できているのでヨシとします。

●344-347行:
Lowレベル発信はdelay関数を使って、一切発信しないようにしました。
前回の記事でも述べたように、ESP32 のウォッチドッグタイマ(WDT)を動作させるためにもこの方が都合良いためです。

●349-354行:
Timeライブラリ関数を使って、その年の1月1日からの通算日を計算しています。
これは、以下の記事を参照して、ほぼそのまま使用させていただきました。

Arduino で電波時計を合わせよう

●356-364行:
ここで、時刻を各変数に代入しています。

●366-402行:
ESP8266用のUdpNtpClient のサンプルスケッチをそのまま流用しています。

コンパイル書き込み、実行

では、いよいよArduino IDE でコンパイル書き込みおよび実行させてみてください。

こちらの記事のコメント欄で、Kat-Kai さんからとても有効な情報をいただきました。
ESP32 のクロックカウントから正確な時間経過をμセコンドオーダーで微調整ができるとのことです。
パルス長が格段に安定しますので、是非試してみて下さい。
また、コンパイルする時に、Arduino IDE の Core Debug Level を "なし" に選択してください。
Debugモードにしてしまうと、100ms毎にウォッチドッグタイマが作動して、パルス周波数が不安定になりますのでご注意ください。

Arduino IDE のシリアルモニターを 115200 bps で起動します。
日本標準時刻がゼロ秒になっていなければ、ゼロ秒になるまでタイムコード生成するのを待ちます。

ゼロ秒になったら下図の様に表示されます。

マーカー送信、分送信、マーカー送信、時送信・・・という感じで、1ビットを1秒間隔で送信していることが分かると思います。
全体でピッタリ60秒で収まるようになっています。

シリアルモニターのタイムコード送信動作状況の動画はこんな感じです。

日本標準電波のタイムコード例のページを見てもイマイチよく分からなかったのですが、こうやって動かすと良く理解できると思います。

実際に電波時計合わせた動画は先に紹介しましたので、それをご覧ください。

電波時計によってはすぐに同期して修正してくれるものと、10分以上経ってから修正されるものといろいろです。

電波時計の取扱説明書を見ると、AUTO受信設定されているものは、最初に60kHz搬送波の受信をし始めて、受信できなければ 40kHz 搬送波を受信するという設定になっていたりします。

正常に受信できれば、概ね15分以内で時計が修正されるようです。
早いものでは、3分くらいで受信するものがありました。

Amazon.co.jp で買ったこの電波腕時計

これは、最初の5分間で60kHz の電波を受信し、失敗したら次の5分間で 40kHz を受信するとのことです。

また、40kHzの搬送波を出しているつもりでも、60kHz で受信したりします。
パルス波(方形波)なので、仕方ないところですね。

下図の様にアンテナ線にくっ付けるように時計を置くと電波を受信しやすいです。
アンテナ線上が一番電波が飛んでいます。

ただ、電波の偏波面、いわゆる方向があるので、時計の置き方によっては受信が悪くなります。
いろいろ置き方を変えたりして一番受信し易い方向を探ってみて下さい。

うまく受信できない場合

うまく受信できない場合があると思います。
その場合、時計の取扱い説明書をもう一度読み返してみて、40kHz受信と60kHz 受信を切り替えてみて下さい。

その他、電波時計によっては搬送波の周波数が同期しない場合があります。
私の場合は10年前くらいに買った古い電波腕時計がなかなか受信しませんでした。

可能ならば 100MHz クラスのオシロスコープでGPIO の波形を計測して、1パルスが25μsになっているかどうか確認しながら、ソースコードの327-328行目の数値を変えてみて下さい。

ESP-WROOM-32 ( ESP32 )にも個体差があって、上記で紹介したプログラムでは40kHz からズレてしまっているかも知れません。

例えば、私の場合は、ほぼピッタリ25μsのパルス長に調整しても、オシロスコープのFFT解析で周波数を見ると39.5kHz だったりします。

ですから、私はプログラムの325-342行を以下の3種類でいろいろ試して、よく受信できる方を使っています。

325-342行 例1

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void Generator_High(uint16_t H_time){
  int i;
  uint16_t H_cnt = 493; //Highレベルが12.5μs になるようにこの値を微調整
  uint16_t L_cnt = 422; //LOWレベルが12.5μs になるようにこの値を微調整

  uint32_t LastTime1 = millis();
  while(1){
    digitalWrite(Denpa_pin, HIGH);
    for(i=0; i<H_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    digitalWrite(Denpa_pin, LOW);
    for(i=0; i<L_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    if(millis() - LastTime1 == H_time) break;
  }
}

325-342行 例2

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void Generator_High(uint16_t H_time){
  int i;
  uint16_t HL_cnt = 460; //1パルスが25μsになるようにこの数値を微調整
  uint32_t LastTime1 = millis();
  
  while(1){
    if(millis() - LastTime1 >= H_time) break;

    digitalWrite(Denpa_pin, HIGH);
    for(i=0; i<HL_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    digitalWrite(Denpa_pin, LOW);
    for(i=0; i<HL_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
  }
}

325-342行 例3

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void Generator_High(uint16_t H_time){
  int i;
  uint16_t H_cnt = 481; //FFTアナライザで40kHzになるようにこの値を微調整
  uint16_t L_cnt = 411; //FFTアナライザで40kHzになるようにこの値を微調整
  
  uint32_t LastTime1 = millis();
  while(1){
    digitalWrite(Denpa_pin, HIGH);
    for(i=0; i<H_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    digitalWrite(Denpa_pin, LOW);
    for(i=0; i<L_cnt; i++){
      NOP();  //No Operation(無演算命令)関数
    }
    if(millis() - LastTime1 == H_time) break;
  }
}

例2のように if文の条件式も、 == とするのと、>=とするのでは計算速度が違っているように思います。

ということで、安定した搬送波を得るためには、やはりマイコンのGPIO ではかなり難しいものがありますね。
良いライブラリがあればそれを使うか、別途デバイスで搬送波を発信させる方が良いですね。

まとめ

どうでしょうか?
うまく動作しましたでしょうか?

結局のところ、この簡易プログラムでは安定した搬送波とは程遠いので、うまく受信できない場合があると思います。
その場合は40kHz または60kHzのサイン波を発信できるデバイスを別途構成した方が現実的ですね。
それはいつか試してみたいと思います。

また、普段は取扱説明書など読まなかった電波時計ですが、こうやって実験してみると電波時計のしくみが何となく分かってきてオモシロイですよね。
電波の送信ができれば、受信もできそうな気がします。
これを突き詰めると、無線装置開発なんていう夢が膨らんでしまいますね・・・。

イカンイカン・・・。
またドロ沼にハマりそうでした。

ところで、前回の記事でも言いましたが、くれぐれも電波を増強したりして微弱電波機器の範囲を超えて電波法違反にならないように注意してください。

以上、今回はここまでです。

ではまた・・・。

この記事以降、以下の記事でさらに電波時計を合わせやすくするものを作りました。
波形発生器で純粋なサイン波を発生させて搬送波を作っています。
ただし、自己満足工作で、大げさです。
合わせてご覧ください。
https://www.mgo-tec.com/blog-entry-ad9833-sign-radio-clock-jjy-esp32.html

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/11/21 16:13時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥19,358(2024/11/21 21:56時点)
Excelでわかるディープラーニング超入門
技術評論社
¥2,068(2024/11/21 12:24時点)

コメント

タイトルとURLをコピーしました