PR

Arduinoでリモコンのデータを受信する

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

ArduinoのDIに赤外線受信機モジュールセンサーを接続することでリモコンと通信することができます。Arduinoスターターキットに付属していたリモコンと赤外線受信センサーを使って赤外線による遠隔操作の方法をまとめました。

使用するリモコンはCar mp3リモコン(メーカ不明)、赤外線受信機モジュールはOSRB38C9AA(シャープ)を使用しています。

ArduinoのDIを使って赤外線受信機モジュールが受信したデータを解析してリモコンのボタン操作による指令値を取得する方法をまとめています。また、IRremoteライブラリを使用する方法についても説明しています。

Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

赤外線受信機モジュールからデータを取得する

赤外線受信機モジュールはリモコン(送信機)から送出された900nm帯のバースト波を受信してデジタル信号に変換するモジュールです。デジタル信号は赤外線リモコン用の通信フォーマットによって決められており、通信フォーマットを解析することでリモコンの指令に応じた動作ができます。赤外線リモコンの通信フォーマットについては下記リンクを参考にしています。

赤外線リモコンの通信フォーマット (elm-chan.org)

よく使用される通信フォーマットはシェアの関係上、NEC/家電協/SONYによるものが多くあります。本記事のリモコンは通信フォーマットの確認の方法で確認したところNECフォーマットでしたので、NECフォーマットのデータ取得を中心に説明します。

NECフォーマットの構成
NECフォーマットの構成

NECフォーマットの構成はリーダーコード、カスタムコード、データコード、ストップビットの順にLSBファーストで送信されます。通信フォーマットは最短でも108msの間隔で送信されます。108ms以上リモコンのボタンを押し続けているとリピートコードが送信されます。最初のリーダーコードを解析することで通信フォーマットを区別することができます。

リモコンの出力が赤外線受信機モジュールの内部でデジタル信号に変換されますが、受信機モジュールの出力はプルアップされているためリモコンの通信フォーマットに対してLOW(0)とHIGH(1)が反転するので注意が必要です。通信フォーマットからデータを取得する手順を説明します。以下ではLOWを0、HIGHを1とします。

データの取得手順
  • 手順1
    通信フォーマットの確認

    リーダーコードの長さから通信フォーマットを判断する。

  • 手順2
    データコードの取得

    リーダーコード以降はデータコードとなるので通信フォーマットに応じたビット分のデータビットを1バイトデータで取得する。データビットの0/1は1ビットあたりの1の長さで判断する。

  • 手順3
    データの確認

    手順2で取得したデータコードから1バイトのデータと反転データを生成し、2つのデータの論理和(0xFF)または論理積(0x00)でデータの健全性を確認する。

下記の説明で使用しているT(変調単位)はNECフォーマットでは562usになります。図では()内にms換算して表記しています。

手順1では通信フォーマットの確認を行います。リーダーコードの長さから通信フォーマットを判定します。

リーダーコードによる通信フォーマットの判定
リーダーコードによる通信フォーマットの確認

NECフォーマットを受信機から見た場合、0の区間が16T(9ms)間続き、1の区間が8T(4.5ms)間続きます。リーダーコードで通信フォーマットを解析することができます。通信フォーマットの確認でDIで取得したデータから通信フォーマットを判断する方法を説明しています。

手順2ではデータコードのDIを確認して0の区間と1の区間の長さから各データビットの判定を行います。データビットの判断はデータビットの1と0を判定するで説明しています。

データコードのビット確認
データコードのビット確認

データコードはカスタム番号、データ値、データ反転値で構成されています。カスタム番号はメーカ識別番号で16bit構成されており、データは8ビット構成、チェック用の反転データが8ビット構成になっています。

受信機から見た場合、1の区間が1T(0.5ms)の場合はデータビットが0と判断できます。1の区間が3T(1.69ms)の場合はデータビットが1と判断できます。この判定を繰り返し行いデータコードとストップビットを取得します。

手順3では手順2で取得したデータの配列から1バイトのデータ値と反転データを生成し、論理和または論理積を計算してデータの健全性を確認します。データが正常であればリモコンのデータとして処理します。

スポンサーリンク

通信フォーマットの確認

rcnt_l = 0;
while( digitalRead(REMOTE_DI) == LOW && errflg == false){
  ++rcnt_l;
  delayMicroseconds(100);
}
rcnt_h = 0;
while( digitalRead(REMOTE_DI) == HIGH && errflg == false){
  ++rcnt_h;
  delayMicroseconds(100);
}

Serial.print(rcnt_l);
Serial.print("--");        
Serial.println(rcnt_h);

