2014年5月8日木曜日

Arduinoで赤外線リモコンの学習、送信(EEPROMに保存)




後半のスケッチが長いので重いです。すいません。

以前、赤外線リモコンを受信してフォーマットの判別とデコードをしたことがあります。
Arduinoで赤外線リモコンを受信、解析してみる
Arduinoで赤外線リモコンを受信、解析してみる(2)


受信だけでなく、学習、送信機能をスケッチに追加。

さらに受信結果を配列に保存しただけでは、電源が切れると学習内容が消えてしまうので、EEPROMに保存するようにしました。

ArduinoUNOのEEPROMサイズは1KB、書き換え可能回数は10万回程度。
EEPROMに書き込まれると困るときは、スケッチをarduinoにアップロードしないでください。初回起動時に消去されます。


参考:赤外線リモコンの通信フォーマット




ユーザーインターフェースはシリアル通信によるコマンドです。
これなら、bluetoothモジュールを接続すれば、スマートフォンからターミナルソフトで操作できます。
真ん中がPC用bluetoothドングルでシリアル通信するためのモジュールSDBDT(3.3V)
(イーサネットシールドは要りません)

学習できるのは、国内で広く使われているNECフォーマット、家電製品協会(AEHA)フォーマット、SONYフォーマットの3つ。
手持ちのテレビ、BDレコーダー、DVDプレーヤー、CATVチューナー、照明、エアコンの操作は出来ました。
フォーマットが上記3つ以外のDENONのAVアンプ、パイオニアのDVDレコーダーはフォーマットが合わず使用不可。

大体うまく操作できたのですが、SHARPアクオス(AEHA)の操作が今ひとつ。
単発送信が認識されないことが多いです。(何故か電源は認識)
連続送信すると認識してくれましたが、原因の特定ができませんでした。
38KHzパルスの精度なのかなぁとは思っていますが・・・確認できる機材がありません。


シールド化
開いたスペースは赤外線LEDの増設に備えて。

◆使用部品 動作確認表示用に緑LED、赤LED
タクトボタンx3(シリアル通信のコマンドだけでも操作可能)
赤外線リモコン受信モジュール(IL-IRM0101-3+周辺部品)
赤外線LED(OSI5FU5111C-40)x3
トランジスタC1815
抵抗220Ωx2、2.2KΩx2、10Ω

◆回路

受信モジュールの周辺部品はネットにあったデータシートに書いてあったもの。
自分が購入した時の秋月付属の資料には書いてありませんでした。
モジュールの出力を直接入力しても動くとは思います。

使用した赤外線LEDはVf1.35V、If100mAです。
OSI5FU5111C-40のデータシートにはPulseWidth≦100μs,duty≦1/100であれば1000mAいけると書いています。
5V-(1.35Vx3)÷100mA=9.5Ωで、10Ωとしました。
指向性が強いようで広範囲、遠距離の送信には工夫が要りそうです。
aruduinoの1本のピンは40mAまでしか流せないので、トランジスタでスイッチングしています。

◆赤外線LEDを接続したピンのポート操作
赤外線LEDを接続したピン番号を変更する場合は、ir_pulse()のポート操作をしている部分の変更もしてください。
PORTB = PORTB | B00000001;  //8番ピンをHIGHにしている
PORTB = PORTB & B11111110;  //8番ピンをLOWにしている

arduinoUNO以外に変えた場合も変更が必要かもしれません。


◆EEPROM
スケッチデフォルトでは、1パターンあたり50バイト、20パターン分でEEPROMを使用。
NEC:4バイト固定
AEHA:可変長
(家にあるエアコンは13バイトと33バイト。照明は5バイト。それ以外の家電は6バイトでした。)
SONY:7ビット+5/8/13ビット

赤外線コードが一般的に長いエアコンを使わなければ、10バイトx100パターンでも可能だと思いますが実験はしてません。(使用領域は1000Bほどに抑えてください)
エアコン本体とリモコン間で双方向通信が完了しないといけないタイプはたぶん操作できません。

EEPROMとスケッチを書き込む領域は違うので、違うスケッチを書き込んでもEEPROMの内容は保持されます。


