Arduino nanoを使った赤外線リモコンデータの受信器

概要

アマゾンで買ったArduino nano互換ボード (広東省東莞市からの送料込で320円) と赤外線受信モジュールを組み合わせて、家電用リモコンのコマンドコードを解析できるようにする。

以前作ったESP-WROOM-02を使った構成と同様のことを、+5V、16MHz動作のATmega328Pを載せたArduino nanoでできるようにすることが目標。nanoにはUSB-シリアル変換チップも載っており、電源もUSB経由で与えられるので少ない部品点数で作れるのがいいところだろう。

実のところ、Arduino PRO MINIやESP-WROOM-02の開発時に必要なUSB-シリアル変換ボード (AE-FT231X) を1枚しか持っていないので、リモコンの発信側と受信側を同時に作るとき、しょうしょう不便だったせいもある。

Arduino Nano互換ボードについて

今回使った互換ボードは以下のような外観をしている。

Arduino nano 互換
Arduino nano 互換ボード

ミニブレッドボードとほぼ同じ長さで、写真左端にはUSB miniBコネクタを備えている。右側の6ピンのポストはISP(In-System Programmer)を利用するときに使うSPI接続のためのもの。ピンポストは商品に含まれており、自分でハンダ付けする必要がある。

Arduino nano 互換ボード裏面
Arduino nano 互換 裏面

裏側には、左側 (USBコネクタ側) にUSB-シリアル変換チップのCH-340G、右側には定電圧レギュレータのAMS1117が載っている。AMS1117の仕様ではドロップアウトが1Vということなので、+5Vを得るには6V以上を与える必要があるだろう。表側の左下端あたりに3V3というピンが出ているが、CH-340Gが供給する+3.3Vが出力されている。残念ながらデータシートを見ても仕様が分からないので、流せる電流が不明で利用できない。

このnanoをWindows7 proが動いているPCにつなぐときには、メーカーサイトからCH340Gのドライバをダウンロードしてきてインストールする必要があったのだけど、zipファイルの中のreadme.txtは中国語で書いてあった。このあたり、ちょっと引いてしまう人もいるかもしれない。

ついでながら、nanoとPRO MINI (The Simple)の互換ボードを並べてみるとこんな感じ。

nanoとPRO MINI
nanoとPRO MINI どちらもchinese.

USB-シリアルが載っている分、nanoの方が長い。その分、両脇にアナログ入力用のピンを余計に出せている。

回路

回路図は以下のとおりで、Arduino nanoと赤外線受信モジュールのOSRB38C9AAを直結しただけ。電源はセルフパワーのUSBハブから得ることになり実にシンプル。

a_nano_ir_rx-1

この回路図をふつうのブレッドボード上で実現すると、以下のようになった。

Arduino nano 互換ボードと赤外線受信モジュール

Arduino nano(互換) と赤外線受信モジュール (OSRB38C9AA) と配線用のワイヤのみである。ブレッドボード代やワイヤ代を除いて370円。

この写真だけはOlympus Pen mini (E-PM2) に、MZD14-42EZを着けて42mm端で撮影。手ブレ補正機能が弱いので卓上三脚を使ったが、意外と寄れます。

スケッチ

ESP-WROOM-02で動いていたスケッチを持ってきて、IOピンの番号を変え、とりあえずコンパイルしたところ、

最大30,720バイトのフラッシュメモリのうち、スケッチが8,790バイト(28%)を使っています。
最大2,048バイトのRAMのうち、グローバル変数が7,382バイト(360%)を使っていて、ローカル変数で-5,334バイト使うことができます。

と表示されてしまった。5kバイト以上メモリが足りないとのこと。
Arduino (ATmega328P)は、プログラムから使えるRAMが2,048バイトしかない。それに対して、ESP8266EXでArduino Coreを使うときには、81,920バイトのRAMを利用できる。80kバイト近くも差があったとは。制約が緩いときは、大らかに心の赴くままに(?)プログラムを書いてしまうのだけど、今回は節約を意識する必要があった。

