Arduinoで赤外線リモコン方式を使って双方向通信

この記事で学習できること

  • Arduinoを使って赤外線リモコンから発信される信号を解析できます。
  • Arduinoを使って赤外線リモコンと同じ信号を送信できます。
  • Arduinoを2台使って、赤外線リモコンの技術でデータの双方向通信を実現します。

赤外線リモコン通信の仕組み

  • テレビを使うときに電源ON/OFFやチャンネルの切り替えでリモコンを使います。リモコンのスイッチを押すとリモコンから信号が出て、テレビで受けているのだろうなと想像できます。家電製品のリモコン通信には赤外線を用いた通信がよく使われます。
  • テレビなどの使われるリモコン用赤外線LEDの波長は950nm付近の赤外線です。人間の目は、波長が約380nmから780nmまでの光を感じることができます。リモコン用赤外線は人間の目で感じることはありません。リモコン側に赤外線LEDが取り付けられており、このLEDを点滅させて信号を送信します。テレビ側には受光素子が取り付けられており赤外線を受信します。
  • 赤外線リモコンでテレビが操作できるのは、赤外線LEDを発光する手順(プロトコル)が決められているからです。テレビなどの受信側では受信した赤外線の信号をこのプロトコルを用いて解読できます。日本では、NEC、家電協(家電製品協会)、SONYなどのプロトコルがよく使われます。
赤外線リモコンによる信号送信
— 赤外線リモコンによる信号送信 —

NEC赤外線通信プロトコル仕様

NEC赤外線通信プロトコルの概要

基本時間単位 (T)562.5 µs
リーダーコード (データフレーム用)ON: 16 × T OFF: 8 × T
リーダーコード (リピートフレーム用)ON: 16 × T OFF: 4 × T
データ[0]ON: 1 × T OFF: 1 × T
データ[1]ON: 1 × T OFF: 3 × T
ストップビットON: 1 × T
データ長32 bit(カスタムコード16bit + データコード16bit)
搬送波周波数38 KHz

データフレームは[リーダーコード]+[カスタムコート]+[データーコード]+[ストップビット]の構成となっています。データ[0]とデータ[1]の平均を基本時間単位の3Tとすると、全体の長さは、24T + 32 x 3T + T = 121Tとなります。基本時間単位が562.5usですから、平均的な転送時間は、121T x 0.5625msc ≒ 68msec程度になります。

NECフォーマット
— NECフォーマット —

リモコンの同じキーを押さえ続けた場合は、リピートフレームと呼ばれる、[リーダーコード] + [ストップビット]の組み合わせが 108msec毎に送信される仕様になっています。

データフレームとリピートフレームは38KHz(推奨 1/3 ON、2/3 OFF)の搬送波で変調されて送信されます。

信号変調
— 信号変調 —

赤外線通信に使うライブラリ

赤外線信号の受信

赤外線受信用部品

  • 赤外線受信用モジュール 1838B を購入しました。3端子の部品で、1838Bの受光面側から見て、左端: Vout、中央: GND、右端: Vccの端子です。
1838B 外観 寸法
— 1838B 外観 寸法 —
  • 内部に受光素子とICが内蔵されており、赤外線感度の中心波長は940nm、内部のBPFの中心周波数は38KHzとなっています。赤外線のリモコン信号を受けると、増幅しアンプのゲイン調整を行い信号がVoutから出力されます。38KHzの搬送波は出力には現れません。
1838B 回路ブロック図
— 1838B 回路ブロック図 —
  • 赤外線受信用IC 1838Bの仕様書に記載されているBPFの周波数特性と赤外線の感度です。搬送波(38KHz)を通過させ、赤外線感度は波長950nm付近に設計されています。
1838B 赤外感度 周波数応答
— 1838B 赤外感度 周波数応答 —

赤外線受信用回路

  • 赤外線受信用モジュール 1838Bの仕様書に使用例が記載されています。5V電源から47Ωを介してVccに接続されており、出力は10KΩ以上の抵抗でプルアップとなっています。
1838B 回路図例
— 1838B 回路図例 —
赤外線受光回路 配線図
— 赤外線受光回路 配線図 —
  • 動作確認用の回路では、VoutをArduino UNO R3のDigtal 2Pinに接続します。

赤外線受信用回路部品

部品名個数個入先
赤外線受信モジュール 1838B1Amazon
Arduino UNO R31Amazon *1
抵抗 (47Ω)1Amazon *1
抵抗 (10KΩ)1Amazon *1
コンデンサ (47uF/16V)1Amazon *1

*1 Amazonで購入した[Elegoo Arduino 学習キット]に付属していました。

