Raspberry piで電波時計を修正(完成版)

はんだ付けなしで、Raspberry Pi Zero WHを使用する電波発生機を作成し、電波時計の修正に成功した。

前のエントリ投稿から1年半ほど経ってしまった。チコちゃんも驚きのぼーっと加減である。いや、勤め人は休日もなかなか時間が取れないんですよ。まとまった休みはぼーっとしていたいし。

主がこのような体たらくなので、依然我が家の電波時計たちは思い思いの時刻を指し続けていた。なんたることか。さすがにこれは、と一念発起し、全エントリの問題点2点の修正を試みた。

  1. 時刻合わせのできる時計とできない時計がある
  2. 電波の到達距離が10 cm程度である

前者については、原因追及に時間を要したものの、分かってしまえば何ということはなく、localtime()が返すtm構造体のtm_ydayメンバは1月1日を0日目と数えるので1を足す必要があった。確かに、日付を無視する目覚まし時計では時刻が合い、カレンダー機能のある腕時計では合わなかった。

一方後者は、出力を上げる以外に方法はない。そこで、トランジスタでGPIO信号を増幅することにした。今回は10 kΩ固定としているが、可変抵抗を使えばアンテナ線に通じる電流を観測しながら出力を変えるのも容易である。

ハードウェア編

それではようやく本題。 発信機を作成する。実体は以下のとおりである。

トランジスタは、平らな面を手前に置いて、左からエミッタ(E)、コレクタ(C)、ベース(B)である。エミッタを0 Ω抵抗でGNDと、コレクタをアンテナと、ベースを10 KΩ抵抗でGPIOと、それぞれ連結する。ブレッドボードは図の縦(列)は全て連結されている。増幅回路に加え、強制シャットダウンボタンも作っておく。タクトスイッチを押すとGPIOとGNDが短絡するように、向きに気を付けて連結する。

ブレッドボードを使用するので、はんだ付けに自信のない方でも大丈夫ではないだろうか。腕に覚えのある方は、8桁×2行のLEDキットを付けて、時刻表示ができるようにしてもよい。

これらの部品は、すべて秋月電子通商で購入可能である。以下はご参考まで。全て買うと、6千5百円程度と、かなり値の張るものとはなる。

商品名商品コード個数
Raspberry Pi Zero WHケース付きM-129581
Micro SDカードS-130021
スイッチングACアダプターMicroBオス5 V 3 AM-120011
ブレッドボードBB-801P-052941
トランジスタ2SC1815Y(10個入)I-042681
カーボン抵抗1/2 W 0 Ω(100本入)R-077921
カーボン抵抗1/2 W 10 KΩ(100本入)R-078381
タクトスイッチ(黒色)P-036471
ジャンパーワイヤ(オス-メス)15 cm(白、10本入)C-089351
耐熱電子ワイヤー2 m×7色 外径1.22 mmP-067561
Raspberry Piキャラクタ液晶ディスプレイモジュールキット バックライト付K-113541

ソフトウェア編

次に、プログラムである。最初に、前エントリに従ってchrony、pigpio両パッケージをインストールしておく。それから、以下に記す修正版のJJY_simulator.cを、やはり前エントリ記載の方法でコンパイルして実行可能としておく。LEDモジュールを使用される方は、コメントアウトしている76行目と217行目を生かしておくのをお忘れなく。

#include <pigpio.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

#define WAIT_DURATION 180       // 3 min in sec
#define COMMAND_DURATION 200000 // 0.2 millisec in microsec
#define MIN_TOLERANCE 500000    // 0.5 sec in microsec
#define SEC_TOLERANCE 10000     // 10 millisec in microsec
#define MARKER -1
#define I2C_BUS 1
#define LCD_ADDRESS 0x3E     // for AQM0802
#define I2C_FLAGS 0          // should be set to 0
#define SET_COMMAND 0x00     // ST7032 instruction
#define WRITE_DATA 0x40      // ditto
#define OFF_SWITCH_PIN 16    // GPIO16
#define OUTPUT_PIN 20        // GPIO20
#define BACKLIGHT_PIN 4      // GPIO4
#define OUTPUT_FREQ 40000    // 40 KHz
#define OUTPUT_MARKER 200000 // 0.2 sec in microsec
#define OUTPUT_ZERO 800000   // 0.8 sec in microsec
#define OUTPUT_ONE 500000    // 0.5 sec in microsec
#define SUCCEEDED 0
#define FAILED -1