◆data_buff[]、EEPROMの中身
EEPROMアドレス0:最大赤外線パターン数
EEPROMアドレス1:1パターンあたりの使用量(赤外線コード長+管理用2バイト)
初回起動時とスケッチ内の上記値を変更してのスケッチアップロード
       → EEPROM消去

[フォーマット][データ長][データ0]・・・[データn]
フォーマット
  0:不明
  1:NEC
  2:AEHA
  3:SONY

NEC、AEHAの時、データ長の単位はバイト
SONYの時データ長の単位はビット


◆学習方法
・シリアルポートからコマンドを送る(改行コードが必要です)
「9」に続けてパターン番号を送るとその番号で保存されます。
例 915 (パターン15番で保存する )
    >赤LED点灯(点灯中、送信/キャンセルボタンで学習キャンセル)
・受信モジュールに向けて赤外線送信
 学習成功 >緑LED点灯、消灯
 学習失敗 >赤、緑LED点灯、消灯


◆送信方法
・シリアルポートからコマンドを送る(改行コードが必要です)
「0」に続けてパターン番号を送るとその番号で送信されます。
例 015
パターン15番を送信する  >緑LED点灯、消灯

送信についてはタクトスイッチでも操作できます。
・ボタン0を押すごとにパターン番号が増加
・ボタン1を押すごとにパターン番号が減少
・送信ボタンで送信    >緑LED点灯、消灯

学習していないパターン番号 >赤、緑LED点灯、消灯

◆EEPROMクリア
・シリアルポートからコマンド「clear」を送る(改行コードが必要です)
             >数秒後消去

◆EEPROMダンプ
・シリアルポートからコマンド「dump」を送る(改行コードが必要です)
             >EEPROMの内容を出力

◆スケッチ

// Learn, Send and Save IR_remote_code in EEPROM by cranberry
// http://cranberrytree.blogspot.jp/2014/05/arduinoeeprom.html
//                           2014/5/8

#include <EEPROM.h>

byte pin_button_send = A0;   //送信ボタン、学習キャンセルボタン
byte pin_button_0 = A1;      //パターン番号増加ボタン
byte pin_button_1 = A2;      //パターン番号減少ボタン
byte pin_green = 6;          //動作確認用 緑LED
byte pin_red = 7;            //動作確認用 赤LED

byte pin_ir_rcv = 2;                 //赤外線受信モジュールを接続するピン
byte pin_ir_send = 8;                //赤外線LEDを接続するピン ※ir_pulse()内も変更すること
unsigned long st;                    //計測開始時間μs
unsigned long dur;                   //時間μs
long timeout = 10000;                //リピートコード無視、無受信時のタイムアウトμs

int cnt_pulse = 0;                   //38KHzのサブキャリア信号を生成するための回数
float cyc = 26.3;                    //1サイクル26.3μs
int t[] = {0, 562, 425, 600};        //1Tの時間μs  (dummy, NEC, AEHA, SONY)
byte ir_format = 0;                  //0:UNKNOWN 1:NEC 2:AEHA 3:SONY

int ir_num = 0;                      //パターン番号
int array_x_max = 50;                //1パターンあたりの使用バイト数 4以下禁止
byte num_pattern_max = 20;           //学習させる赤外線パターン数  array_x_max * num_pattern_max が1000バイト以下ぐらいに
byte data_buff[255];                 //EEPROM-SRAM間の橋渡し
//ex){ 0x1, 0x4, 0x40, 0xBF, 0x12, 0xED },  //東芝の古いテレビの電源 ON/OFF
//内容:フォーマット(0~3)、赤外線コード長(バイトorビット)、赤外線コード
//赤外線コード長は NEC,AEHA:バイト数、SONY:ビット数

int b_cnt = 0;                       //ユーザーインターフェース用パターン番号
String cmd;                          //シリアルから読み込んだものを改行コードまで連結したもの

void setup() {
  Serial.begin(9600);
  pinMode(pin_button_send, INPUT_PULLUP);
  pinMode(pin_button_0, INPUT_PULLUP);
  pinMode(pin_button_1, INPUT_PULLUP);
  pinMode(pin_ir_rcv, INPUT);
  pinMode(pin_ir_send, OUTPUT);
  digitalWrite(pin_ir_send,LOW);
  pinMode(pin_green, OUTPUT);
  pinMode(pin_red, OUTPUT);
  error_led();
  check_array_size();
}