赤外線受信用のスケッチ

  • リモコンからの赤外線信号を受け取ると以下の情報をシリアルモニタに表示します。
    • プロトコルの種類
    • 受信したフレームのデータ(Data frameのData Code)
    • 受信したフレームのアドレス(Data frameのCustom Code)
    • 受信したフレームの生データ(デコード前のデータ)
  • シリアル通信の速度は115200bpsに設定しています。
#include <Arduino.h>
#include <IRremote.hpp> // v3.x以降の新しいヘッダーファイル

const int IR_RECEIVE_PIN = 2; // 受信ピンをDigital 3に指定

void setup() {
  Serial.begin(115200); // シリアル通信の速度を115200bpsに設定
  while (!Serial);      // シリアルポートの準備完了を待つ

  Serial.println(F("=== 赤外線リモコン受信テスト開始 ==="));
  Serial.print(F("受信ピン: Digital "));
  Serial.println(IR_RECEIVE_PIN);

  // 赤外線受信の開始(LEDフィードバック有効:ボード上のLEDが受信時に点滅します)
  IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
}

void loop() {
  // 赤外線信号を受信したかチェック
  if (IrReceiver.decode()) {
    
    // リピートコード(長押し信号)の場合はスキップしたい場合などの判定用
    if (IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT) {
      Serial.println(F("-> [Repeat Code]"));
    } else {
      Serial.println(F("------------------------------------"));
      
      // 1) 通信方式(プロトコル)の出力
      Serial.print(F("通信方式 (Protocol) : "));
      Serial.println(getProtocolString(IrReceiver.decodedIRData.protocol));

      // 2) 受け取ったデータ(コマンドコード / アドレス / Rawデータ)
      Serial.print(F("コマンドデータ (Command)  : 0x"));
      Serial.println(IrReceiver.decodedIRData.command, HEX);

      Serial.print(F("アドレスデータ (Address)  : 0x"));
      Serial.println(IrReceiver.decodedIRData.address, HEX);

      // 参考:デコードされた生の32bitデータ
      Serial.print(F("Decoded Raw Data         : 0x"));
      Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX);
    }

    // 次の信号を受信するためにレシーバーを再開
    IrReceiver.resume();
  }
}
IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK)
  • IRremoteを開始します。IR_RECEIVE_PINは必須です。信号を入力するピン番号を設定します。ENABLE_LED_FEEDBACKは必須ではありません。赤外線信号を受信した際に、Arduino本体のオンボードLED(13番ピン/LED_BUILTIN)を自動で一瞬点滅させてフィードバックを表示します。
if (IrReceiver.decode()) { 
  ...

  IrReceiver.resume();
}
  • IrReceiver.decode()は受信したデータのデコードが完了すれば真の値を返す関数です。if文の評価に使っていれば、if文の処理を開始します。真の値を返すと、受信信号のデコードを中止します
  • IrReceiver.resume()は、受信信号のデーコードを再開する関数です
IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)
  • IrReceiver.decodedIRData.flagsは受信した赤外線フレームのステータス情報保持している変数です。
  • IRDATA_FLAGS_IS_REPEATは次のFrame開始が、前のFrameの最長よりも早く発生した場合Repeatと判断するフラグです。
  • スケッチでは、フラグがIRDATA_FLAGS_IS_REPEATの場合は、Repeatと判断して”-> [Repeat Code]”を表示します。
getProtocolString()
  • getProtocolString()はIRremote ライブラリ内部で定義されている通信プロトコル番号を、人間が読みやすい文字列に変換して返してくれる関数です。
IrReceiver.decodedIRData.protocol
IrReceiver.decodedIRData.command
IrReceiver.decodedIRData.address
IrReceiver.decodedIRData.decodedRawData
  • 赤外線信号のデコードが成功した場合に信号の各要素はdecodedIRData構造体に書き込まれます。
  • IrReceiver.decodedIRData.protocol : プロトコル名
  • IrReceiver.decodedIRData.command : Data Codeの値
  • IrReceiver.decodedIRData.address : Custom Codeの値
  • IrReceiver.decodedIRData.decodedRawData : Data CodeとCustom Codeの生の値

赤外線受信モジュールからの出力波形測定

  • Arduino UNO R3に接続されている1838B(赤外線受信モジュール)の出力信号をオシロスコープで測定します。
  • Arduinoを使うElegoo製のスタータキットにNECフォーマットのリモコンが付属していました。このリモコンを学習に使います。リモコンの’0’キーを押したときの波形です。