/*
 * https://www.denshi.club/make/2016/10/aqmi2clcd1.html
 * https://www.crystalfontz.com/controllers/Sitronix/ST7032/276/
 */
const int LCD_INITIALIZE_COMMANDS[] = {
    0x38, // 8-bit bus mode, 2-line display mode, display font is normal,
          // normal instruction
    0x39, // 8-bit bus mode, 2-line display mode, display font is normal,
          // extension instruction
    0x14, // the bias will be 1/4, frame frequency = 120 Hz @ VDD = 5.0 V
    0x75, // low contrast byte is set to 1
    0x55, // ICON display off, booster circuit is turn on, high contrast
          // byte is set to 0
    0x6C, // internal follower circuit is turn on,
    0x38, // 8-bit bus mode, 2-line display mode, display font is normal,
          // normal instruction
    0x0C, // Display ON & Cursor OFF & Cursor Blink OFF
    0x01, // Clear Display
};

struct timeval now_epoch;
struct tm *now;
struct tm *next;
int timecode[60] = {0}; // consists of codes from 0 sec to 59 sec
int i2c_handle = 0;

void initialize_gpio(void);
void initialize_i2c(void);
void wait_until_59_sec(void);

void set_timecode(void);
void set_bcd(int, int, int);

void output_timecode(void);
void wait_until_exact_second(void);
void display_time(void);
void transmit_wave(int);

void set_now(void);
void set_next(void);
void print_now(int);
void print_timecode(void);

void off_switch_handler(int, int, uint32_t);
void signal_handler(int);
void finalize(int);
//--------------------------------------
int main(int argc, char *argv[]) {
  initialize_gpio();
  // initialize_i2c();
  wait_until_59_sec();

  while (1) {
    set_timecode();
    output_timecode();
  }
}
//--------------------------------------
void initialize_gpio(void) {
  if (gpioInitialise() < 0) {
    perror("failed in gpioInitialise()");
    finalize(FAILED);
  }

  if (gpioSetSignalFunc(SIGHUP, signal_handler) < 0 ||
      gpioSetSignalFunc(SIGINT, signal_handler) < 0 ||
      gpioSetSignalFunc(SIGQUIT, signal_handler) < 0 ||
      gpioSetSignalFunc(SIGTERM, signal_handler) < 0) {
    perror("failed in gpioSetSingalFunc()");
    finalize(FAILED);
  }

  if (gpioSetMode(OFF_SWITCH_PIN, PI_INPUT) < 0 ||
      gpioSetPullUpDown(OFF_SWITCH_PIN, PI_PUD_UP) < 0 ||
      gpioSetAlertFunc(OFF_SWITCH_PIN, off_switch_handler) < 0) {
    perror("failed to initialize OFF_SWITCH_PIN");
    finalize(FAILED);
  }

  if (gpioSetMode(OUTPUT_PIN, PI_OUTPUT) < 0 ||
      gpioSetPullUpDown(OUTPUT_PIN, PI_PUD_DOWN) < 0) {
    perror("failed to initialize OUTPUT_PIN");
    finalize(FAILED);
  }

  if (gpioSetMode(BACKLIGHT_PIN, PI_OUTPUT) < 0 ||
      gpioSetPullUpDown(BACKLIGHT_PIN, PI_PUD_DOWN) < 0) {
    perror("failed to initialize BACKLIGHT_PIN");
    finalize(FAILED);
  }
}

