電波時計モジュールで受信したJJYタイムコードを復号した日時データとRTCモジュールRX8900を同期させて時刻合わせを行うLCD時計を作りました。
ビル内など電波を良好に受信できない環境では、疑似JJY電波を発生させるスマートフォンアプリを使っていつでも時刻合わせができます。
電波時計モジュールでJJY電波(タイムコード)から日時情報の取得
長波標準電波の伝搬については、NICT-情報通信研究機構サイト「標準電波の伝播(電波の飛び方)、都道府県別の電界強度の予測値」で詳しい説明があります。
JJY電波(タイムコード)
公開されている「JJY – 標準電波で送信する時刻符号 」「JJY標準電波の出し方について」を参考に受信した信号(疑似JJY信号)をデコードします。
- 1周期60秒(60ビット)の組み合わせ。受信完了時には1分前の時刻となる。
- 1分加算と桁上がり(分、時、日、年、うるう年、曜日)を計算。
- 6つのゾーン(分、時、通算日、西暦(下2桁)、曜日など)に分かれている。
- 1月から順に月の日数を引いて月、日を計算。月の日数はうるう年を考慮。
- 秒の信号は、パルス信号の立ち上がり。パルス幅の長さによって、その秒での情報を符号化。
- ゾーン毎に0.2秒のマーカーパルス( M(0秒)、P1(9秒)、P2(19秒)、P3(29秒)、P4(39秒)、P5(49秒)、P0(59秒) )が出る。その後に続くパルス幅 0.5秒が「1」、0.8秒が「0」。
- 59秒と0秒でマーカーが2つ重なることで先頭のM(0秒)を判定。
- 今回のスケッチではP0~P5も表記はMで処理。
- 毎時15分、45分の40秒からは呼出符号 JJY(モールス符号)列が送出されるため、この間は秒信号、年のデータが取得できない。
- 今回のスケッチでは60秒間の全データが取得できない時はスキップ処理。
秒 | コード 「1」の重み |
0 | M |
1 | 40(分) |
2 | 20(分) |
3 | 10(分) |
4 | 0 |
5 | 8(分) |
6 | 4(分) |
7 | 2(分) |
8 | 1(分) |
9 | P1(M) |
10 | 0 |
11 | 0 |
12 | 20(時) |
13 | 10(時) |
14 | 0 |
15 | 8(時) |
16 | 4(時) |
17 | 2(時) |
18 | 1(時) |
19 | P2(M) |
秒 | コード 「1」の重み |
20 | 0 |
21 | 0 |
22 | 200(日) |
23 | 100(日) |
24 | 0 |
25 | 80(日) |
26 | 40(日) |
27 | 20(日) |
28 | 10(日) |
29 | P3(M) |
30 | 8日 |
31 | 4日 |
32 | 2日 |
33 | 1日 |
34 | 0 |
35 | 0 |
36 | パリティ(時) |
37 | パリティ(分) |
38 | 予備 |
39 | P4(M) |
秒 | コード 「1」の重み |
40 | 予備 |
41 | 80(年) |
42 | 40(年) |
43 | 20(年) |
44 | 10(年) |
45 | 8(年) |
46 | 4(年) |
47 | 2(年) |
48 | 1(年) |
49 | P5(M) |
50 | 4(曜日) |
51 | 2(曜日) |
52 | 1(曜日) |
53 | うるう秒 |
54 | うるう秒 |
55 | 0 |
56 | 0 |
57 | 0 |
58 | 0 |
59 | P0(M) |
参考:
・JJY – 標準電波で送信する時刻符号 | NICT-情報通信研究機構
標準電波のタイムコード、標準電波の伝播(電波の飛び方)、都道府県別の電界強度の予測値
・JJY標準電波の出し方について
・標準電波送信所
おおたかどや山標準電波送信所(40 kHz)
はがね山標準電波送信所(60 kHz)
スマホアプリの疑似JJY信号を使って時刻合わせ
ビル内では電波の受信環境が良くなかったのでスマホアプリの疑似JJY信号を使って時刻合わせしています。iPhone12(iOS16) と Pixel7 (Anroid13) で動作確認できました。
iPhoneアプリ:JJY Simulatorなど
Androidアプリ: JJYEmulatorなど
JJY Simulatorの使い方(iPhone画面右下の青い
アイコン)には「日本の電波時計の基準局(JJY)で使用される標準電波を模擬した信号を、13.3kHzの音声として出力し、3倍高調波(40kHz)を利用して時刻を合わせます」との記載があります。準備したパーツ
ネット通販(aitendo、秋月電子通商など)でパーツを集めてブレッドボードで組み立てました。
今回、電波時計モジュールにはC-MAX社AMレシーバーチップCME6005を使ったD606Cを使いました。
D606Cの動作電源は1.5〜3.5VなのでArduino利用時は注意。動作電圧が1.5〜5VのMJU823RCCもあるようです。
# | パーツ | 個数 | 備考 |
1 | 電波時計モジュール [D606C] 40/60KHz 6P仕様 バーアンテナ付属 出力信号:負論理 | 1 | aitendo |
2 | プルダウン抵抗 3.3kΩ | 3 | 手持ち |
3 | トランジスタ 2SC1815 | 1 | 手持ち |
4 | LED(赤色) | 1 | 手持ち |
5 | 電流制限抵抗 3.3kΩ | 1 | 手持ち |
6 | Arduino Uno Rev3 | 1 | 【M-07385】 |
7 | ブレッドボード EIC-801 | 1 | 【P-00315】 |
8 | ジャンパーワイヤ オス-オス 10cm | 適量 | 手持ち |
RTCジュールRX8900を使ったLCD時計のパーツです。温度補償発振器(DTCXO)を内蔵しており高精度で時刻を管理できます。
# | パーツ | 個数 | 備考 |
1 | RX8900CE UA DIP化モジュール | 1 | 【K-13009】 |
2 | I2C接続 16×2行 白色バックライト [ACM1602NI-FLW-FBW-M01] | 1 | 【P-05693】 |
3 | I2Cプルアップ抵抗 10kΩ ※モジュール内の抵抗を有効化しない場合 | 2 | 手持ち |
4 | 半固定ボリューム 3362P 10KΩ | 1 | 【P-03277】 |
5 | 電気二重層コンデンサ 1.5F 耐電圧 5.5V(タテ型) | 1 | 【P-04300】 |
6 | 保護抵抗 200Ω | 1 | 手持ち |
結線図
電波時計モジュール D606C のピンアサインのメモです。
ピン | 実装 | D606Cの仕様 |
V | 3.3V | 動作電源 1.5〜3.5V |
G | GND | GND |
F | プルダウン lowレベル | 動作モード設定: ・lowレベル → 40KHzモード おおたかどや山標準電波送信所(40 kHz、大鷹鳥谷山) https://jjy.nict.go.jp/LFstation/otakado/index.html ・highレベル → 60KHzモード はがね山標準電波送信所(60 kHz、羽金山) https://jjy.nict.go.jp/LFstation/hagane/index.html ※iPhoneアプリの疑似JJY信号出力(40KHz)に合わせた |
TN | 信号出力 | ネガティブ信号出力 |
TP | ー | ポジティブ信号出力 |
P | プルダウン | Power ON制御、lowレベルでON状態 |
RTCモジュールRX8900を使ったLCD時計部分の結線は本サイトの下記記事と同じです。この記事のスケッチでは、起動時にArduino IDEのコンパイル時間を使って時刻合わせを行いました。今回電波時計モジュールを追加したことでJJY信号の日時情報を使ってPC接続なしで時刻合わせできます。
スケッチ:受信状況をシリアルモニタに表示、RTCを更新してLCD表示
復号状況をシリアルモニタで観察
JJYのタイムコードに沿って、M、0、1のデコード進捗をリアルタイムでシリアルモニタに表示します。
配列に格納したデータが下記条件に合致すると「1」の重みづけに従って演算、日時を計算します。
・データ(0、1)と マーカー(M、P0~P5)の総数が「60」
・マーカー(M、P0~P5)の位置が 0秒、9秒、19秒、29秒、39秒、49秒、59秒 に存在
デジタルピンD2の入力信号波形をデジタルオシロスコープで観察
回路図中のTP端子(ArduinoデジタルピンD2とトランジスタ:2SC1815のコレクタ端子の間)にデジタルオシロスコープを接続して観察しました。
疑似JJYを受信した際のデジタルピンD2の入力信号波形は矩形波(振幅は約3.7V)でした。
RTCモジュールRX8900を更新、LCDに日時、受信状況を表示
時刻合わせしたい時に、疑似JJY信号を発生するアプリを起動して最大ボリュームで再生、スピーカ付近を電波時計モジュールのバーアンテナに密着します。起動直後の受信状況のLCD表示は「i n i」です。
2~3分ほど密着してLCD画面上に「r e c」が表示されると同期完了です。下記スケッチでは密着して受信している間は1分に1回の頻度でRTCを更新(同期)しつづけます。
スマートフォンを遠ざけたり、アプリを終了するなど60秒間の全データの受信が正しくできなくなるとLCD表示が「– – – 」変わり、RTCの更新(同期)処理は中断されます。RTCの更新(同期)が中断されてもLCDの時刻表示は続きます。
スケッチ(2023/02/26時点)
Arduino Uno Rev3で動作確認できたスケッチです。
スケッチ中の「//—-LCD時計x—」部分の5箇所がLCD時計に関係するスケッチ。それ以外のコードが電波時計モジュールから取得した日時信号をシリアルモニタに表示するスケッチです。LCD時計に関係するスケッチ部分を削除することでシリアルモニタのみで動作確認できます。
217~221行が電波時計モジュールで取得した日時データをLCD時計のRTCに書き込む箇所です。
arduino_radio_clock-v5.ino
※ここをクリックするとコード表示を開閉できます。
//下記スケッチ中の「//----LCD時計x---」5箇所がLCD時計に関係するスケッチ
//それ以外のコードが電波時計モジュールから取得した日時信号をシリアルモニタに表示するスケッチ
//----LCD時計1--------------------------------------------
#include <Wire.h> //Arduino IDE のI2Cライブラリ
#include <TimeLib.h> //https://github.com/PaulStoffregen/Time
#include <RX8900RTC.h> //https://github.com/citriena/RX8900RTC
#include <LcdCore.h> //http://100year.cocolog-nifty.com/blog/2012/05/arduinoliquidcr.html
#include <LCD_ACM1602NI.h> //http://100year.cocolog-nifty.com/blog/2012/05/i2clcdarduino-b.html
RX8900RTC RTC;
LCD_ACM1602NI lcd(0xa0); //0xa0は液晶モジュールのI2Cアドレス
//--------------------------------------------------------
char curr; //現在の読み込み値
char prev; //前回の読み込み値
long c_time; //ループ処理の経過時間
long p_time; //前回のループ処理の経過時間
long s_time; //ループ処理の経過時間の差分
char m01_code; //コード仕分け値(M、0、1)
int rp; //秒のカウント
int ct; //ループ回数のカウント、積算値
int ct_prev; //前回ループ時の回数
int ct_subt; //ループ回数の差分、正常読み込みであれば毎ループ終了時は「60」
int st_rec; //受信フラグ(初期化、成功、失敗)
char d2bits[61]; //60間の読み込み値格納、null終端
void setup() {
Serial.begin( 9600 );
pinMode(2,INPUT); //D2ピンをINPUTモードに設定
c_time = 0;
p_time = 0;
s_time = 0;
m01_code = '0';
ct = 0;
ct_prev = 0;
ct_subt = 0;
st_rec = 0; //受信フラグ:初期化
//----LCD時計2--------------------------------------------
Wire.begin(); //I2C初期化
lcd.begin(16, 2); //ディスプレイの行数(16)と桁数(2)
RTC.init(); //RTC初期化
//RTC.get()よりcompileTime()が新しい時はcompiletimeをRTC.set
time_t time_now, compiletime;;
time_now = RTC.get();
compiletime = compileTime();
if (time_now < compiletime) {
RTC.set(compiletime); // set compiled time to RTC
//日時の手動設定(second, minute, hour, dayofweek, day, month, year)
//tmElements_t tm = {0, 0, 0, 4, 24, 3, CalendarYrToTm(2023)};
//RTC.write(tm);
}
//--------------------------------------------------------
}
void loop() {
//----LCD時計3--------------------------------------------
serialTime(RTC.read());
//--------------------------------------------------------
//D2ポートから受信データの読み込み
curr = digitalRead( 2 );
//前回読み込み値と現在の読み込み値の比較
if( prev != curr ) {
c_time = millis(); //ループ処理の経過時間(ms)
//デジタルポートD2が 0(LOW)の場合の処理
if( curr == LOW ) {
//前回処理と時間差がある場合のみ実行
s_time = c_time - p_time;
//コード仕分け(パルス幅:M、P0~P5 200ms < 1:500ms < 0:800ms )
//M、P0~P5 (200ms)の判定:マージンを見込んで(300ms)
if( s_time < 300 ) { //マージンを見込んで300ms
ct++;
//前のコードがMか(Mが2回続いているか)のチェック、初期マーカー位置の判定
//毎時15分と45分のコールサイン(モールス符号)受信時にctとct_prevの値はリセット
if( m01_code == 'M' ) {
rp = 0; //60回(秒のカウント)のリセット、初期マーカー位置
Serial.println( "" );
Serial.print( "読み込んだデータ数:" );
//Serial.print( ct );
//Serial.print( " - " );
//Serial.print( ct_prev );
//Serial.print( " = " );
ct_subt = ct - ct_prev;
Serial.print( ct_subt );
//配列(タイムコード)をシリアルモニタに表示して確認
Serial.print( " ( " );
for(int mp=0;mp<60;mp++){
char c = char(d2bits[mp]);
Serial.print(c);
}
Serial.print( " ) " );
Serial.println( "" );
//配列(タイムコード)内のマーカー位置の確認
int mp_chk = 0;
if(d2bits[0]=='M' && d2bits[9]=='M' && d2bits[19]=='M' && d2bits[39]=='M' && d2bits[49]=='M' && d2bits[59]=='M'){
mp_chk = 1;
} else{
mp_chk = 2;
}
Serial.print( "マーカー位置の確認:" );
if( mp_chk == 1 ){ Serial.print( "OK" );}
if( mp_chk == 2 ){ Serial.print( "NG" );}
Serial.print( " ( " );
Serial.print( d2bits[0] );
Serial.print( "-" );
Serial.print( d2bits[9] );
Serial.print( "-" );
Serial.print( d2bits[19] );
Serial.print( "-" );
Serial.print( d2bits[29] );
Serial.print( "-" );
Serial.print( d2bits[39] );
Serial.print( "-" );
Serial.print( d2bits[49] );
Serial.print( "-" );
Serial.print( d2bits[59] );
Serial.println( " )" );
//60秒間のデータが揃ったか、マーカー位置チェックOKかの判定
if( ct_subt == 60 && mp_chk == 1) {
st_rec = 1; //受信フラグ:成功
//配列d2bitsから1bit毎に時刻データ取り出し、重みづけ
//分
int min = 0;
if(d2bits[8]=='1'){ min = min + 1; }
if(d2bits[7]=='1'){ min = min + 2; }
if(d2bits[6]=='1'){ min = min + 4; }
if(d2bits[5]=='1'){ min = min + 8; }
if(d2bits[3]=='1'){ min = min + 10; }
if(d2bits[2]=='1'){ min = min + 20; }
if(d2bits[1]=='1'){ min = min + 40; }
//時
int hour = 0;
if(d2bits[18]=='1'){ hour = hour + 1; }
if(d2bits[17]=='1'){ hour = hour + 2; }
if(d2bits[16]=='1'){ hour = hour + 4; }
if(d2bits[15]=='1'){ hour = hour + 8; }
if(d2bits[13]=='1'){ hour = hour + 10; }
if(d2bits[12]=='1'){ hour = hour + 20; }
//通算日
int day = 0;
if(d2bits[33]=='1'){ day = day + 1; }
if(d2bits[32]=='1'){ day = day + 2; }
if(d2bits[31]=='1'){ day = day + 4; }
if(d2bits[30]=='1'){ day = day + 8; }
if(d2bits[28]=='1'){ day = day + 10; }
if(d2bits[27]=='1'){ day = day + 20; }
if(d2bits[26]=='1'){ day = day + 40; }
if(d2bits[25]=='1'){ day = day + 80; }
if(d2bits[23]=='1'){ day = day + 100; }
if(d2bits[22]=='1'){ day = day + 200; }
//西暦(下2桁)
int year = 0;
if(d2bits[48]=='1'){ year = year + 1; }
if(d2bits[47]=='1'){ year = year + 2; }
if(d2bits[46]=='1'){ year = year + 4; }
if(d2bits[45]=='1'){ year = year + 8; }
if(d2bits[44]=='1'){ year = year + 10; }
if(d2bits[43]=='1'){ year = year + 20; }
if(d2bits[42]=='1'){ year = year + 40; }
if(d2bits[41]=='1'){ year = year + 80; }
//西暦(4桁)
int ayear = ( year + 2000 ) ;
//曜日
int youbi = 0;
if(d2bits[52]=='1'){ youbi = youbi + 1; }
if(d2bits[51]=='1'){ youbi = youbi + 2; }
if(d2bits[50]=='1'){ youbi = youbi + 4; }
//1分加算と分、時、日、年、うるう年、曜日への影響反映
min++; //1分加算
if(min>=60){ min=0; hour++;
if(hour>=24){ hour=0; day++;
int leap_year = ( (1 / (ayear % 4 + 1)) * (1 - 1 / (ayear % 100 + 1)) + (1 / (ayear % 400 + 1)) ); //うるう年判定
int set_year = ( 365 + leap_year );
if(day > set_year ){ day=1; year++; }
youbi++;
if( youbi>=7){ youbi=0; }
}
}
//通算日から月、日への変換
int t_day = 0; //通算日の計算用
int mm; //月
int dd; //日
int leap_year = ( (1 / (ayear % 4 + 1)) * (1 - 1 / (ayear % 100 + 1)) + (1 / (ayear % 400 + 1)) ); //うるう年判定
int mdtable[2][12] = { 31,28,31,30,31,30,31,31,30,31,30,31, 31,29,31,30,31,30,31,31,30,31,30,31 }; //通年とうる年の日数
//1月から順に月の日数を引いて月、日を算出
for(int i=0;i<12;i++){
if((t_day + mdtable[leap_year][i])>=day){
mm = i+1;
dd = day - t_day;
break;
}
t_day = t_day + mdtable[leap_year][i];
}
//曜日を3桁英字への変換
char dayofweek[7][4] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"} ;
char f_youbi[4];
strcpy(f_youbi, dayofweek[youbi]);
//日時をシリアルモニタに表示して確認
Serial.print( "日時変換:" );
Serial.print(ayear);
Serial.print( "/" );
Serial.print(mm);
Serial.print( "/" );
Serial.print(dd);
Serial.print( "(" );
Serial.print(f_youbi);
Serial.print( ")" );
Serial.print( " " );
Serial.print(hour);
Serial.print( ":" );
Serial.print(min);
Serial.print( ":" );
Serial.print(rp); //秒
Serial.println( "" );
//----LCD時計4--------------------------------------------
//受信した日時データをRTCに書込み
tmElements_t tm = {rp, min, hour, youbi, dd, mm, CalendarYrToTm(ayear)};
RTC.write(tm);
//--------------------------------------------------------
} else {
//配列クリア
for(int i=0;i<60;i++){
d2bits[i] = ' ';
}
//受信フラグ:失敗
st_rec = 9;
Serial.println( "日時変換:SKIP" );
}
Serial.println( "" );
ct_prev = ct;
}
Serial.print( "M" );
Serial.print( "(" );
Serial.print( rp ); //マーカー位置の番号
Serial.print( ")" );
m01_code = 'M';
d2bits[rp] = 'M'; //rp番目の配列に格納
rp++; //60ループ回のカウントアップ
//1(500ms)の判定:マージンを見込んで(600ms)
} else if( s_time < 600 ) {
ct++;
Serial.print( "1" );
d2bits[rp] = '1'; //rp番目の配列に格納
m01_code = '1';
rp++; //60回ループのカウントアップ
//0(800ms)の判定
} else {
ct++;
Serial.print( "0" );
d2bits[rp] = '0'; //rp番目の配列に格納
m01_code = '0';
rp++; //60回ループのカウントアップ
}
}
}
prev = curr; //現在の読み込み値を保存
p_time = c_time; //現在の経過時間(ms)を保存
}
//----LCD時計5---------------------------------------------
//秋月I2C液晶ディスプレイに日時を表示
void serialTime(tmElements_t tm) {
//年月日の表示
lcd.setCursor(0, 0);
lcd.print(tmYearToCalendar(tm.Year));
lcd.setCursor(4, 0);
lcd.print("/");
lcd.setCursor(5, 0);
lcdzeroSup(tm.Month);
lcd.setCursor(7, 0);
lcd.print("/");
lcd.setCursor(8, 0);
lcdzeroSup(tm.Day);
//曜日の表示 0=日曜 1=月曜 2=火曜 3=水曜 4=木曜 5=金曜 6=土曜
int day_week ;
char DayWeekData[7][4] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"} ;
day_week = getDayWeek(tmYearToCalendar(tm.Year), tm.Month, tm.Day) ;
lcd.setCursor(11, 0);
lcd.print("(");
lcd.print(DayWeekData[day_week]) ;
lcd.print(")");
//時分秒の表示
lcd.setCursor(0, 1);
lcdzeroSup(tm.Hour);
lcd.setCursor(2, 1);
lcd.print(":");
lcd.setCursor(3, 1);
lcdzeroSup(tm.Minute);
lcd.setCursor(5, 1);
lcd.print(":");
lcd.setCursor(6, 1);
lcdzeroSup(tm.Second);
lcd.setCursor(12, 1);
//受信状況の表示
if (st_rec == 0) {
lcd.print( "ini" ); //初期化完了
}else if(st_rec == 1) {
lcd.print( "rec" ); //受信成功
}else if(st_rec == 9) {
lcd.print( "---" ); //受信失敗
}else{
lcd.print( "ooo" );
}
}
//曜日の計算(ツェラー(Zeller)の公式)
int getDayWeek(int year,int month,int day) {
int w ;
if(month < 3) {
year = year - 1;
month = month + 12 ;
}
w = (year + (year/4) - (year/100) + (year/400) + (13*month+8)/5 + day ) % 7;
return w;
}
//月、日、時、分、秒が0~9の場合、1桁目を 空白 もしくは 0 に置換
void lcdzeroSup(int digit) {
if(digit < 10)
lcd.print(' '); //現在「空白」
lcd.print(digit);
}
// function to return the compile date and time as a time_t value
// from alarm_ex1.ino in Arduino DS3232RTC Library sample sketch by Jack Christensen.
time_t compileTime() {
const time_t FUDGE(10); //fudge factor to allow for upload time, etc. (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char compMon[4], *m;
strncpy(compMon, compDate, 3);
compMon[3] = '\0';
m = strstr(months, compMon);
tmElements_t tm;
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; //add fudge factor to allow for compile time
}
//--------------------------------------------------------