リーダーコードで通信フォーマットを判断する例を説明します。受信機は待機状態では1になります。リモコンのボタンが押すと受信機が応答し通信フォーマットに従って0を出力します。0の区間は100us毎にカウント(rcnt_l)を更新します。この区間はリーダーコードの16T(9.0ms)に相当します。

次に1の区間を100us毎にカウント(rcnt_h)を更新します。この区間はリーダーコードの8T(4.5ms)に相当します。

シリアルモニターでrnct_l及びrcnt_hを確認すると通信フォーマットを判定することができます。100us毎のカウントとなるためタイミングによっては-3~-2程度カウント数が変化しますが、通信フォーマットの判定には問題ない誤差となります。

本記事で使用しているリモコンでrcnt_l及びrnct_hの値を確認したところそれぞれの値が87(8.7ms)、42(4.2ms)であったことからNECフォーマットであると判断しています。

広告

データビットの1と0を判定する

データビットの判定
データビットの判定

受信機から見た場合データビットは1の区間の長さで決まります。100us毎にDIを確認してカウント値を更新しながら0の区間と1の区間の長さを確認します。0の区間はデータビットの判定に使用しませんが、カウント値は5以下(3~5)の値になります。

データビットが0の場合は1の区間が1Tになるので0の区間と同様の値となります。一方データビットが1の場合は1の区間が3Tになるのでカウント値が16以下(14~16)になります。

データビットの送信タイミングと100usの遅延などのタイミングでカウント値が少しばらつきますが、1の区間の長さが1Tから3Tと判定に十分に余裕があるため誤差は問題になりません。ただし、割り込みによる処理によって遅延が大きくなると判定がうまくいかなくなる場合があります。

以下にデータビットの判定の例を示します。判定の結果を1バイトデータの配列に格納します。

uint8_t *dat;

dat = &remote.cus[0]; //1バイトの配列にデータビットを格納

for(uint8_t i=0; i < 33; i++){
  lcnt = 0; //LOWの部分のカウントをクリア
  while( digitalRead(REMOTE_DI ) == LOW && errflg == false){
    ++lcnt;
    delayMicroseconds(100); //100usウェイト
  }
  hcnt = 0; //HIGHの部分のカウントをクリア
  while( digitalRead(REMOTE_DI ) == HIGH && errflg == false){
    ++hcnt;
    if(hcnt >= 14 && i == 32){
      break; //ストップビットは1なので14以上であれば処理を停止
    }
    delayMicroseconds(100); //100usウェイト
  }
  if(hcnt >= 10){ //10(2T)以上であれば1と判断する
    *dat = 1;
  }
  else{
    *dat = 0;
  }
  ++dat;
}

digitalRead()関数でDIを確認します。最初にwhile()で0の区間のカウント値(lcnt)を確認します。delayMicroseconds()関数で100usのウェイトを置くことで更新頻度の調整を行っています。DIが1の区間に切り替わるとループを抜けます。

次にwhile()で1の区間のカウント値(hcnt)を確認します。delayMicroseconds()関数で100usのウェイトを置くことで更新頻度の調整を行っています。DIが0の区間に切り替わるとループを抜けますが、ストップビットの受信後DIが1に固定されるためストップビットの場合のみカウント値が14以上でループを抜けるようにしています。

通信フォーマットがうまく処理しきれずwhile()内で処理が停滞することを防ぐためにerrflgでエラーを監視しています。リモコンの最小の送信タイミングで108ms以内に通信フォーマットが処理できない場合はerrflgをtrueにしてエラーを通知してループから抜けるようにしています。

データビットの0か1かは1の区間のカウント値で判定します。データビットが0となる場合のカウント値の目安は6以下であり、データビットが1となるカウント値の目安は16以下であることから6よりも大きければデータビットが1となると判断できます。

判定の基準に余裕をみて2T分の経過カウント数の目安である10以上であれば該当のビットが1であると判断して1を格納します。10未満であればビットが0であると判断して0を格納します。

データビットの判定までの1セットとしてカスタムからストップビットまでの33ビット分を繰り返します。

PR:スキマ時間を有効に!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!

リモコンのデータの生成とチェック

uint8_t i;
uint16_t cus_l=0;
uint16_t cus_h=0;
uint16_t cus =0;
uint8_t data=0;
uint8_t idata=0;
uint8_t chk;

for(i=0; i < 8; i++){ //16ビットのうち下位8ビット
  cus_l |= ( remote.cus[i] << i); //カスタムコード下位
}

