概要

以前、ATmega328pとUSBポートを備えたArduino nanoを使って、家電用赤外線リモコンの送信データを解析する仕組みを作ったのだけど、AV機器用のリモコンデータを取り始めてみたら、NECフォーマットのリピート機能にまったく対応できていないことが分かった。

信号のタイミングについては、WROOM-02を使った赤外線リモコンの調査 という投稿内にリピート時の動作図を追加。対応する回路図は、元のページの「追記(ミニブレッドボード版)」を参照。

改訂したスケッチは以下の通り。

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

// for Arduino (ATMega328P 16MHz)
#define IR_PIN 2
#define READ_IR_PIN()  (uint8_t)(PIND & B00000100)
void decode_data(int count);

void setup()
{
  delay(100);
  Serial.begin(115200);
  pinMode(IR_PIN, INPUT);
  Serial.println(START_MSG);
  TCCR1A = 0;
  TCCR1B = 3; // x1/64 prescaler. (250kHz)
}
//  IRリモコン受信モジュールの出力をとらえて、H期間、L期間をusec単位に記憶する。
//  立下り~立上りの時間がH期間、立上り~立下りの時間がL期間となる。
// TC1をx1/64プリスケーラで動かし(250kHz)、4usecの粒度のカウンタ値を記憶する。

typedef struct {
  unsigned short  high;  // H期間のusec (4usec単位)
  unsigned short  low;   // L期間のusec (4usec単位)
} irdata_t;

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

#define DATA_COUNT  330
irdata_t irdata[DATA_COUNT] = {0};
unsigned short t_length;

const inline unsigned long get_low(int index) { return (unsigned long) irdata[index].low << 2; }
const inline unsigned long get_high(int index) { return (unsigned long) irdata[index].high << 2; }


#define TIME_LIMIT  1000000L  // 最長で1秒間とる。
void loop()
{
  int data_count;
  while(READ_IR_PIN())
    ;
  uint8_t last_state = 0;
  data_count = 0;
  unsigned long start_time = micros();
  TCNT1 = 0;
  while(data_count < DATA_COUNT) { 
    if (micros() - start_time > TIME_LIMIT)
      break; 
    uint8_t state = READ_IR_PIN();
    if (state != last_state) {
      last_state = state;
      if (!state)
        irdata[data_count++].low = TCNT1;
      else
        irdata[data_count].high = TCNT1;
      TCNT1 = 0;
    }
  }
 // 検出終了
  if (data_count > 23) {
    // H期間の長さにより、T値を決める。
    // 最初の20個のH期間の長さが、標準T値の近傍に収まっている数が10個以上かどうか。
    int nec = 0;
    int aeha = 0;
    for(int i = 0; i < data_count - 1; i++) {
      if (i < 20) {
        unsigned long high = get_high(i);
        if (high > 800)
            continue;
        if (high > AEHA_T - 45 && high < AEHA_T + 65)
          aeha++;
        if (high > NEC_T - 60 && high < NEC_T + 80)
          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)(get_high(index)) / t_length);
  low = (int) round((float)(get_low(index)) / 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) {
  return (get_low(index) > 8000) ? true : false;
}

bool isRepeat(int high, int low) {
  if (t_length == NEC_T && high == 16 && low == 4)
    return true;
  return false;    
}

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

void show_data(int start, int count) {
  Serial.print(F("\nTransfer DATA = "));
  while(count >= 8) {
    uint8_t cur = 0;
    for(int i = 0; i < 8; i++) {
      int high_T, low_T;
      calc_t(start++, high_T, low_T);
      uint8_t value = get_bit(high_T, low_T);
      cur = cur >> 1 | (value ? 0x80 : 0);       
    }
    count -= 8;
    char tmp[4];
    sprintf(tmp, "%02x ", cur);
    Serial.print(tmp);
  }
  Serial.println();
}

void decode_data(int total) {
  enum { s_none = 0, s_post_leader, s_data, s_trailer, s_repeat} state = s_none;
  int index = 0;
  int high_T, low_T;
  int data_start = 0;
  char tmp[60];

  while (index < total) {
      delay(1);
      calc_t(index, high_T, low_T);
      uint8_t val = get_bit(high_T, low_T);
    switch(state) {
    case s_none:
      if (isLeader(high_T, low_T)) {
        Serial.println(F("\n--- LEADER ---"));
        state = s_post_leader;
      } else if (isRepeat(high_T, low_T))
        Serial.println(F("\n--- REPEAT ---"));
      break;
    case s_post_leader:      
      Serial.println(F("\n--- DATA ---"));
      data_start = index;
      state = s_data;
      break;      
    case s_data:
      if (isTrailer(index)) {
        show_data(data_start, index - data_start);
        Serial.println(F("\n--- TRAILER or STOP ---"));
        state = s_none;
        data_start = 0;
      }
      break;
    }
    sprintf(tmp, "%d : high = %lu, low = %lu, T:%u, %u, bit=%d", index + 1, get_high(index), get_low(index), high_T, low_T, val);
    Serial.println(tmp);
    index++;
  }
  if (data_start > 0)
    show_data(data_start, index - data_start);
  Serial.println(F("\n---- END ----\n"));  
}

おもな変更点は以下のとおり。

  • リピート出力間隔は90msec以上あるので、μsec単位では16ビット長のデータに格納できない。そのため、4μsec単位にタイミングを得ることにした。
  • タイマカウンター1(TC1)ユニットのカウンタを使って赤外線受信機の出力レベルが変わったタイミングを記憶する。システムクロック16MHzなので、TC1のプリスケーラによって1/64として250kHzでカウントアップさせ、TCNT1の値を読み取ることにした。同時にTC1を別の用途(タイマー割込みとか)に使うことはできないことに注意。
  • 取り込み精度を上げるため、余計なことを減らした。

といったところ。