ATmega328Pに1 MHz版ブートローダを書き込み

Arduino Unoを書き込み装置として、ATmega328Pに1 MHz版ブートローダを書き込む。

以前、『ArduinoのWatchdog timerを使用』でArduinoの低電力稼働を試みた。ウォッチドッグタイマのリファレンスとしてはある程度まとまったものの、残念ながら思ったほどの効果は得られなかった。これは、周辺部品の存在(LEDもそれなりに消費する)に加え、動作電圧と動作周波数の高さも原因のようだ。ATmega328Pのデータシートによると、5 V、16 MHzで稼働させた場合のアイドル電流は約2.4 mAである(Fig. 34-7)。一方、3.3 V、1 MHzの場合は約0.08 mA(Fig. 34-6)と、消費電力は約1/45程度で済む。そこで、ATmega328Pに1 MHz版ブートローダを書き込む方法を調査したので、以下にまとめる。出来の悪い卒論の出だしのようである。

ブートローダの作成

8 MHzのブートローダに比べ、1 MHzのブートローダに関する情報はそれほど多くはなかった。
MakerSpace
に記載されている方法で自分でコンパイルするのが安心のようである。まず、公式の“From Arduino to a Microcontroller on a Breadboard”からリンクされているbreadboard-1-6-x.zipをダウンロードした。当方で使用したArduino IDEは本記事記載時点で最新の1.8.5であるが、問題なかった。以後、ファイルをダウンロードしたフォルダを(DOWNLOAD_HOME)、Arduino IDEインストールフォルダを(ARDUINO_HOME)とする。ダウンロードしたファイルは(ARDUINO_HOME)\hardware以下に展開した。

展開すると、(ARDUINO_HOME)\hardware\breadboard\avr\boards.txtファイルが作成されている。これを退避させた後、boards.txtで置き換えた。ヒューズビットについては、よくまとまっている“Arduino / ATmega 328P fuse settings”を参考にした。次に、(ARDUINO_HOME)\hardware\arduino\avr\bootloaders\atmega\ATmegaBOOT_168.c(ARDUINO_HOME)\hardware\breadboard\avr\bootloaders\atmega下にコピーした。また、同フォルダにMakefileを作成した(雛形は(ARDUINO_HOME)\hardware\arduino\avr\bootloaders\atmega\Makefileである)。

最後に、MSYS2環境下でコンパイルした。ここまでの過程を以下に記す。当方で作成したATmegaBOOT_168_atmega328_1MHz.hexATmegaBOOT_168_atmega328_8MHz.hexは自己責任でお使いください。

$ cd (ARDUINO_HOME)/hardware/
$ unzip (DOWNLOAD_HOME)/breadboard-1-6-x.zip
Archive:  (DOWNLOAD_HOME)/breadboard-1-6-x.zip
   creating: breadboard/
   creating: breadboard/avr/
  inflating: breadboard/avr/boards.txt
   creating: breadboard/avr/bootloaders/
   creating: breadboard/avr/bootloaders/atmega/
  inflating: breadboard/avr/bootloaders/atmega/ATmegaBOOT_168_atmega328_pro_8MHz.hex
   creating: breadboard/avr/variants/
$ mv breadboard/avr/boards.txt breadboard/avr/boards.txt.orig
$ cp (DOWNLOAD_HOME)/boards.txt breadboard/avr/
$ cd breadboard/avr/bootloaders/atmega/
$ cp (ARDUINO_HOME)/hardware/arduino/avr/bootloaders/atmega/ATmegaBOOT_168.c .
$ cp (DOWNLOAD_HOME)/Makefile .
$ export PATH="(ARDUINO_HOME)/hardware/tools/avr/bin:$PATH"
$ export CC="avr-gcc -I(ARDUINO_HOME)/hardware/tools/avr/include"
$ make atmega328_8MHz
avr-gcc -g -Wall -Os -mmcu=atmega328p -DF_CPU=8000000L  '-DMAX_TIME_COUNT=F_CPU>>4' '-DNUM_LED_FLASHES=1' -DBAUD_RATE=57600 -DDOUBLE_SPEED   -c -o ATmegaBOOT_168.o ATmegaBOOT_168.c
avr-gcc -g -Wall -Os -mmcu=atmega328p -DF_CPU=8000000L  '-DMAX_TIME_COUNT=F_CPU>>4' '-DNUM_LED_FLASHES=1' -DBAUD_RATE=57600 -DDOUBLE_SPEED -Wl,--section-start=.text=0x7800 -o ATmegaBOOT_168_atmega328_8MHz.elf ATmegaBOOT_168.o
avr-objcopy -j .text -j .data -O ihex ATmegaBOOT_168_atmega328_8MHz.elf ATmegaBOOT_168_atmega328_8MHz.hex
rm ATmegaBOOT_168_atmega328_8MHz.elf ATmegaBOOT_168.o
$ make atmega328_1MHz
avr-gcc -g -Wall -Os -mmcu=atmega328p -DF_CPU=1000000L  '-DMAX_TIME_COUNT=F_CPU>>4' '-DNUM_LED_FLASHES=1' -DBAUD_RATE=9600 -DDOUBLE_SPEED   -c -o ATmegaBOOT_168.o ATmegaBOOT_168.c
avr-gcc -g -Wall -Os -mmcu=atmega328p -DF_CPU=1000000L  '-DMAX_TIME_COUNT=F_CPU>>4' '-DNUM_LED_FLASHES=1' -DBAUD_RATE=9600 -DDOUBLE_SPEED -Wl,--section-start=.text=0x7800 -o ATmegaBOOT_168_atmega328_1MHz.elf ATmegaBOOT_168.o
avr-objcopy -j .text -j .data -O ihex ATmegaBOOT_168_atmega328_1MHz.elf ATmegaBOOT_168_atmega328_1MHz.hex
rm ATmegaBOOT_168.o ATmegaBOOT_168_atmega328_1MHz.elf

