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

現在、福島第1原子力発電所の事故の影響で、東日本で電波時計の基準となる福島の送信所からの標準電波(JJY)の運用が止まっています。このため、電波時計の自動調整が行えなくなっています。(西日本は九州からの電波があるため問題ありません。)


このJJYを模擬した出力を、NTPを使って取得した時刻をもとにArduinoを使って発信*1して、電波時計を合わせるのに挑戦してみました。


PCのオーディオ端子を駆使して、同じことをするWindows用ソフトがあり、こちらに触発されて作ってみました。


JJYのフォーマットについては、下に詳しく説明があります。40kHzの電波のON/OFF時間により、1秒に1bitずつ、60bitの信号を1分かけて送信します。
標準電波の出し方


ハードウェアはこんなかんじ。

Arduinoの3pinに送信確認用LEDのアノードを接続し、カソードには適当にリード線を繋ぎます。リード線は、少しくるくる巻いておき、もう片方をGNDに繋ぎます。このリード線がアンテナの役割をします。信号は1mも飛ばないので、すぐ近くに電波時計を置きます。*2


電源を入れると、Ethernetシールドを使ってNTPで正確な時刻を取得します。取得に成功すると、0秒(正分)になったところから信号の送信が開始され、LEDがいろいろな長さで点滅しはじめます。(時刻やNTPの取得状況はシリアル出力を見ると分かります。)
送信が始まったら、合わせたい電波時計を強制受信モードにして、時刻を合わせるのがよいと思います。(なおArduino側は10分に1度NTPで時刻を再取得して時刻を補正しています。)


ソフトウェア的な工夫として、Arduinoからの40kHzの信号を出力するために、今回はPWM出力を使ってみました。AVRのレジスタを直接操作してPWMの周波数を40kHzに変更し、これをJJYのフォーマットに合わせてON/OFFしています。
ちなみにNTPのコードはIDE付属のEthernetライブラリのサンプルを改良してNTPと秒以下まで同期するようにしたものです。


以下、スケッチの内容です。Arduino IDE 0019以降用です。MACアドレスEthernetの設定などは、お使いの環境に合わせて変更して下さい。
また、下記からダウンロードできるArduino用のTimeライブラリをインストールしておく必要があります。
http://www.arduino.cc/playground/Code/Time


※updated at 3/29 - 日付計算に一部誤りがあったのを修正しました。

#include <SPI.h>
#include <Ethernet.h>
#include <UDP.h>
#include <Time.h>  

// bit set / clear
#ifndef cbi
#define cbi(PORT, BIT) (_SFR_BYTE(PORT) &= ~_BV(BIT))
#endif
#ifndef sbi
#define sbi(PORT, BIT) (_SFR_BYTE(PORT) |= _BV(BIT))
#endif

// Circuit
// pin3 - LED -------- GND

byte timeServer[] = { 133,243,238,164 }; // ntp.nict.jp NTP server

const unsigned int localPort = 8888;   // local UDP port
const int NTP_PACKET_SIZE= 48;
byte packetBuffer[NTP_PACKET_SIZE];
byte timecode[60];
unsigned long lastNTPTime = 0;

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

  // Ethernet settings
  byte mac[] = { 0xDE,0xAD,0xBE,0xEF,0xFE,0xED };
  byte ip[] = { 192,168,0,177 };
  byte gateway[] = { 192,168,0,1 };
  byte subnet[] = { 255,255,255,0 };

  Ethernet.begin(mac, ip, gateway, subnet);
  delay(1000);
  Udp.begin(localPort);
  NTPSetTime();
  setupTimeCode();
}

void loop()
{
  int wait_start = second();
  while (wait_start == second()); // wait until time is corrected
  unsigned long startTime = millis();

  // generate 40khz from 3 pin using PWM
  pinMode(3, OUTPUT);
  digitalWrite(3, LOW);
  TCCR2A = _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS20);
  OCR2A = F_CPU / 2 / 40000/*hz*/;
  OCR2B = OCR2A / 2; /* 50% duty */
  sbi(TCCR2A,COM2B1);

  // print out current time
  Serial.print(year());
  Serial.print('/');
  Serial.print(month());
  Serial.print('/');
  Serial.print(day());
  Serial.print(' ');
  Serial.print(hour());
  Serial.print(':');
  Serial.print(minute());
  Serial.print(':');
  Serial.print(second());
  Serial.print('(');
  Serial.print(weekday());
  Serial.print(')');
  Serial.println(dayOfYear());

  // calc signal duration (ms)
  int ms = calcTimeCodeDuration();

  // wait ms and stop PWM
  while (millis() - startTime < ms);
  cbi(TCCR2A,COM2B1);
  
  if (millis() - lastNTPTime > 10*60*1000L) {
    NTPSetTime();
    lastNTPTime = millis();
  }
}

