この記事では、赤外線リモコンの信号を、専用ライブラリを使わずに解析して、こちらから信号を送信するところまでやります。リモコンの中身は「決まった長さのパルスの並び」です。前回のDHT11で身につけたパルス幅の測り方と似たような感じです。

受信はIRremoteのようなライブラリを使えば数行で動きます。ただ、エアコンのリモコンのように信号が長い機器では、ライブラリが対応していなかったり、うまく解釈できなかったりすることがあります。そのときに頼れるのは、信号そのものを読み解く力。このドリルでは、受信=パルス幅を測ってフレームを解析する、送信=38kHzのキャリアをタイマのレジスタ設定から自作する、の両方を自分の手で書きます。

<自己紹介>
筆者は現役の車載組込みソフトエンジニアです。転職前にArduinoで「温度が上がったらエアコンを赤外線で自動起動する装置」を自作しました。そのとき一番時間がかかったのが、この赤外線信号の解析です。だからこそ、どこから手を付ければいいかを順番に示しながら進めます。

赤外線リモコンで学ぶこと

リモコンは、目に見えない赤外線のLEDを高速に点滅させてデータを送っています。テレビでもエアコンでも仕組みは同じで、点滅の「ある・なし」の時間の並びが0と1を表します。

このテーマの要点

  • リモコンは38kHzで点滅する赤外線(キャリア)を、決まった長さで出したり止めたりしてデータを送る
  • 受信モジュールがキャリアの検出までやってくれるので、マイコン側はパルス幅を測るだけで解析できる
  • 送信側は、その38kHzのキャリアをTimer2のレジスタ設定(分周と比較一致)で自作する
  • フレームを自力で読む練習は、実務の通信ドライバ(CANなど)を読む練習につながる

本記事のコードは、Arduino Uno(ATmega328P)向けにPlatformIOでコンパイル確認しました。実際に信号が読めるか・家電が反応するかは、手元のリモコンと部品で確認しながら進めてください。ボードはこれまでと同じくUno R3(ATmega328P搭載)前提です。

準備するもの

使うもの 用途
Arduino Uno R3またはR3互換ボード 本記事はUno R3(ATmega328P)を前提にする
赤外線受信モジュール(VS1838B・TL1838など) リモコンの信号を受けて復調する。スターターキットによく入っている
リモコン 解析する信号源。キット付属のリモコンでも、家のテレビ・エアコンのリモコンでもよい
赤外線LED+抵抗(100〜220Ω) 送信用。キットに入っていないことがあるので、下の配線するで確認する
ジャンパ線・USBケーブル 配線と、シリアルでの解析結果の確認に使う

受信モジュールとリモコンは、定番のスターターキットにはほぼ入っています。注意したいのは送信用の赤外線LEDで、受信モジュールはあってもこちらは入っていないキットがあります。見た目は透明の砲弾型LEDなので、キットの部品表で「IR LED」「赤外線LED」の記載を確認してください。無くても1個数十円で買えるので、まず受信の解析から始めて、送信は部品が届いてからでも大丈夫です。

配線する

受信モジュールは3本つなぐだけです。VCCを5V、OUTをD2、GNDをGNDへつなぎます。DHT11と同じく、端子の並びは製品によって違うので、位置ではなく基板の印字を確認してください。

Arduino Uno R3と赤外線受信モジュールの配線図。5VをVCC、D2をOUT、GNDをGNDへ接続する
図:赤外線受信モジュールの配線。VCC・OUT・GNDの印字を確認してつなぐ

送信側の赤外線LEDは、D11から抵抗を通してつなぎます。あとで見るとおり、38kHzを出すTimer2の出力ピン(OC2A)がD11に固定されているためで、他のピンには変更できません。LEDには極性があり、足の長いほう(アノード)を抵抗側につなぎます。

Arduino Uno R3と赤外線LEDの配線図。D11から電流制限抵抗を通して赤外線LEDをつなぎGNDへ落とす
図:送信側の配線。D11(PB3/OC2A)→抵抗→赤外線LED→GND
赤外線LEDの光は見えない赤外線LEDは通電しても肉眼では光って見えません。動作確認はスマホのカメラ越しに見ると、紫っぽく光るのが分かります(カメラによっては赤外線カットフィルタで見えないこともあります)。

まずライブラリで受信してみる

最初に、定番のIRremoteライブラリを使った受信を見ておきます。数行でリモコンのコードが取れます。

