M5StickC(ESP32)の赤外線(RMT)受信を調べた

※本ブログは現時点での情報です、最新情報はM5StickC非公式日本語リファレンスを参照してください。

赤外線送信を実験するために、受信をまずは調べました。思ったより情報が少なくてハマりました。。。

関数について

driver/rmt.hを利用するもの

rmt_driver_install()で利用を開始するものです。一般的な作例は、すべてこちらの関数群を利用していました。ESP-IDFで使われている関数群なのでリファレンスやサンプルがそれなりにあります。

利用するためには明示的にinclude “driver/rmt.h”を追加する必要があります。

esp32-hal-rmt.hを利用するもの

Arduino IDEで宣言なしで利用できる関数群です。analogRead()などのArduino互換のための関数群で、一般的にはこちらのほうが標準的なはずですが、rmt関係に関してはまったく情報がないです!

最初動かないと思って、諦めかけたのですが内部ソースをじっくり解読してなんとか受信ができました。

環境

センサーとリモコンはElegooのセットに入っていた物を利用しました。

センサー単体は非常に安いのですが、データシートを見た限り抵抗などを繋げないといけないので、モジュールになっているものを利用したほうが楽そうでした。

pinは26にデータを、電源は3.3Vに接続し、あとはGND同士を繋げています。

リモコンがあったほうがテストがしやすいので、キット付属のリモコンを使いました。どんなリモコンでもいいのですが、リモコンによって送信フォーマットが違うので注意してください。

この付属リモコンはNECフォーマットでした。

サンプルスケッチ

driver/rmt.hの場合

#include "driver/rmt.h"

RingbufHandle_t buffer = NULL;  // リングバッファ

void setup() {
  Serial.begin(115200);

  setCpuFrequencyMhz(getXtalFrequencyMhz() / 4);   // CPU周波数(最低速に設定)

  // 赤外線リモコン初期設定
  rmt_config_t rmtConfig;
  rmtConfig.rmt_mode = RMT_MODE_RX;                // 受信
  rmtConfig.channel = RMT_CHANNEL_0;               // チャンネル0(0-3:受信, 4-7:送信)
  rmtConfig.clk_div = getApbFrequency() / 1000000; // ペリフェラル周波数
  rmtConfig.gpio_num = GPIO_NUM_26;                // 26pinに赤外線レシーバーを接続
  rmtConfig.mem_block_num = 1;                     // メモリブロック数(1-255:1ブロックあたり64ペアの送受信)
  rmtConfig.rx_config.filter_en = 1;               // フィルター有効フラグ(ONに設定)
  rmtConfig.rx_config.filter_ticks_thresh = 255;   // フィルターしきい値(この設定より短いパルスを除外)
  rmtConfig.rx_config.idle_threshold = 10000;      // フィルターしきい値(この設定より長いパルスを除外)

  // 初期化
  rmt_config(&rmtConfig);                          // RMTコンフィグ設定
  rmt_driver_install(rmtConfig.channel, 2048, 0);  // RMTドライバー初期化

  // 受信スタート
  rmt_get_ringbuf_handle(RMT_CHANNEL_0, &buffer);
  rmt_rx_start(RMT_CHANNEL_0, 1);
}

void loop() {
  size_t rxSize = 0;

  // リングバッファ取得
  rmt_data_t *item = (rmt_data_t *)xRingbufferReceive(buffer, &rxSize, 10000);

  // データがある場合処理
  if (item) {
    // 受信した生データを出力
    Serial.printf("receive_data(size:%d) :\n", rxSize);
    for (int i = 0 ; i < rxSize ; i++) {
      Serial.printf(" %d %d %d %d\n", item[i].duration0, item[i].level0, item[i].duration1, item[i].level1 );
    }
    Serial.println();

    // リングバッファ開放
    vRingbufferReturnItem(buffer, (void*) item);
  }
}

非常にサンプルがたくさんあるので、サクッと組めます。

チェンネルは0-8まであって受信は0-3の空いている場所を指定します。clk_divがわかりにくいのですが、 サンプルのコードだと80固定が多かったですが、CPU速度を落として、ペリフェラル周波数を下げると不安定になったので、ペリフェラル周波数を指定するのが正しい気がします。

