赤外線通信の送受信機能を備えた「M5StickS3」を使用して、赤外線通信の仕組みから、使い方、データの解析方法等をサンプルプログラムを使用して詳しく紹介します。
送信と受信が1つのデバイスで動作確認でき、赤外線通信の仕組みや使い方をより深く理解することができるため、以下から一緒に動作確認していきましょう♪
1.M5StickS3とは
2.赤外線通信とは
・データ部(MARK と SPACE)
・リーダー部詳細
・実際の受信データの解析結果
3.M5StickS3の赤外線通信機能の詳細
・ESP32 RMT の使用
・スピーカーアンプはOFF設定
・送受信距離は30cm以上
4.動作確認(サンプルプログラム)
・送信動作の確認
・受信動作の確認
・受信データの解析(NEC・家製協)
・受信データをリピート送信
5.まとめ
1.M5StickS3とは
「M5StickS3」とは「M5Stackテクノロジー社」のマイコンボードで「M5Stick」シリーズの最新バージョンです。

旧モデルと同様に、1.14インチTFT液晶画面や入出力端子、ボタン、スピーカー、3軸ジャイロ+3軸加速度センサ、マイク、バッテリーが内蔵されています。
他にもシリアル通信はもちろんWiFiやBluetooth通信にも対応しており、これらの機能をプログラムで自由に使用することができるため、電子工作やデータ表示器、IoT機器の製作等いろいろなアイデアを形にすることができます。
プログラムは「C言語」ベースの「ArduinoIDE」「PlatformIO」や「ビジュアルプログラミング(UiFlow)」「Python(MicroPython)」さらに、「AiFlow」 という自然言語で指示を書くとMicroPythonコードを自動生成してくれる最新の「AIプログラミング環境」も使用することができます。
「M5StickS3」の使い方や端子配列、初期設定については、以下のリンクで詳しく紹介しています。

2.赤外線通信とは
赤外線通信とは、「赤外線LED」を高速に点滅させ、その光を「受信機」で受け取ってデータを伝送する通信方式です。しかし、赤外線を単純にON/OFFするだけでは、「太陽光」や「蛍光灯」、「LED照明」等の光で誤動作してしまいます。
このため実際は、「赤外線LED」を38kHzの高速で点滅させ、「受信機」はその38kHzの信号だけを受信することで信号を区別しています。
・データ部(MARK と SPACE)
実際の受信データ(NEC、AEHA)の取得方法は下図のようになります。