void initialize_i2c(void) {
  i2c_handle = i2cOpen(I2C_BUS, LCD_ADDRESS, I2C_FLAGS);

  if (i2c_handle < 0) {
    perror("failed in i2cOpen()");
    finalize(FAILED);
  }

  int max_index = sizeof(LCD_INITIALIZE_COMMANDS) / sizeof(int);

  for (int index = 0; index < max_index; index++) {
    if (i2cWriteByteData(i2c_handle, SET_COMMAND,
                         LCD_INITIALIZE_COMMANDS[index]) < 0) {
      perror("failed to initialize I2C");
      finalize(FAILED);
    }

    gpioDelay(COMMAND_DURATION);
  }

  gpioWrite(BACKLIGHT_PIN, PI_ON);
}

void wait_until_59_sec(void) {
  set_now();
  time_t start = now_epoch.tv_sec;

  while (1) {
    set_now();

    if (now->tm_sec == 59 && now_epoch.tv_usec <= MIN_TOLERANCE) {
      break;
    }

    if (now_epoch.tv_sec > start + WAIT_DURATION) {
      perror("failed in wait_until_59_sec()");
      finalize(FAILED);
    }
  }
}
//--------------------------------------
void set_timecode(void) {
  set_now();
  set_next();

  int min = next->tm_min;
  int hour = next->tm_hour;
  int yday = next->tm_yday + 1;
  int year = next->tm_year;
  int wday = next->tm_wday;
  /* not elegant but elephant */
  int min10 = min / 10;
  int min1 = min % 10;
  int hour10 = hour / 10;
  int hour1 = hour % 10;
  int yday100 = yday / 100;
  int yday10 = yday % 100 / 10;
  int yday1 = yday % 10;
  int year10 = year % 100 / 10;
  int year1 = year % 10;
  int wday1 = wday;

  timecode[0] = MARKER;
  set_bcd(1, 3, min10);
  set_bcd(5, 4, min1);
  timecode[9] = MARKER;
  set_bcd(12, 2, hour10);
  set_bcd(15, 4, hour1);
  timecode[19] = MARKER;
  set_bcd(22, 2, yday100);
  set_bcd(25, 4, yday10);
  timecode[29] = MARKER;
  set_bcd(30, 4, yday1);
  timecode[36] = (timecode[12] + timecode[13] + timecode[15] + timecode[16] +
                  timecode[17] + timecode[18]) %
                 2;
  timecode[37] = (timecode[1] + timecode[2] + timecode[3] + timecode[5] +
                  timecode[6] + timecode[7] + timecode[8]) %
                 2;
  timecode[39] = MARKER;
  set_bcd(41, 4, year10);
  set_bcd(45, 4, year1);
  timecode[49] = MARKER;
  set_bcd(50, 3, wday1);
  timecode[59] = MARKER;

  // print_timecode();
}

void set_bcd(int start, int bits, int value) {
  for (int i = 0; i <= start + bits - 1; i++) {
    timecode[start + i] = value & (1 << (bits - 1 - i)) ? 1 : 0;
  }
}
//--------------------------------------
void output_timecode(void) {
  for (int sec = 0; sec < 60; sec++) {
    wait_until_exact_second();
    // display_time();
    // print_now(timecode[sec]);

    switch (timecode[sec]) {
    case 0:
      transmit_wave(OUTPUT_ZERO);
      break;
    case 1:
      transmit_wave(OUTPUT_ONE);
      break;
    case MARKER:
      transmit_wave(OUTPUT_MARKER);
      break;
    default:
      perror("failed in output_timecode()");
      finalize(FAILED);
    }
  }
}

void wait_until_exact_second(void) {
  set_now();
  time_t next_sec = now_epoch.tv_sec + 1;

  while (1) {
    set_now();
    time_t now_sec = now_epoch.tv_sec;

    if (now_sec == next_sec && now_epoch.tv_usec <= SEC_TOLERANCE) {
      break;
    }

    if (now_sec > next_sec) {
      perror("failed in wait_until_exact_second()");
      finalize(FAILED);
    }
  }
}