ブートローダの書き込み

書き込みの方法は、ブートローダ作成の方法に比べ、ネット上に情報が潤沢にある。上述の公式サイトもその一つだ。ところが、サイトによって微妙に配線が異なる。ここでは、『ATMEGA328PをArduinoとして使う』に記載のものをベースに、『Arduino Unoを使ってATmega328Pにブートローダを書き込む』に従ってリセットピンに10 µFの電解コンデンサを接続した。また、8 MHzの水晶発振子の代わりに、“How to make an Arduino-compatible minimal board”で紹介されているD9ピンを使用した。

後は、Arduino Unoを書き込み装置に設定し、ボードを“ATmega328 on a breadboard (1 MHz internal clock)”としてブートローダを書き込めば終わりである。当初、breadboard-1-6-x.zipに含まれるATmegaBOOT_168_atmega328_pro_8MHz.hexをブートローダに用いてLow Fuse Byteの7 bit目を立てればよい、という記載に従ったが、単に動作が8倍遅くなるだけであった。つまり、&ldquot;Blink&rdquot;のdelay(1000)が8秒待ちとなる。ここまで記した方法ではクロックは合っているようである。1 MHzで動いているよね・・・。

サイト移転

http://firewheel.m40.coreserver.jp/からhttps://firewheel.coresv.com/へ移転した。

CORESERVERのサーバリニューアルに伴い、無料SSLが使用できるようになった。しかし、サーバ証明書は”*.coreserver.jp”に発行されているため、サブドメインが2階層(でいいのかな?)ついた元サイト”http://firewheel.m40.coreserver.jp”では「不正な証明書」扱いされてしまう。そこで、CORESERVERの無料サブドメイン”https://firewheel.coresv.com/”に形式的に移行した。元サイトから新サイトへは301でリダイレクトしていることもあり、事実上何ら変わらないかと。

これからもよろしくお願いします。また、移行不行き届きの箇所がありましたら、ご連絡いただければ幸いです。内容の不行き届きは平にご容赦を。

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エナメル線の在庫があったので、使用されるとよいだろう。ただ、当方で所有する時計のいくつかは、時刻合わせに長時間を要したり失敗したりしており、なお調査が必要と思われる。

CORESERVERのメールフィルタ

CORESERVERのサーバーに付与されるメールアカウントで、カスタムフィルタ同様、特定のドメインから送付されたメールを削除する。

前エントリの電波時計が決着しないまま、またも公私とも多忙となり、月日は無情に過ぎ去っていった。月日の代わりにやってきたものは、日本海対岸からのスパムメールの嵐である。題名・本文とも、とんと皆目つかない、いわゆる「とんとかいも」である(嘘)。明鏡止水の心境で捨て置く、というにはあまりに数が多すぎる。なにせ1通/分だから。

お陰様でこのブログの『CORESERVERのメール設定』は人気があるようで、ひとつこれに乗じてメールネタを増やしてみようと思った。まず試したのは、CORESERVERコントロールパネルの「ドメインメール」→「カスタムフィルタ」からの設定である。ウェブ上にもいくつかの情報があった。ところが、これはサーバ受信時に働くものではないようである。そこで、カスタマサポートにそれとなくprocmailが使えないか問い合わせたところ、以下の回答を得た。

誠に恐縮ですが、サーバーに付与されるメールアカウントにつきましては
カスタムフィルタ機能を有しておりません。

メールソフトなどの機能を用いて、制限いただきますよう
お願い申しあげます。

その間、30分程度。5つ星を付けて問い合わせをクローズにした。さすがCORESERVER、こうこなくっちゃ。簡にして潔、かつ高速。Simple is best。ggrks(いや、出てこなかったんだけど)。しかし、となると、自分で何とかしなければならない。

Perlのバージョンアップ

前置きが長くなりすぎた。使い慣れたPerlでIMAPクライアントを作り、特定のメールを削除することにする。当m40サーバのPerlは本記事作成時点で5.8系なので、まずはperlbrewで次期仕様の5.16系まであげておく。また、cpanminusをインストールし、クライアントモジュールNet::IMAP::Clientもインストールする。

$ curl -L http://install.perlbrew.pl | bash
$ source ~/perl5/perlbrew/etc/bashrc
$ perlbrew --notest install perl-5.16.3
$ perlbrew switch perl-5.16.3
$ perlbrew install-cpanm
$ cpanm -f Net::IMAP::Client

IMAPクライアントの動作確認

使用するスクリプトは以下のとおり。送信元の正規表現が$TARGET_PATTERNと一致したら削除、というごく簡単なものである(しれっと送信先をさらしておく)。このサイトの場合、firewheel<ユーザ名>m40<サーバ番号>に相当する。アップグレード済みのサーバではIMAPサーバ(server)が異なっているので、ご注意を。

#!/usr/bin/env perl
use 5.12.0;
use warnings;

use Net::IMAP::Client;

my $FOLDER_TO_SEARCH =  q{INBOX};
my $TARGET_PATTERN   = qr{qq[.]com \z}imsx;

my $client = Net::IMAP::Client->new(
    server          => '<サーバ番号>-coreserver-jp.value-domain.com',
    user            => '<ユーザ名>@<サーバ番号>.coreserver.jp',
    pass            => '<パスワード>',
    ssl             => 1, ## use SSL
    ssl_verify_peer => 0, ## don't use CA
    port            => 993,
) or die "failed to connect to the server.\n";
$client->login()
  or die "failed to login to the server.\n"
         . $client->last_error() . "\n";
$client->select($FOLDER_TO_SEARCH)
  or die "failed to select the folder.\n"
         . $client->last_error() . "\n";
my $message_ids_ref = $client->search('ALL', '^DATE', 'iso-8859-1')
  or die "failed to fetch messages.\n"
         . $client->last_error() . "\n";

process($client, $message_ids_ref);

$client->logout();

