いろいろ作るよ!

ものづくりの記録

光線銃のための赤外線通信(ソフト編)

目的

赤外線通信を用いた光線銃を製作します。
赤外線通信の仕組み、送信・受信素子の制御、M5Stack上のアプリケーションについて本記事にまとめます。 ※赤外線通信のハード編は前の記事を参照ください。

参考資料

  1. 赤外線通信規格について参考にしました。 elm-chan.org

  2. ESP32マイコンでのタイマー割込の実装方法を参考にしました。 marchan.e5.valueserver.jp

  3. M5Stackをオシロスコープ化するプログラムを利用させていただきました。 goji2100.com

赤外線通信の仕組み

f:id:neet2121:20200112014752p:plain (wikipediaより)
赤外線は波長780nm以上の目に見えない電磁波です。テレビリモコンなどの赤外線通信では940nmの赤外線が良く利用されています。これは太陽光線のスペクトルのうち940nm付近が大気中の水蒸気で良く吸収され、地表付近で放射強度が低下し、太陽光により明るい環境下でも通信しやすいことが理由になっています。
赤外線LEDは波長940nmで発光し、受光モジュールは波長940nmで受信感度が一番高くなっています。
赤外線通信においてこの波長は"キャリア"と呼ばれています。
f:id:neet2121:20200112015856p:plain
もう一つ通信を安定させるための仕組みがあり、受光モジュールには38kHzのバンドパスフィルタが搭載されています。バンドパスフィルタとは、ある範囲の周波数の信号のみ通過するフィルタです。つまり、940nmの赤外線が38kHzでON/OFFを繰り返している場合だけ、受光モジュールは赤外線を検知することができます。
送信側にて赤外線が38kHzでON/OFFを繰り返している時間、また完全に消灯している時間を制御し、受信側でその時間を計測することで、通信が実現されています。 赤外線通信においてこの周波数が"サブキャリア"と呼ばれています。

光線銃のための通信フォーマット

どのようなタイミングで赤外線を点灯・消灯させるか、送信側と受信側で取り決めがなされています。各社で”通信フォーマット”としてルール化されています。
既存の通信フォーマットを参考に、光線銃に特化した通信フォーマットを検討します。
通信中は赤外線を受信モジュールに当て続ける必要があります。通信時間が長いフォーマットでは光線銃の手ブレやターゲットの動きにより通信が困難です。
また、送信するデータはチームが識別できる程度で良く、よって可能な限り短時間で通信が終了できる通信フォーマットを目指します。
狙い:

  • 可能な限り短時間で通信完了できる
  • データ部はチームの識別のみ

f:id:neet2121:20200112020230p:plain
仕様:

  • キャリア:940nm
  • サブキャリア:38kHz
  • Duty比:1/3
  • T:500us
  • データ部:001,010,100の3ケース
  • 通信時間合計:5msec

受信側でLeaderを検出した場合にデータ取得するモードに入り、Footerを検出とデータを確定します。
データ部は各チームを表しています。

  • チーム1:001
  • チーム2:010
  • チーム3:100

5msecで通信完了でき、既存の通信フォーマットに比べ相当に短い通信時間となっています。
f:id:neet2121:20200112020503p:plain
照射角度±1.6度と通信時間5msecをもとに、自分が回りながら射撃するケースを想定して、上限の回転速度を計算してみます。(1.6[deg]/5[msec])*1000/360[deg]=0.9[回転/sec]より、1秒間に0.9回転するほどグルグル回っていても、通信時間が原因での通信失敗は発生しないことになります。

M5Stackよる赤外線データ送信の実装

ESP32マイコンの機能により、下記を実装しています。

  1. PWM出力機能により38kHzで赤外線LEDをON/OFFさせる。
  2. タイマー割込みにより2kHz(周期500um)で送信待ちデータの左端ビットを判定し、赤外線LEDのON/OFFを切り替え、送信待ちデータを1ビットシフトする。
  3. ボタンが押されたことを検出し、送信待ちデータに指定のデータを格納する。
  4. マルチタスク機能により、LCDの描画処理を赤外線送信処理とは別のCPUで実行し、送信遅延を防ぐ。

ソースコードです。

#include <M5Stack.h>

/* IR通信定義 */
#define IR_HZ        38000 //赤外線搬送波周波数
#define IR_DATA_T    500 //通信プロトコルのT[us]
#define IR_DUTY      682  //duty比 100%→2048

/* I/O定義 */
#define SEND_PIN     26   //赤外線送信ピン

/* IRグローバル変数 */
volatile int irSendData = 0;      //IR送信データ