ir_library.ino

#include <IRremote.hpp>

void setup() {
    Serial.begin(115200);
    IrReceiver.begin(2);     // 受信モジュールのOUTをD2へ
}

void loop() {
    if (IrReceiver.decode()) {
        IrReceiver.printIRResultShort(&Serial);   // プロトコル名とコードを表示
        IrReceiver.resume();
    }
}

これで多くのリモコンは読めます。ただし、プロトコル名とコードが表示されるだけで、信号がどんな形をしているかは見えません。エアコンのような長い信号だと、ライブラリのバッファに収まらず取りこぼすこともあります。ここからは、この中身を自分で書いてみます。

次にライブラリなしで受信する

自力で受信するには、まず「受信モジュールが何をどこまでやってくれるか」を知る必要があります。

リモコンの赤外線は、太陽光や照明にも含まれる赤外線と区別するために、38kHzで高速に点滅させたキャリア(搬送波)として送られます。受信モジュールは、この38kHz成分だけを取り出して復調してくれます。出てくるのはキャリアがある間はLow、ない間はHighというきれいなデジタル信号です(TL1838/VS1838データシート:動作電圧2.7〜5.5V・中心周波数38kHz)。

つまりマイコン側の仕事は、DHT11のときと同じです。LowとHighが続いた時間をmicros()で測り、その並びを解釈する。これだけです。

NECフォーマットを読み解く

信号の並び方(フォーマット)にはいくつか種類があり、代表格がNECフォーマットです。多くの解説や機器で使われている基準的な形式なので、まずこれを読み解きます。仕様の数値はSB-ProjectsのNECプロトコル解説赤外線リモコンの通信フォーマット(ELM)で確認できます。

NECフォーマットのフレーム構造図。リーダ9ms+4.5ms、アドレス8bit、反転アドレス、コマンド8bit、反転コマンド、ストップビットの並びと、ビット0とビット1のパルス幅の違いを示す
図:NECフォーマットのフレーム構造。0と1はスペースの長さだけで区別される
部分 キャリアあり(mark) キャリアなし(space)
リーダ(開始合図) 9ms 4.5ms
ビット0 562.5µs 562.5µs
ビット1 562.5µs 1687.5µs
ストップ 562.5µs

データ部は32ビット固定で、アドレス8bit → アドレスの反転8bit → コマンド8bit → コマンドの反転8bitの順に、各バイトの最下位ビット(LSB)から送られます。反転バイトは受信側の誤り確認用で、元のバイトと反転バイトを足すと必ず0xFFになります。DHT11のチェックサムと同じ発想です。

ではコードにします。信号の始まりを待って、LowとHighが続いた時間を配列へ記録していきます。

ir_record.ino

#define IR_RX_PIN 2
#define MAX_EDGES 200
#define FRAME_END_US 15000UL

uint16_t durations[MAX_EDGES];   // 偶数番=mark、奇数番=spaceの長さ(µs)
uint16_t edge_count = 0;

void ir_record(void) {
    while (digitalRead(IR_RX_PIN) == HIGH) {
        // 受信モジュールはアイドルHigh。最初のLow(受信開始)を待つ
    }

    uint8_t level = LOW;
    edge_count = 0;
    while (edge_count < MAX_EDGES) {
        unsigned long t0 = micros();
        while (digitalRead(IR_RX_PIN) == level) {
            if (micros() - t0 > FRAME_END_US) {
                return;              // 15ms変化なし=フレーム終了
            }
        }
        durations[edge_count] = (uint16_t)(micros() - t0);
        edge_count++;
        level = (level == LOW) ? HIGH : LOW;
    }
}

記録したdurationsをシリアルへそのまま表示すれば、リモコンの生の波形が数値で見えます。ボタンを押して「9000前後、4500前後、そのあと560前後と560/1690前後の繰り返し」が並べば、それはNECフォーマットです。

並びが確認できたら、デコードまで書けます。リーダを確認し、スペースの長さで0と1を判定して32ビットへ積みます。

このコードで見るのは、さきほどの波形のうち次の3つの数字だけです。上へ戻らずに済むよう、ここへ並べておきます。

リーダmark 9000µsspace 4500µs
ビット0mark 560µsspace 560µs
ビット1mark 560µsspace 1690µs
mark(キャリアあり)=どのビットでも560µsで同じspace(キャリアなし)=ここだけが0と1を分ける