sub process {
    my ($client, $message_ids_ref) = @_;

    MESSAGE:
    for my $message_id (@$message_ids_ref) {
        my $summary_ref = $client->get_summaries($message_id);
        next MESSAGE
          if !defined $summary_ref || scalar @$summary_ref == 0;

        my $addresses_ref = $summary_ref->[0]{'from'};
        next MESSAGE
          if !defined $addresses_ref || scalar @$addresses_ref == 0;

        my $host = $addresses_ref->[0]{'host'};

        if ($host =~ $TARGET_PATTERN) {
            $client->delete_message($message_id);
        }
    }

    $client->expunge();

    return;
}
__END__

これを下記のようにして目的どおりメールが削除されることを確認する。

$ vi ~/bin/mail_processor.pl
$ chmod u+x ~/bin/mail_processor.pl
$ ~/bin/mail_processor.pl

一定時間ごとの自動作動

上記でスクリプトの動作確認が取れたら、一定時間ごとに自動的に作動させる。そう、cron。まず、"As a Futurist…"さんのポストを参考に、以下のシェルスクリプトを作成する。これを経由しないと、システムデフォルトの5.8系が使用されてしまう。

#!/bin/sh
export HOME=/virtual/<ユーザ名>
export BREWPERL=5.16.3

source ~/perl5/perlbrew/etc/bashrc
perlbrew use "perl-$BREWPERL"

exec "$@"

念のため、このスクリプト経由でもメールが削除することを再確認する。

$ vi ~/bin/env
$ chmod u+x ~/bin/env
$ /virtual/<ユーザ名>/bin/env perl /virtual/<ユーザ名>/bin/mail_processor.pl >/dev/null 2>&1

再確認後、CORESERVERコントロールパネルの「CRONジョブ」で毎xx分に、つまり1時間ごとに上記コマンドが実行されるようにすればよい。

本記事作成後、念のためメールボックスを覗いたら嵐は過ぎ去った後のようであった。何たることか。

Raspberry PiにWiring Piをインストール

Raspberry PiでハードウェアPWMの使用を試み、まずWiring PiおよびWiringPi-Perlをインストールする。

すっかり更新が止まっていた本ブログ、今年もゴールデンウィークを活用して若干ながらエントリを増やしたい。実用的なものを、とまず思いついたのは時刻の同期である。我が家には数台の電波時計があるが、残念ながら集合住宅内には標準電波が届かず、各自は同期せず思い通りの時刻を指している。何たることか。

そこで、NTPで時刻合わせの行われているRaspberry PiからJJY相当の電波を発射させ、放埓な時計どもにその本分を知らしめることにする。東日本ではJJYの周波数は40 KHz、矩形波では25 µsecのパルス周期となるため、精度よく出力するにはハードウェアPWMを使用する必要があるとのことである。幸い、Raspberry Piには簡単にアクセスできるWiring Piライブラリが存在するので、そのPerlラッパであるWiring-Perlとともにインストールする。

Wiring Pi、WiringPi-PerlともGitリポジトリ上に公開されているソースコードを入手し、ビルドして使用することになっている。

$ dpkg -l | grep -e '\sgit\s'
ii  git                                   1:1.7.10.4-1+wheezy1+rpi2               armhf        fast, scalable, distributed revision control system

gitはインストール済みなので、早速Wiring Piのソースコードを入手、ビルドする。

$ mkdir ~/git
$ cd ~/git/
$ git clone git://git.drogon.net/wiringPi
Cloning into 'wiringPi'...
remote: Counting objects: 742, done.
remote: Compressing objects: 100% (676/676), done.
remote: Total 742 (delta 536), reused 96 (delta 58)
Receiving objects: 100% (742/742), 264.80 KiB | 109 KiB/s, done.
Resolving deltas: 100% (536/536), done.
$ cd wiringPi/
$ ./build
wiringPi Build script
=====================


WiringPi Library
[sudo] password for firewheel:
[UnInstall]
[Compile] wiringSerial.c
[Compile] wiringPi.c
[Compile] piHiPri.c
[Compile] wiringShift.c
[Compile] piThread.c
[Compile] wiringPiSPI.c
[Compile] wiringPiI2C.c
[Compile] softPwm.c
[Compile] softTone.c
[Compile] mcp23008.c
[Compile] mcp23016.c
[Compile] mcp23017.c
[Compile] mcp23s08.c
[Compile] mcp23s17.c
[Compile] sr595.c
[Compile] pcf8574.c
[Compile] pcf8591.c
[Compile] mcp3002.c
[Compile] mcp3004.c
[Compile] mcp4802.c
[Compile] mcp3422.c
[Compile] max31855.c
[Compile] max5322.c
[Compile] sn3218.c
[Compile] drcSerial.c
[Compile] wpiExtensions.c
[Link (Dynamic)]
[Install Headers]
[Install Dynamic Lib]

WiringPi Devices Library
[UnInstall]
[Compile] ds1302.c
[Compile] maxdetect.c
[Compile] piNes.c
[Compile] gertboard.c
[Compile] piFace.c
[Compile] lcd128x64.c
[Compile] lcd.c
[Compile] piGlow.c
[Link (Dynamic)]
[Install Headers]
[Install Dynamic Lib]

GPIO Utility
[Compile] gpio.c
[Compile] readall.c
[Compile] pins.c
[Link]
[Install]

All Done.

NOTE: To compile programs with wiringPi, you need to add:
    -lwiringPi
  to your compile line(s) To use the Gertboard, MaxDetect, etc.
  code (the devLib), you need to also add:
    -lwiringPiDev
  to your compile line(s).

続いて、WiringPi-Perl。こちらは、そのままではビルドに失敗するため、0と1の欠片さんで紹介されている修正を加える。

