勝手な電子工作・・

勝手なオリジナル電子工作に関する記事を書きます

型取りゲージの電子化?(ToFで実験)

f:id:a-tomi:20200830203817j:plain

これは「型取りゲージ」の写真です。複雑な形に合わせて床材やカーペット等をカットするための巧妙なゲージで、型取り定規と言われることもあります。英語ではProfie GaugeとかContour Gaugeとか。

もしこの道具を使わずに、例えば次のような複雑断面の柱に合わせてカーペットを切ろうとすると大変なことになります^^;

f:id:a-tomi:20200830204523j:plain

1mmφのピアノ線のエレメントが自由にズレて型をとる仕掛けで、これを使えば切断材料に相手の形をなぞり書きすることができます。それなりの精度とスムーズな動きが必要なために安価な道具ではありません。

しかし最近では安価なABS樹脂製(中国製)が出回っているようです。少し目が粗い(次の写真のものはエレメントが1.5ミリ幅)とはいっても、材料の革新だなあと勝手に感心します。型をとった状態でロックするメカニズムもなかなか素晴らしい!

f:id:a-tomi:20200830205054j:plain

とはいうものの、現代のことですからデジタル化などできないの?と、ふと思いついてしまいました^^;

仕組みはいくつか考えられますが、手始めにToFセンサー(Time of flight 光が反射して戻るまでの時間で距離を測るセンサー)を使うとどうなるでしょうか?できるような気がしますので、まずはやってみることにします。

ToFセンサーはカメラで近距離を測る用途などにも使われるようになり、ずいぶん安価になりました。たとえば次のToFセンサーVL6180Xのブレークアウトモジュールは海外ネットでは約2ドル強で入手できます。

 

f:id:a-tomi:20200830210212j:plain

真ん中にあるデバイスがVL6180X 本体で、赤外線ビームを出す部分と反射した赤外線を受ける部分があります。使われている赤外線の波長は850nmですので、もちろん目には全く見えません。

このセンサーを型取りゲージと同じ1ミリ単位で正確に水平移動させながら、反射対象までの距離を測っていくことで、同じことができるのではないかと考えました。実験用回路は、毎度手書きのきたないメモですみませんが、次のような簡単なものです。ステッパーの駆動にA4988というモジュールを使い、送りねじ機構で動かします。

f:id:a-tomi:20200830210940j:plain

プログラムではArduinoから測定データをPCへ送って、PC側でリアルタイムのグラフ表示などをしています。Arduino側とPC側の両方の実験プログラムをこの記事の最後の方につけます、とても小さいですが。

実験装置全体は次のように作りました。

f:id:a-tomi:20200831103553j:plain

動かすセンサーを前から見たのが次の写真です。センサーにある丸い窓2つ(赤外線ビーム放出窓と反射光検出窓だろうと思います)が見えています。

f:id:a-tomi:20200831103727j:plain

 

スライドユニットにセンサーをとりつけるために、木片をつけそれにブレッドボードをとりつけてあります。大きさを合わせるため、次の写真のように上のボードを下のサイズへ強引にカットして加工^^

f:id:a-tomi:20200830211500j:plain

ToFセンサーが揺れないよう、ブレークアウトモジュールに足をしっかりつけます。穴のない側にもホットボンドで無理に台と足2ピンだけをつけます。

f:id:a-tomi:20200830211734j:plain

表から見ると次。2つのネジ穴にホットボンドとピンの端が見えています。

f:id:a-tomi:20200830211858j:plain

 

まずはこういう紙型をいじりながら試験。暗い時のほうが結果は良いかと思いますが、かなり明るい状態でも特に問題なさそう。

f:id:a-tomi:20200830211337j:plain

まずは次の型取りができました。なんとなくうまくできてる!かな?

f:id:a-tomi:20200830212243j:plain

それでもよくみると、対象への距離は設定どおりで正確ですが、形が実際より滑らかです。変ですね。

測ったときの断面は下から見るとこれです。不細工ながら、円弧、台形、三角形にしたつもり。

f:id:a-tomi:20200831105148j:plain

手でほぼ正確に測った緑の破線と重ね合わせると次のようになっています。

 

f:id:a-tomi:20200830212513j:plain