void loop() {
  if(Serial.available()){                    //バッファにデータがあれば
    read_serial();
  }
  if(digitalRead(pin_button_send) == LOW){    //送信ボタンが押されたら
    digitalWrite(pin_green, HIGH);
    pressed_button_send();
    delay(200);
    digitalWrite(pin_green, LOW);
  }
  if(digitalRead(pin_button_0) == LOW){       //ボタン0が押されたら番号増加
    b_cnt += 1;
    if(b_cnt >= num_pattern_max || b_cnt < 0){
      b_cnt = 0;
    }
    Serial.print("number: ");
    Serial.println(b_cnt);
    delay(200);
  }
  if(digitalRead(pin_button_1) == LOW){       //ボタン1が押されたら番号減少
    b_cnt -= 1;
    if(b_cnt <= -1){
      b_cnt = num_pattern_max - 1;
    }
    Serial.print("number: ");
    Serial.println(b_cnt);
    delay(200);
  }
}

void pressed_button_send(){
  send_ir(b_cnt);
  output_array();
}

void start_learn(){
  Serial.println("waiting IR.....");
  boolean f_cancel = false;
  digitalWrite(pin_red, HIGH);
  rcv_ir(b_cnt);                       //リーダー部受信開始 パターン番号を引数にする
  rcv_data();                          //データ部受信開始
  output_array();                      //配列記述用出力
  if(ir_format != 0){                  //フォーマット判明時
    digitalWrite(pin_red, LOW);
    digitalWrite(pin_green, HIGH);
    delay(1000);
    digitalWrite(pin_green, LOW);
  }
  else{
    error_led();                       //受信信号フォーマット不明
  }
}

void read_serial(){
  char c = Serial.read();               //1バイト読み込む
                                         //CR+LF,CRのみ,LFのみ 対応
  if( cmd == "" && c == 0x0A ){          //改行コード0x0A(LF)
    cmd = "";
  }
  else if(c == 0x0D || c == 0x0A){       //改行コード0x0D(CR) or 0x0A(LF)
    Serial.print("command = \t");
    Serial.println(cmd);
    check_cmd();
    cmd = "";
  }
  else{
    cmd += String(c);
  }
}

void check_cmd(){
  boolean f_all_digit = true;
  for(byte i = 0; i < cmd.length(); i++){
    if( isDigit( cmd.charAt(i)) == 0){                 //全文字数字かチェック
      f_all_digit = false;
      break;
    }
  }
  if(f_all_digit == true){
    if(cmd.length() >= 2 && cmd.charAt(0) == '0'){
      Serial.print("send IR pattern No.");            //0X パターン番号Xを送信
      b_cnt = cmd.substring(1).toInt();                //String to Int
      Serial.println( b_cnt );
      if(b_cnt < 0 || b_cnt > num_pattern_max - 1){
        Serial.println("invalid number");
        Serial.print("maximum ");
        Serial.println(num_pattern_max - 1);
      }
      else{
        pressed_button_send();                //指定パターン番号(b_cnt)で送信
      }
    }
    else if(cmd.length() >= 2 && cmd.charAt(0) == '9'){
      Serial.print("learn IR pattern No.");           //9X パターン番号Xに学習
      b_cnt = cmd.substring(1).toInt();                //String to Int
      Serial.println( b_cnt );
      if(b_cnt < 0 || b_cnt > num_pattern_max - 1){
        Serial.println("invalid number");
        Serial.print("maximum ");
        Serial.println(num_pattern_max - 1);
      }
      else{
        start_learn();                        //指定パターン番号(b_cnt)で学習
      }
    }
  }
  if(cmd == "clear"){                         //EEPROMクリアコマンド
    Serial.println("EEPROM clearing...");
    eeprom_clear();
  }
  if(cmd == "dump"){                          //EEPROMダンプコマンド
    Serial.println("EEPROM DUMP");
    eeprom_dump();
  }
}

void error_led(){
  digitalWrite(pin_green, HIGH);
  digitalWrite(pin_red, HIGH);
  delay(1000);
  digitalWrite(pin_green, LOW);
  digitalWrite(pin_red, LOW);
}