$ cd ~/git/
$ git clone git://github.com/WiringPi/WiringPi-Perl.git
Cloning into 'WiringPi-Perl'...
remote: Counting objects: 38, done.
remote: Total 38 (delta 0), reused 0 (delta 0), pack-reused 38
Receiving objects: 100% (38/38), 388.16 KiB | 211 KiB/s, done.
Resolving deltas: 100% (15/15), done.
$ cd WiringPi-Perl/
$ git clone git://github.com/WiringPi/WiringPi.git
Cloning into 'WiringPi'...
remote: Counting objects: 206, done.
remote: Total 206 (delta 0), reused 0 (delta 0), pack-reused 206
Receiving objects: 100% (206/206), 185.49 KiB | 122 KiB/s, done.
Resolving deltas: 100% (86/86), done.
$ mv build.sh build.sh.orig
$ cp build.sh.orig build.sh
$ vi build.sh
$ diff build.sh.orig build.sh
1c1
< gcc -fpic -c -Dbool=char -I/usr/lib/perl/5.10.1/CORE wiringpi_wrap.c WiringPi/wiringPi/wiringPi.c -D_GNU_SOURCE
---
$ gcc -fpic -c -Dbool=char -I/usr/lib/perl/5.14.2/CORE wiringpi_wrap.c WiringPi/wiringPi/wiringPi.c -D_GNU_SOURCE
$ pushd WiringPi/wiringPi/
~/git/WiringPi-Perl/WiringPi/wiringPi ~/git/WiringPi-Perl
$ ln -s wiringSerial.h serial.h
$ popd
~/git/WiringPi-Perl
$ ./build.sh

後は電波を発射するのみである、と思ったのも束の間、40 KHzは可聴域を遥かに超えている。どうすれば出力されているパルスが正しい周波数になっていることを確かめることができるのであろうか。

Arduinoの気象観測データを取得

Arduinoで気象観測データサーバを構築し、Raspberry Piでデータを取得、音声出力する。

Arduinoを無線LAN接続にちらりと気象観測データサーバのことを書いたきり、あっという間に3か月以上経ってしまった。サーバへの接続リクエストをトリガにして休止モードから復帰できたらいいな、などと考えていたのだが、勤め人には勉強する時間が乏しく、何よりその能力に欠けていることに気づいた。無念である。ともあれ、お盆休みで到達したところまでを公開するものである。

気象観測データサーバ

サーバは、汎用性を持たせるため、HTTPサーバとしてHTMLデータを出力することにした。下記のスケッチはArduino付属のスケッチ例WebServerをほぼそのまま流用している。MACADDRESSの値は適宜変更のこと。

/*
 * Weather Data Server
 * Based on the Example Scketch "WebServer", which was
 * created 18 Dec 2009
 * by David A. Mellis
 * modified 9 Apr 2012
 * by Tom Igoe,
 * Copyright 2014 FireWheel <firewheel@m40.coreserver.jp>
 * GNU General Public License V3
 * see <http://www.gnu.org/licenses/>
 */
#include <SPI.h>
#include <Ethernet.h>
#include <RHT.h>
#include <MPL.h>
#include <Wire.h>

const unsigned int PORT = 80;
const char CR = 0x0D;
const char LF = 0x0A;
const char QQ = 0x22; // double quotation mark
const String CHARSET = "UTF-8";

byte MAC[] = {0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX};
uint8_t ADDRESS[] = {192, 168, 0, 101};

IPAddress ip(ADDRESS);
EthernetServer server(PORT);

void setup() {
  Ethernet.begin(MAC, ip);
  MPL.begin();
}

void loop() {
  EthernetClient client = server.available();

  if (client) {
    boolean current_line_is_blank  = true;

    while (client.connected()) {
      if (!client.available())
        continue;

      char c = client.read();

      if (c == LF && current_line_is_blank) {
        send_response_message(client);
        break;
      }

      current_line_is_blank
        = (c == LF) ? true
        : (c != CR) ? false
        :             current_line_is_blank
        ;
    }

    delay(1);
    client.stop();
  }
}

void send_response_message(EthernetClient client) {
  String content_type = "Content-Type: text/html; charset=" + CHARSET;
  String meta_charset = "<meta charset=" + quoted(CHARSET) + ">";
  String dd_temperature
    = "<dd id=" + quoted("temperature") + ">" + RHT.thermoString() + "</dd>";
  String dd_humidity
    = "<dd id=" + quoted("humidity")    + ">" + RHT.hygroString()  + "</dd>";
  String dd_atmosphere
    = "<dd id=" + quoted("atmosphere")  + ">" + MPL.atmString()    + "</dd>";

  client.println("HTTP/1.1 200 OK");
  client.println("Connection: close");
  client.println(content_type);
  client.println();
  client.println("<!DOCTYPE html>");
  client.println("<html>");
  client.println("<head>");
  client.println(meta_charset);
  client.println("<title>Weather Data</title>");
  client.println("</head>");
  client.println("<body>");
  client.println("<dl>");
  client.println("<dt>temperature</dt>");
  client.println(dd_temperature);
  client.println("<dt>humidity</dt>");
  client.println(dd_humidity);
  client.println("<dt>atmosphere</dt>");
  client.println(dd_atmosphere);
  client.println("</dl>");
  client.println("</body>");
  client.println("</html>");
}

String quoted (String to_quote) {
  return QQ + to_quote + QQ;
}

気象観測データクライアント

クライアントは使い慣れたPerlで作成した。まず、スクレイピングに必要なURIWeb::Scraper両モジュールをインストールする。Smart::Commentsはデバッグ用なので、お好みでどうぞ。

> sudo apt-get install liburi-perl
> sudo apt-get install libweb-scraper-perl
> sudo apt-get install libsmart-comments-perl

折角Raspberry Piでデータを受けるのだから、結果を音声出力させる。Raspberry Piの設定には明記していなかったが、ダウンロードしたパッケージはホームディレクトリ直下で展開している。

