RMT
ESP32

概要

Arduino core for the ESP32を使った、ESP-WROOM-32開発ボードのRMT(Remote Control) Moduleの実験です。赤外線リモコンの信号を制御します。

ESP-WROOM-32には、RMT(Remote Control) Module という、赤外線リモコン信号を制御するための機能が実装されています。キャリア信号の生成や信号の受信が簡単にできるようになっています。Arduino core for the ESP32には、Arduino向けのAPIも用意されていますが、私にはよく理解できなかったので、ここではESP-IDFの機能をそのまま使っています。

この機能のもともとの目的は赤外線リモコンの制御かもしれませんが、本質的には、変調をかけたパルスの生成と、入力されたパルスのオン・オフの長さの抽出機能です。

今回は、NECフォーマットの赤外線リモコンの信号を送信する実験と赤外線リモコンのデータを受信する実験を行いました。赤外線LEDや赤外線リモコン受信モジュールは内蔵されていないので、別途準備が必要です。

Arduino UnoでのNECフォーマットの赤外線リモコンの信号の送信実験はこちら。受信実験はこちら

よくわかっていないこともあります。あくまで、実験ベースなので、間違いが含まれている可能性が高いので注意してください。「おそらく」などの言葉がちりばめられています。

Arduino core for the ESP32のインストールのページはこちら

実験

  1. 設定
  2. データの送信
  3. データの受信

設定

RMTを利用するための設定には、以下の3種類があります。

  1. 送受信共通設定
  2. 送信設定
  3. 受信設定

これらは、rmt_config_t 型の変数を使って設定します。

送受信共通設定

rmt_config_t 型は、以下のように定義されています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef struct {
    rmt_mode_t rmt_mode;               /*!< RMT mode: transmitter or receiver */
    rmt_channel_t channel;             /*!< RMT channel */
    uint8_t clk_div;                   /*!< RMT channel counter divider */
    gpio_num_t gpio_num;               /*!< RMT GPIO number */
    uint8_t mem_block_num;             /*!< RMT memory block number */
    union{
        rmt_tx_config_t tx_config;     /*!< RMT TX parameter */
        rmt_rx_config_t rx_config;     /*!< RMT RX parameter */
    };
} rmt_config_t;

コメントに記載されていますが、各項目は以下の通りです。

項目 説明 備考
rmt_mode 送信に利用するか受信に利用するかを設定します。 送信のときはRMT_MODE_TX 、受信のときは RMT_MODE_RXを指定する。
channel 利用するチャネルを設定します。 RMT_CHANNEL_0からRMT_CHANNEL_7のいずれかを指定する。
clk_div クロックの分周率を指定します。 1~255の値を指定する。 通常クロックは80MHzなので、80を指定すると、分周後のクロック数は1MHzとなり、1クロックが1マイクロ秒になります。
gpio_num 赤外線LEDを接続するGPIO番号を設定します。 利用するGPIO番号を指定する。
mem_block_num 利用するメモリブロック数を指定します。 1~255の値を指定する。 64x32ビットのメモリが1ブロックのようです。32ビットでONとOFFを送信する時間を一組設定できるので、1ブロックで64回のON/OFFの組を送信できます。

送信用設定

送信用の設定は、rmt_config_t型の、rmt_tx_config_t型の変数tx_configを設定します。rmt_tx_config_t型は、以下のように定義されています。

1
2
3
4
5
6
7
8
9
typedef struct {
    bool loop_en;                         /*!< RMT loop output mode*/
    uint32_t carrier_freq_hz;             /*!< RMT carrier frequency */
    uint8_t carrier_duty_percent;         /*!< RMT carrier duty (%) */
    rmt_carrier_level_t carrier_level;    /*!< RMT carrier level */
    bool carrier_en;                      /*!< RMT carrier enable */
    rmt_idle_level_t idle_level;          /*!< RMT idle level */
    bool idle_output_en;                  /*!< RMT idle level output enable*/
}rmt_tx_config_t;

コメントに記載されていますが、各項目は以下の通りです。