/* 演出トリガー用フラグ */
int shotFlag = 0;

//作成タスクのHandleへのポインタ
TaskHandle_t th[4]; 

/* IR送信用タイマー割込み設定 */
volatile int timeCounter1;
hw_timer_t *timer1 = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

/* IR送信用タイマー割込み処理 */
void IRAM_ATTR onTimer1(){
  // Increment the counter and set the time of ISR
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter1++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

/* setup */
void setup(){
  
  // M5初期化
  M5.begin();
  M5.Lcd.setTextSize(4);

  /* I/O setup */
  
  // LED PWM駆動の設定
  ledcSetup(1, IR_HZ, 11); //11bitのときは最大39062Hz
  ledcAttachPin(SEND_PIN, 1);
  ledcWrite(1,0);

  /* タイマー割込み初期化 */
  timer1 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer1, &onTimer1, true);
  timerAlarmWrite(timer1, IR_DATA_T, true);
  timerAlarmEnable(timer1);

  /* マルチタスク */
  
  //LCD制御タスク
  xTaskCreatePinnedToCore(lcdControl,"lcdControl", 4096, NULL, 1, &th[0], 0);
}

/* メインルーチン */
void loop() {

  /* タイマー割込み処理 */
  
  //2kHzで実行
  if (timeCounter1 > 0) {
    portENTER_CRITICAL(&timerMux);
    timeCounter1--;
    portEXIT_CRITICAL(&timerMux);

    //IR送信処理
    irSend();
  }
  
  //ボタン判定
  if (M5.BtnA.wasPressed()) {
    irSendData = 0b1110100011;
    shotFlag = 1;
  }
  if (M5.BtnB.wasPressed()) {
    irSendData = 0b1110010011;
    shotFlag = 2;
  }
  if (M5.BtnC.wasPressed()) {
    irSendData = 0b1110001011;
    shotFlag = 3;
  }
  m5.update();
}

/* LCD制御タスク */
void lcdControl(void *pvParameters) {
  while(1){
    //射撃判定
    if (shotFlag) {
      M5.Lcd.print(shotFlag);
      shotFlag = 0;
    }
    delay(1);
  }
}

/* バッファ内容を赤外線送信 */
void irSend() {
  if ((irSendData & 0b1000000000) == 0b1000000000){
    ledcWrite(1,IR_DUTY);
  } else {
    ledcWrite(1,0);
  }
  irSendData = (irSendData << 1) & 0b1111111111;
}

M5Stackオシロスコープで受信状況を確認してみます。

受信モジュールは、サブキャリア38kHzの赤外線を受信しているときにLowを出力し、受信していないときにHighを出力します。受信データを扱う際に注意が必要です。

M5Stackによる赤外線データ受信の実装

次に受信側です。以下の機能を追加します。

  1. 外部割込みにより赤外線信号のHigh/Lowの切り替わり間隔を計測する
  2. Leaderを受信すると受信データを初期化、Footerを受信すると受信データを確定、それ以外の信号は受信データに追加する
  3. 確定した受信データがチーム1、チーム2、チーム3に合致しているか判断する
  4. 送信側と同様に、マルチタスク機能で受信時LCD表示を通信と別のCPUで実行し、受信遅延を防ぐ

f:id:neet2121:20200113211633p:plain
マルチタスク機能によりコア0とコア1に処理を割り振っています。

ソースコードです。

#include <M5Stack.h>

/* IR通信定義 */
#define IR_HZ        38000 //赤外線搬送波周波数
#define IR_DATA_T    500 //通信プロトコルのT[us]
#define IR_DUTY      682  //duty比 100%→2048
#define IR_TOLERANCE_H  50  //IR_H信号の誤差裕度[us]
#define IR_TOLERANCE_L  200  //IR_L信号の誤差裕度[us]
int ir_tolerance[2] = {IR_TOLERANCE_L,IR_TOLERANCE_H};

/* I/O定義 */
#define SEND_PIN     26   //赤外線送信ピン
#define RECIEVE1_PIN  35   //赤外線受信ピン

/* IRグローバル変数 */
volatile int irSendData = 0;      //IR送信データ
volatile unsigned int irTime = 0; //IR信号受信時間
volatile int irCount = 0;         //IR信号カウンター
volatile int irTmpData = 0;       //IR一時データ
volatile int irRecieveData = 0;   //IR受信データ

/* 演出トリガー用フラグ */
int damageFlag = 0;
int shotFlag = 0;

//作成タスクのHandleへのポインタ
TaskHandle_t th[4]; 

/* IR送信用タイマー割込み設定 */
volatile int timeCounter1;
hw_timer_t *timer1 = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

