ESP32のRMTで赤外線リモコンを作る

Category: 電子工作
Tag: ESP32

Arduinoでリモコンを作るときの定番はIRRemoteですが、ESP32-H2でMatter対応のスマートリモコンを作ろうとしたところ、コンパイルは通るのに波形がおかしくなり正常に動作しない問題が生じました。
どうやら、ESPシリーズの内、IRRemoteがサポートするのはESP32,ESP32-C3のみで、それ以外のチップではテストされていないようです。
さらに対応するチップでもWiFiを使っていると同様の問題が生じることを確認しました。

代わりとなるライブラリがないか探していたところ、ESP32シリーズ(ESP32-C2を除く)にはRMTというリモコン送受信用のペリフェラルがあるということを知りました。

esp-idfのサンプルを見るとリモコンの他にNeoPixelやステッピングモーター、1-Wireなどがあります。要するにIOを正確に制御できる機能なのでリモコン以外にも様々な応用ができる、ラズピコでいうPIOのような立ち位置になっているようです。

使ってみる

開発環境はArduino IDE、チップはESP32-C3
今回は送信のみで受信はしません。

Arduino ESP32 公式ドキュメント - Remote Control (RMT):
https://docs.espressif.com/projects/arduino-esp32/en/latest/api/rmt.html

ESP-IDF Programming Guide - Remote Control Transceiver (RMT):
https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/rmt.html

サンプルコード

0番ピンに接続した赤外線LEDから、5秒ごとにNECプロトコルでコマンドを送信するサンプルです。

#include "driver/rmt.h"

#define IR_PIN 0

void setup() {
  rmtInit(IR_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 1000000);  // RMT初期化 1MHz = 1µs/tick
  rmtSetCarrier(IR_PIN, true, false, 38000, 0.33);  // 搬送波 38kHz, duty=1/3
}

void loop() {
  uint32_t address = 0x6D82;
  uint32_t command = 0xAB;
  uint32_t raw_data = address | (command<<16) | (~command<<24);

  uint32_t T = 562;

  rmt_data_t data[34];
  data[0]  = {16*T, 1, 8*T, 0};  // leader code 16T high + 8T low
  data[33] = {560, 1, 0, 0};     // stop bit 560µs

  for (uint8_t i=0; i<32; i++) {
    bool bit = (raw_data>>i) & 1;
    data[i+1] = {T, 1, (bit?3:1)*T, 0};
  }

  rmtWrite(IR_PIN, data, 34, RMT_WAIT_FOR_EVER);

  delay(5000);
}

初期化

まずrmtInit関数で初期化します。

rmtInit(IR_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 1000000);

3つ目の引数は送信データを保管するためのメモリブロックの予約数で、今回の用途では1ブロックで十分です。
4つ目の引数がRMTのカウンター周波数で312.5kHzから80MHzが指定できます。今回は1000000 = 1MHzなので、1tick=1µsの制御になります。

Note

メモリブロック
ESP32は最大8、それ以外は最大2つのblockを予約できる。
blockはESP32/ESP32-S2で64個、それ以外は48個のsymbolを保管できる
symbolは要するにrmt_data_t構造体のこと、サンプルではrmt_data_t data[34]、つまり34symbolを使用しているので1blockで足りる。
ただ、実際にESP32-H2で試したところ、予約block数に関わらず255symbolまで動作した。何か勘違いしているのかもしれない。

キャリア(搬送波)設定

rmtSetCarrier(IR_PIN, true, false, 38000, 0.33);

赤外線リモコンでは38kHzの搬送波を使うので、その設定もします。
4つ目の引数はduty比です。規格では1/3なので0.33としていますが、0.1くらいでも普通に動作します。リモコンの反応を良くするには、duty比を上げることよりもパルスの電流を増やすことが有効です。

送信データ

NECプロトコルではメーカーや製品に固有のアドレスが16bit、次にコマンドが8bit、最後にコマンドが正常に送信できているかを検証するための反転データが続きます。

uint32_t address = 0x6D82;
uint32_t command = 0xAB;
uint32_t raw_data = address | (command<<16) | (~command<<24);

反転ビットは~演算子で反転。シフト演算子でずらしてからOR演算子で結合して送信データを作っています。