for(i=0; i < 8; i++){ //16ビットのうち上位8ビット
  cus_h |= ( remote.cus[i + 8] << i); //カスタムコード上位
}

cus = (cus_h << 8) | cus_l; //16ビットデータを生成

for(i=0; i < 8; i++){
  data |= ( remote.data[i] << i); //データ
  idata |= ( remote.Idata[i] << i); //反転データ
}

chk = ~idata; //反転データを反転

if( data == chk){ //データが一致するか
  btnval = data; //データを格納
}

1バイトデータの配列にデータビットを順番に格納しているためデータの配列からシフト演算を使ってデータビットを1バイトのリモコンのデータを生成します。データコードはLSBファーストで送信されているため配列の1番目から順に左にシフトしながら論理和を計算することで1バイトデータを生成することができます。

データと反転データの生成を例にすると、LSBファースト(最下位ビットから送信)するので配列のデータを左に0回シフト(×1と同じ)して論理和を計算します。続けて2ビット目は更新した配列のデータを左に1回シフト(×2と同じ)して論理和を計算します。同様にして3ビット目~MSBまで左にシフトしながら論理和を計算すると1バイトデータ(8ビットのデータ)が生成できます。

カスタムコードについても同様の方法で生成できますが、16ビットの構成なので下位の8ビットと上位の8ビットが入れ替わっているため注意が必要です。例えばカスタムコードが0xFF00の場合は、LSBファーストで送っているので2バイトデータの場合は上位ビットが0x00で下位ビットが0xFFになります。

データと反転データでデータの健全性を確認します。反転データを反転(データを反転)して2つのデータを比較する方法、2つのデータの論理和を計算して0xFFを確認する方法、2つのデータの論理積を計算して0x00を確認する方法があります。これらの方法でデータが健全であると判断できればリモコンのデータとして採用します。

スポンサーリンク

IRremoteライブラリを追加して使用する(参考)

IRremoteライブラリの追加
IRremoteライブラリの追加

Arduino IDEでIRremoteライブラリを追加する方法を説明します。Arduino IDEのライブラリマネージャの検索欄にirremoteを入力するとライブラリの候補が表示されます。候補の中からIRremote by shirriff,z3t0,ArminJoをインストールします。使用例は以下の通りです。

#include <IRremote.hpp>

#define TIME_UP 0
#define TIME_OFF -1
#define TIME_LED 50
#define BASE_CNT 10 //10msがベースタイマとなる
#define PIN_DO 7
#define REMOTE_DI 2

int16_t timled;
int16_t ledval;
uint32_t beforetimCnt = millis();
uint16_t btnval;

/* プロトタイプ宣言 */
void mainTimer(void);

void setup() {
  
  Serial.begin(115200);
  pinMode(PIN_DO,OUTPUT);

  IrReceiver.begin(REMOTE_DI);
  ledval = TIME_OFF;
  timled = ledval;
}

void loop() {

  mainTimer();

  if( IrReceiver.decode()){ //受信したか
    Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX); //受信データの表示
    IrReceiver.printIRResultShort(&Serial); //通信フォーマットなどデータを表示
    IrReceiver.printIRSendUsage(&Serial);   //リモコンのアドレスやボタンによる指令値の表示
    IrReceiver.resume(); //次の受信を許可
    btnval = IrReceiver.decodedIRData.command;

    switch(btnval){
      case 22: //0
        ledval = TIME_OFF;
        digitalWrite(PIN_DO,LOW);
        break;
      case 12: //1
        ledval = TIME_OFF;
        digitalWrite(PIN_DO,HIGH);
        break;
      case 24: //2
        ledval = TIME_LED;
        break;
      case 94: //3
        ledval = TIME_LED*2;
        break;
      case 8: //4
        Serial.println("push-4");
        break;
      case 28: //5
        Serial.println("push-5");
        break;
      case 90: //6
        Serial.println("push-6");
        break;
      case 66: //7
        Serial.println("push-7");
        break;   
      case 82: //8
        Serial.println("push-8");
        break;
      case 74: //9
        Serial.println("push-9");
        break;                      
    }
    timled = ledval;
  }
}

/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt >= BASE_CNT ){
    beforetimCnt = millis();

    if( timled > TIME_UP ){
      --timled;
    }
  }

  if( timled == TIME_UP ){
    timled = ledval;
    digitalWrite(PIN_DO, !digitalRead(PIN_DO));
  }
}

IRremote.hppをインクルードします。IrRecvクラスでインスタンス化されているIrReciver変数を使って処理を行います。setup()関数内でメンバー関数のbegin()関数で受信機からのデータを受け取るDIピンを指定します。