#!/usr/bin/env perl
# weather_data_client.pl - 気象データを取得して出力する
# Copyright 2014 FireWheel <firewheel@m40.coreserver.jp>
# GNU General Public License V3
# see <http://www.gnu.org/licenses/>
use 5.10.0;
use strict;
use warnings;
use utf8;

use URI;
use Web::Scraper;
#use Smart::Comments;

my $URI = q{http://192.168.0.101};
my $COMMAND_FORMAT   = q{~/aquestalkpi/AquesTalkPi %s | aplay >/dev/null 2>&1};
my $TIME_TEXT_FORMAT = q{%d時%d分};
my $WEATHER_TEXT_FORMAT
  = q{"気温 %.1f度、湿度 %.1fパーセント、気圧 %.1fヘクトパスカル"};

my $scraper = scraper {
    process '#temperature', temperature => 'TEXT';
    process '#humidity',    humidity    => 'TEXT';
    process '#atmosphere',  atmosphere  => 'TEXT';
    result 'temperature', 'humidity', 'atmosphere';
};

my $weather_data_ref = $scraper->scrape(URI->new($URI));
my ($hour, $minute)  = (localtime)[2, 1];
### $weather_data_ref
my $time_text    = sprintf $TIME_TEXT_FORMAT, $hour, $minute;
my $weather_text = sprintf $WEATHER_TEXT_FORMAT,
                           $weather_data_ref->{temperature},
                           $weather_data_ref->{humidity},
                           $weather_data_ref->{atmosphere};
my $time_command    = sprintf $COMMAND_FORMAT, $time_text;
my $weather_command = sprintf $COMMAND_FORMAT, $weather_text;
qx{$time_command};
qx{$weather_command};
__END__

cron(8)で15分毎に出力させる、などという使い方も面白いかもしれない。

Raspberry Piにリモートデスクトップログイン

Raspberry PiのAPT設定を変更、パッケージアップデートを行うとともに、日本語フォントとリモートデスクトップサーバxrdpをインストールした。

Arduinoで遊ぶのを少しお休みし、久しぶりにRaspberry Piに電源を入れた。何はさておき、パッケージアップデートを行う。日本国内にリポジトリがあるので、そちらに参照先を変更する。

--- /etc/apt/sources.list.orig  2014-01-08 08:30:39.000000000 +0900
+++ /etc/apt/sources.list       2014-05-24 13:20:57.384028810 +0900
@@ -1 +1,2 @@
-deb http://mirrordirector.raspbian.org/raspbian/ wheezy main contrib non-free rpi
+#deb http://mirrordirector.raspbian.org/raspbian/ wheezy main contrib non-free rpi
+deb http://ftp.jaist.ac.jp/pub/Linux/raspbian-archive/raspbian/ wheezy main contrib non-free rpi

sudo apt-get update && sudo apt-get upgradeすると、保留中のパッケージが1つあるので、何の気なしにsudo apt-get dist-upgradeした。対象のwolfram-engineは200 MB超のパッケージであった。やむを得ず待つことにする。

待てど暮らせど、ダウンロードは終了しない。途中で回線が切断し、リトライを繰り返しているようである。全く根拠はないが、有線のPC上で当該パッケージをダウンロードすることにする。ここで、RedHat系のyumはローカルパッケージのインストールが可能であるが、apt-getにはその機能がないため、gdebiをインストールして使用する。wgetに後を任せて、お昼寝に向かう。

$cd tmp/
$wget http://archive.raspberrypi.org/debian/pool/main/w/wolfram-engine/wolfram-engine_10.0.0+2014012903_armhf.deb
$scp wolfram-engine_10.0.0+2014012903_armhf.deb firewheel@192.168.0.5:/home/firewheel/tmp
$ssh firewheel@192.168.0.5
> sudo apt-get install gdebi-core
> sudo gdebi tmp/wolfram-engine_10.0.0+2014012903_armhf.deb

うまくいった。おしまい。ではなく、ついでに日本語フォントとリモートデスクトップ接続環境を整える。日本語フォントはOpenTypeとビットマップフォントを、リモートデスクトップサーバはWindowsからの接続ということもありxrdpを入れることにした。

> sudo apt-get install fonts-ipafont fonts-vlgothic xfonts-intl-japanese xfonts-intl-japanese-big xfonts-jisx0213
> sudo apt-get install xrdp

自動的にリモートデスクトップサーバが起動するので、早速Windowsから接続してみた。日本語も問題なく表示されている。
Raspberry Piのリモートデスクトップ画面
素敵。

Arduinoを無線LAN接続

Arduinoで測定した屋外気象データを屋内で取得するため、観測シールドに子機を接続し、無線LANに接続させた。

前回までに、Arduinoの気象測定シールドとマイクロSDシールドを作製して屋外気象データの取得と保存はできるようになった。しかし、都度外に出てマイクロSDカードを回収してくるのは面倒なので、持ち腐れてしまう。そこで、屋内からArduinoにアクセス、データが取得できないかと調査を行った。

XBee WiFiや無線LAN内臓SDカードなど、いくつか面白そうなアイテムを見つけたが、技術的に対応するのが難しそうと判断、おとなしく無線LAN子機を接続させることにする。AmazonからArduinoイーサネットシールド小型無線LANルータ「ちびファイ」(MZK-RP150NA)を購入した。だんだん大ごとになりつつある。なお、前者は、イーサネットシールドの中でMACアドレス付与が明記されているため、後者はQooskyに投稿された、とある勘違いさんのご推薦によるものである。

「ちびファイ」の設定は、付属する『コンバータ設定ガイド』に従えばそれほど難しくはないが、いくつかのはまりどころはあった。以下、作業手順をメモする。ポイントは、設定時のみ無線LANルータをSSID通知モード(機種によってはANY接続可)、PCをDHCP使用にすること、設定の度に「ちびファイ」のIPアドレスが変わることであった。

  1. 無線LANルータをSSID通知モードにする
  2. PCのIPアドレスを自動取得(DHCP)にする
  3. PCと「ちびファイ」をLANケーブルで接続する(LANケーブルは自前)
  4. PCのUSBポートから「ちびファイ」に給電する(USBケーブルは付属)
  5. PCのブラウザから192.168.111.1に接続する
  6. 「ステータス」→「モード」→「コンバータモード」を選択、「適用」をクリックする
  7. 「ちびファイ」再起動後、PCのブラウザから192.168.1.249に接続する
  8. 「セットアップウィザード」を選択、「次へ」をクリックする
  9. 無線LAN親機のSSIDを選択、「次へ」をクリックする
  10. 「無線接続のパスワード」を入力する
  11. ネットワーク構成に応じて、「IPアドレス」と「サブネットマスク」を例えば192.168.0.101255.255.255.0に変更する
  12. 「適用」をクリックする
  13. PCと製品のLANケーブル接続を外し、PCはルータに再接続させる
  14. PCのIPアドレスを固定設定に戻す
  15. 無線LANルータをSSID非通知モードに戻す
  16. PCのブラウザから上記で設定したIPアドレス(例では192.168.0.101)に接続する
  17. 「管理」→「ユーザー設定」を選択、パスワードを変更する
  18. 再接続、「無線LAN」→「基本設定」→「SSIDの通知無効」を選択、「適用」をクリックする

上記のように「ちびファイ」のコンバータモードを設定した後、イーサネットシールドに接続、Arduino IDEの“WebServer”テストスケッチで動作を確認した。時間を見つけて気象観測データサーバを仕立ててみよう。

Raspberry Piの設定

Raspberry PiにRaspbianをインストールし、USB無線LAN子機と音声出力の設定をするまでの作業メモ。

Raspbianのインストールと初期設定

RedHat 7.1からVine、CentOSとRedHat系を使ってきたが、Debianを使うテストも兼ねてインストール事例の豊富なRaspbianをインストールする。記載時点でのNOOBSのバージョンは1.3.4であった。1 GB少しあってダウンロードにかなりの時間を要するけれど、Raspbianのイメージファイルのみでも700 GB超だしddで書き込む必要もあり後々面倒そうなので、気長に待つことにする。

待った。ダウンロードしたzipファイルを解凍してSDカードに書き込み、キーボード、マウスとともにRaspberry Piに接続、TVとHDMIケーブルでつないでから電源を投入する。インストール手順は極めて簡単であり、OSに[RECOMENNDED]とされたRaspbian、言語に「日本語」、Keyboardに「jp」を選択した後、画面左上の「インストール」アイコンをクリックするだけであった。クラス10、16 GBのSDカードを使用して10分強の時間を要した。

インストール終了後、自動的に再起動、Raspberry Pi Software Configuration Tool (raspi-config)が立ち上がる。ここで一つ問題が。インストールオプションで日本語を選択したにもかかわらず日本語フォントはインストールされないようで、raspi-configの表示ががこれでもかというくらい豆腐まみれとなっている。そのため、「8 Advanced Options」、「A4 SSH」、「<Enable>」の順で選択するにとどめた。初期設定後はリモートからコンソールログインするので、日本語フォントは追加インストールしない。<Finish>を選択すると、また自動的に再起動される。

家庭内で使用するとはいえ、scott/tiger的なpi/raspberryのままでは気持ちが悪いので、一般ユーザを追加する。ここでまた問題発生。Debian系ではuseradd -mしないとホームディレクトリを作ってくれないようである。su - firewheelして気づいた。

> sudo bash
# passwd
# useradd -G sudo firewheel
# passwd firewheel
# su - firewheel
(ありゃ)
> exit
# mkdir /home/firewheel
# cp /etc/skel/.* /home/firewheel
# chown -R firewheel:firewheel /home/firewheel
# chmod -R go-rwx /home/firewheel
# su - firewheel
(よし)
>exit
# passwd -l pi

USB無線LAN子機の設定

これは丸ごと補助記憶さんの「Raspberry PiでUSB-Wifiアダプタを使う 」を踏襲した。当方で使用した子機は、AmazonでRaspberry Piを買ったときにお薦めされたBuffalo社のWLI-UC-GNMで、マウスを外したポートに挿すと自動認識された。設定ファイルの記載例を以下に示す。

/etc/network/interfaces

auto lo

iface lo inet loopback
iface eth0 inet dhcp

allow-hotplug wlan0
iface wlan0 inet manual
wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp

iface usb_wifi inet static
address 192.168.0.5
netmask 255.255.255.0
gateway 192.168.0.1
dns-nameservers 192.168.0.1

/etc/wpa_supplicant/wpa_supplicant.conf

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
        ssid="(無線LANルータのSSID)"
        #psk="(無線LANルータのkey)"
        psk=******
        scan_ssid=1
        proto=WPA2
        key_mgmt=WPA-PSK
        pairwise=CCMP TKIP
        group=CCMP TKIP
        id_str="usb_wifi"
}