データを構造体に変換

RMTにはrmt_data_t構造体の配列を渡します。
これで1つのHIGH/LOWの時間を定義して、これを作りたい波の数だけ配列にします。

// rmt_data_t構造体の定義
typedef union {
    struct {
        uint32_t duration0 : 15;  // Duration of first pulse in RMT ticks
        uint32_t level0    : 1;   // Level of first pulse (0 = LOW, 1 = HIGH)
        uint32_t duration1 : 15;  // Duration of second pulse in RMT ticks
        uint32_t level1    : 1;   // Level of second pulse (0 = LOW, 1 = HIGH)
    };
    uint32_t val;  // Access as 32-bit value
} rmt_data_t;

Tを562μsとして、
初めのリーダーコードは16TのHIGHと8TのLOWを、
終わりのストップビットは560µsのHIGHを送信します。

uint32_t T = 562;

rmt_data_t data[34];
data[0]  = {16*T, 1, 8*T, 0};  // leader code 16T high + 8T low
data[33] = {560, 1, 0, 0};     // stop bit 560µs high

次に、データ本体です。
1とAND演算することで先頭のビットだけを取り出すことができます。これをループしながらループ回数分シフト演算子でずらすことで、全てのビットを順番に取り出しています。
赤外線リモコンで1と0はLOWの期間のみが異なるので三項演算子で分岐しています。

for (uint8_t i=0; i<32; i++) {
    bool bit = (raw_data>>i) & 1;
    data[i+1] = {T, 1, (bit?3:1)*T, 0};
}

送信

送信する関数には以下の4つがあります。

  • rmtWrite: ブロッキング処理で送信
  • rmtWriteAsync: 非同期に送信
  • rmtWriteLooping: 無限にループ送信
  • rmtWriteRepeated: 指定回数だけループ送信

引数は3つ目まで共通で、

  1. ピン番号
  2. 送信データ(rmt_data_tの配列)
  3. 送信するsymbolの数

4つ目の引数は

  • rmtWrite: timeout時間
  • rmtWriteAsync: x
  • rmtWriteLooping: x
  • rmtWriteRepeated: 繰り返し回数

サンプルではrmtWriteのtimeoutをRMT_WAIT_FOR_EVERで無限に設定しています。

rmtWrite(IR_PIN, data, 34, RMT_WAIT_FOR_EVER);

余談1 Lチカ

RMT(&ESP32-H2)で1HzのLチカしてみようと試したのですが、うまく動作しませんでした。
rmt_data_tのdurationがuint32_tなので、指定できる値としては最大4,294,967,296ですが、実際に試してみると32000くらいが限界で、それ以上は指定値より短くなってしまいました。いろいろ試した結果、カウンタ=320000Hzのduration=32000tickで5Hzが限界でした。

#include "driver/rmt.h"

#define LED_PIN 0

void setup() {
  rmtInit(LED_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 320000);
  rmt_data_t data[1];
  data[0] = {32000, 1, 32000, 0};
  rmtWriteLooping(LED_PIN, data, 1);
}

void loop() {}

余談2 高速Lチカ

先ほどは低速の限界を試しました、となれば次は高速の限界です。カウンターは最大80MHz、HIGH/LOWを最小の1にすると理論上は40MHzとなります。
ただ、自分の持っている測定機器が中華製24MHzロジックアナライザーとアリエクで買った帯域幅 10MHzのオシロスコープだけなので測定できません。
仕方ないので測定できる最大周波数として8MHzを生成してみました。

#include "driver/rmt.h"

#define LED_PIN 0

void setup() {
  rmtInit(LED_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 16*1000*1000); // 16MHz
  rmt_data_t data[32];
  for (uint8_t i=0; i<32; i++) {
    data[i] = {1, 1, 1, 0};
  }
  rmtWriteLooping(LED_PIN, data, 32);
}

void loop() {}

もはや正弦波ですが、ちゃんと8MHzになっています。

おわりに

PicoのPIOと比べると柔軟性は低いですが、アセンブリの知識がなくても高速なIO制御ができるので、さまざまな用途に使えそうな予感がします。
ESP32の中ではニッチな機能ですが、もっと認知されても良いのではないでしょうか。