IRremoteライブラリを使用する場合は、ライブラリが通信フォーマットを解析している間に割り込みが入らないようにする必要があります。

Loop()関数内で受信したときの処理を追加します。decode()関数はデータの受信を確認します。受信があれば戻り値にtrueを返します。

受信後はdecodedIRData.decodedRawDataに受信したデータが格納されています。シリアルモニターで確認する場合はSerial.print()関数の引数に指定して表示します。第2引数にHEXを指定することで16進数で表示することができます。

printIRResultShort()関数は通信フォーマットデータの情報やデータの表示をまとめた文字列を生成します。引数にシリアル通信のハードウェアのアドレスを指定します。Serialのアドレスを指定することで文字列がシリアルモニターで表示されます。

printIRsendUsage()関数はリモコンのアドレスやデータの表示をまとめた文字列を生成します。引数にシリアル通信のハードウェアのアドレスを指定します。Serialのアドレスを指定すると文字列がシリアルモニターに表示されます。

decodedIRData.commandはリモコンのデータが格納されています。リモコンごとにボタンのデータが異なるためシリアルモニターで確認しながら処理のパターンを追加します。

resume()関数はリモコンからの次の受信を許可します。

各種メーカーの通信フォーマットに応じて処理ができるように作りこまれているため手軽にリモコンよる遠隔操作が実装できるのは良いと思います。

広告

動作確認

動作確認用の回路図
動作確認用の回路図

電源を入れるとリモコンからの受信を待ちます。受信するとリモコンのボタンによる指令の処理を行います。私が使用しているリモコンにおいて0~9のボタンで処理を分岐させて動作確認を行います。

「0」を押すとLED1を消灯します。「1」を押すとLED1を点灯します。「2」を押すとLED1を点灯/消灯(500msでフリッカ点灯)「3」を押すとLED1を点灯/消灯(1sでフリッカ点灯)します。「4」~「9」を押すとpush-4~push-9をシリアルモニターで表示します。

リモコンのボタンを108ms以上長押しするとリピートコードが送信されます。「4」~「9」を長押しすると定期的にシリアルモニターの文字を更新することが確認できます。シリアルモニターで確認した結果は以下の通りです。

シリアルモニターの確認
シリアルモニターの確認

シリアルモニターにカスタムコード(cus:)、データ(data:)、反転データ(Idata:)を表示しています。「1」を押してLED1を点灯し、「0」でLED1が消灯することが確認できました。「2」を押すとLED1がフリッカ点灯し、「3」を押すことでフリッカのタイミングが遅くなることが確認できました。「4」~「9」を順に押していくとpush-4~push-9までがシリアルモニターに表示されることが確認できました。

リモコンで複数のボタン処理のパターンを作ることができるため使用用途が広がりますが、最大70ms程度処理が停滞するので頻繁にメイン処理が必要なプログラムとは相性が悪そうです。

広告

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <MsTimer2.h>

#define REMOTE_DI 2
#define TIME_UP 0
#define TIME_OFF -1
#define TIME_WAIT 9
#define TIME_LED 50
#define REPEAT_CNT 120
#define PIN_DO 7

struct SIG_TYP{
  uint8_t cus[16];
  uint8_t data[8];
  uint8_t Idata[8];
  uint8_t stop;
};

enum REMOTE_MD{
  REMOTE_READ = 0,
  REMOTE_DATA,
  REMOTE_END,
  REMOTE_MAX
};

SIG_TYP remote;
uint8_t re_md;
int16_t timrepeat;
int16_t timled;
int16_t ledval;
uint8_t btnval;
bool errflg;
bool btnflg;
bool repeatflg;

/* プロトタイプ宣言 */
void mainTimer(void);
void mainApp(void);
void chkData(void);
void remoteControl(void);

void setup() {
  
  Serial.begin(115200);
  pinMode(REMOTE_DI,INPUT);
  pinMode(PIN_DO,OUTPUT);

  MsTimer2::set(10, mainTimer); // 10ms period
  MsTimer2::start();
  ledval = TIME_OFF;
  timled = ledval;
}

void loop() {

  mainApp();
  remoteControl();
}