受信データの「0/1」を決める「MARK:SPACE」の時間比率はメーカによって異なり、「NEC」と「AEHA(家製協)」では下表のようになります。
| 方式 | 0ビット | 1ビット | 代表メーカ |
|---|---|---|---|
| NEC | 560μs : 560μs | 560μs : 1690μs | NEC、東芝など |
| AEHA(家製協) | 425μs : 425μs | 425μs : 1275μs | シャープ、パナソニック等 |
・リーダー部詳細
赤外線通信のデータの最初にはリーダー(Leader、Headerとも呼ばれる)というデータが存在します。
リーダーには「これからデータが始まる」ことを知らせる役割があり、この後に続けてデータが送信されてきます。
リーダーは通常のデータより長い「MARK」時間と、長い「SPACE」時間で構成され、「NEC」と「AEHA」の場合は下表のようになります。
| 方式 | MARK | SPACE |
|---|---|---|
| NEC | 9000μs | 4500μs |
| AEHA(家製協) | 3400μs | 1700μs |
・実際の受信データ解析結果
実際に「NEC」と「AEHA」の受信データの解析例は下表のようになります。
NECリモコン「Vol+ボタン」の受信データ
手持ちのNECリモコンの「Vol+ボタン」を押した時の受信データは下表のようになりました。
続けて16bitのアドレスは4ビットづつHEX変換(LSB First)すると→00FF
次の8bitはデータは同様に→A8
次の8bitはデータの反転で→57(A8の反転と一致)
| 受信順 | MARK (μs) | SPACE (μs) | 0/1判定 | HEX データ | データ 詳細 |
| 0 | 9354 | 4546 | ー | ー | リーダー |
| 1 | 580 | 600 | 0 | 0 | カスタム コード |
| 2 | 580 | 570 | 0 | ||
| 3 | 606 | 653 | 0 | ||
| 4 | 527 | 572 | 0 | ||
| 5 | 608 | 572 | 0 | 0 | |
| 6 | 604 | 629 | 0 | ||
| 7 | 551 | 601 | 0 | ||
| 8 | 579 | 601 | 0 | ||
| 9 | 579 | 1684 | 1 | F | |
| 10 | 578 | 1653 | 1 | ||
| 11 | 606 | 1652 | 1 | ||
| 12 | 606 | 1653 | 1 | ||
| 13 | 606 | 1650 | 1 | F | |
| 14 | 609 | 1650 | 1 | ||
| 15 | 604 | 1652 | 1 | ||
| 16 | 606 | 1651 | 1 | ||
| 17 | 608 | 1650 | 1 | A | データ コード |
| 18 | 608 | 570 | 0 | ||
| 19 | 606 | 1651 | 1 | ||
| 20 | 608 | 569 | 0 | ||
| 21 | 607 | 1652 | 1 | 8 | |
| 22 | 606 | 570 | 0 | ||
| 23 | 610 | 595 | 0 | ||
| 24 | 581 | 571 | 0 | ||
| 25 | 609 | 569 | 0 | 5 | データ コード (反転) |
| 26 | 611 | 1648 | 1 | ||
| 27 | 606 | 570 | 0 | ||
| 28 | 610 | 1648 | 1 | ||
| 29 | 606 | 572 | 0 | 7 | |
| 30 | 608 | 1650 | 1 | ||
| 31 | 608 | 1649 | 1 | ||
| 32 | 606 | 1652 | 1 | ||
| 33 | 606 | 0 | ー | ー | ストップビット |
SHARPのテレビAQUOSの「電源ボタン」の受信データ
手持ちのSHARPのテレビAQUOSリモコンの「電源ボタン」を押した時の受信データは下表のようになりました。
続けて16bitのカスタムコードは4ビットづつHEX変換(LSB First)すると→555A
次の32bitはデータは同様に→F148688B
| 受信順 | MARK (μs) | SPACE (μs) | 0/1判定 | HEX データ | データ 詳細 |
|---|---|---|---|---|---|
| 0 | 3452 | 1746 | ー | ー | リーダー |
| 1 | 448 | 414 | 0 | 5 | アドレス |
| 2 | 497 | 1235 | 1 | ||
| 3 | 442 | 427 | 0 | ||
| 4 | 471 | 1263 | 1 | ||
| 5 | 443 | 484 | 0 | 5 | |
| 6 | 393 | 1278 | 1 | ||
| 7 | 446 | 395 | 0 | ||
| 8 | 469 | 1293 | 1 | ||
| 9 | 443 | 424 | 0 | 5 | |
| 10 | 444 | 1291 | 1 | ||
| 11 | 522 | 346 | 0 | ||
| 12 | 442 | 1290 | 1 | ||
| 13 | 417 | 1339 | 1 | A | |
| 14 | 418 | 454 | 0 | ||
| 15 | 389 | 1316 | 1 | ||
| 16 | 442 | 426 | 0 | ||
| 17 | 446 | 1286 | 1 | F | 32ビット データ |
| 18 | 442 | 1291 | 1 | ||
| 19 | 496 | 1240 | 1 | ||
| 20 | 446 | 1282 | 1 | ||
| 21 | 442 | 428 | 0 | 1 | |
| 22 | 444 | 424 | 0 | ||
| 23 | 444 | 425 | 0 | ||
| 24 | 469 | 1267 | 1 | ||
| 25 | 444 | 422 | 0 | 4 | |
| 26 | 450 | 1281 | 1 | ||
| 27 | 418 | 456 | 0 | ||
| 28 | 446 | 424 | 0 | ||
| 29 | 444 | 1290 | 1 | 8 | |
| 30 | 446 | 396 | 0 | ||
| 31 | 472 | 422 | 0 | ||
| 32 | 451 | 445 | 0 | ||
| 33 | 423 | 475 | 0 | 6 | |
| 34 | 389 | 1286 | 1 | ||
| 35 | 472 | 1261 | 1 | ||
| 36 | 446 | 399 | 0 | ||
| 37 | 473 | 1282 | 1 | 8 | |
| 38 | 416 | 484 | 0 | ||
| 39 | 418 | 427 | 0 | ||
| 40 | 446 | 418 | 0 | ||
| 41 | 446 | 1261 | 1 | 8 | |
| 42 | 476 | 445 | 0 | ||
| 43 | 419 | 450 | 0 | ||
| 44 | 422 | 478 | 0 | ||
| 45 | 517 | 1156 | 1 | B | |
| 46 | 445 | 425 | 0 | ||
| 47 | 414 | 1318 | 1 | ||
| 48 | 478 | 1227 | 1 | ||
| 49 | 476 | 0 | ー | ー | ストップビット |
3.M5StickS3の赤外線通信機能の詳細
今回の動作確認に使用する「M5StickS3」には「赤外線送信LED」と「赤外線受信機(受信LED+受信アンプVSOP38338)」が内蔵されており、赤外線の送受信の動作確認を行うことができますが、いくつか注意点があるためここで確認しておきましょう。
・ESP32 RMT の使用
「RMT」とは、「ESP32」シリーズに内蔵されている「赤外線リモコンなどのパルス信号を高精度で送受信するためのタイマキャプチャ回路」です。
赤外線通信の信号は「数百μ秒」の短い時間の信号を処理する必要がありますが、GPIOの入力端子で受信信号を処理しようとすると、CPUが他の処理をしている間に信号を取りこぼす可能性があります。
「RMT」を使用するとCPUを介さず、「MARK(検知)時間とSPACE(未検知)時間」をハードウェアが記録して、後からCPU側から読み込むことができます。
・スピーカーアンプはOFF設定
「M5StickS3」の赤外線受信機能を使用する時は、スピーカーのアンプをオフにする必要があるため、下記のコードを初期設定に書いておく必要があります。
M5.Speaker.end(); // M5StickS3ではIR受信時にスピーカーアンプを停止する・送受信距離は30cm以上
赤外線通信を行うときは、送信側と受信側の距離は30cm以上離さないと安定した送受信ができません。
近すぎると受信が異常を起こす可能性があります。
4.動作確認(サンプルプログラム)
赤外線通信の送受信について、サンプルプログラムを使用して動作確認していきます。
「M5StickS3」の初期設定やライブラリについては、以下のリンクで詳しく紹介しています。