void display_time(void) {
  set_now();

  char now_date[8] = {'\0'};
  char now_time[8] = {'\0'};
  sprintf(now_date, "   %02d/%02d", now->tm_mon + 1, now->tm_mday);
  sprintf(now_time, "%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec);
  /* write data on the 1st line */
  i2cWriteByteData(i2c_handle, SET_COMMAND, 0x80);

  for (int i = 0; i < 8; i++) {
    i2cWriteByteData(i2c_handle, WRITE_DATA, now_date[i]);
  }
  /* write data on the 2nd line */
  i2cWriteByteData(i2c_handle, SET_COMMAND, 0xC0);

  for (int i = 0; i < 8; i++) {
    i2cWriteByteData(i2c_handle, WRITE_DATA, now_time[i]);
  }
}

void transmit_wave(int microsec) {
  if (gpioHardwareClock(OUTPUT_PIN, OUTPUT_FREQ) < 0) {
    perror("failed in pioHardwareClock()");
    finalize(FAILED);
  }

  gpioDelay(microsec);

  if (gpioHardwareClock(OUTPUT_PIN, 0) < 0) {
    perror("failed in gpioHardwareClock()");
    finalize(FAILED);
  }
}
//--------------------------------------
void set_now(void) {
  if (gettimeofday(&now_epoch, NULL) < 0) {
    perror("failed in gettimeofday()");
    finalize(FAILED);
  }

  now = localtime(&now_epoch.tv_sec);

  if (now == NULL) {
    perror("failed in localtime()");
    finalize(FAILED);
  }
}

void set_next(void) {
  time_t next_sec = now_epoch.tv_sec + 1;
  next = localtime(&next_sec);

  if (next == NULL) {
    perror("failed in localtime()");
    finalize(FAILED);
  }
}

void print_now(int timecode) {
  set_now();

  fprintf(stdout, "%04d-%02d-%02d %02d:%02d:%02d.%06lu->%d\n",
          now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour,
          now->tm_min, now->tm_sec, now_epoch.tv_usec, timecode);
}

void print_timecode(void) {
  for (int sec = 0; sec <= 59; sec++) {
    switch (timecode[sec]) {
    case 0:
      putchar('0');
      break;
    case 1:
      putchar('1');
      break;
    case MARKER:
      putchar('m');
      break;
    default:
      putchar('\n');
      perror("failed in print_timecode()");
      finalize(FAILED);
    }
  }

  putchar('\n');
}
//--------------------------------------
void off_switch_handler(int gpio, int level, uint32_t tick) {
  int duration = 0;

  while (1) {
    if (gpioRead(OFF_SWITCH_PIN) == PI_ON) {
      break;
    }

    duration++;
  }

  if (duration > 10000000) {
    fprintf(stdout, "caught shutdown signal\n");
    system("/sbin/shutdown -h now");
  }
}

void signal_handler(int signal) {
  fprintf(stdout, "caught signal %d\n", signal);
  finalize(SUCCEEDED);
}

void finalize(int status) {
  if (i2c_handle >= 0) {
    /* clear LCD */
    i2cWriteByteData(i2c_handle, SET_COMMAND, 0x01);
    i2cClose(i2c_handle);
  }

  gpioHardwareClock(OUTPUT_PIN, 0);
  gpioWrite(BACKLIGHT_PIN, PI_OFF);
  gpioTerminate();
  exit(status);
}

作動

配線に間違いのないことを確認したら、sudo ~/bin/JJY_simulator(前エントリ記載どおりバイナリを作成した場合)としてプログラムを実行する。いかがであろうか。手元で調べた場合、ベース電流は約0.1 mA、コレクタ電流は約15 mAであった。これで見通し2 m程度は十分電波が届いているようである。プログラムはCtrl-Cで安全に終了でき、タクトスイッチの長押しでRaspberry Piを強制シャットダウンすることもできる。

さらに出力を上げたい場合は、やはりテスターは必要となろう。ベース抵抗を変えながら200 mAを超えないように出力を上げる必要がある。Pi Zero自体は、総出力1.2 Aまでは大丈夫とのことではあるが。皆様のご家庭でお役に立てれば幸いである。