赤外線通信の仕組みや、送受信方法をM5StickS3で詳しく紹介

赤外線通信の仕組みアイキャッチ
スポンサーリンク

赤外線通信の送受信機能を備えた「M5StickS3」を使用して、赤外線通信の仕組みから、使い方、データの解析方法等をサンプルプログラムを使用して詳しく紹介します。

「M5Stackシリーズ」には、今まで赤外線ダイオードによる送信機能はありましたが、「M5StickS3」には初めて赤外線通信の送信と受信機能の両方が搭載されました。
送信と受信が1つのデバイスで動作確認でき、赤外線通信の仕組みや使い方をより深く理解することができるため、以下から一緒に動作確認していきましょう♪
スポンサーリンク

1.M5StickS3とは

「M5StickS3」とは「M5Stackテクノロジー社」のマイコンボードで「M5Stick」シリーズの最新バージョンです。

M5StickS3外観

旧モデルと同様に、1.14インチTFT液晶画面や入出力端子、ボタン、スピーカー、3軸ジャイロ+3軸加速度センサ、マイク、バッテリーが内蔵されています。

旧モデルとの大きな違いは、赤外線が送信だけでなく受信もできるようになり、スピーカーは圧電ブザーではなく音声も再生できるスピーカーアンプが内蔵されました。さらに、出力端子は8ピンから16ピンに倍増し、「M5Stack」シリーズ並みの拡張性があります。

他にもシリアル通信はもちろんWiFiやBluetooth通信にも対応しており、これらの機能をプログラムで自由に使用することができるため、電子工作やデータ表示器、IoT機器の製作等いろいろなアイデアを形にすることができます。

プログラムは「C言語」ベースの「ArduinoIDE」「PlatformIO」や「ビジュアルプログラミング(UiFlow)」「Python(MicroPython)」さらに、「AiFlow」 という自然言語で指示を書くとMicroPythonコードを自動生成してくれる最新の「AIプログラミング環境」も使用することができます。


 

「M5StickS3」の使い方や端子配列、初期設定については、以下のリンクで詳しく紹介しています。

M5StickS3の使い方、初期設定、サンプルプログラム、旧モデルとの違いを詳しく紹介
M5Stickシリーズの最新版、M5StickS3ついて旧モデルとの違いを確認しながら、初期設定や端子配列、機能、使い方をサンプルプログラムで詳しく紹介します。
スポンサーリンク

2.赤外線通信とは

赤外線通信とは、「赤外線LED」を高速に点滅させ、その光を「受信機」で受け取ってデータを伝送する通信方式です。しかし、赤外線を単純にON/OFFするだけでは、「太陽光」や「蛍光灯」、「LED照明」等の光で誤動作してしまいます。

このため実際は、「赤外線LED」を38kHzの高速で点滅させ、「受信機」はその38kHzの信号だけを受信することで信号を区別しています。

受信データの判定方法はメーカーの方式によって異なりますが、ここでは主に「NEC」または「AEHA(家製協)※SHARP等」を例に紹介していきます。

 

・データ部(MARK と SPACE)

実際の受信データ(NEC、AEHA)の取得方法は下図のようになります。

赤外線通信の仕組み
「赤外線受信機」が38kHzのパルスを受信している間だけ受信アンプは「検知」状態を出力し、この状態を「MARK」と言います。
「赤外線LED」がOFFし続けると「未検知」状態を出力しこの状態を「SPACE」と言います。
この「MARK」と「SPACE」の時間比率で「受信データ」の「0/1判定」を行っています。

受信データの「0/1」を決める「MARK:SPACE」の時間比率はメーカによって異なり、「NEC」と「AEHA(家製協)」では下表のようになります。

方式0ビット1ビット代表メーカ
NEC560μs : 560μs560μs : 1690μsNEC、東芝など
AEHA(家製協)425μs : 425μs425μs : 1275μsシャープ、パナソニック等
SONYはNECとは「MARK:SPACE」の比率が逆で「1200μs:600μs」が「1ビット」、「600μs:600μs」が「0ビット」となります。

・リーダー部詳細

赤外線通信のデータの最初にはリーダー(Leader、Headerとも呼ばれる)というデータが存在します。
リーダーには「これからデータが始まる」ことを知らせる役割があり、この後に続けてデータが送信されてきます。

リーダーは通常のデータより長い「MARK」時間と、長い「SPACE」時間で構成され、「NEC」と「AEHA」の場合は下表のようになります。

方式MARKSPACE
NEC9000μs4500μs
AEHA(家製協)3400μs1700μs
「NEC」と「AEHA」では、ほぼ倍の時間の違いがあるため、受信時にリーダー部を解析することで受信データが「NEC」か「AEHA」かを判定することができ、その後に続くデータ部の解析も容易になります。

・実際の受信データ解析結果

実際に「NEC」と「AEHA」の受信データの解析例は下表のようになります。

NECリモコン「Vol+ボタン」の受信データ

手持ちのNECリモコンの「Vol+ボタン」を押した時の受信データは下表のようになりました。

先頭のリーダー部はAEHAの「4500μs:1700μs」とほぼ一致します。
続けて16bitのアドレスは4ビットづつHEX変換(LSB First)すると→00FF
次の8bitはデータは同様に→A8
次の8bitはデータの反転で→57(A8の反転と一致)
手持ちのNECリモコンの「Vol+ボタン」の赤外線データは「00FFA857」であることがわかりました。
受信順MARK
(μs)
SPACE
(μs)
0/1判定HEX
データ
データ
詳細
093544546ーーリーダー
158060000カスタム
コード
25805700
36066530
45275720
560857200
66046290
75516010
85796010
957916841F
1057816531
1160616521
1260616531
1360616501F
1460916501
1560416521
1660616511
1760816501Aデータ
コード
186085700
1960616511
206085690
21607165218
226065700
236105950
245815710
2560956905データ
コード
(反転)
2661116481
276065700
2861016481
2960657207
3060816501
3160816491
3260616521
336060ーーストップビット

SHARPのテレビAQUOSの「電源ボタン」の受信データ

手持ちのSHARPのテレビAQUOSリモコンの「電源ボタン」を押した時の受信データは下表のようになりました。

