2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

電波時計時刻合わせ(JJYシミュレータ)

2
Last updated at Posted at 2026-06-20

マンションだからか、電波時計に電波が届きません。これまでズレてくるとベランダに置いたりしていましたが、それって???って思っていたので、対策を考えました。

インターネット(NTPサーバー)から時刻を自動取得し、日本の標準電波(JJY 40kHz)のタイムコード信号を自ら生成します。

いつも使っている ESPrDeveloper(ESP8266)を使いました。

準備したもの

コイル(アンテナ)

ポイントはアンテナだと思ったのですが、自分でコイルを巻くのは技術的に怪しいと思い(^^;、既存の電波時計からコイルを取り出すことにしました。
とは言っても、新品を買うような話しじゃないと思ったので、ヤフオクで、ジャンクの電波時計をゲット。コイルが壊れていることはないと信じて、コイルを分解します。

今回は、マルマン電波時計 GREENWICH MJA370 を購入しました。

分解すると下記な感じです。

IMG_0725.jpeg

電流がたくさん流れると、マイコンが壊れると思ったので、抵抗も準備、ちゃんと計算してませんが、なんとなく、余っていた1kΩの抵抗も準備しました。

電池ボックス

いつもは、USB電源でやっているのですが、時計のところにコンセントがないので、電池駆動も考慮しました。
おもちゃを分解したときに取っておいた単4の電池ボックスを流用。
アマゾンで買うほどじゃないと思って・・

スライドスイッチ

電流が逆流しないように、電池を使うときにはスイッチでつなぐようにします。
これも、なんかのときに買ったスライドスイッチの余り物を流用。タミヤのスライドスイッチです。
もったいないけど、6Pのいいスイッチです。

OLED(おまけ)

機能的に動いているのかいないのかわかりずらい工作だったので気休めで、画面もつけることにしました。
以前CO2センサーを作ったときに、余計に買った、OLED(SSD1366)があるので、ステータスを表示するようにしました。
ESP8266は、SSD1366を簡単に制御できるので楽ちんです。

配線図

こんな配線です。
JJY配線図.png

部品のはんだ付け

今回は抵抗(容量は割とテキトー)とか電池ボックスとかあったので、はんだ付けが多かったです。組み立てしやすいように、ジャンパー線にハンダ付けしました。

IMG_0727.jpeg

組み立て

写真取るの忘れましたが、これもいつも使っている、ダイソーの糸ようじのケースに穴をあけてパーツを入れます。
スイッチはネジ止めしましたが、他は、クロコダイルテープ(両面テープ)で止めてるだけです。

ソフトウェア

下記がコードです。
JJYに仕様は下記に公開されています。
https://www.nict.go.jp/sts/jjy_signal.html

#include <ESP8266WiFi.h>
#include <time.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const char* ssid="自分のSSID";
const char* password="自分のWiFiパスワード";

const int JJY_PIN = 14; // アンテナを繋ぐピン (14番ピン)
#define J_P 2           // ポジションマーカー
#define J_1 1           // 1
#define J_0 0           // 0

int jjy_timecode[60];   // 1分(60秒)分の信号データを格納する配列

// OLEDの設定 (画面サイズ 128x64)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(115200);
  pinMode(JJY_PIN, OUTPUT);
  digitalWrite(JJY_PIN, LOW);

  // OLED I2Cの初期化 (ESPr Developer標準の 4=SDA, 5=SCL)
  Wire.begin(4, 5);
  
  // 【画面がつかない対策】アドレス0x3Cで初期化。ダメなら0x3Dを試す処理
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("OLED 0x3C failed. Trying 0x3D..."));
    if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) {
      Serial.println(F("SSD1306 allocation failed. Check wiring."));
      for(;;); // 両方ダメなら停止
    }
  }
  
  // 画面が初期化できたら表示をクリアして文字を出す
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,0);
  display.println("[JJY SIMULATOR]");
  display.println("Connecting Wi-Fi...");
  display.display();

  // 1. Wi-Fiに接続してNTP時刻を取得
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  
  display.println("Wi-Fi Connected!");
  display.println("Fetching NTP Time...");
  display.display();

  // 日本標準時(JST)のNTPサーバーを設定
  configTime(9 * 3600, 0, "ntp.nict.jp", "://google.com");
  delay(5000); // 同期完了まで5秒待機
  
  // 2. 時刻が取れたら電池節約のためWi-Fi機能をオフにする
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  
  display.println("NTP OK. Wi-Fi OFF.");
  display.println("Waiting for 00s...");
  display.display();
}

// 40kHzのパルス信号を正確な時間(ミリ秒)だけ出力する関数
void pulse40kHz(unsigned long duration_ms) {
  unsigned long start = millis();
  while (millis() - start < duration_ms) {
    digitalWrite(JJY_PIN, HIGH);
    delayMicroseconds(12); // 40kHzの周期(ON: 12.5us / OFF: 12.5us)
    digitalWrite(JJY_PIN, LOW);
    delayMicroseconds(13);
  }
}

// 1秒分の電波を送る(JJYのルール)
void sendJJYBit(int bitType) {
  if (bitType == J_0) {
    pulse40kHz(800); // 0.8秒ON
    delay(200);      // 0.2秒OFF
  } else if (bitType == J_1) {
    pulse40kHz(500); // 0.5秒ON
    delay(500);      // 0.5秒OFF
  } else if (bitType == J_P) {
    pulse40kHz(200); // 0.2秒ON
    delay(800);      // 0.8秒OFF
  }
}