void send_ir(byte ir_num_send){            //ir_num_sendは送信したいパターン番号
  ir_num = ir_num_send;
  eeprom_to_buff();                        //パターンをEEPROMからバッファへ
  if(data_buff[0]==3){                     //SONYフォーマットはヘッダー部を含め同じコードを3回送る
    for(byte i=0; i<3; i++){
      unsigned long st_sony = millis();
      send_leader();                       //リーダー部送信
      send_data();                         //データ(リーダー部以降)送信
      while(millis() - st_sony < 45 ){}    //送信開始から次の送信開始まで45ms
    }
  }
  else if(data_buff[0] == 1 || data_buff[0] == 2){//NEC,AEHAフォーマットでの送信
    send_leader();                         //リーダー部送信
    send_data();                           //データ(リーダー部以降)送信
  }
  else{                                    //フォーマット不明か空データ
    error_led();
  }
}

void send_leader(){                                    //リーダー部を送信する
  switch(data_buff[0]){
    case 0:                                            //UNKNOWNフォーマット
      break;
    case 1:                                            //NECフォーマット
      cnt_pulse = 16 * t[data_buff[0]] / cyc;          //リーダー部 ON 16T
      for(int i = 0; i < cnt_pulse; i++){
        ir_pulse();
      }
      delayMicroseconds( 8 * t[data_buff[0]] );        //リーダー部 OFF 8T
      break;
    case 2:                                            //AEHAフォーマット
      cnt_pulse = 8 * t[data_buff[0]] / cyc;           //リーダー部 ON 8T
      for(int i = 0; i < cnt_pulse; i++){
        ir_pulse();
      }
      delayMicroseconds( 4 * t[data_buff[0]] );        //リーダー部 OFF 4T
      break;
    case 3:                                            //SONYフォーマット
      cnt_pulse = 4 * t[data_buff[0]] / cyc;           //リーダー部 ON 4T
      for(int i = 0; i < cnt_pulse; i++){
        ir_pulse();
      }
      break;
    default:
      Serial.println("");
  }
}

void ir_pulse(){                   //38KHz 1/3duty → 26.3μs ≒ 8.77μs + 17.5μs
  PORTB = PORTB | B00000001;                       //8番ピンをHIGHにしている
//  digitalWrite(pin_ir_send,HIGH);
  delayMicroseconds(9);
  PORTB = PORTB & B11111110;                       //8番ピンをLOWにしている
//  digitalWrite(pin_ir_send,LOW);
  delayMicroseconds(17);
}

void send_data(){
  if( (data_buff[0] == 1) || (data_buff[0] == 2) ){                //NEC,AEHAフォーマットでの送信
    cnt_pulse = t[data_buff[0]] / cyc;                             //1Tのパルス数
    for(byte cnt_byte = 1; cnt_byte <= data_buff[1]; cnt_byte++){  //バイト数のループ
      for(byte cnt_bit = 0; cnt_bit < 8; cnt_bit++){               //bit数のループ
        for(int i = 0; i < cnt_pulse; i++){
          ir_pulse();
        }
        if( bitRead( data_buff[cnt_byte + 1], cnt_bit) == 1 ){     //LSBファーストで1bit読み取り
          delayMicroseconds( 3 * t[data_buff[0]] );                //1なら3Tの間LED OFF
        }
        else{
          delayMicroseconds( t[data_buff[0]] );                    //0なら1Tの間LED OFF
        }
      }
    }
    for(int i = 0; i < cnt_pulse; i++){
      ir_pulse();
    }
  }
  if( data_buff[0] == 3){                                                 //SONYフォーマットでの送信
    byte digit_sony = 1;                                                  //送信ビット数のカウント
    cnt_pulse = t[data_buff[0]] / cyc;                                    //1Tでのパルスの回数
    for(byte cnt_byte = 1; cnt_byte <= data_buff[1] / 8 + 1; cnt_byte++){ //バイト数のループ
      for(byte cnt_bit = 0; cnt_bit < 8; cnt_bit++){                      //bit数のループ
        delayMicroseconds( t[ data_buff[0] ]);                            //1Tの間LED OFF
        if( bitRead( data_buff[cnt_byte + 1], cnt_bit) == 1 ){            //LSBファーストで1bit読み取り
          for(int i = 0; i < 2 * cnt_pulse; i++){                         //2Tの間パルス送信
            ir_pulse();
          }
        }
        else{
          for(int i = 0; i < cnt_pulse; i++){                             //1Tの間パルス送信
            ir_pulse();
          }
        }
        digit_sony += 1;
        if(digit_sony > data_buff[1]){
          break;
        }
      }
    }
  }
}