フィルターは2種類あって、短いパルスデータを除外するfilter_ticks_threshと長いパルスデータを除外するidle_thresholdがあります。

ノイズとして短いパルスを受信しちゃうことがあるので、filter_ticks_threshは設定したほうがいいです。NECフォーマットだと最低でも562us以上の長さなので255以下を除外しています。

長い方は、受信完了を識別するための長さになります。NECフォーマットだとリーダーが最長で9000usですので、10000を指定している例が多かったです。この数字を大きくすると、ボタンを押している間に送信されるリピートも受信してしまって、ボタンを離すまで1つの受信データとして処理されます。

利用するリモコンによって、フォーマットが違うのでこの辺の数字は用途に合わせて変更しましょう。最初は生データで受信確認してみてから、フォーマットを調べて実装したほうがよいと思います。

driver/rmt.hの場合(NECフォーマットのパース)

#include "driver/rmt.h"

RingbufHandle_t buffer = NULL;  // リングバッファ

// 赤外線リモコンデータ構造体
typedef struct {
  int type;
  int repeat;
  uint16_t customer;
  uint8_t data;
} IrData;

void setup() {
  Serial.begin(115200);

  setCpuFrequencyMhz(getXtalFrequencyMhz() / 4);   // CPU周波数(最低速に設定)

  // 赤外線リモコン初期設定
  rmt_config_t rmtConfig;
  rmtConfig.rmt_mode = RMT_MODE_RX;                // 受信
  rmtConfig.channel = RMT_CHANNEL_0;               // チャンネル0(0-3:受信, 4-7:送信)
  rmtConfig.clk_div = getApbFrequency() / 1000000; // ペリフェラル周波数
  rmtConfig.gpio_num = GPIO_NUM_26;                // 26pinに赤外線レシーバーを接続
  rmtConfig.mem_block_num = 1;                     // メモリブロック数(1-255:1ブロックあたり64ペアの送受信)
  rmtConfig.rx_config.filter_en = 1;               // フィルター有効フラグ(ONに設定)
  rmtConfig.rx_config.filter_ticks_thresh = 255;   // フィルターしきい値(この設定より短いパルスを除外)
  rmtConfig.rx_config.idle_threshold = 10000;      // フィルターしきい値(この設定より長いパルスを除外)

  // 初期化
  rmt_config(&amp;rmtConfig);                          // RMTコンフィグ設定
  rmt_driver_install(rmtConfig.channel, 2048, 0);  // RMTドライバー初期化

  // 受信スタート
  rmt_get_ringbuf_handle(RMT_CHANNEL_0, &amp;buffer);
  rmt_rx_start(RMT_CHANNEL_0, 1);
}

void loop() {
  size_t rxSize = 0;

  // リングバッファ取得
  rmt_data_t *item = (rmt_data_t *)xRingbufferReceive(buffer, &amp;rxSize, 10000);

  // データがある場合処理
  if (item) {
    // データパース
    IrData data;
    parseIr( item, rxSize, &amp;data );

    // 出力(type=0は解析エラー)
    Serial.printf( "Recv(%d)\n", rxSize );
    Serial.printf( " type     : %d\n", data.type );
    Serial.printf( " repeat   : %d\n", data.repeat );
    Serial.printf( " customer : %04x\n", data.customer );
    Serial.printf( " data     : %02x\n", data.data );
    Serial.printf( "\n" );

    // リングバッファ開放
    vRingbufferReturnItem(buffer, (void*) item);
  }
}

// リモコンデータ1Byte解析
uint8_t parseData(rmt_data_t *item ) {
  // 8bit分処理をする
  uint8_t data = 0;
  for ( int i = 0 ; i < 8 ; i++ ) {
    int t3time = item[i].duration0 * 3;         // 3Tの時間
    int errorRange = t3time * 0.3;              // 30%までの誤差許容

    if ( abs( item[i].duration1 - t3time ) < errorRange ) {
      data += 1 << i;
    }
  }

  return data;
}