NECフォーマット 受信波形
— NECフォーマット 受信波形 —
  • 波形を眺めます。短い周期の波形は周期[0]、長い周期の波形は[1]に対応します。これを書きとって、NECフォーマットに従って手作業でデコードしてみます。LSB firstなので少し戸惑いますが、桁数は少ないので以下のような結果となりました。後述する、シリアルモニタの出力と同じであることが解ります。
  • Leader Code
  • Custom Code : 00000000111111 -> LSB first 0xFF00
  • Data Code : 0110100010010111 ->LSB first 0xE916
  • Stop Bit
  • 1838BはNECフォーマットの説明と比べると出力のHi-Loが反転していますが、[IRremote]は反転を標準として扱い、波形がNECフォーマットの説明通りのActive Hiの場合は、下記の書式のように、’INVERT_INPUT’オプションを加えることで正常に処理できます。
IrReceiver.begin(IR_RECEIVE_PIN, DISABLE_LED_FEEDBACK, INVERT_INPUT)

シリアルモニタの出力

  • Arduinoの学習用キットに付属していたリモコンから送信される信号をArduinoで受信し、解析しました。シリアルモニタに以下の結果が出力されます。
    • プロトコル : (NEC、Panasonic など)
    • アドレスデータ: (設定されているデータ)
    • Decoded Raw Data : (送信されたデータ)
    • リモコンのキーを長く押していると、”-> [Repeat Code]”が表示されます。
  • シリアルモニタの出力から以下が解ります。
    • Arduinoに付属していたリモコンの通信方式は、 NECフォーマット
    • ‘0’キーに対応して送信されるコマンドデータは、 0x16
    • Arduinoに付属していたリモコンのアドレスデータは、 0x0

=== 赤外線リモコン受信テスト開始 ===
受信ピン: Digital 2
------------------------------------
通信方式 (Protocol) : NEC
コマンドデータ (Command)  : 0x16
アドレスデータ (Address)  : 0x0
Decoded Raw Data         : 0xE916FF00
-> [Repeat Code]
-> [Repeat Code]
  • IRremoteライブラリにはプロトコルの自動判別機能があります。試しに、家庭で使っているPanasonicのテレビ用リモコンで’0’を押したときの信号を受信しデータを解析しました。同じキーでも異なった数値が割り当てられていることが解ります。
=== 赤外線リモコン受信テスト開始 ===
受信ピン: Digital 2
------------------------------------
通信方式 (Protocol) : Panasonic
コマンドデータ (Command)  : 0x69
アドレスデータ (Address)  : 0x98
Decoded Raw Data         : 0xE0690980
-> [Repeat Code]
  • 作成したスケッチで各社のリモコンからの信号を受信して解析すれば、通信方式(プロトコル)の種類とそれぞれのキーにどのような数値が割付けられているかを確認できます。

赤外線信号の送信

  • Arduino UNO R3に赤外線LEDを取り付けて赤外線信号を送信します。[赤外線信号の受信]で作成した回路とスケッチを使って信号を受信します。

赤外線送信回路と使用部品

  • 赤外線LEDを購入しました。赤外線波長は940nmの仕様です。Arduino UNO R3のポートの電流は最大で20mAです。ダイオードの電圧降下を1.5Vとすると、Vcc = 5Vの場合 電流を15mA流すためには、(5 – 1.5)/0.015 =233Ω となりますが、よく使われる220Ωを使うとします。この場合の電流は、(5 – 1.5)/220 ≒ 16mAとなります。
  • 赤外線LEDは極性があります。アノードを’+’側に接続します。足が長い方がアノードです。
  • LEDの仕様範囲内で多く電流を流せば輝度が増して遠くまで通信ができます。その場合はトランジスタを使ってのLED駆動が必要になります。

— 送信回路 —

部品名個数個入先
赤外線LED1Amazon
Arduino UNO R31Amazon *1
抵抗 (220Ω)1Amazon *1

*1 Amazonで購入した[Elegoo Arduino 学習キット]に付属していました。

赤外線送信用のスケッチ

  • 赤外線信号を以下の仕様で送信します。
    • プロトコル : NECフォーマット
    • Data frameのData Code : 0x42
    • Data frameのCustom Code : 0x01
  • Data CodeとCustom Codeは適当に決めた数値です。
  • 送信は5秒間隔で繰り返します。
  • シリアル通信の速度は115200bpsに設定しています。
#include <Arduino.h>
#include <IRremote.hpp>

// 赤外線LEDを接続するデジタルピンを指定
const int IR_SEND_PIN = 3;