void rcv_ir(byte ir_num_rcv){                        //リーダー部でフォーマットを判定する ir_num_rcvはパターン番号
  ir_num = ir_num_rcv;
  boolean f_cancel = false;
  while(digitalRead(pin_ir_rcv) == HIGH){            //受信が始まるまでループ
    if(digitalRead(pin_button_send) == LOW){         //学習のキャンセル
      Serial.println("Canceled");
      f_cancel = true;
      delay(200);
      digitalWrite(pin_red, LOW);
      break;
    }
  }
  if(f_cancel == false){
    st = micros();                                     //計測開始の時間を記憶
    wait_change_sensor_output(LOW);                    //リーダー部の時間計測開始
    dur = micros() - st;
    Serial.println("");
    if( (dur >= 15*t[1]) && (dur <= 17*t[1]) ){        //NECフォーマットかどうか +-1Tで判断
      ir_format = 1;
      wait_change_sensor_output(HIGH);                 //リーダー部の終了を待つ
      Serial.println("NEC FORMAT");
    }
    else if( (dur >= 7*t[2]) && (dur <= 9*t[2]) ){     //AEHAフォーマットかどうか +-1Tで判断
      ir_format = 2;
      wait_change_sensor_output(HIGH);                 //リーダー部の終了を待つ
      Serial.println("AEHA FORMAT");
    }
    else if( (dur >= 3*t[3]) && (dur <= 5*t[3]) ){     //SONYフォーマットかどうか +-1Tで判断
      ir_format = 3;
      Serial.println("SONY FORMAT");
    }
    else{
      ir_format = 0;
      Serial.println("UNKNOWN FORMAT or NOISE");
    }
  }
}

void wait_change_sensor_output(boolean output){
  while(digitalRead(pin_ir_rcv) == output){
  }
}

void rcv_data(){                                //フォーマットごとに0,1を判定する
  data_buff[0] = ir_format;                     //バッファ配列の先頭にフォーマットを保存
  int num = 2;                                  //バイト数をカウント
  byte digit = 0;                               //8ビットごとに区切るためのカウント
  byte data_temp = 0;                           //受信データを一時的に格納
  boolean f_data_end = false;                   //受信が終わったかのフラグ
  if(ir_format == 1 || ir_format == 2){         //NEC、AEHAフォーマット
    while( f_data_end == false){
      wait_change_sensor_output(LOW);           //HIGHになるのを待つ
      st = micros();                            //赤外線モジュールの出力HIGHの時間を測る
      while(digitalRead(pin_ir_rcv) == HIGH){
        if( micros() - st > timeout ){          //タイムアウト
          f_data_end = true;
          break;
        }
      }
      dur = micros() - st;
      if( (dur <= 2 * t[ir_format]) ){          //2T以下なら0
//        Serial.print("0");
      }
      if( (dur > 2 * t[ir_format]) && (dur <= 4 * t[ir_format]) ){   //2T~4Tなら1
        bitSet(data_temp,digit);                //LSBファーストなので受信した順番に最下位ビットから埋めていく
//        Serial.print("1");
      }
      digit += 1;
      if(digit == 8){                           //8bitごとに16進数に変換
        if(num <= 255){                         //バッファサイズ以下であれば保存
          data_buff[num] = data_temp;
        }
        num += 1;
        digit = 0;
        data_temp = 0;
      }
    }
    Serial.print("recieved ");
    data_buff[1] = num - 2;                     //配列2つ目にデータ長(バイト)を格納
    Serial.print(data_buff[1]);
    Serial.println(" byte");
    if(num > array_x_max){                      //受信コードが長すぎた
      Serial.println("too long IR code.");
      error_led();
    }
  }
  if(ir_format == 3){                           //SONYフォーマット
    byte digit_sony = 1;                        //データ長をビット数でカウント
    while( f_data_end == false){
      while(digitalRead(pin_ir_rcv) == HIGH){
        if( micros() - st > timeout ){          //タイムアウト
          f_data_end = true;
          break;
        }
      }
      if( f_data_end == false){
        st = micros();                          //赤外線モジュールの出力LOWの時間を測る
        wait_change_sensor_output(LOW);         //HIGHになるのを待つ
        dur = micros() - st;
        if( (dur <= 1.5 * t[ir_format]) ){      //1.5T以下なら0
//          Serial.print("0");
        }
        if( (dur > 1.5 * t[ir_format]) && (dur <= 2.5 * t[ir_format]) ){  //1.5T~2.5Tなら1
          bitSet(data_temp,digit);              //LSBファーストなので受信した順番に最下位ビットから埋めていく
//          Serial.print("1");
        }
        digit += 1;
        digit_sony += 1;
        if(digit == 8){                         //8bitごとに16進数に変換
          data_buff[num] = data_temp;
          num += 1;
          digit = 0;
          data_temp = 0;
        }
      }
    }
    data_buff[num] = data_temp;              //sonyフォーマットは7bit + 5/8/13bitで中途半端
    num += 1;
    data_buff[1] = digit_sony - 1;           //配列2つ目にデータ長(ビット)を格納
    Serial.print("recieved ");
    Serial.print(data_buff[1]);
    Serial.println(" bit");
  }
  if(ir_format > 0 && data_buff[1] <= array_x_max - 2 && data_buff[1] != 0){
    buff_to_eeprom();
  }
}

