Raspberry PiのGPIOを利用して赤外線リモコンの信号をスキャン・送信

IoT便利ですよね。
最近はネットワーク経由で操作できる学習リモコンも増えてることですし。
Raspberry Piを使えば簡単に自作することができるのもいいところ。
なので、Raspberry Piを使ってリモコン制御を調べると沢山ヒットします。
でも、その中にはRaspberry Piを使って学習リモコンを制御する記事がちらほら...
Raspberry Piである必要なくないか?
せっかく超絶便利なGPIOがあるのに。
まあ確かにLIRCって何かとややこしくて面倒ですね。
でも、リモコンって単純に赤外線をON/OFFしてるだけなので、簡単にC言語でプログラム書けてしまうんです。

注意

僕は電気回路についてそんなに詳しくないので配線とか結構適当です。
この記事を参考にして機器が壊れても責任取れないので自己責任でお願いします。

必要なもの

  • Raspberry Pi
    • 今回はRaspberry Pi 2を使用
  • 赤外線リモコン受信モジュール
    • 使わなくなったキットのロボットに付いていた。はんだで引っこ抜いて利用。秋月とかで普通に売ってるのでOK。
  • 赤外線LED
    • これもキットのロボットから強奪。波長が900nm前後の赤外線LEDであればなんでもよい。買ってもいいし、使わなくなったリモコンを分解してはんだで引っこ抜いて利用することもできる。
  • ブレッドボード・ジャンパワイヤ
    • 配線に利用

スキャン

配線

自分が今回使った受信モジュールのデータシートは見つけられなかった(そもそも探していない)のだが、いくつかの受信モジュールのデータシート見ていると足の配置は同じでVccも5Vだったので、これもそうだろうと思って配線した。
データシート確認できる人はちゃんと確認してそれに合わせて配線しよう。

おそらく受信モジュールは
LEDSensor.png
みたいになってるので、Raspberry PiのGPIOにおいて
GPIO.png

赤い場所にVcc(どちらでもいい)、黒い場所にGND(どこでもいい)、緑のGPIOピンにVout(どこでもいいが、番号は覚えておくこと。この記事では20番を使っている)をつなぐ

プログラム

リモコンの信号は赤外線LEDが点滅しているだけなので、点滅のパターンを記録すればそれでOKです。
なので、while文を回して、受信モジュールからの入力が以前までの状態から変化があったらその時間を記録して、最後に時間の配列から点灯・消灯していた時間を計算すればいい。

//
//  scan.c
//  Copyright © 2018 Hiroki Kawakami. All rights reserved.
//

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include<wiringPi.h>

struct timeval sharedTimeval;
double current() {
    gettimeofday(&sharedTimeval, NULL);
    return sharedTimeval.tv_sec * 1e6 + sharedTimeval.tv_usec;
}

int main(int argc, char **argv) {

    // wiring pi setup
    if (wiringPiSetupGpio() < 0) {
        fprintf(stderr, "Failed to setup wiring pi\n");
        return 1;
    }

    // obtain read pin number
    int pin = 20;
    if (argc > 1) {
        pin = atoi(argv[1]);
    }
    fprintf(stderr, "GPIO Pin Number : %d\n", pin);

    // obtain wait interval (us)
    double wait = 4e4;
    if (argc > 2) {
        wait = atoi(argv[2]);
    }

    // set gpio pin mode
    pinMode(pin, INPUT);

    double buffer[1024];
    double currentTime;
    int currentState, lastState;
    int offset = 0;

    lastState = digitalRead(pin);

    fprintf(stderr, "Scanning begin\n");

    while(1) {
        currentTime = current();
        if (offset && currentTime > buffer[offset - 1] + wait) {
            break;
        }

        currentState = digitalRead(pin);
        if (currentState != lastState) {
            buffer[offset++] = currentTime;
            lastState = currentState;
        }
    }

    fprintf(stderr, "Scanning end\n");

    fprintf(stderr, "Output begin\n");

    int i;
    for (i = 1; i < offset; i++) {
        printf("%.0lf%s", buffer[i] - buffer[i - 1], (i & 1) ? "\t" : "\n");
    }

    fprintf(stderr, "Output end\n");

    return 0;
}