先頭のリーダー部はNECの「9000μs:4500μs」とほぼ一致します。
続けて16bitのカスタムコードは4ビットづつHEX変換(LSB First)すると→555A
次の32bitはデータは同様に→F148688B
SHARPのテレビAQUOSリモコンの「電源ボタン」の赤外線データは「555AF148688B」であることがわかります。
受信順MARK
(μs)
SPACE
(μs)
0/1判定HEX
データ
データ
詳細
034521746ーーリーダー
144841405アドレス
249712351
34424270
447112631
544348405
639312781
74463950
846912931
944342405
1044412911
115223460
1244212901
1341713391A
144184540
1538913161
164424260
1744612861F32ビット
データ
1844212911
1949612401
2044612821
2144242801
224444240
234444250
2446912671
2544442204
2645012811
274184560
284464240
29444129018
304463960
314724220
324514450
3342347506
3438912861
3547212611
364463990
37473128218
384164840
394184270
404464180
41446126118
424764450
434194500
444224780
4551711561B
464454250
4741413181
4847812271
494760ーーストップビット
スポンサーリンク

3.M5StickS3の赤外線通信機能の詳細

今回の動作確認に使用する「M5StickS3」には「赤外線送信LED」と「赤外線受信機(受信LED+受信アンプVSOP38338)」が内蔵されており、赤外線の送受信の動作確認を行うことができますが、いくつか注意点があるためここで確認しておきましょう。

・ESP32 RMT の使用

「RMT」とは、「ESP32」シリーズに内蔵されている「赤外線リモコンなどのパルス信号を高精度で送受信するためのタイマキャプチャ回路」です。

赤外線通信の信号は「数百μ秒」の短い時間の信号を処理する必要がありますが、GPIOの入力端子で受信信号を処理しようとすると、CPUが他の処理をしている間に信号を取りこぼす可能性があります。

「RMT」を使用するとCPUを介さず、「MARK(検知)時間とSPACE(未検知)時間」をハードウェアが記録して、後からCPU側から読み込むことができます。

送信に関しては「MARK時間とSPACE時間」を正確に指定する必要がある場合は「RMT」を使用する必要がありますが、送信データがわかっている場合は「IRremoteESP8266」ライブラリを使用した方が簡単に送信できるため、ここでは送信には「IRremoteESP8266」ライブラリも使用しています。

・スピーカーアンプはOFF設定

「M5StickS3」の赤外線受信機能を使用する時は、スピーカーのアンプをオフにする必要があるため、下記のコードを初期設定に書いておく必要があります。

M5.Speaker.end();       // M5StickS3ではIR受信時にスピーカーアンプを停止する

・送受信距離は30cm以上

赤外線通信を行うときは、送信側と受信側の距離は30cm以上離さないと安定した送受信ができません。
近すぎると受信が異常を起こす可能性があります。

受信アンプの感度が高いため、自然光や蛍光灯の光に含まれる38kHzの赤外光で頻繁に誤動作してしまいます。このため「MARK:SPACE」時間をフィルタとして、特定の受信データにしか反応しないような処理を入れておくと、安定した受信ができるようになります。
スポンサーリンク

4.動作確認(サンプルプログラム)

赤外線通信の送受信について、サンプルプログラムを使用して動作確認していきます。
「M5StickS3」の初期設定やライブラリについては、以下のリンクで詳しく紹介しています。

M5StickS3の使い方、初期設定、サンプルプログラム、旧モデルとの違いを詳しく紹介
M5Stickシリーズの最新版、M5StickS3ついて旧モデルとの違いを確認しながら、初期設定や端子配列、機能、使い方をサンプルプログラムで詳しく紹介します。

 

最初は解析済みのデータでライブラリを使用して送信動作の確認をします。
次に受信データを確認し、さらに受信したデータを「NEC」か「AEHA」かを判定します。最後に受信したデータをそのまま送信するプログラムで動作確認していきます。

動作確認には下画像の「M5StickS3」を使用しました。
本体上部で赤外線の送受信ができます。下写真の左側が送信ダイオード、右側が受信ダイオードです

M5StickS3外観

動作確認に使用したリモコンは下画像になります。

NECリモコン

NECリモコン

このリモコンは電子工作キットの中に入っていたものでNECのデータ確認に使用しました。

SHARP AQUOS テレビリモコン

SHARP AQUOSテレビリモコン

SHARPのリモコンはAEHA(家製協)のデータ確認に使用しました。

・送信動作の確認

送信動作については「IRremoteESP8266」ライブラリを使用して確認していきます。

「IRremoteESP8266」ライブラリでは、「送信方式(メーカー)」と「送信データ」がわかれば、各送信方式の関数を使用してデータを指定するだけで、赤外線データを送信することができるので便利です。
「SHARP」には独自の通信方式もありますが、今回動作確認に使用したAQUOSテレビのリモコンは「AEHA方式」でした。
IRremoteESP8266」ライブラリの「AEHA方式」のデータ送信には「

irsend.sendPanasonic()」を使用しています。

送信するデータは以下表のように、事前に解析して確認しておいたもので、手持ちのNECのリモコンとSHARP製テレビ(AQUOS)リモコンのものになります。

NECリモコン データ例
ボタンカスタム
コード
データデータ反転
CHー00FFA25D
CH〃629D
CH+〃E21D
PREV〃22DD
NEXT〃02FD
PLAY/PAUSE〃C23D
VOLー〃E01F
VOL+〃A857
EQ〃906F
0〃6897
100+〃9867
200+〃B04F
1〃30CF
2〃18E7
3〃7A85
4〃10EF
5〃38C7
6〃5AA5
7〃42BD
8〃4AB5
9〃52AD
SHARP(テレビ)リモコン データ例
ボタンアドレスデータ
電源555AF148688B
選局+〃F1488885
選局ー〃F1484889
VOL+〃F148288F
VOLー〃F148A887
1〃F148724C
2〃F148F244
3〃F1480A43
4〃F1488A4B
5〃F1484A47
6〃F148CA4F
7〃F1482A41
8〃F148AA49
9〃F1486A45
10〃F148EA4D
11〃F1481A42
12〃F1489A4A
サンプルプログラムでは「M5StickS3」本体正面のAボタンを押すと「SHARP」の「電源ボタン」の信号を送信、本体側面のBボタンを押すと「NEC」の「VOL+ボタン」の信号を送信することができます。

サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include <M5Unified.h>       // M5Stackシリーズの統合ライブラリ
#include <IRremoteESP8266.h> // ESP8266/ESP32で赤外線通信を行うための基本ライブラリ
#include <IRsend.h>          // 赤外線信号を「送信」するためのクラスや関数を利用するためのライブラリ

static const uint16_t IR_LED_PIN = 46;  // M5StickS3 の赤外線LEDのピン番号(Plusは 9)

IRsend irsend(IR_LED_PIN);  // 赤外線送信クラスのインスタンスを作成

