LPC810メモ:赤外線リモコン受信クラス

テレビやエアコンなどを操作する赤外線リモコンの信号を読み取るクラスを作成。
NECフォーマットと家製協フォーマット(AEHA)に対応したつもり。SONYフォーマットは非対応。


サンプルでは、読み取ったデータを、前回定義したモールス信号出力クラスを用いて16進数出力する。

前提とする環境はこちら→LPC810メモ 共通の準備
基本的なサンプルについてはこちら→LPC810みっかぼ自作サンプル集

利用した赤外線リモコン受信モジュールはRL-IRM2161、しかし秋月を見たらもう無かった。見た目は違うが、OSRB28C9AAとほぼ同じと思う。要するに3Vで動いて、受信時にLOW出力のもの。

赤外線リモコンの通信フォーマットはこちらで勉強した。
実装はmbedの赤外線送受信ライブラリを参考にしたが作りはシンプルにしてしまって、仕様もだいぶ小さい。

具体的には以下のとおり。

  • NECフォーマットと家製協フォーマット(AEHA)のみ対応。
    • SONYフォーマットは無視。一つだけ考え方違うみたいだし、我が家にも一台もないから試せないし。
  • NECフォーマットと家製協フォーマットのどちらを受信したか」を判別する機能なし。
    • 後述するとおり、実装的に区別してない。
  • カスタマーコード、パリティ、データの区別無し。リーダーの後のビットは全部8bit区切りのデータと見なす。
  • フレームとリピートの区別無し。利用する側で「受信サイズが0バイトならリピートとみなす」とすればいいはず(実験してないが)。

実装は思いつく範囲で手抜きした。

  • 信号の立ち上がりしか見ない。
    • 正確には、使ってるセンサーが「信号がアクティブだとlowになる」らしいので、プログラム的にはGPIOのfallingエッジを見る。
  • エッジ間の時間は、NEC、家製協の共通集合を取れるように、アバウトに0.1ms単位で計る。
    • 4ms以上ならリーダーとみなす。
    • データ受信中は1.3ms未満ならbit値0、それ以上ならbit値1とする。
    • データ受信中に2.5ms以上経過したらタイムアウトとして、その時点で読めていたビット数が8の倍数なら成功とみなす。

以下をtemplate引数としてカスタマイズ可能。

  • GPIOの番号(0 .. 5)
  • MRTの番号(0 .. 3)
  • バッファのサイズ

メンバ関数は以下。具体的な使い方はサンプル参照。

static void init() GPIO出力とMRTの割り込みを開始させる。最初に必ず呼ぶ。
static void handleMrt() MRTの割り込みハンドラ。void MRT_IRQHandler(void) から必ず呼ぶ。
static void handlePinInt() GPIOの割り込みハンドラ。void PININT?_IRQHandler(void) から必ず呼ぶ。?はpin interruptの番号。
static bool received() 受信完了してデータ取り出しOKの状態であればtrueになる
static void clear() 受信待ち状態に戻す
static uint8_t getSize() 受信したデータのbyte数を取得
static volatile uint8_t *getData() 受信したデータ列を取得

サンプルコード

MorseBase.hは前回の例から持ってくる前提

sample.cpp(クラスを使う側)

#include "LPC8xx.h"

#include "MorseBase.h"
#include "IrReceiverBase.h"

// モールス信号出力クラス
using Morse = MorseBase<0,3,800,80,32>;
// テンプレート引数の意味
// GPIO 0番(8番pin)を利用
// MRT 3番を利用
// 音は800Hz
// 短音1つは80ミリ秒
// 文字列のバッファは32byte

// 赤外線リモコン受信クラス
using IrReceiver = IrReceiverBase<4,2,7,32>;
// テンプレート引数の意味
// GPIO 4番(2番pin)を利用
// MRT 2番を利用
// pin interrupt 7番を利用
// 受信用のバッファは32byte

extern "C" {
    void MRT_IRQHandler(void) {
    	Morse::handleEvent();    // MRT割り込み用のハンドラからこれを必ず呼ぶ(モールス信号用)
    	IrReceiver::handleMrt(); // MRT割り込み用のハンドラからこれを必ず呼ぶ(赤外線リモコン受信用)
    }
}

extern "C" {
    // pin interrupt 7の割り込みハンドラ。
    // cr_startup_lpc8xx.cpp で定義されている名称。
    void PININT7_IRQHandler(void)
    {
    	IrReceiver::handlePinInt();
    		// pin interruptの割り込みハンドラからこれを必ず呼ぶ
    		// テンプレート引数で指定した番号(本例では7番)の必要がある
    }
}