// リモコンデータ解析
void parseIr(rmt_data_t *item, int rxSize, IrData* data) {
  int necframe = 9000;
  int errorRange = necframe * 0.3; // 30%までの誤差許容

  // 判定誤差より小さければNEC
  if ( abs( item[0].duration0 - necframe ) < errorRange ) {
    data->type = 1; // NEC

    // リピートチェック(長さはフレームの半分)
    necframe /= 2;
    errorRange /= 2;
    if ( abs( item[0].duration1 - necframe ) < errorRange ) {
      // リピートでは無いデータ
      data->repeat = 0;
    } else {
      // リピートなのでここで終わり
      data->repeat = 1;
      return;
    }
  }
  item++;

  // データ解析
  if ( data->type == 1 ) {
    // NEC

    // カスタマー取得
    data->customer = parseData(item) << 8;
    item += 8;
    data->customer += parseData(item);
    item += 8;

    // データ取得
    uint8_t data1 = parseData(item);
    item += 8;
    uint8_t data2 = parseData(item);
    item += 8;

    // データ整合性確認
    if ( data1 == ( data2 ^ 0xff ) ) {
      data->data = data1;
    } else {
      // データがおかしいので全部0にセット
      memset( data, 0, sizeof( IrData ) );
    }
  }
}

ざっくりとパースしてみました。

誤差をどこまで許容するかは環境によって難しいところです。広げすぎると、似たようなフォーマットのリモコンを間違ってパースしてしまうことになります。

esp32-hal-rmt.hの場合

rmt_obj_t* rmt_recv = NULL;
int realTick;

// 受信コールバック関数
void receive_data(uint32_t *data, size_t len) {
  rmt_data_t* it = (rmt_data_t*)data;

  // 長さ0は処理しない
  if ( len == 0 ) {
    return;
  }

  // 受信した生データを出力
  Serial.printf("receive_data(size:%d) :\n", len);
  for (int i = 0 ; i < len ; i++) {
    Serial.printf(" %d %d %d %d\n", it[i].duration0 * realTick, it[i].level0, it[i].duration1 * realTick, it[i].level1 );
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);

  // 初期化
  if ((rmt_recv = rmtInit(GPIO_NUM_26, false, RMT_MEM_192)) != NULL) {
    Serial.println("Init Receiver");
  }

  // 1Tickを80usに設定
  realTick = rmtSetTick(rmt_recv, 80000) / 1000;
  Serial.printf("real tick set to: %dus\n", realTick);

  // フィルターしきい値(この設定より短いパルスを除外)
  rmtSetFilter(rmt_recv, true, 255);

  // フィルターしきい値(この設定より長いパルスを除外)
  rmtSetRxThreshold(rmt_recv, 10000 / realTick );

  // 受信開始
  rmtRead(rmt_recv, receive_data);
}

void loop() {
  delay(500);
}

コールバック以外の受信関数はなんと受信したデータサイズがわかりません!

受信も安定していないので、rmtRead()以外の受信関数は使わないほうがいいと思います。そしてrmtRead()も使わないほうが、、、

コード自体は非常にすっきりして、使いやすそうにみえます。しかしながらここまで来るのに相当苦労しました。

まずrmtSetTick()関数が非常に重要で、オフィシャルのサンプルスケッチだと80とか100が指定されていますが、その値だとちゃんと受信できません。80000を指定すると内部で1000で割れれて、80になってdriver/rmt.hと同じような動きになりました。

ちなみにこちらはCPU速度を落としても、動きに影響がなかったので80000固定で大丈夫そうです。

driver/rmt.hの場合には受信データはus単位の実時間が戻ってきましたが、esp32-hal-rmt.hの場合にはTick数が戻って来ますので、実時間にするにはTickの単位をかける必要があります。rmtSetTick()の戻り値はnsなので1000で割ってusにしてからduration0 * realTickで実時間に変換しています。

rmtSetRxThreshold()関数もTick単位みたいで10000を指定すると、内部で800000us(0.8s)となり、後続のリピートの受信までしてしまうのと、最終受信から0.8s経過後に受信コールバックが呼び出されるので、非常にレスポンスが悪くなります。