// 初期設定 *********************************************
void setup() {
  auto cfg = M5.config(); // M5Unifiedの設定を取得
  M5.begin(cfg);          // M5Unifiedの初期化

  irsend.begin(); // 赤外線送信の初期化

  M5.Display.setRotation(3);          // 液晶の回転設定
  M5.Display.clear();                 // 液晶をクリア
  M5.Display.setFont(&fonts::Font4);  // フォントをFont4に設定
  M5.Display.setTextSize(1);          // 文字サイズを1に設定
  M5.Display.setCursor(0, 0);         // カーソル位置を左上に設定
  M5.Display.println("IR TEST");
}

// メインループ ******************************************
void loop() {
  M5.update();  // ボタン状態更新

  // ボタンAが押された場合、SHARP(AEHA)の赤外線信号を送信
  if (M5.BtnA.wasPressed()) {
    M5.Display.clear();          // 液晶をクリア
    M5.Display.setCursor(0, 0);  // 座標指定
    M5.Display.println("SHARP(AEHA)\nSEND!");

    // SHARP(AEHA) の赤外線信号を送信(555AF148688B:電源ボタン)
    irsend.sendPanasonic( // SHARP(AEHA)はPanasonicと互換性があるため、sendPanasonicを使用
      0x555A,      // アドレス
      0xF148688B   // データ
    );
  }
  // ボタンBが押された場合、NECの赤外線信号を送信
  if (M5.BtnB.wasPressed()) {
    M5.Display.clear();          // 液晶をクリア
    M5.Display.setCursor(0, 0);  // 座標指定
    M5.Display.println("NEC SEND!");

    // NEC の赤外線信号を送信(00FFA857:Vol+ボタン)
    irsend.sendNEC(
      0x00FFA857, // アドレス16bit(8bit + 8bit反転 ※拡張は16bit) + データ8bit + データ8bit反転
      32          // データ長(ビット数)
    );
  }
}

・受信動作の確認

赤外線通信の受信には「ESP32」の「RMT(Remote Control Transceiver)」を使用します。

「RMT」とは、「ESP32」シリーズに内蔵されている「赤外線リモコンなどのパルス信号を高精度で送受信するための専用ハードウェア」です。

以下のサンプルプログラムでは、受信データの検知時間の「MARK」と未検知時間の「SPACE」を1シンボル(1受信)として確認することができます。(受信データの前半が MARK、後半が SPACE)

実際に「M5StickS3」で「SHARPのテレビ(AQUOS)」のリモコンの電源ボタンを押した時の受信結果は以下のようになりました。
液晶画面には「Symbols:」として受信データの総数とその下に最初の8個の受信データが表示されます。

赤外線通信 受信動作の確認

シリアル出力では以下のように、受信データの全てを確認することができます。

データを見ると受信時間のばらつきが大きいため、このデータをそのまま送信して同じデータを再現するのは難しそうです。
受信データを送信したい場合は、受信したデータをHEXデータとして解析してから送信するのがいいと思います。
Captured 50 symbols
000 :  3330  1686
001 :   361   587
002 :   280  1482
003 :   197   483
004 :   384  1270
005 :   359   640
006 :   227  1295
007 :   359   508
008 :   359  1323
009 :   331   642
010 :   225  1484
011 :   200   612
012 :   200  1322
013 :   357  1350
014 :   333   507
015 :   386  1297
016 :   331   508
017 :   359  1295
018 :   359  1323
019 :   360  1480
020 :   200  1322
 ・
 ・
 ・
「M5StickS3」の赤外線受信機は感度がいいのか、リモコンデータを送信していないときもデータが受信されることがあります。
受信データにもノイズが混じるため、正しいデータを受信するまで何度か試す必要がありました。
実際の使用にはこのようなノイズデータを無視するような仕組みを組み込んでおく必要があります。

 

サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include "M5Unified.h"      // M5Unifiedライブラリをインクルード
#include "driver/rmt_rx.h"  // RMT受信ドライバをインクルード

#define IR_RECEIVE_PIN 42   // IR受信モジュールの接続ピン番号

rmt_channel_handle_t rx_chan = NULL;          // RMT受信チャネルハンドル
static rmt_symbol_word_t rx_raw_symbols[128]; // RMT受信用バッファ

static volatile bool rx_done = false;     // 受信完了フラグ
static volatile size_t rx_symbol_num = 0; // 受信シンボル数

// 受信完了コールバック ============================================================
bool rmt_rx_done_callback(rmt_channel_handle_t, const rmt_rx_done_event_data_t *edata, void *) {
  rx_symbol_num = edata->num_symbols; // 受信シンボル数を保存
  rx_done = true;                     // 受信完了フラグをセット
  return false; // falseを返すとタスク切り替え不要
}

// RMT受信初期化 ============================================================
void setup_rmt_rx() {
  rmt_rx_channel_config_t rx_cfg = {
      .gpio_num = (gpio_num_t)IR_RECEIVE_PIN, // IR受信モジュールの接続ピン
      .clk_src = RMT_CLK_SRC_DEFAULT,         // デフォルトクロックソース
      .resolution_hz = 1000000,               // 1MHz = 1us単位
      // 元の設定に戻す
      .mem_block_symbols = 128,               // 128シンボル分のメモリブロックを使用
  };
  // 新しいRMT受信チャネルを作成し、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_cfg, &rx_chan ));
  // 受信完了コールバックを登録
  rmt_rx_event_callbacks_t cbs = {.on_recv_done = rmt_rx_done_callback, };
  // コールバック登録とチャネル有効化、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan, &cbs, NULL));
  ESP_ERROR_CHECK(rmt_enable(rx_chan)); // チャネル有効化
}

// 受信開始 ============================================================
void start_rmt_receive() {
  rmt_receive_config_t cfg = {  // 受信設定
      .signal_range_min_ns = 1000,      // 1us以上の信号を受信する
      .signal_range_max_ns = 26300000,  // 20ms(38kHz)以下の信号を受信する
  };
  // 受信開始、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_receive(rx_chan, rx_raw_symbols, sizeof(rx_raw_symbols), &cfg));
}

// RAWシリアル出力表示 ============================================================
void dump_raw_us(rmt_symbol_word_t *symbols, size_t count) {

  Serial.printf("\nCaptured %u symbols\n", (unsigned)count);

  for (size_t i = 0; i < count; i++) {
    Serial.printf(
        "%03u : %5u %5u\n",
        (unsigned)i,
        (unsigned)symbols[i].duration0,
        (unsigned)symbols[i].duration1);
  }
  Serial.println("----------------");
}