markは0でも1でも560µsなので、判定に使うのはspaceの長さだけです。下のコードでnear_us(space, 1690)だけが分岐しているのはこのためです。

ir_decode_nec.ino

// 幅が目安に近いか(±25%)を判定する
bool near_us(uint16_t value, uint16_t target) {
    uint16_t low = target - target / 4;
    uint16_t high = target + target / 4;
    if (value >= low && value <= high) {
        return true;
    } else {
        return false;
    }
}

bool ir_decode_nec(unsigned long *result) {
    if (edge_count < 2 + 64) {
        return false;                            // リーダ+32bit分のエッジがない
    }
    if (!near_us(durations[0], 9000) || !near_us(durations[1], 4500)) {
        return false;                            // リーダが9ms+4.5msでない
    }

    unsigned long value = 0;
    for (uint8_t i = 0; i < 32; i++) {
        uint16_t mark = durations[2 + i * 2];
        uint16_t space = durations[3 + i * 2];
        if (!near_us(mark, 560)) {
            return false;
        }
        if (near_us(space, 1690)) {
            value |= (1UL << i);                 // LSBファーストで積む
        } else if (!near_us(space, 560)) {
            return false;
        }
    }
    *result = value;
    return true;
}

±25%の幅を持たせているのは、実際のパルス幅が理論値どおりには来ないためです。リモコン側の精度、受信モジュールの応答遅れ、micros()の分解能(4µs単位)が全部少しずつずれを作ります。仕様値ぴったりの一致判定にすると、まともに動きません。

エアコンのリモコンはもっと長い

家のエアコンのリモコンだと上述とは異なります。日本の家電の多くはNECではなく家製協(AEHA)フォーマット系だからです。

ただ、フォーマットが違っても構造は同じです。どちらも「単位時間T」という基準の長さを決め、その何倍かでリーダとビットを表します。違うのはTの値と倍数だけです。

項目 NEC 家製協(AEHA)
単位時間 T 562µs 350〜500µs(代表425µs)
リーダ(mark+space) 16T+8T=9.0ms+4.5ms 8T+4T=約3.4ms+1.7ms
ビット0 1T+1T 1T+1T
ビット1 1T+3T 1T+3T
データ長 32ビット固定 可変(48ビットが代表。エアコンは数百ビット)
繰り返し 押しっぱなしで約108ms周期 規定なし(通常130ms前後)

出典:赤外線リモコンの通信フォーマット(ELM)(T・データ長・繰り返し)、SB-ProjectsのNECプロトコル解説(NECのリーダ9ms+4.5ms・キャリアバースト560µs)。

ここで実用上いちばん大事なのは、リーダの長さです。ビット0と1の作り方はどちらも同じ(1T+1T/1T+3T)なので見分けられませんが、リーダのmarkが9ms前後ならNEC、3.4ms前後ならAEHAと判断できます。さきほどのir_record()で記録したdurations[0]を見れば、手元のリモコンがどちらかはその場で分かります。

そのうえで、エアコンが厄介なのはデータ長が可変なことです。温度・モード・風量など設定を丸ごと毎回送るため、フレームが数百ビットになることがあります。

エアコンの信号解析に時間がかかった転職前に作ったエアコン自動制御でも、ここが一番の難所でした。通信パラメータがどういうものか、それをまず解析しないといけないので、その検証に時間がすごいかかりましたね。ボタン1回で何が送られているのかを、記録した数値の羅列とにらめっこしながら切り分けていく作業です。

ただし、解析の考え方は変わりません。リーダを見つけて、パルス幅で0と1を区別する。それだけです。実用上は、デコードして意味を理解しなくても、記録したmark/spaceの長さの列をそのまま覚えて、そのまま再生する方法が確実です。この「生データの再生」は次の送信でやります。バッファ(MAX_EDGES)は、エアコンの長いフレームに合わせて増やしてください。Unoでは1エッジ2バイトなので、たとえば400エッジで800バイトです。

次にライブラリなしで送る

送信は受信の逆です。38kHzのキャリアを「出す・止める」を、決まった長さで繰り返すことでフレームを作ります。問題は、その38kHzをどう作るかです。

ここで、いま作ろうとしているものを図で確認しておきます。2つの時間軸が重なっているのが、この節で混乱しやすい点です。

