赤外線リモコンの送受信をしてみる[IRRemote]
赤外線リモコンのプロトコル
リモコンが赤外線を利用して通信していることは広く知られていますが、どのような信号が送信されているかは皆さんご興味ないでしょう...
ということで実際の信号をロジックアナライザーで解析するとこのようになっています。上が赤外線LEDの波形、下が受光素子の出力です。

LEDの波形が塗りつぶされたようになっていますが、拡大するとさらに細かい波形の集まりであることがわかります。この波(搬送波)は38kHz(duty=1/3)になっており、受光素子がフィルターすることで下のようなよく見るデジタル信号になります。受光素子がアクティブロー出力なのでパルス波を受信しているときにLOWになっています。(遅延がありズレていますが...)

最初から下のように点滅させればいいように思えますが、こうすることでリモコンと環境中の赤外線を区別し、誤動作を防ぐことができるのです。
ちなみに赤外線リモコンのプロトコルには複数の種類がありますが、このあたりの仕組みは共通しているため、どのプロトコルでも共通の回路を使用することができます。

一般的なデジタル信号では単にHIGHを1、LOWを0としていますが、クロック信号の使えない赤外線リモコンではモールス信号のようにHIGHとLOWの比で0と1を表します。
基準となる単位時間tを562μsとして、0はHIGH 1t+LOW 1t 、1はHIGH 1t+LOW 3tを送信します。
このへんからプロトコルによる違いが出てきますが、ここではシンプルで、かつ最も普及しているNECフォーマットを解説します。
NECフォーマットはリーダー、アドレス、コマンドの3つから成ります。
- リーダー: データ送信の始まりを知らせるもので、16tのHIGHと8tのLOWを送信
- アドレス: メーカーや機器を識別するもの、16bitの固定長
- コマンド: 8bitにエラーチェック用の反転データを加えた、合計16bitの固定長
- (ストップビット: 560µsのHIGHを送信)
画像の例で送られているデータは10000000 00001011 00110001 11001110です。
最初の16bitがアドレスなのですが、LSB first(最後のビットから送られてくる)なので、11010000 00000001、16進数表記で0xD001がアドレスになります。
次の8bitがコマンドでアドレス同様にLSB firstなので0x8Cです。
最後の8bitは反転データです。11001110を反転すると00110001となり、コマンドと一致するため正常に受信したことが確認できました。プログラムとしてはXOR演算して0xffになるかを検証します。(XORは入力が違うときに1、同じ時に0となる)
ラズピコで送受信してみる
これで赤外線リモコンのプロトコルを理解できたので解析プログラムを書いてみましょう... と言いたいところですが、これを実装するのは大変なのでIRRemoteというライブラリを使います。ArduinoのライブラリマネージャでIRRemoteと検索してインストールしてください。
Note
エアコンのリモコンについて
照明やテレビのリモコンがボタンごとに固定のデータを送るのに対し、エアコンのリモコンはどれかを押すたびに全ての状態のデータを送信します。これは、うまく受信ができないとリモコン上の画面と実際の設定がずれてしまうためです。そのため頑張って解析するか、ある状態の時の送信データをプリセットとして使います。
受信
信号の受信にはXINGLIGHTのXL-IRM0038Aを使いました。LCSCで20円ほどです。
https://lcsc.com/product-detail/Infrared-Remote-Receiver-IRM_XINGLIGHT-XL-IRM0038A_C19725366.html
同様の製品が秋月などでも販売されています。どれを買っても使い方は大体同じなのでお好きな店で入手してください。
https://akizukidenshi.com/catalog/c/cirreceiv/
回路は基本的に電源の配線とOUTピンをGPIOに接続するだけです。(詳細はデータシートを参照)
Note
XL-IRM0038Aのピン配置
受光部側からみて、左から[OUT, GND, VCC]
コンデンサとかプルアップ抵抗なしでも問題なく動作した
サンプルコード
#include <IRremote.hpp>
#define RECEIVER_PIN 0
void setup() {
Serial.begin(115200);
IrReceiver.begin(RECEIVER_PIN);
}
void loop() {
if (IrReceiver.decode()) {
IrReceiver.printIRResultShort(&Serial);
IrReceiver.resume();
}
}
シーリングライトのリモコンを受信すると以下のような結果になりました。
明るく
Protocol=NEC Address=0x6D82 Command=0xBA Raw-Data=0x45BA6D82 32 bits LSB first Gap=3276750us Duration=67350us
暖色
Protocol=NEC Address=0x6D82 Command=0xA8 Raw-Data=0x57A86D82 32 bits LSB first Gap=453550us Duration=67350us
寒色
Protocol=NEC Address=0x6D82 Command=0xA7 Raw-Data=0x58A76D82 32 bits LSB first Gap=473050us Duration=67350us
暗く
Protocol=NEC Address=0x6D82 Command=0xBB Raw-Data=0x44BB6D82 32 bits LSB first Gap=796550us Duration=67350us
Protocol=NEC Address=0x6D82 Command=0xBB Repeat Gap=41050us Duration=11900us
Protocol=NEC Address=0x6D82 Command=0xBB Repeat Gap=96600us Duration=11850us
最後の2行は長押ししたときのリピート信号です。
送信
今度は先程受信したデータを送信してみます。
回路
一番シンプルな回路はGPIO(3.3V)に赤外線LED(Vf=1.2V)2つと47Ω抵抗を直列接続するものです。近くからであれば反応しますが、向きを少し変えるだけでも反応しなくなってしまいました。赤外線LEDは結構明るくしないと反応しないようです。(見えないですが)
最終的にVBUSの5Vに4つ直列接続してトランジスタで制御することで安定して反応するようになりました。
秋月に100mA流せる高輝度タイプがあるのでこっちを使う方がいいかもしれません。
https://akizukidenshi.com/catalog/g/g112612/
ただ、PicoのGPIOは100mAも流せないので、結局トランジスタが必要になります。

サンプルコード
#include <IRremote.hpp>
#define IR_LED_PIN 0
#define BUTTON_PIN 1
#define ADDRESS 0x6D82
#define COMMAND 0xBF
void send() {
IrSender.sendNEC(ADDRESS, COMMAND, 0);
}
void setup() {
IrSender.begin(IR_LED_PIN, DISABLE_LED_FEEDBACK, 25);
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), send, FALLING);
}
void loop() {}
アドレスとコマンドは受信した結果に応じて変更してください。
IrSender.begin関数の2つ目の引数をENABLE_LED_FEEDBACKにするとボード上のLEDがフィードバックとして光ります。
もしくは、赤外線LEDをカメラ(iPhoneの場合はインカメラ)越しに見るとわかります。