次に受信データを確認し、さらに受信したデータを「NEC」か「AEHA」かを判定します。最後に受信したデータをそのまま送信するプログラムで動作確認していきます。
動作確認には下画像の「M5StickS3」を使用しました。
本体上部で赤外線の送受信ができます。下写真の左側が送信ダイオード、右側が受信ダイオードです

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

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

SHARPのリモコンはAEHA(家製協)のデータ確認に使用しました。
・送信動作の確認
送信動作については「IRremoteESP8266」ライブラリを使用して確認していきます。
送信するデータは以下表のように、事前に解析して確認しておいたもので、手持ちのNECのリモコンとSHARP製テレビ(AQUOS)リモコンのものになります。
NECリモコン データ例
| ボタン | カスタム コード | データ | データ反転 |
|---|---|---|---|
| CHー | 00FF | A2 | 5D |
| CH | 〃 | 62 | 9D |
| CH+ | 〃 | E2 | 1D |
| PREV | 〃 | 22 | DD |
| NEXT | 〃 | 02 | FD |
| PLAY/PAUSE | 〃 | C2 | 3D |
| VOLー | 〃 | E0 | 1F |
| VOL+ | 〃 | A8 | 57 |
| EQ | 〃 | 90 | 6F |
| 0 | 〃 | 68 | 97 |
| 100+ | 〃 | 98 | 67 |
| 200+ | 〃 | B0 | 4F |
| 1 | 〃 | 30 | CF |
| 2 | 〃 | 18 | E7 |
| 3 | 〃 | 7A | 85 |
| 4 | 〃 | 10 | EF |
| 5 | 〃 | 38 | C7 |
| 6 | 〃 | 5A | A5 |
| 7 | 〃 | 42 | BD |
| 8 | 〃 | 4A | B5 |
| 9 | 〃 | 52 | AD |
SHARP(テレビ)リモコン データ例
| ボタン | アドレス | データ |
|---|---|---|
| 電源 | 555A | F148688B |
| 選局+ | 〃 | 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 |
サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。
#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)」を使用します。
以下のサンプルプログラムでは、受信データの検知時間の「MARK」と未検知時間の「SPACE」を1シンボル(1受信)として確認することができます。(受信データの前半が MARK、後半が SPACE)
実際に「M5StickS3」で「SHARPのテレビ(AQUOS)」のリモコンの電源ボタンを押した時の受信結果は以下のようになりました。
液晶画面には「Symbols:」として受信データの総数とその下に最初の8個の受信データが表示されます。

シリアル出力では以下のように、受信データの全てを確認することができます。
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
・
・
・
サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。
#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データ」として確認することができます。
実際に動作確認した結果は下画像のようになります。

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

「SHARP(AEHA)」テレビAQUOSのリモコンの「電源ボタン」を押したときの受信データは上画像のようになります。
サンプルプログラムは以下になります。コピペで貼り付けて書き込んでください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。
#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機器との連携や独自の赤外線制御システムの開発など、さまざまな応用が可能です。
ぜひ今回のサンプルプログラムをベースに、自分だけの赤外線制御ツール作りに挑戦してみてください♪











コメント