フレーム側mark 560µsspace 560µsmark 560µs
キャリア側38kHzで反転(周期26.3µs)止める(Lowのまま)38kHzで反転
markの区間=キャリアを出すその中で26.3µs周期の反転が約21回入るspaceの区間=キャリアを止める

560µsのmark1つの中に、26.3µsの波が約21回入っています(560 ÷ 26.3 ≒ 21)。つまりタイマが作るのはキャリア側だけで、560µsや1690µsというフレーム側の長さは、キャリアを出す・止める時間としてソフトが数えます。この分担が、次のコードのir_mark()ir_space()にそのまま出てきます。

digitalWriteとdelayMicrosecondsの手作業でも作れなくはないですが、38kHzは周期26.3µs、半周期13µs強で、ソフトの手動制御では精度が出ません。ここでタイマのドリルで学んだ考え方の出番です。タイマの比較一致でピンを自動で反転(トグル)させれば、CPUの手を借りずに正確な38kHzが出続けます。Timer1は周期処理で使っているので、今回は8ビットのTimer2を使います。

データシートからTimer2の設定を読み解く

① どのピンに出るかを確認する

Timer2の比較一致出力はOC2Aという機能ピンで、ATmega328Pの端子ではPB3です。データシートのポートB代替機能表に、こう書かれています。

PB3: MOSI (SPI bus master output/slave input), OC2A (Timer/Counter2 output compare match A output), PCINT3 (pin change interrupt 3)
(訳:PB3端子は、SPIのMOSI、Timer/Counter2の比較一致A出力(OC2A)、ピン変化割り込み3を兼ねる。)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR–01/15)Table 13-3 Port B Pins Alternate Functions, p.65 より原文引用(© Microchip Technology)

Arduinoのピン ATmega328Pの端子 Timer2の機能
D11 PB3(Port Bの3番ビット) OC2A=比較一致A出力

D11とPB3の対応は、Lチカのドリルでも使ったArduino UNO R3データシート(A000066)のピン配置図で確認できます。ハードウェアがこの組み合わせで配線されているため、キャリア出力はD11から変更できません

② 制御レジスタTCCR2Aのビット構成を見る

8ビットそれぞれに役割があり、今回触るのは色の付いた2つだけです。

TCCR2ACOM2A1COM2A0COM2B1COM2B0WGM21WGM20
COM2A0=比較一致でOC2A(D11)を反転させるWGM21=CTCモードにする

bit7がいちばん上の位です。「−」は使われていないビットで、触りません。

COM2Aビットの役割は、データシートにこう書かれています。

These bits control the output compare pin (OC2A) behavior. If one or both of the COM2A1:0 bits are set, the OC2A output overrides the normal port functionality of the I/O pin it is connected to. However, note that the data direction register (DDR) bit corresponding to the OC2A pin must be set in order to enable the output driver.
(訳:これらのビットはOC2Aピンの動作を制御する。COM2A1:0のどちらかを立てると、OC2A出力がそのI/Oピンの通常のポート機能を上書きする。ただし、出力ドライバを有効にするには、OC2Aピンに対応するデータ方向レジスタ(DDR)のビットをセットしておく必要がある。)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR–01/15)17.11.1 TCCR2A – Bits 7:6 – COM2A1:0, p.127 より原文引用(© Microchip Technology)

③ 使うビットの設定値を決める

非PWMモードでCOM2A1:0=01にすると「Toggle OC2A on compare match」=比較一致のたびにOC2Aが反転します(Table 17-2, p.128)。動作モードはWGM21=1でCTCモード(比較一致でカウンタが0へ戻る。Table 17-8, p.130)。分周はTCCR2BのCS22:0=001で分周なしです(Table 17-9, p.131)。

レジスタ・ビット 設定値 役割
TCCR2AのWGM21 1 CTCモード(OCR2Aの値まで数えたら0へ戻る)
TCCR2AのCOM2A0 1(キャリアON時のみ) 比較一致のたびにOC2A(D11)を反転
TCCR2BのCS20 1 分周なし(16MHzのまま数える)
OCR2A 209 比較一致までのカウント数=周波数を決める

折り返しの値を入れるOCR2Aの役割も、原文で確認しておきます。

The output compare register A contains an 8-bit value that is continuously compared with the counter value (TCNT2). A match can be used to generate an output compare interrupt, or to generate a waveform output on the OC2A pin.
(訳:出力比較レジスタAは8ビットの値を保持し、カウンタ値(TCNT2)と常に比較される。一致は出力比較割り込みの発生にも、OC2Aピンへの波形出力にも使える。)