void output_array(){                            //学習内容確認用
  byte len_data;
  Serial.print("[ ");
  Serial.print(ir_num);
  Serial.print(" ]  ");
  Serial.print("{ ");
  if(data_buff[0] == 3){
    len_data = data_buff[1] / 8 + 1 + 2;
  }
  else{
    len_data = data_buff[1] + 2;
  }
  for(byte i = 0; i < len_data; i++){
    Serial.print("0x");
    Serial.print(data_buff[i], HEX);
    if( i < len_data - 1){
      Serial.print(", ");
    }
  }
  Serial.println(" }");
}

void buff_to_eeprom(){                       //EEPROMに保存する
  if( data_buff[1] <= array_x_max - 2 ){     //赤外線コードの長さが範囲内であれば
    for(int i = 0; i < data_buff[1] + 2; i++){
      EEPROM.write(array_x_max * ir_num + 2 + i, data_buff[i]);
    }
  }
}

void eeprom_to_buff(){
  for(int i = 0; i < EEPROM.read(array_x_max * ir_num + 2 + 1) + 2; i++){
    data_buff[i] = EEPROM.read(array_x_max * ir_num + 2 + i);
  }
}

void check_array_size(){     //パターン数やパターンあたりの使用バイト数を変更した時には、EEPROMをクリアする
  if( num_pattern_max != EEPROM.read(0) || array_x_max != EEPROM.read(1) ){
    Serial.println("array size is changed.");
    EEPROM.write(0, num_pattern_max);
    EEPROM.write(1, array_x_max);
    eeprom_clear();
  }
}

void eeprom_clear(){
  for (int i = 2; i < 1024; i++){
    EEPROM.write(i, 0);
  }
  Serial.println("EEPROM is Cleared.");
}

void eeprom_dump(){
  for(int i = 0; i < 1024; i++){
    Serial.print( i );
    Serial.print("\t\t");
    Serial.println( EEPROM.read(i), HEX );
  }
}





twitterからarduinoに指示を出すのスケッチと上記スケッチを合わせたところ、arduinoUNOではSRAM不足のようで正常に動きませんでした。
SRAM2.5KBのダ・ヴィンチ32Uにアップロードしたところ、動きました。
ツイッターからのコマンドで赤外線の送信、テレビの電源操作には成功。 
ちなみにスケッチは616行、コンパイル後のサイズは28,424Bでした。



環境:arduinoUNO、arduinoIDE1.0.5、win7(64)



0 件のコメント:

コメントを投稿