/* タイマ管理 */
void mainTimer(void){

  if( timled > TIME_UP ){
    --timled;
  }

  if( timrepeat == TIME_UP ){
    timrepeat = TIME_OFF;
    re_md = REMOTE_END;
  }

}
/* リモコン受信処理 */
void mainApp(void){
  uint8_t i;
  uint16_t lcnt;
  uint16_t hcnt;
  uint16_t rcnt_h;
  uint16_t rcnt_l;
  uint8_t *dat;
  uint16_t rep_judge;

  switch(re_md){
    case REMOTE_READ:
      if(digitalRead(REMOTE_DI) == LOW ){
        memset(&remote,0, sizeof(remote));
        timrepeat = TIME_WAIT;
        re_md = REMOTE_DATA;
        errflg = false;
        repeatflg = false;
      }    
      break;
    case REMOTE_DATA:
      dat = &remote.cus[0];
      rcnt_l = 0;
      while( digitalRead(REMOTE_DI) == LOW && errflg == false){
        ++rcnt_l;
        delayMicroseconds(100);
      }

      rcnt_h = 0;
      while( digitalRead(REMOTE_DI) == HIGH && errflg == false){
        ++rcnt_h;
        delayMicroseconds(100);
      }

      //Serial.print(rcnt_l);
      //Serial.print("--");        
      //Serial.println(rcnt_h);
      rep_judge = rcnt_h + rcnt_l;

      if( rep_judge >= REPEAT_CNT){
        for(uint8_t i=0; i < 33; i++){
          lcnt = 0;
          while( digitalRead(REMOTE_DI ) == LOW && errflg == false){
            ++lcnt;
            delayMicroseconds(100);
          }

          hcnt = 0;
          while( digitalRead(REMOTE_DI ) == HIGH && errflg == false){
            ++hcnt;
            if(hcnt >= 14 && i == 32){
              break; //ストップビットは1なので14以上であれば処理を停止
            }
            delayMicroseconds(100);
          }

          if(hcnt >= 10){ //10(2T)以上であれば1と判断する
            *dat = 1;
          }
          else{
            *dat = 0;
          }
  
          ++dat;
        }
                
        chkData();
        re_md = REMOTE_END;
        timrepeat = TIME_OFF;
      }
      else{
        re_md = REMOTE_END;
        repeatflg = true;
      }
      break;
    case REMOTE_END:
      if( digitalRead(REMOTE_DI ) == HIGH ){
        errflg = false;
      //btnflg = true; 
        re_md = REMOTE_READ;
      }
      break;
  }
}

/*データのチェックと変換*/
void chkData(void){
  uint8_t i;
  uint16_t cus_l=0;
  uint16_t cus_h=0;
  uint16_t cus =0;
  uint8_t data=0;
  uint8_t idata=0;
  uint8_t chk;

  for(i=0; i < 8; i++){ //16ビットのうち下位8ビット
    cus_l |= ( remote.cus[i] << i); //カスタムコード下位
  }

  for(i=0; i < 8; i++){ //16ビットのうち上位8ビット
    cus_h |= ( remote.cus[i + 8] << i); //カスタムコード上位
  }

  cus = (cus_h << 8) | cus_l; //16ビットデータを生成

  for(i=0; i < 8; i++){
    data |= ( remote.data[i] << i); //データ
    idata |= ( remote.Idata[i] << i); //反転データ
  }

  //if( (data & idata) == 0x00){ //論理積で判定
  if( (data | idata) == 0xFF){ //論理和で判定
    btnval = data;
    btnflg = true;
    Serial.print("cus:");
    Serial.print(cus,HEX);    
    Serial.print(" data:");
    Serial.print(data,HEX);
    Serial.print(" Idata:");
    Serial.print(idata,HEX);    
    Serial.println();
  }
}

/* リモコンによる処理(LED操作) */
void remoteControl(void){

  if(btnflg | repeatflg){
    btnflg = false;
    repeatflg = false;

    switch(btnval){
      case 22: //0
      ledval = TIME_OFF;
        digitalWrite(PIN_DO,LOW);
        break;
      case 12: //1
      ledval = TIME_OFF;
        digitalWrite(PIN_DO,HIGH);
        break;
      case 24: //2
        ledval = TIME_LED;
        break;
      case 94: //3
        ledval = TIME_LED*2;
        break;
      case 8: //4
        Serial.println("push-4");
        break;
      case 28: //5
        Serial.println("push-5");
        break;
      case 90: //6
        Serial.println("push-6");
        break;
      case 66: //7
        Serial.println("push-7");
        break;   
      case 82: //8
        Serial.println("push-8");
        break;
      case 74: //9
        Serial.println("push-9");
        break;                      
    }
    timled = ledval;
  }

  if( timled == TIME_UP ){
    timled = ledval;
    digitalWrite(PIN_DO, !digitalRead(PIN_DO));
  }
}

使用するリモコンによって通信フォーマットや指令値が変わるためシリアルモニターであらかじめ指令値を確認して処理を分岐するように調整する必要があります。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました