「#7-3 Raspberry Piで気温・湿度センサを読み出す」では、気温・湿度センサDHT11をPi ZeroWで読み出そうとして、微妙〜な、結論に終わりました。
「まだ懲りずに続けるの?」……続けますよ。でも、今度のRTC(DS1302、下図)は、前回(DHT11)とはちょっと、違います。
※ArduinoでDS1302を制御した記事は、「#2-5 リアルタイムクロックを使ってみる」です。
前回のDHT11が上手く読み出せなかった理由は、一度通信プロトコルが始まると、DHT11側が規定したタイミングで滔々とデータが流れてくるのを、OSのタスクスイッチによってCPUタイムが剥奪されている間に取りこぼしてしまうことでした。
今回のデバイスDS1302のどこが違うか?と言うと、DS1302はクロック同期型の通信方式(独自方式)を取っていることです。明暗を分ける、この違いについては後ほど説明します。
実は、Raspberry PiのRTCと言えば、DS1307みたいです(あるいは、DS3231)。
Raspberry PiのRTCを検索(別ウインドウが開きます)
Raspberry Pi用として販売されている、DS1307(やDS3231)ベースのRTCモジュールが有れば、適切なソフトウェアをインストールすることで、Raspbianのハードウェアクロック(hwclock)として動作させることができるみたいです(手元に無いので、未確認)。
DS1307は、今回使うDS1302と型番は近いですが、I2Cで接続するタイプです。I2Cなら、ARM11 SOC(BCM2835)に搭載されたI2Cユニットと、デバイスドライバ経由で通信できます。
「#7-2 デザートにラズベリーパイはいかが?」で立ち上げた環境(Pi ZeroW)では、ネットワークに接続してNTPで時計を合わせることが前提なので、わざわざDS1307(あるいはDS3231)のモジュールを購入する意味は無いと考えています。しかも、「買って来て繋いで、設定したらお〜しまい」では、面白味がありません(1^^;)。
しかし、何らかのトラブルでネットワークに接続できなかった場合、設定したNTPサーバ全てが応答しなかった場合、インターネットにルーティングできなかった場合、あるいはLAN接続機能を有しないPi Zeroを使う場合、等々、何らかの方法でハードウェアクロックを提供できる手段は合っても良いかな?と思ったのが今回のきっかけです。
手元にあったDS1302を活かして、NTP接続に失敗した場合のバックアップとして使用することを目指します。
ちょっと横道にそれますが、Linuxには、ハードウェアクロックと、システムクロックと言う概念があるそうです。
※ちょっと、紛らわしいですが、ここで使っているのは、同期信号と言う意味で使う「クロック」ではなくて、時計と言う意味の「クロック」です。
以上に加えて、Raspbianには、その中間を埋める、フェイクハードウェアクロック(fake-hwclock)という機能が備わっています。
Raspbian(に限らずUNIX)のシステムでは、ファイルのタイムスタンプや、その他諸々の処理において、時計が単調増加することを期待しています。従って、ハードウェアクロックの存在しないシステムだからと言って、起動の度に特定の時刻(例えば、1970/01/01 00:00:00等)に巻き戻ってしまう様では、色々と不具合が生じかねません。
基本的に、ボード上にバッテリバックアップRTCを持たないRaspberry Piの機体で、このような不具合を起こさないために導入された苦肉の策なのだと思います。
fake-hwclockの動作によって、少なくとも、Raspbianのシステムクロックが単調増加することは保証されます。
一度電源を落として(Shutdown)、暫く後、再び電源を入れて動作させると、Raspbianは、前回Shutdownした時の時刻から、再び時刻を刻み始めます。あたかも、電源を落としていた間の時間がすっぽり抜け落ちたかのように。
これが、ハードウェアクロック(RTC)を持たない場合の動作です。ハードウェアクロックが認識されていると、RTCから時計を読み出して、システムクロックに設定されるという訳です。さらに、NTPとの同期が有効に設定されていて、指定したNTPサーバとの通信が可能な状態にある場合には、NTPとの同期が行われます。
何らかの理由で、NTPとの同期ができない環境にある場合、Raspberry Piに対応したRTCモジュールを購入した方が便利です。本ページは、RaspbianのhwclockとしてDS1302を使う、と言うのとはちょっと違って、単に、DS1302モジュールをPi ZeroWに接続して、RTC時計を読み・書きする、だけですので、予めご承知の上、ご覧ください。
Arduinoを使った、DS1302の制御については、「#2-5 リアルタイムクロックを使ってみる」で解説済みですので、そちらを参照してください。
「#7-3 Raspberry Piで気温・湿度センサを読み出す」で、満足できる結果を出せなかったDHT11と、今回のDS1302のどこが違うか?と言うと、DS1302がクロック同期型のシリアル通信方式を採用していて、全てのデータはクロックに同期して変化する点です。基準となるクロックそのものも、Raspberry Piが作って出力するので、仮に、OSが20ms以上CPUタイムを取って行ったとしても、クロック波形が間延びするだけのことで、通信に影響しないのが、今回の特徴です(下図)。
※再び、紛らわしいですが、ここの「クロック」は同期信号の方の意味です。
つまり、DS1302はDHT11と異なり、Raspbian(Linux)というOSの制約(非RTOS)を乗り越えて、制御可能なはずです。
前節に登場した、DS1307は、クロック同期シリアル通信規格のI2Cを用いてデータをやり取りします。ここで使うDS1302は、独自仕様のクロック同期シリアルで通信する必要があります。ちゃんと探していないのでわかりませんが、多分、Raspbianからは認識してもらえないので、自分で作ることにします。それが、DIYの醍醐味でもありますので。
まず、RaspbianからDS1302にアクセスして、時計の読み出し・書き込みを行うコマンドrtc_ds1302を作ります。以下、目標仕様です。
基本的には、Arduinoで作ったプログラム(rtc_ds1302.ino)からの移植で、そのまま使えるはずです。だがしかし、Pi ZeroWは、ARM11 @1GHzという高速CPUを搭載しているので、生成する信号波形のタイミングに注意が必要です。
DS1302のデータシート(別ウインドウが開きます)の最後の方に、電気的特性(Electrical Characteristic)という章があって、さらにこの中でAC特性(AC Electrical Characteristic)という節に規定された、タイミングスペックを満たす必要があります。
AC特性を満たさなくてはならないのは、Arduinoも同じなのですが、Arduinoの場合は、ATmega328 @16MHzと遅かったため、特に何も考えなくても、AC特性を満たすことができました。
Pi ZeroWでは、そうはいかないので、データシートを確認したところ、以下のスペックを満たす必要があることが判りました。
const int rtc_usec_tdc=1; // rtc: tDC=200ns(MIN) -> 1us
const int rtc_usec_tcdh=1; // rtc: tCDH=280ns(MIN) -> 1us
const int rtc_usec_tcdd=1; // rtc: tCDD=800ns(MAX) -> 1us
const int rtc_usec_tclh=1; // rtc: tCL/tCH=1us(MIN)
const int rtc_usec_tcc=4; // rtc: tCC=4us(MIN)
const int rtc_usec_tcch=1; // rtc: tCCH=240ns(MIN) -> 1us
const int rtc_usec_tcwh=4; // rtc: tCWH=4us(MIN)
Pi ZeroWはARM11 @1GHzと言う、高速なCPUなので、ナノ秒([ns]、10-9秒)単位で制御しても良かったかもしれませんが、今回はマイクロ秒([μs]、10-6秒)単位で制御することとしました。よって、上の定数も、1μsを下回る規定の場合は、1μsに切り上げて保証します。
AC特性を満たすため、以下の様に、時間待ちする関数を起こしました。
void delayUsec (
int usec_dly)
{
// wait <usec_dly>[us] in real time
uint32 usec;
if (usec_dly<=0)
return;
usec=micros ();
while (micros () - usec<uint32 (usec_dly))
;
}
delayMicroseconds()関数をそのまま使っても良かったのかもしれませんが、wiringPiライブラリのdelayMicroseconds()関数の実装がわからなかったので、敬遠しました。
ArduinoのdelayMicroseconds()関数は、アセンブラレベルで作成した空のループを何回実行するか?によって、待ち時間を調整しています。つまり、待ちループの最中に、OSにCPU時間を取って行かれても、戻って来た時にそのまま、決められた回数をループするので、指定した時間より間延びすることになります。
上で示したコードでは、ループ内で毎回micros()を取得して、実際の経過時間(リアルタイム)で、指定時間ウェイトするので、OSに取って行かれたCPUタイムも含めて、ウェイトできることが解ると思います。さらに、ウェイト時間に負の数を指定された場合に、そのまま関数を抜けるようにしたかったので、引数を符号付整数(int)で受けたかったのも、理由の一つです。
あとは、digitalWrite()、digitalRead()等の関数で各I/Oポートを制御する間に、最初に示した定数(AC特性)に関係したdelayUsec()が挟まって、AC特性を満たしています。元のプログラム(rtc_ds1302.ino)と比較すると、変更点は明確になると思います。
完成プログラム:rtc_ds1302.cc
コンパイル:【g++ -lwiringPi rtc_ds1302.cc -o rtc_ds1302】
使い方:【rtc_ds1302 -h】で表示されるメッセージをご覧ください。
前節で検索して見つけたDS1302のデータシートで、DC特性(DC Electrical Characteristic)の節をご覧ください。電源電圧(Supply Voltage)の規定は、2V(MIN)〜5.5V(MAX)で、3.3V(TYP)となっています。
上限の電圧(MAX)が5.5Vまで許容されていて、Arduinoは5VのMCUだったので、Arduinoの時は5Vを供給しましたが、元々は3.3V定格のチップみたいです。Pi ZeroWも3.3V I/Oなので、電源電圧には3.3Vを供給すれば、そのまま(レベルシフタなしで)直結することができます。
Pi ZeroW | 接続 | RTCモジュール(DS1302) |
3.3V(phys#1等) | → | VCC |
GND(phys#9等) | ⇔ | GND |
phys#11 | → | CLK |
phys#13 | ⇔ | DAT |
phys#15 | → | RST |
Pi ZeroWは物理ピン#です。プログラム先頭付近の定数で変更できるので、お好みで変更してください。
すべてが整いましたので、実験です。以下、実験中の様子です。
メス-メスのデュポンワイヤが30cmの物しか無かったので、ちょっと余ってます……。六角スペーサ(一点固定)で2段積みにしてあります。
実験サンプル:【 】内はshellウインドウでのコマンド入力
【rtc_ds1302】
2019-08-31 xx:xx:xx …… 実行時の時間を示しても無意味なので伏せ字。
【rtc_ds1302 -set "2019-01-02 03:04:05"】
2019-01-02 03:04:05
※日付は"2018/08/31"ではなく、"2018-08-31"です(ハイフン、マイナス記号)。理由は次の節でわかります。
「-set」オプション実行時に表示される時刻は、設定(書き込み)後に読み出した結果ですので、正しく書き込み出来ていれば、「-set」オプションに渡した時刻が読み出されるはずです。
もし、「-set」オプションで、RTCの時計を変更できなかった場合は、書き込み禁止フラグが立っているかもしれませんので、以下をお試しください。
【rtc_ds1302 -wp=false】
OK: write WP bit=0x00 (Write enabled)
【rtc_ds1302 -set "2019-01-02 03:04:05"】
2019-01-02 03:04:05
DS1302には、アクセス時のエラーを検出できる機構が無いので、エラーメッセージは出力できないのですが、もし、うまく動かない場合は、どこか、回路か接続が、間違っているかもしれません。このプログラムが動作しないと、先に進んでも仕方ないので、デバッグしてみてくださいm(1__)m。
なお、DS1302の時計を設定(書き込み)する場合、秒レジスタ〜年レジスタまで、全てのレジスタを1s以内に書き込む必要があります(「#2-5 リアルタイムクロックを使ってみる」参照)。試した範囲では、数〜数10[ms]で書き込みが完了していたので、問題ないはずですが、OSや他のサービス等の処理が重くなった場合、1s以上かかってしまうと、エラーメッセージが出力されます。その場合、書き込みは実行完了していますが、恐らく、正しくない時刻を書き込んでしまっていますので、リトライしてください。
※さすがに、このエラーにぶち当たることは無いはずですが、念のため書き添えておきました。
前節で、Pi ZeroWにRTCモジュールを接続し、RTCの読み出し・書き込みを実行するコマンドが完成しました。NTP同期が失敗した場合のバックアップに使うための次のステップは、Raspbianのシステムクロックを設定する方法を知ることです。
初回、「#7-2 デザートにラズベリーパイはいかが?」でいきなり実行した、dateコマンドでは、現在のシステムタイムを表示することができることを知りました。実は、このコマンドでシステムタイムを変更することが可能です。
実験サンプル:【 】内はshellウインドウでのコマンド入力
【date】
Sat Aug 31 xx:xx:xx JST 2019 …… 実行時の時刻が表示された
【man date】
: dateコマンドの使い方が表示される。
【date -s "2019-01-02 03:04:05"】
date: cannot set date: Operation not permitted
…… ユーザpiではシステムクロックを設定できないみたいです。
Wed Jan 2 03:04:05 JST 2019
【sudo date -s "2019-01-02 03:04:05"】
Wed Jan 2 03:04:05 JST 2019
…… rootに格上げして実行したので、ノーエラーだった。
【date】
Sat Aug 31 xx:xx:xx JST 2019 …… 変わってない!
あれれ?変更できていません。【man date】では、「-s」オプションでシステムタイムを設定できると書いてあるし、【sudo date -s "xxx"】で、エラーも表示されませんでしたが……。
ダメですね。もう少し調べてみると、timedatectlと言うコマンドに辿り着きました。
【man timedatectl】
: timedatectlコマンドの使い方が表示される。
このコマンドを実行すると、NTP同期の設定状況や同期状態なども知ることができるみたいです。あとは、NTP同期の無効化や、システムタイムの設定変更も、このコマンドでできます。
【timedatectl show】
Timezone=Asia/Tokyo
LocalRTC=no
CanNTP=yes
NTP=yes …… NTP同期設定が有効
NTPSynchronized=yes …… NTP同期が成功した
TimeUSec=Sat 2019-08-31 xx:xx:xx JST …… 実行時点の時刻
【timedatectl set-time "2019-01-02 03:04:05"】
Failed to set time: Automatic time synchronization is enabled
「自動的な時刻同期が有効なので、時刻の設定に失敗」。NTP同期のことかな?さっき、dateコマンドでシステムクロックを設定できなかったのも、同じ理由だと推察できます。何かエラーメッセージを出してくれても良いのに……。
尚、rtc_ds1302で表示する時刻のフォーマットは、上の【timedatectl set-time "xxx"】の書式に合わせてあります。
NTP同期設定を無効にしてから、システムクロックを設定してみましょう。
【timedatectl set-ntp false】
…… ここで、資格を問うウインドウが開く。ユーザpiとパスワードを入力
【timedatectl set-time "2019-01-02 03:04:05"】
…… ここでも、資格を問うウインドウが開く。ユーザpiとパスワードを入力
【date】
Wed Jan 2 03:04:16 JST 2019 …… 変更できた。
なるほどね。良くわかりませんが、Raspbianでは、何か1枚皮を被っているみたいです。timedatectlコマンドを通して操作しないと、システムクロックを設定できない模様(少なくとも、dateコマンドでは設定できなかった)。しかも、NTP同期が有効の状態だと、システムクロックを変更できないようになっている。
NTP同期設定の変更や、システムタイムの設定時に、一々GUIが開いて資格を問われてしまうと、自動処理にし難いですね。以下の様に、sudoでrootに格上げすれば、GUIが開かずに設定変更ができました。
【sudo timedatectl set-ntp true】
…… パスワードを聞かれることなく、無言で終了
※sudoの実行時にパスワードを問う様に設定している方は、上の方法でも、自動処理にはできません。あしからず。
更に続けます。NTP同期が成功している間は、RTCの出番は無いので、一旦NTP同期を使えない状態にして見ます。【sudo timedatectl set-ntp false】で無効化してしまうと、システムクロック設定の挙動が変わってしまって、実験にならないので、無効化するのではなく、NTP同期が失敗する設定にしてみます。
NTP同期の設定ファイルは、/etc/systemd/timesyncd.confだそうです。
【sudo vi /etc/systemd/timesyncd.conf】 …… rootに格上げして編集
[Time]
NTP=FAIL.ntp.xxxx.jp
FallbackNTP=FAIL.ntp.yyyy.jp FAIL.ntp.zzzz.jp
【ZZ】 …… セーブしてエディタを抜ける
※後で元に戻せるように、変更点はご自身で覚えておいてください。
「NTP=」と、「FallbackNTP=」が、NTPサーバのURL設定行です。存在しなかった場合は追加して、無効なNTPサーバのURLを記述しておきます。既にこの行が存在した場合は、有効なNTPサーバ名が記述されていますので、頭に適切な文字列を追加して、無効なURLに変更します(上の例では「FAIL.」を追加)。
【sudo reboot】 …… 設定を有効にするために再起動
…… 以下、再起動後に実行
【timedatectl show】
Timezone=Asia/Tokyo
LocalRTC=no
CanNTP=yes
NTP=yes …… NTP同期設定が有効
NTPSynchronized=no …… NTP同期できていない→失敗
TimeUSec=Sat 2019-08-31 xx:xx:xx JST
NTP同期設定を有効にしたまま、NTP同期に失敗して、システムクロックを設定できない状況を作り出しました。
では、手動でシステムクロックを設定してみましょう。
【timedatectl set-time "2019-01-02 03:04:05"】
Failed to set time: Automatic time synchronization is enabled
やっぱり、NTP同期設定が有効だと、システムクロックを設定させてくれないようです。めんどくさい。試しに以下を実行してみます。
【sudo date -s "2019-01-02 03:04:05"】
Wed Jan 2 03:04:05 JST 2019
【date】
Wed Jan 2 03:04:09 JST 2019 …… アレ?変わった。
今度は、dateコマンドでシステムクロックを設定できてしまいました。違いは、NTP同期が失敗した状態と、成功した状態、それだけ。
コマンド | NTP同期有効 | NTP同期成功 | システムクロック |
date -s | 無関係 | 成功(NTPSynchronized=yes) | 設定できない |
失敗(NTPSynchronized=no) | 設定できる | ||
timedatectl | 有効(NTP=yes) | 無関係 | 設定できない |
無効(NTP=no) | 設定できる |
こんな感じ?なんだかよくわからないシステムですね……。皮一枚被ったせいで、複雑怪奇な挙動になっています。今後バージョンが変わると、この挙動も変わってしまう可能性あり、です……。
ともあれ、システムクロックを設定したいケースは、NTP同期が失敗(NTPSynchronized=no)している場合だけ、ですので、何らかのプログラムミスで、誤ってシステムクロックを変更してしまうことが無い点も含めて、dateコマンドを使うのが良さそうです。
忘れずに、/etc/systemd/timesyncd.confを元に戻して、【sudo reboot】してください。
時計の同期方向は、2方向考えられます。Raspbian→RTC(DS1302)と、その逆RTC(DS1302)→Raspbianです。
同期可能な条件を自動で判定して、適切な方向に時計を同期するスクリプトを作成しました。
時計同期スクリプト:rtc_ds1302.csh
実験サンプル:【 】内はshellウインドウでのコマンド入力
…… NTP同期が成功していた場合の実行例
【rtc_ds1302.csh】
1302: 2019-08-31 xx:xx:35
DATE: 2019-08-31 xx:xx:28
--------
clock sync Raspbian --> RTC
2019-08-31 xx:xx:29
--------
1302: 2019-08-31 xx:xx:29
DATE: 2019-08-31 xx:xx:29
…… NTP同期が失敗していた場合の実行例
【rtc_ds1302.csh】
1302: 2019-08-31 xx:xx:xx
DATE: 2019-01-02 03:04:21
--------
clock sync RTC --> Raspbian
Sat Aug 31 xx:xx:xx JST 2019
--------
1302: 2019-08-31 xx:xx:xx
DATE: 2019-08-31 xx:xx:xx
rtc_ds1302.cshの実行には、rtc_ds1302コマンドにパスが通っている必要があります。スクリプト内の処理については、rtc_ds1302.cshを直接参照してください。RTC(DS1302)の書き込み禁止フラグはクリアされていることが前提となっています。
さて、起動時に1回、実行させるには?どうすれば良いのかな?crontabを使うのが良さそう。……まあ、いっか。ここまでにしておこう。基本はNTPで同期する前提なので、あくまでバックアップ手段。必要な時に手動でrtc_ds1302.cshを実行することにします。
※【man crontab】と【man 5 crontab】にて、設定方法を知ることができますので、必要に応じて各自で設定してください。「@reboot」が目的のトリガになるはずです。
OSの挙動を制御したくなると、途端にめんどくさくなる……。昔は面白がってやってたんだけどね……。もういいや、ウンザリ、ご馳走様。今回の収穫は、モノによっては、ちゃんとRaspberry Piから制御可能な電子回路もあるんだね、ってことが判ったこと。これでまた、一歩前進。