// 液晶表示 ============================================================
void display_raw(rmt_symbol_word_t *symbols, size_t count) {
  M5.Display.clear();
  M5.Display.setCursor(0, 0);
  M5.Display.printf("Symbols:%u\n", (unsigned)count);

  size_t lines = min((size_t)9, count);

  for (size_t i = 0; i < lines; i++) {
    M5.Display.printf(
        "%02u:%4u %4u\n",
        (unsigned)i,
        (unsigned)symbols[i].duration0,
        (unsigned)symbols[i].duration1);
  }
}

// 初期設定 ****************************************************
void setup() {
  auto cfg = M5.config(); // M5Unifiedの設定を取得
  M5.begin(cfg);          // M5Unifiedの初期化
  Serial.begin(115200);   // シリアル通信の初期化
  M5.Speaker.end();       // M5StickS3ではIR受信時にスピーカーアンプを停止する

  // IR受信モジュール電源ON
  M5.Power.setExtOutput(true, m5::ext_none);

  setup_rmt_rx();       // RMT受信初期化
  start_rmt_receive();  // 最初の受信

  Serial.println("IR RAW Receiver Ready");

  M5.Display.setRotation(2);          // 液晶の回転設定
  M5.Display.clear();                 // 液晶をクリア
  M5.Display.setTextColor(WHITE);     // 文字色を白に設定
  M5.Display.setFont(&fonts::Font4);  // フォントをFont4に設定
  M5.Display.setTextSize(0.9);        // 文字サイズを1に設定
  M5.Display.setCursor(0, 0);         // カーソル位置を左上に設定
  M5.Display.println("IR\nReceive\nReady"); // 受信準備完了メッセージを表示
}

// メインループ *************************************************
void loop() {
  if (!rx_done) { // 受信完了フラグが立っていない場合は何もしない
    delay(1); // 1ms待機してループを抜ける
    return;
  }
  rx_done = false;  // ここで受信完了フラグをクリア

  // 受信数を取得
  size_t count = rx_symbol_num;

  if (count > 128) {  // 受信シンボル数が128を超えた場合は128に制限
    count = 128 ;
  }
  dump_raw_us(rx_raw_symbols, count); // シリアル出力表示
  display_raw(rx_raw_symbols, count); // 液晶表示

  start_rmt_receive();  // 次の受信を開始
  delay(1); // 1ms待機
}

・受信データの解析(NEC・家製協)

赤外線の送受信が確認できたので、次は受信したデータが何のデータかを解析するサンプルプログラムを紹介します。

ここのサンプルプログラムでは、受信したデータが「NEC」または「SHARP(AEHA)」だったときに、受信データを「HEXデータ」として確認することができます。

受信した「HEXデータ」は、先に紹介した「送信動作の確認」のサンプルプログラムで指定して、自由に送信することができるようになります。

 

実際に動作確認した結果は下画像のようになります。

受信したデータが「NEC」または「SHARP(AEHA)」だった場合、「M5StickS3」の液晶画面で、どちらのデータかの判定と受信データ(HEX)、受信データ数(Symbols:)、リーダーの「MARK、SPACE時間」が確認できます。
赤外線受信データNEC

「NEC」リモコンの「VOL+ボタン」を押したときの受信データは上画像のようになります。

赤外線受信データSHARP(AEHA)

「SHARP(AEHA)」テレビAQUOSのリモコンの「電源ボタン」を押したときの受信データは上画像のようになります。

受信したデータが「NEC」か「SHARP」かを判定するには、受信したシンボル数と、リーダーの「MARK」と「SPACE」の時間で判定しています。
受信距離は30cm以上離してください。
また、受信アンプの感度が高いため、自然光や蛍光灯でも誤動作したりノイズが混じって正しいデータがなかなか受信できないことがあります。
この場合は何度かリモコンのボタンを押して試してください。

サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include "M5Unified.h"      // M5Unifiedライブラリをインクルード
#include "driver/rmt_rx.h"  // RMT受信ドライバをインクルード

#define IR_RECEIVE_PIN 42   // IR受信モジュールの接続ピン番号

rmt_channel_handle_t rx_chan = NULL;          // RMT受信チャネルハンドル
static rmt_symbol_word_t rx_raw_symbols[128]; // RMT受信用バッファ
static rmt_symbol_word_t work_symbols[128];   // 解析用バッファ、受信バッファとは完全に分離する

static volatile bool rx_done = false;     // 受信完了フラグ
static volatile size_t rx_symbol_num = 0; // 受信シンボル数

// 受信完了コールバック ============================================================
bool rmt_rx_done_callback(rmt_channel_handle_t, const rmt_rx_done_event_data_t *edata, void *) {
  rx_symbol_num = edata->num_symbols; // 受信シンボル数を保存
  rx_done = true;                     // 受信完了フラグをセット
  return false; // falseを返すとタスク切り替え不要
}

// RMT受信初期化 ============================================================
void setup_rmt_rx() {
  rmt_rx_channel_config_t rx_cfg = {
      .gpio_num = (gpio_num_t)IR_RECEIVE_PIN, // IR受信モジュールの接続ピン
      .clk_src = RMT_CLK_SRC_DEFAULT,         // デフォルトクロックソース
      .resolution_hz = 1000000,               // 1MHz = 1us単位
      // 元の設定に戻す
      .mem_block_symbols = 128,               // 256シンボル分のメモリブロックを使用
  };
  // 新しいRMT受信チャネルを作成し、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_cfg, &rx_chan ));
  // 受信完了コールバックを登録
  rmt_rx_event_callbacks_t cbs = {.on_recv_done = rmt_rx_done_callback, };
  // コールバック登録とチャネル有効化、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan, &cbs, NULL));
  ESP_ERROR_CHECK(rmt_enable(rx_chan)); // チャネル有効化
}

// 受信開始 ============================================================
void start_rmt_receive() {
  rmt_receive_config_t cfg = {  // 受信設定
      .signal_range_min_ns = 1000,      // 1us以上の信号を受信する
      .signal_range_max_ns = 20000000,  // 20ms以下の信号を受信する
  };
  // 受信開始、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_receive(rx_chan, rx_raw_symbols, sizeof(rx_raw_symbols), &cfg));
}

// RAWシリアル出力表示 ============================================================
void dump_raw_us(rmt_symbol_word_t *symbols, size_t count) {
  Serial.println("IR RAW (us):");
  for (size_t i = 0; i < count; i++) {  // 受信シンボル数分ループ
    Serial.printf(
        "%3u: %5u %5u\n", // シンボル番号、MARK時間、SPACE時間を表示
        (unsigned)i,
        (unsigned)symbols[i].duration0,
        (unsigned)symbols[i].duration1
    );
  }
  Serial.println();
}

