家の電波時計を合わせるためにJJYシミュレーターを作りました。

公開日: 2021年03月10日最終更新日: 2021年11月25日

thumb

家にある電波時計を合わせたい

うちにある電波時計が標準電波を全く受信しなくて困っています。 家の立地的な問題なんでしょうが、少しずつずれていくのでなんとかしたいです。 そこで、NTPで時刻同期して標準電波を模擬して出してあげれば時刻同期できるんじゃないかと考えました。

ネットで調べてみるとこれはJJYシミュレーターというらしく、作っている人が何人か見つかりました。電波出すのでやっていいものかと気になったのですが、微弱無線の範囲内で行えば問題ないようです。

今回は、家にある電波時計の時刻を合わせるためにJJYシミュレーターを作成したときのことを記録に残しておきます。

以下は波形やLC共振回路など詳しく書いてありとても参考になりました。

標準電波の内容

標準電波の仕様は、標準電波の出し方についてに仕様が記載されています。

信号の仕様がすごく気になるので以下のようになっている理由を時間があるときに調べてみたい。人の目に見やすいかと言われると必ずしもそうでなく、冗長化と考えても最適ではなさそう?

  • 二進化十進法
  • 10の位と1の位の間に0を挟んでいたり、位置を揃えるために0を入れている
  • マーカーの必要性
  • 0を入れているせいで4bit区切りになっていない

回路の作成

LC発振回路でSin波を作ってPWMで制御するのが良さそうですが、今回は手抜きで行きます。 矩形波は正弦波の合成でできているのでローパスフィルターを使って低周波だけを取り出すようにします。また、振幅変調(AM)で、Lowが10%とありますが0%でも行けそうな気がしますので試してみます。

RLCローパス・フィルタ計算ツールを使って抵抗、インダクタ、コンデンサの値を決めます。
それと電流を制限するために抵抗(100Ω)を挟んでおきます。電流計で計測しながら電流が数mAになるちょうどいい抵抗値を探します。

回路は以下のようになりました。 jjy-circuit

アンテナは適当なリード線やエナメル線をループにして使います。

コード

ESP32のCPU2コアを活用して、NTP時刻同期と電波送信をマルチスレッドで処理します。 xTaskCreatePinnedToCoreを使うと別スレッドでタスクを実行できます。

#include <WiFi.h>

#define NTP_SYNC_INTERVAL 30 * 60 * 1000
#define TIMECODE_LENGTH 60
#define FREQUENCY 40 * 1000
#define LEDC_CHANNEL 0
#define LEDC_TIMER_BIT 10    //max 16bit
#define LEDC_BASE_FREQ 40000 //40kHxz
#define LEDC_PIN 27

//
enum Pulse
{
  ZERO = 800,
  ONE = 500,
  MARKER = 200
};

const char *ssid = "WLAN24";
const char *password = "213a12bb25b01";

void setup()
{
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
  WiFi.disconnect();

  if (WiFi.begin(ssid, password) != WL_DISCONNECTED)
  {
    ESP.restart();
  }

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(1000);
  }

  Serial.println("Connected to the WiFi network!");

  ledcSetup(LEDC_CHANNEL, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcAttachPin(LEDC_PIN, LEDC_CHANNEL);
  ledcWrite(LEDC_CHANNEL, 0x0000);

  syncNTPdatetime();
  delay(3000);
  xTaskCreatePinnedToCore(syncNTPTask, "syncNTPTask", 4096, NULL, 2, NULL, 1);
}

struct tm timeInfo; //時刻を格納
char s[32];         //文字格納用

void loop()
{
  getLocalTime(&timeInfo);
  Serial.print("START: ");
  printCurrentHoldTime();
  // 正分を待つ
  while(timeInfo.tm_sec != 0)
  {
    Serial.print('.');
    getLocalTime(&timeInfo);
    delay(10);
  }

  Pulse timeCode[TIMECODE_LENGTH] = {};
  delay(1);

  buildTimeCode(timeInfo, timeCode);
  sendTimeCode(timeCode);
}

void syncNTPTask(void *arg)
{
  while (1)
  {
    delay(NTP_SYNC_INTERVAL);
    syncNTPdatetime();
  }
}

void syncNTPdatetime()
{
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); //NTP同期
  getLocalTime(&timeInfo);
  Serial.println("sync");
  printCurrentHoldTime();
}

void printCurrentHoldTime()
{
  sprintf(s, " %04d/%02d/%02d %02d:%02d:%02d",
          timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
          timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);

  Serial.println(s);
}