// 送信するアドレスとコマンドの定義
const uint16_t sAddress = 0x01; // アドレス(実験用リモコンでは0x00が一般的)
const uint8_t  sCommand = 0x42; // 指定のコマンド(16進数の42 = 0x42)
const int      sRepeats = 2;    // リピート回数

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

  Serial.println(F("=== 赤外線送信テスト開始 ==="));
  Serial.print(F("送信ピン: Digital "));
  Serial.println(IR_SEND_PIN);

  // 送信機能の初期化
  IrSender.begin(IR_SEND_PIN);
}

void loop() {
  Serial.println(F("NEC信号を送信します..."));
  Serial.print(F("  Address : 0x"));
  Serial.println(sAddress, HEX);
  Serial.print(F("  Command : 0x"));
  Serial.println(sCommand, HEX);
  Serial.print(F("  Repeat  : "));
  Serial.println(sRepeats); 

  // NEC方式でデータ送信
  // sendNEC(Address, Command, Repeats)
  IrSender.sendNEC(sAddress, sCommand, sRepeats);

  Serial.println(F("送信完了。5秒待機します...\n"));
  
  // 5秒(5000ミリ秒)間隔
  delay(5000);
}
IrSender.begin(IR_SEND_PIN)
  • IRremoteを使って赤外線信号送信を開始します。IR_SEND_PIは必須です。信号を出力するピン番号を設定します。
IrSender.sendNEC(sAddress, sCommand, sRepeats)
  • IrSender.sendNEC()は設定された下記の引数をNECフォーマットでエンコードして送信する関数です。
  • sAddress : Data frameのCustom Code
  • sCommand : Data frameのData Code
  • sRepeats : 繰り返し回数 (0でも良い)

送信信号と受信信号の出力波形測定

  • 赤外線モジュールの受信信号出力と赤外線送信信号をオシロスコープで測定しました。
    • 測定箇所 : リーダーコード (リピートフレーム用)
    • 1-Ch : 赤外線受光モジュール(1838B)の出力
    • 2-Ch : 赤外線LEDの出力
— NECフォーマット 信号変調 —
  • 受信モジュールは搬送波(38Hz)を内部で検出し信号波形のみ出力します。実際に送信されている信号は搬送波により変調されていることが解ります。

シリアルモニタの出力

  • 送信用スケッチを走らせると信号の送信条件がシリアルモニタに表示されます。
=== 赤外線送信テスト開始 ===
送信ピン: Digital 3
NEC信号を送信します...
  Address : 0x1
  Command : 0x42
  Repeat  : 2
送信完了。5秒待機します...
  • 受信学習で使った受信用スケッチを2台目のArduinoで走らせます。送信用のArduinoからの赤外線信号を受信すると、受信用Arduinoに受信した信号の解析結果が表示されます。送信で設定された条件どおりに受信していることが解ります。
=== 赤外線リモコン受信テスト開始 ===
受信ピン: Digital 2
------------------------------------
通信方式 (Protocol) : NEC
コマンドデータ (Command)  : 0x42
アドレスデータ (Address)  : 0x1
Decoded Raw Data         : 0xBD42FE01
-> [Repeat Code]
-> [Repeat Code]
  • 赤外線LEDから設定した信号を送信する方法が解りました。各種リモコンの信号設定は受信用のスケッチで確認できます。確認した信号の設定を送信側のスケッチに書き込み、その信号をArduinoを使って送信できます。これが学習型リモコンの動作です。
  • 自作のリモコンでおもちゃを動かしたい場合など、前後左右のボタンを配置してそれぞれにコードを設定すれば専用リモコンが作れます。

赤外線信号の双方向通信

  • 赤外線通信の送受信は受信回路に赤外線受信モージュールを使い、送信回路には赤外線LEDを使っています。この両方の回路を一つのブレッドボードに取り付け、RS-232C通信のTX、RXのようにハンドシェーク的な機能を持たせて、2台のArduino間で赤外線通信させてみます。1台のArduinoには温度センサーを取り付けて温湿度を測定し、測定した温湿度データをもう一台のArduinoに転送します。
  • 温度データは、1台のArduinoからもう1台に送られますが、実際のデータ転送は、2台のArduinoでデータ転送後に受信側から送信側に受取りの確認の送信を行いデータ転送の安全性を高めてた双方向通信にしてます。
— 赤外線 双方向通信回路 —

赤外線送受信回路と使用部品

  • Arduino UNO R3を2台使います。2台ともに受信回路と送信回路を組み込みます。
  • Adafruit社製温度センサADT7410を1台に追加します。SDA,SCL,GND,VCCの端子をArduinoに接続します。温湿度センサ を組み込んだArduinoは、5秒間隔で温度を測定し、もう一台のArduinoにデータを転送します。
  • Adafruit社製温度センサADT7410の使い方は[Arduinoで高精度温度センサデータを取得する方法]を参照ください。