項目 説明 備考
loop_en データをループして送信するかどうかを設定します。 true/false
carrier_freq_hz 送信時のキャリア周波数を指定します。 32ビット非負数
carrier_duty_percent キャリアのデューティ比を設定します。 8ビット非負数
carrier_level キャリアの出力レベルを設定します。 RMT_CARRIER_LEVEL_LOWかRMT_CARRIER_LEVEL_HIGHを指定する。
carrier_en キャリアの利用有無を指定します。 true/false
idle_level アイドル時の出力レベルを指定します。 RMT_CARRIER_LEVEL_LOW かRMT_CARRIER_LEVEL_HIGHを指定する。 用途がよくわかっていません。
idle_output_en アイドル時の出力の有無を指定します。 true/false 用途がよくわかっていません。

受信用設定

受信用の設定は、rmt_config_t型の、rmt_rx_config_t型の変数rx_configを設定します。rmt_rx_config_t型は、以下のように定義されています。

1
2
3
4
5
typedef struct {
    bool filter_en;                    /*!< RMT receiver filer enable*/
    uint8_t filter_ticks_thresh;       /*!< RMT filter tick  number */
    uint16_t idle_threshold;           /*!< RMT RX idle threshold */
}rmt_rx_config_t;

コメントに記載されていますが、各項目は以下の通りです。

項目 説明 備考
filter_en 受信時にフィルタを適用するか設定します。 true/false
filter_ticks_thresh フィルタのスレッショルドを設定します。設定した値(クロック数)より短いパルスを無視します。 0-255
idle_threshold レシーバをアイドルにするスレッショルドを設定します。この値(クロック数)より長い間受信している信号に変化がない場合、受信を終了します。 0-65535

設定の適用

設定した値を適用するには、以下のAPIを呼び出します。

1
2
esp_err_t rmt_config(const rmt_config_t *rmt_param);
esp_err_t rmt_driver_install(rmt_channel_t channel, size_t rx_buf_size, int intr_alloc_flags);

rmt_paramは、上記で設定した変数です。

channelは、利用するチャネル(rmt_config_tの中にあるので不要な気もしますが)です。rx_buf_sizeは、受信に使うバッファのサイズです。

intr_alloc_flagsは割り込みハンドラのフラグで、デフォルト値を利用する場合は0を設定すればいいようです。詳細はよくわかっていません。

データの送信

データの設定

データを送信する際は、必要な分だけメモリブロック(rmt_item32_t型)に送信するデータを設定します。rmt_item32_t型の定義は以下の通りです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
typedef struct {
    union {
        struct {
            uint32_t duration0 :15;
            uint32_t level0 :1;
            uint32_t duration1 :15;
            uint32_t level1 :1;
        };
        uint32_t val;
    };
} rmt_item32_t;

level0とlevel1は、出力を制御するメンバ変数で、1にすると出力ON、0にすると出力OFFになります。

duration0とduration1は、それぞれlevel0とlevel1に設定した出力を行う時間を表します。時間の単位は、おそらく、分周後のクロック数です。ESP32の場合、ソースクロックは2種類利用可能みたいですが、何もしなければ、おそらく80MHzのクロックが利用されていると思います。rmt_config_tのclk_divに設定した分周率で割った値により導出されるクロックの周期が、ここで利用されるクロック数となります。このため、clk_divを80に設定すると、分周後のクロック数は1MHzとなり、1クロック当たり1マイクロ秒に相当することになります。

データの送信

設定したデータを送信する際は、以下のAPIを呼び出します。

1
2
esp_err_t rmt_write_items(rmt_channel_t channel, const rmt_item32_t *rmt_item, int item_num, bool wait_tx_done);
esp_err_t rmt_wait_tx_done(rmt_channel_t channel, TickType_t wait_time);

channelは、利用するチャネルです。rmt_itemは設定したメモリブロックの先頭アドレスです。item_numは、送信するデータ数(ON/OFFの組)です。

wait_tx_doneは、trueを設定するとデータの送信が完了するまで待ちます。falseを設定するとデータの送信の完了を待ちません。この場合、rmt_wait_tx_done()を呼び出して、完了を確認することができます。

他にも、rmt_fill_tx_items()、 rmt_tx_start() 、rmt_tx_stop()といったAPIを利用してデータを送信することもできるようです。

スケッチ例

