公開日:2020/06/20
title.png

11th Season


#11-5 WiFi電波時計

 「電波時計」というと、普通はNICT(情報通信研究機構)が運用する標準周波数局JJYが送信する、40kHz/60kHzの電波を受信して、自動的に正確な時間を表示する時計のことを指します。ArduinoでJJYの電波をエミュレート(送信)した記事については「#5-9 JJYのエミュレーション」をご参照ください。
 Arduinoで電波時計を作ろうとする場合、今度はこの電波を「受信」しないとならないのですが、それはちょっと難しそうです。電波の受信回路の知識も無ければ、それを設計する能力もありません。そもそも筆者宅では、市販の電波時計でJJY電波をキャッチできない環境にあります。だからこそ、苦肉の策でJJYをエミュレートするシステムをArduinoで作成したのが「#5-9 JJYのエミュレーション」でした。

 一方、PCの時計はどうしているか?というと、インターネットからSNTP(Simple Network Time Protocol)で日時を取得して使っています。

※蛇足ですが、拙作iConClockを使うと、SNTPサーバ(時刻を提供する側)としても使用することが出来るようになっています。

 WiFiルータやWiFiアクセスポイントを使ってインターネットにつながるように設定してあるお宅であれば、Arduinoをインターネットに接続してSNTPで日時を取得した方が、安定して正確な日時を取得・表示できるはずです。Arduino+ESP32を使ったWiFiワイヤレス通信を選択すると、WiFi電波時計の完成、という訳です📡

ESP32を使う(おさらい)

 ESP32(下図)を使うのは久しぶりですね。

ar41_esp32.jpg

 2.4GHz帯の802.11gとして、WiFiアクセスポイント/WiFiステーションとして機能できるMCUボードです。Arduinoとしてセットアップすると、Arduino IDEを使ってC言語で開発可能な機体なのですが、筆者はデフォルトファームウェアのATコマンドを非同期シリアル(UART)通信で制御して使っています(例えば、以下記事など)。

  「#4-5 WiFiインターネット通信

  「#4-10 ArduinoとESP32の非同期シリアル通信

 メインに使用しているArduino UNOをESP32のホストとして使用するため、115,200baudの非同期シリアル通信が可能な高速ソフトウェアシリアル(HsswSerial)ライブラリを作成してあります。今回もHsswSerialを使いますので、ダウンロードしてインストールしておいてください。

  「#5-1 高速ソフトウェアシリアル(前編)

  「#5-2 高速ソフトウェアシリアル(後編)

  「#5-3 高速ソフトウェアシリアル(続編)

 115,200baudの通信速度はArduino UNOにとっては結構高速なので、ESP32から大量のデータが送りつけられた時に取りこぼさない様、HsswSerialのハードウェアフロウ制御(RTS)を有効にして通信します。

 Arduinoとの接続回路図は以下の様になります(使い回し)。

ar4a_schem.png

 今回はUNOと接続するので、スラッシュ(/)の右側の端子と接続してください。また、D4(CTS)は使わないので、今回は接続不要です。ESP32は3.3V定格のMCUなので、UNOとの接続にはレベルシフタが必要です。

ESP32のセッティング

 ESP32は、起動すると自動的にアクセスポイント(AP)に接続してIPアドレスを取得する様に設定しておく必要があります。設定が必要だと思われるATコマンドは以下の通りです。

コマンド/応答(例) 説明
送信 AT+CWMODE=1 動作モードを1=ステーションモードに変更
この設定は、不揮発メモリに記憶される。
受信 OK
送信 AT+CWJAP="<AP_SSID>","<passwd>" APのSSIDとパスワードを指定して、APに接続する。
この設定は、不揮発メモリに記憶される。
次回以降、ESP32の電源を投入すると自動的に、ここで指定したAPに接続するようになる。
受信 WiFi CONNECTED
WiFi GOT IP
OK
送信 AT+CWAUTOCONN=1 起動時にAPへ自動接続するかどうかの設定値。
この設定は、不揮発メモリに記憶される。
受信 OK
送信 AT+CWDHCP=1,1 DHCPでIPアドレスを取得する。
この設定は、不揮発メモリに記憶される。
DHCPを使わない場合は、"AT+CIPSTA"コマンドなどで指定しても良い。
受信 OK

 その他の細かな設定値は、「工場出荷時のデフォルト」を期待しています。デフォルト設定から変更している設定値がある場合、元の設定に戻しておく必要があるかもしれません。「#4-5 WiFiインターネット通信」なども参考にしてみてください。