再起動後、PCのコンソールから無事接続できることを確認する。

$ping 192.168.0.5

192.168.0.5 に ping を送信しています 32 バイトのデータ:
192.168.0.5 からの応答: バイト数 =32 時間 =99ms TTL=64
192.168.0.5 からの応答: バイト数 =32 時間 =109ms TTL=64
192.168.0.5 からの応答: バイト数 =32 時間 =21ms TTL=64
192.168.0.5 からの応答: バイト数 =32 時間 =52ms TTL=64

192.168.0.5 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 21ms、最大 = 109ms、平均 = 70ms

$ssh-copy-id -i ~/.ssh/id_rsa.pub firewheel@192.168.0.5
The authenticity of host '192.168.0.5 (192.168.0.5)' can't be established.
ECDSA key fingerprint is 8b:bd:fd:8f:7f:b0:94:6b:0c:cb:89:32:84:a1:85:32.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
firewheel@192.168.0.5's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'firewheel@192.168.0.5'"
and check to make sure that only the key(s) you wanted were added.

サウンド出力設定

こちらはほぼ“Raspberry-Pi Enable Sound”を踏襲した。audioグループに所属していないとサウンドデバイスにアクセスできず、amixer(1)asound(1)が使用できない。

> sudo apt-get update
> sudo apt-get upgrade
> cd /usr/local/bin/
> sudo wget https://raw.github.com/Hexxeh/rpi-update/master/rpi-update
> sudo chmod u+x rpi-update
> sudo ./rpi-update
 *** Raspberry Pi firmware updater by Hexxeh, enhanced by AndrewS and Dom
 *** Performing self-update
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   135  100   135    0     0     93      0  0:00:01  0:00:01 --:--:--   125
100  7026  100  7026    0     0   4210      0  0:00:01  0:00:01 --:--:-- 47795
 *** Relaunching after update
 *** Raspberry Pi firmware updater by Hexxeh, enhanced by AndrewS and Dom
 *** We're running for the first time
 *** Backing up files (this will take a few minutes)
 *** Backing up firmware
 *** Backing up modules 3.10.25+
 *** Downloading specific firmware revision (this will take a few minutes)
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   168  100   168    0     0    136      0  0:00:01  0:00:01 --:--:--   181
100 21.0M  100 21.0M    0     0  74097      0  0:04:57  0:04:57 --:--:-- 78795
 *** Updating firmware
 *** Updating kernel modules
 *** depmod 3.12.18+
 *** Updating VideoCore libraries
 *** Using HardFP libraries
 *** Updating SDK
 *** Running ldconfig
 *** Storing current firmware revision
 *** Deleting downloaded files
 *** Syncing changes to disk
 *** If no errors appeared, your firmware was successfully updated to d9a32ac33e3d19797ab8370d1f340cabf4167e28
 *** A reboot is needed to activate the new firmware