出典:ATmega328P Datasheet(Microchip, 7810D–AVR–01/15)17.11.4 OCR2A – Output Compare Register A, p.131 より原文引用(© Microchip Technology)

OCR2A=209の根拠は計算で出ます。比較一致1回でピンが1回反転するので、出力1周期には反転2回が必要です。タイマのドリルでやった分周の計算と同じ形になります。

出力周波数 = 16MHz ÷ (2 × 分周比 × (OCR2A + 1)) = 16,000,000 ÷ (2 × 1 × 210) ≒ 38,095Hz

となり、38kHzにほぼ一致します。受信モジュールの中心周波数には多少の幅があるので、この誤差0.25%は問題になりません。

コードにします。キャリアのON/OFFは、COM2A0ビットを立てる・落とすで切り替えるのがコツです。タイマは回しっぱなしにして、ピンへの接続だけを切り替えます。

ir_send.ino

void ir_tx_init(void) {
    DDRB |= (1 << DDB3);                 // D11(PB3)を出力に
    TCCR2A = (1 << WGM21);               // CTCモード。OC2Aはまだ切断
    TCCR2B = (1 << CS20);                // 分周なし
    OCR2A = 209;                         // 16MHz/(2*1*(209+1)) = 38.095kHz
}

void carrier_on(void) {
    TCCR2A |= (1 << COM2A0);             // 比較一致でOC2Aをトグル=38kHz出力
}

void carrier_off(void) {
    TCCR2A &= (uint8_t)~(1 << COM2A0);   // OC2Aを切断して通常ポートに戻す
    PORTB &= (uint8_t)~(1 << PORTB3);    // 出力をLowに固定(LED消灯)
}

void ir_mark(uint16_t us) {
    carrier_on();
    delayMicroseconds(us);
}

void ir_space(uint16_t us) {
    carrier_off();
    delayMicroseconds(us);
}

// NECフォーマットで1フレーム送る
void ir_send_nec(uint8_t addr, uint8_t cmd) {
    uint8_t bytes[4];
    bytes[0] = addr;
    bytes[1] = (uint8_t)~addr;
    bytes[2] = cmd;
    bytes[3] = (uint8_t)~cmd;

    ir_mark(9000);                       // リーダ
    ir_space(4500);
    for (uint8_t i = 0; i < 4; i++) {
        for (uint8_t b = 0; b < 8; b++) {
            ir_mark(560);
            if ((bytes[i] >> b) & 1) {   // LSBファースト
                ir_space(1690);
            } else {
                ir_space(560);
            }
        }
    }
    ir_mark(560);                        // ストップビット
    carrier_off();
}

// 記録した生のパルス幅列をそのまま再生する(エアコン等の長いフレーム用)
void ir_send_raw(const uint16_t *buf, uint16_t len) {
    for (uint16_t i = 0; i < len; i++) {
        if (i % 2 == 0) {
            ir_mark(buf[i]);
        } else {
            ir_space(buf[i]);
        }
    }
    carrier_off();
}

最後のir_send_raw()が、エアコン対策の本命です。受信で記録したdurationsをそのまま渡せば、フォーマットの意味を完全に理解していなくても、同じ信号を再現できます。市販の「学習リモコン」と同じ原理ですね。エアコンの完全な制御(温度を自由に変えるなど)はフレームの中身の解読が必要ですが、「記録したボタンと同じ操作をさせる」ならこれで足ります。

ライブラリ版との対応IRremoteのIrSender.sendNEC()がやっているのは、まさにこのタイマ設定とmark/spaceの切り替えです。ライブラリではTimer2のPWMモードでキャリアを作る実装ですが、「タイマでキャリアを作り、接続を切り替えて変調する」という骨格は同じです。

練習問題(基礎5+発展2)

基礎5問は、信号の仕組みと受信モジュールの動きを確認します。発展2問は、キャリアの計算とraw再生を扱います。まず自分で考えてから回答を開いてください。

はじめの1問は、前回のDHT11からのおさらいです。間隔をあけて思い出すほど後に残るので、覚えている自信があっても一度書いてみてください。

おさらい Q0

前回のDHT11のドリルから1問です。DHT11では、各ビットの0と1を何の長さで見分けましたか。また、そのしきい値は何µsでしたか。