赤外線通信を使った実験用双方向プロトコル

  • Arduino UNO R3 2台を使ったデータ送信は外部のノイズによる送受信の失敗を考慮して、通信の確立とデータ転送それぞれで受信側から受信確認を受け取りながら1バイト毎にデータを送ります。
  • NECフォーマットではCustom Codeは各メーカの固有番号ですが、双方向通信の実験ではこの領域もデータ転送の確認用に使いました。
- Address とは Custom Codeのこと
- Data とは Data Codeのこと

- Address
  00 通信状態の確認に使用する
  01以降 データ通信時に01からデータ一つ毎に一つ増やし同時に送信する
  
(1) (送信側)
  Address を 0、 Dataを '0x01'にセットし受信側へ送信する
  
   (受信側)
  上記を正常に受信した場合は
  Address を 0、 Dataを '0x02'にセットし送信側へ送信する
  
 (送信側)
  正常に返信されない場合は(1)へ返る
  返信待ち(ACK待ち)のタイムアウトは1000msecとする
  
(2) (送信側)
  Address を 01、 Dataを温度データのMSBをアスキーコードに変換した数値を受信側へ送信する

  (受信側)
  上記を正常に受信した場合は
  Address を 01、 Dataを温度データのMSBをアスキーコードに変換した数値を送信側へ送信する  
  
  (送信側)
  正常に返信されない場合は(2)へ返る 
  返信待ち(ACK待ち)のタイムアウトは1000msecとする  
  正常に返信された場合は、温度の次の数値を受信側に送る、同時にアドレスの数値はひとつづつ増やす

  ...  温湿度データを送り終わるまでこれを繰り返す
  
(3) (送信側)
  データがAddress を 0、 Dataを '0x03'にセットし受信側へ送信する   
  
  (受信側)
  上記を正常に受信した場合は
  Address を 0、 Dataを '0x04'にセットし送信側へ送信する  

  (送信側)
  正常に返信されない場合は(3)へ返る

正常に終了した場合は、5秒間待ち、(1)からの手順で次の温湿度データを送信する。
赤外線 双方向通信 手順
— 赤外線 双方向通信 手順 —

赤外線の双方向通信用のスケッチ

  • 赤外線信号の双方向通信は、基本的には、赤外線信号の受信と赤外線信号の送信の機能の組み合わせになります。これに加えて温湿度データの転送方法と送受信に係るハンドシェークの動作を実装しています。
  • 解析用にデバッグの出力ができるようにしています。#define DEBUGをコメントアウトすると出力されません。CPUの負荷を減らすことができます。
  • シリアル通信の速度は115200bpsに設定しています。
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_ADT7410.h>
#include <IRremote.hpp>

#define DEBUG

#ifdef DEBUG
  #define DEBUG_PRINT(x)       Serial.print(x)
  #define DEBUG_PRINTLN(x)     Serial.println(x)
  #define DEBUG_PRINT_HEX(x)   Serial.print(x, HEX)
  #define DEBUG_PRINTLN_HEX(x) Serial.println(x, HEX)
#else
  #define DEBUG_PRINT(x)
  #define DEBUG_PRINTLN(x)
  #define DEBUG_PRINT_HEX(x)
  #define DEBUG_PRINTLN_HEX(x)
#endif

enum VerifyResult {
  RESULT_SUCCESS = 0,
  RESULT_TIMEOUT,
  RESULT_MISMATCH
};

const int IR_RECEIVE_PIN = 2;
const int IR_SEND_PIN    = 3;

Adafruit_ADT7410 tempsensor = Adafruit_ADT7410();

VerifyResult sendAndVerify(uint16_t sendAddr, uint8_t sendData, uint16_t expectedAddr, uint8_t expectedData);
bool getTemperatureWithTimeout(float &temp);

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

  Serial.println(F("=== 送信側 (Arduino #1) 起動 ==="));

  if (!tempsensor.begin(0x48)) {
    Serial.println(F("[ERROR] ADT7410センサが検出できません。配線を確認してください。"));
    while (1);
  }

  IrSender.begin(IR_SEND_PIN);
  IrReceiver.begin(IR_RECEIVE_PIN, DISABLE_LED_FEEDBACK);
}

