NetBSD/rpiにRTCを繋げる

これはNetBSD Advent Calendar 2019の12日めの記事です。

はじめに

Raspberry PiでNetBSDを動かしていると単にメモリやCPUの性能以前にハードウェア面で不満なところがあります。一番面倒だと思っているのがバッテリーバックアップされた内蔵時計(RTC:Real Time Clock)が無いことです。そのため一度電源を切ってしまうと次回起動時に時間が狂ってしまいます(RTCが無い場合、ファイルシステムの最後の記録時刻を使って起動します)。もちろん常にネットワークに接続され、起動時に常にNTP等で時間を合わせられるような環境であれば良いのですが、せっかくの小型軽量組み込みボードです。私はモバイルバッテリで動かして持ち歩いているため、電池が切れる度に時計がずれていくのは正直辛いものがあります。たとえネットワーク接続性が無くても気軽に持ち運べる機器として使いたいし、ファイル等への記録日時も合っていて欲しいものです。

そんなわけで、Raspberry Pi用を謳った各種RTCボードが既に世の中にたくさんあります。また、もちろんICチップレベルで自分で継げても良いし、メジャーなチップなら既に多くのドライバやモジュールが用意されていて一般的には簡単に導入できるようになっています。ただしそれはもちろん標準のLinux環境での話。じゃぁNetBSDだとどうなんだろ、と。というわけでこれはRPIにRTCを接続し、NetBSDから使ってみた記録です。

使用したRTCについて

たぶんRPI用として売られているのであれば仕様はなんとかわかるだろ、てな軽い気持ちで今年の正月2日に名古屋大須をぶらぶらしていたら、大須アメ横ビルのボントンでRPI用RTCモジュールというまさにそのものが売られていました。値段も380円と安かったので即買い。もう1年近く前のことです。

採用されているチップはDS3231、ボタン電池付き(ただし電池ソケットではないので半田作業無しでの交換負荷)。データシートはぐぐったら出てきたこのあたり(PDF)を参照。

これ、RPIのソケットに直接挿せるようにピンヘッダ用コネクタが付いてます。I2C接続なので40ピンコネクタの1番ピン側(内側の端側)からつなげれば使えます。使用ピンは1,3,5,9。電源3.3VとGNDも(7番ピン=GPIO4は無接続。多分)

ただしこうして繋げると当然コネクタ埋めてしまいます。I2Cは他のデバイスも並列でつなげられるので、とりあえず試すにはいいのですが、他のセンサーとかデバイス繋げたいなら何らかのブリッジ基盤が必要です。

私の場合I2CにテキストLCDと環境(温度/湿度/気圧)センサを継げているので普通に線引っぱってコネクタをピンヘッダに付け替えてユニバーサル基板にハンダ付けしてしまいました。

とりあえずこれでハードウェアの準備は完了です。

なお、使用するRPIは以下の環境です。

NetBSD/rpiでRTCデバイスを見る

とりあえずNetBSD/rpiを通常のカーネルで起動します。手元環境はあいかわらずnetbsd-8ベースです(当時のcurrentはI2C使うと頻繁に固まってしまうため。その後のnetbsd-9ブランチはちゃんと試していない)。起動してもまだRTCは認識していません。I2Cデバイスなので、i2cscan(8)でデバイス接続状況を見てみます。

$ i2cscan iic1
iic1: found device at 0x3e
iic1: found device at 0x68
iic1: found device at 0x76
iic1: 3 devices found

ここで0x68というのがRTCデバイス(DS3231)です(0x3eはテキストLCD,0x76は環境センサ)。ちゃんとハードウェア的には認識されていることがわかりました。あとは時刻の読み出し/書き込みができればいいわけですね。

RTCの読みだしをしてみる

I2Cデバイスなのでioctl(I2C_IOCTL_EXEC)を使って読み書きできます。データシートを見るとデータはBCD形式でレジスタアドレス0x00~0x12に入っていますがアラームやその他のデータもあり、実質的には時計だけなら7byteを読めば良いようです。とりあえず適当なプログラムをでっち上げて読み出してみます。下記のi2c_read()は昨年の記事に書いたのと同じものです。

rtctest.c:

#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <dev/i2c/i2c_io.h>

int i2c_read(int fd, int addr, uint8_t *cmd, size_t cmdlen,
                uint8_t *buf, size_t buflen) {

        i2c_ioctl_exec_t i2cop;

        i2cop.iie_op =  I2C_OP_READ_WITH_STOP;      /* データの連続読み込み */
        i2cop.iie_addr = (i2c_addr_t)addr;          /* スレーブアドレス */
        i2cop.iie_cmd = cmd;                        /* コマンドポインタ */
        i2cop.iie_cmdlen = cmdlen;                  /* コマンドの長さ */
        i2cop.iie_buf = buf;                        /* データバッファポインタ */
        i2cop.iie_buflen = buflen;                  /* データ長 */
        return ioctl(fd, I2C_IOCTL_EXEC, &i2cop);  /* ioctl発行 */
}

int main() {
        int i, i2cfd;
        uint8_t cmd=0;
        uint8_t data[8];

        if ((i2cfd = open("/dev/iic1", O_RDWR)) == -1 )
                err(1,"open");

        if ( i2c_read(i2cfd, 0x68, &cmd, 1, data, 7))
		err(1,"ioctl");
        for (i=0;i<7;i++)
                printf("%2.2x ", data[i]);
        printf("\n");
}