(再起動)
> sudo vi /boot/config.txt
> sudo vi /etc/asound.conf
> useradd -G sudo,audio firewheel

その後、N.Yamazaki’s blogさんの「AquesTalk Piの使い方まとめ」に従って音声出力されることを確認する。ショウ君的なたどたどしさはほとんどない。これが数千円のコンピュータで出力されるとは、便利な世の中になったものである。

ArduinoのWatchdog timerを使用

作製した「気象観測シールド」で数分おきにデータを取得するため、ArduinoのWatchdog timerの使用法と省電力動作を調査した。

Arduinoを休止モードからタイマー割り込みで復帰させるには、watchdog timerを使用するのが常法らしい。例えば“Sleeping Arduino – Part 1”から始まるポストなどにサンプルコードを見ることができる。しかし、レジスタやフラグの意味などが詳細に解説されたサイトはなかなか見当たらず、結局\hardware\tools\avr\avr\include\以下にインストールされたヘッダファイルと、ATmega328Pのデータシートに辿り着いた。それらを基にいろいろ試した結果、下記のコードとなった。

/*
 * Log Weather Data
 * Copyright 2014 FireWheel <firewheel@m40.coreserver.jp>
 * GNU General Public License V3
 * see <http://www.gnu.org/licenses/>
 */
#include <SD.h>
#include <Wire.h>
#include <RHT.h>
#include <MPL.h>
#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>

const char FILENAME[] = "weather.csv";
const uint16_t SLEEP_CYCLE = 30; // x 8 sec = 4 min to sleep
/* more than 1 sec */
const uint16_t WAIT_FOR_RHT = 1100;
const uint8_t CS_PIN = 10;

volatile uint16_t counter = 0;

void setup() {
  /* simply exit if SD initialization failed */
  if (!SD.begin(CS_PIN))
    return;
  MPL.begin();
  /* enable only the timer0 module for using millis() in RHT */
  power_all_disable();
  power_timer0_enable();
  /*
   * set unused pins power save mode
   * pins 9 to 13 and 17 to the end (19) are in use
   */
  for (uint8_t i = 0; i <= 16; i++) {
    if (9 <= i && i <= 13)
      continue;
    pinMode(i, INPUT_PULLUP);
  }
  /* the watchdog settings */
  /* disable interrupt */
  cli();
  /* watchdog timer reset */
  wdt_reset();
  /* clear WatchDog system Reset Flag on MCU Status Register */
  MCUSR &= ~(1 << WDRF);
  /*
   * set WatchDog Change Enable and WatchDog system reset Enable
   * on WatchDog Timer Control Register
   * (setting only WDCE does not seem to work)
   */
  WDTCSR |= 1 << WDCE | 1 << WDE;
  /*
   * set WatchDog Interrupt Enable, clear WDE
   * and set prescaler to 8 sec
   */
  WDTCSR = 1 << WDIE | 0 << WDE
         | 1 << WDP3 | 0 << WDP2 | 0 << WDP1 | 1 << WDP0;
  /* enable interruput */
  sei();
}

void loop() {
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  sleep_cpu();
  /* wait until the watch dog barks "bow-wow !" */
  if (counter == SLEEP_CYCLE) {
    sleep_disable();
    wake_up_routine();
  }
}

void wake_up_routine() {
  delay(WAIT_FOR_RHT);
  power_spi_enable(); // SD
  power_twi_enable(); // MPL

  String data = "";
  data += RHT.thermoString();
  data += ",";
  data += RHT.hygroString();
  data += ",";
  data += MPL.atmString();

  File logger = SD.open(FILENAME, FILE_WRITE);

  if (logger) {
    logger.println(data);
    logger.close();
  }

  power_spi_disable();
  power_twi_disable();
}

ISR(WDT_vect) {
  if (++counter > SLEEP_CYCLE)
    counter = 1;
}

