M5Stack 赤外線利用で時間計測
2024.7.28 Coskx Lab
1 はじめに
カートなどがスタートラインからスタートし,ゴールラインをカートが越えるまでの時間を測定することが目的です。
図のように赤外線の発光受光を行い,カートが移動するときに赤外光路を遮断することを利用し,スタートラインの赤外線光路遮断のはじめから,ゴールラインの赤外線光路遮断のはじめまでの時間を測定します。
使用するのは赤外LED(light emitting diode)と赤外線受光モジュールです。
2 使用環境
- Windows 10 64-bit
- Arduino IDE 2.3.2
- M5Stack Gray ESP32-D0WDQ6-V3 (revision 3)
MPU6886 + BMM150 16MB 40MHz
または M5Stack Basic V2.7
- 赤外LED OSI5FU5111C-40 2個
- 赤外線受光モジュール OSRB38C9AA 2個
3 赤外線受光モジュール
赤外線受光モジュールのデータシート
赤外線受光モジュールは,家庭電化製品(テレビなど)の赤外線リモコンに使用されているセンサです。
赤外線発光器以外からの赤外線を含む光に反応しないように,赤外線受光モジュールは38kHzで強度変化(ON-OFFでよい)のある赤外線(38kHz変調赤外線)に対して反応するようになっています。
(テレビなどのリモコンではリモコンボタンを押すと,38kHz変調赤外線の有無のパターンの一連の信号が送られます。)
データシート(赤外線受光モジュール OSRB38C9AA)の動作図を見ると,赤外線LED側は38kHz変調赤外線を発したり休止したりしています。38kHz変調赤外線を発している期間も,休止している期間もそれぞれ600μsになっています。
それに対応して,赤外線受光モジュールは38kHz変調赤外線を受光しているときはLを,38kHz変調赤外線を受光していないときはHを,モジュール出力にしています。(負論理)
しかも,100msecが1ブロックとなっていて,ブロックの最後はある長さの休止期間が必要となっています。
この休止区間(長めの非受光期間)はリモコンボタンの一連の信号パターンの区切りを表すと同時に,受光モジュールのAGC(Automatic Gain Control)に無信号レベルをセットするのに使われているようです。
データシート(赤外線受光モジュール OSRB38C9AA)にある動作図
赤外線受光モジュールのテスト
赤外線受光モジュール OSRB38C9AAで次のテストをしました。
(1) 赤外線LEDから600μsの38kHz変調赤外線発光と600μsの休止区間を連続して与え続けたところ,最初の0.1秒程度は赤外線受光モジュールが受光検知の信号を見せますが,その後,受光しているはずなのに非受光状態を示す信号を発するようになりました。
長めの休止区間(非受光期間)がないため,AGCのレベルセットが出来ず誤動作に至ったものと考えられます。
(2) 次の図に示すように,600μsの休止区間を挟んで,600μsの38kHz変調赤外線を2回与える動作を25msec周期で与える動作をしたところ,(約23msecの非受光区間を与えたことになります)光路に遮光物があるか無しかを正しくとらえることが出来るようになりました。
テストした25msec周期の駆動パターン(1周期に2回の受光区間)
テスト結果からの指針
(2)の方式では,連続して正常な動作を行いますが,25msec毎の遮光物検査しかできません。遮光物検査の時間分解能は25msecになります。
(1)で起動直後の短かな時間では正しく動作することを利用して,遮光物検査の度に赤外線受光モジュールに通電し,検査が終わったら通電をやめる方式では,5msec毎の遮光物検査が出来ることがわかりました。
この方式では,遮光物検査の度に赤外線受光モジュールに通電し,1500μs待ってから,600μsの短い休止区間を挟んで,赤外線LEDから600μsの38kHz変調赤外線を2回与える動作になります。(2回の検査で信頼性を上げます。)また,遮光物検査の時間分解能は5msecになります。
こちらの方式を採用することにしました。
4 M5stackピン割り当てと回路
M5stackピン割り当て
M5Stackは,
(1)赤外LEDの点滅のために38kHz変調赤外線生成
(2)赤外線受光モジュールへ通電コントロール
(3)赤外線受光モジュールからの信号を受け取り
を行います。
赤外LEDの点滅のためにPWM出力できる出力ピンとして,Start側にはデジタル出力ピンGPIO2を,Goal側にはデジタル出力ピンGPIO5を割り当てます。
赤外線受光モジュールの仕様は電源供給は3.3V,Max1.5mAになっています。
そのため赤外線受光モジュールの電源供給にはGPIOの出力をそのまま使ってコントロールします。Start側にはデジタル出力ピンGPIO16を,Goal側にはデジタル出力ピンGPIO17を割り当てます。
赤外線受光モジュールからの信号は論理信号なので,Start側にはデジタル入力ピンGPIO35を,Goal側にはデジタル入力ピンGPIO36を割り当てます。
赤外LED
赤外LEDの電流は100mAまで可能ですが,M5Stack側のデジタル出力に無理をしないように,20mAで駆動するように制限抵抗を使います。
M5Stackのデジタルピン出力;3.3V
LED順電圧:1.35V
駆動電流:20mA
3.3V-1.35V=1.95V
R=V/I=1.95V/0.02A=97.5Ω
ということで,電流制限抵抗は100Ω程度を使います。
赤外線受光モジュール
赤外線受光モジュールの安定動作のため,1μFのバイパスコンデンサを付けます。
またM5Stack出力ピンの電流制限のため,100Ω程度の抵抗を使います。
次のような回路となります。
赤外発光アセンブリ,赤外受光アセンブリともに,動作確認用の可視光LEDを付けています。
5 赤外線利用で時間計測のプログラム
赤外LED38kHz変調駆動には38kHzのPWM動作を使用します。
PWM信号のデューティ比は,2/256, 4/256, 8/256, 16/256, 32/256,64/256,96/256,128/256を使用し,デフォルトは32/258です。
ボタンCでPWM信号のデューティ比を変更します。
起動直後のM5Stackの画面2行目に使用中のデューティ比が表示されます。
デューティ比が大きいほど,赤外LEDの強度は大きくなり,赤外発光アセンブリと赤外受光アセンブリ間の距離を大きくとることが出来ますが,場合によっては乱反射による赤外線の回り込みが生じ,常に受光モジュールが反応してしまうことがあります。
このプログラムでは,起動時には赤外発光アセンブリ,赤外受光アセンブリの動作確認のための設定モードになります。
M5Stackの画面に多くの情報を表示すると,表示時間がかかってしまい,5msec周期の遮光物検査が出来なくなるため,測定モードと設定モードの動作を必要としました。
設定モードでは,start側goal側の検査値を常時監視し,画面に表示します。Start=X Goal=xで表示し,(start側の検査値,goal側の検査値)を意味しています。ただし,0:遮光物無し,1:遮光物あり,です。
設定モードのとき,ボタンAで,計測モードになります。スタートラインからゴールラインまでのカートなどの遮光物の移動時間も時間分解能5msecで測定できます。
カートなどの遮光物がゴールすると1/100secまで表示されます。
計測モードにおいては,ボタンAでリセットして次の計測ができます。
(ボタンBで設定モードに戻ることもできます。)
loop関数シーケンス内で5msecの周期で受光モジュールの状態を検査します。
検査動作では,600μsecの赤外LED38kHz変調駆動を行います。
赤外LED38kHz変調駆動開始後300μsec経過後に受光モジュールの信号をチェックし,遮光物がないとき(受光時)にLを,遮光物があるとき(非受光時)にHをデジタル値で受け取ります。この検査は2回行われ,検査の信頼性を高めています
計測モードにおいては,loop関数シーケンス内では,リセット時からスタート検出までがスタート側アセンブリペアのみが動作し,その後ゴール検出までゴール側アセンブリペアのみが動作します。
注 カートのように前輪後輪がそれぞれ検出ラインを通過すると,スタート側もゴール側も2回遮光物が通過したように検出するケースがあります。(場合によっては3回以上検出することもあります。)このような場合でも,スタート側もゴール側がそれぞれ最初に遮光物を検出した時刻をもってスタート時刻とゴール時刻と解釈します。
//infraredsensorB.ino
//2つのIRLED-RecModuleペアでstart-Goal通過時間計測
#include <M5Stack.h>
#include "IRSensorPair.h"
IRSensorPair StartLine, GoalLine;
const uint8_t nBits_forPWM = 8; // PWMに使用するビット数 n=1~16[bit]
const uint8_t PWM_Values[] = {2, 4, 8, 16, 32, 64, 96, 128}; //デューティ
//MaxDuty=2^n DutyRatio = Duty/MaxDuty
const int nValues = sizeof(PWM_Values)/sizeof(uint8_t);
int ValueIndex = 4;
uint8_t PWM_Value = PWM_Values[ValueIndex]; //duty ratio = 32/256 に設定
const double PWM_Frequency = 38000.0; // PWM周波数 Maxfreq=80000000.0/2^n[Hz] 38kHz
const uint8_t StartLineLED_CH = 2; // PWMチャンネル
const uint8_t StartLineLED_PIN = 2; // PWM出力に使用するGPIO PIN番号
const uint8_t StartLineIRMdl_VPIN = 16; // 赤外線センサ電源に使用するGPIO PIN番号
const uint8_t StartLineIRMdl_OPIN = 35; // 赤外線センサ入力に使用するGPIO PIN番号
const uint8_t GoalLineLED_CH = 3; // PWMチャンネル
const uint8_t GoalLineLED_PIN = 5; // PWM出力に使用するGPIO PIN番号
const uint8_t GoalLineIRMdl_VPIN = 17; // 赤外線センサ電源に使用するGPIO PIN番号
const uint8_t GoalLineIRMdl_OPIN = 36; // 赤外線センサ入力に使用するGPIO PIN番号
bool onSetting = true; //設定中
long msecperiod = 5; // 赤外線光路チェック周期 msec
long usecWaitingtime = 1500; // 赤外線モジュール通電後の待機時間 μsec
long msectimetocheck;
void printTitle()
{
M5.Lcd.clear(TFT_BLACK);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setCursor(0,20);
M5.Lcd.setTextSize(3);
M5.Lcd.print(" FMIR Beam Clock");
}
void printSetting(int duty)
{
M5.Lcd.setCursor(0, 20+24+20);
M5.Lcd.setTextSize(3);
M5.Lcd.printf("Setting Dty= %-3d", duty);
}
void printSensorStatus(int startstatus, int goalstatus)
{
M5.Lcd.setCursor(0, 20+24+20+24+20);
M5.Lcd.setTextSize(3);
M5.Lcd.printf(" Start=%d Goal=%d", startstatus, goalstatus);
}
void printReadytogo()
{
M5.Lcd.setTextColor(CYAN, BLACK);
M5.Lcd.setCursor(4*18, 20+24+20);
M5.Lcd.setTextSize(5);
M5.Lcd.print("READY");
M5.Lcd.setCursor(4*18, 20+24+20+40);
M5.Lcd.setTextSize(5);
M5.Lcd.print("TO GO");
}
void printUndermeasureing()
{
M5.Lcd.setTextColor(PINK, BLACK);
M5.Lcd.setCursor(4*18, 20+24+20);
M5.Lcd.setTextSize(5);
M5.Lcd.print("UNDER");
M5.Lcd.setCursor(2*18, 20+24+20+40);
M5.Lcd.setTextSize(5);
M5.Lcd.print("MEASURE");
M5.Lcd.setCursor(7*18, 20+24+20+40+40);
M5.Lcd.setTextSize(5);
M5.Lcd.print("-MENT");
M5.Lcd.setTextColor(WHITE, BLACK);
}
void printElapsedtime(int msec)
{
// Because of the slow display speed,
// three characters are shown in two parts each.
static int upperprev = -1;
static int lowerprev = -1;
if (msec<0) {
upperprev = -1;
lowerprev = -1;
return;
}
int cstime = (msec%1000000) /10; //centisec
int upper, lower;
upper = cstime/100;
lower = cstime%100;
if (upperprev != upper) {
M5.Lcd.setCursor(3*18, 20+24+20+40+40+40+20);
M5.Lcd.setTextSize(3);
M5.Lcd.printf("%3d", upper);
upperprev = upper;
} else if (lowerprev != lower) {
M5.Lcd.setCursor(6*18, 20+24+20+40+40+40+20);
M5.Lcd.setTextSize(3);
M5.Lcd.printf(".%02d", lower);
lowerprev = lower;
}
//M5.Lcd.setCursor(3*18, 20+24+20+40+40+40+20);
//M5.Lcd.setTextSize(3);
//M5.Lcd.printf("%3d", mstime/1000);
//M5.Lcd.printf("%3d.%02d", mstime/1000, (mstime%1000)/10);
}
void printMeasuredRecord(int msec)
{
int mstime = msec%10000000;
M5.Lcd.setCursor(0, 20+24+20);
M5.Lcd.setTextSize(3);
M5.Lcd.print(" Measured Record");
M5.Lcd.setTextColor(YELLOW, BLACK);
M5.Lcd.setCursor(3*18, 20+24+20+24+20);
M5.Lcd.setTextSize(5);
M5.Lcd.printf("%4d.%02d", mstime/1000, (mstime%1000)/10);
}
void initialDisplayOnSetting()
{
printTitle();
printSetting(PWM_Value);
}
void initialDisplayOnMeasurements()
{
printTitle();
printReadytogo();
}
void setup() {
M5.begin();
M5.Power.begin();
StartLine.setIRLED(StartLineLED_PIN, StartLineLED_CH, PWM_Frequency, nBits_forPWM);
StartLine.setRecModul(StartLineIRMdl_VPIN, StartLineIRMdl_OPIN);
StartLine.setWaitingtime(usecWaitingtime);
StartLine.setpwmValue(PWM_Values[ValueIndex]);
GoalLine.setIRLED(GoalLineLED_PIN, GoalLineLED_CH, PWM_Frequency, nBits_forPWM);
GoalLine.setRecModul(GoalLineIRMdl_VPIN, GoalLineIRMdl_OPIN);
GoalLine.setWaitingtime(usecWaitingtime);
GoalLine.setpwmValue(PWM_Values[ValueIndex]);
initialDisplayOnSetting();
}
int status = 0;
/*
status について
初期状態 status = 0
status = 0 && IRRecModulestatus(start) = 1 then→ status = 1 (started)
status = 1 && IRRecModulestatus(goal) = 1 then→ status = 2 (finisheded)
*/
void loop() {
static long firstdetectedmsectime = 0; //最初の遮光の時刻
M5.update();
if(M5.BtnA.wasPressed()){
onSetting= false;
status = 0;
initialDisplayOnMeasurements();
static const int cleartimer = -1;
printElapsedtime(cleartimer);
delay(10);
msectimetocheck = millis() + msecperiod*2;
}
if(M5.BtnB.wasPressed()){
onSetting = true;
initialDisplayOnSetting();
delay(10);
}
if(M5.BtnC.wasPressed() && onSetting){
ValueIndex++;
if (ValueIndex == nValues) ValueIndex = 0;
PWM_Value = PWM_Values[ValueIndex];
StartLine.setpwmValue(PWM_Value);
GoalLine.setpwmValue(PWM_Value);
printSetting(PWM_Value);
delay(10);
}
if (onSetting) { //setting and adjustment
int IRRecModuleSLstatus = StartLine.checkIRRecModule();
int IRRecModuleGLstatus = GoalLine.checkIRRecModule();
printSensorStatus(IRRecModuleSLstatus, IRRecModuleGLstatus);
delay(100);
} else { //measurement sequence
if (msectimetocheck <= millis()) {
msectimetocheck += msecperiod;
int IRRecModulestatus;
if (status == 0) {
IRRecModulestatus = StartLine.checkIRRecModule();
if (IRRecModulestatus == 1) { //start
firstdetectedmsectime = millis();
status = 1;
printUndermeasureing();
}
} else if (status == 1) {
IRRecModulestatus = GoalLine.checkIRRecModule();
long elapsedmsectime = millis() - firstdetectedmsectime;
printElapsedtime(elapsedmsectime);
if (IRRecModulestatus == 1) { //finish
printTitle();
printMeasuredRecord(elapsedmsectime);
status = 2;
}
}
}
}
}
赤外LEDと赤外受光モジュールの協調で遮光物の有無を検査する機能は次のようにクラス化しました。
//IRSensorPair.h
#ifndef IRSensorPair_h
#define IRSensorPair_h
class IRSensorPair {
public:
void setIRLED(int LEDPin, int Channel, int Frequency, int nBits);
void setRecModul(int ModulVPin, int ModulOPin);
void setWaitingtime(long Waitingtime);
void setpwmValue(int value);
int checkIRRecModule(); // 0: notdetectObject, 1:detectObject, 9:invalid
private:
int IRLEDPin;
int RecModulVPin, RecModulOPin;
int PWM_CH, PWM_Frequency, nBits_forPWM;
int pwmValue;
long usecWaitingtime; // 赤外線モジュール通電後の待機時間 μsec
};
#endif
//IRSensorPair.cpp
#include <M5Stack.h>
#include "IRSensorPair.h"
void IRSensorPair::setIRLED(int LEDPin, int Channel, int Frequency, int nBits)
{
IRLEDPin = LEDPin;
PWM_CH = Channel;
PWM_Frequency = Frequency;
nBits_forPWM = nBits;
//IRLED向けPWM信号発生ピンを出力モードにする
pinMode(IRLEDPin, OUTPUT);
// チャンネルと周波数の分解能を設定
ledcSetup(PWM_CH, PWM_Frequency, nBits_forPWM);
// IRLED向けPWM出力ピンとチャンネルの設定
ledcAttachPin(IRLEDPin, PWM_CH);
//IRLED向けPWM出力ピンと周波数,分解能,チャンネルの設定 (これはESP32用の新しい関数なので,まだ使えない)
//ledcAttachChannel(IRLEDPin, PWM_Frequency, nBits_forPWM, PWM_CH);
}
void IRSensorPair::setRecModul(int ModulVPin, int ModulOPin)
{
RecModulVPin = ModulVPin;
RecModulOPin = ModulOPin;
pinMode(RecModulVPin, OUTPUT);
pinMode(RecModulOPin, INPUT);
digitalWrite(RecModulVPin, LOW);
}
void IRSensorPair::setWaitingtime(long Waitingtime)
{
usecWaitingtime = Waitingtime;
}
void IRSensorPair::setpwmValue(int value)
{
pwmValue = value;
}
int IRSensorPair::checkIRRecModule() // 0: notdetectObject, 1:detectObject, 9:invalid
{
digitalWrite(RecModulVPin, HIGH);
delayMicroseconds(usecWaitingtime);
//PWM信号発生開始
ledcWrite(PWM_CH, pwmValue);
delayMicroseconds(300);
int result1 = digitalRead(RecModulOPin);
delayMicroseconds(300);
//PWM信号発生停止
ledcWrite(PWM_CH, 0);
delayMicroseconds(600);
//PWM信号発生開始
ledcWrite(PWM_CH, pwmValue);
delayMicroseconds(300);
int result2 = digitalRead(RecModulOPin);
//delayMicroseconds(300);
//PWM信号発生停止
ledcWrite(PWM_CH, 0);
digitalWrite(RecModulVPin, LOW);
if (result1==0 && result2==0) return 0;
if (result1==1 && result2==1) return 1;
return 9;
}
6 実行の様子
次のテスト装置で動作を確認しました。
右側に赤外発光アセンブリ(赤外LED搭載)があり,左側に赤外受光アセンブリ(赤外線受光モジュール搭載)があります。
【設定モード】
M5Stack起動直後は設定モードになり,次のようになります。
Start=0 Goal=0 はStart側センサ,Goal側センサともに,遮光物検知無しを意味しています。
Dty= 32 は赤外線発光の強さを表す。
設定モードでスタートラインに遮光物があるときは,次のようになります。
Start=1 はStart側センサに,遮光物検知ありを意味しています。
設定モードでゴールラインに遮光物があるときは,次のようになります。
Goal=1 はゴール側センサに,遮光物検知ありを意味しています。
〇センサを設置直後で遮光物が無ければ,Start=0 Goal=0 になるはずですが,1を表示している場合は,不正な配線になっている,またはセンサペアの発光・受光が正しく向き合っていない可能性があります。
〇赤外線発光の強さは,設定モードであれば,ボタンCで変更可能。ボタンCを何回か押すと最初の値に戻ります。設定値32が普通ですが,他のシステムに干渉する場合は,値を小さくします。
〇ボタンAで測定モードに移行します
【測定モード】
測定モードに移ったときは,測定準備完了状態となります。
測定モード測定準備完了状態で,スタート側センサが反応したときは,測定状態となります。
測定モード測定状態で,ゴール側センサが反応したときは,測定終了状態となり,測定された時間を表示します。
ボタンAで測定準備完了状態に戻り,次の計測ができます。
ボタンBで「設定モード」に移行できます。
ボタンAおよびボタンBはいつでも押すことが出来ます。(計測中でも可能)
【実行時の動画】
無線カートを使って「設定モード」と「測定モード」の動作を検証しました。
最初の状態は遮光物が何もないため,スタート側の検出値,ゴール側の検出値の両方が0であることが確認できます。
次にカートが前進・後退するときに,カートの車輪部が赤外線ビームを遮るため,検出値が変化しているのがわかります。
カートが初期位置にいるところで,ボタンAを押して測定モードに移行しています。
カートの車輪部がスタート側赤外線ビームを遮ると,測定が開始されます。
カートの車輪部がゴール側赤外線ビームを遮ると,測定が終了し,記録時間は表示されます。
カートが初期位置に戻ってからボタンAが押され,測定準備完了状態になって,次の測定が可能になっています。
7 赤外線LED駆動信号と,赤外線受光モジュール駆動信号の観察
測定モードにおいて,カートのスタートを検知した後の測定状態では,0.005秒ごとに,小さな文字で経過時間を0.01秒単位で画面表示しています。しかし画面表示はけっこう時間がかかり,0.005秒の周期を守れるかどうか検証する必要がありました。そこで,オシロスコープで赤外線LED駆動信号(ゴール側)と,赤外線受光モジュール駆動信号(ゴール側)を観察して,5msec周期の遮光物のあるなしを検査が行えているかどうかをチェックしました。
ゴール側の赤外線LED駆動信号と赤外線受光モジュール駆動信号
実は,0.005秒間隔での遮光物検査+画面表示では,3文字までの表示しかできませんでした。そこで,整数部の3桁と,小数点を含む小数部の3桁表示を必要に応じて片方だけ表示させる裏技を,プログラムで使っています。
8 使用デューティ比と,センサペア間の距離
デューティ比によって利用可能な赤外発光アセンブリと赤外受光アセンブリ間の距離は変わります。実測の結果,次のようになっていました。
デューティ比 |
発光と受光間の最大距離 |
4/256 |
20cm |
8/256 |
40cm |
16/256 |
100cm |
32/256 |
150cm |
64/256 |
230cm |
9 まとめ
M5Stackで赤外線を使った計時システムのテストができました。
50cm程度の赤外線光路でうまく動作しています。
なお測定値には±5msecの誤差が含まれています。
製作・テストにおいて誤動作がありました。対処は次の通りでした。
(1)赤外線受光モジュールにはノイズの少ない電源を与える必要がある。セラミックコンデンサ1μFを赤外線受光モジュール直近に付加することで対処出来ました。
(2)赤外線受光モジュールのAGC(Automatic Gain Control)が働かないようにして動作しているため,外乱光にかく乱されることがありました。外乱光を防ぐためのカバーを使うことで対処しました。
感想
2000年頃にRS232C通信の途中に赤外線通信で中継してロボコンのロボットの遠隔操作機能を作ったときには,単純に38kHz変調・復調しただけで通信ができたと記憶しています。そのころの受光モジュールは単純に38kHz変調された赤外線を復調しているだけのものが入手出来たのではないかと思います。現在入手できる赤外線受光モジュールは用途が家電製品のリモコンに特化され,耐ノイズ性能は向上したものの,特定のバーストパターンしか受け入れないようにできていて,受光状態か非受光状態かを常時監視するにはプログラムの工夫が必要となっています。
いろいろ調べていくと,OSRB38C9AA を含む最近の赤外線受光モジュールはリモコン用と書かれていて,ある長さの非受光状態をAGCが必要としているようで,Net検索してみると困っている人も多いようです。
今は廃版になってしまった赤外線受光素子 NJL21V380A や PL-IRM1261-C438 はある長さの非受光状態を必要としていないようです。(これらのモジュールは現在入手がきわめて困難です)
現状では測定値に±5msecの誤差が含まれていますが,赤外線受光素子 NJL21V380A や PL-IRM1261-C438 を使うことが出来れば,誤差を現状の1/2程度まで小さくできるはずです。