早速実行してみます。

% ./rtctest
50 01 01 20 01 03 13

とりあえずエラーにはならず実行できてます。値は最初なのでむちゃくちゃかもしれないですが、連続して実行すると先頭の値が1秒おきに変わるので時計として動いているようです。

% ./rtctest
32 07 01 20 01 03 13
% ./rtctest
33 07 01 20 01 03 13
% ./rtctest
34 07 01 20 01 03 13

また、この値はRPIの電源を切って再度電源を入れると継続していることが確認でき、電池によるバックアップが有効であることもわかりました。

注: 上記の表示値は適当です。私が買った個体の初期値が何だったかは既に覚えていません。でも時計が動いていることの確認をこうして実行したことは間違いありません。

上記は読み出しだけですが、データシートの形式に合わせて7バイトを作りI2C_OP_WRITE_WITH_STOPで書き込みを行えば時刻設定できましたが、とりあえずプログラムは省略。

NetBSD/rpiの内蔵時計として認識させる

さてRTCの時計を合わせて実験してみると電源を一旦切ってまた電源を入れるとちゃんとバッテリバックアップされて時刻が読み出せることを確認できましたが、まだシステム時刻と連系していません。今までどおり電源を一度落とすと、次に起動したときに時刻がずれてしまいます。これを

などのような操作をすれば時刻は常に正しくなります。これを上記のサンプルのようなユーザ空間でのプログラムで実行しても良いのですが(実際あまり精度を必要としない自分の用途では十分に事足りていたりもします)、若干のタイムラグがあるし、せっかくだからカーネルのRTCとして認識させてやりたいところです。

ということで、やっとドライバの話。結論から言えばNetBSDには既にこのRTCチップをサポートするドライバ、dsrtc(4)存在します。man pageがなぜか存在しないようなのですが(netbsd-9でも)、とりあえずRPIのkernel configには以下の用にコメントアウトされています。

# 'DS3231 Raspberry Pi RTC Board Real Time Clock Module for Arduino'
# sold by linksprite.com
#dsrtc* at iic1 addr 0x68 flags 3231

てなわけでこれを有効にしたkernelを作ります。本当はモジュールになっていてくれればkernel作り直さなくてもいいのですが、残念ながら無さそうです。このconfigを直接編集するのではなく適当に以下のようなkernel configをsys/arch/evbarm/conf/RPIRTCとして作成しました。

include "arch/evbarm/conf/RPI"
dsrtc* at iic1 addr 0x68 flags 3231

当然ですが動かす本体がRPI2の場合はRPI2をincludeします。とりあえず今回はターゲットがRPI Zeroなのでこれで作成します(手元ではRPI2での動作は未確認)。

% cd /usr/src
% ./build.sh -m evbearmv6hf-el -T (path/to/tools) -O (path/to/obj) ... kernel=RPIRTC

正常に終わったら(path/to/obj)/sys/arch/evbarm/compile/RPIRTC/以下に通常のkernelであるnetbsdと、boot用のbinary netbsd.binが出来ています。このnetbsd.binを/boot/以下にコピー(カーネル名をconfig.txtで変更していなければファイル名をkernel.imgに変更)して再起動します。無事に起動したら、dmesgで以下の用にiic1の下にdsrtc0が表示されて、無事認識されているようです。

bcmspi0 at obio0 intr 54: SPI
spi0 at bcmspi0: SPI bus
bsciic0 at obio0 intr 53: BSC0
iic0 at bsciic0: I2C bus
bsciic1 at obio0 intr 53: BSC1
iic1 at bsciic1: I2C bus
dsrtc0 at iic1 addr 0x68: DS3231 Real-time Clock

これで後はntpdate(8)等で一度正確な時刻を合わせてやれば、あとは電源を切っても以降ちゃんと復帰してくれるようになります。

おまけ効果

dsrtc(4)として認識された後、時計以外にこのチップにある温度センサをenvstat(8)で取得できるようになります。

% envstat
                 Current  CritMax  WarnMax  WarnMin  CritMin  Unit
[dsrtc0]
  temperature:    23.500                                      degC
[vcmbox0]
  temperature:    34.166   85.000                             degC

RTCの温度が何に使えるかわかりませんが、とりあえずおもしろいところ(場所がメイン基板との距離で結構変わる)。私は他に(線をかなり伸ばして)BM280継げているので、外付け温度計が2つになりました。

おわりに

結論としては以下の二点で時計が保存されるようになりました。

一応今年9月の台風シーズンから運用してて、時計のずれも多少はあれどそこそこ精度もよさそうです

私のRPI Zeroには前記のとおりI2CでBM280とテキストLCDを繋げ、モバイルバッテリーとともに小箱に入れて持ち歩いています(山歩きのお供)。当然通常は操作コンソールがありませんしネットワークもありません。でも時計合ったおかげで行動ログの時刻を後で合わせる必要もなくなり大変便利になりました。NetBSDと、自分で組み合わせたセンサ等のデバイスと、たとえ自己満足であってもPCとは明かに違う、ちゃんと意味ある使い方が出来ているのがとても嬉しいし楽しいと感じます。 あ、イマドキだとiPhoneとかのスマホでできるって? でもそれ、NetBSDじゃないから。