Hatena::ブログ(Diary)

Okiraku Programming RSSフィード Twitter

2011-03-28 Arduinoで電波時計を合わせよう

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

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


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


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


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

標準電波の出し方


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

f:id:NeoCat:20110327042859j:image:w320

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:もうちょっと共振回路らしくしても良いと思いますが、うちではこれで合わせられてしまったので追求しないことにしました。

paku7651paku7651 2011/03/31 06:24 我が家にあるのはJapaninoで、イーサのシールドなどは買っていません。
きっとPCと連携させたらUSBとかシリアルとかの通信と連携してやることでそれなりに出来るのかなぁ。と思って拝見しました。
(もちろん、Arduino+シールドで完結するのはすっきりですね。)

NeoCatNeoCat 2011/03/31 08:00 任意の時刻に合わせるのでしたら、Ethernet関連、NTPの部分をごっそり削って、
代わりにsetupでシリアルから数値(epoch: 1970/1/1 0:00:00からの経過時間)+改行を受け取り、
setTime(epoch + 9*60*60);
を呼び出す、というコードにすれば良さそうですね。

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。