解答・解説を見る
解答:先頭のLowに続くHighの長さ。約40µsより長ければ1、短ければ0。
解説:この回の赤外線も、やっていることは同じです。DHT11は「Highの長さ」、NECは「spaceの長さ」で0と1が決まります。相手が変わってもパルス幅を測って判定するという手順は変わりません。違うのは基準の数字だけです。忘れていたらArduino DHT11のドリルへ戻ってください。

基礎 Q1

リモコンの赤外線は、なぜ38kHzで点滅させた光(キャリア)として送りますか。

解答・解説を見る

解答:太陽光や照明に含まれる赤外線と区別するため。

解説:受信モジュールは38kHz付近で点滅する光だけを通すフィルタを内蔵しています。ただ光るだけの赤外線はノイズとして無視されるので、離れた場所からでも安定して通信できます。

基礎 Q2

受信モジュール(VS1838B系)のOUTは、リモコンの信号を受けている間、HighとLowのどちらになりますか。

解答・解説を見る

解答:Low(何も受けていないときはHigh)。

解説:キャリアを検出している間だけOUTがLowへ落ちます。記録コードで最初に「Lowになるのを待つ」のはこのためです。リモコンの点滅そのものではなく、復調済みの信号が出てくる点も確認しておいてください。

基礎 Q3

NECフォーマットの1ビット分を判定する処理を書いてください。near_us()は使ってよいものとし、引数のmarkspace(それぞれµs)から、そのビットが1ならtrue、0ならfalseを返す関数nec_bit()を作ります。仕様に合わない幅だったときはfalseで構いません。

解答・解説を見る
q3_nec_bit.ino

bool nec_bit(uint16_t mark, uint16_t space) {
    if (!near_us(mark, 560)) {
        return false;        // markが仕様外=正しいビットではない
    }
    if (near_us(space, 1690)) {
        return true;         // 長いspace=1
    }
    return false;            // 560µs前後のspace=0
}

解説:判定に使うのはspaceの長さだけです。markは0でも1でも562.5µsで同じなので、mark側は「仕様どおりか」の確認にしか使いません。0は562.5µs、1はその3倍の1687.5µsです。
DHT11が「Highの長さ」で0と1を分けていたのと同じ発想で、パルス幅を測れば判定できます。本文のir_decode_nec()は、この判定を32回繰り返してvalueへ積んでいるだけです。

基礎 Q4

NECフレームの「反転アドレス」「反転コマンド」は何のために送られますか。

解答・解説を見る

解答:受信側の誤り確認のため。元のバイトと反転バイトを足して0xFFにならなければ、受信に失敗している。

解説:赤外線は外乱を受けやすいので、データの半分を検証用に使う設計になっています。DHT11のチェックサム、実務の通信のCRCと同じ「受信データを疑う」仕組みです。

基礎 Q5

デコードのパルス幅判定で、仕様値ぴったりではなく±25%の幅を持たせているのはなぜですか。

解答・解説を見る

解答:実際のパルス幅は、リモコンの精度・受信モジュールの応答遅れ・micros()の分解能でずれるから。

解説:通信の受信処理では、仕様値からの許容範囲を決めて判定するのが基本です。厳しすぎると正常な信号を捨て、緩すぎると別のフォーマットを誤認します。

発展 Q6

SONY系のリモコンはキャリアが40kHzです。分周なしのまま、OCR2Aをいくつにすれば40kHzになりますか。

解答・解説を見る

解答:OCR2A = 199。

// 出力周波数 = 16MHz / (2 * 分周比 * (OCR2A + 1))
// 40000 = 16000000 / (2 * 1 * (OCR2A + 1))
// OCR2A + 1 = 16000000 / 80000 = 200
OCR2A = 199;

解説:式を変形してOCR2A+1について解くだけです。タイマのドリルでやった「目標の時間から逆算して設定値を決める」流れが、周波数でも同じように使えます。

発展 Q7

ir_send_raw()では、配列の偶数番目をmark、奇数番目をspaceとして再生しています。この前提はどこから来ていますか。

解答・解説を見る

解答:受信側のir_record()が、必ずmark(最初のLow)から記録を始めるから。

