Raspberry Piで電波時計を修正

Raspberry Piだけで標準電波相当の電波を出力し、電波時計を修正する。

試行から3年、ようやく標準電波相当の電波の出力に成功した。受信可能距離は10 cm程度ではあるが、何かのご参考まで。ポイントとなったのは、格安のオシロスコープキットDSO150pigpioライブラリであった。以下、Raspberry Pi 3 Model BにRaspbian Stretch (2017-09-07)をクリーンインストールした状態から説明する。

chronyのインストール

Raspbian Stretchはntpdがデフォルトでインストールされていない。そこで、最近はRedHat系Linuxディストリビューションでは標準となっているchronydをインストールし、上位NTPサーバと時刻同期を行う。apt-getコマンドでインストールから稼働まで簡単に終わる。当方では、上位サーバにntp.nict.jpを指定した。

$ sudo apt-get update
$ sudo apt-get install chrony
$ sudo vi /etc/chrony/chrony.conf
(以下のとおり修正)
   #pool 2.debian.pool.ntp.org iburst
   server ntp.nict.jp iburst
$ sudo systemctl restart chrony 
(しばらく待ってから)
$ chronyc sources

pigpioのインストール

こちらもapt-getコマンドで。Python使いの方はpython-pigpiopython3-pigpioもインストールすると幸せになれるだろう。

$ sudo apt-get install pigpio

標準電波相当の電波の出力

さて本題の電波出力である。関東では40 KHzとなる標準電波は、12.5マイクロ秒毎に出力のオン/オフを切り替えることで代替可能と考えていた。ところが、既存のbcm2835ライブラリーWiringPiライブラリーを用いても良い結果が得られずにいた。これは、bcm2835_delayMicroseconds()delayMicroseconds()によるタイミングでは正確にならず、最大で44 KHzまで振れて安定しないためであった(bcm2835ライブラリーやWiringPiライブラリー自身の問題ではない)。一方、pigpioライブラリーのgpioHardwareClock()関数は正確に40 KHzの出力が可能であった。以下、使用したプログラムである。

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

#define WAIT_DURATION 180    // 3 min in sec
#define RUN_DURATION  30     // 5 min
#define MIN_TOLERANCE 500000 // 0.5 sec in microsec
#define SEC_TOLERANCE 5000   // 5 millisec in microsec
#define MARKER        2
#define OUTPUT_PIN    4      // GPIO4 -> pin 7
#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

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

void wait_until_59_sec(void);
void set_now(void);

void set_timecode(void);
void set_next(void);
void set_bcd(int start, int bits, int value);

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

void terminate(int signal);

void print_now(void);
void print_timecode(void);

int main(int argc, char *argv[]) {
    if (gpioInitialise() < 0) {
        perror("failed gpioInitialise()");
        exit(EXIT_FAILURE);
    }
    if (gpioSetMode(OUTPUT_PIN, PI_OUTPUT)) {
        perror("failed gpioSetMode()");
        exit(EXIT_FAILURE);
    }

    struct sigaction action;
    action.sa_handler = terminate;
    action.sa_flags   = SA_RESETHAND;
    sigemptyset(&action.sa_mask);

    if (sigaction(SIGINT,  &action, NULL)) {
        perror("failed sigaction(SIGINT)");
        exit(EXIT_FAILURE);
    }
    if (sigaction(SIGQUIT, &action, NULL)) {
        perror("failed sigaction(SIGQUIT)");
        exit(EXIT_FAILURE);
    }
    if (sigaction(SIGTERM, &action, NULL)) {
        perror("failed sigaction(SIGTERM)");
        exit(EXIT_FAILURE);
    }

    fprintf(stdout, "wainting for the next minute ...\n");
    wait_until_59_sec();

    for (int timer = 1; timer <= RUN_DURATION; timer++) {
        set_timecode();
        output_timecode();
    }

    terminate(0);
}
//-------------------------------------
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 wait_until_59_sec()");
            exit(EXIT_FAILURE);
        }
    }
}

void set_now(void) {
    if (gettimeofday(&now_epoch, NULL)) {
        perror("failed gettimeofday()");
        exit(EXIT_FAILURE);
    }

    now = localtime(&now_epoch.tv_sec);

    if (now == NULL) {
        perror("failed localtime()");
        exit(EXIT_FAILURE);
    }
}
//-------------------------------------
void set_timecode(void) {
    set_now();
    set_next();

    /* not elegant but elephant */
    int min10   = next->tm_min  / 10;
    int min1    = next->tm_min  % 10;
    int hour10  = next->tm_hour / 10;
    int hour1   = next->tm_hour % 10;
    int yday100 = next->tm_yday / 100;
    int yday10  = next->tm_yday % 100 / 10;
    int yday1   = next->tm_yday % 10;
    int year10  = next->tm_year % 100 / 10;
    int year1   = next->tm_year % 10;
    int wday1   = next->tm_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;
}

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

    if (next == NULL) {
        perror("failed localtime()");
        exit(EXIT_FAILURE);
    }
}

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 <= 59; sec++) {
        wait_until_exact_second();

        if (sec == 0) {
            print_now();
            putchar(':');
            print_timecode();
            putchar('\n');
        }

        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 output_timecode()");
            exit(EXIT_FAILURE);
        }
    }
}

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 wait_until_exact_second()");
            exit(EXIT_FAILURE);
        }
    }
}

void transmit_wave(int microsec) {
    /*
    print_now();
    */
    if (gpioHardwareClock(OUTPUT_PIN, OUTPUT_FREQ)) {
        perror("failed gpioHardwareClock()");
        exit(EXIT_FAILURE);
    }

    gpioDelay(microsec);
    gpioHardwareClock(OUTPUT_PIN, 0);
    /*
    fprintf(stdout, ":%d\n", microsec);
    */
}
//-------------------------------------
void terminate(int signal) {
    gpioHardwareClock(OUTPUT_PIN, 0);
    gpioTerminate();

    if (signal) {
        fprintf(stdout, "terminated by signal %d.\n", signal);
    }

    exit(EXIT_SUCCESS);
}
//-------------------------------------
void print_now(void) {
    set_now();
    fprintf(stdout, "%04d-%02d-%02d %02d:%02d:%02d.%06lu",
           now->tm_year + 1900,
           now->tm_mon + 1,
           now->tm_mday,
           now->tm_hour,
           now->tm_min,
           now->tm_sec,
           now_epoch.tv_usec);
}

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 print_timecode()");
            exit(EXIT_FAILURE);
        }
    }
}

上記を例えば~/src/JJY_simulator.cとして保存し、gcc -Wall -pthread -lpigpio -lrt -o ~/bin/JJY_simulator ~/src/JJY_simurator.cとしてコンパイルした後、sudo ~/bin/JJY_simulatorすれば動作する。
Raspberry Pi 7番ピンからの出力
40 KHzの出力図
図では分かりづらくなってしまったが、7番ピン(GPIO 7)と6番ピン(GND)をDSO150に接続すると、安定して40 KHzの出力が得られていることが見て取れる。

DSO150ではなくぐるぐる巻きにしたリード線を接続すると、弱いながらも電波が出力される。執筆時点では、アマゾンにメスメスのジャンパワイヤ0.6 mmエナメル線の在庫があったので、使用されるとよいだろう。ただ、当方で所有する時計のいくつかは、時刻合わせに長時間を要したり失敗したりしており、なお調査が必要と思われる。

コメントをどうぞ