NECフォーマットのリモコン信号を送信します。私の家のテレビのチャンネルをアップする信号(カスタムコード: 0x40bf、データコード: 0x1b)を3秒ごとに送信します。

赤外線LEDは、25番ピンに接続する設定になっています。適切に電流制限抵抗を入れてください。

APIに渡すパラメータは、あまり考慮されていません。とりあえず動いているという状態です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#include "driver/rmt.h"
 
const int rmtDataLength = 34;        // NEC format data length 34 bit
rmt_item32_t rmtData[rmtDataLength]; // data to send
 
const rmt_channel_t channel = RMT_CHANNEL_0;
const gpio_num_t irPin = GPIO_NUM_25;
 
const int leaderOnUs = 9000;
const int leaderOffUs = 4500;
const int dataOnUs = 560;
const int data1OffUs = 1690;
const int data0OffUs = 560;
const int stopbitOnUs = 560;
const int stopbitOffUs = 0x7fff;
 
/* send NEC format remote control data */
void sendData(uint16_t customCode, uint8_t dataCode) {
  /* leader code 1bit: ON 9000us, OFF 4500us */
  rmtData[0].duration0 = leaderOnUs;
  rmtData[0].level0 = 1;
  rmtData[0].duration1 = leaderOffUs;
  rmtData[0].level1 = 0;
 
  /*
   * custom code 16 bit
   * INPUT series: b15 b14 b13 b12 b11 b10 b09 b08 b07 b06 b05 b04 b03 b02 b01 b00
   * SEND  series: b08 b09 b10 b11 b12 b13 b14 b15 b00 b01 b02 b03 b04 b05 b06 b07
   */
  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 8; j++) {
      /* 
       * 1: ON 560us + OFF 1690us 
       * 0: ON 560us + OFF  560us 
      */
      rmtData[8 * i + j + 1].duration0 = dataOnUs;
      rmtData[8 * i + j + 1].level0 = 1;
      if (customCode & (1 << ((1 - i) * 8 + j))) {
        rmtData[8 * i + j + 1].duration1 = data1OffUs;
      } else {
        rmtData[8 * i + j + 1].duration1 = data0OffUs;
      }
      rmtData[8 * i + j + 1].level1 = 0;
    }
  }
 
  /*
   * data code 8bit
   * INPUT series: b7 b6 b5 b4 b3 b2 b1 b0
   * SEND  series: b0 b1 b2 b3 b4 b5 b6 b7 ~b0 ~b1 ~b2 ~b3 ~b4 ~b5 ~b6 ~b7
   */
  for (int i = 0; i < 8; i++) {
    rmtData[i + 17].duration0 = dataOnUs;
    rmtData[i + 25].duration0 = dataOnUs;
    rmtData[i + 17].level0 = 1;
    rmtData[i + 25].level0 = 1;
    if (dataCode & (1 << i)) {
      rmtData[i + 17].duration1 = data1OffUs;
      rmtData[i + 25].duration1 = data0OffUs;
    } else {
      rmtData[i + 17].duration1 = data0OffUs;
      rmtData[i + 25].duration1 = data1OffUs;
    }
    rmtData[i + 17].level1 = 0;
    rmtData[i + 25].level1 = 0;
  }
 
  /* stop bit 1bit: ON 560 */
  rmtData[33].duration0 = stopbitOnUs;
  rmtData[33].level0 = 1;
  rmtData[33].duration1 = stopbitOffUs;
  rmtData[33].level1 = 0;
 
  rmt_write_items(channel, rmtData, rmtDataLength, true);
}
 
void setup() {
  // put your setup code here, to run once:
  rmt_config_t rmtConfig;
 
  rmtConfig.rmt_mode = RMT_MODE_TX;  // transmit mode
  rmtConfig.channel = channel;  // channel to use 0 - 7
  rmtConfig.clk_div = 80;  // clock divider 1 - 255. source clock is 80MHz -> 80MHz/80 = 1MHz -> 1 tick = 1 us
  rmtConfig.gpio_num = irPin; // pin to use
  rmtConfig.mem_block_num = 1; // memory block size
   
  rmtConfig.tx_config.loop_en = 0; // no loop
  rmtConfig.tx_config.carrier_freq_hz = 38000;  // IR remote controller uses 38kHz carrier frequency
  rmtConfig.tx_config.carrier_duty_percent = 33; // duty 
  rmtConfig.tx_config.carrier_level =  RMT_CARRIER_LEVEL_HIGH; // carrier level
  rmtConfig.tx_config.carrier_en = 1;  // carrier enable
  rmtConfig.tx_config.idle_level =  RMT_IDLE_LEVEL_LOW ; // signal level at idle
  rmtConfig.tx_config.idle_output_en = 1;  // output if idle
 
  rmt_config(&rmtConfig);
  rmt_driver_install(rmtConfig.channel, 0, 0);
}
 