※"AT+CWJAP"コマンドに指定する"<passwd>"などをArduinoのプログラム上に記述したくないので、ESP32の不揮発メモリに書き込んで、自動的に接続するモードにしておきます。

SNTPによる日時の問い合わせ

 ESP32でSNTPから日時を取得するコマンドは、「AT+CIPSNTPTIME?」です。予め、SNTPの設定をしておく必要があります。

コマンド/応答(例) 説明
送信 AT+CIPSNTPCFG=1,-9,"<SNTP>" 日本標準時(JST)のタイムゾーン(-9)を指定。
SNTPのサーバ名<SNTP>を設定しておく(オプション)。
この設定は、不揮発メモリには書き込まれないので毎回(リセットする度)設定する必要がある。
受信 +CIPSNTPCFG:1,-9,"<SNTP>"
OK
送信 AT+CIPSNTPTIME? SNTPリクエストで日時を取得して表示。
※SNTP取得に失敗しても「OK」が返ってくるので、エラーチェックできない。
受信 +CIPSNTPTIME:Tue Jun 9 20:54:16 2020
OK

 「#4-5 WiFiインターネット通信」の解説も参照しておいてください。サーバ名は、以下より検索してみてください。

  日本国内のSNTPサーバを検索する(別ウインドウが開きます)

 SNTPサーバ名は、プログラム(wifi_clock.ino)先頭付近の#defineに指定することが出来ます。

  #define ESP_SNTP ",\"ntp.xxx.jp\""  // esp: sntp server name

 マクロESP_SNTPには、「AT+CIPSNTPCFG=1,-9」の後ろに続くカンマ(,)から、サーバ名をダブルクオテーションマーク(”)で括って記述する必要があります。サーバ名は、カンマ(,)で区切って3個まで設定することが出来ます。
 サーバ名を指定しなかった場合、中国国内のSNTPサーバが指定されたことになります。ちょっと、ネットワーク的な距離が遠いので、日本国内のサーバを指定した方が良いと思います。

 ArduinoからESP32を制御するプログラムとしては、「#4-10 ArduinoとESP32の非同期シリアル通信」で作成したtelnet_server_esp.inoのesp_*()関数群が使用できるので、ここから流用しました。
 まず、ESP32の応答メッセージの中から、「+CIPSNTPTIME:」で始まる行を取り出します。この行に含まれる日付や曜日は不要なので、時刻「hh:mm:ss」の所を抜き出して、現在時刻を取り込みます。単なる文字列操作なので説明は省略します。

時計表示

 SNTPで時刻を取得できたら、後は表示なのだけれど、4桁程度の7SEG LEDとか、LCDディスプレイとかのデジタル表示はこれまでにも何度か扱って来たし、実際に時計を作ったこともありました(以下記事等)。

  「#2-7 7SEG LEDを表示する

  「#8-1 Pi ZeroWのI2C通信

  「#10-3 LCDサブシステムを何とかせねば

 上の記事は、タイトルこそ時計表示とは関係ない題名になっていますが、各テーマのアプリケーションとしてデジタル時計を作っていますので、ご参照ください。

 今回は円形配置RGB LED(以下図)を使って、アナログ表示にしてみたいと思います。