int main(void) {
	Morse::init();
	IrReceiver::init(); // 赤外線リモコン受信クラス初期化、必ず1回呼ぶ

	for(;;) {
		// なんらか受信するまで待つ
		if(IrReceiver::received()) { // 受信した
			uint8_t size = IrReceiver::getSize(); // 受信したバイト数を取得
			for(uint8_t i{0}; i < size; i++) { // 各バイトを16進数表記でモールス信号出力
				Morse::outHex(IrReceiver::getData()[i], 2);
			}
			Morse::wait(); // モールス出力が終わるまで待つ
			IrReceiver::clear(); // 受信状態をクリア、受信待ちにする
		}
	}
	return 0;
}

IrReceiverBase.h(赤外線リモコン受信クラス、今後使いまわす予定)

#ifndef IRRECEIVERBASE_H_
#define IRRECEIVERBASE_H_

// 受信状態管理用のenum
enum class IrReceiverStatus : uint8_t {
	Ready, // 受信待ち
	Leader, // leader部分受信中
	Data, // Data部分受信中
	Received // 受信完了
};

// テンプレート引数
// Port -> GPIOの番号、Channel -> MRTの番号、
// PinInt -> pin interruptの番号
// BUfferSize -> 一度に読み込める最大byte数
// 例) using IrReceiver = IrReceiverBase<4,2,7,32>;
template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
class IrReceiverBase {
public:
	static void init(); // 最初に必ず呼ぶ
	static void handleMrt();  // void MRT_IRQHandler(void) から必ず呼ぶ
	static void handlePinInt();  // void PININT★_IRQHandler(void) から必ず呼ぶ、★はpin interruptの番号
	static bool received(); // 受信完了の状態であればtrue
	static void clear(); // 受信待ち状態に戻す
	static uint8_t getSize(); // 受信したデータのbyte数を取得
	static volatile uint8_t *getData(); // 受信したデータを取得

private:
	volatile static IrReceiverStatus status; // 受信状態
	volatile static uint32_t timerCount; // 時間計測用のカウント変数
	volatile static uint8_t bufferPosition; // 現在読み込んでいるデータがbufferの何バイト目か
	volatile static uint8_t bitPosition; // 現在読み込んでいるbitが、バイトの何bit目か
	volatile static uint8_t buffer[BufferSize]; // データ読み込み用のバッファ
};

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
volatile uint8_t IrReceiverBase<Port,Channel,PinInt,BufferSize>::bufferPosition;

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
volatile uint8_t IrReceiverBase<Port,Channel,PinInt,BufferSize>::bitPosition;

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
volatile IrReceiverStatus IrReceiverBase<Port,Channel,PinInt,BufferSize>::status {IrReceiverStatus::Ready};

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
volatile uint32_t IrReceiverBase<Port,Channel,PinInt,BufferSize>::timerCount {0};

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
volatile uint8_t IrReceiverBase<Port,Channel,PinInt,BufferSize>::buffer[BufferSize];

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
bool IrReceiverBase<Port,Channel,PinInt,BufferSize>::received() {
	return status == IrReceiverStatus::Received;
}

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
void IrReceiverBase<Port,Channel,PinInt,BufferSize>::clear() {
	status = IrReceiverStatus::Ready;
	timerCount = 0;
}

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
volatile uint8_t *IrReceiverBase<Port,Channel,PinInt,BufferSize>::getData() {
	return buffer;
}

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
uint8_t IrReceiverBase<Port,Channel,PinInt,BufferSize>::getSize() {
	return bufferPosition + 1;
}

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
void IrReceiverBase<Port,Channel,PinInt,BufferSize>::handleMrt() {
	// 0.1ms毎に呼び出される割り込みハンドラ
    if (LPC_MRT->Channel[Channel].STAT & 0x1) {
        // 同じbitに1を書き込むことで割り込みを処理したことを伝える
        LPC_MRT->Channel[Channel].STAT = 0x1;
        timerCount++;

        if(timerCount > 140 && status == IrReceiverStatus::Leader) {
        	// leader受信中、14.0ms何もなかったら、タイムアウト扱い
        	clear();

        } else if(timerCount > 25 && status == IrReceiverStatus::Data) {
        	// data受信中、bit1より確実に長い時間(2.5ms)経過したら、読み込みそこまで
            if(bitPosition == 0) {
            	// bit位置が切りよく0なら1byte単位で読めたとみなし受信成功
            	bufferPosition--;
               	status = IrReceiverStatus::Received;
            } else {
            	// そうでなければ単なるタイムアウト
               	status = IrReceiverStatus::Ready;
            }
        }
    }
}

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
void IrReceiverBase<Port,Channel,PinInt,BufferSize>::handlePinInt() {
	// 赤外線入力が立ち上がる(受信器の出力がlowになる)エッジで呼び出されるハンドラ
    LPC_PIN_INT->IST = (1<<PinInt);
    switch(status) {
    case IrReceiverStatus::Ready: // 受信待ち
    	status = IrReceiverStatus::Leader; // Leaderに移行
    	break;

    case IrReceiverStatus::Leader: // leader受信中
    	if(timerCount >= 40) { // 4ms以上経過していたら、data受信中に移行
    		status = IrReceiverStatus::Data;
    		bitPosition = 0;
    		bufferPosition = 0;
    	} else { // そうでなければエラー、受信待ちに戻る
    		status = IrReceiverStatus::Ready;
    	}
    	break;

    case IrReceiverStatus::Data: // data受信中
    	if(timerCount < 13) { // 1.3ms未満 -> 短音とみなしてbitに0を設定
    		buffer[bufferPosition] &= ~(1 << bitPosition);
    	} else { // それ以上  -> 長音とみなしてbitに1を設定
    		buffer[bufferPosition] |= 1 << bitPosition;
    	}
    	if(bitPosition < 7) {
    		bitPosition++; // 7bit目まではincrement
    	} else {
    		bitPosition = 0; // 7bit目に達したら0に戻して、byteを1つ進める
    		if(bufferPosition < BufferSize) {
    			bufferPosition++;
    		} else {
    			// バッファの最後まで読み切った -> 成功扱いとする
        		status = IrReceiverStatus::Received;
    		}
    	}
    	break;

    default: // IrReceiverStatus::Receivedの場合。信号を無視。
    	break;
    }
	timerCount = 0;
}