void loop() {
  float tempVal = 0.0;

  Serial.println(F("\n----------------------------------------"));
  Serial.println(F("[ADT7410] 温度測定開始..."));

  if (!getTemperatureWithTimeout(tempVal)) {
    Serial.println(F("[ERROR] 温度データ取得タイムアウト (5秒)"));
    delay(1000);
    return;
  }

  char tempStr[6];
  dtostrf(tempVal, 4, 1, tempStr);
  if (tempStr[0] == ' ') tempStr[0] = '0';
  String sendString = String(tempStr);

  Serial.print(F("[測定成功] 温度: "));
  Serial.print(tempVal, 1);
  Serial.print(F(" ℃ (送信データ: \""));
  Serial.print(sendString);
  Serial.println(F("\")"));

  // (1) 通信状態確認
  DEBUG_PRINTLN(F("[Step 1] 通信確認要求 (Addr:0x00, Data:0x01)"));
  while (true) {
    VerifyResult res = sendAndVerify(0x00, 0x01, 0x00, 0x02);
    if (res == RESULT_SUCCESS) {
      DEBUG_PRINTLN(F("  [OK] 通信確認ACK受信"));
      break;
    } else if (res == RESULT_TIMEOUT) {
      DEBUG_PRINTLN(F("  [TIMEOUT] ACK未受信。再試行..."));
    } else if (res == RESULT_MISMATCH) {
      DEBUG_PRINTLN(F("  [MISMATCH] ACK不一致。再試行..."));
    }
    delay(200);
  }

  // (2) 温度文字列送信
  uint16_t currentAddr = 0x01;
  int strLen = sendString.length();

  for (int i = 0; i < strLen; i++) {
    uint8_t charToSend = sendString.charAt(i);
    delay(100);

    DEBUG_PRINT(F("[Step 2] データ送信 ["));
    DEBUG_PRINT(i + 1);
    DEBUG_PRINT(F("/"));
    DEBUG_PRINT(strLen);
    DEBUG_PRINT(F("]: Addr:0x"));
    if (currentAddr < 0x10) DEBUG_PRINT('0');
    DEBUG_PRINT_HEX(currentAddr);
    DEBUG_PRINT(F(", Data:'"));
    DEBUG_PRINT((char)charToSend);
    DEBUG_PRINTLN(F("'"));

    while (true) {
      VerifyResult res = sendAndVerify(currentAddr, charToSend, currentAddr, charToSend);
      if (res == RESULT_SUCCESS) {
        DEBUG_PRINTLN(F("  [OK] ACK受信"));
        break;
      } else if (res == RESULT_TIMEOUT) {
        DEBUG_PRINTLN(F("  [TIMEOUT] ACK未受信。再試行..."));
      } else if (res == RESULT_MISMATCH) {
        DEBUG_PRINTLN(F("  [MISMATCH] ACK不一致。再試行..."));
      }
      delay(200);
    }

    currentAddr++;
  }

  // (3) 送信完了通知
  delay(100);
  DEBUG_PRINTLN(F("[Step 3] 完了通知送信 (Addr:0x00, Data:0x03)"));
  while (true) {
    VerifyResult res = sendAndVerify(0x00, 0x03, 0x00, 0x04);
    if (res == RESULT_SUCCESS) {
      DEBUG_PRINTLN(F("  [OK] 完了応答ACK受信"));
      break;
    } else if (res == RESULT_TIMEOUT) {
      DEBUG_PRINTLN(F("  [TIMEOUT] ACK未受信。再試行..."));
    } else if (res == RESULT_MISMATCH) {
      DEBUG_PRINTLN(F("  [MISMATCH] ACK不一致。再試行..."));
    }
    delay(200);
  }

  Serial.println(F("[送信完了] 5秒間待機します。"));
  delay(5000);
}

bool getTemperatureWithTimeout(float &temp) {
  unsigned long start = millis();
  while (millis() - start < 5000) {
    float t = tempsensor.readTempC();
    if (!isnan(t) && t > -40.0 && t < 125.0) {
      temp = t;
      return true;
    }
    delay(100);
  }
  return false;
}