// 現在の「分」「時」「1月1日からの通算日」「年」「曜日」をJJYのBCDフォーマットに変換
void setupJJYFrame(struct tm *tm_time) {
  for(int i=0; i<60; i++) jjy_timecode[i] = J_0;
  
  // 固定マーカー(P)の配置 (毎分0, 9, 19, 29, 39, 49, 59秒)
  jjy_timecode[0]  = J_P; jjy_timecode[9]  = J_P; jjy_timecode[19] = J_P;
  jjy_timecode[29] = J_P; jjy_timecode[39] = J_P; jjy_timecode[49] = J_P;
  jjy_timecode[59] = J_P;

  // 「分」の書き込み
  int m = tm_time->tm_min;
  if ((m / 10) & 4) jjy_timecode[5] = J_1;
  if ((m / 10) & 2) jjy_timecode[6] = J_1;
  if ((m / 10) & 1) jjy_timecode[7] = J_1;
  if ((m % 10) & 8) jjy_timecode[1] = J_1;
  if ((m % 10) & 4) jjy_timecode[2] = J_1;
  if ((m % 10) & 2) jjy_timecode[3] = J_1;
  if ((m % 10) & 1) jjy_timecode[4] = J_1;

  // 「時」の書き込み
  int h = tm_time->tm_hour;
  if ((h / 10) & 2) jjy_timecode[12] = J_1;
  if ((h / 10) & 1) jjy_timecode[13] = J_1;
  if ((h % 10) & 8) jjy_timecode[15] = J_1;
  if ((h % 10) & 4) jjy_timecode[16] = J_1;
  if ((h % 10) & 2) jjy_timecode[17] = J_1;
  if ((h % 10) & 1) jjy_timecode[18] = J_1;

  // 「通算日」の書き込み
  int d = tm_time->tm_yday + 1;
  if ((d / 100) & 2) jjy_timecode[22] = J_1;
  if ((d / 100) & 1) jjy_timecode[23] = J_1;
  if (((d % 100) / 10) & 8) jjy_timecode[25] = J_1;
  if (((d % 100) / 10) & 4) jjy_timecode[26] = J_1;
  if (((d % 100) / 10) & 2) jjy_timecode[27] = J_1;
  if (((d % 100) / 10) & 1) jjy_timecode[28] = J_1;
  if ((d % 10) & 8) jjy_timecode[30] = J_1;
  if ((d % 10) & 4) jjy_timecode[31] = J_1;
  if ((d % 10) & 2) jjy_timecode[32] = J_1;
  if ((d % 10) & 1) jjy_timecode[33] = J_1;

  // 「年(下2桁)」の書き込み
  int y = tm_time->tm_year % 100;
  if ((y / 10) & 8) jjy_timecode[41] = J_1;
  if ((y / 10) & 4) jjy_timecode[42] = J_1;
  if ((y / 10) & 2) jjy_timecode[43] = J_1;
  if ((y / 10) & 1) jjy_timecode[44] = J_1;
  if ((y % 10) & 8) jjy_timecode[45] = J_1;
  if ((y % 10) & 4) jjy_timecode[46] = J_1;
  if ((y % 10) & 2) jjy_timecode[47] = J_1;
  if ((y % 10) & 1) jjy_timecode[48] = J_1;
  
  // 「曜日」の書き込み
  int w = tm_time->tm_wday;
  if (w & 4) jjy_timecode[50] = J_1;
  if (w & 2) jjy_timecode[51] = J_1;
  if (w & 1) jjy_timecode[52] = J_1;
}

void loop() {
  time_t t = time(NULL);
  struct tm *tm_time = localtime(&t);

  // 毎分0秒になった瞬間に、次の1分間の電波を発信開始
  if (tm_time->tm_sec == 0) {
    setupJJYFrame(tm_time);
    
    // 60秒間の送信ループ
    for (int i = 0; i < 60; i++) {
      display.clearDisplay();
      display.setCursor(0,0);
      
      display.setTextSize(2); // 時刻を大きく表示
      display.printf("%02d:%02d\n", tm_time->tm_hour, tm_time->tm_min);
      
      display.setTextSize(1);
      display.println("\n[JJY SENDING]");
      display.printf("Progress: %02d / 60s\n", i);
      
      display.print("Signal: ");
      if(jjy_timecode[i] == J_0) display.println("0 (0.8s)");
      else if(jjy_timecode[i] == J_1) display.println("1 (0.5s)");
      else display.println("P (0.2s)");
      
      display.display();

      sendJJYBit(jjy_timecode[i]);
    }
  }
  
  // 待機画面表示
  display.clearDisplay();
  display.setCursor(0,0);
  display.setTextSize(2);
  display.printf("%02d:%02d:%02d\n", tm_time->tm_hour, tm_time->tm_min, tm_time->tm_sec);
  display.setTextSize(1);
  display.println("\nSyncing internal clock...");
  display.display();
  
  delay(100); 
}

おためし実行

IMG_0729.jpeg

電波時計をとなりに置いて、USBつないで実行。

強制受信にします。

アンテナマークが点滅しているから受信してるのかな??
全然わかんない・・

ちょっと、放置・・・

(10分くらいしてもどったら)、時間あってるかも!

やったー。

めでたしめでたし。

課題

電池でもやってみました。

途中で、画面が消えちゃったり、再起動したりします。

電力が足らないのか・・

疲れたので、今日はここまで・・

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?