// NEC判定 ============================================================
bool is_nec_signal(rmt_symbol_word_t *symbols, size_t count) {
  // NEC信号の最低シンボル数
  if (count < 2) {
    return false;
  }

  // リーダー信号の取得
  uint32_t mark_us  = symbols[0].duration0; // MARKの時間
  uint32_t space_us = symbols[0].duration1; // SPACEの時間

  // NECリーダー判定(MARK: 9000us ±30%、SPACE: 4500us ±30%)
  bool leader_ok =
    (mark_us  >= 6300 && mark_us  <= 11700) &&
    (space_us >= 3150 && space_us <= 5850);

  return leader_ok;
}

// SHARP判定 ============================================================
bool is_sharp_signal(rmt_symbol_word_t *symbols, size_t count) {
  // SHARP信号の最低シンボル数
  if (count < 45) {
    return false;
  }

  // リーダー信号の取得
  uint32_t mark_us  = symbols[0].duration0; // MARKの時間
  uint32_t space_us = symbols[0].duration1; // SPACEの時間

  // SHARPリーダー判定(MARK: 3400us ±約18%、SPACE: 1700us ±約18%)
  bool leader_ok =
    (mark_us  >= 2800 && mark_us  <= 4000) &&
    (space_us >= 1400 && space_us <= 2000);

  return leader_ok;
}

// NECデコード ============================================================
bool decode_nec(
    rmt_symbol_word_t *symbols, // 受信シンボル配列
    size_t count,               // 受信シンボル数
    char *hex_string,           // デコード結果を格納する文字列バッファ
    size_t hex_string_size) {   // 文字列バッファのサイズ

  if (!is_nec_signal(symbols, count)) { // NEC信号でない場合はデコード失敗
    return false;
  }

  uint8_t value = 0;  // 4bitごとにまとめるための変数
  int bit_count = 0;  // 4bitごとにまとめるためのカウンタ
  int hex_pos = 0;    // hex_stringの書き込み位置
  int data_bits = 0;  // デコードしたビット数

  for (size_t i = 1; i < count; i++) {  // 先頭1シンボルはヘッダーなのでスキップ
    uint32_t mark = symbols[i].duration0;   // MARKの時間
    uint32_t space = symbols[i].duration1;  // SPACEの時間
    if (space == 0) { // SPACEが0の場合は終端とみなす
      break;
    }

    // NECのMARK MARKが560us程度でない場合はデコード失敗
    if (mark < 300 || mark > 800) { 
      return false;
    }
    uint8_t bit;  // デコードするビット

    // NECのSPACE 560us程度 = 0、1690us程度 = 1 なので1200usを境界として判定
    if (space < 1200) {
      bit = 0;  // 1200us未満は0
    } else {
      bit = 1;  // 1200us以上は1
    }
    value = (value << 1) | bit; // 1bit追加
    bit_count++;  // 4bitごとにまとめるためのカウンタをインクリメント
    data_bits++;  // デコードしたビット数をインクリメント

    // 4bitごとにまとめる
    if (bit_count == 4) {
      if (hex_pos >= (int)hex_string_size - 1) {  // hex_stringのサイズを超えないようにチェック
        return false;
      }
      if (value < 10) { // 0~9の場合は'0'~'9'に変換
        hex_string[hex_pos++] = '0' + value;
      } else {          // 10~15の場合は'A'~'F'に変換
        hex_string[hex_pos++] = 'A' + (value - 10);
      }
      value = 0;      // 4bitごとにまとめるための変数をリセット
      bit_count = 0;  // 4bitごとにまとめるためのカウンタをリセット
    }
    if (data_bits == 32) {  // デコードしたビット数が32bitになったら終了
      break;
    }
  }

  if (data_bits != 32) {  // デコードしたビット数が32bitでない場合はデコード失敗
    return false;
  }
  if (bit_count != 0) {   // 4bitごとにまとめるためのカウンタが0でない場合はデコード失敗
    return false;
  }
  hex_string[hex_pos] = '\0'; // 文字列の終端を設定

  return hex_pos == 8;  // デコード結果が8文字であることを確認
}

// SHARPデコード ============================================================
bool decode_sharp(
    rmt_symbol_word_t *symbols, // 受信シンボル配列
    size_t count,               // 受信シンボル数
    char *hex_string,           // デコード結果を格納する文字列バッファ
    size_t hex_string_size) {   // 文字列バッファのサイズ

  if (!is_sharp_signal(symbols, count)) { // Sharp信号でない場合はデコード失敗
    return false;
  }
  // 先頭1シンボルがヘッダー
  const size_t HEADER_SYMBOLS = 1;

  uint8_t value = 0;  // 4bitごとにまとめるための変数
  int bit_count = 0;  // 4bitごとにまとめるためのカウンタ
  int hex_pos = 0;    // hex_stringの書き込み位置
  int data_bits = 0;  // デコードしたビット数

  for (size_t i = HEADER_SYMBOLS; i < count; i++) { // 先頭2シンボルはヘッダーなのでスキップ
    uint32_t space = symbols[i].duration1;  // SPACEの時間
    if (space == 0) { // SPACEが0の場合は終端とみなす
      break;
    }
    uint8_t bit;  // デコードするビット

    // 約350~600us  = 0 / 約1200~1400us = 1 なので800usを境界として判定
    if (space < 800) {
      bit = 0;  // 800us未満は0
    } else {
      bit = 1;  // 800us以上は1
    }

    value = (value << 1) | bit; // 1bit追加
    bit_count++;  // 4bitごとにまとめるためのカウンタをインクリメント
    data_bits++;  // デコードしたビット数をインクリメント

    // 4bitごとにまとめる
    if (bit_count == 4) {
      if (hex_pos >= (int)hex_string_size - 1) {  // hex_stringのサイズを超えないようにチェック
        return false;
      }
      if (value < 10) { // 0~9の場合は'0'~'9'に変換
        hex_string[hex_pos++] =
            '0' + value;
      } else {          // 10~15の場合は'A'~'F'に変換
        hex_string[hex_pos++] =
            'A' + (value - 10);
      }
      value = 0;      // 4bitごとにまとめるための変数をリセット
      bit_count = 0;  // 4bitごとにまとめるためのカウンタをリセット
    }

    if (data_bits == 48) {  // デコードしたビット数が48bitになったら終了
      break;
    }
  }
  if (data_bits != 48) {    // デコードしたビット数が48bitでない場合はデコード失敗
    return false;
  }
  if (bit_count != 0) {     // 4bitごとにまとめるためのカウンタが0でない場合はデコード失敗
    return false;
  }
  hex_string[hex_pos] = '\0'; // 文字列の終端を設定

  return hex_pos == 12; // デコード結果が12文字であることを確認
}