//=========================== NTP ===========================
void NTPSetTime()
{
  sendNTPpacket(timeServer);
  Serial.println("Waiting NTP response ...");
  delay(100);  // wait 100 ms to ensure the packet is sent

  if (!Udp.available()) { } // wait the reply packet
  Udp.readPacket(packetBuffer,NTP_PACKET_SIZE);
  unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
  unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
  unsigned long secsSince1900 = highWord << 16 | lowWord;

  unsigned int fraction_hi = word(packetBuffer[44], packetBuffer[45]);

  // Unix time starts on Jan 1 1970, v.s. NTP ans is since Jan 1 1900.
  const unsigned long seventyYears = 2208988800UL;     
  unsigned long epoch = secsSince1900 - seventyYears;  

  // wait until next sencod
  delay(900 - fraction_hi / (65536/1000));

  // Set current time in JST (GMT+0900)
  setTime(epoch + 1 + 9*60*60);

  Serial.print("localtime = ");
  Serial.println(epoch);
}

unsigned long sendNTPpacket(byte *address)
{
  memset(packetBuffer, 0, NTP_PACKET_SIZE); 
  // Initialize values needed to form NTP request
  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
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49; 
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  //send NTP request packet (port 123)
  Udp.sendPacket( packetBuffer,NTP_PACKET_SIZE,  address, 123); 
}


//=========================== JJY ===========================

unsigned int calcTimeCodeDuration()
{
  int s = second();
  if (s == 0)
    setupTimeCode();
  return timecode[s] * 100;
}

void setupTimeCode()
{
  int i;
  memset(timecode, 8, sizeof(timecode));

  setupTimeCode100(minute(), 0);
  timecode[0] = 2;

  setupTimeCode100(hour(), 10);

  int d = dayOfYear();
  setupTimeCode100(d/10, 20);
  setupTimeCode100(d%10*10, 30);

  int parity1 = 0, parity2 = 0;
  for (i = 12; i < 20; i++) parity1 ^= timecode[i] == 5;
  for (i =  1; i < 10; i++) parity2 ^= timecode[i] == 5;
  timecode[36] = parity1 ? 5 : 8;
  timecode[37] = parity2 ? 5 : 8;

  setupTimeCode100(year()%100, 40);
  for (i = 44; i > 40; i--)
    timecode[i] = timecode[i-1];
  timecode[40] = 8;

  int w = weekday() - 1;
  timecode[50] = (w & 4) ? 5 : 8;
  timecode[51] = (w & 2) ? 5 : 8;
  timecode[52] = (w & 1) ? 5 : 8;
  timecode[59] = 2;
  
  /* dump */
  for (i = 0; i < 60; i++) {
    Serial.print(timecode[i], DEC);
    Serial.print(i % 10 == 9 ? "\r\n" : " ");
  }
}

void setupTimeCode100(int m, int i)
{
  timecode[i+0] = ((m/10) & 8) ? 5 : 8;
  timecode[i+1] = ((m/10) & 4) ? 5 : 8;
  timecode[i+2] = ((m/10) & 2) ? 5 : 8;
  timecode[i+3] = ((m/10) & 1) ? 5 : 8;
  timecode[i+4] = 8;
  timecode[i+5] = ((m%10) & 8) ? 5 : 8;
  timecode[i+6] = ((m%10) & 4) ? 5 : 8;
  timecode[i+7] = ((m%10) & 2) ? 5 : 8;
  timecode[i+8] = ((m%10) & 1) ? 5 : 8;
  timecode[i+9] = 2;
}

int dayOfYear()
{
  tmElements_t tm = {0, 0, 0, 0, 1, 1, CalendarYrToTm(year())};
  time_t t = makeTime(tm);
  return (now() - t) / SECS_PER_DAY + 1;
}

*1:勝手に電波を出しても良いの?という疑問が湧きますが、市販のFMトランスミッタ等よりもさらに弱い出力なので問題ありません。実際試してみたところ、到達範囲は感度の良い電波時計でも1mもいかない程度でした。

*2:もうちょっと共振回路らしくしても良いと思いますが、うちではこれで合わせられてしまったので追求しないことにしました。