まず、省電力設定として、avr/power.hの中で定義されているpower_all_disable()マクロで全モジュールを使用不可とし、必要なモジュールだけ使用可能にする。Arduino UNOで使用されているATmega328Pでは、以下の7モジュールを使用可能にするマクロが定義されている。これらには対応するpower_*_disable()マクロも存在する。

  • power_adc_enable()(ADコンバータ使用時)
  • power_spi_enable()(SPI使用時)
  • power_usart0_enable() (シリアルコンソール使用時)
  • power_timer0_enable() (タイマー0使用時、delay()millis()も)
  • power_timer1_enable()(タイマー1使用時)
  • power_timer2_enable()(タイマー2使用時、tone()も)
  • power_twi_enable()(I2C使用時)

また、使用していないピンの出力も抑える。arduino 720の“Power Saving Techniques”によると、消費電力はINPUT_PULLUPのほうがOUTPUTLOWよりも低いとのことであった。

watchdog timer設定のコードは上述のデータシート11.8.2の記載に倣った。割り込みを禁止するcli()と許可するsei()avr/interrupts.hで、watchdog timerをリセットするwdt_reset()avr/wdt.hで定義されている。MCUSRは11.9.1に記載があり、以下の4フラグを持つ。電源電圧低下など何らかの原因でwatchdog timerが有効になると、システムリセットの無限ループに入ることになるため、セットアップルーチンでWDRFフラグををクリアすることが推奨されていた。

WDRF
第3ビット。watchdogタイマーによるリセット発生時に立つ。
BORF
第2ビット。電源電圧低下によるリセット発生時に立つ。
EXTRF
第1ビット。外部リセット発生時に立つ。
PORF
第0ビット。電源投入時リセット発生時に立つ。

WDTCSRは11.9.2に記載があり、以下の8フラグを持つ。

WDIF
第7ビット。割り込みとして設定されたwatchdogタイマーのタイムアウト発生時に立つ。1を書き込むとクリアされる。
WDIE
第6ビット。watchdogタイマーを割り込みとして設定する場合に1をセットする。下記のWDEにも1をセットして割り込み+リセットとして設定した場合は、リセット発生の度に1を再セットする必要がある。
WDCE
第4ビット。WDEやプリスケーラビット(WDP3-0)を変更する前に1をセットする必要がある(WDCE単独では効果がないようで、WDEにも1をセットする)。4クロック後に自動的にクリアされるので注意。
WDE
第3ビット。watchdogタイマーをリセットとして設定する場合に1をセットする。MCUSRで説明したWDRFで上書きされるので、WDEをクリアする場合はまずWDRFをクリアする必要がある。
WDP3-0
WDP3(第5ビット)、WDP2(第2ビット)、WDP1(第1ビット)、WDP0(第0ビット)でプリスケール(PS)値を設定する。watchdogタイマーは、水晶発振ではなくRC発振で得られるので、タイムアウト(TO)値は入力電圧や周辺温度に依存する。

watchdogタイマーのプリスケール設定
WDP3 WDP2 WDP1 WDP0 PS値 TO値
0 0 0 0 2K (2048) 16 ms
0 0 0 1 4K 32 ms
0 0 1 0 8K 64 ms
0 0 1 1 16K 128 ms
0 1 0 0 32K 0.25 s
0 1 0 1 64K 0.5 ms
0 1 1 0 128K 1 s
0 1 1 1 256K 2 s
1 0 0 0 512K 4 s
1 0 0 1 1024K 8 s

watchdogタイマーの設定が終わったらスリープモードに入れる。avr/sleep.hで定義されているset_sleep_mode()でモードを設定し、sleep_enable()でスリープを許可、sleep_cpu()でスリープを開始する。スリープから復帰した後は、sleep_disable()でスリープを禁止してから次の命令に移る。sleep_enable()sleep_cpu()sleep_disable()を順に実施するsleep_mode()マクロも定義されているが、割り込みの競合は考慮されていない。avr/sleep.hで定義されているset_sleep_mode()の引数は以下の6種。SLEEP_MODE_IDLESLEEP_MODE_PWR_DOWNあたりが頻用されるようである。

SLEEP_MODE_IDLE
Idleモード。CPUクロックとフラッシュメモリ用クロックのみを停止する。
SLEEP_MODE_ADC
ADC Noise Reductionモード。CPUクロック、フラッシュメモリ用クロック、およびI/Oクロックを停止する。ADCクロックや非同期タイマークロックは動作したままなので、高分解能測定が可能となり、ノイズの多い環境下のA/D変換を改善する。
SLEEP_MODE_PWR_DOWN
Power-downモード。すべてのシステムクロックとオシレータを停止する。外部割(INT0、INT1)、ピン変化、2線モジュール(I2C)、watchdog、電源電圧低下の各割り込みでスリープから復帰する。
SLEEP_MODE_PWR_SAVE
Power-saveモード。Power-downモードに加え、動作しているtimer2を停止させず、割り込みに使用できる。
SLEEP_MODE_STANDBY
Standbyモード。外部クロックオプションが設定されている場合、Power-downモードに加え、オシレータを停止させない。
SLEEP_MODE_EXT_STANDBY
Extended Standbyモード。外部クロックオプションが設定されている場合、Power-saveモードに加え、オシレータを停止させない。

以上でwatchdogタイマーを使用したスリープモードの設定は終了した。お疲れさまでした。と、言いたいところであるが、watchdogタイマーは最長8秒(8,192ミリ秒)である。分の単位でスリープはできない。割り込み時に呼び出されるISRでカウントアップし、規定回数に達するまでは速やかにスリープモードに再突入させるようにして擬似的にプリスケールした。

結果、データの取得は意図どおりに行えたものの、駆動時間は約20時間にとどまった。1,900 mAhのニッケル水素電池(1.2 V)2本直列電源なので、5 Vで20時間駆動とすると消費電力は約46 mAh(計算合ってるかな)。思ったより電力消費は抑えられていないようである。どこか設定に間違いがあるのか・・・。

%d人のブロガーが「いいね」をつけました。