void loop() {
  // put your main code here, to run repeatedly:
  sendData(0x40bf, 0x1b);
  delay(3000);
}

データの受信

受信バッファの設定

データを受信する際は、受信に利用するバッファを設定します。このバッファのサイズは、rmt_driver_install()を呼び出すときの、rx_buf_sizeです。このバッファにアクセスするためには、rmt_get_ringbuf_handle()を呼び出します。

1
2
3
esp_err_t rmt_get_ringbuf_handle(rmt_channel_t channel, RingbufHandle_t* buf_handle);
void *xRingbufferReceive(RingbufHandle_t ringbuf, size_t *item_size, TickType_t ticks_to_wait);
void vRingbufferReturnItem(RingbufHandle_t ringbuf, void *item);

channelは、利用するチャネルです。返却されたbuf_handleを、xRingbufferReceive()に渡すことで、受信したデータにアクセスすることができます。データは送信にも用いたrmt_item32_t型です。item_sizeは、データのバイト数のようです。sizeof(rmt_item32_t)が4なので、赤外線リモコンのオン/オフの組数の4倍の値が返却されます。

バッファを利用した後は、vRingbufferReturnItem()を使って、リングバッファに戻します。

データの受信

データを受信するには、以下を呼び出します。

1
2
esp_err_t rmt_rx_start(rmt_channel_t channel, bool rx_idx_rst);
esp_err_t rmt_rx_stop(rmt_channel_t channel);

channelは、利用するチャネルです。rx_idx_rstは、trueを設定するとメモリインデックスをリセットします。

受信をやめるには、rmt_rx_stop()を呼び出します。

スケッチ例

赤外線リモコン信号のオン/オフの時間をそのまま表示するプログラムと、NECフォーマットリモコン信号を解析プログラムを作成しました。

赤外線リモコン受信モジュールは、25番ピンに接続する設定になっています。出力は負論理なので、注意が必要です。

APIに渡すパラメータは、あまり考慮されていません。とりあえず動いているという状態です。

赤外線リモコン信号のオン/オフの時間をそのまま表示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include "driver/rmt.h"
#include "driver/periph_ctrl.h"
#include "soc/rmt_reg.h"
 
const rmt_channel_t channel = RMT_CHANNEL_0;
const gpio_num_t irPin = GPIO_NUM_25;
 
RingbufHandle_t buffer = NULL;
 
void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  rmt_config_t rmtConfig;
 
  rmtConfig.rmt_mode = RMT_MODE_RX;
  rmtConfig.channel = channel;
  rmtConfig.clk_div = 80;
  rmtConfig.gpio_num = irPin;
  rmtConfig.mem_block_num = 1;
 
  rmtConfig.rx_config.filter_en = 1;
  rmtConfig.rx_config.filter_ticks_thresh = 255;
  rmtConfig.rx_config.idle_threshold = 10000;
 
  rmt_config(&rmtConfig);
  rmt_driver_install(rmtConfig.channel, 2048, 0);
 
  rmt_get_ringbuf_handle(channel, &buffer);
  rmt_rx_start(channel, 1);
}
 
void loop() {
  // put your main code here, to run repeatedly:
  size_t rxSize = 0;
 
  rmt_item32_t *item = (rmt_item32_t *)xRingbufferReceive(buffer, &rxSize, 10000);
 
  if (item) {
    printData(item, rxSize);
    vRingbufferReturnItem(buffer, (void*) item);
  }
}
 