// 液晶表示 ============================================================
void display_result(const rmt_symbol_word_t *symbols, const char *protocol, const char *data, size_t symbol_count) {
  M5.Display.clear();               // 液晶をクリア
  M5.Display.setCursor(0, 0);       // カーソル位置を左上に設定
  M5.Display.setTextColor(WHITE);   // 文字色を白に設定
  M5.Display.setTextSize(1);        // 文字サイズを1に設定
  M5.Display.println(protocol);     // プロトコル名を表示

  M5.Display.setTextColor(YELLOW);  // 文字色を黄色に設定
  M5.Display.setTextSize(1);        // 文字サイズを1に設定
  M5.Display.println(data);         // デコード結果を表示 

  M5.Display.setTextColor(WHITE);   // 文字色を白に設定
  M5.Display.printf("RX symbols: %u\n", (unsigned)symbol_count);  // 受信シンボル数を表示
  M5.Display.printf("Mark:    %d us\nSpace: %d us", symbols[0].duration0, symbols[0].duration1);  // 受信シンボルのMARKとSPACEの時間を表示
}

// 初期設定 ****************************************************
void setup() {
  auto cfg = M5.config(); // M5Unifiedの設定を取得
  M5.begin(cfg);          // M5Unifiedの初期化
  Serial.begin(115200);   // シリアル通信の初期化
  M5.Speaker.end();       // M5StickS3ではIR受信時にスピーカーアンプを停止する

  M5.Display.setRotation(3);          // 液晶の回転設定
  M5.Display.clear();                 // 液晶をクリア
  M5.Display.setTextColor(WHITE);     // 文字色を白に設定
  M5.Display.setFont(&fonts::Font4);  // フォントをFont4に設定
  M5.Display.setTextSize(1);          // 文字サイズを1に設定
  M5.Display.setCursor(0, 0);         // カーソル位置を左上に設定
  M5.Display.println("IR Receive Ready"); // 受信準備完了メッセージを表示

  setup_rmt_rx();       // RMT受信初期化
  start_rmt_receive();  // 最初の受信

  // IR受信モジュール電源有効化
  M5.Power.setExtOutput(true, m5::ext_none);
}

// メインループ *************************************************
void loop() {
  M5.update();  // M5Unifiedの更新処理

  if (!rx_done) { // 受信完了フラグが立っていない場合は何もしない
    delay(1); // 1ms待機してループを抜ける
    return;
  }
  rx_done = false;  // ここで受信完了フラグをクリア

  // 受信数を取得
  size_t count = rx_symbol_num;

  if (count > 128) {  // 受信シンボル数が128を超えた場合は128に制限
    count = 128;
  }

  // RMT受信バッファを解析用バッファへコピー(次の受信でrx_raw_symbolsが上書きされても、work_symbolsは影響を受けない)
  memcpy(work_symbols, rx_raw_symbols, count * sizeof(rmt_symbol_word_t));
  start_rmt_receive();  // コピーが完了してから次の受信を開始
  dump_raw_us(work_symbols, count); // シリアル出力でRAWデータを表示

  // デコード
  char ir_data[65];     // デコード結果を格納するバッファ
  ir_data[0] = '\0';    // バッファを初期化
  bool decoded = false; // デコード成功フラグ
  const char *protocol = NULL;  // デコードプロトコル名

  // NEC判定
  if (is_nec_signal(work_symbols, count)) {
    if (decode_nec(work_symbols, count, ir_data, sizeof(ir_data))) {
      protocol = "NEC"; // デコードプロトコル名を設定
      decoded = true;   // デコード成功フラグを設定
    }
  }

  // SHARP判定
  else if (is_sharp_signal(work_symbols, count)) {
    if (decode_sharp(work_symbols, count, ir_data, sizeof(ir_data))) {
      protocol = "SHARP"; // デコードプロトコル名を設定
      decoded = true;     // デコード成功フラグを設定 
    }
  }

  // デコード成功のみ表示(UNKNOWN、長押し時のリピート信号などは無視)
  if (decoded) {
    Serial.printf("Protocol: %s\n", protocol);  // シリアル出力
    Serial.printf("IR DATA: %s\n", ir_data);
    display_result(work_symbols, protocol, ir_data, count);   // 液晶表示
  }
  delay(1); // 1ms待機
}

・受信データをリピート送信

最後に受信したデータを記憶して、ボタンを押すとそのデータを送信するサンプルプログラムを紹介します。

「受信動作の確認」で見たように受信データは不安定で、受信したデータをそのまま送信して動作する確率は低いです。
このため、受信したデータを閾値で判定して「正規化」してから送信するようにして、解析精度を上げるようにしています。
受信感度が高くノイズを拾うため、正しいデータを受信できる確率は低いです。
シリアルモニタで受信データを確認しながら、何度かデータを受信して試してみてください。

サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include "M5Unified.h"      // M5Unifiedライブラリをインクルード
#include "driver/rmt_rx.h"  // RMT受信ドライバをインクルード
#include "driver/rmt_tx.h"  // RMT送信ドライバをインクルード
#include "driver/rmt_encoder.h" // RMTエンコーダドライバをインクルード

#define IR_RECEIVE_PIN 42   // IR受信モジュールの接続ピン番号
#define IR_SEND_PIN    46   // IR送信モジュールの接続ピン番号

static const uint32_t CARRIER_HZ = 38000; // 赤外線送信の搬送波周波数(38kHz)

// ======================================================================
// RX
rmt_channel_handle_t rx_chan = NULL;          // RMT受信チャネルハンドル
static rmt_symbol_word_t rx_raw_symbols[128]; // RMT受信用バッファ
static rmt_symbol_word_t work_symbols[128];   // 解析用バッファ、受信バッファとは完全に分離する

static volatile bool rx_done = false;     // 受信完了フラグ
static volatile size_t rx_symbol_num = 0; // 受信シンボル数

size_t captured_count = 0;  // 受信したIR信号のシンボル数

// ======================================================================
// TX
rmt_channel_handle_t tx_chan = NULL;      // RMT送信チャネルハンドル
rmt_encoder_handle_t _copy_encoder = NULL; // RMTコピーエンコーダハンドル

