ESP32のアクセスポイント (つまりスタンドアローンWiFi-LAN)
この週末にESP32で2つの実験をしたので、簡単にまとめます。プログラムは記事の後ろのほうにつけておきます。
1つ目はスタンドアロンーンのWeb.サーバー(つまり既存のWiFi LANを使わないアクセスポイント)。2つ目は勝手な「リモート・サーボ」をESPとステッパーで作ってみたので紹介。
この記事は、まず1つ目の独立WiFiサーバーです。ありきたりのようにも見えますが、これが便利だと思う勝手な理由は次の3つ。
①ふつうは立ち上げ時にメインのWiFi-LANにつないで、その中のIPアドレスとして動かすのですが、場合によりいちいちMACアドレスフィルタリングを通さないといけません。その手間がたいへん(MACアドレスフィルタリングを使ってない場合は手間はないですがSecurity心配ですね)。
②AlexaやGoogleHomeなどがたくさん加わり、WiFiルーターによっては数の限界に近づいています。そして電子工作を楽しむ人にとっては、とにかくESPマイコンの最近の低価格化はありがたい事でESP8266Development Boardに至っては、海外ネットで今や200円台で買えますから、何にでも使うわけでどんどん増えるわけ^^;
③既存のWiFi-LANの中で動かすと、場所を変える場合には、最初の接続SSIDの変更または追加を要する。スタンドアローンのアクセスポイントにすれば、その手間がいらない、つまりプログラムをいじらなくてよい。
ここで使うESP32DevelopmentBoardは次の38ピンのもので、次のように結線。
電源はUSBからの5Vでよいですが、自由に持ち回るときは上のように電池をつなぎます。最近のESP development Boardは比較的消費電流が小さくなりました(とはいえ実行中は100mA程度)。
ブレッドボードは、普通の1列5ピンのは使いにくいので6ピン配置のもの。これは秋月でも売っています。
とはいっても、実はどんなブレッドボードでも工夫すれば自由に使えます。そのことは次回の記事で書くことにします。
なお、Arduino-IDEでESP32ボードを開発するためには、ESP32用のボードマネジャーをArduino IDEへインストールする必要があります。導入法はネットの色々なところに出ていますが、Windowsでの現時点の入れ方を簡単に書いておきます。
====================
・ESP32をArduino-IDEで開発するためのソフト導入
Arduino IDE自体は次のアドレスから導入します。https://www.arduino.cc/
そしてIDEを開いたら、上のメニューバーにある「ファイル」→「環境設定」をクリックします。
環境設定画面の下のほうにある「追加のボードマネジャのURL欄」にボードマネジャーのあるURLアドレスを入力しますが、ESP32の場合は次を入力。
https://dl.espressif.com/dl/package_esp32_index.json
次に、IDE本体のメニューバーで「ツール」→「ボード」→「ボードマネジャ」と進むと使えるボードマネジャーのメニューが表示されるので、その中から
esp32by Espressif Systemsと表示されている欄内の右のほうにある「インストール」をクリックすれば、導入が始まり、しばらくすると終わります。
====================
IDEで後述するプログラムを作って、「ツール」で「ESP32 Dev Module」を選んで書き込む際に、画面の下の欄に赤色で書き込み開始の合図「・・・___」が始まったらESP32開発ボード上でUSB端子の隣にあるBootボタンを押すと書き込まれます。Arduinoと違って書き込み量が多いので少し時間がかかります。
そうすると「勝手なWiFi-LANサーバー」が立ち上がり、勝手に決めたアドレスが独立のアクセスポイントとなります。PCやスマホなどでこれを選んで(PWはもちろん入れて)接続します。もちろんPCやスマホでなく、工作用には他のESPからでもよいわけですが、、(あたりまえか)。
ここでのプログラム(この記事の後ろのほうに載せます)ではIPアドレス192.168.32.2を指定しています。PCやスマホならインターネットブラウザーでそこをアクセスすると次の画面。PCでも同じです。簡単な実験のためにテキストだけのページですが。
そしてリンクをタッチすれば指定LEDが点きます。
この実験装置全体の様子は次です。下の写真ではサーバーの電源をUSBで供給しています。
このサーバーの電源としては、前述のように電池(単三電池を3本直列)をつかっても問題ありません。そうすれば動くものやロボカーなどにも便利です。
さて、こうやって工作室に勝手なWiFi-LANをどんどん作るとどうなるか?
PCやスマホにSSIDがじゃんじゃん表示されますし、運用場所によっては近所からも丸見えですからあまり変なSSID名を使わないのがいいですね^^;
最後にこの実験用に作ったプログラムをつけておきます。
/********************************************************
* Independent Access-Point - ESP32 simple example
* Initial version V.00 Sept.26 2020
* by Akira Tominaga
********************************************************/
#include "WiFi.h"
const char ssid[] = "HOGEHOGE-00X"; // *** set any ssid ***
const char pass[] = "hoge00xpw"; // *** set any pw ***
const IPAddress ip(192, 168, 32, 2); // *** set any addr ***
const IPAddress subnet(255, 255, 255, 0);
WiFiServer server(80);
// ESP32 GPIO pin definitions (should not be #define)
gpio_num_t Red = GPIO_NUM_21; // Red LED
gpio_num_t Green = GPIO_NUM_22; // Green LED
gpio_num_t Blue = GPIO_NUM_23; // Blue LED
void setup() { // ***** Arduino (ESP32) Setup *****
Serial.begin(9600);
pinMode(Red, OUTPUT); // Red pin as output
digitalWrite(Red, LOW);
pinMode(Green, OUTPUT); // Green pin as output
digitalWrite(Green, LOW);
pinMode(Blue, OUTPUT); // Blue pin as output
digitalWrite(Blue, LOW);
delay(100);
WiFi.softAP(ssid, pass);
delay(100);
WiFi.softAPConfig(ip, ip, subnet);
IPAddress myIP = WiFi.softAPIP();
server.begin();
Serial.print("SSID= ");
Serial.println(ssid);
Serial.print("Fixed IP addr= ");
Serial.println(myIP);
Serial.println("Server starting!");
}
void loop() { // ***** Arduino (ESP32) Loop *****
WiFiClient client = server.available();
if (client) { // if accessed
Serial.println("Accessed");
String inMsg = "";
while (client.connected()) { // loop while client connected
if (client.available()) { // if a message,
char c = client.read(); // read each byte, and
Serial.write(c); // write to Serial
if (c == '\n') { // if LF and
if (inMsg.length() == 0) { // if new, send response
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println(); // send NewLine
client.print("
- Touch to switch LED on/off -
");
client.print("
- Switch Red-on
- Switch Green-on
- "); client.print("
- Switch Blue-on
- "); client.print("
- "); client.print("
- Switch Off
");
client.println(); // send another NewLine
break; // go out of while loop
} else { // if not new
inMsg = ""; // initialize inMsg
}
} else if (c != '\r') { // when not (c == '\n') ,too
inMsg += c; // add to inMsg
}
if (inMsg.endsWith("GET /R")) {
digitalWrite(Red, HIGH); // Red on
}
if (inMsg.endsWith("GET /G")) {
digitalWrite(Green, HIGH); // Green on
}
if (inMsg.endsWith("GET /B")) {
digitalWrite(Blue, HIGH); // Blue on
}
if (inMsg.endsWith("GET /F")) {
digitalWrite(Red, LOW); // Red off
digitalWrite(Green, LOW); // Green off
digitalWrite(Blue, LOW); // Blue off
}
} // end of (client.available())
} // end of (client.connected())
client.stop();
Serial.println("Client Disconnected.");
} // end of (client)
}
// end of program
上のプログラムはPreとCodeで挟みましたが、肝心のHTMLを送るところはソースが表示されず、結果が表示されてしまいますね。はてなブログでのタグの認識防止方法がまだしらべきれてないので、当面はそこの行以下最後までを次に直接かいておきます^^; 良い方法がわかった時点で直しますが、たいへんすみません<(_ _)>
if (c == '\n') { // if LF and
if (inMsg.length() == 0) { // if new, send response
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println(); // send NewLine
client.print("<font size=15 clolor=black><br>- Touch to switch LED on/off -<br>");
client.print("<ul><li>Switch <a href=\"/R\"><font color=red>Red-on</font></a></li><br>");
client.print("<li>Switch <a href=\"/G\"><font color=green>Green-on</font></a></li><br>");
client.print("<li>Switch <a href=\"/B\"><font color=blue>Blue-on</font></a></li><br>");
client.print("<li>Switch <a href=\"/F\"><font color=black>Off</font></a></li></ul></font>");
client.println(); // send another NewLine
break; // go out of while loop
} else { // if not new
inMsg = ""; // initialize inMsg
}
} else if (c != '\r') { // when not (c == '\n') ,too
inMsg += c; // add to inMsg
}
if (inMsg.endsWith("GET /R")) {
digitalWrite(Red, HIGH); // Red on
}
if (inMsg.endsWith("GET /G")) {
digitalWrite(Green, HIGH); // Green on
}
if (inMsg.endsWith("GET /B")) {
digitalWrite(Blue, HIGH); // Blue on
}
if (inMsg.endsWith("GET /F")) {
digitalWrite(Red, LOW); // Red off
digitalWrite(Green, LOW); // Green off
digitalWrite(Blue, LOW); // Blue off
}
} // end of (client.available())
} // end of (client.connected())
client.stop();
Serial.println("Client Disconnected.");
} // end of (client)
}
// end of program
この記事は、自分のまとめのために書きましたが、もしもお役に立つことがあれば幸いです。
次回はESPとステッパーで作った「勝手なリモートサーボ」について、近日中に書きたいと思います。
©2020 Akira Tominaga, All rights reserved.
ガチャポンの回転ダイヤルをデジタル化
上の回転ダイヤルは、ガチャポンから出てくる玩具で、なぜかこの春から突然人気となったようです。ネット通販で4つセットが売られていましたので5月末に予約して最近ようやく手に入れました^^;
小さいですが精巧によくできていて、ダイヤルが戻る感触は抜群です、なるほど・・・人気に納得できそう。
説明書には対象年齢がまさかの「15歳以上」と書いてありますが、こんな電話機はひょっとすると50歳以上しか使ったことがないかもしれませんね。
単に回すだけで楽しむ玩具ですが、キーボードに飽きたらこれを使ったデジタル入力も面白そう。
裏蓋を開けてみると中にはゼンマイ仕掛けのごく小さなギアボックスが入っているだけです。それでもご丁寧にちゃんとネジ2本を使って正確に蓋をしてあります。これならデジタル化できないものかと、ふと考えついてしまったのが事の始まり^^;
ギアボックス自体は開かないし、軸のでっぱりなど、余計なものもありません。もしそういうのがあれば磁石を仕込むとか、何らかのしかけができそyですが、一見取りつくしまがない感じ。こうなるとますます挑戦したくなりますね^^
しばらくは考えるだけでしたが、この4連休週末がきたので、ついにやってみることにしました。
気付いた中で一番かんたんそうな解き方:ダイヤルが戻る際の音の時間を測定してはどうかな!
戻る時間を測ってみた結果、回転量で安定して決まるようです。たぶんぜんまいギアの油による定速と思われます。それならきちんとマイクで音を拾って詳しく調べてみることに。
というわけで小さなコンデンサマイク・モジュールを仕込むことにしました。
1ドルで数個買えるモジュールですがゲインの調整もできるので一応使えそう。やってみると中になぜかぴったりと収まり、蓋をするとちょうどうまく固定されます。
よけいな飾りの鎖部分はカットして、その付近にそれらしくケーブルを通しました。
表から見るとサマになってますね(・・?
ダイヤルをしながらオシロスコープで測定します。
マイクモジュールは若干のノイズ(というより明らかに一定の寄生発振)を出していますが、この用途には問題ないですね。この際音のゲインが振り切るように調整しました。ダイヤル5を回すと次の出力になります。
オフセットは2.5V(電源電圧の丁度半分)で、ダイヤル音で0ボルトから電源電圧(5V)まで振り切るようにゲインを調整しました。
これでダイヤルごとの戻り時間を調べた結果、つぎのようになっています。
式に従って計算をし、次の判定表を作りました。
とはいえ、音の出ている間の波形は次のように乱れていますから、ダイヤルの識別には少し工夫が要ります。
絶対値のオンオフだけで識別するわけにはいきません。しかし1回の測定に数ミリセカンドかかっても問題ないわけなので、多数回のサンプリングをして「オフセットからのずれ幅が一定割合以上起きている場合にダイヤルが戻り中」と判定すればよいと考えつきました。
というわけで値をTM1637ィスプレイに表示するとして、次の回路にしました。ATmega328pで書いてありますが、Arduino-UnoでOKです。毎度きたない手書きのメモですみませんが、今回は回路を書くほどのものではありませんね。
実験した写真は次です。Arduinoのプログラムはこの記事の最後につけておきます。
これでテストすると、マイクを閉じ込めただけあって周囲の雑音はほぼ拾いません。玩具をガンガンたたいたりすると時々1が表示されます。そこで、短い信号を無視したり、定数を調整したり、ダイヤルがずれにくいように滑り止めマットを敷いたりして、無事にうまくできあがりました。
調整には思いのほか時間がかかりました^^;
ちなみに、このおもちゃはガチャポン(たぶん¥300)での購入用のようですが、次のネット販売で5月末に予約して9月に入手しました。1個250円程でした。
1パックに4個はいっています。どれも精巧ですばらしい!
というわけで、今回試験したArduinoスケッチをつけておきます。あまりきれいな書き方ではありませんが。
/* *********************************************************
Rotating dial to TM1637 (for Arduino demonstration)
Initial version V00 Sept. 19, 2020 (c) Akira Tominaga
* ********************************************************/
// for measurement
#define aPin 0 // A0 pin for analogRead
uint32_t onS = 0; // signal-on started time
uint32_t Now; // current time mS from start
uint8_t nSamp; // number of voltage samplings
#define maxSamp 40 // max number of samplings
uint8_t nOn; // counter of signal-on
#define thOn 10 // threshold to detect signal-on
#define Vcenter 511 // analog voltage center value
#define Vth 300 // threshold offset from center
uint32_t sigLen; // signal length in milli second
#define offSt 0 // last satus was off
#define onSt 1 // last satus was on
uint8_t lastSt = offSt; // last status code
uint16_t aVal; // analog read value
uint16_t vVal; // offset = | Vcenter - aVal |
// each dial max duration milli seconds
#define d1max 417
#define d2max 546
#define d3max 674
#define d4max 802
#define d5max 931
#define d6max 1059
#define d7max 1188
#define d8max 1316
#define d9max 1445
#define d0max 1650
#define d1min 340
#define dAdj 27 // adjusting lenght for handling
// for led TM1637
#define lDIO 6 // DIO for TM1637 LED
#define lCLK 7 // CLK for TM1637 LED
#define lBrt 0x03 // 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[] = { 0x08, 0x08, 0x08, 0x08 }; // LED init 8888
void setup() { // ***** Arduino setup *****
pinMode(lCLK, OUTPUT);
pinMode(lDIO, OUTPUT);
digitalWrite(lCLK, HIGH);
digitalWrite(lDIO, HIGH);
Serial.begin(9600);
lDispData(); // display 8888
delay(1000);
for (uint8_t i = 0; i < 4; i++) {
Data[i] = 0x10; // display blank
}
lDispData();
delay(1000);
Serial.println("Starting");
/*****************************************
Measure voltage to find signal
*****************************************/
mainLp:
nOn = 0; // set sig counter zero
for (nSamp = 0; nSamp < maxSamp; nSamp++) {
aVal = analogRead(aPin);
vVal = Vcenter - aVal;
if (Vcenter < aVal) { // get abs offset vVal
vVal = aVal - Vcenter;
}
if (vVal > Vth) {
nOn++;
}
}
Now = millis();
if (nOn >= thOn) {
goto onRtn;
}
// goto offRtn;
offRtn:
if (lastSt == offSt) {
goto mainLp;
}
sigLen = Now - onS;
if (sigLen < (d1min+dAdj)) goto mainLp; // if short, noise
lastSt = offSt;
onS = 0;
// valid on length here
//Serial.println(sigLen);
if (sigLen <= (d1max+dAdj)) {
dispDial(0x01);
goto mainLp;
}
if (sigLen <= (d2max+dAdj)) {
dispDial(0x02);
goto mainLp;
}
if (sigLen <= (d3max+dAdj)) {
dispDial(0x03);
goto mainLp;
}
if (sigLen <= (d4max+dAdj)) {
dispDial(0x04);
goto mainLp;
}
if (sigLen <= (d5max+dAdj)) {
dispDial(0x05);
goto mainLp;
}
if (sigLen <= (d6max+dAdj)) {
dispDial(0x06);
goto mainLp;
}
if (sigLen <= (d7max+dAdj)) {
dispDial(0x07);
goto mainLp;
}
if (sigLen <= (d8max+dAdj)) {
dispDial(0x08);
goto mainLp;
}
if (sigLen <= (d9max+dAdj)) {
dispDial(0x09);
goto mainLp;
}
if (sigLen <= (d0max+dAdj)) {
dispDial(0x00);
goto mainLp;
}
dispDial(0x11); // for others, too long signal "L"
goto mainLp;
/*****************************************
Signal on case
*****************************************/
onRtn:
if (lastSt == offSt) {
onS = Now;
}
lastSt = onSt;
goto mainLp;
} // end of Setup
void loop() { // ****** Arduino Loop *****
// never comes here
}
/**************************************
User defined functions
* *********************************** */
// *** dispDial(#) *** display dial number ***
void dispDial(byte dN) {
for (int k = 0; k < 3; k++) {
Data[k] = Data[k + 1]; // shift Data up
}
Data[3] = dN; // set dial number to digit 3
lDispData();
}
// *** lDispData *** display data to 4dig LED TM1637
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 0x10: lChr = 0x00; break; // blank
case 0x11: lChr = 0x38; break; // L for too large value
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 ** clock on to show data to TM1637
void lCLKon(void) {
digitalWrite(lCLK, HIGH);
delayMicroseconds(lTu);
digitalWrite(lCLK, LOW);
delayMicroseconds(lTu);
}
/* End of program */
何とかできることが分かったので、同じ処理を小さなPICなどに組んでケース内に収め、本物の回転ダイヤルと同じように数値パルスを出せば完璧な模型となりますね^^
では今回はこのへんで。
©2020 Akira Tominaga, All rights reserved.
型取りゲージの電子化?(ToFで実験)
これは「型取りゲージ」の写真です。複雑な形に合わせて床材やカーペット等をカットするための巧妙なゲージで、型取り定規と言われることもあります。英語ではProfie GaugeとかContour Gaugeとか。
もしこの道具を使わずに、例えば次のような複雑断面の柱に合わせてカーペットを切ろうとすると大変なことになります^^;
1mmφのピアノ線のエレメントが自由にズレて型をとる仕掛けで、これを使えば切断材料に相手の形をなぞり書きすることができます。それなりの精度とスムーズな動きが必要なために安価な道具ではありません。
しかし最近では安価なABS樹脂製(中国製)が出回っているようです。少し目が粗い(次の写真のものはエレメントが1.5ミリ幅)とはいっても、材料の革新だなあと勝手に感心します。型をとった状態でロックするメカニズムもなかなか素晴らしい!
とはいうものの、現代のことですからデジタル化などできないの?と、ふと思いついてしまいました^^;
仕組みはいくつか考えられますが、手始めにToFセンサー(Time of flight 光が反射して戻るまでの時間で距離を測るセンサー)を使うとどうなるでしょうか?できるような気がしますので、まずはやってみることにします。
ToFセンサーはカメラで近距離を測る用途などにも使われるようになり、ずいぶん安価になりました。たとえば次のToFセンサーVL6180Xのブレークアウトモジュールは海外ネットでは約2ドル強で入手できます。
真ん中にあるデバイスがVL6180X 本体で、赤外線ビームを出す部分と反射した赤外線を受ける部分があります。使われている赤外線の波長は850nmですので、もちろん目には全く見えません。
このセンサーを型取りゲージと同じ1ミリ単位で正確に水平移動させながら、反射対象までの距離を測っていくことで、同じことができるのではないかと考えました。実験用回路は、毎度手書きのきたないメモですみませんが、次のような簡単なものです。ステッパーの駆動にA4988というモジュールを使い、送りねじ機構で動かします。
プログラムではArduinoから測定データをPCへ送って、PC側でリアルタイムのグラフ表示などをしています。Arduino側とPC側の両方の実験プログラムをこの記事の最後の方につけます、とても小さいですが。
実験装置全体は次のように作りました。
動かすセンサーを前から見たのが次の写真です。センサーにある丸い窓2つ(赤外線ビーム放出窓と反射光検出窓だろうと思います)が見えています。
スライドユニットにセンサーをとりつけるために、木片をつけそれにブレッドボードをとりつけてあります。大きさを合わせるため、次の写真のように上のボードを下のサイズへ強引にカットして加工^^
ToFセンサーが揺れないよう、ブレークアウトモジュールに足をしっかりつけます。穴のない側にもホットボンドで無理に台と足2ピンだけをつけます。
表から見ると次。2つのネジ穴にホットボンドとピンの端が見えています。
まずはこういう紙型をいじりながら試験。暗い時のほうが結果は良いかと思いますが、かなり明るい状態でも特に問題なさそう。
まずは次の型取りができました。なんとなくうまくできてる!かな?
それでもよくみると、対象への距離は設定どおりで正確ですが、形が実際より滑らかです。変ですね。
測ったときの断面は下から見るとこれです。不細工ながら、円弧、台形、三角形にしたつもり。
手でほぼ正確に測った緑の破線と重ね合わせると次のようになっています。
台形は丸身を帯び、三角形も丸身を帯びてます(・・? なぜ? ん?
原因を調べるため、もっと厳しい直方体の木材や柱を立ててはかってみましたら、ありゃ? 少し差がでるという状況ではありませんね、これは! とくに形の間の深いところはひどく浅くなっています、、、
原因をあれこれ考え、ToFの赤外線は勝手に想定していた細いビームではない?!と勘づきます。あちこちで反射している可能性がありそう。
850ナノメーターの赤外線放出の様子を見るのは難しそうですが、この際は勝手な赤外線カメラで観察することにします。そのためにセンサーだけをつけた単体をこしらえて、これでじっくり観察します。
使う赤外線カメラは、古いデジカメの撮像面にセットされていたIRカットフィルター(赤外線を遮蔽し可視光だけを通すフィルター)を外して(というよりピンセットで無理にはぎ取って)、代わりにローパスフィルター(可視光を通さず波長780ナノメーター以上の赤外線だけ通すもの)をとり付けたまさしく勝手な赤外線カメラです。
これにより、センサーから出ている赤外線はビームの形どころか、びっくりする程広がっているのがわかりました。
これは想定外です!
ひょっとしてこのToFセンサー固有のことなのかなと微かな希望をもって、他のセンサーも調べてみることに。手元にある別のToFセンサーはVL50L0Xで、そちらでも観察することにしました。
こちらはVL6180X よりも数年前の製品です。
同じ方法で観察したビーム形状が次です。
赤外線が弱いようですが、やはり細いビームではなくこちらも光芒がずいぶん広がっています。
他の製品についてもよく調べる必要がありますが、センサーの精度を高める目的から言えば細いビームのもあってよいかな?と考えゆっくり調べることにします。あるとしてもこの目的には高価ではないかな?もしよいのがない場合は、センサーの前に広がりを絞る反射しにくい筒でもつけるのがよいかな?などともあれこれ考え中。
というわけで、すぐできると思ったこの実験、すこしずつ長引きそうです。精度の高いのが完成するまで待つといつになるかわからないので、いったんご紹介することにしました。どなたか良い案を考えたら是非デジタル化してくださいね^^;
最後に、実験に使ったかんたんな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の大きさで、顕微鏡を低倍率にして見ると次のような形。
顕微鏡のIR遮蔽が弱いので850nmの平面Laser光が写っています。上の穴が反射光の検知窓で、穴の間はおよそ3.5mm離れています。ですので、ここに何か細工ができないかなあと考えました。
最初にレンズで集光してみることに。次の状態は何もしない場合の赤外線写真です。
この出力窓の前に樹脂製のレンズをおいてみると、だいぶ収束するのがわかります。
この写真では高さの位置を出力窓にあわせるために、レンズの下にティッシュペーパーを敷いています。この状態でレンズを前後させるとさらに収束できる位置があるようです。赤外線カメラで見て一番良い位置を確認したのですが写真を撮りそこないました^^;
とはいえ直径が10mmほどのレンズのため、反射が受光窓にもかぶるのでもっとずっと小さいレンズでないといけません。プラスチックレンズは頑張れば切れるかも^^; モノはためし、幅2.5mmほどの凸レンズを作ってみました。
こういう形でも受光窓を避けるにはなんとかなりそうな感じがします。
これは度の強いメガネ(リーディンググラス、f=16.7cm)から、愛用のProxxon「サーキュラーソー」で下の写真のようにエイヤッと切りとったもの。
ひどいことをしますが、ヒンジが壊れて不要になった百均のグラスなのでご安心を^^;
これで早速やってみると、測定結果の距離が極端に小さくでます。何のことはない、出力窓から少し離しただけで、レンズの反射がどうやっても受光窓にはいります。レンズを出力窓にほぼくっつけないとだめそう。焦点距離の相当短いものを使わないと無理そうなのでレンズでの収束はとりあえず断念。
それなら、次はパイプで広がらなくする案。外径2.5mm、穴径1.5mmのアルミパイプを切り出して、木片を支えにしてやってみました。光が漏れないように出力窓にピッタリ付けます。
赤外線のカメラで見るとパイプによる集光はバッチリだったのですが。。。測定値が不安定。どうやら測定対象からの反射光がパイプをセットした台で反射しているようです。なぜ短く計測されることがあるのかはわかりませんが、ひょっとして単に往復時間だけでなく位相なども検知してるのかなあ、う~ん ;;
どっちにしてもパイプが使えるかを確かめるには、パイプ自体の支持方法を工夫しないといけない感じです・・ICに接着してしまえばいいかな?それも気が引ける・・
そこでふと気づいたのですが、スリットで広がりを防ぐのはどうだろう?左右だけ広がらなければよいわけなので・・。
しかし、どっこい。そうはいきませんでした。左右の壁で反射して短く検出されてしまいます。それなら反射を防ぐ紙を貼るとどうなるか・・。下のように広げてみてもやっぱり両脇での反射が検出され、短くでます。
木製の台が光線の近くにあること自体もまずいかも。
ここまででのところ、測距対象が近くにあるときは正しい距離が得られる場合もありますが、対象が離れると測定値が異常に小さくなるのに気づきました。反射光の強度が小さい場合(測定対象からの反射光が弱いと)センサーは次第にゲインを上げて近くの弱い反射を検出するようにみえます。ゲインは40倍までコントロールできる仕様です。測り方の指定にもよるでしょうが。
他の用途ではどうしているのか?自動掃除機などのLidarに使われているToFセンサーでも光線の性格は同じで、かなり広がっています。ロボットや掃除機が壁にぶつからないようにするためには、それでもとくに問題ないわけです・・・。ポピュラーなこの種のToFセンサーでは正面の距離は正確に測れても、回転時の角度にまつわる分解能は高くはないということでしょうか。
というわけで、まだ光線が絞れてません。もし別の知恵が出たらこの続きをやろうと思います。ここまでおつきあいいただきありがとうございます (o*。_。)o tks
©2020 Akira Tominaga, All rights reserved.
マイコンとPCのデータ授受ーSmallbasicなら簡単 (2020.12.05 LiquidCrystal_I2Cについて補足しました)
マイコンのデータを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)を次からインストールします。
LitDevのZipを解凍してすべてを次のフォルダーに入れます。C:\Program Files (x86)\Microsoft\Small Basic\Lib
これで後はプログラミングが自由自在です。めったにバージョンアップがないのは却ってありがたいことです^^
Small Basicの使い方は、非常にわかりやすい日本語サイトも沢山でていますので必要に応じてご参照下さい。LitDevで使える多数の関数については上のサイトで、英語ではありますがわかりやすく説明されています。ここではそのなかのLDCommPortだけを使いますが、他に便利なAPIは多数あります。LitDevは、SmallBasicの開発環境で言語そのものに交じって同じに使えます。
まずは、ArduinoからPCのSmall Basicへ送る場合の例です。
次がこの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で液晶に次々に表示する例です。
この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に、この送受の内容が次のように順次表示されます。
次がこれを受けて表示する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モジュール裏の右上にはんだ付けされているのがその変換モジュールです。
その便利な変換ICは同じでもボードには複数種があるため、LCDへのピン接続の明示的な指定の必要性があるわけです。ピン接続部を表から見ればどれでも、元祖の1602LCDモジュール(HD44780)と同じで海外製なのにカタカナを含む中身のフォントも全く同じものが多いです。
ところが、同じに見えても4ビット接続の場合は8ビット接続と異なり、データの授受にD0~D3を使わないためI2Cプロトコルは煩雑になります。
このため、現時点ではArduinooのLiquidCrystal_I2Cライブラリーをスーパーセットである次のものへ入れ替え、他のLiquidCrystal_I2Cライブラリーを削除することで問題なく使えるようになっています。16桁2行でも20桁4行でも上のスケッチ例のように指定できます。
名称が重なる他のLiquidCrystal_I2Cライブラリーは以後使わないかと思います。万一必要になれば、そのときに元のを特定スケッチのフォルダーに改めて再導入すればよいかと思われますし、もし心配なら上記を入れる前に古いのをどこか別フォルダーにバックアップしておくとか(参照先の名前が重なるのを防ぐために)。
今やOLEDが多く使われるようになりましたが、そちらはメモリーの必要量が多いためこういう安価で楽なI2C接続LCDはまだまだ長く使われるのではないかと思います。
以上、ご自分がお使いになる同様な液晶文字ディスプレイについてご判断の上、自己責任にてお願いしますね。
©2020 Akira Tominaga, All rights reserved.
回路不要のかんたんなRTCライター:Arduinoで (2022.11.22更新)
RTC(リアルタイムクロック)は今やとても正確で、1年間の最大の狂いが秒単位のものがあります。上のArduinoにつないだRTCは、なかでも正確なDS3231というICのブレークアウトモジュールです。ファームウェアで短い間隔で温度補正をする工夫がなされたコストパフォーマンスの優れたRTCです。
このモジュールは、比較的容量の大きいCR2032電池を使い、大体5年ほどはそのまま動きます。小さな電池を入れる型であればもちろんもっと短いですが。
この精密なクロックが海外ネットでは1ドル未満で買えるので驚きです。今はコロナの影響でなんでも少し高いですが、今ちらりとAliexpressをみると、やはりそんな廉価です。中には、ばか高い同じものもあるので、気を付けないといけませんが。
(2022.11.22追記:ここ数か月で急に高くなっていますが、米中貿易摩擦の関係でなければいいのですが・・。)
何かの自作にRTCを使うためには、普通はカレンダーや時刻の設定機能を作りこみます。そのため少なくとも3つ(Mode、 Time、Set)のボタンかタクトスイッチが要ります。
しかし、RTCがこれだけ正確だと、自作機器では読み込むだけにしたい感じもします。必要に応じて外して更新したり、あるいはRTC更新の際は外部からの入力に差し替えるなどすればよいわけなので。(ブレークアウトモジュールではなく、DS323xのICを直接組みこむ場合はもちろんそうはいきませんね。ただし大量ではなく趣味用の少量購入ならブレークアウトモジュールが圧倒的に廉価。)
読み込むだけにする場合は、別途RTCへの書き込み装置を必要とするわけですが、それをArduinoで回路なしに楽に行う勝手な方法をご紹介したいと思います。
これだけのことなら最近の標準ライブラリーを使っても良いかもしれませんが、一定間隔の割り込みなどを使いたいときは、次のようなI2Cへの直接コーディングなら自由度が高くて楽です。
電池を入れて最初の写真のように接続し、まずArduinoにIDEで次のプログラムを書き込みます。(更新: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の右上あたりにある虫眼鏡のマークをクリックします。
次にシリアルモニターで、操作を次の様に行います。
詳細は次のとおりです。
①現在の数値を更新しないでよいかを聞いてきます。更新する場合は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):換気は大事!
前にブレッドボードでの試作を紹介したNDIR(赤外線吸収型)CO2ロガーを組み立て、室内の測定等色々やってみたのでご紹介します。
前の記事はこれです:テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型 - 勝手な電子工作・・
今回のセンサー(MH-Z19b)は、改善の歴史の長いNDIR型です。さすがにキャリブレーションもしやすく安定し、ロガーの用途には使いやすいことがわかりました。
次はこのロガーによる実際の記録結果です。
測定開始から安定まで数分ですが一応開始後約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時間、常に人がいる部屋では不都合であり、オフィスのように人がいない時間が毎日ある場所での測定に適しています。
このセンサーは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はセンサーからマイコンへの信号)。
これにはとまどいましたが調べていくと、単に、最新Arduinoライブラリーがこのセンサーの改善(バージョンアップ)に追い付いていないことがわかりました。
そして古い0x85や0xA2コマンドも互換性を維持するためにサポートされているものです。この際はライブラリーを使わずに躊躇なく^^;最新の仕様で手作りしたわけです。
なお、上に示したようなUART接続の信号トレースはArduino Unoで簡単にできます。必要な方は次の記事の最後の方をご参照ください。これを行ったArduinoスケッチをつけてあります:
ソフトウェアシリアルをコンパクトに作るーArduino編-そしてトレースに使う - 勝手な電子工作・・
以下に製作について、手短かに説明させていただきます。
まずは、あらためて回路図です。汚い手書きで毎度すみませんが。
今回は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ロガーではマイコンの容量を増やしたくないのでつけてないだけです。つけている一例は次。
そして次が基板のデザインで、108mmx78mmです。部品面から見た図なので銅箔面にするときは裏返し。この記事の後の方に書くケースに合わせてあります。(また、銅箔を残さないところを黒にしているので、もしエッチングする場合はネガポジを反転。)
私はこういう片面基板はCNCルーターで削って作るので、ソフトで銅箔面のツールパス図に変換します。
作った基板は次です。今回は初めて使う中国製の頼りないA4ベーク樹脂生基板でしたが、幸い問題なくできました。
まず、ジャンパー線5つを配線。
そして部品をはんだ付けして基板は完成。
半田付けはきたないですが^^ この基板はアルコールで拭いてもハンダが結構つきにくいので、コテの温度を少し上げました。
これでテストをして問題なし。そしてケースに組み込んでできあがり。ケースは秋月で買ったポリカーボネート製。深さに3種類ありますがこれは中間の深さのもの。とても丈夫で蓋が開くので、こういう用途に重宝です。
センサーの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編-そしてトレースに使う
シリアル通信を小さく作る例をArduinoの例で説明したいと思います。少し複雑な工作でも、ESPなどを持ち出さずにUNO(ATmega328P)などありきたりのマイコンで作る「生活の知恵」でもありますし、シリアル通信のアナライザーを作る知恵でもあります。この前後の記事でご紹介する二酸化炭素濃度ロガーなど、通信種類が多い場合などに役立ちます。
上の写真は左がArduino-UNO、右がコンパチ機、間に信号を観察するための小さなブレッドボードを挟んでいます。
そもそもIDE環境がArduinoとPCの接続にハードウェア・シリアル通信を使うため、それが1つしかないUNOの場合には、2つ目が必要なら「ソフトウェアシリアル」を使います。ところがその標準ライブラリーは資源を意外に多く消費します。そこで必要な、小容量で複数つなぐ方法としてここで説明したいと思います。
ご紹介する手作りソフトの受信ロジックは、デバイス間のシリアル通信を外部システムとして勝手に受信(つまりトレースやブロードキャスト通信も)できます。そのスケッチ例は、この記事の最後のほうにつけておきます(2020.8.13追加)。
雑談ですが、シリアル通信はUART通信、非同期通信、スタートストップ通信、調歩式、等々色んな名称がありますかね。物理的なリンクだけ見れば各デバイスの時計に頼り、同期をとらずにビットをやりとりします。現代から見れば乱暴^^; 世界でオンラインシステムが始まった時から使われ、日本では1964年東京オリンピックがその通信システムの始まり。同期式が使われだしたのはだいぶ後です。
それがマイコンで今でも広く使われているのが不思議ですが、デバイスは今やどれも正確なクォーツクを使い問題はほぼないわけです。フィリップスI2C通信の絶妙アーキテクチャには程遠いですが、単純な2線で便利です。次の写真のマイクロSDリーダーライターは、シリアル通信で動かす日本製モジュールで、小さなマイコンでもSDカード処理がらくちんでできてしまいます。
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)も多いのでまあ一理あるわけですが。
オシロではなく、ロジックアナライザーなら少しわかりやすいですが、時間計算する以外にタイミングの手がかりが少ないもの。そこで、勝手な電子工作の知恵では、分析のためにはU(0x55)という文字を含めるようにします。Uならオンオフのビットが交互になりますからタイミングが確実にわかりますね。次はUAUと打ち込み送信をロジックアナライザーで見たもの。
Sはスタートビットで、Pはストップビット。間のビットの並びを前後逆転すれば0x55、0x41、0x55、0x0Aと簡単に読めます。送信側ではUSBキーボードを打ち込んでハードウェアシリアルに渡されたものをDLE記号が来る前までそのままを送っていますから、最後に0x0AのLF(改行つまりエンターキー)がくっついてます。
ロジックアナライザーは次を使っていますが、今どきは中国製のコンパチモデルというよりも、形状を変えたコピー機が超廉価でずいぶん出回ってるようです。
ロジックアナライザーで使わないチャネルのプローブ先端はグランドに落としますから、次のようなアナライザー専用のブレッドボードを常備しておくと測定が手短かに行え、かつ、刺し間違いも減ります。
これは普通のブレッドボードの一部を強引に切り取ってヤスリをかけただけ。小さくて必要な穴が十分なので、場所を取らず迅速に使え、さしっぱなしで保管できます。
とはいえ、シリアル通信の信号解析が圧倒的に楽な方法は、プログラムで分析することです。さきほど紹介した受信側のロジックを使えば簡単です。
標準のソフトウェアシリアルは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 */
受信のタイミング信号を含めて記録したものは次です。
送る側のRYOプログラムを標準ソフトウェアシリアルの受信側で受けると同じ動きをしますし、逆の組みあわせ、つまり標準ソフトウェアシリアルの送り側を今回のRYOの受け側を組み合わせても、同じ動きをします。つまり他の相手ともつながるということが分ります。
ついでにビットの解析を書けば次のようになります。
以上のように、必要部分だけを勝手なRYOプログラムにすると、Arduinoの容量はずいぶん節約されて、UNOでは通常は難しいことも、余裕でできてしまいます。1例としては次の記事などをご参照ください。
テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型 - 勝手な電子工作・・
資源の減らすだけでなく、このようなRYOプログラムなら通信のトレースも簡単にできます。
次の例は、マイコンとデバイスの間のシリアル通信でのやりとりを、Arduinoのプログラムで外部から観察したものです。
プログラムは分析相手に合うように適当に加工すればよいものですが、ここで用いたスケッチは次のものです。
/* 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.