#include <math.h>
#define START_MSG "\n" + String(__FILE__) + " start."

#define IR_PIN 4
// tools/avr/avr/include/avr/sfr_defs.h:#define _BV(bit) (1 << (bit))
#define READ_IR_PIN()  (PIND & B00010000)

#define OP_LED 13
void decode_data(int count);
void setup()
{
  delay(100);
  Serial.begin(115200);
  pinMode(IR_PIN, INPUT);
  Serial.println(START_MSG);
  pinMode(OP_LED, OUTPUT);
  digitalWrite(OP_LED, 0);
}
// OSRB38C9AAは active low出力。
//  最大0.5秒間またはDATA_COUNT個の取り込みを行う。取り込み後に解析してデコードし、コード列を出力する。

typedef struct {
  unsigned short  high;  // H期間のusec
  unsigned short  low;   // L期間のusec
} irdata_t;

#define AEHA_T  425
#define NEC_T 560
#define UNKNOWN_T 500

#define DATA_COUNT  330
irdata_t irdata[DATA_COUNT];
unsigned short t_length;

// OSRB38C9AAは active low出力。
void loop()
{
  int data_count = 0;
  unsigned long start_H;
  unsigned long start_L;
  
  int state = READ_IR_PIN();
  unsigned long first_time = micros();
  if (!state) { // 最初の立下り検出
    int last_state = 0;
    data_count = 0;
    start_H = first_time;
    while(data_count < DATA_COUNT) { 
      unsigned long now = micros();  
      if (now < first_time) {
        data_count = 0;
        break;
      }
      if (now - first_time > 500000L)
        break;  // 500msec経過したらやめ。
      state = READ_IR_PIN();
      if (state == last_state)
        continue;
      last_state = state;
      if (!state) {
        irdata[data_count++].low = now - start_L;
        start_H = now;
      } else {
        irdata[data_count].high = now - start_H;
        start_L = now;
      }
    }
    // 検出終了
    if (data_count > 23) {
      // H期間の長さにより、T値を決める。
      // 最初の20個のH期間の長さが、標準T値の近傍に収まっている数が10個以上かどうか。
      int i;
      int m = 0;
      int nec = 0;
      int aeha = 0;
      for(i = 0; i < data_count - 1; i++) {
        if (i < 20) {
          int high = irdata[i].high;
          if (high > 800)
              continue;
          if (abs(high - AEHA_T) <= AEHA_T * 0.18)
            aeha++;
          if (abs(high - NEC_T) <= NEC_T * 0.15)
            nec++; 
        }
      }
      t_length = UNKNOWN_T; // どちらでもない。適当な値
      if (aeha > 10 && aeha > nec)
        t_length = AEHA_T;
      else if (nec > 10 && nec > aeha)
        t_length = NEC_T;
       Serial.println("use T = " + String(t_length) + " usec.");
       decode_data(data_count);
      }
    }
}

// indexはデータ数の範囲内であること。
void calc_t(int index, int& high, int& low) {
  high = (int) round((float)irdata[index].high / t_length);
  low = (int) round((float)irdata[index].low / t_length);
}

bool isLeader(int high, int low) {
  if ((t_length == AEHA_T && high == 8 && low == 4) ||
      (t_length == NEC_T && high == 16 && low == 8))  // リーダー検出
    return true;
  return false;        
}

bool isTrailer(int index) {
  if (irdata[index].low > 8000) 
    return true;
  return false;  
}

byte get_bit(int high, int low) {
  byte value = 0;
  if (high == 1 && low == 3)
    value = 1;
  return value;      
}

static const char str_mark[] = "\n-----";
void dump_all(int total) {
  int i;
  int high_T, low_T;
  
  for(i = 0; i < total; i++) {
    char tmp[64];
    calc_t(i, high_T, low_T);
    byte val = get_bit(high_T, low_T);
    sprintf(tmp, "%d : high = %u, low = %u, T:%u, %u, bit=%d", i + 1, irdata[i].high, irdata[i].low, high_T, low_T, val);
    Serial.println(tmp);
    if (isLeader(high_T, low_T))
      Serial.println(str_mark);
    else if (isTrailer(i))    
      Serial.println(str_mark);
    delay(1);
  }
}