void printData(rmt_item32_t *item, size_t size) {
  int n = -1;
  do {
    n++;
    Serial.printf("%4d %s: %5d, %s: %5d\n", n,
                  item[n].level0 ? "ON " : "OFF", item[n].duration0,
                  item[n].level1 ? "ON " : "OFF", item[n].duration1);
  } while (item[n].duration1) ;
  Serial.printf("size = %d, %d\n\n", size, n + 1);
}

NECフォーマットのリモコン信号の解析を行います。customCodeとdataCodeを表示します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include "driver/rmt.h"
#include "driver/periph_ctrl.h"
#include "soc/rmt_reg.h"
 
const rmt_channel_t channel = RMT_CHANNEL_0;
const gpio_num_t irPin = GPIO_NUM_25;
 
const int leaderOnUs = 9000;
const int leaderOffUs = 4500;
const int dataOnUs = 560;
const int data1OffUs = 1690;
const int data0OffUs = 560;
const int repeatOffUs = 2250;
const int dataErrorRange = 100;
 
RingbufHandle_t buffer = NULL;
 
void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  rmt_config_t rmtConfig;
 
  rmtConfig.rmt_mode = RMT_MODE_RX;
  rmtConfig.channel = channel;
  rmtConfig.clk_div = 80;
  rmtConfig.gpio_num = irPin;
  rmtConfig.mem_block_num = 1;
 
  rmtConfig.rx_config.filter_en = 1;
  rmtConfig.rx_config.filter_ticks_thresh = 255;
  rmtConfig.rx_config.idle_threshold = 10000;
 
 
  rmt_config(&rmtConfig);
  rmt_driver_install(rmtConfig.channel, 2048, 0);
 
  rmt_get_ringbuf_handle(channel, &buffer);
  rmt_rx_start(channel, 1);
}
 
void loop() {
  // put your main code here, to run repeatedly:
  size_t rxSize = 0;
 
  rmt_item32_t *item = (rmt_item32_t *)xRingbufferReceive(buffer, &rxSize, 10000);
 
  if (item) {
    parseData(item, rxSize);
    vRingbufferReturnItem(buffer, (void*) item);
  }
}
 
int checkRange(int value, int target, int errorRange) {
  return ((value > (target - errorRange)) && (value < (target + errorRange)));
}
 
int checkLeaderOrRpeat(rmt_item32_t item) {
  if ((item.level0 != 0) || (item.level1 != 1)) {
    return -1;
  }
 
  if (!checkRange(item.duration0, leaderOnUs, dataErrorRange)) {
    return -1;
  }
 
  if (checkRange(item.duration1, leaderOffUs, dataErrorRange)) {
    return 1;
  }
 
  if (checkRange(item.duration1, repeatOffUs, dataErrorRange)) {
    return 2;
  }
 
  return -1;
}
 
int checkZeroOne(rmt_item32_t item) {
  if ((item.level0 != 0) || (item.level1 != 1)) {
    return -1;
  }
 
  if (!checkRange(item.duration0, dataOnUs, dataErrorRange)) {
    return -1;
  }
 
  if (checkRange(item.duration1, data1OffUs, dataErrorRange)) {
    return 1;
  }
 
  if (checkRange(item.duration1, data0OffUs, dataErrorRange)) {
    return 0;
  }
 
  return -1;
}
 
int printData(rmt_item32_t *item, int index) {
  for (int i = 0; i < 2; i++) {
    int code = 0;
    for (int j = 0; j < 8; j++) {
      int k;
      Serial.printf("%d ", k = checkZeroOne(item[index++]));
      code += (k << j); 
    }
    Serial.printf(": %02x\n", code); 
  }
  return index;
}
 
void parseData(rmt_item32_t *item, size_t rxSize) {
  int index = 0;
  int code;
 
  switch (checkLeaderOrRpeat(item[index])) {
    case 1:
      break;
    case 2:
      Serial.printf("Repeat code\n");
      return;
    default:
      return;
  }
 
  index++;
  Serial.printf("Custom Code\n");
  index = printData(item, index);
 
  Serial.printf("\nData Code\n");
  index = printData(item, index);
  Serial.printf("--------------------\n");
}

参考文献

バージョン

Hardware:ESP-WROOM-32
Software:Arduino 1.8.4/Arduino core for the ESP32

最終更新日

November 1, 2022

inserted by FC2 system