// 受信完了コールバック ============================================================
bool rmt_rx_done_callback(rmt_channel_handle_t, const rmt_rx_done_event_data_t *edata, void *) {
  rx_symbol_num = edata->num_symbols; // 受信シンボル数を保存
  rx_done = true;                     // 受信完了フラグをセット
  return false; // falseを返すとタスク切り替え不要
}

// RMT受信初期化 ============================================================
void setup_rmt_rx() {
  rmt_rx_channel_config_t rx_cfg = {
    .gpio_num = (gpio_num_t)IR_RECEIVE_PIN, // IR受信モジュールの接続ピン
    .clk_src = RMT_CLK_SRC_DEFAULT,         // デフォルトクロックソース
    .resolution_hz = 1000000,               // 1MHz = 1us単位
    // 元の設定に戻す
    .mem_block_symbols = 128,               // 256シンボル分のメモリブロックを使用
  };
  // 新しいRMT受信チャネルを作成し、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_cfg, &rx_chan ));
  // 受信完了コールバックを登録
  rmt_rx_event_callbacks_t cbs = {.on_recv_done = rmt_rx_done_callback, };
  // コールバック登録とチャネル有効化、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan, &cbs, NULL));
  ESP_ERROR_CHECK(rmt_enable(rx_chan)); // チャネル有効化
}

// 受信開始 ============================================================
void start_rmt_receive() {
  rmt_receive_config_t cfg = {  // 受信設定
    .signal_range_min_ns = 1000,      // 1us以上の信号を受信する
    .signal_range_max_ns = 20000000,  // 20ms以下の信号を受信する
  };
  // 受信開始、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_receive(rx_chan, rx_raw_symbols, sizeof(rx_raw_symbols), &cfg));
}

// RMT送信初期化 ============================================================
void setup_rmt_tx() {
  rmt_tx_channel_config_t tx_cfg = {      // 送信チャネル設定
    .gpio_num = (gpio_num_t)IR_SEND_PIN,  // IR送信モジュールの接続ピン
    .clk_src = RMT_CLK_SRC_DEFAULT,       // デフォルトクロックソース
    .resolution_hz = 1000000,         // 1MHz = 1us単位
    .mem_block_symbols = 64,          // 64シンボル分のメモリブロックを使用
    .trans_queue_depth = 1,           // 転送キューの深さを1に設定
  };
  ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_cfg, &tx_chan)); // 新しいRMT送信チャネルを作成し、エラーがあれば停止

  rmt_carrier_config_t carrier_cfg = {  // 搬送波設定
    .frequency_hz = CARRIER_HZ,     // 搬送波周波数を設定
    .duty_cycle = 0.33,           // 搬送波のデューティ比を設定(33%)
    .flags = {                    // フラグ設定
      .polarity_active_low = false  // 搬送波の極性を設定(false = 高レベルで変調)
    }
  };
  ESP_ERROR_CHECK(rmt_apply_carrier(tx_chan, &carrier_cfg)); // 搬送波設定を適用し、エラーがあれば停止
  rmt_copy_encoder_config_t encoder_cfg = {}; // コピーエンコーダ設定(空の構造体)

  ESP_ERROR_CHECK(rmt_new_copy_encoder(&encoder_cfg, &_copy_encoder)); // 新しいコピーエンコーダを作成し、エラーがあれば停止
  ESP_ERROR_CHECK(rmt_enable(tx_chan)); // 送信チャネルを有効化し、エラーがあれば停止
}

// Dump表示関数 =================================================================
void dump_raw_us(
  const rmt_symbol_word_t *symbols, // 受信シンボル配列
  size_t count) { // 受信シンボル数

  Serial.printf(
    "\nCaptured %u symbols\n",  // 受信シンボル数を表示
    (unsigned)count // 受信シンボル数を表示
  );
  // 受信シンボルの時間を表示
  for (size_t i = 0; i < count; i++) {
    Serial.printf(
      "%3u: %5u %5u\n", // 受信シンボルの時間を表示
      (unsigned)i,      // 受信シンボルのインデックスを表示
      (unsigned)symbols[i].duration0, // 受信シンボルのMARK時間を表示
      (unsigned)symbols[i].duration1  // 受信シンボルのSPACE時間を表示
    );
  }
  Serial.println();
}

// 受信データをシンボル単位で表示 =========================================
void dump_tx_symbols(
  const rmt_symbol_word_t *symbols, // 送信シンボル配列
  size_t count) { // 送信シンボル数

  Serial.println();
  Serial.println("=== TX DATA ===");
  // 送信シンボルの時間を表示
  for (size_t i = 0; i < count; i++) {
    Serial.printf(
      "%3u: %5u %5u\n", // 送信シンボルの時間を表示
      (unsigned)i,      // 送信シンボルのインデックスを表示
      (unsigned)symbols[i].duration0, // 送信シンボルのMARK時間を表示
      (unsigned)symbols[i].duration1  // 送信シンボルのSPACE時間を表示
    );
  }
  Serial.println("================");
  Serial.println();
}

// 受信データの送信関数 =============================================================
void sendCapturedIR() {
  if (captured_count == 0) {  // 受信データがない場合は送信しない
    return;
  }
  static rmt_symbol_word_t tx_symbols[128]; // 送信用シンボル配列
  // 受信データを送信用シンボル配列にコピー
  for (size_t i = 0; i < captured_count; i++) {
    tx_symbols[i].duration0 = work_symbols[i].duration0;  // MARK時間をコピー
    tx_symbols[i].duration1 = work_symbols[i].duration1;  // SPACE時間をコピー
    tx_symbols[i].level0 = 1; // MARKレベルを1に設定
    tx_symbols[i].level1 = 0; // SPACEレベルを0に設定
  }
  // 送信データをシンボル単位で表示
  rmt_transmit_config_t tx_cfg = {
    .loop_count = 0,  // ループ回数を0に設定(1回だけ送信)
    .flags = {        // フラグ設定
        .eot_level = 0  // 送信終了時のレベルを0に設定
    }
  };
  // 送信データを送信、エラーがあれば停止
  ESP_ERROR_CHECK(
    rmt_transmit(
      tx_chan,
      _copy_encoder,
      tx_symbols,
      captured_count *
      sizeof(rmt_symbol_word_t),
      &tx_cfg
    )
  );
  ESP_ERROR_CHECK(rmt_tx_wait_all_done(tx_chan, -1)); // 送信完了まで待機、エラーがあれば停止
}