/* IR送信用タイマー割込み処理 */
void IRAM_ATTR onTimer1(){
  // Increment the counter and set the time of ISR
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter1++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

/* setup */
void setup(){
  
  // M5初期化
  M5.begin();
  M5.Lcd.setTextSize(4);

  /* I/O setup */
  
  // 受信ピンの入力設定
  pinMode(RECIEVE1_PIN, INPUT);
  
  // LED PWM駆動の設定
  ledcSetup(1, IR_HZ, 11); //11bitのときは最大39062Hz
  ledcAttachPin(SEND_PIN, 1);
  ledcWrite(1,0);

  /* タイマー割込み初期化 */
  timer1 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer1, &onTimer1, true);
  timerAlarmWrite(timer1, IR_DATA_T, true);
  timerAlarmEnable(timer1);

  //IR受信割り込みの設定
  attachInterrupt(digitalPinToInterrupt(RECIEVE1_PIN), irInterrupt, CHANGE);

  /* マルチタスク */
  
  //LCD制御タスク
  xTaskCreatePinnedToCore(lcdControl,"lcdControl", 4096, NULL, 1, &th[0], 0);
}

/* メインルーチン */
void loop() {

  /* タイマー割込み処理 */
  
  //2kHzで実行
  if (timeCounter1 > 0) {
    portENTER_CRITICAL(&timerMux);
    timeCounter1--;
    portEXIT_CRITICAL(&timerMux);

    //IR送信処理
    irSend();
  }

  /* IR受信判定 */
  
  if (irRecieveData!=0){
    if(irRecieveData == 0b1110100011) {
      //被弾フラグ
      damageFlag = 1;
    } else if(irRecieveData == 0b1110010011) {
      //被弾フラグ
      damageFlag = 2;
    } else if(irRecieveData == 0b1110001011) {
      //被弾フラグ
      damageFlag = 3;
    }
    irRecieveData = 0;
  }
  
  //ボタン判定
  if (M5.BtnA.wasPressed()) {
    irSendData = 0b1110100011;
    shotFlag = 1;
  }
  if (M5.BtnB.wasPressed()) {
    irSendData = 0b1110010011;
    shotFlag = 2;
  }
  if (M5.BtnC.wasPressed()) {
    irSendData = 0b1110001011;
    shotFlag = 3;
  }
  m5.update();
}

/* LCD制御タスク */
void lcdControl(void *pvParameters) {
  while(1){
    //被弾演出
    if (damageFlag) {
      M5.Lcd.print(damageFlag);
      damageFlag = 0;
    }
    //射撃演出
    if (shotFlag) {
      M5.Lcd.print(shotFlag);
      shotFlag = 0;
    }
    delay(1);
  }
}

/* バッファ内容を赤外線送信 */
void irSend() {
  if ((irSendData & 0b1000000000) == 0b1000000000){
    ledcWrite(1,IR_DUTY);
  } else {
    ledcWrite(1,0);
  }
  irSendData = (irSendData << 1) & 0b1111111111;
}

/* IR変更割り込み処理 */
void irInterrupt() {  
  
  //割り込み処理停止
  detachInterrupt(digitalPinToInterrupt(RECIEVE1_PIN));
  
  //信号H/L取得
  int irState = digitalRead(RECIEVE1_PIN);
  unsigned int signalTime = micros() - irTime;

  //T=500us未満の場合は処理しない
  if (signalTime > IR_DATA_T - ir_tolerance[irState]) {
  
    //前回信号受信時間を更新
    irTime = irTime + signalTime;
  
    //信号長取得
    int irCount = int((signalTime + ir_tolerance[irState])/IR_DATA_T);

    if(irCount == 3 & irState == HIGH){
      
      //Leader Pulseを受信すれば、一時データをクリアする
      irTmpData = 0b111;

    } else if(irCount == 2 & irState == HIGH){
      
      //Footer Pulseを受信すれば、受信データを確定する
      irTmpData=(irTmpData<<2)+0b11;
      irRecieveData=irTmpData;
      irTmpData = 0;
      
    } else {
  
      //受信データに追加する
      irTmpData = irTmpData << irCount;
      if(irState == HIGH){
        irTmpData = irTmpData + (1<<irCount-1);
      }
    }
  }
  
  //割り込み再開
  attachInterrupt(digitalPinToInterrupt(RECIEVE1_PIN), irInterrupt, CHANGE);
  
}

まとめ

送信側のM5Stackで1ボタンを押すと、受信側のM5Stackで画面に1が表示されます。 レンズで集光することで、長距離での通信が可能です。