void buildTimeCode(const tm timeInfo, Pulse timeCode[TIMECODE_LENGTH])
{
  Serial.println("buildTimeCode");
  timeCode[0] = MARKER;

  // 分を二進化十進数に変換
  int upperMin = timeInfo.tm_min / 10;
  Serial.println(upperMin);
  timeCode[1] = (upperMin >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[2] = (upperMin >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[3] = (upperMin & 0x01) == 1 ? ONE : ZERO;
  timeCode[4] = ZERO;

  int lowerMin = timeInfo.tm_min % 10;
  Serial.println(lowerMin);
  timeCode[5] = (lowerMin >> 3 & 0x01) == 1 ? ONE : ZERO;
  timeCode[6] = (lowerMin >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[7] = (lowerMin >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[8] = (lowerMin & 0x01) == 1 ? ONE : ZERO;

  timeCode[9] = MARKER;

  // 時を二進化十進数に変換
  int upperHour = timeInfo.tm_hour / 10;
  Serial.println(upperHour);
  timeCode[10] = ZERO;
  timeCode[11] = ZERO;
  timeCode[12] = (upperHour >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[13] = (upperHour & 0x01) == 1 ? ONE : ZERO;

  timeCode[14] = ZERO;

  int lowerHour = timeInfo.tm_hour % 10;
  Serial.println(lowerHour);
  timeCode[15] = (lowerHour >> 3 & 0x01) == 1 ? ONE : ZERO;
  timeCode[16] = (lowerHour >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[17] = (lowerHour >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[18] = (lowerHour & 0x01) == 1 ? ONE : ZERO;

  timeCode[19] = MARKER;

  // 1月1日からの通算日を二進化十進数に変換
  int yearDay = timeInfo.tm_yday + 1;

  int upperYearDay = yearDay / 100;
  Serial.println(upperYearDay);
  timeCode[20] = ZERO;
  timeCode[21] = ZERO;
  timeCode[22] = (upperYearDay >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[23] = (upperYearDay & 0x01) == 1 ? ONE : ZERO;

  timeCode[24] = ZERO;

  int midYearDay = (yearDay / 10) % 10;
  Serial.println(midYearDay);
  timeCode[25] = (midYearDay >> 3 & 0x01) == 1 ? ONE : ZERO;
  timeCode[26] = (midYearDay >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[27] = (midYearDay >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[28] = (midYearDay & 0x01) == 1 ? ONE : ZERO;

  timeCode[29] = MARKER;

  int lowerYearDay = yearDay % 10;
  Serial.println(lowerYearDay);
  timeCode[30] = (lowerYearDay >> 3 & 0x01) == 1 ? ONE : ZERO;
  timeCode[31] = (lowerYearDay >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[32] = (lowerYearDay >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[33] = (lowerYearDay & 0x01) == 1 ? ONE : ZERO;

  timeCode[34] = ZERO;
  timeCode[35] = ZERO;

  // パリティ
  int parity1 = 0;
  for (int i = 12; i <= 18; i++)
    parity1 ^= (timeCode[i] == ONE ? 1 : 0);
  Serial.println(parity1);
  int parity2 = 0;
  for (int i = 1; i <= 8; i++)
    parity2 ^= (timeCode[i] == ONE ? 1 : 0);
  Serial.println(parity2);

  timeCode[36] = (parity1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[37] = (parity2 & 0x01) == 1 ? ONE : ZERO;

  timeCode[38] = ZERO;
  timeCode[39] = MARKER;
  timeCode[40] = ZERO;

  // 年を(ry
  int upperYear = timeInfo.tm_year % 100 / 10;
  Serial.println(upperYear);
  timeCode[41] = (upperYear >> 3 & 0x01) == 1 ? ONE : ZERO;
  timeCode[42] = (upperYear >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[43] = (upperYear >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[44] = (upperYear & 0x01) == 1 ? ONE : ZERO;

  int lowerYear = timeInfo.tm_year % 10;
  Serial.println(lowerYear);
  timeCode[45] = (lowerYear >> 3 & 0x01) == 1 ? ONE : ZERO;
  timeCode[46] = (lowerYear >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[47] = (lowerYear >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[48] = (lowerYear & 0x01) == 1 ? ONE : ZERO;

  timeCode[49] = MARKER;

  // 曜日
  int day = timeInfo.tm_wday;
  Serial.println(day);
  timeCode[50] = (day >> 2 & 0x01) == 1 ? ONE : ZERO;
  timeCode[51] = (day >> 1 & 0x01) == 1 ? ONE : ZERO;
  timeCode[52] = (day & 0x01) == 1 ? ONE : ZERO;

  // うるう秒、とりあえずなし。
  timeCode[53] = ZERO;
  timeCode[54] = ZERO;

  timeCode[55] = ZERO;
  timeCode[56] = ZERO;
  timeCode[57] = ZERO;
  timeCode[58] = ZERO;
  timeCode[59] = MARKER;

  for (int i = 0; i < TIMECODE_LENGTH; i++)
  {
    Serial.print(timeCode[i]);
  }
  Serial.println("");
}

void sendTimeCode(const Pulse timeCode[TIMECODE_LENGTH])
{
  for (int i = 0; i < TIMECODE_LENGTH; i++)
  {
    genearatePulse(timeCode[i]);
  }
}

void genearatePulse(Pulse p)
{
  ledcWrite(LEDC_CHANNEL, 0x0200);
  delay(p);
  ledcWrite(LEDC_CHANNEL, 0x0000);
  delay(1000 - p);
}

動作確認

腕時計、目覚まし時計、置き時計すべての時刻を合わせることができました。 実際に時計が合うのを見ていると達成感があって面白いです。 微弱電波の範囲ということで受信距離は90cm未満でした。 JJYシミュレーターと時計を近付けないといけないのでちょっと面倒くさいですが仕方ありません。 いくつか作って時計の近くに置いておくという手もありますね。