// 正規化関数 ========================================
uint16_t normalize_duration(uint16_t t) {
  // ヘッダMARK
  if (t >= 2500 && t <= 3500) { // 3000us近辺
    return 3000;  // 3000usに正規化
  }
  // ヘッダSPACE
  if (t >= 1500 && t <= 2500) { // 2000us近辺
    return 2000;  // 2000usに正規化
  }
  // 短パルス
  if (t >= 200 && t <= 380) {   // 300us近辺
    return 300;   // 300usに正規化
  }
  // 400us近辺
  if (t >= 380 && t <= 600) {   // 430us近辺
    return 430;   // 430usに正規化
  }
  // 1200us近辺
  if (t >= 1100 && t <= 1450) { // 1250us近辺
    return 1250;  // 1250usに正規化
  }
  return t; // それ以外はそのまま返す
}

// ノイズ除去+正規化処理 ========================================
void cleanup_symbols(rmt_symbol_word_t *symbols, size_t *count) {
  size_t newCount = 0;
  // 受信シンボルを順番に処理
  for (size_t i = 0; i < *count; i++) {
    uint16_t d0 = symbols[i].duration0; // MARK時間を取得
    uint16_t d1 = symbols[i].duration1; // SPACE時間を取得

    // 極端に短いノイズ除去
    if ((d0 && d0 < 80) || (d1 && d1 < 80)) { // 80us未満のシンボルはノイズとみなす
      continue;
    }
    symbols[newCount].duration0 = normalize_duration(d0); // MARK時間を正規化
    symbols[newCount].duration1 = normalize_duration(d1); // SPACE時間を正規化
    symbols[newCount].level0 = 1; // MARKレベルを1に設定
    symbols[newCount].level1 = 0; // SPACEレベルを0に設定
    newCount++; // 新しいシンボル数をインクリメント
  }
  *count = newCount;  // 新しいシンボル数を更新
}

// 初期設定 ****************************************************
void setup() {
  auto cfg = M5.config(); // M5Unifiedの設定を取得
  M5.begin(cfg);          // M5Unifiedの初期化
  Serial.begin(115200);   // シリアル通信の初期化
  M5.Speaker.end();       // M5StickS3ではIR受信時にスピーカーアンプを停止する

  M5.Display.setRotation(3);          // 液晶の回転設定
  M5.Display.clear();                 // 液晶をクリア
  M5.Display.setTextColor(WHITE);     // 文字色を白に設定
  M5.Display.setFont(&fonts::Font4);  // フォントをFont4に設定
  M5.Display.setTextSize(1);          // 文字サイズを1に設定
  M5.Display.setCursor(0, 0);         // カーソル位置を左上に設定
  M5.Display.println("IR Receive Ready"); // 受信準備完了メッセージを表示

  setup_rmt_rx();       // RMT受信初期化
  setup_rmt_tx();       // RMT送信初期化
  start_rmt_receive();  // 最初の受信

  // IR受信モジュール電源有効化
  M5.Power.setExtOutput(true, m5::ext_none);
  Serial.println("Ready");
}

// メインループ *************************************************
void loop() {
  M5.update();  // M5Unifiedの更新処理
    // IR Capture
    if (rx_done) {  // 受信完了フラグが立っている場合
      rx_done = false;  // 受信完了フラグをクリア
      size_t count = rx_symbol_num; // 受信シンボル数を取得

      if (count > 128) {  // 受信シンボル数が128を超える場合は128に制限
          count = 128;
      }
      // 受信データを解析用バッファにコピー
      memcpy(work_symbols, rx_raw_symbols, count * sizeof(rmt_symbol_word_t));

      captured_count = count; // 受信シンボル数を保存

      cleanup_symbols(work_symbols, &captured_count); // ノイズ除去+正規化処理
      dump_raw_us(work_symbols, captured_count);  // 受信データをシンボル単位で表示 

      M5.Display.fillScreen(BLACK);       // 液晶を黒でクリア
      M5.Display.setCursor(0, 0);         // カーソル位置を左上に設定
      M5.Display.println("IR Captured");  // 受信完了メッセージを表示
      M5.Display.printf("Symbols: %u\n", (unsigned)captured_count); // 受信シンボル数を表示
      // 受信データの先頭シンボルのMARKとSPACEの時間を表示
      if (captured_count > 0) { // 受信シンボル数が0より大きい場合
        M5.Display.printf(
          "Lead: %u / %u us\n",       // 先頭シンボルのMARKとSPACEの時間を表示
          work_symbols[0].duration0,  // 先頭シンボルのMARK時間を表示
          work_symbols[0].duration1   // 先頭シンボルのSPACE時間を表示
        );
      }
      start_rmt_receive();  // 次の受信を開始
    }
    // ボタンAが押されたら、受信したIR信号を送信
    if (M5.BtnA.wasPressed()) {
      sendCapturedIR(); // 受信したIR信号を送信
    }
    delay(1); // 1ms待機してループを繰り返す
}
スポンサーリンク

5.まとめ

今回は、「M5StickS3」に搭載された「赤外線送受信機能」を使用して、赤外線通信の基本的な仕組みから実際の送受信、さらに受信データの解析とリピート送信までサンプルプログラムを使用して詳しく紹介しました。

赤外線通信は単純なON/OFF信号ではなく、38kHzの搬送波を利用しながら「MARK(検知)」と「SPACE(未検知)」の時間比率でデータを表現しています。また、「NEC方式」と「AEHA(家製協)方式」では、リーダー部やデータ部のタイミングが異なるため、受信データを解析することで通信方式や送信内容を判別できることができます。

「M5StickS3」は赤外線の送信機能だけでなく受信機能も搭載しているため、1台だけで信号の取得・解析・送信まで行えます。

「ESP32」の「RMT機能」を活用することで、数百μs単位の短いパルス信号も高精度に扱うことができ、赤外線リモコンの解析や学習リモコンの製作などにも活用できます。

一方で、受信機の感度が高いため、自然光や照明によるノイズの影響を受けやすく、受信データのフィルタリングや正規化処理が実用上は重要になります。今回紹介したように、リーダー部やデータ長を利用した判定処理を組み込むことで、より安定した受信が可能になります。

「M5StickS3」の赤外線機能を活用すれば、家電リモコンの解析や学習リモコンの作成はもちろん、IoT機器との連携や独自の赤外線制御システムの開発など、さまざまな応用が可能です。
ぜひ今回のサンプルプログラムをベースに、自分だけの赤外線制御ツール作りに挑戦してみてください♪

M5StickS3の使い方、初期設定、サンプルプログラム、旧モデルとの違いを詳しく紹介
M5Stickシリーズの最新版、M5StickS3ついて旧モデルとの違いを確認しながら、初期設定や端子配列、機能、使い方をサンプルプログラムで詳しく紹介します。

コメント

タイトルとURLをコピーしました