a112_rgbring.jpg

 そもそも、筆者がこの円形配置RGB LEDを見た時に最初に思いついたアプリケーションが時計でした。

 手持ちの円形配置RGB LEDは、円周上にRGB LED、WS2812Bが24個連なっています。これを時計の文字盤に見立てて表示します。文字盤の数字は1〜12までの12個ですが、そこに対しては×2倍のピクセルを有しています。しかし、アナログ時計として使う場合、1周で60分や60秒を表現する必要があり、こっちに対しては×24/60=1/2.5倍のピクセル数しかありません(足りない)。

 文字盤の針の角度で、直観的にざっくり時間を知ることが出来るのがアナログ時計の良いところだし、解像度(ピクセル数)不足の方はそんなに気にならないでしょう(1^^;)。

表示仕様

 円形配置RGB LEDのピクセル数に関しては、前節で考察の通りです。これを時計の表示に使うに当たっては、時計の短針・長針・秒針を表示する必要があるのですが、LEDは1重で長さの違いを表現するのは不可能です。そこで、短針・長針・秒針を各々、RGBの各原色に割り当てます。

 例えば秒針をGreen、長針をRed、短針をBlueで表現して、1重の円周上にマッピングします。秒針と長針が重なった場合は、Green+Red=Yellowと、合成した色で表示することにすれば、1重の円周上に3つの情報を同時に表示することが可能です。
 更に、表示効果として、以下のバリエーションを使い分けようと思います。

 秒針は二点表示としました。
 長針は現在時刻を点表示しますが、分が変化してから3(=clk_TRAC)秒間だけ線表示します。
 短針は現在時刻を点表示しますが、時が変化してから3(=clk_TRAC)秒間だけ線表示します。

ピクセルのマッピング

 円形配置RGB LEDのピクセルは、配線引き出し用のピンヘッダを取り付けた位置のピクセル#0から、時計回りに#23まで並んでいます(下図右端)。

a112_pinh.jpg

 ピクセルバッファpbuf[ ]は、添え字[0]を時計の0時位置として固定しておきます。一方、円形配置RGB LEDモジュールの設置状態次第では、ピクセル#0が、時計の何時位置に来るかわかりません。プログラムの実装に合わせた角度で円形配置RGB LEDを設置しなければならない、と言うのも難儀な話です。

 そこで、ピクセルバッファpbuf[ ]からWS2812Bへ送信するためのデューティバッファdbuf[ ]に変換する時に、オフセットを使って、ピクセル#0を何時位置とするか指定可能としておきました(↓)。

  // make send data
  cnt=0;
  for (idx=0, idxo=rgb_OFST*rgb_SIZ_PIX;
    idx<rgb_MAX_PIX*rgb_SIZ_PIX; idx++, idxo++)
  {
    if (idxo>rgb_MAX_PIX*rgb_SIZ_PIX - 1)
      idxo=0;
    dat=pbuf[idxo];
    for (idx2=0; idx2<8; idx2++)
    {
      // duty
      if (dat&0x80)
        dbuf[cnt]=rgb_DTY1 - 1;
      else
        dbuf[cnt]=rgb_DTY0 - 1;
      cnt++;
      dat <<= 1;
    }
  }


 オフセットrgb_OFSTは、ピクセル#0が表示すべき時間(0〜11時)×2とします。通常、rgb_OFST=0とするとピクセル#0が0時位置となり、rgb_OFST=6×2とするとピクセル#0が6時位置となります。

※WS2812Bとの通信方法については、「#11-2 円形配置RGB LED(WS2812B)を使う」で詳しく解説済みです。

 良く理髪店や美容院などで使われている、鏡に映した時に正常な方向に見える時計にしたい場合には、上コードidxoのインクリメント→デクリメント等に変更すれば、対応できるのではないか?と思います。……必要に応じて各自対応して見てください。

時計の実装

タイマの用途

 ここで、Arduino(ATmega328)のタイマを用途毎にまとめておきます。ATmega328のタイマは、システムタイマ(TC0)、16ビットタイマ(TC1)、8ビットタイマ(TC2)の3本です。

 と、言う事で、基本的に3本しかないタイマは全て用途が決まってしまいました。この他に時計を刻むためのタイマが1本必要なのですが、もう空きが無いので、うまいことTC0のmillis()関数を使って時計を実装する必要がある様です。