プログラムの初めの方は引数を評価してWiring Piのセットアップをしているだけなので説明は省略します。
buffer配列は受信モジュールからの入力に変化があった時間を入れておく配列です。1024個あれば足りるだろうと思いますが、もし足りなかったら増やせばいいです。
while文の中に入って、
1行目は上の方に定義してある現在時刻をマイクロ秒単位で返すcurrent関数を使って時間を取得しています。
次のif文はwhile文の終了条件です。このプログラムでは参考サイトに習って一定時間入力信号に変化がないときにスキャンが終了するようにしてあり、バッファの最後の値と現在時刻を比較して実装してます。
そのあとは、digitalReadで入力状態を受け取り、それと以前までの状態を比較して違っていればその時間をバッファに記録しているだけです。
while文を抜けてからは、バッファの中身を順番に引き算していき、その結果を標準出力に出力しています。

使い方

コンパイル

$ gcc scan.c -o scan -lwiringPi

実行

受信モジュールのVoutをGPIO20番ピンに繋いでいる場合。
スキャン結果をlightOnという名前のファイルに出力する。

$ ./scan 20 > lightOn

また、終了条件の待機時間を第2引数で指定することもできる。(省略した場合は40000μs)

$ ./scan 20 1000000 > lightOn

これで終了条件が入力信号が変化せずに1秒経過になる。

コマンドを実行してから受信モジュールに向かってリモコンの信号を送信するとスキャンされてファイルに出力されます。

結果

ファイルにただ数字の羅列が出てきます。

3546    1709
447 422
446 422
449 1294
444 1296
445 424
444 1297
445 423
444 426
445 426
444 1295
445 423
447 421
446 1297
445 420
447 1297
445 424
445 1293
448 422
446 424
446 1293
446 424
447 423
447 420
447 425
447 1293
446 424
446 1293
446 1293
446 424
446 1293
446 424
446 424
445 425
446 424
446 1292
447 424
446 424
446 1293
446 424
447 424
445 

これはパナソニック製シーリングライトのリモコンの"点灯"ボタンを押した時の信号です。左側に並んでいるのが赤外線LEDが点灯している時間、右側に並んでいるのが消灯している時間です。最後は消灯してから消灯しっぱなしなので消灯している時間が記録されていません。

送信

これまでで赤外線リモコンの信号をスキャンできたので、今度はこれを赤外線LEDを用いて送信してみます。

配線

赤外線リモコンは33~40kHzのサブキャリアを利用しています。なので、この周波数でLEDを点滅させなきゃいけません。参考にしたサイトではその点滅制御をプログラムで行なっていたのですが、プログラム制御ではCPUに負荷がかかるとスケジューラの順番回ってこなくて不安定になります。
でも、Raspberry PiにはハードウェアPWMが搭載されており、せっかくなのでこれを利用します。
そのため、LEDのアノード(+極)はハードウェアPWMを使えるGPIOのピンにつなげる必要があります。

LED.png

LEDの極性は足の長さで判断できますが、足が切断されてるときは中を覗けばわかります。大きな素子につながってるほうがカソード(-極)です。

GPIO.png

LEDのカソードは黒い場所どこでもいいです。
アノードを緑の中でハードウェアPWM対応のピンにつなぎます。

Raspberry PiのハードウェアPWMをpigpioで出力する

ここを参考にすると12,13,18,19が使えるみたいです。僕は18番を使いました。

ちなみに、LEDを繋ぐときは適切な抵抗を挟みましょう。
(僕の場合、どーせパルス信号だしちょっと多く電流流れてもいいやって超適当な思考で抵抗使ってないけど、良い子はマネしないでね)

プログラム

スキャンしてできたファイルを読み取り、それに従って38kHz、Duty比1/3の赤外線信号をON/OFFします。同じ信号を繰り返し送れるようにするために一旦読み取った数値をバッファに入れてから出力を行なっています。

//
//  send.c
//  Copyright © 2018 Hiroki Kawakami. All rights reserved.
//

#include<stdio.h>
#include<stdlib.h>
#include<wiringPi.h>