void decode_data(int total) {
    bool in_frame = false;
    int index = 0;
    int high_T, low_T;

    if (total == 0)
      return;

    dump_all(total);
    Serial.println(str_mark); 
    while (index < total) {
      delay(1);
      calc_t(index, high_T, low_T);
      if (!in_frame && isLeader(high_T, low_T)) {
        in_frame = true;
        index++;
        continue;
      }
      if (!in_frame) {
        index++;
        continue;
      }
      if (isTrailer(index)) {
        index++;
        in_frame = false;
        Serial.println(str_mark);  
        continue;
      }
      if (total - index < 8)  // バイトにならない。
        break;
      byte cur = 0;
      for(int i = 0; i < 8; i++) {
        calc_t(index, high_T, low_T);
        byte value = get_bit(high_T, low_T);
        cur = cur >> 1 | (value ? 0x80 : 0);       
        index++;
      }
      char tmp[6];
      sprintf(tmp, "%02x ", cur);
      Serial.print(tmp);  
    }
    Serial.println(str_mark);  
}
スケッチについて

赤外線リモコンの信号やデータについては、以前の投稿を参照してください。

プログラム構造やデータの持ち方

setup()で初期化した後は、loop()で赤外線受信モジュールの出力の取り込み、取り込んだデータの解析とコマンドデータへのデコードを行っており、大きな流れは以前のものと同じ。ただ、使用するメモリを減らす必要があるので、取り込んだ内容を保持したり、デコードのために必要な情報を保持するための構造体の内容を変更した。

typedef struct {
  long  high;   // ↓遷移検出タイミング
  long  low;   // ↑遷移検出タイミング
  int high_T; // H持続時間(usec)
  int low_T; // L持続時間(usec)
  int value;  // 論理値 : 0 or 1
} irdata_t;

以前は上記のような具合で贅沢にいろいろと詰め込んでいた。この構造体を要素としてもつ配列がデカイので、要素サイズを縮めることにした。

typedef struct {
  unsigned short  high;  // H期間のusec
  unsigned short  low;   // L期間のusec
} irdata_t;

今回はこれだけ。longやintが4バイト、shortが2バイトなので、以前は1要素ありた20バイトだったものを4バイトにしたことになる。

データを取り込む際には、赤外線データがHだった長さとLだった長さを解釈する必要があるため、micros()によって開始からの時間を得ている。以前のスケッチではmicros()の返す値自体を記憶し、取込み終了後にH期間、L期間に直していたのだけど、今回は取り込みのループ内 (48行目のwhile()ループ )でそれぞれのμ秒数を配列に格納するようにした。また、H期間やL期間のT値への変換や論理値(ビット値)への変換も、取り込みの後に必要に応じて行うことにした。

ただ、以前のスケッチでは配列の要素数 (DATA_COUNT) を500としていたが、500×4=2000 ではリミットに達してしまうため少なくする必要があった。赤外線データを今回のような具合で格納する場合、配列の1要素が1bitに相当するので、要素数が320で40バイト分のデータが取り込めることになる。リーダーやトレーラー、ゴミなどを考慮して330とした。したがって、取り込み用の配列 ( irdata[DATA_COUNT] ) で1320バイト消費する。

上記のスケッチをコンパイルしてみると、

最大30,720バイトのフラッシュメモリのうち、スケッチが8,298バイト(27%)を使っています。
最大2,048バイトのRAMのうち、グローバル変数が1,672バイト(81%)を使っていて、ローカル変数で376バイト使うことができます。
スケッチが使用できるメモリが少なくなっています。動作が不安定になる可能性があります。