基準時間

 まず、必要になった時にESP32経由でSNTPから日時を取得し、この時の時刻(時,分,秒)を各々変数clk_h0,clk_m0,clk_s0に保持します。この時、同時にmillis()関数でその時点のタイムスタンプを変数clk_msecに取得します。
 こうすることで、millis()によって取得したタイムスタンプclk_msecが、clk_h0時clk_m0分clk_s0秒に相当することを知ることが出来ました。時間が経過したある時点で、現在時刻を知りたい時には、以下の様に算出することが出来ます。

  // reference time to current clock
  msec=millis () - clk_msec;
  ss=clk_s0 + msec/1000;
  mm=clk_m0 + ss/60;
  hh=clk_h0 + mm/60;
  ss%=60;
  mm%=60;
  hh%=24;


 上の処理後、現在時刻がhh時mm分ss秒だと知ることが出来ます。
 もうお解りでしょう、millis()で時計を刻むので、328duinoなど2冪周波数以外の機体では、少しづつ時計がずれてしまいます。A328dMicrosErrなどを使うとある程度補正することは可能ですが、2冪周波数以外の328duinoを使う場合は、ご注意ください。

※例えば、20MHzにクロックアップした328duinoの場合、millis()の誤差は−0.391%でした。1時間経過後の誤差は、1時間×(−0.391)≒−14秒となります。1時間で14秒の誤差はいただけないので、A328DUINO_ERR等を使って校正した方が良いでしょう(必要に応じて各自対応)。

 尚、hh,mm,ss等の変数をint(16ビット符号付整数)で保持する場合、clk_s0+msec/1000≦32,767である必要があります。すなわちmsec≦32,708秒≒9時間となり、最低でも9時間以内に、新たにSNTP日時を取得して基準時間clk_msecを更新する必要があります。
 プログラム(wifi_clock.ino)では、1時間毎の毎時clk_SNTP分に、ESP32からSNTP日時を再取得して基準時間を更新する様になっています。

割り込みの競合

 適切なタイミングで割り込みが発生する必要のある、システムタイマのTC0、HsswSerialライブラリのTC1が存在するのに対して、逆に割り込みを禁止して全データを一気に送信しきる必要のあるRGB LED(WS2812B)の制御が必要となります。
 本節では、割り込みと処理の競合について考察しておきます。

 これらの割り込みや処理が競合する(同時に起こる)と、不具合が発生します。

  例:

  実害:

 結論としては「問題なし(プログラムで制御可能)」と判定できます。

実験

 以下に完成したプログラムをリンクしておきます。

  完成プログラム:wifi_clock.ino

 コンパイルしてUNOに転送すると、シリアルモニタから以下のコマンドを受け付けるようになっています。

  コマンド仕様:

 デバッグやESP32の初期設定用のコマンドなので、単にWiFi電波時計として使用する場合には、シリアルモニタは必要ありません。プログラム先頭付近に「#define USE_SERIAL」というマクロの設定が有りますが、これをコメントすると、シリアルモニタを無効化してコンパイルすることが出来るようになっています。

 こちらの回路図と、以下ネットリストに従って、Arduino UNO⇔ESP32⇔円形配置RGB LEDを接続します。

Arduino UNO 接続 円形配置RGB LED
5V 5V
GND GND
D3(OC1B) DI
  × DO

 以下、実験中の様子です。

a115_expr.jpg

 上図の通り、円形配置RGB LEDのピンヘッダをブレッドボードに挿して固定した都合上、ピクセル#0が時計の6時位置となりました。よって、前の節で説明したrgb_OFST=6×2に設定してあります。


 本当は、毎正時に時報(音階で言うとA4とA5)を鳴らしたり、12時とか15時とかのポイントでは音楽を演奏しながら円形配置RGB LEDをカラフルに光らせたりしたかったのですが、音楽演奏play_mml()関数に必要なタイマに空きがなく、断念しました。


btn_arduino.png

btn_prev.png btn_next.png

home