template<uint8_t Port, uint8_t Channel, uint8_t PinInt, uint8_t BufferSize>
void IrReceiverBase<Port,Channel,PinInt,BufferSize>::init() {
    // GPIOに対してclockを有効化
    LPC_SYSCON->SYSAHBCLKCTRL |= (1<<6);

	// UM10601 8.3 Basic configuration
	// に従った手順
	LPC_SYSCON->PINTSEL[PinInt] = Port;

	// GPIOに対してclockを有効化(pin interruptと共通の6番)
	// UM10601 4.6.13 System clock control register
	LPC_SYSCON->SYSAHBCLKCTRL |= (1<<6);

	//  指定されたpinに対応するPin interrupt の割り込み有効化。
	//  UM10601  3.3.1 Interrupt sources
	NVIC->ISER[0] = (0x1 << (24 + PinInt));

    // 指定されたpinに対応するPin interruptの割り込み、falling edgeに対して有効化
    // UM10601  8.6.6 Pin interrupt active level or falling edge interrupt set register
    LPC_PIN_INT->SIENF = (1 << PinInt);

    // MRTに対してclockを有効化、
    // UM10601 11.3 Basic configuration
    // enable the clock to the register interface.
    // In the SYSAHBCLKCTRL register, set bit 10 (Table 30) to
    if(!(LPC_SYSCON->SYSAHBCLKCTRL & (0x1<<10))) {
		LPC_SYSCON->SYSAHBCLKCTRL |= (0x1<<10);

		// UM10601 11.3 Basic configuration
		// Clear the MRT reset using the PRESETCTRL register (Table 19).
		LPC_SYSCON->PRESETCTRL &= ~(0x1<<7);
		LPC_SYSCON->PRESETCTRL |= (0x1<<7);
	}

    // The global MRT interrupt is connected to interrupt #10 in the NVIC.
    // 以下にしたがって設定
    // UM10601 3.4.1 Interrupt Set Enable Register 0 register
    NVIC->ISER[0] = (0x1 << 10);

    // Repeat interrupt modeで割り込み設定
    // UM10601 11.6.3 Control register
    LPC_MRT->Channel[Channel].CTRL = 0x1;

    // 割り込み発生させる周期を設定
    // UM10601 11.6.1 Time interval register
    LPC_MRT->Channel[Channel].INTVAL = 1200; // 0.1ms(100μ秒)毎に割り込み
    LPC_MRT->Channel[Channel].INTVAL |= 0x1UL<<31;
}

#endif

接続

  • スピーカは本例ではGPIO_0を使うため、8番pinとGNDにつながっている
  • 2箇所ついてるコンデンサパスコン、0.1μFだが無くても動く
  • 赤外線リモコン受信機はVcc,GNDと、GPIO_4である2番pinにつながっている
  • 以下のリモコンでテストした。左が昔使ってたVICTORのビデオのリモコン、NECフォーマットのはず。右が今使ってるPanasonicのビデオのリモコン、家製協フォーマットのはず。

    • たとえば左のリモコンの電源ボタンの場合、{0x43,0x0b}のデータを受信し、"430b"とモールス信号で出力。
    • 右のリモコンの電源ボタンでは、{0x02,0x20,0xb0,0x00,0x3d,0x8d}のデータを受信し、"0220b0003d8d"とモールス信号で出力。

補足/雑感

  • どうもvolatileのつけ忘れでハマることが多い。部品の不具合や接続ミスを疑う前に、とにかくvolatileつけよう。最適化が弱くなっても、見当違いの原因調査で時間ムダにするよりマシ。

みっかぼの無料Androidアプリはこちら。