rmtSetFilter()関数は255のままで動いているので、そのまま設定しています。このフィルターがないと、照明などのノイズをたまに拾ってしまうことがあるので、何らかの数値を設定したほうがいいと思います。

esp32-hal-rmt.hの場合 (NECフォーマットのパース)

rmt_obj_t* rmt_recv = NULL;
int realTick;

// 赤外線リモコンデータ構造体
typedef struct {
  int type;
  int repeat;
  uint16_t customer;
  uint8_t data;
} IrData;

// 受信コールバック関数
void receive_data(uint32_t *data, size_t len) {
  rmt_data_t* it = (rmt_data_t*)data;

  // 長さ2未満は処理しない
  if ( len < 2 ) {
    return;
  }

  // データパース
  IrData irdata;
  parseIr( it, len, &amp;irdata );

  // 出力(type=0は解析エラー)
  Serial.printf( "Recv(%d)\n", len );
  Serial.printf( " type     : %d\n", irdata.type );
  Serial.printf( " repeat   : %d\n", irdata.repeat );
  Serial.printf( " customer : %04x\n", irdata.customer );
  Serial.printf( " data     : %02x\n", irdata.data );
  Serial.printf( "\n" );
}

// リモコンデータ1Byte解析
uint8_t parseData(rmt_data_t *item ) {
  // 8bit分処理をする
  uint8_t data = 0;
  for ( int i = 0 ; i < 8 ; i++ ) {
    int t3time = item[i].duration0 * 3;         // 3Tの時間
    int errorRange = t3time * 0.3;              // 30%までの誤差許容

    if( abs( item[i].duration1 - t3time ) < errorRange ){
      data += 1 << i;
    }
  }

  return data;
}

// リモコンデータ解析
void parseIr(rmt_data_t *item, int rxSize, IrData* data) {
  int necframe = 9000 / realTick;
  int errorRange = necframe * 0.3; // 30%までの誤差許容

  // 判定誤差より小さければNEC
  if( abs( item[0].duration0 - necframe ) < errorRange ){
    data->type = 1; // NEC

    // リピートチェック(長さはリーダーの半分)
    necframe /= 2;
    errorRange /= 2;
    if( abs( item[0].duration1 - necframe ) < errorRange ){
      // リピートでは無いデータ
      data->repeat = 0;
    } else {
      // リピートなのでここで終わり
      data->repeat = 1;
      return;
    }
  }
  item++;

  // データ解析
  if ( data->type == 1 ) {
    // NEC

    // カスタマー取得
    data->customer = parseData(item) << 8;
    item += 8;
    data->customer += parseData(item);
    item += 8;

    // データ取得
    uint8_t data1 = parseData(item);
    item += 8;
    uint8_t data2 = parseData(item);
    item += 8;

    // データ整合性確認
    if ( data1 == ( data2 ^ 0xff ) ) {
      data->data = data1;
    } else {
      // データがおかしいので全部0にセット
      memset( data, 0, sizeof( IrData ) );
    }
  }
}

void setup() {
  Serial.begin(115200);

  // 初期化
  if ((rmt_recv = rmtInit(GPIO_NUM_26, false, RMT_MEM_192)) != NULL) {
    Serial.println("Init Receiver");
  }

  // 1Tickを80usに設定
  realTick = rmtSetTick(rmt_recv, 80000) / 1000;
  Serial.printf("real tick set to: %dus\n", realTick);

  // フィルターしきい値(この設定より短いパルスを除外)
  rmtSetFilter(rmt_recv, true, 255);

  // フィルターしきい値(この設定より長いパルスを除外)
  rmtSetRxThreshold(rmt_recv, 10000 / realTick);

  // 受信開始
  rmtRead(rmt_recv, receive_data);
}

void loop() {
  delay(100);
}

同じようにパースしてみました。

まとめ

リファレンスを書くために、調査していましたがesp32-hal-rmt.hの関数群は資料がないのと、動作確認を全部自分でしないと使うことができないので、おすすめしません。

情報が多いdriver/rmt.hか、そもそもライブラリ化されているものをそのまま使ったほうがおすすめです!

受信ができたので、やっと送信側の検証ができます、、、

コメントする

メールアドレスが公開されることはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)