台形は丸身を帯び、三角形も丸身を帯びてます(・・? なぜ? ん?

 

原因を調べるため、もっと厳しい直方体の木材や柱を立ててはかってみましたら、ありゃ? 少し差がでるという状況ではありませんね、これは! とくに形の間の深いところはひどく浅くなっています、、、

f:id:a-tomi:20200830212915j:plain

原因をあれこれ考え、ToFの赤外線は勝手に想定していた細いビームではない?!と勘づきます。あちこちで反射している可能性がありそう。

850ナノメーターの赤外線放出の様子を見るのは難しそうですが、この際は勝手な赤外線カメラで観察することにします。そのためにセンサーだけをつけた単体をこしらえて、これでじっくり観察します。

f:id:a-tomi:20200830214541j:plain

使う赤外線カメラは、古いデジカメの撮像面にセットされていたIRカットフィルター(赤外線を遮蔽し可視光だけを通すフィルター)を外して(というよりピンセットで無理にはぎ取って)、代わりにローパスフィルター(可視光を通さず波長780ナノメーター以上の赤外線だけ通すもの)をとり付けたまさしく勝手な赤外線カメラです。

これにより、センサーから出ている赤外線はビームの形どころか、びっくりする程広がっているのがわかりました。

f:id:a-tomi:20200830213856j:plain

これは想定外です!

ひょっとしてこのToFセンサー固有のことなのかなと微かな希望をもって、他のセンサーも調べてみることに。手元にある別のToFセンサーはVL50L0Xで、そちらでも観察することにしました。

こちらはVL6180X よりも数年前の製品です。

f:id:a-tomi:20200830214411j:plain

f:id:a-tomi:20200830214439j:plain

同じ方法で観察したビーム形状が次です。

f:id:a-tomi:20200830214754j:plain

赤外線が弱いようですが、やはり細いビームではなくこちらも光芒がずいぶん広がっています。

他の製品についてもよく調べる必要がありますが、センサーの精度を高める目的から言えば細いビームのもあってよいかな?と考えゆっくり調べることにします。あるとしてもこの目的には高価ではないかな?もしよいのがない場合は、センサーの前に広がりを絞る反射しにくい筒でもつけるのがよいかな?などともあれこれ考え中。

というわけで、すぐできると思ったこの実験、すこしずつ長引きそうです。精度の高いのが完成するまで待つといつになるかわからないので、いったんご紹介することにしました。どなたか良い案を考えたら是非デジタル化してくださいね^^;

 最後に、実験に使ったかんたんなArduinoスケッチとPC側のプログラム(例によって簡単なSmall Basicで書いてます)をつけておきます。

まずArduinoのスケッチ。

/********************************************************** 
 *  Profile gauge with VL6180X to Xfer data to Small Basic
 *    Initial version V00    Aug.29,2020 Akira Tominaga
***********************************************************/
#include "Wire.h"
#include "VL6180X.h"
VL6180X ToF;
#define Dir 3           // A4988 stepper direction
#define Step 4          // A4988 Step
#define Nreset 5        // A4988 !reset
#define Nsleep 6        // A4988 !sleep
#define Led 13          // LED
uint8_t nTimes = 32;    // number of ToF measures for average

void setup() { // ***** Arduino Setup *****
  float Kyori;
  float Tot;
  float Ave;
  uint16_t iAve;
  pinMode(Nsleep, OUTPUT);
  sSleep();  // sleep A4988
  pinMode(Led, OUTPUT);
  digitalWrite(Led, LOW);
  Serial.begin(9600);
  Wire.begin();
  bLED(2, 300, 100);    // show I am awake
  ToF.init();
  ToF.configureDefault();
  ToF.setTimeout(200);
  pinMode(Dir, OUTPUT);
  pinMode(Step, OUTPUT);
  digitalWrite(Step, LOW);
  sWake();              // wake A4988 up
  pinMode(Nreset, OUTPUT);
  sReset();             // home at current position
Repeat:
  while (Serial.available() < 1) {} // wait for Smallbasic ready
  String s = Serial.readString(); // read Smallbasic message
  delay(1000);

  // move stepper to left end
  sN(1600);              // X-1600 times (X-80 mm)negative
  sSleep();             // stop and sleep to avoid heat
  sWake();              // and wake stepper
  sReset();             // reset at Left end
  delay(100);           //
  bLED(3, 300, 100);    // blink 3 times for start..

  /*******************************************
     Measurement
   *******************************************/
 #define xMax 160
  for (uint8_t j = 0; j < xMax; j++) {
    sP(20);             // X + 1mm = 20 cycles
    sSleep();           // stop and sleep to avoid heat
    Tot = 0;
    for (uint8_t i = 0; i < nTimes; i++) {
Try:
      Kyori = ToF.readRangeSingleMillimeters();
      if (ToF.timeoutOccurred()) {
        // Serial.println("TO");
        goto Try;
      }
      Tot = Tot + Kyori;
    }
    Ave = (float)(Tot / nTimes);
    iAve = Ave * 10.00 + 0.50;

    uint8_t yh = iAve >> 8;       // get y-high byte
    uint8_t yl = iAve - 256 * yh; // get y-low byte
    Serial.write(yh);
    Serial.write(yl);
  } // end of For loop

  bLED(5, 300, 100);    // end..
  // return stepper to center
   sReset();            // Reset at right end
  sN(1600);             // X - 1600 times (X-80 mm)
  sSleep();             // sleep to avoid heat
  bLED(100, 20, 300);   // blink LED 100 times
  goto Repeat;          // and repeat again
}

void loop() { // ***** Arduino Loop *****
}                       // never comes here

/**************************************
      User defined functions
 **************************************/
#define tmStp 2000      // stepper phase timing in μS

// *** bLED() *** Blink LED
void bLED(int nTimes, int onLen, int offLen) {
  for (int m = 0; m < nTimes; m++) {
    digitalWrite(Led, HIGH);
    delay(onLen);
    digitalWrite(Led, LOW);
    delay(offLen);
  }
}

//   *** sP(n-rotations) *** move stepper positive
void sP(int nRot) {
  sWake();                // wake-up stepper
  digitalWrite(Dir, LOW);
  mvS(nRot, tmStp);
}
//  *** sN(n-rotations) *** move steppe negative
void sN (int nRot) {
  sWake();                // wake-up stepper
  digitalWrite(Dir, HIGH);
  mvS(nRot, tmStp);
}
//  *** mvS(nRot, ndelay) *** move stepper
void mvS(int nRot, long nDelay) {
  for (int l = 0; l < nRot; l++) {
    digitalWrite(Step, HIGH);
    delayMicroseconds(nDelay);
    digitalWrite(Step, LOW);
    delayMicroseconds(nDelay);
  }
}

// *** sReset() *** Reset stepper
void sReset() {
  digitalWrite(Nreset, LOW);
  delay(1);
  digitalWrite(Nreset, HIGH);
  delay(1);
}

// *** sSleep() *** Sleep stepper to avoid heat
void sSleep(void) {
  digitalWrite(Nsleep, LOW);
}

// *** sWake() *** Wake up stepper
void sWake(void) {
  digitalWrite(Nsleep, HIGH);
  delay(1);         // wait for charge pump readiness
} 
// end of program

次はPC側のプログラムです。

myTitle="Receiving ToF data from Arduino  Aug.29, 2020   (c)Akira Tominaga"
TextWindow.Title=myTitle
TextWindow.Write("Opening CommPort..")
connectPort()
' ***** Define Graphics Window *****
gw = 854
gh = 480
margin = 50
xMax=160 ' max X = 160mm
yMax=100 ' max Y =100mm
yMaxX10=yMax*10
sp =10 ' space between lines (mm)
GraphicsWindow.Width = gw+margin
GraphicsWindow.Height = gh+margin
GraphicsWindow.Title=myTitle
GraphicsWindow.BackgroundColor="Black"
GraphicsWindow.PenWidth=3
GraphicsWindow.FontName="Arrial"
GraphicsWindow.FontSize=15
GraphicsWindow.BrushColor="White"

'  ***** Y scale and horizontal lines *****
For xx=0 To xMax
 X=gw*(xx/160)+margin/2  ' Set x position X
 For yy=0 To yMaxX10 Step sp*10
   Y=gh*(yy/(yMaxX10))+Margin/2 'Set y position Y
   GraphicsWindow.SetPixel(X,Y,"White")
   If xx=0 Then
    X=margin*0.1              'set scale position
    Y=gh*(yy/yMaxX10)+Margin*0.4 'adjust scale position
    GraphicsWindow.DrawText(X,Y,yy/10) ' draw Y scale
  EndIf
 EndFor
EndFor

' ***** X scale and vertical lines *****
For yy=0 To yMaxX10
 For xx=0 To xMax Step sp
  X=gw*(xx/xMax)+margin/2      ' Set x position X
  Y=gh*(yy/yMaxX10)+margin/2      'Set y position Y
  GraphicsWindow.SetPixel(X,Y,"White")
  If yy=0 Then
    X=gw*(xx/xMax)+margin*0.3 
    Y=margin*0.1              'adjust scake position
    GraphicsWindow.DrawText(X,Y,xx) ' draw X scale
  EndIf
 EndFor
EndFor

' ***** Draw a graph, receiving data from UNO ToF *****
GraphicsWindow.PenWidth=3
GraphicsWindow.PenColor="Yellow"
'EOT=254*256 ' define 0xFExx as End of Transmission from Arduino
LDCommPort.TXString("SB") ' tell to UNO  "SB is ready"
For xx=0 to xMax
  yh=LDCommPort.RXByte() 
  yl=LDCommPort.RXByte()
  yy=256*yh+yl
  TextWindow.Write("yy=")
  TextWindow.WriteLine(yy)
  X=gw*(xx/xMax)+margin/2  ' Set x position X
  TextWindow.Write("X=")
  TextWindow.writeLine(X)
  Y=gh*(yy/yMaxX10)+Margin/2  ' Set y position Y
  TextWindow.Write("Y=")
  TextWindow.writeLine(Y)
  If xx>0 Then
    GraphicsWindow.DrawLine(xs, ys,X,Y)
  EndIf
  xs=X ' save X
  ys=Y ' save Y
EndFor
LDCommPort.ClosePort()
Sound.PlayBellRing()

' ***** Subroutines *****
Sub connectPort
  for i=1 to 16
    portName="COM"+i
    status=LDCommPort.OpenPort(portName,9600)
    If status="SUCCESS" Then 
      Goto Out
    EndIf
  EndFor
  Out:
  TextWindow.Write("Connected to ")
  TextWindow.WriteLine(portName)
  If status="CONNECTIONFAILED" Then
    ComFailed:
    Goto ComFailed ' stop here
  EndIf
EndSub

 

 

2020.9.9追記

精度がなんとかならないか・・、その後も少しずつですが実験したので以下に追加します。

このセンサー自体は2.8mmx4.8mmx1.0mmの大きさで、顕微鏡を低倍率にして見ると次のような形。

f:id:a-tomi:20200909203951j:plain

顕微鏡のIR遮蔽が弱いので850nmの平面Laser光が写っています。上の穴が反射光の検知窓で、穴の間はおよそ3.5mm離れています。ですので、ここに何か細工ができないかなあと考えました。

最初にレンズで集光してみることに。次の状態は何もしない場合の赤外線写真です。

f:id:a-tomi:20200909204819j:plain

この出力窓の前に樹脂製のレンズをおいてみると、だいぶ収束するのがわかります。

f:id:a-tomi:20200909204946j:plain

この写真では高さの位置を出力窓にあわせるために、レンズの下にティッシュペーパーを敷いています。この状態でレンズを前後させるとさらに収束できる位置があるようです。赤外線カメラで見て一番良い位置を確認したのですが写真を撮りそこないました^^;

とはいえ直径が10mmほどのレンズのため、反射が受光窓にもかぶるのでもっとずっと小さいレンズでないといけません。プラスチックレンズは頑張れば切れるかも^^;  モノはためし、幅2.5mmほどの凸レンズを作ってみました。

f:id:a-tomi:20200909205328j:plain

こういう形でも受光窓を避けるにはなんとかなりそうな感じがします。

これは度の強いメガネ(リーディンググラス、f=16.7cm)から、愛用のProxxon「サーキュラーソー」で下の写真のようにエイヤッと切りとったもの。

f:id:a-tomi:20200909205618j:plain

ひどいことをしますが、ヒンジが壊れて不要になった百均のグラスなのでご安心を^^;

 これで早速やってみると、測定結果の距離が極端に小さくでます。何のことはない、出力窓から少し離しただけで、レンズの反射がどうやっても受光窓にはいります。レンズを出力窓にほぼくっつけないとだめそう。焦点距離の相当短いものを使わないと無理そうなのでレンズでの収束はとりあえず断念。

それなら、次はパイプで広がらなくする案。外径2.5mm、穴径1.5mmのアルミパイプを切り出して、木片を支えにしてやってみました。光が漏れないように出力窓にピッタリ付けます。

f:id:a-tomi:20200909210400j:plain

赤外線のカメラで見るとパイプによる集光はバッチリだったのですが。。。測定値が不安定。どうやら測定対象からの反射光がパイプをセットした台で反射しているようです。なぜ短く計測されることがあるのかはわかりませんが、ひょっとして単に往復時間だけでなく位相なども検知してるのかなあ、う~ん ;; 

どっちにしてもパイプが使えるかを確かめるには、パイプ自体の支持方法を工夫しないといけない感じです・・ICに接着してしまえばいいかな?それも気が引ける・・

 

そこでふと気づいたのですが、スリットで広がりを防ぐのはどうだろう?左右だけ広がらなければよいわけなので・・。

しかし、どっこい。そうはいきませんでした。左右の壁で反射して短く検出されてしまいます。それなら反射を防ぐ紙を貼るとどうなるか・・。下のように広げてみてもやっぱり両脇での反射が検出され、短くでます。

f:id:a-tomi:20200909211337j:plain

木製の台が光線の近くにあること自体もまずいかも。

ここまででのところ、測距対象が近くにあるときは正しい距離が得られる場合もありますが、対象が離れると測定値が異常に小さくなるのに気づきました。反射光の強度が小さい場合(測定対象からの反射光が弱いと)センサーは次第にゲインを上げて近くの弱い反射を検出するようにみえます。ゲインは40倍までコントロールできる仕様です。測り方の指定にもよるでしょうが。

他の用途ではどうしているのか?自動掃除機などのLidarに使われているToFセンサーでも光線の性格は同じで、かなり広がっています。ロボットや掃除機が壁にぶつからないようにするためには、それでもとくに問題ないわけです・・・。ポピュラーなこの種のToFセンサーでは正面の距離は正確に測れても、回転時の角度にまつわる分解能は高くはないということでしょうか。

 というわけで、まだ光線が絞れてません。もし別の知恵が出たらこの続きをやろうと思います。ここまでおつきあいいただきありがとうございます (o*。_。)o tks

 

©2020 Akira Tominaga, All rights reserved.

マイコンとPCのデータ授受ーSmallbasicなら簡単 (2020.12.05 LiquidCrystal_I2Cについて補足しました)

f:id:a-tomi:20200823175731g:plain

ArduinoのデータをPCで受けて表示した例

マイコンのデータをPCで受けて自由にグラフィック処理したり、逆にPCからデータをマイコンへ渡したりするには、PC側にそれなりの言語環境が必要。ところがちゃんと維持していないと、Anacondaなどのバージョン不整合を起こし、まるで王手飛車とりで攻めまくられたりして・・・^^; 思わぬことで手こずりたくないものです。

私はマイクロSDカードを介したりすることが多いですが、リアルタイムで処理したい場合はそうはいきません。そんなとき、単純・簡単で便利なのがマイクロソフトが無償提供している Small Basic という教育用言語です。

ここではマイコンとの間のインターフェイスを、単純で特別な機器の要らないシリアル通信でつなぎます(それなりのデバイスを用意すれば、BlueToothなど他の方法ももちろん可能ですが)。

使うにはまず、Small BasicをPCにインストールします。バージョンアップの頻度は少なくとても安定しています。今は次のサイトが本家になっています。

https://smallbasic-publicwebsite.azurewebsites.net/

 英語版でとくに困ることはありませんが、日本語版(各国言語版)を入れたいときは次からインストールします。

https://www.microsoft.com/ja-jp/download/details.aspx?id=46392

 Small Basic自体はいちじるしく単純ですが、便利な拡張APIが沢山出ています。なかでも英国の団体が運営するLitDevには豊富な関数がそろっていて世界中で多く使われています。LitDevの最新版(現時点は LitDev 1.2.23.0)を次からインストールします。

http://litdev.co.uk/

LitDevのZipを解凍してすべてを次のフォルダーに入れます。C:\Program Files (x86)\Microsoft\Small Basic\Lib

これで後はプログラミングが自由自在です。めったにバージョンアップがないのは却ってありがたいことです^^

Small Basicの使い方は、非常にわかりやすい日本語サイトも沢山でていますので必要に応じてご参照下さい。LitDevで使える多数の関数については上のサイトで、英語ではありますがわかりやすく説明されています。ここではそのなかのLDCommPortだけを使いますが、他に便利なAPIは多数あります。LitDevは、SmallBasicの開発環境で言語そのものに交じって同じに使えます。

 

まずは、ArduinoからPCのSmall Basicへ送る場合の例です。

f:id:a-tomi:20200823210916j:plain

次がこのArduinoスケッチです。

/******************************************************************
    Sending data to Microsoft-Smallbasic via Serial port, example.
     Initial version V.00 Aug.22, 2020  (c) Akira Tominaga
     Function:
      Send data of Lissage graph to PC Smallbasic.
  *****************************************************************/
#include "Wire.h"
#include "LiquidCrystal_I2C.h"
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
#define cPl 20                    // characters per line
#define lMax 4                    // number of lines
#define wSize 854                 // graphics window width
#define hSize 480                 // graphics window height
uint8_t Line = 0;                 // set LCD initial line
byte EOT = 0xFE;

void setup()  {  // ***** Arduino Setup *****
  Serial.begin(9600);
  lcd.begin(20, 4);        // initialize 20 x 4 LCD turn on backlight
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Talk with SmallBasic");
Hajime:
  lcd.setCursor(0, 1);
  lcd.print("Start Smallbasic pgm");
  while (Serial.available() < 1) {}
  String s = Serial.readString(); // read message from Smallbasic pgm
  uint8_t nPads=20-s.length();    // # of blanks req'd to pad
  lcd.setCursor(0, 1);
  lcd.print(s);
  for (uint8_t i=0;i<nPads;i++){
    lcd.print(" ");
  }
  delay(10);
  lcd.setCursor(0, 1);
  lcd.print("Lissage data sent");
  for (float t = 0; t < 2.1 * 3.1416; t = t + 0.01) {
    uint16_t px = (float)((wSize/2) * sin(3 * t) + wSize/2);
    uint16_t py = (float)((hSize/2) * sin(4 * t) + hSize/2);
    uint8_t xh=px>>8;             // get x-high byte
    uint8_t xl=px-256*xh;         // get x-low byte
    uint8_t yh=py>>8;             // get y-high byte
    uint8_t yl=py-256*yh;         // get y-low byte
    Serial.write(xh);             // send x-high byte
    Serial.write(xl);             // send x-low byte
    Serial.write(yh);             // send y-high byte
    Serial.write(yl);             // send y-low byte
    delay(1);
  }
  for (uint8_t i=1;i<4;i++){      // LCD lines from 1 to 3 
    lcd.setCursor(0,i);           // set cursor 
    for (uint8_t j=0;j<20;j++){   // and clear each line  
      lcd.print(" ");
    }
  }
  Serial.write(EOT);              // send End of Transimission
  Serial.write(0x00);             // of which low byte to add
  goto Hajime;                    // return to Hajime and wait
}

void loop() { // ***** Arduino Loop *****
}             // not used in this example

 

この記事のいちばん最初に掲げた動画は、これをSmallBasicの次のプログラムで処理したものです。

myTitle="Receiving data from Arduino  Aug.22, 2020 (c)Akira Tominaga"
TextWindow.Title=myTitle
TextWindow.Write("Open CommPort..")
status = LDCommPort.OpenPort("COM5",9600)
TextWindow.WriteLine(status)
gw = 854
gh = 480
margin = 50
GraphicsWindow.Width = gw+margin
GraphicsWindow.Height = gh+margin
GraphicsWindow.Title=myTitle
GraphicsWindow.BackgroundColor="Green"
GraphicsWindow.PenColor="White"
GraphicsWindow.PenWidth=3
EOT=254*256 ' define 0xFE.. as End of Transmission from Arduino
xs=gw/2 ' initial x position in Graphics window
ys=gh/2 ' initial y position in Graphics window
LDCommPort.TXString("SB started.")

Loop1:
xh=LDCommPort.RXChar()
xl=LDCommPort.RXChar()
xx=xh*256+xl+Margin/2  ' Set x position
'TextWindow.Write("XX=")
'TextWindow.WriteLine(Text.GetCharacterCode(XX))
If XX>=EOT Then
  Goto Lpe
EndIf
yh=LDCommPort.RXChar()
yl=LDCommPort.RXChar()
yy=yh*256+yl+Margin/2  ' Set y position
GraphicsWindow.DrawLine(xs, ys,XX,YY)
xs=xx ' save xx
ys=yy ' save yy
Goto Loop1

Lpe:
LDCommPort.ClosePort()
GraphicsWindow.BrushColor="Yellow"
GraphicsWindow.FontSize=30
GraphicsWindow.DrawText(gw/2-60,gh/2,"Completed!")
Sound.PlayBellRing()

 グラフはSmallBasicのGraphicsWindowでごく簡単にかいていますが、かなり高度の事も色々できます。一例:https://a-tomi.hatenablog.com/entry/2018/09/04/162615

LDCommPortのうち、OpenPort()、TXString()、RXChar()、ClosePort()しか使っていませんが、他に、RXByte()、RXAll()、TXByte()などの関数も使えます。

他の命令は全てSmallBasicそのものですが、変数に型の概念がないこと(つまり宣言不要)や、ファイルやイメージなどの扱いも含めて楽ちんそのものです^^

 

次に、SmallBasicが非常に長い文章をArduinoへ送り、Arduinoで液晶に次々に表示する例です。

f:id:a-tomi:20200823211138j:plain

 このSmallBasic側プログラムは次です。

myTitle="Sending text to Arduino   Aug.22, 2020 (c)Akira Tominaga"
TextWindow.Title=myTitle
TextWindow.Write("Open CommPort..")
status = LDCommPort.OpenPort("COM5",9600)
TextWindow.WriteLine(status)

Bun=File.ReadContents("D:\SmallBasic\SBtxt\Test00.txt")
Bl=Text.GetLength(Bun)
If Bl=0 Then
  TextWindow.WriteLine("*** File not found")
EndIf

tLen=20 ' text length to be sent at one time
sP=1    ' start pposition of  Text reading
LF=Text.GetCharacter(10) ' ending char sent by Arduino

While(sP<Bl)
  Bpart=Text.GetSubText(Bun,sP,tLen)
  TextWindow.Write("SB sent: ")
  TextWindow.WriteLine(Bpart)
  LDCommPort.TXString(Bpart)
  sP=sP+tLen
  TextWindow.Write("Arduino: ")
  Resp=""
    While(Resp <> LF)
      Resp=Text.GetCharacter(LDCommPort.RXByte())
      TextWindow.Write(Resp)
    EndWhile 
EndWhile
status=LDCommPort.ClosePort()
Sound.PlayBellRing()

 ここでは簡単のため、テキストファイルを開いて、全量を読み込んでそれを送っていますが、Webの特定場所から読み込んで送るなども、もちろんできます。

SmallBasicのTextWIndowに、この送受の内容が次のように順次表示されます。

f:id:a-tomi:20200823212742j:plain

  次がこれを受けて表示するArduinoのスケッチです。

実行順序としてはArduinoを先に接続して動かしてから、次にSmallBasicのプログラムを立ち上げます。なお接続Port番号が一定しないマイコンの場合は、SmallBasicでのOpenPortの処理方法をこの記事の最後にかいておきます。

/*****************************************************************
    communications with MS Smallbasic via Serial port, example.
     Initial version V.00 Aug.22, 2020  (c) Akira Tominaga
     Function:
      Receive text from Smallbasic and display to LCD 20x4.
     Major revisions:
 *****************************************************************/
#include "Wire.h"
#include "LiquidCrystal_I2C.h"
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
#define cPl 20                    // characters per line
#define lMax 4                    // number of lines
uint8_t Line = 0;                 // set LCD initial line

void setup()  { // ***** Arduino Setup *****
  lcd.begin(cPl, lMax);           // initialize I2C and LCD
  lcd.backlight();                // and turn on backlight
  lcd.setCursor(0, 0);            // LCD home position
  lcd.print("Receive text from PC");
  Serial.begin(9600);             // the same bps with PC
}

void loop() {  // ***** Arduino Loop *****
  while (Serial.available() < 1) {}
  String s = Serial.readString(); // receive text string
  lcd.setCursor(0, Line);         // point line to show text
  lcd.print(s);                   // and show text to LCD
  int j = Line + 1;               // point next line to use
  if (j >= lMax) j = 0;
  lcd.setCursor(0, j);
  for (int k = 0; k < cPl; k++) { // and clear it
    lcd.print(" ");
  }
  delay(1000);                    // time to read text
  Serial.write("OK!");            // send message OK! to PC
  Serial.write(0x0A);             // LF as end of message
  Line++;                         // point next line of LCD
  if (Line >= lMax) Line = 0;
}

 上の例は電光掲示板のように表示するだけですが、適度な速度でやれば結構よみやすいものです。

 

以上での留意点は

①先にマイコンをシリアルポート(今ではUSBシリアル)につなぎ、そのあとでSmallBasicプログラムを立ち上げる

②COMポート番号が接続のつど一定にならないマイコン接続の場合は、LDCommPort.AvailablePorts()関数でポート番号を得てからOpenPort処理するように組むようになっていますが、これは文字の操作が結構面倒です。しかしポートを1つしか使わないことが圧倒的に多いでしょうから、その場合は次のコーディングでPort番号を得てからOpenPortをするコーディングにすれば大丈夫です。

kekka="x"
for i=1 to 16
  portName="COM"+i
  kekka=LDCommPort.OpenPort(portName,9600)
  If kekka="SUCCESS" Then 
    Goto Out
  EndIf
EndFor
Out:
TextWindow.Write("CommPort is ")
TextWindow.WriteLine(portName)

 

では今回はこのへんで。

一番簡単な言語のひとつであるSmall Basicを使い始めていただき、プログラミングする習慣が少しでも広まるきっかけになればと思います。

 

2020.12.05追記:

I2Cシリアル変換モジュールで接続する液晶文字ディスプレイは種類が複数あり、ライブラリーが混乱しがちです。ここではArduinoのLiquidCrystal_I2C.hライブラリー(New-liquidCrystal-master)を使う方法を補足させていただきます。

元祖の日立1602LCDモジュール(HD44780)と同じ仕様のLCDディスプレイをI2Cシリアル変換モジュール(PCF8574ボード)で4ビットデータを使って動かす価格の安い(海外ネットで16文字x2行は200円台、20文字x4行は400円台で購入できた)ものが圧倒的に多く出ている感じです。次の写真で、各LCDモジュール裏の右上にはんだ付けされているのがその変換モジュールです。

f:id:a-tomi:20201205121018j:plain

その便利な変換ICは同じでもボードには複数種があるため、LCDへのピン接続の明示的な指定の必要性があるわけです。ピン接続部を表から見ればどれでも、元祖の1602LCDモジュール(HD44780)と同じで海外製なのにカタカナを含む中身のフォントも全く同じものが多いです。

f:id:a-tomi:20201205121404j:plain

ところが、同じに見えても4ビット接続の場合は8ビット接続と異なり、データの授受にD0~D3を使わないためI2Cプロトコルは煩雑になります。

このため、現時点ではArduinooのLiquidCrystal_I2Cライブラリーをスーパーセットである次のものへ入れ替え、他のLiquidCrystal_I2Cライブラリーを削除することで問題なく使えるようになっています。16桁2行でも20桁4行でも上のスケッチ例のように指定できます。

GitHub - fmalpartida/New-LiquidCrystal: Clone of the new liquid crystal library from: https://bitbucket.org/fmalpartida/new-liquidcrystal

名称が重なる他のLiquidCrystal_I2Cライブラリーは以後使わないかと思います。万一必要になれば、そのときに元のを特定スケッチのフォルダーに改めて再導入すればよいかと思われますし、もし心配なら上記を入れる前に古いのをどこか別フォルダーにバックアップしておくとか(参照先の名前が重なるのを防ぐために)。

今やOLEDが多く使われるようになりましたが、そちらはメモリーの必要量が多いためこういう安価で楽なI2C接続LCDはまだまだ長く使われるのではないかと思います。

以上、ご自分がお使いになる同様な液晶文字ディスプレイについてご判断の上、自己責任にてお願いしますね。

 

 

©2020 Akira Tominaga, All rights reserved.

 

 

 

回路不要のかんたんなRTCライター:Arduinoで (2022.11.22更新)

f:id:a-tomi:20200816121455j:plain

RTC(リアルタイムクロック)は今やとても正確で、1年間の最大の狂いが秒単位のものがあります。上のArduinoにつないだRTCは、なかでも正確なDS3231というICのブレークアウトモジュールです。ファームウェアで短い間隔で温度補正をする工夫がなされたコストパフォーマンスの優れたRTCです。

f:id:a-tomi:20200816121832j:plain

このモジュールは、比較的容量の大きいCR2032電池を使い、大体5年ほどはそのまま動きます。小さな電池を入れる型であればもちろんもっと短いですが。

f:id:a-tomi:20200816122012j:plain

この精密なクロックが海外ネットでは1ドル未満で買えるので驚きです。今はコロナの影響でなんでも少し高いですが、今ちらりとAliexpressをみると、やはりそんな廉価です。中には、ばか高い同じものもあるので、気を付けないといけませんが。

 

f:id:a-tomi:20200816122243j:plain

(2022.11.22追記:ここ数か月で急に高くなっていますが、米中貿易摩擦の関係でなければいいのですが・・。)

何かの自作にRTCを使うためには、普通はカレンダーや時刻の設定機能を作りこみます。そのため少なくとも3つ(Mode、 Time、Set)のボタンかタクトスイッチが要ります。

しかし、RTCがこれだけ正確だと、自作機器では読み込むだけにしたい感じもします。必要に応じて外して更新したり、あるいはRTC更新の際は外部からの入力に差し替えるなどすればよいわけなので。(ブレークアウトモジュールではなく、DS323xのICを直接組みこむ場合はもちろんそうはいきませんね。ただし大量ではなく趣味用の少量購入ならブレークアウトモジュールが圧倒的に廉価。)

読み込むだけにする場合は、別途RTCへの書き込み装置を必要とするわけですが、それをArduinoで回路なしに楽に行う勝手な方法をご紹介したいと思います。

これだけのことなら最近の標準ライブラリーを使っても良いかもしれませんが、一定間隔の割り込みなどを使いたいときは、次のようなI2Cへの直接コーディングなら自由度が高くて楽です。

電池を入れて最初の写真のように接続し、まずArduinoIDEで次のプログラムを書き込みます。(更新:2022.11.22 あまりにも書きなぐりのひどいコーディングでしたので、構造化だけしておきます。機能は全て前と同じです。)

/*************************************************************
    Simple RTC writer with Arduino-IDE Serial-Monitor only
    Initial Version V.00 8/15,2020     (c) Akira Tominaga
    V.01: Just re-coded to be structured.       11/22, 2022
    How to use:
      1) Connect RTC to I2C pins (ex.UNO; SCL=A5, SDA=A4).
      2) Use Serial-Monitor for dialogues.
    V.00 Aug.15,2020 (c) Akira Tominaga, All rights reserved.
**************************************************************/
#include "Wire.h"             // use I2C library
#define RTC_addr 0x68         // DS3231 I2C addr
byte  vR[8];                  // values in RTC registers
uint8_t vI[8];                // integer values for RTC data
char YMDwHMS[20];             // editing area to show time
uint8_t kVal;                 // user keyed-in number value
byte Data[2];                 // 2 bytes work area
byte rYN;                     // response keyed-in

void setup() { // ***** Arduino setup *****
  Serial.begin(9600);
  Wire.begin();
  Serial.println("** RTC editor **");
  getRTC();
  Serial.println("YY/MM/DD-W-hh:mm:ss =") ; Serial.println(YMDwHMS);
  while (true) {
    Serial.print("Updating time? (Y or N)");
    rKBin(); rYN = Data[0] & (0xFF - 0x20); // get upper-case
    if (rYN == 'Y') {
      Serial.println("=Y"); updateVal(); break;
    }
    if (rYN == 'N') {
      Serial.println("=N"); break;
    }
  }
}

void loop() { // ****** Arduino Loop *****
  getRTC();                   // get and edit RTC contents
  Serial.println(YMDwHMS);    // show Real time clock
  delay(1000);
}

/**************************************
      User defined functions
* *************************************/
// ***** read serial Keyboard input *** rKBin() *****
void rKBin(void) {
  String strTyped;             // characters typed-in
  while (Serial.available() < 1) {}
  strTyped = Serial.readStringUntil(0x10); // read KB until DLE
  Data[0] = strTyped.charAt(0);
  Data[1] = strTyped.charAt(1);
}

// *** Get RTC values in vI[] & YMDwHMS *** getRTC() *****
void getRTC(void) {
  rRTC();                     // read RTC into integers
  edTMchr();                  // edit time to characters
}

// macros used in the following subroutines
#define mds 1                 // ss
#define mdm 2                 // mm
#define mdh 3                 // hh
#define mdW 4                 // W = Day of Week (Sunday=1)
#define mdD 5                 // DD
#define mdM 6                 // MM
#define mdY 7                 // YY

//*** edit Time to characters YMDwHMS *** edTMchr() *****
void edTMchr(void) {
  sprintf(YMDwHMS, "%02d/%02d/%02d-%01d-%02d:%02d:%02d",
          vI[mdY], vI[mdM], vI[mdD], vI[mdW], vI[mdh], vI[mdm], vI[mds]);
}

// *** update RTC *** updateVal() *****
void updateVal(void) {
  char mC[] = {0xFF, 's', 'm', 'h', 'W', 'D', 'M', 'Y' }; // mode chars
  uint8_t maxvI[8] = { 0, 59, 59, 23, 7, 31, 12, 99}; // max.values
  uint8_t minvI[8] = {0, 0, 0, 0, 0, 1, 1, 0};        // min.values
  // W(day of the week) can be either 0-6 or 1-7
  getRTC();
  while (true) {              // big loop1
    uint8_t mdN = mdY;        // RTC register position index
    while (mdN > 0) {         // for all the modes
      while (true) {          // smaller loop2
        Serial.print("Input ");  Serial.print(mC[mdN]);
        if (mdN != mdW) {     // excluding day-of-week,
          Serial.print(mC[mdN]); // set double characters
        }
        Serial.print("=");  rKBin();
        if (Data[0] == 0x0D) {      // if just enter only
           kVal = vI[mdN];           // use as-is value
        }else{
          kVal = (Data[0] & 0x0F) * 10 + (Data[1] & 0x0F);
          if (mdN == mdW) kVal = (Data[0] & 0x0F);
        }
        if ((kVal < 10) & (mdN != mdW)) { // if value<10 and not mdW Serial.print("0"); // insert left zero } Serial.println(kVal); if ((kVal > maxvI[mdN]) | (kVal < minvI[mdN])) { Serial.println("Err "); } else { break; } }// exit from while(true) loop 2 vI[mdN] = kVal; mdN = mdN - 1; // next mode } // exit from while(mdN > 0) edTMchr(); // edit Time to characters YmDwHMS Serial.println("YY/MM/DD-W-hh:mm:ss =") ; Serial.println(YMDwHMS); Serial.print("OK? to update RTC (Y or N)"); rKBin(); rYN = Data[0] & (0xFF - 0x20); // get upper-case if (rYN == 'Y') { Serial.println("=Y"); wRTC(); // Yes, write to RTC Serial.println("** RTC updated **"); Serial.println(""); break; } if (rYN == 'N') { Serial.println("=N"); } } // while true loop1 exit (else, re-inquiry ) } // *** read RTC and convert values to integers *** rRTC() ***** void rRTC(void) { Wire.beginTransmission(RTC_addr); Wire.write(0x00); Wire.endTransmission(); Wire.requestFrom(RTC_addr, 7); while (Wire.available() < 7) {} // wait for data ready for (int i = 1; i < 8; i++) { vR[i] = Wire.read(); } Wire.endTransmission(); // convert RTC values to integers vI[mds] = ((vR[mds] & 0x70) / 16) * 10 + (vR[mds] & 0x0F); vI[mdm] = ((vR[mdm] & 0x70) / 16) * 10 + (vR[mdm] & 0x0F); vI[mdh] = ((vR[mdh] & 0x20) / 32) * 20 + ((vR[mdh] & 0x10) / 16) * 10 + (vR[mdh] & 0x0F); vI[mdW] = vR[mdW]; vI[mdD] = ((vR[mdD] & 0x70) / 16) * 10 + (vR[mdD] & 0x0F); vI[mdM] = ((vR[mdM] & 0x10) / 16) * 10 + (vR[mdM] & 0x0F); vI[mdY] = ((vR[mdY] & 0xF0) / 16) * 10 + (vR[mdY] & 0x0F); } // *** write to RTC - converting Int. to RTC values *** wRTC() ***** void wRTC(void) { vR[mds] = (vI[mds] / 10) * 16 + vI[mds] % 10; vR[mdm] = (vI[mdm] / 10) * 16 + vI[mdm] % 10; vR[mdh] = (vI[mdh] / 20) * 32 + ((vI[mdh] % 20) / 10) * 16 + vI[mdh] % 10; vR[mdW] = vI[mdW]; vR[mdD] = (vI[mdD] / 10) * 16 + vI[mdD] % 10; vR[mdM] = (vI[mdM] / 10) * 16 + vI[mdM] % 10; vR[mdY] = (vI[mdY] / 10) * 16 + vI[mdY] % 10; Wire.beginTransmission(RTC_addr); Wire.write(0x00); // RTC register addr 0x00 for (int i = 1; i < 9; i++) { Wire.write(vR[i]); } Wire.endTransmission(); } // End of program //

 シリアルモニターを起動すると操作ができます。

シリアルモニターを起動するには、IDEのメニューで「ツール」→「シリアルモニター」とクリックするか、または、IDEの右上あたりにある虫眼鏡のマークをクリックします。

次にシリアルモニターで、操作を次の様に行います。

f:id:a-tomi:20200816133338j:plain

詳細は次のとおりです。

①現在の数値を更新しないでよいかを聞いてきます。更新する場合はNかnを入力してEnterキーをおします。その場合入力は一番上②の送信欄です。また入力の最後には必ずEnterキーを入れてください。答がYかyだと更新せずに終えて、そのまま⑥の時計表示へ行ってしまいます。

④で、設定したいYY、MM、DD、W(曜日で1が日曜で月曜は2、・・・土曜が7)、hh、mm、ssとして年、月、日、曜、時、分、秒を順番に聞いてきます。W以外は2桁の数値を入力します。この時に、何も入れずにEnterキーを押せば、①に出た値を使います。

最後にss(秒)をいれたら、⑤でそれでよいかを聞いてきます。Yまたはyと答えればそのタイミングでRTCが更新されます。もしNまたはnと答えれば、再び④を繰り返します。

間違えて更新を終えた時には、ArduinoのUSB接続を外して再度つけなおしてやりなおせばよいだけです。

 

なお、上記のスケッチは必要ならお好きなようにお直しください。またDS3231以外のRTCでも、デバイスがもつレジスターの内容はよく似ています。データシートをご参照のうえ比較して必要な個所があれば直してお使いください。

 

短いですが、この記事は以上です。お役に立てば幸いです。

 

©2020-2022 Akira Tominaga, All rights reserved.

 

NDIR-CO2ロガーの製作と測定(CO2その3):換気は大事!

f:id:a-tomi:20200815154305j:plain

前にブレッドボードでの試作を紹介したNDIR(赤外線吸収型)CO2ロガーを組み立て、室内の測定等色々やってみたのでご紹介します。

前の記事はこれです:テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型 - 勝手な電子工作・・

今回のセンサー(MH-Z19b)は、改善の歴史の長いNDIR型です。さすがにキャリブレーションもしやすく安定し、ロガーの用途には使いやすいことがわかりました。

次はこのロガーによる実際の記録結果です。

f:id:a-tomi:20200815155616j:plain

測定開始から安定まで数分ですが一応開始後約10分間はカットしました。

ご覧のとおり、閉め切った部屋(広さ12平米)だと1人で作業しても、二酸化炭素濃度はみるみるあがっていきます。10時20分に2か所の窓を2センチずつあけて換気を開始すると素直に下がります。外の風向きなどで勾配は変化するようですが。

次に食事で不在の間、濃度は一気に下がっていきます(ただし廊下のドアは開けている)。自分がいかにCO2を出しているかがわかります^^; なにせ、戻ってくるとすぐまた上がります。13:45に同様の換気開始。その後14時28分に突然強い雨、窓を閉めざるを得なくなりました。するとまたぐんぐん上がります。15時22分に多少小降りとなり、窓を少し(2箇所15ミリほどずつ)開けて換気を再開。また少し下がります。

16時50分に雨が少し降りこむ側の窓を閉じ、空いている窓は1つで15mmだけ。閉まっている場合よりは勾配が緩やかですが、また徐々に上昇。結局18時55分に退室時は1000ppm寸前になっていました。しかし人がいなくなればどんどん下がります(廊下のドアを開けておきましたが)。

このセンサーは安いのになかなか大したものですね。

蛇足ですが、調べるとどのセンサーも長時間使えばドリフトが生じます。そこを工夫してたいていは自動キャリブレーション(Automated Baseline Calibration)があります。その場合、24時間、常に人がいる部屋では不都合であり、オフィスのように人がいない時間が毎日ある場所での測定に適しています。

f:id:a-tomi:20200815162134j:plain

このセンサーは3通りの測定法がありますが、前に書いたように、UART接続でデジタル数値を取り出して使用しています。

ロガーのケースには2箇所穴をあけて、センサーの空気の出入りができるようにしました。

このセンサーの現バージョンでは、コマンド(9バイトからなるセンサー宛信号)が5種類あります。①0x86(Co2濃度読取)、②0x87(ゼロ点校正)、③0x88(スパン点校正)、④0x79(自動校正設定/解除)、⑤0x99(測定レンジ設定)。

今出荷されるものは、⑤は既定、④はでデフォールトで設定済なので通常必要なコマンドは①のみです。また、②のゼロ点校正はコマンドからではなく物理的な回路設定(HgピンをLOWに設定)でもできます。具体的にはCO2が400ppm以下の環境に20分以上置いてHgを7秒間接地。

400ppm以下の環境は、植物の多いところなら日照の良い日の午後に確実に得られます(逆に、植物の少ないところや光合成が行われない暗さでは無理)。

以上はデータシート(User Manual)に記述されており、センサーはそのように動きます。ところが、念のためにArduinoのMHZ19ライブラリーを使って、Setupからのコマンドをトレースしたところ、データシートと異なり次のプロトコルになっています(Line1はマイコンからこのセンサーへ、Line2はセンサーからマイコンへの信号)。

f:id:a-tomi:20200815163917j:plain

これにはとまどいましたが調べていくと、単に、最新Arduinoライブラリーがこのセンサーの改善(バージョンアップ)に追い付いていないことがわかりました。

そして古い0x85や0xA2コマンドも互換性を維持するためにサポートされているものです。この際はライブラリーを使わずに躊躇なく^^;最新の仕様で手作りしたわけです。

なお、上に示したようなUART接続の信号トレースはArduino Unoで簡単にできます。必要な方は次の記事の最後の方をご参照ください。これを行ったArduinoスケッチをつけてあります:

ソフトウェアシリアルをコンパクトに作るーArduino編-そしてトレースに使う - 勝手な電子工作・・

 

以下に製作について、手短かに説明させていただきます。

まずは、あらためて回路図です。汚い手書きで毎度すみませんが。

f:id:a-tomi:20200815165015j:plain

今回はRTC(リアルタイムクロック、DS3231ブレークアウトモジュール)の更新機能はつけてありませんで、読み取るだけです。RTCは部品として簡単に抜き差しできるようにして、事前に「RTCライター」で時刻を設定すれば、少なくとも3年間は合わせなくても分単位ではずれません。今やDS323xはとても正確ですし電池は長期間持ちますが、電池交換の時や、もし時刻がずれた時だけ外して設定すればよいわけです。RTCライターについては、次の記事をご参照ください。

リアルタイムクロック書込器(ソースコード付き) - 勝手な電子工作・・

 RTC writer(リアルタイムクロック・ライター) 8ピンPICで簡素に - 勝手な電子工作・・

この際、回路不要のRTCライターを作りますので、この記事の中に追記しておきます。部品なにもなしでArduino-UNOだけでできる一番簡単なRTCライターです。今書きましたのでリンクを入れますね(2020.8.16更新)。

回路不要のかんたんなRTCライター:Arduinoで - 勝手な電子工作・・

なお、ArduinoでRTCライターを作るときにはたいていスイッチを3つつけて、RTC更新機能を組み込むようにしていますが今回のCO2ロガーではマイコンの容量を増やしたくないのでつけてないだけです。つけている一例は次。

デュアルGMTレコーダー - 勝手な電子工作・・

そして次が基板のデザインで、108mmx78mmです。部品面から見た図なので銅箔面にするときは裏返し。この記事の後の方に書くケースに合わせてあります。(また、銅箔を残さないところを黒にしているので、もしエッチングする場合はネガポジを反転。)

f:id:a-tomi:20200815165123j:plain

私はこういう片面基板はCNCルーターで削って作るので、ソフトで銅箔面のツールパス図に変換します。

f:id:a-tomi:20200815165343j:plain

作った基板は次です。今回は初めて使う中国製の頼りないA4ベーク樹脂生基板でしたが、幸い問題なくできました。

f:id:a-tomi:20200815165430j:plain

まず、ジャンパー線5つを配線。

f:id:a-tomi:20200815165513j:plain

そして部品をはんだ付けして基板は完成。

f:id:a-tomi:20200815165834j:plain

半田付けはきたないですが^^ この基板はアルコールで拭いてもハンダが結構つきにくいので、コテの温度を少し上げました。

f:id:a-tomi:20200815170103j:plain

これでテストをして問題なし。そしてケースに組み込んでできあがり。ケースは秋月で買ったポリカーボネート製。深さに3種類ありますがこれは中間の深さのもの。とても丈夫で蓋が開くので、こういう用途に重宝です。

f:id:a-tomi:20200815170249j:plain

センサーの2つの通気口が右端と上の蓋に近くなるようにデザインしました。それぞれの近くに5φの丸穴をあけてあります。蓋を閉めてのテストの結果、これで問題ありません。

最後に、ArduinoUnoのスケッチをつけておきます。UNOにIDEでプログラムし、ATmega328pマイコンだけを取り出して単体で使います。

/*
   CO2 Logger with MH-Z19b NDIR CO2 sensor (May-2020),
   with RYO coding for RTC, TM1637, MHZ19, and Soft-Serial
        Initial version V.00 Aug.1, 2020 (c) Akira Tominaga
        V.01: FileName=.csv. Replaced pins 8,9. Aug.10,2020
*/
#include "Wire.h"             // I2C for RTC-DS3231 
#include "SPI.h"              // SPI for SD drive interface
#include "SD.h"               // Micro SD drive
// *** for micro SD writer ***
File aqLog;                   // aqLog as SD file symbol
#define Cs  10                // SD Chip-select pin is D10
//  MOSI=11, MISO=12, CLK=13
#define SDsw 4                // button to close SD file
// *** for TM1637 display ***
#define lDIO 6                // DIO for TM1637 LED
#define lCLK 7                // CLK for TM1637 LED
#define lBrt 0x02             // duty-r 1/16x 02:4,03:10,05:12,07:14
#define lTu 50                // time unit in micro-second
byte lChr;                    // single byte sent to LED
byte Data[] = { 0x00, 0x00, 0x00, 0x00 }; // LED initial value 0000
// *** UART for CO2 sensor ***
#define sRx 9                 // Master-Rx (MISO) = MHZ19-Tx
#define sTx 8                 // Master-Tx (MOSI) = MHZ19-Rx
// (for sending)
#define sTuS 104-3            // time unit (9600bps) for sending
#define sTuhS 51              // time unit half for sending                  
char sByte;                   // character work area to be sent
// (for receiving)
#define tmChk 16              // timing checker for test (D16=A2)
#define sTuR 104-10           // time unit (9600bps) for receive
#define sTuhR 51-21           // time unit / 2 for receive
byte rByte;                   // character receiving byte
// *** for NDIR CO2 sensor MH-Z19b(May,2020)
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
//                flags, req, rCO2,  0,    0,    0,    0,    0,  chksum
// byte sABCcmd[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x86};
// //               flags, req, rABC,  0,    0,    0,    0,    0,  chksum
// ABC(auto baseline calib)default, since Jul.2015, hence no setup required.
// Ref:datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte hV;                      // response byte2 high value
byte lV;                      // response byte3 low value
uint16_t CO2 = 600;           // CO2 ppm smoothed value, init. 600
char strCO2[5];               // edit. area for SD recds and display
// *** for Real Time Clock DS3231
byte  vR[8];                  // values in RTC registers
uint8_t vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};  // RTC data, init 0000
#define RTC_addr 0x68
#define mdI 0                 // Initial mode
#define mds 1                 // ss
#define mdm 2                 // mm
#define mdh 3                 // hh
#define mdW 4                 // WW=Day of Week
#define mdD 5                 // DD
#define mdM 6                 // MM
#define mdY 7                 // YY
#define sS 0
#define mM 1
#define hH 2
char  strMDhm[11];            // edit area for calendar and clock
uint8_t mmSv;                 // minute-llvalue save area

void setup() { // ***** Arduino setup *****
  Serial.begin(9600);         // start hardware serial to PC
  // *** for RYO-SW-UART
  pinMode(sRx, INPUT_PULLUP); // soft Rx to be pulled-up
  pinMode(sTx, OUTPUT);       // soft Tx as output
  digitalWrite(sTx, HIGH);    // set it high
  pinMode(tmChk, OUTPUT);     // timing checker
  digitalWrite(tmChk, LOW);   // set it LOW
  // *** for RYO-TM1637 interface
  pinMode(lCLK, OUTPUT);      // lCLK as output
  pinMode(lDIO, OUTPUT);      // lDIO as output
  digitalWrite(lCLK, HIGH);   // lCLK high
  digitalWrite(lDIO, HIGH);   // then lDIO high to avoid Start sig
  lDispData();                // display 0000
  // *** for SD
  pinMode(SDsw, INPUT_PULLUP); // SD file closing button
  Wire.begin();
  if (!SD.begin(Cs)) {
    Data[0] = "E";            // show error
    lDispData;                // display E000
    while (1) {}
  }
  // set SD file name MMDDhhmm
  getTime();
  String s = strMDhm;
  s.concat(".csv");           // complete file name
  aqLog = SD.open(s, FILE_WRITE);
  aqLog.println("MMDDhhmm,CO2"); // write column hdr
  mmSv = vI[mdm];             // save current minute value
  // *** for MHZ19 -- no setup required for current MHZ19b
  // for (uint8_t j=0; j<9; j++){ // set-ABC-logic command
  //  sByte=sABCcmd[j];
  //  sSend();                    // and send it
  // }
  delay(1000);                 // enough time for MHZ19 readiness
}

void loop() { // ***** Arduino loop *****
  getTime();
  // *** send "Read CO2" command ***
  for (uint8_t j = 0; j < 9; j++) {
    sByte = rCO2cmd[j];
    sSend();
  }
  // *** get CO2 value ***
  sRcv();                     // byte0 is 0x86
  sRcv();                     // byte1 is Sensor number
  sRcv();                     // byte2 is CO2 value high
  hV = rByte;                 // set it to hV
  sRcv();                     // byte3 is CO2 value low
  lV = rByte;                 // set it to lV
  // for (uint8_t j = 0; j < 5; j++) { // read residual 5 bytes
  // sRcv();                         // *** can be ignored
  // }                                 // hence, test use only
  uint16_t mCO2 = hV * 256 + lV; // get CO2 PPM value
  CO2 = (float)(0.250 * mCO2 + 0.750 * CO2); // smoothed with exp.factor 0.25
  // plot bottom, ceiling and CO2 to PC via HW-serial
  int lowL = 400;
  int highL = 1000;
  Serial.print(lowL);
  Serial.print(",");
  Serial.print(CO2);
  Serial.print(",");
  Serial.println(highL);
  edDisp();                    // edit CO2 Data and display
  // *** Write SD data every minute ***
  if (vI[mdm] != mmSv) {       // if minute changed
    mmSv = vI[mdm];            // save new minute
    String s = strMDhm;        // set date-time
    s.concat(",") ;            // set comma
    s.concat(strCO2);          // set data
    aqLog.println(s);          // write a record
    //Serial.println(s);       // this is for debug only
  }
  // *** 2s delay, w/ SD-close-button checks every 100mS ***
  for (int i = 0; i < 20; i++) {
    if (digitalRead(SDsw) == LOW) { // if SD sw to close
      aqLog.close();
      // clear display and stop
      for (int j = 0; j < 4; j++) {
        Data[j] = 0x10;
      }
      lDispData();
      while (1) {}
    }
    delay(100);
  }
}

/**************************************
      User defined functions
* *********************************** */
// *** edDisp() *** Edit and display values
void edDisp(void) {
  sprintf(strCO2, "%04d", CO2);
  // display CO2 value via Data
  for (int i = 0; i < 4; i++) {
    Data[i] = strCO2[i] & 0x0F;
  }
  lDispData();
}

// *** lDispData() *** display data to TM1637 4digits
void lDispData(void) {
#define showDat 0x40          // show this is data
#define showAd0 0xC0          // LED register addr is zero
#define showDcB 0x88+lBrt     // show dCtl + brightness
  lStart();                   // start signal
  lChr = showDat;             // identifier for data
  lSend();                    // send it
  lStop();                    // stop signal
  lStart();                   // and restart
  lChr = showAd0;             // identifier for address
  lSend();                    // send it
  for (int j = 0; j < 4; j++) { // for Data[0] to Data[3]
    byte edChr = Data[j];     // set a byte to edChr for edit
    switch (edChr) {
      case 0x00: lChr = 0x3F; break; // 0
      case 0x01: lChr = 0x06; break; // 1
      case 0x02: lChr = 0x5B; break; // 2
      case 0x03: lChr = 0x4F; break; // 3
      case 0x04: lChr = 0x66; break; // 4
      case 0x05: lChr = 0x6D; break; // 5
      case 0x06: lChr = 0x7D; break; // 6
      case 0x07: lChr = 0x07; break; // 7
      case 0x08: lChr = 0x7F; break; // 8
      case 0x09: lChr = 0x6F; break; // 9
      case 0x10: lChr = 0x00; break; // blank
      default:   lChr = 0x79;        // E for error
    }
    lSend();                  // send each byte continuously
  }                           // end of four bytes
  lStop();                    // stop signal
  lStart();                   // restart
  lChr = showDcB;             // identifier for display brightness
  lSend();                    // send it
  lStop();                    // stop signal
}

// *** lSend() *** send a charater in lChr to TM1637
void lSend(void) {
#define LSb B00000001           // Least Significant bit
  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((lChr & LSb) == LSb) {  // if one then
      digitalWrite(lDIO, HIGH); // set lDIO high
    } else {                    // else
      digitalWrite(lDIO, LOW);  // set lDIO low
    }
    lCLKon();                   // clock on to show lDIO
    lChr = lChr >> 1;           // shift bits to right
  }
  digitalWrite(lDIO, LOW);      // pull down during Ack
  lCLKon();                     // clock on to simulate reading
}

// *** lStart() *** send Start signal to TM1637
void lStart(void) {
  digitalWrite(lDIO, LOW);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** lStop() *** send stoP signal to TM1637
void lStop(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lDIO, HIGH);
  delayMicroseconds(lTu);
}

// *** lCLKon() ** TM1637 clock on to show data to TM1637
void lCLKon(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** getTime() *** get RTC MMDD & hhmm into strMDhm
void getTime(void) {
  rRTC();      // read RTC device
  cR2I();      // convert RTC-format to Integers
  sprintf(strMDhm, "%02d%02d%02d%02d", vI[mdM], vI[mdD], vI[mdh], vI[mdm]);
}

// *** rRTC() *** read Real-Time-Clock
void rRTC(void) {
  Wire.beginTransmission(RTC_addr);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC_addr, 7);
  while (Wire.available() < 7) {}  // wait for data ready
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
}

// *** cR2I() *** convert RTC-format to Integers
void cR2I(void) {
  vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
  vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
  vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  vI[mdW] = vR[mdW];
  vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}

// *** sSend() *** send a charater in sByte via SW UART
void sSend(void) {
#define LSb B00000001         // Least Significant bit
  digitalWrite(sTx, LOW);
  delayMicroseconds(sTuS);

  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((sByte & LSb) == LSb) { // if one then
      digitalWrite(sTx, HIGH); // set sTx high
    } else {                  // else
      digitalWrite(sTx, LOW); // set sTx low
    }
    delayMicroseconds(sTuS);  // take required time
    sByte = sByte >> 1;       // shift a bit to right
  }

  digitalWrite(sTx,  HIGH);   // set high as a stoP bit
  delayMicroseconds(sTuS);    // consume time-unit
}

// *** sRcv() *** receive a charater in rByte, via SW UART
void sRcv(void) {
#define MSb B10000000          // Most-Significant-bit
#define nMSb B01111111         // not Most-Significant-bit
  while (digitalRead(sRx) == HIGH) {} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH); // sampling-time-checker on
    digitalWrite(tmChk, LOW);  // sampling-time-checker off
    if (digitalRead(sRx) == HIGH) { // if sRx high, then
      rByte = rByte | MSb;     // set MSb on
    } else {                   // else
      rByte = rByte & nMSb;    // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}

/* End of program */

 

このスケッチはUNOの容量の50%台に収まり、余裕があります。

 

後半少し急いで書いたのでよみにくいかもしれませんが、ご容赦ください。では今回はこのへんで。

 

©2020 Akira Tominaga, All rights reserved.

 

 

 

ソフトウェアシリアルをコンパクトに作るーArduino編-そしてトレースに使う

f:id:a-tomi:20200805173852j:plain

シリアル通信を小さく作る例をArduinoの例で説明したいと思います。少し複雑な工作でも、ESPなどを持ち出さずにUNO(ATmega328P)などありきたりのマイコンで作る「生活の知恵」でもありますし、シリアル通信のアナライザーを作る知恵でもあります。この前後の記事でご紹介する二酸化炭素濃度ロガーなど、通信種類が多い場合などに役立ちます。

上の写真は左がArduino-UNO、右がコンパチ機、間に信号を観察するための小さなブレッドボードを挟んでいます。

そもそもIDE環境がArduinoとPCの接続にハードウェア・シリアル通信を使うため、それが1つしかないUNOの場合には、2つ目が必要なら「ソフトウェアシリアル」を使います。ところがその標準ライブラリーは資源を意外に多く消費します。そこで必要な、小容量で複数つなぐ方法としてここで説明したいと思います。

ご紹介する手作りソフトの受信ロジックは、デバイス間のシリアル通信を外部システムとして勝手に受信(つまりトレースやブロードキャスト通信も)できます。そのスケッチ例は、この記事の最後のほうにつけておきます(2020.8.13追加)。

雑談ですが、シリアル通信はUART通信、非同期通信、スタートストップ通信、調歩式、等々色んな名称がありますかね。物理的なリンクだけ見れば各デバイスの時計に頼り、同期をとらずにビットをやりとりします。現代から見れば乱暴^^;  世界でオンラインシステムが始まった時から使われ、日本では1964年東京オリンピックがその通信システムの始まり。同期式が使われだしたのはだいぶ後です。

それがマイコンで今でも広く使われているのが不思議ですが、デバイスは今やどれも正確なクォーツクを使い問題はほぼないわけです。フィリップスI2C通信の絶妙アーキテクチャには程遠いですが、単純な2線で便利です。次の写真のマイクロSDリーダーライターは、シリアル通信で動かす日本製モジュールで、小さなマイコンでもSDカード処理がらくちんでできてしまいます。

f:id:a-tomi:20200805195805j:plain

2011年に放射能測定器を色々作りましたが、簡単なインターフェイスでロギングするのに随分重宝です。高価でしたが。今も時々使いますが、この製造元である塩尻市の(有)Suntechさんが2020年4月末で営業を終了されたのは残念。あ、雑談はここまで。つまりシリアル接続は単純なのでポピュラーに使われているわけです。

 

まずArduino標準ライブラリーのソフトウェアシリアル通信を使うサンプルスケッチを載せます。最初に送る側(仮にMasterと呼びます)です。ここではハードウェアシリアルでPCのキーボードから受けた文字を、そのままソフトウェアシリアルで送信します。

/* Software serial - exapmle 
 * Sender (Master) V.00 July 2020 (c)Akira Tominaga
*/
#include "SoftwareSerial.h"
#define sRx 7                     // Master sRx (MISO) = Slave sTx
#define sTx 8                     // Master sTx (MOSI) = Slave sRx
#define sSbaud 9600               // SW-serial baud rate
SoftwareSerial sS(sRx, sTx);      // create SW-serial as symbol sS
String strTyped;                  // string from KB and to send to Slave
char rBt;                         // receiving char from software Serial

void setup() {  // ***** Arduino Setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial
  Serial.println("Master ready");
}

void loop() {  // ***** Arduino Loop *****
  while (Serial.available() < 1) {}
  strTyped = Serial.readStringUntil(0x10); // read KB until DLE
  uint8_t strL = strTyped.length(); // get length
  for (uint8_t i = 0; i < strL; i++) {
    sS.write(strTyped.charAt(i)); // send each byte
  }
  Serial.print(strTyped);         // show what was sent
  Serial.println("was sent by Master");
}
/* *** End of sketch *** */

 次に、受ける側の標準ソフトウェアシリアルによるスケッチです。仮にSlaveと呼びますが、受けた文字をそのまま印字するようにハードウェアシリアルに出します。

/* Software serial - exapmle 
 * Receiver (Slave) V.00 July 2020 (c)Akira Tominaga
*/
#include "SoftwareSerial.h"
#define sRx 7                     // sS-Slave Rx (MOSI) = sS Master Tx
#define sTx 8                     // sS-Slave Tx (MISO) = sS Master Rx
char rBt;                         // receiving char from software Serial
#define sSbaud 9600               // SW-serial baud ratea
SoftwareSerial sS(sRx, sTx);      // create SW-serial sS as symbol

void setup() { // ***** Arduino setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial sS
  Serial.println("Receiver ready");
}

void loop() {  // ***** Arduino Loop *****
  if (sS.available()) {
    rBt = sS.read();
    Serial.print(rBt);
    if (rBt == 0x0A) {
      Serial.println("was received by Slave");
    }
  }
}
/* *** End of Sketch *** */

この2つのペアによる送受をオシロで観察しても、中身が分り難いのが非同期通信の特徴。手がかりのクロック信号がないですからね。次の写真で下がMasterから出たもの、上がSlaveから返したもの(2020.8.6追加記述:失礼、上に示したレシーバーのソースに1行追加して送り返しています)。スタートビットはH(シリアル通信ではHをスペースという)からL(マーク)になるところを指し、ストップビットは最後に信号線をH(スペース)に戻すだけです。その間に8ビットの列があり、ビットの順番は普通のバイトとは逆に(つまりLSb=Least Significant bit から)送られます。コンピュータによってメモリー内への収容が逆順の機種(Little Endian)も多いのでまあ一理あるわけですが。

f:id:a-tomi:20200805182725j:plain

オシロではなく、ロジックアナライザーなら少しわかりやすいですが、時間計算する以外にタイミングの手がかりが少ないもの。そこで、勝手な電子工作の知恵では、分析のためにはU(0x55)という文字を含めるようにします。Uならオンオフのビットが交互になりますからタイミングが確実にわかりますね。次はUAUと打ち込み送信をロジックアナライザーで見たもの。

f:id:a-tomi:20200805201804j:plain

Sはスタートビットで、Pはストップビット。間のビットの並びを前後逆転すれば0x55、0x41、0x55、0x0Aと簡単に読めます。送信側ではUSBキーボードを打ち込んでハードウェアシリアルに渡されたものをDLE記号が来る前までそのままを送っていますから、最後に0x0AのLF(改行つまりエンターキー)がくっついてます。

ロジックアナライザーは次を使っていますが、今どきは中国製のコンパチモデルというよりも、形状を変えたコピー機が超廉価でずいぶん出回ってるようです。

 

f:id:a-tomi:20200805203049j:plain

 

 ロジックアナライザーで使わないチャネルのプローブ先端はグランドに落としますから、次のようなアナライザー専用のブレッドボードを常備しておくと測定が手短かに行え、かつ、刺し間違いも減ります。

f:id:a-tomi:20200805203409j:plain

これは普通のブレッドボードの一部を強引に切り取ってヤスリをかけただけ。小さくて必要な穴が十分なので、場所を取らず迅速に使え、さしっぱなしで保管できます。

 

とはいえ、シリアル通信の信号解析が圧倒的に楽な方法は、プログラムで分析することです。さきほど紹介した受信側のロジックを使えば簡単です。

 

標準のソフトウェアシリアルはUNOの容量の1割近くを占拠するので、一般的な工作で小マイコンに組み込むために手で書く例(RYO=Roll-Your-Ownプログラム)が次です。

まず送る側。Uの文字を含んで測定しながら定数(sTuS)を設定するだけでよいものです。

/*  Roll-Your-Own software-serial-communication; Sender
 *  Sample sketch V.00  July 31, 2020 (c) Akira Tominaga
*/
#define sRx 7                     // Sender sRx (MISO) = Receiver sTx
#define sTx 8                     // Sender sTx (MOSI) = Receiver sRx
#define sTuS 104-3                // time unit (9600bps) for sending
#define sTuhS 51                  // time unit half for sending                  
char tByte;                       // typed-in character
char sByte;                       // character work area to be sent

void setup() {  // ***** Arduino setlup ***** 
  Serial.begin(9600);             // start hardware serial
  pinMode(sRx,INPUT_PULLUP);      // soft Rx pulled-up
  pinMode(sTx,OUTPUT);            // soft Tx as output
  digitalWrite(sTx,HIGH);         // set it HIGH
  Serial.println("Snd ready");
}

void loop() {  // ***** Arduino loop *****
  while (Serial.available() < 1) {} // wait until type-in
  tByte = Serial.read();          // read typed-in character
  sByte = tByte;                  // set it into work byte to be sent
  sSend();                        // send it via software serial
  Serial.print(tByte);            // print char sent(unchanged byte)
  delayMicroseconds(sTuS*3);        // *** for receiver printing time
}

/**************************************
      User defined functions
* *********************************** */
// *** sSend *** send a charater in sByte
void sSend(void) {
#define LSb B00000001             // Least Significant bit
  digitalWrite(sTx, LOW);
  delayMicroseconds(sTuS);
  
  for (int i = 0; i < 8; i++) {   // do the following for 8bits
    if ((sByte & LSb) == LSb) {   // if one then
      digitalWrite(sTx, HIGH);    // set sTx high
    } else {                      // else
      digitalWrite(sTx, LOW);     // set sTx low
    }
    delayMicroseconds(sTuS);      // take required time
    sByte = sByte >> 1;           // shift a bit to right
  }
  
  digitalWrite(sTx,  HIGH);       // set high as a stoP bit
  delayMicroseconds(sTuS+sTuhS);  // consume time-unit x 1.5
}

/* End of program */

そして次が受ける側のRYOプログラム。こちらはタイミング調整の勝手な生活の知恵として、読取タイミングをどこかのピン(tmChk)に出力します。定数(sTuR)の値を減らしながら、検査すべきビットの真ん中に読取タイミングが来るようにすれば、それだけで以後はもう大丈夫です。

/*  Roll-Your-Own software-serial-communication Receiver
    Sample sketch V.00  July 31, 2020 (c) Akira Tominaga
*/
#define tmChk 6                   // timing checker for measurement
#define sRx 7                     // Receiver sRx (MOSI) = Sender sTx
#define sTx 8                     // Receiver sTx (MISO) = Sender sRx
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte

void setup() {  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial
  pinMode(sRx, INPUT_PULLUP);     // soft Rx pulled-up
  pinMode(sTx, OUTPUT);           // soft Tx as output
  digitalWrite(sTx, HIGH);        // set it high
  pinMode(tmChk, OUTPUT);         // timing checker
  digitalWrite(tmChk, LOW);       // set it LOW
  Serial.println();               // LF for repeated testing
  Serial.println("Rcv ready");
}

void loop() {  // ***** Arduino loop *****
  sRcv();                         // receive data into rByte
  Serial.print(rByte, HEX);
  Serial.print(" ");              // insert blank as a separator
}

/**************************************
      User defined functions
* *********************************** */
// *** sRcv *** receive a charater in rByte
void sRcv(void) {
#define MSb B10000000             // Most-Significant-bit
#define nMSb B01111111            // not Most-Significant-bit
  while(digitalRead(sRx)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(sRx) == HIGH) { // if sRx high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}
/* End of program */

 受信のタイミング信号を含めて記録したものは次です。

f:id:a-tomi:20200805210353j:plain

送る側のRYOプログラムを標準ソフトウェアシリアルの受信側で受けると同じ動きをしますし、逆の組みあわせ、つまり標準ソフトウェアシリアルの送り側を今回のRYOの受け側を組み合わせても、同じ動きをします。つまり他の相手ともつながるということが分ります。

ついでにビットの解析を書けば次のようになります。

f:id:a-tomi:20200805210611j:plain

以上のように、必要部分だけを勝手なRYOプログラムにすると、Arduinoの容量はずいぶん節約されて、UNOでは通常は難しいことも、余裕でできてしまいます。1例としては次の記事などをご参照ください。

テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型 - 勝手な電子工作・・

 

資源の減らすだけでなく、このようなRYOプログラムなら通信のトレースも簡単にできます。

次の例は、マイコンとデバイスの間のシリアル通信でのやりとりを、Arduinoのプログラムで外部から観察したものです。

f:id:a-tomi:20200812172946j:plain

 プログラムは分析相手に合うように適当に加工すればよいものですが、ここで用いたスケッチは次のものです。

/*  Roll-Your-Own software-serial-communication Receiver
    Initial version V.00  Aug. 12, 2020 (c) Akira Tominaga
*/
#define tmChk 6                   // timing checker for adjustment use
#define Line1 7                   // Line1 to analyze
#define Line2 8                   // Line2 to analyze
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte
#define numB  128                 // number of bytes to be analyzed
byte rB[numB * 2 + 5];            // analyzed data accumulation area
uint16_t rBi = 0;                 // accumulation area index

void setup() {  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial
  pinMode(tmChk, OUTPUT);         // timing checker as output
  digitalWrite(tmChk, LOW);       // set it LOW
  pinMode(Line1, INPUT_PULLUP);   // Line1 pulled-up
  pinMode(Line2, INPUT_PULLUP);   // Line1 pulled-up

  Serial.println("Analyzing...");
}

void loop() {  // ***** Arduino loop *****
  if (digitalRead(Line1) == LOW) sRcv1();
  if (digitalRead(Line2) == LOW) sRcv2();
  if (rBi > numB * 2) {
    byte Lnosv = 0x03; // set temp value (not 01 or 02)
    for (rBi = 0; rBi < numB * 2 - 1; rBi = rBi + 2) {
      byte Lno = rB[rBi];
      byte Dat = rB[rBi + 1];
      if (Lnosv == Lno) {
        Serial.print(" "); Serial.print(Dat, HEX);
      } else {
        Serial.println(); Serial.print("Line=");
        Serial.print(Lno, HEX); Serial.print(" ");
        Serial.print(Dat, HEX);
        Lnosv = Lno;
      }
    }
    while (1) {}
  }
}
/**************************************
      User defined functions
* *********************************** */
#define MSb B10000000             // Most-Significant-bit
#define nMSb B01111111            // not Most-Significant-bit

// *** sRcv1 *** receive Line1 charater in rByte
void sRcv1(void) {
  // while(digitalRead(Line1)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(Line1) == HIGH) { // if Line1 high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(Line1) == LOW) {}; // wait until HIGH (stop bit)
  rB[rBi] = 0x01; rBi++;
  rB[rBi] = rByte; rBi++;
}

// *** sRcv2 *** receive Line2 charater in rByte
void sRcv2(void) {
  // while(digitalRead(Line2)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(Line2) == HIGH) { // if Line1 high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(Line2) == LOW) {}; // wait until HIGH (stop bit)
  rB[rBi] = 0x02; rBi++;
  rB[rBi] = rByte; rBi++;
}

/* End of program */

アナライザープログラムとしてのこの使い方は、ピン7、ピン8を調べたいシリアルラインに接続するだけです。もちろんグランドの接続は前提ですし、クロックスピードは相手に合わせられる範囲だけですが。

 

以上で簡単な説明を終わりたいと思いますが、他のマイコンでも似たようなものですね。ご質問いただいた方のお役に立てば幸いです。

 

©2010 Akira Tominaga, All rights reserved.

 

テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型

f:id:a-tomi:20200804105556j:plain

テレワークで部屋に籠ることが多い場合は換気が必要です。実際に2酸化炭素が1000PPMを超えると眠くなったり飽きたり・・・。前々回作った測定装置でそうなるのを体感しました。

梅雨明けした暑い夏は、部屋にエアコンかけて閉じっぱなしにすると益々危険そう。今回は赤外線吸収型(NDIR=nondispersive infrared )のCO2センサーを使って室内の炭酸ガス濃度ロガーを作ってみました。

2酸化炭素が4260nmの赤外線を吸収するという原理を使う古典的な方法のセンサーですが、長年にわたって改良されてきたもので小型化や高性能化が色々行われています。上の写真は今回使うMH-Z19Bという安価なセンサーモジュールです。

f:id:a-tomi:20200804112644j:plain

前々回の記事に金属酸化物を使ったCCS811半導体センサーで作ったロガーを紹介しました。なぜ、新たにNDIRセンサーでロガーを作ってみるかと言えば、半導体センサー(動作の殆どは内蔵ソフトウェアの機能)は比較的新しいセンサーで鋭敏なのですが、長時間記録するロガーとするには結構手間のかかるキャリブレーション(Baseline registerの設定)が度々必要です。

ArduinoのCCS811標準ライブラリーでは、なんと2019年1月にその設定機能が追加されたばかりです。現在それを別プログラムで行っていますが、校正がしばしば必要なのは、扱いが結構手間です。2台作った測定値の間にはこれまで大きな狂いはないですが、正しい数値かどうかを別原理のロガーと比較してみたいという興味もあります。

NDIRセンサーは多くのメーカーから工夫された安定製品がでていますが、どれも比較的高価です。調べた限りは、長期間の使用では当然キャリブレーションを必要としますが、方法も色々あります。MH-Z19Bではシンプルな方法3つが提供されていますし、なによりも安価(海外ネットでは15ドル程ですから、CCS811の倍ぐらい)なので、これを2個入手して挑戦しました。

測定値のとりかたには次の3通りあります。

①シリアル(UART)通信でコマンドを送り、値をデジタルで直接得る

②PWMのDuty幅(アナログ値)を介して得る

③アナログ出力値を読み取る

まず一番簡単な③をやってみたら旨く測れるのですが、なんと!周期的に特定ノイズが載ってきます。2個とも同じ現象があり、配線の問題ではなくこのセンサーモジュール自体の性格のようです。これを無視するようにプログラムすればできそうですが、この際は③を使わないことにしました。ついでに②もけっこう面倒そうなので、一番確実な①でやってみることにしました。

f:id:a-tomi:20200804114351j:plain

試しに値をとりプロットすると、おお!長時間忠実にアウトプットされるではないですか!

上は測定開始1010秒(約17分)後から2610秒(約44分)後の間のグラフです。エアコンの風があたっている間は測定値がギザギザになることがわかりました。空気取り入れ口にフィルターがあるからそういう現象になるかも。風のあたらない場所で測るとスムーズです。しかし、風が当たる場所でもプログラムでスムージングすれば問題ない感じです。

このセンサーは約1秒強の間隔でデータが得られますが、ロガーとしては安全のために2秒間隔で測定し毎分1度記録するとします。たとえば測定値を指数平滑法を使って、測定値x0.25+前回保存値x0.75でスムージングするとした場合、仮に500ppmの急激な変化があっても、記録する1分(つまり測定29回)以内で追従できます(計算してみました)ので問題ないわけです。

さて、UART接続したArduinoスケッチは次のように短い超簡単なものです。

/* CO2 plotter example with NDIR MH-Z19b via serial interface  */
#include "MHZ19.h"
#include "SoftwareSerial.h"
#define Rx 8                      // Master Rx (MISO) = MHZ19 Tx
#define Tx 9                      // Master Tx (MOSI) = MHZ19 Rx
#define swSerB 9600               // MH-Z19 spec. serial baud rate
MHZ19 NDIR;                       // MH-Z19 symbol to be NDIR
SoftwareSerial swSer(Rx, Tx);     // (Uno example) create device to MH-Z19 serial
void setup(){ // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial to PC
  swSer.begin(swSerB);            // start software serial to MH-Z19
  delay(100);
  NDIR.begin(swSer);              // begin NDIR CO2 sensor MHZ19
  NDIR.autoCalibration();         // set auto caliration TRUE
}
void loop(){ // ***** Arduino loop *****
  int CO2 = NDIR.getCO2();        // get CO2 PPM
  int lL = 400;  int hL = 1000;   // set scale lines
  Serial.print(lL);  Serial.print(",");  Serial.print(CO2);
  Serial.print(",");  Serial.println(hL);
  delay(2000);                    // measure every 2 seconds
}
// *** end of sketch ***

 このスケッチをためして気づいた重要なことは、次の2つです。

①これにArduino標準ライブラリーを使うとマイコン容量をかなり消耗する。ロガーには他のデバイスも複数使用するため、そのやりかたではUNOではとても困難になる。

②MH-Z19というセンサーモジュールは4ピンと5ピンの列があるが、列間距離の仕様が異常。このままブレッドボードなどに刺すとモジュールを壊す恐れがある。

①についてはコンパイル結果が次のとおりで、グローバル変数で実に34%消耗!

f:id:a-tomi:20200804120814j:plain

 ②については、サイズの仕様が次のとおりです。

f:id:a-tomi:20200804121005j:plain

29.54mmという間隔には、ホントにまいってしまいます!端数にわざわざ0.54mmをつけたりして。30.48mmとか、27.94mmとかなら適切で納得できますが。・・・きっと過去に仕様をきめたときの誰かの間違いが長年ずっとひきずっているのでしょうかね。

とにかくこのままブレッドボードなどに刺してはだめ(データシートにはPCBに無理な力をかけないようにと注意書きがあります、ということはそういうことで壊す例が多い?)。とにかく気を付けましょう。

さて、標準ライブラリーが大きすぎるなら、UNOをあきらめて別のモデルを使う方法もありますが、UNOにこだわるなら手で書けばよいわけです。このセンサーとのやりとりも、ソフトウェアSerialも、複雑なものではありませんから。次のようにRYO(Roll-Your-Own)プログラムに書き換えれば問題なく動作をしますし、容量もあまり食いません。

/* CO2 (PPM) testing plotter with MH-Z19b NDIR sensor,
 * w/RYO serial-com. & RYO handling V00 Aug.2000 (c) Akira Tominaga */
#define sRx 8                     // Master Rx (MISO) = MHZ19 Tx
#define sTx 9                     // Master Tx (MOSI) = MHZ19 Rx
// for serial sending
#define sTuS 104-3                // time unit (9600bps) for sending
#define sTuhS 51                  // time unit half for sending                  
char sByte;                       // character work area to be sent
// for serial receiving
#define tmChk 16                  // timing checker for test (D16=A2)
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte
// for MH-Z19b, preset & auto-calib since July 2015, hence no setup required doday.
//  Ref:Datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
//               flags, req, rCO2,  0,    0,    0,    0,    0,  chksum
byte hV;                          // response byte2 high value
byte lV;                          // response byte3 low value
uint16_t CO2;                     // CO2=hV*256+lV

void setup(){  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial to PC
  pinMode(sRx, INPUT_PULLUP);     // soft Rx to be pulled-up
  pinMode(sTx, OUTPUT);           // soft Tx as output
  digitalWrite(sTx, HIGH);        // set it high
  pinMode(tmChk, OUTPUT);         // timing checker
  digitalWrite(tmChk, LOW);       // set it LOW
}

void loop(){  // ***** Arduino loop *****
  // send "Read CO2" command
  for (uint8_t j=0; j<9; j++){
    sByte=rCO2cmd[j];
    sSend();
  }
  // receive CO2 value
    sRcv();                     // byte0 is ignored (0x86 response) 
    sRcv();                     // byte1 is ignored (sensor number)
    sRcv();                     // byte2 is CO2 value high
    hV=rByte;                   // set it to highValue
    sRcv();                     // byte3 is CO2 value low
    lV=rByte;                   // set it to lowValue
    for (uint8_t j=0; j<5; j++){ // read and ignore residual 5 bytes
      sRcv();
    }
    CO2=hV*256+lV;              // calculate CO2 PPM value
  // plot scales and CO2 via hardware serial to PC
  int lowL = 400;  int highL = 1000; // set scales
  Serial.print(lowL);   Serial.print(",");  Serial.print(CO2);
  Serial.print(",");  Serial.println(highL);
  delay(2000);                 // measure every 2 seconds
}

/**************************************
      User defined functions
* *********************************** */
// *** sSend *** send a charater in sByte
void sSend(void) {
#define LSb B00000001             // Least Significant bit
  digitalWrite(sTx, LOW);
  delayMicroseconds(sTuS);

  for (int i = 0; i < 8; i++) {   // do the following for 8bits
    if ((sByte & LSb) == LSb) {   // if one then
      digitalWrite(sTx, HIGH);    // set sTx high
    } else {                      // else
      digitalWrite(sTx, LOW);     // set sTx low
    }
    delayMicroseconds(sTuS);      // take required time
    sByte = sByte >> 1;           // shift a bit to right
  }

  digitalWrite(sTx,  HIGH);       // set high as a stoP bit
  delayMicroseconds(sTuS + sTuhS); // consume time-unit x 1.5
}

// *** sRcv *** receive a charater in rByte
void sRcv(void) {
#define MSb B10000000             // Most-Significant-bit
#define nMSb B01111111            // not Most-Significant-bit
  while(digitalRead(sRx)==HIGH){} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH);    // sampling-time-checker on
    digitalWrite(tmChk, LOW);     // sampling-time-checker off
    if (digitalRead(sRx) == HIGH) { // if sRx high, then
      rByte = rByte | MSb;        // set MSb on
    } else {                      // else
      rByte = rByte & nMSb;       // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}
/* End of program */

標準ライブラリーではSetupでABC設定(Automatic Baseline calibration)コマンドを送りますが、こちらでは出しません。このデバイスの最近の仕様は出荷時にABC設定済で設定不要となっており、設定の必要性がありません。このスケッチで標準ライブラリーと同じく長時間安定した値が測定されます。

これならArduino Mega2560とかESPなどを持ち出さなくとも、Arduino UNO(ATmega328P)単体で余裕をもってできそうです。なお、タイミングが重要なシリアル通信などをどうやって簡単にRYOで作ったり観察したりするかの方法は、少し長くなりそうなので回を改めてご紹介したいと思います。

 ロガーの回路は次のようにすれば、5.0V供給だけですむので前回の半導体センサーよりだいぶ単純です。毎度汚い手書きですみませんが。じつはこれを考えてからプロッターでのテストをしてみて、ソフトのRYO化が必要なことに気づいたのですが。

f:id:a-tomi:20200804122602j:plain

次はブレッドボードで試作したロガーです。

f:id:a-tomi:20200804131402j:plain

今回のセンサーはまだキャリブレーションをしていませんが、数値は前回作ったロガー(下の写真の下側)とあまり大きくは違いません。キャリブレーションはこのセンサーに接続したスライドスイッチをオンに(接地)するだけでできるようにしました。CO2が400ppm以下の環境を必要とするので、緑の多いところで光合成の盛んな午後に行うのが妥当かと思います。(あるいはCO2ゼロで赤外線吸収のない窒素ガスかアルゴンガスの中でやればよいのですが、そういうのはなかなかね)。

f:id:a-tomi:20200804131506j:plain

 

今回のロガーのプログラムでは、比較的複雑なSD入出力、SPI通信、I2C通信に標準ライブラリーを使い、単純なRTC(リアルタイムクロック)、TM1637表示、このMH-Z19センサー処理、およびソフトウェアシリアル通信をRYOにすることで、十分小さく収まりました。そういうわけでプログラムの記述は少し長いですがArduinoスケッチは次のとおりです。

(2020.8.11 下のスケッチで2行を直接直しました。①回路図に合わせてピン8と9を逆にしました。②Setupの最後の待ち時間を1000ミリ秒に直しました。前の100ミリ秒の場合にはMH-Z19センサーがReadyになってないことがまれにありますので。最近作った基板とともに使い心地の比較等を含めて、今後別記事で最新プログラムとともに改めて紹介するつもりです。)

/*
   CO2 Logger with MH-Z19b NDIR sensor,
   with RYO coding for RTC, TM1637, MHZ19, and Soft-Serial
        Initial version V.00 Aug.1, 2020 (c) Akira Tominaga
*/
#include "Wire.h"             // I2C for RTC-DS3231 
#include "SPI.h"              // SPI for SD drive interface
#include "SD.h"               // Micro SD drive

// *** for micro SD writer ***
File aqLog;                   // aqLog as SD file symbol
#define Cs  10                // SD Chip-select pin is D10
//  MOSI=11, MISO=12, CLK=13
#define SDsw 4                // button to close SD file

// *** for TM1637 display ***
#define lDIO 6                // DIO for TM1637 LED
#define lCLK 7                // CLK for TM1637 LED
#define lBrt 0x02             // duty-r 1/16x 02:4,03:10,05:12,07:14
#define lTu 50                // time unit in micro-second
byte lChr;                    // single byte sent to LED
byte Data[] = { 0x00, 0x00, 0x00, 0x00 }; // LED initial value 0000

// *** UART for CO2 sensor ***
#define sRx 9                 // Master Rx (MISO) = MHZ19 Tx
#define sTx 8                 // Master Tx (MOSI) = MHZ19 Rx
// (for sending)
#define sTuS 104-3            // time unit (9600bps) for sending
#define sTuhS 51              // time unit half for sending                  
char sByte;                   // character work area to be sent
// (for receiving)
#define tmChk 16              // timing checker for test (D16=A2)
#define sTuR 104-10           // time unit (9600bps) for receive
#define sTuhR 51-21           // time unit / 2 for receive
byte rByte;                   // character receiving byte

// *** for NDIR CO2 sensor MH-Z19b
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
//               flags, req, rCO2,  0,    0,    0,    0,    0,  chksum
// Auto-calib & preset since Jul.2015, hence no setup required as of doday.
// Ref:datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte hV;                      // response byte2 high value
byte lV;                      // response byte3 low value
uint16_t CO2=600;             // CO2 ppm smoothed value, init. 600
char strCO2[5];               // edit. area for SD recds and display

// *** for Real Time Clock DS3231
byte  vR[8];                  // values in RTC registers
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};  // RTC data, init 0000
#define RTC_addr 0x68
#define mdI 0                 // Initial mode
#define mds 1                 // ss
#define mdm 2                 // mm
#define mdh 3                 // hh
#define mdW 4                 // WW=Day of Week
#define mdD 5                 // DD
#define mdM 6                 // MM
#define mdY 7                 // YY
#define sS 0
#define mM 1
#define hH 2
char  strMDhm[11];            // edit area for calendar and clock
uint8_t mmSv;                 // minute-llvalue save area

void setup()  // ***** Arduino setup *****
{
  Serial.begin(9600);         // start hardware serial to PC
  // *** for RYO-SW-UART
  pinMode(sRx, INPUT_PULLUP); // soft Rx to be pulled-up
  pinMode(sTx, OUTPUT);       // soft Tx as output
  digitalWrite(sTx, HIGH);    // set it high
  pinMode(tmChk, OUTPUT);     // timing checker
  digitalWrite(tmChk, LOW);   // set it LOW
  // *** for RYO-TM1637 interface
  pinMode(lCLK, OUTPUT);      // lCLK as output
  pinMode(lDIO, OUTPUT);      // lDIO as output
  digitalWrite(lCLK, HIGH);   // lCLK high
  digitalWrite(lDIO, HIGH);   // then lDIO high to avoid Start sig
  lDispData();                // display 0000
  // *** for SD
  pinMode(SDsw, INPUT_PULLUP); // SD file closing button
  Wire.begin();
  if (!SD.begin(Cs)) {
    Data[0]="E";              // show error
    lDispData;                // display E000
    while (1) {}
  }
  // set SD file name MMDDhhmm
  getTime();
  String s = strMDhm;
  s.concat(".txt");           // complete file name
  aqLog = SD.open(s, FILE_WRITE);
  aqLog.println("MMDDhhmm,CO2"); // write column hdr
  mmSv = vI[mdm];             // save current minute value
  delay(1000);                // enough time for stability
}

void loop()  // ***** Arduino loop *****
{
  getTime();
  // *** send "Read CO2" command ***
  for (uint8_t j = 0; j < 9; j++) {
    sByte = rCO2cmd[j];
    sSend();
  }
  // *** get CO2 value ***
  sRcv();                     // byte0 is 0x86
  sRcv();                     // byte1 is Sensor number
  sRcv();                     // byte2 is CO2 value high
  hV = rByte;                 // set it to hV
  sRcv();                     // byte3 is CO2 value low
  lV = rByte;                 // set it to lV
  // for (uint8_t j = 0; j < 5; j++) { // read residual 5 bytes
    // sRcv();                         // *** can be ignored
  // }                                 // hence, test use only
  uint16_t mCO2 = hV * 256 + lV; // get CO2 PPM value
  CO2=(float)(0.250*mCO2+0.750*CO2); // smoothed with exp.factor 0.25
  // plot bottom, ceiling and CO2 to PC via HW-serial
  int lowL = 400;
  int highL = 1000;
  Serial.print(lowL);
  Serial.print(",");
  Serial.print(CO2);
  Serial.print(",");
  Serial.println(highL);
  edDisp();                    // edit CO2 Data and display
  
  // *** Write SD data every minute ***
  if (vI[mdm] != mmSv) {       // if minute changed
    mmSv = vI[mdm];            // save new minute
    String s = strMDhm;        // set date-time
    s.concat(",") ;            // set comma
    s.concat(strCO2);          // set data
    aqLog.println(s);          // write a record
    //Serial.println(s);       // this is for debug only
  }
 
  // *** 2s delay, w/ SD-close-button checks every 100mS ***
  for (int i = 0; i < 20; i++) {
    if (digitalRead(SDsw) == LOW) { // if SD sw to close
      aqLog.close();
      // clear display and stop
      for (int j=0; j<4; j++){
        Data[j]=0x10;
      }
      lDispData();
      while (1) {}
    }
    delay(100);
  }
}

/**************************************
      User defined functions
* *********************************** */
// *** edDisp() *** Edit and display values
void edDisp(void) {
  sprintf(strCO2, "%04d", CO2);
  // display CO2 value via Data
  for (int i = 0; i < 4; i++) {
    Data[i] = strCO2[i] & 0x0F;
  }
  lDispData();
}

// *** lDispData() *** display data to TM1637 4digits
void lDispData(void) {
#define showDat 0x40          // show this is data
#define showAd0 0xC0          // LED register addr is zero
#define showDcB 0x88+lBrt     // show dCtl + brightness
  lStart();                   // start signal
  lChr = showDat;             // identifier for data
  lSend();                    // send it
  lStop();                    // stop signal
  lStart();                   // and restart
  lChr = showAd0;             // identifier for address
  lSend();                    // send it
  for (int j = 0; j < 4; j++) { // for Data[0] to Data[3]
    byte edChr = Data[j];     // set a byte to edChr for edit
    switch (edChr) {
      case 0x00: lChr = 0x3F; break; // 0
      case 0x01: lChr = 0x06; break; // 1
      case 0x02: lChr = 0x5B; break; // 2
      case 0x03: lChr = 0x4F; break; // 3
      case 0x04: lChr = 0x66; break; // 4
      case 0x05: lChr = 0x6D; break; // 5
      case 0x06: lChr = 0x7D; break; // 6
      case 0x07: lChr = 0x07; break; // 7
      case 0x08: lChr = 0x7F; break; // 8
      case 0x09: lChr = 0x6F; break; // 9
      case 0x10: lChr = 0x00; break; // blank
      default:   lChr = 0x79;        // E for error
    }
    lSend();                  // send each byte continuously
  }                           // end of for bytes
  lStop();                    // stop signal
  lStart();                   // restart
  lChr = showDcB;             // identifier for display brightness
  lSend();                    // send it
  lStop();                    // stop signal
}

// *** lSend() *** send a charater in lChr to TM1637
void lSend(void) {
#define LSb B00000001           // Least Significant bit
  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((lChr & LSb) == LSb) {  // if one then
      digitalWrite(lDIO, HIGH); // set lDIO high
    } else {                    // else
      digitalWrite(lDIO, LOW);  // set lDIO low
    }
    lCLKon();                   // clock on to show lDIO
    lChr = lChr >> 1;           // shift bits to right
  }
  digitalWrite(lDIO, LOW);      // pull down during Ack
  lCLKon();                     // clock on to simulate reading
}

// *** lStart() *** send Start signal to TM1637
void lStart(void) {
  digitalWrite(lDIO, LOW);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** lStop() *** send stoP signal to TM1637
void lStop(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lDIO, HIGH);
  delayMicroseconds(lTu);
}

// *** lCLKon() ** TM1637 clock on to show data to TM1637
void lCLKon(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** getTime() *** get RTC MMDD & hhmm into strMDhm
void getTime(void) {
  rRTC();      // read RTC device
  cR2I();      // convert RTC-format to Integers
  sprintf(strMDhm, "%02d%02d%02d%02d", vI[mdM], vI[mdD], vI[mdh], vI[mdm]);
}

// *** rRTC() *** read Real-Time-Clock
void rRTC(void) {
  Wire.beginTransmission(RTC_addr);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC_addr, 7);
  while (Wire.available() < 7) {}  // wait for data ready
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
}

// *** cR2I() *** convert RTC-format to Integers
void cR2I(void) {
  vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
  vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
  vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  vI[mdW] = vR[mdW];
  vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}

// *** sSend() *** send a charater in sByte via SW UART
void sSend(void) {
#define LSb B00000001         // Least Significant bit
  digitalWrite(sTx, LOW);
  delayMicroseconds(sTuS);

  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((sByte & LSb) == LSb) { // if one then
      digitalWrite(sTx, HIGH); // set sTx high
    } else {                  // else
      digitalWrite(sTx, LOW); // set sTx low
    }
    delayMicroseconds(sTuS);  // take required time
    sByte = sByte >> 1;       // shift a bit to right
  }

  digitalWrite(sTx,  HIGH);   // set high as a stoP bit
  delayMicroseconds(sTuS + sTuhS); // consume time-unit x 1.5
}

// *** sRcv() *** receive a charater in rByte, via SW UART
void sRcv(void) {
#define MSb B10000000          // Most-Significant-bit
#define nMSb B01111111         // not Most-Significant-bit
  while (digitalRead(sRx) == HIGH) {} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH); // sampling-time-checker on
    digitalWrite(tmChk, LOW);  // sampling-time-checker off
    if (digitalRead(sRx) == HIGH) { // if sRx high, then
      rByte = rByte | MSb;     // set MSb on
    } else {                   // else
      rByte = rByte & nMSb;    // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}

/* End of program */

 このスケッチのコンパイルサイズは次のとおり、UNOの容量の50%台にしっかり収まっています。

f:id:a-tomi:20200804131933j:plain

測定値はいままでのところ長時間使っても安定しており、数値には換気がしっかり反映されています。とりあえず、めでたしめでたし^^

 

では今回はこの辺で。

 

追加記事はこちらにあります:

a-tomi.hatenablog.com

 

©2010 Akira Tominaga, All rights reserved.

TM1637-LEDをライブラリーなしで簡単表示

f:id:a-tomi:20200725160057j:plain

4桁の7セグメントLEDを直接ダイナミック点灯する場合は、少なくとも11本以上のピンを使います。このTM1637というICを用いたディスプレイなら2ピンしか使わずにすみ、見やすく廉価。色々な機会にこれを使っていますが、使い方を質問されることが結構多い部品です。

外出自粛の週末ですが、おまけにすごい雨です。こういう機会に、ごく簡単ですがこれの要点と簡単なプログラムの例を書いておこうと思います。

次の写真は10年ほど前にPCの元電源オン表示の代わりとして、ちょこっと作ってみた温湿度計です。8ピンマイコンで作りましたが今でも明るく動いています。8ピンなのに温湿度センサーの他に、明るさ表示変更用のボタンなどもつけています。

f:id:a-tomi:20200725161457j:plain

TM1637というICはずいぶん前に出たものですが、最初の頃はとくにライブラリーもなく、中国語のデータシートしかなくて苦労しましたが、その後は文書類もLED用のライブラリーなども整ったので世界で普及したと思います。

第一に4桁LED+TM1637IC込みで値段が1ドル程度と圧倒的に安いし、Duty比が可変で電流消費も少なくできたり、接続やプロトコルがシンプルだったり。7セグメントが自由自在(これはあたりまえか^^;)。

とはいえ、デバイス1つ毎にIOピンを2つも(!?)使うので、本当はI2C接続のものが欲しかったわけですが、最近になってHT16K33+4桁LEDのモジュールが出回り始めたので早速注文中。単価はこの約3倍ほどします。そのうちに記事で紹介したいと思いますが。

TM1637を用いた4桁LED表示器の種類(大きさ、表示色、コロンとピリオドの有無など)は色々あります。

f:id:a-tomi:20200725161853j:plain

今eBayを覗いてみると、現時点はコロナの影響もあって送料がかかるものが多く少々高いかもしれませんが、それなりに経済的。

f:id:a-tomi:20200725161946j:plain

Arduinoとは試作では例えば次のようにつなぎます。

f:id:a-tomi:20200725162137j:plain

各7セグメント表示用のデータは次の形のバイナリー形式です。

f:id:a-tomi:20200725162504j:plain

pで示したものはピリオドだけの場合もコロンだけの場合もありますが、両方表示できても予めハードウェア回路で固定され、変更できないものが殆どです。ちなみにピリオドだけのは次のもの。写真がちょっとぼけていてすみませんが。

f:id:a-tomi:20200725162718j:plain

Arduinoでプログラムを組むときは、今ではTM1637用ライブラリー(例えばTM1637Display)が数種類揃っています。しかし、ライブラリーの使い方を確認するのが面倒なら、必要な機能だけを自分でコーディングしてもこのデバイスなら比較的単純にできます。例えば次です。

/* *********************************************************
   Simple roll-your-own TM1637 operations without library
      Sample sketch V00  July 25, 2020 (c) Akira Tominaga 
 *  ********************************************************/
#define lDIO 6              	// DIO for TM1637 LED
#define lCLK 7              	// CLK for TM1637 LED
#define lBrt 0x02           	// duty-r 1/16x 02:4,03:10,05:12,07:14
#define lTu 50              	// time unit in micro-second
byte lChr;                  	// single byte sent to LED
byte Data[] = { 0x00, 0x00, 0x00, 0x00 }; // LED initial value 0000

void setup() { // ***** Arduino setup *****
  pinMode(lCLK, OUTPUT);
  pinMode(lDIO, OUTPUT);
  digitalWrite(lCLK, HIGH);
  digitalWrite(lDIO, HIGH);
  lDispData();              	// display 0000
  delay(1000);
}

void loop() { // ****** Arduino Loop *****
  // example 
  for (int k = 0; k < 4; k++) {
    uint8_t Rand = random(0, 15);
    Data[k] = Rand;
  }
  lDispData();
  delay(1000);
}

/**************************************
      User defined functions
* *********************************** */
// *** lDispData *** display data to 4dig LED
void lDispData(void) {
#define showDat 0x40          	// show this is data
#define showAd0 0xC0          	// LED data addr is zero
#define showDcB 0x88+lBrt     	// show dCtl + brightness
  lStart();                   	// start signal
  lChr = showDat;             	// identifier for data
  lSend();                    	// send it
  lStop();                    	// stop signal
  lStart();                   	// and restart
  lChr = showAd0;             	// identifier for address
  lSend();                    	// send it
  for (int j = 0; j < 4; j++) { // for Data[0] to Data[3]
    byte edChr = Data[j];     	// set a byte to edChr for edit
    switch (edChr) { 
      case 0x00: lChr = 0x3F; break; // 0
      case 0x01: lChr = 0x06; break; // 1
      case 0x02: lChr = 0x5B; break; // 2
      case 0x03: lChr = 0x4F; break; // 3
      case 0x04: lChr = 0x66; break; // 4
      case 0x05: lChr = 0x6D; break; // 5
      case 0x06: lChr = 0x7D; break; // 6
      case 0x07: lChr = 0x07; break; // 7
      case 0x08: lChr = 0x7F; break; // 8
      case 0x09: lChr = 0x6F; break; // 9
      case 0x0A: lChr = 0x77; break; // A
      case 0x0B: lChr = 0x7C; break; // b
      case 0x0C: lChr = 0x39; break; // C
      case 0x0D: lChr = 0x5E; break; // d
      case 0x0E: lChr = 0x79; break; // E
      case 0x0F: lChr = 0x71; break; // F
      case 0x10: lChr = 0x00; break; // blank
      default:   lChr = 0x7B;        // e for error
    }
    lSend();                    // send each byte continuously
  }                             // end of four bytes
  lStop();                      // stop signal
  lStart();                     // restart
  lChr = showDcB;               // identifier for display brightness
  lSend();                      // send it
  lStop();                      // stop signal
}

// *** lSend *** send a charater in lChr
void lSend(void) {  
#define LSb B00000001           // Least Significant bit
  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((lChr & LSb) == LSb) {  // if one then
      digitalWrite(lDIO, HIGH); // set lDIO high
    } else {                    // else
      digitalWrite(lDIO, LOW);  // set lDIO low
    }
    lCLKon();                   // clock on to show lDIO
    lChr = lChr >> 1;           // shift bits to right
  }
  digitalWrite(lDIO, LOW);      // pull down during Ack
  lCLKon();                     // clock on to simulate reading
}

// *** lStart *** send start signal 
void lStart(void) {
  digitalWrite(lDIO, LOW);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** lStop *** send stop signal
void lStop(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lDIO, HIGH);
  delayMicroseconds(lTu);
}

// *** lCLKon ** clock on to show data
void lCLKon(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}
/* End of program */

 上では表示するコードを7セグに変えるためswitch-case-defaultのロジックで行っていますが、固定byte列をコード値でインデクシングしてもよいでしょうね。その場合は、インデクシング前に0x0Fでandをとるなどしてコード値を制限しないといけませんね。

こういう勝手で単純な”Roll-Your-Own"と呼ばれるプログラムは、ライブラリーの使い方を調べなくてよいのと、マイコン容量消費の最小化ができる利点があるかも^^;  上のプログラムは、pのセグメントを使う場合には適当に加工して応用いただくと良いかと思います。

この場合、CLKとDIOは次の図のように動かしています。下は0000を表示している時のロジックアナライザー表示です。簡便法としてI2Cとして分析していますが、I2Cとは逆に、TM1637での約束は各バイトを下位ビット(つまりLSb)から順に送るので、各バイトごとにビットの並びが逆転しているので注意がいります。またスレーブアドレスなどはありません。

 

f:id:a-tomi:20200725163834j:plain

TM1637側は受信バイトに対してAck以外の応答をしませんから、DIOピンのモードをいちいち送信から受信に切り替えなくとも、早めのタイミングで自分でLOWにすれば送信モードのまま変えずにいけます。もしLOWにするタイミングが遅すぎて、スレーブが先にLOWを出す場合は正負で引きあうので注意。もし仮にそういう間違いをしても数マイクロセカンドだけですが心配ならDIOへの接続に数百オームの抵抗をいれれば安心でしょう。上のコーディングの場合は不要です。

 

その他もろもろの情報が必要な場合、TM1637のDatasheetをググってご確認いただくと良いと思います。

 

今回は超簡単な解説記事でしたが、このへんまでにしたいと思います。TM1637をお知りになりたい方々へ、少しでもお役に立つなら幸いです。

 

©2020 Akira Tominaga, All rights reserved.