と表示された。取り込み用配列以外の1672 – 1320 = 352バイトは、他の変数や文字列定数が使っており、その中でもSerial オブジェクトが結構使っているようだ。
「動作が不安定になる可能性があります。」については、動かしてみて、さほど問題なさそうなので無視している。

少々の高速化

以前はloop() の最後に何気なく delay(1); と置いていた。そのため、赤外線受信モジュールが最初に出す立下りエッジの検出に遅れが生じてしまい、最初のリーダーの検出に失敗したりしていたが、delay()を除去することで解決。

また、受信モジュールの出力を接続したD4ピンからの読み込みについても、digitalRead(4); ではなく、(PIND & B00010000) とした。得られる値が0か1かではなく、0か16かになってしまうが、I/Oレジスタから直接読み出すようにした。今回のようなアプリケーションでは、ここで頑張っても大した違いはなさそうな気もするが。

関数の分割

大元の配列がもつ情報が減ったので、いくつかの処理を関数とし、受信データのダンプ時や最終的なコマンドデータの取得時に利用できるようにした。

  • void calc_t() は、受信データ内の指定の配列要素がもつH期間、L期間の値をT値に変換する。
  • bool isLeader() は、T値に変換済のデータ(オン期間およびオフ期間)から、それがリーダーかどうかを判断する。
  • bool isTrailer() は、受信データ内の指定の配列要素がもつL期間からトレーラーかどうかを判断する。なお、トレーラーが規定されているのは、AEHAフォーマットのみ。
  • byte get_bit() は、T値に変換済のデータから、論理値に変換して返す。1Tオンで3Tオフならば1で、それ以外は0としている。
    例えば1Tオンで2Tオフといった受信データもあるかもしれない。結果を信用するためには、void dump_all() が出力する受信内容と見比べる必要があるだろう。

きょうのまとめ

  • 得られる結果(各リモコンのコマンドデータの内容)は、前回と同様なので省略する。ただ、電機大手H社のエアコン用リモコンは52バイトのコマンドデータを出してくるので、この構成では無理。エアコン大手のD社のリモコンについては39バイトなので解析可能だった。
  • 同じ結果を得るためにスケッチの書き直しが必要になったが、以前よりシンプルな構成でリモコンデータの解析ができるようになり、Arduino IDEを仕込んだノートPCと共に外に持ち出すことも考えるようになった。
  • ESP-WROOM-02とIrLEDを組み合わせて家電リモコンのフリをする機能は既に作っているので、ヒマがあれば、環境データ(温度、湿度)や時刻に応じてエアコンや照明をコントロールする仕組みを作ってみようと思っている。
  • ESP-WROOM-02とBME280 電池駆動で温度/湿度/気圧測定」という話が途中になっているが、何か勘違いがあるのか、あるいは必然なのか、思っていた以上に早く電池が消耗した。これも知見の一つになるので近々にまとめる予定です。

追記(ミニブレッドボード版)

Arduino nanoと赤外線受信モジュールしか部品が必要ないので、更に小さいミニブレッドボードに載せることにした。そのために受信モジュールを載せる場所をちょっと変更し、以下のような回路にした。

A_NANO_IR_RX-2

スケッチ側は、読み取りのためのポート定義を変更するため最初の方にある2行を以下のように変更した。

#define IR_PIN 4
#define READ_IR_PIN()  (PIND & B00010000)

となっている部分を、

#define IR_PIN 2
#define READ_IR_PIN()  (PIND & B00000100)

とした。

Arduno Nano
リモコンデータを解析するための道具

ジャンパワイヤもArduino nanoの+5Vを赤外線受信モジュールの向かって右側の足に接続するだけで済む(写真では見えませんが)。しばしば活躍してくれます。

NECフォーマットのリピートに対応

うちにあった、リピート出力を行うリモコンに対応した。変更箇所が多くなったので、別ページに改訂したスケッチを掲載しました。リピート出力時のタイミングなどについては、WROOM-02を使った赤外線リモコンの調査 に追加。