解説:記録はLowの検出から始まるので、durations[0]は必ずmarkの長さです。以降は交互に並ぶため、偶数=mark、奇数=spaceが保証されます。送信と受信でデータの並びの約束(フォーマット)を共有するのは、あらゆる通信の基本です。添字の偶数・奇数で意味を分ける書き方は、C言語 配列のドリルの2次元配列と同じ考え方です。

例:実務で実際に書くコード

「フレームの生データを記録して、リーダを見つけて、決められた幅で0/1を切り出す」。この作業は、私の実務でもほぼ同じ形で出てきます。車の中では、ECU同士がCAN通信というプロトコルで常にデータをやり取りしていて、その1フレームはIDとデータ8バイトの決まった構造をしています。

can_receive.c

/* 受信フレームのIDを見て、担当するデータだけ取り出す */
if (frame.id == CAN_ID_ENGINE_TEMP) {
    /* 仕様書どおりのバイト位置・ビット位置から値を切り出す */
    temp = (int16_t)((frame.data[0] << 8) | frame.data[1]);
    if (check_crc(&frame) != CRC_OK) {
        return ERR_CRC;          /* 壊れたフレームは捨てる */
    }
}

フレームのどこに何のビットがあるかは通信仕様書に定義されていて、その仕様書と受信データを突き合わせながら解析します。リモコンの解析でやった「数値の羅列を仕様と見比べて意味を見つける」経験は、この仕事の入り口の練習になります。私が転職時の面接で製作物を語れたのも、この解析を自力でやり切った経験があったからです。

最後につまずきポイントをおさらい

送信しても家電が反応しないまず疑うのは距離と向きです。ピンからの直接駆動は電流が最大でも20mA程度に制限され、リモコン製品より光が弱いので、まず数十cmの近距離で、LEDを受光部へまっすぐ向けて試してください。それでも動かなければ、スマホカメラでLEDの発光を確認し、記録データの再生範囲(エッジ数)が足りているかを見直します。

受信側でつまずくのは、リモコンによってフォーマットが違うことです。NECのつもりでデコードして失敗するときは、まず生データの表示に戻って、リーダの長さを見てください。9ms前後ならNEC系、3.4ms前後(8T)なら家製協系の可能性が高いです。あとは、ボタンを押し続けたときに送られるリピートフレーム(NECでは9ms+2.25ms+ストップだけの短い信号)をデータ本体と混同しないことも大事です。

もう一つ、Timer2を使う本記事の送信コードは、D3・D11のanalogWrite(Timer2のPWM)と同時には使えません。タイマは有限の共有資源で、どの機能がどのタイマを使うかを把握して割り当てる。これも実務のマイコン開発で毎回やる調整です。

よくある質問(FAQ)

ライブラリを使ってはいけないのですか?

実際の工作ではIRremoteを使ってかまいません。このドリルの目的は、ライブラリが中で何をしているかを理解することです。中身が分かっていれば、ライブラリが対応していないリモコンや、うまく動かないときにも生データへ立ち返って対処できます。

エアコンが操作できません

エアコンのフレームは長いので、記録バッファ(MAX_EDGES)に収まりきっていない可能性をまず疑ってください。記録したエッジ数を表示して、フレームの途中で切れていないかを確認します。また、機種によっては1回の操作で複数フレームを間隔を空けて送るものがあり、その場合はフレーム間の待ち時間も含めて再現する必要があります。

何も受信されません

配線(VCC・OUT・GNDの印字とピンの対応)と、リモコンの電池を確認してください。受信モジュールのOUTをそのままdigitalReadして、リモコンを近づけてボタンを押したときに値が変わるかを見るのが最小の切り分けです。太陽光が受光部へ直接当たる場所では誤動作することもあります。

1週間後に、もう一度ここへ戻る

この回の仕上げ方1週間後にこの記事をもう一度開き、解説を読まずに練習問題だけを解き直してください。特にQ3のnec_bit()と、Q6のOCR2Aの計算です。そのとき書けた分が、身についた分です。

間隔をあけて思い出す形にすると、続けて2回解くより後に残ります。「読み返す」ではなく「解き直す」ことだけ守ってください。読み返すと分かった気になりますが、書けるかどうかは書いてみるまで分かりません。

次に読む

これで、温湿度を読む入力(DHT11)と、エアコンを動かす出力(赤外線)の両方の部品がそろいました。次は、電源が切れても設定や状態を失わないための保存の仕組みへ進みます。総合課題の「自動温湿度調整システム」に、また一歩近づきます。