int main(int argc, char **argv) {

    // wiring pi setup
    if (wiringPiSetupGpio() < 0) {
        fprintf(stderr, "Failed to setup wiring pi\n");
        return 1;
    }

    // obtain send pin number
    int pin = 18;
    if (argc > 1) {
        pin = atoi(argv[1]);
    }
    fprintf(stderr, "GPIO Pin Number: %d\n", pin);

    // obtain repeat number
    int repeat = 1;
    if (argc > 2) {
        repeat = atoi(argv[2]);
    }

    // obtain repeat delay
    int delay = 50000;
    if (argc > 3) {
        delay = atoi(argv[3]);
    }

    // set gpio pin mode
    pinMode(pin, PWM_OUTPUT);
    pwmSetMode(PWM_MODE_MS);
    pwmSetRange(3);
    pwmSetClock(168);

    int count, offset;
    unsigned int on, off;
    unsigned int buffer[1024];
    offset = 0;

    fprintf(stderr, "Reading begin\n");

    while(1) {
        count = scanf("%u%u", &on, &off);

        if (count > 0) {
            buffer[offset++] = on;
        }

        if (count < 2 || off == 0) {
            break;
        }
        buffer[offset++] = off;
    }

    fprintf(stderr, "Reading end\n");

    int i, j, state = 0;
    pwmWrite(pin, 0);

    fprintf(stderr, "Sending begin\n");

    for (i = 0; i < repeat; i++) {
        for (j = 0; j < offset; j++) {
            pwmWrite(pin, state = !state);
            delayMicroseconds(buffer[j]);
        }
        pwmWrite(pin, state = 0);
        delayMicroseconds(delay);
    }

    fprintf(stderr, "Sending end\n");

    return 0;
}

初めのほうはWiringPiのセットアップの引数の評価のみです。

GPIOのセットアップでは、指定された番号のピンをPWM出力モードにして、PWMモードをマークスペースモードにします。
pwmSetRangeには何段階でDuty比を調節するかで、Duty比の分母にあたります。今回出力したいのはDuty比が1/3と0/3の2つの信号なのでここに与える数値は3になります。
pwmSetClockにはハードウェアPWMが動く際のクロックを与えます。
Raspberry PiのハードウェアPWMのベースクロックは19200kHzなので、ここの計算式は欲しいパルス信号の周波数が38kHz、Duty比の分母が3として

$$
clock = 19200 \div 38 \div 3 \approx 168
$$

で求められます。

その後のwhile文ではデータの取得を行なっています。
このプログラムには信号のデータを標準入力経由で渡すので、データを取るのにはscanfを使います。
得られた時間を順番にバッファに格納していき、値が1つしかない最後の行になると読み込みを終了するようにしてあります。

その後のfor文で実際にLEDを点滅させています。
外側のfor文は指定された回数繰り返すためのもので、内側のfor文でbufferの中身を順番に評価しています。
pwmWriteの第1引数にはピン番号、第2引数にはDuty比の分子を与えます。ここではDuty比が1/3と0/3の信号を交互に出力させるため、第2引数には1と0を交互に与えればいいことになります。なのでここでは単純に否定の論理演算を使って1と0が交互に入るようになっています。
delayMicrosecondsはwiringPiに定義されている関数で、その名のとおり指定したマイクロ秒プログラムの実行を待機させます。
これを使って点灯・消灯している時間を調整しています。

使い方

コンパイル

$ gcc send.c -o send -lwiringPi

実行

赤外線LEDがGPIOピン18番に接続されており、
lightOnというスキャン時に生成されたファイルがワーキングディレクトリにある場合

$ sudo ./send 18 < lightOn

もしくは

$ cat lightOn | sudo ./send 18

とすることで送信できます。僕の環境だとsudoをつけ忘れるとRaspberry Piがフリーズして、電源を抜き差しして再起動しないと操作ができないようになってしまったので、必ずsudoをつけて実行するようにしてください。

ちなみに、機器によってはこれだけでは反応せず、何回か同じ信号を繰り返し送らなきゃいけない場合があります。
実際に、これではシーリングライトは点灯しませんでした。
そんなとき、第2引数に繰り返し回数、第3引数に再送するまでの待ち時間(μs)を与えると動くことがあります。

$ cat lightOn | sudo ./send 18 3 50000

これでRaspberry Piからシーリングライトを操作できるようになりました。
繰り返し回数を上手く使えば明るさなんかもある程度操作できるようになります。

LIRCを使おうと思っている方、自分でプログラムを組むと案外簡単にできるので、一度やってみてはいかがですか?

参考

赤外線リモコン信号受信・送信 ラズベリーパイ研究室
赤外線リモコンの通信フォーマット
Raspberry PiのハードウェアPWMをpigpioで出力する

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.