VerifyResult sendAndVerify(uint16_t sendAddr, uint8_t sendData, uint16_t expectedAddr, uint8_t expectedData) {
  // 1. 送信前にバッファを完全クリア
  IrReceiver.resume();

  // 2. パルス送信
  IrSender.sendNEC(sendAddr, sendData, 0);

  // 3. 送信パルス(約68ms)+余韻が完全に消去されるのを待つ
  delay(90);

  // 4. 自局送信の回り込みデータをすべて読み捨ててパージする
  while (IrReceiver.decode()) {
    IrReceiver.resume();
    delay(5);
  }

  // 5. ACK受信用にバッファを最終リセット
  IrReceiver.resume();

  unsigned long startTime = millis();

  // 6. 相手からのACK待機 (1000ms)
  while (millis() - startTime < 1000) {
    if (IrReceiver.decode()) {
      if (IrReceiver.decodedIRData.protocol == NEC && 
          !(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {

        uint16_t recvAddr = IrReceiver.decodedIRData.address;
        uint8_t  recvData = IrReceiver.decodedIRData.command;

        // 自分が今送ったデータ(回り込みの遅延)なら無視して待機継続
        if (recvAddr == sendAddr && recvData == sendData && 
           !(sendAddr == expectedAddr && sendData == expectedData)) {
          IrReceiver.resume();
          continue;
        }

        // 期待するACKが来たら成功
        if (recvAddr == expectedAddr && recvData == expectedData) {
          IrReceiver.resume();
          return RESULT_SUCCESS;
        } else {
          DEBUG_PRINT(F("    [ミスマッチ] 期待: Addr:0x"));
          DEBUG_PRINT_HEX(expectedAddr);
          DEBUG_PRINT(F(" Data:0x"));
          DEBUG_PRINT_HEX(expectedData);
          DEBUG_PRINT(F(" / 受信: Addr:0x"));
          DEBUG_PRINT_HEX(recvAddr);
          DEBUG_PRINT(F(" Data:0x"));
          DEBUG_PRINTLN_HEX(recvData);

          IrReceiver.resume();
          return RESULT_MISMATCH;
        }
      }
      IrReceiver.resume();
    }
  }

  return RESULT_TIMEOUT;
}
  • シリアル通信の速度は115200bpsに設定しています。
#include <Arduino.h>
#include <IRremote.hpp>

#define DEBUG

#ifdef DEBUG
  #define DEBUG_PRINT(x)       Serial.print(x)
  #define DEBUG_PRINTLN(x)     Serial.println(x)
  #define DEBUG_PRINT_HEX(x)   Serial.print(x, HEX)
  #define DEBUG_PRINTLN_HEX(x) Serial.println(x, HEX)
#else
  #define DEBUG_PRINT(x)
  #define DEBUG_PRINTLN(x)
  #define DEBUG_PRINT_HEX(x)
  #define DEBUG_PRINTLN_HEX(x)
#endif

const int IR_RECEIVE_PIN = 2;
const int IR_SEND_PIN    = 3;

void sendResponse(uint16_t addr, uint8_t data);

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

  Serial.println(F("=== 受信側 (Arduino #2) 起動 ==="));

  IrSender.begin(IR_SEND_PIN);
  IrReceiver.begin(IR_RECEIVE_PIN, DISABLE_LED_FEEDBACK);
}

void loop() {
  static String receivedString = "";
  static uint16_t expectedAddr = 0x01;
  static bool inDataSequence   = false;

  if (IrReceiver.decode()) {
    if (IrReceiver.decodedIRData.protocol == NEC && 
        !(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {

      uint16_t addr = IrReceiver.decodedIRData.address;
      uint8_t  data = IrReceiver.decodedIRData.command;

      // (1) 通信開始要求
      if (addr == 0x00 && data == 0x01) {
        DEBUG_PRINTLN(F("\n[Step 1] 通信開始要求を受信 (Addr:0x00, Data:0x01)"));
        
        receivedString = "";
        expectedAddr   = 0x01;
        inDataSequence = true;

        DEBUG_PRINTLN(F("  -> ACK応答送信 (Addr:0x00, Data:0x02)"));
        sendResponse(0x00, 0x02);
      }
      // (3) 通信完了通知
      else if (addr == 0x00 && data == 0x03) {
        DEBUG_PRINTLN(F("[Step 3] 通信完了通知を受信 (Addr:0x00, Data:0x03)"));
        DEBUG_PRINTLN(F("  -> ACK応答送信 (Addr:0x00, Data:0x04)"));
        sendResponse(0x00, 0x04);

        Serial.print(F("\n========================================\n"));
        Serial.print(F("[受信成功] 取得温度: "));
        Serial.print(receivedString);
        Serial.println(F(" ℃\n========================================\n"));

        inDataSequence = false;
      }
      // (2) データ1文字受信
      else if (inDataSequence && addr == expectedAddr) {
        char c = (char)data;
        receivedString += c;

        DEBUG_PRINT(F("[Step 2] データ受信 [Addr:0x"));
        if (addr < 0x10) DEBUG_PRINT('0');
        DEBUG_PRINT_HEX(addr);
        DEBUG_PRINT(F(", Data:'"));
        DEBUG_PRINT(c);
        DEBUG_PRINT(F("'] -> 現在の文字列: \""));
        DEBUG_PRINT(receivedString);
        DEBUG_PRINTLN(F("\""));

        DEBUG_PRINT(F("  -> エコーバック送信 (Addr:0x"));
        if (addr < 0x10) DEBUG_PRINT('0');
        DEBUG_PRINT_HEX(addr);
        DEBUG_PRINT(F(", Data:'"));
        DEBUG_PRINT(c);
        DEBUG_PRINTLN(F("')"));

        sendResponse(addr, data);
        expectedAddr++;
      }
      else {
        DEBUG_PRINT(F("[IGNORE] 予期しないデータ: Addr:0x"));
        DEBUG_PRINT_HEX(addr);
        DEBUG_PRINT(F(", Data:0x"));
        DEBUG_PRINTLN_HEX(data);
        IrReceiver.resume();
      }
    } else {
      IrReceiver.resume();
    }
  }
}

void sendResponse(uint16_t addr, uint8_t data) {
  // 送信側が「パルス送信+自局パルス破棄」を終えるまで十分待つ
  delay(120);

  // ACK送信
  IrSender.sendNEC(addr, data, 0);

  // 送信パルス完了待ち
  delay(90);

  // 自局ACK送信パルスの回り込みを完全パージ
  while (IrReceiver.decode()) {
    IrReceiver.resume();
    delay(5);
  }
  IrReceiver.resume();
}

シリアルモニタの出力

  • 送信側のArduino UNO R3のシリアルモニタの出力です。温度を測定した後、通信を確認し、データを転送、通信を終了するまでの流れです。設計したフロー通りにデータがやり取りされているのが解ります。
  • データ送信は5秒間隔で実行しています。
[ADT7410] 温度測定開始...
[測定成功] 温度: 27.3 ℃ (送信データ: "27.3")
[Step 1] 通信確認要求 (Addr:0x00, Data:0x01)
  [OK] 通信確認ACK受信
[Step 2] データ送信 [1/4]: Addr:0x01, Data:'2'
  [OK] ACK受信
[Step 2] データ送信 [2/4]: Addr:0x02, Data:'7'
  [OK] ACK受信
[Step 2] データ送信 [3/4]: Addr:0x03, Data:'.'
  [OK] ACK受信
[Step 2] データ送信 [4/4]: Addr:0x04, Data:'3'
  [OK] ACK受信
[Step 3] 完了通知送信 (Addr:0x00, Data:0x03)
  [OK] 完了応答ACK受信
[送信完了] 5秒間待機します。
  • 受信側のArduino UNO R3のシリアルモニタの出力です。送信側から通信開始の要求を受けた後、データを受信し、通信完了まで終了しています。
[Step 1] 通信開始要求を受信 (Addr:0x00, Data:0x01)
  -> ACK応答送信 (Addr:0x00, Data:0x02)
[Step 2] データ受信 [Addr:0x01, Data:'2'] -> 現在の文字列: "2"
  -> エコーバック送信 (Addr:0x01, Data:'2')
[Step 2] データ受信 [Addr:0x02, Data:'7'] -> 現在の文字列: "27"
  -> エコーバック送信 (Addr:0x02, Data:'7')
[Step 2] データ受信 [Addr:0x03, Data:'.'] -> 現在の文字列: "27."
  -> エコーバック送信 (Addr:0x03, Data:'.')
[Step 2] データ受信 [Addr:0x04, Data:'3'] -> 現在の文字列: "27.3"
  -> エコーバック送信 (Addr:0x04, Data:'3')
[Step 3] 通信完了通知を受信 (Addr:0x00, Data:0x03)
  -> ACK応答送信 (Addr:0x00, Data:0x04)

========================================
[受信成功] 取得温度: 27.3 ℃
========================================

よくあるトラブルと対処法

リモコン信号を受信しない配線の確認、赤外線の遮蔽の有無を確認
赤外線信号を発信できない配線の確認、赤外線の遮蔽の有無を確認
双方向の送受信ができない配線の確認、適切なスケッチが使われているかを確認

まとめ

  • 赤外線通信を使ったリモコン信号の受信回路および送信回路とそれぞれのデコードの方法を学習できました。
  • 赤外線通信の送信と受信の機能を使って、離れた2点でデータの双方向通信ができることを学習できました。双方向の通信でデータ送受信を確認しながらデータ転送を進める手順を学習できたなと思います。いったん送信したデータを再度送りなおしていますので、冗長な手順ですがこの方法で2台のArduino間で赤外線通信ができることが解りました。転送速度が遅くても良い小さなデータであれば使えると思います。

ご質問、誤植の指摘などありましたら。「問い合わせ 」のページからお願いします。